• Effective C++读书笔记~02 构造/析构/赋值运算


    条款05:了解C++默认编写并调用了哪些函数

    Know what functions C++ silently writes and calls.

    如果你没自己声明,编译器就会为你的class声明:一个copy构造函数、一个copy assignment操作符,一个析构函数。如果没有声明任何构造函数,编译器会为你声明一个default构造函数。

    例如,你写这样的class:

    class Empty {};
    

    编译器会为你添加一些必要的函数,就好像是这样:

    class Empty 
    {
    public:
        Empty() { ... } // default 构造函数
        Empty(const Empty& rhs) { ... } // copy构造函数
        ~Empty() { ... } // 析构函数, 是否是virtual见后
    
        Empty& operator=(const Empty& rhs) { ... } // copy assignment操作符
    };
    

    不过,只有这些函数真正被调用时,编译器才会创建出来(如果 从来没被调用,就不会创建)。比如,下面就是调用相应函数的时候:

    Empty e1; // 调用default 构造函数, 还会调用析构函数
    
    Empty e2(e1); // 调用copy 构造函数
    e2 = e1; // 调用copy assignment操作符
    

    对于default构造函数和析构函数

    只有当你没有声明任何构造函数的时候,编译器才会为你合成default构造函数。
    编译器合成的函数,像是调用base classes和non-static(对象)成员变量的构造函数和析构函数。

    对于析构函数

    编译器合成的析构函数是non-virtual的,除非该class的base class自身声明有virtual析构函数。

    对于copy构造函数和copy assignment操作符(=)

    编译器合成的版本,只是单纯的将来源对象的每个non-static成员变量拷贝到目标对象。
    对于合成的代码不合法,或无意义时,编译器会拒绝为class合成operator =。

    例如,下面的代码,

    template<class T>
    class NamedObject {
    public:
           NamedObject(string& name, const T& value) : nameValue(name),  objectValue(value) { }
           void show() { cout << nameValue << ", " << objectValue << endl; }
    private:
           string& nameValue;        // reference
           const T objectValue;      // const
    };
    
    string newDog("Persephone");
    string oldDog("Satch");
    NamedObject<int> p(newDog, 2);
    NamedObject<int> s(oldDog, 36);
    p.show();
    s.show();
    p = s;    // 错误:因为编译器拒绝为class合成operator =, 无法调用赋值运算符
    

    编译器不确定合成copy assignment操作符(operator =)是修改reference指向的内容,还是修改reference本身,会拒绝合成。
    另外,修改const 成员,也会导致状况,编译器亦会拒绝合成。

    当编译器拒绝合成copy assignment操作符时,程序员需要自行手动编写。
    一种可行方案:自定义operator =,对需要赋值的成员手动添加赋值,如果是const成员,可以去掉const属性,或者不对const属性赋值。

    template<class T>
    class NamedObject {
    public:
           NamedObject(string& name, const T& value) : nameValue(name),  objectValue(value) { }
           NamedObject& operator = (const NamedObject& obj) {
                  nameValue = obj.nameValue;
                  objectValue = obj.objectValue;
                  return *this;
           }
           void show() { cout << nameValue << ", " << objectValue << endl; }
    private:
           string& nameValue; // reference
           T objectValue; // 去掉const属性
    };
    

    小结

    编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符、析构函数,前提是能合成合法且有明确意义的函数。

    [======]

    条款06:若不想使用编译器自动生成的函数,就该明确拒绝

    Explicitly disallow the use of complier-generated functions you do not want.

    由于编译器会自动合成copy构造函数和copy assignment操作符,因此,如果不需要编译器自动生成的函数时,应明确拒绝。
    自然而然想到的方式是,在class中声明copy构造函数、copy assignment操作符为private:

    // 不安全做法: 将copy构造函数,copy assign. 操作符设为private, 阻止编译器生成函数
    class HomeForSale
    {
    public:
           HomeForSale() {}
    private:
           HomeForSale(const HomeForSale&); // 只是声明, 无需实现 -- 阻止编译器合成copy构造函数
           HomeForSale& operator = (HomeForSale&); // 只是声明, 无需实现-- 阻止编译器合成copy assignment操作符
    };
    
    HomeForSale h1;
    HomeForSale h2;
    HomeForSale h3(h1); // 企图拷贝h1用于构造h3 -- 不应通过编译
    h1 = h2; // 企图将 h2赋值给h1 -- 不应通过编译
    

    虽然声明private函数,可以阻止编译器自动生成对应的函数,但是这样并不安全,因为可以在member function和friend function中调用。有没有更安全的做法?
    答案是有的。可以设置一个基类,在基类中将copy构造函数、copy assignment操作符设为private,编译器为派生类合成函数时,会自动调用基类对应的函数,而基类函数为private,导致编译器无法合成。

    // 更安全的做法: 将基类的copy构造函数, copy assign.操作符设为private,阻止编译器为派生类生成函数
    class Uncopyable
    {
    protected:
           Uncopyable() { }
           ~Uncopyable() { }
    private:
           Uncopyable(const Uncopyable&);
           Uncopyable& operator = (const Uncopyable&);
    };
    class HomeForSale : private Uncopyable
    { // class不再声明copy构造函数、copy assign.操作符
    public:
           HomeForSale() {}
    };
    ...
    

    delete与default

    C++ 11以后,可以使用delete表示禁止编译器自动生成函数;default表示使用编译器自动生成的函数。

    class HomeForSale
    {
    public:
           HomeForSale() {}
           HomeForSale(const HomeForSale&) = delete; // delete关键字阻止编译器合成copy构造函数
           HomeForSale& operator = (HomeForSale&) = delete; // delete关键字阻止编译器合成copy assignment操作符
    };
    

    小结

    • 为阻止编译器自动合成函数,可以将相应成员函数声明为private并且不实现。
    • 使用uncopyable这样的base class是一种更安全的做法。
    • C++11以后可以用delete关键字,更简洁、安全。

    [======]

    条款07:为多态基类声明virtual析构函数

    Declare destructors virtual in polymorphic base classses.

    当一个基类指针指向派生类对象,在delete释放时,如果基类析构函数是非virtual的,那么会直接调用基类析构函数,造成灾难后果;只有当基类析构函数是virtual的,才会正确调用派生类的析构函数析构对象。

    i.e. 当一个类的析构函数不是virtual时,说明该类不希望被继承。

    运行期如何决定调用哪一个virtual函数?

    通常由vptr(virtual table pointer)指针指出:每个class包含一个vptr,而vptr指向一个由函数构成的数组,称为vtbl(virtual table,虚函数表);每个带有virtual函数的class,都会有一个相应的vtbl。当对象调用某一个virtual函数时,实际被调用的函数取决于对象的vptr所指的那个vtbl -- 编译器在其中寻找适当的函数指针。

    virtual函数的缺点

    为何不把所有函数声明为virtual函数,以避免调用错误?这是因为virtual也是有缺点的:
    1)会占用更多内存,在32bit系统中,至少会额外占用4byte vptr + 4byte typeinfo + 4byte 函数指针(1个虚函数);
    2)不再具有移植性,虚函数不再和C语言的函数具有相同的结构;

    C++禁止派生 -- final

    一个类不希望被继承时,最好的方式是使用保留字final(适用于C++11以上版本)。

    class A final // 禁止A被派生
    {
    public:
        int val;
    };
    
    class B : public A // 错误: A无法被派生
    {...}
    

    C++希望派生 -- 纯虚函数

    推荐将析构函数设为纯虚函数。纯虚函数无法实例化,只能通过被继承后,才能实例化派生类。

    class AWOV
    {
    public:
        virtual ~AWOV() = 0; // 声明pure virtual 析构函数
    };
    

    小结

    • 带多态性质的base class应该声明一个virtual析构函数。如果一个class带有任何virtual函数,那么它就应该拥有一个virtual析构函数;
    • class的设计目的如果不是作为base class使用,或者不是为了多态(被继承),就不该声明virtual析构函数。

    [======]

    条款08:别让异常逃离析构函数

    Prevent exceptions from leaving destructors.

    C++不禁止析构函数吐出异常,但不建议这么做。因为抛出异常会导致不明确行为:剩余资源是继续释放,还是不释放?如果继续释放,那么异常谁捕获,如何处理?如果不继续释放,那么内存就会发生泄漏。

    例如,DBConn类负责管理DBConnection对象,DBConnect对象负责建立数据库连接的建立和释放。

    // 负责数据库连接
    class DBConnection
    {
    public:
        ...
        static DBConnection create(); // 返回DBConnection对象
        void close(); // 关闭数据库连接, 失败则抛出异常
    };
    
    // 管理DBConnection对象
    class DBConn
    {
    public:
        ...
        ~DBConn()
        { // 析构函数中调用数据块连接
            db.close(); // 会抛出异常
        }
    private:
        DBConnection db;
    };
    
    DBConn dbc(DBConnection::create()); // 构建DBConn对象时, 就建立数据库连接
    ...
    

    如果~DBConn中db.close调用异常,就会允许离开这个析构函数,就会造成问题,因为抛出了难以处理的麻烦。
    两种办法避免这个问题:
    1)如果close抛出异常,就直接结束程序(通过调用abort):

    DBConn::~DBConn()
    { // 析构函数中调用数据块连接
        try { db.close(); }
        catch(...) {
            // 记录日志, 记下对close的调用失败
            abort();
        }
    }
    

    2)吞下调用close而发生的异常:

    DBConn::~DBConn()
    {
        try { db.close(); }
        catch(...) {
            // 记录日志, 记下对close的调用失败
        }
    }
    

    上面2个办法都不是很好,因为都无法对“导致close抛出异常”的情况作出任何反应。
    一个更好的办法:重新设计DBConn接口,让客户有机会对可能出现的问题作出反应。即把可能抛出异常的代码,转移到客户手上。

    class DBConn
    {
    public:
        ...
        void close()
        { // 供客户使用的新函数, 客户有机会在这里处理异常, 也可以不处理
            db.close();
            closed = true;
        }
        ~DBConn()
        { // 析构函数中调用数据块连接
            if (!closed) {
                try { db.close(); } // 如果客户没有关闭连接, 这里就关闭连接
                catch(...) {
                    // 记录日志, 记下对close的调用失败
                    ...
                }
            }
        }
    private:
        DBConnection db;
        bool closed;
    };
    

    小结

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

    [======]

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

    Never call virtual functions during construction or destruction.

    问题案例

    假设你有个class继承体系,用来model股市交易如买进、卖出的订单等。这样的交易一定要经过审计,所有每当创建一个交易对象,在审计日志(audit log)中需要创建一笔适当记录。
    如果按下面的做法:

    class Transaction // 所有交易的base class
    {
    public:
           Transaction();
           virtual void logTransaction() const = 0; // 日志记录(log entry),因类型不同而不同
           ...
    };
    Transaction::Transaction()
    {
           ...
           logTransaction(); // 日志记录这笔交易
    }
    class BuyTransaction : public Transaction // 派生类 买进
    {
    public:
           virtual void logTransaction() const; // log此类型交易
           ...
    };
    class SellTransaction : public Transaction
    {
    public:
           virtual void logTransaction() const; // log此类型交易
           ...
    };
    // case1: 构造BuyTransaction对象
    BuyTransaction b; // 错误:BuyTransaction构造函数首先调用基类Transaction构造函数,而基类构造函数调用了基类的纯虚函数
    

    当执行case1 构造BuyTransaction对象时,会发生什么?
    BuyTransaction构造函数被调用前,会先去调用基类Transaction的构造函数,而基类构造函数末尾会调用纯虚函数logTransaction。由于此时派生类BuyTransaction对象尚未构造完成,logTransaction只会调用基类的版本,而不会下降到派生类阶层
    问题在于,基类的logTransaction是纯虚函数,没有实现,无法被调用。因此会产生危险的结果,编译器不会让你这么做。
    当然,如果基类logTransaction函数只是普通虚函数(非纯虚函数),它就会被正常调用。但如果是这样,就完全没必要用virtual函数,用普通函数即可。

    还有一种常见情况,构造函数中虽然没有直接调用虚函数,但是通过其他函数间接调用了虚函数。

    class Transaction // 所有交易的base class
    {
    public:
           Transaction();
           virtual void logTransaction() const = 0; // 日志记录(log entry),因类型不同而不同
           ...
    private:
           init();
    };
    Transaction::Transaction()
    {
           init();
    }
    
    Transaction::init()
    {
           logTransaction(); // 日志记录这笔交易
    }
    

    这种情况本质上和上面情况一样,不过通常比较难以发现。那么要如何避免呢?
    一种可行的做法是在class Transaction内的virtual 函数logTransaction改为non-virtual,然后要求derived class构造函数传递必要信息给Transaction构造函数,这样Transaction构造函数就能安全地调用non-virtual函数了。

    小结

    在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class;

    [======]

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

    Have assignment operators return a reference to *this.

    为了遵循标准赋值协议:赋值操作符必须返回一个reference指向操作符的左侧实参。虽然不这么做,也可以通过编译。

    int x, y, z;
    x = y = z = 15; // 赋值连锁形式
    x = (y = (z = 15)); // (z = 15)的值是操作符左侧实参z, (y = (z = 15))的值是操作符y左侧实参y
    

    class如果重载了赋值操作符,也应该遵循这个协议。

    class Widget 
    {
    public:
        Widget& operator=(const Widget& rhs) // 返回类型是个reference,指向当前对象
        {
            ...
            return *this;        // 返回左侧对象
        }
    };
    

    [======]

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

    Handle assignment to self in operator=.

    什么是“自我赋值”?

    class Widget { ... };
    Widget w;
    ...
    w = w; // 自己赋值给自己
    

    如何避免“自我赋值”?

    传统的做法是使用证同测试(identity test):

    Widget& Widget::operator=(const Widget& rhs)
    {
        if (this == &rhs) return *this; // 证同测试
    
        delete pb;
        pb = new Bitmap(*rhs.pb);
        return *this;
    }
    

    上面代码虽然“自我赋值安全”,但存在不具备“异常安全”的问题。当new Bitmap异常时,新空间无法申请,而旧的pb却已经被删除,导致无法读取。
    解决办法:记住原来的pb,确认申请新空间有效后,再删除之。

    Widget& Widget::operator=(const Widget& rhs)
    {
        Bitmap* pOrig = pb; // 记住旧的pb
        pb = new Bitmap(*rhs.pb); // 另pb指向*pb的一个副本
        delete pOrig; // 删除旧的pb
        return *this;
    }
    

    上面代码同时具备“自我赋值安全”和“异常安全”。不过,当自我赋值时,效率可能会比较低。一种新的效率更高的方案是使用copy and swap技术。推荐做法。

    class Widget
    {
    public:
        ...
        void swap(Widget& rhs); // 交换*this和rhs的数据
        ...
    };
    Widget& Widget::operator=(const Widget& rhs)
    {
        Widget temp(rhs); // 为rhs数据制作一份副本
        swap(temp); // 将*this数据和副本temp的数据交换数据
        return *this;
    }
    

    变种:如果operator=传入参数是以值传递方式,可以无需再为rhs数据制作一份副本

    Widget& Widget::operator=(Widget rhs)
    {
    swap(rhs); // 将*this数据和副本rhs的数据交换数据
    return *this;
    }

    小结

    1)确保当对象自我赋值时,operator= 自我赋值安全、异常安全。用到的技术,包括“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
    2)确定任何函数如果操作同一个对象(包括自身)时,其行为仍然正确。

    [======]

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

    Copy all parts of an object.

    copying函数

    我们把copy构造函数,copy assignment操作符统称为copying函数。
    当手工编写copying函数出错时,如漏掉某些class的local成员,编译器不会报错,但派生类如果依赖该copying函数可能会造成严重后果。需要是否小心处理。

    当编写一个copying函数时,需要确保:
    1)复制所有local成员变量;
    2)调用所有base classes内适当的copying函数;

    copying函数如何避免代码重复?

    不要在copying函数之间相互调用,比如用copy构造函数来实现copy assignment操作符,因为这就像试图构造一个已经存在的对象。反过来,也没有意义,因为copy assignment操作符只能施加于已经初始化的对象上。

    要解决2者代码重复问题,可以设置第三个成员函数,通常是设置名为init的private函数,给两者调用。

    小结

    1)Copying 函数应该确保复制“对象内的所有成员变量”(即local变量)以及“所有base class成分”(调用base class的copying函数);
    2)不要尝试以一个copying函数实现另一个copying函数。应该将共同部分放到第三个函数中,并由2个copying函数共同调用;

    [======]

  • 相关阅读:
    21.算法实战---栈
    初见Gnuplot——时间序列的描述
    MATLAB连接MySQL数据库
    用python做些有意思的事——分析QQ聊天记录——私人订制
    遇见蒙特卡洛
    层次分析模型(AHP)及其MATLAB实现
    CPP,MATLAB实现牛顿插值
    CPP&MATLAB实现拉格朗日插值法
    python3爬虫再探之豆瓣影评数据抓取
    R——启程——豆瓣影评分析
  • 原文地址:https://www.cnblogs.com/fortunely/p/15560697.html
Copyright © 2020-2023  润新知