《Effective C++》——实现(item 26 ~ item 31)

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

  • "尽可能延后变量定义式的出现。这样做可增加程序的清晰度并改善程序效率。"

    • 不只应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。
    cpp 复制代码
    std::string encryptPassword(const std::string& password)
    {
        ...
        std::string encrypted;
        encrypted = password;
        encrypt(encrypted);
        return encrypted;
    }
    
    // 跳过毫无意义的默认构造过程
    std::string encryptPassword(const std::string& password)
    {
        ...
        std::string encrypted(password);
        encrypt(encrypted);
        return encrypted;
    }    
    • 考虑循环:
    cpp 复制代码
    // 方法 A:定义于循环外
    Widget w;
    for(int i = 0; i < n; i++)
    {
        w = ....;
        ...
    }
    
    // 方法 B:定义于循环内
    for(int i = 0; i < n; i++)
    {
        Widget w(...);
        ...
    }

    做法 A: 1个构造函数 + 1个析构函数 + n个赋值操作

    做法 B: n个构造函数 + n个析构函数

    除非 (1)你知道赋值成本比 "构造 + 析构" 成本低;(2)你正在处理代码中效率高度敏感的部分,否则你应该使用做法 B

Item 27:尽量少做转型动作

  • "宁可使用 C++ style (新式)转型,不要使用旧式转型。前者很容易识别出来,而且也比较有着分门别类的职掌。"

    • C 风格的转型动作
    cpp 复制代码
    (T)expression;
    T(expression);    // 函数风格的转型动作
    • C++ 新式转型
      • const_cast:将对象的常量行移除。是唯一有此能力的 C++ type 转型操作符。
      • dynamic_cast:用于在继承层次结构中进行指针或引用的转换,通常用于将基类指针转换为派生类指针。
      • reinterpret_cast:用于进行几乎任何类型之间的低级别类型转换。它直接解释数据的内存布局,而不考虑类型的实际含义,因此可能存在危险。
      • static_cast:用于在相关类型之间进行明确的类型转换。例如可以将 void* 指针转为 typed 指针。
  • "如果可以,尽量避免转型,特别是在注重效率的代码上避免 dynamic_cast。如果有个设计需要转型动作,试着发展无需转型的替代设计。"

  • "如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内。"

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

  • "避免返回 handles (包括 refrences、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助 const 成员函数的行为像个 const,并将发生 '虚吊号码牌(dangling handles)的可能性降至最低。'"
cpp 复制代码
class MyClass { 
private: 
    int data; 
public: 
    int& getData() { // 返回引用 
        return data; 
    } 
}; 

void clientCode() { 
    MyClass obj; 
    int& ref = obj.getData(); // 直接获取内部成员的引用 
    ref = 42; // 通过引用修改内部数据,破坏封装性 
}
cpp 复制代码
class GUIObject {...};

const rectangle boundingBox(const GUIObject& obj);

GUIObject* pgo;
...
const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft());
  • boundingBox(*pgo) 返回一个 rectangle 对象,但由于 boundingBox 的返回类型是 按值返回 ,它会返回一个临时对象(temporary object)。在表达式结束时,这个临时对象会被销毁。

  • upperLeft() 方法返回的是临时 rectangle 对象的成员。当你取其地址时,由于 rectangle 是一个临时对象,它在这一行代码执行后立即销毁,导致你保存的指针指向一个已经销毁的对象的内存。这就是悬空指针问题。

Item 29:为"异常安全"而努力是值得的

  • "异常安全函数(Exception-safe function)即使发生异常也不会泄漏资源或允许任何数据结构破坏。 这样的函数分为三种可能的保证:基本型、强烈型、不抛异常型。"

    • 基本保证:

    即使在异常发生的情况下,程序中的对象状态仍然保持一致,不会出现资源泄漏或数据损坏。虽然某些对象的状态可能会发生改变,但不会处于不确定或损坏的状态。函数在异常发生后,所有的资源(如内存、文件句柄等)依然会被正确释放,且所有的对象处于有效状态,但对象的值可能没有恢复到异常发生之前的状态

    • 强烈保证:

    如果异常被抛出,程序状态不改变。调用这样的函数需要有这样的认知:如果函数成功,就是完全成功,如果函数失败,程序会回到 "调用函数之前" 的状态

    • 不抛掷(nohrow)保证:

    保证函数在任何情况下都不会抛出异常。这是最强的异常安全保证,通常用于某些关键的操作,如析构函数、内存释放函数或在异常处理期间需要执行的代码中(例如在 catch 语句中调用的函数)。

  • "'强烈保证' 往往能够以 copy-and-swap 实现出来,但 '强烈保证' 并非对所有函数都可实现或具备现实意义。"

    copy-and-swap 思想是先创建一个临时对象,执行所有的操作(如拷贝、赋值等),确保这些操作不会影响原对象。如果操作成功,则通过 swap 函数交换原对象和临时对象的内容,从而确保要么操作成功,要么原对象保持不变。

  • "函数提供的 '异常安全保证' 通常最高只等于其所调用之各个函数的 '异常安全保证' 中的最弱者。"

    函数的异常安全性不能超过它所调用的最弱的函数的异常安全性。如果某个函数中的某一部分代码只能提供基本保证,那么即使该函数中的其他代码能够提供更强的异常安全性,整个函数的异常安全性最终仍然是基本保证。

Item 30:透彻了解 inlining 的里里外外

  • "将大多数 inlining 限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级(binary upgradability)更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。"

  • "不要只因为 function templates 出现在头文件,就将他们声明为 inline。"

    如果你正在写一个 template 而你认为所有根据此 template 具现出来的函数都应该 inlined,请将此 template 声明为 inline;但如果你写的 template 没有理由要求它所具现的每一个函数都是 inlined,就应该避免将这个 template 声明为 inline

    审慎使用 inline:在考虑将模板函数声明为内联时,应该评估函数的大小、复杂性以及调用频率。只有在小型、频繁调用的函数时,内联才可能带来真正的性能优势。

Item 30:将文件间的编译依存关系降到最低

  • "支持 '编译依存性最小化' 的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是 Handle classes 和 Interface classes。"

      1. 编译依存性最小化:依赖声明式而非定义式
      • 依赖声明式 :指在代码中,尽可能地使用类的前向声明,而不是直接包含类的头文件。例如,只在必要时才包含完整的类定义,而在其他情况下只需提供类的前向声明。这样可以减少文件间的依赖,从而加快编译速度。
      • 依赖定义式:指直接包含类的头文件,导致编译器需要处理类的完整定义。如果头文件中有很多依赖,任何一个文件的改动都可能触发大范围的重编译,增加编译开销。
      1. Handle 类

      Handle 类(也称为指针类或 Pimpl 模式,即 Pointer to Implementation)是一种设计模式,它将类的接口和实现分离,通过将实现细节隐藏在指针后面来最小化编译依赖性。具体做法是,在头文件中只暴露类的接口,而将实现细节放在源文件中,从而隐藏实现的复杂性。

      例子:

      cpp 复制代码
      // MyClass.h
      class MyClassImpl;  // 前向声明实现类
      
      class MyClass {
      public:
          MyClass();
          ~MyClass();
      
          void doSomething();
      
      private:
          MyClassImpl* pImpl;  // 指向实现的指针
      };
      cpp 复制代码
      // MyClass.cpp
      #include "MyClass.h"
      #include "MyClassImpl.h"  // 实现类的定义
      
      MyClass::MyClass() : pImpl(new MyClassImpl()) {}
      MyClass::~MyClass() { delete pImpl; }
      
      void MyClass::doSomething() {
          pImpl->doSomething();  // 委托给实现类
      }

      好处

      • 用户只需要包含 MyClass 的头文件,而无需了解实现细节,这减少了编译时的依赖。
      • 改变实现类的代码不会导致依赖它的代码重新编译,从而降低了编译开销。
      1. Interface 类

      Interface 类是一种纯虚类(abstract class),它只声明接口(方法),而没有提供任何实现。通过让其他类继承这个接口类来实现具体的功能。这种方法也有助于最小化依赖,因为使用接口的类只需要依赖接口的声明,而不需要依赖具体实现类的定义。

      例子:

      cpp 复制代码
      // IShape.h
      class IShape {
      public:
          virtual ~IShape() = default;
          virtual void draw() const = 0;  // 纯虚函数
      };
      cpp 复制代码
      // Circle.h
      #include "IShape.h"
      
      class Circle : public IShape {
      public:
          void draw() const override {
              // Circle 的具体绘制实现
          }
      };
      cpp 复制代码
      // ClientCode.cpp
      #include "IShape.h"
      
      void renderShape(const IShape& shape) {
          shape.draw();  // 通过接口调用,而不需要知道具体类型
      }

      好处

      • 客户代码(例如 renderShape 函数)只依赖于接口类,而不需要包含具体实现类(如 Circle)的头文件,这减少了代码耦合。
      • 通过接口类,用户可以方便地扩展不同的实现,而不影响使用者的代码,增强了可扩展性和可维护性。
  • "程序库头文件应该以 '完全且仅有声明式'(full and declaration-only forms)的形式存在。这种做法不论是否涉及 templates 都适用。"

    即头文件应该只包含函数或类的声明,而将具体的实现放在源文件中。

相关推荐
szuzhan.gy2 分钟前
DS查找—二叉树平衡因子
数据结构·c++·算法
火云洞红孩儿27 分钟前
基于AI IDE 打造快速化的游戏LUA脚本的生成系统
c++·人工智能·inscode·游戏引擎·lua·游戏开发·脚本系统
FeboReigns2 小时前
C++简明教程(4)(Hello World)
c语言·c++
FeboReigns2 小时前
C++简明教程(10)(初识类)
c语言·开发语言·c++
zh路西法2 小时前
【C++决策和状态管理】从状态模式,有限状态机,行为树到决策树(二):从FSM开始的2D游戏角色操控底层源码编写
c++·游戏·unity·设计模式·状态模式
.Vcoistnt2 小时前
Codeforces Round 994 (Div. 2)(A-D)
数据结构·c++·算法·贪心算法·动态规划
小k_不小2 小时前
C++面试八股文:指针与引用的区别
c++·面试
沐泽Mu2 小时前
嵌入式学习-QT-Day07
c++·qt·学习·命令模式
ALISHENGYA3 小时前
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之分支结构(实战训练三)
数据结构·c++·算法·图论
GOATLong3 小时前
c++智能指针
开发语言·c++