第1章:让自己习惯C++(Accustoming Yourself to C++)
一、导读
1.1 本章学习目标
第1章是《Effective C++》的开篇,Scott Meyers 在此帮助读者从 C 的思维模式转换到 C++ 的思维模式。本章涵盖了 C++ 中最基础也最重要的编程习惯:使用 const、初始化、理解编译器默认行为等。
本章核心学习目标:
- 理解 C++ 的编程范式:C++ 不是"更好的 C",而是多范式语言
- 掌握 const 的正确使用:const 指针、const 引用、const 成员函数
- 理解初始化的重要性:为什么应该用初始化列表而非赋值?
- 掌握编译器默认生成的函数:默认构造、拷贝构造、拷贝赋值、析构
- 理解 C++ 的"规则"思维:为什么 C++ 有这么多规则?
1.2 为什么本章如此重要
C++ 是一门复杂的语言,但大多数问题源于对基本规则的忽视。Meyers 在开篇就强调:先养成正确的习惯,再学习高级技巧。
1.3 核心问题引导
为什么应该尽量用 const 和 enum 替代 #define?
C++ 的初始化语法有几种?各有什么优劣?
编译器在什么情况下会生成默认函数?如何禁止?
二、核心概念详解
2.1 用 const 和 enum 替代 #define
cpp
// 不好: 使用 #define
#define ASPECT_RATIO 1.653
#define MAX(a, b) ((a) > (b) ? (a) : (b))
// 好: 使用 const
const double AspectRatio = 1.653;
// 好: 使用 enum (编译期常量)
class GameCharacter {
enum { NumTurns = 5 }; // 编译期常量,不占内存
int scores[NumTurns];
};
// #define 的问题:
// 1. 调试器看不到符号名(只看到 1.653)
// 2. 宏展开可能导致意外行为
// MAX(a++, b) → ((a++) > (b) ? (a++) : (b)) → a 被递增两次!
// 3. 没有类型检查2.2 初始化 vs 赋值
cpp
class PhoneNumber {
std::string name;
int age;
std::string phone;
// 不好: 赋值(先默认构造,再赋值)
PhoneNumber(std::string n, int a, std::string p) {
name = n; // 赋值
age = a; // 赋值
phone = p; // 赋值
}
// 好: 初始化列表(直接构造)
PhoneNumber(std::string n, int a, std::string p)
: name(n), age(a), phone(p) {} // 初始化
// 更好: C++11 统一初始化
PhoneNumber(std::string n, int a, std::string p)
: name{std::move(n)}, age{a}, phone{std::move(p)} {}
};
// 为什么初始化列表更好?
// 1. const 成员只能初始化,不能赋值
// 2. 引用成员只能初始化,不能赋值
// 3. 没有默认构造函数的类成员只能初始化
// 4. 效率更高(避免默认构造 + 赋值的双重开销)2.3 了解编译器默认生成的函数
cpp
class Empty {
// 编译器默认生成:
// Empty() {} // 默认构造
// Empty(const Empty& rhs) {} // 拷贝构造
// ~Empty() {} // 析构
// Empty& operator=(const Empty& rhs) {} // 拷贝赋值
};
// 编译器只在需要时生成,且只在没有用户定义版本时生成
// C++11 的 =default 和 =delete:
class NonCopyable {
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
};
class MoveableOnly {
MoveableOnly(MoveableOnly&&) = default; // 显式要求生成移动构造
};2.4 使用 const 修饰成员函数
cpp
class TextBlock {
std::string text;
public:
// const 成员函数: 承诺不修改对象状态
const char& operator[](std::size_t pos) const {
return text[pos];
}
// 非 const 版本
char& operator[](std::size_t pos) {
return text[pos];
}
};
const TextBlock ctb("Hello");
ctb[0] = 'h'; // 编译错误: ctb 是 const,调用 const 版本的 operator[]
TextBlock tb("World");
tb[0] = 'w'; // OK: 调用非 const 版本三、实战案例
3.1 案例一:const 正确性的实践
cpp
class Matrix {
std::vector<std::vector<double>> data;
public:
Matrix(int rows, int cols) : data(rows, std::vector<double>(cols, 0.0)) {}
// const 版本
double at(int r, int c) const {
return data[r][c];
}
// 非 const 版本
double& at(int r, int c) {
return data[r][c];
}
// const 成员函数可以调用其他 const 成员函数
double sum() const {
double total = 0;
for (const auto& row : data)
for (double val : row)
total += val;
return total;
}
// 错误: const 函数不能调用非 const 函数
// void print() const {
// at(0, 0) = 1.0; // 错误: at 的非 const 版本修改了对象
// }
};
void processMatrix(const Matrix& m) {
// m 是 const 引用,只能调用 const 成员函数
double val = m.at(0, 0); // OK
// m.at(0, 0) = 1.0; // 编译错误
double s = m.sum(); // OK
}四、本章小结
用 const 和 enum 替代 #define,获得更好的类型安全和调试体验
使用初始化列表而非赋值,效率更高且支持 const 和引用成员
了解编译器默认生成的函数,必要时用 =delete 禁止
const 成员函数承诺不修改对象状态,是接口设计的重要工具
五、思考与练习
思考题
为什么 const 对象只能调用 const 成员函数?
C++11 的统一初始化语法 {} 相比 () 有什么优势?
编程练习
设计一个不可变(immutable)的字符串类,所有操作返回新对象。
实现一个矩阵类,正确使用 const 修饰所有成员函数。