文章目录
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函数,运行下来是没有问题的。
#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类中新增加了一个成员变量,因为是自定义的,在无明显语法错误的情况下,他并不会提示你代码不完整。毕竟自定义在提供一定“自主权”的前提下,编码者也要承担一定的责任。
#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:
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:
这是一个完整的可以编译的代码。
#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;
}
编译输出结果:
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
正面教材:
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:
输出结果:
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:
输出结果
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_ptr
和std::shared_ptr
)可以简化资源管理,因为它们会自动处理资源的复制和删除。
- 避免切片问题:
- 当基类指针或引用指向派生类对象,并通过复制操作传递给函数或赋值给另一个基类对象时,可能会丢失派生类特有的部分,只保留基类部分。这称为切片问题。
3、总结
天堂有路你不走,地狱无门你自来。
4、参考
4.1 《Effective C++》