条款 1:视 C++ 为一个语言联邦
C++ 拥有多种不同的编程范式,而这些范式集成在一个语言中,使得 C++ 是一门即灵活又复杂的语言:
- 传统的面向过程 C:区块,语句,预处理器,内置数据类型,数组,指针。
- 面向对象的 C with Classes:类,封装,继承,多态,动态绑定(virtual函数)。
- 泛型编程 Template C++ 和堪称黑魔法的模板元编程(TMP)。
- C++ 标准库 STL(容器、迭代器、算法、适配器、空间配置器、算法)。
C++ 高效编程守则视情况而变化,例如,对内置类型而言,pass-by-value(按值传递)通常比pass-by-reference(按引用传递)高效,而对于用户自定义的类型,由于用户自定义的构造函数和析构函数的存在,pass-by-reference-const更好。
条款 2:尽量以 const, enum, inline 替换 #define
“宁可以编译器替换预处理器”——在编译器开始处理源码之前,记号名称就被预处理器移走了,,记号名称可能没进入记号表(symbol table)内。
在原书写成时 C++11 中的constexpr
还未诞生,现在一般认为应当用constexpr
定义编译时的常量来替代大部分的#define
宏常量定义:
#define ASPECT_RATIO 1.653
替代为:
constexpr auto aspect_ratio = 1.653;
我们也可以将编译时的常量定义为类的静态成员:
class GamePlayer {
public:
static constexpr auto numTurns = 5;
};
enum
可以用于替代类内静态非整型的常量定义,并且在模板元编程中应用广泛(见条款 48):
class GamePlayer {
public:
enum { numTurns = 5 };
};
enum和define行为比较像,而不像const常量。enum和define不能进行取地址,它们一样绝不会导致非必要的内存分配。
大部分#define
宏常量应当用内联模板函数替代:
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
替代为:
template<typename T> //由于我们并不知道T是什么,所以采用pass-by-reference-to-const。
inline void CallWithMax(const T& a, const T& b) {
f(a > b ? a : b);
}
需要注意的是,宏和函数的行为本身并不完全一致,宏只是简单的替换,并不涉及传参和复制。可以写出一个“class内的private inline函数”,而宏无法完成。
条款 3:尽可能使用 const
若你想让一个常量只读,那你应该明确说出它是const常量,对于指针来说,更是如此:
char greeting[] = "Hello";
char* p = greeting; // 指针可修改,数据可修改
const char* p = greeting; // 指针可修改,数据不可修改
char const* p = greeting; // 指针可修改,数据不可修改
char* const p = greeting; // 指针不可修改,数据可修改
const char* const p = greeting; // 指针不可修改,数据不可修改
对于 STL 迭代器,分清使用const
还是const_iterator
:
const std::vector<int>::iterator iter = vec.begin(); // 迭代器不可修改,数据可修改
std::vector<int>::const_iterator iter = vec.begin(); // 迭代器可修改,数据不可修改
面对函数声明时,如果你不想让一个函数的结果被无意义地当作左值,请使用const返回值:
const Rational operator*(const Rational& lhs, const Rational& rhs);
const成员函数:
const成员函数允许我们操控const对象,这在传递常引用时显得尤为重要:
class TextBlock {
public:
const char& operator[](std::size_t position) const { // const对象使用的重载
return text[position];
}
char& operator[](std::size_t position) { // non-const对象使用的重载
return text[position];
}
private:
std::string text;
};
编译器对待const对象的态度通常是 bitwise constness(位常量性或物理常量性)(const成员函数不能修改对象内任何非静态成员变量),而我们在编写程序时通常采用 logical constness(逻辑常量性),这就意味着,在确保客户端不会察觉的情况下,我们认为const对象中的某些成员变量应当是允许被改变的,使用关键字mutable
来标记这些成员变量:
class CTextBlock {
public:
std::size_t Length() const;
private:
char* pText;
mutable std::size_t textLength;
mutable bool lengthIsValid;
};
std::size_t CTextBlock::Length() const {
if (!lengthIsValid) {
textLength = std::strlen(pText); // 可以修改mutable成员变量
lengthIsValid = true; // 可以修改mutable成员变量
}
return textLength;
}
在重载const和non-const成员函数时,需要尽可能避免书写重复的内容,这促使我们去进行常量性转除。在大部分情况下,我们应当避免转型的出现,但在此处为了减少重复代码,转型是适当的:
class TextBlock {
public:
const char& operator[](std::size_t position) const {
// 假设这里有非常多的代码
return text[position];
}
char& operator[](std::size_t position) {
//两次转型
//(第一次:为*this添加const(为了调用operatorp[]的const版本);第二次从const operato[]的返回值中移除const。)
return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
}
private:
std::string text;
};
需要注意的是,反向做法:令const版本调用non-const版本以避免重复——并不被建议,一般而言const版本的限制比non-const版本的限制更多,因此这样做会带来风险。