• 《Effective C++》简明笔记上


    在学习算法导论的过程中,我深深地震撼于自己笔下C++代码的丑陋。于是我决定捧起这本《Effective C++》。本来打算看完这本书,写一篇完整的笔记博文,但是刚刚看到一半,我已经跃跃欲试地想动手改善我的代码了。所以,我将写完的这部分笔记整理成单独的一篇博文。

    1. 视C++为一个语言联盟。

    • C++ 包括 C & OO C++ & Template C++ & STL

    2. 使用 const,enum,inline 代替#define。

    3. 尽可能使用 const

    • const 修饰指针的不同含义
      char* const p1 = "hello"; // 固定指针:不能使p2指向其他对象
      const char* p2 = "hello"; // 固定数据:不能修改p2指向的对象
    • const 修饰函数时的不同含义
      class Text
      {
      public:
          const std::size_t length() const; 
          // 返回文本的长度
          // 第一个 const 表示 函数返回一个常量,不可作为左值使用
          // 第二个 const 表示 函数不修改 Text 类中的数据成员(除了 static 和 mutable 修饰过的)
      private:
          char* data;
      };

      当类的实例被声明为 const 时,只能调用被第二个 const 修饰过的函数。

    4. 保证使用对象前进行初始化

    • 内置数据类型不进行初始化,所有对象类型都有默认初始化函数。
    • 在构造函数中对类成员赋值并不是真正意义的初始化,进入构造函数体时,对象成员都已经调用过默认初始化函数了。应当使用初始值列进行初始化。
      class Person
      {
      public:
          Person():
            name(), // 调用 string 类默认构造函数
            sex_isMale(true) // 内置类型,必须初始化
          {};
          Person(const std::string& tname, const bool& isMale):
            name(tname), // 调用 string 类的复制构造函数
            sex_isMale(isMale)
          {};
      private:
          std::string name;
          bool sex_isMale;
      };
    • 不同编译单元的 non-local static 对象(如全局对象)的初始化顺序不可控,将对象放在一个全局函数中,并将对象其声明为静态成员。第一次调用该函数时必定会初始化该对象。

    5. 了解C++默默做的事

    • 如果没有声明任何构造函数,则编译器自动为类实现默认构造函数。
    • 如果你没有实现,编译器会自动为类实现复制构造函数,复制运算符(operator=)函数,析构函数。
    • 如果类中包含引用类型的成员 或 const 成员,则编译器不会实现复制运算符函数。因为更改 引用 或 const 成员是不允许的。

    6. 如果不想使用编译器自动生成函数,就该明确拒绝

    • 将不想使用(如果你不声明,编译器就会自动生成)的函数 声明 为 private,并且不实现它(防止友元类调用)。
    • 声明基类,并在基类中将不想使用的函数声明为 private,且不实现。继承基类的派生类,编译器不会自动生成相应函数。
      class Uncopyble
      {
      protected:
          Uncopyble(){}
          ~Uncopyble(){}
      private:
          // 声明但不实现复制构造函数,其派生类无法调用基类的复制构造函数(由于private)
          // 因此编译器无法自动生成派生类的复制构造函数(默认的逻辑上,该函数应当调用基类的复制构造函数)
          Uncopyble(const Uncopyble&); 
          // 复制操作符函数同理
          Uncopyble& operator=(const Uncopyble&); 
      };

    7. 为多态基类声明virtual析构函数

    • 如果基类的析构函数不是虚函数,那么通过基类指针引用的派生类对象,在其销毁时,只能销毁积累部分,而不能销毁派生类部分。

    8. 不让异常逃离析构函数

    • 析构函数往往并不由类的使用者亲自调用,因此在析构函数中抛出的异常难以捕捉。
    • 如果在对象的销毁阶段确实可能抛出异常(比如,由于网络原因,关闭远程数据库失败),应该另外实现一个使用者亲自调用的销毁函数如close(),在该函数中抛出异常,以此给用户以机会处理异常。在析构函数中,检查用户是否调用了销毁函数:如果用户已经调用过,则不再调用该函数;如果用户未曾调用过,则调用该函数,在出现异常的情况下,并吞下异常或直接停止程序(用户没有立场抱怨,因为我们已经给了他机会)。

    9. 不在构造函数或析构函数中调用virtual函数

    • 派生类初始化时,先对基类部分初始化,然后才是派生部分。基类的构造函数运行时,派生类还不存在,此时调用虚函数并试图完成派生类中相应地逻辑:如果该虚函数有实现,就仅仅调用虚函数而不是派生类中的函数;如果没有定义虚函数,会出现连接错误。
    • 析构函数同理。

    10. 令 operator= 返回一个对 this 的引用

    • 这样就可以使用连等式来赋值了。

    11. 在 operator= 中处理自我赋值

    • 在 operator= 中需要考虑参数就是自身的情况,也要注意语句顺序,以保证“异常安全性”。

    12. 复制对象时不要忘了对象的每一个部分

    • 如果自己实现复制构造函数和复制运算符函数(而不使用编译器自动生成的版本),一定要记得将所有的成员都复制过来,编译器不会因为这是个复制构造函数或operater=而帮你检查。
    • 如果你在派生类中自己实现以上两种函数,一定要记得显式地调用基类的相应函数,编译器不会帮你调用。
      class Person{
      public:
          Person(){}
          Person(const std::string& tname):name(tname){}
      private:
          std::string name;
      };
      class Citizen:public Person{
      public:
          Citizen():Person(),married(false){}
          Citizen(Citizen& pcitizen):
            Person(pcitizen), // 显式调用基类的复制构造函数, 
                              // 注意传入的是pcitizen而不是pcitizen.name,
                              // 因为调用的是基类的复制构造函数而不是构造函数,
                              // 而且基类的private也不允许你这样做
            married(pcitizen.married){} // 派生类部分的初始化
      private:
          bool married;
      };

    13. 以对象管理资源

    • 所谓资源,往往是由 new 运算符产生的,由指针控制和管理的对象和数组。它们通常分配在堆(而不是栈)上,所以程序流程发生变化时,这些对象和数组不能自动销毁(而分配在栈上的对象是可以的),需要手动销毁。
    • RAII:对象的取得时机就是最好的初始化时机,两种常用的RAII对象(智能指针):std::auto_ptr<T>和std::tr1::shared_ptr<T>,前者的复制方案为“转让所有权”,后者的复制方案为“计数器”。
    • 一个RAII对象示例
      class FontHandle;
      
      class Font{
      public:
          Font(FontHandle* ft):
            f(ft){}
          ~Font(){delete f;}
      ...
      private: FontHandle* f; };

      Font类的实例并不分配在堆上,但其指针成员 f 指向的对象 *f 分配在堆上。当流程变化时,Font 实例被正常销毁,其析构函数被调用,析构函数中将指针成员指向的对象销毁。这就保证了 *f 没有泄露。

    14. 在资源管理器中小心 copying 行为

    • 资源管理器的资源:即指针指向的对象,由资源管理器维护。当自己实现智能指针对象时,考虑一下四种 copying 行为。
      • 禁止复制
      • 引用计数(如shared_ptr,需用到类的静态成员)
      • 深度复制
      • 转让所有权(如auto_ptr)
    • 考虑着四种 copying 行为的目的就是,避免在析构函数中多次试图销毁指针所指对象,或者完全不销毁。

    15. 在资源管理器中提供对原始资源的访问

    • 往往对 RAII 对象实现 operator-> 和 operator* 以实现对资源对象内部成员的访问。
    • 实现显式转换函数,如 Font.get() 返回资源对象。
    • 实现隐式转换函数,如 Font::operator FontHandle() 返回资源对象。此时,Font 对象 可 隐式转换为 FontHandle 对象,但也会带来部分风险。
      class Font{
      public:
          Font(FontHandle* ft):
            f(ft){}
          ~Font(){delete f;}
          operator FontHandle(){return *f;}
          FontHandle get(){return *f;}
          ...
      private:
          FontHandle* f;
      };

    16. 成对使用 new 与 delete 时采取相同的形式

    • 事实上,编译器中实现了两种指针,指向单个变量/对象 的 和指向变量/对象 数组的。使用 new 和 delete 时应当采取对应的形式。
      std::string* s1 = new std::string("hello");
      std::string* s2 = new std::string[100];
      ...
      delete s1;
      delete [] s2;

    17. 以独立语句将 newed 对象置入智能指针中

    • 考虑这样做:
      Font f1(new FontHandle);

      独立语句的含义是:不将该语句拆开,也不将其合并到其他语句中,这样可以确保资源不被泄露,如:

      // 不将其拆开
      FontHandle* fh1 = new FontHandle;
      ... // 发生异常怎么办?
      Font f1(fh1);
      
      // 不将其合并
      AnotherFunction(Font(new FontHandle), otherParameters /*发生异常怎么办?*/);

    18. 让接口易于使用,难于误用

    • 让接口易于使用,一般来说,就是尽量保持与内置类型(甚至STL)同样的行为。比如,你应当为 operator+ 函数返回 const 值,以免用户对计算结果进行赋值操作,内置类型不允许(对 int 型变量,语句 a+b=c 不能通过编译,所以你的类型也应该尽量保持同样的性质,除非你有更好的理由);又比如,对象的主要组成部分如果是一个数组,那么数组的长度的成员名最好使用 size 而不是 length,因为 STL 也这么做了。
    • 让借口难于误用,包括在类中限制成员的值(比如 Month 类型不可能表示 13 月),限制类型上的操作,在工厂函数中返回智能指针。

    19. 设计 class 犹如 设计 type

    20. 用 pass-by-reference-const 替换 pass-by-value

    • 为函数传递参数时,使用使用 const 引用传递变量。在定义函数时:
      class Person{...};
      class Citizen:public Person{...};
      
      bool validatePerson(Person psn){...} // 值传递,尽量不要这样做
      bool validatePerson(const Person& psn){...} // const引用传递
    • 使用const引用类型传递函数参数的好处在于:
      • 免去不必要的构造开销:如果使用值传递,实参到形参的过程调用了类型的复制构造函数,而引用不会。
      • 避免不必要的割裂对象:如果函数的参数类型是基类,而函数中又调用了派生类中的某种逻辑(即调用了基类中的虚函数),那么值传递的后果就是,形参仅仅是个基类对象,虚函数也仅仅就调用了虚函数自己(而不是派生类中的函数)。
      • 对于C++内置类型和STL迭代器,使用值传递,以保持一致性。

    21. 必须返回对象时,不要试图返回 reference

    • 考虑一个有理数类:
      class Rational{
      public:
          Rational(int numerator=0, int denominator=1):n(numerator),d(denominator){}
      private:
          int n, d;
      };
    •  任何有理数可用分数表示, n 和 d 分别为分子和分母,他们都是 int 型的。现在考虑为该类实现乘法,我们希望它能像内置类型一样工作。

      Rational x = 2;
      Rational y(1,3);
      Rational z = x*y*y; // z等于2/9
    • 我们可能会令函数返回引用类型(尤其是意识到20条中关于值传递的种种劣迹后):
      class Rational{
          ... 
      private:
          // 错误的代码
          friend const &Rational operator* (const Rational& lhs, const Rational& rhs){
              Rational result(lhs.n*rhs.n, lhs.d*lhs.d);
              return result;
          }
          ...
      };

       result对象在 operator* 函数结束后就销毁了,但我们返回了它的引用!这个引用指向 result 对象原先的位置(编译器往往用指针实现引用),而且该位置在栈上!不仅无效,而且危险。

    • 我们也可能用new运算符建立一个新的对象(以防止在函数结束后被销毁),并返回该对象的引用:
      // 错误的代码
      friend const &Rational operator* (const Rational& lhs, const Rational& rhs){
              Rational* result = new Rational(lhs.n*rhs.n, lhs.d*lhs.d);
              return *result;
          }

       这次,*result 对象不会因为函数结束而销毁了,它分配在堆上。但问题是,谁来负责销毁它?尤其是上文 z=x*y*y 中,由 y*y 计算而得到的临时变量,几乎不可能正常销毁。

    • 正确的做法是:
      // 正确的代码
      friend const Rational operator* (const Rational& lhs, const Rational& rhs){
          return Rational(lhs.n*rhs.n, lhs.d*lhs.d);
      }

      虽然产生了构造消耗,但这是值得的。返回的对象 z 分配在栈上,也就是说会在适当的时候销毁,而原先函数中的临时变量也正常销毁了。

    22. 将成员变量声明为 private

    23. 以 non-member 和 non-friend 函数替换 non-member 函数

    • 类的 public 方法越多,其封装性就越差,内部实现弹性就越小。设计类的时候应由其细心。对于一些便利函数(这些函数往往只调用函数的其他 public 方法),可考虑将其放置在类外。C++允许函数单独出现在类外,即使在C#等语言中,也可以使其出现在 工具 对象中。
    • 将类外的函数与类声明在同一个命名空间中是不错的选择。

    24. 如果函数的所有参数都需要类型转换,采用 non-member 函数

    • 第21条中的代码已经体现出这一条的意思了。这一条大致就是希望 Rational 对象能像其他内置对象一样,直接参与运算。比如,希望这样:
      Rational x(2,5);
      Rational y = x*2;
      Rational z = 2*x;
      • 首先,Rational 构造函数没有使用 explicit 修饰,这意味着 x*2 可以正常计算,因为这会调用 x.operator*(Rational& a),而整数 2 会隐式转换成 Rational 对象。(等等,在第21条中我们好像没有定义,x.operator*(Rational& a)函数?对,这是因为其中的代码已经遵循了本条的忠告,定义了 non-member 函数。)
      • 如果在 Rational 中定义了x.operator*(Rational& a),那么计算 z 时会遇到困难,因为系统会试图调用 Int32.operator*(Rational& a),这根本没有定义。所以,我们在代码中并没有定义成员函数,而是定义了友元函数 Rational operator*(Rational& a, Rational& b),正如在第21条的代码中显示的那样。

    25. 考虑写一个不抛出异常的 swap 函数

    • std::swap 函数采取复制构造的方法,效率比较低。
      namespace std{
          template <typename T>
          void swap(T& a, T& b){
              T temp(a);
              a = b;
              b = a;
          }
      }
    • 为自己的类实现 swap 方法并 特化 std::swap

      class Person{
      private:
          void* photo;
      };
      
      namespace std{
          template <> // 特化std::swap方法
          void swap<Person>(Person& a, Person& b){
              std::swap(a.photo, b.photo);
          }
      }

      当自己的类较大时,可在类中定义swap方法,并在 std::swap<YourClass> 中调用该方法。

    26. 尽量延后变量定义式的时间

    • 仅当变量第一次具有“具有明显意义的初值”时,才定义变量,以避免不必要的构造开销。避免这样做:
      std::string s; // 调用默认构造函数
      ...  // 如果发生异常呢,如果含有某个return语句呢?第一次调用构造函数的开销被浪费了
      s = "Hello"; // 再一次调用构造函数,第一次调用构造函数的开销依然被浪费了

       应当这样做:

      std::string s("Hello"); // “hello”是具有明显意义的初值,只调用了一次构造函数 

    27. 尽量少做转型动作

    • 四种转型动作
      • const_cast:消除对象的常量性
      • dynamic_cast:动态转换,开销较大。使用的场合往往是:想要在派生类上执行派生类的某个函数,但是手头上只有基类的指针指向该对象。
      • reinterpret_cast:依赖于编译器的低级转型
      • static_cast:强迫隐式转换,类似于C风格的转换,例如将int转换为double等
    • 不要试图在派生类的成员函数中,通过dynamic_cast将(*this)转换为基类对象,并调用基类成员函数。
      class Person{
      public:
          void showMessage(){}
      };
      class Citizen:public Person{
      public:
          void showMessage(){
              dynamic_cast<Person>(*this).showMessage(); // 错误,这样转型得到的并不是期望的“基类对象
          }
      };

       而应当这样做:

      Person::showMessage(); // 这就对了

    28. 避免返回 handles 指向对象内部部分

    • handle 包括指针,引用,迭代器,用来获取某个对象,以前被翻译成句柄。
    • 在函数的方法中返回对象内部成员的 handle 可能遭致这样的风险:返回的 handle 比对象本身更长寿,当对象销毁后,handle 所指向的区域就是不确定的。
    • string 和 vector 类型的 operator[] 就返回了对象内部成员的 handle ,这只是例外。

    29. 为“异常安全”而作的努力是值得的

    • 函数异常安全类型:
      • 基本承诺:如果异常抛出,程序内的所有元素仍然在有效状态下,没有任何元素受到损坏(如释放了指针指向资源却没有为其指定新的资源,该指针通向了不确定性)。
      • 强烈保障:如果异常抛出,程序内的所有元素保持函数调用前的状态。
      • 不throw异常:承诺绝不抛出异常。
      • 一个函数异常安全的程度取决于所调用函数中异常安全程度最弱的。
    • copy & swap 策略:为对象的数据制造一份副本,并对副本进行修改。如果发生异常,抛弃副本并返回;如果成功,则将对象数据与副本数据做 swap 操作,swap 操作承诺绝不抛出异常。
    作者:一叶斋主人
    出处:www.cnblogs.com/yiyezhai
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
  • 相关阅读:
    urql 高度可自定义&&多功能的react graphql client
    使用vault pki 为nginx 生成tls 证书文件
    使用vault pki engine 方便的管理证书
    使用terraform 生成自签名证书
    Kapitan 通用terraform&& kubernetes 配置管理工具
    sqler 集成 terraform v0.12 生成资源部署文件
    检查cgroup v2 是否安装
    centos 较新版本kernel安装方法
    tbls ci 友好的数据库文档化工具
    graphql-query-rewriter 无缝处理graphql 变更
  • 原文地址:https://www.cnblogs.com/yiyezhai/p/2938172.html
Copyright © 2020-2023  润新知