01 视C++为一个语言联邦

C++高效编程守则视状况而变化,取决于你使用C++的哪一部分

  • C++支持面向过程、面向对象、面向函数、泛型编程、元编程,因此可以将C++视为一个由相关语言组成的联邦而非单一语言(各个方面的编程范式不太相同):
    • C:有指针、数组,没有模板、重载和异常
    • Object-Oriented C++:类、封装、继承、多态、虚函数
    • Template C++:模板元编程
    • STL:
  • 编程范式(或者编程技巧)的区别:
    • 对于C而言,传值比传引用更加高效
    • 对于Object-Oriented C++而言,常量引用传递往往更好(可以传递左值、右值)
    • 对于Template C++而言,模板往往不知道处理的对象是什么类型
    • 对于STL而言,迭代器和函数对象是基于C的指针,所以此时应该选择值传递

02:尽量以const,enum,inline替换#define

  • 尽量使用编译器操作代替预处理器操作:
    • 对于常量,尽量使用const对象或enum来替换#define
    • 对于形似函数的宏,最好改用inline替换#define
  • 尽量使用编译器操作代替预处理器操作
    • #define是在预处理阶段进行替换,宏的名字不会出现在符号表中。
  • 对于常量,尽量使用const对象或enum来替换#define
    • 两个典型场景:
      • 定义常量指针
      • 定义class专属常量,比如const static成员
        • 类内static成员可以进行【声明时初始化】,虽然不是定义(即没有分配空间),但是只要不取地址,此时也可以使用该变量
        • 如果类内static成员进行【声明时初始化】,而且需要取地址,则需要在类外对变量进行定义
          1
          2
          3
          4
          
          class Widget{
              const static int val = 0;
          };
          const int Widget::val; // 由于const,无法进行赋值
          
  • 对于形似函数的宏,最好改用inline替换#define
    • 虽然使用宏本身少了一次调用过程,但是有时即使加上括号,结果也不正确
      1
      2
      3
      4
      5
      
      #define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
      
      int a = 5, b = 0;
      CALL_WITH_MAX(++a, b);      // a 累加了一次
      CALL_WITH_MAX(++a, b + 10); // a 累加了两次
      
    • 使用inline可以保证正确性,并且可以使用模板
  • 参考

03:尽可能使用const

  • 声明为const可以帮助编译器检测错误
  • const成员函数默认遵循bitwise constness,但是编写程序时应该使用logical constness,必要时将成员声明为mutable来保证可以修改
  • constnon-const成员函数有实质等价的实现,令non-const版本调用const版本可以避免代码重复
  • const和指针:顶层const与底层const
  • const和STL:const迭代器是顶层const,const_iterator是底层const
  • const和函数:
    • 函数返回值和函数形参尽量声明为const的,有助于编译器定位相关报错
      • 比如将比较运算符==误写为赋值运算符=
    • 成员函数声明为const的
      • 使得成员函数更容易被理解(这个成员函数不能修改成员),而且此时形参往往也是const引用
      • 一个const成员函数,一个non-const成员函数,可以进行重载
        • const对象调用const版本成员函数,普通对象调用non-const版本成员函数
        • 常量性转移
          • 背景:const成员函数与non-const成员函数中间逻辑相同,可能存在大量的重复代码,一个方法是将重复的代码写成函数放在private中
          • 更好的办法是,让non-const成员函数调用const成员函数(如果反过来,const成员函数调用non-const成员函数,不能保证对象不被修改)
             1
             2
             3
             4
             5
             6
             7
             8
             9
            10
            11
            12
            13
            14
            15
            
            class TextBlock{
                public:
                    const char& operator[] (std::size_t pos) const{
                        // do something
                        return text[pos];        
                    }
                    char& operator[] (std::size_t pos) {
                        return const_cast<char&>(
                            static_cast<const TextBlock&>(*this) // *this是TextBlock&, 强转加上const
                            [pos] // const TextBlock&调用operator[],否则TextBlock&调用operator[]一直重复调用自己
                        );
                    }
                private:
                    std::string text;
            }
            
      • mutable:使得成员变量即使在const成员函数中也可以被修改,主要是为了实现logical constness
        • 背景:bitwise constnesslogical constness
          • bitwise constness:成员函数不应该修改任何non-static成员变量(const成员函数的默认方式)
            • 编译器容易实现,只需要寻找成员变量的赋值操作
          • logical constness:允许成员函数修改成员变量,对于使用者而言,可以体现出constness即可
            • 比如一个指针成员变量,按照bitwise constness,限定指针为顶层的,但是却无法保证不修改所指对象
  • 参考

04:确定对象被使用前已被初始化

  • 内置类型对象一定要进行手动初始化
  • 构造函数中最好使用初始化列表对成员变量进行初始化,而非在函数体中进行赋值
  • 为了避免跨编译单元的初始化顺序问题,尽量以local static对象代替non-local static对象
  • 内置类型变量的初始化
    • 内置类型变量(即使是类中的内置类型成员变量)是否会初始化,取决于其在内存中的位置(堆空间?栈空间?)
  • 自定义类对象的初始化
    • 初始化与赋值的区别
      • 赋值:比如在构造函数函数体中进行“赋值”
        • 非内置类型的成员变量的初始化发生在进入构造函数之前,每个成员变量的default构造函数被自动调用,构造了两次(默认构造一次,复制构造一次)
        • 但是内置类型的成员变量不会自动初始化,此时无区别
      • 初始化:比如在构造函数初始化列表中
        • 此时相当于只调用了一次成员变量的构造函数(赋值构造)
        • 如果是const或者是引用,此时不能被赋值,只能进行初始化
  • 变量初始化顺序
    • 在初始化列表中,编译器按照父类->子类的顺序进行成员变量初始化,但尽量还是与成员声明顺序保持一致
    • 不同编译单元内定义的non-local static对象的初始化顺序
      • 一些情况下,不同编译单元内的non-local static对象的初始化顺序有要求,但是C++没有明确定义(比如要求先FileSystem中tfs初始化,后Diectory中tdr初始化)
      • 将每个 non-local static 对象移至自己的专属函数内(变成 local static 对象)