每周技巧 #182:初始化你的整数!
本节阅读量:本文翻译自 Abseil 官网的 Tip of the Week #182: Initialize Your Ints!。
原文最初作为 TotW #182 发布于 2020 年 7 月 23 日。
更新于 2020 年 7 月 23 日。
快捷链接:abseil.io/tips/182
“在任何决策时刻,你能做的最好事情是正确的事,其次是错误的事,最糟糕的是无所作为。” – Theodore Roosevelt
C++ 太容易留下未初始化变量了。这很可怕,因为几乎任何对未初始化对象的访问都会导致未定义行为。在众多初始化形式中,默认初始化确实是“默认”的,当变量没有指定初始值时就会发生,但它并不总是意味着真正的初始化。
平凡类型的默认初始化
|
|
很多人会惊讶地发现,上面的代码片段触发了未定义行为。在第一条语句中,bool_one 被默认初始化,而这(讽刺的是)并不保证真的初始化变量。在这个例子中,尽管使用了“默认初始化”,bool_one 仍然未初始化。我们如何知道这一点?
为了理解这个现象,先澄清默认初始化何时会、何时不会这样表现。在 C++ 中,并非所有类型都暴露跳过初始化的能力。这里有两个主要类别值得强调。
- 对于有默认构造函数的类型,包括大多数
class类型,默认初始化在所有情况下都会调用默认构造函数。例如,std::string str;保证会初始化str,就像它被值初始化为std::string str{};一样。 - 对于没有构造函数的类型,比如
bool,默认初始化可能表现出两种行为。A)如果被初始化的变量是static或定义在命名空间作用域,会执行所谓的“值初始化”。B)但是,对于非static的块作用域变量,默认初始化对这些类型完全不执行初始化,让变量保持未初始化并具有不确定值。
因此,在上面的例子中,bool_one 未初始化,因为 bool 没有构造函数,且 bool_one 是非 static 的块作用域变量。当 bool_two 的初始化读取 bool_one 的值时,结果行为是未定义的。
C++ 中哪些类型缺少构造函数?
C++ 继承了 C 中的类型,它们被称为平凡默认可构造类型,口语中也叫“平凡”类型,其实现没有构造函数。这包括 int、double 这样的基础类型,也包括只包含平凡字段且没有逐成员初始化器的 struct 类型。它还包括所有原始指针类型,即便它们指向的是类,例如 MyClass*。
换句话说,由于 C 没有构造函数,这类类型在 C++ 中用于默认初始化时保留了这种行为。
为什么 C++ 允许未初始化对象?
少数情况下,保留某些对象未初始化的能力对性能有用,或者可用于提供确实没有初始值的占位符。由于大多数访问未初始化值的模式都是未定义的,sanitizers 也可以利用这些信息查找 bug。
可能平凡的类型的默认初始化
和前面的代码片段一样,下面的代码也使用了默认初始化:
|
|
读取 my_variable 的值安全吗?
要回答这个问题,我们必须了解更多 MyType 的实现。所展示的调用点没有足够信息判断读取 my_variable 是否安全。例如,如果 MyType 是一个简单 struct 类型,只包含 int 字段,没有构造函数,也没有逐成员初始化器,那么 my_variable 将未初始化。然而,如果 MyType 是一个 class 类型,并且有用户定义的 MyType::MyType() 实现,那么构造函数负责初始化变量,使得立刻读取它的值是安全操作。
建议:初始化平凡对象
在大多数代码中,你大概不想要未初始化对象。少数情况下,出于性能或编码语义,这样做可能合理。不过,除非处于这些例外场景之一,否则请优先初始化 struct 字段和变量中的平凡对象,如下面例子所示:
|
|
|
|
此外,在可行时,避免为平凡类型创建类型别名。我们希望 struct 和 class 类型在所有情况下都能安全初始化。由于基础类型(整数、指针等)在默认初始化时不保证初始化,给这些类型起一个看起来安全的名字,会让代码更难推理。
下面是平凡类型别名的例子:
|
|
更多信息