0%

[Effective C++ 笔记]条款12.复制对象时确保复制其一个成员

简介

  • 在定义 copy 函数时,要确保复制了对象中的每一个成员变量
  • 在设计到继承时,对于派生类的 copy 函数要确保调用了基类的相应 copy 函数以完成基类成员变量的复制
  • 不要尝试在其中一种 copy 函数调用另一种 copy 函数,而应该将重复代码抽离出作为第三个函数供两个函数使用。

引子

我们设计 class 时通常会将其内部成员变量封装起来,只用两个函数来进行对象的复制,即 copy 构造函数和 copy assignement 操作符 (即 operator=),这里统称为 copy 函数。通过条款 5 我们可以知道,在我们没有显式定义 copy
函数时,编译器会自动为我们生成这些函数,其内部的定义是为被复制对象中的每一个成员变量进行一次复制。这在大部分情况下是没问题的,需要注意的是当我们自己定义
copy 函数时可能会遇到潜在的问题,如下例所示(完整代码见 ex1 ):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Customer {
public:
Customer(const std::string& name_, const std::string& date_): name(name_), date(date_) {}
Customer(const Customer& rhs);
Customer& operator=(const Customer& rhs);

private:
std::string name;
std::string date;
};

Customer::Customer(const Customer& rhs): name(rhs.name) {
std::cout << "Customer copy constructor called." << std::endl;
}

Customer& Customer::operator=(const Customer& rhs) {
std::cout << "Customer copy assignment called." << std::endl;
name = rhs.name;
return *this;

上面这段代码中,不难发现我们漏复制了 Customer 类中的 date 成员变量,因此我们的复制操作只是局部复制,而编译器即使在警告级别最高的情况下也不会因此抛出错误(因为没有语法错误)。当然在这种情况下我们只需要简单地将其补上即可,但是这意味着我们每添加一个成员变量,都必须记得在 copy 函数中手动添加对应成员变量的
copy,如果忘记了我们可能也不会发现(直到遇到问题调试的时候)。

对象复制时在继承中可能会出现的问题

考虑以下例子(完整代码见 ex2):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class PriorityCustomer: public Customer {
public:
PriorityCustomer(const std::string& name_, const std::string& date_, int priority_) : Customer(name_, date_), priority(priority_) {}
PriorityCustomer(const PriorityCustomer& rhs): priority(rhs.priority) {
std::cout << "PriorityCustomer copy constructor called" << std::endl;
}

PriorityCustomer& operator=(const PriorityCustomer& rhs) {
std::cout << "PriorityCustomer copy constructor called" << std::endl;
priority = rhs.priority;
return *this;
}

// 打印信息用于测试
void printProfile() {
print();
std::cout << "Priority: " << priority << std::endl;
}
private:
int priority;
};

上面的派生类中,看似我们定义的 copy 函数已经将 PriorityCustomer 的每一个成员变量都复制了(即 priority)但是实际上这里我们漏掉了基类当中的成员变量(即 namedate)。通过以下方式测试:

1
2
3
4
5
PriorityCustomer c1("A", "2020-10-03", 1);
PriorityCustomer c2("B", "2020-10-01", 2);
c2 = c1; // no complete copied!
c1.printProfile();
c2.printProfile();

编译运行后结果如下:

1
2
3
4
5
PriorityCustomer copy constructor called
name: A, date: 2020-10-03
Priority: 1
name: B, date: 2020-10-01
Priority: 1

可以发现,这同样是一次不完整的复制。具体来说,这里两种 copy 函数呈现出来的是两种不同的行为:

  • 对于 copy 构造函数而言,由于我们在初始化列表中没有指定基类的构造函数(的参数),所以编译器会自动调用基类的默认构造函数(不带参数)的版本进行基类成员变量的初始化。所以假如我们没有空参数的基类构造函数的话,此时编译会报错。这也是一种防止我们漏复制的方法。
  • 但是对于 operator= 而言,函数内部只涉及到赋值(并不包含构造),所以哪怕我们漏掉了基类成员变量的处理,编译和链接都能顺利通过!这一点我们需要非常注意。

在派生类中正确地定义 copy 函数

由于上面种种问题,对于继承类中的 copy 函数,我们应该记得处理派生类本身的变量复制以外,我们还需要调用基类的相应的 copy
函数。如下例所示(完整代码见 ex3):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class PriorityCustomer: public Customer {
public:
PriorityCustomer(const std::string& name_, const std::string& date_, int priority_) : Customer(name_, date_), priority(priority_) {}
PriorityCustomer(const PriorityCustomer& rhs): Customer(rhs), // 调用基类 copy constructor
priority(rhs.priority) {
std::cout << "PriorityCustomer copy constructor called" << std::endl;
}

PriorityCustomer& operator=(const PriorityCustomer& rhs) {
std::cout << "PriorityCustomer copy constructor called" << std::endl;
Customer::operator=(rhs); // 调用基类 copy assignment
priority = rhs.priority;
return *this;
private:
int priority;
}

测试输出如下:

1
2
3
4
5
6
PriorityCustomer copy constructor called
Customer copy assignment called.
name: A, date: 2020-10-03
Priority: 1
name: A, date: 2020-10-03
Priority: 1

不要互相调用 copy 函数

尽管两种 copy 函数功能上看上去很类似,但是我们还是得记住不要在其中一种 copy 函数内调用另一种 copy 函数以期减少代码量,理由如下:

  • operator= 中调用 copy consturctor 是不合理的,因为这需要构造一个新的对象,这不是我们想要的。
  • 同样地,在 copy consturctor 中调用 operator= 也是不合理的,因为前者希望构造一个新的对象,而后者则只是在现有对象中进行赋值。

如果我们觉得 operator= 和 copy consturctor 中有大量重复代码,一个比较好的做法是将重复代码抽离出作为一个新的函数用于吃初始化(可以将其命名为 init())。