列表初始化是 C++11 引入的一个新特性。**它允许使用花括号 {}
来初始化变量、数组、结构体等。**列表初始化具有更加统一和直观的语法,同时也提供了一些优势,例如防止窄化转换(narrowing conversion)等。
下面是一些列表初始化的例子:
int x{42}; // 初始化整数变量 x 为 42
int arr[]{1, 2, 3}; // 初始化整数数组 arr 为 {1, 2, 3}
struct Point {
int x;
int y;
};
Point p{10, 20}; // 初始化结构体 Point 的对象 p 的成员 x 和 y
#include
std::vector vec{1, 2, 3}; // 初始化整数向量 vec 为 {1, 2, 3}
// 编译错误,禁止窄化转换
int narrowError{3.14};
窄化转换(Narrowing Conversion)指的是在类型转换时可能导致信息的损失或不准确的转换。具体来说,当将一个具有更大范围的值转换为一个较小范围的类型时,可能导致丢失精度或截断。
在 C++ 中,窄化转换是一种类型转换的错误,因为它可能导致数据丢失或错误的结果。C++11 引入了列表初始化,部分是为了解决窄化转换的问题,因为使用花括号 {}
进行列表初始化时,会禁止窄化转换,提高了代码的类型安全性。
以下是一个示例,展示了窄化转换的情况:
#include
int main() {
// 编译通过,但可能导致窄化转换
int x = 3.14; // x 为 3,发生了从 double 到 int 的窄化转换
// 编译错误,禁止窄化转换
int y{3.14}; // 无法将 double 转换为 int,发生窄化转换
std::cout << x << std::endl;
return 0;
}
第一个赋值语句虽然可以通过编译,但它进行了从 double
到 int
的窄化转换,导致小数部分被截断。而第二个赋值语句使用了列表初始化,由于列表初始化禁止窄化转换,所以会导致编译错误。在实践中,推荐使用列表初始化语法,以提高代码的类型安全性。
先看这段代码:
#include
#include
using namespace std;
struct T1
{
int x;
int y;
}a = { 123, 321 };
struct T2
{
int x;
int y;
T2(int, int) : x(10), y(20) {}
}b = { 123, 321 };
int main(void)
{
cout << "a.x: " << a.x << ", a.y: " << a.y << endl;
cout << "b.x: " << b.x << ", b.y: " << b.y << endl;
return 0;
}
程序执行结果:
a.x: 123, a.y: 321
b.x: 10, b.y: 20
可以看出,T2中由于有了构造函数,初始化列表中的数据优先级就低了,对象是通过构造函数进行的初始化。
解释:如果使用列表初始化对对象初始化时,还需要判断这个对象对应的类型是不是一个聚合体,如果是初始化列表中的数据就会拷贝到对象中。
什么是**聚合体**?
在C++中,聚合体(Aggregate)是一种特殊的数据类型,用于将多个数据成员组合成一个单一的数据单元。C++11标准对聚合体进行了一些修订和扩展。
一个聚合体必须满足以下条件:
在C++11之前,聚合体通常用于数组初始化。**在C++11及之后,聚合体的概念被扩展,它还可以通过初始化列表进行初始化。**这意味着在C++11及以后,符合聚合体条件的类型可以使用更灵活的初始化方式。
以下是一个简单的示例:
// 在C++11之前的聚合体
struct Point {
int x;
int y;
};
// 在C++11及以后的聚合体
struct Rectangle {
int width;
int height;
// 在C++11之后,可以包含构造函数
Rectangle(int w, int h) : width(w), height(h) {}
};
int main() {
// C++11之前,使用聚合体初始化数组
Point points[] = { {1, 2}, {3, 4}, {5, 6} };
// C++11及以后,使用聚合体初始化变量
Rectangle rect{10, 20};
return 0;
}
在上述示例中,Point
是一个C++11之前的聚合体,可以用花括号初始化数组。而Rectangle
是一个C++11及以后的聚合体,可以使用花括号初始化变量。这种初始化方式使得代码更为简洁,并提高了可读性。
对于聚合类型的类可以直接使用列表初始化进行对象的初始化,如果不满足聚合条件还想使用列表初始化其实也是可以的,需要在类的内部自定义一个构造函数, 在构造函数中使用初始化列表对类成员变量进行初始化:
#include
#include
using namespace std;
struct T1
{
int x;
double y;
// 在构造函数中使用初始化列表初始化类成员
T1(int a, double b, int c) : x(a), y(b), z(c) {}
virtual void print()
{
cout << "x: " << x << ", y: " << y << ", z: " << z << endl;
}
private:
int z;
};
int main(void)
{
T1 t{ 520, 13.14, 1314 }; // ok, 基于构造函数使用初始化列表初始化类成员
t.print();
return 0;
}
另外,需要额外注意的是聚合类型的定义并非递归的,也就是说当一个类的非静态成员是非聚合类型时,这个类也可能是聚合类型,比如下面的这个例子:
#include
#include
using namespace std;
struct T1
{
int x;
double y;
private:
int z;
};
struct T2
{
T1 t1;
long x1;
double y1;
};
int main(void)
{
T2 t2{ {}, 520, 13.14 };
return 0;
}
可以看到,T1并非一个聚合类型,因为它有一个Private的非静态成员。但是尽管T2有一个非聚合类型的非静态成员t1,T2依然是一个聚合类型,可以直接使用列表初始化的方式进行初始化。
最后强调一下t2对象的初始化过程,对于非聚合类型的成员t1做初始化的时候,可以直接写一对空的大括号{},这相当于调用是T1的无参构造函数。
对于一个聚合类型,使用列表初始化相当于对其中的每个元素分别赋值,而对于非聚合类型,则需要先自定义一个合适的构造函数,此时使用列表初始化将会调用它对应的构造函数。
在C++的STL容器中,可以进行任意长度的数据的初始化,使用初始化列表也只能进行固定参数的初始化,如果想要做到和STL一样有任意长度初始化的能力,可以使用std::initializer_list这个轻量级的类模板来实现。
std::initializer_list
的定义如下:
namespace std {
template
class initializer_list {
public:
typedef T value_type;
typedef const T& reference;
typedef const T& const_reference;
typedef size_t size_type;
initializer_list() noexcept;
size_t size() const noexcept;
const T* begin() const noexcept;
const T* end() const noexcept;
};
}
先来介绍一下这个类模板的一些特点:
如果想要自定义一个函数并且接收任意个数的参数(变参函数),只需要将函数参数指定为std::initializer_list,使用初始化列表{ }作为实参进行数据传递即可。
#include
#include
using namespace std;
void traversal(std::initializer_list a)
{
for (auto it = a.begin(); it != a.end(); ++it)
{
cout << *it << " ";
}
cout << endl;
}
int main(void)
{
initializer_list list;
cout << "current list size: " << list.size() << endl;
traversal(list);
list = { 1,2,3,4,5,6,7,8,9,0 };
cout << "current list size: " << list.size() << endl;
traversal(list);
cout << endl;
list = { 1,3,5,7,9 };
cout << "current list size: " << list.size() << endl;
traversal(list);
cout << endl;
// 直接通过初始化列表传递数据 //
traversal({ 2, 4, 6, 8, 0 });
cout << endl;
traversal({ 11,12,13,14,15,16 });
cout << endl;
return 0;
}
输出结果为:
current list size: 0
current list size: 10
1 2 3 4 5 6 7 8 9 0
current list size: 5
1 3 5 7 9
2 4 6 8 0
11 12 13 14 15 16
std::initializer_list拥有一个无参构造函数,因此,它可以直接定义实例,此时将得到一个空的std::initializer_list,因为在遍历这种类型的容器的时候得到的是一个只读的迭代器,因此我们不能修改里边的数据,只能通过值覆盖的方式进行容器内部数据的修改。虽然如此,在效率方面也无需担心,std::initializer_list的效率是非常高的,它的内部并不负责保存初始化列表中元素的拷贝,仅仅存储了初始化列表中元素的引用。
自定义的类如果在构造对象的时候想要接收任意个数的实参,可以给构造函数指定为std::initializer_list类型,在自定义类的内部还是使用容器来存储接收的多个实参。
#include
#include
#include
using namespace std;
class Test
{
public:
Test(std::initializer_list list)
{
for (auto it = list.begin(); it != list.end(); ++it)
{
cout << *it << " ";
m_names.push_back(*it);
}
cout << endl;
}
private:
vector m_names;
};
int main(void)
{
Test t({ "jack", "lucy", "tom" });
Test t1({ "hello", "world", "nihao", "shijie" });
return 0;
}
输出的结果:
jack lucy tom
hello world nihao shijie