CPU眼里的 class vs struct

转自微信公众号《阿布编程》
“我们能用C语言实现C++的:继承、成员函数、虚函数吗?不仅可以,而且还一摸一样!”

01

提出问题

说到C和C++的差别,大家很容易联想到面向对象和面向过程的差异。毕竟类,也就是class,几乎是所有面向对象设计的标准配置。

当然,也有同学表示不服,C语言里面也有struct呀,很多class能作的事情,用struct也可以完成。如果真是这样的话,为什么很少看见有人用C语言,作面向对象的程序设计呢?

02

基本分析

这里,就让我们一起用CPU的视角,从它们的实现逻辑上,回答这些问题吧。打开Compiler Explorer写一段代码,需要注意的是为了抵消字节对齐,所产生的冗余空间,针对当前的64位编译环境,我们会统一把各种数据结构,定义成8字节的long类型;为了抵消编译器对访问权限的控制,我们会把class的成员变量和成员函数统一设置成:public。

这里,我们先在C++编译环境中定义一个简单的类A,里面有两个成员变量x和y;然后在C编译环境中定义一个struct B,里面也有两个成员变量x和y;最后分别写一个main函数,分别作一下两个成员变量的写操作:

图片

老规矩,不用理会每条CPU指令的具体涵义,我们只对比二者的差异,如你所见,除了变量a、b的内存地址不同外,二者对应的CPU指令完全一致。

而且对象a和结构体b所占的内存空间都是16个字节,正好是两个long类型的数据长度,这看上去非常合理。所以,我们可以确定对象a和变量b,在内存上的空间分布是完全一致的:

图片

03

继承分析

但这还远远不够,因为继承才是class最显著的特征。让我们定义一个类A的派生类AA;同时,也照猫画虎的定义一个派生的结构体BB;最后,对新的成员变量z,作一下写操作:

图片

不出意外,二者对应的CPU指令,仍然是一致的。对象aa和结构体bb所占据的内存空间都同样为新增的变量z,增加了8个字节的存储空间,达到了24个字节:

图片

因为它们在内存空间上的分布完全一致,所以,此时class专属的继承特性,在struct身上,也能窥探一二,甚至还更加直接、清晰。

04

成员函数分析

但class的高级之处,不仅在于可以继承和封装数据,还可以封装方法,也就是成员函数。而由于C语言的语法规则限制,我们一般不能在struct内部定义成员函数,也正是这个原因,我们也常说struct是“面向数据”的。好了,让我们为类A增加一个成员函数func:

图片

你发现了吗?我们虽然增加了一个成员函数func,但对象a所占据的内存空间并没有变化,还是2个成员变量a、b的容量大小,还是只占据16个字节。其实,无论在class内部定义多少个成员函数,都不会影响对象a的大小和内存分布。如果这样的话,成员函数跟定义在class外部的普通函数,就没有本质区别。

是的,让我们也为struct B也写一个“成员函数”func吧:

图片

如果对比一上面2张图中的CPU指令,你会发现:struct B的“成员函数”func跟class A的成员函数func,对应的CPU指令完全相同!而且其函数的调用部分main函数对应的CPU指令,也是完全相同的。

相信如果看过“CPU眼里的:this指针”的话,对这个结果一定不意外。所以,用struct也可以实现class的成员函数和this指针,且效果相同。

05

虚函数分析

好吧,哪又怎么样呢?难道你还能用struct实现虚函数吗?让我们先在类A的成员函数前加上关键字virtual,让它变成一个虚函数。而struct B的“成员函数”则不需要作任何调整,如“CPU眼里的:虚函数”所说,虚函数体的实现,跟普通成员函数体,没有本质区别。

所以,问题的关键在于,我们如何为struct实现“动态绑定”的调用方式,为了简化代码,我们先typedef一个函数指针类型,一个大写的FUNC,

看过“CPU眼里的:虚函数”的同学,还记得虚函数的实现机制吗?编译器会偷偷的为class添加一个隐形的指针变量v,用来记录虚函数表的首地址。

同样的方法,我们也为struct B定义虚函数表,由于struct B没有可以自动调用的构造函数,所以,我们手动的为隐形变量v,作一下虚函数表的初始化。

随后,我们就可以写一个函数test1,用来作struct B的虚函数调用,为了和class的虚函数作对比,我们也写一个函数test2,用来作class A的虚函数调用:

图片

如你所见,虚函数的调用部分,也就是函数test1和函数test2对应的CPU指令,是完全相同!注意哟,它们可都是货真价实的动态绑定。

最后,由于数组和指针常常可以混用的原因,我们还可以把调用struct B的“虚函数”代码,改写成数组的形式,这样,当有多个“虚函数”的时候,我们仅仅调整一下数组的索引,就可以实现不同“虚函数”的调用了:

typedef long (FUNC)(struct B b);
struct B{
FUNC* v;
long x;
long y;
};

long func1(struct B* b){ return b->x; }
long func2(struct B* b){ return b->y; }
long func3(struct B* b){ return b->x + b->y; }

FUNC vfuncTable[] = {
func1,
func2,
func3
};

struct B b = { .v = vfuncTable };
void test(struct B* p)
{
p->v0;//call func1
p->v1;//call func2
p->v2;//call func3
}
如你所见,仅仅通过简单的数组偏移,就可以迅速切换到不同的“虚函数”,并没有出现所谓的搜索、查询虚函数表的行为。这个时候,你还会担心调用虚函数,会大大增加系统开销吗?

至此,面向对象的几大特性:封装、继承、虚函数,我们都能用C语言中的struct,重新实现一遍。如果配合类型转换,我们甚至还可以实现多态,或许有效的重复利用所学的C语言知识,也能帮助我们快速理解C++的运作原理,您觉得呢?

06

总结

排除编译器对访问权限的限制,struct和class在数据结构上,特别是在内存布局上面,有很多相似之处。也正是这个原因,我们用C语言,依然可以实现诸如:继承、虚函数等面向对象的效果。

所以,我们也经常可以在Linux内核的文件系统、驱动框架中,看到很多类似的代码。当然,虽然面向对象的效果是达到了,但显然没有用C++的class那么简洁、优雅,再加上满天飞的函数指针,可读性也会差很多。

当然,这里我们只讨论C语言中的struct跟class之间的差异,因为C++中的struct,已经被强化的很厉害了,甚至可以定义构造函数和析构函数,已经非常接近class了。

07

热点问题

Q1:凭什么说不能在struct的内部定义成员函数?用函数指针就可以实现这样的效果呀,例如下面的代码:

typedef void (*PFUNC)(int a);

struct B{
PFUNC* memFunc;
long x;
long y;
};
A1:不错,虽然我们可以在struct内部定义一个函数指针,间接的实现内部成员函数,这从形式上看,确实跟class的成员函数有些相似之处。但要知道这里的函数指针变量memFunc是需要占用内存的:图片现在的结构体b不仅有两个8字节的long类型变量x和y,还有一个函数指针memFunc,所以它所占据的内存空间,从以前的16字节,扩大到了24字节。而且随着成员函数的增加,也将占用更多的存储空间。同时,初始化这些函数指针也是一个繁琐、必须的工作,这跟class实现成员函数的策略,是完全不同的。

Q2:如何用C语言的struct实现class中的private和protected访问属性呢?

A2:一般来说,C语言编译器是不支持这种private和protected访问属性的。对于如private和protected的访问属性的判定,是C++编译器在语法分析阶段中得出的,对于不合规的访问属性,在这个阶段就被禁止或报错;就像一行代码的末尾没有加分号一样,会被编译器无情的报错,不会继续生成CPU指令。

而一旦通过编译,由private和protected修饰的成员变量或成员函数,跟public修饰的成员变量或成员函数并无本质区别,CPU对它们都一视同仁的。

Q3:所以,面向对象只是一种编程思想,并不是一定要用C++的class来实现,实际上用C语言,也可以实现面向对象的编程思想,对吗?

A3:是的!这是非常经典的阐述。但如果没有本文的铺垫,这可能是一种很难理解的正确结论。但一旦了解底层实现,这种感觉可能就会脱口而出,少很多理解上的烦恼。由于编程语言和编程思想往往是你中由我,我中有你,所以,有时候通过对细节的追踪、挖掘,也能帮助我们准确理解一些抽象、奇怪的知识,更好的区分出语言和思想的边界。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/136850.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java @NotBlank反射校验

在实际项目中,遇到了导入数据校验是否为空的情况,只使用Javax的NotBlank注解并没有什么用,还需要使用工具类校验,具体代码如下: pojo代码如下: import com.alibaba.excel.annotation.ExcelIgnore; import …

多篇论文介绍-摘要

论文地址https://arxiv.org/pdf/2301.10051.pdf 目录 01CIEFRNet:面向高速公路的抛洒物检测算法 02改进 YOLOv5 的 PDC 钻头复合片缺损识别 03 基于SimAM注意力机制的DCN-YOLOv5水下目标检测 04 基于改进YOLOv7-tiny 算法的输电线路螺栓缺销检测 ​编辑05 基于改进Y…

亚马逊鲲鹏系统能做什么

亚马逊鲲鹏系统是一款能绕过亚马逊智能检测,完全模拟人类真实行为,通过模拟真实的人流量来帮助你提升你的产品排名,让你的产品出现在搜索首页,从而快速提高你的销售业绩的营销工具! 主要的功能有批量注册买家号、AI智能…

【原创学位论文】基于python和定向爬虫的商品比价系统.docx

基于python和定向爬虫的商品比价系统 Price Comparison System for Products Based on Python and Targeted Web Crawling 目录 目录 2 摘要 3 关键词 3 第一章 绪论 4 1.1 研究背景 4 1.2 研究意义 5 1.3 国内外研究现状 7 1.4 本文主要工作和章节安排 8 第二章 Python基础…

新生儿疝气:原因、科普和注意事项

引言: 新生儿疝气是一种在婴儿中相对较常见的状况,很多新父母可能对这一现象感到困惑和焦虑。疝气发生时,内腹腔的一部分可能穿过腹壁的弱点,导致腹部出现凸起。本文将科普新生儿疝气的原因,提供相关信息,…

计算机基础知识48

web应用程序 # Django框架是一款专门用来开发web应用的框架 # Web应用程序是一种可以通过浏览器访问的应用程序, B/S架构 案例:淘宝网、京东网... # 应用程序有两种模式: C/S:客户端/服务器端程序,这类程序一般独立运行 B/S&#xff1…

C++跨DLL内存所有权问题探幽(二)CRT中MT和MD混用导致的所有权问题

0xC0000374: 堆已损坏。 (参数: 0x00007FFA1E9787F0)。 _Mem 是 nullptr 我在开发的过程中有遇到上面两个东西的bug,百思不得其解,最后才发现这个和两个DLL中的MT和 MD选项有关系。 具体情境时:我在一个MT编译的DLL A中引用了一个MD编译的D…

数据结构与算法C语言版学习笔记(5)-串,匹配算法、KMP算法

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、串的定义二、串的存储结构1.顺序结构2.链式结构 三、串的朴素的模式匹配算法(暴力匹配算法)1.背景2.假设我们要从下面的主串 S"…

“最强”机器学习辅助!利用自然语言让机器人更好地理解开放性世界

原创 | 文 BFT机器人 想象一下,你正在国外拜访朋友,打开他的冰箱看看有没有能够制作一顿美味早餐的食材。最初,冰箱里的许多物品对你来说都很陌生,每个物品的包装都是你不熟悉的。你开始试图理解每个物品的用途,并根据…

重温设计模式之什么是设计模式?

设计面向对象软件比较困难,而设计可复用的面向对象软件就更加困难。你必须找到相关的对象,以适当的粒度将它们归类,再定义类的接口和继承层次,建立对象之间的基本关系。你的设计应该对手头的问题有针对性,同时对将来的…

万界星空科技MES系统软件体系架构及应用

MES系统是数字化车间的核心。MES通过数字化生产过程控制,借助自动化和智能化技术手段,实现车间制造控制智能化、生产过程透明化、制造装备数控化和生产信息集成化。生产管理MES系统主要包括车间管理系统、质量管理系统、资源管理系统及数据采集和分析系统…

go语言相关bug

第一个bug itcastitcast:/home/jian/share/src/go-test/homeweb-client$ go mod tidy go: finding module for package github.com/micro/go-grpc go: found github.com/micro/go-grpc in github.com/micro/go-grpc v1.0.1 go: homeweb-client/handler importsgithub.com/micr…

Ubuntu开机无法进入系统,文件根系统目录空间不足导致?

前言: 自己电脑上装的是Win11和Ubuntu20双系统,平时就是切换着用。 偶然有次,Ubuntu提示文件根系统目录空间不足,自己没在意。 结果下次开机进入Ubuntu时候,芭比Q了。。进不了系统 这样的事情发生很多次了,…

学者观察 | 数字经济中长期发展中的区块链影响力——清华大学柴跃廷

导语 区块链是一种全新的分布式基础架构与计算范式,既能利用非对称加密和冗余分布存储实现信息不可篡改,又可以利用链式数据结构实现数据信息可溯源。当前,区块链技术已成为全球数据交易、金融结算、国际贸易、政务民生等领域的信息基础设施…

xLua Lua访问C#注意事项(七)

调用成员方法 注意:调用成员方法,第一个参数需要传该对象,建议用冒号语法 loacl camera CS.UnityEngine.GameObject.Find("Main Camera") --冒号语法 camera:GetComponent("Camera") --点语法 camera.GetComponent(camera,"…

事务(本地事务与分布式事务)

事务 1 本地事务1.1 事务的特性1.2 事务的隔离级别1.3 事务的传播属性 2 分布式事务2.1 分布式事务基础2.1.1 CAP定理2.1.2 BASE定理 2.2 分布式事务的解决方案2.2.1 两阶段提交(2PC)2.2.2 TCC补偿式事务2.2.3 消息事务最终一致性 1 本地事务 1.1 事务的…

could not read ok from ADB Server

执行adb devices提示 List of devices attached * daemon not running; starting now at tcp:5037 could not read ok from ADB Server * failed to start daemon 方法1,关闭防火墙, could not read ok from ADB Server_夜星辰2023的博客-CSDN博客 我…

module ‘torch‘ has no attribute ‘_six‘

主要问题是torchvision的问题 在122服务器上的scvi-env2环境中 import torch import torch.nn as nnimport numpy as npfrom tqdm import tqdm from torchvision.utils import save_image, make_grid # Model Hyperparametersdataset_path ./datasetscuda True DEVICE tor…

httpRequest库代码示例

python # 首先导入所需的库 library(httpRequest) # 设置主机名和端口号 proxy_host <- proxy_port <- # 使用httpRequest库的get函数下载图片 response <- httpRequest(", proxyHost proxy_host, proxyPort proxy_port) # 确保请求成功 if (response$sta…

腾讯云真的是良心云!服务器带宽、CPU、硬盘IO性能大揭秘!

本文将通过对腾讯云服务器CVM S5 4核配置的云服务器进行测试&#xff0c;来评估其在带宽、CPU和硬盘IO性能方面的表现。 在云服务器的并发处理中&#xff0c;带宽是一个重要的因素。经过测试&#xff0c;腾讯云的带宽网络表现非常出色&#xff0c;能够跑满带宽&#xff0c;同时…