C++11 统一的对象初始化方法

C++98/03 对象的初始化方式

原生类型和数组

在 C++ 中对象的初始化方法有很多种,针对原生类型、数组、原生字符串、struct、class 等,有不同的方式进行对象初始化。
对于 int,char,int[] 等原生类型,如下面的代码所示:

    int x = 1;
    int y(1);

    char ch1 = 'A';
    char ch2('B');

    int count_array[] = {1, 2, 3};
    char str1[] = {'H', 'E', 'W'};
    char str2[] = "HEW";
    char str3[] = { "HEW" };

原生类型可以使用 = 或者 () 进行初始化,而对于原生类型的数组,使用的是 {1, 2, 3} 初始化列表进行初始化。对于原生字符串,由于其特殊性,额外多支持了下面的方式:

    char str2[] = "HEW";
    char str3[] = { "HEW" };

struct 和 class 类型

对于 POD 类型 的 struct 对象,这些 struct 没有构造函数,可以使用
= {11, 12};
这样的初始化列表进行初始化,初始化列表可以嵌套使用,
= {10, 20, { 300, 400} };,等同于 = {10, 20, 300, 400};

    struct SSize {
        int width;
        int height;
    };

    SSize size1 = {11};
    SSize size2 = {11, 12};


    struct SRect {
        int x;
        int y;
        SSize size;
    };

    SRect rect1 = {10, 20, 300, 400};
    SRect rect2 = {10, 20, { 300, 400} };

上面这种初始化列表本质上是按照内存布局进行类似于 memset 的初始化事情,这也是为什么要求必须是 POD 类型 的原因。
一旦 struct 中有构造函数,那么在 C++98/03 标准中便不能再使用= { } 初始化列表进行初始化,这时 struct 就和 class 一样,必须使用构造函数进行初始化。

    // struct with constructor function
    struct SBook {
        int bookId;
        int bookCount;

        SBook() : bookId(0), bookCount(0) {

        }

        SBook(int _bookId, int _bookCount) : bookId(_bookId), bookCount(_bookCount)
        {

        }
    };

    SBook book1 = {1, 2}; // ❌ Compile Error in C++98/03 
    SBook book2;
    SBook book3(1001, 10);
    SBook * book4 = new SBook(10002, 20);


    // class
    class CAnimal
    {
        int legs;

    public:
        CAnimal(int _legs) : legs(_legs)
        {

        }
    };

    CAnimal animal1(2);
    CAnimal * animal2 = new CAnimal(1);

总结一下:在 C++98/03 标准中,对于 struct,如果是 POD 类型, 可以使用 ={...} 初始化列表进行初始化,否则只能使用构造函数、拷贝构造函数、operator= 等进行初始化。而对于 class ,不管是不是 POD 类型,都不能使用 ={...}

C++11 统一的初始化

可以看到,在 C++98/03 标准中这些不同的初始化方法,都有各自的使用范围。最关键的是,没有一种方式可以通用于所有情况。
C++11 为了统一初始化方式,把 { ... } 这种初始化列表的适用范围大大增加了,使它可以用于任何类型对象的初始化。这样, { ... } 就是统一的初始化方式了。如下代码所示:

    int x = {1};
    int count_array1[] = {1, 2, 3};
    int *count_array2 = new int[3] {1, 2 , 3};

    char str1[] = {'H', 'E', 'W'};


    struct SSize {
        int width;
        int height;
    };
    SSize size2 = {11, 12};

    // class
    class CAnimal
    {
        int legs;

    public:
        CAnimal(int _legs) : legs(_legs)
        {

        }
    };

    CAnimal animal2 = {3};

可以看到,原生类型、数组、自定义 struct 和 class 等,任何类型都可以使用 { } 进行初始化。

关于 { } 初始化方式的使用场景

{ } 这种统一的初始化方式,乍一看,语法很直观,感觉挺好的。可是,细细想来,在有些场景下的工作机制还是有些模糊。

当 struct 中包含构造函数时:

比如下面这个场景:

    struct SSizeA {
        int width;
        int height;
    };
    SSizeA sizea_1 = {11, 12};
    SSizeA sizea_2 = {11};


    struct SSizeB {
        int width;
        int height;

        SSizeB(int w, int h) : width(w), height(h)
        {
        }
    };
    SSizeB sizeb_1 = {11, 12};
    SSizeB sizeb_2 = {11};//❌Compile Error, No Matching Constructor

SSizeA 沿用的是 POD 类型的 { } 工作原理,没有什么疑问。而对于 SSizeB 由于在 struct 内部定义了构造函数,所以适用于 C++11 标准的 { } 的工作原理,寻找适用的构造函数进行初始化。所以,SSizeB sizeb_2 = {11}; 由于没有能接受一个参数的构造函数而出现编译错误。

当包含参数为 std::initializer_list 的构造函数时:

std::initializer_list 是 C++11 新增的类模板。 对于 auto temp = {1, 2, 3};temp 的类型就是 std::initializer_list<int> 。在 class 使用 { } 初始化时,会优先尝试匹配std::initializer_list 作为参数的构造函数,匹配失败后再尝试其他形式的构造函数。如下:

    struct SSizeB {
        int width;
        int height;

    public:
        SSizeB(int w, int h) : width(w), height(h)
        {

        }

        SSizeB(std::initializer_list<int> list)
        {
            // ...
        }
    };
    
    // call constructor: SSizeB(std::initializer_list<int> list)
    SSizeB sizeb_1 = {11, 199992}; 

防止类型收窄

使用 { } 初始化时是不允许类型收窄的,比如:

    struct SSizeB {
        int width;
        char height;

    public:
        SSizeB(int w, char h) : width(w), height(h)
        {

        }
    };
    SSizeB sizeb_1(11, 199992);// Warnning
    SSizeB sizeb_2 = {11, 199992};// Error
    SSizeB sizeb_3 = {11, 19};// OK

sizeb_1使用普通的构造函数初始化,虽然 199992 无法正确转换为 char ,但是编译器只会给出警告,不会报编译错。这是沿用了 C++98 的标准。
使用 { } 初始化的 sizeb_2 会直接编译错误,因为 199992 超出了 char 的范围。
对于 {11, 19} 来说,虽然也需要从 int 转为 char,但是 19char 的范围之内,所以不会编译报错。