第5课 统一初始化

一、统一初始化(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 vec = {1,2,3,4}; //不支持=()初始化。

   3、对不支持拷贝操作的对象赋值。如std::unique_ptr p{};//不支持等号。

【编程实验】统一初始化聚合类型与非聚合类型的区别

#include 
#include 
#include 
#include 

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(_Last - _First);
    }

private:
    const _Elem* _First;
    const _Elem* _Last;
};
initializer_list源码(VC++2019)

   1. 它是一个轻量级的容器类型,内部定义了iterator等容器必需的概念。其中有3个成员接口:size()、begin()和end()。遍历时取得的迭代器是只读的,无法修改其中的某一个元素的值

   2. 对于std::initializer_list而言,它可以接收任意长度的初始化列表,但要求元素必须是同种类型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
#include 
#include 

using namespace std;
//辅助函数,用于打印initializer_list信息
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 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 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与重载构造函数的关系

(一)当构造函数形参中不带initializer_list时,小括号和大括号的意义没有区别。

(二)如果构造函数中带有initializer_list形参,采用大括号初始化语法会强烈优先匹配带有initializer_list形参的重载版本,而其他更精确匹配的版本可能没有机会被匹配

(三)空大括号构造一个对象时,表示“没有参数”(而不是空的initializer_list对象),因此,会匹配默认的无参构造函数,而不是匹配initializer_list形参的版本的构造函数。

(四)vector vec(10, 2) 和 vector vec{10,2}, 前者是含有10个元素值为2的对象, 而后者是只包含10和2两个元素的对象。

(五)拷贝构造函数和移动构造函数也可能被带有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_list 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)
    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对象。

你可能感兴趣的:(第5课 统一初始化)