成员逐一复制,位拷贝,浅拷贝和深拷贝有什么区别?

18

我已经谷歌了这些术语,但仍然感到困惑。

有些人说成员逐一复制是深拷贝,位拷贝是浅拷贝,但也有人说不是这样的。

有人能解释一下默认拷贝构造函数和用户自定义拷贝构造函数使用哪种类型的拷贝吗?


1
一个_深拷贝_可能包括复制构造函数,而_位拷贝_则不包括。 - πάντα ῥεῖ
9
如果这个问题实际上“过于宽泛”,那么你会期望看到大量答案,但没有一个能够给出令人满意的覆盖范围。我们可以看到,到目前为止只有两个答案,它们都做得相当不错。请不要因为你觉得写出足够长的文本很烦而投票关闭问题。还有其他人喜欢写作,并且会写作。 - Mike Nakis
3个回答

33

逐成员复制

是指访问每个成员并显式地复制它,调用其复制构造函数。这是复制事物的正确方法。如果做得对,它就相当于深层复制,因为你调用的每个具有复制构造函数的成员将(或应该)反过来执行其自己成员的逐成员复制,如此类推。相反的是按位复制,这是一个hack,见下文。

按位复制

是一种特定形式的浅拷贝。它是指仅仅复制源类的位到目标类,使用memcpy()或类似的东西。构造函数不会被调用,所以你往往会得到一个看起来很好的类,但是一旦你开始使用它,事情就会以可怕的方式开始破裂。这是逐成员复制的相反,是一种快速而肮脏的hack,有时可以在我们知道没有要调用的构造函数和没有要复制的内部结构时使用。关于这可能出现的问题的讨论,请参见此Q&A:C++按位与逐成员复制?

浅拷贝

是指仅复制对象的直接成员,而不复制这些成员所指向的任何结构。这就是当你进行按位复制时得到的结果。

(注意:没有“阴影拷贝”这种东西。我的意思是,在文件系统中确实有这样的东西,但那可能不是你想要的。)

深层复制

是指不仅复制对象的直接成员,而且还复制这些成员所指向的任何结构。这通常是在进行逐成员复制时得到的结果。

总之:

有两个类别:

  • 浅拷贝
  • 深层复制

然后,有两种广泛使用的技术:

  • 按位复制(一种浅层复制)
  • 成员复制(如果正确执行,一种深度复制)

至于那些关于某人说了什么和另一个人说了什么的传闻:按位复制肯定总是浅层复制。成员复制通常是深度复制,但你可能会搞砸它,所以你可能认为你正在进行深度复制,而实际上你并没有。正确的成员复制依赖于拥有适当的复制构造函数。

最后:

默认的复制构造函数会在对象被知道是平凡可复制时进行按位复制,或者在不可复制时进行成员复制。然而,编译器并不总是具有足够的信息来执行每个成员的正确复制。例如,指针通过复制指针本身而不是指向的对象来被复制。这就是为什么当你的对象不是平凡可复制时,通常不应该依赖编译器提供默认的复制构造函数。

用户提供的构造函数可以执行用户想要的任何类型的复制。希望用户明智选择并进行成员复制。


2
C++11模板std::is_trivially_copyable旨在确定何时浅复制有效,特别是何时可以使用memcpy() - Persixty
例如,指针的复制是通过复制指针本身而不是指向的对象来完成的。我一直在寻找位运算名称背后的原因,这句话为我解开了谜团,谢谢! - gawkface
“成员逐个复制……通常等同于深复制” - 我认为这需要重新考虑。编译器生成的复制构造函数进行成员逐个复制,但在存在原始指针的情况下,这将不是深复制。 - Martin Ba
@MartinBa,在上面的“最后”一段中,我写道:“如果已知对象是可平凡复制的,则默认复制构造函数将执行按位复制,否则将执行成员逐个复制。然而,编译器并不总是有足够的信息来执行每个成员的正确复制。例如,指针通过复制指针本身而不是指向的对象来进行复制。”这难道没有解决您的问题吗? - Mike Nakis
@MikeNakis - 这正是我认为写“成员逐一复制通常等同于深拷贝”是一个过度简化的原因。标准将“成员逐一复制”作为生成的复制操作的特定形式进行了调用,我认为这里不是“通常”或“那里”。它只是愚蠢/机械的,它所做的取决于成员类型。 - Martin Ba
@MartinBa 好的,你是对的。我修改了答案的那一部分。希望现在更好了。 - Mike Nakis

3
简短版:
- 位拷贝是对一块内存的复制; - 成员拷贝需要对被复制数据的结构有深入了解。
最简单的区分方式是指针成员:
- 进行位拷贝只会复制指针地址; - 进行成员拷贝会先复制指针所指向的内存,然后将新内存的地址赋值给被拷贝的指针成员。
示例:
struct A 
{
   int* m_a;

   A() 
   {
      m_a = new int[1];
   }

   ~A()
   {
     delete [] m_a;
   }
};

A obj;

printf("%p\n", obj.m_a);  // prints: 0x10001000 (for example)

按位复制

A copiedObj = obj;        // the default copy constructor is employed
                          // it will perform a bitwise copy

printf("%p\n", copiedObj.m_a);  // prints: 0x10001000 - the same address

现在,obj.m_a和copiedObj.m_a都指向内存中的同一个地址。

请注意,使用这样的类定义,如果您尝试删除这两个对象,将会遇到内存访问异常(作为第二个要被删除的对象会尝试释放已经被释放的内存)。

成员逐个复制

struct A 
{
    A(const A& rhs)  // let's define a custom copy constructor
    {
        m_a = new int[1];
        memcpy(m_a, rhs.m_a, sizeof(int) * 1);
    }
}

A copiedObj = a;

printf("%p\n", copiedObj.m_a);  // prints: 0x9a001234 - a totally different address

我们在自定义复制构造函数中执行的自定义操作,复制了原始对象指针所指向的内存的副本。
最后但并非最不重要的是 - 成员逐一复制不仅限于复制构造函数。可以将其视为对对象所驻留的内存区域进行操作的操作。

两者是相同的。我的意思是通过执行按位拷贝,只会复制指针的内容。指针只是一个4(或8)字节长的值,包含内存地址。只有这个值会被复制,而不是地址所指向的内存。 - Piotr Trochim

2
这4个术语:
- memberwise copy (成员逐一复制) - bitwise copy (位逐一复制) - shallow copy (浅复制) - deep copy (深复制)
并不在同一意义轴上。
在编程中,当人们说:“计算机,给我一个副本”时,大多数情况下会期望得到 Deep Copy。具体来说,在 C++ 上下文中,这意味着现有对象的副本已经递归地复制了所有子对象,无论它们是如何实现的。(指针、int、其他复杂对象等)
当您没有考虑到某些直接存在于内存中的对象成员实际上是指针(某种类型),并且只复制它们的“指针值”而不是跟随间接引用并创建指向对象的正确副本时,您将得到 Shallow Copy
如果您在对象中包含(原始)指针,则在 C++ 中进行简单的成员逐一复制时,将获得 Shallow Copy。如果您对任何对象进行简单的复制,则在 Java 或 C# 中也会得到 Shallow Copy,因为在那里,“一切”都是指针/引用。
如果您有一个平凡可复制的对象,例如只包含 int 的 struct,则浅复制与深复制相同。
当您使用 memcpy() 复制内存中的对象时,您将得到 Bitwise Copy。结果可能是语义上的 Deep Copy,或者(更可能)是 Shallow Copy(如果涉及指针),或者可能仅仅是一堆不再是从复制源类型的有效对象形式的位。
最后,Memberwise Copy 是 C++ 中一个特定的术语,由 C++ 标准为调用编译器生成的复制构造函数时执行的操作。具体来说:

非联合类 X 的隐式定义复制/移动构造函数对其基类和成员执行成员逐一复制/移动。 (...)

[class.copy.ctor] 中描述了这意味着什么:

每个基类或非静态数据成员都以适合其类型的方式进行复制/移动:(...)

(!!) 对象可以根据情况 语义上等同于 上述所有 3 种类型的 Memberwise Copy。
(我在此指出,C++ 标准始终使用“memberwise”,而不是“member-wise”,但我的自动修正程序更喜欢“member-wise”或“member wise”——这可能有助于谷歌搜索。)
据我所知,如果你有一个用户定义的复制构造函数,那么你永远不会(也不应该)看到1:1成员逐个复制,因为如果你进行1:1成员逐个复制,你不需要实现构造函数,编译器会自动生成它。
做一些比较:
- 如果你有一个int结构体,则成员逐个复制的结果与位逐位复制相同,并且它是浅复制和深复制(好吧,没有什么深度可言)。 - 如果您有一个指向int *指针的结构体,则成员逐个复制与位逐位复制相同,它也是浅复制,但它不再是深复制,因为副本将指向与原始相同的整数。 - 如果您有包含任何非平凡对象的结构体(例如std :: string),则位逐位复制不再有效,成员逐个复制是浅复制还是深复制取决于子对象的实现:如果所有子对象都在复制构造时实现了深拷贝,则复制是深层的。如果您有任何只进行浅拷贝的子对象成员,则成员逐个复制也将是(部分)浅复制。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接