什么是三项规则?

复制对象意味着什么? 什么是复制构造函数和复制赋值运算符? 我什么时候需要自己申报? 我怎样才能防止我的对象被复制?


介绍

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)部分感到困惑,这称为成员初始化程序列表。)

特殊会员功能

复制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被销毁,则删除悬挂指针会产生未定义的行为。
  • 由于作业没有考虑作业之前指定的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 = xthis->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成语,但我不会在这里详细讨论异常安全。 我只提到例外来说明以下几点: 编写管理资源的类很困难。

    非复制资源

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

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

    或者,您可以从boost::noncopyable继承或将它们声明为已删除(C ++ 0x):

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

    三条规则

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

    如果您需要自己显式声明析构函数,复制构造函数或复制赋值运算符,则可能需要明确声明它们中的全部三个。

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

    忠告

    大多数时候,你不需要自己管理资源,因为现有的类如std::string已经为你做了。 只需将使用std::string成员的简单代码与使用char*的错综复杂且容易出错的替代方法进行比较,就可以确信。 只要你远离原始指针成员,三条规则不太可能涉及你自己的代码。


    三条法则是C ++的基本原则,基本上是这样说的

    如果你的课程需要任何

  • 一个拷贝构造函数
  • 一个赋值操作符
  • 析构函数
  • 明确地定义,那么它可能需要全部三个

    原因是他们三个人通常都用来管理资源,如果你的班级管理资源,通常需要管理复制和释放资源。

    如果没有良好的语义来复制类的管理资源,则考虑通过将复制构造函数和赋值运算符声明(不定义)为private来禁止复制。

    (请注意,即将推出的新版本的C ++标准(即C ++ 11)将语义添加到C ++中,这可能会改变三项规则。但是,我对此知之甚少,无法编写C ++ 11部分关于三的规则。)


    三巨头的法律如上所述。

    简单的英语就是一个简单的例子,它解决了这种问题:

    非默认析构函数

    你在你的构造函数中分配了内存,所以你需要编写一个析构函数来删除它。 否则你会造成内存泄漏。

    你可能会认为这是工作完成。

    问题在于,如果复制是由对象组成的,则副本将指向与原始对象相同的内存。

    一旦它们中的一个在其析构函数中删除了内存,另一个将会有一个指向无效内存的指针(这称为一个悬挂指针),当它试图使用它时,事情会变得多毛。

    因此,你写了一个拷贝构造函数,以便为它分配新的对象自己的内存块来销毁。

    赋值运算符和复制构造函数

    您将构造函数中的内存分配给您的类的成员指针。 当您复制此类的对象时,默认赋值运算符和复制构造函数会将此成员指针的值复制到新对象。

    这意味着新对象和旧对象将指向同一片内存,因此当您在一个对象中更改它时,它也会针对其他对象进行更改。 如果一个对象删除了这个内存,另一个将继续尝试使用它 - eek。

    为了解决这个问题,你需要编写自己的拷贝构造函数和赋值操作符。 您的版本为新对象分配单独的内存,并复制第一个指针所指向的值而不是其地址。

    链接地址: http://www.djcxy.com/p/2007.html

    上一篇: What is The Rule of Three?

    下一篇: Why would one replace default new and delete operators?