avatar

Catalog
C++ 中的拷贝构造函数和赋值运算符的重载

概念

C++ 的空类默认会有默认构造函数、析构函数、拷贝构造函数以及赋值函数(赋值运算符重载)(C++ 11 前)

  • 拷贝构造函数和赋值函数接受一个该类对象的引用作为入参。拷贝构造函数入参必须是引用,不能是值传递,如果拷贝构造函数入参是值传递的话,传值的过程本身要调用一次拷贝构造函数,就无限循环了。
  • 拷贝构造函数用一个对象初始化另一个对象,赋值函数将一个对象赋值给另一个对象,区别在于另一个对象此前是否存在。
  • 拷贝构造函数的主要应用场景:
    • 对象作为函数的参数,以值传递的方式传给函数
    • 对象作为函数的返回值,以值的方式从函数返回
    • 使用一个对象给另一个对象初始化

例子

考虑以下情形:

c++
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
#include <iostream>
using namespace std;

class Person {
public:
Person() {}
Person(const Person& p) { cout << "Copy Constructor" << endl; }
Person& operator=(const Person& p) { cout << "Assign" << endl; return *this; }
};

void f(Person p) { return; }

Person f1() { Person p; return p; }

int main()
{
Person p;
Person p1 = p; // 1, Copy Constructor

Person p2;
p2 = p; // 2, Assign

f(p2); // 3, Copy Constructor

p2 = f1(); // 4, (Copy Constructor) + Assign

Person p3 = f1(); // 5, (Copy Constructor) + (Copy Constructor)

return 0;
}
  • 前三个例子很好理解
  • 第四个例子理论上应该是先拷贝构造再赋值,因为有一次返回值的值传递和一次赋值操作。但是我实际运行的时候发现只有一次赋值,猜想应该是编译器做了优化,把拷贝构造函数的第二个应用场景(返回值值传递)给优化掉了,比如返回的时候就不再产生一个中间的临时对象了。
  • 第五个例子理论上应该是会调用两次拷贝构造函数,一次返回值值传递,一次用对象初始化,但是由刚才的例子可以知道编译器会将返回值值传递那一次调用优化掉。但是实际上,我运行的时候一次拷贝构造函数都没有调用,应该也是编译器做了更进一步的优化,比如将 p3 直接作为 f1() 中的 p 来初始化,这样整个过程只用调一次默认构造函数。之所以有这个猜测是因为我发现 p3 和 f1() 中的 p 地址相同,更进一步地,在 f1() 中定义很多 Person 对象,只有 p 对象的地址没有按序排列在其中,而是和 main 中的 p3 相同。
  • 经过另一番学习,发现确实是编译器做了称为 RVO 的优化(Return Value Optimization),如果在用 gcc 编译时加上 -fno-elide-constructors 选项可以禁用这个优化,就能看到第四个例子的确是 Copy Constructor + Assign,以及第五个例子的确是 Copy Constructor + Copy Constructor 了。

Reference

Author: Gusabary
Link: http://gusabary.cn/2020/02/29/C++%E4%B8%AD%E7%9A%84%E6%8B%B7%E8%B4%9D%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0%E5%92%8C%E8%B5%8B%E5%80%BC%E8%BF%90%E7%AE%97%E7%AC%A6%E7%9A%84%E9%87%8D%E8%BD%BD/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.

Comment