📘【EffectiveC++】Chapter2-构造/析构/赋值运算
5. 了解C++默默编写了那些函数
核心原因
编译器会帮你生成函数
原因解释
当你只是声明了一个类,相当于你声明了:
- default构造函数
- copy构造函数
- 析构函数
- 编译器生成的析构是一个non-virtual,请注意[[#为多态基类声明virtual析构函数]]
- copy运算符
- 编译器单纯的把源对象的每一个non-static成员赋值给目标对象
- 移动构造函数
- 移动赋值运算符
注意:
如果你手动声明了任何一个函数,编译器便不会为你再自动生成。 只有当你没有声明,且这些函数被调用时,编译器才会为你生成。 所有编译器产出的函数都是public。
6. 如果你不想使用编译器为你声明的函数,请明确拒绝
核心原因
条款5
原因解释
编译器产出的函数都是public,可能会导致你声明了一个类,但你并不希望它可以被copy,所以你没有定义copy函数,而编译器会帮你做。 这显然不是我们希望的。
最佳实践
C11之后,我们只需要,在函数声明之后使用
Widget(const Widget &) = delete;
C98以前的做法是:
1
- 主动定义它(而不是让编译器来干)
- 让它为private。(所以不会被外部调用)
- 不实现它。(所以member调用到它也不会被执行)
故意不实现的函数,它是可以没有参数名的。因为没有必要。 这种不实现的方法在C++ iostream库中被广泛使用。
2
这里还有另外一种方法,即定义一个类,它的copy函数和copy操作符是private且未实现的。让你的类继承它。总结是:继承一个Uncopyable类来拒绝编译器自动生成。 其实和上述的方法是一个思路。
7. 为多态基类声明virtual析构函数
核心原因
- 多态基类的析构函数non-virtual时,通过基类指针删除派生对象时,析构函数会调用基类的。
- 虚函数通过vptr实现,这会增加一个指针结构的空间。
- 纯虚析构函数可强制基类为抽象类。
原因解释
1
如果你有一个non-virtual的析构函数的基类,当你使用一个base类指针指向了derived类实例,并用这个base类指针去销毁实例时,这个行为是未定义的。 这可能会导致可怕的事情。 很有可能的情况是derived自己的那一步没被销毁,而base类的那部分被销毁了。 这会产生一个局部销毁的诡异情况。
2
声明一个virtual会导致这个类增加一个指针空间。这个指针是所谓的vptr。 这可能会导致移植性问题。
3
如果你希望有一个抽象类,但又没有任何合适的函数可以声明为纯虚,那么你可以把析构函数声明为[[纯虚函数]]。
注意如果你这样做了,那么你应该在类定义之外的地方给这个纯虚析构函数一个实现。
语法上纯虚函数可以有实现,但必须在类定义之外。
因为析构函数被调用时,最深层派生的那个类的析构函数先被调用,然后再是基类。在派生类的析构函数中会去调用基类的析构函数,如果你不实现它,连接器会抱怨。
最佳实践
1
多态基类必须有一个virtual析构函数
2
不准备被用做多态基类的类不要声明virtual析构函数
3
抽象类可以选择析构函数为pure virtual
8. 不要让异常逃离析构函数
核心原因
C++运行时无法同时处理两个活跃异常。(中止或导致不明确的行为)
原因解释
如果你的析构函数中会吐出异常。 当你声明了一个这个类的vector对象。当对象销毁时,它应该销毁vector内的所有对象。那如果有两个对象的析构函数都出现异常,就会使程序变得不明确或中止。
这里有两个办法,如果析构函数可能产生异常,那么用try catch捕捉异常后:
- catch后直接中止。
- catch后吞下异常。
总结
“不让异常逃离”的目标的是让析构函数成为异常的终点站,内部消化处理,而不是传播出来。
9. 不要在析构或构造中调用virtual函数
核心原因
- 当base类构造执行完成前,对象是一个base类对象
- 当base类析构开始执行时,对象是一个base类对象。
原因解释
因为构造函数的调用规则是,先调用base的构造,再调用derived的构造。 如果你的基类的构造函数中调用了一个virtual函数,那当它的继承类初始化时,它就会执行它属于base类的那个版本的函数,而不是像你预期的那样执行继承类的那部分。
这很好理解,因为在base class构造时,derived类的那一部分成员还没有初始化,而derived类的函数几乎肯定会调用那些local成员。这就相当于“要求使用对象内部尚未初始化的部分”。
还有一个更根本的原因,当base类构造时,对象是一个base类对象,而不是derived类。对象在执行完derived类构造之前不会成为一个derived对象。
类似的就像析构函数一样,对象在执行到base析构时,对象就成为一个base类对象。
10. 令operator= 返回一个this指针的引用
核心原因
支持连锁赋值操作(如a=b=c),保持与内置类型一致的行为约定。
原因解释
C++中内置类型(如int)的赋值操作返回左值引用,允许连续赋值。
若用户自定义类型的赋值操作符不返回引用,将导致:连锁赋值失败:(a=b)=c
会编译错误(违反条款17的最小惊讶原则)。
赋值操作里即使随意返回任何值,也不会对赋值操作产生影响。但是返回引用符合和内置类型操作一贯的行为风格。即“赋值表达式的返回值是赋值后的值”
11. 在operate= 中处理“自我赋值”
核心原因
自我赋值会导致多个指针指向同一个内存的情况。
原因解释
如果你把a赋值给了a,比如: a=a
。
这种情况会导致你可能有多个指针指向了同一片内存。
往往我们不会写a=a
, 但是有时这种自我赋值可能是隐晦的:a[i] = a[j]
。
如果这种情况发生,那如果你销毁了a[i]
的内存空间,你就把a[j]
变成了野指针。
所以最好是在operator=中处理掉这个“自我赋值”的问题。
最佳实践
- 可以比较地址来检测是否是自我赋值,如果是则不做任何事。
- 可以声明新的对象,然后使用swap,再返回this所指数据的引用别名。
12. 复制对象时确保赋值它每一个成分
核心原因
避免复制对象时遗漏成员或基类部分。
原因解释
如果你使用了自定义的赋值函数,而不是用编译器自带的。 若你在拷贝构造中编写了拷贝操作后,而基类后续又新增了一个成员。 按理说你应该再修改拷贝构造函数的。 可是如果你不改,编译器也不会报警。这就会导致你的新成员并不会被赋值。