【C++】继承
前言
C++有三大特性——封装、继承、多态,是面向对象的基石。此前模拟实现string、vector、list等容器时,我们也就体会到封装的价值,迭代器本身属于三大特性中的封装,所有会感到string、vector、list的结构很相似,但底层天差地别,这就在于把底层复杂的细节全部屏蔽掉,然后用相似的迭代器来访问,这就是封装带来的便利之处。
前面我们模拟实现过string、vector、list、stack、queue的底层结构,那这篇博客就来细讲C++三大特性之一的继承。
继承
- 一、继承的概念及定义
- 二、继承的基础语法
- 三、基类与派生类的类型转换
- 四、继承的核心坑点:作用域与隐藏
- 4.1 类域的隐藏规则
- 五、派生类的默认成员函数
- 六、继承的特殊场景
- 七、多继承与菱形继承(C++ 的坑)
- 八、继承 vs 组合(设计原则)
一、继承的概念及定义
C 语言的复用停留在函数层级,而 C++ 的继承实现了类层级的复用 —— 在保留原有类(基类)成员的基础上,扩展新成员生成派生类,贴合 “从简单到复杂” 的认知逻辑。
1.1 无继承的痛点:代码冗余
以Student和Teacher类为例,二者包含大量重复的成员(姓名、地址、身份验证等),仅少数成员不同:
#define_CRT_SECURE_NO_WARNINGS#include<iostream>usingnamespace std;// 学生类classStudent{public:voididentity(){/* 身份验证逻辑 */}// 重复voidstudy(){/* 学习逻辑 */}// 独有protected: string _name; string _address; string _tel;int _age;// 重复int _stuid;// 独有};// 教师类classTeacher{public:voididentity(){/* 身份验证逻辑 */}// 重复voidteaching(){/* 授课逻辑 */}// 独有protected: string _name; string _address; string _tel;int _age;// 重复 string _title;// 独有};重复代码不仅增加开发量,还会导致后续维护成本翻倍(比如修改身份验证逻辑需改两处)。
1.2 继承的解决方案:抽离公共部分
将重复成员抽离为Person基类,Student和Teacher通过继承复用这些成员,仅需定义独有部分:
#define_CRT_SECURE_NO_WARNINGS#include<iostream>usingnamespace std;// 基类:封装学生/教师的公共成员classPerson{public:voididentity(){ cout <<"身份验证:"<< _name << endl;}protected: string _name ="yuuki"; string _address; string _tel;int _age =18;};// 派生类:学生(public继承Person)classStudent:publicPerson{public:voidstudy(){/* 学习逻辑 */}protected:int _stuid;// 独有成员};// 派生类:教师(public继承Person)classTeacher:publicPerson{public:voidteaching(){/* 授课逻辑 */}protected: string _title;// 独有成员};intmain(){ Student s; Teacher t; s.identity();// 复用基类的identity方法 t.identity();// 复用基类的identity方法return0;}输出结果:
身份验证:yuuki 身份验证:yuuki 二、继承的基础语法
2.1 继承的定义格式
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继承扩展性差)。 |
#define_CRT_SECURE_NO_WARNINGS#include<iostream>usingnamespace std;classPerson{public:voidPrint(){ cout << _name << endl;}// public成员protected: string _name;// protected成员private:int _age;// private成员};// public继承(推荐)classStudent:publicPerson{public:voidTest(){Print();// 可访问(基类public→派生类public) _name ="Tom";// 可访问(基类protected→派生类protected)// _age = 20; // 不可访问(基类private)}protected:int _stuid;};2.3 类模板的继承
继承模板类时,需通过类模板名<类型>::指定基类域(编译器无法自动推导):
#include<iostream>#include<vector>usingnamespace std;namespace yuuki {// 继承std::vector模板类实现栈template<classT>classstack:public std::vector<T>{public:voidpush(const T& x){vector<T>::push_back(x);// 必须指定vector<T>域}voidpop(){vector<T>::pop_back();}const T&top(){returnvector<T>::back();}boolempty(){returnvector<T>::empty();}};}intmain(){ yuuki::stack<int> st; st.push(1); st.push(2); st.push(3);while(!st.empty()){ cout << st.top()<<" ";// 输出:3 2 1 st.pop();}return0;}三、基类与派生类的类型转换
public继承下的类型转换是面试高频考点,核心规则:
- 派生类对象 → 基类指针 / 引用:直接支持(称为 “切片”—— 切出派生类中的基类部分);
- 基类对象 → 派生类对象:不支持(基类不含派生类的独有成员);
- 基类指针 → 派生类指针:需强制类型转换(仅当基类指针指向派生类对象时安全)。
#include<iostream>usingnamespace std;classPerson// 基类{virtualvoidfunc(){}// 虚函数(为dynamic_cast做准备)protected: string _name;int _age;};classStudent:publicPerson// 派生类{public:int _stuid;};intmain(){ 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;// 空地址return0;}四、继承的核心坑点:作用域与隐藏
4.1 类域的隐藏规则
继承体系中,基类和派生类有独立作用域,若出现同名成员,派生类成员会 “隐藏” 基类成员:
- 同名成员变量:优先访问派生类的;
- 同名成员函数:仅函数名相同就隐藏(无需参数 / 返回值一致);
- 访问被隐藏的基类成员:需加
基类名::。

#include<iostream>usingnamespace std;classPerson// 基类{protected: string _name ="yuuki";int _num =18;// 身份证号};classStudent:publicPerson// 派生类{public:voidPrint(){ cout <<"姓名:"<< _name << endl;// 复用基类_name cout <<"学生编号:"<< _num << endl;// 访问派生类_num(隐藏基类) cout <<"身份证号:"<< Person::_num << endl;// 访问被隐藏的基类_num}protected:int _num =999;// 学生编号(与基类_num同名)};intmain(){ Student s; s.Print();return0;}输出结果:
姓名:yuuki 学生编号:999 身份证号:18 4.2 经典面试题:函数隐藏 vs 重载
classA{public:voidfunc(){ cout <<"func()"<< endl;}};classB:publicA{public:voidfunc(int i){ cout <<"func(int i): "<< i << endl;}};intmain(){ B b; b.func(10);// 正常调用B::func(int)// b.func(); // 报错!A::func()被B::func(int)隐藏,无法直接访问 b.A::func();// 正确:显式访问基类被隐藏的函数return0;}结论:A 和 B 的func是隐藏关系(而非重载)—— 重载要求函数在同一作用域,而隐藏是不同作用域的同名函数。
五、派生类的默认成员函数
派生类的 6 个默认成员函数(构造、拷贝构造、赋值重载、析构等),需遵循 “先基类、后派生类” 的规则:
5.1 构造函数
- 派生类构造必须先调用基类构造,初始化基类成员;
- 若基类无默认构造(无参 / 全缺省),派生类需在初始化列表显式调用基类构造。
classPerson{public:// 基类无默认构造(必须传参)Person(constchar* name):_name(name){ cout <<"Person构造"<< endl;}protected: string _name;};classStudent:publicPerson{public:// 派生类构造:先调用Person(name),再初始化_stuidStudent(constchar* name,int stuid):Person(name)// 显式调用基类构造(必须),_stuid(stuid){ cout <<"Student构造"<< endl;}protected:int _stuid;};intmain(){ Student s("Tom",1001);// 输出:Person构造 → Student构造return0;}5.2 析构函数
- 派生类析构执行完毕后,编译器自动调用基类析构(保证 “先析构派生、后析构基类”);
- 析构函数名会被编译器统一处理为
destructor(),因此基类析构不加virtual时,派生类析构会隐藏基类析构。
classPerson{public:~Person(){ cout <<"Person析构"<< endl;}};classStudent:publicPerson{public:~Student(){ cout <<"Student析构"<< endl;}};intmain(){ Student s;// 析构顺序:Student析构 → Person析构return0;}5.3 拷贝构造 / 赋值重载
- 拷贝构造:派生类需先拷贝基类部分,再拷贝自身成员;
- 赋值重载:派生类需先调用基类的
operator=,再赋值自身成员。
classPerson{public:Person(constchar* 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;};classStudent:publicPerson{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;};intmain(){ Student s1; Student s2 = s1;// 拷贝构造:Person拷贝构造 → Student拷贝构造 Student s3; s3 = s1;// 赋值重载:Person赋值重载 → Student赋值重载return0;}5.4 总代码
#define_CRT_SECURE_NO_WARNINGS#include<iostream>usingnamespace std;classPerson{public:/*默认构造第一种情况*///// 默认构造(有初始化)//Person(const char* name = "YUUKI")// :_name(name)//{// cout << "Person()" << endl;//}/*默认构造第二种情况,需要子类帮助*/// 默认构造(无初始化)Person(constchar* 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;};classStudent:publicPerson{public:// 默认生成的构造函数的行为// 1、内置类型->不确定// 2、自定义类型->调用默认构造// 3、继承父类成员看做一个整体对象,调用父类的默认构造// 子类默认构造函数,Student(constchar* name,int num,constchar* 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 ="广东佛山市";// 自定义类型会调用自动生成的构造};intmain(){ Student s1("yuuki",18,"广东佛山市"); Student s2(s1); Student s3("YUUKI",28,"广东深圳市"); s1 = s3;// 不需要在子类写赋值运算符,只需要父类里写即可return0;}方法:将父类看成一个类型,与其他类型一起编写,可更好理解
六、继承的特殊场景
6.1 不能被继承的类
- 方法 1(C++98):将基类构造函数设为私有(派生类无法调用构造,无法实例化);
- 方法 2(C++11):用
final关键字修饰基类(直接禁止继承)。
// 方法2:final修饰(推荐)classBasefinal{public:voidfunc(){ cout <<"Base::func()"<< endl;}};// class Derive : public Base {}; // 报错!Base被final修饰,不能继承6.2 继承与友元
友元关系不能继承—— 基类的友元无法直接访问派生类的私有 / 保护成员,需在派生类中重新声明友元:
classStudent;// 前向声明classPerson{public:friendvoidDisplay(const Person& p,const Student& s);protected: string _name ="yuuki";};classStudent:publicPerson{public:friendvoidDisplay(const Person& p,const Student& s);// 重新声明友元protected:int _stuid =1001;};// 友元函数:可访问Person和Student的保护成员voidDisplay(const Person& p,const Student& s){ cout << p._name << endl; cout << s._stuid << endl;}intmain(){ Person p; Student s;Display(p, s);// 输出:yuuki → 1001return0;}6.3 继承与静态成员
基类的静态成员在整个继承体系中只有一份(所有派生类共享):
classPerson{public:staticint _count;// 静态成员:统计对象数量};int Person::_count =0;// 静态成员类外初始化classStudent:publicPerson{};classTeacher:publicPerson{};intmain(){ Person::_count++; Student::_count++; Teacher::_count++; cout << Person::_count << endl;// 输出:3(三者共享_count)return0;}七、多继承与菱形继承(C++ 的坑)
7.1 多继承的基本概念
- 单继承:一个派生类只有一个基类(推荐使用);
- 多继承:一个派生类有多个基类(易出问题,尽量避免);
- 菱形继承:多继承的特殊情况(A→B、A→C、B+C→D),会导致数据冗余和二义性(D 对象中有两份 A 的成员)。

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

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