Menu

C++ 继承:基类与派生类详解

了解 C++ 继承如何让派生类复用并扩展基类:语法、public 与 private 继承、构造函数和析构函数的调用顺序,以及对象切片等常见陷阱。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

通过扩展来复用类

你已经学会了用构造函数创建类、用析构函数做清理。继承是下一步:不必把一个类的成员复制到另一个类中,而是声明新类是某个已有类的特化版本,让它自动继承所有内容。

已有的类叫作基类(或父类),新类叫作派生类(或子类)。派生类一开始就拥有基类的全部数据和行为,然后再添加或修改让它与众不同的部分。这是 C++ 表达“is-a”(是一个)关系的主要工具:Dog 是一种 Animal,SavingsAccount 是一种 BankAccount。

基本语法:class Derived : public Base

在派生类名后面加上冒号、访问说明符和基类名,就完成了继承。最常见的形式是 public 继承。

Dog 从未声明 name 或 eat(),但两者都能在 Dog 对象上使用,因为它们继承自 Animal。派生类还可以随意添加 bark() 这样的成员,而基类对此一无所知。

protected:只给子类用的成员

基类的 private 成员在派生类中不能访问,继承并不会破坏封装。如果你希望基类的内部细节对外部隐藏、但对子类可用,就使用 protected 访问说明符。

可以把这三个级别想象成同心圆:private 是“只有这个类”,protected 是“这个类及其后代”,public 是“所有人”。完整说明请参阅访问说明符。

构造函数和析构函数的顺序

派生对象中包含一个基类子对象,在构建派生部分之前,基类部分必须已经存在。所以构造时先基类,析构时先派生类(恰好相反)。如果基类的构造函数需要参数,就通过成员初始化列表传给它。

输出直观地展示了这个顺序:

Animal ctor: Rex   // base built first
Dog ctor           // then the derived part
Dog dtor           // destroyed in reverse...
Animal dtor: Rex   // ...base last

如果你忘了写 : Animal(n),而基类又没有默认构造函数,代码就无法编译,因为 C++ 不知道如何构建基类部分。打算被继承的基类几乎总是应该声明一个析构函数(下一页会讲到,通常还应该是虚析构函数)。

重写:重新定义基类方法

派生类可以声明一个签名相同的方法来替换继承来的方法。仍然可以通过 Base::method() 调用原来的版本。

这只是普通的名字隐藏,而不是多态:执行哪个 describe(),在编译时就由变量的静态类型决定了。这是一个关键的限制:如果你通过一个实际指向 Circle 的 Shape& 或 Shape* 调用,得到的仍然是 Shape::describe()。要解决这个问题需要 virtual,这正是下一页的主题。

小心对象切片

因为基类的引用或指针可以指向派生对象,人们很容易想把派生对象复制到一个基类类型的变量中。不要这样做,派生部分会被切掉。

a 是一个真正的 Animal,而不是贴着基类标签的 Dog,所以它根本没有 breed。要以多态的方式使用派生对象,必须使用基类的引用(Animal&)或指针(Animal*),绝不能用基类的值。切片是静默发生的:代码能顺利编译,只是悄悄丢掉了数据,这让它成为最容易被带进正式代码的继承 bug 之一。

要避免的常见错误

  • 以为子类能访问基类的 private 成员。 不能。派生类确实需要的数据用 protected,真正的内部状态保持 private。
  • 忘记把参数传给基类构造函数。 如果基类没有默认构造函数,就必须在派生类构造函数的初始化列表中显式调用它(: Base(args))。
  • 把派生对象切片成基类的值。 把 Dog 复制到 Animal 中,会丢掉所有 Dog 特有的内容。应该传递和保存基类的引用或指针。
  • 为了复用代码而滥用继承。 继承表达的是“is-a”关系。如果关系其实是“has-a”(Car 有一个 Engine),就优先使用组合(一个成员对象),而不是继承。

下一步:虚函数

刚才看到的重写是在编译时确定的,所以通过基类指针调用时会忽略派生类的版本。下一页“虚函数”会介绍 virtual 关键字和 override,正是这一机制让运行时的实际类型决定执行哪个方法,从而实现真正的多态,并解释为什么基类需要虚析构函数。

常见问题

C++ 中的继承是什么?

继承让你基于一个已有的类(基类)来定义一个新类(派生类)。派生类会自动获得基类的数据成员和成员函数,并且可以添加新的成员或替换已有的行为。它表达的是“is-a”(是一个)关系,比如 Dog 是一种 Animal,也是 C++ 在类层次结构中复用和扩展代码的主要方式。

C++ 中 public 继承和 private 继承有什么区别?

使用 public 继承(class Dog : public Animal)时,基类的公有接口在派生类中仍然是公有的,所以 Dog 是一个 Animal,可以用在任何需要 Animal 的地方。使用 private 继承时,继承来的成员都变成私有的:派生类复用了基类的实现,但不能替代基类使用。public 继承是绝大多数情况下的选择;只有在“以基类来实现”的复用场景中才考虑 private 继承。

C++ 继承中构造函数和析构函数的执行顺序是怎样的?

构造函数先执行基类的:在派生类构造函数的函数体执行之前,基类已经完全构造好了。析构函数的顺序则恰好相反:先派生类,再基类。这保证了在派生对象构造或销毁的过程中,它所依赖的每个部分都已经存在(或仍然存在)。

Coddy 编程语言插图

和 Coddy 一起学编程

开始