突破编程_C++_C++11新特性(列表初始化)

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++ 标准没有提供直接在类定义中进行初始化的机制,因为位字段的初始化通常依赖于具体的实现细节和存储布局。位字段的初始化通常是在构造函数的初始化列表中完成的,但即使在那里,也只能通过赋值来进行,而不是通过列表初始化。

  • 22
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值