《Effective C++》笔记:Tips09-Tips12

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

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


1、若是在基类的构造函数中调用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;

程序报错ide

p_w_picpath013

从错误中就能够看出,Transaction的构造函数并无调用子类的virtual方法logTransaction(),而是调用自己的,可是自身的logTransaction()初纯虚函数,并无实现,因此程序保存。固然,咱们能够实现Transaction的纯虚函数logTransaction(),来进一步验证,虽然这么作没意义函数

p_w_picpath014

能够看出,确实是调用基类Transaction自己的构造函数。this

2、为何构造函数是调用自己的virtual方法。

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


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


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


1、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;
}


客户端代码:orm

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

p_w_picpath015

和预期的同样,赋值操做正常执行。xml

那咱们换成返回值,再来看看有何反应对象

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=中处理“自我赋值”

1、不安全的“自我赋值”

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的成员指针已经成为空悬指针。


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

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

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


3、拷贝交换技术(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:赋值对象时勿忘每个成分

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

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

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

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

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

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


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

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


3、自定义子类的拷贝构造函数(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;
}


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

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

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

相关文章
相关标签/搜索