列表初始化 分析initializer_list<T>的实现
1. 统一初始化(Uniform Initialization)
(1)在C++11之前,很多程序员特别是初学者对如何初始化一个变量或对象的问题很容易出现困惑。因为可以用小括号、大括号或赋值操作符等多种方式进行初始化。
(2)基于这个原因,C++11引入了“统一初始化”的概念。这意味着我们可以使用{}这种通用的语法在任何需要初始化的地方。
【实例分析】初始化列表
#include <iostream> #include <vector> #include <map> #include <complex> using namespace std; //编译选项:g++ -std=c++11 test1.cpp -fno-elide-constructors struct Test { int x; int y; } test {123, 321}; //等价于test = {123,321} int main() { int i; //未初始化 int j{}; //j被初始化为0 int* p; //未初始化 int* q{}; //j被初始化为nullptr int* a = new int{123}; //等价于int* x=new int(123); double b = double{12.12}; //等价于double(12.12)产生一个临时对象,再拷贝初始化 int* arr = new int[3]{1, 2, 3}; //C++11中新增的初始化堆上数组的方式 std::map<std::string, int> mm {{"2", 1},{"2", 2}, {"3", 3}}; //相当于map<string,int> mm = {...}; int values[]{1, 2, 3}; //等价于int values[]={1, 2, 3}; vector<int> v{2, 3, 5, 7, 11, 13, 17}; complex<double> c{4.0, 3.0}; //等价于c(4.0, 3.0); cout << test.x << endl; cout << test.y << endl; return 0; }
2. 列表初始化的使用细节
(1)引入初始化列表(initializer-list)出现的一些模糊概念
//x,y究竟为0,0还是123,321? struct A { int x; int y; A(int,int):x(0), y(0){} //非聚合类型,使用{}初始化时会调用相应的构造函数 } a = {123, 321}; //a.x=0, a.y=0
(2)聚合类型的定义
①类型是一个普通类型的数组(如int[10]、char[]、long[2][3])
②类型是一个类(class、struct或union),且:
A.无基类、无虚函数以及无用户自定义的构造函数。
B.无private或protected的普通数据成员(即非静态数据成员)。
C.不能有{}和=直接初始化的非静态数据成员(“就地初始化”)
【实例分析】聚合类型与非聚合类型的初始化
#include <iostream> using namespace std; //x,y究竟为0,0还是123,321? struct A { int x; int y; A(int,int):x(0), y(0){} //非聚合类型,使用{}初始化时会调用相应的构造函数 } a = {123, 321}; //a.x=0, a.y=0 struct Base{}; //聚合类型的定义 struct Foo : public Base //不能有基类 { private: double z; //不能有private的普通成员 static int k; //ok,但必须在类外用int Foo::k = 0的方式初始化 public: Foo(int x, int y, double z):x(x),y(y),z(z) //不能有构造函数! { cout<< "Foo(int x, int y, double z)" << endl; } Foo(const Foo& foo) //不能有构造函数! { this->x = foo.x; this->y = foo.y; this->z = foo.z; cout<< "Foo(const Foo& foo)" << endl; } int x; int y; //不能通过int y=0;或int y{0}来"就地初始化" virtual void F(){}; //不能有虚函数! }; int main() { Foo f1(1, 2, 3.0); //直接调用构造函数初始化 Foo f2{4, 5, 6.0}; //由于Foo是个非聚合类型,使用{}时会调用相应的构造函数来初始化。 Foo f3 = {7, 8, 9.0}; //非聚合类型会调用构造函数来初始化 cout <<"a.x = " << a.x << ", a.y = " << a.y << endl; return 0; } /*输出结果 Foo(int x, int y, double z) Foo(int x, int y, double z) Foo(int x, int y, double z) a.x = 0, a.y = 0 */
(3)注意事项
①聚合类型的定义是非递归的。简单来说,当一个类的普通成员是非聚合类型时,这个类也有可能是聚合类型,也就是说可以直接用列表初始化。
②对于一个聚合类型,可以直接使用{}进行初始化,这时相当于对其中每个元素分别赋值;而对于非聚合类型,则需要先自定义一个合适的构造函数才能使用{}进行初始化,此时使用初始化列表将调用它对应的构造函数。
(1)当编译器看到{t1,t2…tn}时便会生成一个initializer_list<T>对象(其中的T为元素的类型),它关联到一个array<T,n>。
(2)对于聚合类型,编译器会将array<T,n>内的元素逐一分解并赋值给被初始化的对象。这相当于为该对象每个字段分别赋值。
(3)对于非聚合类型。如果该类存在一个接受initializer_list<T>类型的构造函数,则初始化时会将initializer_list<T>对象作为一个整体传给构造函数。如果不存在这样的构造函数,则array内的元素会被编译器分解并传给相应的能接受这些参数的构造函数(比如列表中有2个元素的,就传给带2个参数的构造函数。有3个元素的,就传给带3个参数的构造函数,依此类推……)。
【实例分析】initializer_list<T>初体验
#include <iostream> #include <vector> #include <map> #include <complex> using namespace std; //编译选项:g++ -std=c++11 test1.cpp -fno-elide-constructors class Foo { public: Foo(int) { cout << "Foo(int)"<< endl; } Foo(int, int) { cout << "Foo(int, int)"<< endl; } Foo(const Foo& f) { cout << "Foo(const Foo& f)"<< endl; } }; int main() { Foo f1(123); Foo f2 = 123; //先将调用Foo(int)将123转为Foo对象,再调用拷贝构造函数(后面这步可能被优化) Foo f3 = {123}; //生成initializer_list<int>,然后分解元素后,由于列表中只有1个元素,所以将其传给Foo(int) Foo f4 = {123, 321}; //生成initializer_list<int>,然后分解元素后,由于列表中有两个元素,所以将其传给Foo(int, int) //编译器会为以下花括号形成一个initializer_list<string>,背后有个array<string,6> //调用vector<string>的构造函数时,编译器会找到一个接受initializer_list<string> //的重载的构造函数。所有的容器均有这样的构造函数。在这个构造函数里会利用 //initializer_list<string>来初始化。 vector<string> city{"Berlin", "New York", "London", "Cairo","Tokyo", "Cologne"}; //编译器会为以下花括号形成一个initializer_list<double>,背后有个array<double,2>。 //调用complex<double>的构造函数时,array内的2个元素被分解并传给 //Comlex<double>(double,double)这个带有两个参数的构造函数。因为comlex<double>并无 //任何接受initializer_list的构造函数。 complex<double> c{4.0, 3.0}; //等价于c(4.0, 3.0) return 0; }
2. initializer_list<T>模板
//initializer_list<T>源码分析
#include <iostream> template <class T> class initializer_list { public: typedef T value_type; typedef const T& reference; //注意说明该对象永远为const,不能被外部修改! typedef const T& const_reference; typedef size_t size_type; typedef const T* iterator; //永远为const类型 typedef const T* const_iterator; private: iterator _M_array; //用于存放用{}初始化列表中的元素 size_type _M_len; //元素的个数 //编译器可以调用private的构造函数!!! //构造函数,在调用之前,编译会先在外部准备好一个array,同时把array的地址传入模板 //并保存在_M_array中 constexpr initializer_list(const_iterator __a, size_type __l) :_M_array(__a),_M_len(__l){}; //注意构造函数被放到private中! constexpr initializer_list() : _M_array(0), _M_len(0){} // empty list,无参构造函数 //size()函数,用于获取元素的个数 constexpr size_type size() const noexcept {return _M_len;} //获取第一个元素 constexpr const_iterator begin() const noexcept {return _M_array;} //最后一个元素的下一个位置 constexpr const_iterator end() const noexcept { return begin() + _M_len; } };
(1)initializer_list是一个轻量级的容器类型,内部定义了iterator等容器必需的概念,本质上是一个迭代器!
(2)对于std:: initializer_list<T>而言,它可以接收任意长度的初始化列表,但要求元素必须是同种类型(T或可转换为T)。
(3)它有3个成员函数:size()、begin()和end()。
(4)拥有一个无参构造函数,可以被直接实例化,此时将得到一个空的列表。之后可以进行赋值操作,如initializer_list<int> list; list={1,2,3,4,5};
(5)initializer_list<T>在进行复制或赋值时,它内部将保存着列表的地址保存在_M_array中,它进行的是浅拷贝,并不真正复制每个元素,因此效率很高。
【编程实验】打印初始化列表的每个元素
#include <iostream> //打印初始化列表的每个元素 void print(std::initializer_list<int> vals) { //遍历列表中的每个元素 for(auto p = vals.begin(); p!=vals.end(); ++p){ std::cout << *p << " "; } std::cout << std::endl; } //std::initializer_list<T>的浅拷贝。以下的返回值应改为std //以下的返回值应改为std::vector<int>类型,而不是std::initializer_list<int>类型。 std::initializer_list<int> func(void) { int a = 1; int b = 2; return {a, b}; //编译器看到{a, b}时,会做好一个array<int,2>对象(其生命 //期直至func结束),然后再产生一个initializer_list<int> //临时对象,由于initializer_list<int>采用的是浅拷贝,当 //函数返回后array<int,2>会被释放,所以无法获取到列表中的元素! } int main() { print({1,2,3,4,5,6,7,8,9,10}); print(func()); return 0; } /*测试结果: e:\Study\C++11\7>g++ -std=c++11 test1.cpp e:\Study\C++11\7>a.exe 1 2 3 4 5 6 7 8 9 10 */
3. 让自定义的类可以接受任意长度初始化列表
(1)自定义类中重载一个可接受initializer_list<T>类型的构造函数
(2)在该构造函数中,遍历列表元素并赋值给相应的字段。
【编程实验】自定义类的初始化列表
#include <iostream> #include <map> using namespace std; class Foo { public: Foo(int a, int b) { cout << "Foo(int a, int b)" << endl; } Foo(initializer_list<int> list) { cout << "Foo(initializer_list<int> list) : "; for(auto i : list){ cout <<i<< " "; } cout << endl; } }; class FooMap { std::map<int, int> content; using pair_t = std::map<int, int>::value_type; public: FooMap(std::initializer_list<pair_t> list) { for(auto it = list.begin(); it!=list.end(); ++it){ content.insert(*it); std::cout << "{" << (*it).first <<"," <<(*it).second <<"}" << " "; } std::cout << std::endl; } }; int main() { Foo f1(77, 5); //Foo(int a, int b), a = 77, b = 5; //注意:由于定义了Foo(initializer_list<int> list)函数,以下3种方 //式的初始化都会将{...}作为一个整体传递给该函数。如果没有定义该函 //数,则由于该类是个非聚合类用{}初始化时,会调用构造函数来初始化。 //但由于Foo类不存在3个参数的构造函数,所以f3那行会编译失败! Foo f2{77, 5}; //Foo(initializer_list<int> list) Foo f3{77, 5, 42}; //Foo(initializer_list<int> list) Foo f4 = {77, 5}; //Foo(initializer_list<int> list) FooMap fm = {{1,2}, {3,4},{5,6}}; return 0; } /*测试结果: e:\Study\C++11\7>g++ -std=c++11 test2.cpp e:\Study\C++11\7>a.exe Foo(int a, int b) Foo(initializer_list<int> list) : 77 5 Foo(initializer_list<int> list) : 77 5 42 Foo(initializer_list<int> list) : 77 5 {1,2} {3,4} {5,6} */