目录
条款12(拷贝构造/拷贝赋值):拷贝对象的所有部分(Copy all parts of an object)
如何自定义拷贝函数
拷贝函数能否互相调用
条款12(拷贝构造/拷贝赋值):拷贝对象的所有部分(Copy all parts of an object)
概念辨析:拷贝函数 = 拷贝构造函数 + 拷贝赋值运算符函数
如何自定义拷贝函数
结论1:在实现拷贝函数时,必须确保(1)复制当前类中的所有非静态成员变量;(2)显式调用所有基类的拷贝函数,以正确复制基类子对象。所谓“复制每一个成分”,指的正是同时涵盖派生类自身成员与继承自基类的部分。只有做到这两点,才能保证对象在语义上被完整、正确地拷贝。
场景描述:考虑一个用于表示“顾客(Customer)”的类
- 成员变量:name表示顾客的姓名
- 业务需求:每当外界创建一个Customer对象的副本时,都需要记录相应的日志信息。换言之,每当对Customer对象执行拷贝操作时,都需要调用如下的logCall()日志记录函数。

实现1(编译器生成版本,不可行):“条款5”表明,当程序需要时,编译器会自动生成拷贝函数。
- 优点:“编译器生成版本”的行为是“对被拷贝对象的所有成员变量逐一进行拷贝,同时调用基类的拷贝函数”。从“数据完整复制”的角度来看,这种做法是正确且完整的。
- 缺点:编译器生成的拷贝函数无法插入日志逻辑,因此不满足业务需求。
实现2(自定义版本,可行):手工编写(而非由编译器创建)拷贝函数
- 程序示例:


- 运行结果:

- 优点:自定义的拷贝函数允许在其中加入日志记录逻辑(精准控制拷贝行为),使得外界对它们的调用会被日志记录下来,因此满足业务需求。
- 缺点:一旦声明自定义的拷贝函数,意味着告诉编译器“我不需要/不喜欢你的默认实现”。此时,编译器将完全信任你,不再为你检查拷贝逻辑是否正确且完整,这在需求变更时可能带来严重隐患(详见“需求变更1”和“需求变更2”)。(重要!!!)
需求变更1:新增成员变量(lastTransaction表示最后一次交易的时间)


- 问题分析:若忘记修改自定义拷贝函数,则既有的拷贝函数执行的是局部拷贝(partial copy),仅复制原有的成员变量(如name),但没有复制新增的成员变量(如lastTransaction)(具体分析如下)。更严重的问题在于:大多数编译器对此不会发出任何警告,即使开启最高警告级别。这可以看作是编译器的“沉默回应”:既然你拒绝使用它自动生成的拷贝函数,那么拷贝是否完整就完全由你负责。
- 拷贝构造函数:原有的成员变量执行拷贝构造函数(能够从被拷贝对象中正确取值);新增的成员变量执行默认构造函数(无法从被拷贝对象中正确取值)(由编译器自动调用)。
- 拷贝赋值函数:原有的成员变量执行拷贝赋值函数(能够从被拷贝对象中正确取值);新增的成员变量保持原值不变(无法从被拷贝对象中正确取值)。

- 解决方案(结论1.1):任何时候,只要你为class添加新的成员变量,就必须同时修改以下函数。否则,编译器通常不会提醒你,你的代码却已经进入“逻辑不完整”的危险状态。
- 拷贝构造函数、拷贝赋值运算符函数
- 所有构造函数(见“条款4”和“条款45”)
- 任何非标准形式的operator=(见“条款10”)

需求变更2:新增派生类/引入继承体系(PriorityCustomer表示带有优先级的客户,即VIP客户)


- 问题分析:PriorityCustomer的拷贝函数仅复制自身声明的成员变量,但没有复制继承的Customer成员变量。
- 拷贝构造函数:若没有显式调用基类的拷贝构造函数(即在成员初值列中没有指定实参传给基类构造函数),则编译器会自动调用基类的默认构造函数(必定有一个,否则无法通过编译),从而对派生类对象的基类成分进行初始化。这意味着,派生类的自身成分(priority)能够从被拷贝对象中正确复制(正确取值并执行拷贝构造函数);基类成分(name和lastTransaction)则会执行“默认初始化”,而无法从被拷贝对象中正确复制。
- 拷贝赋值操作符函数:若没有显式调用基类的拷贝赋值运算符函数,则编译器不会修改其基类的成员变量。这意味着,派生类的自身成分(priority)能够从被拷贝对象中正确复制(正确取值并执行拷贝赋值运算符函数);基类成分(name和lastTransaction)则会保持“原值不变”,而无法从被拷贝对象中正确复制(略微不同)。

解决方案(结论1.2):任何时候,只要你承担起“为派生类编写拷贝函数”的责任,就必须同时确保“基类成分也被正确复制”。一个重要的现实问题是:基类成员通常是private(见“条款22”),派生类无法直接访问这些成员。因此,你不应该尝试“手动复制”基类数据成员,而应该在派生类的拷贝函数中“显式调用”相应的基类拷贝函数。


拷贝函数能否互相调用
结论2:当两个拷贝函数之间有着实质等价的实现时(往往如此),不要尝试让某个拷贝函数调用另一个拷贝函数以避免代码重复(见“实现1”和“实现2”)。正确的做法是应该将两者的共同逻辑提取到第三个成员函数中,再由两个拷贝函数共同调用(见“实现3”)。
拷贝构造函数的职责是“构造一个新对象”,而拷贝赋值运算符的职责是“对一个已存在对象进行赋值”。
实现1(不可行):令拷贝赋值运算符调用拷贝构造函数
- 分析:等价于“试图在一个已经存在的对象上重新构造它”。这在语义上是错误的——构造行为只能发生在对象尚未被构造之前。虽然某些语法形式看似可以达到类似效果(例如借助placement new等技术),但这往往会破坏对象的生命周期语义,甚至导致资源泄漏或对象状态不一致等严重问题。因此,不应尝试让拷贝赋值运算符调用拷贝构造函数。
实现2(不可行):令拷贝构造函数调用拷贝赋值运算符
- 分析:等价于“试图对一个尚未构造完成的对象进行赋值操作”。这违背了对象生命周期的基本规则——赋值操作的前提是对象已经处于有效、已初始化状态。因此,这种做法在语义上也是不成立的,不应尝试。
实现3(可行):将拷贝函数的共同逻辑提取到一个独立的成员函数(通常声明为private,且命名为init)中,以供两者调用。此策略可以安全消除拷贝函数之间的代码重复。