一、统一初始化(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
3、对不支持拷贝操作的对象赋值。如std::unique_ptr
【编程实验】统一初始化聚合类型与非聚合类型的区别
#include#include #include
二、初始化列表
(一)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(_Last - _First); } private: const _Elem* _First; const _Elem* _Last; };
1. 它是一个轻量级的容器类型,内部定义了iterator等容器必需的概念。其中有3个成员接口:size()、begin()和end()。遍历时取得的迭代器是只读的,无法修改其中的某一个元素的值。
2. 对于std::initializer_list
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#include #include
三、initializer_list
(一)当构造函数形参中不带initializer_list时,小括号和大括号的意义没有区别。
(二)如果构造函数中带有initializer_list形参,采用大括号初始化语法会强烈优先匹配带有initializer_list形参的重载版本,而其他更精确匹配的版本可能没有机会被匹配。
(三)空大括号构造一个对象时,表示“没有参数”(而不是空的initializer_list对象),因此,会匹配默认的无参构造函数,而不是匹配initializer_list形参的版本的构造函数。
(四)vector
(五)拷贝构造函数和移动构造函数也可能被带有initializer_list形参的构造函数劫持。
【编程实验】初始化列表与函数重载的关系
#include#include #include 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 ) " << 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_listil) " << 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) Widget w3(10, 5.0); //Widget(int, double) Widget w4{ 10, 5.0 }; //Widget(std::initializer_list) Widget w5(w4); //调用拷贝构造函数: Widget(const Widget&) Widget w6{ w4 }; //vc:调用拷贝构造函数,Widget(const Widget&) //g++:为了尽可能调用带initializer_list形参的构造函数,会先调用operator float()将 //w4转为float,然后再匹配Widget(std::initializer_list)函数。 //2.2 {}与移动构造的决议 Widget w7(std::move(w4)); //VC:调用移动构造函数:Widget(Widget&&) Widget w8{ std::move(w4) }; //VC:调用移动构造函数:Widget(Widget&&) //g++:匹配Widget(std::initializer_list),原因同w6 //2.3 {}会强烈地优先匹配带initializer_list形参的构造函数,哪怕存在精确匹配的函数也会被无视! //Bar bar{ 10, 5.0 }; //编译失败,哪怕存在精确匹配的 Bar(int i, double b)构造函数。 //失败的原因:通过{}构造对象时,会先查找带initializer_list形参的构造函数,而该形参为 //initializer_list型,所以就会试图将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) Widget w14{ {} }; //调用Widget(std::initializer_list) //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) Widget(int, double) Widget(std::initializer_list */) Widget(const Widget&) Widget(const Widget&) Widget(Widget&&) Widget(Widget&&) Widget() Widget() Widget(std::initializer_list ) Widget(std::initializer_list )
四、小结
(一)大括号初始化应用的语境最为宽泛,可以阻止隐式窄化类型转换,还对“最令人苦恼之解析语法(most vexing parse)”免疫。
(二)在构造函数重载匹配时,只要有任何可能,大括号初始化就会与带有std::initializer_list类型的形参相匹配,即使其他重载版本有着更精确的匹配形参表。
(三)使用小括号和大括号,会造成结果截然不同的例子是:使用两个实参来创建vector