🔷 第十四章 RTTI与异常处理
📄 一句话概括

多重继承会让"一个对象"同时拥有好几个合法的地址 ——因为它的每个基类子对象都在这块内存里的不同偏移处。 于是发生三件事: 1. 用 == 比较两个基类指针会说"不相等",其实它们是同一个对象(要用 dynamic_cast<void *> 或转成共同基类); 2. static_cast 在基类指针之间转换时会悄悄调整地址(它不是"值不变"); 3. dynamic_cast 拿到一项独门绝技 ——交叉转换(cross-cast):沿着"共同的后代"从一个基类指针转到另一个无关的基类指针,这是编译期做不到的事。 结论:多重继承本身就要慎用(11.3),一旦用了,指针比较就得改用 dynamic_cast<void *>。

📝 本节和前面几节的关系

11.3 讲过多重继承的两个独有麻烦:二义性与菱形(那是"取名字"和"取数据"的麻烦)。本节讲的是第三个麻烦:地址。 14.1.1 / 14.1.2 讲的 typeid 和 dynamic_cast 在多重继承下照样好用(typeid 完全不受影响);本节要单独拿出来讲的,是"指针值"这件事上出的岔子 ——typeid 是拿类型信息比较,不碰地址,所以它没事。 往回想:7.4.1 说"指针是存地址的变量"、7.4.4 的 void * 是"万能口袋"(能装任何地址、不能解引用) ——本节最后的解药就是 dynamic_cast<void *>。 顺带一个字面提醒:dynamic_cast<void *> 看着别扭("转到 void 是什么意思?"),它是标准专门为"求最派生对象的地址"设计的特殊形式*。

一、先看"一个对象好几个地址"是怎么来的

class Flyer   { public: virtual ~Flyer() {} };
class Swimmer { public: virtual ~Swimmer() {} };
class Duck : public Flyer, public Swimmer { };     // 鸭子又会飞、又会游

一个 Duck 对象在内存里长这样(按"先父后子"的顺序,11.1.1):

       &d(Duck 对象的起始地址)
        │
        ▼
        ┌───────────────────────────┐
        │ Flyer 子对象              │  ← (Flyer *)&d 指向这里(偏移 0)
        │  ├ vptr → Flyer 的段       │
        ├───────────────────────────┤
        │ Swimmer 子对象            │  ← (Swimmer *)&d 指向这里(偏移若干字节)
        │  ├ vptr → Swimmer 的段     │
        ├───────────────────────────┤
        │ Duck 自己的成员(如果有)  │
        └───────────────────────────┘

所以这三个指针里装的值并不都相同:

表达式 指向
&d 整个 Duck 对象的起始地址
(Flyer *)&d Flyer 子对象的地址(偏移 0,通常和 &d 相同)
(Swimmer *)&d Swimmer 子对象的地址(有偏移,和上面不同)

它们都是"合法的、正确的、指向同一个对象的指针",但装的值不一样。

二、后果一:用 == 比较基类指针会骗你

Duck d;
Flyer   *pf = &d;
Swimmer *ps = &d;

(pf == ps)          // ⚠️ 直接比较:编译不过(两个指针类型无关,不允许比)
((void *)pf == (void *)ps)      // 0 —— 同一个对象,却说不相等!

这就是多重继承给"指针比较"设的坑:把两个指针转成 void * 再比,本来是"判断是不是同一个对象"的常见手法,在多继承下会给出错误答案。

正确的三种办法:

办法 写法 说明
① 比"最派生对象"的地址(推荐) dynamic_cast<void *>(pf) == dynamic_cast<void *>(ps) 标准专门为此提供的:dynamic_cast<void *> 返回"指向最派生对象的指针"
② 转到共同基类再比 dynamic_cast<Common *>(pf) == dynamic_cast<Common *>(ps) 前提是它们有一个共同基类
③ 别比地址 在基类里加一个 whoAmI() / 用 typeid 比类型 "是不是同一个对象"用别的办法回答(很多时候你真正想问的其实是"是不是同一类")

dynamic_cast<void *> 的规则(记住这三点):

void *p1 = dynamic_cast<void *>(pf);     // 指向整个 Duck 对象
void *p2 = dynamic_cast<void *>(ps);     // 也指向整个 Duck 对象
cout << (p1 == p2);                      // 1 —— 这次对了

三、后果二:static_cast 会"调整地址"

这件事反直觉,但必须知道:

Duck *pd = &d;
void *v = pd;                 // 地址 = d 的起始
Swimmer *ps = static_cast<Swimmer *>(pd);
void *w = ps;                 // ⚠️ 地址 = d 的起始 + 偏移

两个 void * 不一样。 因为 static_cast 不只是"改个标签",它还会按继承布局算出正确的偏移、把地址调整过去。

这是"必要之恶":不调整的话,ps 指向的就不是 Swimmer 子对象,调 ps->swim() 就会拿到错误的 vptr、跳到错误的函数。 所以编译器必须调整。

推论:

四、后果三:dynamic_cast 的独门绝技 ——交叉转换

回到上面的鸭子:

Flyer *pf = &d;                          // 现在我只剩一个 Flyer*,但我知道它其实是鸭子
Swimmer *ps = static_cast<Swimmer *>(pf);   // ❌ 编译不过!Flyer 和 Swimmer 没关系
Swimmer *ps = dynamic_cast<Swimmer *>(pf);  // ✅ 成功!而且 ps 指向正确的 Swimmer 子对象

这就是交叉转换(cross-cast):从一个基类指针,直接转到"另一个不相关的基类"指针 ——条件是"它们其实是同一个对象的两张脸"。

转换方向 例子 谁做得到
向上(子 → 父) Duck * → Flyer * static_cast 就行(隐式转换也可)
向下(父 → 子) Flyer * → Duck * static_cast 能编过但不检查;dynamic_cast 会检查
横向 / 交叉(父 → 兄弟父) Flyer * → Swimmer * 只有 dynamic_cast 能做

交叉转换是"编译期信息不够"的典型例子:编译器只知道 pf 的静态类型是 Flyer *,它凭这个信息想不出 Swimmer 子对象在哪儿(可能它根本不是鸭子,是个纯 Flyer)。只有运行期问一句"你到底是谁",才能算出来。

所以:交叉转换是 RTTI 最"不可替代"的用途之一。(另一个是 14.1.4 会讲的 dynamic_cast 向下转型。)

五、菱形继承下会怎样

顺便把 11.3.2 的菱形接过来:

class A  { public: virtual ~A() {} };
class B1 : public virtual A {};      // 虚继承
class B2 : public virtual A {};
class D  : public B1, public B2 {};
情况 A *pa = &d; dynamic_cast<A *>(pb1) 与 dynamic_cast<A *>(pb2)
非虚继承(普通菱形) 编译错误:ambiguous(d 里有两份 A 子对象,编译器不替你猜) 各自给"自己那条路上的那一份 A",两个地址不同
虚继承(11.4.3) 合法(A 子对象全对象只有一份) 两个得到同一个地址(都指向那唯一的一份)

一句话:虚继承就是"用运行期的绕路,换回'祖先只有一份'" ——它让地址重新变得可预测,代价是多一层间接(11.4.3 说过)。

六、动手 Demo:会不会飞的鸭子

任务:把"一个对象好几个地址"完整看一遍 ——包括"错误地比较"和"正确地比较"。

#include <iostream>
#include <typeinfo>
using namespace std;

class Flyer {
public:
    virtual ~Flyer() {}
    virtual void fly() const { cout << "(飞起来了)" << endl; }
};

class Swimmer {
public:
    virtual ~Swimmer() {}
    virtual void swim() const { cout << "(游起来了)" << endl; }
};

class Duck : public Flyer, public Swimmer {      // 鸭子两样都会
public:
    void fly()  const { cout << "(鸭子飞起来了)" << endl; }
    void swim() const { cout << "(鸭子下水了)" << endl; }
    void quack() const { cout << "嘎嘎" << endl; }
};

int main() {
    Duck d;
    Flyer   *pf = &d;
    Swimmer *ps = &d;

    cout << "-- (1) 三个指针的值 --" << endl;
    cout << "(void *)&d   = " << (void *)&d  << "   ← 整个对象" << endl;
    cout << "(void *)pf   = " << (void *)pf  << "   ← Flyer 子对象" << endl;
    cout << "(void *)ps   = " << (void *)ps  << "   ← Swimmer 子对象(不一样!)" << endl;

    cout << "-- (2) 判断是不是同一个对象 --" << endl;
    cout << "(void *)pf == (void *)ps            → "
         << ((void *)pf == (void *)ps) << "   ← 错了!" << endl;
    cout << "dynamic_cast<void *> 比较            → "
         << (dynamic_cast<void *>(pf) == dynamic_cast<void *>(ps)) << "   ← 对了" << endl;

    cout << "-- (3) 交叉转换:只剩 Flyer*,也能变回 Swimmer* --" << endl;
    Swimmer *back = dynamic_cast<Swimmer *>(pf);      // ★ 横向转
    if (back != nullptr) {
        cout << "转换成功,鸭子:";
        back->swim();
    } else {
        cout << "它不是鸭子,只是会飞" << endl;
    }

    cout << "-- (4) typeid 完全不受地址影响 --" << endl;
    cout << "typeid(*pf).name() = " << typeid(*pf).name() << endl;
    cout << "typeid(*ps).name() = " << typeid(*ps).name() << endl;

    cout << "-- (5) 万一它不是鸭子 --" << endl;
    Flyer onlyFly;
    Flyer *pf2 = &onlyFly;
    Swimmer *no = dynamic_cast<Swimmer *>(pf2);
    cout << "对一只纯 Flyer 做交叉转换:"
         << (no == nullptr ? "给了 nullptr(安全)" : "成功了") << endl;
    return 0;
}

看到什么算成功:

-- (1) 三个指针的值 --
(void *)&d   = 0x62fdf0   ← 整个对象
(void *)pf   = 0x62fdf0   ← Flyer 子对象
(void *)ps   = 0x62fdf8   ← Swimmer 子对象(不一样!)
-- (2) 判断是不是同一个对象 --
(void *)pf == (void *)ps            → 0   ← 错了!
dynamic_cast<void *> 比较            → 1   ← 对了
-- (3) 交叉转换:只剩 Flyer*,也能变回 Swimmer* --
转换成功,鸭子:(鸭子下水了)
-- (4) typeid 完全不受地址影响 --
typeid(*pf).name() = 4Duck
typeid(*ps).name() = 4Duck
-- (5) 万一它不是鸭子 --
对一只纯 Flyer 做交叉转换:给了 nullptr(安全)

读结果:

30 秒实验

  1. 把第 (3) 的 dynamic_cast<Swimmer *>(pf) 改成 static_cast<Swimmer *>(pf) → 编译不过(invalid static_cast from type 'Flyer*' to type 'Swimmer*')。确认"交叉转换只有 dynamic_cast 能做"。
  2. 在第 (5) 把 dynamic_cast 改成 reinterpret_cast(14.1.4 会讲)→ 编过了,但 no 不是 nullptr ——它会给你一个"指错了地方"的指针,一用就崩。 这就是"带检查"和"不带检查"的区别。
  3. 加一行 cout << (void *)dynamic_cast<void *>(pf) << endl; → 它和 (void *)&d 相同(都是"对象从头开始"那个地址)。
  4. 把两个基类改成"都虚继承自一个公共基类"(组成菱形),再跑一次:两个 dynamic_cast<A *> 会给同一个地址(11.4.3 的虚继承在起作用)。

常见误区

  1. "'同一个对象'就是'指针相等'" ——在多继承下不成立。要用 dynamic_cast<void *> 比,或者干脆别比地址。
  2. "static_cast 转指针不会改变值" ——会改变(它要按基类布局调整偏移)。"零开销"说的是"不需要运行期查找",不是"值不变"。
  3. "dynamic_cast 只能向下转(父 → 子)" ——还能横向转(交叉转换)。这是它独有的能力,也是 RTTI 最正当的用途之一。
  4. "dynamic_cast 到了非多态类型也能用" ——不能。两边都必须是多态类型(有虚函数)。多重继承里如果某一层忘了虚函数,交叉转换就会失败或编译不过。
  5. "指针比较的问题只有多继承才有" ——单继承里也有,只是更隐蔽:单继承下"基类子对象"常常在偏移 0,所以两个指针碰巧相等(看起来没事)。一旦加了第二个基类,偏移不再是 0,坑才浮出水面。 "碰巧对"不是"对"。
  6. "那我干脆不用多重继承" ——该不该用是 11.3 的事(能用组合就别继承)。本节要你记住的是一条"用了之后必须遵守的规矩":拿基类指针做"同一性判断"or 当哈希 key 时,必须走 dynamic_cast<void *>。
  7. "typeid 在多重继承下也会错" ——不会。typeid 拿到的是最派生类型,和你在哪张"脸"上问无关(Demo 第 (4) 步:不管 pf 还是 ps,都答 Duck)。
自测三问(点开对答案) 1. **为什么多重继承下"一个对象会有多个地址"?** ——因为**对象的每个基类子对象都排在内存的不同偏移处**(按继承声明顺序),**指向不同基类子对象的指针,装的值自然不同**(偏移 0 的那个常常和对象起始地址相同)。 2. **要判断两个基类指针"是不是同一个对象",正确写法是什么?** ——**`dynamic_cast(p) == dynamic_cast(q)`** ——**它返回"指向最派生对象的指针",也就是对象从头开始的那个地址**。**(前提:类型是多态的。)** 另一条路是转到共同基类再比,或者改用 `typeid` 比类型。 3. **什么是交叉转换?为什么只有 `dynamic_cast` 能做?** ——**从一个基类指针转到"另一个不相关的基类"指针**(如 `Flyer *` → `Swimmer *`),条件是它们其实是同一个对象。**编译期只知道静态类型,算不出另一个基类子对象的偏移**(甚至可能根本不是那个对象),**所以必须在运行期问类型信息才能算出地址** ——**这正是 RTTI 的存在意义。**

相关笔记