【问题标题】:When do we have to use copy constructors?我们什么时候必须使用复制构造函数?
【发布时间】:2011-03-17 18:09:55
【问题描述】:

我知道 C++ 编译器会为一个类创建一个复制构造函数。在这种情况下,我们必须编写用户定义的复制构造函数吗?你能举一些例子吗?

【问题讨论】:

标签: c++ copy-constructor


【解决方案1】:

让我们考虑下面的代码sn-p:

class base{
    int a, *p;
public:
    base(){
        p = new int;
    }
    void SetData(int, int);
    void ShowData();
    base(const base& old_ref){
        //No coding present.
    }
};
void base :: ShowData(){
    cout<<this->a<<" "<<*(this->p)<<endl;
}
void base :: SetData(int a, int b){
    this->a = a;
    *(this->p) = b;
}
int main(void)
{
    base b1;
    b1.SetData(2, 3);
    b1.ShowData();
    base b2 = b1; //!! Copy constructor called.
    b2.ShowData();
    return 0;
}

Output: 
2 3 //b1.ShowData();
1996774332 1205913761 //b2.ShowData();

b2.ShowData(); 给出垃圾输出,因为创建了一个用户定义的复制构造函数,没有编写任何代码来显式复制数据。所以编译器不会创建相同的。

只是想与大家分享这些知识,尽管你们中的大多数人已经知道了。

干杯... 编码愉快!!!

【讨论】:

    【解决方案2】:

    我有点生气,Rule of Five 的规则没有被引用。

    这条规则很简单:

    五法则
    每当您编写析构函数、复制构造函数、复制赋值运算符、移动构造函数或移动赋值运算符之一时,您可能需要编写其他四个。

    但您应该遵循一个更通用的准则,它源于编写异常安全代码的需要:

    每个资源都应该由一个专门的对象管理

    这里@sharptooth 的代码仍然(大部分)没问题,但是如果他要在他的类中添加第二个属性,那就不行了。考虑以下类:

    class Erroneous
    {
    public:
      Erroneous();
      // ... others
    private:
      Foo* mFoo;
      Bar* mBar;
    };
    
    Erroneous::Erroneous(): mFoo(new Foo()), mBar(new Bar()) {}
    

    如果new Bar throws 会发生什么?如何删除 mFoo 指向的对象?有一些解决方案(函数级别的 try/catch ...),它们只是无法扩展。

    处理这种情况的正确方法是使用正确的类而不是原始指针。

    class Righteous
    {
    public:
    private:
      std::unique_ptr<Foo> mFoo;
      std::unique_ptr<Bar> mBar;
    };
    

    使用相同的构造函数实现(或者实际上,使用make_unique),我现在可以免费获得异常安全!!!是不是很刺激?最重要的是,我不再需要担心适当的析构函数!不过,我确实需要编写自己的Copy ConstructorAssignment Operator,因为unique_ptr 没有定义这些操作……但在这里没关系;)

    因此,sharptooth 的班级重访:

    class Class
    {
    public:
      Class(char const* str): mData(str) {}
    private:
      std::string mData;
    };
    

    我不了解你,但我觉得我更容易;)

    【讨论】:

    • 对于 C++ 11 - 五个规则添加到移动构造函数和移动赋值运算符的三个规则。
    • @Robb:请注意,实际上,如上一个示例所示,您通常应该以零规则为目标。只有专门的(通用)技术类应该关心处理一个资源,所有其他类应该使用那些智能指针/容器而不用担心它。
    • @MatthieuM。同意 :-) 我提到了五法则,因为这个答案在 C++11 之前并以“三巨头”开头,但应该提到的是,现在“五巨头”是相关的。我不想对这个答案投反对票,因为它在所要求的上下文中是正确的。
    • @Robb:很好,我更新了答案,提到了五规则而不是三巨头。希望大多数人现在已经转向支持 C++11 的编译器(我很遗憾那些仍然没有的人)。
    【解决方案3】:

    我可以从我的实践中回忆起以下情况,当人们必须明确地声明/定义复制构造函数时。我将案例分为两类

    • 正确性/语义 - 如果您不提供用户定义的复制构造函数,则使用该类型的程序可能无法编译,或者可能无法正常运行。
    • 优化 - 为编译器生成的复制构造函数提供一个很好的替代方案,可以让程序更快。


    正确性/语义

    我在本节中介绍了声明/定义复制构造函数对于正确操作使用该类型的程序所必需的情况。

    阅读完本节后,您将了解允许编译器自行生成复制构造函数的几个陷阱。因此,正如seand 在他的answer 中指出的那样,关闭新类的可复制性并有意在以后真正需要时启用它总是安全的。

    如何在 C++03 中使类不可复制

    声明一个私有的复制构造函数并且不为其提供实现(这样即使该类型的对象被复制到类自己的范围内或被其朋友复制,构建也会在链接阶段失败)。

    如何在 C++11 或更高版本中使类不可复制

    =delete 结尾声明复制构造函数。


    浅拷贝与深拷贝

    这是最好理解的案例,实际上是其他答案中唯一提到的案例。 shaprtoothcovered 很好。我只想补充一点,应该由对象独占的深度复制资源可以应用于任何类型的资源,其中动态分配的内存只是一种。如果需要,深度复制对象可能还需要

    • 复制磁盘上的临时文件
    • 打开单独的网络连接
    • 创建单独的工作线程
    • 分配单独的 OpenGL 帧缓冲区

    自注册对象

    考虑一个类,其中所有对象(无论它们是如何构造的)都必须以某种方式注册。一些例子:

    • 最简单的例子:维护当前存在对象的总数。对象注册只是增加静态计数器。

    • 一个更复杂的例子是有一个单例注册表,其中存储了对该类型的所有现有对象的引用(以便可以将通知传递给所有对象)。

    • 引用计数的智能指针可以被认为只是这个类别中的一种特殊情况:新指针将自己“注册”到共享资源而不是全局注册表中。

    这种自注册操作必须由该类型的任何构造函数执行,复制构造函数也不例外。


    具有内部交叉引用的对象

    某些对象可能具有非平凡的内部结构,它们的不同子对象之间存在直接的交叉引用(事实上,只有一个这样的内部交叉引用就足以触发这种情况)。编译器提供的复制构造函数会破坏内部 intra-object 关联,将它们转换为 inter-object 关联。

    一个例子:

    struct MarriedMan;
    struct MarriedWoman;
    
    struct MarriedMan {
        // ...
        MarriedWoman* wife;   // association
    };
    
    struct MarriedWoman {
        // ...
        MarriedMan* husband;  // association
    };
    
    struct MarriedCouple {
        MarriedWoman wife;    // aggregation
        MarriedMan   husband; // aggregation
    
        MarriedCouple() {
            wife.husband = &husband;
            husband.wife = &wife;
        }
    };
    
    MarriedCouple couple1; // couple1.wife and couple1.husband are spouses
    
    MarriedCouple couple2(couple1);
    // Are couple2.wife and couple2.husband indeed spouses?
    // Why does couple2.wife say that she is married to couple1.husband?
    // Why does couple2.husband say that he is married to couple1.wife?
    

    只允许复制满足特定条件的对象

    在某些类中,对象在某些状态下(例如默认构造状态)可以安全复制,而在其他情况下可以安全复制。如果我们希望允许复制可安全复制的对象,那么——如果是防御性编程——我们需要在用户定义的复制构造函数中进行运行时检查。


    不可复制的子对象

    有时,应该是可复制的类会聚合不可复制的子对象。 通常,这发生在具有不可观察状态的对象上(这种情况将在下面的“优化”部分中更详细地讨论)。编译器只是帮助识别这种情况。


    准可复制子对象

    一个应该是可复制的类,可以聚合一个准可复制类型的子对象。准可复制类型不提供严格意义上的复制构造函数,但具有允许创建对象的概念副本的另一个构造函数。使类型成为准可复制的原因是当类型的复制语义没有完全一致时。

    例如,重新审视对象自注册案例,我们可以认为 可能存在必须向全局注册对象的情况 对象管理器仅当它是一个完整的独立对象时。如果它是一个 另一个对象的子对象,则管理它的责任在于 它的包含对象。

    或者,必须同时支持浅拷贝和深拷贝(它们都不是默认的)。

    那么最终决定权留给该类型的用户 - 复制对象时,他们必须明确指定(通过附加参数)预期的复制方法。

    在非防御性编程方法的情况下,也可能同时存在常规复制构造函数和准复制构造函数。当在绝大多数情况下应该应用单一复制方法时,这是合理的,而在极少数但很好理解的情况下应该使用替代复制方法。那么编译器就不会抱怨它无法隐式定义复制构造函数;记住并检查该类型的子对象是否应通过准复制构造函数复制是用户的唯一责任。


    不要复制与对象身份密切相关的状态

    在极少数情况下,对象的可观察状态的子集可能构成(或被视为)对象身份的不可分割部分,并且不应转移到其他对象(尽管这可能会引起争议) .

    例子:

    • 对象的UID(但这个也属于上面的“自注册”情况,因为id必须在自注册行为中获得)。

    • 对象的历史(例如撤消/重做堆栈)在新对象不能继承源对象的历史,而是从单个历史项“复制于 来自 ”。

    在这种情况下,复制构造函数必须跳过复制相应的子对象。


    强制复制构造函数的正确签名

    编译器提供的复制构造函数的签名取决于子对象可用的复制构造函数。如果至少一个子对象没有真正的复制构造函数(通过常量引用获取源对象)而是有一个可变复制构造函数(获取源通过非常量引用的对象),那么编译器将别无选择,只能隐式声明然后定义一个变异的复制构造函数。

    现在,如果子对象类型的“变异”复制构造函数实际上并没有改变源对象(并且只是由不了解const 关键字的程序员编写)怎么办?如果我们不能通过添加缺少的const 来修复该代码,那么另一种选择是使用正确的签名声明我们自己的用户定义的复制构造函数并犯下转向const_cast 的罪行。


    写时复制(COW)

    如果 COW 容器直接引用了其内部数据,则必须在构造时进行深度复制,否则它可能会充当引用计数句柄。

    虽然 COW 是一种优化技术,但复制构造函数中的这个逻辑 对其正确实施至关重要。这就是为什么我把这个案子放在这里 而不是在“优化”部分,我们接下来要去哪里。



    优化

    在以下情况下,出于优化考虑,您可能希望/需要定义自己的复制构造函数:


    复制过程中的结构优化

    考虑一个支持元素删除操作的容器,但可以通过简单地将已删除元素标记为已删除来实现,并稍后回收其插槽。当制作这样一个容器的副本时,压缩幸存的数据而不是保留“已删除”的插槽可能是有意义的。


    跳过复制不可观察状态

    一个对象可能包含不属于其可观察状态的数据。通常,这是在对象的生命周期内累积的缓存/记忆数据,以加速对象执行的某些慢速查询操作。跳过复制该数据是安全的,因为它将在执行相关操作时(如果!)重新计算。复制这些数据可能是不合理的,因为如果对象的可观察状态(从中派生缓存数据)被变异操作修改(如果我们不打算修改对象,为什么我们要创建一个深然后复制?)

    仅当辅助数据与表示可观察状态的数据相比较大时,此优化才是合理的。


    禁用隐式复制

    C++ 允许通过声明复制构造函数explicit 来禁用隐式复制。然后该类的对象不能按值传递给函数和/或从函数返回。这个技巧可以用于看起来很轻量但复制起来确实非常昂贵的类型(尽管,使其准可复制可能是更好的选择)。

    在 C++03 中声明复制构造函数也需要定义它(当然,如果 你打算使用它)。因此,仅仅使用这样的复制构造函数 正在讨论的问题意味着您必须编写与 编译器会自动为你生成。

    C++11 和更新的标准允许声明特殊的成员函数( 默认和复制构造函数、复制赋值运算符和 析构函数)与an explicit request to use the default implementation (只需以=default 结束声明)。



    待办事项

    这个答案可以改进如下:

    • 添加更多示例代码
    • 说明“具有内部交叉引用的对象”案例
    • 添加一些链接

    【讨论】:

      【解决方案4】:

      编译器生成的复制构造函数进行成员复制。有时这还不够。例如:

      class Class {
      public:
          Class( const char* str );
          ~Class();
      private:
          char* stored;
      };
      
      Class::Class( const char* str )
      {
          stored = new char[srtlen( str ) + 1 ];
          strcpy( stored, str );
      }
      
      Class::~Class()
      {
          delete[] stored;
      }
      

      在这种情况下,stored 成员的成员方式复制不会复制缓冲区(只会复制指针),因此共享缓冲区的第一个被销毁的副本将成功调用 delete[],第二个将运行进入未定义的行为。您需要深拷贝复制构造函数(以及赋值运算符)。

      Class::Class( const Class& another )
      {
          stored = new char[strlen(another.stored) + 1];
          strcpy( stored, another.stored );
      }
      
      void Class::operator = ( const Class& another )
      {
          char* temp = new char[strlen(another.stored) + 1];
          strcpy( temp, another.stored);
          delete[] stored;
          stored = temp;
      }
      

      【讨论】:

      • 它不执行按位复制,而是按成员复制,特别是为类类型成员调用 copy-ctor。
      • 不要这样写赋值运算符。它不是异常安全的。 (如果 new 抛出异常,则对象将处于未定义状态,store 指向已释放的内存部分(仅在所有可以抛出的操作成功完成后才释放内存))。一个简单的解决方案是使用复制交换 idium。
      • @sharptooth 从底部算起第三行你有delete stored[];,我相信它应该是delete [] stored;
      • 我知道这只是一个例子,但您应该指出更好的解决方案是使用std::string。总体思路是,只有管理资源的实用程序类才需要重载三巨头,而所有其他类都应该只使用这些实用程序类,而无需定义三巨头中的任何一个。
      • @Martin:我想确保它是刻在石头上的。 :P
      【解决方案5】:

      除非类特别需要它,否则禁用复制 ctor 和 operator= 通常是个好主意。这可以防止效率低下,例如在打算引用时按值传递 arg。编译器生成的方法也可能无效。

      【讨论】:

        【解决方案6】:

        当对象按值传递、按值返回或显式复制时,将调用复制构造函数。如果没有复制构造函数,c++ 会创建一个默认的复制构造函数来进行浅拷贝。如果对象没有指向动态分配内存的指针,那么浅拷贝就可以了。

        【讨论】:

          【解决方案7】:

          如果你有一个动态分配内容的类。例如,您将一本书的标题存储为 char * 并将标题设置为新的,副本将不起作用。

          您必须编写一个复制构造函数来执行title = new char[length+1]strcpy(title, titleIn)。复制构造函数只会做一个“浅”的复制。

          【讨论】:

            猜你喜欢
            • 2020-03-06
            • 1970-01-01
            • 2019-05-08
            • 1970-01-01
            • 2013-06-02
            • 1970-01-01
            • 2013-01-20
            • 2015-03-18
            • 2011-07-26
            相关资源
            最近更新 更多