欢迎光临
我们一直在努力

条款12:拷贝对象的所有部分

目录

条款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能够从被拷贝对象中正确复制(正确取值并执行拷贝构造函数)基类成分(namelastTransaction则会执行“默认初始化”,而无法从被拷贝对象中正确复制
    • 拷贝赋值操作符函数:若没有显式调用基类的拷贝赋值运算符函数,则编译器不会修改其基类的成员变量。这意味着,派生类的自身成分(priority能够从被拷贝对象中正确复制(正确取值并执行拷贝赋值运算符函数)基类成分(namelastTransaction则会保持“原值不变”,而无法从被拷贝对象中正确复制(略微不同)。

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

拷贝函数能否互相调用

结论2:当两个拷贝函数之间有着实质等价的实现时(往往如此),不要尝试让某个拷贝函数调用另一个拷贝函数以避免代码重复(见“实现1”和“实现2”)。正确的做法是应该将两者的共同逻辑提取到第三个成员函数中,再由两个拷贝函数共同调用(见“实现3”)。

拷贝构造函数的职责是“构造一个新对象”,而拷贝赋值运算符的职责是“对一个已存在对象进行赋值”。

实现1(不可行):令拷贝赋值运算符调用拷贝构造函数

  • 分析:等价于“试图在一个已经存在的对象上重新构造它”。这在语义上是错误的——构造行为只能发生在对象尚未被构造之前。虽然某些语法形式看似可以达到类似效果(例如借助placement new等技术),但这往往会破坏对象的生命周期语义,甚至导致资源泄漏或对象状态不一致等严重问题。因此,不应尝试让拷贝赋值运算符调用拷贝构造函数。

实现2(不可行):令拷贝构造函数调用拷贝赋值运算符

  • 分析:等价于“试图对一个尚未构造完成的对象进行赋值操作”。这违背了对象生命周期的基本规则——赋值操作的前提是对象已经处于有效、已初始化状态。因此,这种做法在语义上也是不成立的,不应尝试。

实现3(可行):将拷贝函数的共同逻辑提取到一个独立的成员函数(通常声明为private,且命名为init)中,以供两者调用。此策略可以安全消除拷贝函数之间的代码重复。

赞(0)
未经允许不得转载:171主机测评 » 条款12:拷贝对象的所有部分
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址