Three 为何物?

所谓的 Three 其实就是 copy constrcutor (拷贝构造函数)copy assignment operator (拷贝赋值操作符)destructor (析构函数) 。 在介绍 Rule-of-Three 之前,我们回顾一下 C++ Class 的声明与定义。

class Empty 
{

}; 

Empty 十分简单,我没有为它赋予任何 data members (数据成员) 或显式地声明/定义 member functions (成员函数) 。但事实上,编译器会在必要时为类 Empty 合成必要的成员函数。

int main()
{
    Empty e1 {}; // 创建一个Empty对象
}

由于我没有为类 Empty 声明构造函数函数,编译器自然会为我补充一个 default constructor (默认构造函数)

// C++ 概念代码
Empty::Empty() {}

当我们需要通过 e1 去构造更多的相同类型对象的时候,编译器又帮我们做了以下的事情。编译器会为类 Empty 添加拷贝构造函数 Empty::Empty(const Empty&) 和拷贝赋值操作符 Empty& operator=(const Empty& other)

事实上就类 Empty 的结构而言,编译器根本不需要生这两个函数。因为类 Empty 里根本没有其他的 class object (类对象) 数据成员。编译器只需要通过 bitwise copy (位逐次拷贝) 把内存逐一拷贝便能完成任务。不过我们可以假设编译器会自动生成所需要的函数。

// C++ 概念代码
class Empty 
{
public:
    Empty(int val) : _val(val) {} 
private:
    int _val { 0 };
}; 

Empty::Empty(const Empty& other)
{
    _val = other._val;
}

Empty& Empty::operator=(const Empty& other)
{
    _val = other._val;
    return *this;
}

Empty::~Empty()
{

}

// 执行代码
int main()
{
    Empty e1 {}; 
    Empty e2 { e1 };
    
    Empty e3;
    e3 = e2; 
}

为什么要给 Three 定规则?

我们已经对 Three 有了初步的了解,同时编译器可能会在背后做了很多小动作。所以我们不能完全依赖编译器的行为。

由于类 Empty 添加了新的数据成员,所以我定义了新的构造函数。同时为了避免内存泄漏,我也补上了析构函数 Empty::~Empty()

class Empty 
{
public:
    Empty(int val, char c) : _val(val), _cptr(new char(c)) {}
    ~Empty()
    {
        delete _cptr;
    }
    
private:
    int _val { 0 };
    char* _cptr { nullptr };
};

然后我尝试对类 Empty 的一些对象进行拷贝操作。此时编译器再次帮我添加两个成员函数。

// 概念代码
Empty(const Empty& other)
{
    _val = other._val;
    _cptr = other._cptr;
}

Empty& operator=(const Empty& other)
{
    _val = other._val;
    _cptr = other._cptr;
    return *this;
}

// 执行代码
int main()
{
    Empty e1 {"1", "empty"};
    Empty e2 { e1 }; 
    
    Empty e3;
    e3 = e2; 
}

编译器把 e1 的成员逐个拷贝给 e2 。不过遗憾的是,程序结束之前会崩溃。崩溃原因是 Empty::_cptr 被重复释放。因为它拷贝的是 Empty::_cptr 这个指针,而非 Empty::_cptr 这个指针指向的地址存放的值。

int main()
{
    Empty e1(10, 'h');
    auto e2 = e1;
} // creash !!!

所以当 Three 同时存在的时候,为了让它们都 "安分守己",我们必须给它们顶下规矩,这就是所谓的 Rule of Three

Rules

Three 的问题在于,如果一个类里面有指针类型 (或者需要手动释放的资源类型) 的数据成员,编译器的位逐次拷贝会让程序变得不可靠。所以我们必须为让程序变得安全。基于上面的问题,我们有两个解决方案。要么这个类的对象是不允许被拷贝;要么这个类的对象允许被拷贝,但我们必须亲自重写这个类的 Three

方案一:

class Empty
{
public:
    // other user-definfed ctors
    ~Empty()
    {
        delete _cptr;
    }
    
    Empty(const Empty& other) = delete;
    Empty& operator=(const Empty& other) = delete;
    
private:
    // data members
};

方案二:

class Empty
{
public:
    // other user-definfed ctors
     ~Empty()
    {
        delete _cptr;
    }
  
    Empty(const Empty& other)
    {
        _val = other._val;     
        _cptr = new char(*other._cptr);
    }

    Empty& operator=(const Empty& other)
    {
        if (*this == other)
            return *this;
            
        _val = other._val;
        
        if (_cptr != nullptr)
            delete _cptr;

        _cptr = new char(*other._cptr);
    }
    
private:
    // data members
};

启发

C++ 的对象模型往往不是我们想象中的那么简单,编译器暗地里会做很多额外的工作。所以我们在管理类对象的资源的时候需要格外小心。


IcedBabyccino
1 声望0 粉丝

编程、潮流