zhouqijie

构造析构和赋值



了解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。

  1. 籍由明确声明一个成员函数,你阻止了编译器暗自创建其专属版本。
  2. 令这些函数为private,使你得以阻止人们调用它们。
class UncopyableObject
{
private:
    UncopyableObject(const UncopyableObject&);
    UncopyableObject& operator=(const UncopyableObject&);
}
//有了上述定义,当客户企图拷贝对象,编译器会阻止他,如果在友元或者成员之内那么做,轮到连接器发出抱怨。    



为多态基类声明virtual析构函数

带多态性质的基类应该声明一个virtual析构函数,如果class带有任何virtual函数,他就应该拥有一个virtual析构函数。
类的设计目的如果不是作为基类使用,或不是为了具备多态性,就不该声明virtual析构函数。

当派生类对象经由一个基类指针被删除,而该基类带着一个non-virtaul析构函数。其结果未有定义–实际执行时通常发生的是对象的派生成分没被销毁。

一个“局部销毁”的对象,是形成资源泄露、破坏的数据结构。



别让异常逃离析构函数

析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或者结束程序。
如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。

GPT:不鼓励异常逃离析构函数的原因:

  1. 异常处理机制复杂化。
  2. 资源释放问题。
  3. 异常安全性。

GPT:解决办法:

  1. 使用try/catch语句捕获处理所有异常。
  2. 将可能抛出异常的操作移到析构函数外。



绝不在构造和析构过程中调用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)