悦读上品,得乎益友
1.基础议题
条款1: 仔细区分pointers 和 references
- 引用不能为空,指针可以为空
- 引用必须初始化,指针可以不初始化
- 引用不能改变指向,指针可以重新赋值
- 特定操作符需返回引用:当重载操作符(如
operator[])时,为了语法上的直观(如v[i] = 10),必须返回引用而非指针。 - 函数参数传递指针和传递引用:- 传引用在底层就是用指针实现的,都会多一次指针大小的拷贝。
条款2:最好使用C++转型操作符
- 尽量使用C++新式转型,避免使用旧式转型:C的旧式转型几乎允许你将任何类型转换为任何其他类型,风险极高。新式转型(
static_cast、const_cast、dynamic_cast、reinterpret_cast)通过名称清晰表达了转型的意图,编译器也更易检查。 static_cast用于基本类型转换和下行转换(无安全检查) :它执行隐式转换能进行的任何转换,以及反向的转换(如父类转子类),编译时类型检查(只要相关类型就可转),但不会进行运行时类型检查。const_cast唯一用于去除常量性dynamic_cast用于继承体系中的安全向下转型:它执行运行时类型检查,确保转换的安全性。但成本较高,应谨慎使用,尤其是在性能关键代码中。reinterpret_cast用于底层的强制位模式解释:它依赖于编译器实现,不可移植。通常用于将指针转换为整型或其他不相关类型,应尽量避免在应用层使用。功能上类似C风格转换。
条款3:绝对不要以多态(polymorphically)方式处理数组
Base[10],该数组中存了它了derived类,错误,数据截断等一系列错误。
条款4:非必要不提供default constructor
1.提供缺省构造函数的代价
- 效率损失:成员函数需测试字段是否初始化,付出时间和空间代价
- 意义缺失:无意义的初始化导致对象状态不确定
2.不提供缺省构造函数的限制
- 数组定义困难:无法直接定义
T obj[N]或new T[N] - 模板兼容问题:某些模板容器要求类型有缺省构造函数
- 虚基类麻烦:派生类必须理解虚基类构造参数
3.最终判断
宁愿带来使用限制,也要保证对象的完整初始化——这才是真正的效率和安全
2.操作符
条款5: 对定制的“类型转换函数”保持警觉
1.单自变量constructors
2.隐式类型转换操作符
class Rational{
operator double() const; //将Rational 转换为 double
3.解决方案
- 使用
explicit关键字:这是最直接的方法。将单自变量构造函数声明为explicit,可以禁止编译器用它进行隐式类型转换,但允许显式转换(如static_cast) - 提供显式函数替代:用功能对等的普通成员函数(如
toDouble()、asDouble()或标准库中的c_str())取代隐式类型转换操作符。虽然调用时需显式写出函数名,但换来了代码的清晰和安全 - 使用Proxy类(代理类) :这是一种更精巧的技术。通过引入一个新类(如
ArraySize)作为构造函数的参数,利用“禁止连续两次用户定制转换”的规则,既能实现用整数构造对象的需求,又能阻止整数被隐式转换为临时对象 - 除非你真的、真的很需要,否则不要提供隐式类型转换函数。
条款6:区别 increment/decrement 操作符的前置(prefix)和后置(postfix)形式
🔧 语法上的“小技巧”:用int参数区分
C++允许重载++和--操作符,但前置和后置形式都没有参数,无法直接通过参数类型区分。为了解决这个语言漏洞,标准规定:后置形式接受一个int参数(通常不命名,也不使用),编译器在调用时会自动传一个0值作为占位
- 前置声明:
UPInt& operator++();//返回引用 - 后置声明:
const UPInt operator++(int);//返回const对象,为什么必须返回const对象,阻止C++++这样的形式编译通过,阻止对C++返回的临时对象进行++,违背直觉。
-
前置形式 (
++i) :累加然后取出。直接修改自身,然后返回修改后的自身的引用。这种方式效率高,没有临时对象产生。UPInt& UPInt::operator++() { *this += 1; // 累加 return *this; // 返回自身引用 } -
后置形式 (
i++) :取出然后累加。为了返回旧值,必须创建一个临时对象保存当前状态,然后对自身进行累加,最后返回那个临时对象。const UPInt UPInt::operator++(int) { UPInt oldValue = *this; // 取出(创建临时对象) ++(*this); // 累加(调用前置++,实现代码复用) return oldValue; // 返回旧值 }
以前置形式实现后置,前置形式效率更高,没有临时对象的创建。
条款7: 千万不要重载&&,||和,操作符
重载这些操作符会剥夺其固有的短路求值特性,并改变其求值顺序的确定性。
🚨 重载 && 和 ||:短路求值消失
逻辑与(&&)和逻辑或(||)最核心的特性是短路求值。这意味着一旦表达式的值可以由左侧操作数确定,右侧操作数将不会被计算。
-
内置行为示例:
if (p && p->size() > 10) // 如果p是null,则不会尝试访问p->size()如果
p为空指针,左侧为false,整个表达式确定为false,右侧p->size()根本不会执行,这保证了程序的安全。 -
重载后的行为:
当重载&&或||时,它们变成了函数调用。if (expression1 && expression2) // 内置版本:短路求值 if (expression1 || expression2) // 内置版本:短路求值 if (a && b) // 如果a和b是重载了&&的类型 // 编译器看到的是:a.operator&&(b) 或 operator&&(a, b)作为函数参数,两个表达式在进入函数体之前都会被计算。 这意味着短路求值特性完全丢失,右侧表达式(可能包含副作用或重要操作)总是会被执行,即便左侧的结果已经可以决定整个表达式的值。
🚨 重载 ,(逗号)操作符:求值顺序不确定
逗号操作符用于构成逗号表达式,其内置行为是:从左到右依次计算各个表达式,整个逗号表达式的结果是最后一个表达式的结果。
-
内置行为示例:
int i = 0, j = 1; int k = (i++, j++); // i先自增变为1,然后j自增变为2,最后k被赋值为j++的结果(即1)这里
i++保证在j++之前执行。 -
重载后的行为:
重载,操作符同样使其变成了函数调用(如a, b变成a.operator,(b)或operator,(a, b))。
函数参数的求值顺序是未指定的。 这意味着你无法保证a一定在b之前被计算。当逗号表达式的求值顺序对结果有影响时(如上述例子中的副作用),重载它将带来灾难。
条款8:了解不同意义的new和delete
🧩 三种不同的 new 场景
1. new 操作符 (new operator)
这是你最常用的 new,它完成两件事:
- 分配内存:自动调用
operator new分配足够的内存。 - 构造对象:在分配的内存上调用对象的构造函数。
示例:
string* ps = new string("test");
这段代码不能改变其行为(不能手动分配内存然后调用构造函数替代它)。
2. operator new 函数
这是 new 操作符用来分配内存的底层函数。它只负责分配原始内存(类似 malloc),不调用构造函数。
- 你可以重载
operator new来定制内存分配策略。 - 其原型通常为:
void* operator new(size_t size);
示例:
void* rawMemory = operator new(sizeof(string)); // 只分配内存
// 此时 rawMemory 中的对象还未构造,还不能当 string 用
3. Placement new
这是一种特殊的 operator new,它在一个已经分配好的指针上构造对象(调用构造函数)。主要用在内存池、自定义容器等场景。
- 最常用的形式是:
operator new(size_t, void* location),它在指针location指向的内存上构造对象。 - 不能直接使用对应的
delete来释放 placement new 构造的对象(因为内存不是它分配的)。
示例:
void* buffer = malloc(sizeof(string)); // 分配原始内存
string* ps = new (buffer) string("test"); // placement new:在buffer上构造对象
// ...
ps->~string(); // 必须手动调用析构函数
free(buffer); // 然后释放原始内存
⚙️ 对应的 delete 逻辑
1. delete 操作符 (delete operator)
与 new 操作符对应,它完成两件事:
- 析构对象:调用对象的析构函数。
- 释放内存:自动调用
operator delete释放内存。
2. operator delete 函数
与 operator new 对应,它只负责释放原始内存(类似 free)。
- 你重载
operator delete通常是为了配合重载的operator new。
3. Placement delete
如果 placement new 在构造对象时抛出异常,C++ 运行时系统会查找对应参数列表的 operator delete(placement delete)来清理内存,防止泄漏。但在正常情况下,你不应该直接调用 placement delete,而应该手动调用析构函数。
🔄 数组形式的 new[] 和 delete[]
条款也强调了 new[] 和 delete[] 的特殊性:
new[]:先调用operator new[]分配足够容纳多个对象的内存,然后对每个元素调用构造函数。delete[]:先对每个元素调用析构函数,然后调用operator delete[]释放整体内存。- 致命错误:如果对
new[]分配的内存使用delete(而非delete[]),程序行为是未定义的(通常会导致部分对象未析构或内存释放错误)。
💡 核心总结
new操作符 是语言特性,完成内存分配 + 对象构造。operator new是函数,只负责内存分配(可重载)。- Placement new 是在已有内存上构造对象,需要手动管理析构。
new[]必须配delete[],否则会导致资源泄漏和未定义行为。
3. 异常
条款9: 利用destructors避免资源泄露
利用析构函数避免资源泄露的本质,就是将资源的生命周期与对象的生命周期绑定。这是 C++ 中极其重要的 RAII(资源获取即初始化) 思想的直接体现。它是编写异常安全代码和防止资源泄露的最基本、最强大的武器。
条款10: 在constructros内组织资源泄露(resource leak)
在构造函数内组织资源泄露的本质是:由于对象构造失败时析构函数不会运行,你必须依靠 “已构造成功的成员对象”的析构函数来清理资源。因此,最好的策略就是将所有资源句柄(尤其是裸指针)替换为具有析构函数的 RAII 对象(如智能指针) 。
条款11: 禁止exceptions流出destructors之外
-
基本原则
- 绝对不能让异常从析构函数中传播出去
- 如果析构函数可能抛出异常,必须在内部捕获并处理
-
主要原因
- 当容器或数组析构时,会依次调用多个对象的析构函数
- 如果同时出现多个异常,C++无法处理这种情况,会导致程序终止
- 异常传播出去会使程序行为不可预测
-
实现方法
-
在析构函数内使用try-catch块捕获所有异常
-
可以选择:
- 记录日志后终止程序
- 吞掉异常(通常不推荐)
- 提供类方法让用户有机会处理可能出错的资源释放
-
4.栈展开机制(stack_unwinding)
当异常抛出时,C++会进行栈展开:
- 从异常抛出点开始,逐层向上寻找匹配的catch语句
- 在这个过程中,会自动销毁沿途的所有局部对象
- 这些局部对象的析构函数会被调用
5.双重异常的危险性
如果在栈展开期间:
- 某个局部对象的析构函数又抛出了新的异常
- 此时C++运行时系统已经处于处理第一个异常的过程中
- 同时存在两个活跃的异常,C++无法决定应该处理哪一个
- 最终结果:程序立即调用
terminate()函数,进程被强制结束
条款12: 了解”抛出一个exception“与”传递一个参数“或”调用一个虚函数“之间的差异
1. 异常对象的拷贝机制
抛出异常时,异常对象总是被拷贝(by value),而参数传递可以有多种方式:
// 异常抛出 - 总是拷贝
Widget w;
throw w; // 抛出的是w的拷贝,不是w本身
// 函数参数 - 可以传引用
void func(Widget& w) { } // 直接操作原对象
关键区别:
- 抛出异常时,编译器会创建一个异常的静态拷贝
- 即使原对象在栈展开过程中被销毁,异常对象依然存在
- 异常对象存储在特殊的内存位置(不是栈也不是堆)
2. 类型转换的限制
异常匹配的类型转换比函数参数严格得多:
// 函数参数允许的隐式转换
void func(double d) { }
func(1); // int→double 隐式转换 OK
// 异常匹配几乎不允许隐式转换
try {
throw 1; // 抛出int
}
catch(double d) { } // 不会捕获!int不能转成double
catch(const std::exception& e) { } // 也不行,没有继承关系
// 仅允许的两种转换:
// 1. 继承关系:catch (base) 可以捕获 derived
// 2. 类型化指针→void*:catch (void*) 可以捕获任何指针
3. 匹配顺序的差异
- 虚函数:根据对象的动态类型选择最匹配的函数(运行时多态)
- 异常处理:按照catch子句的书写顺序匹配,选择第一个匹配的,不是最佳匹配
cpp
// 虚函数 - 最佳匹配
class Base { virtual void f(); };
class Derived : public Base { void f(); };
Base* pb = new Derived;
pb->f(); // 调用Derived::f() - 最佳匹配
// 异常 - 顺序匹配
try {
throw Derived();
}
catch(Base b) { } // 第一个匹配,被捕获!
catch(Derived d) { } // 永远不会执行(unreachable)
4. 异常catch的两种写法
cpp
// 方式1:传值(会再次拷贝)
catch(Widget w) { } // 拷贝异常对象到w
// 方式2:传引用(推荐)
catch(Widget& w) { } // 直接引用异常对象
catch(const Widget& w) { } // const引用
建议: 总是使用引用捕获,避免不必要的拷贝,也能保留多态性。
总结表
| 特性 | 抛出异常 | 传递参数 | 调用虚函数 |
|---|---|---|---|
| 对象传递 | 总是拷贝 | 可选传值/传引用 | 取决于参数类型 |
| 类型转换 | 极少(继承和void*) | 丰富(隐式转换) | 支持向上转换 |
| 匹配规则 | 顺序匹配 | 重载决议 | 动态类型匹配 |
| 性能 | 较慢(拷贝+栈展开) | 快速 | 快速(虚表) |
条款13:以by reference方式捕捉exceptions
1. 为什么不能用值捕获(catch by value)
class Base {
public:
virtual const char* what() const { return "Base"; }
};
class Derived : public Base {
public:
virtual const char* what() const { return "Derived"; }
};
try {
throw Derived(); // 抛出Derived对象
}
catch (Base b) { // 值捕获!发生对象切片
std::cout << b.what(); // 输出"Base",丢失多态信息
}
值捕获的两大问题:
- 对象切片:派生类对象被切割成基类部分,虚函数调用失效
- 二次拷贝:抛出时一次拷贝,catch时又一次拷贝,性能损失
2. 为什么重新抛出要用 throw 而不是 throw w
// 错误做法:throw w
catch (const Base& w) {
// 想要重新抛出当前捕获的异常
throw w; // 问题:抛出的是w的拷贝,不是原异常对象
// 如果w是基类引用,这里会发生对象切片!
}
// 正确做法:throw;
catch (const Base& w) {
// 记录日志或其他操作
throw; // 重新抛出原来的异常对象,保留完整类型
}
// 完整示例
try {
throw Derived(); // 抛出Derived对象
}
catch (const Base& w) {
// throw w; // 如果错误地写成这样,会抛出Base的拷贝,丢失Derived部分
throw; // 正确:重新抛出原来的Derived对象
}
throw w 的两大问题:
- 对象切片:
throw w会创建一个新异常对象,如果w是基类引用,新对象会被切片 - 丢失类型信息:重新抛出的对象类型是w的静态类型,而不是原异常的实际类型
throw; 的两大优势:
- 保留原异常:直接重新抛出当前捕获的异常对象
- 保持多态性:异常对象的动态类型保持不变
3.核心结论
用by reference方式捕获异常是C++异常处理的黄金法则,它同时解决了对象切片、类型安全和性能损失三大问题。记住:捕获用 const&,重新抛出用 throw;。
条款14:明智运用exception specifications
此章节在现在C++中已经不做讨论,略。
条款15:了解异常处理(exception handling)的成本
成本一:编译时成本(代码体积)
即使程序从未使用try/catch/throw,只要启用了异常支持,编译器就会为每个函数生成异常表,记录栈展开时需要调用的析构函数信息。这导致可执行文件体积增加5-15% 。
成本二:进入try块的成本
// 即使不抛出异常,进入try块也有开销
try { // 这里:需要"激活"异常处理机制,约几条指令
func();
}
catch(...) {
}
- 进入try块会激活异常处理机制
- 成本大约相当于几条指令
- try块嵌套越深,成本越高
成本三:抛出异常的成本(异常路径)
void func() {
std::string s = "hello";
std::vector<int> v(1000);
throw std::runtime_error("error"); // 抛出点
// 栈展开:自动销毁s和v
}
try {
func(); // 正常路径:接近普通函数调用
}
catch(...) {
// 异常路径:成本是正常路径的千倍
}
抛出异常的三个阶段:
- 抛出异常:创建异常对象、查找catch、约10000-100000条指令
- 栈展开:销毁沿途所有局部对象,调用每个对象的析构函数
- 捕获处理:执行catch块代码,销毁异常对象
核心结论
异常处理有三重成本:
- 编译时:代码体积增加5-15%
- 正常路径:进入try块有少量开销
- 异常路径:抛出异常成本是正常返回的千倍以上
使用原则:
- 异常只用于真正的异常情况(如内存耗尽、构造函数失败)
- 不要用异常处理预期中的错误(如输入验证、文件不存在)
- 性能关键代码避免使用异常
条款16:谨记 80-20法则
一个程序80%的执行时间花费在20%的代码上。 优化工作的关键是找出这20%的代码进行优化,而不是均匀地优化所有代码。
条款17:考虑使用lazy evaluation(缓式评估)
1. Reference Counting(引用计数)
string s1 = "Hello"; string s2 = s1;
在对s2只进行读操作时,s2只会和s1共享数据,只有对s2进行真正的写操作,资源拷贝才会发生。
### 2. 区分读和写
### 3. Lazy (缓式取出)
有些LargeObject的体积很大,欲取出此类对象的所有数据,数据库相关操作程序可能成本极高,Lazy (缓式取出)只有在真正访问**对象的该数据**时才会从数据库中加载。<br>
利用此技巧可以实现出“demand-paged”式的对象初始化行为,即在初始化LargeObject时,成员变量字段赋空,只有在对成员变量进行操作时,该字段才会从数据库中读取。
### 4. Lazy Expression Evaluation(表达式缓评估)
不立刻计算表达式的值,只在需要时计算,可以用数据结构存储这种操作。
## 条款18: 分期摊还预期的计算成本(over-eager evaluation)
```cpp
class Test{
public:
int min() const;
int max() const;
int avg() const;
...
};
eager evaluation 调用时立即评估 lazy evaluation 调用时存在某些数据结构中,只有在“这些函数的返回值真正需要被用到时”才计算 &emsp;over-eager evaluation,随时记录数据集中的最小值,最大值,使用时立刻返回。
1. Catching
指将频繁访问的数据暂存在更快的存储层,以减少后续访问延迟。
2. Prefetching
Prefetching 指提前预测即将访问的数据,并将其主动加载到缓存中(空间局部性)
vectort初始化有10个元素,但其在new操作时往往会申请两倍以上的空间。避免数组扩张的成本,空间换时间。
条款19:了解临时对象的来源
临时对象是不可见的:不会在源码出现。
1.当隐式类型转换(implicit type conversions)被施行起来以求函数调用能够成功
1.1函数参数传值
func(MyClass()); // 这里的 MyClass() 是临时对象
1.2 对象被传递给一个reference-to-const参数时
char s[25];
int test(const string& str);
test(s) //可以通过编译,期间会产生一个string临时对象,str参数被绑定到该临时对象上
当对象被传递给一个reference-to-non-const参数时,这种转换并不会发生,例子如下:
char s[25];
int test( string& str);
test(s) //无法通过编译,如果产生一个string临时对象,str参数被绑定到该临时对象上,str的改动不会影响s,违背了引用的直觉
2.当函数返回对象的时候
条款20:协助完成“返回值优化(RVO,return value optimition)"
在早期 C++(C++98 时代),下面代码在直觉上会产生多个对象
class Widget {};
Widget makeWidget() {
return Widget();
}
Widget w = makeWidget();
如果按“语法表面理解”,似乎会发生:
- 构造一个临时
Widget - 拷贝到函数返回值临时对象
- 再拷贝到
w
看起来是:
1 次构造 + 2 次拷贝 + 若干析构
但编译器可以消除这些临时对象,只会发生一次构造。RVO 的核心思想是:
直接在调用者的内存空间中构造返回对象。
函数匿名对象和命名对象编译器都可以进行RVO。
class Widget {};
Widget makeWidget() {
Widget w;
return w;
}
Widget w = makeWidget();
条款21:利用重载技术(overload)避免隐式类型转换(implicit typr conversions)
class Rational {
public:
Rational(int numerator, int denominator = 1);
};
Rational r(1, 2);
r + 2; // 2 会被隐式转换成 Rational(2,1)
不如直接提供
Rational operator+(const Rational&, int);
Rational operator+(int, const Rational&);
条款22: 考虑以操作符复合形式(op=)取代其独身形式(op)
T& T::operator+=(const T& rhs)
{
// 核心逻辑
return *this;
} // operator+=没有临时对象的产生,直接复值给左值自变量
T operator+(const T& lhs, const T& rhs)
{
//核心逻辑
return 局部对象
} //有临时对象的产生
T operator+(T lhs, const T& rhs)
{
lhs += rhs;
return lhs;
} //通过前者实现后者
operator+=没有临时对象的产生,因故比operator+有更高的性能倾向。
条款23: 考虑使用其他程序库
iostream库(c++)类型安全,可扩充,stdio库(c)性能更高
条款24: 了解virtual functions、multiple inheritance、 virtual base classes、runtime type identification(RTTI)的成本
虚函数通过virtual tables (vtbl)与 virtual table pointer (vptr)实现,vbtl是一个虚函数指针构成的数组,在单一继承时,一个class只需一个vbtl和vptr就可以,在多重继承中,需要多个,每个基类对应一个。
凡是声明虚函数的对象,都有一个隐藏的数据成员vptr,用来指向vtbl的位置,vptr在构造函数执行过程中被逐步设置(派生类对象生成,先执行基类构造函数,此时vptr被设置成基类的,再执行剩下的派生类特有的构造函数部分,此时vptr被设置成派生类的)。
virtual base classes解决菱形继承中基类成员重复的问题
class A{...};
class B: virtual public A {};
class C: virtual public A {};
class D: public B, public C {};
其中A是virtual base class, B和C都采用虚拟继承,利用指针指向虚拟virtual base class成分,D对像的内存布局如下图:
上图如果考虑到vptrs,D对象的内存布局为
Rtti 即是runtime type identification让基类指针在运行时识别对象的真实类型机制,其类型也存储在vbtl中,
1. 虚函数为什么不能inline
Base* p = new Derived;
p->f(); // 虚调用
底层等价于 p->vptr->f(p); vptr在运行时通过对象的构造函数才能确定。
技术 Techniques, Idioms, Patterns,本章讨论的是设计模式
条款25: 将constructor 和 non-member functions 虚化
1.协变返回类型(Covariant Return Types)
class Base {
public:
virtual Base* clone() const {
return new Base(*this);
}
};
class Derived : public Base {
public:
Derived* clone() const override { // ✅ 协变返回
return new Derived(*this);
}
};
返回值类型变为子类完全合法,所谓constructor指的是一种设计模式
Base* p1 = new Derived;
Base* p2 = p1->clone(); // ⭐ 像“虚构造”
2. 将非成员函数的行为虚化
ostream& operator<<(ostream& s, const NLComponent& c)
{
return c.print(s); //print为虚函数
}
写一个虚函数做实际工作,非虚函数只负责调用虚函数。
条款26: 限制某个class所能产生的对象数量
1. 允许零个或一个对象
class Singleton {
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
static Singleton& getInstance() {
static Singleton instance; // C++11保证静态局部变量的线程安全初始化
return instance;
}
void doSomething() {
// 业务逻辑
}
};
也可以加入
private:
static size_t numObjects ;
用来限制对象的数量。
2. 不同的对象构造状态
私有构造函数的类不允许作为base class
3. 允许对象的生生灭灭
略
4. 一个用来计算对象个数的base class
略
条款27: 要求(或禁止)对象产生于heap之中
1. 要求对象产生于heap之中(Heap-Based Objects)
修改类constructor和dctor的属性为private、protected,比如单例模式的设计无营养
2. 判断某个对象是否位于Heap内
重写operator new,当使用new operator申请对象时,记录该对象的地址到一个list中,就可以判断对象是否位于heap内,小聪明。
3. 禁止对象产生于heap中
重写static void *operator new(size_t size)操作符,并将其声明为private。
条款28: 智能指针
略
条款29: 引用计数(reference counting)
1.共享机制
2.写时复制
lazy evaluation的一种方式 String的operator[]有const版本和非const版本,编译在调用时根据对象是否是const调用相应的operator[]版本。 String的实现方式和shared ptr的实现方式非常相似,都用了写时复制和引用计数。
条款30:Proxy classes(替身类、代理类)
访问模式:User → Proxy → RealObject,可以帮助我们完成多维数组的重载,左值/右值的区分,压制隐式类型转换。 略
条款31:让函数根据一个以上的对象类型来决定如何虚化
一个“虚函数调用动作”称为“message dispatch”,函数根据多个函数而虚化则被称为multiple dispatch。 此章主要讨论这个问题,用到时查看。略
杂项讨论
条款32: 在未来时态下发展程序
略
条款33: 将非尾端类(non-leaf classes)设计为抽象类(abstract classes)
略
条款34: 如何在同一个程序中结合c++和c
extern C的作用
- C++有函数重载 → 编译时会修改函数名(比如
func变成_Z4funci) - C语言没有函数重载 → 函数名就是原名(就是
func)
如果不加extern "C",C++编译器会把C函数名也改掉,导致链接时找不到函数。
条款35:让自己习惯于标准c++语言
略