☰
C++基础笔记(5):继承
2026/10/5 2:04:04 网站建设 项目流程

在前面的内容中,我们已经学习了类与对象、成员函数、构造与析构、静态成员等基础知识”。然而,仅仅能够定义独立的类还远远不够。在实际软件开发中,各个对象之间往往存在明确的层次关系,例如“学生属于人”“狗属于动物”“电动车属于交通工具”等。如果每个类都从零开始编写,不仅效率低下,还会造成大量重复代码。

为了解决这一问题,面向对象编程引入了继承(Inheritance)机制。继承允许我们在已有类的基础上创建新类,新类自动拥有原有类的属性和行为,并且可以根据需要进行扩展或修改。正如笔记中所强调的,继承体现的是一种典型的“is-a” 关系,它不仅实现了代码复用,更重要的是建立了清晰的类型层次结构,使程序设计更加符合现实世界的逻辑。

1.继承的基本概念

继承(Inheritance)允许我们在已有类的基础上创建新的类,使新类自动获得原有类的数据成员和成员函数,从而避免重复编写相同代码,提高开发效率和程序的可维护性。

1.1.基类和派生类

在继承关系中,存在两个基本角色:基类(Base Class)和派生类(Derived Class)。

  • 基类:被继承的类,也称为父类(Parent Class)
  • 派生类:通过继承得到的新类,也称为子类(Child Class)

当一个类继承另一个类时,派生类会自动拥有基类中定义的成员变量和成员函数。开发者只需要在派生类中添加新的成员或修改部分行为,就可以构建出新的功能类,而不需要重新实现已有逻辑。例如,可以定义一个表示动物的基类Animal,其中包含动物共有的行为,如“吃”和“睡”。随后再定义一个Dog类继承Animal,这样Dog就天然拥有这些行为,并可以额外添加“吠叫”等特有行为。

1.2.继承表达的关系

继承最重要的语义是表达一种“is-a” 关系,也就是“某个对象属于某一类对象”。这种层次关系可以通过继承结构自然地表达出来。通过建立合理的继承体系,程序中的类之间就可以形成类似树状的结构,使系统设计更加直观。例如:

  • 哺乳动物是动物
  • 狗是哺乳动物
  • 因此狗也是动物

在实际的开发过程中,如果两个类之间存在明确的is-a 关系,通常就可以考虑使用继承来实现。如果只是“拥有关系”(has-a),则更适合使用组合(Composition)而不是继承。

1.3.继承的基本语法

在 C++ 中,继承通过在类定义后使用冒号:来声明继承关系,其基本语法如下:

class 派生类 : 继承方式 基类 { // 派生类新增成员 };

我们来举例说明一下,还是按照上文所述的哺乳动物和狗的关系,我们可以编写父类是哺乳动物,基类是狗,那么我们需要从父类传递吃和睡两种关系到子类,并且子类还需要加入叫的功能。

// 基类 class Animal { public: void eat() {} void sleep() {} }; // 派生类 class Dog : public Animal { public: void bark() {} };

在这个示例中:

  • Animal是基类
  • Dog是派生类
  • Dog继承了Animal的成员函数eat()和sleep()
  • Dog同时新增了自己的成员函数bark()

除了单继承之外,C++ 还支持多继承,即一个类可以同时继承多个基类。例如:

class Derived : public Base1, public Base2 { };

这意味着派生类可以同时获得多个基类的成员变量和成员函数。不过在实际开发中,多继承会增加系统复杂度,因此通常需要谨慎使用。

2.继承类型与访问权限

接下来就需要进一步弄清楚一个非常关键的问题:派生类究竟能继承到什么,以及继承之后这些成员的访问权限会发生怎样的变化。在 C++ 中,继承并不只是简单地“把基类成员拿过来”,它还受到继承方式和成员访问权限的共同影响。

2.1.三种继承类型

在 C++ 中,一个类在继承基类时,可以使用三种不同的继承方式:public、protected和private。它们写在派生类定义时基类名前面,用来决定基类成员进入派生类后具有什么访问级别。如果没有显式写出继承方式,那么对于class来说默认是private继承。

但从实际开发来看,最常用的是public继承,因为它最符合面向对象中的 “is-a” 关系,也就是“派生类本质上就是一种特殊的基类”。而protected和private继承在工程中相对少见,通常只在一些需要限制接口暴露范围的特殊设计中使用。

这三种继承方式的核心可以概括为:

  • 公有继承(public):基类的公有成员在派生类中仍然是公有成员,基类的保护成员在派生类中仍然是保护成员。
  • 保护继承(protected):基类的公有成员和保护成员在派生类中都变成保护成员。
  • 私有继承(private):基类的公有成员和保护成员在派生类中都变成私有成员。

一句话总结就是:继承方式的本质,就是控制“基类中原本可以访问的成员,在派生类里会降到什么权限”。

2.2. 继承中的访问权限规则

除了继承方式本身,我们还必须先明确 C++ 类成员原有的三种访问权限:public、protected和private。

  • public成员:类内可以访问,类外也可以通过对象访问。
  • protected成员:类内可以访问,派生类也可以访问,但类外不能直接访问。
  • private成员:只有当前类内部可以访问,派生类和类外都不能直接访问。

在继承关系中,派生类只能直接访问基类中的非私有成员,也就是public和protected成员,而private成员不能被派生类直接访问。总结出继承后的变化规律:

总结:继承方式只会影响基类中的public和protected成员在派生类中的可见级别,而private成员无论如何都不能被派生类直接访问。本节的重点主要在以下两个结论:

  • 继承方式决定的是基类非私有成员进入派生类后的权限变化,而不是能不能继承到成员本身。
  • 基类中的private成员始终不能被派生类直接访问,哪怕它们客观上仍然存在于派生类对象中。

3. 继承中的内存

继承不仅是一种语法机制,更是一种明确的内存组织方式。理解这一点,对于掌握类型转换、对象大小、构造析构顺序以及后续的多态机制都至关重要。因为派生类对象在内存中包含一份完整的基类子对象,并在其后追加自己的成员。这意味着继承并不是共享同一块内存,而是对原有结构的扩展。

3. 1. 内存关系

(1)内存结构

当一个类继承另一个类时,派生类对象在内存中的布局可以理解为:

[ 基类部分 ][ 派生类新增部分 ]

也就是说:

  • 基类成员位于对象的起始地址
  • 派生类成员紧随其后
  • 整体构成一个连续的内存块

例如对下面这个实例代码分析他的内存结构,对象A的内存布局为:

class Base { public: int index; int x; int y; }; class A : public Base { public: int a_index; };

对象A的内存布局应该为:[index][x][y][a_index]

(2)基类子对象位于起始地址

由于基类部分位于对象最前面,因此:派生类对象的起始地址 == 基类子对象地址。如读者有兴趣可以通过打印地址验证了这一点:

#include <iostream> using namespace std; class Base { public: int index{0}; }; class A : public Base { public: int a_index{0}; }; int main() { A a1; cout << "&a1 : " << (long long)&a1 << endl; cout << "&a1.index : " << (long long)&a1.index << endl; cout << "&a1.a_index : " << (long long)&a1.a_index << endl; return 0; }

这说明了:

  • a1的地址就是基类部分的地址
  • 基类永远在最前面
  • 派生类成员排在后面

结论:基类子对象始终位于派生类对象的起始地址,派生类对象的地址等价于其基类部分的地址。

这一特性并非实现细节,而是 C++ 语言规范所保证的,它直接支撑了向上转型(Derived → Base)能够零成本完成。

(3)向上转型

因为基类部分就在对象起始地址,所以派生类对象可以无额外开销地当作基类对象使用,这称为向上转型(Upcasting)。例如

Base* p = new A;

这里无需额外转换步骤,因为:p 指向的地址本来就是 Base 子对象地址。

(4)决定了构造与析构的顺序

内存布局还直接决定了对象构造和销毁的顺序:销毁顺序必须与构造顺序相反,确保资源释放安全。

  • 构造顺序:先构造基类 → 再构造派生类
  • 析构顺序:先析构派生类 → 再析构基类

3.2. 继承中的内存机制特征

继承在内存层面并不是“共享”或“引用”,而是一种严格定义的结构扩展机制,可以概括为三个关键特征:

  • 基类子对象位于派生类对象的起始地址
  • 派生类在基类之后追加新的成员
  • 对象大小等于基类大小加新增成员大小(考虑对齐)

正因为采用这种“前缀扩展结构”,C++ 才能够实现高效的类型转换、稳定的构造析构顺序,以及零开销的向上转型。

4.继承中同名成员变量与函数

在继承体系中,除了访问权限和内存布局之外,还有一个非常容易被忽视但又十分重要的问题:派生类与基类出现同名成员时会发生什么?这很容易误以为派生类“覆盖”或“替换”了基类成员,但实际上,在 C++ 中默认发生的是名称隐藏(name hiding),而不是多态意义上的覆盖。也就是说,基类成员并不会消失,它仍然存在于对象中,只是默认访问被派生类成员遮住了。

4.1. 同名成员变量

当派生类定义了与基类同名的成员变量时:

  • 派生类成员会隐藏基类成员
  • 两个成员同时存在于对象中
  • 默认访问派生类版本

可以通过一个简单示例理解:

#include <iostream> using namespace std; class Base { public: int value{100}; }; class A : public Base { public: int value{200}; // 与基类同名 }; int main() { A a; cout << a.value << endl; // 访问派生类成员 cout << a.Base::value << endl; // 显式访问基类成员 return 0; }

输出结果:

200
100

这说明:

  • 基类成员并没有被删除
  • 只是默认访问的是派生类版本
  • 可以通过作用域限定符Base::访问基类成员

4. 2. 同名成员函数

对于成员函数,同样遵循名称隐藏规则。如果派生类定义了与基类同名的函数:

  • 默认调用派生类版本
  • 基类函数仍然存在
  • 可通过Base::函数名调用

示例代码:

#include <iostream> using namespace std; class Base { public: void Test() { cout << "Base Test" << endl; } }; class A : public Base { public: void Test() { cout << "A Test" << endl; } }; int main() { A a; a.Test(); // 调用派生类函数 a.Base::Test(); // 调用基类函数 return 0; }

输出:

A Test
Base Test

需要特别注意: 这不是虚函数机制。与运行时多态无关,属于编译期名称解析规则。

名称隐藏还有一个容易踩坑的地方:一旦派生类定义了同名函数,基类中所有同名重载版本都会被隐藏。例如:

class Base { public: void func(int) {} void func(double) {} }; class A : public Base { public: void func(int) {} // 隐藏 Base 中所有 func };

此时若调用

A a; a.func(3.14); // 编译错误

虽然基类中存在func(double),但它已被隐藏。如果希望保留基类重载,可以使用:

using Base::func;

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询