代码之家  ›  专栏  ›  技术社区  ›  penguru

我们什么时候必须使用复制构造函数?

  •  75
  • penguru  · 技术社区  · 15 年前

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

    7 回复  |  直到 6 年前
        1
  •  66
  •   fredoverflow    15 年前

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

    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;
    }
    
        2
  •  42
  •   Matthieu M.    8 年前

    我有点恼火的是 Rule of Five 没有被引用。

    这条规则非常简单:

    五法则 :
    无论何时编写析构函数、复制构造函数、复制赋值运算符、移动构造函数或移动赋值运算符中的任何一个,都可能需要编写其他四个。

    但是,您应该遵循一个更一般的指导原则,这源于编写异常安全代码的需要:

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

    在这里 @sharptooth 的代码仍然(大部分)很好,但是如果他要向类中添加第二个属性,则不会。考虑以下类别:

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

    如果发生了什么 new Bar 投掷?如何删除指向的对象 mFoo ?有解决方案(功能级别的Try/Catch…),它们只是不可扩展。

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

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

    使用相同的构造函数实现(或实际使用 make_unique )我现在有了免费的例外安全!!!!是不是很刺激?最重要的是,我不再需要担心一个合适的析构函数了!我需要自己写 Copy Constructor Assignment Operator 虽然,因为 unique_ptr 不定义这些操作…但这里没关系;)

    因此, sharptooth 重新访问的类:

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

    我不知道你的情况,但我觉得我的比较容易;)

        3
  •  28
  •   Community CDub    8 年前

    我可以从我的实践中回忆起,当必须处理显式声明/定义复制构造函数时,我会想到以下情况。我把这些案件分为两类

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


    正确性/语义

    在本节中,我将说明声明/定义复制构造函数对于使用该类型的程序的正确操作是必要的。

    在阅读本节之后,您将了解允许编译器自己生成复制构造函数的几个陷阱。因此,作为 seand 注意到他的 answer ,关闭新类的可复制性是安全的,并且 故意地 以后需要时启用。

    如何在C++ 03中制作一个不可复制的类

    声明一个私有的复制构造函数,不要为它提供实现(这样,即使该类型的对象是在类自己的作用域或由它的朋友复制的,构建也会在链接阶段失败)。

    如何使类在C++ 11或更新中不可复制

    用声明复制构造函数 =delete 最后。


    浅拷贝与深拷贝

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

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

    自注册对象

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

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

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

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

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


    具有内部交叉引用的对象

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

    一个例子:

    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)。

    • 如果新对象不能继承源对象的历史记录,而是以单个历史记录项开始,则为对象的历史记录(例如撤消/重做堆栈)。 在<time>从<Other_Object_ID>复制 “。

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


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

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

    现在,如果子对象类型的“变化的”复制构造函数实际上没有改变源对象(并且只是由一个不知道 const 关键字?如果我们不能通过添加缺少的 康斯特 然后,另一个选项是用正确的签名声明我们自己的用户定义的复制构造函数,并将转换为 const_cast .


    书面副本(COW)

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

    虽然cow是一种优化技术,但复制构造函数中的这个逻辑 对其正确实施至关重要。所以我把这个箱子放在这里 而不是在“优化”部分,我们接下来要做的。



    优化

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


    复制过程中的结构优化

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


    跳过复制不可见状态

    对象可能包含不属于其可观测状态的数据。通常,这是在对象的生命周期中积累的缓存/内存化数据,以加速对象执行的某些较慢的查询操作。跳过复制该数据是安全的,因为它将在何时(如果!)执行相关操作。复制此数据可能是不合理的,因为如果对象的可观察状态(从中派生缓存数据)通过改变操作进行了修改(如果我们不打算修改对象,那么为什么要创建一个深度副本?),则复制可能会很快失效。

    只有当辅助数据比代表可观测状态的数据大时,这种优化才是合理的。


    禁用隐式复制

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

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

    C++ 11和更新的标准允许声明特殊的成员函数( 默认和复制构造函数、复制分配运算符和 析构函数) an explicit request to use the default implementation (在声明结束时 =default )



    托多斯

    这个答案可以改进如下:

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

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

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

        5
  •  2
  •   josh    15 年前

    当对象按值传递、按值返回或显式复制时,调用复制构造函数。如果没有复制构造函数,C++将创建一个默认拷贝构造器,它创建浅拷贝。如果对象没有指向动态分配内存的指针,那么Shallow Copy将执行此操作。

        6
  •  0
  •   seand    15 年前

    通常最好禁用copy ctor,operator=除非类特别需要它。这可以防止效率低下,例如在预期引用时按值传递参数。另外,编译器生成的方法可能无效。

        7
  •  -1
  •   pkthapa    6 年前

    让我们考虑下面的代码片段:

    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(); 提供垃圾输出,因为存在一个用户定义的复制构造函数,该构造函数没有为显式复制数据而编写的代码。所以编译器不会创建相同的。

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

    干杯。。。 快乐编码!!!!