
[1. 1C++98 中的 {}](#1. 1C++98 中的 {})
[1.2 C++11:统一的列表初始化](#1.2 C++11:统一的列表初始化)
[1.3 {} 初始化自定义类时到底发生了什么?](#1.3 {} 初始化自定义类时到底发生了什么?)
[1.1 构造函数的"固有问题"](#1.1 构造函数的“固有问题”)
[1.2 C++98 时代的"曲线救国"方案](#1.2 C++98 时代的“曲线救国”方案)
[方案一:多次 push_back](#方案一:多次 push_back)
[2. C++11 的解决方案:std::initializer_list](#2. C++11 的解决方案:std::initializer_list)
[2.1 initializer_list 的底层实现](#2.1 initializer_list 的底层实现)
[2.2 编译器的魔法:花括号列表到 initializer_list 的转换](#2.2 编译器的魔法:花括号列表到 initializer_list 的转换)
[3. 容器如何利用 initializer_list?](#3. 容器如何利用 initializer_list?)
[3.1 新增的构造函数](#3.1 新增的构造函数)
[3.2 完整的调用链](#3.2 完整的调用链)
[3.3 多种使用形式](#3.3 多种使用形式)
[3.4 复杂嵌套:map 的初始化](#3.4 复杂嵌套:map 的初始化)
[4. 内存布局与性能考量](#4. 内存布局与性能考量)
[4.1 内存布局图示](#4.1 内存布局图示)
[4.2 性能特点](#4.2 性能特点)
[4.3 常见问题答疑(FAQ)](#4.3 常见问题答疑(FAQ))
[Q1: initializer_list 会拷贝数据吗?](#Q1: initializer_list 会拷贝数据吗?)
[Q2: 为什么 initializer_list 的元素是 const 的?](#Q2: 为什么 initializer_list 的元素是 const 的?)
[Q3: initializer_list 和原生数组有什么区别?](#Q3: initializer_list 和原生数组有什么区别?)
[Q4: 所有 STL 容器都支持 initializer_list 构造吗?](#Q4: 所有 STL 容器都支持 initializer_list 构造吗?)
bash
C++11系列的全部目录
C++11 新特性详解(一)
├── 1. C++11简介
├── 2. 列表初始化
│ ├── C++98中的{}
│ ├── C++11统一初始化
│ └── initializer_list
└── 3. 右值引用基础
├── 左值和右值
├── 左值引用和右值引用
├── std::move
└── 右值引用变量为什么又是左值
C++11 新特性详解(二)
├── 4. 移动语义
│ ├── 为什么需要移动构造
│ ├── 移动构造与移动赋值
│ ├── 深拷贝类中的资源转移
│ ├── 传值返回
│ └── STL中的移动
├── 5. 右值引用进阶
│ ├── 值类别
│ ├── 引用折叠
│ └── 万能引用
C++11 新特性详解(三)
├── 6. 完美转发
│ ├── 为什么需要forward
│ ├── T&&到底是什么
│ └── std::forward的原理
├── 7. 可变参数模板
│ ├── 参数包
│ ├── sizeof...
│ ├── 包扩展
│ └── 递归展开
└── 8. emplace系列
C++11 新特性详解(四)
├── 9. Lambda表达式
│ ├── Lambda是什么
│ ├── 为什么需要Lambda
│ ├── 仿函数与Lambda
│ ├── operator()
│ ├── auto为什么可以接Lambda
│ ├── 捕捉列表
│ ├── 值捕捉/引用捕捉
│ ├── 隐式捕捉
│ ├── mutable
│ └── Lambda底层原理
C++11 新特性详解(五)
├── 10. std::function
├── 11. bind
├── 12. function + Lambda + bind
├── 13. 逆波兰表达式案例
└── 14. C++11知识体系总结
前言
C++11 是 C++ 发展历史上非常重要的一次标准更新。它并不是简单地增加几个语法,而是对 C++ 的对象管理、泛型编程、STL、函数对象等多个方面进行了系统性的增强。C++11 在 2011 年正式被 ISO 采纳,在 C++98 到 C++11 之间经历了较长的标准空窗期,因此它也是现代 C++ 的一个重要分界点。
本文按照 C++11 的知识脉络,从最容易理解的列表初始化开始,逐步进入 C++11 最重要的核心内容之一------右值引用和移动语义。学习这一部分时不要急着背语法,尤其是右值引用、std::move、移动构造这些概念,它们真正解决的是一个问题:
如何减少不必要的深拷贝,让对象的资源能够被高效地"转移"。
一、列表初始化
1. 1C++98 中的 {}
在 C++98 中,{} 主要用于数组、结构体等聚合类型的初始化。
cpp
struct Point
{
int _x;
int _y;
};
int main()
{
int array1[] = { 1, 2, 3, 4, 5 };
int array2[5] = { 0 };
Point p = { 1, 2 };
return 0;
}
这里的 {} 本质上是传统的聚合初始化方式。
但是这种初始化方式存在一个明显的问题:不同类型需要不同的初始化方法
cpp
//例如数组可以:
int a[] = {1, 2, 3};
//结构体可以:
Point p = {1, 2};
但是一个普通的自定义类,C++98 并不能简单地把 {} 作为统一的初始化方式使用。
C++11 就开始尝试解决这个问题。
1.2 C++11:统一的列表初始化
C++11 引入了一个重要思想:
尽可能统一对象的初始化方式,让各种类型都可以使用
{}进行初始化。
因此下面这些代码都成为合法形式:
cpp
int x1 = { 2 };
Point p1 { 1, 2 };
Date d1 = { 2025, 1, 1 };
Date d2 { 2024, 7, 25 };
甚至 = 都可以省略:
cpp
int x { 2 };
Point p { 1, 2 };
Date d { 2024, 7, 25 };
这里要注意一个非常容易忽略的地方:
cppPoint p {1, 2};并不是说
{}本身就是一种特殊对象,而是 C++11 给出了统一的列表初始化语法,再根据目标类型去匹配相应的构造方式。课件中的Date示例也专门展示了这一点。
1.3 {} 初始化自定义类时到底发生了什么?
假设我们有:
cpp
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
cpp
Date d1 = { 2025, 1, 1 };
可以从语义上理解为:
Date d1(2025, 1, 1);
一个容易让初学者困惑的问题:从语言语义层面可以理解为列表形成临时对象,再进行初始化,但编译器通常会进行拷贝消除等优化,最终直接构造目标对象,因此实际运行时不一定真的发生一次额外的拷贝构造。
所以学习 C++ 时一定要注意:
"语言层面的抽象过程"和"编译器最终生成的机器代码"不是一回事。
我们首先应该理解语言规则,然后再去理解编译器优化。
二,std::initializer_list
1.1 构造函数的"固有问题"
我们知道,vector 是一个类模板,它的构造函数(包括重载)的参数个数和类型在编译时必须是确定的。例如:
cpp
cpp
template<class T>
class vector {
public:
explicit vector(size_t count); // 1个参数
vector(size_t count, const T& value); // 2个参数
vector(const vector& other); // 1个参数(拷贝构造)
// ... 等等
};
如果要支持 vector<int> v = {1, 2, 3, 4, 5};,那么 vector 必须有一个构造函数能够接受任意数量的 int 参数。但是,构造函数的重载数量是有限的,我们不可能为每一种可能的元素个数都写一个重载:
cpp
cpp
vector(int e1); // 支持 {1}
vector(int e1, int e2); // 支持 {1,2}
vector(int e1, int e2, int e3); // 支持 {1,2,3}
// ... 无穷无尽
这显然是不可行的。
1.2 C++98 时代的"曲线救国"方案
在 C++98 中,只有原生数组 (built-in array)可以享受 {1,2,3,4,5} 这种初始化方式,而像 vector 这样的标准容器却不行。为了达到类似的效果,程序员只能采用一些笨拙的变通方法:
方案一:多次 push_back
cpp
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
缺点 :代码冗长,尤其是当元素很多时;并且会多次调用 push_back,可能引发多次内存分配和拷贝,效率低下。
方案二:使用数组作为中介
cpp
int arr[] = {1, 2, 3, 4, 5};
vector<int> v(arr, arr + 5); // 利用迭代器区间构造函数
缺点:需要额外定义一个数组,而且这种用法不够直观,破坏了代码的简洁性。
方案三:仅原生数组独享的特权
cpp
int arr[] = {1, 2, 3, 4, 5}; // 只有数组可以!
原生数组是 C 语言遗留下来的特性,C++ 标准委员会一直希望将这种便利性推广到所有对象,尤其是容器。
2. C++11 的解决方案:std::initializer_list
为了从根本上解决"构造函数参数个数不固定"的问题,C++11 引入了一个全新的类型模板------std::initializer_list<T>。它的核心思想是:编译器遇到花括号列表时,会将其转换为一个 initializer_list 对象,该对象内部保存了指向一个匿名数组的指针,这个数组存储了列表中的所有元素。 这样,容器只需要提供一个以 initializer_list 为参数的构造函数,就能接受任意数量的元素进行初始化。
2.1 initializer_list 的底层实现
std::initializer_list 是一个非常轻量的类模板,其内部实现大致如下(实际标准库实现可能略有不同):
cpp
cpp
template<class T>
class initializer_list {
private:
const T* _begin; // 指向匿名数组的第一个元素
const T* _end; // 指向匿名数组的最后一个元素的下一个位置
public:
// 构造函数(由编译器隐式调用,用户不能直接构造)
constexpr initializer_list(const T* b, const T* e) noexcept
: _begin(b), _end(e) {}
// 获取元素个数
constexpr size_t size() const noexcept { return _end - _begin; }
// 迭代器支持(只读)
const T* begin() const noexcept { return _begin; }
const T* end() const noexcept { return _end; }
};
关键点:
-
initializer_list不拥有数据,它只是一个"视图"(view),指向编译器生成的匿名数组。 -
该数组的元素是
const的,所以initializer_list只能提供只读访问。 -
initializer_list的大小固定为两个指针(在 64 位系统上通常为 16 字节),非常轻量。
2.2 编译器的魔法:花括号列表到 initializer_list 的转换
当我们在代码中写下 {10, 20, 30} 时,编译器会执行以下步骤:
-
创建一个匿名数组 (通常在栈上):
int __hidden_array[] = {10, 20, 30}; -
构造一个
std::initializer_list<int>对象 :std::initializer_list<int> il(&__hidden_array[0], &__hidden_array[3]);
这个转换过程是编译器自动完成的,对程序员来说是完全透明的。
我们可以通过一个小实验来验证这一点(来自 C++11 课件第 5 页):
cpp
cpp
#include <iostream>
#include <initializer_list>
int main() {
std::initializer_list<int> mylist;
mylist = {10, 20, 30};
std::cout << sizeof(mylist) << std::endl; // 通常输出 16(两个指针)
int i = 0;
std::cout << mylist.begin() << std::endl; // 输出某个栈地址,例如 0x61fdf0
std::cout << mylist.end() << std::endl; // 输出另一个栈地址,例如 0x61fdfc
std::cout << &i << std::endl; // 输出另一个栈地址,例如 0x61fde0
return 0;
}
观察输出可以发现,begin() 和 end() 返回的地址与局部变量 i 的地址非常接近,这有力地证明了匿名数组存在于栈上。
3. 容器如何利用 initializer_list?
3.1 新增的构造函数
既然有了 initializer_list 这个"承载任意多个值"的工具,STL 容器(如 vector、list、map 等)在 C++11 中都增加了一个新的构造函数重载:
cpp
cpp
template<class T>
class vector {
public:
// 新增的 initializer_list 构造函数
vector(std::initializer_list<T> il) {
// 预分配足够的空间
reserve(il.size());
// 遍历 initializer_list,将元素拷贝到容器中
for (const T& elem : il) {
push_back(elem);
}
}
// ... 其他构造函数
};
3.2 完整的调用链
现在,当我们写下 vector<int> v = {1, 2, 3, 4, 5}; 时,整个流程如下:
编译器遇到
{1, 2, 3, 4, 5},在栈上创建匿名数组int __tmp[] = {1, 2, 3, 4, 5};构造一个
std::initializer_list<int>临时对象 ,它持有指向__tmp的指针。调用
vector的initializer_list构造函数,将临时对象作为参数传入。构造函数内部遍历
initializer_list,将每个元素push_back到新构造的vector中。最终,
v拥有了{1,2,3,4,5}这五个元素。
这个过程可能涉及一次拷贝(从匿名数组到 vector 内部存储),但由于 vector 在构造函数中会预先分配空间,所以效率是完全可以接受的。
3.3 多种使用形式
C++11 支持多种形式的列表初始化,它们都依赖 initializer_list:
cpp
cpp
// 1. 直接构造(显式调用)
vector<int> v1({1, 2, 3, 4, 5});
// 2. 拷贝初始化(最常见的形式)
vector<int> v2 = {1, 2, 3, 4, 5}; // 编译器会优化为直接构造,避免临时对象的拷贝
// 3. 省略等号(最简洁)
vector<int> v3 {1, 2, 3, 4, 5};
// 4. const 引用绑定临时对象(生命周期延长)
const vector<int>& v4 = {1, 2, 3, 4, 5};
// 5. 赋值操作(operator= 也有 initializer_list 重载)
v1 = {10, 20, 30, 40, 50};
3.4 复杂嵌套:map 的初始化
initializer_list 的强大之处还在于它可以嵌套使用,例如初始化 map:
cpp
cpp
map<string, string> dict = {
{"sort", "排序"},
{"string", "字符串"}
};
解析过程:
-
外层
{ ... }是一个initializer_list<pair<string, string>> -
内层
{"sort", "排序"}则是一个initializer_list用于构造pair<string, string>对象(因为pair也支持列表初始化) -
map的initializer_list构造函数接收这些pair对象,插入到红黑树中。
这种嵌套机制让 C++ 的初始化语法变得极其统一和强大。
4. 内存布局与性能考量
4.1 内存布局图示
栈内存: ┌─────────────────────┐ │ __hidden_array: │ 地址:0x1000 │ [10] │ 0x1000 │ [20] │ 0x1004 │ [30] │ 0x1008 └─────────────────────┘ ┌─────────────────────┐ │ mylist (initializer_list): │ │ _begin = 0x1000 │ │ _end = 0x100c │ (0x1000 + 3*sizeof(int)) └─────────────────────┘
4.2 性能特点
| 特性 | 说明 |
|---|---|
| 只读访问 | 元素是 const 的,不可修改,保证了安全性。 |
| 轻量 | 只包含两个指针,复制成本极低(类似复制两个指针)。 |
| 临时性 | 通常作为临时对象传递,生命周期由编译器管理(绑定到引用或构造容器时会延长)。 |
| 容器无关 | 所有 STL 容器都支持,统一了初始化语法。 |
| 拷贝开销 | 初始化时会将匿名数组的元素拷贝到容器中,但这是必要的;对于移动语义友好的类型,可能进一步优化。 |
4.3 常见问题答疑(FAQ)
Q1:
initializer_list会拷贝数据吗?A: 是的,当编译器创建匿名数组时,数据被拷贝到该数组中。但这是合理的,因为字面量本身不是持久存储的。在容器构造过程中,数据还会被再次拷贝到容器的内部存储中。
Q2: 为什么
initializer_list的元素是const的?A: 因为匿名数组通常位于只读内存区域或栈上,修改它可能导致未定义行为。设计为只读更安全,也符合其作为"视图"的语义。
Q3:
initializer_list和原生数组有什么区别?A:
原生数组 :大小固定,类型为
T[N],可以取地址,支持修改。
initializer_list:大小在运行时确定,类型为initializer_list<T>,提供迭代器接口,可以像容器一样遍历,但元素只读。Q4: 所有 STL 容器都支持
initializer_list构造吗?A : 是的。
vector、list、deque、map、set、unordered_map等所有标准容器都在 C++11 中增加了相应的构造函数和赋值操作符。