在C ++中复制构造函数


Copy Constructor in C++

本问题已经有最佳答案,请猛点这里访问。

这是一个一般性的问题,我已经问了一段时间,但没有得到明确的答案。当这个类中的所有实例数据字段都是标准的C++数据类型时,是否需要为类编写一个复制构造函数?


成员变量的类型对此(1)并不重要,它们的语义是。规则很简单:

如果不提供复制构造函数,编译器将尝试为您生成一个。这个默认生成的操作将对所有成员变量执行默认的复制操作。对于类类型,这意味着调用复制构造函数。对于基元类型,这意味着按位复制。

如果默认生成的构造函数满足您的需要,则不要声明自己的构造函数。如果它不能满足你的需要,你自己声明一个。可以使用非基元成员变量和完全正常的默认复制语义创建类:

1
2
3
4
5
struct PersonId
{
  std::string surname;
  std::vector<std::string> givenNames;
};

同样,可以使用基本类型成员变量创建一个类,其中默认的复制语义将不正常:

1
2
3
4
5
6
7
8
9
10
11
12
class UniqueNamed
{
  int id;
  UniqueNamed() : id(0) {}

public:
  UniqueNamed(const UniqueNamed &src) : id(src.id + 1) {}

  int getId() const { return id; }

  static UniqueNamed initial;
};

所以它取决于类的语义,而不是它的数据成员的类型。

这涉及到复制、移动和占线的语义的一般概念,以及它们在C++中的实现。你可能想读一些关于零、三和五的规则。

(1)当然,如果任何成员变量是不可复制的类型,并且您希望类是可复制的,那么您必须自己提供复制构造函数,因为默认声明的构造函数将被定义为删除。


如果您需要通过复制每个数据成员的值来复制对象,那么您不需要编写一个;隐式生成的对象将完全做到这一点。

如果您在复制对象时需要发生其他事情,或者如果某些事情阻止了隐式对象的生成(例如,const数据成员),并且您仍然希望能够复制它,那么您需要编写一个。


通常,复制构造函数是由编译器为您创建的。只有当类中有特殊对象或指针,或者希望进行特殊处理,或者定义特殊检查或其他特殊行为时,才需要创建一个/和分配运算符。例如,需要用户定义的复制构造函数的对象是一个文件。另一个需要用户定义的复制构造函数的例子是,默认的复制构造函数只复制数据指针。这意味着它们将指向相同的对象/相同的内存块/。如果原始对象在堆栈上,当它超出范围时,析构函数将释放原始对象中的指针,并将为您留下指向无效内存块的指针,您将出现分段错误。


如果不编写复制构造函数的代码,则会创建一个默认值,该值会逐个复制类中的每个字段,如果有的话,会调用它们的复制构造函数。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Test {
public:
  int toto;
  char titi;
};

int main() {
    Test a;

    a.toto = 42;
    a.titi = 'a';
    Test b(a); // b will be initialized with same fields than a.

    return (0);
}

注意这个方法:只在简单的类中使用它,正如你所说的,只有来自C++中的标准数据类型的字段。

这里最常见的错误是当你的类有一个指针字段时:指针被复制了,但没有重新分配,所以你的类有两个实例,一个指针在同一个对象上,如果其中一个修改或删除了它,另一个会感觉到后果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Test {
public:
  std::string* field;

  Test() {
      field = new std::string("toto");
  }
  ~Test() {
      delete (field);
  }
};

int main () {
    Test a; // a.field is allocated.

    Test b(a); // b have the same pointer than a, as if you did b.field = a.field.
    // Here a and b destructors are called. They will delete the same pointers twice.
    // It will result as a segmentation fault.
    return (0);
}

注意:对于=操作符也是如此,如果您不重载它的话。


不,如果要将其用作pod(普通旧数据)对象,则不需要。编译器将为您生成默认的复制构造函数,其行为如您所期望的那样——只需将一个对象中的所有字段分配给另一个对象。