继承的写法只有一行:class 派生类名 : 继承方式 基类名 { 新增成员 };。派生类吸收父类的一切成员(除了构造/析构/复制构造/赋值那四个"特殊函数"),再扩充自己的。理解继承最有用的一幅图是对象的内存布局:父类那一份永远在最前面,子类自己新增的跟在后面。
11.1 讲了"为什么要继承",本节把语法外壳补全——和 10.1.2 补全"类的语法外壳"是同一个套路。本节还会用到 10.2/10.3 的知识来回答一个关键问题:"构造函数、析构函数为什么不继承"(因为它们跟"对象的一生"绑定,而每个类的一生不一样)。
一、语法拆解
class 派生类名 : 继承方式 基类名 {
// 派生类新增的成员(父类已有的,不用重复写)
};
逐项说明:
| 部分 | 说明 | 容易错的地方 |
|---|---|---|
: 冒号 |
分隔"新类的名字"和"它继承谁" | 忘了冒号:报 expected '{' before ':' 之类 |
| 继承方式 | public / protected / private 三选一 |
不写的话,class 默认 private、struct 默认 public——这一条坑了很多人的第一次继承 |
| 基类名 | 已经定义好的类(必须先有定义,或至少前向声明) | 基类还没定义就继承:invalid use of incomplete type |
{} + 分号 |
和普通类一样,末尾分号别漏(10.1.2 的老坑) | 漏了报错跑到后面几十行去 |
三种继承方式并排看:
class D1 : public B { }; // 最常用:D1"是一种" B(is-a)
class D2 : protected B { }; // 少见:B 的接口只留给 D2 的子孙
class D3 : private B { }; // "借 B 的实现用",不算"是 B"(实现复用)
class D4 : B { }; // ⚠️ class 默认就是这一种 = private 继承
三种方式的差别,全在"权限怎么变"上——那是 11.1.2 的内容。 本节先记一条:99% 的场合写 public。
二、派生类吸收了什么、没吸收什么
这是本节最该记住的一张表。父类的东西不是"全都能用",也不是"全都继承":
| 父类的成员 | 是否进入派生类 | 在派生类里能否直接用 |
|---|---|---|
public 成员(变量/函数) |
✅ 进入 | ✅ 能(看继承方式,11.1.2) |
protected 成员 |
✅ 进入 | ✅ 能(这是它存在的意义) |
private 成员 |
✅ 进入(占着内存) | ❌ 不能(只能通过父类的 public/protected 函数去用) |
| 静态成员 | ✅ 进入(全类共享那一份) | ✅ 能(10.4.3) |
| 构造函数 | ❌ 不继承 | 必须自己写,或显式调用父类的(11.1.4) |
| 析构函数 | ❌ 不继承 | 必须自己写(需要的话) |
| 复制构造函数 | ❌ 不继承(10.2.2) | 需要就自己写,里面记得调父类的 |
operator= 赋值运算符 |
❌ 不继承(同复制构造一个道理) | 需要就自己写 |
那四个特殊函数为什么不继承? 因为它们管的是"对象自己的一生"(出生、复制、赋值、消亡)。父类的一生和子类的一生不一样——子类多了成员,就有多出来的活要干(多初始化一个导师、多释放一块内存)。编译器不放心让父类的做法直接接管子类,所以这四件事每个类都要自己表态。
注意一个很容易误解的点:父类的 private 成员也"进了"子类对象——它在内存里确实存在,只是语法上不许你碰。第十章那个 Account 类如果被继承,balance 依然占着子类对象的字节。
三、继承的两种口径:吸收 + 扩充
教科书常用这组词,很好用:
派生类 = 基类(吸收来的全部) + 新增成员(扩充出来的)
这句话在"内存"和"权限"两个层面都成立:
- 内存:子类对象 = 父类子对象 + 自己新增的成员(父类部分在前)。
- 权限/接口:子类对外暴露的接口 = 继承来的接口 + 自己新增的接口。
四、对象里到底长什么样
class Point {
protected:
int x, y;
public:
// ...
};
class Point3D : public Point {
private:
int z;
public:
// ...
};
Point3D 对象在内存里是这样排的:
┌──────────── Point3D 对象 ────────────┐
│ ┌──── Point 那部分(父类子对象)──┐ │
│ │ x (4 字节) y (4 字节) │ │
│ └─────────────────────────────────┘ │
│ ┌──── Point3D 自己新增的 ─────────┐ │
│ │ z (4 字节) │ │
│ └─────────────────────────────────┘ │
└───────────────────────────────────────┘
三条由此推出的结论:
sizeof(Point3D)= 12 =sizeof(Point)(8) +z(4)(可能因对齐再多几个字节的填充)。- "父类部分在最前面" → 这也是为什么
Point *p = &p3d;是合法的(父类指针指向子类对象时,地址可以直接用,不用调整);多重继承时(11.3)就不一定了,那时可能需要指针调整。 p3d.x和((Point*)&p3d)->x说的是同一块内存——"同一个对象的两个视角"。
五、动手 Demo:二维点 → 三维点
#include <iostream>
using namespace std;
class Point {
protected:
int x, y;
public:
void set(int a, int b) { x = a; y = b; }
void show() const { cout << "(" << x << "," << y << ")"; }
};
class Point3D : public Point {
private:
int z;
public:
void set3(int a, int b, int c) {
set(a, b); // ← 借父类的 set(不用写 Point::)
z = c;
}
void show3() const {
show(); // ← 借父类的 show
cout << " z=" << z;
}
};
int main() {
Point3D p;
p.set3(1, 2, 3);
p.show3();
cout << endl; // (1,2) z=3
p.set(10, 20); // ★ 父类的公有函数,子类对象照样能直接调
p.show3();
cout << endl; // (10,20) z=3
cout << "sizeof(Point) = " << sizeof(Point) << endl;
cout << "sizeof(Point3D) = " << sizeof(Point3D) << endl;
return 0;
}
看到什么算成功:
(1,2) z=3
(10,20) z=3
sizeof(Point) = 8
sizeof(Point3D) = 12
读结果:
p.set(10, 20)能直接调——父类的public成员,就是子类对象的成员,不需要什么特殊写法。Point3D的对象既能当二维点用,又能当三维点用——这就是"站在肩膀上"的直观感受。
30 秒实验:把 class Point3D : public Point 里的 public 删掉(变成 class Point3D : Point),只管保存、重新编译。
- 你会看到 p.set(10, 20); 报错:'void Point::set(int, int)' is inaccessible。
- 原因:class 默认 private 继承,把父类所有公有成员在子类里一键降级成了 private——于是外界就碰不到了(11.1.2 讲全)。
- 把 public 加回去,就好。 这一删一加,"继承方式不能省"这条就再也忘不了。
六、常见误区
- "继承方式不写就是
public"——错。class默认private继承,struct默认public继承(和默认成员权限的规律完全一致)。这也是"新手写继承编译不过"的最常见原因——明明父类成员是public,子类里却"访问不到"。 - "构造函数也会被继承"——不会。子类构造时必须自己写,或者在初始化列表里显式调用父类的构造函数(11.1.4)。如果父类没有无参构造,子类不显式调用就编译不过。
- "父类的
private成员在子类对象里没有"——有,只是碰不到。它在内存里实实在在占着位置,也会被父类的构造函数初始化、被父类的公有函数读写。"看不见"≠"不存在"。 - "子类可以随便把父类的
private改成public用using提上来"——不行。using Base::成员;只能改变子类能访问到的成员的权限(比如把父类的protected在子类里提升成public);父类的private成员,子类连名字都访问不到,更谈不上提升。 - "多继承一份父类的代码,代码量会增加"——恰恰相反。代码量减少(共同的只写一遍),代价是耦合增加(父类一改,所有子类都要重新想一遍)。继承省的是行数,多的是责任。
自测三问(点开对答案)
1. `class D : B { };` 里 D 是什么继承方式?为什么这是坑?——**`private` 继承**(`class` 的默认)。因为它会把父类的 `public` / `protected` 成员**在子类里统统降级成 `private`**,外界就访问不到了——**代码看起来没写错,却报 `is inaccessible`**。 2. 父类哪四个成员函数不会被继承?——**构造函数、析构函数、复制构造函数、`operator=` 赋值运算符**。因为它们都跟"对象的一生"绑定,而子类的一生比父类多几件事要干。 3. 父类的 `private` 成员在子类对象里存在吗?子类能直接用它吗?——**存在**(占着内存,被父类正常初始化);但**子类不能直接访问**,只能通过父类留下的 `public` / `protected` 函数间接使用。- ☐ 11.1.2 继承后的可访问性(下一节)