C++ 列表初始化

一、列表初始化

1.1 出现原因

C++98/03 中的对象初始化方法有很多种方法,这些不同的初始化方法,都有各自的适用范围和作用。最关键的是,这些种类繁多的初始化方法,没有一种可以通用所有情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 基于数组的初始化
int arr1[] = {1,2,3};
int arr2[3] = {4,5,6};

/*
基于POD(plain old data)类型,这里的先调用构造函数传入参数1构造一个A类型匿名对象,然后调用拷贝构造函数传入匿名对象参数构造对象a1,不过编译器会进行优化,实际只调用一次构造函数。
说明:当构造函数被explicit修饰,或者拷贝构造函数为private时,a1会失败。
*/
class A
{
public:
A(int) {}
} a1 = 1;

// 对于STL中容器不能想数组那样进行初始化,只能一个一个插入
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);

1.2 统一的初始化

C++11标准引入了列表初始化,它使用大括号{}对任意类型对象进行初始化。

1
2
3
4
5
6
7
8
9
10
11
// 内置类型初始化
int i1{10} <=> int i0 = {10} <=> int i2 ={10} <=> int i3 =(10) <=> int i4 = int(10)

// 数组
int arr1[3] {1,2,3} <=> int arr2[]{1,2,3} int arr3[] = {1,2,3}

// 动态数组,在C++98/03中不支持
int* arr4 = new int[]{1,2,3};

// STL容器
vector<int> v{1,2,3} <=> vector<int> v = {1,2,3}

1.2.1 自定义类型初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Point {
public:
Point(int x = 0, int y = 0) : _x(x), _y(y) {}

private:
Point(Point &obj) : _x(obj._x), _y(obj._y) {}

private:
int _x;
int _y;
};

Point p1{1,2} <=> Point p2(1,2) // 直接调用构造函数
Point p2 = {1,2} // 先调用构造函数,再调用拷贝构造,编译器可能会优化为只调用一次构造函数

1.3 initializer-list

std::initialzer-list是C++11引入的轻量级类模板,只提供begin、end以及size成员函数。编译器将{ }内的数据存放到std::array中,并构建initializer_list 对象引用这个array对象中的元素,但并不包含它们,拷贝一个 initializer_list 对象会生成另一个引用相同底层元素的对象,而不是创建它们的新副本,即浅拷贝。

C++11起,STL容器之所以能够支持列表初始化原因在于每个容器都提供了一个形参为initializer_list的构造函数。编译器会先构造一个initializer_list对象,然后调用对应容器中形参为initializer_list的构造函数。这个构造函数的逻辑非常简单,它们只需要调用initializer_list对象的begin和end 函数,循环对本对象进行初始化。

1
2
3
4
5
Point p1(1,2);
Point p2(2,3);
vector<int> v1{1,2,3,4,5};
vector<Point> v2{p1,p2} <=> vector<Point> v3{Point(1,2),Point(1,2)};
map<int, int> m{{1,1}, {2,2,},{3,3},{4,4}}

说明:自定义数据类型如果需要实现该功能,需要自己实现参数为initializer-list 的构造函数

1.3.1 initializer-list其它用途

initializer-list不仅可以用于自定义类型的列表初始化,也可以用于传递相同类型数据的集合,例如作为函数的形参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void print(std::initializer_list<int> l)
{
for (auto it = l.begin(); it != l.end(); it++)
{
cout << *it << endl;
}
}

int main()
{
print({});//传递一个空集
print({ 1, 2, 3 });//传递int类型的集合

return 0;
}

1.4 注意事项

1.4.1 Narrowing conversions(缩窄转换)

在 C++ 中,缩窄转换是一种不安全的类型转换,因为目标类型可能无法保存源类型的所有值。缩窄转换是在编写代码中稍不留意就会出现,而且它的出现并不一定会引发错误,甚至有可能连警告都没有,所以有时候容易被人们忽略,比如:

1
2
int x = 2025;
char c = x; // 2015 超过了char类型范围,会导致溢出,c的值不可预测

C++标准规定下列情况属于缩窄转换:

  • 从浮点类型转换整数类型
  • 从long double转换到double或float,或从double转换 到float,除非转换的值是constexpr并且在目标类型的范围内
  • 从整数类型转换到浮点类型,除非转换的值是constexpr并且在目标类型的范围内
  • 从整数类型转换为另一种整数类型,该整数类型不能表示原始类型的所有值,除非转换的值是 constexpr 并且其值可以精确存储在目标类型中

使用列表初始化,编译器对于缩窄转换会报编译错误(MSVC、clang)或警告(GCC)。

1
int i{3.14}; // 存在缩窄转换

1.4.2 列表初始化的优先级

列表初始化既可以支持普通的构造函数,也能够支持以initializer_list为形参的构造函数。如果这两种构造函数同时出现在同一个类里,initializer_list为形参的构造函数优先级更高

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Point
{
public:
Point(int x = 0, int y = 0) : _x(x), _y(y) { cout << "Point(int x = 0, int y = 0)" << endl; }

Point(initializer_list<int> l) { cout << "Point(initializer_list<int> l)" << endl; }

private:
int _x;
int _y;
};


Point p1(1, 2); // 调用 Point(int x = 0, int y = 0) 构造函数
Point p2{1, 2}; // 使用列表初始化,调用Point(initializer_list<int> l)构造函数