C语言学习记录230628

        不得不说,工作+运动+学习+出差,这一天下来,确实挺磨人的,但是好在今天还是完成了任务,白天把调试部分的课程继续深化学习了一下,其实也就是主要讲了下调试的实际讲解,里面还有一些对代码进行保护优化便于后期调试的一些方法,这些收获不小。

        再有就是今晚上的一堂课,主要讲解了栈区里,一个十分简单的程序,在调用函数,程序运行过程中,内存空间里的各种各样的变化,这部分讲解的很深刻,我现在都有种不太肯定自己记全的感觉,总之我也本着能记住多少说多少的原则,接下来总结吧。

        (一)调试技巧

                其实昨天还忘记说了一件事情,我昨天才知道原来BUG指的是蛾子,称程序的错误为BUG的原因是最早的,很笨重的计算机某天发现出了问题,程序运行老错误,最后检查了一遍发现是机器上有一只蛾子,所以后来就约定俗成的,将程序的错误称之为BUG,寻找错误点就是找BUG,那么减少错误,自然也就称呼了Debug了。   

#include<stdio.h>
int main(void)
{
    int i;
    int number[5]={1,2,3,4,5};
    for(i=0;i<=8;i++)
    {
        number[i]=0;
    }
    return 0;
}

                接下来简单说一下今天利用调试实例讲解过程中的例子,看上面代码,思考数组越界访问,并赋值可能会产生什么问题?在今天的课上,老师给出的例子是无限循环的情况,为什么无限循环?原因在于先声明了i,后声明了number,在main函数的栈帧内,目前为止还没有什么问题,问题在于栈顶和栈底跟内存里地址的关系是相反的,栈顶位于低地址,而栈底位于高地址,而声明变量从栈底一步一步到栈顶的,i的变量先声明,那么i距离栈底比arr更近,i的地址就越高。

                知道前面的情况以后,我们回过头来讨论number的问题,我们知道数组的下标是同内存地址一样由低到高的,换句话说也是从低地址到高地址。而我们的数组声明并初始化的时候,里面只有十个元素,证明其下标最大只能为9,而下面的for循环i所代表的下标最大将达到12,当给arr对应i下标的内存空间赋值时,自然会产生越界的情况。当数组越界访问时,会正常对越界访问的元素进行操作,编译器是不会提示我们越界或者阻止我们给超出范围的内存进行操作的,所以我们这个循环会给下标10~12的内存空间赋值0。

                我们知道i变量先声明,所以i变量在内存中的地址比number要高,所以假如i变量的地址恰好在arr数组10~12下标的对应的内存空间内,那么可能会因数组访问赋值,导致i归零,当归零后,回到for语句的控制表达式,i因为归0,所以无法到达13结束循环,从而导致无限循环。

                上面说的是其中一种情况,实际上这种越界访问会产生未定义的结果,我们也不知道到底会发生什么,甚至可能什么影响也没有,我们只能在可能的情况下进行分析。

                除上面的内容以外,还有几个很有用的工具,一个是assert函数,使用这个函数能对某个表达式进行“断言”,假如这个表达式的值为假,则编译器会报错。这个assert函数只在debug版本中会编译出来,而在release版本里,将会被自动优化删除。需要注意点的一点是assert函数需要在开头对应给出头文件assert.h。

                除了assert以外,还有const,利用const对调用的函数形参进行保护,这样可以有效提高后期代码的出错率。实际上不管是assert还是const,本质上都是对变量或者说某个数据的保护,而这种保护在我们用了assert或者const之后,都将能通过编译器自动识别出来,这能为我们后期调试省却不少功夫。

        (二)栈帧的创建与销毁

                这才是今天的重头戏,虽然上面调试课程的时间比这个栈帧的内容长,但实际上这个栈帧的内容丰富度,信息密度确实远远高于调试。由于哪怕是讲师也在讲解过程中运用了大量的图文说明,所以我这边就简单口头复述一下我今天的理解。

                假如我们已经写好了一个主程序main,还有一个自定义函数sub,那么我们顺着程序的运行顺序依次讲解。

                首先我们知道main主程序其实也是一个函数,我们在进入这个main函数之前,实际上要首先进入更上一步的函数,当然那些函数不是我们本次讲解的重点,但是我们要知道,我们的main主函数实际上也是被调用的一个函数。

                当我们进入main函数开始,那就是重头戏了,首先程序会在内存的栈区内在为main函数开辟一片空间前,已经有其他的函数已经开辟了空间,每个函数的空间我们都称之为对应函数的函数栈帧,而调用main函数的前一个函数,寄存器esp和ebp内分别存储这这个栈帧的栈顶和栈底地址,这里要强调一下,在内存内,栈帧的栈顶位于低地址,而栈底位于高地址,这与内存相反。

                然后我们开始考虑main函数的函数栈帧时如何生成的,首先会将ebp当前栈帧栈底地址的值压栈压到栈顶,同时esp移动一位,指向对应的存有当前栈帧栈底地址的内存单元;随后esp将他的地址传给ebp,之后将自己当前的地址减去一个值,这个值实际上就是编译器给main函数留出来的内存空间,也就是main函数的栈帧;再一下步,继续往栈顶压栈压上寄存器ebx、esi、edi的值,同时esp对应移动,这三个寄存器的值我们也不知道,但影响不是很大,实际上这三个寄存器的作用体现在最后一步,下一步将压上三个寄存器值之前的esp地址传给edi,然后将依照寄存器eax、ecx的数值,对ebp栈底至原esp栈顶之间的所有内存空间进行一个赋值,这个值不同的编译器不一样,这也是我们声明一个变量,如果未对其初始化,他会有一个随机值的原因。

                经过了以上这样的步骤后,才算是main函数栈帧创建的完整过程。

                随后假如我们声明了一个变量a,b,我们调用函数sub,并将变量a,b的值作为实际参数传入函数,这时会内存内是如何运行的呢?
                实际上在正式进入自定义函数sub之前,main函数栈帧会将a和b的值依照从右向左的顺序,依次压栈压到esp栈顶上,esp同步移动指针,压完之后,会将进入sub函数的下一条指令的内存空间压栈压到栈顶, 然后再将main函数的ebp栈底的地址压栈压到栈顶。

                做完这些之后,将当前esp的值给到ebp,然后同样esp减去一个值,这个值就是编译器预留给sub函数的内存空间,也就是sub函数的函数栈帧大小,之后再分别压上ebx、esi、edi的值,同时eax和ecx对这sub函数内所有的内存单元进行赋值,之后就依照程序运行完。这里值得注意的是形参并没有在sub函数的函数栈帧内声明,而是在sub函数的函数栈帧生成之前,就已经通过压栈的方式压在了sub函数的高地址区域,在程序内对这几个形参调用是,实际上是通过ebp的地址偏移量对压栈压上去的形参值进行调用,所以我们说形参是实参的复制体,这个角度来看真是一点都没错。

                计算完,需要离开sub函数前需要返回值,这个返回值实际上也很讲究,假如sub函数最后算出了一个值,这个值会传给寄存器eax,因为这个函数里的局部变量在离开函数后会直接消亡,所以给寄存器eax保存这个数据。

                之后sub函数的函数栈帧会依次弹出其对应的edi,esi和ebx,然后将sub函数栈帧的栈底ebp的值传给esp,随后弹出之前压在当前最上面的main函数的ebp的值,并将这个值给到ebp,从而使esp当前指在栈顶,而ebp指在main函数的栈底。

                之后就是正式的返回了,我们知道之前存有main函数栈帧栈底地址值的内存空间下面是存有调用sub函数下一步指令对应地址的内存单元,当我们弹出main函数栈帧的栈底地址内存单元后,esp指向了存有下一条指令地址的内存单元,这时弹出这个内存单元,我们就回到了之前调用sub函数对应语句的下一条指令的位置。

                回到下一条指令的位置后,程序立刻移动esp的位置,移动到压到栈顶的两个形参值内存单元的下面,从而弹出了两个形参的栈空间,随后将eax内保存的返回值传给对应main函数内的变量,至此,sub函数的栈帧销毁就完成了。

                我们可以继续推演main函数如何销毁,假如有返回值,首先将返回值传给寄存器eax保存下来,依次弹出栈顶的edi、esi和ebx,将ebp的值给到esp,弹出存有上一个函数栈底地址值的内存单元,并把值给ebp。同样继续弹出存有下一条指令地址的内存单元,让我们回到对应的函数下一条指令位置,随后移动esp的位置,弹出形参,最后将eax的值返回给对应函数内的变量。

        今天关于栈帧的创建与销毁,乍一看确实很复杂,特别是我现在描述还没有图文,可能更难理解,如果有兴趣,建议可以通过别的途径去了解下,这里仅仅是我个人梳理自己今天所学的地方。如果有问题,也欢迎私信或者评论交流。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值