1️⃣ 问题引入

  • 当定义一个 empty class 会发生什么?
  • 编译器会为这个 empty class 自动声明:
    • copy 构造函数
    • copy assignment 操作符
    • 析构函数
  • 同理,如果没有定义任何构造函数,编译器也会声明一个 default 默认构造函数。

2️⃣ 案例

1
class Empty {};

等价于:

1
2
3
4
5
6
7
class Empty {
public:
Empty() { ... }
Empty(const Empty& rhs) { ... }
Empty& operator=(const Empty& rhs) { ... }
~Empty() { ... }
};

3️⃣ 产生时机

  • 只有这些函数被调用的时候,它们才会被编译器创建出来。
1
2
3
Empty e1;           // default构造函数 + 析构函数
Empty e2(e1); // copy构造函数
e2 = e1; // copy assignment操作符

4️⃣ 对于拷贝的理解

  • 浅拷贝:
    • 对于 copy 构造函数、copy assignment 操作符,编译器创建的版本只是将来源对象的每一个非静态成员变量拷贝到目标对象。

案例:

1
2
3
4
5
6
7
8
9
template<typename T>
class NamedObject {
public:
NamedObject(const char* name, const T& value);
NamedObject(const std::string& name, const T& value);
private:
std::string nameValue;
T objectValue;
};
  • NamedObject 没有声明 copy 构造函数,也没有声明 copy assignment 操作符。
  • 如果发生如下代码,会怎么样?
1
2
NamedObject<int> no1("Smallest Prime Number", 2);
NamedObject<int> no2(no1);

解析:

  • 编译器生成的 copy 构造函数将以 no1.nameValueno1.objectValue 为初值,设定 no2 对应的两个成员。
  • nameValuestd::string 类型,会调用 stringcopy 构造函数。
  • objectValueint 类型(T = int),为内置类型,因此会直接拷贝 bits。

5️⃣ 对于 copy assignment 的理解

  • 一般情况下,copy assignment 操作符和 copy 构造函数相似。
  • 但有时,编译器会拒绝为某些类生成 copy assignment 操作符。

案例:

1
2
3
4
5
6
7
8
template<class T>
class NamedObject {
public:
NamedObject(std::string& name, const T& value);
private:
std::string& nameValue; // 这是个 reference
const T objectValue; // 这是个 const
};

如果执行如下代码:

1
2
3
4
5
6
7
std::string newDog("Persephone");
std::string oldDog("Satch");

NamedObject<int> p(newDog, 2);
NamedObject<int> s(oldDog, 36);

p = s; // 会发生什么?

解析:

  • 赋值前:p.nameValues.nameValue 分别指向不同的 string 对象。
  • 是否能让 p.nameValue 指向 s.nameValue
    • 不行,因为 C++ 不允许让 reference 改指向不同的对象。

编译器的应对方案:

  • 如果 class 包含 reference 成员,想要支持赋值操作,必须自己定义 copy assignment 操作符
  • 类中如果包含 const 成员,也一样。
  • 如果某个基类将 copy assignment 操作符声明为 private,编译器也不会为派生类自动生成 copy assignment 操作符。

💡 总结
了解编译器在背后默默生成的函数有助于我们编写更稳定、可控的 C++ 类。
尤其当类中包含 referenceconst 或继承关系时,更要注意手动定义复制相关操作。