电脑基础 · 2023年3月27日

C++之列表初始化详解

1、列表初始化的规则

在C++11中可以直接在变量名后面加上初始化列表来进行对象的初始化。

struct A {
public:
    A(int) {}
};
int main() {
    A a(123);
    A c = { 123 };
    A d{123}; // c++11
    int e = {123};
    int f{123}; // c++11
    return 0;
}

聚合类型可以进行直接列表初始化,那么什么是聚合类呢?

(1)类型是一个普通数组,如int[5],char[],double[]等

(2)类型是一个类,且满足以下条件:

  • 没有用户声明的构造函数
  • 没有用户提供的构造函数(允许显示预置或弃置的构造函数)
  • 没有私有或保护的非静态数据成员
  • 没有基类
  • 没有虚函数
  • 没有{}和=直接初始化的非静态数据成员
  • 没有默认成员初始化器

文字难以琢磨可以看下面例子帮助理解:

// 有自定义的构造函数,不能列表初始化
class A {
public:
    A(int, int){}
    int a;
    int b;
    int c;
};
// 含有虚函数,不是聚合类
class B {
public:
    virtual void func() {}
    int a;
    int b;
};
// 有基类,不是聚合类
class Base {};
class C : public Base {
public:
    int a;
    int b;
};
// 有等号初始化,不是聚合类
class D {
public:
    int a;
    int b = 10;
};
// 含有私有的非静态数据成员,不是聚合类
class E {
public:
    int a;
    int b;
private:
    int c;
};
// 含有默认成员初始化器,不是聚合类
class F {
public:
    F() : a(0), b(0) {}
    int a;
    int b;
};

2、列表初始化的优点

(1)高效,减少调用构造函数的次数

#include<iostream>
using namespace std;
class Data
{
public:
    // 无参构造函数
	Data() {cout<<"This is Data constructor1"<<endl;}
	// 拷贝构造函数
	Data(const Data&) {cout<<"This is Data constructor2"<<endl;}
	// 拷贝赋值构造函数
	Data& operator=(const Data&) {cout<<"This is Data constructor3"<<endl;}
};
// 低效写法
class Test1
{
public:
	Test1(Data data) {m_data = data;}
private:
	Data m_data;
};
// 高效写法
class Test2
{
public:
	Test2(Data data) : m_data(data){}
private:
	Data m_data;
};
// 更高效的写法
class Test3
{
public:
	Test3(Data& data) : m_data(data){}
private:
	Data m_data;
};
int main()
{
    Data a;
    cout<<"---------------THIS IS TEST1---------------"<<endl;
    Test1 t1(a);
    cout<<"---------------THIS IS TEST2---------------"<<endl;
    Test2 t2(a);
    cout<<"---------------THIS IS TEST3---------------"<<endl;
    Test3 t3(a);
    return 0;
}
/*
	输出结果:
	This is Data constructor1
	---------------THIS IS TEST1---------------
	This is Data constructor2
	This is Data constructor1
	This is Data constructor3
	---------------THIS IS TEST2---------------
	This is Data constructor2
	This is Data constructor2
	---------------THIS IS TEST3---------------
	This is Data constructor2
	(1)对于TEST1,没有使用列表初始化,所以其私有变量m_data是通过调用Data()定义的,所以会出现“This 	is Data constructor1”,而“This is Data constructor2”是在发生在参数传递调用的拷贝构造,最		后“m_data = data”会发生拷贝赋值,从而调用“This is Data constructor3”
	(2)对于TEST2,“This is Data constructor2”也是在发生在参数传递调用的拷贝构造,而另外一次调用是	发生在列表初始化“Test2(Data data) : m_data(data){}”
	(3)对于TEST3,则是TEST3的构造函数中参数使用了引用,就避免了参数传递而调用的拷贝构造,所以只有一次		调用拷贝构造是发生在列表初始化的时候
*/

(2) 防止类型窄化,避免精度丢失的隐式类型转化

C++11可使用 explicit 关键字对单参数的构造函数进行声明,让编译器无法进行隐式类型转换,但仅限于单参数或者其他参数有默认值的情况下使用。而列表初始化是禁止避免精度丢失的隐式类型转化,不像 explicit 禁止所有的隐式类型转换

int main() {
    // 浮点型到整型的转换
    int a = 1.2; // ok
    int b = {1.2}; // error
    // 整型到浮点型的转换
    float c = 1e70; // ok
    float d = {1e70}; // error
	// char的枚举类型
    const int i = 1000;
    const int j = 65;
    char k = i; // ok
    char l = {i}; // error
    char m = j; // ok,m为'A'
    char m = {j}; // ok,因为是const类型,这里如果去掉const属性,也会报错
}

(3)可以使用初始化列表接受任意长度的参数

std::initializer_list,它可以接收任意长度的初始化列表,但是里面必须是相同类型T,或者都可以转换为T

class Test {
public:
    Test(std::initializer_list<int> list) {
        for (auto iter = list.begin(); iter != list.end(); ++iter) {
            vec.push_back(*iter);
        }
    }
private:
    std::vector<int> vec;
};