首页 文章

什么是三法则?

提问于
浏览
1909
  • 复制对象意味着什么?

  • 什么是复制构造函数和复制赋值运算符?

  • 我什么时候需要自己申报?

  • 如何防止复制对象?

8 回答

  • 1561

    C中的三条规则是设计和开发三个要求的基本原则,如果在以下一个成员函数中有明确的定义,那么程序员应该将另外两个成员函数一起定义 . 即以下三个成员函数是必不可少的:析构函数,复制构造函数,复制赋值运算符 .

    C中的复制构造函数是一个特殊的构造函数 . 它用于构建新对象,该对象是与现有对象的副本等效的新对象 .

    复制赋值运算符是一种特殊的赋值运算符,通常用于将现有对象指定给同一类型对象的其他对象 .

    有一些简单的例子:

    // default constructor
    My_Class a;
    
    // copy constructor
    My_Class b(a);
    
    // copy constructor
    My_Class c = a;
    
    // copy assignment operator
    b = a;
    
  • 465

    三巨头的法则如上所述 .

    一个简单的例子,用简单的英语,它解决了一个问题:

    Non default destructor

    您在构造函数中分配了内存,因此您需要编写一个析构函数来删除它 . 否则会导致内存泄漏 .

    你可能认为这是完成的工作 .

    问题是,如果复制了对象,则复制将指向与原始对象相同的内存 .

    有一次,其中一个删除了它的析构函数中的内存,另一个将有一个指向无效内存的指针(这称为悬空指针)当它试图使用它时会发生毛茸茸的事情 .

    因此,您编写一个复制构造函数,以便为新对象分配自己的内存块以进行销毁 .

    Assignment operator and copy constructor

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

    这意味着新对象和旧对象将指向同一块内存,因此当您在一个对象中更改它时,它也将被更改为另一个对象 . 如果一个对象删除了这个内存,另一个对象将继续尝试使用它 - eek .

    要解决此问题,请编写自己的复制构造函数和赋值运算符版本 . 您的版本为新对象分配单独的内存,并复制第一个指针指向的值而不是其地址 .

  • 39

    基本上,如果你有一个析构函数(不是默认的析构函数),这意味着你定义的类有一些内存分配 . 假设该类在某些客户端代码之外或由您使用 .

    MyClass x(a, b);
        MyClass y(c, d);
        x = y; // This is a shallow copy if assignment operator is not provided
    

    如果MyClass只有一些原始类型成员,则默认赋值运算符可以工作,但如果它有一些指针成员和没有赋值运算符的对象,则结果将是不可预测的 . 因此我们可以说如果在类的析构函数中有删除的东西,我们可能需要一个深度复制操作符,这意味着我们应该提供一个复制构造函数和赋值操作符 .

  • 12

    许多现有的答案已经触及了复制构造函数,赋值运算符和析构函数 . 然而,在后C11中,移动语义的引入可能会扩展到3以上 .

    最近迈克尔·克莱斯发表了一篇涉及这个话题的演讲:http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class

  • 142

    我什么时候需要自己申报?

    三法则规定如果你声明任何一个

    • 复制构造函数

    • 复制赋值运算符

    • 析构函数

    那么你应该宣布这三个 . 它源于这样的观察,即接管复制操作的意义的需要几乎总是源于执行某种资源管理的类,并且几乎总是暗示

    • 在一次复制操作中正在进行的任何资源管理可能需要在另一个复制操作中完成

    • 类析构函数也将参与资源的管理(通常是释放它) . 要管理的经典资源是内存,这就是为什么管理内存的所有标准库类(例如,执行动态内存管理的STL容器)都声明“三大”:复制操作和析构函数 .

    A consequence of the Rule of Three 是用户声明的析构函数的存在表明简单的成员明智副本不太适合于类中的复制操作 . 反过来,这表明如果一个类声明了析构函数,那么复制操作可能不应该自动生成,因为它们不会做正确的事情 . 在采用C 98时,这种推理的重要性并未得到充分认识,因此在C 98中,用户声明的析构函数的存在对编译器生成复制操作的意愿没有影响 . 在C 11中仍然如此,但仅仅因为限制生成复制操作的条件也会破坏很多遗留代码 .

    如何防止复制对象?

    将复制构造函数和复制赋值运算符声明为私有访问说明符 .

    class MemoryBlock
    {
    public:
    
    //code here
    
    private:
    MemoryBlock(const MemoryBlock& other)
    {
       cout<<"copy constructor"<<endl;
    }
    
    // Copy assignment operator.
    MemoryBlock& operator=(const MemoryBlock& other)
    {
     return *this;
    }
    };
    
    int main()
    {
       MemoryBlock a;
       MemoryBlock b(a);
    }
    

    在C 11以后,您还可以声明复制构造函数和赋值运算符已删除

    class MemoryBlock
    {
    public:
    MemoryBlock(const MemoryBlock& other) = delete
    
    // Copy assignment operator.
    MemoryBlock& operator=(const MemoryBlock& other) =delete
    };
    
    
    int main()
    {
       MemoryBlock a;
       MemoryBlock b(a);
    }
    
  • 21

    Rule of Three是C的经验法则,基本上是说

    如果您的类需要任何明确定义的复制构造函数,赋值运算符或析构函数,那么它可能需要所有这三个 .

    原因是它们中的所有三个通常用于管理资源,如果您的类管理资源,它通常需要管理复制和释放 .

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

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

  • 31

    简介

    C使用值语义处理用户定义类型的变量 . 这意味着对象在各种上下文中被隐式复制,我们应该理解"copying an object"实际上意味着什么 .

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

    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 section12.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.pdf12.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 成员只是复制指针,而不是它指向的字符数组!这有几个不愉快的影响:

    • 可以通过 b 观察到 a 的变化 .

    • 一旦 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,但我不会在这里详细介绍异常安全性 . 我只提到了例外以表达以下观点: Writing classes that manage resources is hard.

    不可复制的资源

    无法或不应复制某些资源,例如文件句柄或互斥锁 . 在这种情况下,只需将复制构造函数和复制赋值运算符声明为 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;
    

    三个规则

    有时您需要实现一个管理资源的类 . (永远不要在一个类中管理多个资源,这只会导致痛苦 . )在这种情况下,请记住 rule of three

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

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

    建议

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

  • 8

    复制对象意味着什么?有几种方法可以复制对象 - 让我们来谈谈你最有可能提到的两种 - 深拷贝和浅拷贝 .

    因为我们是面向对象的语言(或者至少是假设的),所以假设你分配了一块内存 . 由于它是一种OO语言,我们可以很容易地引用我们分配的内存块,因为它们通常是原始变量(整数,字符,字节)或我们定义的由我们自己的类型和基元组成的类 . 所以我们假设我们有一类汽车如下:

    class Car //A very simple class just to demonstrate what these definitions mean.
    //It's pseudocode C++/Javaish, I assume strings do not need to be allocated.
    {
    private String sPrintColor;
    private String sModel;
    private String sMake;
    
    public changePaint(String newColor)
    {
       this.sPrintColor = newColor;
    }
    
    public Car(String model, String make, String color) //Constructor
    {
       this.sPrintColor = color;
       this.sModel = model;
       this.sMake = make;
    }
    
    public ~Car() //Destructor
    {
    //Because we did not create any custom types, we aren't adding more code.
    //Anytime your object goes out of scope / program collects garbage / etc. this guy gets called + all other related destructors.
    //Since we did not use anything but strings, we have nothing additional to handle.
    //The assumption is being made that the 3 strings will be handled by string's destructor and that it is being called automatically--if this were not the case you would need to do it here.
    }
    
    public Car(const Car &other) // Copy Constructor
    {
       this.sPrintColor = other.sPrintColor;
       this.sModel = other.sModel;
       this.sMake = other.sMake;
    }
    public Car &operator =(const Car &other) // Assignment Operator
    {
       if(this != &other)
       {
          this.sPrintColor = other.sPrintColor;
          this.sModel = other.sModel;
          this.sMake = other.sMake;
       }
       return *this;
    }
    
    }
    

    深层复制是指如果我们声明一个对象然后创建一个完全独立的对象副本...我们最终在2个完整的内存集中有2个对象 .

    Car car1 = new Car("mustang", "ford", "red");
    Car car2 = car1; //Call the copy constructor
    car2.changePaint("green");
    //car2 is now green but car1 is still red.
    

    现在让我们做一些奇怪的事情 . 假设car2编程错误或故意意图分享car1的实际内存 . (这通常是一个错误,在课堂上通常会讨论它 . )假设你在询问car2时,你真的正在解决指向car1内存空间的指针...这或多或少是一个浅拷贝是 .

    //Shallow copy example
    //Assume we're in C++ because it's standard behavior is to shallow copy objects if you do not have a constructor written for an operation.
    //Now let's assume I do not have any code for the assignment or copy operations like I do above...with those now gone, C++ will use the default.
    
     Car car1 = new Car("ford", "mustang", "red"); 
     Car car2 = car1; 
     car2.changePaint("green");//car1 is also now green 
     delete car2;/*I get rid of my car which is also really your car...I told C++ to resolve 
     the address of where car2 exists and delete the memory...which is also
     the memory associated with your car.*/
     car1.changePaint("red");/*program will likely crash because this area is
     no longer allocated to the program.*/
    

    因此,无论您使用何种语言编写,在复制对象时要非常小心,因为大多数情况下您需要深层复制 .

    什么是复制构造函数和复制赋值运算符?我已经在上面使用过了 . 当您键入诸如 Car car2 = car1; 之类的代码时,将调用复制构造函数 . 实际上,如果您声明一个变量并将其分配到一行,那就是调用复制构造函数时 . 赋值运算符是使用等号时发生的 - car2 = car1; . 注意 car2 isn 't declared in the same statement. The two chunks of code you write for these operations are likely very similar. In fact the typical design pattern has another function you call to set everything once you'满足初始拷贝/赋值是合法的 - 如果你看一下我写的长手代码,函数几乎是相同的 .

    我什么时候需要自己申报?如果您不是以某种方式编写要共享或 生产环境 的代码,则实际上只需要在需要时声明它们 . 如果你选择“偶然”使用它并且没有制作程序语言,你需要知道你的程序语言会做什么 - 即 . 你得到编译器默认值 . 我很少使用复制构造函数,但赋值运算符覆盖非常常见 . 你知道吗你可以覆盖加法,减法等意思吗?

    如何防止复制对象?覆盖允许使用私有函数为对象分配内存的所有方法都是一个合理的开端 . 如果你真的不希望有人复制它们,你可以将它公开,并通过抛出异常并且不复制对象来提醒程序员 .

相关问题