C++ 三的法则是什么? 复制对象意味着什么? 什么是复制构造函数和复制赋值运算符? 我什么时候需要自己申报? 如何防止复制我的对象? 介绍

C++ 三的法则是什么? 复制对象意味着什么? 什么是复制构造函数和复制赋值运算符? 我什么时候需要自己申报? 如何防止复制我的对象? 介绍,c++,copy-constructor,assignment-operator,c++-faq,rule-of-three,C++,Copy Constructor,Assignment Operator,C++ Faq,Rule Of Three,C++使用值语义处理用户定义类型的变量。 这意味着对象在各种上下文中被隐式复制, 我们应该理解复制一个对象实际上意味着什么 让我们考虑一个简单的例子: class person { std::string name; int age; public: person(const std::string& name, int age) : name(name), age(age) { } }; int main() { person a(

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?
}
如果您对姓名、年龄部分感到困惑, 这被称为一个

特殊成员职能 复制person对象意味着什么? 主功能显示两种不同的复制场景。 初始化人员ba;由复制构造函数执行。 它的工作是基于现有对象的状态构造一个新对象。 分配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()
{
}
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;
    }
};
在这种情况下,成员复制正是我们想要的: 姓名和年龄被复制,因此我们得到一个独立的person对象。 隐式定义的析构函数始终为空。 在本例中,这也很好,因为我们没有在构造函数中获取任何资源。 成员的析构函数在person析构函数完成后隐式调用:

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

管理资源 那么,我们应该在什么时候明确声明这些特殊的成员函数呢? 当我们班管理一个资源时,就是, 当类的对象负责该资源时。 这通常意味着资源是在构造函数中获取的 或传递到构造函数并在析构函数中释放

让我们回到标准的C++中。 没有std::string这样的东西,程序员喜欢指针。 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()
{
}
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就是一个悬空的指针。 如果已销毁,则删除悬空指针会产生错误。 由于分配未考虑分配前所指的名称, 迟早你会发现到处都是内存泄漏。 明确定义 由于memberwise复制没有所需的效果,我们必须明确定义复制构造函数和复制赋值运算符,以便对字符数组进行深度复制:

// 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->name和that.name都包含相同的指针

例外安全 不幸的是,如果新字符[…]由于内存耗尽而引发异常,此解决方案将失败。 一种可能的解决方案是引入局部变量并对语句重新排序:

// 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;
}
这还可以在不进行显式检查的情况下进行自我分配。 对于这个问题,一个更可靠的解决方案是, 但我不会在这里详细介绍异常安全。 我提到异常只是为了说明以下观点:编写管理资源的类很难

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

private:

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

person(const person& that) = delete;
person& operator=(const person& that) = delete;
三法则 有时,您需要实现一个管理资源的类。 不要在一个类中管理多个资源, 这只会导致痛苦。 在这种情况下,请记住三条规则:

如果需要显式声明析构函数, 自己复制构造函数或复制赋值运算符, 您可能需要显式地声明这三个

不幸的是,这个规则不是由C++标准或任何我意识到的编译器执行的。 五常 从C++11开始,对象有两个额外的特殊成员函数:移动构造函数和移动赋值。五个州的规则也可以实现这些功能

带有签名的示例:

class person
{
    std::string name;
    int age;

public:
    person(const std::string& name, int age);        // Ctor
    person(const person &) = default;                // 1/5: Copy Ctor
    person(person &&) noexcept = default;            // 4/5: Move Ctor
    person& operator=(const person &) = default;     // 2/5: Copy Assignment
    person& operator=(person &&) noexcept = default; // 5/5: Move Assignment
    ~person() noexcept = default;                    // 3/5: Dtor
};
零法则 3/5规则也称为0/3/5规则。规则的零部分表示,在创建类时,不允许编写任何特殊成员函数

劝告 大多数情况下,您不需要自己管理资源, 因为像std::string这样的现有类已经为您完成了这项工作。 只需使用std::string成员比较简单代码 对于复杂且容易出错的使用char*的替代方案,您应该确信。 只要你远离原始指针成员,三的规则就不太可能涉及到你自己的代码。

< P>这是C++的经验法则,基本上说

如果你的班级需要

复制构造函数, 赋值运算符, 或者是一个破坏者, 如果定义明确,则可能需要所有三个

原因是这三个类通常都用于管理资源,如果类管理资源,则通常需要管理复制和释放

如果复制类所管理的资源没有好的语义,则考虑禁止复制构造函数和赋值操作符声明为私有,禁止复制。p>


注意到,即将到来的C++标准新版本是C++ 11,它将移动语义添加到C++中,这可能改变三的规则。然而,我对此知之甚少,无法写一篇关于三法则的C++11章节。

三大法则如上所述

一个简单的例子,简单地说,它解决了什么样的问题:

非缺省析构函数

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

你可能认为这项工作已经完成了

问题是,如果复制的对象是您的对象,那么该复制将指向与原始对象相同的内存

一旦其中一个删除了析构函数中的内存,另一个将有一个指向无效内存的指针。当它试图使用它时,这称为悬空指针。事情将变得棘手

因此,您可以编写一个复制构造函数,以便它为新对象分配它们自己要销毁的内存

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

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

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


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

复制对象意味着什么? 有几种方法可以复制对象让我们来谈谈最有可能指的两种类型:深度复制和浅层复制

因为我们使用的是面向对象的语言,或者至少假设是这样,所以假设您分配了一块内存。由于它是一种面向对象语言,我们可以很容易地引用我们分配的内存块,因为它们通常是由我们自己的类型和原语组成的原语变量int、char、bytes或类。假设我们有一类汽车,如下所示:

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

}
深度复制是指如果我们声明一个对象,然后创建一个完全独立的对象副本…我们最终在两个完整的内存集中创建了两个对象

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不是在同一个语句中声明的。您为这些操作编写的两段代码可能非常相似。事实上,典型的设计模式有另一个函数,您可以调用它来设置一切,一旦您确信初始副本/分配是合法的,如果您查看我编写的原始代码,这些函数几乎是相同的

我什么时候需要自己申报? 如果您没有编写要以某种方式共享或用于生产的代码,那么实际上只需要在需要时声明它们。如果您选择“偶然”使用您的程序语言,并且没有使用它,那么您确实需要了解它的功能,即您获得了编译器的默认值。例如,我很少使用复制构造函数,但赋值运算符重写非常常见。你知道你也可以覆盖加法、减法等的意思吗

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

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

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

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

三是一个基本的设计原则和三个要求的发展,如果在下面的一个成员函数中有明确的定义,然后程序员应该一起定义其他两个成员函数。即以下三个成员函数是必不可少的:析构函数、复制构造函数、复制赋值运算符

<> C++中的复制构造函数是一种特殊的构造函数。它用于构建新对象,该新对象相当于现有对象的副本

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

这里有一些简单的例子:

// default constructor
My_Class a;

// copy constructor
My_Class b(a);

// copy constructor
My_Class c = a;

// copy assignment operator
b = a;

许多现有答案已经涉及复制构造函数、赋值运算符和析构函数。 然而,在后C++11中,移动语义的引入可能会将其扩展到3之外

最近,Michael Claisse做了一个涉及到这个话题的演讲:

我什么时候需要自己申报

三条规则规定,如果你宣布

复制构造函数 复制赋值运算符 析构函数 那么你应该申报这三个。它源于这样一种观察,即接管复制操作的意义的需要几乎总是源于执行某种资源管理的类,这几乎总是意味着

在一个拷贝操作中执行的任何资源管理都可能需要在另一个拷贝操作中执行,并且

类析构函数还将参与资源的管理,通常会释放资源。要管理的经典资源是内存,这就是为什么所有标准库类 管理内存,例如,执行动态内存管理的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&s,要么跳过实现所有这些成员B,你会缩短前半部分,这与RoT没有什么关系;你会讨论移动语义学的引入,以及这对RoT意味着什么。但我认为,这篇文章应该是C/W。我喜欢你保持术语的准确性,也就是说你说的复制赋值运算符,而且你没有陷入一个常见的陷阱,即作业并不意味着复制。@Prasony:我不认为删掉一半答案会被视为对非CW答案的公平编辑。如果你更新你的文章以获得C++11,即移动构造函数/assignment@solalito使用后必须释放的任何内容:并发锁,文件句柄、数据库连接、网络套接字、堆内存……请在投票关闭前阅读和。@Binary:投票前至少花些时间阅读评论讨论。课文过去要简单得多,但弗雷德被要求对其进行进一步的阐述。另外,虽然从语法上讲这是四个问题,但实际上这只是一个问题,有几个方面。如果您不同意这一点,那么请通过单独回答这些问题来证明您的观点,并让我们对结果进行投票。弗雷德,以下是关于C++1x的有趣补充:。我们如何处理这一问题?相关:请记住,从C++11开始,我认为这已经升级到了5规则或类似的规则。防止复制的另一个解决方案是从一个不能像boost::noncopyable那样复制的类中私下继承。它也可以更加清晰。我认为C++0x和删除函数的可能性在这里会有所帮助,但忘记了语法:/@Matthieu:Yep,这也行。但是除非不可复制是STD LIB的一部分,我不认为它有很大的改进。哦,如果你忘了删除语法,你就忘了我认识的mor ethan@达安:看。不过,我还是建议你坚持使用的。对我来说,这是C++ C++在过去十年中最重要的经验法则之一。Martinho的零规则现在没有明显的Advices接管,而是被标记为C++。这个伪代码的解释最多只能澄清定义良好的三个规则,最坏的情况下只能传播混乱。嗨,你的答案没有添加任何新的内容。其他人则更深入、更准确地讨论了这个问题——你的回答是近似的,事实上在某些地方是错误的,也就是说,这里没有必要这样做;很可能应该这样做。对于已经完全回答过的问题,发布这样的答案是不值得的。除非你有新的东西要补充。另外,这里有四个简单的例子,它们在某种程度上与三法则所说的三个例子中的两个有关。太混乱了。所以如果我们使用复制构造函数,那么复制是在不同的内存位置进行的,如果我们不使用复制构造函数,那么复制是在相同的内存位置进行的。这就是你想说的吗?因此,没有复制构造函数的副本意味着新指针将在那里,但指向相同的内存位置。但是,如果用户明确定义了复制构造函数,那么我们将有一个单独的指针,指向不同的内存位置,但有数据。抱歉,我很久以前就回答了这个问题,但我的回答似乎已经不在这里了:-基本上,是的-你明白了:-原则E如何到达复制赋值操作符?如果提到三元规则中的第三条,这个答案会更有用。@DBedrenko,您编写了一个复制构造函数,以便它为新对象分配自己的内存块。。。这与复制赋值运算符的原理相同。你不认为我已经说清楚了吗?@DBedrenko,我已经补充了一些信息。这是否更清楚?