Effective C++(四)

条款10:令Operator=返回一个reference to *this

这个条款很直观,就是为了提供一个语法糖类似的功能使之可以:

1
x = y = z;

这样的连续赋值,按照惯例贴下样例实现吧:

1
2
3
4
5
6
7
8
9
10
11
class Object
{
public:
...
Object& operator=(const Object& rhs)
{
...
return *this;
}
...
}

条款11:在operator=中处理“自我赋值”

例如可能发生如下的情况:

1
2
3
Object o;

o = o;

最简单的方法就是做一次判断,如果是自己就什么都不做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Object
{
public:
...
Object& operator=(const Object& rhs)
{
if(this == &rhs)
{
return *this;
}
...
return *this;
}
...
}

当然书上也给出了另外一个应对异常安全的做法,就是定制一个swap(),这个方法会在条款29提及,以及笔者感觉特意为了这样的自我赋值做一个判断就够了。

条款12:复制对象时切勿忘其每一个成分

书中这个条款只是提醒了类变量成员的复制问题,而且前提是这些成员是对象成员,而不是一个指针,拥有指针时的复制就更加需要注意了。

先来简要介绍一下仅含对象成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class Base
{
public:
Base(const Base& rhs) : 初始化列表
{
...
}
...
Base& operator=(const Base& rhs)
{
...
}
private:
...
};

class Mem1
{
public:
Mem1(const Mem1& rhs) : 初始化列表
{
...
}
Mem1& operator(const Mem1& rhs)
{
...
}
private:
...
};

class Mem2
{
public:
Mem2(const Mem2& rhs) : 初始化列表
{
...
}
Mem2& operator(const Mem2& rhs)
{
...
}
private:
...
};

class Com : public Base
{
public:
Com(const Com& rhs) : Base(rhs), mem1(rhs.mem1), mem2(rhs.mem2), ...
{
...
}
Com& operator=(const Com& rhs)
{
Base::operator=(rhs);
this.mem1 = rhs.mem1;
this.mem2 = rhs.mem2;
...
}
private:
Mem1 mem1;
Mem2 mem2;
...
}

注意好复制构造函数以及赋值操作符重载基本上问题就不大了,就是编写的时候不要漏掉该处理的成员,调用基类的赋值操作符重载补全复制不到的基类成员。

但是当成员中存在指针的时候,就涉及到深度复制的问题了,设想上述的改动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Com : public Base
{
public:
Com(const Com& rhs) : Base(rhs), mem1(rhs.mem1), mem2(rhs.mem2), ...
{
...
}
Com& operator=(const Com& rhs)
{
Base::operator=(rhs);
this.mem1 = rhs.mem1;
this.mem2 = rhs.mem2;
...
}
Mem1& getMem1() const
{
return *mem1;
}
...
~Com()
{
delete mem1;
delete mem2;
}
private:
Mem1* mem1;
Mem2* mem2;
...
}

当使用该类的代码这么写:

1
2
3
4
5
6
7
Com* com1 = new Com(...);

Com* com2(com1); // com2 = com1;

delete com1;

com->getMem1().调用一些Mem1的成员函数

此时成员mem1mem2已经被释放了,所以com2中mem1指向的是一块未定义的内存,这样的调用是十分危险的,基本上都会导致程序意外终止,所以涉及到指针的复制要更加慎重。

Java里面,所有类的根类是Object,而其拥有的九个方法其中之一就是clone(),可惜C++编程太自由了,无法强制规定使用者要遵循一些编程规范。以笔者的经验来看,在C++里面一般涉及到深度复制的之后,也只能靠自身养成的良好的编程习惯,例如给Mem1类添加copy()或者clone()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Mem1
{
public:
...
Mem1* clone() const
{
Mem1* newObj = new Mem1(this->{自身一些非指针型成员数据的初始化列表});

// 如果具有指针型的成员则递归调用其clone或者copy函数,否则在此手动深度复制
}
private:
...
};

这样定义之后,Com可以这样深度复制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Com : public Base
{
public:
Com(const Com& rhs) : Base(rhs), mem1(rhs.mem1.clone()), mem2(rhs.mem2.clone()), ...
{
...
}
Com& operator=(const Com& rhs)
{
Base::operator=(rhs);
this.mem1 = rhs.mem1.clone();
this.mem2 = rhs.mem2.clone();
...
}
Mem1& getMem1() const
{
return *mem1;
}
...
~Com()
{
delete mem1;
delete mem2;
}
private:
Mem1* mem1;
Mem2* mem2;
...
}

经过这样的处理,即使原来的实例已经被释放了,但是因为复制的数据是完整的,两个对象之间并没有指针引用,所以就不会造成上述的错误。

当然实际应用场景中也有需要对象引用相同的成员的的需求,这时候要依据具体的需求设定好接口,不过clonecopy这类函数的语义最好设定为深度复制。

当然使用指针之后的问题就是需要手动管理内存了,在这里暂时不展开这个话题,毕竟C++内存管理一直是个大问题。