《Effective C++》《构造/析构/赋值运算——12、复制对象时勿忘记其每一个成分》

文章目录

1、Terms12:Copy all parts of an object

先给出结论:

  • ①Copying函数应该确保复制"对象内的all的成员变量"and"all的base class的成分"
  • ②不要尝试以某一个copying函数来实现另外一个copying函数,若想消除重复的代码,具体做法就是建立一个新的成员函数给copy构造和copy assignment函数来调用,而这样的函数往往是private的且常常被命名为init。这个策略可以安全地消除copy构造函数和copy assignment操作符之间的重复代码。

示例代码1:

这是一个正常的,简单的函数,也没有继承。此时自定义他的copying函数,运行下来是没有问题的。

cpp 复制代码
#include <iostream>  
#include <string>  
using namespace std;

// logCall 函数的实现  
void logCall(const std::string& funcName) {  
    std::cout << "Logging call to: " << funcName << std::endl;  
}  
  
class Customer {  
public:  
    // 默认构造函数  
    Customer() = default;  
  
    // 复制构造函数  
    Customer(const Customer& rhs)  
        : name(rhs.name) {  
        logCall("Customer copy constructor");  
    }  
  
    // 复制赋值运算符  
    Customer& operator=(const Customer& rhs) {  
        if (this != &rhs) { // 检查自赋值  
            logCall("Customer copy assignment operator");  
            name = rhs.name;  
        }  
        return *this;  
    }  
  
    // 析构函数(可选,但通常是好的做法)  
    ~Customer() = default;  
  
    // 其他成员函数...  
  
private:  
    std::string name;  
};  
  
int main() {  
    Customer customer1;             // 使用默认构造函数  
    Customer customer2(customer1);  // 使用复制构造函数  
    Customer customer3;  
    customer3 = customer1;          // 使用复制赋值运算符  
    return 0;  
}

示例代码2:

在示例代码2中,在原先的Customer类中新增加了一个成员变量,因为是自定义的,在无明显语法错误的情况下,他并不会提示你代码不完整。毕竟自定义在提供一定"自主权"的前提下,编码者也要承担一定的责任。

cpp 复制代码
#include <iostream>  
#include <string>  
using namespace std;  
  
// logCall 函数的实现  
void logCall(const std::string& funcName) {  
    std::cout << "Logging call to: " << funcName << std::endl;  
}  
  
// Date 类的简单实现  
class Date {  
public:  
    Date() {  
        // 初始化代码,如果有的话  
    }  
  
    // 假设这里还有其他的成员函数,比如设置日期、获取日期等  
    // ...  
  
private:  
    // 日期成员变量,比如年、月、日  
    int year;  
    int month;  
    int day;  
};  
  
class Customer {  
public:  
    // 默认构造函数  
    Customer() = default;  
  
    // 复制构造函数  
    Customer(const Customer& rhs)  
        : name(rhs.name) {  
        logCall("Customer copy constructor");  
    }  
  
    // 复制赋值运算符  
    Customer& operator=(const Customer& rhs) {  
        if (this != &rhs) { // 检查自赋值  
            logCall("Customer copy assignment operator");  
            name = rhs.name;  
        }  
        return *this;  
    }  
  
    // 析构函数(可选,但通常是好的做法)  
    ~Customer() = default;  
  
    // 其他成员函数...  
  
private:  
    std::string name;  
    Date lastTransaction; // 使用 Date 类型的成员变量  
};  
  
int main() {  
    Customer customer1; // 使用默认构造函数  
    Customer customer2(customer1); // 使用复制构造函数  
    Customer customer3;  
    customer3 = customer1; // 使用复制赋值运算符  
    return 0;  
}

如果一个类拥有基类,那么在拷贝复制的时候不仅要复制派生类的部分,还要复制基类的部分。举出正反两个例子。

示例代码3:

cpp 复制代码
class PriorityCustomer :public Customer
{
public:
...
PriorityCustomer(const PriorityCustomer& rhs);
PriorityCustomer& operator=(const PriorityCustomer& rhs);
...
private:
	int priority;
};
反面示例:
 // 复制构造函数
   
    PriorityCustomer(const PriorityCustomer& rhs)  
        : priority(rhs.priority) {  
        logCall("PriorityCustomer copy constructor");  
    }  
  
    // 复制赋值运算符  
    PriorityCustomer& operator=(const PriorityCustomer& rhs){  
     logCall("PriorityCustomer copy assignment operator");
        if (this != &rhs) { // 检查自赋值  
            priority = rhs.priority;  
        }  
        return *this;  
    }  
正面示例:
    // 复制构造函数  
    PriorityCustomer(const PriorityCustomer& rhs)  
        : Customer(rhs), // 调用基类的拷贝构造函数  
        priority(rhs.priority) {  
        logCall("PriorityCustomer copy constructor");  
    }  
  
    // 复制赋值运算符  
    PriorityCustomer& operator=(const PriorityCustomer& rhs) {  
        if (this != &rhs) { // 检查自赋值  
            Customer::operator=(rhs); // 对基类部分进行赋值操作  
            priority = rhs.priority;  
        }  
        return *this;  
    }  

在反面例子中,派生类的成员函数好像复制了派生类的每一样东西。但是派生类的copy构造函数并没有指定实参给其基类的构造函数,因此派生类对象的基类成份会被不带实参的基类构造函数初始化,default构造函数将会对name和lastTransaction执行缺省的初始化动作。

示例代码4:

这是一个完整的可以编译的代码。

cpp 复制代码
#include <iostream>  
#include <string>  
using namespace std;  
  
// logCall 函数的实现  
void logCall(const std::string& funcName) {  
    std::cout << "Logging call to: " << funcName << std::endl;  
}  
  
// Date 类的简单实现  
class Date {  
public:  
    // 构造函数,接受年、月、日作为参数  
    Date(int y, int m, int d) : year(y), month(m), day(d) {  
        // 可以在这里添加一些验证逻辑,确保日期是有效的  
    }  
  
    // 默认构造函数  
    Date() = default;  
  
    // 访问器方法  
    int getYear() const {  
        return year;  
    }  
  
    int getMonth() const {  
        return month;  
    }  
  
    int getDay() const {  
        return day;  
    }  
  
    // 可能还需要添加设置日期的方法、比较日期的方法等  
  
private:  
    // 日期成员变量,比如年、月、日  
    int year;  
    int month;  
    int day;  
};  
  
class Customer {  
public:  
    // 默认构造函数  
    Customer() = default;  
    // 添加一个新的构造函数,接受一个字符串和一个Date对象  
    Customer(const std::string& name, const Date& lastTransaction)   
        : name(name), lastTransaction(lastTransaction) {}  
  
    // 复制构造函数  
    Customer(const Customer& rhs)  
        : name(rhs.name), lastTransaction(rhs.lastTransaction) { // 初始化 lastTransaction  
        logCall("Customer copy constructor");  
    }  
  
    // 复制赋值运算符  
    Customer& operator=(const Customer& rhs) {  
        if (this != &rhs) { // 检查自赋值  
            logCall("Customer copy assignment operator");  
            name = rhs.name;  
            lastTransaction = rhs.lastTransaction;  
        }  
        return *this;  
    }  
  
    // 析构函数(可选,但通常是好的做法)  
    ~Customer() = default;  
    std::string getName()
	{
		return this->name;
	}
	// 获取最后交易日期的年份  
    int getLastTransactionYear() const {  
        return lastTransaction.getYear();  
    }  
  
    // 获取最后交易日期的月份  
    int getLastTransactionMonth() const {  
        return lastTransaction.getMonth();  
    }  
  
    // 获取最后交易日期的日  
    int getLastTransactionDay() const {  
        return lastTransaction.getDay();  
    }  
    virtual void showInfo() {}
  
private:  
    std::string name;  
    Date lastTransaction; // 使用 Date 类型的成员变量  
};  
  
// PriorityCustomer 类继承自 Customer 类  
class PriorityCustomer : public Customer {  
public:  
    // 默认构造函数  
    PriorityCustomer() = default; 
    // 原始的构造函数,接受名字、最后交易日期和优先级 
    PriorityCustomer(const std::string& name, const Date& lastTransaction, int priority)  
        : Customer(name, lastTransaction), priority(priority) {}  
    // 新的构造函数,接受一个 Customer 对象和一个优先级参数  
    PriorityCustomer(const Customer& baseCustomer, int priority)  
        : Customer(baseCustomer), priority(priority) {} // 委托给基类的拷贝构造函数 
    
    // 复制构造函数  
    PriorityCustomer(const PriorityCustomer& rhs)  
        : Customer(rhs), // 调用基类的拷贝构造函数  
        priority(rhs.priority) {  
        logCall("PriorityCustomer copy constructor");  
    }  
  
    // 复制赋值运算符  
    PriorityCustomer& operator=(const PriorityCustomer& rhs) {  
        if (this != &rhs) { // 检查自赋值  
            Customer::operator=(rhs); // 对基类部分进行赋值操作  
            priority = rhs.priority;  
        }  
        return *this;  
    }  
  
    // 析构函数(可选,但通常是好的做法)  
    ~PriorityCustomer() = default;  
    
    // 获取优先级的方法  
    int getPriority() const {  
        return priority;  
    }  
    
    // 其他成员函数...  
    virtual void showInfo(){
         cout << " customer name: " << this->getName()
         << "\t transaction year: " << this->getLastTransactionYear() 
         << "\t  transaction month: " << this->getLastTransactionMonth() 
         << "\t  transaction day: " << this->getLastTransactionDay() << std::endl; 
    }
  
private:  
    int priority; // PriorityCustomer 特有的成员变量  
};  
void test(){
    Date date1(2024,3,11);
    Customer customer1("Alice",date1);
    PriorityCustomer priorityCustomer1(customer1,1);
    PriorityCustomer priorityCustomer2;
    priorityCustomer2 = priorityCustomer1;
    PriorityCustomer priorityCustomer3(priorityCustomer1);
    cout << "priorityCustomer1 info" <<endl;
    priorityCustomer1.showInfo();
    cout << "priorityCustomer2 info" <<endl;
    priorityCustomer2.showInfo();
    cout << "priorityCustomer3 info" <<endl;
    priorityCustomer3.showInfo();
}
int main() {  
    test();
    return 0;  
}

编译输出结果:

cpp 复制代码
Logging call to: Customer copy constructor
Logging call to: Customer copy assignment operator
Logging call to: Customer copy constructor
Logging call to: PriorityCustomer copy constructor
priorityCustomer1 info
 customer name: Alice    transaction year: 2024   transaction month: 3    transaction day: 11
priorityCustomer2 info
 customer name: Alice    transaction year: 2024   transaction month: 3    transaction day: 11
priorityCustomer3 info
 customer name: Alice    transaction year: 2024   transaction month: 3    transaction day: 11

示例代码5

cpp 复制代码
正面教材:
PriorityCustomer(const PriorityCustomer& rhs)  
        : Customer(rhs), // 调用基类的拷贝构造函数  
        priority(rhs.priority) {  
        logCall("PriorityCustomer copy constructor");  
    }  
反面教材1:
PriorityCustomer(const PriorityCustomer& rhs):priority(rhs.priority){  
        logCall("PriorityCustomer copy constructor"); 
    }
    
反面教材2:
    //反面教材
     PriorityCustomer(const PriorityCustomer& rhs):priority(rhs.priority){ 
         Customer::Customer(rhs);//在一个copying函数中去调用另一个copying函数
         logCall("PriorityCustomer copy constructor"); 
     }

反面教材1:

输出结果:

cpp 复制代码
Logging call to: Customer copy constructor
Logging call to: Customer copy assignment operator
Logging call to: PriorityCustomer copy constructor
priorityCustomer1 info
 customer name: Alice    transaction year: 2024   transaction month: 3    transaction day: 11
priorityCustomer2 info
 customer name: Alice    transaction year: 2024   transaction month: 3    transaction day: 11
priorityCustomer3 info
 customer name:          transaction year: 0      transaction month: 0    transaction day: 0

在正面例子中用初始化列表来给派生类的对象rhs中的Base class成分赋值,这就是良好的codes!如果你不这样干的话当你使用 PriorityCustomer priorityCustomer3

(priorityCustomer1);类似这样拷贝的操作的时候就没法把Base class中的成分赋值过来,这样的话你再输出PriorityCustomer priorityCustomer3的all属性的话,其中priorityCustomer3的Base class 成员属性就会乱码!因为你没有给它们初始化!!!

反面2:

输出结果

cpp 复制代码
main.cpp: In copy constructor 'PriorityCustomer::PriorityCustomer(const PriorityCustomer&)':
main.cpp:125:27: error: cannot call constructor 'Customer::Customer' directly [-fpermissive]
  125 |         Customer::Customer(rhs);//在一个copying函数中去调用另一个copying函数
      |         ~~~~~~~~~~~~~~~~~~^~~~~
main.cpp:125:27: note: for a function-style cast, remove the redundant '::Customer'

这里虽然也调用了Base class 的copy 构造函数,但是这里是把PriorityCustomer的copy构造函数内去调用的Customer::Customer(rhs);中的rhs当做是一个已经存在了的对象,把它里面的Base class 成分赋值给自己的Base class 成分,这不就是自己赋值 给自己嘛,本来自己PriorityCustomer3就没初始化,还有用自己的Base class成分给自己赋值一遍,这显然不会如你所愿!

或者,你可以这样理解,因为这里PriorityCustomer的copy 构造函数中,传入的参数是const PriorityCustomer& rhs有引用的类型,因此你不能再让rhs赋值为别人(也包括rhs赋值为自己),也即对于引用类型而言,因为为引用必须在定义的时候初始化,并且不能重新赋值,所以必须要写在初始化列表中,也即调用Base的copy构造函数初始化PriorityCustomer类对象的Base class类成分(成员属性)时,必须要写在派生类的copy构造函数中的初始化列表中 !

2、面试相关

2.1 浅拷贝与深拷贝

  • 浅拷贝:只是简单地复制对象的指针或引用,而不是实际的数据。这可能导致两个对象共享相同的资源,从而引发问题(如悬挂指针、数据不一致等)。
  • 深拷贝:创建对象的一个新副本,包括所有动态分配的内存或其他资源。这样可以确保每个对象都有自己独立的资源。

2.2 复制构造函数

  • 当使用=操作符赋值或传递对象给函数时,C++会调用复制构造函数来创建一个新的对象副本。如果没有显式定义复制构造函数,编译器会生成一个默认的复制构造函数。
  • 复制构造函数应确保对象的所有成员都被正确复制,包括动态分配的内存。

2.3 赋值运算符重载

  • 与复制构造函数类似,赋值运算符operator=也需要正确处理对象的所有成分。
  • 在实现赋值运算符时,通常需要处理自赋值的情况(即对象赋值给自己),并考虑资源管理和异常安全性。

2.4 资源管理

  • 当对象包含动态分配的内存或其他资源时,复制操作必须确保这些资源也被正确复制。
  • 使用智能指针(如std::unique_ptrstd::shared_ptr)可以简化资源管理,因为它们会自动处理资源的复制和删除。
  1. 避免切片问题
    • 当基类指针或引用指向派生类对象,并通过复制操作传递给函数或赋值给另一个基类对象时,可能会丢失派生类特有的部分,只保留基类部分。这称为切片问题。

3、总结

天堂有路你不走,地狱无门你自来。

4、参考

4.1 《Effective C++》

相关推荐
奋斗的小花生8 分钟前
c++ 多态性
开发语言·c++
魔道不误砍柴功10 分钟前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
闲晨13 分钟前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
老猿讲编程40 分钟前
一个例子来说明Ada语言的实时性支持
开发语言·ada
UestcXiye2 小时前
《TCP/IP网络编程》学习笔记 | Chapter 3:地址族与数据序列
c++·计算机网络·ip·tcp
Chrikk2 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*2 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue2 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man2 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
霁月风3 小时前
设计模式——适配器模式
c++·适配器模式