c++:面向对象三大特性--继承

c++:面向对象三大特性--继承
在这里插入图片描述

面向对象三大特性--继承

一、继承的概念及定义

(一)概念

继承是⾯向对象程序设计使代码可以复用的最重要的⼿段,它允许我们在保持原有类特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称派⽣类。

(二)继承格式

1、继承方式

我们前面对类的成员有三种限制方式,这里也就对应了三种继承方式

在这里插入图片描述

2、格式写法

在这里插入图片描述

3、派生类继承后访问方式的变化

1、通过表格可以发现,如果是private成员,那么无论哪种继承方式都不可以访问到这个权限。
2、此外,structclass这两个关键字在继承时也有差距,struct默认继承方式为公有,而class默认继承方式为私有。
我们如果将权限的大小定义为 public > protected > private, 那么其余访问方式变化就是将大于该继承方式的权限降到继承方式的权限即可。
在这里插入图片描述

(三)普通类继承

这里用到的是继承最基本的语法,采用public继承,那么除了父类的private变量不可访问以外,成员的权限保持不变。

classPerson{public:voidPrint(){ cout << _name << endl; cout << _age << endl;}protected: string _name ="张三";// 姓名private:int _age =18;// 年龄};classStudent:publicPerson{public:voidfunc(){Print();}protected:int _stunum;// 学号};

(四)类模板继承

在之前我们实现stack时,采用的是新建了一个容器类型,在这里我们亦可以采用继承的方式来实现。
需要注意的是,派生类在继承时,如果需要访问父类的成员函数,需要指定类域,模板的成员函数采用的是按需实例化。

namespace wgm {template<classT>classstack:public std::vector<T>{public:voidpush(const T& x){// 基类是类模板时,需要指定⼀下类域,// 否则编译报错:error C3861: “push_back”: 找不到标识符// 因为stack<int>实例化时,也实例化vector<int>了// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到vector<T>::push_back(x);}voidpop(){vector<T>::pop_back();}const T&top(){returnvector<T>::back();}boolempty(){returnvector<T>::empty();}};}

二、基类和派生类的转换

(一)基类转换派生类

1、基类对象不能赋值给派⽣类对象。
2、基类的指针或者引⽤可以通过强制类型转换赋值给派⽣类的指针或者引⽤。但是必须是基类的指针
是指向派⽣类对象时才是安全的。这⾥基类如果是多态类型,可以使⽤RTTI(Run-Time Type Information)dynamic_cast 来进⾏识别后进⾏安全转换。

(二)派生类转换基类

1、public继承的派⽣类对象 可以赋值给基类的指针/基类的引⽤。这⾥有个形象的说法叫切⽚或者切
割。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分。

值得注意的是,之前在隐式类型转换时会生成临时变量,因此在应用时需要加上const,而在切片时不会生成中间的临时变量。
classPerson{protected: string _name;// 姓名 string _sex;// 性别public:int _age =18;// 年龄};classStudent:publicPerson{public:int _No;// 学号};intmain(){ string s1 ="11111";const string& s2 ="11111"; Student sobj;// 赋值兼容转换,特殊处理// 1.派生类对象可以赋值给基类的指针/引用 Person* pp =&sobj; Person& rp = sobj; rp._age++;return0;}
在这里插入图片描述
接下来通过下面的例子发现,继承后的基类私有变量虽然访问不到,但是我们可以发现它在派生类的对象中依旧占据相应的空间,而经过赋值兼容转换变量的大小为基类的大小。
在这里插入图片描述
在这里插入图片描述
接下来更加深层的来了解赋值兼容,发现基类的指针或引用在调用重名函数的时候,调用的是父类的函数,而派生类调用时因为隐藏的特点,派生类对象调用的是派生类的函数。
classA{public:voidfunc(){ cout <<"A::func()"<< endl;}protected:int _a;int _b;private:int _c;};classB:publicA{public:voidfunc(){ cout <<"B::func()"<< endl;}public:int _d;};intmain(){ B obj_b; A* ptr_a =&obj_b; A& ref_a = obj_b; obj_b.func(); ptr_a->func(); ref_a.func();return0;}

2、子类的变量可以复制给父类。

 Person pobj = sobj;
在这里插入图片描述

三、几个重要细节

(一)继承与作用域

1、作用域

在继承体系中基类和派⽣类都有独⽴的作⽤域。

2、隐藏

派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏(在派⽣类成员函数中,可以使⽤基类::基类成员 显⽰访问)

需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。

(二)继承与友元

在继承时,友元关系是不接受继承的。所以如果友元函数需要访问派生类的成员,需要重新声明友元。

(三)继承与静态成员

在继承后,静态成员变量始终只有基类在定义的这一份。通过下面的代码可以发现,我们可以用类域加静态变量的方式来访问静态变量,但是打印的地址是同一份。

classPerson{public: string _name;staticint _count;};int Person::_count =0;classStudent:publicPerson{protected:int _stuNum;};intmain(){ Person p; Student s; cout <<&p._count << endl; cout <<&s._count << endl; cout << Person::_count << endl; cout << Student::_count << endl;return0;}
在这里插入图片描述

四、继承中派生类的构造函数

派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。基类没有默认的构造函数必须在派⽣类构造函数的初始化列表阶段显⽰调⽤。派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化。派⽣类的operator=必须要调⽤基类的operator=。需要注意的是派⽣类的operator=隐藏了基类的operator=,所以指定基类作⽤域显⽰调⽤基类的operator=派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。才能保证先清理派⽣类成员再清理基类成员。因为多态中⼀些场景析构函数需要构成重写。,那么编译器会对析构函数名进⾏特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系。
classPerson{public: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;}// destructor()~Person(){ cout <<"~Person()"<< endl;}protected: string _name;// 姓名};classStudent:publicPerson{public:Student(int num,constchar* address,constchar* name):_num(num),_address(address),Person(name){ cout <<"Student()"<< endl;}Student(const Student& s):Person(s),_num(s._num),_address(s._address){ cout <<"Student(const Student& s)"<< endl;} Student&operator=(const Student& s){ cout <<"Student& operator= (const Student& s)"<< endl;if(this!=&s){ _num = s._num; _address = s._address; Person::operator=(s);}return*this;}// destructor()~Student(){// 不需要写,子类析构函数结束后,会自动调用父类析构//Person::~Person(); cout <<"~Student()"<< endl;}protected:int _num;//学号 string _address;};

五、多继承与菱形继承

(一)多继承

单继承:⼀个派⽣类只有⼀个直接基类时称为单继承
多继承:⼀个派⽣类有两个或以上直接基类时称为多继承

多继承的指针偏移问题

多继承对象在内存中的模型是,先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后⾯

在这里插入图片描述


通过上面的例子,我们可以清晰的认识到基类在派生类的储存情况。

(二)菱形继承

菱形继承:菱形继承是多继承的⼀种特殊情况,有数据冗余和⼆义性的问题

在这里插入图片描述
classPerson{public: string _name;// 姓名};classStudent:publicPerson{protected:int _num;//学号};classTeacher:publicPerson{protected:int _id;// 职工编号};//给类加上 virtual 关键字,解决菱形继承造成的二义性和数据冗余。//class Student : virtual public Person//{//protected:// int _num; //学号//};////class Teacher : virtual public Person//{//protected:// int _id; // 职工编号//};classAssistant:publicStudent,publicTeacher{protected: string _majorCourse;// 主修课程};intmain(){ Assistant obj; obj.Student::_name ="张三"; obj.Teacher::_name ="李四";return0;}
在这里插入图片描述


通过调试窗口,可以发现我们在继承时同时继承了来自Person和来自Teacher_name我们在写代码时无法处理这个二义性,同时也形成了数据冗余。

(三)虚继承

为了解决这个现象,我们只需要在继承同一个基类成员的派生类加上一个virtual关键字,底层会自行加工,使得我们后面访问的_name只是一份数据。
classPerson{public: string _name;// 姓名};//给类加上 virtual 关键字,解决菱形继承造成的二义性和数据冗余。classStudent:virtualpublicPerson{protected:int _num;//学号};classTeacher:virtualpublicPerson{protected:int _id;// 职工编号};classAssistant:publicStudent,publicTeacher{protected: string _majorCourse;// 主修课程};intmain(){ Assistant obj; obj.Student::_name ="张三"; obj.Teacher::_name ="李四";return0;}

下列窗口显示出来的_name实则是同一份数据,最开始指定类域Student::初始化_name为张三

在这里插入图片描述


我们通过Teacher::修改数据为李四,那么数据被修改为李四。

在这里插入图片描述
切记,尽量不用使用菱形继承,因为virtual关键字在解决问题的同时造成了效率的降低,代价有点大。

六、继承和组合

继承组合
定义public继承是⼀种is-a的关系。也就是说每个派⽣类对象都是⼀个基类对象。组合是⼀种has-a的关系。假设B组合了A,每个B对象中都有⼀个A对象。
复用方式白箱复用:在继承⽅式中,基类的内部细节对派⽣类可⻅⿊箱复⽤:通过调用对象的接口实现,对象的内部细节是不可⻅的
耦合度继承⼀定程度破坏了基类的封装,基类的改变,对派⽣类有很⼤的影响。派⽣类和基类间的依赖关系很强,耦合度高组合类之间没有很强的依赖关系,耦合度低

我们可以发现,组合的好处要大于继承,在两种都可以的情况下,优先使⽤组合,⽽不是继承。实际尽量多去⽤组合,组合的耦合度低,代码维护性好。

结束语:

感谢一直以来支持的朋友,支持一路走来披荆斩棘的道友,或许不识,一路同行!

Read more

2026 Python+AI 学习方向拆解:3 个高性价比赛道,新手优先学

2026 Python+AI 学习方向拆解:3 个高性价比赛道,新手优先学

欢迎文末添加好友交流,共同进步! “ 俺はモンキー・D・ルフィ。海贼王になる男だ!” * 前言 * 一、AI数据处理与分析赛道 * 1.1 为什么选择这个方向? * 1.2 核心技能树 * 1.3 实战代码示例 * 数据清洗与预处理 * 1.4 学习路线图 * 二、AI应用开发赛道(LLM + RAG) * 2.1 为什么选择这个方向? * 2.2 RAG技术架构流程 * 2.3 实战代码:构建RAG问答系统 * 2.4 学习路线图 * 三、AI自动化办公赛道 * 3.1 为什么选择这个方向? * 3.2 自动化办公应用场景 * 3.3 实战代码示例

By Ne0inhk

Python 虚拟环境管理工具 UV:从安装到高级用法的详细教程

前言 在 Python 开发中,管理不同项目的依赖包和 Python 版本是开发者常常遇到的问题。不同项目可能依赖不同版本的库,甚至同一个库在不同版本下的行为可能不同。为了避免这些问题,使用虚拟环境成为了解决方案。虚拟环境通过隔离每个项目的依赖,避免了版本冲突问题。 在 Python 中,常用的虚拟环境管理工具有 virtualenv、venv 和一些第三方工具,如 UV。本文将详细介绍如何使用 UV 虚拟环境管理工具,从安装、创建虚拟环境、管理 Python 版本和依赖包,到切换和删除虚拟环境。 一、什么是 UV? UV 是一个简洁、轻量级的 Python 虚拟环境管理工具。它与传统的虚拟环境管理工具(如 virtualenv 或 venv)相比,提供了更加简单和清晰的命令行界面,使得开发者可以高效管理 Python 环境。UV

By Ne0inhk
Python操作国产金仓数据库(KingbaseES)全流程:搭建自己的网页数据管理(增删改查)

Python操作国产金仓数据库(KingbaseES)全流程:搭建自己的网页数据管理(增删改查)

Python操作国产金仓数据库(KingbaseES)全流程:搭建自己的网页数据管理(增删改查) Python操作国产金仓数据库(KingbaseES)全流程:搭建自己的网页数据管理(增删改查),现在国产化替代是大趋势,国产数据库的应用越来越广,金仓数据库(KingbaseES)作为其中的佼佼者,在政务、金融这些领域用得特别多。今天我就带大家从0到1,一步步实现用Python操作KingbaseES数据库,还会基于Flask框架搭一个可视化的网页管理系统,数据的增删改查全流程都能搞定,不管你是Python开发者还是数据库管理员,跟着学都能用得上。 前言     中电科金仓(北京)科技股份有限公司(以下简称“电科金仓”)成立于1999年,是成立最早的拥有自主知识产权的国产数据库企业,也是中国电子科技集团(CETC)成员企业。电科金仓以“提供卓越的数据库产品助力企业级应用高质量发展”为使命,致力于“成为世界卓越的数据库产品与服务提供商”。     电科金仓自成立起始终坚持自主创新,专注数据库领域二十余载,具备出色的数据库产品研发及服务能力,核心产品金仓数据库管理系统Kingbas

By Ne0inhk
2026年最新版Python安装和PyCharm安装教程(图文详细 附安装包)

2026年最新版Python安装和PyCharm安装教程(图文详细 附安装包)

2026年最新版Python安装和PyCharm安装教程 * 前言:安装前友好提示 * 一、Python安装 * 1、 下载python安装包 * 2、 安装python * 3、验证安装成功 * 二、 安装Pycharm * 1、Pycharm介绍 * 2、Pycharm安装 * 3、Pycharm使用 前言:安装前友好提示 (1) 避免安装路径有中文 / 空格; (2) 如果非指定版本建议安装最新版; (3) 无特殊要求随便选一个python版本(建议3.11.9) (4)pycharm无特殊要求建议选择2025版本 一、Python安装 1、 下载python安装包 Python-Pycharm安装包:https://pan.quark.cn/s/6878d7cc5460 安装包我已经下载好了,点击上面网盘链接直接获取就行 2、 安装python 双击下载好的 .exe

By Ne0inhk