一、从变量初始化开始谈起
一般而言,对变量或对象使用括号初始化的方式被称为直接初始化,其本质是调用了相应的构造函数;而使用等号初始化的方式则被称为拷贝初始化,说到拷贝大家可能马上就会想到拷贝构造函数、operator =()函数,但此时并不一定是调用了这两个函数,这点极容易混淆!!!
为了阐明上述的观点,我们不妨通过一个小实验的方式来验证上述的说法:
#include<iostream>
class Test
{
public:
Test(int i) {}
};
Test func(Test t)
{
return t;
}
int main()
{
int a = 0; //拷贝初始化
int b(100); //直接初始化
Test t = 1; //拷贝初始化, 实际上是隐式调用了转换构造函数Test(int i){}
Test t1(5); //直接初始化, 调用构造函数Test(int i){}
//传参以及返回值都是拷贝初始化, 但本质是调用了拷贝构造函数(可能存在编译器的优化,拷贝次数不定)
Test t2 = func(t1);
return 0;
}
如何证明Test t = 1是编译器隐式地调用了转换构造函数来初始化对象t的呢?只需在该构造函数前面加上explicit前缀修饰,此时就能发现编译器的错误提示(如下)。
二、有意或无意中使用的列表初始化
除了圆括号和等号的初始化方式,我们还能在一些代码中看到用大括号{}来初始化变量或对象的身影,比如数组的初始化int a[] = {10,20,30,40}。除了数组以外,可能读者还看到在容器、自定义类类型中也用到了这种大括号{}的初始化方式,其背后的原因究竟是什么呢?
自C++11标准开始就引入了列表初始化的概念,即支持使用{}对变量或对象进行初始化,且与传统的变量初始化的规则一样,也分为拷贝初始化和直接初始化两种方式。
下面是这种列表初始化的使用方式,相信读者已经或多或少地使用过它了:
#include<iostream>
#include<vector>
#include<map>
class Test
{
public:
Test(std::string s, int val) {}
};
int main()
{
int arr[] = {10, 20, 30, 40}; //拷贝初始化
int brr[]{10, 20, 30, 40}; //直接初始化
std::vector<int> vc1 = {10, 20, 30 ,40};
std::vector<int> vc2{10, 20, 30, 40};
std::map<std::string, int> m1 = { {"a", 1}, {"b", 2}, {"c", 3} };
Test *pt = new Test{"test", 100};
delete pt;
return 0;
}
一些读者可能对于m1对象初始化的写法上会感到困惑。这里的大括号嵌套大括号是什么玩意儿?实际上这里发生了两步构造:
-
{"a", 1}, {"b", 2}, {"c", 3}先调用了std::pair的构造函数pair(const T1& x, const T2& y)获得了3个pair对象。
-
外层的{}又调用了map的构造函数,那这里传递给map构造函数的参数是什么呢?先留个悬念,顺着往下看你就懂了!
三、std::initializer_list的本质
上面所举的例子中用到了{}对标准库容器进行初始化,而标准库容器之所以能够支持初始化列表,除了有编译器的支持外,更直接的是这些容器存在以std::initializer_list为形参的构造函数。
说到这,std::initializer_list到底是什么呢?不妨通过源码的方式来认识一下它:
template<class _E>
class initializer_list
{
public:
typedef _E value_type;
typedef const _E& reference;
typedef const _E& const_reference;
typedef size_t size_type;
typedef const _E* iterator;
typedef const _E* const_iterator;
private:
iterator _M_array;
size_type _M_len;
// The compiler can call a private constructor.
constexpr initializer_list(const_iterator __a, size_type __l)
: _M_array(__a), _M_len(__l) { }
public:
constexpr initializer_list() noexcept
: _M_array(0), _M_len(0) { }
// Number of elements.
constexpr size_type
size() const noexcept { return _M_len; }
// First element.
constexpr const_iterator
begin() const noexcept { return _M_array; }
// One past the last element.
constexpr const_iterator
end() const noexcept { return begin() + size(); }
};
上面贴出了STL中std::initializer_list的源码,从中也可以看出来std::initializer_list是一个类模板,其内部机制是const T* _M_array,且携带有长度信息size_t _M_len,同时还提供了指向该类对象首端、尾端的迭代器(即常量对象指针const T*)。
另外,从下图也可以看出,编译器会负责将列表里的元素(大括号中的内容)构造为一个std::initializer_list对象,实际也就是编译器调用了这个私有的构造函数:
当得到了一个std::initializer_list对象后,再来寻找标准容器中以std::initializer_list为形参的构造函数,并调用该构造函数对容器进行初始化。
四、std::initializer_list能被进行遍历吗?
因为std::initializer_list是一个类模板,当然也能使用它来创建相应的类对象,但该对象一般只能用来初始化标准容器或特定的自定义类对象,但感觉它确实又是存放了一堆元素在一个列表中,有点容器的感觉,而且initializer_list的底层机制不就是指针空间嘛,说到此处,读者是否与我一样会心一笑呢?来看下该怎么遍历initializer_list吧:
#include<iostream>
#include<initializer_list>
int main()
{
using namespace std;
initializer_list<int> list;
list = {10, 20, 30 ,40};
for(auto it = list.begin(); it != list.end(); ++it) {
cout << *it << endl;
}
//cout << list[0] << endl; //error, 源码中并没有重载[]
return 0;
}