2.构造,析构,赋值运算--条款09-12

Posted Super_J

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2.构造,析构,赋值运算--条款09-12相关的知识,希望对你有一定的参考价值。

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

为什么?

作者用了一段简单的买卖订单代码来辅助解释:

//交易的base class
class Transaction
{
public:
    Transaction();  
    virtual void logTransaction() const = 0;    //用来写日志的日志记录函数
}

Transaction::Transaction()
{
    ... // 诸如初始化等操作
    logTransaction();   // 写日志
}

// 买入的类,继承自基类
class BuyTransaction
{
public:
    ...
    virtual void logTransaction() const;
}
// 卖出的类,继承自基类
class SellTransaction
{
    public:
    ...
    virtual void logTransaction() const;
}

有了以上代码,接着考虑执行以下代码段:

BuyTransaction b;

声明一个变量b,按照继承体系的规则,我们要先执行基类Transaction的构造函数,基类的构造函数中调用了虚函数logTransaction,所以这个时候调用的事基类中的logTransaction,并不是BuyTransaction的logTransaction函数!就算b这个变量是一个BuyTransaction类型的,它也不会执行自己的logTransaction函数。

我们通过以下3个方面来解释

(1) 基类的构造期间virtual函数是绝不会下沉到derived class层的。所以在构造函数中调用虚函数在此时并不能达到我们需要的结果。

(2) (解释为何不能下沉)当基类的构造函数在执行的时候,派生类的成员变量尚未初始化,如果此时下沉到了派生类之中,去执行了派生类的virtual函数,virtual函数中非常有可能用到这些未初始化的成员变量,那这将是通往不明确行为和彻夜调试大会的门票。

(3) 根本原因:在派生类对象的base class构造期间,此对象的类型是一个base class而不是derived class.不只是virtual函数会被编译器解析成基类的virtual函数,若使用运行期类型信息(如dynamic_cast何typeid),也会把对象视为base class类型。所以一开始初始化的是derived class中的base class成分。

同样的,析构函数也是如此。 一旦派生类对象进入了析构函数开始执行,对象内的派生类的成员变量就呈现了未定义的值,如果这时候调用了virtual函数,就会使用这个未定义的值,这也会导致不明确的行为和通往彻夜调试大会的门票。

作者总结

在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数那层)。


条款10: 令operator=返回一个reference to *this

这只是一个协议,并不强制性要求,但是习惯上都这么做。
因为返回一个reference to * this 可以实现连锁赋值。

int x,y,z;
x=y=z=10;

就像上述的简单代码一样。

所以我们写operator=的时候,最最最最好都要返回reference to *this.

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

### 作者总结

令赋值操作符返回一个reference to *this.

条款11:在operator=中处理“自我赋值”

为什么要处理?

1.1 先看一下一个不安全的operator=函数:

存在一个位图类和Widget类:

class BitMap
{
    ...
}
class Widget
{
    ...
private:
    BitMap *pb;
}
Widget& Widget::operator=(Widget& rhs)
{
    delete pb;
    pb = new BitMap(*rhs.pb);
    return *this;
}

乍一看好像没有错误,现在考虑“自我赋值”的问题:

假设rhs和 * this是同一个对象的时候。我们在operator=中第一步就删除了pb,那么rhs对象的pb就也被我们删除了,那么就根本无法new出来一个pb给this。

1.2 现在看一个经过“证同测试”的operator函数:

Widget& Widget::operator=(Widget& rhs)
{
    if(&rhs == this)
        return *this;
    delete pb;
    pb = new BitMap(*rhs.pb);
    return *this;
}

这个是可以用的。但还是存在一些风险:当new抛出了异常的时候,那么pb已经被删除了,返回的将是一个指向被删除位置的指针。

1.3 在复制pb所指的东西之前不要删除pb即可。

Widget& Widget::operator=(Widget& rhs)
{
    BitMap *pOrig = pb; //记录原来的pb
    pb = new BitMap(*rhs.pb);
    delete pOrig;
    return *this;
}

相比于1.2的代码来看:

(1) 记录了原来的pb指向的数据。这样待会删除pOrig指针就可以达到删除pb的效果。

(2) 使用rhs的数据new一块新内存出来。

  • new失败:我们也没有把原来的数据删除。此次操作不会影响任何东西。
  • new成功:就分配了一个新内存来保存数据,在“自我赋值”的情况下,就是在新的地址里面又保存了一分副本。待会删除原来的地址即可。

(3) 删除原来this->pb的内存。这样在“自我赋值”的情况下也不会出现删除掉之后返回已被删除的指针了。因为这是两块不同的内存,不会相互影响。

tips: 这里虽然可以达到“自我赋值”的作用,但是其实也可以在代码最前面加上:

if(&rhs == this)
    return *this;

这样做的效率反而会更高,但其实没有频繁用到的话也是没什么差别的。

作者总结

确保当对象自我赋值时operator=有良好的行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、记忆copy-and-swap。

确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象的时,其行为仍然正确。

条款12:复制对象时勿忘其每一个成分

假设一开始你有个Customer类:

void logCall(const string &funcName)
class Customer
{
public:
    Customer(const Customer& rhs);
    Customer &operator=(const Customer& rhs);
    ...
private:
    string name;
}

// 构造函数的实现
Customer::Customer(const Customer& rhs)
:name(rhs.name)
{
}
// copy assignment函数实现
Customer& Customer::operator=(const Customer& rhs)
{
    this->name = rhs.name;
    return *this;
}

现在看起来是正确的,但是一旦加入了一个新的成员,我们切记一定要去operator=函数中将新的成员变量也拷贝进去。

现在我们用一个PriorityCustomer类继承Customer类:

class PriorityCustomer : public Customer
{
public:
    PriorityCustomer(const PriorityCustomer &rhs);
    PriorityCustomer& operator=(const PriorityCustomer &rhs);
    ...
private:
    int Priority;
}

这时候我们实现operator=的时候,不仅仅需要拷贝当前类的成分,还需要拷贝在基类所继承下来的成分,才是完整的。

// copy 构造函数
PriorityCustomer::PriorityCustomer(const PriorityCustomer &rhs)
: Customer(rhs),Priority(rhs.Priority)
{
    
}

PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
    Customer::operator=(rhs);
    Priority = rhs.Priority;
    return *this;
}

从上面的代码可以看到,我们必须拷贝对象的每一个成分,包括它的基类。每一份都不要忘记。

所以,编写一个copying函数,确保:

(1) 复制所有的local成员变量。

(2) 调用所有base class内的适当的copying函数。

作者总结

Copying函数应该确保复制“对象内的所有成员变量”及“所有的base class成分。”

不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。

以上是关于2.构造,析构,赋值运算--条款09-12的主要内容,如果未能解决你的问题,请参考以下文章

构造析构赋值运算:条款5-条款12

构造/析构/赋值运算

C++类与对象(详解构造函数,析构函数,拷贝构造函数,赋值重载函数)

链表:如何实现析构函数、复制构造函数和复制赋值运算符?

effective c++学习笔记

构造/析构/赋值运算