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
posted @ 2026-04-21 11:16  noonafter  阅读(44)  评论(0)    收藏  举报