Tips09:绝不在构造和析构过程中调用virtual函数

PS:本人觉得,应该改成不要在基类的构造和析构中调用virtual函数


一、如果在基类的构造函数中调用virtual函数,调用谁的?
class Transaction
{
public:
       Transaction(void){
              logTransaction();
       }
       virtual ~Transaction(void){}
       virtual void logTransaction() const = 0;
};
                                                                           
class BuyTransaction :
       public Transaction{
public:
       BuyTransaction(void){}
       ~BuyTransaction(void){}
       void logTransaction() const{
              std::cout << "BuyTransaction::logTransaction()" << std::endl;
       }
};

客户端代码:

BuyTransaction b;

程序报错

p_w_picpath013

从错误中就可以看出,Transaction的构造函数并没有调用子类的virtual方法logTransaction(),而是调用本身的,但是自身的logTransaction()初纯虚函数,并没有实现,所以程序保存。当然,我们可以实现Transaction的纯虚函数logTransaction(),来进一步验证,虽然这么做没意义

p_w_picpath014

可以看出,确实是调用基类Transaction本身的构造函数。

二、为什么构造函数是调用本身的virtual方法。

根据多态的定义,当子类重载基类的virtual成员方法,应该调用子类的重载方法。但是这里有个前提,子类对象必须存在!如果连对象都不存在,那对象的成员变量也就不存在,如果成员方法要访问成员变量,访问什么?


本例中,是在基类的构造函数中调用virtual方法。构造子类对象的顺序是,从基类到子类,依次调用构造函数。实际上,在调用基类的构造函数时,压根就不知道子类对象的存在。这时的对象实际上就是基类对象,而virtual函数自然被编译器解析至基类的virtual函数。


Tips10:令operator返回一个reference to *this      


一、operator=返回引用和返回值的区别

先来看返回引用

class Widget{
public:
       Widget(void);
       Widget(int value);
       ~Widget(void);
       Widget& operator=(const Widget& rhs);
       void log();
private:
       int mVaule;
};
                                                                
Widget::Widget(void) : mVaule(0){}
                                                                
Widget::Widget( int value ) : mVaule(value){}
                                                                
Widget::~Widget(void){}
                                                                
void Widget::log(){
       cout << "Widget = " << mVaule << endl;
}
                                                                
Widget& Widget::operator=( const Widget& rhs ){
       mVaule = rhs.mVaule;
       return *this;
}


客户端代码:

Widget w1 = 1;
Widget w2 = 2;
Widget w3 = 3;
w1 = w2 = w3 = 123;
w1.log();
w2.log();
w3.log();

p_w_picpath015

和预期的一样,赋值操作正常执行。

那我们换成返回值,再来看看有何反应

Widget Widget::operator=( const Widget& rhs ){
       mVaule = rhs.mVaule;
       return *this;
}

p_w_picpath016

一样可以执行,那为什么要让operator=返回引用呢?

接下来,我们分别在拷贝构造函数拷贝赋值运算符以及析构函数加上打印语句

operator=返回引用

Widget::~Widget(void){
       cout << "deconstructor" <<endl;
}
                                                       
Widget::Widget( const Widget &rhs ) : mVaule(rhs.mVaule){
       cout << "copy constructor" << endl;
}
                                                       
Widget& Widget::operator=( const Widget& rhs ){
       cout << "Widget copy assignment " << endl;
       mVaule = rhs.mVaule;
       return *this;
}

p_w_picpath017


operator=返回值

Widget Widget::operator=( const Widget& rhs ){
       cout << "Widget copy assignment " << endl;
       mVaule = rhs.mVaule;
       return *this;
}

p_w_picpath018

对比运行结果,可以清楚的发现,operator=返回值的话,在连续赋值时,会多调用三次拷贝构造函数和三次析构函数

我们来分析在operator=返回值的情况下

w1 = w2 = w3 = 123;

发生了什么?

首先w3 = 123,这部分调用operator=,注意由于返回的值,在return *this,会调用拷贝构造函数,返回的是w3的拷贝.

然后w2 = w3,首先也是调用operator=,在return *this,返回的是w2的拷贝,再一次调用拷贝构造函数

最后w1 = w2,和上述的情况一样,返回的是w1的拷贝。

赋值操作结束后,w3,w2,w1的拷贝是临时对象,被销毁。所以,调用了三次析构函数。

所以,operator=返回引用,不仅是协议,还可以又可避免拷贝构造函数和析构函数的调用。


Tips11:在operator=中处理“自我赋值”

一、不安全的“自我赋值”
class SelfAssignment{
public:
       ...
       SelfAssignment& operator=(const SelfAssignment& rhs);
private:
       int *mValuePtr;
};
                                                
SelfAssignment& SelfAssignment::operator=( const SelfAssignment& rhs ){
       delete mValuePtr;
       mValuePtr = new int(*rhs.mValuePtr);
       return *this;
}

一旦客户端这样写

SelfAssignment s;
s = s;

s的成员指针已经成为空悬指针。


二、自我赋值安全和异常性安全

一个简单的方法,就可以避免一、中出现的问题,复制之前先检查是否是同一个对象

SelfAssignment& SelfAssignment::operator=( const SelfAssignment& rhs ){
       if(this == &rhs)
              return *this;
       delete mValuePtr;
       mValuePtr = new int(*rhs.mValuePtr);
       return *this;
}

但是,如果在创建对象时,即new的时候,抛出异常的话,s的成员指针依然是空悬指针。这里就需要异常性检查

SelfAssignment& SelfAssignment::operator=( const SelfAssignment& rhs ){
       int *pOrig = mValuePtr;
       mValuePtr = new int(*rhs.mValuePtr);
       delete pOrig;
       return *this;
}

实际上,就是在确保成员指针正常复制之前,先不要删除器指向的内存。一旦过程中出现异常,成员指针还可以指向原来的内存。此外,就算复制的对象是本身,也可以正常运行。

这是《C++ Primer》上的方法,个人觉得《Primer》上的方法更容易理解


SelfAssignment& SelfAssignment::operator=( const SelfAssignment& rhs ){
       int *newPtr = new int(*rhs.mValuePtr);
       delete mValuePtr;
       mValuePtr = newPtr;
       return *this;
}

Primer》提供的建议是,现将右侧的运算对象拷贝纸一个局部临时对象。当拷贝完成后(异常没有发生),销毁左侧运算对象的现有成员就安全了。一旦左侧运算对象被销毁,剩下的就是数据从临时对象拷贝到左侧对象的成员中。


三、拷贝交换技术(copy and swap)实现自赋值

这项技术用到了标准库的swap()函数

SelfAssignment& SelfAssignment::operator=( const SelfAssignment rhs ){
       std::swap(*this, rhs);
       return *this;
}

要注意的就是参数constSelfAssignment rhs是非引用参数,使用的是值传递。swap交换对象本身和参数的成员变量,操作完成之后,rhs的成员变量实际上就是原来对象的成员变量。离开拷贝赋值运算符后,rhs销毁,也就是对象原先的成员变量销毁。

这项技术解决了异常安全和自赋值安全。在传递参数的时候,就已经得到右值的副本。相当于

int*newPtr = new int(*rhs.mValuePtr);

只要参数传递的过程中没有异常,就解决了异常安全的问题。

同时swap使得对象和副本交换,就算rhs是对象本身,也是和自己的副本交换,销毁的也是副本,这也解决了自我赋值安全的问题。


Tips12:赋值对象时勿忘每一个成分

一、编译器默认的拷贝构造函数(copy)和拷贝赋值运算符(copy assignment)

默认生成的两个负责拷贝的函数,会赋值所有的non-static成员变量。

l内置类型和对象类型:值拷贝

如果是指针类型,只会拷贝指针,不会拷贝指针所指的内存。

如果是引用类型,sorry,必须自定义拷贝函数。

也就是说,编译器生成的拷贝函数是浅拷贝


二、自定义的拷贝构造函数(copy)和拷贝赋值运算符(copy assignment),一定要复制每一个non-static成员变量

因为,当自定义的拷贝函数忘记复制某一个成员变量,编译器不会发出任何警告。


三、自定义子类的拷贝构造函数(copy)和拷贝赋值运算符(copy assignment),一定要复制基类(base class)的成员变量

这一点,很容易遗漏。可能在自定义拷贝函数的过程中,只复制了子类本身的成员变量,而遗漏了父类的成员变量。

然而,父类的成员变量往往是private,子类不能直接访问,复制父类的成员变量的方法就是调用对应的父类方法。拷贝构造函数就调用父类的拷贝构造函数,拷贝赋值运算符就调用父类的拷贝赋值运算符。

Copy

DerivedClass::DerivedClass(const DerivedClass& rhs) : BasedClass(rhs),初始化成员变量{}

Copy assignment

DerivedClass& DerivedClass::operator=(const DerivedClass& rhs){
       BasedClass::operator=(rhs);
       复制成员变量
       return *this;
}


四、特殊的函数,不要相互调用

这里指的特殊函数,就是构造函数,析构函数,拷贝构造函数,拷贝赋值运算符

尤其是拷贝构造函数和拷贝赋值运算符,绝大多数时候,代码是一样的。正确的做法是将相同的代码提取出来,封装成普通的方法,然后让拷贝函数区调用。