C++ 初始化详解
一、初始化
变量的初始化在构造时提供其初始值。初始值可以在声明符或 new 表达式的初始化器部分提供,也发生在函数调用期间:函数参数和函数返回值也会被初始化。
理解初始化的框架:语法是入口,语义是结果。写法决定走哪条语义路径,语义路径决定编译器最终如何处理数据。
本文主线聚焦对象的初始化。引用的初始化规则独立,见引用初始化。非局部变量在语法规则上与局部变量相同,但额外涉及初始化时机问题,见非局部变量的初始化时机。
二、语法分类:四种写法
对于一个声明,初始化器有四种形式:
| 写法 | 语法名称 | 示例 |
|---|---|---|
| 无初始化器 | 默认初始化 | int x; |
= expr |
拷贝初始化语法 | int x = 1; |
(expr-list) |
直接初始化语法 | std::string s("hi"); |
{expr-list} 或 = {expr-list} |
列表初始化语法(C++11 起) | int x{1}; |
注意:T x() 不是初始化,而是声明一个返回 T、无参数的函数。这是经典的 Most Vexing Parse。规避方式是用 T x{} 代替。
std::string s1; // 默认初始化
std::string s2(); // 函数声明,不是初始化!
std::string s3 = "hello"; // 拷贝初始化
std::string s4("hello"); // 直接初始化
std::string s5{"hello"}; // 列表初始化(C++11 起)
三、语义分类:六条路径
语义描述编译器最终如何处理数据。六条主要路径:
1、默认初始化
无初始化器时触发。行为取决于类型和存储期:
- 局部内置类型:不做任何事,值不确定
- 局部类类型:调用默认构造函数
- 全局/静态变量:先进行零初始化(静态初始化阶段),如果是类类型,还会再调用默认构造函数(动态初始化阶段)
int g; // 全局变量:零初始化,g = 0
void foo() {
int x; // 局部变量:默认初始化,x 的值不确定,读取是 UB
std::string s; // 局部类类型:调用默认构造函数,s = ""
}
2、值初始化
T x{} 或 T x = T() 触发。目标是给对象一个确定的值:
- 内置类型:零初始化(int 为 0,double 为 0.0)
- 类类型:若有用户提供的构造函数(任意构造函数),调用默认构造函数(必须存在);否则先零初始化,再调用合成默认构造函数
int x{}; // 值初始化,x = 0
double d{}; // 值初始化,d = 0.0
static int s; // 静态存储期,零初始化,s = 0(与 int s{} 效果相同)
struct A { int x; int y; };
A a{}; // 值初始化 → 零初始化,a.x = 0, a.y = 0
struct B { int x; B(int v) : x(v) {} }; // 有用户提供的构造函数
B b{}; // 值初始化 → 调用默认构造函数(此处编译错误,B 无默认构造)
3、拷贝初始化
= expr 触发。语义上,编译器先从 expr 构造一个临时对象(可能涉及隐式转换),再用它初始化目标对象。实践中,编译器通常通过 copy elision 直接构造,省略临时对象。
拷贝初始化不能调用 explicit 构造函数。
std::string s1 = "hello"; // 隐式转换:const char* → std::string,再拷贝/移动
std::string s2 = s1; // 拷贝构造
std::string s3 = std::move(s1); // 移动构造
int x = 1; // 内置类型,直接赋值
struct Foo { explicit Foo(int) {} };
Foo f = 1; // 编译错误:explicit 构造函数不参与拷贝初始化
拷贝初始化也发生在函数传参和返回值场景:
void bar(std::string s); // 调用时,实参拷贝初始化形参 s
std::string baz() { return "hi"; } // 返回值被拷贝/移动初始化(通常被 NRVO 优化)
4、直接初始化
(args) 触发。重载解析范围比拷贝初始化更宽,可以调用 explicit 构造函数。可以理解为:直接告诉编译器调用哪个构造函数。
std::string s("hello"); // 直接初始化,调用 string(const char*)
std::vector<int> v(3, 0); // 直接初始化,调用 vector(size_t, int)
std::vector<int> v2(v); // 直接初始化,调用拷贝构造函数
std::vector<int> v3(std::move(v2)); // 直接初始化,调用移动构造函数
struct Foo { explicit Foo(int) {} };
Foo f(1); // 合法:直接初始化可调用 explicit 构造函数
Foo g = 1; // 编译错误:拷贝初始化不可调用 explicit
5、列表初始化
{list} 触发(C++11 起)。这是一个"入口",内部根据类型再分派:
1. 若目标类型是聚合体 → 聚合初始化
2. 若类型有 std::initializer_list 构造函数 → 优先调用它
3. 否则 → 直接初始化或值初始化
std::vector<int> v1{3, 0}; // 列表初始化 → initializer_list 构造,v1 = {3, 0},2 个元素
std::vector<int> v2(3, 0); // 直接初始化,v2 = {0, 0, 0},3 个元素
6、聚合初始化
列表初始化的子路径,适用于聚合类型(无用户提供构造函数的结构体或数组)。按成员声明顺序依次初始化,未提供的成员零初始化:
struct Point { int x; int y; };
Point p{1, 2}; // 聚合初始化,p.x = 1, p.y = 2
char a[3] = {'a', 'b'}; // 聚合初始化,a[2] = '\0'(零初始化)
四、语法到语义的映射
| 语法 | 触发的语义路径 |
|---|---|
T x; |
默认初始化 |
T x = expr; |
拷贝初始化 |
T x(args); |
直接初始化 |
T x{}; |
列表初始化 → 值初始化(空列表) |
T x{args}; |
列表初始化 → 聚合初始化 / initializer_list 构造 / 直接初始化 |
T x = {}; |
列表初始化(C++11 起,同 T x{}) |
同一语法可能走不同语义路径,取决于类型。T x{args} 对聚合类型走聚合初始化,对有 initializer_list 构造函数的类型优先走该构造函数。
五、常见陷阱
陷阱 1:局部内置类型值不确定
void foo() {
int x; // 默认初始化,值不确定
int y = x; // UB:读取未初始化变量
}
规避:始终显式初始化局部变量。
陷阱 2:Most Vexing Parse
std::string s(); // 函数声明,不是初始化!
std::string s{}; // 正确:值初始化,s = ""
陷阱 3:{} 优先匹配 initializer_list
std::vector<int> v1{3, 0}; // {3, 0},2 个元素
std::vector<int> v2(3, 0); // {0, 0, 0},3 个元素
当类型有 initializer_list 构造函数时,{} 会优先调用它,而不是其他构造函数。需要调用特定构造函数重载时,用 ()。
陷阱 4:加了构造函数就不再是聚合类型
struct A { int x; int y; };
A a{1, 2}; // 聚合初始化,合法
struct B { int x; int y; B() {} }; // 有用户提供的构造函数
B b{1, 2}; // 错误:B 不再是聚合类型
陷阱 5:{} 禁止窄化转换
int x(3.14); // 直接初始化,静默截断,x = 3
int y{3.14}; // 编译错误:窄化转换
{} 的这个特性是优点,能在编译期捕获潜在的精度损失。
六、非局部变量的初始化时机
非局部变量(全局变量、静态变量)的初始化分两个阶段,发生在 main 之前:
静态初始化(编译期/加载期):
- 常量初始化:值在编译期已知,存储在程序镜像中
- 零初始化:其余静态变量放入 .bss 段,由 OS 加载时清零
动态初始化(运行时): - 同一翻译单元内,按定义顺序初始化 - 跨翻译单元,顺序不确定——这是静态初始化顺序问题(SIOF)的来源
// 跨翻译单元的依赖是危险的
// file1.cpp
extern int y;
int x = y + 1; // y 可能还未初始化
// 规避:用函数内 static(Meyers Singleton)
int& get_y() {
static int y = compute_y(); // 首次调用时初始化,线程安全(C++11 起)
return y;
}
七、最佳实践
- 局部变量始终显式初始化,优先用
{},可防止窄化转换 - 需要特定构造函数重载时用
(),避免initializer_list歧义 - 聚合类型用
{}做成员初始化,清晰且安全 - 全局/静态变量尽量用
constexpr,走常量初始化路径,避免动态初始化的顺序问题 - 跨翻译单元的静态依赖用 Meyers Singleton 规避 SIOF

浙公网安备 33010602011771号