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


当前回答

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

因为我们使用的是面向对象的语言(或者至少假设是这样),所以假设您分配了一块内存。由于它是一种OO语言,我们可以很容易地引用我们分配的内存块,因为它们通常是原始变量(int、chars、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.*/

因此,无论你用什么语言写作,在复制对象时都要非常小心,因为大多数时候你都需要一个深度复制。

什么是复制构造函数和复制赋值运算符?我已经在上面使用过了。当您键入诸如Carcar2=car1之类的代码时,将调用复制构造函数;本质上,如果您声明一个变量并在一行中分配它,那就是调用复制构造函数的时候。赋值运算符是使用等号--car2=car1;时发生的情况;。注意car2没有在同一语句中声明。您为这些操作编写的两块代码可能非常相似。事实上,典型的设计模式有另一个函数,当您满足初始复制/分配是合法的时,您可以调用它来设置所有内容——如果您查看我编写的长代码,这些函数几乎完全相同。

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

如何防止复制对象?使用私有函数覆盖允许为对象分配内存的所有方式是一个合理的开始。如果你真的不想让人复制它们,你可以公开它,并通过抛出一个异常来警告程序员,同时也不要复制对象。

其他回答

三巨头的法则如上所述。

一个简单的例子,用简单的英语来说,就是它解决的问题:

非默认析构函数

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

你可能认为这已经完成了。

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

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

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

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

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

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

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

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

三人法则规定,如果您声明

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

那么你应该申报这三项。这是由于观察到,需要接管复制操作的含义几乎总是源于执行某种资源管理的类,这几乎总是意味着

无论在一个复制操作中进行了什么资源管理,都可能需要在另一复制操作中完成,并且类析构函数也将参与资源的管理(通常是释放资源)。要管理的经典资源是内存,这就是为什么所有标准库类管理内存(例如,执行动态内存管理的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++中的三原则是设计和开发三个要求的基本原则,即如果以下成员函数中的一个有明确的定义,那么程序员应该一起定义其他两个成员函数。也就是说,以下三个成员函数是必不可少的:析构函数、复制构造函数、复制赋值运算符。

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

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

以下是一些快速示例:

// default constructor
My_Class a;

// copy constructor
My_Class b(a);

// copy constructor
My_Class c = a;

// copy assignment operator
b = a;

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

因为我们使用的是面向对象的语言(或者至少假设是这样),所以假设您分配了一块内存。由于它是一种OO语言,我们可以很容易地引用我们分配的内存块,因为它们通常是原始变量(int、chars、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.*/

因此,无论你用什么语言写作,在复制对象时都要非常小心,因为大多数时候你都需要一个深度复制。

什么是复制构造函数和复制赋值运算符?我已经在上面使用过了。当您键入诸如Carcar2=car1之类的代码时,将调用复制构造函数;本质上,如果您声明一个变量并在一行中分配它,那就是调用复制构造函数的时候。赋值运算符是使用等号--car2=car1;时发生的情况;。注意car2没有在同一语句中声明。您为这些操作编写的两块代码可能非常相似。事实上,典型的设计模式有另一个函数,当您满足初始复制/分配是合法的时,您可以调用它来设置所有内容——如果您查看我编写的长代码,这些函数几乎完全相同。

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

如何防止复制对象?使用私有函数覆盖允许为对象分配内存的所有方式是一个合理的开始。如果你真的不想让人复制它们,你可以公开它,并通过抛出一个异常来警告程序员,同时也不要复制对象。

介绍

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

(如果您对姓名(name)、年龄(age)部分感到困惑,这称为成员初始值设定项列表。)

特殊成员功能

复制个人对象意味着什么?主函数显示两种不同的复制场景。初始化人员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()
{
}

在这种情况下,按成员复制正是我们想要的:姓名和年龄都是复制的,所以我们得到了一个独立的独立人物对象。隐式定义的析构函数始终为空。在这种情况下,这也很好,因为我们没有在构造函数中获取任何资源。成员的析构函数在人员析构函数完成后被隐式调用:

在执行析构函数的主体并销毁主体内分配的任何自动对象之后,类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成员只是复制一个指针,而不是它指向的字符数组!这有几个不愉快的影响:

可以通过b观察通过a的变化。一旦b被破坏,a.name就是一个悬空指针。如果a被破坏,则删除悬空指针会产生未定义的行为。由于分配不考虑在分配之前指向的名称,迟早你会发现到处都是内存泄漏。

显式定义

由于按成员复制没有所需的效果,因此必须显式定义复制构造函数和复制赋值运算符,才能对字符数组进行深度复制:

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

注意初始化和赋值之间的区别:我们必须在将旧状态分配给名称之前将其删除,以防止内存泄漏。此外,我们必须防止x=x形式的自赋值。如果没有该检查,delete[]name将删除包含源字符串的数组,因为当您写入x=x时,this->name和that.name都包含相同的指针。

异常安全性

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

// 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成员比较简单代码对于使用字符的复杂且容易出错的替代方案,您应该相信。只要您远离原始指针成员,三的规则就不太可能涉及您自己的代码。