构造析构和赋值
了解C++默默编写并调用哪些函数
编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符、析构函数。
一个class定义,如果你自己没有声明,编译器会为它声明编译器版本的一个copy构造函数、一个copy assignment操作符、一个析构函数。如果你没有声明任何构造函数,编译器也会为你生成一个default构造函数。这些都是public且inline的。
//定义一个空类
class Empty{}
//就如同写如下代码
class Empty{
// public:
// Empty(){...} //default构造函数
// Empty(const Empty& rhs){...} //copy构造函数
// ~Empty(){...} //析构函数
// Empty& operator=(const Empty& rhs){...} //copy assignment操作符
}
唯有这些函数被调用,它们才会被编译器创建出来。
//以下代码会造成上述每一个函数被编译器生成
Empty e1;
Empty e2(e1);
e2 = e1;
编译器产出的析构函数是一个non-virtual,除非基类自身声明有virtual析构函数。
编译器创建的copy构造函数和copy assignment操作符只是单纯地将来源对象的每一个non-static成员变量拷贝到目标对象。
声明了一个构造函数后,编译器就不会再为它创建默认构造函数。
拒绝编译器自动生成的函数
为驳回编译器暗自提供的技能,可以将相应的成员函数声明为private并且不予实现。使用一个Uncopyable基类也是一种做法。
通常如果你不希望class支持某一特定机能,只要不声明对应函数就行了,但是这个策略对于copy构造函数和copy assignment操作符无效,因为编译器会自动生成。为了阻止这些函数被创建出来,你得自行声明它们,可以声明为private。
- 籍由明确声明一个成员函数,你阻止了编译器暗自创建其专属版本。
- 令这些函数为private,使你得以阻止人们调用它们。
class UncopyableObject
{
private:
UncopyableObject(const UncopyableObject&);
UncopyableObject& operator=(const UncopyableObject&);
}
//有了上述定义,当客户企图拷贝对象,编译器会阻止他,如果在友元或者成员之内那么做,轮到连接器发出抱怨。
为多态基类声明virtual析构函数
带多态性质的基类应该声明一个virtual析构函数,如果class带有任何virtual函数,他就应该拥有一个virtual析构函数。
类的设计目的如果不是作为基类使用,或不是为了具备多态性,就不该声明virtual析构函数。
当派生类对象经由一个基类指针被删除,而该基类带着一个non-virtaul析构函数。其结果未有定义–实际执行时通常发生的是对象的派生成分没被销毁。
一个“局部销毁”的对象,是形成资源泄露、破坏的数据结构。
别让异常逃离析构函数
析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或者结束程序。
如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。
GPT:不鼓励异常逃离析构函数的原因:
- 异常处理机制复杂化。
- 资源释放问题。
- 异常安全性。
GPT:解决办法:
- 使用try/catch语句捕获处理所有异常。
- 将可能抛出异常的操作移到析构函数外。
绝不在构造和析构过程中调用virtual函数
构造和析构期间不要调用virtual函数,因为这类调用从不下降至派生类。
在基类构造期间,virtual函数不是virtual函数。
基类构造函数执行更早于派生类构造函数,当基类构造函数执行时派生类成员变量尚未初始化。所以C++不让你走这条路。
GPT:其实C#也不推荐构造函数调用虚函数,应为对象不完整。
令operator=
返回一个reference
//赋值可以写成连锁形式
x = y = z = 999;
//赋值采用右结合律,所以上述连锁赋值被解析为
x = (y = (z = 15));
为了实现“连锁赋值”,赋值操作符必须返回一个reference指向操作符的左侧实参。
在operator=
中处理自我赋值
确保当对象自我赋值时
operator=
有良好行为。其中技术包括对象地址比较(证同测试)、精心周到的语句顺序、copy-and-swap。
确定任何函数如果操作一个以上对象,而其中多个对象是同一个对象时,其行为仍然正确。
复制对象时勿忘其每一个成分
复制函数应该确保复制“对象内的所有成员变量”以及“所有的基类成分”。
不要尝试以某个复制函数实现另一个复制函数。应该将共同机能放进第三个函数中,由两个复制函数共同调用。
如果你为class添加一个成员变量,你必须同时修改复制函数。(你也需要修改所有构造函数)如果你忘记,编译器不太可能提醒你。
任何时候只要你承担起“为派生类撰写copying函数”的重责大任,必须很小心地也复制其base class成分。那些成分往往是private的,所以你无法直接访问它们,你应该让派生类的copying函数调用相应的基类函数。
复制构造函数和operator=
操作符这两个copying函数往往有近似相同的实现本体。但是你用一个调用另一个来实现代码复用,这样是不合理的。如果要代码复用,你可以定义一个init
函数,然后两个copy函数都调用这个init
函数。
CRE:注意初始化语句调用的是复制构造函数而不是
operator=
操作符。
Person p2 = p1;//复制构造函数
Person p2; p2 = p1;//调用operator=操作符
(END)