0xAA55 发表于 2018-2-17 04:24:34

【C++】面向对象潜规则之:构造析构赋值——三五零(The rule of three/five/zero)

规则之“三”:如果你要自己编写类的构造方法的话,你就很有可能逃不掉不得不编写类的析构方法和赋值方法的命运。

如果你写了个构造函数,然后你在构造函数里面malloc了,或者new了,那么你就得在析构函数里free,或者delete。也就是说,这些配套的函数和你的类的构造和析构函数也是配套的。

而且如果你把你的类赋值给别的类,但是你没有实现赋值方法的话,虽然编译器会帮你复制值,但它复制的只是“表面上”的值。也就是说,它其实是把你new出来的、或者是malloc出来的指针,直接赋值给别的类,而不是赋值指针指向的内存(当然你还得先分配内存才行,但它没有。)

此事要是发生了内存泄漏的话,那也只能怪自己咯。不掌握底层原理的话,就很容易犯这样的错误。class rule_of_three
{
    char* cstring; // 一个“原始指针”,用来处理动态分配的内存。
public:
    rule_of_three(const char* arg)
    : cstring(new char) // 分配内存
    {
      std::strcpy(cstring, arg); // 复制值
    }
    ~rule_of_three()
    {
      delete[] cstring;// 释放内存
    }
    rule_of_three(const rule_of_three& other) // 构造赋值
    {
      cstring = new char;
      std::strcpy(cstring, other.cstring);
    }
    rule_of_three& operator=(const rule_of_three& other) // 赋值运算符重载
    {
      char* tmp_cstring = new char;
      std::strcpy(tmp_cstring, other.cstring);
      delete[] cstring;
      cstring = tmp_cstring;
      return *this;
    }
// 或者另外的例子,重用析构和复制器
//rule_of_three& operator=(rule_of_three other)
//{
//      std::swap(cstring, other.cstring);
//      return *this;
//}
};如果一个类它通过可复制的句柄(之类的玩意儿等)来管理一些无法复制的资源的话,你就得把它的赋值运算符重载(copy-assignment operator)和复制构造方法(copy-constructor)声明为私有,并且不提供它们的定义,或者把它们定义为已删除。

规则之“五”:

由于存在用户定义的析构方法(destructor)、复制构造(copy-constructor)、赋值运算符重载(copy-assignment operator)阻止了隐式定义的移动构造(move constructor)和移动赋值运算符(move assignment operator),任何想要设计为允许移动语义的类,都必须声明以下所有五个特殊的成员方法:class rule_of_five
{
    char* cstring; // 用于管理一个动态分配的内存块的原始指针
public:
    rule_of_five(const char* arg)
    : cstring(new char) // 分配
    {
      std::strcpy(cstring, arg); // 填充
    }
    ~rule_of_five()
    {
      delete[] cstring;// 释放
    }
    rule_of_five(const rule_of_five& other) // 复制构造
    {
      cstring = new char;
      std::strcpy(cstring, other.cstring);
    }
    rule_of_five(rule_of_five&& other) : cstring(other.cstring) // 移动构造
    {
      other.cstring = nullptr;
    }
    rule_of_five& operator=(const rule_of_five& other) // 复制赋值
    {
      char* tmp_cstring = new char;
      std::strcpy(tmp_cstring, other.cstring);
      delete[] cstring;
      cstring = tmp_cstring;
      return *this;
    }
    rule_of_five& operator=(rule_of_five&& other) // 移动赋值
    {
      if(this!=&other) // 防止自己移动到自己
      {
            delete[] cstring;
            cstring = other.cstring;
            other.cstring = nullptr;
      }
      return *this;
    }
// 或者把两个赋值运算符都换成下面的
//rule_of_five& operator=(rule_of_five other)
//{
//      std::swap(cstring, other.cstring);
//      return *this;
//}
};与规则之“三”不同,不提供移动构造和移动赋值通常不会造成错误,而是错过了优化机会。

规则之“零”:

一些类(下例代码中的std::string)如果它有自己的析构方法,复制/移动构造,或者复制/移动赋值运算符重载,需要有独占性的所有权(遵循“单一责任原则”Single Responsibility Principle)。其它的类(下例代码中的rule_of_zero)不应该有自己的析构方法,复制/移动构造,或者复制/移动赋值运算符重载。class rule_of_zero
{
    std::string cppstring;
public:
    rule_of_zero(const std::string& arg) : cppstring(arg) {}
};当一个基类被用于多态用途时,它的析构函数大概需要被声明为公开虚函数。这样就会阻止隐式移动(并且废除隐式复制),然后这些特殊的成员方法应该被定义为默认。class base_of_five_defaults
{
public:
    base_of_five_defaults(const base_of_five_defaults&) = default;
    base_of_five_defaults(base_of_five_defaults&&) = default;
    base_of_five_defaults& operator=(const base_of_five_defaults&) = default;
    base_of_five_defaults& operator=(base_of_five_defaults&&) = default;
    virtual ~base_of_five_defaults() = default;
};然而如果派生类对象不是动态分配的,或者被动态分配在智能指针std::shared_ptr(比如用std::make_shared),你就可以不用这么干。智能指针甚至可以在把派生类转换为std::shared_ptr<Base>后依然能调用派生类析构函数。

参考资料:
规则之三五零
http://en.cppreference.com/w/cpp/language/rule_of_three
规则之零
https://blog.rmf.io/cxx11/rule-of-zero
关注规则之零
http://scottmeyers.blogspot.jp/2014/03/a-concern-about-rule-of-zero.html

勇芳软件 发表于 2018-2-17 09:49:47

:lol不错
页: [1]
查看完整版本: 【C++】面向对象潜规则之:构造析构赋值——三五零(The rule of three/five/zero)