前言
C++有三大特性------封装、继承、多态,是面向对象的基石。此前模拟实现
string、vector、list等容器时,我们也就体会到封装的价值,迭代器本身属于三大特性中的封装,所有会感到string、vector、list的结构很相似,但底层天差地别,这就在于把底层复杂的细节全部屏蔽掉,然后用相似的迭代器来访问,这就是封装带来的便利之处。前面我们模拟实现过
string、vector、list、stack、queue的底层结构,那这篇博客就来细讲C++三大特性之一的继承。
继承
- 一、继承的概念及定义
-
-
- [1.1 无继承的痛点:代码冗余](#1.1 无继承的痛点:代码冗余)
- [1.2 继承的解决方案:抽离公共部分](#1.2 继承的解决方案:抽离公共部分)
-
- 二、继承的基础语法
-
- [2.1 继承的定义格式](#2.1 继承的定义格式)
- [2.2 继承方式与成员访问权限](#2.2 继承方式与成员访问权限)
- [2.3 类模板的继承](#2.3 类模板的继承)
- 三、基类与派生类的类型转换
- 四、继承的核心坑点:作用域与隐藏
- [4.1 类域的隐藏规则](#4.1 类域的隐藏规则)
-
- [4.2 经典面试题:函数隐藏 vs 重载](#4.2 经典面试题:函数隐藏 vs 重载)
- 五、派生类的默认成员函数
-
- [5.1 构造函数](#5.1 构造函数)
- [5.2 析构函数](#5.2 析构函数)
- [5.3 拷贝构造 / 赋值重载](#5.3 拷贝构造 / 赋值重载)
- [5.4 总代码](#5.4 总代码)
- 六、继承的特殊场景
-
- [6.1 不能被继承的类](#6.1 不能被继承的类)
- [6.2 继承与友元](#6.2 继承与友元)
- [6.3 继承与静态成员](#6.3 继承与静态成员)
- [七、多继承与菱形继承(C++ 的坑)](#七、多继承与菱形继承(C++ 的坑))
-
- [7.1 多继承的基本概念](#7.1 多继承的基本概念)
- [7.2 菱形继承的问题](#7.2 菱形继承的问题)
- [7.3 虚继承解决菱形继承(不推荐)](#7.3 虚继承解决菱形继承(不推荐))
- [7.4 多继承中指针偏移问题](#7.4 多继承中指针偏移问题)
- [7.5 IO库中的菱形虚拟继承](#7.5 IO库中的菱形虚拟继承)
- [八、继承 vs 组合(设计原则)](#八、继承 vs 组合(设计原则))
一、继承的概念及定义
C 语言的复用停留在函数层级 ,而 C++ 的继承实现了类层级的复用 ------ 在保留原有类(基类)成员的基础上,扩展新成员生成派生类,贴合 "从简单到复杂" 的认知逻辑。
1.1 无继承的痛点:代码冗余
以Student和Teacher类为例,二者包含大量重复的成员(姓名、地址、身份验证等),仅少数成员不同:
cpp
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
// 学生类
class Student
{
public:
void identity() { /* 身份验证逻辑 */ } // 重复
void study() { /* 学习逻辑 */ } // 独有
protected:
string _name; string _address; string _tel; int _age; // 重复
int _stuid; // 独有
};
// 教师类
class Teacher
{
public:
void identity() { /* 身份验证逻辑 */ } // 重复
void teaching() { /* 授课逻辑 */ } // 独有
protected:
string _name; string _address; string _tel; int _age; // 重复
string _title; // 独有
};
重复代码不仅增加开发量,还会导致后续维护成本翻倍(比如修改身份验证逻辑需改两处)。
1.2 继承的解决方案:抽离公共部分
将重复成员抽离为Person基类,Student和Teacher通过继承复用这些成员,仅需定义独有部分:
cpp
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
// 基类:封装学生/教师的公共成员
class Person
{
public:
void identity() {
cout << "身份验证:" << _name << endl;
}
protected:
string _name = "yuuki";
string _address;
string _tel;
int _age = 18;
};
// 派生类:学生(public继承Person)
class Student : public Person
{
public:
void study() { /* 学习逻辑 */ }
protected:
int _stuid; // 独有成员
};
// 派生类:教师(public继承Person)
class Teacher : public Person
{
public:
void teaching() { /* 授课逻辑 */ }
protected:
string _title; // 独有成员
};
int main()
{
Student s;
Teacher t;
s.identity(); // 复用基类的identity方法
t.identity(); // 复用基类的identity方法
return 0;
}
输出结果:
身份验证:yuuki
身份验证:yuuki
二、继承的基础语法
2.1 继承的定义格式
plaintext
class 派生类名 : 继承方式 基类名 {
// 派生类独有成员
};
- 基类(父类) :被继承的类(如
Person); - 派生类(子类) :基于基类扩展的类(如
Student/Teacher); - 继承方式 :
public/protected/private(实际开发优先用public)。
2.2 继承方式与成员访问权限
基类成员有public/protected/private三种访问权限,不同继承方式会改变派生类中基类成员的访问权限,核心规则如下(记重点即可):
| 核心规则 | 说明 |
|---|---|
| 1 | 基类private成员:无论哪种继承方式,派生类中不可访问(仅基类自身可访问); |
| 2 | 基类protected成员:派生类可访问,外部不可访问; |
| 3 | class默认继承方式为private,struct默认为public; |
| 4 | 实际开发仅用public继承(protected/private继承扩展性差)。 |
cpp
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class Person
{
public:
void Print() { cout << _name << endl; } // public成员
protected:
string _name; // protected成员
private:
int _age; // private成员
};
// public继承(推荐)
class Student : public Person
{
public:
void Test() {
Print(); // 可访问(基类public→派生类public)
_name = "Tom"; // 可访问(基类protected→派生类protected)
// _age = 20; // 不可访问(基类private)
}
protected:
int _stuid;
};
2.3 类模板的继承
继承模板类时,需通过类模板名<类型>::指定基类域(编译器无法自动推导):
cpp
#include<iostream>
#include<vector>
using namespace std;
namespace yuuki
{
// 继承std::vector模板类实现栈
template<class T>
class stack : public std::vector<T>
{
public:
void push(const T& x) {
vector<T>::push_back(x); // 必须指定vector<T>域
}
void pop() {
vector<T>::pop_back();
}
const T& top() {
return vector<T>::back();
}
bool empty() {
return vector<T>::empty();
}
};
}
int main()
{
yuuki::stack<int> st;
st.push(1); st.push(2); st.push(3);
while (!st.empty()) {
cout << st.top() << " "; // 输出:3 2 1
st.pop();
}
return 0;
}
三、基类与派生类的类型转换
public继承下的类型转换是面试高频考点,核心规则:
- 派生类对象 → 基类指针 / 引用:直接支持(称为 "切片"------ 切出派生类中的基类部分);
- 基类对象 → 派生类对象:不支持(基类不含派生类的独有成员);
- 基类指针 → 派生类指针:需强制类型转换(仅当基类指针指向派生类对象时安全)。
cpp
#include<iostream>
using namespace std;
class Person // 基类
{
virtual void func() {} // 虚函数(为dynamic_cast做准备)
protected:
string _name;
int _age;
};
class Student : public Person // 派生类
{
public:
int _stuid;
};
int main()
{
Student sobj;
// 1. 派生类对象 → 基类指针/引用(切片)
Person* pp = &sobj;
Person& rp = sobj;
Person pobj = sobj; // 切片赋值
// 2. 基类对象 → 派生类对象(报错)
// sobj = pobj;
// 3. 基类指针 → 派生类指针(安全场景)
Student* ps1 = dynamic_cast<Student*>(pp); // pp指向sobj,转换成功
cout << ps1 << endl; // 非空地址
// 3. 基类指针 → 派生类指针(不安全场景)
Person pobj2;
pp = &pobj2;
Student* ps2 = dynamic_cast<Student*>(pp); // pp指向基类对象,转换失败
cout << ps2 << endl; // 空地址
return 0;
}
四、继承的核心坑点:作用域与隐藏
4.1 类域的隐藏规则
继承体系中,基类和派生类有独立作用域,若出现同名成员,派生类成员会 "隐藏" 基类成员:
- 同名成员变量:优先访问派生类的;
- 同名成员函数:仅函数名相同就隐藏(无需参数 / 返回值一致);
- 访问被隐藏的基类成员:需加
基类名::。 
cpp
#include<iostream>
using namespace std;
class Person // 基类
{
protected:
string _name = "yuuki";
int _num = 18; // 身份证号
};
class Student : public Person // 派生类
{
public:
void Print() {
cout << "姓名:" << _name << endl; // 复用基类_name
cout << "学生编号:" << _num << endl; // 访问派生类_num(隐藏基类)
cout << "身份证号:" << Person::_num << endl; // 访问被隐藏的基类_num
}
protected:
int _num = 999; // 学生编号(与基类_num同名)
};
int main()
{
Student s;
s.Print();
return 0;
}
输出结果:
姓名:yuuki
学生编号:999
身份证号:18
4.2 经典面试题:函数隐藏 vs 重载
cpp
class A
{
public:
void func() { cout << "func()" << endl; }
};
class B : public A
{
public:
void func(int i) { cout << "func(int i): " << i << endl; }
};
int main()
{
B b;
b.func(10); // 正常调用B::func(int)
// b.func(); // 报错!A::func()被B::func(int)隐藏,无法直接访问
b.A::func(); // 正确:显式访问基类被隐藏的函数
return 0;
}
结论 :A 和 B 的func是隐藏关系(而非重载)------ 重载要求函数在同一作用域,而隐藏是不同作用域的同名函数。
五、派生类的默认成员函数
派生类的 6 个默认成员函数(构造、拷贝构造、赋值重载、析构等),需遵循 "先基类、后派生类" 的规则:
5.1 构造函数
- 派生类构造必须先调用基类构造,初始化基类成员;
- 若基类无默认构造(无参 / 全缺省),派生类需在初始化列表显式调用基类构造。
cpp
class Person
{
public:
// 基类无默认构造(必须传参)
Person(const char* name) : _name(name) {
cout << "Person构造" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
// 派生类构造:先调用Person(name),再初始化_stuid
Student(const char* name, int stuid)
: Person(name) // 显式调用基类构造(必须)
, _stuid(stuid)
{
cout << "Student构造" << endl;
}
protected:
int _stuid;
};
int main()
{
Student s("Tom", 1001); // 输出:Person构造 → Student构造
return 0;
}
5.2 析构函数
- 派生类析构执行完毕后,编译器自动调用基类析构(保证 "先析构派生、后析构基类");
- 析构函数名会被编译器统一处理为
destructor(),因此基类析构不加virtual时,派生类析构会隐藏基类析构。
cpp
class Person
{
public:
~Person() { cout << "Person析构" << endl; }
};
class Student : public Person
{
public:
~Student() { cout << "Student析构" << endl; }
};
int main()
{
Student s; // 析构顺序:Student析构 → Person析构
return 0;
}
5.3 拷贝构造 / 赋值重载
- 拷贝构造:派生类需先拷贝基类部分,再拷贝自身成员;
- 赋值重载:派生类需先调用基类的
operator=,再赋值自身成员。
cpp
class Person
{
public:
Person(const char* name = "yuuki") : _name(name) {}
// 基类拷贝构造
Person(const Person& p) : _name(p._name) {
cout << "Person拷贝构造" << endl;
}
// 基类赋值重载
Person& operator=(const Person& p) {
if (this != &p) _name = p._name;
cout << "Person赋值重载" << endl;
return *this;
}
protected:
string _name;
};
class Student : public Person
{
public:
// 派生类拷贝构造
Student(const Student& s)
: Person(s) // 拷贝基类部分
, _stuid(s._stuid)
{
cout << "Student拷贝构造" << endl;
}
// 派生类赋值重载
Student& operator=(const Student& s) {
if (this != &s) {
Person::operator=(s); // 调用基类赋值重载
_stuid = s._stuid;
}
cout << "Student赋值重载" << endl;
return *this;
}
protected:
int _stuid = 1001;
};
int main()
{
Student s1;
Student s2 = s1; // 拷贝构造:Person拷贝构造 → Student拷贝构造
Student s3;
s3 = s1; // 赋值重载:Person赋值重载 → Student赋值重载
return 0;
}
5.4 总代码
cpp
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class Person
{
public:
/*默认构造第一种情况*/
//// 默认构造(有初始化)
//Person(const char* name = "YUUKI")
// :_name(name)
//{
// cout << "Person()" << endl;
//}
/*默认构造第二种情况,需要子类帮助*/
// 默认构造(无初始化)
Person(const char* name)
:_name(name)
{
cout << "Person()" << endl;
}
// 拷贝构造
Person(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;
};
class Student : public Person
{
public:
// 默认生成的构造函数的行为
// 1、内置类型->不确定
// 2、自定义类型->调用默认构造
// 3、继承父类成员看做一个整体对象,调用父类的默认构造
// 子类默认构造函数,
Student(const char* name, int num, const char* addrss)
// 将Person看做一个整体
:Person(name)// 错误写法 -> :_name(name)
,_num(num)
,_addrss(addrss)
{}
// 报错:因为父类和子类改成隐藏,因重复调用子类,导致栈溢出
/*Student& operator=(const Student& s)
{
operator=(s);
}*/
~Student()
{
// 错误写法: ~Person();
// 原因:
// 1. 语法定义,先子类后父类。如果写成上面,就成了先父类再子类
// 2. 子类析构完后,编译器会自动掉用父类的析构
}
protected:
// 无缺省值
/*int _num;
string _addrss;*/
// 有缺省值
int _num = 18;
string _addrss = "广东佛山市"; // 自定义类型会调用自动生成的构造
};
int main()
{
Student s1("yuuki", 18, "广东佛山市");
Student s2(s1);
Student s3("YUUKI", 28, "广东深圳市");
s1 = s3; // 不需要在子类写赋值运算符,只需要父类里写即可
return 0;
}
方法:将父类看成一个类型,与其他类型一起编写,可更好理解
六、继承的特殊场景
6.1 不能被继承的类
- 方法 1(C++98):将基类构造函数设为私有(派生类无法调用构造,无法实例化);
- 方法 2(C++11) :用
final关键字修饰基类(直接禁止继承)。
cpp
// 方法2:final修饰(推荐)
class Base final
{
public:
void func() { cout << "Base::func()" << endl; }
};
// class Derive : public Base {}; // 报错!Base被final修饰,不能继承
6.2 继承与友元
友元关系不能继承------ 基类的友元无法直接访问派生类的私有 / 保护成员,需在派生类中重新声明友元:
cpp
class Student; // 前向声明
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name = "yuuki";
};
class Student : public Person
{
public:
friend void Display(const Person& p, const Student& s); // 重新声明友元
protected:
int _stuid = 1001;
};
// 友元函数:可访问Person和Student的保护成员
void Display(const Person& p, const Student& s) {
cout << p._name << endl;
cout << s._stuid << endl;
}
int main()
{
Person p;
Student s;
Display(p, s); // 输出:yuuki → 1001
return 0;
}
6.3 继承与静态成员
基类的静态成员在整个继承体系中只有一份(所有派生类共享):
cpp
class Person
{
public:
static int _count; // 静态成员:统计对象数量
};
int Person::_count = 0; // 静态成员类外初始化
class Student : public Person {};
class Teacher : public Person {};
int main()
{
Person::_count++;
Student::_count++;
Teacher::_count++;
cout << Person::_count << endl; // 输出:3(三者共享_count)
return 0;
}
七、多继承与菱形继承(C++ 的坑)
7.1 多继承的基本概念
- 单继承:一个派生类只有一个基类(推荐使用);
- 多继承:一个派生类有多个基类(易出问题,尽量避免);
- 菱形继承 :多继承的特殊情况(A→B、A→C、B+C→D),会导致数据冗余 和二义性(D 对象中有两份 A 的成员)。

7.2 菱形继承的问题
cpp
class Person { public: string _name; }; // 基类
class Student : public Person { public: int _stuid; }; // 派生类1
class Teacher : public Person { public: string _title; }; // 派生类2
class Assistant : public Student, public Teacher { public: int _id; }; // 菱形顶点
int main()
{
Assistant a;
// a._name = "Tom"; // 报错!二义性:_name来自Student还是Teacher?
a.Student::_name = "Tom"; // 显式指定,解决二义性(但数据冗余仍存在)
a.Teacher::_name = "Jerry";
return 0;
}
7.3 虚继承解决菱形继承(不推荐)
通过virtual关键字实现虚继承,可消除数据冗余和二义性,但底层实现复杂、性能损耗大,实战中建议避免设计菱形继承:
cpp
class Person { public: string _name; };
class Student : virtual public Person { public: int _stuid; }; // 虚继承
class Teacher : virtual public Person { public: string _title; }; // 虚继承
class Assistant : public Student, public Teacher { public: int _id; };
int main()
{
Assistant a;
a._name = "Tom"; // 正常访问(仅一份_name)
return 0;
}
7.4 多继承中指针偏移问题
选择以下选项:()
A: p1 == p2 == p3 B: p1 < p2 < p3 C: p1 == p3 != p2 D: p1 != p2 != p3
cpp
class Base1 { public: int _b1;};
class Base2 { public: int _b2;};
class Derive : public Base1, public Base2 { public: int _d;};
int main()
{
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}
7.5 IO库中的菱形虚拟继承

八、继承 vs 组合(设计原则)
| 特性 | 继承(is-a 关系) | 组合(has-a 关系) |
|---|---|---|
| 关系 | 派生类是一个基类(如 Student 是 Person) | 类包含另一个类(如 Car 包含 Engine) |
| 封装性 | 破坏基类封装(派生类可访问基类保护成员) | 高封装(被组合类的细节不可见) |
| 耦合度 | 高(基类修改会影响派生类) | 低(被组合类修改不影响组合类) |
| 复用方式 | 白箱复用(基类细节可见) | 黑箱复用(仅通过接口访问) |
设计原则:优先使用组合
- 若类之间是 "is-a" 关系(如 Student 是 Person),用继承;
- 若类之间是 "has-a" 关系(如 Car 有 Engine),用组合;
- 若两者皆可,优先选组合(降低耦合,提升代码可维护性)。
总结
- 继承的核心是代码复用 ,实战中优先用
public继承; - 继承的核心坑点是同名成员隐藏 ,需通过
基类::访问被隐藏成员; - 派生类默认成员函数需遵循 "先基类、后派生类" 的规则;
- 多继承(尤其是菱形继承)易出问题,尽量避免;
- 设计类时,优先用组合而非继承(降低耦合)。
继承是 C++ 多态的基础,但滥用会导致代码臃肿、难以维护 ------ 理解继承的规则,更要理解 "何时不用继承",才是面向对象设计的关键。





