从一道面试题来看计算机基础知识的重要性

用户头像
周三不加班
关注
发布于: 2020 年 05 月 06 日

以前碰到过这样一道面试题:



请分析如下代码的执行结果。



#include <stdio.h>
int main()
{
int i = 0;
int arr[3] = {0};
for(;i<=3;i++) {
arr[i] = 0;
printf("Hello, World! \n");
}
return 0;
}



我心想,头一次面试碰到这么简单的面试题,面试官你是不是看不起我?



奋笔疾书,我写了个三行 Hello,World



可是当我自信满满的交给面试官的时候他却诡异的笑了,仿佛在说,小伙计,你太天真了,你是不是看不起我出的题?



当然,以上都是我胡诌出来的,一个面试不可能有这么多内心戏,我是一个写代码的程序员,又不是戏精,哪来那么多想法。



但是,虽然内心戏是胡诌的,可是这道面试题目确实实打实的。



接下来我们仔仔细细分析下这道题目,为了先快速得到执行结果,我们先运行一遍看看:





执行结果



可以看到,这段代码竟然无限循环了,是不是很难理解。



想分析这段代码,我们首先需要了解数组这个数据结构,简单来说:数组是用一组连续的内存空间,存储一组具有相同类型的数据的线性数据结构。





数组逻辑结构



根据数组的逻辑结构图我们可以总结出一位数组的寻址公式为:arr[i]_address = base_address + i*data_size,其中 data_size 表示每个数据的磁盘空间大小。



根据这个寻址公式,我们来对上边的代码做出寻址行为的分析,当 i = 3 时,理论上寻址公式为 arr[3]_address = arr[2]_address + i*data_size,此处需要注意的是因为 C 语言中,数组越界是一种未决行为,如果你是从事 Java 或者其他高级语言的话会发现对于数组越界是当做一种异常行为来处理的,但是 C 语言不是。对于 C 语言来说,只要不是访问受限的内存空间,所有的内存空间都是可以自由访问的,哪么根据既定寻址公式,arr[3]会被定位到某块本不属于数组的内存地址上,而这块地址恰恰存储的是我们定义的变量 i,也就是说此时 arr[3] = 0 ,相当于 i = 0 时的情况,这样就会导致无限循环。



如果i<=3改成i<3,结果才是我们想要的样子。





我们想要的结果



到此,程序的执行结果我们分析完了,但是你可能对于上边加粗的 而这块地址恰恰存储的是我们定义的变量 i 这句话有疑问,怎么就 arr[3] = 0 了呢?



这个时候我们大学学过的操作系统和计算机体系结构以及甚至可能是编译原理的知识就要排上用场啦。



我们都知道在写一个函数时会使用形参,形参实例化时会形成一份拷贝,调用这个函数时会把实参传进去,调用完之后那些临时拷贝又被释放,那么计算机在调用函数时是如何进行形参的保存和释放的呢?这个时候会用到一个叫栈的数据结构。



栈用于维护函数调用的上下文,离开了栈,函数调用就无法实现。栈是从高地址向低地址延伸的。每个函数的每次调用都有它自己独立的一个栈帧,这个栈帧中有它所需要的各种信息。



回到上边那段代码,产生死循环的第一个原因就是因为函数调用栈的特殊性:函数体内的局部变量是存在栈上的,且是连续压栈。在 Linux 进程的内存布局中,栈区在高地址空间,从高向低增长。变量 i 和 arr 在相邻地址,且 i 比 arr 的地址大,所以 arr 越界正好访问到i。当然,前提是 i 和 arr 元素要同类型,否则那段代码仍是未决行为。



还有另一个原因是因为编译器分配内存和字节对齐,我们定义 3 个元素的数组加上一个变量 i 。4 个整数刚好能满足 8 字节对齐 所以 i 的地址恰好跟着 arr[2] 后面导致死循环。如果数组本身有 4 个元素 则这里不会出现死循环。因为编译器 64 位操作系统下,默认会进行 8 字节对齐 变量 i 的地址就不紧跟着数组后面了。



通过今天这道看似简单实则还是比较复杂的题目,可以说坑很多,涉及到的知识点也不少,但恰恰这些知识点是我们大学学过的一些计算机基础知识,没有涉及任何框架,也没有任何的新技术,可很多人还是答不上来。



简言之,你别看有些人整天这框架那框架的玩的很嗨,但是它的计算机专业素养是远远不够的,职业生涯前期可能不会有什么区别,但是长远来看,掌握了基础知识的人上限势必要高一些。



发布于: 2020 年 05 月 06 日 阅读数: 65
用户头像

周三不加班

关注

还未添加个人签名 2019.02.15 加入

还未添加个人简介

评论

发布
暂无评论
从一道面试题来看计算机基础知识的重要性