Skip to content

列表初始化 ​

C++11扩大了用大括号括起的列表(初始化列表)的适用范围,使其可用于所有内置类型和用户自定义的类型。

使用初始化列表时,可添加等号(=),也可不添加。

cpp
T var { value };	// 推荐写法(强类型)
T var = { value };	// 也支持

统一初始化示例 ​

cpp
// 1. 内置类型
int x {10};
double d {3.14};

// 2. 数组
int arr1[] { 1, 2, 3 };
int* arr = new int [4] { 1, 2, 3, 4 };

// 3. 结构体
Point p1 {1, 2};

// 4. 类
Person p {"Tom", 30};	// 使用构造函数,统一初始化构造

// 5. 使用容器
std::vector<int> vec {1, 2, 3, 4, 5};
std::map<string, int> mp { {"apple", 3}, {"banana", 5} };

// 6. 动态分配对象
Point* p = new Point{10, 20};

统一初始化使用场景 ​

统一代码风格 ​

cpp
int a1;
int a2 = 1;
int a3(2);
int a4[3] = {1, 2};
cpp
int a1 {};
int a2 { 1 };
int a3 { 2 };
int a4[] { 1, 2 };

防止窄化转换 ​

窄化转换指的是将一个值赋给一个类型容量更小的变量时,可能导致数据丢失或精度损失。

cpp
double d = 3.14159;

// C++98 的问题
// 在 C++98/03 中,使用 =、() 进行初始化,编译器通常会允许 窄化转换,最多给一个警告
int i1 = d;		// 允许,i1 = 3,损失了精度
int i2(d);		// 允许,i2 = 3,损失了精度

// C++11 统一初始化{} 的优势
// 使用 {} 进行初始化,C++11 强制执行,如果初始化列表中的值可能导致 窄化转换,编译器将拒绝编译
int i3 {d};		// ❌ 编译错误,C++11 强制禁止了从 double 到 int 的窄化转换
bash
$ g++ main.cpp -o main -std=c++11 -Werror=narrowing
main.cpp: In function ‘int main()’:
main.cpp:8:11: error: narrowing conversion of ‘d’ from ‘double’ to ‘int’ [-Werror=narrowing]
    8 |   int i3 {d};
      |           ^
cc1plus: some warnings being treated as errors

但允许转换为更宽的类型。只要值在较窄类型的取值范围 内,将其转换为较窄的类型也是允许的:

cpp
char c1 {66};	// 66 整型,char 字符型更窄,但66 在 char类型 范围内
double c2 {66};	// 66 整型,double 更宽类型,允许

提高容器初始化的简洁性 ​

cpp
// 传统 C++ 初始化一个容器
int arr[] = {1, 2, 3, 4, 5};
std::vector<int> v(arr, arr + sizeof(arr) / sizeof(int));

// 列表初始化容器
std::vector<int> v { 1, 2, 3, 4, 5 };

避免初始化语法陷阱 ​

最令人头疼的解析(The Most Vexing Parse, MVP)是 C++ 语法中的一条歧义规则:

如果一个语句可以被解析为变量定义,同时也可以被解析为函数声明,那么编译器必须选择函数声明。

cpp
class Timer {
public:
	Timer() { /* 构造函数体 */ }
};

// 目标:创建一个名为 t 的 Timer 对象
Timer t();	// t 没有被创建,而是声明了一个函数

根据 MVP 规则,编译器会将其解析为一个函数声明:

  • 函数名:t
  • 参数:空 ()
  • 返回值类型:Timer
cpp
Timer t{}; // 强制对象初始化,调用 Timer 的默认构造函数,创建一个对象 t

底层机制 ​

std::initializer_list<T> 是一个轻量级、只读的代理对象,它的核心作用是让 C++ 支持 {} 初始化语法的统一接口。

初始化列表的类型为 std::initializer_list,如果相关构造函数对其有重载,则调用重载了的构造函数。

当编译器看到 {} 语法来构造一个对象时,它会优先寻找一个接受 std::initializer_list<T> 作为参数的构造函数。

cpp
#include <initializer_list>
#include <iostream>

class MyContainer {
public:
  // 关键,定义一个接受 std::initializer_list 的构造函数
  MyContainer(std::initializer_list<int> list) {
    std::cout << "调用 initializer_list 构造函数,元素数量" << list.size()
              << std::endl;
    // 在这里可以遍历 list 并将元素复制到你的容器内部存储中
    for (int val : list) {
      // ... 存储 val ...
    }
  }
};

MyContainer c {10, 20, 30}; // 编译器调用上面的构造函数

引出的另一种歧义 ​

当类中存在 std::initializer_list 构造函数时,它会劫持你的初始化意图,造成意图和语义上的困惑。

cpp
#include <iostream>
#include <initializer_list>

class Confused {
public:
	// 1. 普通构造函数(预期:单个值)
	Confused(int x) {
		std::cout << "▶️ 调用了普通构造函数 T(int): " << x << std::endl;
	}

	// 2. 列表构造函数(意图:一个值的序列)
	Confused(std::initializer_list<int> list) {
		std::cout << "▶️ 调用了列表构造函数 T(std::initializer_list),元素数: " << list.size() << std::endl;
	}
};

// 有3种不同的方式来初始化 Confused 对象
Confused c1(1);		// ✅ 意图调用 T(int),实际调用 T(int)
Confused c2{1, 2};	// ✅ 意图调用 T(list),实际调用 T(std::initializer_list)
Confused c3{1};		// ❌ 意图调用 T(int),实际调用 T(std::initializer_list)

引入了陷阱: 尽管 T(int) 是一个比 T(std::initializer_list) 更精确的匹配(因为不需要额外的类型转换和打包),但 C++11 标准强制要求列表构造函数具有绝对优先权。

当使用 {} 初始化时,如果你本意是调用一个普通构造函数,但类中又恰好有一个兼容的 initializer_list 构造函数,你的意图就会被劫持。