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
来保证可以修改const
和non-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 constness
与logical constness
bitwise constness
:成员函数不应该修改任何non-static
成员变量(const成员函数的默认方式)- 编译器容易实现,只需要寻找成员变量的赋值操作
logical constness
:允许成员函数修改成员变量,对于使用者而言,可以体现出constness即可- 比如一个指针成员变量,按照
bitwise constness
,限定指针为顶层的,但是却无法保证不修改所指对象
- 比如一个指针成员变量,按照
- 背景:
- 函数返回值和函数形参尽量声明为const的,有助于编译器定位相关报错
- 参考
04:确定对象被使用前已被初始化
- 内置类型对象一定要进行手动初始化
- 构造函数中最好使用初始化列表对成员变量进行初始化,而非在函数体中进行赋值
- 为了避免跨编译单元的初始化顺序问题,尽量以local static对象代替non-local static对象
- 内置类型变量的初始化
- 内置类型变量(即使是类中的内置类型成员变量)是否会初始化,取决于其在内存中的位置(堆空间?栈空间?)
- 自定义类对象的初始化
- 初始化与赋值的区别
- 赋值:比如在构造函数函数体中进行“赋值”
- 非内置类型的成员变量的初始化发生在进入构造函数之前,每个成员变量的default构造函数被自动调用,构造了两次(默认构造一次,复制构造一次)
- 但是内置类型的成员变量不会自动初始化,此时无区别
- 初始化:比如在构造函数初始化列表中
- 此时相当于只调用了一次成员变量的构造函数(赋值构造)
- 如果是const或者是引用,此时不能被赋值,只能进行初始化
- 赋值:比如在构造函数函数体中进行“赋值”
- 初始化与赋值的区别
- 变量初始化顺序
- 在初始化列表中,编译器按照父类->子类的顺序进行成员变量初始化,但尽量还是与成员声明顺序保持一致
- 不同编译单元内定义的non-local static对象的初始化顺序
- 一些情况下,不同编译单元内的non-local static对象的初始化顺序有要求,但是C++没有明确定义(比如要求先FileSystem中tfs初始化,后Diectory中tdr初始化)
- 将每个 non-local static 对象移至自己的专属函数内(变成 local static 对象)