【C++ 进阶】继承(上):解锁代码复用的核心密码,体会代码复用的魅力!
前言:C++的三大核心特性是封装、继承和多态。在前文中,我们已经通过类和对象讲解了封装特性。接下来,本文将深入探讨C++继承机制的奥秘。
🌟 专注用图文结合拆解难点+代码落地知识,让技术学习从「难懂」变“一看就会”!
🏠 个人主页 :MSTcheng · ZEEKLOG
💻 代码仓库 :MSTcheng · Gitee📚 精选专栏 :📖 :《C语言》🧩 :《数据结构》💡 :《C++由浅入深》💬 座右铭 :“路虽远行则将至,事虽难做则必成!”
文章目录
一、继承的概念及定义
1.1继承的概念
继承是面向对象编程(OOP)中的核心机制之一,允许一个类(子类/派生类)基于另一个类(父类/基类)来构建。子类自动获得父类的属性和方法,并可扩展或修改这些功能。继承呈现了⾯向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们触的函数层次的复⽤,继承是类设计层次的复⽤。
举个例子:
classStudent{public:// 身份验证voididentity(){}// 学习voidstudy(){ cout << _name <<":正在学习"<< endl;}protected: string _name ="张三";// 姓名 string _address;// 地址 string _tel;// 电话int _age =18;// 年龄int _stuid;// 学号};classTeacher{public:// 省份验证voididentity(){}// 授课voidteaching(){ cout << _name <<":正在授课"<< endl;}protected: string _name ="李四";// 姓名int _age =25;// 年龄 string _address;// 地址 string _tel;// 电话 string _title;// 职称};通过比较教师类和学生类可以发现,它们具有多个相同的特性:
共有成员变量: 姓名、年龄、电话、地址
共有成员函数: 身份验证
而它们的独有特性分别是:
教师类: 职称(成员变量)、授课(成员函数)
学生类: 学号(成员变量)、学习(成员函数)
既然存在这些共性,我们可以将共有属性提取到一个基类中,然后让教师类和学生类通过继承来复用这些属性。
1.2继承的定义
下面我们就来定义一个基类,然后让教师类和学生类来复用这些属性:
//定义一个person类定义共同特性classPerson{public://身份验证voididentity(){ cout <<"void identity()"<< _name << endl;}protected: string _name ="张三";// 姓名 string _address;// 地址 string _tel;// 电话int _age;// 年龄};//定义一个student类继承person类classStudent:publicPerson{public:voidprint(){ _name ="李四"; _age =18; cout <<"姓名:"<< _name << endl; cout <<"学号:"<< _stuid << endl;}// 学习voidstudy(){ cout << _name <<":正在学习"<< endl;}protected:int _stuid=241610101;// 学号};//定义一个teacher类继承person类classTeacher:publicPerson{public:voidprint(){ _name ="王五"; _age =25; cout <<"姓名:"<< _name << endl; cout <<"职称:"<< _title << endl;}// 授课voidteaching(){ cout << _name <<":正在授课中"<< endl;}protected: string _title="老师";// 职称};intmain(){ Student s; s.print(); s.study(); Teacher t; t.print(); t.teaching();return0;}
我们看到student类和teacher类的姓名都继承至person类,学号,职称,还有学习和授课(成员函数)是自身特有的,这样就实现了代码的复用。
通过上面的代码我们可以看到继承的定义格式:

Person是基类,也称作⽗类。Student是派⽣类,也称作⼦类。

1.3继承方式与访问方式的组合
以下是C++中基类成员在不同继承方式下于派生类中的访问权限表格:
| 基类成员访问权限 \ 继承方式 | public 继承 | protected 继承 | private 继承 |
|---|---|---|---|
基类public 成员 | 派生类 public 成员 | 派生类protected 成员 | 派生类private 成员 |
基类protected 成员 | 派生类 protected 成员 | 派生类 protected 成员 | 派生类 private 成员 |
基类private 成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
对于上面的这个表格有以下几点注意事项:
1.基类的私有成员无论以何种方式继承,在派生类中均不可访问。这里的不可访问性是指,虽然 基类的私有成员确实存在于派生类对象中,但从语法层面限制了派生类对象(无论在类内部还是外部)都无法直接访问这些成员。
2.基类的私有成员在派生类中不可访问。若希望基类成员在类外不可直接访问,但在派生类中可访问,则应将其定义为protected。由此可见,保护成员访问修饰符是专门为继承机制而设计的。
3.通过上面的表格我们会发现:基类的私有成员在派生类都是不可见。基类的其他成员在派生类的访问⽅式 == Min(成员在基类的访问限定符,继承⽅式),且遵循给public >protected > private。
例如:public继承
- 父类的
public成员,在派生类中任为public成员。 - 父类的
protect成员,在派生类任然为protect成员。 - 父类的
private成员在派生类不可见。
protect继承
- 父类的
public成员,在派生类中任为protect成员。 - 父类的
protect成员,在派生类任然为protect成员。 - 父类的
private成员在派生类不可见。
private继承以此类推
我们会发现,派生类对于基类的访问,要取决于继承方式和基类访问限定最小的那个!
4.在C++中,class默认采用private继承方式,而struct默认采用public继承方式。但为了代码清晰性,建议显式指定继承方式。
1.4继承类模板
#include<iostream>#include<vector>usingnamespace std;//继承/组合namespace my_stack {//=========================//类模板:stack<T>//继承自std::vector<T>的模板//使用vector的特定来模拟栈//=========================template<classT>classstack:public std::vector<T>{//不写public公有 默认就是私有 私有的话类外面访问不到public:voidpush(const T& x){//C++中栈的底层是使用vector来实现的 所以是栈继承了vector//当基类(父类)是模板时,派生类(子类)在使用父类的函数时就要指定类域//否则就会编译报错说push_back找不到标识符// 因为stack<int>实例化时,也实例化vector<int>了// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到//push_back(x);vector<T>::push_back(x);}voidpop(){vector<T>::pop_back();}const T&top(){returnvector<T>::back();}boolempty(){returnvector<T>::empty();}};}intmain(){ bit::stack<int> st; st.push(1); st.push(2); st.push(3);while(!st.empty()){ cout << st.top()<<" "; st.pop();}return0;}二、基类和派生类对象的赋值转换
1、通常情况下我们把⼀个类型的对象赋值给另⼀个类型的指针或者引⽤时,存在类型转换,中间会产⽣临时对象,所以需要加const,如:
int a=1;constdouble& b=a;//不加const编译报错!而在public继承中存在一个特殊处理:派生类对象可以直接赋值给基类指针或引用,且无需添加const限定。 此时指针或引用绑定的是派生类对象中的基类部分(如下图所示)。这意味着基类指针或引用既可以指向基类对象,也可以指向派生类对象。

2、派生类对象赋值给基类对象是通过基类的拷贝构造函数或赋值运算符完成的(这两个函数的细节将在后续小节详细讲解)。这个过程类似于将派生类特有的成员部分"切除",因此也被称为对象切割或切片,如下图所示。

下面来看代码:
#include<iostream>usingnamespace std;// 基类classBase{public:int base_data;};// 派生类classDerived:publicBase{public:int derived_data;};intmain(){ Derived d; d.base_data =10; d.derived_data =20; Base b; b = d;//发生切片 cout << b.base_data << endl;//基类b只能看到base_data这一成员!!return0;}切片的关键点
派生类对象d 赋值给基类对象b 时,编译器仅拷贝 Base 部分的成员(base_data),而 Derived 特有的成员(derived_data)会被丢弃。这就是对象切片的本质。
通过指针或引用可以避免切片:
Base& ref = d;// 通过引用访问,保留派生类完整性 Base* ptr =&d;// 通过指针访问,保留派生类完整性此时的ref和ptr引用/指向的就是整个派生类对象的基类部分,至于派生类特有的部分虽然看不到,但它还是存在派生类对象中,并没有发生切片!。
举个例子:
classPerson{public:voidDisplay(){ cout <<"姓名"<< _name << endl; cout <<"年龄"<< _age << endl;}protected: string _name;// 姓名 string _sex;// 性别int _age;// 年龄};classStudent:publicPerson{public:voidstudy(){ _name ="张三"; cout << _name <<"正在学习"<< endl;}public:int _No;// 学号};intmain(){ Student sobj;//基类指针/引用指向/引用派生类对象 Person* p=&sobj;//基类指针指向 student Person& rp = sobj;//引用绑定到 student//=========基类的指针访问派生类========= p->Display();//✅只能访问派生类中基类部分的成员//p->stduy();//❌语法上看不到派生类特有的部分//=========基类引用访问派生类=========== rp.Display();//✅同上//rp.study();//❌//==============赋值问题==============// 派生类对象可以赋值给基类的对象是通过调用//Person pobj = sobj;//2.基类对象不能赋值给派生类对象,这里会编译报错//sobj = pobj;return0;}注意:子类对象能赋值给父类对象,但父类对象不能赋值给子类对象!
三、继承中的作用域
谈到作用域,作用域机制主要用于解决命名冲突问题,在继承场景中也不例外。当基类和派生类出现同名变量或函数时,究竟会优先调用哪个?这就需要了解隐藏规则了。
3.1隐藏规则
- 在继承体系中基类和派生类都有独立的作用域
- 当派生类与基类存在同名成员时,派生类的成员会优先被访问,从而屏蔽基类中的同名成员,这种现象称为"隐藏"(如需访问基类的同名成员,可通过"基类::成员名"的方式显式调用)。
- 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
- 注意在实际中在继承体系里面最好不要定义同名的成员。
classPerson{protected: string _name ="张三";// 姓名int _age =20;// 年龄};classStudent:publicPerson{public:voidprint(){ cout <<"姓名:"<< _name << endl;//=================隐藏============== cout <<"年龄:"<< _age << endl;//这里的_age与派生类的_age就构成了隐藏关系 默认访问的是派生类的 cout <<"年龄:"<< Person::_age << endl;//想要访问基类的_age 指定类域!!}protected:int _stuid =241610101;// 学号int _age =18;};intmain(){ Student t; t.print();return0;}3.2继承作用域的两道笔试题

第一个问题:显然派生类B与基类A出现了同名函数,那么他们之间就构成隐藏关系!⚠️区别于函数重载:函数重载是在同一作用域下的同名函数(参数不同)才构成重载!而在继承中基类和派生类都有自己独立的作用域所以不可能构成重载!。
第二个问题:在main函数中调用了两个fun函数,区别就是一个有参一个无参,有参的fun函数肯定调用的是派生类的fun函数 (未指定默认调用派生类的)。而无参的fun函数既可以是派生类的fun也可以是基类的fun(函数名相同构成隐藏),编译器不知道是哪个fun函数所以就会报编译错误!
常见的一些报错的原因:
| 阶段 | 核心错误类型 | 典型表现/原因 |
|---|---|---|
| 编译期 | 语法错误 | 缺少分号、括号不匹配 |
| 类型不匹配 | 变量/返回值类型不符 | |
| 符号未定义/冲突 | 变量未声明、宏冲突 | |
| 依赖缺失 | 缺少头文件/库 | |
| 运行时 | 内存访问错误 | 空指针、数组越界 |
| 运算错误 | 除零 | |
| 资源耗尽 | 堆栈溢出、文件句柄耗尽 | |
| 逻辑错误 | 算法实现错误 | |
| 输入/数据问题 | 非法输入未校验、文件操作失败 | |
| 其他 | 内存泄漏 | 未释放动态内存 |
| 精度/编码问题 | 浮点误差、字符编码错误 | |
| 超时/硬件问题 | 操作超时、硬件故障 |
四、总结
本文系统讲解了继承的核心概念,首先明确定义了继承的基本原理,接着详细分析了不同继承方式与访问权限的组合应用。随后深入探讨了基类与派生类对象间的赋值转换问题,最后解析了继承体系中的作用域规则,并通过两道典型笔试题进行实战演练。
由于篇幅问题,后续的继承相关的内容将在下一篇文章介绍!