在 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;
}
我们来看看上面出现的一个问题:
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;
};
}