一、统一初始化(Uniform Initialization)
(一)C++四种初始化方式
1. 小括号:int x(0); //C++98
2. 等号:int x = 0; //C++98
3. 大括号:int x{0}; //C++98成功,C++11成功
4. 等号和大括号:int x = {0}; //C++98失败,C++11成功
(二)统一初始化(也叫大括号初始化)
1、聚合类型定义与大括号初始化
(1)聚合类型的定义
①类型是一个普通类型的数组(如int[10]、char[]、long[2][3])
②类型是一个类(class、struct或union),且:
A.无基类、无虚函数以及无用户自定义的构造函数。
B.无private或protected的非静态数据成员。
C.不能有{}和=直接初始化的非静态数据成员“就地”初始化。(C++14开始己允许这种行为)。
(2)初始化方式:将{t1,t2…tn}内的元素逐一分解并赋值给被初始化的对象,相当于为该对象每个元素/字段分别赋值。(注意不会调用构造函数)
2、非聚合类型的大括号初始化:调用相应的构造函数。
3、注意事项
(1)聚合类型的定义是非递归的。简单来说,当一个类的普通成员是非聚合类型时,这个类也有可能是聚合类型,也就是说可以直接用列表初始化。
(2)对于一个聚合类型,可以直接使用{}进行初始化,这时相当于对其中每个元素分别赋值;而对于非聚合类型,则需要先自定义一个合适的构造函数才能使用{}进行初始化,此时使用初始化列表将调用它对应的构造函数。
(三)大括号初始化的使用场景
1、为类非静态成员指定默认值。//成员变量不支持使用小括号初始化
2、为数组或容器赋值,如vector<int> vec = {1,2,3,4}; //不支持=()初始化。
3、对不支持拷贝操作的对象赋值。如std::unique_ptr<int> p{};//不支持等号。
【编程实验】统一初始化聚合类型与非聚合类型的区别
#include <iostream> #include <vector> #include <map> #include <atomic> using namespace std; //聚合类型(用{}初始化,相当于分别为各成员直接赋值,不会调用构造函数) struct ST { int x; double y = 0.0; //C++11失败,C++14通过 } st = { 1, 2 }; struct Foo { int x; struct ST { int i; int j; } st; public: int k = 0; private: Foo() = default; //C++14允许用default声明为默认构造函数,仍为POD类型。 Foo(const Foo& foo) = default; }; //非聚合类型(用{}初始化时,会调用相应的构造函数) class Base{}; class Bar : Base //1. 有基类,为非聚合类型 { double x; //2. 有private普通成员,为非聚合类型 static int k; //允许静态成员,但必须在类外定义用int Bar::k =0的方式初始化 public: int z; int y{ 0 }; //3. 通过=或{}来就地“就地”初始化,为非聚合类型 public: //4. 类中自定义构造函数,为非聚合类型 Bar(double x, int y, int z): x(x), y(y), z(z) { cout << "Bar(double x, int y, int z)" << endl; } Bar(const Bar& bar) //自定义拷贝构造函数,为非聚合类型 { x = bar.x; y = bar.y; z = bar.z; } virtual void func() {}; //5. 存在虚函数,为非聚合类型 }; int Bar::k = 0; //x,y究竟为0,0还是123,321? //由于非聚合类型,是调用构造函数初始化的。即会将实参123、321传入Test(int,int) //中,但该函数未使用这个实参,而是直接用0来初始化x和y。 struct Test { int x; int y; Test(int, int):x(0),y(0){} } t = { 123, 321 }; //t.x = ?, t.y=? int main() { //1.四种初始化方式对比 int a = 0; //等号=初始化 C++98 int b(2 + 3); //小括号直接初始化 C++98 int c = { 0 }; //大括号等号初始化 C++98、C++11 int d{ 0 }; //大括号直接初始化 C++11 int i; //未初始化 int j{}; //j被初始化为0 int* p; //未初始化 int* q{}; //j被初始化为nullptr int x[] = { 1, 3, 5 }; // C++98通过,C++11通过 float y[4][3] = { {1, 3, 5},{2, 4, 6},{3, 5, 7},{4, 6, 8} }; // C++98通过,C++11通过 int z[]{ 1, 3, 5 }; // C++98失败,C++11通过 vector<int> v{ 1, 3, 5 }; // C++98失败,C++11通过 map<int, double> m = { {1, 1.0f}, {2, 2.0f}, {3, 3.0f} };// C++98失败,C++11通过 //2.聚合类型和非聚合类型的初始化的对比 Foo foo = { 1,{2, 3} }; //POD类型,相当于从大括号中的值逐个赋值给foo对应的成员,不会调用构造函数。 Foo foo2{ 4, 5, 6 }; cout << "st.x = " << st.x << ", st.y = " << st.y << endl; //st.x=1, st.y=2; Bar bar = { 1, 2, 3 }; //非聚合类型,调用构造函数初始化: Bar(double,int,int) Bar bar2{ 1,2,3 }; //非聚合类型,调用Bar(double,int,int)构造函数 //x,y究竟为0,0还是123,321? cout << "t.x = " << t.x << ", t.y = " << t.y << endl; //t.x = 0, t.y = 0,当统一初始化遇到构造函数时,优先调 //用构造函数初始化 return 0; } /*输出结果 st.x = 1, st.y = 2 Bar(double x, int y, int z) Bar(double x, int y, int z) t.x = 0, t.y = 0 */
二、初始化列表
(一)initializer_list的实现细节
template <class _Elem> class initializer_list { // list of pointers to elements public: using value_type = _Elem; using reference = const _Elem&; using const_reference = const _Elem&; using size_type = size_t; using iterator = const _Elem*; using const_iterator = const _Elem*; constexpr initializer_list() noexcept : _First(nullptr), _Last(nullptr) { // empty list } constexpr initializer_list(const _Elem* _First_arg, const _Elem* _Last_arg) noexcept : _First(_First_arg), _Last(_Last_arg) { // construct with pointers } _NODISCARD constexpr const _Elem* begin() const noexcept { // get beginning of list return _First; } _NODISCARD constexpr const _Elem* end() const noexcept { // get end of list return _Last; } _NODISCARD constexpr size_t size() const noexcept { // get length of list return static_cast<size_t>(_Last - _First); } private: const _Elem* _First; const _Elem* _Last; };
1. 它是一个轻量级的容器类型,内部定义了iterator等容器必需的概念。其中有3个成员接口:size()、begin()和end()。遍历时取得的迭代器是只读的,无法修改其中的某一个元素的值。
2. 对于std::initializer_list<T>而言,它可以接收任意长度的初始化列表,但要求元素必须是同种类型T(或可转换为T)。
3、它只能被整体初始化或赋值。由于拥有一个无参构造函数,可以利用它来直接定义一个空的initializer_list对象,之后利用初始化列表对其赋值。
4、实际上,Initializer_list内部并不负责保存初始化列表中的元素拷贝,他们仅仅是列表中元素的引用而己。因此,通过过拷贝构造的initializer_list会与原initializer_list共享列表中的元素空间。
(二)防止类型收窄(以下为类型收窄的几种情况)
1. 从浮点数隐式转换为一个整型数,如int i=2.2。
2. 从高精度浮点数隐式转换为低精度浮点数,如从long double隐式转换为double或float。
3.从一个整型数隐式转换为一个浮点数,并且超出了浮点数的表示范围,如x=(unsigned long long)-1。
4. 从一个整型数隐式转换为一个长度较短的整型数,并且超出了长度较短的整型数表示范围,如char x = 65536;
【编程实验】initializer_list分析及类型收窄
#include<iostream> #include <vector> #include <map> using namespace std; //辅助函数,用于打印initializer_list<int>信息 void printlist(std::initializer_list<int>& list) { cout << "list = "; for (const auto& elem : list) { cout << elem << " "; } cout << " size = " << list.size() << endl; } //自定义的类拥有接受任意长度的初始化列表 class FooVec { vector<int> content; public: FooVec(std::initializer_list<int> list) { for (const auto& elem : list) { content.push_back(elem); } } void print() { for(const auto& val : content){ cout << val << " "; } cout << endl; } }; class FooMap { using pair_t = std::map<int, int>::value_type; std::map<int, int> content; public: FooMap(std::initializer_list<pair_t> list) { for (const auto& elem : list) { content.insert(elem); } } void print() { for (const auto& val : content) { cout << "key = " << val.first << ", value = "<< val.second << endl; } } }; //initializer_list保存的是元素的引用! std::initializer_list<int> func(void) // { int a = 1, b = 2; return { a, b }; //由于initializer_list保存的是对象的引用,但a与b是局部变量在 //func返回后会被释放,initializer_list内部会存在空悬指针!危险! //正确的做法可以将返回值改为保存副本的容器,如vector<int> } int main() { //1. 自定义类接受初始化列表 FooVec fv = { 1, 2, 3, 4, 5, 6 }; fv.print(); FooMap fm = { {1,2},{3,4},{5,6} }; fm.print(); //2. initializer_list的构造和拷贝 std::initializer_list<int> ls; //调用无参构造函数,创建空列表 printlist(ls); ls = { 1, 2, 3, 4, 5, 6 }; printlist(ls); ls = { 7, 8, 9 }; printlist(ls); //3. initializer_list共享元素存储空间 //注意下面s1、 s2、s3和s4均共享元素空间(VS2019下可以在IDE中查看到这四个对象 //的_First、_Last成员的地址都是一样的) initializer_list<string> s1 = { "aa", "bb", "cc", "dd" }; initializer_list<string> s2 = s1; initializer_list<string> s3(s1); initializer_list<string> s4; s4 = s1; //4.initializer_list保存的是元素的引用 std::initializer_list<int> ret = func(); //func中的a、b局部变量被释放! printlist(ret); //5. 防止类型收窄 int a = 1.1; //ok //int b = { 1.1 }; //error, double到int:{}防止类型收窄 float fa = 1e40; //ok,浮点常量溢出,但编译器允许隐式转换 //float fb = { 1e40 }; //error,{}防止类型收窄 float fc = (unsigned long long) - 1; //ok, 从“unsigned __int64”到“float”发生截断 //float fd = { (unsigned long long) - 1 }; //error,{}防止类型收窄 float fe = (unsigned long long) 1; //ok float ff = { (unsigned long long) 1 }; //ok const int x = 2014, y = 1; //x、y: const int类型 char c = x; //ok, 截断常量值 //char d = { x }; //error, {}防止了“const int”转换到“char”的收缩转换 char e = y; //ok char f = { y }; //ok!!! y为const int编译期被放于符号表中,此处y被用1替换掉了。如果去掉 //const会出现类型收窄而导致编译失败! return 0; } /*输出结果 1 2 3 4 5 6 key = 1, value = 2 key = 3, value = 4 key = 5, value = 6 list = size = 0 list = 1 2 3 4 5 6 size = 6 list = 7 8 9 size = 3 list = -858993460 -858993460 size = 2 */
三、initializer_list<T>与重载构造函数的关系
(一)当构造函数形参中不带initializer_list时,小括号和大括号的意义没有区别。
(二)如果构造函数中带有initializer_list形参,采用大括号初始化语法会强烈优先匹配带有initializer_list形参的重载版本,而其他更精确匹配的版本可能没有机会被匹配。
(三)空大括号构造一个对象时,表示“没有参数”(而不是空的initializer_list对象),因此,会匹配默认的无参构造函数,而不是匹配initializer_list形参的版本的构造函数。
(四)vector<int> vec(10, 2) 和 vector<int> vec{10,2}, 前者是含有10个元素值为2的对象, 而后者是只包含10和2两个元素的对象。
(五)拷贝构造函数和移动构造函数也可能被带有initializer_list形参的构造函数劫持。
【编程实验】初始化列表与函数重载的关系
#include <iostream> #include <vector> #include <atomic> using namespace std; //初始化列表的使用场景 class Foo { private: int x{ 0 }; //ok, x的默认值为0 int y = 0; //ok //int z(0); //error,成员变量不能用小括号初初始化 public: Foo() : x(0), y(0) {} Foo(int x, int y) :x(x), y(y) {} }; //重载函数与initializer_list class Widget { public: Widget() //无参构造函数 { cout << "Widget()" << endl; } Widget(int i, bool b) { cout <<"Widget(int, bool)" << endl; } Widget(int i, double d) { cout << "Widget(int, double)" << endl; } Widget(std::initializer_list<long double> il) //具有initializer_list形参 { cout << "Widget(std::initializer_list<long double>)" << endl; } Widget(const Widget& widget) //拷贝构造函数 { cout << "Widget(const Widget&)" << endl; } Widget(Widget&& widget) noexcept //移动构造函数 { cout << "Widget(Widget&&)" << endl; } public: operator float() //强制转换成float类型 { cout << "operator float() const" << endl; return 0; } }; // class Bar { public: Bar(int i, bool b) { cout << "Bar(int i, bool b)" << endl; } Bar(int i, double b) { cout << "Bar(int i, double b)" << endl; } Bar(std::initializer_list<bool> il) { cout << "Bar(std::initializer_list<bool> il)" << endl; } }; int main() { //1.初始化列表的使用场景 //1.1 为类非静态成员指定默认值(见Widget类) //1.2 不可复制的对象的初始化(如atomic) std::atomic<int> ai1{ 0 }; //ok,大括号初始化 std::atomic<int> ai2(0); //ok std::atomic<int> ai3 = 0; //warning, 由于调用拷贝构造,gcc编译器无法通过。vc2019可以! //1.3 避免“最令人苦恼的解析语法”(most vexing parse) Foo foo1(); //注意此处声明一个函数!即声明一个名为foo1无参的函数,返回值为Foo。而不是调用 //Foo无参构造函数定义w1对象! Foo foo2{}; //使用{}初始化,调用无参的Foo构造函数。most vexing parse消失!!! //2. initializer_list与重载构造函数的关系(大括号和小括号初始化的区别) //2.1 {}与拷贝构造函数的决议 Widget w1(10, true); //Widget(int, bool) Widget w2{ 10, true }; //Widget(std::initializer_list<long double>) Widget w3(10, 5.0); //Widget(int, double) Widget w4{ 10, 5.0 }; //Widget(std::initializer_list<long double>) Widget w5(w4); //调用拷贝构造函数: Widget(const Widget&) Widget w6{ w4 }; //vc:调用拷贝构造函数,Widget(const Widget&) //g++:为了尽可能调用带initializer_list形参的构造函数,会先调用operator float()将 //w4转为float,然后再匹配Widget(std::initializer_list<long double>)函数。 //2.2 {}与移动构造的决议 Widget w7(std::move(w4)); //VC:调用移动构造函数:Widget(Widget&&) Widget w8{ std::move(w4) }; //VC:调用移动构造函数:Widget(Widget&&) //g++:匹配Widget(std::initializer_list<long double>),原因同w6 //2.3 {}会强烈地优先匹配带initializer_list形参的构造函数,哪怕存在精确匹配的函数也会被无视! //Bar bar{ 10, 5.0 }; //编译失败,哪怕存在精确匹配的 Bar(int i, double b)构造函数。 //失败的原因:通过{}构造对象时,会先查找带initializer_list形参的构造函数,而该形参为 //initializer_list<bool>型,所以就会试图将int(10)与double(0.5)强制转为bool类型的, //此时会发生类型窄化现象,但这在大括号初始化中是被禁止的,所以编译失败 //2.4 空大括号:表示“无参数” Widget w10; //调用无参构造函数: Widget(); Widget w11{}; //调用无参构造函数: Widget();(注意,空大括号表示“无参数”,而不是空的initializer_list对象) Widget w12(); //函数声明 //将{}放入一对大、小括号内,表示传递空的initializer_list对象给构造函数 Widget w13({}); //调用Widget(std::initializer_list<long double>) Widget w14{ {} }; //调用Widget(std::initializer_list<long double>) //3. vector中使用()和{}需要注意的问题 vector<int> v1(10, 20); //调用非initializer_list形参的构造函数。结果是:创建10个int型的元素。 vector<int> v2{ 10, 20 }; //调用带initializer_list形参的构造函数。结果是:创建两个分别为10和20的int型元素。 } /*输出结果(VC++2019) Widget(int, bool) Widget(std::initializer_list<long double>) Widget(int, double) Widget(std::initializer_list<long double>) Widget(const Widget&) Widget(const Widget&) Widget(Widget&&) Widget(Widget&&) Widget() Widget() Widget(std::initializer_list<long double>) Widget(std::initializer_list<long double>) */
四、小结
(一)大括号初始化应用的语境最为宽泛,可以阻止隐式窄化类型转换,还对“最令人苦恼之解析语法(most vexing parse)”免疫。
(二)在构造函数重载匹配时,只要有任何可能,大括号初始化就会与带有std::initializer_list类型的形参相匹配,即使其他重载版本有着更精确的匹配形参表。
(三)使用小括号和大括号,会造成结果截然不同的例子是:使用两个实参来创建vector<T>对象。