天天看点

Google C++每周贴士 #146: 默认与值初始化每周贴士 #146: 默认与值初始化

(原文链接:https://abseil.io/tips/146 译者:[email protected])

每周贴士 #146: 默认与值初始化

  • 最初发布于:2018-04-19
  • 作者:Dominic Hamon
  • 更新于:2020-04-06
  • 短链接:abseil.io/tips/146

“通往成功的路总是在施工”——莉莉·汤普琳

长话短说(TL;DR)

为安全性和可读性起见,你应该假设标量对象(scalar objects)在被显式赋值之前都没有初始化为合理值。使用初始化器可以确保标量值被初始化成了安全的值。

介绍

对象被创建的时候,也许初始化了,也许没初始化。没初始化的对象不能被安全地读取,但要理解对象什么时候没被初始化可没那么容易。

第一件需要了解的事,是被构造的类型是标量(scalar)、聚合(aggregate)还是其他类型。标量 类型可以被想象成简单类型:整数或浮点数对象;指针;枚举;成员指针;

nullptr_t

。聚合 类型是数组或简单类(只有公有、非静态数据成员,没有自定义构造函数,没有基类,没有虚函数,没有默认成员初始化器的类)。

为判断一个实例是否被初始化为可安全读取的值,还有一个因素是,它有没有显式的 初始化器。也就是说,语句里的对象名后面,有没有跟着

()

{}

= {}

因为这些规则并不直观,所以确保对象被初始化的最简单规则是,提供一个初始化器。这被叫做 值初始化(value-initialization),区别于 默认初始化(default-initialization),后者是编译器为标量和聚合类型执行的操作。

自定义构造函数

如果类型的定义中有自定义构造函数,那它就不是聚合类型,初始化过程就非常简单了:值初始化和默认初始化都调用该构造函数:

struct Foo {
  Foo() : v() {}

  int v;
  std::string s;
};

int main() {
  Foo default_foo;
  Foo value_foo = {};
  ...
}
           

= {}

触发了

value_foo

的值初始化,调用了

Foo

的默认构造函数。然后,

v

就可以被安全地读取了,因为构造函数初始化列表以值初始化了它。事实上,因为

v

不是类类型,所以这是值初始化的一个特例,被称为 零值初始化(zero-initialization),

value_foo.v

的值为

类似地,因为

default_foo

是默认初始化的,它调用了同样的构造函数,所以

default_foo.v

也被零值初始化,可以被安全地读取。

注意

Foo::s

有自定义构造函数,所以它在两种情况下都是被值初始化的,可以被安全地读取。

自定义构造函数中未初始化的成员

struct Foo {
  Foo() {}

  int v;
};

int main() {
  Foo foo = {};
}
           

这种情况下,虽然

Foo

有自定义构造函数,但它没有初始化

v

。这时候,

v

又被默认初始化了,也就是说其值是不确定的,不能被安全地读取。

显式值初始化

一般情况下,为了读者考虑,最好将初始化器替换为显式地初始化为一个值,即使那个值是0。这被称作 直接初始化(direct-initialization),是值初始化的特殊形式。

struct Foo {
  Foo() : v(0) {}

  int v;
};
           

默认成员初始化

比为类定义构造函数更简单——且仍然绕过默认和值初始化的陷阱——的方案是,尽可能地在成员声明的时候初始化:

struct Foo {
  int v = 0;
};
           

这保证了无论

Foo

的实例如何构造,

v

都将被初始化为一个确定的值。

默认成员初始化 还充当了文档,尤其是对布尔类型或非零的初始值,说明了该成员安全的初始值。

专业建议:标量零值初始化

标量类型初始化后可以被安全读取的完整规则:

  • 类型后跟着显式地

    ()

    {}

    = {}

    的初始化器。
  • 构造中的类型的实例是数组中的一个元素,且后面跟着如前所述的初始化器。例如,

    new int[10]()

  • 构造中的类型的实例是一个类成员,该类的默认构造函数被禁用(译者注:有自定义构造函数),且外层对象(译者注:该类的实例)是以值初始化的。
  • 构造中的类型的实例是静态的(static)或线程局部的(thread-local)。
  • 构造中的类型的实例是一个类成员,该成员是聚合类型且有初始化器。

数组类型

人们很容易忘记给数组声明加一个显式的初始化器,但这会带来相当要命的初始化问题。

int main() {
  int foo[3];
  int bar[3] = {};
  ...
}
           

foo

的每个元素都是默认初始化的(译者注:对于

int

类型意味着没有初始化,随机值),然而

bar

的每个元素都会被零值初始化。

跑个题:辨别默认初始化的不同声明

热门小测:这两个风格不同的声明是否影响代码的行为?

struct Foo {
  Foo() = default;

  int v;
};

struct Bar {
  Bar();

  int v;
};

Bar::Bar() = default;

int main() {
  Foo f = {};
  Bar b = {};
  ...
}
           

很多开发者会合理地假设这可能会影响到生成代码的质量,但除此之外只是个风格偏好。你也许会猜到,因为我问了,所以这肯定不对。

原因要追溯到上面第一节说到的自定义构造函数。因为

Foo

的构造函数是默认声明的,所以它不是自定义构造函数。这意味着

Foo

是一个聚合类型,而

f.v

是零值初始化的。然而,

Bar

有自定义构造函数,虽然被编译器实现为默认构造函数。因为该构造函数没有显式地初始化

Bar::v

b.v

将被默认构造,不能被安全读取。

建议

  • 显式地指定标量类型的初始值,而不是依赖零值初始化。
  • 在显式地初始化或赋值给标量类型的实例之前,假设它们有不确定的值。
  • 如果一个成员有敏感的默认值,且这个类有多个构造函数,请使用默认成员初始化器来确保其不会被忘了初始化。注意构造函数中的成员初始化器会覆盖默认值。

延伸阅读

  • Tip #61: Default Member Initializers
  • Tip #88: Initialization:

    =

    ,

    ()

    , and

    {}

  • Tip #131: Special member functions and

    =default

  • C++参考——初始化

继续阅读