Effective C++(五)接口实现

来源:互联网 发布:纪梵希禁忌香水知乎 编辑:程序博客网 时间:2024/04/25 15:57
  1. 条款26:尽可能延后变量定义式的出现时间

    • 请记住:尽可能延后变量定义式的出现,这样做可增加程序的清晰度并改善程序效率。
    • 只要你定义了一个变量而其类型带有一个构造函数或析构函数,那么当程序的控制流到达这个变量定义式时,你便得承受构造成本;变量离开其作用域,要承受析构成本。即使这个变量最终并未被使用,仍需耗费这些成本。
    • 最好延后变量的定义式,直到确实需要它。
    • 你应该尝试延后变量定义直到能够给它初值实参为止。以“具明显意义之初值”。
    • 除非(1)你知道赋值成本比“构造+析构”成本低,(2)你正在处理代码中效率高度敏感的部分,否则你应该采用方法B
      //方法A:定义于循环外Widget w;for(int i=0; i <n; ++i){    w = 某个值;}//方法B:定义于循环内for(int i=0; i <n; ++i){  Widget w = 某个值;}
  2. 条款27:尽量少做转型动作

    • 请记住:
      • 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_casts。
      • 如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进它们的代码内。
      • 宁可使用C++ style(新式)转型,不要使用旧式转型
    • C++旧式转型:(T)expression; 或者 T(expression)
    • 新式转型:
      const_cast<T> (expression)dynamic_cast<T> (expression)reinterpret_cast (expression)static_cast <T> (expression)
      • const_cast通常被用来将对象的常量性转除。它也是唯一有此能力的C++-style转型操作符
      • dynamic_cast主要用来执行“安全向下转型”,也就是用来决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作
      • reinterpret_cast意图执行低级转型,实际动作(及结果)可能取决于编译器,这也就表示它不可移植
      • static_cast用来强迫隐式转换,例如将non-const对象转为const对象,或将int转为double。它也可以用来执行上述多种转换的反向转换。但它无法将const转为non-const
    • 任何一个类型转换(无论通过转型操作而进行的显示转换,或通过编译器完成的隐式转换)往往真的令编译器编译出运行期间执行的码。
    • 单一对象(例如derived class对象)可能拥有一个以上的地址(例如以“Base 指向它”时的地址和“以derived ”指向它)。
    • 对象的布局方式和它们的地址计算方式随编译器的不同而不同
    • dynamic_cast的许多实现版本速度相当慢。深度继承或多重继承的成本更高!
    • 优良的C++代码很少使用转型。我们应该尽可能隔离转型动作
  3. 条款28:避免返回handles指向对象内部成本

    • 请记住:避免返回handles(包括reference、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌(dangling handles)”的可能性降至最低
    • 第一,成员变量的封装性最多只等于“返回其reference”的函数的访问级别。第二,如果const成员函数返回一个reference,后者所指数据与对象自身有关联,而它又被存储于对象之外,那么这个函数的调用者可以修改那笔数据。
    • Reference、指针和迭代器使所谓的handles,而返回一个“代表对象内部数据”的handles,随之而来的是“降低对象封装性”的风险。
    • 你绝对不该令成员函数返回一个指针指向“访问级别较低”的成员函数。这样,后者实际访问级别会提高如前者
    • 虚吊号码牌(dangling handles):这种handles所指东西不复存在。常见的来源就是函数返回值。
  4. 条款29:为“异常安全”而努力是值得的

    • 请记住:
      • 异常安全函数即使发生异常也不会泄露资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈性、不抛异常型
      • “强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。
      • 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。
    • 较少的码就是较好的码,因为出错机会比较少
    • 异常安全函数提供三个保证之一:
      • 基本承诺:如果异常被抛出,程序内的任何事情仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态。然后程序的现实状态恐怕不可预料。
      • 强烈保证:如果异常被抛出,程序状态不会改变。如果函数成功,就是完全成功。如果函数失败,程序会回复到“调用函数之前”的状态
      • 不抛掷保证:承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。作用于内置类型身上的所有操作都提供nothrow保证
    • copy and swap:为你打算修改的对象(原件)做出一份副本,然后在那副本身上做一切必要修改。若有任何修改动作抛出异常,原对象仍保持未改变状态。待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换。实现上通常是将所有“隶属对象的数据”从原对象放进另一个对象内,然后赋予原对象一个指针,指向那个所谓的实现对象,这种手法被称为pimpl idiom
    • 一个软件系统要不就具备异常安全性,要不就全然否定。
    • 如果系统内有一个函数不具备异常安全性,整个系统就不具备异常安全性
    • 如果具备异常安全性:首先是“以对象管理资源”,那可阻止资源泄露。然后是挑选三个“异常安全保证”中的某一个实施于你所写的每一个函数身上。
  5. 条款30:透彻了解inlining的里里外外

    • 请记住:
      • 将大多数inlinin限制在小型、被频繁调用的函数身上
      • 不要只因为function templates出现在头文件中,就将它们声明为inline
    • 编译器最优化机制通常被设计用来浓缩那些“不含函数调用”的代码
    • inline是对此函数的每一个调用都以函数本体替换之。这样做可能增加你的目标码大小。inline造成的代码膨胀亦会导致额外的换页行为,降低指令高度缓存装置的击中率,以及伴随这些而来的效率损失。
    • inline只是对编译器的一个申请,不是强制命名。这项申请可隐喻提出,也就是定义于class定义式内。明确声明inline函数式在定义式前加上关键字inline。
    • inline函数通常一定被置于头文件内。inlining在大多数C++程序中是编译器行为。
    • Templates通常也被置于头文件内,因为它一旦被使用,编译器为了将它具现化,需要知道它长什么样子。编译期完成具现化动作比较常见。Templates的具现化与inlining无关。
    • 一个表面上看似inline的函数是否真是inline,取决于你的建置环境,主要取决于你的编译器。有时候虽然编译器有意愿inlining某个函数,还是可能为该函数生成一个函数主体。比如,如果程序要取某个inline函数的地址,编译器通常必须为此函数生成一个outlined函数本体。编译器通常不对“通过函数指针而进行的调用”实施inlining。有时候编译器会生成构造函数和析构函数的outline副本。而实际上,构造函数和析构函数往往是inlining的糟糕候选人。Derived构造至少一会陆续调用其成员变量和base class两者的构造函数,而那些调用(它们自身也会被inline)会影响编译器是否对此空白函数的inlining。
    • 大部分调试器面对inline函数都束手无策。慎重使用inline便是对日后使用调试器带来帮助。
    • 80-20经验法则:平均而言一个程序往往将80%的执行时间花费在20%的代码上头。
  6. 条款31:将文件间的编译依存关系降至最低

    • 请记住:
      • 支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classes和Interface classes
      • 程序库头文件应该以“完全且仅有声明式”的形式存在。这种做法无论是否涉及templates都适用
0 0