前言
最近在看《Effective C++》这本书,这博客相当于是个浓缩版的总结吧。
在这里你可以大致游览下在 C++ 开发中前人给了我们哪些建议,有机会我觉得最好还是可以看看原书,因为里面会有不少具体的例子告诉你为什么这么做以及这么做的好处。
一、让自己习惯 C++
1. 视 C++ 为一个语言联邦
我们可以视 C++ 为一个由相关语言组成的联邦而非单一语言,例如:
- C:包括区块 blocks,语句 statements,预处理 preprocessor,内置数据类型 build-in data types,数组 arrays,指针 pointers 等。
- C++:包括类 classes,封装 encapsulation,继承 inheritance,多态 polymorphism,virtual 函数等。
- Template C++:泛型编程 generic programming。
- STL:标准模板库 standard template library。
2. 尽量使用 const
,enum
,inline
替换 #define
- 对于常量,最好使用 const 或者 enum。
- 对于函数宏,最好使用 inline 函数。
3. 尽可能使用 const
- 将某些东西声明为 const 可帮助编译器侦测出一些错误用法。
- 注意 const 和 non-const 是可以发生 重载 的,如果你要这么做,那么令 non-const 版本调用 const 版本可避免代码重复,如:
class TextBlock {
public:
const char& operator[] (std::size_t position) const {
// do something
return text[position];
}
char& operator[] (std::size_t position) {
// 调用 const 版本的重载函数,避免代码重复
return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
}
private:
std::string text;
};
4. 确定对象在使用前初始化
其中构造函数最好使用使用成员初值列,而不是在构造函数中使用赋值操作。如:
Person::Person(const std::string& name, const int age)
:mName(name), mAge(age) // 成员初始列
{
// mName = name; // 赋值操作
// mAge = age; // 赋值操作
}
二、构造、析构、赋值运算
5. 了解 C++ 默默编写和调用的函数
编译器会自动给类创建 default 构造函数、copy 构造函数、赋值操作符(operator=)、析构函数。
class Empty {}; // 等同于下面写法
class Empty {
public:
Empty() {} // default构造函数
Empty(const Empty& rhs) {} // copy构造函数
~Empty() {} // 析构函数
Empty& operator=(const Empty& rhs) {} // copy assignment 操作符
};
6. 若不想编译器自动生成上述函数,就要明确拒绝
将相应的成员函数声明为 private
,并不予实现即可。
7. 为多态基类声明 virtual 析构函数
如果一个类带有任何 virtual 函数,它都应该拥有一个 virtual 析构函数。
8. 别让异常逃离析构函数
析构函数不要抛出异常。如果析构函数中调用的函数可能抛出异常,那么也要捕捉并吞掉这个异常或结束程序。
9. 绝不在构造函数或析构函数中调用 virtual 函数
在父类构建的过程,virtual 函数还没有下降到子类中去。
10. 赋值操作符都应返回一个 *this
的引用
operator=
,operator+=
,operator-=
等等赋值运算符,都返回一个 reference to *this。如:
Widget& operator=(const Widget& rhs) {
// do something
return *this;
}
11. 在 operator=
中处理 “自我赋值”
- 确保任一函数如果操作多个对象时,其中多个对象是同一对象时,其行为仍然正确。
- 确保当对象自我赋值时有良好的行为。例如:
Widget& operator=(const Widget& rhs) {
if (this == &rhs) {
return *this; // 如果是自我赋值,就不做任何事
}
// do something
return *this;
}
12. 复制对象时勿忘其每一个成分
- 复制函数应该确保复制 “对象内的所有成员变量” 以及 “调用基类适当的复制函数” 完成完整的复制。
- 不要尝试在赋值运算符(operator=)中调用复制构造函数,亦或是在复制构造函数中调用赋值运算符。如果你的复制构造函数和赋值运算符代码基本一样,消除重复代码的做法是写一个 private 的 init() 方法供两者调用。
三、资源管理
13. 以对象管理资源
- 把资源放进对象内,我们便可依赖 析构函数 自动调用的机制确保资源被释放。
- 使用智能指针(如
auto_ptr
和shared_ptr
)来管理资源类,避免你忘记 delete 资源类。
14. 在资源管理类中小心 copy 行为
- 复制资源类时必须一并复制它所管理的资源,资源的 copy 行为决定资源类的 copy 行为。
- 常见的 copy 行为有:禁止复制、使用引用计数法、进行深拷贝、转移资源的拥有权。
15. 在资源管理类中提供对原始资源的访问
例如提供一个 get()
方法获取原始资源。
16. 成对使用 new
和 delete
时要采取相同形式
- 在 new 表达式中使用 [],就必须在 delete 的时候也使用 []。
- 在 new 表达式中不使用 [],也一定不要在 delete 的时候使用 []。
17. 以独立的语句将 new 的对象置入智能指针中
避免发生异常时,导致难以察觉的资源泄漏产生。如:
std::tr1::shared_ptr<Widget> pWidget(new Widget);
processWidget(pWidget);
// 不建议下面这样做,如果 processWidget 发生异常可能造成泄漏
// processWidget(std::tr1::shared_ptr<Widget> (new Widget));
四、设计与声明
18. 让接口容易被正确使用,不易被误用
- 保持 “一致性” 使得接口容易被正确使用。例如 STL 容器的接口就十分一致,都有一个名为
size
的成员函数返回目前容器的大小。 - “阻止误用” 的方法包括建立新类型、限制类型上的操作、束缚对象值,以及消除客户的资源管理责任。
19. 设计 class 犹如设计 type
- 新 type 的对象应该如何被创建和销毁?
- 初始化和赋值该有什么样的差别?
- 新 type 如果被 passed by value(值传递)意味着什么?即复制构造方法该怎么实现。
- 新 type 存在哪些继承关系?
- 新 type 需要什么样的转换?即将其它类型转换为 type 类型的行为。
- 函数或成员的访问权限?public、protected、private。
- 新 type 有多么一般化?真的需要一个新 type 吗?
20. 宁以 pass-by-reference-to-const 替换 pass-by-value
- 尽量使用 “引用传递” 参数而不是 “值传递” 参数。前者更加高效且可避免切割问题。
- 对于 int 等内置类型,以及 STL 的迭代器和函数对象,使用 “值传递” 更好。
21. 必须返回新对象时,别妄想返回其引用
当一个 “必须返回新对象” 的函数,我们就直接返回一个新对象,而不要返回引用或者指针。
22. 将成员变量声明为 private
实现数据的封装性。且可以更方便的控制数据的访问。
23. 宁以 non-member、non-friend 替换 member 函数
- 这样做可以增加封装性、包裹弹性和机能扩充性。
- 非成员函数不会增加 “能访问 class 内私有变量” 的函数数量,具有更大的封装性。
24. 如果所有参数都需要类型转换,请采用 non-member 函数
如果一个有理数的类 Rational,我们常常喜欢直接使用 int 类型的数和 Rational 对象进行混合运算。那么使用 non-member 函数将是更好的选择,它允许每一个参数都进行隐式类型转换。
class Rational {
public:
Rational(int numerator = 0, int denominator = 1)
: mNumerator(numerator), mDenominator(denominator) {}
int numerator() const { return mNumerator; }
int denominator() const { return mDenominator; }
private:
int mNumerator; // 分子
int mDenominator; // 分母
};
const Rational operator*(const Rational& lhs, const Rational& rhs) {
return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
int main() {
Rational oneFourth(1, 4);
Rational result;
result = oneFourth * 2;
result = 2 * oneFourth; // 如果不是 non-member 的形式将不支持这种写法
}
25. 考虑写出一个不抛异常的 swap 函数
当系统的 std::swap
对你的类型效率不高时,提供一个 swap 成员函数,并确保这函数不会抛出异常。
如果提供一个 member swap,也该提供一个 non-member swap 来调用前者。例如:
class WidgetImpl {
public:
// ...
private:
int a, b, c; // 可能有很多数据
std::vector<double> v; // 意味着复制时间很长
// ...
};
// 这个 class 使用 pimpl 手法
// pimpl 是 pointer to implementation 的缩写
class Widget {
public:
Widget(const Widget& rhs) {}
Widget& operator=(const Widget& rhs) {
// ...
*pImpl = *(rhs.pImpl);
// ...
}
// 我们需要交换两个 Widget 对象,但是直接使用 std::swap 将导致很多多余的复制
// 好的做法是只交换两个 WidgetImpl* 指针对象即可,所以我们考虑写一个 swap 方法
void swap(Widget& other) {
using std::swap; // 这个声明至关重要,使得C++编译器能够找到正确的函数调用
swap(pImpl, other.pImpl); // 我们只需要交换 pImpl 即可
}
private:
WidgetImpl* pImpl;
};
// 提供一个 non-member swap 来调用 member swap
namespace std {
template<>
void swap<Widget>(Widget& a, Widget& b) {
a.swap(b);
}
}
五、实现
26. 尽可能延后变量定义式的出现时间
将一些变量定义式放在常规的参数检查后面,避免无用的构造方法带来的耗费。有助于增加程序清晰度和改善程序效率。
27. 尽量少做转型
- 如果可以,尽量避免转型,特别是注重效率的代码中避免使用效率低的
dynamic_cast
。 - 如果转型是必要的,尽量将其隐藏在函数背后。客户调用该函数而不需将转型放在自己代码内。
- 尽可能使用 C++ 风格的转型,而不要使用旧式的 C 风格转型。
28. 避免返回 handles 指向对象内部成分
- 引用、指针、迭代器 统统都是所谓的 handles。如果我们返回了对象的 handles,意味着对象被销毁时这个 handles 将会变得空悬,这是比较危险的。
- 我们应该尽量避免这么做,但有时候你必须这么做,例如
operator[]
操作允许你获取个别元素的引用。
29. 为 “异常安全” 而努力是值得的
- 基本承诺:如果异常抛出,程序仍保持有效状态,没有对象和数据结构会因此被破坏。
- 强烈保证:如果函数成功,就是完全成功,如果函数失败,程序会回复到调用前的状态。
- 不抛掷保证:承诺绝不抛出异常,作用于内置类型身上的所有操作都提供 nothrow 保证。
- 异常安全函数即使发生异常也不会泄漏资源或允许数据结构败坏。这样的函数区分三种可能的保证:基本承诺、强烈型、不抛异常型。
- 强烈保证往往能够以 copy-and-swap 实现出来,但它并非对所有函数都具备现实意义。
备注: copy-and-swap 是指拷贝并修改对象数据副本,函数调用完后,最后在一个不抛出异常的步骤里将修改后的数据和原件替换。
30. 了解 inline
里里外外
- 将大多数 inlining 限制在小型、频繁调用的函数身上。
- 不要只因为模板方法出现在头文件中就将它们声明为 inline,除非你认为模板具现化出来的函数都应该被 inline。
31. 将文件间的编译依存关系降至最低
- 依赖关系复杂导致的问题就是你修改了某个实现却需要编译很多文件,最好是 接口和实现分离。
- 支持 “编译依存最小化” 的一般构想是:相依于声明式,不相依于定义式。基于此构想的两个手段是 Handle classes 和 Interface classes。
- 程序库头文件应该以 “完全且仅有声明式” 的形式存在。