Skip to content

Latest commit

 

History

History
324 lines (173 loc) · 9.36 KB

initialization.rst

File metadata and controls

324 lines (173 loc) · 9.36 KB

初始化

C++ 的初始化方式之繁多,估计是所有编程语言之最。 我们先通过一个列表来感受一下:

  • 默认初始化
  • 值初始化
  • 直接初始化
  • 拷贝初始化
  • 零初始化
  • 聚合初始化
  • 引用初始化
  • 常量初始化
  • 数组初始化
  • 列表初始化

以至于仅仅是初始化,都被专门开发成了一门课程。

但这些看似繁杂的初始化方式,背后有没有一些简单的线索可循?

直接初始化

我们先来看看最为简单的 直接初始化

我们首先定义一个类 Foo :

下面列表中所包含的构造表达式均为 直接初始化

简单说,当初始化参数非空时(至少有一个参数),如果你

  1. 使用 圆括号 初始化(构造)一个对象,或者
  2. 圆括号花括号 来初始化一个 non-class 类型的数据时(基本类型,指针,枚举等,因而只可能是单参)时,

这就是直接初始化。

这种初始化方式,对于 non-class 类型被称作 直接初始化 很容易理解。而对于 class 类型, 直接初始化 的含义也很明确,就是直接匹配对应的构造函数。 伴随着匹配的过程:

  1. 参数允许窄向转换 ( narrowing );
  2. 允许隐式转换;

比如:

除此之外,还有几种表达式也属于 直接初始化

  1. static_cast<T>(value) ;
  2. 使用 圆括号 的类成员初始化列表;
  3. lambda 的捕获初始化列表

列表初始化

不难看出,除了 lambda 的场景,以及用 花括号 初始化 non-class 类型之外, 直接初始化 正是石器时代 ( C++ 11 之前) 的经典初始化方式。

到了摩登时代 ( 自 C++ 11 起), 引入了被称作 universal 的统一初始化方式:列表初始化 。 之所以被称作 universal ,是因为之前花括号只被用来初始化聚合和数组,现在可以用来初始化一切: 基本类型,枚举,指针,引用,类。

由于列表为空有非常特殊而明确的定义,我们在这里仅仅考虑列表非空的场景。

我们先看看如下表达式:

以及如下表达式:

这两组表达式都被称为 列表初始化 。唯一的差别是,后者使用了等号,看起来像赋值一样。前者被称为 列表直接初始化 ,后者则叫做 列表拷贝初始化

虽然后者名字里有 拷贝 二字,并不代表其背后真的会进行拷贝操作。仅仅是因为历史的原因,以及为了给出两个名字以区分两种方式。

但事实上,对于 class 的场景,两者都是直接匹配并调用类的构造函数,并无根本差异。

其中一点细微的差别是:如果匹配到的构造函数,或者类型转换的 operator T 被声明为 explicit ,一旦你使用等号,则必须明确的进行指明:

对于类来说,而列表初始化(使用 花括号 ),相对于直接初始化(使用 圆括号 ),其差异主要体现在两个方面:

  1. 如果类存在一个单一参数是 std::initializer_list<T> ,或者第一个参数是 std::initializer_list<T> ,但后续参数都有默认值, 使用 花括号 构造,总是会优先匹配初始化列表版本的构造函数。
  2. 花括号 不允许窄向转换。

值初始化

值初始化 ,简单来说,就是用户不给出任何参数,直接用 圆括号 或者 花括号 进行的初始化:

注意,这里面没有 Bar bar() 的初始化形式。由于这样的形式与函数声明无法区分,因而被明确定义为这是一个名为 bar ,返回值类型为 Bar 的函数声明。

而在石器时代,为了能够进行 值初始化 ,只能使用 Bar bar = Bar(); 的形式。而这种形式在当时的语意为:等号右侧实例化了一个临时变量,通过拷贝构造构造了等号左侧的 bar ,但当时编译器基本上都会将这个不必要的拷贝给优化掉。到了 C++ 17 ,这类表达式的拷贝语意被终结。更详细的细节请参照 value-object

值初始化 的最大好处是,无论你是一个对象,还是一个基本类型或指针,你总是可以得到初始化(这也是为何被称作值初始化):

  1. 如果一个类有 自定义默认构造函数 ,则其直接被调用;
  2. 如果一个类没有 自定义默认构造 ,但有一个系统自动生成的默认构造函数(或用户明确声明为 default 的默认构造函数),则系统会先将其对象内存完全清零(包括 padding ) ,随后,如果这个类的任何非静态成员有 非平凡默认构造 的话,在调用这些默认构造;
  3. 对于基本类型和指针,直接清零。

默认初始化

相对于程序员会直接给出 () 或者 {}值初始化 ,虽然都是无参数初始化, 默认初始化 什么括号也不给:

如果一个类有非平凡的默认构造函数,则会直接调用。否则什么都不做,让那么没有非平凡构造的成员的内存状态留在它们被分配时内存(无论是在堆中还是栈中)的状态。 比如:

或许有人会倡导不要使用 默认初始化 ,而是统统使用 值初始化 。这在很多情况下都是正确的,但却并非全无代价。对于可平凡构造的对象而言, 值初始化会导致整个对象清零,如果对象较大,而随后的过程,你肯定会对对象的内容一一赋值(做真正的初始化),那么清零的过程其实是一种不必要的浪费。这对于关注性能的项目,可能是一个 concern

拷贝初始化

拷贝初始化非常简单:

请注意,拷贝初始化并不意味着必然发生拷贝,随着历史的车轮滚滚向前,曾经以为属于拷贝语义的表达式,如今早已面目全非。

零初始化

零初始化,并非 C++ 的某种语法形式,而是伴随着其它语法形式的行为定义。比如:

这样的数据定义,最终必然会被放入 bss 数据段,从而在程序加载时,被 loader 全部清零。

再比如:

这事实上是 值初始化 的范畴,只不过其结果是清零。

Important

  • 无参数初始化有两种形式: 值初始化 (带有 (){} )和 默认初始化 (无 (){} )。前者会保证进行初始化(调用默认构造,或清零,或混合);后者只会调用默认构造(如果是平凡的,则什么都不做)。
  • 有参数初始化,可以通过 () 或者 {} 的方式进行,两者的差异在于后者更优先匹配初始化列表,以及窄向转换的约束。
  • 在不使用 () 或者 {} 的场景下,使用 = 进行的初始化,属于 拷贝初始化 。如果被初始化对象是一个 class 类型, copy构造move构造 会被调用;在使用 (){} 的场景下,在 C++ 17 之后,除了 explicit 的约束之外,和 直接初始化 没有任何语义上的差异。