在 C++98 中,标准允许使用花括号 {} 对数组或者结构体元素进行统一的列表初始值设定。比如:
struct Point
{
int _x;
int _y;
};
int main()
{
int a1[] = { 1,2,3,4,5 };
int a2[5] = { 0 };
Point p1 = { 1, 2 };
Point p2{ 1, 2 }; // C++98不支持这种写法
return 0;
} 对于一些自定义的类型,却无法使用这样的初始化,比如:
vector<int> v{1,2,3,4,5}; 在 c++98 中无法通过编译,导致每次定义 vector 时,都需要先把 vector 定义出来,然后使用循环对其赋初始值,非常不方便。
所以 C++11 扩大了用花括号括起的列表的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号 = ,也可不添加。
初始化列表的优点在于可以简化代码并提高可读性。在某些情况下,它还可以提高性能,因为使用初始化列表可以避免不必要的对象拷贝和转换。
列表初始化在初始化时,如果出现类型截断,编译器是会报警告或者错误的,具体的行为取决于编译器的实现,(例如将一个较大的数值赋值给一个较小的整数类型)比如下述代码:
int main()
{
int a = 10;
double b{ a }; // ❌因为从int到double需要收缩转换
double c = 10.0;
int d{ c }; // ❌因为从double到int也需要收缩转换
}
int main()
{
int m = 100;
char n = { m }; // ❌收缩,无法通过编译
char m1 = 100;
int n1 = { m1 }; // 可以通过编译
const int x = 1024;
const int y = 10;
char a = x; // 收缩,但可以通过编译
char* b = new char(1024); // 收缩,但可以通过编译
char c = { x }; // ❌收缩,无法通过编译,因为x超出char的范围
char d = { y }; // 可以通过编译,因为y的值没有超过char的范围,这种转化称为整数类型收缩
char e = static_cast<char>(x); // 显式进行类型转换,并进行范围检查,比上面的做法安全的多
unsigned char e{ -1 }; // ❌收缩,无法通过编译,从int转换到unsigned char需要收缩转换
float f{ 7 }; // 可以通过编译,由小到大转化不会影响
int g{ 2.0f }; // ❌收缩,无法通过编译
float* h = new float{ 1e48 }; // ❌收缩,无法通过编译,该浮点常量算术溢出
float i = 1.2l; // 可以通过编译,从long double到float截断
return 0;
} 在 C++ 中,将一个 const int 类型的值赋给 char 类型的变量时,编译器会发生一种叫做 整数类型收缩 的隐式类型转换,而不是发生强制类型转换。
整数类型收缩发生在以下场景中:将一个整数类型的值赋给另一个较小的整数类型的变量时,编译器会将原来类型的值的高位截断,然后赋给目标类型的变量。这个过程中,可能会导致信息的丢失或不可预期的行为。但是这种类型转换不会导致编译器报错,因为 C++ 标准规定了这种类型转换是可以进行的。
为了避免类型收缩转换带来的问题,建议在转换时进行类型检查,并使用合适的转换函数(例如 static_cast 或 dynamic_cast)来确保转换的正确性。如果必须进行类型收缩转换,建议进行额外的检查和处理,以避免不可预期的行为。
-
内置类型的列表初始化:
int main() { // 内置类型变量 int x1 = {10}; int x2{10}; int x3 = 1+2; int x4 = {1+2}; int x5{1+2}; // 数组 int arr1[5] = {1,2,3,4,5}; int arr2[]{1,2,3,4,5}; // 动态数组,在C++98中不支持 int* arr3 = new int[5]{1,2,3,4,5}; // 标准容器 vector<int> v1{1,2,3,4,5}; vector<int> v2 = {1,2,3,4,5}; map<int, int> m1{{1,1}, {2,2},{3,3},{4,4}}; map<int, int> m2 = {{1,1}, {2,2},{3,3},{4,4}}; return 0; } -
自定义类型的列表初始化:
class Point { public: Point(int x = 0, int y = 0): _x(x), _y(y) {} private: int _x; int _y; }; int main() { // 本质是{1, 2}调用了initializer_list构造出一个对象然后再初始化,这个下面会讲 Point p{ 1, 2 }; // 使用new和上面的p就不太一样了,会去调用构造函数初始化 // 若没有自己写的构造函数,则下面的初始化对内置类型_X,_y是没有影响的,也就是没有初始化值 Point* ptr = new Point[2]{{1, 1}, {2, 2}}; return 0; }
Ⅱ. 引入 std::initializer_list
我们来看看上面出现的一个问题:
vector<int> v1{1,2,3,4,5}; 这么仔细一想,这里的 {1,2,3,4,5} 是怎么构造给 v1 的呢?是用迭代器区间构造的吗?
答案:不是!这里其实是 C++11 引入的新特性:initializer_list 。
这是一个专门用来初始化列表的类!
它可以将你放入 {} 中的元素按照你要的类型 T 生成一个 initializer_list 对象,接着还有重要的一步,就是如 vector、list 等容器中,C++11 已经添加了新的构造函数参数:以 initializer_list 为参数的构造函数,这样子的话我们就能运用 {} 来初始化容器了!
注意:一般 initializer_list 的头文件被容器的头文件包含了,所以如果引入了容器的头文件了,那么就不需要再包 initializer_list 的头文件了!
int main()
{
// the type of il is an initializer_list
auto il = { 10, 20, 30 };
cout << typeid(il).name() << endl;
return 0;
}
// 运行结果
class std::initializer_list<int> 那么我们如何在模拟实现 vector、list 等容器的时候增加 initializer_list 初始化呢?
💡 只需要多写一个构造函数,然后利用 initializer_list 的迭代器进行插入元素即可:
namespace liren
{
template<class T>
class vector {
public:
typedef T* iterator;
vector(initializer_list<T> l)
{
_start = new T[l.size()];
_finish = _start + l.size();
_endofstorage = _start + l.size();
iterator vit = _start; // 定义一个当前vector的迭代器
// 主要是下面这些步骤:
// 这里typename是去取initializer_list中的内嵌类型iterator
typename initializer_list<T>::iterator lit = l.begin();
while (lit != l.end())
{
*vit++ = *lit++;
}
//for (auto e : l) 也可以直接使用范围for来简化代码
// *vit++ = e;
}
vector<T>& operator=(initializer_list<T> l)
{
vector<T> tmp(l);
std::swap(_start, tmp._start);
std::swap(_finish, tmp._finish);
std::swap(_endofstorage, tmp._endofstorage);
return *this;
}
private:
iterator _start;
iterator _finish;
iterator _endofstorage;
};
}