转载来自:链接: https://subingwen.cn/cpp/list-init/

关于 C++ 中的变量,数组,对象等都有不同的初始化方法,在这些繁琐的初始化方法中没有任何一种方式适用于所有的情况。为了统一初始化方式,并且让初始化行为具有确定的效果,在 C++11 中提出了列表初始化的概念。

1. 统一的初始化
在 C++98/03 中,对应普通数组和可以直接进行内存拷贝(memcpy ())的对象是可以使用列表初始化来初始化数据的

// 数组的初始化
int array[] = { 1,3,5,7,9 };
double array1[3] = { 1.2, 1.3, 1.4 };

// 对象的初始化
struct Person
{
    int id;
    double salary;
}zhang3{1, 3000 };

在 C++11 中,列表初始化变得更加灵活了,来看一下下面这段初始化类对象的代码:

#include <iostream>
using namespace std;

class Test
{
public:
    Test(int) {}
private:
    Test(const Test&);
};

int main(void)
{
    Test t1(520);
    Test t2 = 520;
    Test t3 = { 520 };
    Test t4{ 520 };
    int a1 = { 1314 };
    int a2{ 1314 };
    int arr1[] = { 1, 2, 3 };
    int arr2[]{ 1, 2, 3 };
    return 0;
}

具体地来解读一下上面代码中使用的各种初始化方式:

t1:最中规中矩的初始化方式,通过提供的带参构造进行对象的初始化

t2:语法错误,因为提供的拷贝构造函数是私有的。

如果拷贝构造函数是公共的,520 会通过隐式类型转换被 Test(int) 构造成一个匿名对象然后再通过对这个匿名对象进行拷贝构造得到 t2(这个错误在 VS 中不会出现,在 Linux 中使用 g++ 编译会提示描述的这个错误,截图如下。)

 C++11——列表初始化-冯金伟博客园

t3 和 t4:使用了 C++11 的初始化方式来初始化对象,效果和 t1 的方式是相同的。

在初始时,{} 前面的等号是否书写对初始化行为没有任何影响。
t3 虽然使用了等号,但是它仍然是列表初始化,因此私有的拷贝构造对它没有任何影响。
t1、arr1 和 t2、arr2:这两个是基础数据类型的列表初始化方式,可以看到,和对象的初始化方式是统一的。

t4、a2、arr2 的写法,是 C++11 中新添加的语法格式,使用这种方式可以直接在变量名后边跟上初始化列表,来进行变量或者对象的初始化。

既然使用列表初始化可以对普通类型以及对象进行直接初始化那么在使用 new 操作符创建新对象的时候可以使用列表初始化进行对象的初始化吗?答案是肯定的,来看下面的例子:

int * p = new int{520};
double b = double{52.134};
int * array = new int[3]{1,2,3};

指针p 指向了一个 new 操作符返回的内存,通过列表初始化将内存数据初始化为了 520
变量b 是对匿名对象使用列表初始之后,再进行拷贝初始化。
数组array 在堆上动态分配了一块内存,通过列表初始化的方式直接完成了多个元素的初始化
除此之外,列表初始化还可以直接用在函数返回值上:

#include <iostream>
#include <string>
using namespace std;

class Person
{
public:
    Person(int id, string name)
    {
        cout << "id: " << id << ", name: " << name << endl;
    }
};

Person func()
{
    return { 9527, "华安" };
}

int main(void)
{
    Person p = func();
    return 0;
}

代码中的 return { 9527, “华安” }; 就相当于 return (9527, “华安” );,直接返回了一个匿名对象。通过上面的几个例子可以看出在 C++11 使用列表初始化是非常便利的,它统一了各种对象的初始化方式,而且还让代码的书写更加简单清晰。

2. 列表初始化细节
2.1 聚合体
在 C++11 中,列表初始化的使用范围被大大增强了,但是一些模糊的概念也随之而来,在前面的例子可以得知,列表初始化可以用于自定义类型的初始化,但是对于一个自定义类型,列表初始化可能有两种执行结果:

#include <iostream>
#include <string>
using namespace std;

struct T1
{
    int x;
    int y;
}a = { 123, 321 };

struct T2
{
    int x;
    int y;
    T2(int, int) : x(10), y(20) {}
}b = { 123, 321 };

int main(void)
{
    cout << "a.x: " << a.x << ", a.y: " << a.y << endl;
    cout << "b.x: " << b.x << ", b.y: " << b.y << endl;
    return 0;
}

程序执行的结果是这样的:

a.x: 123, a.y: 321
b.x: 10, b.y: 20

在上边的程序中都是用列表初始化的方式对对象进行了初始化,但是得到结果却不同,对象 b 并没有被初始化列表中的数据初始化,这是为什么呢?

对象 a 是对一个自定义的聚合类型进行初始化,它将以拷贝的形式使用初始化列表中的数据来初始化 T1 结构体中的成员
在结构体 T2 中自定义了一个构造函数,因此实际的初始化是通过这个构造函数完成的
现在很多小伙伴可能就一头雾水了,同样是自定义结构体并且在创建对象的时候都使用了列表初始化来初始化对象,为什么在类内部对对象的初始化方式却不一样呢?因为如果使用列表初始化对对象初始化时,还需要判断这个对象对应的类型是不是一个聚合体,如果是初始化列表中的数据就会拷贝到对象中。

那么,使用列表初始化时,对于什么样的类型 C++ 会认为它是一个聚合体呢?

普通数组本身可以看做是一个聚合类型

int x[] = { 1,2,3,4,5,6 };
double y[3][3] = {
{1.23, 2.34, 3.45},
{4.56, 5.67, 6.78},
{7.89, 8.91, 9.99},
};
char carry[] = {'a', 'b', 'c', 'd', 'e', 'f'};
std::string sarry[] = {"hello", "world", "nihao", "shijie"};

满足以下条件的类(class、struct、union)可以被看做是一个聚合类型

无用户自定义的构造函数。

无私有或保护的非静态数据成员。

场景 1: 类中有私有成员,无法使用列表初始化进行初始化

struct T1
{
    int x;
    long y;
protected:
    int z;
}t{ 1, 100, 2 }; // error, 类中有私有成员, 无法使用初始化列表初始化

场景 2:类中有非静态成员可以通过列表初始化进行初始化,但它不能初始化静态成员变量。

struct T2
{
    int x;
    long y;
protected:
    static int z;
}t{ 1, 1002 }; // error

结构体中的静态变量 z 不能使用列表初始化进行初始化,它的初始化遵循静态成员的初始化方式

struct T2
{
    int x;
    long y;
protected:
    static int z;
}t{ 1, 100 }; // ok
// 静态成员的初始化
int T2::z = 2;

无基类。

无虚函数。

类中不能有使用 {} 和 = 直接初始化的非静态数据成员(从 c++14 开始就支持了)。

#include <iostream>
#include <string>
using namespace std;

struct T2
{
    int x;
    long y;
protected:
    static int z;
}t1{ 1, 100 }; // ok
// 静态成员的初始化
int T2::z = 2;

struct T3
{
    int x;
    double y = 1.34;
    int z[3]{ 1,2,3 };
};

int main(void)
{
    T3 t{ 520, 13.14, {6,7,8} }; // error, c++11不支持,从c++14开始就支持了
    return 0;
}

从C++14开始,使用列表初始化也可以初始化在类中使用{}和=初始化过的非静态数据成员。

2.2 非聚合体
对于聚合类型的类可以直接使用列表初始化进行对象的初始化,如果不满足聚合条件还想使用列表初始化其实也是可以的,需要在类的内部自定义一个构造函数, 在构造函数中使用初始化列表对类成员变量进行初始化:

#include <iostream>
#include <string>
using namespace std;

struct T1
{
    int x;
    double y;
    // 在构造函数中使用初始化列表初始化类成员
    T1(int a, double b, int c) : x(a), y(b), z(c) {}
    virtual void print()
    {
        cout << "x: " << x << ", y: " << y << ", z: " << z << endl;
    }
private:
    int z;
};

int main(void)
{
    T1 t{ 520, 13.14, 1314 }; // ok, 基于构造函数使用初始化列表初始化类成员
    t.print();
    return 0;
}

另外,需要额外注意的是聚合类型的定义并非递归的,也就是说当一个类的非静态成员是非聚合类型时,这个类也可能是聚合类型,比如下面的这个例子:

#include <iostream>
#include <string>
using namespace std;

struct T1
{
    int x;
    double y;
private:
    int z;
};

struct T2
{
    T1 t1;
    long x1;
    double y1;
};

int main(void)
{
    T2 t2{ {}, 520, 13.14 };
    return 0;
}

可以看到,T1 并非一个聚合类型,因为它有一个 Private 的非静态成员但是尽管 T2 有一个非聚合类型的非静态成员 t1,T2 依然是一个聚合类型,可以直接使用列表初始化的方式进行初始化

最后强调一下 t2 对象的初始化过程,对于非聚合类型的成员 t1 做初始化的时候,可以直接写一对空的大括号 {},这相当于调用是 T1 的无参构造函数。

对于一个聚合类型,使用列表初始化相当于对其中的每个元素分别赋值,而对于非聚合类型,则需要先自定义一个合适的构造函数此时使用列表初始化将会调用它对应的构造函数。

3. std::initializer_list
在 C++ 的 STL 容器中,可以进行任意长度的数据的初始化使用初始化列表也只能进行固定参数的初始化,如果想要做到和 STL 一样有任意长度初始化的能力,可以使用 std::initializer_list 这个轻量级的类模板来实现。

先来介绍一下这个类模板的一些特点:

它是一个轻量级的容器类型,内部定义了迭代器 iterator 等容器必须的概念,遍历时得到的迭代器是只读的。
对于 std::initializer_list<T> 而言,它可以接收任意长度的初始化列表,但是要求元素必须是同种类型 T
在 std::initializer_list 内部有三个成员接口:size(), begin(), end()。
std::initializer_list 对象只能被整体初始化或者赋值。
3.1 作为普通函数参数
如果想要自定义一个函数并且接收任意个数的参数(变参函数),只需要将函数参数指定为 std::initializer_list,使用初始化列表 { } 作为实参进行数据传递即可。

#include <iostream>
#include <string>
using namespace std;

void traversal(std::initializer_list<int> a)
{
    for (auto it = a.begin(); it != a.end(); ++it)
    {
        cout << *it << " ";
    }
    cout << endl;
}

int main(void)
{
    initializer_list<int> list;
    cout << "current list size: " << list.size() << endl;
    traversal(list);

    list = { 1,2,3,4,5,6,7,8,9,0 };
    cout << "current list size: " << list.size() << endl;
    traversal(list);
    cout << endl;

    list = { 1,3,5,7,9 };
    cout << "current list size: " << list.size() << endl;
    traversal(list);
    cout << endl;

    ////////////////////////////////////////////////////
    ////////////// 直接通过初始化列表传递数据 //////////////
    ////////////////////////////////////////////////////
    traversal({ 2, 4, 6, 8, 0 });
    cout << endl;

    traversal({ 11,12,13,14,15,16 });
    cout << endl;


    return 0;
}

示例代码输出的结果:

current list size: 0

current list size: 10
1 2 3 4 5 6 7 8 9 0

current list size: 5
1 3 5 7 9

2 4 6 8 0

11 12 13 14 15 16

std::initializer_list拥有一个无参构造函数,因此,它可以直接定义实例,此时将得到一个空的std::initializer_list,因为在遍历这种类型的容器的时候得到的是一个只读的迭代器,因此我们不能修改里边的数据,只能通过值覆盖的方式进行容器内部数据的修改。虽然如此,在效率方面也无需担心,std::initializer_list的效率是非常高的,它的内部并不负责保存初始化列表中元素的拷贝,仅仅存储了初始化列表中元素的引用。

3.2 作为构造函数参数
自定义的类如果在构造对象的时候想要接收任意个数的实参,可以给构造函数指定为 std::initializer_list 类型,在自定义类的内部还是使用容器来存储接收的多个实参。

#include <iostream>
#include <string>
#include <vector>
using namespace std;

class Test
{
public:
    Test(std::initializer_list<string> list)
    {
        for (auto it = list.begin(); it != list.end(); ++it)
        {
            cout << *it << " ";
            m_names.push_back(*it);
        }
        cout << endl;
    }
private:
    vector<string> m_names;
};

int main(void)
{
    Test t({ "jack", "lucy", "tom" });
    Test t1({ "hello", "world", "nihao", "shijie" });
    return 0;
}

输出的结果:

jack lucy tom
hello world nihao shijie