Effective C++(二)

几乎每一个class都会有一个或多个构造函数,一个析构函数,一个copy assignment操作符。必须保证它们的行为正确。本文提供的引导可以让你将这些函数良好的集结在一起,形成classes的脊柱。

  • 了解C++默默编写并调用了哪些函数

当你声明一个空类时,编译器就会为它声明一个copy构造函数,一个copy assignment操作符和一个析构函数。此外如果你没有声明任何构造函数,编译器也会为你声明一个default构造函数。这些函数都是public且inline

1
2
3
4
5
6
7
8
9
10
11
12
class Empty{};
//编译器生成的
//当这些函数被需要(即被调用),它们才会被编译器创建出来
class Empyt{
public:
Empty(){...}
Empyt(const Empty& rhs){...}
~Empty(){...}
Empty& operator=(const Empty& rhs){...}
};

default构造函数和析构函数主要是给编译一个用来放置幕后代码(比如调用base classes和non-static成员变量的构造和析构函数)的地方,注意,编译器产生的析构函数是non-virtual的。
至于copy构造函数和copy assignment操作符,编译器创建的版本只是单纯地将来源对象的每一个non-static成员变量拷贝到目标对象(浅拷贝)。

当类中含有引用、常量成员变量时,编译器将会拒绝自动生成copy构造函数和copy assignment操作符。因为引用在第一次指向对象之后,就不允许被修改为指向不同对象,而常量就更显而易见了。另外,当base classes将copy assignment操作符声明为private,那么编译器同样将拒绝为其派生类生成一个copy assignment操作符。

请记住:

  • 编译器可以暗自为class创建default构造函数,copy构造函数,copy assignment操作符,以及析构函数。

  • 若不想使用编译器自动生成的函数,就应该明确拒绝

声明函数为private,并且不实现他们(即定义)。即使在member函数或friend函数之内使用它们,连接器会因为找不到其具体实现而报错。
如果希望在编译时期就报错,那么可以通过继承关系来实现,即将基类的copy构造函数和copy assignment操作符声明为private,当任何人尝试拷贝派生类对象时,编译器自动生成的函数会尝试调用其基类的函数,此时编译器就会报错。


  • 绝不在构造和析构过程中调用virtual函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Transaction{
public:
Transaction();
virtual void logTransaction() const = 0;
};
Transaction::Transaction(){
...
logTransaction();
}
class BuyTransaction:public Transaction{
public:
virtual void logTransaction() const;
};

base class构造期间virtual函数绝对不会下降到derived class.非正式的说法或许比较传神:在base class构造期间,virtual函数不是virtual函数。其真正原因是:base class构造期间derived class对象实体并没有构造出来,其vbtl同样没有被构造出来,因此如果此时virtual函数访问derived class内定义,将产生为定义行为,这是很危险。在derived class对象在base class构造期间,其对象的类型是base class。不只virtual函数会被编译器解析至base class,若使用运行期类型信息(如dynamic_cast, typeid),也会把对象视为base class类型。 相同的道理也适用于析构函数。


  • 令operator= 返回一个reference to *this
1
2
3
4
5
6
7
8
9
int x, y, z;
x = y = z = 15;
x = (y = (z = 15));
Widget& operator= (const Widget& rhs){
...
return *this;
}

同时在赋值操作时,需要额外考虑这样一种情况:“自我赋值”

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
class Bitmap{...};
class Widget{
...
private:
Bitmap* pb;
};
Widget& Widget::operator=(const Widget& rhs){
//如果此处传入的rhs和this是同一个对象,那么就会导致异常。
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
//改进方法1:证同测试
if(this == &rhs) return *this;
//上述方式在后续new Bitmap过程中如果抛出异常,则仍会有问题,即pb指向一块已经被删除的内存
//改进2:临时保存原有内存地址,在new成功之后,再进行释放。
Bitmap * pOrig = pb;
pb = new Bitmap(*rhs.pb);
delete pOrig;
return *this;
//改进3:compare-and-swap
Widget& operator=(const Widget& rhs){
Widget temp(rhs);
//自定义swap函数,实现this对象和temp对象数据的交换
swap(temp);
return *this;
}

  • 复制对象时勿忘其每一个成分
  • 复制所有local成员变量
  • 调用所有base classes内的适当的copying函数
  • 不要尝试以某个copying函数实现另一个copying函数。应将共同机能放进第三个函数中,并由两个copying函数共同调用。