列表初始化
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 构造函数,你的意图就会被劫持。