c语言面试深度

上传人:今*** 文档编号:105859537 上传时间:2019-10-13 格式:DOCX 页数:31 大小:58.52KB
返回 下载 相关 举报
c语言面试深度_第1页
第1页 / 共31页
c语言面试深度_第2页
第2页 / 共31页
c语言面试深度_第3页
第3页 / 共31页
c语言面试深度_第4页
第4页 / 共31页
c语言面试深度_第5页
第5页 / 共31页
点击查看更多>>
资源描述

《c语言面试深度》由会员分享,可在线阅读,更多相关《c语言面试深度(31页珍藏版)》请在金锄头文库上搜索。

1、第1章 :概念定义篇 1.1、局部变量能否和全局变量重名? 解答: 能。在局部变量的作用域内,局部变量会屏蔽掉同名的全局变量。此时若需要使用全局变量,需要使用”:”符号(linux中必须使用g+编译,使用gcc时并不识别域操作符:)。 深度解析: C语言中有“作用域”的概念。譬如全局变量作用域为整个文件(准确的说是定义该全局变量的文件中该变量定义/声明之后的部分。只不过一般情况下全局变量都在文件头部定义,因此说全局变量为文件作用域。),局部变量为代码块作用域。 所谓代码块作用域,代码块是指用一对大括号括起来的部分(譬如一个函数的函数体,for循环的循环体等)。也就是说局部变量的作用域其实是定义

2、这个局部变量的代码块中该变量定义体之后的部分。 这样看来,至少得到以下两个结论: l (1)变量的作用域是有大有小的 l (2)变量的作用域是有重叠部分的。譬如在一个函数内,该函数的局部变量和整个文件的全 局变量都覆盖这个作用域,这就是作用域的重叠。 重叠作用域中,如果全局变量名和局部变量名不同不会造成困扰。因为我们可以很容易的通过变量名来区分两个变量(我们班有个叫旺财的,别人班有个叫富贵的,我叫旺财你肯定知道我要找的是谁吧!)。但是在两个变量名相同时要怎么办呢(我们班有个旺财,隔壁班也有个旺财,我在走廊里喊一声旺财,你觉得我在叫谁)? 这种情况逻辑学上叫二义性(ambiguity)。即有两种

3、可能的解释,却没有任何区分的方法。怎么办呢?人为规定嘛。C语言规定:在变量作用域重叠时,作用域为小范围的变量覆盖大范围的变量。譬如函数内有个局部变量var,文件内有个全局变量var。则在该函数内部(准确的说是函数内部var局部变量定义体之后的部分),你使用var访问的是var局部变量,此处全局变量var被掩蔽(要想在此处访问全局变量var,对于C+可以使用:符号,而C语言中则没有域操作符:)1.2、堆栈溢出一般是什么原因导致的?解答:堆栈溢出一般都是由堆栈越界访问导致的。例如函数内局部变量数组越界访问,或者函数内局部变量使用过多,超出了操作系统为该进程分配的栈的大小也会导致堆栈溢出。深度解析:

4、 首先要区分清楚堆、栈、堆栈这几个名词。堆(heap)和栈(stack)是两种不同的内存管理机制。 堆被称为动态内存,由堆管理器(系统里的大人物,山高皇帝远不用去管它)管理,程序中可以使用malloc函数来(向堆管理器)申请分配堆内存,使用完后使用free函数释放(给堆管理器回收)。堆内存的特点是:在程序运行过程中才申请分配,在程序运行中即释放(因此称为动态内存分配技术)。 栈是C语言使用的一种内存自动分配技术(注意是自动,不是动态,这是两个概念),自动指的是栈内存操作不用C程序员干预,而是自动分配自动回收的。C语言中局部变量就分配在栈上,进入函数时局部变量需要的内存自动分配,函数结束退出时局

5、部变量对应的内存自动释放,整个过程中程序员不需要人为干预。 堆栈这个词纯粹是用来坑人的。堆就是堆(heap),栈就是栈(stack),根本没有另外一种内存管理机制叫堆栈。大多数时候有人说起堆栈,其实他想说的是栈,以前早些的时候,这方面的命名并不是特别准确。(别人说堆栈的时候,大家知道他其实想说的是栈就行了,自己就不要再用这个不准确的词了)。 既然堆和栈都是用来管理内存的机制,使用时就有一定的规则。无视规则的错误使用(C语言设计时赋予了程序员很大的自由度,所以有些错误语言本身是不会检查的,全凭程序员自己把握。)就可以导致一些内存错误,如内存泄漏、溢出错误等。 内存泄漏主要发生在堆内存使用中。譬如

6、我们使用malloc申请了内存,使用过后并未释放而丢弃了指向该内存的指针(这个指针是这段内存的唯一记录,程序中释放该段内存都靠这个指针了),那么这段堆内存就泄漏掉了(堆管理器以为程序还在使用,所以不会将这段内存再次分配给别的程序)。必须等到这个程序彻底退出后,系统回收该程序所使用的所有资源(申请的内存,使用的文件描述符等)时这些泄漏的内存才重新回到堆管理器的怀抱。 内存溢出在堆和栈中都有可能发生。参见章节示例1_2_stack_overflow.c中的8个示例函数,其中前三个函数与堆溢出有关,后五个函数与栈溢出有关。 函数heap_overflow中使用malloc申请了16字节动态内存,然后

7、尝试去读写这16个内存之中的第n个。三个测试分别给n赋值9,99和9999999,得到的结果很有意思(见程序后面的注释,大家也可以自己编译运行测试),现在我们来探讨其中的原理。 n等于9的时候没什么好说的,本该正确运行,这个相信大家没有异议。n等于99的时候竟然也可以正确运行,这个相信很多人就有点想不通了。我们申请的空间只有16字节啊,怎么竟然还可以访问第99个字节空间呢(这就是所谓的堆溢出访问)?这时候实际已经堆溢出了,但是为什么结果没有出错呢?原因在操作系统的内存分配策略中。譬如linux中内存是按照页(Page,一般是4K字节一个页)来管理的,操作系统给进程分配内存本质上都是以页为单位进

8、行的。也就是说你虽然只要求了16个字节,但是实际分配给你这个进程的可能是一个页(4K字节)。这个页中只有这16个字节是你自己的“合法财产”,其他部分你不该去访问(一访问就堆越界)。但是因为操作系统对内存的访问权限管理是以页为单位的,因此本页内16字节之外的内存你(非法)访问时系统仍然不会报错,并且确实能够达成目的(示例中n等于99时读写仍然正确)。 那是不是说堆越界是无害的,完全不用担心呢?显然不是。因为堆越界最大的伤害不是对自己,而是对“别人”。因为除了你申请的16字节外本页面内其他内存可能会被堆管理器分配给其他变量,你越界访问时意味着你可能践踏了其他变量的有效区域(譬如我们给第99个字节赋

9、值为g时,很可能把别处动态分配的一个变量的一部分给无意识的修改了)。因此其他变量会“莫名其妙”的出错,而且最可怕的是这种出错编译器无法帮你发现,大多数时候隐藏的很深,极难发现,往往令调试者抓狂、痛不欲生。因此访问堆内存时应该极为小心,一定要检验访问范围,谨防堆访问越界。 最后一个示例中n等于9999999,这是我随便写的一个很大的数,执行结果为:段错误(Segmentationfault)。熟悉C语言的同学都知道,一般段错误都是因为程序访问了不该访问的区域(譬如试图写代码段),这里也不例外。什么原因?考虑下上文中提到的以页为单位的内存管理策略。给你分配了一个页(一般是4KB),你访问时索引值太

10、大已经超出了这个页(跑到下个页甚至更后面的页面去了),那边的内存页面根本不归你使用,你试图读写的时候操作系统的内存管理部分就会一巴掌把你扇回来,给你个Segmentationfault。那个数字式我随便写的,你也可以自己试试先给个小数字,然后逐渐加大,总会有个临界点,过了那个点就开始段错误了。 func1到func5这五个示例用来演示栈溢出。 func1是典型的数组越界造成的栈溢出,压栈越界导致冲毁了函数调用堆栈结构,致使整个程序崩溃。由此可见,在C语言中数组访问时一定要小心检查,保证不越界。C语言为了追求最高的效率,并未提供任何数组访问动态检查(实际上也没有提供编译时数组访问是否越界的静态检

11、查,其原因是C语言愿意相信程序员,而将检查的重任交给了程序员自己果然是权力越大、责任越大啊!),因此“保卫世界和平的重任就靠你了”。 func2和func3是一对对比测试。其中调用了一个递归函数factorial,该函数用来求一个正整数n的阶乘。func2中n等于10,计算结果为3628800,是正确的(大家可以用计算器自己验证)。func3中n等于10000000,运行结果为段错误(其实即使不段错误,factorial函数本身也无法计算很大数字的阶乘,原因在于函数中使用unsignedint类型来存阶乘值,这个类型的取值范围非常有限,n稍微大一点就会溢出。但溢出只会导致计算结果不对,不会造成

12、段错误的)。 怎么会段错误呢?因为递归次数太多,栈终于被撑爆了。递归函数运行时,实际上相当于不停在执行子函数调用,因此栈一直在分配而没有释放。若在栈使用完之前递归仍然没有结束返回(此时会逐层释放栈)就会发生段错误。这是栈溢出的另一个典型情况,请大家以后使用递归算法解决问题时注意这个限制 func4和func5是一对对比测试。其中均定义了一个局部变量数组a,不同的是a的大小。func4中数组大小为1M(注意a的类型是int,因此这里单位是4字节),运行成功。而func5中数组大小为4M,运行时则发生段错误。相信有了上面上面的讲解,大家能够很容易想明白,局部变量分配太多把栈用完了,所以就段错误了,

13、就这么简单。 以上,通过5个示例程序为大家演示了栈溢出的三种情况。一般来说,第一种情况是明显的错误,且每次执行都确定会发生错误。而后两种错误则稍微复杂一些,原因在于这两种错误都依赖于栈的大小。而栈的大小在操作系统中不是固定的,是可以人为设置的(譬如linux中使用ulimits来查看和设置用户进程栈大小)。这就会带来一些很“神奇”的bug,如程序在你的计算机中运行良好,调试通过。结果发给客户,10个客户中8个运行良好,另外两个会报错、死机这时候只要重新设置一个更大的用户栈容量就可以解决问题。所以大家在写代码时一定要注意,考虑到你的代码有可能潜在的问题。这样一旦问题暴露即可迅速定位,并最快的找到

14、解决方案。不过更高级的做法是:在写代码时尽量减少可能存在的问题,让你的程序尽量更加健壮(robust)。第二章:关键字篇2.1、如何引用一个已经定义过的全局变量?解答:如果要引用的全局变量在同一文件内定义,则可以直接引用;如果要引用的全局变量在另外的C文件中定义,则有两种引用方式。第一种是使用#include包含声明了该全局变量的头文件,第二种是使用extern关键字在本文件中再次声明该全局变量。深度解析:C语言中有定义和声明,需搞清楚两者的联系和区别。变量定义的本质是新建一个变量(或者向系统申请一个变量),系统需要为新建立的变量分配内存空间。因此变量的定义意味着变量的生成;而声明却不产生新的

15、变量,只是告诉编译器有这么一个变量存在(也许是在别的地方定义的)。变量的定义好理解,因此要搞清楚两者的区别关键在于理解声明。首先要明白为什么需要声明(我相信没有人想问为什么需要定义吧)。这是个很深刻的问题,要理解这个问题需要从编译连接系统的工作原理入手。C语言的编译系统工作方式是:第一阶段:单个.c源文件先独立分开编译(生成对应的.o目标文件),由编译器完成,此阶段发现的错误称为编译错误。如语法错误、变量未定义等都是编译错误。第二阶段:所有的.o目标文件连接生成可执行程序,由连接器完成。此阶段发现的错误成为连接错误。如某个符号未定义,多重定义等。(注:实际的过程可能更复杂,譬如要考虑预处理器、汇编器的工作,连接阶段要考虑预编译库等等。这里为了简单起见,大家先不管这些细节了。)编译时,变量必须先经过声明才能使用。没有声明就直接使用的变量会被判编译错误(原因是编译器需要变量或者函数的类型原型声明以判断类型不匹配错误)。也就是说编译阶段编译器只看声明而不要求定义。连接时,同名变量必须有且仅有一次定义。变量只有声明而没有定义,或者多处定义同名变量都会报连接错误。因为同名变量(也就是同一个变量)只能分配一处内存空间,因此只能定义一次。好了,废了半天口舌,背景基本交代清楚了。现在我们假设这样一种情况:由两个文件a.c和b.c组成的工程,在a.

展开阅读全文
相关资源
正为您匹配相似的精品文档
相关搜索

最新文档


当前位置:首页 > 高等教育 > 大学课件

电脑版 |金锄头文库版权所有
经营许可证:蜀ICP备13022795号 | 川公网安备 51140202000112号