item26 尽可能延后变量定义式出现的时间

太快定义变量并且最终未使用,仍需耗费这些成本(构造&析构)

  1. std::string encryptPassword(const std::string& password){
  2. using namespace std;
  3. string encrypted;
  4. if(password.length()<MinmumPasswordLength){
  5. throw logic_error("Password is too short");
  6. }
  7. //...
  8. return encrypted;
  9. }

上述代码中encrypted太早定义了,因为可能并不会对他赋值;
此外虽然它定义了,但是却没有初始化,因此下次会调用拷贝赋值或者拷贝构造; :::tips 尽量延后定义式出现的时间,甚至应该尝试延后知道能够给他初值为止; :::

  1. std::string encryptPassword(const std::string& password){
  2. using namespace std;
  3. if(password.length()<MinmumPasswordLength){
  4. throw logic_error("Password is too short");
  5. }
  6. //...
  7. std::string encrypted(password);
  8. return encrypted;
  9. }

item27 尽量少做转型动作

转型语法的形式

通常有三种不同的转型形式:

  • 旧式转型:
    1. (T) expression //将expression转型为T
    1. T(expression)

  • 新式转型

    1. const_cast<T> (expression);
    2. dynamic_cast<T> (expression);
    3. reinterpret_cast<T> (expression);
    4. static_cast<T> (expression)
  • const_cast :用来将常量性转除,C++中唯一可以进行此操作的转型操作符

  • dynamic_cast:执行安全向下转型。无法通过旧式语法执行,可能耗费较大运行成本
  • static_cast:强迫隐式转换。但是无法将const转成non-const(但是反之可以); :::tips 使用新式转型可以带来好处:

  • 更容易在代码中查找出来

  • 转型动作的目标更狭窄,编译器更好判断错误运用。 :::

转型带来的开销和问题

转型实际上是会带来开销或者产生代码的,比如用一个基类指针去指向派生类的地址,可能会有offset(偏移量)被施加在派生类上,从而取得正确的基类指针值。
此外,转型可能会产生临时对象或者副本。

  1. class Window
  2. {
  3. private:
  4. /* data */
  5. public:
  6. virtual void onResize();
  7. };
  8. class SpecialWindow:public Window
  9. {
  10. private:
  11. /* data */
  12. public:
  13. virtual void onResize(){
  14. static_cast<Window>(*this).onResize();
  15. }
  16. };

上述实现中,derived classonResize先是将自己的**this**指针转型成**Base class**类型,然后调用基类的**onResize**,这是不行的! :::tips 此时调用的并不是当前对象上的函数,而是转型动作建立的一个临时副本上的函数
如果onResize改动了内容,那么实际上改动的只是副本对象的内容,而非当前对象的内容。 ::: 正确方式:

  1. virtual void onResize(){
  2. Window::onResize();
  3. }

dynamic_cast带来的开销

dynamic_cast的许多实现版本执行速度相当慢,例如有一个很普遍的实现版本是基于class名称的字符串比较。每实现一次就会调用一次strcmp。 :::info 之所以需要dynamic_cast,通常是因为想要在一个用户认定的derived class对象身上执行derived class的操作函数,但是手上只有指向base class的指针或者引用。 ::: 一般有两种解决方案:

  • 使用容器并且在其中存储直接指向derived class对象的指针(通常是智能指针);
  • 使用虚函数。 :::tips

  • 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast

  • 如果转型是必要的,尽量将他隐藏在某个函数背后,客户可以随后调用此函数而不用将转型放在自己的代码内
  • 尽量使用新式的转型。 :::

item28 避免返回handles指向对象内部成分

有些程序返回reference的形式的成员变量;这在某种程度上会改变成员变量的封装性 :::tips

  • 成员变量的封装性只等于返回其reference的函数的访问级别。
  • 如果const成员函数返回一个reference,后者所指数据与对象自身有关,且他又被存储在对象之外,那么函数调用者可以修改此数据。
  • 返回的如果是迭代器或者指针也会有同样的问题 ::: :::info 解决方案:只对他们的返回值加上const即可; :::

item29 为“异常安全”而努力是值得的

异常安全性函数

异常被抛出时,异常安全性函数会 :::info

  • 不泄露任何资源,保证动态创建的资源会被释放。
  • 不允许数据被破坏,保证要么原始数据完好,要么新的数据替换。 ::: 异常安全性函数会提供以下三个保证之一: :::tips

  • 基本承诺:如果异常被抛出,程序内任何事物仍保持在有效状态下。

  • 强烈保证:如果异常被抛出,程序状态不变;(换句话说,无异常则完全成功,否则恢复到调用函数前状态)
  • 不抛掷保证,承诺绝不抛出异常。 ::: 一般而言,不抛置保证很难实现,常常回在基本和强烈中间选择。

    copy and swap策略

    :::info
  1. 为打算修改的对象做出一个副本
  2. 在该副本上做出一切修改
  3. 若有修改抛出异常,则原来对象仍未改变
  4. 否则将修改过的副本和源对象在一个不抛置异常swap中交换 :::

:::danger 函数提供的“异常安全保证”通常最高只等于其调用之各个函数的“异常安全保证”中的最弱者 :::

item30 透彻了解inlining的里里外外

inline函数是什么?

inline是C++关键字,在函数声明或定义中,函数返回类型前加上关键字inline,即可以把函数指定为内联函数。这样可以解决一些频繁调用的函数大量消耗栈空间(栈内存)的问题。关键字inline必须与函数定义放在一起才能使函数成为内联函数,仅仅将inline放在函数声明前面不起任何作用。inline是一种“用于实现”的关键字,而不是一种“用于声明”的关键字。对于内联函数,C++有可能直接用函数体代码来替代对函数的调用,这一过程称为函数体的内联展开。

:::tips Key:用空间换时间 ::: inline函数通常置于哪里?
inline函数通常在头文件中,因为函数需要在编译时吧inline展开,替换时需要知道该函数长得什么样子。
inline常不做用于哪些函数? :::info

  • virtual函数
  • 构造和析构 ::: template通常也被放在头文件内,理由和inline一样:编译器需要在编译的时候知道template的全部内容。但是这并不意味着我们要把template声明为inline

item31 将文件间的编译依存关系降至最低

对于编译依存性高的代码,一个文件改动了可能所有涉及该文件的代码都需要重新编译;
通过用前置声明替代定义的方式并不能改变这一问题,因编译器在编译期间需要知道对象的大小。

handle class

pointer to implementation

:::tips 一个技巧是,通过声明的依存性替换定义的依存性。将需要的类分割成两个:

  • 一个只提供接口
  • 一个负责实现接口

这样修改时只需要编译负责实现的部分? :::

  1. //Person.h的内容
  2. class PersonImpl;
  3. class Date;
  4. class Address;
  5. class Person{
  6. public:
  7. Person(string& name,Date& birthday,Address& addr);
  8. string name();
  9. string birthday();
  10. string address();
  11. private:
  12. shared_ptr<PersonImpl> pImpl;
  13. }
  14. //-------------------------
  15. #include"Person.h"
  16. #include"PersonImpl.h"
  17. Person::Person(string& name,Date& birthday,Address& addr)
  18. :pImpl(new PersonImpl(name,birthday,addr))
  19. {}
  20. string Person::birthday()const{
  21. return pImpl->birthday();
  22. }
  • 如果使用引用或者指针可以完成任务,就不要使用对象
  • 如果能够,尽量以class的声明替代class的定义式。
  • 为声明和定义提供不同的头文件。

    interface class

    另一种方式实现handle class 的形式是使用将Person类变成一个特殊的抽象基类。这种类的目的是为了描述派生类的接口,通常不带成员变量也没有构造函数,只有一个virtual的析构函数。
    1. class Person{
    2. public:
    3. virtual ~Person();
    4. virtual std::string name() const =0;
    5. virtual std::string birthday() const =0;
    6. virtual std::string address() const =0;
    7. }
    客户必须以Personpointer或者reference来撰写应用程序,除非interface class的接口被修改了,否则客户不需要重新编译。