当前位置: 首页 > 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…...

【Linux】shell脚本忽略错误继续执行

在 shell 脚本中&#xff0c;可以使用 set -e 命令来设置脚本在遇到错误时退出执行。如果你希望脚本忽略错误并继续执行&#xff0c;可以在脚本开头添加 set e 命令来取消该设置。 举例1 #!/bin/bash# 取消 set -e 的设置 set e# 执行命令&#xff0c;并忽略错误 rm somefile…...

椭圆曲线密码学(ECC)

一、ECC算法概述 椭圆曲线密码学&#xff08;Elliptic Curve Cryptography&#xff09;是基于椭圆曲线数学理论的公钥密码系统&#xff0c;由Neal Koblitz和Victor Miller在1985年独立提出。相比RSA&#xff0c;ECC在相同安全强度下密钥更短&#xff08;256位ECC ≈ 3072位RSA…...

R语言AI模型部署方案:精准离线运行详解

R语言AI模型部署方案:精准离线运行详解 一、项目概述 本文将构建一个完整的R语言AI部署解决方案,实现鸢尾花分类模型的训练、保存、离线部署和预测功能。核心特点: 100%离线运行能力自包含环境依赖生产级错误处理跨平台兼容性模型版本管理# 文件结构说明 Iris_AI_Deployme…...

【位运算】消失的两个数字(hard)

消失的两个数字&#xff08;hard&#xff09; 题⽬描述&#xff1a;解法&#xff08;位运算&#xff09;&#xff1a;Java 算法代码&#xff1a;更简便代码 题⽬链接&#xff1a;⾯试题 17.19. 消失的两个数字 题⽬描述&#xff1a; 给定⼀个数组&#xff0c;包含从 1 到 N 所有…...

最新SpringBoot+SpringCloud+Nacos微服务框架分享

文章目录 前言一、服务规划二、架构核心1.cloud的pom2.gateway的异常handler3.gateway的filter4、admin的pom5、admin的登录核心 三、code-helper分享总结 前言 最近有个活蛮赶的&#xff0c;根据Excel列的需求预估的工时直接打骨折&#xff0c;不要问我为什么&#xff0c;主要…...

Python实现prophet 理论及参数优化

文章目录 Prophet理论及模型参数介绍Python代码完整实现prophet 添加外部数据进行模型优化 之前初步学习prophet的时候&#xff0c;写过一篇简单实现&#xff0c;后期随着对该模型的深入研究&#xff0c;本次记录涉及到prophet 的公式以及参数调优&#xff0c;从公式可以更直观…...

【Java_EE】Spring MVC

目录 Spring Web MVC ​编辑注解 RestController RequestMapping RequestParam RequestParam RequestBody PathVariable RequestPart 参数传递 注意事项 ​编辑参数重命名 RequestParam ​编辑​编辑传递集合 RequestParam 传递JSON数据 ​编辑RequestBody ​…...

拉力测试cuda pytorch 把 4070显卡拉满

import torch import timedef stress_test_gpu(matrix_size16384, duration300):"""对GPU进行压力测试&#xff0c;通过持续的矩阵乘法来最大化GPU利用率参数:matrix_size: 矩阵维度大小&#xff0c;增大可提高计算复杂度duration: 测试持续时间&#xff08;秒&…...

HashMap中的put方法执行流程(流程图)

1 put操作整体流程 HashMap 的 put 操作是其最核心的功能之一。在 JDK 1.8 及以后版本中&#xff0c;其主要逻辑封装在 putVal 这个内部方法中。整个过程大致如下&#xff1a; 初始判断与哈希计算&#xff1a; 首先&#xff0c;putVal 方法会检查当前的 table&#xff08;也就…...

OD 算法题 B卷【正整数到Excel编号之间的转换】

文章目录 正整数到Excel编号之间的转换 正整数到Excel编号之间的转换 excel的列编号是这样的&#xff1a;a b c … z aa ab ac… az ba bb bc…yz za zb zc …zz aaa aab aac…; 分别代表以下的编号1 2 3 … 26 27 28 29… 52 53 54 55… 676 677 678 679 … 702 703 704 705;…...