effective-c++系列:对象模型杂谈(1)

编译器版本的默认构造/析构/赋值/拷贝构造函数

  • 当用户没有在类内声明上述三个函数时,如果程序中需要调用,编译器会为用户自动编写默认构造/析构/拷贝构造这三个重要的函数,即所谓的Big Three,和拷贝构造函数,并且这些函数都是inline的。在c++ 11后编译器新增了move构造和move赋值两个函数:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
        template<class T>
    class Handle {
    T* p;
    public:
    Handle(T* pp) : p{pp} {}
    // 用户定义构造函数: 没有隐式的拷贝和移动操作
    ~Handle() { delete p; }
    Handle(Handle&& h) :p{h.p}//移动拷贝
    { h.p=nullptr; };
    Handle& operator=(Handle&& h) //移动赋值
    { delete p; p=h.p; h.p=nullptr; }
    Handle(const Handle&) = delete; //禁用拷贝构造函数
    Handle& operator=(const Handle&) = delete;
    };
    ```
    - 一旦我们显式地指明( 声明, 定义, =default, 或者 =delete )了上述五个函数之中的任意一个,编译器将不会默认自动生成move操作。
    - 一旦我们显式地指明( 声明, 定义, =default, 或者 =delete )了上述五个函数之中的任意一个,编译器将默认自动生成所有的拷贝操作。但是,我们应该尽量避免这种情况的发生,不要依赖于编译器的默认动作。

    ### 使用默认版本函数会发生什么?
    - 编译器产生的析构函数是none-virtual的,除非class的base class自身声明有virtual构造函数。
    - 编译器产生的构造函数和拷贝构造函数只是单纯将non-static成员变量拷贝到目标对象,考虑一个对象内部如果存在指针,那么只是单纯地复制指针,对于指针所指的内存区域不进行拷贝,这样的浅拷贝技术可能在后续使用过程中酿成大祸。

    ### 如何避免使用编译器自动生成的函数?
    - 如果你不想让类支持拷贝构造或者拷贝赋值函数
    使用private关键字
    ```cpp
    class A{
    private:
    A(const A&);
    A& operator=(const A&);
    };
    继承一个不可拷贝赋值和拷贝构造的类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Uncopyable{
    protected:
    Uncopyable(){}
    ~Uncopyable(){}
    private:
    Uncopyable(const Uncopyable&);
    Uncopyable& operator=(const Uncopyable&);
    };
    class A:private Uncopyable{
    };
    c++11后可以使用delete关键字
    1
    2
    3
    4
    class A{
    A(const A&) =delete;
    A& operator=(const A&) =delete;
    };

基类析构函数可以是none-virtual的吗?

  • 当基类析构函数带有多态性质,也就是使用基类指针调用子类函数的时候,析构函数必须是virtual类型的。
    • 当需要多态的时候,一般情况下需要在堆中分配内存,这样在delete的时候就存在问题:delete的是一个父类指针,但是父类析构函数不是虚函数,那么这个子类对象中子类的部分就无法释放,这样就会存在局部销毁的情况,会导致严重的资源泄露。
  • 凡是一个类中带有virtual字样的函数,一般情况下析构函数都要是虚函数。
  • 当一个类不是父类的时候,析构函数尽量不要设置成虚函数。
    • 因为一个类只要有虚函数,就携带虚表指针,占用额外的内存,当类内数据量很小的时候,虚表指针就回造成很大比例的内存浪费。

析构函数可以抛出异常吗?

  • 一般情况下不要在析构函数中抛异常
    比如在析构函数中释放多个资源,但是在释放过程中出现异常,那么剩余资源就无法被释放,就会造成内存泄露。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class A{
    public:
    vector<int> data;
    ~A(){
    // throws...
    }
    };
    void dosomething(){
    vector<A> resource;
    //...
    //dtor
    }
  • 异常处理
    捕捉异常,结束程序
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class A{
    public:
    vector<int> data;
    ~A(){
    try{
    //do delete
    }catch(){
    std::abort();
    }
    }
    };
    捕捉异常,不执行任何操作
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class A{
    public:
    vector<int> data;
    ~A(){
    try{
    //do delete
    }catch(){
    //记录
    }
    }
    };