C++: 继承
1. 继承的基本概念及定义
1.1 继承的概念
继承是面向对象编程的核心特性之一,是使代码可以复用的重要手段,它允许我们基于已有的类创建新的类,称派生类。
新类(派生类)可以继承原有类(基类)的属性和方法,同时可以添加新的功能或修改现有功能。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
现实类比:
-
基类:
Person
(人) -
派生类:
Student
(学生)、Teacher
(教师)、Staff
(后勤人员)
继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这些派生类都继承了Person
的基本属性(姓名、年龄等),同时拥有自己特有的属性。
#include <iostream>using namespace std;class Person
{
public:void print(){cout << _name << endl;}protected:string _name = "peter";int _age = 18;
};class Student : public Person
{
protected:int _stuid;
};class Teacher : public Person
{
protected:int _jobid;
};int main()
{Student s;Teacher t;s.print();t.print();return 0;
}
虽然子类没有显示说明有_name这个成员,但是都可以调用父类的函数打印出父类的成员变量_name,说明子类继承了父类的成员变量以及成员函数。
1.2 继承定义
定义格式
下面我们看到Person是父类,也称作基类。Student是子类,也称作派生类。
继承关系和访问限定符
继承基类成员访问方式的变化
我们之前说暂时认为private 和 protacted 功能是一样的,现在开始不一样了哈哈。
- 关键字class默认继承方式是private,使用struct默认的继承方式是public
- 基类的private成员无论不准访问
- 基类的protected成员只有派生类内部可以访问(父类的protected成员只有子类可以访问)
- 在开发中,99%用的都是public
对于基类的public成员,在派生类中,访问权限变成了基类权限和继承权限较小的那一个权限。
- 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片 或者切割。寓意把派生类中父类那部分切来赋值过去。
- 基类对象不能赋值给派生类对象。
- 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。
1.3 继承的作用域
- 在继承体系中基类和派生类都有独立的作用域。
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这个叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员显示访问)
- 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。注意不是重载,重载要求在同一作用域。
- 注意在实际中在继承体系里面最好不要定义同名的成员。
在派生类中访问基类和派生类的同名成员(成员变量,成员函数),会优先访问派生类的成员。
(不建议这么做)
1.4 类生派的默认成员函数
1. 构造函数调用顺序:基类构造 → 派生类构造 ( 建造顺序:先地基,后楼房 )
2. 必须显式调用基类构造的情况
当基类没有默认构造函数时,必须显式调用:
3. 拷贝构造函数
必须调用基类的拷贝构造:
Derived(const Derived& other) : Base(other) { // 调用基类拷贝构造// 复制派生类特有成员
}
4. 赋值运算符
必须调用基类的赋值运算符:
Derived& operator=(const Derived& other) {if (this != &other) {Base::operator=(other); // 调用基类赋值操作// 赋值派生类特有成员}return *this;
}
5. 析构函数调用顺序(拆除顺序:先楼房,后地基)
派生类析构 → 基类析构
6. 重要特性:自动调用基类析构
不需要显式调用基类析构函数,编译器会自动处理:
// 正确:不需要写 Base::~Base()
~Derived() {// 只需清理派生类特有资源
}// 错误:不要这样写!
~Derived() {// 清理派生类资源Base::~Base(); // 绝对不要显式调用!
}
7. 析构函数名称特殊处理
编译器将析构函数统一命名为destructor()
,因此:
-
基类析构函数不加
virtual
时,与派生类析构函数构成隐藏关系 -
这是为了后续多态机制中的函数重写做准备
class Base {
public:~Base() {} // 编译器处理为 destructor()
};class Derived : public Base {
public:~Derived() {} // 编译器也处理为 destructor()// 与基类析构函数构成隐藏关系
};
8. 完整示例
class Base {
public:Base() { cout << "Base构造" << endl; }Base(const Base&) { cout << "Base拷贝构造" << endl; }Base& operator=(const Base&) { cout << "Base赋值" << endl; return *this; }~Base() { cout << "Base析构" << endl; }
};class Derived : public Base {
public:Derived() { cout << "Derived构造" << endl; }Derived(const Derived& other) : Base(other) { cout << "Derived拷贝构造" << endl; }Derived& operator=(const Derived& other) {Base::operator=(other);cout << "Derived赋值" << endl;return *this;}~Derived() { cout << "Derived析构" << endl; }
};
1.5 继承与友元
友元关系不能继承,友元关系是单向的,也就是说基类友元不能访问子类私有和保护成员。
1.6 继承与静态成员
1.7 复杂的菱形继承及菱形虚拟继承
- 单继承:一个派生类只有一个直接基类。
- 多继承:一个派生类有多个直接基类。
- 菱形继承:多继承的特殊问题情况。
菱形继承存在数据冗余和二义性。
Student 和 Teacher 分别继承了 Person,如果 Assistant 同时继承了 Student 和 Teacher,是不是相当于Assistant继承了两遍 Person?
可以使用虚拟继承解决菱形继承问题:
class Person {
public:string name;
};class Student : virtual public Person { // 虚拟继承
public:int studentId;
};class Teacher : virtual public Person { // 虚拟继承
public:int teacherId;
};class Assistant : public Student, public Teacher {
public:// 现在name只有一份
};int main() {Assistant a;a.name = "Tom"; // 正确:无二义性
}
菱形继承在实际中使用非常少,在开发中也尽量避免使用菱形继承,但是在C++的IO流中就是使用的菱形继承。
为了研究虚拟继承原理,这里给出了一个简化的菱形继承体系,再借助内存窗口观察对象或成员的模型。
为了研究虚拟继承原理,这里给出了一个简化的菱形继承体系,再借助内存窗口观察对象或成员的模型。
( 以下原图来源博主Brant_zero2022,增加了解释:)
不使用虚拟继承的情况如下:
每个类的成员都是独立存在的。存在二义性,需要指定类域去访问。
如果使用虚拟继承:
B类和C类就共用一个 _a ,地址相同。
我们发现,在_b、_c 其中存储了一个指针,这个指针指向的值是当前指针相对 _a 的偏移量。
通过了B和C的两个指针,指向的一张表。这两个指针叫虚基指针,这两个表叫虚基表。虚基表中存的是偏移量,通过偏移量就可以找到下面的A。
- 这里的 20 和 12 是一个距离(偏移量),距离共有的 _a 的距离。
- 即从0x00 53 FE 90 偏移 20 个单位 (5个字节) 正好的 _a 的地址。
- 从0x00 53 FE 98 偏移 12 个单位 (3个字节) 到达 _a 的地址 。
下面是关于 Person 关系菱形虚拟继承的原理解释:
这样设计的目的是在子类给父类赋值时,就要通过该指针指向的偏移量计算到公有的成员变量的地址,然后进行切分赋值。
关于以上的内容,我们可以使用 VS 的开发人员命令提示工具来底层是如何存放的。
(为了显示,将Class D 改为 Class Dmp,仅仅是为了方便观察)
- public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
- 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。