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

C++基类和派生类的内存分配,多态的实现

目录

    • 基类和派生类的内存分配
    • 基类和派生类的成员归属
    • 多态的实现

基类和派生类的内存分配

类包括成员变量(data member)和成员函数(member function)。
成员变量分为静态数据(static data)和非静态数据(non-static data),成员函数分为静态成员函数(static function)、非静态成员函数(non-static function)和虚拟成员函数(vritual function)。

C++编译器将类的静态数据、静态成员函数以及非静态成员函数存储在类对象存储空间之外,并且无论该类声明了多少对象,在内存中只存有一份。
虚拟成员函数也存储在类对象存储空间之外,编译器为每个虚拟成员函数产生一个指针,并将这些指针存储在一个被称为虚基表的表格中。

类对象的存储空间中包括:非静态数据以及指向虚基表的指针。
看个例子,为了方便观察做个字节对齐。

//4字节对齐
#pragma pack(push, 4)class C
{int age;static int year;//静态成员变量,不占用类的空间
public:C(){age = 12;printf("C()\n");}~C() = default;
//    virtual ~C() = default;//定义了虚函数,则是多态类,会生成虚函数地址表void TestFunc(){printf("age=%d\n",age);}
};class D : public C
{int price;
public:D(){price = 2000;printf("D()\n");}void TestFunc(){printf("price=%d\n",price);}
};
#pragma pack(pop)//测试调用D dd;printf("sizeof(C)=%ld\n",sizeof(C));printf("sizeof(D)=%ld\n",sizeof(D));

打印

sizeof(C)=4
sizeof(D)=8

观察一:非多态类
类C的析构函数不是虚函数,此时C和D都不是多态类,也就是普通的类。类的大小就是非静态成员变量的大小之和。
观察D dd的内存分配:

dd	@0x7fffffffe388	D[C]	@0x7fffffffe388	Cage	12	intyear	<optimized out>	price	2000	int

观察dd占用的内存,共8字节,前4字节是0c,转换成十进制是12,也就是基类C中age的大小;后4字节转换成十进制是2000,也就是派生类D中price的大小。

0c 00 00 00 d0 07 00 00

观察二:多态类
把上面例子基类C的虚函数定义为virtual虚函数,则C和D是多态类,打印:

sizeof(C)=12
sizeof(D)=16

C和D的大小分别比非多态类大了8字节,多的8字节其实是指向虚函数表的指针,看下面,比上面多了个vptr。

dd	@0x7fffffffe380	D[C]	@0x7fffffffe380	C[vptr]	_vptr.C	 age	12	intyear	<optimized out>	price	2000	int

观察dd占用的内存,共16字节,前8字节是指向虚函数表的指针;后8字节的前4字节转换10进制是12,也就是基类C中age的大小,后4字节转换成十进制是2000,也就是派生类D中price的大小。

50 d9 55 55 55 55 00 00 0c 00 00 00 d0 07 00 00

一个总结
1、一个类的对象所占用的空间大小:非静态成员变量之和,多态类再加上指向虚基表的指针大小。
2、静态变量year是全局变量被优化,不占用类的大小。
3、类D的对象dd,和基类C的指针地址一样。
4、多态类占用空间比非多态类大8字节,多的8字节其实是指向虚函数表的指针[vptr]。
5、创建一个派生类对象时,先执行基类的构造,再执行派生类的构造,因此内存分配中,前面是基类的非静态成员变量,后面是派生类新增的非静态成员变量。
6、虚函数表指针是在基类构造时创建的,属于基类的一个成员,但派生类也可以访问。

一个多态派生类的对象所占用的内存空间:
在这里插入图片描述

基类和派生类的成员归属

访问范围
1、保护成员的可访问范围比私有成员大,比共有成员小。能访问私有成员的地方都能访问保护成员。
2、基类的私有成员只能在基类访问,派生类不能访问。
3、基类的保护成员可以在派生类的成员函数访问。
4、私有成员只能在类的成员函数访问,这和普通类的定义一致。

覆盖和扩充
1、派生类是对基类进行扩充和修改得到的,基类的所有成员自动成为派生类的成员(私有成员除外)。
2、所谓扩充,指的是派生类中可以添加新的成员变量和成员函数。
3、所谓覆盖,指的是派生类中可以重写从基类继承得到的成员。

一个总结
1、构造与析构顺序:构造时先执行基类的构造函数,再执行派生类的构造函数;析构时先执行派生类的析构函数,再执行基类的构造函数。
2、基类的私有成员,不能在派生类的成员函数访问。
3、基类的保护成员,可以在派生类的成员函数中访问。
4、派生类可以定义和基类中同名的成员变量和非虚成员函数,比如例中的age,基类内存中有个age,派生类新增成员内存中也有一个age,这两个成员变量没有联系。
5、派生类成员函数访问基类非私有成员,可以使用基类::访问。
6、基类的析构函数要定义为虚函数,否则在释放基类指针时不会执行派生类的析构函数,造成隐式的内存泄漏。
7、非多态情况下,派生类和基类是包含和被包含的关系,派生类包含了基类,因此派生类指针可以转换为基类指针,但基类指针不能转换为派生类指针(‘A’ is not polymorphic)。
8、多态情况下,基类和派生类指针可以相互转换,但要关注转换后指针是否有效,可以使用dynamic_cast转换,返回nullptr则转换失败。


//4字节对齐
#pragma pack(push, 4)
class A //基类
{
private:int price;//私有成员,只能在基类的成员函数访问
protected:int age;//保护成员,可以在派生类的成员函数中访问
public:char name[20]= "chw";//公有成员,可以在任何地方访问A(){price = 2000;age = 17;printf("A()\n");}virtual ~A(){printf("~A()\n");}void TestFunc(){printf("price=%d\n",price);printf("age=%d\n",age);printf("name=%s\n",name);}virtual void PrintThis(){printf("A=%p\n",this);}
};class B : public A  //派生类
{
private:int age;//派生类中可以重写从基类继承得到的成员char addr[20];//派生类可以扩充新的成员变量
public:B(){age = 27;printf("B()\n");}~B(){printf("~B()\n");}//覆盖了基类的同名成员函数void TestFunc(){//不能访问基类的私有成员
//        printf("price=%d\n",price);// error: 'price' is a private member of 'A'//可以访问基类的保护成员和公有成员printf("age=%d\n",age);printf("name=%s\n",name);printf("A::age=%d\n",A::age);//基类成员被派生类覆盖,可以使用A::访问基类的成员
//        A::TestFunc();//使用A::也可以访问基类的同名成员函数printf("B=%p\n",this);A::PrintThis();}
};//测试调用B* bb = new B;bb->TestFunc();printf("**********分割线***********\n");A* bb_a = dynamic_cast<A*>(bb);bb_a->TestFunc();printf("sizeof(A)=%ld\n",sizeof(A));printf("sizeof(B)=%ld\n",sizeof(B));delete bb;//基类不能转换为派生类,因为类A没有虚函数,不是多态的//如果类A成员函数TestFunc定义为virtual的,可以转换,但转换完成后aa_b==nullptr,不能使用
//    A* aa = new A;
//    B* aa_b = dynamic_cast<B*>(aa);//error: 'A' is not polymorphic
#pragma pack(pop)

打印

A()
B()
age=27
name=chw
A::age=17
B=0x5555559e15b0
A=0x5555559e15b0
**********分割线***********
price=2000
age=17
name=chw
sizeof(A)=36
sizeof(B)=60
~B()
~A()

内存占用

bb	@0x5555559e15b0	B[A]	@0x5555559e15b0	A[vptr]	_vptr.A	 age	17	intname	"chw\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"	char[20]price	2000	intaddr	"nj\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"	char[20]age	27	int

打印分析:
1、派生类和基类指针地址是一样的(0x5555559e15b0)。
2、派生类重新定义了age,和基类的age是两个没有联系的变量。
3、sizeof(A) = 虚函数表指针(8字节) + price(4字节) + age(4字节) + name(20字节) = 36。
4、sizeof(B) = sizeof(A) + age(4字节) + addr(20字节) = 60。

多态的实现

多态的介绍参考:https://blog.csdn.net/weixin_40355471/article/details/124368317#_844。

通过基类指针或基类引用实现多态
1、对于普通函数,不用管指针是指向基类还是派生类,只和指针变量的数据类型相关,即定义指针变量时的指针数据类型,如果是基类,则始终调用基类的普通函数,如果是派生类,则始终调用派生类的普通函数。
2、对于虚函数,要看基类指针当前指向的是基类还是派生类,如果指向基类则调用基类的虚函数,如果指向派生类则调用派生类的虚函数。
3、派生类指针可以赋值给基类指针,但基类指针赋值给派生类指针时要注意转换的有效性,通常使用dynamic_cast转换,失败时返回nullptr。
4、因此通常使用基类指针或引用,根据基类指针是指向基类还是派生类,实现多态。

class A
{
public:void out1()//普通函数{printf("A(out1)\n");};virtual ~A(){};virtual void out2()//虚函数{printf("A(out2)\n");}
};class B:public A
{
public:virtual ~B(){};void out1(){printf("B(out1)\n");}void out2(){printf("B(out2)\n");}
};//测试调用A *aa = new A;//基类指针,无论aa后面指向基类还是派生类,普通函数都是调用基类的普通函数B *bb = new B;//派生类指针aa->out1();//A(out1)aa->out2();//A(out2)bb->out1();//B(out1)bb->out2();//B(out2)aa = bb;//派生类指针赋值给基类指针bb = dynamic_cast<B*>(aa);//基类指针可以转换成派生类指针,转换失败时返回nullptraa->out1();//A(out1)aa->out2();//B(out2)bb->out1();//B(out1)bb->out2();//B(out2)

打印

A(out1)
A(out2)
B(out1)
B(out2)
A(out1)
B(out2)
B(out1)
B(out2)

相关文章:

C++基类和派生类的内存分配,多态的实现

目录 基类和派生类的内存分配基类和派生类的成员归属多态的实现 基类和派生类的内存分配 类包括成员变量&#xff08;data member&#xff09;和成员函数&#xff08;member function&#xff09;。 成员变量分为静态数据&#xff08;static data&#xff09;和非静态数据&…...

C/C++基础

C 二进制 问题&#xff1a;二进制怎么表示整数、小数、正数、负数&#xff0c;如何存储&#xff1f;加减乘除怎么运算&#xff08;见文章《计算机加减乘除本质》&#xff09;&#xff1f; 变量 c定义一个变量的时候&#xff0c;需要事先定义变量大小和变量类型。 //有符号…...

MySQL基础练习题

数据表介绍 --1.学生表 Student(SId,Sname,Sage,Ssex) --SId 学生编号,Sname 学生姓名,Sage 出生年月,Ssex 学生性别 --2.课程表 Course(CId,Cname,TId) --CId 课程编号,Cname 课程名称,TId 教师编号 --3.教师表 Teacher(TId,Tname) --TId 教师编号,Tname 教师姓名 --4.成绩…...

【C语言学习笔记 --- 动态内存管理】

C语言程序设计笔记---029 C语言之动态内存管理1、介绍动态内存管理2、动态内存函数的介绍2.1、malloc和free函数2.2、calloc函数2.3、realloc函数 3、动态内存管理过程中&#xff0c;一些常见的错误3.1、对NULL指针的解引用操作3.2、对动态内存开辟的空间的越界访问3.3、对非动…...

Nougat来了,能否成为pdf格式转换的新神器?

Nougat来了&#xff0c;能否成为pdf格式转换的新神器&#xff1f; 论文链接&#xff1a;https://arxiv.org/pdf/2308.13418v1.pdf 项目地址&#xff1a;https://github.com/facebookresearch/nougat What happened&#xff1f;&#x1f928; 科学知识主要存储在书籍和科学期…...

C++文件和流

到目前为止&#xff0c;我们已经使用了 iostream 标准库&#xff0c;它提供了 cin 和 cout 方法分别用于从标准输入读取流和向标准输出写入流。 本教程介绍如何从文件读取流和向文件写入流。这就需要用到 C 中另一个标准库 fstream&#xff0c;它定义了三个新的数据类型&#x…...

代码随想录算法训练营第23期day31|贪心算法理论基础、455.分发饼干、376. 摆动序列、53. 最大子序和

目录 一、贪心算法理论基础 二、&#xff08;leetcode 455&#xff09;分发饼干 三、&#xff08;leetcode 376&#xff09;摆动序列 四、&#xff08;leetcode 53&#xff09;最大子序和 一、贪心算法理论基础 1.什么是贪心 贪心的本质是选择每一阶段的局部最优&#xf…...

mdadm命令详解及实验过程

mdadm命令详解及实验过程 ⼀.概念 mdadm是multiple devices admin的简称&#xff0c;它是Linux下的⼀款标准的软件 RAID 管理⼯具&#xff0c;作者是Neil Brown ⼆.特点 mdadm能够诊断、监控和收集详细的阵列信息 mdadm是⼀个单独集成化的程序⽽不是⼀些分散程序的集合&#…...

推荐几个程序员必逛的个人技术博客网站

1、美团技术团队 地 址: 美团技术团队简 介&#xff1a;美团技术团队的博客&#xff0c;干货满满。推荐指数&#xff1a;⭐⭐⭐⭐⭐ ​ 2、阮一峰的网络日志 地 址: 阮一峰的个人网站 - Ruan YiFengs Personal Website简 介&#xff1a;大神阮一峰&#xff0c;博客风格真正…...

Python桌面应用之XX学院水卡报表查询系统(Tkinter+cx_Oracle)

一、功能样式 Python桌面应用之XX学院水卡报表查询系统功能&#xff1a; 连接Oracle数据库&#xff0c;查询XX学院水卡操作总明细报表&#xff0c;汇总数据报表&#xff0c;个人明细报表&#xff0c;进行预览并且支持导出报表 1.总明细报表样式 2.汇总明细样式 3.个人明细…...

ubuntu 中使用Qt连接MMSQl,报错libqsqlodbc.so: undefined symbol: SQLAllocHandle

Qt4.8.7的源码编译出来的libqsqlodbc.so&#xff0c;在使用时报错libqsqlodbc.so: undefined symbol: SQLAllocHandle&#xff0c;需要在编译libqsqlodbc.so 的项目pro文件加上LIBS -L/usr/local/lib -lodbc。 这里的路径根据自己的实际情况填写。 编辑&#xff1a; 使用uni…...

笔试,猴子吃香蕉,多线程写法

package demo;import java.util.concurrent.CountDownLatch;/*** description: 猴子吃香蕉* author: wxm* create: 2023-10-23 14:01**/ public class Main {public static void main(String[] args) throws InterruptedException {Monkey[] m new Monkey[3];Resource r new …...

安装docker ,更换docker版本

docker dockerd & containerd Dockerd&#xff08;Docker 守护进程&#xff09;在其底层使用 Containerd 来管理容器。Containerd 是一个开源的容器运行时管理器&#xff0c;由 Docker 公司于2017年开发并开源&#xff0c;它负责实际的容器生命周期管理。 以下是 Docker 守…...

英语小作文写作模板及步骤(1)

...

编写hello驱动程序

hello的驱动编写 编写驱动程序的步骤 1.确定主设备号&#xff0c;也可以让内核分配 2.定义自己的 file_operations 结构体 3.实现对应的 drv_open/drv_read/drv_write 等函数&#xff0c;填入 file_operations 结构 体 4.把 file_operations 结构体告诉内核&#xff1a;regist…...

ZYNQ中断例程

GPIO 中断系统初始化流程&#xff1a; 第一步:初始化 cpu 的异常处理功能 第二步&#xff1a;初始化中断控制器 第三步&#xff1a;向 CPU 注册异常处理回调函数&#xff1b; 第四步&#xff1a;将中断控制器中的对应中断 ID 的中断与中断控制器相连接 第五步&#xff1a;设置 …...

常用linux命令 linux_cmd_sheet

查看文件大小 ls -al 显示每个文件的kb大小 查看系统日志 dmesg -T | tail 在 top 命令中&#xff0c;RES 和 VIRT&#xff08;或者 total-vm&#xff09;是用来表示进程内存使用的两个不同指标&#xff0c;它们之间有以下区别&#xff1a; RES&#xff08;Resident Set Size…...

【proteus】8086 写一个汇编程序并调试

参考书籍&#xff1a;微机原理与接口技术——基于8086和Proteus仿真&#xff08;第3版&#xff09;p103-105&#xff0c;p119-122. 参考程序是p70&#xff0c;例4-1 在上一篇的基础上&#xff1a; 创建项目和汇编文件 写一个汇编程序并编译 双击8086的元件图&#xff1a; …...

大数据之LibrA数据库常见术语(四)

Failover 指当某个节点出现故障时&#xff0c;自动切换到备节点上的过程。反之&#xff0c;从备节点上切换回来的过程称为Failback。 Freeze 在事务ID耗尽时由AutoVacuum Worker进程自动执行的操作。FusionInsight LibrA会把事务ID记在行头&#xff0c;在一个事务取得一行时&…...

Docker基础知识

文章目录 Docker Docker 一次构建&#xff0c;处处运行&#xff0c;类似于JVM 虚拟机是软件硬件&#xff08;需要Hypervisors实现硬件资源虚拟化&#xff09;&#xff1a; 资源占用大启动慢&#xff08;虚拟机是分钟级&#xff0c;Docker是秒级&#xff09;冗余步骤多 sha2…...

测试微信模版消息推送

进入“开发接口管理”--“公众平台测试账号”&#xff0c;无需申请公众账号、可在测试账号中体验并测试微信公众平台所有高级接口。 获取access_token: 自定义模版消息&#xff1a; 关注测试号&#xff1a;扫二维码关注测试号。 发送模版消息&#xff1a; import requests da…...

反向工程与模型迁移:打造未来商品详情API的可持续创新体系

在电商行业蓬勃发展的当下&#xff0c;商品详情API作为连接电商平台与开发者、商家及用户的关键纽带&#xff0c;其重要性日益凸显。传统商品详情API主要聚焦于商品基本信息&#xff08;如名称、价格、库存等&#xff09;的获取与展示&#xff0c;已难以满足市场对个性化、智能…...

.Net框架,除了EF还有很多很多......

文章目录 1. 引言2. Dapper2.1 概述与设计原理2.2 核心功能与代码示例基本查询多映射查询存储过程调用 2.3 性能优化原理2.4 适用场景 3. NHibernate3.1 概述与架构设计3.2 映射配置示例Fluent映射XML映射 3.3 查询示例HQL查询Criteria APILINQ提供程序 3.4 高级特性3.5 适用场…...

Springboot社区养老保险系统小程序

一、前言 随着我国经济迅速发展&#xff0c;人们对手机的需求越来越大&#xff0c;各种手机软件也都在被广泛应用&#xff0c;但是对于手机进行数据信息管理&#xff0c;对于手机的各种软件也是备受用户的喜爱&#xff0c;社区养老保险系统小程序被用户普遍使用&#xff0c;为方…...

Unsafe Fileupload篇补充-木马的详细教程与木马分享(中国蚁剑方式)

在之前的皮卡丘靶场第九期Unsafe Fileupload篇中我们学习了木马的原理并且学了一个简单的木马文件 本期内容是为了更好的为大家解释木马&#xff08;服务器方面的&#xff09;的原理&#xff0c;连接&#xff0c;以及各种木马及连接工具的分享 文件木马&#xff1a;https://w…...

Netty从入门到进阶(二)

二、Netty入门 1. 概述 1.1 Netty是什么 Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients. Netty是一个异步的、基于事件驱动的网络应用框架&#xff0c;用于…...

三分算法与DeepSeek辅助证明是单峰函数

前置 单峰函数有唯一的最大值&#xff0c;最大值左侧的数值严格单调递增&#xff0c;最大值右侧的数值严格单调递减。 单谷函数有唯一的最小值&#xff0c;最小值左侧的数值严格单调递减&#xff0c;最小值右侧的数值严格单调递增。 三分的本质 三分和二分一样都是通过不断缩…...

宇树科技,改名了!

提到国内具身智能和机器人领域的代表企业&#xff0c;那宇树科技&#xff08;Unitree&#xff09;必须名列其榜。 最近&#xff0c;宇树科技的一项新变动消息在业界引发了不少关注和讨论&#xff0c;即&#xff1a; 宇树向其合作伙伴发布了一封公司名称变更函称&#xff0c;因…...

(一)单例模式

一、前言 单例模式属于六大创建型模式,即在软件设计过程中,主要关注创建对象的结果,并不关心创建对象的过程及细节。创建型设计模式将类对象的实例化过程进行抽象化接口设计,从而隐藏了类对象的实例是如何被创建的,封装了软件系统使用的具体对象类型。 六大创建型模式包括…...

MinIO Docker 部署:仅开放一个端口

MinIO Docker 部署:仅开放一个端口 在实际的服务器部署中,出于安全和管理的考虑,我们可能只能开放一个端口。MinIO 是一个高性能的对象存储服务,支持 Docker 部署,但默认情况下它需要两个端口:一个是 API 端口(用于存储和访问数据),另一个是控制台端口(用于管理界面…...