🔷 第十一章 继承与派生
📄 一句话概括

继承的写法只有一行: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 字节)                    │  │
│  └─────────────────────────────────┘  │
└───────────────────────────────────────┘

三条由此推出的结论:

  1. sizeof(Point3D) = 12 = sizeof(Point)(8) + z(4)(可能因对齐再多几个字节的填充)。
  2. "父类部分在最前面" → 这也是为什么 Point *p = &p3d; 是合法的(父类指针指向子类对象时,地址可以直接用,不用调整);多重继承时(11.3)就不一定了,那时可能需要指针调整。
  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

读结果:

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 加回去,就好。 这一删一加,"继承方式不能省"这条就再也忘不了。

六、常见误区

  1. "继承方式不写就是 public"——错。class 默认 private 继承,struct 默认 public 继承(和默认成员权限的规律完全一致)。这也是"新手写继承编译不过"的最常见原因——明明父类成员是 public,子类里却"访问不到"。
  2. "构造函数也会被继承"——不会。子类构造时必须自己写,或者在初始化列表里显式调用父类的构造函数(11.1.4)。如果父类没有无参构造,子类不显式调用就编译不过。
  3. "父类的 private 成员在子类对象里没有"——有,只是碰不到。它在内存里实实在在占着位置,也会被父类的构造函数初始化、被父类的公有函数读写。"看不见"≠"不存在"。
  4. "子类可以随便把父类的 private 改成 public 用 using 提上来"——不行。using Base::成员; 只能改变子类能访问到的成员的权限(比如把父类的 protected 在子类里提升成 public);父类的 private 成员,子类连名字都访问不到,更谈不上提升。
  5. "多继承一份父类的代码,代码量会增加"——恰恰相反。代码量减少(共同的只写一遍),代价是耦合增加(父类一改,所有子类都要重新想一遍)。继承省的是行数,多的是责任。
自测三问(点开对答案) 1. `class D : B { };` 里 D 是什么继承方式?为什么这是坑?——**`private` 继承**(`class` 的默认)。因为它会把父类的 `public` / `protected` 成员**在子类里统统降级成 `private`**,外界就访问不到了——**代码看起来没写错,却报 `is inaccessible`**。 2. 父类哪四个成员函数不会被继承?——**构造函数、析构函数、复制构造函数、`operator=` 赋值运算符**。因为它们都跟"对象的一生"绑定,而子类的一生比父类多几件事要干。 3. 父类的 `private` 成员在子类对象里存在吗?子类能直接用它吗?——**存在**(占着内存,被父类正常初始化);但**子类不能直接访问**,只能通过父类留下的 `public` / `protected` 函数间接使用。

相关笔记