什么是三法则?

fre*_*low 2067 c++ c++-faq copy-constructor assignment-operator rule-of-three

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

fre*_*low 1714

介绍

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?
}
Run Code Online (Sandbox Code Playgroud)

(如果您对该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()
{
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下name,age成员复制正是我们想要的: 并且被复制,因此我们得到一个独立的独立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;
    }
};
Run Code Online (Sandbox Code Playgroud)

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

  1. 通过a可以观察到变化b.
  2. 一旦b被摧毁,a.name就是一个悬垂的指针.
  3. 如果a被销毁,则删除悬空指针会产生未定义的行为.
  4. 由于赋值没有考虑到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;
}
Run Code Online (Sandbox Code Playgroud)

注意初始化和赋值之间的区别:我们必须在分配之前拆除旧状态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;
}
Run Code Online (Sandbox Code Playgroud)

这也需要在没有明确检查的情况下进行自我分配.这个问题的一个更强大的解决方案是复制和交换习惯用法,但我不会在这里详细介绍异常安全性.我只提到了例外来说明以下几点:编写管理资源的类很难.

不可复制的资源

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

private:

    person(const person& that);
    person& operator=(const person& that);
Run Code Online (Sandbox Code Playgroud)

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

person(const person& that) = delete;
person& operator=(const person& that) = delete;
Run Code Online (Sandbox Code Playgroud)

三个规则

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

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

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

五条规则

从C++ 11开始,一个对象有2个额外的特殊成员函数:移动构造函数和移动赋值.五个州的规则也实现了这些功能.

签名示例:

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
};
Run Code Online (Sandbox Code Playgroud)

零的规则

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

忠告

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

  • 如果您更新C++ 11的帖子(即移动构造函数/赋值)会很棒 (67认同)
  • 但是我觉得这篇文章应该是C/W.我喜欢你保持条款大部分是准确的(即你说"*copy*赋值运算符",并且你没有利用分配不能暗示副本的公共陷阱). (6认同)
  • @solalito使用后必须发布的任何内容:并发锁,文件句柄,数据库连接,网络套接字,堆内存...... (4认同)
  • 弗雷德,如果(A)你不会在可复制代码中拼写错误实现的作业,并添加一个说明错误并在精美图纸中寻找其他地方的说明,我会对我的投票感觉更好.要么在代码中使用c&s,要么只是跳过实现所有这些成员(B)你会缩短上半部分,这与RoT没什么关系; (C)您将讨论移动语义的介绍以及对RoT的意义. (3认同)
  • @Prasoon:我不认为裁掉一半的答案会被视为非CW答案的"公平编辑". (3认同)
  • 此外,我可能已经覆盖了它,但您没有提到复制分配操作员应该在执行任何操作之前检查身份. (3认同)
  • @kuroineko`std :: vector`不检查边界(除非你使用`at`,但为什么要使用它而不是`operator []`?) (2认同)
  • @EtiennedeMartel`operator []`经常在调试模式下进行边界检查,这会导致性能降低; 这正是*正是*调试模式的用途.如果有人抱怨调试模式比释放模式更慢更安全,请向他们展示前往坚果屋的方式. (2认同)
  • 这可能是一个愚蠢的问题,但是当您谈论“资源”时,您指的是什么呢? (2认同)

sbi*_*sbi 492

三的规则是拇指C++的规则,基本上说

如果你的班级需要任何一个

  • 一个拷贝构造函数,
  • 一个赋值运算符,
  • 或者是一个析构函数,

明确地定义,那么它可能需要所有这三个.

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

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

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

  • Martinho 的零规则现在更好(没有明显的广告软件接管)位于 [archive.org](https://web.archive.org/web/20150220233719/http://flamingdangerzone.com/cxx11/2012/08/15/零规则.html) (4认同)
  • 防止复制的另一种解决方案是(私有地)从无法复制的类继承(例如`boost :: noncopyable`)。它也可以更加清晰。我认为C ++ 0x和“删除”功能的可能性可能会在这里有所帮助,但忘记了语法:/ (2认同)
  • @Matthieu:是的,这也有效。但是除非`noncopyable` 是std lib 的一部分,我认为它没有太大的改进。(哦,如果你忘记了删除语法,你忘记的比我知道的还要多。`:)`) (2认同)
  • @Daan:见[这个答案](http://stackoverflow.com/a/4782927/140719).但是,我建议坚持[Martinho](http://stackoverflow.com/users/46642/r-martinho-fernandes)的[零度规则](http://flamingdangerzone.com/cxx11/2012 /08/15/rule-of-zero.html).对我而言,这是C++在过去十年中创造的最重要的经验法则之一. (2认同)

Ste*_*fan 151

三巨头的法则如上所述.

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

非默认析构函数

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

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

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

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

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

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

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

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

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

  • 因此,如果我们使用复制构造函数,则复制完成,但完全在不同的内存位置,如果我们不使用复制构造函数,则复制,但它指向相同的内存位置.你想说的是什么?因此,没有复制构造函数的副本意味着新指针将存在但指向相同的内存位置,但是如果我们有用户显式定义的复制构造函数,那么我们将有一个指向不同内存位置但具有数据的单独指针. (3认同)
  • 对不起,我回答这个年龄,但我的回复似乎仍然没有在这里:-(基本上,是的 - 你得到它:-) (3认同)
  • @DBedrenko,我添加了更多信息。这样说清楚了吗? (2认同)

fat*_*ici 42

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

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

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


小智 35

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

因为我们是面向对象的语言(或者至少是假设的),所以假设你分配了一块内存.由于它是一种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;
}

}
Run Code Online (Sandbox Code Playgroud)

深层复制是指如果我们声明一个对象然后创建一个完全独立的对象副本......我们最终在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.
Run Code Online (Sandbox Code Playgroud)

现在让我们做一些奇怪的事情.假设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.*/
Run Code Online (Sandbox Code Playgroud)

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

什么是复制构造函数和复制赋值运算符?我已经在上面使用过了.Car car2 = car1; 如果您声明一个变量并将其分配到一行中,那么在键入诸如Essentially之类的代码时会调用复制构造函数,即在调用复制构造函数时.赋值运算符是使用等号时发生的情况car2 = car1;.通知car2未在同一声明中声明.您为这些操作编写的两个代码块可能非常相似.实际上,典型的设计模式还有另一个函数,一旦你满意初始拷贝/赋值是合法的,你可以调用它来设置所有东西 - 如果你看一下我写的长手代码,那么函数几乎是相同的.

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

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

  • 这个问题被标记为C++.这个伪代码阐述几乎没有澄清关于明确定义的"三个规则"的任何内容,并且只是在最坏的情况下传播混乱. (4认同)

Aja*_*dav 24

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

三法则规定如果你宣布任何一个

  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);
}
Run Code Online (Sandbox Code Playgroud)

在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);
}
Run Code Online (Sandbox Code Playgroud)


wei*_*wei 14

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

近日迈克尔CLAISSE了触及这个话题的演讲: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class


Mar*_*ton 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;
Run Code Online (Sandbox Code Playgroud)

  • 嗨,你的答案没有添加任何新内容.其他人更深入地涵盖了主题,更准确地说 - 你的答案是近似的,实际上在某些地方是错误的(即这里没有"必须";它"非常可能应该").在对已经得到彻底回答的问题发布此类答案时,真的不值得.除非你有新的东西要添加. (5认同)

归档时间:

查看次数:

265567 次

最近记录:

6 年,10 月 前