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

When do we have to use copy constructors?

提问人:penguru 提问时间:7/19/2010 最后编辑:gawkfacepenguru 更新时间:12/18/2018 访问量:80597

问:

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

C++ 复制构造函数

评论

0赞 usman allam 7/29/2013
stackoverflow.com/questions/12577907/default-copy-constructor
1赞 gawkface 7/26/2017
编写自己的copy-ctor的情况之一:当您必须进行深度复制时。另请注意,一旦您创建了 ctor,就不会为您创建默认的 ctor(除非您使用 default 关键字)。

答:

81赞 sharptooth 7/19/2010 #1

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

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

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

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

评论

10赞 Georg Fritzsche 7/19/2010
它不按位执行,而是按成员执行复制,特别是为类类型成员调用 copy-ctor。
7赞 Martin York 7/19/2010
不要这样写 assingment 运算符。它也不例外安全。(如果新对象抛出异常,则对象将处于未定义状态,存储指向内存的已释放部分(仅在所有可以抛出的操作成功完成后才解除分配内存))。一个简单的解决方案是使用复制交换 idium。
0赞 Peter Ajtai 7/19/2010
@sharptooth底部的第三行,我相信应该是delete stored[];delete [] stored;
4赞 GManNickG 7/19/2010
我知道这只是一个例子,但你应该指出更好的解决方案是使用 .一般的想法是,只有管理资源的实用程序类才需要重载三大类,而所有其他类都应该只使用这些实用程序类,从而无需定义三大类中的任何一个。std::string
2赞 GManNickG 7/19/2010
@Martin:我想确保它是用石头雕刻的。:P
6赞 Peter Ajtai 7/19/2010 #2

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

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

2赞 josh 7/19/2010 #3

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

49赞 Matthieu M. 7/19/2010 #4

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

这个规则很简单:

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

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

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

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

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

Erroneous::Erroneous(): mFoo(new Foo()), mBar(new Bar()) {}

如果投掷会怎样?如何删除指向的对象?有一些解决方案(函数级 try/catch ...),它们只是无法扩展。new BarmFoo

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

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

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

因此,的班级重新审视:sharptooth

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

我不了解你,但我发现我的更容易;)

评论

0赞 Robert Andrzejuk 5/20/2017
对于 C++ 11 - 五法则,它增加了移动构造函数和移动赋值运算符的三法则。
1赞 Matthieu M. 5/20/2017
@Robb:请注意,实际上,正如上一个示例所示,您通常应该以零法则为目标。只有专用(通用)技术类才应该关心处理一种资源,所有其他类都应该使用这些智能指针/容器,而不必担心它。
0赞 Robert Andrzejuk 5/21/2017
@MatthieuM。同意:-)我提到了五法则,因为这个答案在 C++11 之前,以“三巨头”开头,但应该提到的是,现在“五巨头”是相关的。我不想对这个答案投反对票,因为它在所问的上下文中是正确的。
0赞 Matthieu M. 5/21/2017
@Robb:说得好,我更新了答案,提到了五法则而不是三大法则。希望大多数人现在已经转向了具有C++11功能的编译器(我很遗憾那些还没有的人)。
0赞 seand 7/19/2010 #5

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

34赞 Leon 6/4/2016 #6

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

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


正确性/语义

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

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

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

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

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

使用 at end 声明 copy-constructor。=delete


浅拷贝与深拷贝

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

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

自注册对象

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

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

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

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

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


具有内部交叉引用的对象

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

举个例子:

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?

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

在某些类中,对象在某种状态(例如 default-constructed-state)时可以安全复制,否则复制就不安全了。如果我们想允许复制安全复制的对象,那么 - 如果进行防御性编程 - 我们需要在用户定义的复制构造函数中进行运行时检查。


不可复制的子对象

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


准可复制的子对象

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

例如,重新审视对象自注册案例,我们可以争辩说 在某些情况下,必须向全局注册对象 仅当它是完全独立对象时才进行对象管理器。如果它是 另一个对象的子对象,那么管理它的责任是 它的包含对象。

或者,必须同时支持浅层复制和深层复制(两者都不是默认的)。

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

在非防御性编程方法的情况下,也可能同时存在常规复制构造函数和准复制构造函数。在绝大多数情况下,应该采用单一的复制方法,而在极少数但众所周知的情况下,应该使用替代复制方法,这是有道理的。这样,编译器就不会抱怨它无法隐式定义复制构造函数;用户将全权负责记住并检查是否应该通过准复制构造函数复制该类型的子对象。


不要复制与对象标识强关联的状态

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

例子:

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

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

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


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

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

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


写入时复制 (COW)

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

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



优化

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


复制过程中的结构优化

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


跳过复制不可观察状态

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

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


禁用隐式复制

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

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

C++11 和更新的标准允许声明特殊成员函数 ( default 和 copy 构造函数、copy-assignment 运算符和 destructor),并明确请求使用默认实现(只需以 . 结束声明)。=default



待办事项

这个答案可以改进如下:

  • 添加更多示例代码
  • 举例说明“具有内部交叉引用的对象”案例
  • 添加一些链接
-1赞 pkthapa 12/18/2018 #7

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

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

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

干杯。。。 祝您编码愉快!!