【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中,我们可以用{}初始化数组和结构体

cpp 复制代码
int arr[] = {1,2,3};

struct Point
{
    int x;
    int y;
};

Point p = {1,2};

但是我们不能这样初始化类对象:

cpp 复制代码
string s = {"hello"}; // C++98不支持

2. C++11的列表初始化

为了解决C++98中列表初始化的问题,C++11的列表初始化应运而生 ,它可以应用于任何数据类型的初始化,实现了初始化方式的统一。

  • 这个初始化内置类型支持,自定义类型也支持 ,自定义类型本质是类型转换,中间会产生临时对象,最后优化了以后变成直接构造
  • {}初始化的过程中,可以省略掉=
  • C++11列表初始化的本意是想实现一个大统一的初始化方式,其次他在有些场景下带来的不少便利,如容器push/insert多参数构造的对象时,{}初始化会很方便

2.1 内置类型的初始化

cpp 复制代码
int a=2;
int b{2};

列表初始化与普通的初始化完全一样,我们可以看看它们底层的汇编语言:

可见它们没的汇编语言一模一样,没有任何区别。

2.2 自定义类型的初始化

cpp 复制代码
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;
}

自定义类型也是一样的,列表初始化与普通初始化没有区别:

如果在编译器不优化的情况下,对于:

cpp 复制代码
Date d2 = { 2026,6,22 };

编译器会先先生成一个{2026,6,22}的临时变量,再拷贝给d2。编译器采取了优化,它会直接构造{2026,6,22}的对象d1。

我们可以运行以上代码验证:

只有两个构造,没有拷贝,印证了之前的结论。

cpp 复制代码
const Date& d2 = { 2026, 6, 22 };

对于d2,它引用的是{2026,6,22}产生的临时对象临时对象具有常性,是一个右值,必须要加const ,至于右值的概念,下一节会详细讲述。

对于类型转换,C++98支持单参数类型转换,可以省去{}

cpp 复制代码
//在构造函数后两个参数为缺省值的情况下
Date d3 = { 2026 };	//C++11列表初始化
Date d4 = 2026;		//C++98单参数

对于列表初始化来讲,可以省略"=",但是C++98的单参数类型转换不能省略"="。

cpp 复制代码
Date d3{ 2026 };	//C++11可行
Date d4 2026;		//C++98不支持

2.3 STL中的应用

我们就以vector的push_back为例:

cpp 复制代码
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

我们先看一行代码:

cpp 复制代码
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. 底层与迭代器

它的底层结构大概是这样:

cpp 复制代码
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的情形

当我们调用:

cpp 复制代码
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*