使用现代C ++和标准的每个修订版,我们都可以采用更舒适的方式来初始化类的字段:静态和非静态:存在非静态数据成员初始化(来自C ++ 11)和内联变量(用于静态) C ++ 17以来的成员)。
在此博客文章中,您将学习如何使用语法以及从C ++ 11到C ++ 14,C ++ 17到C ++ 20多年来的语法变化。
数据成员的初始化
在C ++ 11之前,如果您有一个类成员,则只能通过构造函数中的初始化列表将其初始化为默认值。
<code>struct
SimpleType
{int
field;std
::string
name; SimpleType() : field(0
), name("Hello World"
) { } }/<code>
从C ++ 11开始,语法得到了改进,您可以进行初始化field并name代替声明:
<code>struct
SimpleType
{int
field =0
;std
::string
name {"Hello World "
} SimpleType() { } }/<code>
如您所见,变量将在声明位置获得其默认值。无需在构造函数内设置值。
该功能称为*非静态数据成员初始化*或简称NSDMI。
更重要的是,自C ++ 17起,我们可以使用内联变量来初始化静态数据成员:
<code>struct
OtherType
{static
const
int
value =10
;static
inline
std
::string
className ="Hello Class"
; OtherType() { } }/<code>
现在,无需className在相应的cpp文件中进行定义。编译器保证所有编译单元只能看到静态成员的一个定义。以前,在C ++ 17之前,您必须将定义放入cpp文件之一。
请注意,对于常量整数静态字段(value),即使在C ++ 98中,我们也可以“就地”初始化它们。
让我们探索这些有用的功能:NSDMI和内联变量。我们将看到示例以及这些年来这些功能如何改进。
NSDMI-非静态数据成员初始化
简而言之,编译器将对字段进行初始化,就像您将其写入构造函数初始化程序列表中一样。
<code>SimpleType
() :field
(0
) { }/<code>
让我们详细了解一下:
怎么运行的
只需一点“机器”,我们就可以看到编译器何时执行初始化。
让我们考虑以下类型:
<code>struct
SimpleType
{int
a { initA() };int
b { initB() }; };/<code>
initA()和initB()函数的实现 有副作用,它们会记录其他消息:
<code>int
initA
()
{std
::cout
<<"initA() called\n"
;return
1
; }std
::string
initB
()
{std
::cout
<<"initB() called\n"
;return
"Hello"
; }/<code>
这使我们可以看到何时调用代码。
例如:
<code>struct
SimpleType
{int
a { initA() };std
::string
b { initB() }; SimpleType() { } SimpleType(int
x) : a(x) { } };/<code>
并使用:
<code>std
::cout
<<"SimpleType t10\n"
; SimpleType t0;std
::cout
<<"SimpleType t1(10)\n"
;SimpleType
t1
(
10
) ;/<code>
输出:
<code>SimpleType
t0
:initA
()called
initB
()called
SimpleType
t1
(10
):initB
()called
/<code>
t0 是默认初始化的,因此两个字段都使用其默认值初始化。
在第二种情况下,对于t1,只有一个值是默认初始化的,而另一个则来自构造函数参数。
您可能已经猜到了,编译器将对字段进行初始化,就像在“成员初始化列表”中初始化字段一样。因此,它们在调用构造函数的主体之前会获取默认值。
换句话说,编译器将扩展代码:
<code>int
a
{initA
() };std
::string
b
{initB
() };SimpleType
() { }SimpleType
(int x) :a
(x) { }/<code>
进入
<code>int
a
;std
::string
b
;SimpleType
() :a
(initA()),b
(initB()) { }SimpleType
(int x) :a
(x),b
(initB()) { }/<code>
其他构造函数呢?
复制和移动构造函数
编译器将在所有构造函数中执行字段的初始化,包括复制和移动构造函数。但是,当复制或移动构造函数为默认值时,则无需执行该额外的初始化。
请参阅示例:
<code>struct
SimpleType
{int
a { initA() };std
::string
b { initB() }; SimpleType() { } SimpleType(const
SimpleType& other) {std
::cout
<<"copy ctor\n"
; a = other.a; b = other.b; }; };/<code>
和用例:
<code>SimpleType t1;std
::cout
<<"SimpleType t2 = t1:\n"
; SimpleType t2 = t1;/<code>
输出:
<code>SimpleType
t1:
called
called
SimpleType
t2 = t1:
called
called
copy
ctor
/<code>
在这里查看代码@Wandbox。
在上面的示例中,编译器使用其默认值初始化了这些字段。这就是为什么最好在复制构造函数中使用初始化程序列表的原因:
<code>SimpleType
(const
SimpleType
&other
) :a
(other
.a
),b
(other
.b
) {std
::cout <<"copy ctor\n"
; };/<code>
我们得到:
<code>SimpleType
t1:
called
called
SimpleType
t2 = t1:
copy
ctor
/<code>
如果您依赖于编译器生成的默认副本构造函数,则会发生相同的情况:
<code>SimpleType(const
SimpleType& other) =default
;/<code>
移动构造函数也会发生同样的事情。
NSDMI的优势
- 容易写
- 您确定每个成员都正确初始化。
- 声明和默认值在同一位置
- 当我们有几个构造函数时特别有用。
- 以前,我们将不得不为成员复制初始化代码,或者编写一个自定义方法InitMembers(),该方法将在构造函数中调用。
- 现在,您可以执行默认初始化,构造函数将仅执行其特定工作…
NSDMI是否有负面影响?
很难提出缺点,但让我们尝试:
- 性能:当您具有性能关键型数据结构(例如Vector3D类)时,可能需要使用“空”初始化代码。您可能会有未初始化的数据成员的风险,但是您将保存一些说明。
- 使类在C ++ 11中不聚合,但在C ++ 14中不聚合。请参阅有关C ++ 14更改的部分。
- 由于默认值位于头文件中,因此任何更改都可能导致需要重新编译依赖的编译单元。如果仅在实现文件中设置值,则不是这种情况。感谢Yehezkel在评论中提到它!这个缺点也适用于静态变量,我们将在后面讨论。
您还有其他问题吗?
聚合,NSDMI的C ++ 14更新
最初,在C ++ 11中,如果您使用默认成员初始化,则您的类不能是聚合类型:
<code>struct
Point
{float
x =0.0f
;float
y =0.0f
; }; Point myPt {10.0f
,11.0f
};/<code>
我不知道这个问题,但是Shafik Yaghmour在文章下面的评论中指出了这一点。
在C ++ 11规范中,不允许聚合类型进行此类初始化,但是在C ++ 14中,此要求已删除。链接到带有详细信息的StackOverflow问题
幸运的是,它已在C ++ 14中修复,因此
<code>Point
myPt
{
10.
0f,
11.
0f};
/<code>
如预期编译,请参阅@Wandbox
位域的C ++ 20更新
从C ++ 11开始,代码仅考虑“常规”字段……但是类中的位字段又如何呢?
<code>
class
Type
{unsigned
int
value :4
; };/<code>
这只是C ++ 20中的最新更改,使您可以编写:
<code>class
Type
{unsigned
int
value :4
=0
;unsigned
int
second :4
{10
}; };/<code>
C ++ 20接受的建议是C ++ 20 P0683的默认位字段初始化程序。
内联变量C ++ 17
到目前为止,我们讨论了非静态数据成员。对于在类中声明和初始化静态变量,我们是否有任何改进?
在C ++ 11/14中,您必须在相应的cpp文件中定义一个变量:
<code>struct
OtherType
{static
int
classCounter; };int
OtherType::classCounter =0
;/<code>
幸运的是,对于C ++ 17,我们还获得了内联变量,这意味着您可以static inline在类内定义变量,而无需在cpp文件中定义它们。
<code>struct
OtherType
{static
inline
int
classCounter =0
; };/<code>
编译器保证为所有包含类声明的翻译单元精确定义一个静态变量。内联变量仍然是静态类变量,因此它们将在main()调用函数之前进行初始化(您可以在我的独立文章中阅读更多内容,在程序开始时静态变量会发生什么?)。
该功能使开发仅标头的库变得容易得多,因为无需为静态变量创建cpp文件或使用一些技巧将它们保存在标头文件中。
与案 auto
由于我们可以在类中声明和初始化变量,因此存在一个有趣的问题auto。我们可以使用吗?这似乎是很自然的方法,并且会遵循AAA(几乎始终为自动)规则。
您可以使用auto静态变量:
<code>class
Type
{static
inline
auto
theMeaningOfLife =42
; };/<code>
但不是作为类的非静态成员:
<code>class
Type
{auto
myField {0
};auto
param {10.5f
}; };/<code>
不幸的是,auto不支持。例如在海湾合作委员会,我得到
<code>error: non-static
data member declaredwith
placeholder'auto'
/<code>
虽然静态成员只是静态变量,这就是为什么编译器推断类型相对容易,但对于常规成员而言却不那么容易。主要是因为类型和类布局可能会循环依赖。如果您对全文感兴趣,可以在cor3ntin博客上阅读以下出色的解释:自动非静态数据成员初始化程序的案例| cor3ntin。
CTAD案例-类模板参数推导
同样,auto对于非静态成员变量和CTAD ,我们也有一些限制:
它适用于静态变量:
<code>class
Type
{static
inline
std
::vector
ints {1
,2
,3
,4
,5
,6
,7
}; };/<code>
但不是作为非静态成员:
<code>class
Type
{
std::vector
ints
{
1
,
2
,
3
,
4
,
5
,
6
,
7
};
//
error!
};
/<code>
在GCC 10.0上我得到
<code>error
:'vector'
doesnot
name atype
/<code>
编译器支持
摘要
在本文中,我们回顾了现代C ++如何改变类内成员的初始化。
在C ++ 11中,我们获得了NSDMI-非静态数据成员初始化。现在,您可以声明一个成员变量,并使用默认值对其进行初始化。初始化将在构造函数初始化列表中的每个构造函数主体被调用之前进行。
NSDMI在C ++ 14(聚合)和C ++ 20(现在支持位字段)中得到了改进。
更重要的是,在C ++ 17中,我们获得了内联变量,这意味着您可以声明并初始化静态成员,而无需在相应的cpp文件中执行此操作。
这是一个结合了这些功能的“摘要”示例:
<code>struct
Window
{inline
static
unsigned
int
default_width =1028
;inline
static
unsigned
int
default_height =768
;unsigned
int
_width { default_width };unsigned
int
_height { default_height };unsigned
int
_flags :4
{0
};std
::string
_title {"Default Window"
}; Window() { } Window(std
::string
title) : _title(std
::move(title)) { } };/<code>
为了简单起见default_width,default_height它们是静态变量,例如可以从配置文件中加载这些静态变量,然后使用它们来初始化默认的Window状态。
轮到你了
您在项目中使用NSDMI吗?您是否将静态Inline变量用作类成员?
您是否在代码中使用它?
原文地址:https://www.bfilipek.com/2015/02/non-static-data-members-initialization.html
越是优化代码,越是演练,越是思考,就越能发现C/C++的优势所在。
关注我:带你遨游代码的世界~
私信 “资料” 获取更多~
閱讀更多 我臉上有bug 的文章