




版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
目录作者自序 2第一章:概念定义篇 21.1、局部变量能否和全局变量重名? 21.2、堆栈溢出一般是什么原因导致的? 31.3、? 5第二章:关键字篇 52.1、如何引用一个已经定义过的全局变量? 52.2、单片机程序中经常使用到死循环,如何使用C语言写出一个死循环? 72.3、do···while和while···do有什么区别? 82.4、C语言中static有什么用? 8第三章操作符 103.1、请写出下列代码的输出内容 103.2、写出floatx与“零值”比较的if语句. 122、? 15附录1、C语言运算符表 16作者自序现在再写C语言的书似乎有点不合时宜。原因很简单,C语言的书实在是不少了,而且其中不乏经典优秀之作。如国外的《C程序设计语言》、《C和指针》、《C缺陷与陷阱》,国内林锐的《高质量C/C++编程指南》等。加之C语言本身这些年也没有发生值得讨论的大的更改,因此曾经的经典书籍并没有被岁月磨去使用价值。我们可以轻松得出结论:根本没有必要再去写一本系统介绍、循序渐进的学习C语言的教材。那么本书从何立意?为什么大家需要这样一本C语言书?或者说,大家到底还需要一本什么样的C语言书呢?我将会在本书中为大家揭开答案。首先,从组织形式来看。本书没有像传统的C语言教材一样,以知识点为线索来构架整书。而是另辟蹊径,以专题的形式来平面化组织。各专题之间是彼此独立、平行存在的。因此本书并不是一本系统学习C语言的初级教材,它并不适合没有接触过编程语言的新人初学C语言,而是一本以专题为单位深入挖掘、有理有据、条分缕析,试图让大家对C语言有更深理解和感悟的提高型专作。其次,从选题角度来看。本书中所选的题目,都是软件研发企业面试笔试题中出现率最高的题目集合。因为职业的原因,笔者需要帮助大量应聘者指导面试笔试,从而需要接触和研究各家软件类企业的技术题目。毋庸置疑,C语言在各种技术面试中的比重是最大的(因为C语言的普及度高,最能考核面试者的基本素质,而且C语言在实际开发中应用很广)。就深圳地区来讲,无论是华为、腾讯、联想这样的知名巨头,还是遍布在深圳各地、为数众多的中小型公司,其招聘中单论技术环节,C语言都是其中的重头戏。有意思的是,经常出现的题目也是非常固定的。所以,面试中的技术面,可以说是一场开卷考试。可惜的是,即使是这样的一场开卷考试,仍然有众多考生“屡试不爽”,技术面总是不得要领,或者总是被归入“初级程序员”一列。本书参考众多国内外知名企业如华为、腾讯等的面试题,收录其中有代表性的典型题目,进行题目解答与深入分析。其中的解答部分以被面试者的角度回答问题,可以作为大家面试中被问到相同题目时的可选标准答案来参考。即回答了“是什么样”的问题;而深度解析部分(也是我重点要推荐的)则以该题目为引子,深入探讨了C语言中的相关知识点。并且会结合笔者对C语言的理解和讲述经验,试图带领大家探讨“为什么是这样?怎么来的?”的问题。再次,从表述风格来看。本书贯彻以下一些基本理念,并以之为指导思想:深入浅出。深入指的是深入到技术细节,甚至是技术背后的机制和原理。浅出指的是以举例、图解、类比比喻等手法使大家便于理解。深入进去,才能学到东西;浅出介绍,才容易使人接受。罗嗦(貌似这是个贬义词······)。相信大家都有过这种体验:有些时候看书或者听课时,费尽九牛二虎之力,看过好几个人的文章,听过好几个老师的课才明白到底怎么回事。我认为主要原因就在于:这些文章为了避免罗嗦,追求简洁的语言艺术,没有在关键问题上做详细(甚至是反复的、多种角度的)的说明,刚挠到痒处就“点到为止”了,造成信息传递的不完全,让读书的人“自己猜、自己悟”。本人坚信:语言的主要目的就是信息传递,至少传道授业解惑时是这样。语言有无美感是其次,关键是讲的人要讲清楚,听的人要听明白了。我从来不以罗嗦为耻。幽默。以前说:兴趣是最好的老师。现在又说:愉悦的用户体验是成功的关键。再好的书,不想看、看不下去也是白搭。笔者在实际授课中就以生动幽默、轻松愉悦的课堂氛围为学生所喜爱,在本书中我仍旧坚持发扬这种风格,努力让大家的学习少点枯燥与晦涩,多些欢乐与轻松。最后,随书提供了大量测试用例。这些测试用例都是精心设计编写,有些用来辅助文章描述方便大家理解,有些用来作为佐证证实书中所说,还有些用来验证自己的猜测、也就是通过代码示例的运行结果来学习。编程总的来说是一门技能,光有知识是不够的,还要通过练习将之转化为能力。所以,实际编写代码、调试代码的能力是程序员的核心技能,必须努力训练培养。通过代码调试来验证自己对问题的理解和猜测是一种很好的学习手段,是大家建立自己判断力、靠自己的大脑来认识世界的一个有效途径。别人的书永远只是个引子,别人总结的话永远只是他的理解。授人以鱼,不如授之以渔。本书中我始终试图引导大家自己分析、理解问题。所有的未知都是由已知加上符合逻辑、有道理的推论发展而来的,我试图带领大家从源头开始经历这些推论发展过程,从而建立大家的自我学习、分析、验证能力。常言常大哥说过:尽信书不如无书。知识的世界博大精深,我个人也只是探索其中的一叶孤舟。限于水平和认识,疏漏与不足在所难免。请大家讨论与指正。最后,希望大家从本书中有所得、有所悟。 朱有鹏2014032第一章:概念定义篇1.1、局部变量能否和全局变量重名?解答:能。在局部变量的作用域内,局部变量会屏蔽掉同名的全局变量。此时若需要使用全局变量,需要使用”::”符号(linux中必须使用g++编译,使用gcc时并不识别域操作符::)。深度解析:C语言中有“作用域”的概念。譬如全局变量作用域为整个文件(准确的说是定义该全局变量的文件中该变量定义/声明之后的部分。只不过一般情况下全局变量都在文件头部定义,因此说全局变量为文件作用域。),局部变量为代码块作用域。所谓代码块作用域,代码块是指用一对大括号{}括起来的部分(譬如一个函数的函数体,for循环的循环体等)。也就是说局部变量的作用域其实是定义这个局部变量的代码块中该变量定义体之后的部分。这样看来,至少得到以下两个结论:变量的作用域是有大有小的变量的作用域是有重叠部分的。譬如在一个函数内,该函数的局部变量和整个文件的全局变量都覆盖这个作用域,这就是作用域的重叠。重叠作用域中,如果全局变量名和局部变量名不同不会造成困扰。因为我们可以很容易的通过变量名来区分两个变量(我们班有个叫旺财的,别人班有个叫富贵的,我叫旺财你肯定知道我要找的是谁吧!)。但是在两个变量名相同时要怎么办呢(我们班有个旺财,隔壁班也有个旺财,我在走廊里喊一声旺财,你觉得我在叫谁···)?这种情况逻辑学上叫二义性(ambiguity)。即有两种可能的解释,却没有任何区分的方法。怎么办呢?人为规定嘛。C语言规定:在变量作用域重叠时,作用域为小范围的变量覆盖大范围的变量。譬如函数内有个局部变量var,文件内有个全局变量var。则在该函数内部(准确的说是函数内部var局部变量定义体之后的部分),你使用var访问的是var局部变量,此处全局变量var被掩蔽(要想在此处访问全局变量var,对于C++可以使用::符号,而C语言中则没有域操作符::)。1.2、堆栈溢出一般是什么原因导致的?解答:堆栈溢出一般都是由堆栈越界访问导致的。例如函数内局部变量数组越界访问,或者函数内局部变量使用过多,超出了操作系统为该进程分配的栈的大小也会导致堆栈溢出。深度解析:首先要区分清楚堆、栈、堆栈这几个名词。堆(heap)和栈(stack)是两种不同的内存管理机制。堆被称为动态内存,由堆管理器(系统里的大人物,山高皇帝远不用去管它)管理,程序中可以使用malloc函数来(向堆管理器)申请分配堆内存,使用完后使用free函数释放(给堆管理器回收)。堆内存的特点是:在程序运行过程中才申请分配,在程序运行中即释放(因此称为动态内存分配技术)。栈是C语言使用的一种内存自动分配技术(注意是自动,不是动态,这是两个概念),自动指的是栈内存操作不用C程序员干预,而是自动分配自动回收的。C语言中局部变量就分配在栈上,进入函数时局部变量需要的内存自动分配,函数结束退出时局部变量对应的内存自动释放,整个过程中程序员不需要人为干预。堆栈这个词纯粹是用来坑人的。堆就是堆(heap),栈就是栈(stack),根本没有另外一种内存管理机制叫堆栈。大多数时候有人说起堆栈,其实他想说的是栈,以前早些的时候,这方面的命名并不是特别准确。(别人说堆栈的时候,大家知道他其实想说的是栈就行了,自己就不要再用这个不准确的词了)。既然堆和栈都是用来管理内存的机制,使用时就有一定的规则。无视规则的错误使用(C语言设计时赋予了程序员很大的自由度,所以有些错误语言本身是不会检查的,全凭程序员自己把握。)就可以导致一些内存错误,如内存泄漏、溢出错误等。内存泄漏主要发生在堆内存使用中。譬如我们使用malloc申请了内存,使用过后并未释放而丢弃了指向该内存的指针(这个指针是这段内存的唯一记录,程序中释放该段内存都靠这个指针了),那么这段堆内存就泄漏掉了(堆管理器以为程序还在使用,所以不会将这段内存再次分配给别的程序)。必须等到这个程序彻底退出后,系统回收该程序所使用的所有资源(申请的内存,使用的文件描述符等)时这些泄漏的内存才重新回到堆管理器的怀抱。内存溢出在堆和栈中都有可能发生。参见章节示例1_2_stack_overflow.c中的8个示例函数,其中前三个函数与堆溢出有关,后五个函数与栈溢出有关。<堆溢出>函数heap_overflow中使用malloc申请了16字节动态内存,然后尝试去读写这16个内存之中的第n个。三个测试分别给n赋值9,99和9999999,得到的结果很有意思(见程序后面的注释,大家也可以自己编译运行测试),现在我们来探讨其中的原理。n等于9的时候没什么好说的,本该正确运行,这个相信大家没有异议。n等于99的时候······竟然也可以正确运行,这个相信很多人就有点想不通了。我们申请的空间只有16字节啊,怎么竟然还可以访问第99个字节空间呢(这就是所谓的堆溢出访问)?这时候实际已经堆溢出了,但是为什么结果没有出错呢?原因在操作系统的内存分配策略中。譬如linux中内存是按照页(Page,一般是4K字节一个页)来管理的,操作系统给进程分配内存本质上都是以页为单位进行的。也就是说你虽然只要求了16个字节,但是实际分配给你这个进程的可能是一个页(4K字节)。这个页中只有这16个字节是你自己的“合法财产”,其他部分你不该去访问(一访问就堆越界)。但是因为操作系统对内存的访问权限管理是以页为单位的,因此本页内16字节之外的内存你(非法)访问时系统仍然不会报错,并且确实能够达成目的(示例中n等于99时读写仍然正确)。那是不是说堆越界是无害的,完全不用担心呢?显然不是。因为堆越界最大的伤害不是对自己,而是对“别人”。因为除了你申请的16字节外本页面内其他内存可能会被堆管理器分配给其他变量,你越界访问时意味着你可能践踏了其他变量的有效区域(譬如我们给第99个字节赋值为g时,很可能把别处动态分配的一个变量的一部分给无意识的修改了)。因此其他变量会“莫名其妙”的出错,而且最可怕的是这种出错编译器无法帮你发现,大多数时候隐藏的很深,极难发现,往往令调试者抓狂、痛不欲生。因此访问堆内存时应该极为小心,一定要检验访问范围,谨防堆访问越界。最后一个示例中n等于9999999,这是我随便写的一个很大的数,执行结果为:段错误(Segmentationfault)。熟悉C语言的同学都知道,一般段错误都是因为程序访问了不该访问的区域(譬如试图写代码段),这里也不例外。什么原因?考虑下上文中提到的以页为单位的内存管理策略。给你分配了一个页(一般是4KB),你访问时索引值太大已经超出了这个页(跑到下个页甚至更后面的页面去了),那边的内存页面根本不归你使用,你试图读写的时候操作系统的内存管理部分就会一巴掌把你扇回来,给你个Segmentationfault。那个数字式我随便写的,你也可以自己试试先给个小数字,然后逐渐加大,总会有个临界点,过了那个点就开始段错误了。<栈溢出>func1到func5这五个示例用来演示栈溢出。func1是典型的数组越界造成的栈溢出,压栈越界导致冲毁了函数调用堆栈结构,致使整个程序崩溃。由此可见,在C语言中数组访问时一定要小心检查,保证不越界。C语言为了追求最高的效率,并未提供任何数组访问动态检查(实际上也没有提供编译时数组访问是否越界的静态检查,其原因是C语言愿意相信程序员,而将检查的重任交给了程序员自己······果然是权力越大、责任越大啊!),因此“保卫世界和平的重任就靠你了”。func2和func3是一对对比测试。其中调用了一个递归函数factorial,该函数用来求一个正整数n的阶乘。func2中n等于10,计算结果为3628800,是正确的(大家可以用计算器自己验证)。func3中n等于10000000,运行结果为段错误(其实即使不段错误,factorial函数本身也无法计算很大数字的阶乘,原因在于函数中使用unsignedint类型来存阶乘值,这个类型的取值范围非常有限,n稍微大一点就会溢出。但溢出只会导致计算结果不对,不会造成段错误的)。怎么会段错误呢?因为递归次数太多,栈终于被撑爆了。递归函数运行时,实际上相当于不停在执行子函数调用,因此栈一直在分配而没有释放。若在栈使用完之前递归仍然没有结束返回(此时会逐层释放栈)就会发生段错误。这是栈溢出的另一个典型情况,请大家以后使用递归算法解决问题时注意这个限制。func4和func5是一对对比测试。其中均定义了一个局部变量数组a,不同的是a的大小。func4中数组大小为1M(注意a的类型是int,因此这里单位是4字节),运行成功。而func5中数组大小为4M,运行时则发生段错误。相信有了上面上面的讲解,大家能够很容易想明白,局部变量分配太多把栈用完了,所以就段错误了,就这么简单。以上,通过5个示例程序为大家演示了栈溢出的三种情况。一般来说,第一种情况是明显的错误,且每次执行都确定会发生错误。而后两种错误则稍微复杂一些,原因在于这两种错误都依赖于栈的大小。而栈的大小在操作系统中不是固定的,是可以人为设置的(譬如linux中使用ulimit–s来查看和设置用户进程栈大小)。这就会带来一些很“神奇”的bug,如程序在你的计算机中运行良好,调试通过。结果发给客户,10个客户中8个运行良好,另外两个会报错、死机······这时候只要重新设置一个更大的用户栈容量就可以解决问题。所以大家在写代码时一定要注意,考虑到你的代码有可能潜在的问题。这样一旦问题暴露即可迅速定位,并最快的找到解决方案。不过更高级的做法是:在写代码时尽量减少可能存在的问题,让你的程序尽量更加健壮(robust)。1.3、?解答:能。深度解析:第二章:关键字篇2.1、如何引用一个已经定义过的全局变量?解答:如果要引用的全局变量在同一文件内定义,则可以直接引用;如果要引用的全局变量在另外的C文件中定义,则有两种引用方式。第一种是使用#include包含声明了该全局变量的头文件,第二种是使用extern关键字在本文件中再次声明该全局变量。深度解析:C语言中有定义和声明,需搞清楚两者的联系和区别。变量定义的本质是新建一个变量(或者向系统申请一个变量),系统需要为新建立的变量分配内存空间。因此变量的定义意味着变量的生成;而声明却不产生新的变量,只是告诉编译器有这么一个变量存在(也许是在别的地方定义的)。变量的定义好理解,因此要搞清楚两者的区别关键在于理解声明。首先要明白为什么需要声明(我相信没有人想问为什么需要定义吧···)。这是个很深刻的问题,要理解这个问题需要从编译连接系统的工作原理入手。C语言的编译系统工作方式是:第一阶段:单个.c源文件先独立分开编译(生成对应的.o目标文件),由编译器完成,此阶段发现的错误称为编译错误。如语法错误、变量未定义等都是编译错误。第二阶段:所有的.o目标文件连接生成可执行程序,由连接器完成。此阶段发现的错误成为连接错误。如某个符号未定义,多重定义等。(注:实际的过程可能更复杂,譬如要考虑预处理器、汇编器的工作,连接阶段要考虑预编译库等等。这里为了简单起见,大家先不管这些细节了。)编译时,变量必须先经过声明才能使用。没有声明就直接使用的变量会被判编译错误(原因是编译器需要变量或者函数的类型原型声明以判断类型不匹配错误)。也就是说编译阶段编译器只看声明而不要求定义。连接时,同名变量必须有且仅有一次定义。变量只有声明而没有定义,或者多处定义同名变量都会报连接错误。因为同名变量(也就是同一个变量)只能分配一处内存空间,因此只能定义一次。好了,废了半天口舌,背景基本交代清楚了。现在我们假设这样一种情况:由两个文件a.c和b.c组成的工程,在a.c中定义了全局变量var,同时在a.c和b.c中都使用到该变量。这种情况实际上很常见,我相信大家都遇到过。此时,a.c中引用var很容易。只要将var定义在a.c的最前面部分,即可保证文件中任何函数都可以无障碍访问var,保证编译和连接阶段都不会报错。但是b.c呢?我们在b.c中如何使用var呢?下面来逐步分析一下:<思路1>:在b.c中再次定义var。这种方法可以吗?如果可以那我们在b.c中定义的var和a.c中的是同一个吗?根据C语言的规定,变量定义意味着创造一个新的变量,因此这两个var虽然同名,但肯定是两个不同的变量。因此不能满足我们的需要。思路1失败。<思路2>:在头文件a.h中声明var,然后在b.c中#include<a.h>。这种方法行吗?分析编译过程看看。a.c编译肯定不会有错,b.c编译时因为包含了a.h中var的声明,因此编译器得到了var的变量原型(即编译器知道有一个变量名叫var,并且知道这个var的类型信息等,但编译器不知道这个var在哪里定义的。不过编译器根本不需要知道这个变量到底在哪里定义的,交给连接器同志去处理这些吧!),因此只要在b.c中使用该var的地方符合a.h中var的声明,b.c即可顺利通过编译。至此两个源文件编译通过,进入第二阶段连接。连接器在连接时会为b.o中引用var的部分寻找var的定义体。此时连接器很轻松的发现a.o中即有一个名为var的全局变量的定义体,因此连接器确定了a.o和b.o中的var为同一个变量。至此问题圆满解决。思路2成功。<思路3>:使用extern关键字。a.c中情况不变,仍然定义全局变量var(例如,intvar;),b.c中使用extern关键字声明变量var(externintvar;)。注意没有使用头文件中的声明,这样可以吗?同样的,先分析a.c和b.c各自的编译过程,再分析连接过程。编译时a.c当然没问题,b.c中因为先使用externintvar;对var进行了声明(声明的意义和思路2中相同)因此编译阶段也没问题。连接阶段和思路2相同,因此连接成功。至此,思路3成功。通过以上分析,我们知道引用包含声明的头文件或者extern声明的方式都可以使一个源文件引用其他源文件内定义的变量。下面来探讨一些更细节更有趣的问题。<问题1>:变量能否定义在头文件内?如果可以,那是定义在头文件中好还是定义在源文件中好?实际测试证明,变量是可以定义在头文件内的,测试示例见章节示例test2。那么是不是说变量定义放在源文件与头文件是随意的,没有任何区别呢?请再看章节示例test3。此示例中我们在a.h中定义了全局变量var1和var2,然后在a.c和b.c中都include了该头文件,结果链接时报错,提示var2重复定义了。test3很好的演示了变量定义在头文件中的坏处。当该头文件被多个源文件引用时即会导致该变量被重复定义,造成链接时错误。所以,变量还是定义在源文件中的好。<问题2>:工程中包含两个源文件a.c和b.c,a.c中有一行intvar,并且在其后的函数中引用了该var;b.c中也有一行intvar并且在其后的函数中也使用了var;该工程能否成功编译连接。如果不能,请指出编译还是连接错误,哪个文件会报错?在test4中,我们用一个实例说明了以上的定义是允许的,而且a.c和b.c中定义的同名变量var实际上是同一个变量。怎么回事?为什么没有重复定义呢?大家回忆下test1中对变量var的定义和声明,可以发现其实变量的定义和声明形式上是相同的,有时编译器会将它看作定义,有时会将它看作声明,有时候又是定义加声明,编译器会很智能的处理这个问题(当然了如果使用了赋值运算符=在定义的同时初始化,那这个表达式就一定是定义而肯定不是声明了)。譬如test4的示例中,a.c和b.c中都有intvar;的表达式,所以编译器会自动将其中一个当作定义,而另一个当作声明。因此,就算你再多加几十个源文件都使用这个同名变量,还是只会有一个是定义,其余全是声明,不会报错。那如果我们在定义变量的同时给其赋初值呢?如test5中的样子。会发现出现链接错误,var重复定义了。原因很简单,因为我们对var的两个定义表达式都赋初值了,所以编译器不得不把两个表达式都当成定义,所以连接时会重复定义。2.2、单片机程序中经常使用到死循环,如何使用C语言写出一个死循环?解答:C语言实现死循环有很多种方式,但是最常用的就是for(;;);和while(1);此外,还可以使用goto语句实现类似汇编风格的死循环。深度解析:这个题目比较简单,但是很实用。注意for循环中两个分号分开的三个部分其实都是可以省略的,所以当你看到for循环的某个部分缺失时不用惊讶。goto关键字一向被定义为不建议使用,主要原因是goto无条件到处跳来跳去的超级灵活特性导致它破坏了代码的结构性,使得代码灵活到难以读懂的程度了,这显然是大家不愿意看到的。但是goto也是有其存在的价值的(星爷语录:就算是一张卫生纸,一条破内裤都有它的用处),最适合使用goto的一场场景就是在多重循环体的内部,此时使用goto可以一步直接跳出所有的循环层次(以前使用break则只能逐层跳出,麻烦;现在用goto,一跳顶过去五跳,实惠···)。熟悉汇编的同学应该能感到,goto实际上是汇编中跳转指令的简单封装(譬如MCS51中的jmp指令,ARM汇编中的b指令)。而for和while、dowhile循环则是汇编中这些跳转指令的复杂封装,for循环等在汇编层次也是通过跳转指令实现的。之所以大家觉得C语言编程比汇编容易一些,就是因为C编译器帮大家做了一些基础性的封装,形成了C语言这种更符合人类思维方式的结构化编程语言。而直接使用汇编语言,则需要程序员“委屈自己”,去适应机器的思维方式,按照机器所能理解的方式去“讲话”。2.3、do···while和while···do有什么区别?解答:do···while循环的循环体至少会执行一次,而while···do循环则有可能一次都不被执行。深度解析:do{循环体}while(条件);循环先执行循环体,然后根据条件判断的真假决定是否继续下一次循环;而while(条件)do{循环体}循环则先判断条件的真假,然后决定是否执行循环体。执行完后再次判断条件,直到条件为假则终止循环。请注意dowhile循环中,while后面的()后面是有分号的,而whiledo循环do后面的()后是没有分号的。循环体中要提前终止或跳出循环,可以使用continue和break(当然了goto也可以,但是除了一次跳出多重循环的情况外,其他情况最好不使用goto)。两者的区别在于:continue用于终结本轮循环中continue语句之后的部分,因此continue之后的语句就不用执行了,直接进入到下一轮循环;而break用于终结本层循环,直接跳到本层循环之外的一层。简单来说,continue最温和,break要狠一些,不过最狠的还是goto。2.4、C语言中static有什么用?解答:static可以用来修饰局部变量,全局变量、函数。static修饰局部变量,则该变量成为静态局部变量。静态局部变量只在第一次访问时初始化一次,以后访问时它的值保持和上一次操作结束时一样。static修饰全局变量(函数),则该变量(函数)成为静态全局变量(函数)。静态全局变量(函数)只能在该文件范围内使用,不能被其他文件所使用。这是因为静态全局变量(函数)的连接属性为内连接,这样可以解决不同源文件内全局变量(函数)名称重名冲突问题。深度解析:C语言中有存储类、作用域、生命周期和连接属性等四个概念。存储类用来描述变量、函数等在内存中分配相关的属性(比如全局变量分配在数据区,而普通局部变量分配在栈上,静态局部变量分配在数据区),作用域描述变量可以被引用的范围(譬如全局变量有文件作用域,而局部变量只有代码块作用域),生命周期表述一个变量何时产生何时消亡(如局部变量在进入代码块时产生,代码块执行结束时消亡),连接属性描述一个变量或函数在连接阶段可以被连接的范围(C语言中全局变量和函数默认为外连接,因此同一工程中不同源文件之中的全局变量或函数不能重名,否则会引起链接时错误。用static修饰全局变量或函数形成静态全局变量或函数,则将其连接属性修改为内连接,此时该全局变量或函数名称只在本文件内有效,因此可以避免连接时重名问题)。这四个属性分别从四个角度去描述一个符号(变量或函数都是符号)所处的状态,并且四个属性之间互相影响,互为因果。这里我不打算用枯燥晦涩的概念来跟大家布道(这个世界永远不缺专家教授,这样的书到处都能找到)。按照我一贯的风格,我会讲一些示例,我们要讲的道理就在这些示例中,大家在心里默默体会吸收即可。示例1:全局变量可以在文件内(有些书也写作模块内,这里的模块其实就是指这个全局变量所在的.c源文件,因此我喜欢称文件内。概念不重要,大家能明白我想说的是什么才是关键。)任何地方被访问,而局部变量只能在定义它的代码块内被访问。造成这个事实的主要原因就是:全局变量的作用域为文件作用域,局部变量的作用域为代码块作用域;全局变量在main函数开始之前产生(全局变量的内存空间由加载器分配并初始化,因此在程序进入main,开始执行我们自己写的代码之前,全局变量就已经就位了。),在整个程序结束时死亡(跟随程序一起释放)。而局部变量在代码执行进入代码块时产生,在程序执行退出代码块时死亡。造成这个事实的主要原因是:全局变量的存储类为全局数据区(有时称为数据区,数据段,.data段),而局部变量的存储类为栈(stack)。两者存储类的不同导致了不同的生命周期。示例2:普通局部变量每次进入代码块时都会重新分配(如果定义同时有初始化式,还要再重新初始化一次),然后在代码块中被使用,最后在代码块退出时挂掉(被释放)。而静态局部变量只在第一次使用时初始化一次,以后使用时其值保持上次使用完时的值。也就是说可以认为静态局部变量第一次访问时被分配并初始化(实际上分配时间和全局变量一样),之后一直存在直到程序结束时与程序一起死亡。从以上描述很容易看出,静态局部变量在表现上与全局变量几乎一模一样。都是都是程序开始时产生、程序结束时死亡;都是初始化一次,以后每次访问时保持上次操作后的值。唯一的不同在于作用域(全局变量文件内各处随便访问,静态局部变量只在定义它的代码块内可以访问,代码块外“看不见”它)。为什么会这样?答案在于static修饰局部变量,改变了该变量的存储类(所以内存分配区域改变了,生命周期跟着改变了,表现形态也跟着改变了),由栈改变为全局数据区了。而static对其作用域属性毫无影响,它仍然是代码块作用域,因此仍然只能在代码块内访问,不能在外部访问。示例3:普通全局变量(函数)不能和工程内其他源文件中定义的全局变量(函数)重名,而静态全局变量(函数)却可以;普通全局变量(函数)可以被工程内其他源文件中的函数所引用(#include声明头文件或extern声明,详见第2章第1节),而静态全局变量(函数)不能被其他源文件中的函数引用。造成这一事实的主要原因是:C语言默认全局变量和函数链接属性为外连接(这个外字,可以理解为源文件外、工程内),而static修饰全局变量和函数,将其连接属性修改为内连接(文件内)。而存储类和生命周期等其他属性则不受影响。以上三个示例中,我们以局部变量、全局变量、函数、静态局部变量、静态全局变量、函数等C语言中最常用的一些变量类型为例,给大家演示了四种属性在C语言中的意义。想明白这些示例,大家就可以不用去管那些复杂的概念而直接明白个中道理,从而更好的使用C语言为我们提供的这些基础设施,搭建自己的代码大厦。不妨再多说点废话。大家有没发现:static这个关键字修饰局部变量和全局变量时意义相差极大(一个改变了存储类,一个改变了链接类型)。而且static这个英文单词的本意为静态的、静止的,这个和静态局部变量还有点意义关联(放在全局数据区是比放在栈中要“静”一些吧),但是和改变连接属性由外连接到内连接,这个确实很难找到意义关联。为什么?好像之前有在某本书上看过(也可能是自己脑补,不过即使是在书上看的,也有可能是那本书作者脑补···我一向坚持尽信书不如无书,不管从哪里来,大家只管自己品鉴有无道理),static关键字刚开始在C语言中本来只有修饰局部变量的用法(很合理),后来因为C语言工程变大,文件变多,默认外连接下文件间函数、变量重名问题太麻烦(这个问题后续的语言都是通过命名空间来解决的,如C++,Java,C#等。),于是乎需要一个新的关键字来将根本不需要在别的文件中引用的函数(内部函数)限制为内连接(这样只有极少数需要在外部访问的函数或变量才外连接,减少了重名风险)。本来你新建个关键字就完了(譬如internal,private这些都挺贴切的),但是当时大家都认为C语言的关键字已经挺多了,不想再随便添加(C++的关键字就比C语言多了好多),于是乎可怜的static被看中了(因为static本来的含义只能修饰局部变量,根本没有修饰全局变量和函数的用法),从此后编译器赋予了static可以修饰全局变量和函数的能力,并且有了另外的含义(今天看来,这或许不是一个好的解决方案,至少新建个关键字都比这好用点,或者引入命名空间的概念从根本上解决问题。但是···玩C的都是奔着大神、高手、geek去的,这点槛又算得了什么,让暴风雨来得更猛烈些吧!!!)。第三章操作符3.1、请写出下列代码的输出内容intmain(void){ inta,b,c,d; a=10; b=a++; c=++a; d=10*a++; printf("b,c,d:%d,%d,%d",b,c,d);}解答:输出为:b,c,d:10,12,120。深度解析:此类题目考察的是++符号的前缀(pre-fix)和后缀(post-fix)用法,最好的分析方法是逐行分析,把当前一句代码执行过后各个变量的值写出来,一直到最后一行。下面是我逐行分析的结果:1 inta,b,c,d; //a=?,b=?,c=?,d=?2 a=10; //a=10,b=?,c=?,d=?3 b=a++; //a=11,b=10,c=?,d=?4 c=++a; //a=12,b=10,c=12,d=?5 d=10*a++; //a=13,b=10,c=12,d=120第1行为局部变量定义。注意此处定义的局部变量并没有在定义的同时赋初值,因此这个变量的值此时为随机值。注释中值为?的地方即表示这些变量的值此时为不确定。第2行为普通的赋值操作,执行完成后变量a就有了确定的值。第3行就有点意思了。首先大家来考虑一个问题:b=a++中a先与左边的赋值运算符=结合,还是先与右边的++符号结合?要回答这个问题,依据便是附录1中列出的《C语言运算符表》。查表可知++优先级为2级,而=优先级为14级,所以++的优先级远远高于=,因此a应该先与++结合,然后整体再赋值给b,即相当于b=(a++);好的,那a++先运算,怎么运算呢?++运算符规定:当++被后缀使用时,先返回操作数本身,然后操作数自加1。因此赋值给b的值是自加1之前的a,a是在返回原来的a给b赋值之后才自加的。请注意造成这种结果的原因,不是=比++先执行(++比=优先级高,因此是++先执行的),而是因为++后缀式用法本身的特性决定的。很多人总是把这里跟优先级高低混起来,造成理解上的矛盾。a的值在第2行执行后为10,因此第三行中赋值给b的值是10,因此第三行执行完成后b=10,而a的值赋值后再自加1,因此执行完后a的值是11。第4行的解析方法和第3行相同。理解了后缀式++的分析前缀式也就不难理解了。同样的++先执行,再执行赋值操作。前缀式++运算规则为:先对操作数进行自加1,然后再返回自加后的值作为表达式返回值。第3行完成时a=11,此处先自加则a=12,整个表达式++a的值也为运算后的12,赋值给c后,c的值也为12。第4行结束。第5行,首先要清楚中间的*是乘法符号,而不是指针的解引用符。在C语言中这两个符号都是*,但是使用场合不同。简单来说,*后面的操作数如果是指针那*就是指针解引用,而*后面如果是数值类型(int,float等)则*为乘法运算符。接下来按部就班,查《C语言运算符表》可知,++运算符比乘法运算符优先级高(乘法为3级,而++为2级。有意思的是,*作为指针解引用时优先级也为2级,跟++相同。这个设置会带来一些很有意思的特性,特别是在strcpy等字符串库函数源码分析中会用到,此处暂且按下不表。),因此第5行相当于d=(10*(a++));接下来就好分析了,根据前文对++后缀的分析可知,a应该以原值参与运算,因此d=10*12=120,运算后a再自加,因此运算完后a=13。++符号(--与之类似)的问题是C/C++工程师笔试题目的常客,非常具有代表性,因此值得花时间深入搞明白。以上之外,还有几点需要说明一下:贪心法。贪心法用来分析诸如c=a+++++b;这样的表达式,对于这样的一串+相连的表达式应该怎么去区分呢?首先应该对该连续表达式进行“断句”处理。所谓断句,就是将其分为几部分。那怎么分呢?用贪心法。贪心法:一个表达式中,从左到右,每部分(在保持自身为合法表达式的前提下)总是想得到尽可能多的字符。譬如a+++++b中,首先应该断句出的是a++,因为从左边开始有意义的符号只有a、a++(a+、a+++以及以后的都不是C语言规定的有效符号了),而a++比a长,所以“贪心”的编译器总是从a++处断句的。再下来呢?因为(a++)++是非法的(参见3_1_operator++.c中line11),所以应该从第3个+处断句,最后剩下的++b大家应该一眼就看出它是一个有意义的表达式了。因此整句话断句为:c=a+++++b;它相当于是c=(a++)+(++b);实践中++的使用。实际上大家验证,c=a+++++b;这样的表达式编译是通不过的,也就是说C语言的编译器不接受这样的书写。所以上面的题目纯属“理论探索”,实际开发中是用不上的。但是断句后的书写编译是可以通过的(具体参看章节示例代码3_1_operator++.c)。在这里要强调的是:实际编码中不鼓励(甚至是深恶痛绝)这种编码风格,这种表达式是违背代码的可读性原则的。不使用这种风格,我们同样可以写出效率高、可读性强、干净漂亮的代码。真理就是:代码的复杂度应该是由代码所解决的问题本身的复杂度决定的,而不是由编码人员的编程风格决定的。譬如使用链表存储及操作一些学生信息,要比使用结构体数组来操作要复杂。这是由链表本身操作难度比数组要高决定的(当然复杂的自然有它的好处,不然谁不愿意简单活着)。如果一个程序员写的数组操作读起来比别人写的链表操作还要难读懂,那你觉得这个程序员的水平···是低呢?还是很低呢?如果这样一个人写的代码交给你来维护,你是不是有种想骂娘的冲动了···++跟在指针后面。简单变量(如int)的++比较简单,a++相当于是a+=1。这个是确定的,你完全不用担心哪一天它突然变成了a+=2或者a+=4了。++要是跟在指针后面(或者是前面)就不这么简单了。为此我们特意准备了一个示例程序,代码及运行结果见3_2_pointer++.c。从示例中可以看出,指针p进行++操作时,它的值实际增长多少不是固定的,而是跟指针类型有关。具体来说:int型的指针(int*p)在p++时,指针值实际增加4;short型指针(short*p)在p++时指针值实际增加2;char型指针(char*p)在p++时指针值实际增加1。为什么这样设计呢?从示例中可以看出,这样的设计正好保证了指针++之后指向了数组中下一个元素(请注意,不是下一个字节,而是下一个元素),所以这样的设计最实用。本来嘛,指针就是用来操作地址的。而地址并不都是单字节间隔的,譬如整形数据中,4个字节才是一个地址单元,此时指针加1指向的位置是一个整型数的中部,直接访问是一般是没有意义的。可以结合图3_2_1来理解。结合示例程序3_2_pointer++.c很容易看出,图中的0xbfac13xx是指针p的值,也就是一系列的内存地址,而12、34等等是内存单元中的值。图中用绿色和红色边框分别示出了p++(注意p的类型为int*p)和p=(int*)((char*)p+1)两种不同的操作下,操作前后p所指向内存单元的不同。结合该内存分布图,我相信大家应该很容易理解实验中那个奇怪的数字(570425344)是怎么来的了。3.2、写出floatx与“零值”比较的if语句.解答:if(x>=-0.000001&&x<=0.000001)或#defineEPSINON0.000001if(x>=-EPSINON&&x<=EPSINON)深度解析:C语言中的基本数据类型分数值类型与字符类型。字符类型即char,在所有机器中char型均占1个字节,字符类型一般用来存储一个ASCII码字符,因此称为字符型;数值类型用来存储数值(即所谓数字),按照数值本身的特点又分为整形和浮点型(有些书成为实型···我估计是从数学中的实数概念叫出来的。本来嘛,整数和非整数加在一起组成实数,浮点数就是用来描述小数的,而整数也可以看作是小数部分为0的小数,因此浮点数就对应数学中的实数,所以可以称为实型。),整形一般有short、int、long三种,浮点型一般有float和double两种。先看整形。首先要明白的是,在不同的CPU架构下,三种整形变量各自的定义(即其所占有的内存大小,所表示的数值范围)是不同的。因此,在讲这些数据类型前,一定要搞清楚自己所针对的CPU平台(再次强调,在未特别说明时,本书均以32位ARM体系为目标平台)。在32位体系中,short(也写作shortint,短整形)占2字节,int(整形)占4字节,long(也写作longint,长整形)占4字节。其有符号和无符号的情况下各自表示的数范围是多少大家可以自己计算下(其实关键的不是那个数字,而是要理解其分析方法)。有些人可能觉得longint有点名不符实:人家int就有4字节你也4字节凭什么叫long?还有些人在书上看到过“short和int占用内存是相同的”诸如此类的话,加起来的效果就是搞得人无所适从、晕头转向。其实造成这些困扰的主要原因就是因为大家在讲这些话时没搞清楚平台。就拿流传最广的IntelX86平台来讲吧,最开始的X86(80386以前)是16位的CPU,那时候short和int均都是16位(2字节)。可是后来的80386就变成了32位CPU了,那时候short仍然是16位,而int则变成了32位。还是那句话,在讲这些整型类型前先搞清楚自己针对的平台,这样就不会混乱了。(有些书和网络资料在讲到这些问题时,直接以自己的平台为例得出了结论,而没有指明是适用于特定平台的,造成了大家的困扰。)其实从上面X86平台16位到32位的迁移过程可以看出,C语言原生类型short、int、long这些是非常不靠谱的(一样的代码,今天还是2字节,明天就变成4字节了,让人情何以堪······)。所以很多专业的项目中一般都不推荐使用C语言原生类型,而都是使用typedef关键字,以原生类型来定义(准确的说叫重命名)一些新的类型以供使用,例如:typedefunsignedintu32;typedefsignedints32;typedefunsignedshortu16;typedefunsignedlongu32;typedefvolatileunsignedintvu32;以上的类型重定义简洁明了,可以使代码更加快捷清爽;而有些重命名的类型则暗示了它的用途,如linux中的size_t;另外有些软件项目甚至会在重命名时添加上自己的项目名称,以表示这是项目专用的类型定义。这样使用typedef重命名的专用类型,在CPU平台迁移后,只需要相应修改这些typedef语句即可,而不需要修改整个代码。与编程中使用宏定义有异曲同工之妙,这就是可移植性。再看浮点型,有单精度(float,注意C语言中没有Single关键字,单精度是float)和双精度(double)两种。其差别在于:float占用的内存要少于double(具体是几个字节也是平台相关的,暂不细究),相应的float所表示的数范围大小以及精度(精度指的是小数点后精确到第几位)也小于double。关于浮点型,要注意以下几点:浮点型都是有符号的,不存在无符号浮点型。所以千万不要写出unsignedfloat或unsigneddouble(这个错误倒是不太用担心,因为编译根本通不过,编译器会帮你找到这个错误并提示你修正的。)。浮点型在内存中的存储方式和整形不同。整形是直接以二进制方式存储在内存中,因此譬如对于145这个数字来说,其以char型或者short型或者int型在内存中存在时,内存映像是相同的;而浮点数则是以指数形式表示,即以有效数字和指令幂的方式来描述的(在内存中有一些位负责存储有效数字,另一些位负责存储指数值。这些位越多则表示的数范围越大,精度越高。)在32位系统中float占4字节,而double占8字节。因此double的数值范围和精度要高于float。浮点数对应数学中的小数。因此浮点型与0值比较时可以使用大于号,也可以使用小于号,但是不能使用等于号。这是因为在数学运算当中,真正的零值是很难得到的。而在
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 寒假安全知识教育
- 江苏省南通市海马安13校2024-2025学年八年级下学期3月月考生物学试题(含答案)
- CRRT在ICU的应用及护理
- 开票人员培训
- 培训基地答辩
- 墙板灌浆知识培训课件
- 中药饮片工作规范
- 《GBT 40417-2021电子特气 六氟丁二烯》全新解读
- 引用童话故事的数学知识
- 学校招生接待培训
- 2024年福建广电网络集团有限公司招聘笔试参考题库附带答案详解
- 《养老护理员》-课件:老年人权益保障法相关知识
- 医疗器械冷链(运输、贮存)管理指南
- 医美下店培训课件
- 髂动脉瘤的护理查房
- 语文堂教学中的小组合作学习
- 中小学职业生涯规划
- 门诊导医护理课件
- 第6课+欧洲的思想解放运动【中职专用】《世界历史》(高教版2023基础模块)
- 具有履行合同所必须的设备和专业技术能力声明函正规范本(通用版)
- 安全标准化与企业管理体系融合
评论
0/150
提交评论