关于C++:copy复制对象

What is The Rule of Three?

  • 复制对象是什么意思?
  • 什么是复制构造函数和复制分配运算符?
  • 我什么时候需要自己申报?
  • 如何防止复制对象?


介绍

C++用数值语义处理用户定义类型的变量。这意味着对象在不同的上下文中被隐式复制,我们应该理解"复制一个对象"的真正含义。好的。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)部分感到困惑,这称为成员初始值设定项列表。)好的。特殊成员函数

复制person对象意味着什么?main函数显示了两种不同的复制场景。初始化person b(a);由复制构造函数执行。它的工作是基于现有对象的状态构造一个新对象。分配由拷贝分配操作员执行。它的工作通常要复杂一点,因为目标对象已经处于某个需要处理的有效状态。好的。

因为我们自己既没有声明复制构造函数也没有声明赋值运算符(也没有声明析构函数),这些都是为我们隐式定义的。引用标准:好的。

The [...] copy constructor and copy assignment operator, [...] and destructor are special member functions.
[ Note: The implementation will implicitly declare these member functions
for some class types when the program does not explicitly declare them.
The implementation will implicitly define them if they are used. [...] end note ]
[n3126.pdf section 12 §1]

Ok.

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

The implicitly-defined copy constructor for a non-union class X performs a memberwise copy of its subobjects.
[n3126.pdf section 12.8 §16]

Ok.

The implicitly-defined copy assignment operator for a non-union class X performs memberwise copy assignment
of its subobjects.
[n3126.pdf section 12.8 §30]

Ok.

隐式定义

person隐式定义的特殊成员函数如下:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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析构函数完成后隐式调用:好的。

After executing the body of the destructor and destroying any automatic objects allocated within the body,
a destructor for class X calls the destructors for X's direct [...] members
[n3126.pdf 12.4 §6]

Ok.

管理资源

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

让我们及时回到标准的C++。没有像std::string这样的东西,程序员喜欢指针。person类可能是这样的:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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就是一个悬空指针。
  • 如果a被破坏,删除悬空指针将产生未定义的行为。
  • 由于转让没有考虑到name在转让前所指的内容,迟早你会发现到处都是记忆泄漏。
  • 明确的定义

    由于memberwise复制没有所需的效果,因此必须显式定义复制构造函数和复制分配运算符,以便对字符数组进行深度复制:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 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->namethat.name都包含相同的指针。好的。例外安全

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 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;
    }

    这还可以在不进行明确检查的情况下处理自我分配。对于这个问题,一个更强大的解决方案是复制和交换习惯用法,但我不会在这里详细讨论异常安全。我只提到了例外情况来说明以下几点:编写管理资源的类是困难的。好的。不可复制资源

    某些资源不能或不应该被复制,例如文件句柄或互斥体。在这种情况下,只需声明copy constructor和copy assignment operator为private,而不给出定义:好的。

    1
    2
    3
    4
    private:

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

    或者,您可以从EDCOX1 OR 14继承或声明为删除(在C++ 11和以上):好的。

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

    三法则

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

    If you need to explicitly declare either the destructor,
    copy constructor or copy assignment operator yourself,
    you probably need to explicitly declare all three of them.

    Ok.

    (不幸的是,这个"规则"不是由C++标准或任何我意识到的编译器强制执行的。好的。五法则

    从C++ 11开始,对象有2个额外的特殊成员函数:移动构造函数和移动赋值。实施这些职能的五个国家的规则。好的。

    带有签名的示例:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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
    };

    零的法则

    3/5规则也被称为0/3/5规则。规则的零部分说明在创建类时,不允许编写任何特殊成员函数。好的。忠告

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


    三的规则是C++的经验法则,基本上说

    If your class needs any of

    • a copy constructor,
    • an assignment operator,
    • or a destructor,

    defined explictly, then it is likely to need all three of them.

    这样做的原因是,这三个类通常都用于管理一个资源,如果您的类管理一个资源,那么它通常需要管理复制和释放。

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

    (注意,即将到来的新版本的C++标准(即C++ 11)将移动语义添加到C++中,这将有可能改变三的规则。然而,我对编写一个关于三的规则的C++ 11节知之甚少。


    大三定律如上所述。

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

    非默认析构函数

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

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

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

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

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

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

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

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

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


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

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

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


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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    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;
    }

    }

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

    1
    2
    3
    4
    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内存空间的指针……这或多或少是一个肤浅的副本。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //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没有在同一报表中声明。为这些操作编写的两段代码可能非常相似。事实上,典型的设计模式有另一个函数,当您满意初始复制/分配是合法的时,您可以调用它来设置所有内容——如果您查看我编写的长手代码,这些函数几乎是相同的。

    我什么时候需要自己申报?如果您不编写要共享或以某种方式用于生产的代码,那么您只需要在需要时声明它们。如果您选择"意外地"使用程序语言,并且没有使用它,那么您就需要知道程序语言会做什么——也就是说,您得到了编译器的默认值。例如,我很少使用复制构造函数,但赋值运算符重写是非常常见的。你知道你可以改写加减法的意思吗?

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


    When do I need to declare them myself?

    三条规则规定,如果你申报

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

    • 在一个复制操作中进行的任何资源管理都可能需要在另一个复制操作中进行,并且

    • 类析构函数也将参与资源的管理(通常释放它)。要管理的经典资源是内存,这就是为什么所有标准库类管理内存(例如,执行动态内存管理的STL容器)都声明"三大":复制操作和析构函数。

    第三条规则的一个结果是,用户声明的析构函数的存在表明简单的成员级复制不太可能适用于类中的复制操作。反过来,这表明,如果一个类声明了一个析构函数,那么复制操作可能不应该自动生成,因为它们不会做正确的事情。在采用C++ 98时,这条推理线的意义没有得到充分的理解,因此在C++ 98中,用户声明的析构函数的存在对编译器生成复制操作的意愿没有影响。在C++ 11中,情况仍然如此,但仅仅因为限制生成复制操作的条件会破坏太多的遗留代码。

    How can I prevent my objects from being copied?

    将复制构造函数和复制分配运算符声明为私有访问说明符。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    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中,您还可以声明复制构造函数和赋值操作符被删除。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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/c pp/c-pp-con-2014/the-canonical-class


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

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

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

    有一些简单的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // default constructor
    My_Class a;

    // copy constructor
    My_Class b(a);

    // copy constructor
    My_Class c = a;

    // copy assignment operator
    b = a;