Effective C++ 第三版 条款总结及对应代码描述
- 条款1: 视C++为一个语言联邦
- 条款2: 尽量以const、enum、inline替换#define
- 条款3: 尽可能使用const
- 条款4: 确保对象使用前已先被初始化
- 条款5: 了解C++默默编写并调用哪些函数
- 条款6: 若不想使用编译器自动生成的函数,就该明确拒绝
- 条款7: 为多态基类声明virtual析构函数
- 条款8: 别让异常逃离析构函数
- 条款9: 绝不在构造和析构过程中调用virtual函数
- 条款10: 令operator=返回一个reference to *this
- 条款11:在 operator= 中处理“自我赋值”
- 条款12:复制对象时勿忘其每一个成分
- 条款13:以对象管理资源
- 条款14:在资源管理类中小心 coping 行为
- 条款15:在资源管理类中提供对原始资源的访问
- 条款16:成对使用 new 和 delete 时要采取相同形式
- 条款17:以独立语句将 newed 对象置入智能指针
- 条款18:让接口容易被正确使用,不易被误用
- 条款19:设计 class 犹如设计 type,需要考虑对象创建、销毁、初始化、赋值、值传递、合法值、继承关系、转换、一般化等事项
- 条款20:宁以 pass-by-reference-to-const 替换 pass-by-value
- 条款21:必须返回对象时,别妄想返回其 reference
- 条款22:将成员变量声明为 private
- 条款23:宁以 non-member、non-friend 替换 member 函数
- 条款24:若所有参数皆需类型转换,请为此采用 non-member 函数
- 条款25:考虑写出一个不抛异常的 swap 函数
- 条款26:尽可能延后变量定义式的出现时间
- 条款27:尽量少做转型动作
- 条款28:避免返回 handles 指向对象内部成分
- 条款29:为“异常安全”而努力是值得的
- 条款30:透彻了解 inlining 的里里外外
- 条款31:将文件间的编译依存关系降至最低
- 条款32:确保 non-local static 对象的初始化顺序
- 条款33:避免遮掩继承而来的名称
- 条款34:区分接口继承和实现继承
- 条款35:考虑 virtual 函数以外的其他选择
- 条款36:绝不重新定义继承而来的 non-virtual 函数
- 条款37:绝不重新定义继承而来的缺省参数值
- 条款38:通过复合塑模出 has-a 或 “根据某物实现出”
- 条款39:明智而审慎地使用 private 继承
- 条款40:明智而审慎地使用多重继承
- 条款41:了解隐式接口和编译期多态
- 条款42:了解 typename 的双重含义
- 条款43:学习处理模板化基类内的名称
- 条款44:将与参数无关的代码抽离templates
- 条款45:运用成员函数模板接受所有兼容类型
- 条款46:需要类型转换时请为模板定义非成员函数
- 条款47:请使用 traits classes 表示类型信息
- 条款48:认识 template 元编程的两个世界
- 条款49:了解 new-handler 的行为
- 条款50:明白 new 和 delete 的合理替换时机
- 条款51:编写 new 和 delete 时需固守常规
- 条款52:写了 placement new 也要写 placement delete
- 条款53:不要轻忽编译器的警告
- 条款54:让自己熟悉包括TR1在内的标准程序库
- 条款55:让自己熟悉Boost
条款1: 视C++为一个语言联邦
C++可以被看作是几种不同编程语言的集合:C、Object-Oriented C++、Template C++、以及STL。每种"子语言"都有其优势和用法。例如,C部分提供了对硬件的低级访问,面向对象部分支持封装和继承,模板部分支持泛型编程,STL则提供了一套强大的库。理解这些部分的不同规则和最佳实践对于编写高效、可维护的代码至关重要。
// C-style
int array[10];
// Object-Oriented C++
class Widget {
public:
void draw() const; // 类方法
};
// Template C++
template<typename T>
void swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
// STL
#include <vector>
std::vector<int> vec;
条款2: 尽量以const、enum、inline替换#define
使用预处理指令(#define)定义常量和宏可能导致调试困难,因为预处理器只是简单的文本替换,并不检查类型等。相反,使用const关键字可以确保类型安全,使用enum和inline可以替代宏,提供更清晰、更安全的代码。
// bad
#define ASPECT_RATIO 1.653
// good
const double AspectRatio = 1.653;
条款3: 尽可能使用const
使用const可以提高代码的可读性,减少编程错误,因为它指定了变量不可修改。应用const到各种适当的场合,包括函数参数、返回类型、成员函数本身,确保程序的正确性和清晰性。
void f(const Widget& w) {
// w.doSomething(); // 这样是错误的,如果doSomething()不是const成员函数
w.show() const; // 正确
}
class Widget {
public:
std::size_t size() const {
return data.size(); }
private:
std::vector<int> data;
};
条款4: 确保对象使用前已先被初始化
在C++中,未初始化的变量可能导致随机运行时错误。最好的做法是在构造对象时立即给它赋值。在构造函数初始化列表中初始化字段比在构造函数体内赋值更为高效。
class Widget {
public:
Widget(int i, bool b) : id(i), valid(b) {
} // 初始化列表
private:
int id;
bool valid;
};
条款5: 了解C++默默编写并调用哪些函数
C++编译器会自动为类生成默认构造函数、析构函数、拷贝构造函数和拷贝赋值运算符,如果未明确声明这些。理解这一行为对于设计类及其行为至关重要,尤其是涉及资源管理(如动态内存分配)时。
class Widget {
public:
Widget(const Widget& rhs) : id(rhs.id) {
} // 拷贝构造函数
Widget& operator=(const Widget& rhs) {
// 拷贝赋值运算符
id = rhs.id;
return *this;
}
private:
int id;
};
条款6: 若不想使用编译器自动生成的函数,就该明确拒绝
在C++中,如果你不提供拷贝构造函数和拷贝赋值运算符,编译器会为你自动生成。但有时候,你并不希望你的对象被拷贝,例如,当你的类包含了对资源如文件句柄或网络连接的独占控制时。在这种情况下,你应该阻止生成这些函数,可以通过将它们声明为private并且不提供实现来实现:
class Uncopyable {
protected: // 允许派生类构造和析构
Uncopyable() {
}
~Uncopyable() {
}
private: // 阻止拷贝构造函数和赋值运算符
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
};
class MyResource: private Uncopyable {
// 现在MyResource类不能被拷贝
};
条款7: 为多态基类声明virtual析构函数
当一个类用作基类,并且通过基类的指针或引用来管理派生类对象时,应当声明一个虚析构函数。这确保了在通过基类指针删除一个派生类对象时,可以正确地调用派生类的析构函数,避免资源泄露。
class Base {
public:
virtual ~Base() {
// 基类的析构函数
}
};
class Derived : public Base {
public:
~Derived() {
// 派生类的资源清理
}
};
Base* b = new Derived();
delete b; // 正确调用Derived的析构函数,然后是Base的析构函数
条款8: 别让异常逃离析构函数
在析构函数中抛出异常是非常危险的,因为如果在析构过程中已经因为另一个异常而处于堆栈展开过程中,抛出另一个异常将导致程序终止。因此,析构函数应该捕获并处理所有异常,或者避免调用可能抛出异常的函数。
class ResourceManager {
public:
~ResourceManager() {
try {
// 尝试释放资源,可能抛出异常
releaseResource();
} catch(...) {
// 处理异常,确保不逃离析构函数
handleError();
}
}
private:
void releaseResource();
void handleError();
};
条款9: 绝不在构造和析构过程中调用virtual函数
在构造或析构的过程中调用虚函数不会调用到派生类中覆盖的版本。这是因为在构造和析构过程中,对象的动态类型是正在构造或析构的类。如果调用虚函数,它会调用当前类层次结构中的那个版本,这可能不是你期望的行为。
class Base {
public:
Base() {
call(); }
virtual ~Base() {
call(); }
virtual void call() {
std::cout << "Base::call()\n"; }
};
class Derived : public Base {
public:
void call() override {
std::cout << "Derived::call()\n"; }
};
// 在main中创建Derived对象
// 输出将是 "Base::call()" 而不是 "Derived::call()"
Derived d;
条款10: 令operator=返回一个reference to *this
为了实现连续赋值,赋值运算符应该返回一个指向当前对象的引用。这允许链式赋值,并且是对赋值操作符的一个常见和期望的实现。
class Widget {
public:
Widget& operator=(const Widget& rhs) {
// 检查自赋值
if (this == &rhs) return *this;
// 复制数据
data = rhs.data;
return *this; // 使赋值可以链式进行
}
private:
int data;
};
Widget a, b, c;
a = b = c; // 链式赋值
条款11:在 operator= 中处理“自我赋值”
当设计赋值运算符时,必须考虑到对象可能会将自身赋值给自身的情况。如果不妥善处理,这种自我赋值可能会导致程序错误,比如意外的资源释放。这可以通过检查赋值运算符的参数是否与当前对象相同来防止。
class Widget {
public:
Widget& operator=(const Widget& rhs) {
if (this == &rhs) return *this; // 自我赋值检查
// 释放旧资源
delete[] data;
// 复制赋值的数据
data = new int[rhs.size];
std::copy(rhs.data, rhs.data + rhs.size, data);
size = rhs.size;
return *this;
}
private:
int* data;
std::size_t size;
};
条款12:复制对象时勿忘其每一个成分
在实现拷贝构造函数和赋值运算符时,确保复制对象的所有成员变量和基类部分。忽略任何成员或基类的复制都可能导致运行时错误或对象状态不一致。
class Base {
public:
int b;
};
class Derived : public Base {
public:
Derived& operator=(const Derived& rhs) {
Base::operator=(rhs); // 复制基类部分
d = rhs.d; // 复制派生类部分
return *this;
}
private:
int d;
};
条款13:以对象管理资源
使用对象来管理资源(如动态内存、文件句柄、网络连接等),利用C++的构造函数和析构函数,可以自动化资源的获取和释放,避免资源泄漏。这通常通过实现一个RAII(Resource Acquisition Is Initialization)类来完成。
class FileHandler {
public:
FileHandler(const char* filename) {
file = fopen(filename, "w")