继承
- [基类和派⽣类间的转换(重点)# 作用](# 作用)
- 场景
- 继承方式与访问权限
- 继承类模板
- 继承中的作⽤域(重点)
- 派⽣类的默认成员函数
- 继承和友元
- 多继承及其菱形继承问题
- [4. 终极解决方案:虚继承 (Virtual Inheritance)](#4. 终极解决方案:虚继承 (Virtual Inheritance))
-
-
- [💡 重点代码细节解析](#💡 重点代码细节解析)
-
基类和派⽣类间的转换(重点)# 作用
继承主要是为了实现代码复用,以及抽象出共同的基类,子类继承后可扩展,并可以支持多态。
其中,代码复用就是子类可以调用基类的成员函数。
抽象出共同的基类就是子类可以用基类的成员变量(受限定符的影响)
场景
cpp
class Student
{
public:
// 进入校园/图书馆/实验室刷二维码等身份认证
void identity()
{
// ...
}
// 学习
void study()
{
// ...
}
protected:
string _name = "peter"; // 姓名
string _address; // 地址
string _tel; // 电话
int _age = 18; // 年龄
int _stuid; // 学号
};
class Teacher
{
public:
// 进入校园/图书馆/实验室刷二维码等身份认证
void identity()
{
// ...
}
// 授课
void teaching()
{
//...
}
protected:
string _name = "张三"; // 姓名
int _age = 18; // 年龄
string _address; // 地址
string _tel; // 电话
string _title; // 职称
};
int main()
{
return 0;
}
其中,我们可以看到,重复的成员变量有:姓名,地址,年龄,电话,能否让一个类,包含这4个,这样看起来就没那么重复了。下面的代码,实现的就是将这4个重复的成员变量放到Person类里面,
cpp
class Person
{
public:
// 进入校园/图书馆/实验室刷二维码等身份认证
void identity()
{
cout << "void identity()" << _name << endl;
}
protected:
string _name = "张三"; // 姓名
string _address; // 地址
string _tel; // 电话
int _age = 18; // 年龄
};
class Student : public Person
{
public:
// 学习
void study()
{
// ...
}
protected:
int _stuid; // 学号
};
class Teacher : public Person
{
public:
// 授课
void teaching()
{
//...
}
protected:
string title; // 职称
};
int main()
{
Student s;
Teacher t;
s.identity();
t.identity();
return 0;
}
其中,Person是基类,也叫做父类,student叫做派生类,也成为子类。

继承方式与访问权限
C++提供了三种继承方式:public、protected和private,它们决定了基类成员在派生类中的访问权限。
继承权限表
| 类成员 / 继承方式 | public 继承 | protected 继承 | private 继承 |
|---|---|---|---|
| 基类的 public 成员 | 派生类的 public 成员 | 派生类的 protected 成员 | 派生类的 private 成员 |
| 基类的 protected 成员 | 派生类的 protected 成员 | 派生类的 protected 成员 | 派生类的 private 成员 |
| 基类的 private 成员 | 在派生类中 不可见 | 在派生类中 不可见 | 在派生类中 不可见 |
权限变化规则
- public 继承:保持原样(是公有的还是公有,是受保护的还是受保护)。
- protected 继承:公有变受保护,受保护还是受保护。
- private 继承:全部变私有(除了本来就看不见的私有成员)。
- 私有成员:无论哪种继承方式,基类的私有成员在派生类内部都永远不可直接访问(不可见)。
示例说明
cpp
#include <iostream>
using namespace std;
// 1. 定义基类
class Base {
public:
int public_val; // 基类的 public 成员
};
// 2. 定义派生类,使用 protected 继承
class Derived : protected Base {
// public_val 在这里变成了 protected
};
int main() {
Base b;
Derived d;
// 场景一:基类对象调用
b.public_val = 10;
// ✅ 成功!Base 类里它是 public 的,外部随便调
// 场景二:派生类对象调用
d.public_val = 20;
// ❌ 编译报错!Error: 'int Base::public_val' is protected within this context
// 因为在 Derived 类里,它已经变成了 protected,外部(main函数)无权访问
return 0;
}
讲解:假设父类是public,而子类继承是protected,你正常调用父类的对象,那它还是public的,还是可以在main函数外面调用。如果是用子类的对象,按道理是继承了这个成员变量的,可以使用,但是因为继承是protected继承,导致在子类中,这个成员变量被定义成了protected,main函数就无法调用了。
继承类模板
cpp
#include <iostream>
#include <vector>
namespace bit
{
// stack和vector的关系,既符合is-a,也符合has-a
template<class T>
class stack : public std::vector<T>
{
public:
void push(const T& x)
{
// 基类是类模板时,需要指定一下类域,
// 否则编译报错:error C3861: "push_back": 找不到标识符
// 因为stack<int>实例化时,也实例化vector<int>了
// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到
std::vector<T>::push_back(x);
}
void pop()
{
std::vector<T>::pop_back();
}
const T& top()
{
return std::vector<T>::back();
}
bool empty()
{
return std::vector<T>::empty();
}
};
}
int main()
{
bit::stack<int> st;
st.push(1);
st.push(2);
st.push(3);
while (!st.empty())
{
std::cout << st.top() << " ";
st.pop();
}
std::cout << std::endl;
return 0;
}
通常情况下我们把一个类型的对象赋值给另一个类型的指针或者引用时,存在类型转换,中间会产生临时对象,所以需要加 const,如:int a = 1; const double& d = a; public 继承中,就是一个特殊处理的例外,派生类对象可以赋值给基类的指针 / 基类的引用,而不需要加 const,这里的指针和引用绑定是派生类对象中的基类部分,如下图所示。也就意味着一个基类的指针或者引用,可能指向基类对象,也可能指向派生类对象。
student sobj;
Person* pp=&sobj;
Person& pp=sobj;

当然,派生类对象可以赋值给基类对象,这种情况叫做切片。
Person pobj=sobj;
为什么叫做切片呢?是因为C++在编译器,就确定内存的大小了。基类对象只有固定的内存大小,放不下派生类多出的内存,所以只能把派生类中属于基类的那部分数据"切"出来拷贝了,固叫做切片。
继承中的作⽤域(重点)
1 隐藏规则:
- 在继承体系中基类和派⽣类都有独⽴的作⽤域。
- 派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。
(在派⽣类成员函数中,可以使⽤ 基类::基类成员 显⽰访问) - 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
- 注意在实际中在继承体系⾥⾯最好不要定义同名的成员。
javascript
// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是⾮常容易混淆
class Person
{
protected :
string _name = "⼩李⼦"; // 姓名
int _num = 111; // ⾝份证号
};
class Student : public Person
{
public:
void Print()
{
cout<<" 姓名:"<<_name<< endl;//先去子类找(找不到)再去父类找,小李子
cout<<" ⾝份证号:"<<Person::_num<< endl;//指定作用域,111
cout<<" 学号:"<<_num<<endl;//先去子类找,找到了隐藏父类,999
}
protected:
int _num = 999; // 学号
};
int main()
{
Student s1;
s1.Print();
return 0;
};
重载和隐藏的区别:重载是"同一作用域"内的函数同名不同参,而隐藏是"父子作用域"内的同名函数屏蔽。
题目:
javascript
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
cout << "func(int i)" <<i<<endl;
}
};
int main()
{
B b;
b.fun(10);
b.fun();
return 0;
};
下⾯程序的编译运⾏结果是什么()
A. 编译报错 B. 运⾏报错 C. 正常运⾏
我们这里,在这里会触发隐藏,所以父类中的func会被隐藏,所以b.fun()会报错,很明显是编译报错,编译报错是语法错误。
改成b.A::func()就可以了
派⽣类的默认成员函数
4个常⻅默认成员函数
- 派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造函数,则必须在派⽣类构造函数的初始化列表阶段显⽰调⽤。
- 派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化。
-
- 派⽣类的operator=必须要调⽤基类的operator=完成基类的复制。需要注意的是派⽣类的
operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域
- 派⽣类的operator=必须要调⽤基类的operator=完成基类的复制。需要注意的是派⽣类的
- 派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。因为这样才能保证派
⽣类对象先清理派⽣类成员再清理基类成员的顺序。 - 派⽣类对象初始化先调⽤基类构造再调派⽣类构造。
- 派⽣类对象析构清理先调⽤派⽣类析构再调基类的析构。
- 因为多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同(这个我们多态章节会讲
解)。那么编译器会对析构函数名进⾏特殊处理,处理成destructor(),所以基类析构函数不加
virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系。
基类Person的定义
javascript
#include <iostream>
#include <string>
using namespace std;
class Person {
public:
// 构造函数,带默认参数
Person(const char* name = "peter") : _name(name) {
cout << "Person()" << endl;
}
// 拷贝构造函数,用到指针,new才需要手写,动态申请空间
Person(const const Person& p) : _name(p._name) { // 备注:存在深拷贝时才显式写
cout << "Person(const Person& p)" << endl;
}
// 赋值运算符重载
Person& operator=(const Person& p) {
cout << "Person operator=(const Person& p)" << endl;
if (this != &p) { // 防止自我赋值
_name = p._name;
}
return *this;
}
// 析构函数
~Person() {
cout << "~Person()" << endl;
}
protected:
string _name; // 姓名
};
派生类student的定义与构造,拷贝构造
javascript
class student : public Person {
public:
// 1. 构造函数
student(const char* name, int num, const char* address)
: Person(name) // 核心点 1:必须先调用基类的构造函数初始化父类部分
, _num(num) //再按照声明顺序定义
, _address(address)
{
// 备注拓展:如果基类成员还有 id,则写成 Person(name, id);
}
// 2. 派生类的拷贝构造函数
student(const student& s)
: Person(s) // 核心点 2:这里利用了"向上转型",直接将派生类引用传给父类拷贝构造
, _num(s._num)
, _address(s._address)
{
// 备注:存在深拷贝时才自己显式写,编译器默认生成的已经够用了
}
protected:
int _num; // 学号(内置类型)
string _address; // 地址(调用自定义类的构造)
};
派生类的赋值重载与析构函数
javascript
// 3. 赋值运算符重载
student& operator=(const student& s) {
// 备注:存在深拷贝时才需要显式写
if (this != &s) {
// 显式调用父类的赋值重载
// 注意:必须加 Person:: 作用域,否则会变成无限递归调用派生类自己的 operator=
Person::operator=(s);
_num = s._num;
_address = s._address;
}
return *this;
}
// 4. 析构函数
~student() {
// 显式写成 // ~Person(); 会报错
// 显式写成 Person::~Person(); 虽然不报错,但会导致重复析构!
// 核心点:子类析构函数在【结束后】会自动调用父类的析构函数
// 所以我们不需要、也不应该在子类析构中显式调用父类析构
//子类完全不用操心父类的事,子类只需要对自己新扩展出来的,且需要手动释放的资源负责
}
实现⼀个不能被继承的类
C++11新增了⼀个final关键字,final修改基类,派⽣类就不能继承了。
javascript
class Bate final
{
}
继承和友元
友元关系不能继承,也就是说基类友元不能访问派⽣类私有和保护成员。
解决方法:让派生类也成为友元关系(friend)
javascript
#include <iostream>
#include <string>
using namespace std;
class Student; // 前置声明,解决 Person 中引用 Student 的问题
class Person
{
public:
// 声明 Display 为 Person 的友元函数
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
//friend void Display(const Person& p, const Student& s);解决方法
protected:
int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
int main()
{
Person p;
Student s;
// 编译报错:error C2248: "Student::_stuNum": 无法访问 protected 成员
// 解决方案:Display也变成Student的友元即可
Display(p, s);
return 0;
}
继承与静态函数
基类定义了static静态成员,则整个继承体系⾥⾯只有⼀个这样的成员。⽆论派⽣出多少个派⽣类,都只有⼀个static成员实例。
javascript
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
string _name;
static int _count;
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _stuNum;
};
int main()
{
Person p;
Student s;
// 这里的运行结果可以看到非静态成员_name的地址是不一样的
// 说明派生类继承下来了,父派生类对象各有一份
cout << &p._name << endl;
cout << &s._name << endl;
// 这里的运行结果可以看到静态成员_count的地址是一样的
// 说明派生类和基类共用同一份静态成员
cout << &p._count << endl;
cout << &s._count << endl;
// 公有的情况下,父派生类指定类域都可以访问静态成员
cout << Person::_count << endl;
cout << Student::_count << endl;
return 0;
}
多继承及其菱形继承问题
继承模型
单继承
⼀个派⽣类只有⼀个直接基类时称这个继承关系为单继承
多继承
⼀个派⽣类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型
是,先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后⾯。
菱形继承
菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下⾯的对象成员模型构造,可以
看出菱形继承有数据冗余和⼆义性的问题,在Assistant的对象中Person成员会有两份。⽀持多继承就⼀定会有菱形继承。

3. 菱形继承问题与二义性 (The Diamond Problem)
什么是菱形继承?
当一个基类(Person)被两个子类(Student 和 Teacher)继承,而这两个子类又共同被另一个派生类(Assistant)继承时,就形成了一个菱形(钻石形)的继承结构。
带来的两大问题:
- 二义性 (Ambiguity):当我们在 Assistant 对象中直接访问 _name 时,编译器不知道你指的是从 Student 那边继承过来的 _name,还是从 Teacher 那边继承过来的 _name。
- 数据冗余 (Data Redundancy):在 Assistant 对象的内存中,存在两份完全一样的 Person 基类数据(即包含两份 _name),浪费内存空间。
传统解决方法(只能解决二义性,无法解决冗余):
显式指定类名作用域来访问特定的成员:
cpp
Assistant a;
a.Student::_name = "xxx"; // 明确指定修改 Student 里的 name
a.Teacher::_name = "yyy"; // 明确指定修改 Teacher 里的 name
4. 终极解决方案:虚继承 (Virtual Inheritance)
为了同时解决数据冗余和二义性 ,C++ 引入了虚继承(使用关键字 virtual)。
- 核心原理 :
- 使用虚继承后,共同的基类(Person)在整个派生类对象内存中只会被保留一份实体。
- 在顶层的 Assistant 对象内部,Person 虚基类的数据通常会被搬移到一个公共区域(例如在 VS2022 编译器中会被放到对象内存的最末尾)。
- 中间的子类通过虚基类表指针 (vbptr) 和虚基类表 (vbtable) 记录的偏移量(Offset)来寻址并访问这个唯一的 Person。
⚠️ 注意(手写笔记提醒) :虽然 C++ 提供了虚继承来应对菱形继承,但在实际软件架构开发中,最好不要设计出菱形继承,因为它的底层内存实现较为复杂,容易增加维护成本。
cpp
#include <iostream>
#include <string>
using namespace std;
// 1. 顶层虚基类
class Person {
public:
// 构造函数
Person(const char* name) : _name(name) {}
string _name; // 姓名
};
// 2. 中间子类 Student:虚继承 Person
class Student : virtual public Person {
public:
// 构造函数:需要调用 Person 的构造函数
Student(const char* name, int num)
: Person(name), _num(num) {}
protected:
int _num; // 学号
};
// 3. 中间子类 Teacher:虚继承 Person
class Teacher : virtual public Person {
public:
// 构造函数:需要调用 Person 的构造函数
Teacher(const char* name, int id)
: Person(name), _id(id) {}
protected:
int _id; // 职工编号
};
// 4. 底层派生类 Assistant:同时多继承 Student 和 Teacher
class Assistant : public Student, public Teacher {
public:
/* 【关键点】:在虚继承体系下,最终派生类(Assistant)的构造函数
必须显式地负责调用最顶层虚基类(Person)的构造函数!
此时,Student 和 Teacher 对 Person 的构造调用会被"屏蔽"(忽略)。
*/
Assistant(const char* name1, const char* name2, const char* name3)
: Person(name3), // 👈 真正起作用的初始化:将顶层唯一的 _name 设为 "王五"
Student(name1, 1), // 这里的 Person 构造被屏蔽,只负责初始化 Student 自身的成员
Teacher(name2, 2) // 这里的 Person 构造被屏蔽,只负责初始化 Teacher 自身的成员
{}
protected:
string _majorCourse; // 主修课程
};
int main() {
// 创建 Assistant 对象
// 传入三个参数:"张三" (传给Student), "李四" (传给Teacher), "王五" (真正传给Person)
Assistant a("张三", "李四", "王五");
// 问:a对象中的 name 是谁?
// 答:由于是虚继承,内存中只有一份来自 Person 的 _name,且由最高优先级的 Assistant 直接初始化。
cout << "a对象中的name是: " << a._name << endl; // 输出:王五
return 0;
}
💡 重点代码细节解析
- 构造函数的调用屏蔽 :
在 Assistant 的初始化列表中,虽然同时写了 : Person(name3), Student(name1, 1), Teacher(name2, 2),但因为 Student 和 Teacher 是虚继承自 Person,所以编译器在实例化 Assistant 时,规定只能由最底层的派生类 Assistant 亲自去调用 Person 的构造函数。 - 测试结果 :
中间 Student("张三") 和 Teacher("李四") 内部试图去初始化 Person 的动作直接被系统忽略。因此,最终 a._name 的值就是 Person(name3) 所赋予的 "王五"。