【C++11】列表初始化initializer_list深度剖析
一、C++的发展史
C++11 是 C++ 的第二个主要版本,并且是从 C++98 起的最重要更新。它引入了大量更改,标准化了既有实践,并改进了对C++程序员可用的抽象。在它最终由 ISO 在 2011 年 8月12日采纳前,人们曾使⽤名称“C++0x”,因为它曾被期待在 2010 年之前发布。C++03 与 C++11 期间花了 8 年时间,故而这是迄今为止最长的版本间隔。从那时起,C++ 有规律地每 3 年更新一次。
二、列表初始化
1. 出现的问题
在C++98中,我们可以用{}初始化数组和结构体。
int arr[] = {1,2,3}; struct Point { int x; int y; }; Point p = {1,2};但是我们不能这样初始化类对象:
string s = {"hello"}; // C++98不支持2. C++11的列表初始化
为了解决C++98中列表初始化的问题,C++11的列表初始化应运而生,它可以应用于任何数据类型的初始化,实现了初始化方式的统一。
- 这个初始化内置类型支持,自定义类型也支持,自定义类型本质是类型转换,中间会产生临时对象,最后优化了以后变成直接构造。
- {}初始化的过程中,可以省略掉=
- C++11列表初始化的本意是想实现一个大统一的初始化方式,其次他在有些场景下带来的不少便利,如容器push/insert多参数构造的对象时,{}初始化会很方便
2.1 内置类型的初始化
int a=2; int b{2};列表初始化与普通的初始化完全一样,我们可以看看它们底层的汇编语言:
可见它们没的汇编语言一模一样,没有任何区别。
2.2 自定义类型的初始化
class Date { public: Date(const int year,const int month,const int day) :_year(year),_month(month),_day(day) { cout << "Date(const int year,const int month,const 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; }; int main() { Date d1(2026, 6, 22); Date d2 = { 2026,6,22 }; return 0; }自定义类型也是一样的,列表初始化与普通初始化没有区别:
如果在编译器不优化的情况下,对于:
Date d2 = { 2026,6,22 };编译器会先先生成一个{2026,6,22}的临时变量,再拷贝给d2。编译器采取了优化,它会直接构造{2026,6,22}的对象d1。
我们可以运行以上代码验证:
只有两个构造,没有拷贝,印证了之前的结论。
const Date& d2 = { 2026, 6, 22 };对于d2,它引用的是{2026,6,22}产生的临时对象,临时对象具有常性,是一个右值,必须要加const,至于右值的概念,下一节会详细讲述。
对于类型转换,C++98支持单参数类型转换,可以省去{}
//在构造函数后两个参数为缺省值的情况下 Date d3 = { 2026 }; //C++11列表初始化 Date d4 = 2026; //C++98单参数对于列表初始化来讲,可以省略“=”,但是C++98的单参数类型转换不能省略“=”。
Date d3{ 2026 }; //C++11可行 Date d4 2026; //C++98不支持2.3 STL中的应用
我们就以vector的push_back为例:
vector<Date> v; v.push_back(Date{ 2026,6,22 }); v.push_back({ 2026,6,22 });我们在一个vector< Date >中插入数据,如果不使用初始化列表的话我们是创建一个匿名对象再进行插入;那么现在我们可以直接省略Date,直接插入{}。
三、std::initializer_list
1. 什么是initializer_list
我们先看一行代码:
Date d2 = { 2026,6,22 }; vector<int> v1{ 1,2,3,4,5 };这两个看上去好像是同一种语法,但是它们在底层调用上有所区别。
对于Date来讲,编译器看到{ 2026,6,22 },它会匹配到Date(int,int,int)的构造函数,直接构造。
对于vector来讲,编译器看到{1,2,3,4,5},它会匹配到vector(initializer_list)
引入正题,initializer_list就是C++11 提供的一个轻量级容器,之前我们也有提到过。我们先来看看它的声明:
它的接口也是非常的精简:
2. 底层与迭代器
它的底层结构大概是这样:
template<class T> class initializer_list { public: typedef const T* iterator; typedef const T* const_iterator; iterator begin() const { return _first; } iterator end() const { return _last; } size_t size() const { return _last - _first; } private: const T* _First; const T* _Last; };它实际上就是_First和_Last两个指针,两指针分别指向数据头和尾的下一个,它的本质就是一个只读区间。它里面有begin和end两个迭代器,因此它支持范围for。它的内层是开辟在栈区的。
3. vector的情形
当我们调用:
vector<int> v1{ 1,2,3,4,5 };编译器匹配vector(initializer_list),实际情况下,编译器直接将列表里的数给v1,而不产生额外的开销。
4. 总结
| 项目 | 底层实现 |
|---|---|
| 存储结构 | 两个指针 |
| begin() | const T* |
| end() | const T* |
| iterator | const T* |
| 支持范围for | 支持 |
| 元素可修改 | 不支持 |
| 本质 | 一个只读区间 |
要注意:initializer_list本身不存储数据,只是对编译器生成的临时数组进行包装。它内部仅维护[first,last)两个指针,其迭代器本质就是const T*。
