1 基本概念与使用
C++11 引入了许多新特性,其中列表初始化(List Initialization)或统一初始化(Uniform Initialization)是其中之一。列表初始化是一种新的语法,用于初始化对象,可以使得代码更加清晰、直观,并且有助于防止某些错误。
(1)列表初始化的基本语法
列表初始化使用花括号 {} 来包围初始化列表。例如:
int a{10}; // 基本类型
std::vector<int> v{1, 2, 3, 4, 5}; // STL 容器
std::pair<int, int> p{1, 2}; // STL 对
(2)列表初始化与函数参数
列表初始化也可以用于函数调用,这有助于消除一些歧义,特别是在涉及到多个重载函数时:
void foo(int a);
void foo(const std::initializer_list<int>& list); foo(10); // 调用 foo(int a)
foo({10}); // 调用 foo(const std::initializer_list<int>& list)
(3)列表初始化与构造函数
列表初始化会影响构造函数的选择。如果类有多个构造函数,并且其中一个接受 std::initializer_list 参数,那么使用列表初始化时,会调用这个构造函数:
class MyClass
{
public: MyClass(int a); // 构造函数1 MyClass(const std::initializer_list<int>& list); // 构造函数2
}; MyClass obj1(10); // 调用构造函数1
MyClass obj2{10}; // 调用构造函数2,因为使用了列表初始化
(4)列表初始化与类成员
在类的构造函数体内,列表初始化也可以用于初始化成员变量:
class MyClass
{
public: MyClass(int val, const std::string& str) : x{val}, s{str} {}
private: int x; std::string s;
};
(5)列表初始化创建结构体对象与类对象
列表初始化可以用来创建结构体(struct)对象和类(class)对象。结构体和类在 C++ 中都是用户定义的类型(UDT),它们的主要区别在于访问控制(如公有、私有和保护成员)和继承方面的不同,但在初始化方面,它们通常使用相同的方法。
如果结构体与类的所有成员变量都是公有的,则可以直接使用列表初始化:
#include <iostream>
#include <string> // 定义一个结构体
struct PersonStruct {std::string name;int age;
};// 定义一个类
class PersonClass {
public:std::string name;int age;
};int main()
{// 使用列表初始化创建结构体对象 PersonStruct personStruct1 = { "Alice", 30 };// 使用列表初始化创建类对象PersonClass personClass1 = { "Bob", 25 }; return 0;
}
如果成员变量是私有或者保护的,则需要使用构造函数:
#include <iostream>
#include <string> class PersonClass {
public:// 如果没有该构造函数,则列表初始化会失败PersonClass(const std::string& name, int age) : name(name), age(age) {}
private:std::string name;int age;
};int main()
{PersonClass personClass1 = { "Bob", 25 }; return 0;
}
(6)列表初始化与数组
列表初始化也可以用于初始化数组:
int arr1[3] = {1, 2, 3}; // C++98风格
int arr2[] = {1, 2, 3}; // C++11风格,数组大小由初始化器确定
(7)自动类型推导
在使用auto关键字声明变量时,列表初始化可以自动推导变量的类型:
auto x = {10}; // x的类型是std::initializer_list<int>
auto y{10}; // y的类型是int,因为直接列表初始化不会推导为initializer_list
注意,当使用 = 进行初始化时,如果初始化器是列表,则 auto 会推导为 std::initializer_list 类型。而直接使用 {} 进行初始化时,则会根据列表的内容推导为具体的类型。
(8)嵌套列表初始化
对于包含嵌套结构的对象,列表初始化同样适用:
std::pair<int, std::vector<int>> p{1, {2, 3, 4}};
在这个例子中,p 是一个 pair,其第一个元素是 int 类型,第二个元素是一个 vector<int> 类型。列表初始化允许同时初始化这两个元素。
总体而言,C++11的列表初始化(统一初始化)提供了一种更加清晰、直观的方式来初始化对象。它有助于消除一些歧义,特别是在涉及到重载函数和多个构造函数时。
2 防止类型收窄
列表初始化不仅提供了一种更直观、更统一的对象初始化方式,而且它还能有效地防止类型收窄(narrowing)。
2.1 类型收窄的概念与防止
类型收窄是指在赋值过程中,源类型的值无法精确表示为目标类型的值,从而导致数据丢失或精度降低。
在 C++11 之前,使用圆括号或等号进行初始化时,编译器往往不会检查类型收窄的问题,这可能导致一些难以察觉的错误。然而,在使用列表初始化时,编译器会进行更严格的检查,并在发现类型收窄时发出警告或错误。
类型收窄通常发生在以下几种情况:
- 浮点数到整数的转换:当浮点数的值无法精确表示为整数时。
- 大整数到小整数的转换:当源整数的值超出目标整数类型的表示范围时。
- 字符到整数的转换:当字符的 ASCII 值无法精确表示为整数时(虽然这种情况较少见)。
列表初始化通过以下方式防止类型收窄:
- 在初始化时,如果源类型的值无法精确表示为目标类型的值,编译器会发出警告或错误。
- 适用于基本类型、结构体、联合体以及类的初始化。
如下为一些样例代码:
(1)浮点数到整数的转换
double d = 3.14;
int a(d); // 可能的类型收窄,但 C++11 之前的编译器通常不会警告
int b{d}; // 类型收窄,C++11 编译器会发出警告或错误
在上面的例子中,将 double 类型的值 3.14 转换为 int 类型时,小数部分会被丢弃。使用圆括号初始化时,编译器可能不会发出警告。但是,使用列表初始化时,编译器会检测到这种类型收窄,并发出警告或错误。
(2)大整数到小整数的转换
long long bigNum = 1234567890123456789LL;
int smallNum(bigNum); // 可能的类型收窄,但 C++11 之前的编译器通常不会警告
int smallNumList{bigNum}; // 类型收窄,C++11 编译器会发出警告或错误
在这个例子中,尝试将一个非常大的 long long 整数赋值给一个 int 变量。由于 int 类型的范围远小于 long long,这种转换会导致数据丢失。使用列表初始化时,编译器会检测到这种类型收窄。
2.2 如何避免类型收窄
要避免类型收窄,可以采取以下策略:
(1)显式转换: 如果确实需要进行类型转换,并且你确定转换是安全的,可以使用显式类型转换(如静态转换 static_cast)。但请注意,即使使用显式转换,如果转换会导致数据丢失,编译器仍然可能发出警告。
double d = 3.14;
int a = static_cast<int>(d); // 显式转换,但仍然可能导致数据丢失
(2)使用合适的类型: 在设计程序时,尽量使用能够精确表示所需数据的类型。如果可能的话,避免使用会导致类型收窄的转换。
(3)利用编译器警告: 确保编译器设置为在发现可能的类型收窄时发出警告。这可以帮助在编写代码时及时发现问题。
3 限制和注意事项
(1)类型不匹配: 列表初始化不会自动进行类型转换。如果初始化器的类型与目标类型不匹配,且没有合适的构造函数可以接受该初始化器,则会导致编译错误。
#include <iostream> class MyClass {
public:MyClass(int value) {std::cout << "Constructed with int: " << value << std::endl;}// 注意:没有定义接受double类型的构造函数 // MyClass(double value); // 假设这个构造函数不存在
};int main()
{// 使用圆括号进行初始化时,会尝试类型转换(如果有合适的构造函数) MyClass obj1(3.14); // 编译器会输出告警 // 使用列表初始化时,不会进行类型转换 MyClass obj2{ 3.14 }; // 错误:没有合适的构造函数接受 double 类型的参数,编译失败return 0;
}
上面的代码定义了一个 MyClass 类,它有一个接受 int 类型参数的构造函数。在 main 函数中,试图用一个 double 类型的值(3.14)来初始化 MyClass 的对象。
当使用圆括号进行初始化时(MyClass obj1(3.14);),编译器会尝试找到一个合适的构造函数来接受这个 double 类型的值。如果存在一个接受 double 的构造函数,或者存在一个接受 int 的构造函数并且编译器能够自动将 double 转换为 int,那么初始化就会成功。
当使用列表初始化时(MyClass obj2{3.14};),编译器不会尝试进行任何类型转换。它会直接查找一个接受 double 类型参数的构造函数,但因为没有定义这样的构造函数,所以编译器会报错,指出没有合适的构造函数来接受这个初始化器。
(2)静态成员初始化: 列表初始化不能用于直接在类定义中初始化静态成员变量。
#include <iostream> class MyClass {
public:MyClass() {}public:static int myInt{12}; // 错误:不能在类定义中直接初始化静态成员,编译失败
};
静态成员变量的初始化不能在类定义内部进行,而必须在类定义外部完成:
#include <iostream> class MyClass {
public:MyClass() {}public:static int myInt;
};int MyClass::myInt{ 12 };
(3)位字段: 列表初始化不能用于直接初始化位字段。
class MyClass {
public:MyClass() {}private:unsigned int myBitField: 4; // 位字段 // 下面的尝试是错误的,不能使用列表初始化直接初始化位字段 // unsigned int myBitField : 4 {2}; // 错误:不能使用列表初始化直接初始化位字段
};
对于位字段,C++ 标准没有提供直接在类定义中进行初始化的机制,因为位字段的初始化通常依赖于具体的实现细节和存储布局。位字段的初始化通常是在构造函数的初始化列表中完成的,但即使在那里,也只能通过赋值来进行,而不是通过列表初始化。