条款 01:视 C++ 为一个语言联邦
C++ 并非单一范式的编程语言,而是由四种不同的 “次语言” 组成的联邦,每种次语言都有其独特的编程范式和规范:
- C 语言
-
以过程式编程为核心,包含基本语法、指针、数组、结构体和预处理器等内容。
-
规范:注重内存管理、手动资源释放,使用 malloc/free 进行内存操作。
- 面向对象的 C++
-
涵盖类、继承、多态、虚函数等面向对象的特性。
-
规范:提倡使用构造函数和析构函数进行资源管理,遵循 RAII(资源获取即初始化)原则。
- 模板 C++(泛型编程)
-
以模板元编程(TMP)为核心,支持编写通用的、类型安全的代码。
-
规范:利用模板实现编译时计算,避免运行时开销。
- STL(标准模板库)
-
包含容器(如 vector、map)、算法(如 sort、find)和迭代器等组件。
-
规范:采用统一的接口设计,遵循 “算法与数据结构分离” 的原则。
关键要点:
-
当从一个次语言切换到另一个次语言时,编程规范可能会发生变化(例如,在 STL 中应优先使用迭代器而非原始指针)。
-
优秀的 C++ 代码需要在不同的范式之间灵活切换,以达到最佳的性能和可维护性。
条款 02:尽量以 const、enum、inline 替换 #define
预处理器(如 #define)在编译前会进行文本替换,这可能会引发一些难以调试的问题。我们应尽量使用编译器机制来替代预处理器。
1. 用 const 替代宏常量
// 不良做法
#define ASPECT_RATIO 1.653
// 推荐做法
const double AspectRatio = 1.653; // 具有类型检查,并且可在调试时查看变量名
优势:
-
宏定义的名称在编译时可能会被移除,导致调试困难;而 const 变量会保留在符号表中。
-
宏不具备作用域的概念,可能会引发命名冲突;而 const 变量可以被限制在类、文件或命名空间的作用域内。
在C++11中海引入了constexpr
,它与const的差异点:
const
:其作用是声明一个运行时常量,也就是在程序运行时,这个值被初始化之后就不能再改变了。它主要用于对变量进行修饰,以此来保证变量的值不会被修改。constexpr
:它声明的是编译时常量,意味着这个值在编译阶段就已经确定了。constexpr
能够修饰变量、函数以及构造函数,借助它可以在编译时就完成计算,进而提高程序的运行效率。
2. 类内的专属常量
class GamePlayer {
private:
static const int NumTurns = 5; // 常量声明(编译期常量)
int scores[NumTurns]; // 合法:NumTurns 在编译时已知
};
// 如果需要在类外定义(如获取变量地址)
const int GamePlayer::NumTurns; // 定义(无需重复赋值)
若编译器不支持上述语法(例如旧版本的编译器),可以使用 enum hack:
class GamePlayer {
private:
enum { NumTurns = 5 }; // enum hack,效果类似宏,但具有类型安全性
int scores[NumTurns]; // 同样合法
};
3. 用 inline 函数替代形似函数的宏
// 不良做法
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b)) // 存在优先级和副作用问题
// 推荐做法
template<typename T>
inline void callWithMax(const T& a, const T& b) {
f(a > b ? a : b); // 类型安全,且不会出现宏的副作用
}
示例对比:
int a = 5, b = 0;
CALL_WITH_MAX(++a, b); // a 被递增两次
CALL_WITH_MAX(++a, b+10); // a 被递增一次
关键要点:
-
优先使用 const 和 enum 替代宏常量,以获得类型检查和作用域控制。
-
优先使用 inline 函数替代形似函数的宏,以避免副作用和调试困难的问题。
条款 03:尽可能使用 const
const 是 C++ 中一个非常强大的工具,它能够在编译时强制实现语义约束,提高代码的安全性。
1. 指针与常量的结合
char greeting[] = "Hello";
char* p = greeting; // 非 const 指针,非 const 数据
const char* p = greeting; // 非 const 指针,指向 const 数据(数据不可变)
char* const p = greeting; // const 指针,指向非 const 数据(指针不可变)
const char* const p = greeting; // const 指针,指向 const 数据
记忆方法:
-
const 在星号左边:数据是常量(*p 不可变)。
-
const 在星号右边:指针是常量(p 不可变)。
2. STL 迭代器与 const
std::vector<int> vec = {1, 2, 3};
// 迭代器本身是常量(类似 T* const)
std::vector<int>::iterator const iter = vec.begin();
*iter = 10; // 合法:修改数据
++iter; // 非法:迭代器不可变
// 迭代器指向常量数据(类似 const T*)
const std::vector<int>::iterator cIter = vec.begin(); // 错误!容易混淆的语法
std::vector<int>::const_iterator cIter = vec.begin(); // 正确写法
*cIter = 10; // 非法:数据不可变
++cIter; // 合法:迭代器可变
3. const 成员函数
常量成员函数:承诺不会修改对象的逻辑状态。
class TextBlock {
public:
const char& operator[](std::size_t position) const { // const 版本
return text[position];
}
char& operator[](std::size_t position) { // 非 const 版本
return text[position];
}
private:
std::string text;
};
// 使用示例
const TextBlock ctb("Hello");
ctb[0] = 'X'; // 非法:调用 const 版本,返回 const char&
TextBlock tb("World");
tb[0] = 'X'; // 合法:调用非 const 版本,返回 char&
关键要点:
-
常量成员函数可以被重载,这使得 const 对象和非 const 对象可以调用不同版本的函数。
-
使用 mutable 关键字可以让成员变量在 const 成员函数中被修改(例如用于缓存或锁机制)。
条款 04:确定对象被使用前已先被初始化
1. 区分初始化和赋值
// 赋值(先默认构造,再赋值)
class Person {
public:
Person(const std::string& name, int age) {
theName = name; // 赋值,而非初始化
theAge = age; // 赋值,而非初始化
}
private:
std::string theName;
int theAge;
};
// 初始化(使用成员初始化列表)
class Person {
public:
Person(const std::string& name, int age)
: theName(name), // 初始化
theAge(age) {} // 初始化
};
为什么优先使用初始化列表?
-
对于 std::string 等类类型,初始化列表会直接调用拷贝构造函数,而赋值操作会先调用默认构造函数,再调用赋值运算符,效率较低。
-
对于 const 成员变量和引用成员变量,必须使用初始化列表进行初始化。
2. 成员变量的初始化顺序
成员变量的初始化顺序由它们在类中的声明顺序决定,与初始化列表中的顺序无关。
class X {
private:
int a;
int b;
public:
X(int value) : b(value), a(b) {} // 错误:a 先被初始化,此时 b 尚未初始化
};
3. 局部静态对象的初始化问题
在多线程环境下,局部静态对象的初始化可能会出现竞态条件。C++11 解决了这个问题:
// C++11 保证线程安全的单例实现
class Logger {
public:
static Logger& instance() {
static Logger logger; // 线程安全的初始化
return logger;
}
private:
Logger() = default;
};
关键要点:
-
对于内置类型(如 int、double),在使用前手动初始化,因为它们不会进行默认初始化。
-
使用成员初始化列表替代赋值操作,提高效率。
-
注意成员变量的初始化顺序与声明顺序一致。
总结
第一章强调了 C++ 的多范式特性以及一些基础的编程准则:
-
灵活运用不同的次语言范式,以适应具体的应用场景。
-
减少对预处理器的依赖,充分利用编译器的类型系统。
-
借助 const 增强代码的安全性和可读性。
-
重视对象初始化,避免未定义行为。