复制对象是什么意思?复制构造函数复制赋值运算符是什么?何时需要将它们声明自己?如何防止对象被复制?

2010-11-13 13:27:09
问题评论:

阅读此整个线程c++-faq标记 wiki投票关闭之前.

@Binary︰ 至少花时间来阅读评论讨论之前您投票。使用的文本要简单得多,但是 Fred 要求以展开它。而这就是四个问题从语法角度而言,它确实也与几个方面对其只是一个问题。(如果您不同意的然后通过回答每个靠自己这些问题来证明您 POV 和让我们投票的结果。)

相关︰大两个法则

请记住,从 C + + 11,我认为这已经像这样升级到五,或其他的规则。

@rubenvb︰ 对规则的零文章的直接链接。

回答:

介绍

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

(如果您将 puzzled 的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()
{
}

Memberwise 复制正是在这种情况下我们希望︰ nameage被复制,所以我们得到一个自包含的独立person对象。隐式定义的析构函数始终为空。这也是很好在这种情况下由于我们没有获取构造函数中的任何资源。之后person析构函数隐式调用成员的析构函数︰

之后执行析构函数的主体和销毁任何对象自动分配体内,X 类的析构函数调用析构函数的 X 直接 [...] 成员 [n3126.pdf 12.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的成员复制一个指针,它指向字符数组 !这有几个令人不快的效果︰

  1. 通过a更改可以观察到通过b.
  2. 一旦b被破坏时, a.name将是一个无关联的指针。
  3. 如果a被破坏了,删除无关联的指针会产生未定义的行为.
  4. 由于分配不会考虑什么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 = xthis->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;
}

这还负责自我分配未进行显式检查。此问题更可靠的解决方案是副本交换的用法,但我不会进入异常安全设置的详细信息。我仅提到的例外,以使以下点︰编写管理资源的类很难。

Noncopyable 资源

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

三个规则

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

如果您需要显式声明任何析构函数,自己复制构造函数或复制赋值运算符,您可能需要显式声明所有三人。

(遗憾的是,此"规则"不强制使用 c + + 标准或我所知道的任何编译器。)

建议

大多数情况下,您不必自己,管理资源由于现有类如std::string已执行此操作。只是比较简单的代码使用std::string成员到复杂和易出错替代使用char* ,您应该相信。只要保持原始指针成员离开时,三个规则是不关注自己的代码。

Fred,我会感觉更好有关我向上投票如果 (A) 不会拼写出复制代码中的错误实现分配添加注释说它是错误和 fineprint; 在其他位置查找在代码中使用 c 和 s 或只是跳过所有这些成员 (B) 可以缩短实在 RoT; 与上半年实施(C) 您将讨论引入移动语义和什么意味着 rot。

但然后开机自检应成为 C/W,认为。我喜欢您保留大部分准确条款 (例如,说"复制赋值运算符",并且,不点击到常见的陷阱,分配不能暗示一个副本)。

@Prasoon︰ 我并不认为砍去一半答案可被视为"公平编辑"-CW 答案。

此外,可能我 overread,但没有提及执行任何操作之前应检查标识复制 assingment 运算符。

如果您更新您的 C + + 11 内容那就太棒 (即移动构造函数 / 分配)

的三个规则是 c + +,基本上说一个法则

如果您的类将需要的任何

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

定义显式,那么很可能需要所有三人.

原因是所有三人通常都用来管理资源,并且如果您的类管理资源,通常需要管理复制以及释放。

如果没有良好复制资源语义类管理,则可以考虑禁止复制通过声明复制构造函数和赋值运算符为private的 (未定义.

(请注意,c + + 标准 (目前通常称为 C + + 0x 或 C + + 1x) 即将推出新版将移动语义添加到 c + +,这可能会更改规则的三种。但是,我知道太少有关此规则的三种关于编写 C + + 1x 部分。)

防止复制另一种解决方案是 (私下) 继承的类中不能被复制 (如boost::noncopyable)。它也可以得更清晰。我认为 C + + 0x 和"删除"功能可能可以帮助解决该问题,但忘记了语法:/

@Matthieu︰ 是的一起工作的太。但是除非noncopyable是标准库的一部分,我不认为它多大的改善。(哦,忘了删除语法时,如果您忘记了我曾经知道的 mor ethan 和。:))

任何更新规则的三个和 C + + 1 x?

@Daan︰ 看到此问题的解答不过,我建议要坚持Martinho规则的零对我来说,这是在过去十年中首创的 c + + 的最重要法则之一。

Martinho 的规则的零现位于此处

大三的法律是与上面所指定。

通俗地说,它所解决的问题种类的简单示例︰

在构造函数中分配的内存,那么您需要编写的析构函数,可将其删除。否则会导致内存泄漏。

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

该问题将是对象的,如果您的创建副本,则该副本将指向与原始对象相同的内存。

一次,一个这些删除的析构函数,另中的内存将具有无效的内存 (这称为无关联的指针) 的指针时它会尝试使用它评估状况以获取毛。

因此,您编写复制构造函数,以便分配新对象自己要销毁的内存段。

该原则扩展到其他资源和赋值运算符。

因此如果我们使用复制构造函数然后创建副本的但在不同的内存位置完全,如果我们不使用复制构造函数则的副本,但它指向相同的内存位置。这是要说吗?因此不复制构造函数的情况下复制意味着新指针就有,但指到相同的内存位置,但是如果我们能复制构造函数由显式定义用户然后我们将有一个单独的指针,指向不同的内存位置,但有数据。

很抱歉,我答复前此年龄但我答复似乎仍然是这里:-(从根本上说,是的-:-) 获取

谢谢吨 !:)

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

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

如果 MyClass 只具有某些基元类型化的成员默认赋值运算符将起作用,但它提供了一些指针成员并且不具有赋值运算符的对象会导致不可预知。因此,我们可以说,是否要在类的析构函数中删除的东西,我们可能需要深层复制运算符,这意味着我们应提供一个复制构造函数和赋值运算符。

什么? 复制对象平均值有几种方法,您可以复制的对象--让我们谈一谈 2 种类很有可能要引用到-深层副本和卷影副本。

因为我们正在使用一种面向对象的语言 (或至少假定这样),假设您有一张,而分配的内存。由于它是 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不是在同一语句中声明的。这些操作都有可能为您编写的代码的两个区块非常相似。事实上典型设计模式具有另一个函数调用将一切都设置感到满意后初始副本赋值是合法的-如果您看一下我编写的普通代码,函数是几乎完全相同。

何时需要将它们声明自己?如果您没有编写要共享的代码或用于生产某些方式您事实上只在需要时将它们声明的需要。需要注意的程序语言功能如果您选择意外地使用它并没有进行--即获取编译器默认值。我很少使用复制构造函数,但赋值运算符重写很常见。您是否知道您可以重写加法,减法,等等的含义以及?

如何防止对象被复制?重写所有允许您分配内存,对于与私有函数对象是一个合理的开始的方式。如果真的不希望复制它们的人,可以将其公开,提醒程序员通过引发异常,也未复制对象。

问题标记 c + +。此伪代码展示没有什么充其量,阐明明确定义"规则的三个"有什么用,只是最坏的情况下传播的混乱。

内容来源于Stack Overflow What is The Rule of Three?
请输入您的翻译

What is The Rule of Three?

确认取消