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

(原文链接:https://abseil.io/tips/146 译者:clangpp@gmail.com)

每周贴士 #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的值为0

类似地,因为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::vb.v将被默认构造,不能被安全读取。

建议

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

延伸阅读

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值