第一章见 Effective C++ 学习笔记 第一章:让自己习惯 C++
第二章见 Effective C++ 学习笔记 第二章:构造、析构、赋值运算
第三章见 Effective C++ 学习笔记 第三章:资源管理
第四章见 Effective C++ 学习笔记 第四章:设计与声明
Postpone variable definitions as long as possible.
尽可能的在需要使用变量(或指类型对象)时,再定义对象,尽可能避免无用的构造和析构操作。
如果构造对象之后,紧接着需要对对象进行赋值操作,更好的设计是直接调用有参数的构造函数直接完成。如:
std::string enc; // 调用默认构造函数
enc = pas; // 再调用赋值运算符重载函数
// 以上不如直接替换为以下:
std::string enc(pas); // 调用 copy 构造函数
如果一个对象是循环内变量,若满足以下两个条件:
则将循环内变量外提,在循环之前定义;否则,建议在循环内定义对象。
原因是,前者的代价是:1 次构造 + 1 次析构 + n 次赋值;后者的代价是:n 次构造 + n 次赋值。(n 为循环次数)
建议使用后者的原因是,尽可能的控制对象的作用域。
Minimize casting.
传统的 C 转型方式有两种:
(T)expression // 将 expression 转型成 T 类型
T(expression) // 同理,函数样式转型
C++ 中提供了新型转型:
const_cast<T> (expression) // const 属性去除转型,C 转型没有这个功能
dynamic_cast<T> (expression) // 安全继承向下转型(base 到 derive),谨慎用
reinterpret_cast<T> (expression) // 低级转型,比如将指针类型转型成整形,不常用
static_cast<T> (expression) // 比较常用,强制转换,但不支持 const_cast 的效果
建议使用新型转型,原因:
谨慎使用转型动作,尽可能用其他设计来替代转型动作。
dynamic_cast 转型可能会非常慢,尽量避免使用,可使用多态的方案替代。
或者将转型动作藏起来。
Avoid returnning “handles” to object internals.
本条款中所提的 handles 是指引用、指针等类型,可以间接访问到对象内部数据。
举例说,如果我们设计了一个类内公有成员函数,返回一个类私有成员的引用:
// 假设类 Rec 中有指针私有数据 pData,指向 Point 类型的对象 point
Point& Rec::getPoint() const { return pData->point; }
我们这样的设计,因为加了 const,本身是不希望 point 数据被修改,虽然 getPoint 本身无法修改 point 的值了,但其返回 point 数据的引用,用户可以通过引用来操作 point,所以,本质上并没有保证封装性。
成员变量的封装性最多只等于返回其 handles 的函数的访问级别。
我们可以通过修改为以下形式来避免问题:
const Point& Rec::getPoint() const { return pData->point; }
返回 handles 还会带来另一个潜在的问题,这便是如果返回的 handles 生存期长于 它所指向的对象时,导致 handles 被挂起(dangling)的问题。比如返回指针指向的对象仅仅是函数内部拷贝的临时对象的情况。
有时的设计必须这么做,那便不得不做,但尽可能避免。
Strive for exception-safe code.
当异常抛出时,具有异常安全性的函数应该具有:
如下例子:
class M {
public:
void changeB(std::istream& img);
private:
Mutex mutex; // 图像改变时需要互斥
Image* bgImg; // 保存当前图像
int imageChanges; // 用来记录图像改变几次
};
void M::changeB(std::istream& img) {
lock(&mulex); // 获得互斥器
delete bgImg; // 删除旧的图像
++imageChanges; // 图像更改次数增加
bgImg = new Image(img); // 更换新的图像
unlock(&mutex); // 释放互斥器
}
我们习惯会这么写代码,但是,这不是异常安全性的函数。如果 new Image(img) 时抛出异常,则互斥锁无法释放(违反:不泄露任何资源),而且imageChanges 实际已经改变,且bgImg 将为空指针(均违反:不允许产生错误数据)。
异常安全性的函数提供三种保证之一:
通常来说比较困难,只要使用了自定义的类型,比如容器类,就难免会遇到内存不足的异常(bad_alloc)。如果真的可以做到是最好。
对于上例来说,强烈保证可以通过资源对象和智能指针来管理 bgImg 成员,在之前的条款中大概都提到过。
class M {
std::shared_ptr<Image> bgImg;
...
};
void M::changeB(std::istream& img) {
Lock ml(&mutex); // 将 mutex 交给资源对象 ml 管理
bgImg.reset(new Image(img)); // 这里的 bgImg 是智能指针
++imageChanges;
}
ml 对象来管理 mutex,将不再需要 unlock 操作,这个操作实际放在了 Lock 类的析构函数中,在退出 changeB 函数时会自动调用。而 bgImg 的使用,可以保证在 new Image(img) 完全正常的情况下,才修改 bgImg,如果 new Image(img) 出现异常,则不会进入 reset 函数内部,进而能够保证原始图像不变。
另一种非常巧妙的办法是 copy and swap 方法。也就是,先复制一个副本,对副本做好操作,再用 swap 将副本和原始对象做交换,我们还记得,swap 函数是需要保证不抛出异常的函数。
struct PM { // 我们将所有的类资源拿出来单独做一个结构体
std::shared_ptr<Image> bgImg;
int imageChanges;
};
class M {
private:
Mutex mutex;
std::shared_ptr<PM> pIm;
};
void M::changeB(std::istream& img) {
using std::swap; // 条款 25 中描述很详细
Lock ml(&mutex);
std::shared_ptr<PM> pNew(new PM(*pIm)); // copy 操作,获取副本 pNew
pNew->bgImg.reset(new Image(img)); // 修改副本
++pNew->imageChanges; // 修改副本
swap(pIm, pNew); // 完成交换
} // 最后还会自动释放 mutex,new 出来的资源由 pNew 交给 pIm,不用担心 pNew 析构
有些时候,我们很难做到强烈保证,这是因为连带影响。意思是,虽然我们自己的代码做到了高保证,但是我们的代码调用了一些其他代码,那些代码却没有做到高保证,那么实际上我们的代码也不是高保证。
所谓之,异常安全性级别,通常等价于那段代码中最差的安全性保证的级别。
当然,还有另一个原因是,强烈保证通常需要提供更多的资源来完成,比如为了 copy and swap,我们需要额外的一个资源副本,如果在一些场合下不适合提供这种条件,则我们只能做到基本保证。
将异常安全性做的越好的趋势,是一种期许,并没有说异常安全性差就是错,只是说那样做不够好。