• 二、构造,析构,赋值运算--条款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函数共同调用。

  • 相关阅读:
    .net core相关博客
    通过几个Hello World感受.NET Core全新的开发体验
    ASP.NET Core 介绍
    ASP.NET MVC4企业级实战目录
    ASP.NET MVC4入门到精通系列目录汇总
    HTTP详解
    壮哉大微软,.Net人的春天来了,你准备好了嘛!
    简析 .NET Core 构成体系
    .Net Core[译文]
    OI再见
  • 原文地址:https://www.cnblogs.com/love-jelly-pig/p/9627763.html
Copyright © 2020-2023  润新知