Item 26:尽可能延后变量定义式的出现时间
-
"尽可能延后变量定义式的出现。这样做可增加程序的清晰度并改善程序效率。"
- 不只应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。
cppstd::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。"
-
- 编译依存性最小化:依赖声明式而非定义式
- 依赖声明式 :指在代码中,尽可能地使用类的前向声明,而不是直接包含类的头文件。例如,只在必要时才包含完整的类定义,而在其他情况下只需提供类的前向声明。这样可以减少文件间的依赖,从而加快编译速度。
- 依赖定义式:指直接包含类的头文件,导致编译器需要处理类的完整定义。如果头文件中有很多依赖,任何一个文件的改动都可能触发大范围的重编译,增加编译开销。
-
- 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
的头文件,而无需了解实现细节,这减少了编译时的依赖。 - 改变实现类的代码不会导致依赖它的代码重新编译,从而降低了编译开销。
-
- 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 都适用。"
即头文件应该只包含函数或类的声明,而将具体的实现放在源文件中。