再谈初始化
以前整理过一篇笔记 《初始化》,前两天又翻出来浏览了一下,写得很烂。限于当时的水平,并未像现在这样透彻的理解“默认构造函数”、“拷贝构造函数”、“拷贝赋值运算符”等概念,读书《C++ Primer》时看到好多种“初始化”,整理出那一篇可能只是想罗列名字、定义,让自己知道哪个是哪个罢了,却并不深刻理解中间的关联与区别。我们从头再来。
讲 C++ 的初始化,我们有好几种切入的方式:
- 可以从日常使用的形式,从我们惯用的代码讲起,引出相关的术语;
- 可以直接讲构造函数,构造函数是本质,初始化是呈现,理解了构造函数,也就理解了初始化;
- 可以罗列各种初始化的表现形式,然后归纳,总结出结论。
前一篇笔记带有第三种切入方式的味道,罗列了很多“XX初始化”,却没有归纳总结,意义不大。这一篇笔记我想以前两种方式来写。
内置类型
内置类型和用户自定义类型有些区别,比如内置类型很可能(原谅我,我并不百分百确定)没有构造函数,而对于类类型即便我们不定义构造函数,编译器一般也会默认给我们生成。总的来说,内置类型使用上比类类型简单,但同时也是类类型的基础。对于内置类型,比如 int
,哪怕是新手也是很熟悉的。我们都会对 int
变量进行什么操作呢?我们会怎么写代码呢?
1 | int var1; |
这应该是最普遍的代码书写形式了。我们定义了一个 int
变量,没有明确地给出其初始值,在稍后使用时给它赋值。像 T t;
这种形式我们称为“默认初始化”,或者“缺省初始化”。都是从“default initialization” 翻译过来的。
This is the initialization performed when a variable is constructed with no initializer. 引用来源-default initialization
有些人烦恼上述代码执行效果的“不确定性”(后文详述),更喜欢在定义变量时明确地给出一个初始值。
1 | int var2 = 12; |
像 T t = x;
这种形式我们称为“拷贝初始化”,即术语“copy initialization”。
Initializes an object from another object 引用来源-copy initialization
我们使用内置类型书写的形式一般就是上述两种,有时候我们写在函数里面,有时候写在函数外面(全局的)。至此,我们了解了两个术语:default initialization 和 copy initialization。
tip1:有时候潜意识会让我们写出这样的代码 int var();
,类类型的话更形象 MyClass obj();
,我们本意是想默认初始化一个变量 var 或 obj,但编译器的规则更倾向于将这行代码解析成函数声明。
tip2:T t = x;
的形式有时会让菜鸟误以为是赋值操作。拷贝初始化还有一种不具迷惑性的书写形式:T t(x);
tip3:内置类型的初始化和赋值成本一样。
默认值是什么
如果定义变量时没有指定初值,则变量被默认初始化,此时变量被赋予了“默认值”。默认值到底是什么由变量类型决定,如果是内置类型,定义变量的位置也会对此有影响。内置类型的变量未被显式初始化时:
- 定义于任何函数体之外的变量被初始化为
0
; - 定义在函数体内部的内置类型变量将不被初始化。
1 |
|
执行结果如下:
1 | vimer@debian8light:~/see-the-world/code/initializer_learn$ ./a.out |
这个现象源于,最初 C 语言诞生时机器硬件性能有限,对于节省资源做到了令人发指的地步。定义变量默认只分配内存块,但不对这块内存进行擦除(复位)操作,这种复位操作一般来说都是多余的。假设根据默认初始值 0 进行擦除操作,除非作者后续就是使用初值 0,否则赋值还会再次擦除内存块赋予别的值,初始化时的擦除是没有意义的。
C++ 最初由 C 扩展而来,为了向其兼容保留了这些特征。在性能不再是瓶颈的今天,反而由于程序员粗心使用了无初值且未赋值的变量而产生很多错误。
在堆上创建变量
除了在栈上定义变量,我们还会在堆上动态分配内存。这种情况下怎么初始化呢?
default initialization
int i;
可以对应int *p = new int;
- 还存在一种形式:
int *p = new int();
,虽然我们不能int i();
来定义变量。这个其实是值初始化,后文会提到。
copy initialization
int i = 12;
无法严格对应,但好在拷贝初始化还有更正统的写法int i(12);
可以对应int *p = new int(12);
构造函数
要讲构造函数,我们就得进入类类型(class type),我们无法使用内置类型来讲解构造函数。
从类类型的角度来说,所有的初始化都是匹配参数,找到与之对应的构造函数并执行。参考 std::string
类的构造
1 | // default (1) |
与之对应的构造实例:
1 | // string constructor |
我们看到这么多构造函数每一个都有其特点。但其中 1、2、9是最具通用性的,是所有类的共性。因为其特殊性,所以也就值得拥有名字。
- 1 默认构造函数,在默认初始化时执行;
- 2 拷贝构造函数,在拷贝初始化时执行;
- 9 移动拷贝构造函数,
- 其余的我们可以统称为“直接初始化”
Initializes an object from explicit set of constructor arguments. 引用来源-direct initialization
接下来“斤斤计较”一些,前文提到所有的初始化都是调用相应的构造函数而已(参数匹配),函数调用的形式就是使用括号 ()
,但除此之外,C++ 也提供了两种其他的形式调用构造函数:
- 使用等号
=
- 使用花括号
{}
恶心的是是这三种书写形式都不具有“全局适用性”——我自己造的词,三种书写形式都有广泛适用性,但又各有不足,不能调用 XXX 形式的构造函数——即便是使用 ()
来调用构造函数也有其不能用的情况。接下里我们详细叙述:
使用括号调用
按道理来说,既然所有的函数调用都是使用 ()
来完成,那么调用构造函数应该也没啥问题才对,囧。先说结论—— ()
调用不能适用于默认构造函数。问题出现 C++ 的语法解析上,其优先解释成函数声明。如果我们这么写 MyClass obj();
表示调用 MyClass 的默认构造函数来初始化 obj 对象,那么我们怎么声明“返回 MyClass 类型的空参数函数”呢?或许可以这么写 MyClass obj(void);
,我不确定是否是一种解决方案,即便可行,那么以前写的代码怎么办呢?解决方式就是我们这么多年一直写的 MyClass obj;
,这样就调用默认构造函数了,囧。
有一种略显丑陋的写法,似乎是满足了使用圆括号调用默认构造函数 MyClass obj = MyClass();
。
ps:它是“使用默认构造函数初始得到一临时变量,然后又使用这个临时变量调用拷贝构造函数初始化的 obj 对象”吗?no,使用 g++ 验证,只执行了默认构造函数,未执行拷贝构造函数,猜测是直接使用默认构造函数构建了 obj 对象。
使用等号调用
前文说 =
用来调用拷贝构造函数,严格来说并不准确。在 C++11 标准之前,=
可以用来调用所有【只有单独一个参数的】构造函数:
- 一个参数,其余参数都有默认值的可以吗? ok
- 怎么定义“copy initialization”这个概念,狭义的 or 广义的? 我个人更倾向于限制性更强的定义,从相同类型的变量拷贝
- 隐式类型转换);
在 C++11 之后,=
更是可以和 {}
组合用来调用所有的构造函数,但此种情形其实只是 {}
调用的变形。所以,如果使用 =
(且不使用 {}
)就不能调用多参数(两个及其以上? 两个及其以上无默认值)的构造函数。
1 | class Student |
《C++ Primer》第 263 页“隐式的类类型转化”中明确提到:只允许一步类类型转化。
使用花括号调用
在 C++11 之前,容器类无法使用一个序列初始化。直到在 C++11 中新添了 initializer_list
标准库类型,其形式 {a,b,c};
,以此 Container x = {a,b,c};
。在此基础上,扩展 {}
的使用场景(依据?),不仅仅可以用在所有使用 ()
的场景,还可以用来默认初始化 MyClass obj{};
,用来调用构造函数创建临时变量:
1 | string str{}; |
这种使用 {}
来初始化对象的形式被称为“列表初始化”,可以表现默认初始化、拷贝初始化、STL 容器的初始化等等。
Initializes an object from braced-init-list 引用来源-list initialization
美中不足
我原本以为 {}
是“全局适用”的,是可以用来调用所有构造函数的语法特性,囧,理论上应该是。问题是它有时候不会调用我们预期的构造函数,因为 {}
优先解析成 initializer_list
。举例来说:
1 | // 42 is the ASCII code for '*' |
语法解析时更倾向于 {'*', 'x'}
的序列,而非等价于 (42, 'x')
,使用 {}
应该怎么调用 string(size_t n, char c);
构造函数呢? no idea。ps string str('*', 'x')
是 42 个 ‘x’ 的字符串。
A
std::initializer_list
object is automatically constructed when:
- a braced-init-list is used in list-initialization, including function-call list initialization and assignment expressions
- a braced-init-list is bound to auto, including in a ranged for loop
推荐 《Effective Modern C++》 Item 7: Distinguish between ()
and {}
when creating objects.
备忘:实际测试代码,见 Debian8Light 虚拟机 /home/vimer/see-the-world/code/initializer_learn/MyString 目录。后续修改时将 Demo 嵌到合适的位置。
new 调用构造函数
显然 new
操作不能通过 =
来匹配构造函数,它可以使用 ()
和 {}
,所受的限制和之前一致,唯一的不同之处是此时可以使用 new MyClass();
来调用默认构造函数。
其它
拷贝构造函数和拷贝赋值运算符
=
可以用来初始化,可以用来赋值。不同情况下调用的函数不同。强调一遍又一遍,有=
不一定是赋值操作。std::initializer_list
和 member initializer list前者是标准库类型,后者是在构造函数中初始化成员变量的。
关于
std::initializer_list
类型,推荐一篇帖子:C++11中新特性之:initializer_list详解Is it better to use C void arguments “void foo(void)” or not “void foo()”?
值初始化
值初始化到底是什么?对此一直都有困惑,和 new Type;
new Type();
两者之间的区别有关系吗?和容器类初始化每个元素的“初始化器”有关系吗?
This is the initialization performed when a variable is constructed with an empty initializer. 引用来源-value initialization
我们来和“默认初始化”(或者叫做“缺省初始化”)的定义对比一下:
This is the initialization performed when a variable is constructed with no initializer.
我们前面提到,以下形式都会调用默认构造函数:
1 | MyClass obj; // (1) |
这要学究式地较真起来,1 和 4 是“no initializer”,所以是缺省初始化;其它 4 个是“empty initializer”,所以是值初始化。
这两者在初始化类类型的时候没有区别,都是调用默认构造函数。差别发生在初始化内置类型的时候,默认初始化是“向前看齐”(C 语言)妥协的结果,前面已经介绍过“默认值到底是什么”;值初始化是新时代的新态度,总是会有初值的。在 《POD 类型》 中有一个表格,执行 new
的结果表示了同样的结果。其参考来源于维基百科。
关于值初始化的更详细结果请移步 The effects of value initialization 一节:
大括号的“意外”
In all cases, if the empty pair of braces {} is used and T is an aggregate type, aggregate-initialization is performed instead of value-initialization.
If T is a class type that has no default constructor but has a constructor taking
std::initializer_list
, list-initialization is performed.数组类型
if T is an array type, each element of the array is value-initialized;
何为 aggregate-initialization 呢?
Initializes an aggregate from braced-init-list 引用来源-aggregate initialization
Aggregate initialization is a form of list-initialization, which initializes aggregates
总结
C++ 中的初始化,经历了由少变多,由简入繁,让人不胜其烦;但从 C++11 开始,踏上了由繁入简的尝试。
有哪些 xxx-initialization 呢?除了上面提到的,其实还有一些概念……
- default initialization
- copy initialization
- direct initialization
- aggregate initialization
- list initialization
- Dividing line
- zero initialization
看完以上整理的内容,可以再看几篇问答: