网站首页 文章专栏 改善程序与设计的55个做法.md
改善程序与设计的55个做法.md
创建于:2021-07-04 08:43:42 更新于:2024-11-23 08:10:09 羽瀚尘 466

前言

最近在看《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. 尽量使用 constenuminline 替换 #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;
};
12345678910111213

4. 确定对象在使用前初始化

其中构造函数最好使用使用成员初值列,而不是在构造函数中使用赋值操作。如:

Person::Person(const std::string& name, const int age)
    :mName(name), mAge(age) // 成员初始列
{
    // mName = name; // 赋值操作
    // mAge = age; // 赋值操作
}
123456

二、构造、析构、赋值运算

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 操作符
};
123456789

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;
}
1234

11. 在 operator= 中处理 “自我赋值”

  • 确保任一函数如果操作多个对象时,其中多个对象是同一对象时,其行为仍然正确。
  • 确保当对象自我赋值时有良好的行为。例如:
Widget& operator=(const Widget& rhs) {
    if (this == &rhs) {
        return *this; // 如果是自我赋值,就不做任何事
    }
    // do something
    return *this;
}
1234567

12. 复制对象时勿忘其每一个成分

  • 复制函数应该确保复制 “对象内的所有成员变量” 以及 “调用基类适当的复制函数” 完成完整的复制。
  • 不要尝试在赋值运算符(operator=)中调用复制构造函数,亦或是在复制构造函数中调用赋值运算符。如果你的复制构造函数和赋值运算符代码基本一样,消除重复代码的做法是写一个 private 的 init() 方法供两者调用。

三、资源管理

13. 以对象管理资源

  • 把资源放进对象内,我们便可依赖 析构函数 自动调用的机制确保资源被释放。
  • 使用智能指针(如 auto_ptrshared_ptr)来管理资源类,避免你忘记 delete 资源类。

14. 在资源管理类中小心 copy 行为

  • 复制资源类时必须一并复制它所管理的资源,资源的 copy 行为决定资源类的 copy 行为。
  • 常见的 copy 行为有:禁止复制使用引用计数法进行深拷贝转移资源的拥有权

15. 在资源管理类中提供对原始资源的访问

例如提供一个 get() 方法获取原始资源。

16. 成对使用 newdelete 时要采取相同形式

  • 在 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));
1234

四、设计与声明

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 的形式将不支持这种写法
}
123456789101112131415161718192021

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。
  • 程序库头文件应该以 “完全且仅有声明式” 的形式存在。

六、继承和面向对象设计

32. 确定你的 public 继承塑模出 is-a 关系

继承是 is-a 关系,指 “是一个”,即父类的每条属性和方法都应该适用于子类。

33. 避免遮掩继承而来的名称

  • 对于变量和函数,子类的名称会遮掩父类的名称,即使函数是重载的也不行。
  • 为了避免这个问题,可在子类中使用 using 声明或使用 转交函数(forwarding functions)。如:
class Base {
public:
    void fun1() {
        std::cout << "Base fun1" << std::endl;
    }
};
 
class Derived : public Base {
public:
    // 1. 使用 using 声明
    using Base::fun1;
    // 2. 使用转交函数
    void fun1() {
        Base::fun1();
    }
    // 同名函数会遮掩掉父类的 fun1()
    void fun1(int a) {
        std::cout << "Derived fun1: " << a << std::endl;
    }
};
1234567891011121314151617181920

34. 区分接口继承和实现继承

  • 纯虚函数(pure virtual)目的是让子类只继承函数接口,而不关心子类怎么去实现它。
  • 非纯虚函数(impure vitual)目的是让子类可选择的实现这个接口,或是使用父类的默认实现。
  • 成员函数(non-virtual)目的是让子类继承这个函数以及它的实现,而不去改变它(即所有子类都使用这个相同的实现)。

35. 考虑 virtual 函数以外的其它选择

当我们为了解决问题而寻找某个设计方法时,可以考虑一些 virtual 函数的替代方案,如:

  • 使用 non-virtual interface(NVI)手法,这是一种名为模版方法模式的设计模式,使用成员函数包裹虚函数。
  • 将虚函数替换为函数指针的成员变量。
  • 将虚函数替换为 tr1::function
  • 将继承体系内的虚函数替换为另一个继承体系内的虚函数(策略模式)。

36. 绝不重新定义继承而来的 non-virtual 函数

任何情况下都不该重新定义一个继承而来的 non-virtual 函数。

37. 绝不重新定义继承而来的缺省参数值

  • 如果是 non-virtual 函数,条款 36 已经介绍过不要重新定义了。
  • 如果是 viatual 函数,由于缺省参数都是编译期间静态绑定的,而 viatual 函数确是运行时动态绑定的,所以将会导致混乱。
  • 想要 virtual 函数表现出你想要的行为却有遇到麻烦,聪明的做法就是使用条款 35 里的替代方案。

38. 通过复合塑模出 has-a 或 “根据某物实现出”

  • 复合(也叫组合 composition)是 has-a 关系,指 “有一个”。我们将某种类型中包含其它类型的关系叫做复合。
  • 如果两个类之间并非严格的 is-a 关系,继承不再适用,我们应该选择复合的形式实现它们。

39. 明智而慎重地使用 private 继承

  • private 继承意味着父类所有非私有成员在子类中都是 private 的。这样就帮我们复用父类代码且防止父类接口曝光。
  • 但是私有继承意味着不再是 is-a 关系,而更像是 has-a 关系。我们总是可以通过复合的方式替代私有继承,并且更容易理解,所以无论什么时候,只要可以,我们还是应该选择复合。
  • 一种极端情况下,即我们有一个空白的父类,私有继承可以更小的占用空间。

40. 明智而慎重地使用多继承

  • 多继承比单继承复杂,而且多继承可能导致二义性,以及对 virtual 继承的需要。
  • 父类中存在数据的话,virtual 继承会增加大小、速度、初始化(及赋值)复杂度等成本,应尽量不使用 virtual 继承。
  • 多继承适用的一种场景是:public 继承某个接口类并 private 继承某个协助实现的类。

七、模板与泛型编程

41. 了解隐式接口和编译期多态

  • class 和 template 都支持接口和多态。
  • 在 class 中接口是显式的,即我们有具体的函数签名。多态则通过 virtual 函数发生在运行期。
  • 在 template 中接口是隐式的,基于有效的表达式。多态则是通过 template 的具现化和函数重载解析发生在编译期。

42. 了解 typename 的双重含义

  • 声明 template 参数时,前缀关键字 class 和 template 可互换。如:
// 下面两种写法对编译器来说完全相同
template<class T> class Widget;
template<typename T> class Widget;
  • 请使用 template 标识嵌套从属类型名称,但不能在 base class list(基类列)或 member initialization list(成员初始列)内以它作为基类的修饰符。

这一点我们慢点说,首先解释 “使用 template 标识嵌套从属类型名称”,看下面这个例子:

template<typename T>
void print(const T& container) {
    // 这样是不行的
    // T::const_iterator iter(container.begin());
    // 必须明确告诉 C++ 这个 T::const_iterator 是个类型而不是变量
    typename T::const_iterator iter(container.begin());
}

也许我们初衷是获取到集合的迭代器,但是由于泛型 T 可能并不是一个集合,它里面没有一个叫做 const_iterator 类型的嵌套类,编译器也可以把 const_iterator 当作一个类的静态变量,所以这里会存在歧义。因此我们必须使用 typename 告诉 C++ 这是一个类型而不是静态变量。
typename 被用来标识一个嵌套从属类型名称,然而还有一个例外,即不能在基类列或成员初始列内以它作为基类的修饰符。如:

// Base 是一个基类,Nested 是基类的一个嵌套类
template<typename T>
class Derive
    : public Base<T>::Nested { // base class list 中不允许 typename
public:
    explicit Derive(int x)
    : Base<T>::Nested(x) // member initialization list 中不允许 typename
    {
        typename Base<T>::Nested temp; // 其余情况需要加上 typename
    }
};

43. 学习访问模板化基类中的名称

编译器往往会拒绝在模板化基类中寻找继承而来的名称,因为基类的模板化可能被特化而那个特化版本也许并不会提供某一接口。所以我们可以使用 “this->” 指定,或使用 “using” 告诉编译器假设它存在。

template<typename T>
class Derived : public Base {
public:
    // 方式1,告诉编译器 func 位于基类中
    using Base<T>::func();
     
    void test() {
        // 方式2,假设 func 将被继承
        this->func();
    }
};

44. 将与参数无关的代码抽离 templates

Templates 会生成多个 classes 和函数,所以任何 template 代码都不该与某个造成膨胀的 template 参数产生相依关系。

45. 运用成员函数模板接受所有兼容类型

  • 请使用成员函数模板生成 “可接受所有兼容类型” 的函数。即完成一些类型的指针向上转型(例如 Derive* -> Base*)。
  • 如果声明的成员模板是用于 “泛化 copy 构造” 或 “泛化 assignment 操作” ,你还需要声明正常的 copy 构造和 assignment 操作符。
// 子类 Y 的智能指针指向父类 T 时就不会出问题了
template<class T>
class shared_ptr {
public:
    // copy 构造函数
    shared_ptr(shared_ptr const& rhs);
 
     // 泛化 copy 构造函数
    template<class Y>
    shared_ptr(shared_ptr<Y> const& rhs);
 
     // copy assignment
    shared_ptr& operator=(shared_ptr const& rhs);
 
     // 泛化 copy assignment
    template<class Y>
    shared_ptr& operator=(shared_ptr<Y> const& rhs);
};

46. 需要类型转换时请为模板定义非成员函数

为了让类型转换可能发生于所有实参上,我们需要一个非成员函数(条款 24)。在模板中,为了令这个函数被自动具现化,我们需要将它声明在类内部。为了在类内部声明非成员函数,唯一的办法是令它成为一个 friend。如:

template<typename T>
class Rational {
public:
    // 定义在class内的函数都暗自inline,对于复杂函数而言,我们可以令friend函数调用一个辅助函数避免
    friend const Rational operator*(const Rational& lhs, const Rational& rhs) {
        return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
    }
 
    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; // 分母
};
 
int main() {
    Rational<int> oneFourth(1, 4);
    Rational<int> result;
    result = 2 * oneFourth; // 如果不是 non-member 的形式将不支持这种写法
    cout << "numerator: " << result.numerator() << endl;
    cout << "denominator: " << result.denominator() << endl;
}

47. 请使用 traits classes(特性类)表现类型信息

  • Traits 不是 C++ 关键字或一个预先定义好的构件,而是一种技术,也是 C++ 程序员共同遵守的协议。它要求对于内置类型和用户自定义类型的表现必须一样好。Traits classes 使得 “类型相关信息” 在编译期可用,它们以 templates 和 “templates 特化” 完成实现。
  • 使用重载函数的形式,可以使 Traits classes 在编译期完成对类型的检测。

例子:
以 iterator_traits 为例介绍如何实现和使用 traits classes。STL 提供了很多的容器、迭代器和算法,其中的 advance 便是一个通用的算法,可以让一个迭代器移动给定距离:

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d); // d < 0 就向后移动
STL 迭代器回顾:
struct input_iterator_tag {}; // 输入迭代器,只能向前移动
struct output_iterator_tag {}; // 输出迭代器,只能向前移动
struct forward_iterator_tag: public input_iterator_tag {}; // 稍强的是前向迭代器,可以多次读写它的当前位置
struct bidirectional_iterator_tag: public forward_iterator_tag {}; // 双向迭代器,支持前后移动
struct random_access_iterator_tag: public bidirectional_iterator_tag {}; // 随机访问迭代器,可以支持 +=, -= 等移动操作

回到 advance 上,它的实现取决于 Iter 类型:

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d){
    // 针对 random access 迭代器使用这种方式实现
    if (iter is a random access iterator) {
        iter += d;
    } else {
        // 其它迭代器使用 ++ 或 -- 实现
        if (d >= 0) { while (d--) ++iter; }
        else { while (d++) --iter; }
    }
}

接下来就是怎么判断 Iter 的类型是否是 random access 迭代器了,也就是需要知道它的类型。这真是需要使用到 Traits classes 的地方。

实现 Traits classes:
用户自定义类型:

template<typename IterT>
struct iterator_traits {
    // 类型 IterT 的 iterator_category 就是用来标识迭代器的类别
    typedef typename IterT::iterator_category iterator_category;
};

指针类型:
指针本身就是可以支持随机访问(random access)的,所以我们对指针类型提供一个偏特化版本即可:

template<typename IterT> // template偏特化
struct iterator_traits<IterT*> { // 针对内置指针
    typedef random_access_iterator_tag iterator_category;
};

advance 实现:

不好的实现方式:

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d) {
    if (typeid(typename std::iterator_traits<IterT>::iterator_category) ==
        typeid(std::random_access_iterator_tag))
    // ...
}

IterTiterator_traits<IterT >::iterator_category 都是可以在编译期间确定的,而 if 判断却要在运行期间核定,这样不仅浪费时间,也会导致代码膨胀。
建议做法是建立一组重载函数(doAdvance),接受不同的类型,原函数(advance)调用这些重载函数。

// 原函数
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d) {
    // 调用不同版本的重载函数
    doAdvance(iter, d, typename std::iterator_traits<IterT>::iterator_category());
}
 
// 下面是一系列重载函数
// 随机访问迭代器
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag) {
    iter += d;
}
 
// 双向迭代器
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::bidirectional_iterator_tag) {
    if (d >= 0) { while (d--) ++iter; }
    else { while (d++) --iter; }
}
 
// 输入迭代器
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::input_iterator_tag) {
    if (d < 0 ) {
       throw std::out_of_range("Negative distance");
    }
    while (d--) ++iter;
}

48. 认识 template 元编程

  • 模板元编程(template metaprogramming TMP)可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率。上述 traits 就是 TMP 的应用。
  • TMP 可被用来生成 “基于政策选择组合” 的客户定制代码,而可用来避免生成对某些特殊类型不适合的代码。

八、定制 new 和 delete

49. 了解 new-handler 的行为

  • set_new_handler 允许客户指定一个函数,在内存分配无法获得满足时被调用。
  • new (std::nothrow) 是一个颇为局限的工具,它只适用于内存分配,后续的构造函数调用还是可能抛出异常。
  • new-handler 函数通常需要完成以下事情:
  1. 让更多内存可以使用。既然内存不够那么我们就想办法弄到更大的内存。
  2. 安装另一个 new-handle。例如本 new-handler 无法取得更多内存,那么就找一个可以取得更多内存的 new-handler 替代自己(只要调用 set_new_handler 即可)。
  3. 卸除 new-handle。即将 null 传给 set_new_handler,然后 new 操作会在内存分配失败后抛出异常。
  4. 抛出 bad_alloc 异常。
  5. 不返回。通常调用 abort 或者 exit。
  6. 了解 new 和 delete 的合理替换时机
    有许多理由需要写个自定义 new 和 delete。如:
  7. 检测运行错误。
  8. 收集和统计 heap 使用信息。
  9. 改善性能。例如定制化的分配器来增加分配和归还速度,减少内存管理器带来的额外开销,弥补分配器中的非最佳对齐位等。
  10. 编写 new 和 delete 时需固守常规
    operator_new 应该内含一个无限循环,并在其中尝试分配内存,如果无法满足内存需求,就该调用 new-handle。它也应该有能力处理 0 bytes 申请,还应该能处理 “比正确大小更大的(错误)申请” 。
    operator_delete 应该在收到 null 指针时不做任何事。同样也应该能处理 “比正确大小更大的(错误)申请” 。
  11. 写了 placement new 也要写 placement delete
    当 operator new 接受的参数除了那个 size_t 外还有其它,这称之为 placement new。placement delete 同理。
    当你写一个 placement operator new 时,也务必写出对应的 placement operator delete。否则可能发生内存泄漏。
    当你声明 placement new 和 placement delete,请确定不要无意识地遮掩了它们的正常版本。

九、杂项讨论

53. 不要忽略编译器的警告

请严肃对待编译器的警告,努力在编译器最高警告级别下争取 “无警告”。

54. 让自己熟悉包括 TR1 在内的标准程序库

  • C++ 标准程序库主要由 STL,iostream,locales 组成,并包含 C99 标准程序库。
  • TR1 组件都在 std::tr1:: 命名空间下,以下是组件实例:
  1. 智能指针。
  2. tr1::function,常用于实现回调函数。
  3. tr1::bind,能够做 STL 绑定器 bind1st 和 bind2nd 所做的每一件事,而又更多。
  4. Hash tables,用来实现 sets,multisets,maps 和 multi-maps。
  5. 正则表达式。
  6. Tuples 变量组,这是标准程序库 pair 的升级,pair 只能持有两个对象,而 tr1::tuple 可持有任意个数对象。
  7. tr1::array,本质是个 STL 化的数组,即一个支持成员函数 begin 和 end 的数组。不过它大小固定,并不使用动态内存。
  8. tr1::mem_fn,这是一个语句上构造与成员函数指针(member function pointers)一致的东西。同样容纳并扩充了 C++98 的 mem_funmem_fun_ref 的能力。
  9. tr1::reference_wrapper,一个让引用的行为更像对象的设施。
  10. 随机数生成工具,它大大超越了 rand。
  11. 数学特殊函数,包括 Laguerre 多项式、Bessel 函数、完全椭圆积分,以及更多数学函数。
  12. C99 兼容扩充,这是一大堆函数和模版用来将许多新的 C99 程序库特性带入 C++。
  13. Type traits,一组 traits classes(条款 47),用以提供类型的编译期信息。
  14. tr1::result_of,这是一个用来推导函数调用的返回值类型的模版。

55. 让自己熟悉 Boost

  • Boost 是一个社群,也是一个网站。致力于免费、源码开放、同僚复审的 C++ 程序库开发。
  • Boost 提供许多 TR1 组件的实现品,以及其它许多程序库。
目录