初始化
变量的初始化会在构造时提供变量的初始值。
初始值可以由声明符或 new 表达式的初始化器部分提供。在函数调用时也会发生:函数形参及函数返回值也会被初始化。
初始化器
对于每个声明符,初始化器(如果存在)必须是下列之一:
= 表达式
|
(1) | ||||||||
= {} = { 初始化器列表 } = { 指派初始化器列表 }
|
(2) | (C++20 起) | |||||||
( 表达式列表 ) ( 初始化器列表 )
|
(3) | (C++11 前) (C++11 起) | |||||||
{} { 初始化器列表 } { 指派初始化器列表 }
|
(4) | (C++11 起) (C++11 起) (C++20 起) | |||||||
表达式 | - | (不带括号的逗号表达式以外的)任意表达式 |
表达式列表 | - | 包含(不带括号的逗号表达式以外的)任意表达式的逗号分隔列表 |
初始化器列表 | - | 包含初始化器子句(见下文)的逗号分隔列表 |
指派初始化器列表 | - | 包含指派初始化器子句的逗号分隔列表 |
初始化器子句 必须是下列之一:
表达式 | (1) | ||||||||
{}
|
(2) | ||||||||
{ 初始化器列表 }
|
(3) | ||||||||
{ 指派初始化器列表 }
|
(4) | (C++20 起) | |||||||
语法 (2-4) 统称为花括号包围的初始化器列表。
初始化器语义
如果没有为对象指定初始化器,那么该对象会默认初始化。如果没有为引用指定初始化器,那么程序非良构。
如果为对象指定的初始化器是 ()(由于语法限制不能在声明符中出现),那么该对象会值初始化。如果为引用指定的初始化器是 (),那么程序非良构。
初始化器的语义定义如下:
- 如果要初始化的是引用,那么定义参考引用初始化。
- 否则要初始化的是对象。给定该对象的类型为
T
:
- 如果使用的是语法 (1) 的初始化器,那么对象会复制初始化。
(C++11 前) | |
|
(C++11 起) |
- 如果使用的是语法 (3) 的初始化器,那么对象会直接初始化。
#include <string> std::string s1; // 默认初始化 std::string s2(); // 不是初始化! // 实际上声明了没有形参并且返回 std::string 的函数 “s2” std::string s3 = "hello"; // 复制初始化 std::string s4("hello"); // 直接初始化 std::string s5{'a'}; // 列表初始化(C++11 起) char a[3] = {'a', 'b'}; // 聚合初始化(C++11 起是列表初始化的一部分) char& c = a[0]; // 引用初始化
非局部变量
所有具有静态存储期的非局部变量的初始化,会作为程序启动的一部分在 main 函数的执行之前进行(除非被延迟,见下文)。所有具有线程局部存储期的非局部变量的初始化,会作为线程启动的一部分进行,并按顺序早于线程函数的执行开始。对于这两种变量,初始化发生于两个截然不同的阶段:
静态初始化
静态初始化有两种形式:
实践中:
- 常量初始化通常在编译期进行。预先被计算的对象表示会作为程序映像的一部分存储下来。如果编译器没有这样做,那么它仍然必须保证该初始化发生早于任何动态初始化。
- 零初始化的变量将被置于程序映像的
.bss
段,它不占据磁盘空间,并在加载程序时由操作系统以零填充。
动态初始化
在所有静态初始化完成后,在下列情形中进行非局部变量的动态初始化:
2) 部分有序的动态初始化,适用于并未被隐式或显式实例化的特化的所有内联变量。如果一个部分有序的 V 在每个翻译单元中比有序或部分有序的 W 更早定义,那么 V 的初始化按顺序早于(或若程序启动了线程,则为先发生于)W 的初始化。
|
(C++17 起) |
当拥有静态或线程存储期的非局部变量的初始化通过异常退出时,调用 std::terminate。
提早动态初始化
在下列条件都满足的情况下,允许编译器将动态初始化的变量的初始化作为静态初始化(实为编译期)的一部分进行:
因为上述规则,如果某对象 o1
的初始化涉及到命名空间作用域对象 o2
,而它潜在地要求动态初始化,但在同一翻译单元中在其之后定义,那么所用的 o2
是完全初始化的 o2
的值(因为编译器把 o2
的初始化提升到编译时)还是 o2
仅被零初始化的值是未指明的。
inline double fd() { return 1.0; } extern double d1; double d2 = d1; // 未指明: // 如果 d1 被动态初始化则动态初始化为 0.0,或 // 如果 d1 被静态初始化则动态初始化为 1.0,或 // 静态初始化为 0.0(因为当两个变量都被动态初始化时将为这个值) double d1 = fd(); // 可能静态或动态初始化为 1.0
延迟动态初始化
动态初始化是发生早于(对于静态变量)主函数或(对于线程局域变量)其线程的启动函数的首条语句,还是延迟到发生晚于它们,是由实现定义的。
如果非内联变量的 (C++17 起)初始化延迟到发生晚于主/线程函数的首条语句,那么它发生早于与所初始化的变量定义于同一翻译单元中的任何拥有静态/线程存储期的变量的首次 ODR 使用。如果给定翻译单元中没有 ODR 使用变量或函数,那么在该翻译单元定义的非局部变量可能始终不被初始化(这模仿按需的动态库的行为)。然而,只要翻译单元中 ODR 使用了任何事物,就会初始化所有在初始化或销毁中拥有副作用的非局部变量,即使程序中没有用到它们。
如果内联变量的初始化被延迟,那么它发生早于这个特定变量的首次 ODR 使用。 |
(C++17 起) |
// ============ // == 文件 1 == #include "a.h" #include "b.h" B b; A::A() { b.Use(); } // ============ // == 文件 2 == #include "a.h" A a; // ============ // == 文件 3 == #include "a.h" #include "b.h" extern A a; extern B b; int main() { a.Use(); b.Use(); } // 如果 a 在进入 main 之前被初始化,那么 b 可能在 A::A() 使用它的时间点仍未被初始化 // (因为动态初始化在翻译单元间是顺序不确定的) // 如果 a 在某个 main 的首条语句之后的时间点初始化 // (它 ODR 使用了定义于文件 1 的函数,强制其初始化得以运行), // 那么 b 将在 A::A 使用它前初始化
静态局部变量
有关局部(即块作用域)的静态和线程局部变量,见静态局部变量。
拥有外部或内部链接的变量的块作用域声明中不允许初始化器。这种声明必须带 extern 出现而且不能为定义。
类成员
非静态数据成员可以由成员初始化器列表或由默认成员初始化器初始化。
注解
非局部变量的销毁顺序在 std::exit 中描述。
缺陷报告
下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
缺陷报告 | 应用于 | 出版时的行为 | 正确行为 |
---|---|---|---|
CWG 270 | C++98 | 未指定类模板的静态数据成员的初始化顺序 | 除显式特殊化和定义外均指定为无序 |
CWG 441 | C++98 | 具有静态存储期的非局部引用不一定会在动态初始化前初始化 | 归类为静态初始化,总会在动态初始化前初始化 |
CWG 1415 | C++98 | 块作用域 extern 变量的声明可以是定义 | 不能是定义(禁止在此类声明中出现初始化器) |
CWG 2599 | C++98 | 不明确初始化是否包含对初始化器中函数实参的求值 | 包含 |