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

cpp中的继承

一、继承概念

在cpp中,封装、继承、多态是面向对象的三大特性。这里的继承就是允许已经存在的类(也就是基类)的基础上创建新类(派生类或者子类),从而实现代码的复用。

如上图所示,Person是基类,Stu与Tea是派生类,Stu与Tea分别继承了基类中的对象,同时也有自己的类对象。


1.1派生类对基类的修改

派生类对象可以赋值给基类对象、基类指针、基类引用,这里的赋值只是把派生类中原本继承于父类的类对象赋值回去,对于派生类对象自己的类对象不会赋值。但是基类对象不能赋值给派生类对象。

如上图,派生类只能将基类中原有的(或者说继承过来的)_name和_gender赋值给父类,其余的无法赋值,如果是引用或指针,也是将派生类中基类对应的对象引用给或地址传给基类,基类修改时,子类也会受影响。

如上图,代码验证。注意,以上代码是在public继承时才会生效,如果换成protected时代码就会报错,protected继承下来的父类对象就是protected而非public,不支持修改的,private继承同理。


1.2父子类类成员变量、函数重名

当父类类成员变量名与子类成员变量名冲突时,默认时优先使用子类的。其实子列中也继承了父类中的重名变量,只不过将其隐藏,可以通过指定类成员名::变量名的方式访问。

再提一点,如果子类中没有实现Print函数而是依靠父类中的Print函数,那么打印结果会是这样的,如下图。

这是因为返回给父类的是一个Person类型的this指针,解引用访问的就是Person类中的_val.


当存在同名的函数名时,子类会调用自身的函数,也可以通过类名指定的方式进行访问。


如上图,这里A::func与B::func关系是隐藏,注意与函数重载区分(函数重载条件是同一作用域内函数名相同,参数列表不同构成重载)。


1.3派生类的默认成员函数

#include <iostream>
using namespace std;
class Person {
public://构造函数Person() :_name("张三") {cout << "Person()" << endl;}//析构函数~Person() {cout << "~Person()" << endl;}//拷贝构造Person(const Person& p1):_name(p1._name) {cout << "Person(copy construct)" << endl;}Person& operator=(const Person& p1) {cout << "operator=" << endl;if (this != &p1) {this->_name = p1._name;}return *this;}public:string _name;
};class Son :public Person {
public:Son(const char* name = "", const string id = "111"):_id(id){}void display() {cout << _name << " " << _id << endl;}
private:string _id;
};int main() {Son s1;s1.display();return 0;
}

子类继承父类时会调用父类的构造函数来初始化继承过来的成员,然后子类在初始化自己的成员,同理对于析构、拷贝构造、赋值重载等都是同理。

如上图,s1会对继承的成员调用其对应的类的构造函数,当然,这也是我没有自定义时会调用父类的构造函数对其进行构造。

那么如何进行自定义构造_name呢?

如上图所示,通过son的构造函数对s1进行实例化构造,但是对于从父类继承下来的_name进行自定义时需要注意的是,在初始化_name时我们不能通过直接初始化的方式进行构造(如38行代码,这是错误的),而是通过父类的构造函数对父类成员进行初始化。在上图中也可以看见代码在初始化列表时(代码36行)就会调用父类的构造函数对_name进行初始化。

当然,也可以不自定义,此时_name就调用父类默认的构造函数对其进行初始化(前提是父类要有全缺省的构造函数,不然代码就会报错)。也可以使用初始化匿名对象的方式完成。

如上图所示,同时也要在父类中定义相对应的构造函数类型。

实现子类对象的拷贝构造函数

如上图,在实现子类的拷贝构造函数时,可以用子类类型的s来实例化Person,(这就是切片:父类可以提取子类中从父类继承来的_name进行初始化通过参数来初始化基类成员)

实现子类对象的赋值重载函数

如上图,实现子类对象的赋值重载函数时需要指明具体是哪一个重载函数,否则就会出现死循环,因为子类和父类出现同名函数时会优先调用子类的函数。代码第61行将Son类对象s进行切片,然后调用父类Person的重载函数将s中父类的部分切给Person完成赋值重载。

1.4继承与友元的关系

如上图所示,父类A的友元函数为display,子类B继承了父类A,此时友元函数只能访问子类的公开成员,对于受保护和私有的则无法访问。

1.5继承与静态成员

如上图,父类A中定义的静态成员变量在整个继承体系中都是存在的。

1.6菱形继承

如下图,A是B和C的父类,D又同时继承了B和C,此时D中含有基类成员_d和父类B(_b)和父类C(_c),同时B和C又同时含有A(_a),因此我们在访问_a时需要指定类域。

在上述图中可见,在开辟空间时,内存中64~68是父类B的空间,其中存放了B::_a和B::_b,对应的值就是1和3;而6C~70是父类C的空间,其中存放的就是C::_a和C::_C,对应的值就是2和4,最后一个位置就是D::_d。

如上图,整个44~54是类对象D的空间。

造成代码冗余与二义性问题

在上述代码中,子类D会同时存储了两份A的继承,分别是继承B和C的,这个就造成了代码冗余与内存消耗;其次当D访问A中成员时必须要指定具体哪个类中的(无法通过d._a方式访问)。解决方法就是虚拟继承。


如上图,通过虚拟继承的方法可以直接访问d._a,其实这里的B和C共享同一分A的继承,也就是说代码第91和92行对_a的修改是对同一个对象的修改(这一点在代码运行过程中可以看出)。

如上图所示,不难发现虽然_a是类中共享的一份区域,但是C和B区域与非虚拟继承相比又多出一块区域(如上图中绿色区域所示)。在分析内存时,0x0078FEAC指向的位置是0x00929bf4,0x0078FEC0指向的位置是0x00929c00,其内存图如下图所示

如上图所示,虽然0x00929bf4与0x00929c00指向的位置内容为空,但是其后一个位置的0000000c从十六进制转换为十进制刚好是12,其实这也就是C到_a的偏移量,这个表叫做虚基表,而只想虚机表的指针叫做虚机表指针。

http://www.lryc.cn/news/542068.html

相关文章:

  • DeepSeek全栈接入指南:从零到生产环境的深度实践
  • CSS 真的会阻塞文档解析吗?
  • 大模型的UI自动化:Cline 使用Playwright MCP Server完成测试
  • 碰撞检测 | 图解凸多边形分离轴定理(附ROS C++可视化)
  • Python 基本数据类型
  • 突破“第一崇拜“:五维心理重构之路
  • KubeKey一键安装部署k8s集群和KubeSphere详细教程
  • UE5网络通信架构解析
  • 实验3 知识表示与推理
  • 基于Springboot银行信用卡额度管理系统【附源码】
  • 达梦数据库学习笔记@1
  • 图像处理篇---图像处理中常见参数
  • AI Agent实战:打造京东广告主的超级助手 | 京东零售技术实践
  • 50周学习go语言:第1周 环境搭建
  • 4. MySQL 逻辑架构说明
  • 《AI与NLP:开启元宇宙社交互动新纪元》
  • 面对STM32的庞大体系,如何避免迷失在细节中?
  • ragflow-RAPTOR到底是什么?请通俗的解释!
  • Linux系统移植之Uboot启动流程
  • 【Open X-Embodiment】简单数据下载与预处理
  • 【第四节】C++设计模式(创建型模式)-Builder(建造者)模式
  • 排查JVM的一些命令
  • uni-app(位置1)
  • 某手sig3-ios算法 Chomper黑盒调用
  • 登录-05.JWT令牌-介绍
  • Mac下Python版本管理,适用于pyenv不起作用的情况
  • Ubuntu 服务器Llama Factory 搭建DeepSeek-R1微调训练环境
  • 【redis】redis内存管理,过期策略与淘汰策略
  • RabbitMQ学习—day6—死信队列与延迟队列
  • seacmsv9联合注入数据以及绕过 ORDERBY