初始化列表初始化
在 C++ 中有多种初始化方式,我认为初始化属于非常基础的语法部分,于是把它放到了第一节。
INFO
这些初始化的名字是举足轻重的,不要拘泥于他们叫什么。
拷贝初始化
这应该是我们最常见的一种初始化:
int x = 0; // x的值是0
double y = 0.;有必要注意一下 C++ 的字面量(如 0,0.)也是有类型的,对于 0 为 int,0. 这种带小数点的(隐式 0.0)为 double,其他字面量类型可自行查看文档:
该拷贝初始化的语义可以如下简单理解:当声明一个变量 x 时,为它赋值 0 以初始化。
括号初始化
int x(12); // x的值为12
int y(); // 注意,这行代码是一个函数声明
double z(0.1);
std::vector<int> a(x); // a是一个size为x的vector括号初始化也比较简单理解,但需要注意括号初始化的一个地方为 y 变量的声明。
陷阱
注意在该声明中声明的不是 y 这个 int 变量,而是一个无参数,返回值为 int,名字是 y 的函数声明,括号初始化需要注意这个坑点。凡是 type name() 的写法都可能会被误解释为函数声明。
当然,对于普通字面类型我们不会也不推荐选择使用括号初始化,括号初始化常见的用法是构造函数的调用,如 vector 变量 a 的声明。
初始化列表初始化(花括号初始化 C++11)
int x{};
int y{ 12 };
double z{ 0.666 };
std::vector<int> a{ 1, 2, 3, 4, 5, 6 };
std::vector<int> b{ 4, 0 };
std::vector<int> c{}; // c{}等价于c,后面解释
std::vector<int> aa{ a }; // aa == a
int d[]{ 1, 2, 3, 4, 5 };
int f[] = { 1, 2, 3 }; // 只是继承 C 语言,不过多介绍
int k[100]{};花括号初始化其实也不难理解。
需要注意以下几个点:
- 使用空花括号
{}初始化,效果为值初始化,对于int、double这种字面类型,默认值为0与0.,而对于上面的c变量而言,则是一个空的vector,而对于k数组而言,则是会默认清空所有值为0 a变量是符合预期的,但是b变量可能不符合预期(我可能希望的是size为4,且其元素皆拷贝初始化为0的vector),实际上花括号也是可以调用类的构造函数的,比如aa变量,但是b变量会被解释为容量为 2,b[0] = 4,b[1] = 0。这种情况下,就只能选择括号初始化了,当然这也是我为数不多会使用括号初始化的情景。
匿名变量
C++ 支持匿名变量的写法,匿名变量则有一些其他好处:
import std;
using i64 = long long;
auto func(std::pair<int, int> p, int y) -> int
{
return y;
}
auto main() -> int
{
int(3); // 建立一个匿名变量,类型 int,值为 3
int{ 3 }; // 同上
int x = int{ 4 }; // 其实等价于 int x = 4
i64 b = i64{ 10 };
int ret = func(std::pair<int, int>{ 1, 2 }, short(3));
}对于这行代码:
int x = 4;刚才说到,4 字面量的类型就是 int,所以 4 在某种程度上可以理解为 int{ 4 }。
i64 b = 10;对于这行代码,属于拷贝初始化的范畴,我们可以按步骤理解这个语句:
- 先将
10,也就是int{ 10 },隐式类型转换到i64{ 10 } - 将匿名变量
i64{ 10 }拷贝给b变量完成初始化。
匿名变量还有个好处则是在函数传参的时候,可以直接初始化一个匿名变量,然后完成传参。
你应该会频繁的在算法竞赛中看到如下代码:
std::vector<std::pair<int, int>> a;
a.push_back({ 1, 2 });TIP
对于这种情况下,你应该用 a.emplace_back(1, 2)(此处原本的占位链接已移除)
对于 T 为 std::pair<int, int> 的 vector 的 push_back 函数,其要求传入一个 std::pair<int, int> 变量,那么我们写了一个 { 1, 2 },行为则是:
- 将
{ 1, 2 }花括号类转变为std::pair<int, int>{ 1, 2 } - 将该匿名变量拷贝给函数形参,完成函数形参的拷贝传递
INFO
你可能注意到我说花括号类,实际本质上 C++ 的花括号是一个类类型,在本节最后会给出扩展的讲解。
匿名变量的生命周期仅限那一条语句,也就是说,从那条语句开始执行起,在栈上开辟那个变量的空间,当语句执行结束后立即回收。
函数式类型转换
import std;
using i64 = long long;
auto main() -> int
{
i64 x = 0LL; // 0LL 是 i64 型字面量
x = i64{ 2 };
int y = 13;
x = i64(y); // i64 转换函数?
x = i64{ y };
y = int(x); // int 函数?
}INFO
如果你理解了初始化与匿名变量,那么你应当能理解这里的类型转换,显然这里与 Python 的 int() 是有区别的。
默认初始化
struct node
{
int x, y;
};
struct node2
{
int x{};
int y{};
};
auto main() -> int
{
int x; // x 的值未定义
int x{}; // x 是 0
std::vector<int> a; // a 是空 vector
std::vector<int> a{}; // 等价于上
node nd; // nd.x 与 nd.y 是未定义
node nd2{}; // nd2.x 与 nd2.y 都是 0
node2 b; // b.x = 0, b.y = 0
node2 c{}; // 显然同上
}INFO
对于字面量类型而言,什么也没有的默认初始化会导致值为未定义,对于 STL 而言,大多数下默认初始化等价于值初始化,而对于自己定义的结构体类型,默认初始化的行为取决于自己的设计。
总结
在了解了三个初始化方式,以及匿名变量后,我建议如下初始化:
- 对于字面类型诸如
int、double,直接用最简单的拷贝初始化方式 - 对于必须用括号初始化的地方,比如
std::vector a(10, 0),用括号初始化 - 其余选用花括号初始化
import std;
auto main() -> int
{
int x = 0;
for(int i = 0; i != 10; ++i) {
// 循环体
}
std::vector<int> a(10, 0);
std::vector<int> b{ 1, 2, 3, 4, 5, 6 };
std::string s; // 空字符串
std::string s2{}; // 空字符串 too.
std::string str{ "Hella" };
std::string str2 = "Hella"; // 当然,字符串更推荐这种
}WARNING
另外提醒一下,只有括号初始化和花括号初始化支持匿名变量初始化,拷贝初始化是不行的。