c++ - 什么是三法则?

标签 c++ copy-constructor assignment-operator c++-faq rule-of-three

复制对象是什么意思?
复制构造函数和复制分配运算符是什么?
我什么时候需要自己声明?
如何防止对象被复制?

最佳答案

介绍

C ++使用值语义处理用户定义类型的变量。
这意味着对象会在各种上下文中隐式复制,
我们应该了解“复制对象”的实际含义。

让我们考虑一个简单的示例:

class person
{
    std::string name;
    int age;

public:

    person(const std::string& name, int age) : name(name), age(age)
    {
    }
};

int main()
{
    person a("Bjarne Stroustrup", 60);
    person b(a);   // What happens here?
    b = a;         // And here?
}


(如果您对name(name), age(age)部分感到困惑,
这称为member initializer list。)

特殊成员功能

复制person对象是什么意思?
main函数显示了两种不同的复制方案。
初始化person b(a);由复制构造函数执行。
它的工作是根据现有对象的状态构造一个新对象。
分配b = a由复印分配操作员执行。
它的工作通常比较复杂,
因为目标对象已经处于某种有效状态,需要处理。

由于我们自己都没有声明复制构造函数或赋值运算符(也没有析构函数),
这些是为我们隐式定义的。从标准引用:


  复制构造函数和复制赋值运算符,析构函数是特殊的成员函数。
  [注意:该实现将隐式声明这些成员函数
  对于某些类类型,当程序未显式声明它们时。
  如果使用它们,实现将隐式定义它们。 [...]尾注]
  [n3126.pdf第12节§1]


默认情况下,复制对象意味着复制其成员:


  非联合类X的隐式定义的复制构造函数执行其子对象的成员复制。
  [n3126.pdf第12.8§16节]
  
  非联合类X的隐式定义的副本分配运算符执行成员级副本分配
  它的子对象。
  [n3126.pdf第12.8§30节]


隐式定义

person的隐式定义的特殊成员函数如下所示:

// 1. copy constructor
person(const person& that) : name(that.name), age(that.age)
{
}

// 2. copy assignment operator
person& operator=(const person& that)
{
    name = that.name;
    age = that.age;
    return *this;
}

// 3. destructor
~person()
{
}


在这种情况下,按成员复制正是我们想要的:
复制了nameage,所以我们得到了一个独立的,独立的person对象。
隐式定义的析构函数始终为空。
在这种情况下,这也很好,因为我们没有在构造函数中获取任何资源。
person析构函数完成后,将隐式调用成员的析构函数:


  在执行析构函数的主体并销毁主体中分配的所有自动对象之后,
  X类的析构函数调用X的直接成员的析构函数
  [n3126.pdf 12.4§6]


管理资源

那么,什么时候应该明确声明那些特殊的成员函数呢?
当我们的班级管理资源时,即
当类的对象负责该资源时。
这通常意味着在构造函数中获取资源
(或传递到构造函数中)并在析构函数中释放。

让我们回到过去的标准C ++。
没有std::string这样的东西,程序员爱上了指针。
person类可能看起来像这样:

class person
{
    char* name;
    int age;

public:

    // the constructor acquires a resource:
    // in this case, dynamic memory obtained via new[]
    person(const char* the_name, int the_age)
    {
        name = new char[strlen(the_name) + 1];
        strcpy(name, the_name);
        age = the_age;
    }

    // the destructor must release this resource via delete[]
    ~person()
    {
        delete[] name;
    }
};


即使在今天,人们仍然以这种方式编写课程并陷入困境:
“我把一个人推向一个向量,现在我得到了疯狂的记忆错误!”
请记住,默认情况下,复制对象意味着复制其成员,
但是复制name成员仅复制一个指针,而不复制它指向的字符数组!
这有几个令人不愉快的影响:


通过a可以更改通过b进行的更改。
一旦b被销毁,a.name是一个悬空指针。
如果a被销毁,则删除悬空指针将产生undefined behavior
由于分配未考虑分配前name指向的内容,
迟早您到处都会出现内存泄漏。


明确定义

由于逐成员复制没有达到预期的效果,因此我们必须明确定义复制构造函数和复制赋值运算符以制作字符数组的深层副本:

// 1. copy constructor
person(const person& that)
{
    name = new char[strlen(that.name) + 1];
    strcpy(name, that.name);
    age = that.age;
}

// 2. copy assignment operator
person& operator=(const person& that)
{
    if (this != &that)
    {
        delete[] name;
        // This is a dangerous point in the flow of execution!
        // We have temporarily invalidated the class invariants,
        // and the next statement might throw an exception,
        // leaving the object in an invalid state :(
        name = new char[strlen(that.name) + 1];
        strcpy(name, that.name);
        age = that.age;
    }
    return *this;
}


注意初始化和赋值之间的区别:
我们必须在分配给name之前拆除旧状态,以防止内存泄漏。
同样,我们必须防止x = x形式的自赋值。
如果没有该检查,delete[] name将删除包含源字符串的数组,
因为当您编写x = x时,this->namethat.name都包含相同的指针。

异常安全

不幸的是,如果new char[...]由于内存耗尽而引发异常,则该解决方案将失败。
一种可能的解决方案是引入局部变量并对语句重新排序:

// 2. copy assignment operator
person& operator=(const person& that)
{
    char* local_name = new char[strlen(that.name) + 1];
    // If the above statement throws,
    // the object is still in the same state as before.
    // None of the following statements will throw an exception :)
    strcpy(local_name, that.name);
    delete[] name;
    name = local_name;
    age = that.age;
    return *this;
}


这也可以在没有明确检查的情况下进行自我分配。
copy-and-swap idiom是解决此问题的更可靠的方法,
但是我在这里不会详细讨论异常安全性。
我只提到了例外情况以说明以下几点:编写用于管理资源的类很困难。

不可复制的资源

某些资源不能或不应被复制,例如文件句柄或互斥锁。
在这种情况下,只需将复制构造函数和复制赋值运算符声明为private而不给出定义:

private:

    person(const person& that);
    person& operator=(const person& that);


或者,您可以从boost::noncopyable继承或声明为已删除(在C ++ 11及更高版本中):

person(const person& that) = delete;
person& operator=(const person& that) = delete;


三法则

有时您需要实现一个管理资源的类。
(永远不要在一个类中管理多个资源,
这只会导致疼痛。)
在这种情况下,请记住以下三个规则:


  如果您需要明确声明任一析构函数,
  自己复制构造函数或复制赋值运算符,
  您可能需要显式声明所有三个。


(不幸的是,该“规则”不是由C ++标准或我所知道的任何编译器强制执行的。)

五法则

从C ++ 11开始,对象具有2个额外的特殊成员函数:move构造函数和move赋值。五个州的规则也可以实现这些功能。

带有签名的示例:

class person
{
    std::string name;
    int age;

public:
    person(const std::string& name, int age);        // Ctor
    person(const person &) = default;                // Copy Ctor
    person(person &&) noexcept = default;            // Move Ctor
    person& operator=(const person &) = default;     // Copy Assignment
    person& operator=(person &&) noexcept = default; // Move Assignment
    ~person() noexcept = default;                    // Dtor
};


零法则

3/5的规则也称为0/3/5的规则。规则的零部分表示在创建类时不允许编写任何特殊成员函数。

忠告

大多数时候,您不需要自己管理资源,
因为现有的类(例如std::string)已经为您完成了。
只需使用std::string成员比较简单的代码
到使用char*复杂且容易出错的替代方法,您应该被说服。
只要您远离原始指针成员,三个规则就不太可能涉及您自己的代码。

关于c++ - 什么是三法则?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/27588991/

相关文章:

c++ - 静态库文件中的资源 - MFC

c++ - 编译器报告 'deleted' operator = ,但它在那里

c++ - 在 C++ 中,我可以在定义自己的复制构造函数后跳过定义赋值运算符吗?

c++ - vector 的错误内存分配 C++

postgresql - 被遗忘的赋值运算符 "="和常见的 ":="

c++ - 在 C++ 中读取 python UUID 字节字符串?

c++ - 关于如何将线条图像转换为圆形图像的问题

c++ - C++ 中的 ObjA = ObjB

java - 克隆方法 Java

c++ - 使用智能指针复制构造函数