什么是三的规则?

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

介绍

C ++用值语义处理用户定义types的variables。 这意味着对象被隐式复制到不同的上下文中,我们应该明白“复制对象”实际上是什么意思。

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

 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)部分困惑,这被称为成员初始值列表 。)

特殊会员function

复制person对象意味着什么? mainfunction显示两个不同的复制情况。 初始化person b(a);复制构造函数执行。 它的工作是根据现有对象的状态构build一个新的对象。 赋值b = a复制赋值操作符执行 。 它的工作一般比较复杂一些,因为目标对象已经处于一个需要处理的有效状态。

既然我们自己既没有声明拷贝构造函数也没有赋值操作符(也不是析构函数),所以这些都是为我们隐式定义的。 从标准报价:

复制构造函数和复制赋值运算符和析构函数是特殊的成员函数。 [ 注意当程序没有明确声明它们时,实现会隐式地为这些类types声明这些成员函数。 如果使用的话,这个实现将隐含地定义它们。 […] 结束说明 ] [n3126.pdf第12节§1]

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

非联合类X的隐式定义的拷贝构造函数执行其子对象的成员拷贝。 [n3126.pdf第12.8节16]

非联合类X的隐式定义的复制赋值运算符执行其子对象的成员复制赋值。 [n3126.pdf第12.8节30节]

隐式定义

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

 // 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对象。 隐式定义的析构函数总是空的。 在这种情况下这也很好,因为我们没有在构造函数中获得任何资源。 成员的析构函数在析构函数完成后隐式调用:

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

pipe理资源

那么我们何时应该明确地声明这些特殊的成员函数呢? 当我们的class级pipe理资源时 ,也就是说,class级的一个对象负责这个资源。 这通常意味着资源在构造函数中获得 (或传入构造函数)并在析构函数中释放

让我们回顾一下预标准的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成员只是复制一个指针, 而不是它指向的字符数组! 这有几个不愉快的效果:

  1. 通过b变化可以通过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; } 

注意初始化和赋值之间的区别:我们必须在分配name之前拆除旧的状态以防止内存泄漏。 此外,我们必须防止x = xforms的自我赋值。 没有这个检查, delete[] name会删除包含string的数组,因为当你写x = xthis->namethat.name都包含相同的指针。

exception安全

不幸的是,如果由于内存耗尽导致new char[...]抛出exception,这个解决scheme将会失败。 一个可能的解决scheme是引入一个局部variables并重新sorting语句:

 // 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; } 

这也没有明确的检查照顾自我分配。 这个问题的更强大的解决scheme是copy-and-swap成语 ,但是我不会在这里详细讨论exception安全。 我只是提到了一些例外,主要有以下几点: 编写pipe理资源的类很困难。

不可复制的资源

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

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

或者,您可以从boost::noncopyableinheritance或将其声明为已删除(C ++ 0x):

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

三的规则

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

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

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

忠告

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

三条法则是C ++的一个经验法则,基本上是这样说的

如果你的class级需要任何的

  • 一个拷贝构造函数
  • 一个赋值操作符
  • 或者一个析构函数

明确地定义,那么很可能需要全部三个

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

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

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

三大法则如上所述。

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

非默认的析构函数

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

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

问题是,如果复制是由你的对象组成,那么复制将指向与原始对象相同的内存。

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

因此,你写一个拷贝构造函数,以便它分配新的对象自己的内存碎片。

赋值运算符和拷贝构造函数

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

这意味着新的对象和旧的对象将指向同一块内存,所以当你在一个对象中改变它时,它也将被改变为另一个对象。 如果一个对象删除这个内存,另一个将继续尝试使用它 – eek。

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

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

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

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

复制对象意味着什么? 有几种方法可以复制对象 – 让我们来谈谈你最可能引用的两种 – 深层复制和浅层复制。

由于我们使用的是面向对象的语言(或者至less是这样假设的),所以假设你分配了一块内存。 由于它是OO语言,所以我们可以很容易地引用我们分配的内存块,因为它们通常是我们定义的原始variables(ints,chars,字节)或类,它们是由我们自己的types和基元组成的。 所以我们假设我们有一个Car类,如下所示:

 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; } } 

如果我们声明一个对象,然后创build一个完全独立的对象副本,我们最终得到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内存空间的指针……这或多或less是浅拷贝是。

 //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.*/ 

所以,无论您使用什么语言编写,在复制对象时要非常小心,因为大部分时间都需要深度复制。

什么是复制构造函数和复制赋值运算符? 我已经在上面用过了。 当你input代码如Car car2 = car1;时,会调用复制构造函数Car car2 = car1; 本质上,如果你声明一个variables并将其分配在一行中,那就是调用复制构造函数的时候。 赋值运算符是当您使用等号时发生的情况 – car2 = car1; 。 注意car2没有在同一个语句中声明。 您为这些操作编写的两个代码块可能非常相似。 实际上,典型的devise模式还有另外一个function,你可以调用它来设置所有的东西,只要你满意,初始的复制/赋值是合法的 – 如果你看看我写的那些代码,这些函数几乎是相同的。

我什么时候需要自己申报? 如果您不是要编写代码来共享或以某种方式进行生产,您只需要在需要时声明它们。 如果你select“偶然”使用它,而没有使用它,你需要知道你的程序语言是干什么的,也就是说你得到了编译器的默认值。 我很less使用复制构造函数作为实例,但赋值运算符覆盖非常常见。 你知道你可以重写什么加法,减法等等的意思吗?

我怎样才能防止我的对象被复制? 覆盖所有允许用私有函数为对象分配内存的方式是一个合理的开始。 如果你真的不希望人们复制它们,你可以通过抛出exception并且不复制对象来公开并提醒程序员。

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

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

  1. 复制构造函数
  2. 复制赋值运算符
  3. 析构函数

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

  • 无论在一次复制操作中进行的资源pipe理可能需要在另一次复制操作中完成

  • 类的析构函数也将参与资源的pipe理(通常释放它)。 要pipe理的经典资源是内存,这就是为什么所有pipe理内存的标准库类(例如,执行dynamic内存pipe理的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以后,你也可以声明拷贝构造函数和赋值操作符被删除

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

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

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

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

在C ++中复制构造函数是一个特殊的构造函数。 它被用来build立一个新的对象,这个新的对象相当于一个现有对象的拷贝。

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

有很快的例子:

 // default constructor My_Class a; // copy constructor My_Class b(a); // copy constructor My_Class c = a; // copy assignment operator b = a; 
Interesting Posts