C++ 的初始化是一个庞大而精细的主题,从 C 语言的简单赋值风格,到 C++11 引入的统一初始化,其发展历程旨在解决二义性、提高类型安全性和代码一致性。

为什么初始化如此重要?

在 C++ 中,一个未经初始化的变量(非静态局部变量)拥有一个不确定的值。读取这个值会导致未定义行为 (Undefined Behavior, UB),这是 C++ 中最危险的陷阱之一,可能导致程序崩溃、数据损坏或看似正常运行但结果错误。


C++ 初始化方式的演变与分类

我们可以大致将初始化方式分为两大类:C++11 之前的传统方式和 C++11 及其之后引入的统一初始化(大括号初始化)。

第一部分:C++11 之前的传统初始化方式

在 C++11 之前,主要有以下几种初始化方式,它们的语法不统一,有时会带来困惑。

1. 默认初始化 (Default Initialization)

当一个变量在定义时没有提供显式的初始值时,就会发生默认初始化。

语法

1
T object_name;

行为

  • 对于局部变量(在函数内定义):如果 T 是内置类型(如 int, double, char*),其值是不确定的(包含垃圾值)。如果 T 是类类型,则会调用其默认构造函数。如果类没有默认构造函数,则编译错误。
  • 对于静态或全局变量:所有静态存储期的变量(全局变量、static 局部变量、static 类成员)会被零初始化 (Zero Initialization)。这意味着内置类型被设为 0,指针被设为 nullptr,类类型会调用其默认构造函数。

示例

1
2
3
4
5
6
7
8
9
#include <iostream>
#include <string>

void func() {
int x; // 默认初始化,值不确定!(危险)
std::string s; // 默认初始化,调用 std::string 的默认构造函数,s 为空字符串 ""
// std::cout << x << std::endl; // 读取 x 的值是未定义行为!
std::cout << "s is: '" << s << "'" << std::endl;
}

2. 拷贝初始化 (Copy Initialization)

使用等号 = 进行初始化。它的语义是“拷贝”一个值到新创建的对象中。

语法

1
T object_name = other_value;

行为
编译器会尝试将 other_value 转换(隐式转换)为 T 类型,然后用于初始化 object_name。这通常会调用拷贝构造函数或移动构造函数。虽然看起来像赋值,但它是初始化,不是赋值

注意

  • 拷贝省略 (Copy Elision):现代编译器通常会优化掉这里的拷贝过程,直接在 object_name 的内存上构造对象,但从语法和类型检查的角度,它仍然需要一个可访问的拷贝/移动构造函数。
  • explicit 关键字:如果类的构造函数被标记为 explicit,那么它不能用于隐式类型转换,因此不能用于拷贝初始化。

示例

1
2
3
4
5
6
7
8
9
10
11
12
int x = 10; // 拷贝初始化
std::string s = "hello"; // 拷贝初始化,"hello" (const char*) 隐式转换为 std::string
std::vector<int> v = {1, 2, 3}; // C++11 中的拷贝列表初始化

class Widget {
public:
explicit Widget(int) {} // explicit 构造函数
};

Widget w1 = 10; // 编译错误!因为 Widget(int) 是 explicit 的,不能用于隐式转换
// Widget w1 = {10}; // C++11 中同样编译错误
Widget w2(10); // 正确,这是直接初始化

3. 直接初始化 (Direct Initialization)

使用圆括号 () 进行初始化。

语法

1
T object_name(arg1, arg2, ...);

行为
直接调用与参数列表 (arg1, arg2, ...) 相匹配的构造函数来初始化 object_name。它允许使用 explicit 构造函数。

示例

1
2
3
4
5
int x(10);
std::string s("hello");
std::vector<int> v(10, 5); // 创建一个包含 10 个值为 5 的元素的 vector

Widget w(10); // 正确,即使构造函数是 explicit 的

直接初始化的一个著名陷阱:最令人头疼的解析 (Most Vexing Parse)

如果一个语法可以被解释为一个函数声明,也可以被解释为一个对象定义,C++ 标准规定编译器优先将其解释为函数声明

1
2
3
4
5
6
// 程序员的意图:定义一个名为 my_timer 的 Timer 对象,使用默认构造函数初始化
Timer my_timer();

// 编译器的解释:声明一个名为 my_timer 的函数,
// 该函数不接受参数,返回一个 Timer 对象。
// 这不是一个对象定义!

这是一个巨大的坑,也是 C++11 引入统一初始化的重要原因之一。

4. 聚合初始化 (Aggregate Initialization)

用于初始化聚合类型(没有用户提供的构造函数、没有私有或保护的非静态数据成员、没有基类、没有虚函数的 C 风格 struct 或数组)。

语法
使用花括号 {} 提供初始值列表。

示例

1
2
3
4
5
6
7
8
// C 风格 struct
struct Point {
int x;
int y;
};

int arr[3] = {1, 2, 3}; // 初始化数组
Point p = {10, 20}; // 初始化 struct

这种方式在 C++11 中被极大地扩展了。


第二部分:C++11 的统一初始化 (Uniform Initialization)

C++11 引入了使用花括号 {} 的初始化方式,也称为列表初始化 (List Initialization)大括号初始化 (Brace Initialization)。其目标是提供一种统一、无歧义、更安全的初始化语法。

语法

1
2
T object_name {arg1, arg2, ...};   // 直接列表初始化
T object_name = {arg1, arg2, ...}; // 拷贝列表初始化

统一初始化的三大优势

  1. 统一性与无歧义
    它解决了“最令人头疼的解析”问题。

    1
    2
    Timer my_timer();  // 函数声明
    Timer my_timer{}; // 明确的对象定义,调用默认构造函数

    它可以用在几乎所有初始化场景,包括变量定义、函数返回值、类成员初始化等,语法非常一致。

  2. 防止窄化转换 (Narrowing Conversion)
    这是大括号初始化的一个核心安全特性。它禁止在初始化时发生可能导致数据丢失的类型转换。

    1
    2
    3
    4
    5
    6
    int x = 3.14;    // 合法,但有警告。x 的值为 3 (数据丢失)
    int y(3.14); // 合法,但有警告。y 的值为 3 (数据丢失)
    // int z{3.14}; // 编译错误!大括号初始化禁止从 double 到 int 的窄化转换

    char c1 = 300; // 合法,但行为是实现定义的(溢出)
    // char c2{300}; // 编译错误!300 超出 char 的表示范围,是窄化转换
  3. std::initializer_list 的原生支持
    这是大括号初始化最特殊、也最需要注意的一点。如果一个类有一个接受 std::initializer_list 的构造函数,那么使用大括号初始化时,编译器会强烈优先选择这个构造函数。

    示例
    std::vector 同时有 vector(size_type count, const T& value)vector(std::initializer_list<T>) 两种构造函数。

    1
    2
    3
    4
    5
    std::vector<int> v1(10, 20); // 直接初始化:调用构造函数 vector(size, value)
    // v1 包含 10 个元素,每个都是 20

    std::vector<int> v2{10, 20}; // 列表初始化:优先匹配 initializer_list 构造函数
    // v2 包含 2 个元素:10 和 20

    这个特性非常强大,但也可能与你的直觉相悖,是使用大括号初始化时唯一需要小心的地方。

空大括号 {} 的特殊含义:值初始化

使用空的大括号 {} 会对对象进行值初始化 (Value Initialization)

语法

1
2
T object_name{};
T* ptr = new T{};

行为

  • 对于内置类型,保证被零初始化
  • 对于类类型,调用其默认构造函数

这是一种非常安全和推荐的做法,可以确保所有变量都有一个良好定义的初始状态。

1
2
3
4
int x{};      // x 被初始化为 0
double d{}; // d 被初始化为 0.0
int* p{}; // p 被初始化为 nullptr
std::string s{}; // s 为空字符串 ""

第三部分:特殊的初始化场景

1. 类成员初始化

a. 构造函数成员初始化列表 (Member Initializer List)

这是在构造函数体执行之前初始化成员变量的唯一正确方式。

语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyClass {
public:
MyClass(int a, const std::string& b)
: member_a_(a), // 直接初始化成员
member_b_(b), // 拷贝初始化成员
const_member_(100), // 初始化 const 成员
ref_member_(a) // 初始化引用成员
{
// 构造函数体,此时所有成员都已初始化完毕
// 这里执行的是赋值,而不是初始化
}

private:
int member_a_;
std::string member_b_;
const int const_member_; // const 成员
int& ref_member_; // 引用成员
};

为什么必须使用它?

  • 效率:对于类类型成员,使用初始化列表是直接调用构造函数进行初始化。如果在构造函数体内赋值,则会先调用该成员的默认构造函数(默认初始化),然后再调用赋值操作符,多了一步操作。
  • 正确性const 成员和引用成员必须在成员初始化列表中进行初始化,因为它们一旦创建就不能被重新赋值。
b. 类内成员初始化 (In-class Member Initializers) (C++11)

C++11 允许在类定义中直接为非静态成员变量提供默认初始值。

1
2
3
4
5
6
class MyClass {
private:
int value_ = 42; // C++11 类内初始化
std::string name_ = "default";
std::vector<int> data_{1, 2, 3};
};

这极大地简化了构造函数。如果构造函数没有在成员初始化列表中为这些成员提供不同的初始值,它们就会使用这个默认值。这使得代码更简洁,并减少了因忘记在多个构造函数中初始化某个成员而导致的错误。


总结

初始化方式 语法 示例 优点 缺点/注意事项
默认初始化 T obj; int x; 简单 对局部内置类型导致未定义行为。
拷贝初始化 T obj = val; int x = 5; 语法自然,符合直觉 不支持 explicit 构造函数;可能存在隐式类型转换问题。
直接初始化 T obj(args); int x(5); 支持 explicit 构造函数;功能强大 存在“最令人头疼的解析”问题;语法不统一。
列表初始化 (统一初始化) T obj{args}; T obj = {args}; int x{5}; std::vector<int> v{1,2}; 统一语法无歧义防止窄化转换保证值初始化 ({}) 会优先匹配 std::initializer_list 构造函数,可能不符合直觉。
成员初始化列表 Ctor() : mem(v) {} MyClass() : x_(10) {} 高效,初始化 const/引用成员的唯一方式 只能在构造函数中使用。
类内成员初始化 class T { int m=5; }; int value_ = 42; 简化构造函数,提供默认值,避免重复代码 C++11 及以上版本才支持。

现代 C++ (C++11 及以后) 的推荐实践:

  1. 首选大括号统一初始化 {}

    1
    2
    3
    int x{}; // 保证为 0
    Widget w{}; // 调用默认构造函数
    std::vector<int> v{1, 2, 3};

    它的安全性(防止窄化)和一致性使其成为绝大多数情况下的最佳选择。

  2. 警惕 std::initializer_list 的“陷阱”
    当你需要调用一个非 initializer_list 的构造函数,而恰好又存在一个 initializer_list 构造函数时,请明确使用圆括号 () 进行直接初始化。

    1
    2
    3
    // 意图:创建一个包含 10 个 20 的 vector
    std::vector<int> v(10, 20); // 正确,使用 ()
    // std::vector<int> v{10, 20}; // 错误,这会创建一个包含 {10, 20} 的 vector
  3. 总是使用成员初始化列表来初始化构造函数中的成员,或使用 C++11 的类内成员初始化来提供默认值。避免在构造函数体内进行赋值。

  4. 养成随手初始化的习惯。绝不留下未初始化的变量。对于不确定初始值的变量,使用值初始化 ({}) 将其清零。