📄 一句话概括
语法是 class 子类 : public A, public B { };——一个冒号、一列父类、每个前面都写 public。对象里是这样排的:A 那部分 + B 那部分 + 自己新增的(按继承声明顺序),所以 sizeof 大致是"各父类加起来"。访问不同父类的同名成员时要明确指名:obj.A::who()。
📝 本节和前面几节的关系
上一节(11.3)讲了"多重继承是什么、有什么麻烦";本节把语法和对象布局落定,方式和 11.1.1 落定单继承完全一致。这里能看到一个单继承里看不到的现象:"父类子对象"不止一个,而且各自有各的偏移——这正是 11.3.2 二义性的物理原因,也是 11.4.3 虚继承要动手改的东西。
一、语法:一列父类,每个都带 public
class 子类名 : public 父类1, public 父类2 {
// 新增成员
};
和单继承对照:
class A : public Base { }; // 单继承:一个父类
class B : public P1, public P2, public P3 { }; // 多重继承:一列
四条规则:
| 规则 | 说明 |
|---|---|
| 逗号分隔 | 父类之间用 , |
| 每个父类前写继承方式 | 漏了就是 private(class 的默认)。这是多重继承里最常见的低级错 |
| 同一个直接父类不能写两次 | class C : public A, public A ❌(重复继承同一个类是错的;菱形继承不是"重复写",而是"两条路径"——那是另一回事,11.3.2) |
| 父类要有定义 | 至少要有完整定义(不像前向声明只能用于指针) |
二、对象里怎么排:A 部分 + B 部分 + 自己
class A { public: int a; };
class B { public: int b; };
class C : public A, public B { public: int c; };
C 对象的内存:
┌──────────── C 对象 ────────────┐
│ ┌── A 子对象 ──┐ │ ← 按继承声明顺序,A 在前
│ │ a │ │
│ └──────────────┘ │
│ ┌── B 子对象 ──┐ │
│ │ b │ │
│ └──────────────┘ │
│ ┌── C 自己 ────┐ │
│ │ c │ │
│ └──────────────┘ │
└────────────────────────────────┘
由此推出:
sizeof(C)= 各父类子对象 + 自己新增的(4 + 4 + 4 = 12),可能因对齐再加填充。- 每个父类子对象按"继承声明顺序"排——换一下书写顺序,对象布局就变(
sizeof可能还一样,但构造顺序会变,11.3.3 验证)。 C*转成A*时地址可以不变;转成B*时地址要往后挪(因为B子对象不在开头)——这种"指针调整"是多重继承的真实开销,也是为什么多继承的对象往往比"各部分之和"再大一点点(有些实现要存偏移信息)。
三、访问不同父类的同名成员:必须指名
class A { public: void who() { cout << "A"; } };
class B { public: void who() { cout << "B"; } };
class C : public A, public B { };
C obj;
// obj.who(); // ❌ 二义性:A 的 who 还是 B 的 who?
obj.A::who(); // ✅ 明确指名
obj.B::who(); // ✅
obj.A::who() 读作:"在 obj 这个对象里,找 A 那一部分,调它的 who"。
要区分两个概念(这也是 10.1.2 的 ::):
| 写法 | 含义 |
|---|---|
obj.A::who() |
指名"从哪个父类那一份"调用(解决二义性) |
C::who() |
在类外定义 C 自己的 who 时标归属(10.1.2 的老用法) |
四、动手 Demo:两个父类、三种访问
#include <iostream>
using namespace std;
class A {
public:
int a;
A() : a(1) { }
void who() const { cout << "我是 A" << endl; }
};
class B {
public:
int b;
B() : b(2) { }
void who() const { cout << "我是 B" << endl; }
};
// ★ 两个父类,各自都写 public
class C : public A, public B {
public:
int c;
C() : c(30) { // A、B 会按声明顺序自动先构造
a = 10; // a 是 public 的,可以在函数体里改
b = 20;
}
};
int main() {
C obj;
cout << obj.a << " " << obj.b << " " << obj.c << endl;
// obj.who(); // ❌ 报二义性(A、B 都有 who)
obj.A::who(); // ✅ 指名调用
obj.B::who(); // ✅
cout << "sizeof(A) = " << sizeof(A)
<< ",sizeof(B) = " << sizeof(B)
<< ",sizeof(C) = " << sizeof(C) << endl;
return 0;
}
看到什么算成功:
10 20 30
我是 A
我是 B
sizeof(A) = 4,sizeof(B) = 4,sizeof(C) = 12
读结果:
| 现象 | 说明 |
|---|---|
obj.a、obj.b、obj.c 都能直接访问 |
两个父类的 public 成员都成了 C 对象的成员(没有重名,就没有二义) |
obj.who() 编译不过,obj.A::who() 可以 |
同名成员必须指名——这就是 11.3.2 要展开的二义性 |
sizeof(C) = 12 = 4 + 4 + 4 |
对象里同时存在 A 和 B 两个子对象,再加上自己那个 c |
30 秒实验:
1. 把 class C : public A, public B 改成 class C : public A, B(把 B 前面的 public 删掉)。
2. 重新编译 → obj.b 报错:'int B::b' is inaccessible(或 is private within this context)。
3. 原因:B 变成了 private 继承,于是 B 的 public 成员在 C 里被降级成 private——外界(main)就碰不到了(11.1.2 的表)。
4. 加回 public,就好。 多重继承里,这个错会以"父类名 ×2"的形式出现,务必检查每一个父类前面的关键字。
五、常见误区
- "多个父类之间用分号或空格分隔"——用逗号
,。写成分号是语法错。 - "第二个父类前的继承方式可以省"——不能省,省了就是
private继承,父类公有成员在子类里统统变私有。这是多重继承里最典型的"代码看起来对、实际访问不到"的 bug。 - "对象里两个父类的部分混在一起"——不是。它们是两个独立的子对象,按继承声明顺序排列。这也是为什么
sizeof(C)是各个部分加起来(要留一份 A 的、一份 B 的)。 - "
C*转A*和转B*一样简单"——不一样。只有一个父类子对象在对象开头,另一个不在。"转到不在开头的那个"需要做指针调整,这也是多重继承带来的额外开销。 - "多重继承中的
obj.who()会自动挑一个"——不会,直接报错。C++ 的原则是"有歧义就让编译器报错,绝不替你猜"——报错比猜错好。
自测三问(点开对答案)
1. 多重继承的语法怎么写?最容易漏什么?——`class 子类 : public A, public B { };`。**最容易漏第二个及之后父类前的 `public`**(漏了就是 `private` 继承)。 2. 一个 `C : public A, public B` 的对象里,内存怎么排?——**A 的子对象 + B 的子对象 + C 自己新增的**,顺序按继承声明顺序;`sizeof(C)` 大致是各部分之和(可能加对齐填充)。 3. `obj.who()` 报二义性时怎么解决?——**明确指名**:`obj.A::who();` 或 `obj.B::who();`。**(另一种思路是在 `C` 里自己定义一个 `who()`,把那两个名字都盖住——但通常不如指名清楚。)**- ☐ 11.3.2 二义性(下一节)