0%

5 实现

大多数情况下,适当提出classes或class templates定义以及functions和function
templates声明,是最花费心力的两件事。太快定义变量可能造成效率上的拖延;过度使用转型可能导致代码变慢又难维护,又找来微妙的理解错误;返回对象内部数据的handle可能会破坏封装并留给客户dangling
handles;未考虑异常带来的冲击则可能导致资源泄露和数据败坏;过度热心地inlining可能引起代码膨胀;过度耦合(coupling)则可能导致让人不满意的冗长build
times。

条款26:尽可能延后变量定义式的出现时间

  1. 当定义了一个类型带有构造函数或析构函数,就得付出构造成本和析构成本。如果函数可能抛出异常,最好将未使用的变量尽量延后声明(为何不声明定义分割是因为将函数构造出来再赋值比直接构造时指定初值效率低)。
  2. 对于在循环结构中定义在里头还是外头需要权衡:如果赋值成本比“构造+析构”成本高,那么倒不如在循环结构中定义。

条款27:尽量少做转型动作

  1. 回顾转型语法:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // old-style casts
    (T)expression
    T(expression)

    // C++-style casts
    /*
    const_cast通常用来将对象常量行转除(cast away he constness)
    */
    const_cast<T>(expression)

    /*
    dynamic_cast主要用来执行安全向下转型(safe downcasting),用来决定某对象是否归属继承体系中的某个类型,它是唯一无法由旧式语法执行的动作,也很耗费重大运行成本的转型动作
    */
    dynamic_cast<T>(expression)

    /*
    reinterpret_cast意图执行低级转型,它取决于编译器,不可移植。它可以这么用,将pointer to int转型为一个int
    */
    reinterpret_cast<T>(expression)

    /*
    static_cast用来强迫隐式转换(implicit conversions),它基本什么都能做,就是无法将const转为non-const,这只有const_cast办得到。
    */
    satic_cast<T>(expression)
    有人觉得转型起始什么都没做,其实这是错误的观念:底层转型表述几乎会产生一些代码:
    1
    2
    3
    4
    class Base { ... };
    class Derived : public Base { ... };
    Derived d;
    Base *pb = &d;
    这里不过是建立一个base class指针指向一个derived class对象,但有时候两个指针值并不相同。这种情况下会有个偏移量offset)在运行期被施行于Derived*指针身上,才取得正确Base*指针值。
    我们很容易写出似是而非的代码(在其他语言可能是真的)。例如derived classes内的virtual函数代码调用base class对应函数:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    /* 
    这段程序将*this转型为Window,它调用的不是当前对象上的函数,
    而是*this对象的baseclass部分的一个临时副本。
    */
    class Window
    {
    public:
    virtual void onResize() { ... }
    ...
    };
    class SpecialWindow: public Window
    {
    public:
    virtual void onResize()
    {
    static_cast<Winddow>(*this).onResize();
    // 正确做法是:
    Window::onResize();
    ...
    }
    ...
    };

条款28:避免返回handles指向对象内部成分

  1. 一个成员函数返回referencespointersitertors,这些统统都是所谓的handles(号码牌,用来取得某个对象)。而一个返回“代表对象内部数据”的handle,随之而来的是降低对象封装性的风险。

条款29:为”异常安全“努力是值得的

  1. 当异常被抛出时,带有异常安全性Exception safety)的函数一般不泄露任何资源并不允许数据败坏。这样的函数可分为三种保证:基本型、强烈型和不抛出异常型。
  2. 强烈保证以copy-and-swap实现出来,保证一个动作要么全部完成,否则不进行任何变动。

条款30:透彻了解inlining的里里外外

  1. inline内嵌函数可以免除函数的调用成本,编译器最优化机制通常被设计来浓缩不含函数调用的代码。然而过多的inline可能导致增加目标码object coed)大小。在内存有限 的机器上,过度热衷inlining会使程序体积太大,即使拥有虚内存,造成的代码膨胀会导致额外的换页行为paging),降低指令高速缓存的命中率instruction cache hit rate),以及伴随这些问题而来的效率损失。
  2. Inline函数通常被置于头文件内,因为多数build environments在编译过程中进行inlining,在链接期完成inlining。inlining在多数C++程序中是编译期行为。
  3. inline是一个向编译器发出的申请,而不是强制命令。大部分编译器拒绝过于复杂(如有循环和递归)的函数inlining。对于virtual函数也会,因为virtual意味着”wait,知道runtime才确定调用哪个函数“。

条款31:将文件间的编译依存关系降至最低

  1. #include指示符提供的定义式和其含入文件之间形成了一种编译依存关系compilation dependency)。如果这些头文件中有任何一个被改变,或者这些头文件所依赖的其他头文件有任何改变,那么每个含有class的文件就得重新编译。解决方案是通过前置声明配合指针或引用类型声明来减少编译依赖。