
大家好,欢迎来到 huangjin007_ 的博客
⭐ 个人主页:huangjin007_
🔥 文章收录专栏:零基础入门C++
总会有一些坚持
能从冰封的土地里
培育出十万朵怒放的蔷薇
C++11篇(一) ------ 发展历程、列表初始化详解
从本篇内容开始,正式开启 C++11 深度学习之旅。接下来我会掰开揉碎拆解每一处语法细节,全程干货,坐稳发车~ ദ്ദി˶ー̀֊ー́ )✧
文章目录
- [C++11篇(一) ------ 发展历程、列表初始化详解](#C++11篇(一) —— 发展历程、列表初始化详解)
-
- [1. C++11 的发展历史:一个时代的里程碑](#1. C++11 的发展历史:一个时代的里程碑)
- [2. 列表初始化的演进](#2. 列表初始化的演进)
-
- [2.1 C++98 里的 `{}` ------ 只能用于特定场景](#2.1 C++98 里的
{}—— 只能用于特定场景) - [2.2 C++11 的目标:一切对象皆可 `{}`](#2.2 C++11 的目标:一切对象皆可
{})
- [2.1 C++98 里的 `{}` ------ 只能用于特定场景](#2.1 C++98 里的
- [3. 代码详解:从内置类型到自定义类型](#3. 代码详解:从内置类型到自定义类型)
-
- [3.1 内置类型也可用 `{}`](#3.1 内置类型也可用
{}) - [3.2 自定义类型的列表初始化:临时对象 + 优化](#3.2 自定义类型的列表初始化:临时对象 + 优化)
- [3.3 常量引用绑定临时对象](#3.3 常量引用绑定临时对象)
- [3.4 单参数的隐式转换](#3.4 单参数的隐式转换)
- [3.5 省略 `=`](#3.5 省略
=) - [3.6 容器操作中的大杀器](#3.6 容器操作中的大杀器)
- [3.1 内置类型也可用 `{}`](#3.1 内置类型也可用
- [4. std::initializer_list](#4. std::initializer_list)
-
- [4.1 `initializer_list` 是什么?](#4.1
initializer_list是什么?) - [4.2 验证 `initializer_list` 的实现](#4.2 验证
initializer_list的实现) - [4.3 容器如何支持多元素初始化](#4.3 容器如何支持多元素初始化)
- [4.4 同样适用于赋值操作](#4.4 同样适用于赋值操作)
- [4.1 `initializer_list` 是什么?](#4.1
- [5. 深入本质:类型转换与优化](#5. 深入本质:类型转换与优化)
-
- [5.1 窄化转换的防御](#5.1 窄化转换的防御)
- [5.2 `explicit` 与列表初始化](#5.2
explicit与列表初始化)
- 结语:
1. C++11 的发展历史:一个时代的里程碑
在 C++ 的进化史上,C++11 绝对是一个划时代的存在。它是自 C++98 以来的第二个主要标准版本,同时也是迄今为止改动最大、影响最深远的一次更新。
原本这个版本被社区称作 C++0x ,因为大家满怀期待地认为它能在 2010 年之前发布。但由于标准委员会希望引入的特性实在太多,讨论和设计过程旷日持久,最终它被正式批准并发布的时间是 2011 年 8 月 12 日。从 2003 年的 C++03 到 2011 年的 C++11,中间整整隔了 8 年------这也是 C++ 历史上最长的一次版本更新间隔。从那以后,C++ 进入了稳定的节奏,每隔 3 年就会推出一个新的标准版本(例如 C++14、C++17、C++20 等)。
C++11 的使命是巨大的:它引入了大量全新的语言特性和标准库组件,比如自动类型推导 auto、范围 for 循环、智能指针、lambda 表达式、右值引用和移动语义,以及我们今天要重点聊的------统一的初始化方式(列表初始化)。这些特性极大提升了代码的抽象能力和编写效率。

2. 列表初始化的演进
2.1 C++98 里的 {} ------ 只能用于特定场景
在 C++98 中,花括号 {} 并不是一个通用的初始化工具,它主要用于两种场合:
- 数组初始化
- 结构体/聚合体的成员初始化
例如:
cpp
// C++98 支持
int array1[] = { 1, 2, 3, 4, 5 }; // 数组
int array2[5] = { 0 }; // 数组,其余元素自动置零
struct Point
{
int _x;
int _y;
};
Point p = { 1, 2 }; // 聚合体初始化
除此之外,花括号几乎无用武之地。如果你想用类似的方式初始化一个自定义类,或者直接初始化一个标准库容器,C++98 就无能为力了。
2.2 C++11 的目标:一切对象皆可 {}
C++11 的设计者希望提供一种 统一的初始化方式 ,让所有对象(不管是内置类型还是用户自定义类型)都能使用 {} 来初始化。这就是 列表初始化。
这个想法一经实现,带来了几个重要的特性:
- 内置类型支持
- 自定义类型支持(背后隐含了类型转换和临时对象的优化)
- 可以省略等号
- 在多参数构造时极为便利
3. 代码详解:从内置类型到自定义类型
为了让讲解更加清晰,我们先把需要用到的 Date 类摆出来:
cpp
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year), _month(month), _day(day)
{
cout << "Date(int year,int month,int day)" << endl;
}
Date(const Date& d)
:_year(d._year), _month(d._month), _day(d._day)
{
cout << "Date(const Date& d)" << endl;
}
private:
int _year;
int _month;
int _day;
};
这个类很简单:一个三参数构造函数(每个参数都有默认值),一个拷贝构造函数,并且它们都会在调用时输出信息,方便我们追踪构造过程。
3.1 内置类型也可用 {}
cpp
int x1 = { 2 };
int x2 = 2;
int x1 = { 2 }; 这行代码在 C++11 中是完全合法的。它用列表初始化的方式将 x1 初始化为 2。本质上,这里 {2} 会被解释成一个只有一个元素的列表,然后用来初始化 x1。它和 int x2 = 2; 在效果上完全一样。你可以把 {} 看成一种更严格、更统一的初始化符号。
3.2 自定义类型的列表初始化:临时对象 + 优化
接下来是重点:
cpp
Date d1 = { 2026, 6, 19 };
这行代码是如何工作的?编译器看到 { 2026, 6, 19 },会尝试用这三个值去构造一个 Date 对象。具体流程如下:
{ 2026, 6, 19 }先被用来构造一个Date类型的临时对象 ,也就是调用Date(int year, int month, int day)构造函数。- 这个临时对象再作为参数去拷贝构造
d1,即调用Date(const Date& d)。
按照语言规则,这是一个"构造临时对象 + 拷贝构造"的两步过程。但是 C++ 标准允许一种重要的优化:拷贝省略 ,编译器可以把这个过程合二为一,直接用 { 2026, 6, 19 } 去构造 d1,而无需生成中间的临时对象。你在运行程序时只会看到一次构造函数调用输出,就是这个优化的结果。

注意:即使没有优化,语义上也是先构造临时对象再拷贝。但在现代编译器中,哪怕不开启优化选项,也常常会执行这种"合二为一"的优化。
3.3 常量引用绑定临时对象
cpp
const Date& d2 = { 2026, 6, 19 };
同样,这里 { 2026, 6, 19 } 先构造出一个临时 Date 对象,然后 d2 这个常量引用绑定到该临时对象上。C++ 规定,常量引用可以延长临时对象的生命周期 ,所以 d2 成为该临时对象的别名,你可以安全地使用它,直到 d2 离开作用域。
这在函数传参时非常有用,例如某个函数接受 const Date&,你可以直接传入 {2026,6,19} 作为实参。
3.4 单参数的隐式转换
cpp
Date d3 = { 2026 }; // C++11 列表初始化,只传一个值
Date d4 = 2026; // C++98 也可以,因为构造函数没有 explicit
Date 的构造函数所有参数都有默认值,所以它同时也是一个转换构造函数(只有单个实参,且无 explicit 修饰的构造函数)。当只提供一个参数时,既可以写成 d3 = {2026},也可以直接写成 d4 = 2026。后者是 C++98 就支持的隐式类型转换,而前者是 C++11 统一初始化下的等价写法。
3.5 省略 =
C++11 允许在列表初始化时省略等号,形式更简洁:
cpp
Point p1{ 1, 2 }; // 直接列表初始化
int x3{ 2 }; // 内置类型也可以
Date d6{ 2026, 6, 19 }; // 直接构造,无拷贝
const Date& d7{ 2026, 6, 19 }; // 引用绑定
这种不写 = 的写法叫做直接列表初始化 。它和带了 = 的拷贝列表初始化在语义上有时有细微差别,但对于我们目前遇到的例子,它们的效果是等价的。
需要注意的是:只有 {} 语法才能省略 =,如果你完全不写 {},是不能省略 = 的 。比如 Date d8 2026; 是错误的,编译器无法理解。
3.6 容器操作中的大杀器
在实际编程中,列表初始化最大的便利之一体现在 STL 容器 的 push_back 和 insert 这类操作上:
cpp
vector<Date> v;
v.push_back(d1); // 1. 传一个已存在的对象
v.push_back(Date(2026, 6, 19)); // 2. 显式构造匿名对象传进去
v.push_back({ 2026, 6, 19 }); // 3. 直接用列表初始化,更简洁
对于方法 2,你需要写 Date(2026, 6, 19) 来显式构造一个匿名对象。而方法 3,你只需提供 { 2026, 6, 19 },编译器就会根据 push_back 的参数类型自动构造出一个 Date 临时对象,然后添加到容器中。这种方式显然更直观、更方便。
再看一个更明显的例子------map 的 insert:
cpp
map<string, string> dict;
dict.insert({ "xxx", "yyy" }); // 直接用花括号初始化一个 pair
map<string, string>::insert 接受的参数类型是 pair<const string, string>,我们直接用 { "xxx", "yyy" } 就能构造出这个 pair 对象,不用写成 make_pair("xxx","yyy") 或 pair<string,string>("xxx","yyy")。
4. std::initializer_list
上面的例子中,我们始终是在构造单个对象。但如果我想这样写呢?
cpp
vector<int> v = { 1, 2, 3 };
或者再大胆一点:
cpp
map<string, string> dict = { {"sort", "排序"}, {"string", "字符串"} };
这里的花括号里包含了多个元素。为了让这种语法能够工作,C++11 标准库引入了一个全新的类型:std::initializer_list。
4.1 initializer_list 是什么?
简单来说,initializer_list 是一个轻量级的、只读的"数组视图"。当你写下 { 10, 20, 30 } 这样的列表时,编译器会做以下事情:
- 在栈上 创建一个匿名的、元素类型为
const int的临时数组,存放10, 20, 30。- 用这个数组的首地址和尾后地址(或首地址加长度)构造一个
initializer_list<int>对象。
因此,initializer_list 内部通常只包含两个指针(分别指向数组的起始和末尾,或者一个指针加一个长度)。
4.2 验证 initializer_list 的实现
cpp
initializer_list<int> mylist;
mylist = { 10, 20, 30 };
cout << sizeof(mylist) << endl;
int i = 0;
cout << mylist.begin() << endl; // 指向数组起始地址
cout << mylist.end() << endl; // 指向数组尾后地址
cout << &i << endl; // 栈上变量 i 的地址

initializer_list 只是一个轻量外壳,仅存起始、结束指针,64位系统一个指针占8字节,2个指针合计16字节,所以 sizeof(mylist)=16 。实际 {10,20,30} 数组是编译器在栈上临时创建,列表只是引用这片内存,所以自身大小元素多少无关。
当你打印 mylist.begin() 和 mylist.end() 时,会发现这两个地址与变量 i 的地址很接近(都在栈空间内),这说明底层数组确实分配在栈上 ,而不是堆上。这也是为什么 initializer_list 的拷贝是浅拷贝,多个 initializer_list 可能指向同一块数组,使用时要确保底层数组的生命周期问题。
initializer_list 提供了 begin()、end() 等接口,因此可以像普通容器一样用范围 for 遍历:
cpp
for (auto e : mylist)
{
cout << e << " ";
}
4.3 容器如何支持多元素初始化
STL 容器(如 vector、list、map)都增加了一个接受 std::initializer_list 的构造函数,这才使得我们可以这样写:
cpp
vector<int> v1({ 1, 2, 3, 4, 5 }); // 直接传递 initializer_list
vector<int> v2 = { 1, 2, 3, 4, 5 }; // 拷贝列表初始化
const vector<int>& v3 = { 1, 2, 3, 4, 5 }; // 引用绑定到临时 vector
这三个写法在最终效果上都是把 1,2,3,4,5 放进 vector 中,但它们背后发生的细节略有不同:
v1({...}):用{...}生成一个initializer_list<int>,直接传给vector的构造函数,构造出v1。v2 = {...}:先生成initializer_list构造临时vector,再用这个临时对象拷贝构造v2,然后编译器通常会优化为直接构造(同前面Date的优化)。v3:临时vector被常量引用绑定,生命周期延长至v3作用域结束。
map 的多元素初始化更是 initializer_list 与列表初始化的完美结合:
cpp
map<string, string> dict = { {"sort", "排序"}, {"string", "字符串"} };
这个花括号的整体是一个 initializer_list<pair<const string, string>>。里层的每个 {"sort", "排序"} 又会各自去调用 pair 的构造函数,形成一个个 pair 元素。所有元素被遍历并插入到 map 中。一行代码完成了一个字典的初始化。
4.4 同样适用于赋值操作
initializer_list 不仅用于构造,也用于赋值。标准库容器同样提供了接受 initializer_list 的赋值运算符重载:
cpp
vector<int> v1 = { 1, 2, 3, 4, 5 };
v1 = { 10, 20, 30, 40, 50 }; // 用 initializer_list 替换所有内容
5. 深入本质:类型转换与优化
我们来回顾一下列表初始化背后发生的事情,并把它归纳成一条清晰的规则:
当使用 {} 初始化一个对象时,编译器会尝试找到一个可以通过这些参数调用的构造函数。 如果目标类型是类类型,{} 里的值会被视为构造函数参数,并触发构造。这一过程中可能产生临时对象,但编译器有权(而且几乎总是会)通过拷贝省略优化掉不必要的拷贝。
对于容器而言,多元素列表会隐式生成 std::initializer_list,然后调用专有的构造函数或赋值函数。
5.1 窄化转换的防御
列表初始化还有一个额外的好处:禁止窄化转换。
cpp
int x = 3.14; // 允许,但会丢失精度,x 变成 3
int y = { 3.14 }; // 错误!窄化转换在列表初始化中不被允许
这种防御性使得代码更加安全,当你需要显式进行窄化转换时,必须使用 static_cast<int>(3.14) ,强制写明意图,避免无意丢数据。
5.2 explicit 与列表初始化
当构造函数被声明为 explicit 时:
cpp
class Date
{
public:
explicit Date(int year, int month = 1, int day = 1)
{ ... }
};
此时,Date d = {2026,6,19}; 等号+花括号 = 拷贝式初始化,禁止调用 explicit 构造函数,编译报错,不允许隐式转换。
而 Date d{ 2026, 6, 19 }; 是直接列表初始化,可以调用 explicit 构造函数。
结语:
今天的内容到这里就结束了,希望你能有所收获~
干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _