首页
社区
课程
招聘
[原创]从底层视角看面向对象
发表于: 2024-9-21 13:43 3093

[原创]从底层视角看面向对象

2024-9-21 13:43
3093

假如让计算机运算1+2,包含如下几步操作

将数值1写入寄存器A

数值2写入寄存器B

将寄存器A和寄存器B的数据相加并将结果保存到寄存器A

我们知道计算机是用二进制来表示数据或指令的,假设向寄存器传送数据的指令用0001表示,加法指令用0010表示,寄存器A用0000表示,寄存器B用0001表示,那么上述步骤可表示为:

0001       0000           0001

传送数据 到寄存器A   传送数值1


0001       0001           0010

传送数据 到寄存器B   传送数值2


0010       0000           0001

加法运算  寄存器A      寄存器B


那么将上述指令连起来就是:0001 0000 0001 0001 0001 0010 0010 0000 0001。这就是机器语言,早期纸带打孔编程,就用的这种形式,很明显机器语言有个缺陷:计算器看得懂,但是人看不懂。

人类能看懂什么?文字,符号,所以能否将机器指令表示为人类易于理解的形式?比如用mov表示向寄存器传送数据,用add表示加法运算,用ax表示寄存器A,bx表示寄存器B,所以上述步骤又可以表示为:

这就是汇编语言,汇编就是机器语言的符号化表示。

那么有了这种便于人类理解的形式,我们可以编写一些复杂的指令,比如:

上述指令演示了一个分支结构,如果ax <= 2 则执行ab+bx,否则执行bx-1。

再比如:

这组指令表示了一个循环结构,ax的值从0开始递增,直到100跳出循环。

可以看出,汇编语言虽然转化为了人类易于理解的形式,但想要编写复杂的程序,还需要写大量的指令,并不是很方便,能否用一种更简洁的形式来代表这些指令?比如分支用if else表示,循环用while, do while, for表示,于是C语言诞生了,C语言表示分支结构是这样:

循环结构是这样:

用起来舒服多了,C语言的出现极大提高了编程效率,并且一定程度上解决了跨平台的问题,过去用汇编写程序时,在x86平台上编写的指令是无法拿到arm平台上执行的,但是有了C语言,在不改动源代码的情况下,只要有目标平台的编译环境,就可以将代码编译为该平台的汇编指令。


我们常说C语言是面向过程的语言,我倒是认为面向过程更符合人类在编程时的直觉,只要明确了要实现的功能,只需按照步骤

step1;

step2;

step3;

...

往下写就行了,举个例子,假设要实现一个时钟的功能,包含时分秒三个数据,能够设置这三个数据,并且格式化显示出来,按照面向过程的思维就这么写:

很直观,但是也存在一些问题,数据是零散的,能否抽象出一个实体表示时钟,将时分秒集成到时钟内部,那么我们可以定义一个结构体:

还有一个问题,如果买来一个钟表想要调它的时间,难道要把表拆开亲自去拨动里面的齿轮吗?当然不是,通常钟表后面有两个可以旋转的按钮,通过旋转它们即可调整到正确的时间,那么我们的程序也不应该直接修改stgClock里的变量,而是提供修改时分秒的接口,通过调用接口来设置时间,所以我们提供一些函数:

这就叫做”封装“

有了封装,于是main函数改为这样:

数据都被集成到结构体内部,对外通过调用接口来操作时钟,而并非直接修改里面的变量。


可以通过调试InitClock函数,观察stgClock的内存结构:

当三条赋值语句执行完后,内存中psClock指向的3个连续的4字节被分别初始化为0x02,0x14,0x36。


现在假设推出一款精度更高的时钟,除了拥有原先stgClock的功能外,还要求精确到毫秒,那么我们再定义一个结构体:

包含了原stgClock的同时,新增了一个成员m_nMs表示毫秒,同时也提供其对应的接口:

这两个函数都复用了stgClock的接口,并且扩展了自己的功能,这叫”继承“

在main函数中增加stgClockPlus的调用:


可以调试一下InitClockPlus函数,观察继承的内存结构:

InitClock执行完后,在内存窗口中,psClockPlus指向的3个连续的4字节已初始化为0x3, 0x12, 0x45,


当初始化毫秒执行完后,紧随其后的4个字节被初始化为0x666:

从内存结构的角度来看,继承就是在原先的内存结构后面追加新数据。


接下来,我们对FormatTimePlus函数做一点修改:

第一个参数由原来的 stgClockPlus* 改为 stgClock* 类型,在格式化输出毫秒的时候再强转为 stgClockPlus* 类型,这样FormatTimePlus和FormatTime函数的返回值、参数列表、调用约定都相同,我们可以认为是同一种类型的函数。


然后在stgClock结构前面插入一个该类型的函数指针__vfptr,姑且称之为”虚函数指针“:

在stgClock和stgClockPlus的初始化函数中,分别对虚函数指针赋值:

另外再封装一个新函数ShowTime:

该函数传入一个stgClock类型的指针,并且调用其__vfptr所代表的函数。


最后将main函数改为这样:

接下来进行调试,在执行完InitClock后,clock的__vfptr被赋值为0x00411046,这是函数FormatTime地址;



然后步入InitClockPlus函数内部,InitClockPlus要复用InitClock,所以在执行完InitClock后__vfptr同样被赋值为0x00411046:


但是下面的语句又将__vfptr替换为函数FormatTimePlus的地址0x0041118b:

接下来跟进ShowTime函数,第一次传入&clock,在执行到调用__vfptr所指向的函数时,按F11步入发现跳转到了FormatTime函数内部:

第二次传入&clockPlus,在执行到调用__vfptr所指向的函数时,按F11步入发现跳转到了FormatTimePlus函数内部:

也就是说,函数ShowTime的入参接收一个stgClock类型的指针,但无论是传入 stgClock* 类型还是扩展的 stgClockPlus* 类型,都能调用到其对应的格式化函数,我姑且称之为”多态“


现在,封装,继承,多态,面向对象三大特性都集齐了,所以C语言是可以进行面向对象编程的,但是C并不适合写面向对象,比如我非要在main函数里写这么一个语句:

从语法的角度讲没有任何问题,但破坏了对象的封装性,也就是说用C写面向对象,代码的合理性完全取决于使用者的素质,为了更自然的使用面向对象,C++诞生了。


在C++里,有一个与结构体对标的概念叫类(class),我们想要解决类的内部成员不能被外部随意修改的问题,可以给它声明权限为私有(private),于是我们定义出一个类:

最后于 4天前 被米龙·0xFFFE编辑 ,原因:
收藏
免费 3
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//