《第20章编译及预处理.ppt》由会员分享,可在线阅读,更多相关《第20章编译及预处理.ppt(20页珍藏版)》请在淘文阁 - 分享文档赚钱的网站上搜索。
1、第20章 编译及预处理如果一个源程序由多个诸如A.c,B.h等的源文件组成,使用的编译链接器是如何根据这些文件生成可执行文件的?编译链接的机理到底是什么,这是本章要学习的内容。对C语言来说,除了掌握必要的语法机制外,学好预处理命令也是写出高质量代码的前提。20.1 编译流程本书前面给出了很多示例代码,实际上,哪怕是像Hello,World这样简单的示例程序,都要经过编辑、预处理、编译、链接4个步骤,才能变成可执行程序,鼠标双击就弹出命令窗口,显示“Hello,World”。这也是一般C语言程序的编译流程,如所示。20.1.1 编辑编辑可能就是通常所说的“写代码”,用集成开发工具也好,用记事本也
2、好,按C语言的语法规则组织一系列的源文件,主要有两种形式,一种是.c文件,另一种是.h文件,也称头文件。20.1.2 预处理前面接触到的“#include”和“#define”都属于编译预处理,C语言允许在程序中用预处理指令写一些命令行。预处理器在编译器之前根据指令更改程序文本。编译器看到的是预处理器修改过的代码文本,C语言的编译预处理功能主要包括宏定义、文件包含和条件编译3种。预处理器对宏进行替换,并将所包含的头文件整体插入源文件中,为后面要进行的编译做好准备。20.1.3 编译编译器处理的对象是由单个c文件和其中递归包含的头文件组成的编译单元,一般来说,头文件是不直接参加编译的。编译器会将
3、每个编译单元翻译成同名的二进制代码文件,在DOS和Windows环境下,二进制代码文件的后缀名为.obj,在Unix环境下,其后缀名为.o,此时,二进制代码文件是零散的,还不是可执行二进制文件。错误检查大多是在编译阶段进行的,编译器主要进行语法分析,词法分析,产生目标代码并进行代码优化等处理。为全局变量和静态变量等分配内存,并检查函数是否已定义,如没有定义,是否有函数声明。函数声明通知编译器:该函数在本文件晚些时候定义,或者是在其他文件中定义。20.1.4 链接链接器将编译得到的零散的二进制代码文件组合成二进制可执行文件,主要完成下述两个工作,一是解析其他文件中函数引用或其他引用,二是解析库函
4、数。举例来说,某个程序由两个c文件组成,分别为A.c、B.c,两个c文件和其中递归包含的头文件组成的两个编译单元,经过预处理和编译生成二进制代码文件A.obj和B.obj,假设A.c中调用了函数C,可函数C定义在B.c中,A.obj中实际上仅仅包括着对C函数的引用,其二进制定义代码需要从B.obj中提取,插入到A.obj的调用处,这个过程称为函数解析(resolve),由链接器完成。不仅仅是函数,变量(诸如有外部链接性的全局变量)也牵扯到解析的问题。当B.c没有定义函数C时,编译时不会产生错误,但链接时却会提示,有未解析的对象,据此可分析出问题出在编译阶段还是链接阶段。20.2 程序错误兴致勃
5、勃地写完程序,编译链接,一大堆的错误提示,不要沮丧,再优秀的程序员也会犯错,有人说,程序编写的过程大部分的时间都是用在错误调试上。有时为了排除一个小问题,可能会几天几夜地跟踪代码,正因为如此,有人把问题找到并解决的刹那称为“痛苦的幸福”。继续说明程序错误前,有个观点要说明:没有完美的程序,不存在没有缺陷的程序,如果一个程序运行很完美,那是因为它的缺陷到现在还没有被发现。同样,软件测试是为了发现程序中可能存在的问题,而不是证明程序没有错误。20.2.1 错误分类错误可分两大类,一是程序书写形式在某些方面不合C语言要求,称为语法错误,这种错误将会由编译器指明,是种比较容易修改的错误,二是程序书写本
6、身没错,编译链接能够完成,但输出结果与预期不符,或着执行着便崩溃掉,称为逻辑错误。细分下去,语法错误又可分为编译错误和链接错误,很明显,编译错误就是在程序编译阶段出的错误,而链接错误就是在程序链接阶段出的问题。20.2.2 编译错误如果文件中出现编译错误,编译器将给出错误信息,并指明错误所在的行,提示用户修改代码,编译错误主要有两类:(1)语法问题,缺少符号,如缺分号,缺括号等,符号拼写不正确,一般来说,编译器都会指明错误所在行,但由于代码是彼此联系的,有时编译器给出的信息未必正确。一般来说,源程序中出错位置要么就是编译器提示位置,要么在提示位置之前,甚至是在前面很远的地方。另一个问题是有时一
7、个实际错误会让编译器给出很多出错提示,所以,面对成百上千个错误提示时,不要害怕,没准修改一处代码,所有的问题都解决了。(2)上下文关系有误,程序设计中有很多彼此关联的东西,比如变量要先创建再使用,有时编译器会发现某个变量尚未定义,便会提示出错。这种情况有时是因为变量名拼写有误,有时是因为确实忘了定义。除了错误外,编译器还会对程序中一些不合理的用法进行警告(warning),尽管警告不耽误程序编译链接,但对警告信息不能掉以轻心,警告常常预示着隐藏很深的错误,特别是逻辑错误,应当仔细排查。20.2.3 链接错误当一个编译单元中调用了库函数或定义在其他编译单元中的函数时,在链接阶段就需要从库文件或其
8、他目标文件中抽取该函数的二进制代码,以便进行组合等一系列工作,当函数名书写错误时,链接器无法找到该函数对应的代码,便会提示出错,指出名字未解析(unresolved)。一般来说,链接器给出的错误提示信息是关乎函数名、变量名等的。20.2.4 逻辑错误即使程序顺利通过了编译链接,也不是说万事大吉,可以收工了,要检查生成的可执行程序,看其是否实现了所需的功能。实际上,运行阶段出现的逻辑错误更难排查,更让人头疼,编译错误和链接错误好歹有提示信息,但面对逻辑错误,就像浑水摸鱼。可能出现的逻辑错误有以下情况:与操作系统有关的操作,是否进行了非法操作,如非法内存访问等。是否出现了死循环,表现为长时间无反应
9、,假死,注意,长时间无反应并不一定都是死循环,有的程序确实需要很长时间,这种情况要仔细分析。程序执行期间发生了一些异常,比如除数为0等,操作无法继续进行。程序能正确执行,但结果不对,此时应检查代码的编写是否合乎问题规范。20.2.5 排错排除错误,有两层含义,找到出错的代码,修改该代码。排错也有两种形式,一是静态排错,编译器和链接器发现的错误基本都属于这一类,通过观察源程序便能确定问题所在并改正它。另一种是动态排错,逻辑错误的发现和纠正都比较困难,要综合考虑代码、使用的数据和输出结果的关联,仔细思考,尝试更换数据,观察结果的改变,依此分析错误可能存在的地方。如果还是不行,就要使用动态检查机制,
10、最基本的方法是“分而治之”,检查程序执行的中间状态,最常用的方法是在可能出错的地方插入一些输出语句,让程序输出一些中间变量的值,确定可能出错的区域。此外,还可利用编译环境提供的DEBUG工具,对程序进行跟踪、监视和设断点等,定位并排错,这在第2章中已经讨论过。20.3 预处理命令之宏定义预处理命令的引入是为了优化程序设计环境,提高编程效率,合理使用预处理命令能使编写的程序易于阅读、修改、移植和调试,也有利于程序的模块化设计。预处理命令必须独占一行,并以#开头,末尾不加分号,以示与普通C语句的区别。原则上,预处理行可以写在程序的任何位置,但推荐(或是惯常写法)写在程序文件的头部。编译器在对文件进
11、行实质性的编译之前,先处理这些预处理行,这也是“预”字的含义。C语言的编译预处理功能主要包括宏定义、文件包含和条件编译3种。20.3.1 宏定义读者对宏已经不再陌生,在字符常量一节中,已经介绍过#define的用法,宏即是用#define语句定义的。宏定义是用宏名来表示一个字符串,在编译预处理时,对程序中所有出现的宏名,都用宏定义中的字符串来替换,称为“宏代换”或“宏展开”。宏展开只是种简单的代换,字符串中可以包含任何字符,可以是常数,也可以是表达式,预处理器进行宏展开时并不进行语法检查。20.3.2 不带参的宏定义不带参数宏的一般定义形式为:#define 宏名 宏体例如:#define X
12、 100#define PI 3.14159265宏名的命名规则与变量相同,一般习惯用大写字母,以便与变量区分,当然,这并不是说不允许使用小写字母作为宏名。来看一个不带参数宏的示例,从中学习一些宏的用法:20.3.3 带参的宏定义和函数一样,宏也可以带参数,宏定义中的参数称为形参,宏调用时的参数称为实参。宏调用时,不仅要进行宏展开,而且要用实参代替形参。带参宏定义的一般格式为:#define 宏名(参数表)宏体注意:宏名与参数表之间不能有空格出现,否则,预处理器会将宏名当作不带参数的宏来对待。因为是字面替换,因此,参数表中不需指明参数的数据类型,宏调用的基本格式为:宏名(实参表)宏调用时,要求
13、实参个数与形参个数相同,但没有类型要求,来看一个简单的示例,计算两个数的乘积:20.3.4#define定义常量与宏常量#define定义的常量称为符号常量,而const常量常称静态常量,相比#define,const有很多优势,在实际使用时,推荐采用const定义常量。对#define定义的符号常量,预处理器只是进行简单的字符串替换,并不对其进行类型检查,而且会与程序中定义的同名变量冲突,如:#define num 10;void disp()int num;coutnum;编译时,预处理器会将disp函数中的语句“int num;”替换成“int 10”,若使用“const int num
14、=10;”便不会出现这种问题。20.3.5 文件包含文件包含是C语言预处理的另一个重要功能,用“#include”来实现,将一个源文件的全部内容包含到另一个源文件中,成为它的一个部分,文件包含的一般格式为:#include 或者#include“文件名”两种形式的区别在于:使用尖括号表示在系统头文件目录中查找(由用户在设置编程环境时设置),而不在源文件目录中查找。使用双引号则表示首先在当前的源文件目录中查找,找不到再到系统头文件目录中查找。#include“文件名”格式下,用户可以显式指明文件的位置,如:#include“D:ABC.h”#include“.X.h”/*上级目录下的X.h文件*
15、/一个include命令智能包含一个文件,如果有多个文件要包含,则要使用多个include命令,不允许写成下述形式:#include 20.3.6 条件编译通过某些条件,控制源程序中的某段源代码是否参加编译,这就是条件编译的功能,一般来说,所有源文件中的代码都应参加编译,但有时候希望某部分代码不参加编译,应用条件编译可达到这以目的。条件编译的基本形式为:#if 判断表达式 语句段1#else 语句段2#endif或#if 判断表达式 语句段1#endif20.4 小结本章讨论了C语言程序编译及预处理的相关内容,C程序的编译分编辑、预处理、编译和链接几个步骤,预处理指令是由预处理器负责执行的,主要有头文件包含、宏定义、条件编译等,经过预处理后,编译器才开始工作,将每个编译单元编译成二进制代码文件,但此时分散的二进制代码文件中的变量和函数没有分配到具体内存地址,因而不能执行,需要链接器将这些二进制代码文件、用到的库文件中相关代码,系统相关的信息组合起来,形成二进制可执行文件。掌握程序编译链接的过程能帮助理解错误的根源,提高调试的效率,是写出高质量代码的必要条件。
限制150内