什么是三法则?

时间:2010-11-13 13:27:09

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

  • 复制对象意味着什么?
  • 什么是复制构造函数复制赋值运算符
  • 我什么时候需要自己申报?
  • 如何防止复制对象?

8 个答案:

答案 0 :(得分:1643)

答案 1 :(得分:473)

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

  

如果你的班级需要任何

     
      
  • 复制构造函数
  •   
  • 赋值运算符
  •   
  • 析构函数
  •   
     

明确定义,然后可能需要所有这三个

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

如果复制您的类所管理的资源没有良好的语义,则考虑禁止复制,方法是将复制构造函数和赋值运算符声明为({em> defining ){{1} }。

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

答案 2 :(得分:149)

三巨头的法律如上所述。

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

非默认析构函数

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

您可能认为这是完成的工作。

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

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

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

作业运算符和复制构造函数

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

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

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

答案 3 :(得分:41)

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

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

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

答案 4 :(得分:34)

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

由于我们使用的是面向对象的语言(或者至少是假设的),所以假设你分配了一块内存。由于它是一种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未在同一语句中声明。您为这些操作编写的两个代码块可能非常相似。事实上,典型的设计模式还有另一个函数,一旦你对初始拷贝/赋值是合法的,你就会调用它来设置所有东西 - 如果你看一下我写的长手代码,那么函数几乎是相同的。

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

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

答案 5 :(得分:23)

  

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

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

  1. 复制构造函数
  2. 复制分配操作员
  3. 然后你应该宣布这三个。它源于观察,即接管复制操作的意义的需要几乎总是源于执行某种资源管理的类,并且几乎总是暗示

    • 在一次复制操作中进行的任何资源管理都可能需要在其他复制操作中完成,并且

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

    规则的结果是用户声明的析构函数的存在表明简单的成员明智副本不太适合于类中的复制操作。反过来,这表明如果一个类声明了一个析构函数,那么复制操作可能不应该自动生成,因为它们不会做正确的事情。在采用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及更高版本中,您还可以声明复制构造函数&amp;赋值运算符已删除

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

答案 6 :(得分:14)

许多现有的答案已经触及了复制构造函数,赋值运算符和析构函数。 但是,在后C ++ 11中,移动语义的引入可能会将其扩展到3以上。

最近Michael Claisse发表了一个涉及这个主题的演讲: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class

答案 7 :(得分:9)

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

C ++中的复制构造函数是一个特殊的构造函数。它用于构建一个新对象,它是一个等同于现有对象副本的新对象。

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

有一些简单的例子:

// default constructor
My_Class a;

// copy constructor
My_Class b(a);

// copy constructor
My_Class c = a;

// copy assignment operator
b = a;