0%

C++拷贝构造函数和赋值运算符

整理下C++的拷贝构造函数和赋值运算符的区别,以及在何种情况下调用拷贝构造,何种情况下调用赋值运算符;总结需要自己提供拷贝构造函数的情况。

拷贝构造函数和赋值运算符

默认情况下,C++编译器会自动地隐式提供一个拷贝构造函数和赋值运算符实现。
如果想要禁用拷贝构造函数和赋值运算符,可以使用delete显式地指定;这样的对象就不能通过值传递,也不能进行赋值运算。使用方式如下代码:

1
2
3
4
5
6
7
8
9
10
class Example
{
public:
Example(const Example& p) = delete;
Example& operator=(const Example& p) = delete;

private:
int foo;
int bar;
}

上面的代码中有一点需要特别注意,拷贝构造函数的参数必须以引用方式传递。这是因为如果以传值的方式进行调用,会调用拷贝构造函数生成函数的实参,而拷贝构造函数的参数仍然是以传值的方式,就会导致无限调用拷贝构造函数,造成栈溢出。

拷贝构造函数的调用

如何区分是调用拷贝构造函数还是赋值运算符,主要看是否有新的对象实例产生。如果产生了新的对象,那调用的就是拷贝构造函数;如果没有,那就是对已有的对象赋值,调用的是赋值运算符。

调用拷贝构造函数的主要有一下场景:

  • 对象以传值的方式作为函数的参数
  • 对象以传值的方式作为函数的返回值
  • 使用一个对象对另一个新对象初始化

一个典型的代码示意如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>

class Example {
public:
Example(int foo) :data(foo) {}

Example(const Example& p) {
std::cout << "Copy Constructor" << std::endl;
this->data = p.data;
}

Example& operator=(const Example& p) {
std::cout << "Assign called" << std::endl;
this->data = p.data;
return *this;
}

public:
int data;
};

int main() {
Example p(99);
std::cout << "p.data: "<<p.data << std::endl;

//这里是拷贝构造
Example p1 = p; // Example p1(p);
std::cout << "p1.data: "<<p1.data << std::endl;

Example p2(22);
std::cout << "p2.data: "<<p2.data << std::endl;
//这里是赋值运算
p2 = p;
std::cout << "After assign p2.data: "<<p2.data << std::endl;

return 0;
}

输出结果如下:

1
2
3
4
5
6
7
➜  c++ ./copy_constructor 
p.data: 99
Copy Constructor
p1.data: 99
p2.data: 22
Assign called
After assign p2.data: 99

深拷贝与浅拷贝

默认的拷贝构造函数只是对类的成员变量进行赋值,这对简单数据类型没有问题,但是对指针类型就不行了,需要涉及到指针指向的内存空间的处理。

深拷贝和浅拷贝主要是针对类中的指针和动态分配空间来说的,对指针的简单的复制不能分割两个对象的动态内存空间;这时候需要提供自定义的深拷贝构造函数。通常的原则是:

  • 含有指针类型成员或者有动态内存分配的成员应提供自定义的拷贝构造函数
  • 在提供拷贝构造函数的同时,还应该考虑自定义赋值运算符

小结

  • 可以通过delete禁用拷贝构造和赋值运算符。
  • 拷贝构造函数的参数必须是引用类型。
  • 区别拷贝构造函数和赋值运算符何时调用,主要看是否有新的对象产生。拷贝构造函数使用已有的对象创建一个新的对象,赋值运算符是将一个对象的值复制给另外一个已经存在的对象。
  • 关于深拷贝和浅拷贝,当类有指针成员或者动态分配空间,都应该提供自定义拷贝构造函数和赋值运算符。

参考链接