Skip to content

初始化列表初始化

在 C++ 中有多种初始化方式,我认为初始化属于非常基础的语法部分,于是把它放到了第一节。

INFO

这些初始化的名字是举足轻重的,不要拘泥于他们叫什么。

拷贝初始化

这应该是我们最常见的一种初始化:

cpp
int x = 0; // x的值是0
double y = 0.;

有必要注意一下 C++ 的字面量(如 00.)也是有类型的,对于 0int0. 这种带小数点的(隐式 0.0)为 double,其他字面量类型可自行查看文档:

该拷贝初始化的语义可以如下简单理解:当声明一个变量 x 时,为它赋值 0 以初始化。

括号初始化

cpp
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)

cpp
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]{};

花括号初始化其实也不难理解。

需要注意以下几个点:

  • 使用空花括号 {} 初始化,效果为值初始化,对于 intdouble 这种字面类型,默认值为 00.,而对于上面的 c 变量而言,则是一个空的 vector,而对于 k 数组而言,则是会默认清空所有值为 0
  • a 变量是符合预期的,但是 b 变量可能不符合预期(我可能希望的是size为4,且其元素皆拷贝初始化为0的vector),实际上花括号也是可以调用类的构造函数的,比如 aa 变量,但是 b 变量会被解释为容量为 2,b[0] = 4b[1] = 0。这种情况下,就只能选择括号初始化了,当然这也是我为数不多会使用括号初始化的情景。

匿名变量

C++ 支持匿名变量的写法,匿名变量则有一些其他好处:

cpp
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));
}

对于这行代码:

cpp
int x = 4;

刚才说到,4 字面量的类型就是 int,所以 4 在某种程度上可以理解为 int{ 4 }

cpp
i64 b = 10;

对于这行代码,属于拷贝初始化的范畴,我们可以按步骤理解这个语句:

  • 先将 10,也就是 int{ 10 },隐式类型转换到 i64{ 10 }
  • 将匿名变量 i64{ 10 } 拷贝给 b 变量完成初始化。

匿名变量还有个好处则是在函数传参的时候,可以直接初始化一个匿名变量,然后完成传参。

你应该会频繁的在算法竞赛中看到如下代码:

cpp
std::vector<std::pair<int, int>> a;
a.push_back({ 1, 2 });

TIP

对于这种情况下,你应该用 a.emplace_back(1, 2)(此处原本的占位链接已移除)

对于 Tstd::pair<int, int>vectorpush_back 函数,其要求传入一个 std::pair<int, int> 变量,那么我们写了一个 { 1, 2 },行为则是:

  • { 1, 2 } 花括号类转变为 std::pair<int, int>{ 1, 2 }
  • 将该匿名变量拷贝给函数形参,完成函数形参的拷贝传递

INFO

你可能注意到我说花括号类,实际本质上 C++ 的花括号是一个类类型,在本节最后会给出扩展的讲解。

匿名变量的生命周期仅限那一条语句,也就是说,从那条语句开始执行起,在栈上开辟那个变量的空间,当语句执行结束后立即回收。

函数式类型转换

cpp
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() 是有区别的。

默认初始化

cpp
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 而言,大多数下默认初始化等价于值初始化,而对于自己定义的结构体类型,默认初始化的行为取决于自己的设计。

总结

在了解了三个初始化方式,以及匿名变量后,我建议如下初始化:

  1. 对于字面类型诸如 intdouble,直接用最简单的拷贝初始化方式
  2. 对于必须用括号初始化的地方,比如 std::vector a(10, 0),用括号初始化
  3. 其余选用花括号初始化
cpp
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

另外提醒一下,只有括号初始化和花括号初始化支持匿名变量初始化,拷贝初始化是不行的。

花括号列表的本质 (待补充)