一、何为列表初始化
C++中的列表初始化是一种用一对花括号 {} 来进行对象初始化的语法。它被引入主要是为了提供一种统一的初始化方式,适用于各种不同的数据类型和数据结构,包括基本类型、数组、结构体、类、STL 容器等。列表初始化在 C++11 标准中被引入,是现代 C++ 编程风格的一部分。
基本语法
Type variable = {value1, value2, ...};
- 使用一对花括号 {} 来初始化对象。
- 列表初始化对于类型转换更为严格,不允许缩窄转换(请看下面何为窄转化部分)。
示例
- 基本类型:
int x = {42};
double y = {3.14};
- 数组:
int arr[] = {1, 2, 3, 4, 5};
- 结构体:
struct Point {
int x;
int y;
};
Point p = {10, 20};
- 类:
class MyClass {
public:
int data;
double value;
MyClass(int d, double v) : data(d), value(v) {}
};
MyClass obj = {42, 3.14};
- STL 容器:
#include <vector>
#include <map>
std::vector<int> vec = {1, 2, 3, 4, 5};
std::map<int, std::string> myMap = {{1, "one"}, {2, "two"}};
- 自定义类型:
class MyType {
public:
int x;
double y;
MyType(int a, double b) : x(a), y(b) {}
};
MyType myVar = {5, 2.5};
二、何为窄转化
窄转化(Narrowing Conversion)指的是将一个具有较大范围的值转换为较小范围的类型时可能丢失信息的情况。这种转换可能导致截断或失真,因为目标类型的表示范围比源类型小。在 C++ 中,窄转化是一种不安全的类型转换,因为它可能导致数据丢失或意外的行为。
以下是一些示例说明窄转化:
- 从浮点数到整数:
double myDouble = 3.14;
int myInt = myDouble; // 窄转化,可能会截断小数部分
- 从长整型到整数:
long long myLong = 1000000000000;
int myInt = myLong; // 窄转化,可能会截断或溢出
- 从大范围的整数类型到小范围的整数类型:
long long myLong = 1000000000000;
int myInt = static_cast<int>(myLong); // 窄转化,可能会截断或溢出
窄转化是需要小心处理的,因为它可能导致数据的损失和不确定的行为。在需要进行类型转换时,最好使用安全的转换方式,例如使用 static_cast 并在可能丢失信息的地方进行显式的检查和处理。在 C++11 引入的列表初始化中,提供了对缩窄转换的更严格的检查,不允许在列表初始化时发生缩窄转换,从而帮助程序员避免潜在的问题。
三、列表初始化规则和特点
列表初始化有一些规则和特点,主要包括以下几个方面:
1. 不允许缩窄转换
列表初始化对类型转换更为严格,不允许发生缩窄转换,即不允许将一个精度更高的类型赋值给一个精度较低的类型。
int x = {3.14}; // 错误,尝试缩窄转换
2. 对于数组,列表初始化的大小由元素个数决定
int arr[] = {1, 2, 3}; // 合法,数组大小为3
3. 类型不匹配时可能调用构造函数
当列表初始化的类型和目标类型不匹配时,如果存在适当的构造函数,编译器会尝试调用构造函数进行初始化。
class MyClass {
public:
int data;
double value;
MyClass(int d, double v) : data(d), value(v) {}
};
MyClass obj = {42, 3.14}; // 合法,调用构造函数
4. 空列表初始化
在某些情况下,可以使用空的花括号 {} 进行初始化,这会被解释为对应类型的默认值。
int x = {}; // x 被初始化为 0
double y = {}; // y 被初始化为 0.0
5. 对于类类型,构造函数的匹配规则
当进行列表初始化时,编译器会根据构造函数的参数匹配规则选择相应的构造函数。
class Example {
public:
Example(int a, double b);
Example(std::string str);
};
Example obj1 = {42, 3.14}; // 调用构造函数 Example(int, double)
Example obj2 = {"Hello"}; // 调用构造函数 Example(std::string)
6. 嵌套初始化
可以使用嵌套的列表初始化来初始化嵌套的数据结构。
std::vector<std::vector<int>> matrix = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
总体来说,列表初始化提供了一种简洁且直观的初始化语法,同时对类型匹配和转换有着更为严格的规定,减少了一些初始化时可能发生的错误。
四、列表初始化的好处
列表初始化(Uniform Initialization)在 C++ 中引入的好处主要有以下几点:
- 一致性:列表初始化提供了一种一致的初始化语法,可以用于初始化各种类型的对象,包括基本类型、数组、结构体、类、STL 容器等。这种一致性使得代码更加清晰和易读。
int x = {42}; // 初始化基本类型
int arr[] = {1, 2, 3}; // 初始化数组
Point p = {10, 20}; // 初始化结构体
MyClass obj = {42, 3.14}; // 初始化类
std::vector<int> vec = {1, 2, 3}; // 初始化容器
- 防止窄化转换:列表初始化对类型转换更为严格,不允许发生缩窄转换,从而减少了一些可能引入 bug 的情况。
int x = {3.14}; // 错误,尝试缩窄转换
- 构造函数匹配:当进行列表初始化时,如果存在适当的构造函数,编译器会尝试调用构造函数进行初始化。这提高了代码的灵活性,使得用户定义的类型更容易进行初始化。
class MyClass {
public:
int data;
double value;
MyClass(int d, double v) : data(d), value(v) {}
};
MyClass obj = {42, 3.14}; // 调用构造函数
- 简洁性:列表初始化的语法相对简洁,通过一对花括号{}就可以完成初始化,避免了传统的各种初始化方式可能导致的歧义。
int arr[] = {1, 2, 3}; // 合法,简洁
- 避免 most vexing parse: 传统的初始化语法在某些情况下可能会导致 most vexing parse,而列表初始化语法避免了这一问题。
"Most Vexing Parse" 是一个有趣而令人困扰的 C++ 编程问题,它通常发生在类的对象声明上,导致程序员可能不是按照他们预期的方式初始化对象。这个问题的名字来源于这种情况的令人迷惑和难以理解。
Most Vexing Parse 主要发生在下面这样的情况:
class MyClass {
public:
MyClass() {}
};
int main() {
MyClass obj(); // 最令人迷惑的解析,声明了一个函数而不是对象
// ...
return 0;
}
在上述代码中,MyClass obj(); 被编译器解释为声明一个返回 MyClass 类型的函数而不是创建一个 MyClass 类型的对象。这是因为在 C++ 中,如果声明一个函数的时候带有空括号,编译器会将其解释为一个函数声明而不是一个对象定义。
为了避免 most vexing parse,可以使用以下两种方式之一:
- 使用花括号初始化:
MyClass obj{}; // 使用花括号初始化,避免 most vexing parse
- 使用括号初始化:
MyClass obj(); // 编译器会将其解释为函数声明
MyClass obj{}; // 使用括号初始化,避免 most vexing parse
这个问题是由 C++ 语法规则引起的,对于初学者来说可能会令人困扰。因此,在声明和初始化对象时,特别是在有可能发生 most vexing parse 的地方,建议使用花括号初始化或括号初始化,以避免潜在的问题。
五、不适用列表初始化的情况
什么是聚合类型
1、类型是一个普通数组,如int[5],char[],double[]等
2、类型是一个类,且满足以下条件:
- 没有用户声明的构造函数
- 没有用户提供的构造函数(允许显示预置或弃置的构造函数)
- 没有私有或保护的非静态数据成员
- 没有基类
- 没有虚函数
- 没有{}和=直接初始化的非静态数据成员
- 没有默认成员初始化器
虽然列表初始化是一种很方便和清晰的初始化方式,但有一些情况下不适合或者不能使用列表初始化:
- 不支持聚合初始化的类列表初始化主要用于聚合类型的初始化,而对于不支持聚合初始化的类,不能使用列表初始化。一个类如果有用户自定义的构造函数、私有/受保护的非静态数据成员,或者基类没有默认构造函数,那么该类就不再是聚合类型。
class NotAggregate {
public:
int x;
int y;
NotAggregate(int a, int b) : x(a), y(b) {}
};
NotAggregate obj = {1, 2}; // 错误,NotAggregate 不是聚合类型
- 不能进行窄化转换的地方:列表初始化不允许发生窄化转换,因此在需要执行窄化转换的地方不能使用列表初始化。
double myDouble = 3.14;
int myInt = myDouble; // 合法,但列表初始化会报错
- 需要避免 most vexing parse 的地方:在可能发生 most vexing parse(最令人迷惑的解析)的地方,列表初始化可能不适用。这通常发生在类的默认构造函数被误解为函数声明的情况下。
class MyClass {
public:
MyClass() {}
};
MyClass obj(); // 最令人迷惑的解析,声明了一个函数而不是对象
MyClass obj{}; // 正确的初始化方式
总之,虽然列表初始化是一种很便捷和安全的初始化方式,但在某些情况下,特别是对于非聚合类型和可能导致 most vexing parse 的地方,可能需要考虑其他的初始化方式。