当前位置: 首页 > news >正文

【C++游记】子承父业——乃继承也

 

枫の个人主页

你不能改变过去,但你可以改变未来

算法/C++/数据结构/C

Hello,这里是小枫。C语言与数据结构和算法初阶两个板块都更新完毕,我们继续来学习C++的内容呀。C++是接近底层有比较经典的语言,因此学习起来注定枯燥无味,西游记大家都看过吧~,我希望能带着大家一起跨过九九八十一难,降伏各类难题,学会C++,我会尽我所能,以通俗易懂、幽默风趣的方式带给大家形象生动的知识,也希望大家遇到困难不退缩,遇到难题不放弃,学习师徒四人的精神!!!故此得名【C++游记

 话不多说,让我们一起进入今天的学习吧~~~  

一、继承的概念及核心价值

1.1 继承的本质:类层次的代码复用

继承(inheritance)允许在保持原有类(基类/父类)特性的基础上,扩展新的方法(成员函数)和属性(成员变量),生成新类(派生类/子类)。它区别于函数层次的复用,是面向对象程序设计中"由简单到复杂"认知过程的体现,能有效减少代码冗余。

1.2 继承的优化:一个爸爸

#include <iostream>
#include <string>
using namespace std;// 基类:提取Student和Teacher的共性
class Person
{
public:// 身份认证(复用)void identity() {cout << _name << "身份认证通过" << endl;}
protected:string _name = "张三";  // 姓名(复用)string _address;        // 地址(复用)string _tel;            // 电话(复用)int _age = 18;          // 年龄(复用)
};// 学生类:继承Person,仅定义独有成员
class Student : public Person
{
public:void study() {cout << _name << "(学生)学习中" << endl;}
protected:int _stuid; // 学生独有:学号
};// 教师类:继承Person,仅定义独有成员
class Teacher : public Person
{
public:void teaching() {cout << _name << "(教师)授课中" << endl;}
protected:string _title; // 教师独有:职称
};// 测试:复用基类方法,调用派生类独有方法
int main()
{Student s;Teacher t;// 复用基类identity()方法s.identity(); t.identity(); // 调用派生类独有方法s.study();    t.teaching(); return 0;// 输出结果:// 张三身份认证通过// 张三身份认证通过// 张三(学生)学习中// 张三(教师)授课中
}

二、继承的定义格式与访问权限控制

2.1 继承的定义格式

派生类定义需指定"基类"和"继承方式",语法格式如下:

class 派生类名 : 继承方式 基类名
{// 派生类成员(独有属性/方法)
};

其中:

  • 基类/派生类:基类(Base Class)也称父类,派生类(Derived Class)也称子类(因翻译差异两种称呼通用);
  • 继承方式:包括public(公继承)、protected(保护继承)、private(私有继承);
  • 默认继承方式:使用class定义类时默认private继承,使用struct时默认public继承,建议显式指定继承方式。

2.2 继承方式对成员访问权限的影响

类成员/继承方式public继承protected继承private继承
基类的public成员派生类的public成员派生类的protected成员派生类的private成员
基类的protected成员派生类的protected成员派生类的protected成员派生类的private成员
基类的private成员派生类中不可见派生类中不可见派生类中不可见
  1. 基类private成员"不可见":指成员虽被继承到派生类对象中,但语法限制派生类(类内/类外)均无法直接访问,需通过基类的public/protected接口间接访问;
  2. protected限定符的意义:若基类成员需"类外不可访问、派生类内可访问",则定义为protected,这是专为继承设计的访问限定符;
  3. 实际应用建议:几乎只使用public继承,protected/private继承会限制派生类扩展性,维护成本高。

三、基类与派生类的对象转换规则

3.1 允许的转换:向上转换(派生类→基类)

派生类对象可以赋值给基类的指针、引用或对象,此过程称为"切片/切割"——即仅取派生类中基类对应的部分,丢弃派生类独有成员。实例如下:

#include <iostream>
#include <string>
using namespace std;class Person
{
protected:string _name = "张三"; // 姓名int _age = 18;         // 年龄
public:void showInfo() {cout << "姓名:" << _name << ",年龄:" << _age << endl;}
};class Student : public Person
{
public:int _No = 2024001; // 学号(独有)void showStuInfo() {cout << "学号:" << _No << ",姓名:" << _name << endl;}
};int main()
{Student sobj;// 1. 派生类对象 → 基类指针Person* pp = &sobj;pp->showInfo(); // 调用基类方法,仅访问基类成员// 2. 派生类对象 → 基类引用Person& rp = sobj;rp.showInfo(); // 调用基类方法// 3. 派生类对象 → 基类对象(调用基类拷贝构造)Person pobj = sobj;pobj.showInfo(); // 仅包含基类成员return 0;// 输出结果:// 姓名:张三,年龄:18// 姓名:张三,年龄:18// 姓名:张三,年龄:18
}

3.2 禁止的转换:向下转换(基类→派生类)

基类对象不能直接赋值给派生类对象,因为基类不包含派生类的独有成员,无法完成派生类对象的完整初始化,编译会直接报错。实例如下:

int main()
{Person pobj;Student sobj;// sobj = pobj; // 编译报错:error C2679: 二进制"=": 没有找到接受"Person"类型的右操作数的运算符return 0;
}

四、继承中的作用域与隐藏规则

基类和派生类拥有独立的作用域,当两者存在同名成员时,会触发"隐藏"规则(也称"重定义"),这是继承中易混淆的核心知识点之一。

4.1 隐藏规则的核心内容

  • 基类和派生类的作用域相互独立;
  • 派生类与基类有同名成员时,派生类成员会屏蔽基类同名成员的直接访问,需通过基类名::基类成员显式访问;
  • 成员函数的隐藏:仅需"函数名相同"即构成隐藏,无需参数列表或返回值匹配(与"重载"区分:重载要求同一作用域、参数列表不同);
  • 实际建议:继承体系中尽量不定义同名成员,避免混淆。

4.2 隐藏规则实例演示

#include <iostream>
#include <string>
using namespace std;class Person
{
protected:string _name = "小李子"; // 姓名int _num = 111;          // 基类:身份证号
};class Student : public Person
{
public:void Print(){cout << "姓名:" << _name << endl;          // 无隐藏,访问基类_namecout << "身份证号:" << Person::_num << endl;// 显式访问基类_num(隐藏)cout << "学号:" << _num << endl;            // 访问派生类_num(隐藏基类)}
protected:int _num = 999; // 派生类:学号(与基类_num同名,触发隐藏)
};int main()
{Student s1;s1.Print();// 输出结果:// 姓名:小李子// 身份证号:111// 学号:999return 0;
}

4.3 常见误区:函数隐藏 vs 函数重载

函数隐藏与重载的核心区别在于"作用域":隐藏发生在不同作用域(基类vs派生类),重载发生在同一作用域。实例如下:

#include <iostream>
using namespace std;class A
{
public:void fun() { cout << "A::func()" << endl; } // 基类函数
};class B : public A
{
public:// 函数名相同,参数不同,构成隐藏(非重载)void fun(int i) { cout << "B::func(int i): " << i << endl; } 
};int main()
{B b;b.fun(10);  // 正确:调用B::fun(int)// b.fun();  // 编译报错:B::fun(int)隐藏了A::fun()b.A::fun(); // 正确:显式访问基类fun()return 0;
}

五、派生类的默认成员函数实现规则

C++类有6个默认成员函数(构造、析构、拷贝构造、赋值重载、取地址重载、const取地址重载),其中前4个在派生类中需特殊处理,核心原则是"先初始化基类,后清理派生类"。

5.1 派生类默认成员函数的核心规则

  • 构造函数:派生类构造必须调用基类构造初始化基类部分;若基类无默认构造(无参/全缺省),需在派生类构造的初始化列表显式调用基类构造;
  • 拷贝构造函数:派生类拷贝构造必须调用基类拷贝构造完成基类部分的拷贝初始化;
  • 赋值运算符重载:派生类赋值重载必须调用基类赋值重载完成基类部分的赋值;
  • 析构函数:派生类析构函数执行完后,编译器会自动调用基类析构函数(先清理派生类,后清理基类)。

5.2 派生类默认成员函数实现示例

#include <iostream>
#include <string>
using namespace std;class Person
{
public:// 基类构造函数Person(const char* name = "peter") : _name(name) {cout << "Person()" << endl;}// 基类拷贝构造Person(const Person& p) : _name(p._name) {cout << "Person(const Person& p)" << endl;}// 基类赋值重载Person& operator=(const Person& p) {if (this != &p) {_name = p._name;}cout << "Person::operator=" << endl;return *this;}// 基类析构函数~Person() {cout << "~Person()" << endl;}
protected:string _name;
};class Student : public Person
{
public:// 派生类构造:初始化列表显式调用基类构造Student(const char* name, int num) : Person(name), _num(num) {cout << "Student()" << endl;}// 派生类拷贝构造:显式调用基类拷贝构造Student(const Student& s) : Person(s), _num(s._num) {cout << "Student(const Student& s)" << endl;}// 派生类赋值重载:显式调用基类赋值重载Student& operator=(const Student& s) {if (this != &s) {Person::operator=(s); // 调用基类赋值_num = s._num;}cout << "Student::operator=" << endl;return *this;}// 派生类析构:编译器自动调用基类析构~Student() {cout << "~Student()" << endl;}
protected:int _num; // 学号
};// 测试:初始化与清理顺序
int main()
{Student s1("jack", 18); // 构造顺序:Person() → Student()Student s2(s1);         // 拷贝构造顺序:Person拷贝 → Student拷贝Student s3("rose", 17);s1 = s3;                // 赋值顺序:Person赋值 → Student赋值return 0; // 析构顺序:~Student() → ~Person()(与构造顺序相反)
}

六、继承中的特殊场景处理

6.1 继承与友元:友元关系不可继承

基类的友元不能访问派生类的私有/保护成员,友元关系仅存在于声明它的类与友元之间,不传递给派生类。爸爸的朋友不是我的朋友。

6.2 继承与静态成员:静态成员全局唯一

基类定义的static成员,在整个继承体系中仅存在一份实例,派生类与基类共享该静态成员。

七、多继承与菱形继承问题解析

7.1 多继承的概念

  • 单继承:一个派生类仅有一个直接基类(如:A:public B);
  • 多继承:一个派生类有多个直接基类(如:A:public B,public C);

7.2 菱形继承的问题

菱形继承是多继承的特殊情况,表现为"一个派生类的两个直接基类继承自同一个间接基类",会导致数据冗余二义性

7.3 菱形虚拟继承:解决数据冗余和二义性

C++通过虚拟继承(virtual inheritance)解决菱形继承问题,在直接基类继承间接基类时使用virtual关键字,使间接基类在继承体系中只保留一份实例。

#include <iostream>
#include <string>
using namespace std;// 间接基类
class Person {
public:string _name; // 姓名
};// 虚拟继承:Student -> Person
class Student : virtual public Person {
protected:int _num; // 学号
};// 虚拟继承:Teacher -> Person
class Teacher : virtual public Person {
protected:int _id; // 职工号
};// 派生类:Assistant继承Student和Teacher
class Assistant : public Student, public Teacher {
protected:string _major; // 主修课程
};int main() {Assistant a;a._name = "peter"; // 正确:无二义性(仅一份_name)return 0;
}

虚拟继承的实现原理:

虚拟继承通过引入"虚基表"和"虚基指针"实现,派生类对象中不再直接包含间接基类的成员,而是通过指针访问共享的间接基类实例,从而解决数据冗余问题。注意:虚拟继承仅在菱形继承场景使用,普通继承无需使用,否则会增加内存和时间开销!

八、继承与组合的选择原则

继承和组合都是代码复用的重要方式,各有适用场景,核心区别在于:

  • 继承:体现"is-a"关系(如"学生是一个人"),是一种强耦合关系,派生类依赖基类的实现,基类变化会影响派生类;
  • 组合:体现"has-a"关系(如"汽车有一个发动机"),是一种弱耦合关系,类通过包含其他类的对象实现功能,不依赖其内部实现。

8.1 组合的优势与应用场景

实际开发中优先使用组合,因为它具有更低的耦合度和更高的灵活性。示例如下:

// 发动机类
class Engine {
public:void run() { cout << "发动机启动" << endl; }void stop() { cout << "发动机关闭" << endl; }
};// 汽车类(组合发动机,体现"has-a"关系)
class Car {
private:Engine _engine; // 组合发动机对象
public:void drive() {_engine.run(); // 使用发动机功能cout << "汽车行驶中" << endl;}void park() {_engine.stop(); // 使用发动机功能cout << "汽车已停车" << endl;}
};int main() {Car c;c.drive();c.park();return 0;
}

九、总结

  1. 继承的核心价值是实现类层次的代码复用,减少冗余;
  2. 访问权限由基类成员限定符和继承方式共同决定,实际开发中优先使用public继承;
  3. 基类与派生类的转换遵循"向上兼容"原则,向下转换需谨慎;
  4. 同名成员会触发隐藏规则,继承体系中应避免定义同名成员;
  5. 派生类默认成员函数需正确处理基类部分的初始化与清理;
  6. 菱形继承问题通过虚拟继承解决,但应尽量避免复杂的多继承结构;
  7. 设计时优先考虑组合,仅在必要时使用继承,遵循"高内聚低耦合"原则。

十、结语

今日C++到这里就结束啦,如果觉得文章还不错的话,可以三连支持一下。感兴趣的宝子们欢迎持续订阅小枫,小枫在这里谢谢宝子们啦~小枫の主页还有更多生动有趣的文章,欢迎宝子们去点评鸭~C++的学习很陡,时而巨难时而巨简单,希望宝子们和小枫一起坚持下去~你们的三连就是小枫的动力,感谢支持~

http://www.dtcms.com/a/355536.html

相关文章:

  • 91美剧网官网入口 - 最新美剧资源在线观看网站
  • 保姆级教程 | 在Ubuntu上部署Claude Code Plan Mode全过程
  • 【论文阅读】MotionXpert:基于肌电信号的优化下肢运动检测分类
  • Spring事务管理机制深度解析:从JDBC基础到Spring高级实现
  • [灵动微电子MM32SPIN0280]从灵动微电子看电机专用MCU
  • Deeplizard 深度学习课程(五)—— 模型训练
  • 数据结构01:顺序表
  • react Antd Table 多选大数据量 UI渲染很慢的解决方案
  • 每日五个pyecharts可视化图表日历图和箱线图:从入门到精通
  • ChatGPT登录,拒绝访问,错误1020解决办法
  • THM Whats Your Name WP
  • QT .pro文件的常见用法
  • 与trae携手,构建owtb一体化物流平台之--需求文档V0.3
  • RTL8198E SDK温控机制
  • 家电公司跨界造车,追觅能否造出“电动时代的布加迪”
  • 【架构师干货】软件工程
  • 从卡顿到丝滑:大型前端项目 CSS 优化全攻略
  • Agent实战教程:Langgraph的StateGraph以及State怎么用
  • 如何安装InfluxDB 1.7.0 Windows版(influxdb-1.7.0_windows_amd64.exe使用方法附安装包下载)​
  • 群晖 DS225+ 和绿联 DXP2800:企业文件备份方案对比
  • 仿生纺织飞行模块专利拆解:螺旋旋转结构的空气动力学与升力产生机制
  • curl打印信息实现
  • 如何将yolo训练图像数据库的某个分类的图像取出来
  • Step-by-Step: 接入淘宝商品详情 API 并解析返回数据
  • 无人机+AI光伏热斑检测技术
  • 大模型训练中对SFT和DPO的魔改——PROXIMAL SUPERVISED FINE-TUNING和Semi-online DPO论文阅读笔记
  • 锁的种类都有什么
  • Vue3 + Rsbuild 完全指南:10倍构建速度的现代前端开发方案
  • 解锁AI“黑匣”:监督、无监督与强化学习探秘
  • 某供应链金融公司多场景敏感数据安全保护实践