Effective C++ 学习笔记(一)习惯C++

参考书籍《Effective C++:改善程序与设计的55个具体做法(第三版)》

1. 视C++为一个语言联邦

2. 尽量以const,enum,inline替换#define

  • 为什么要?

    • 首先要知道使用“#define [宏名] [值]”,在编译时,会被预处理器将代码中的 宏名 直接替换掉。这样一来,当你调试时便完全追踪不到 宏名
    • 使用#define也不能够提供任何封装性,例如不能用定义一个类的专属变量。
    • 使用#define“实现”一个“函数”时,习惯了使用函数的你,可能会造成一些意想不到的错误:
      #define CALL_WITH_MAX(a,b) f((a)>(b)?(a):(b))
      
      MAX(++a, b);    // 若a>b,a将自加2次!
      
  • 示例:

    • 使用const常量替代#define
      const double FudgeFactor = 1.35;
      
      // 记得双const
      const char* const authorName = "Scott Meyers"
      
      // 有些编译器要求你需要在非头文件中对其定义,声明时赋值便可。但旧时编译器不允许static成员在声明式上赋值,你就需要在定义式上去赋值了
      //(但这视乎变量的值就没那么直观了,可以的话还是在头文件声明时就赋值吧)。
      
      // GamePlayer.h
      class GamePlayer{
      private:
          static const int NumTurns = 5;
          // static const int NumTurns;
      }
      
      // GamePlayer.cc
      const int GamePlayer::NumTurns;
      // const int GamePlayer::NumTurns = 5;
      
    • enum hack 。如果你不想让别人获得一个pointer或reference指向你的某个整数常量,enum可以帮助你实现这个约束,因为取一个enum的地址是不合法的。
      class GamePlayer{
      private:
          enum {NumTurns = 5};
      
          int scores[NumTurns];
      }
      
    • 使用template inline内联函数替换#define,你也可以享受零函数开销和类型安全性, 同时它也遵守作用域(scope)和访问规则。
      template<typename T>
      inline void CallWithMax(const T& a, const T& b) {
          f(a > b ? a : b);
      }
      

    ■ 对于单纯常量,最好以const对象或enums替换#defines。

    ■ 对于形似函数的宏(macros),最好改用inline函数替换#defines。

3. 尽可能使用const

  • 为什么要?:

    • 使用const可以避免很多人为错误。
  • 示例:

    • 它允许你告诉编译器和其他程序员某值应该保持不变。

      const double FudgeFactor = 1.35;
      
      const char *p1 = "greeting";        // 指针指向的数据不可修改
      char const *p2 = "greeting";        // 指针指向的数据不可修改
      char *const p3 = "greeting";        // 指针本身不可修改
      const char *const p4 = "greeting";  // 指针本身和指向的数据均不可修改
      
      // 与指针类似的迭代器
      const std::vector<int>::iterator it = vec.begin();      // 迭代器本身不可修改
      
      std::vector<int>::const_iterator it  = vec.begin();     // 所指物不可修改
      
    • 在一个函数声明式内,const可以和函数返回值、各参数、函数自身(如果是成员函数)产生关联。

      class Rational {...};
      const Rational operator* (const Rational& lhs, const Rational& rhs);
      
      if(a * b = c)   // 错将"=="打成"=",将报错
      
    • 一个类的成员函数的常量性(constness)是其签名的一部分,也就是一个成员函数可以被另一个与其常量性不同成员函数所重载。

      class TextBlock{
      public:
          const char& operator[] (std::size_t pos) {...}
          char& operator[] (std::size_t pos) {...}
      }
      
      TextBlock tb{"Hello"};
      // 调用 non-const operator[]
      tb[0] = 'x';
      
      const Textblock ctb{"World"};
      // 调用 const operator[]
      ctb[0] = 'x';   // 错误
      
      // 这里也可以想到为什么只有成员函数?因为不同常量性的成员函数用哪一个是由对象的常量性决定的
      
    • 使用与const相关的摆动场:mutable(可变的),可实现在const成员函数中成员变量也可被修改,使用场景:

      class CTextBlock{
      public:
          std::size_t length() const
          {
              if(!lengthIsVaild) {
                  textLength = std::strlen(pText);
                  lenghtIsValid = true;
              }
              return textLength;
          }
      
      private:
          char* pText;
          mutable std::size_t textLength;
          mutable bool  lengthIsVaild;
      }
      
    • 通常都不建议进行类型转换,但在这我们不得已使用类型转换来比避免代码的重复:

      class TextBlock{
      public:
          const char& operator[] (std::size_t pos) {...}
      
          char& operator[] (std::size_t pos) {
              return const_cast<char&>(
                  // 先将自己变为const对象,才会调用const的operator[]
                  static_cast<const TextBlock&>(*this)[pos]
                  );  // 再用const_cast<>()移除const,返回non-const
          }
      }
      
      

    ■ 将某些东西声明为 const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。

    ■ 编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”(conceptualconstness)。

    ■ 当 const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。

4. 确定对象被使用前已先被初始化

  • 为什么要?:

    • 读取未初始化的值会导致不明确的行为,在某些平台上,仅仅只是读取未初始化的值,就可能让你的程序终止运行。更可能的情况是读入一些“半随机”bits。
  • 示例:

    • 一个类的构造函数使用成员初值列(member initialization list)可以更高效率的完成成员变量的初始化。因为C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前,你如果想在构造函数内部对其进行赋值完成“初始化”,这样你的 非内置类型 成员变量就被“初始化”了两次,第一次(在 成员变量 默认的构造函数)完全浪费掉了!成员初值列就可以直接拿初值列中针对各个成员变量而设的实参去作为各 成员变量的构造函数 的实参。有些情况下即使面对的成员变量属于内置类型(那么其初始化与赋值的成本相同),也一定得使用初值列。是的,如果成员变量是const或 references,它们就一定需要初值,不能被赋值(见条款5)。

      class ABEntry{
      public:
          // 成员初值列方法,调用顺序
          // basic_string(const basic_string& _Right)
          // ↓↓↓
          // basic_string(const basic_string& _Right)
          // ↓↓↓
          // ABEntry(const std::string& name, const std::string& address)
          ABEntry(const std::string& name, const std::string& address)
           : theName(name), theAddress(address) { }   // 成员变量按声明的顺序被初始化,建议成员初值列也按声明的顺序写
      
          // 构造函数内赋值方法,调用顺序
          // basic_string()
          // ↓↓↓
          // basic_string()
          // ↓↓↓
          // ABEntry(const std::string& name, const std::string& address)
          // ↓↓↓
          // basic_string& operator=(const basic_string& _Right)
          // ↓↓↓
          // basic_string& operator=(const basic_string& _Right)
          ABEntry(const std::string& name, const std::string& address)
          {
              theName = name;
              theAddress = address;
          }
      
      private:
          string theName;
          string theAddress;
      }
      
    • C++对“定义于不同编译单元内的non-local static对象”的初始化次序并无明确定义。可以用一个函数将non-local static变成local static,就可以保证使用这个static对象时,它已被初始化,因为C++保证,函数内的local static对象会在“该函数被调用期间”“首次遇上该对象之定义式”时被初始化。(懒汉式单例模式就是如此)

      non-local static定义: 函数内的static对象称为local static对象(因为它们对函数而言是local),其他static对象称为non-local static对象。

      编译单元定义: 所谓编译单元(translation unit)是指产出单一目标文件(single object file)的那些源码。

    ■ 为内置型对象进行手工初始化,因为C++不保证初始化它们。

    ■ 构造函数最好使用成员初值列(member initialization list),而不要在构造函数本体内使用赋值操作(assignment)。初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同。

    ■ 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象。

posted @ 2021-02-26 23:40  ithepug  阅读(81)  评论(0编辑  收藏  举报