• 第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<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;
    };
    initializer_list源码(VC++2019)

       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>对象。

  • 相关阅读:
    vsftp 虚拟用户测试
    RHEL7 MariaDB测试
    安装xenapp后,非管理员连接RDP出现桌面当前不可用的解决方法
    sqrt函数的实现
    O2O、C2C、B2B、B2C
    libsvm使用说明
    如何确定最适合数据集的机器学习算法
    知乎日报:她把全世界的学术期刊都黑了
    逻辑回归应用之Kaggle泰坦尼克之灾
    非均衡数据分布的分类问题
  • 原文地址:https://www.cnblogs.com/5iedu/p/11239244.html
Copyright © 2020-2023  润新知