编译原理之预编译处理
预编译处理是在编译器编译之前做的处理,预编译过程主要处理规则如下:
1、将所有的#define删除,并且展开所有的宏定义;
2、处理所有的条件编译指令,如“#if”,“ifdef”,“endif”等;
3、处理所有包含指令“#include”,将被包含的文件插入到该编译指令的位置;
4、删除所有注释“//”,“/**/”;
5、添加行号和文件名标识,方便编译时调试方便查找信息;
6、保留所有的#pragma编译器指令,因为编译器须要使用它们。
总的来说C语言提供的预编译功能主要有三种:
第一:宏定义
第二:文件包含
第三:条件编译
宏定义
宏定义分两种:不带参数和带参数的宏定义,宏定义总的来说就是符号之间简单的替换。
第一:不带参数宏定义
不带参数的宏定义比较好理解,就是用一个指定的标识符来代替一个字符串,一般形式为:
#define 标识符 字符串
例如:#define TODAY 20140701
这个宏定义就是简单的用TODAY代替20140701这个字符串,程序预编译之后,程序中所有出现TODAY的地方都是指20140701。
与#define命令相对应的 #undef命令,#undef命令是用来终止宏定义的作用域。通常这个命令编程时用的比较少。例如:
#define MCU 8051
Main()
{
`````````````````;
}
#undef MCU
Void Metal()
{;}
// #undef MCU 命令以上部分程序出现的MCU都是指定为8051,但是如果#undef MCU 命令下面程序出现MCU,编译器编译时会报错,提示MCU没定义。例如,Metal()函数中出现MCU就会报错。
第二:带参数宏定义
带参数的宏定义的一般形式为:
#define 宏名(参数表) 字符串
如:
#define S(a,b) a*b
temp = S(2,3);这句语句展开为:temp = 2*3;
宏定义要注意的地方:
1、宏定义#define是预编译指令而不是语句,所以define紧跟着的字符串后面不能加分号“;”, 否则,分号也属于字符串的内容。例如:#define TODAY 20140701;则这里的字符串内容是20140701;而不是20140701。
2、宏定义#define命令式用宏名代替一个字符串,就是一个简单的置换,不做任何的词法、语法的判断和计算,不会提示错误信息。
3、带参数宏定义时,宏名与带参数的括号之间不能加空格。否则将空格以后的字符都作为代替字符串的一部分,而变成不带参数的宏定义。
4、宏定义中如果参数表的实参是表达式时就要注意。如:#define S(r) PI*r*r
则S(a) 在程序中的展开式为PI*a*a,这个很好理解;但是S(a+b)展开式为PI*a+b*a+b;显然这不是我们所想得到的结果。如想到PI*(a+b)*(a+b)这个结果,那么宏定义应为:
#define S(r) PI*(r)*(r) 。这也体现了宏定义只是简单愚蠢直接替代功能。
带参数宏定义与函数的区别:
1、函数条用时,线求出实参表达式的值,然后代入形参;而宏定义只是简单的字符替换,不计算。
2、函数条用是在程序运行处理的,为形参分配临时内存单元;而宏展开式在编译前进行的,在展开时不分配内存单元,不进行值的传递。
3、函数的形参和实参的类型有要求,都要相同;宏不存在类型问题,宏的参数只是一个字符代表,展开时只是简单的替换。
4、调用函数只可得到一个返回值,而用宏可以设法得到几个结果。
例如:#define several(a,b,c,d) b = 2*a;c = 3*b;d = 4*c
5、宏替换不占运行时间,只占据编译的时间;而函数调用则占用运行时间。
文件包含
“文件包含”处理是指一个源文件可以将另外一个源文件的全部内容包含进来,即将另外的文件包含本文件中。其一般形式为:
#include“文件名”或者#include <文件名>
这两种的区别只是寻找包含文件的路径不同而已。用<文件名>形式时,系统到存放C库函数头文件所在的目录中寻找要包含的文件,这称为标准方式。用“文件名”方式时,系统线在用户当前目录中寻找包含的文件,若找不到,再按照标准方式寻找。所以调用库函数时一般用<文件名>方式来包含,可以节省寻找时间。如果是用户自己编写的文件,一般用“文件名 ”形式。
示意图表示在编译预处理时,要对#include命令进行文件包含处理,即把file2.c文件的全部内容复制插入到#include“file2.c”命令处,处理后得到的结果如第三个图。在编译中将包含以后的file1.c作为一个源文件单位进行编译。文件包含命令作用很大,在编程时可以节省很多时间,具体就不进行举例。在编程中慢慢体会。
文件包含跟文件链接不同,文件包含只是把该包含的文件插入到包含指令位置,连成一起然后进行编译。文件包含是编译之前进行的,而文件链接是编译之后进行。这种常用在文件头部的被包含的文件称为“标题文件”或者“头部文件”,常以“.h”为后缀,如“stdio.h”。
文件包含命令细节说明:
1、 一个#include命令只能指定一个被包含文件,即一对一包含。
2、 在一个被包含文件中又可以包含另一个被包含的文件,即文件包含是可以嵌套的。
3、 被包含文件(file2.h)与其所在的文件(file1.h),在预编译后已成为同一个文件而不是两个文件。所以如果file2.h中有全局静态变量,它也在file1.c文件中有效,不必用extern声明。
需要注意一点,如果一个被包含文件中有地方修改后,凡是包含此文件的所有文件都要全部重新编译。这点大家可以自己编写程序验证,文件编译时通过看编译信息可以知道。
条件编译
编写程序时如果对其中一些代码只在满足一定条件时才进行编译,这就是“条件编译”。
条件编译有一下几种形式:
第一种:
#ifdef 标识符
程序段1
#else
程序段2
#endif
这种形式条件编译的意思是,如果ifdef后面的标识符已经被#define命令定义过,,则在编译器编译程序段1,否则编译程序段2。其实#else部分可以没有,即
#ifdef 标识符
程序段
#endif
这种条件编译的意思是,如果ifdef后面的标识符已经被#define命令定义过,,则在编译器编译程序段,否则程序段不加以编译。
程序段可以是语句组也可以是命令行,这中条件编译可以提高C语言的源程序的通用性。例如,一个C程序在不同位数处理器的计算机中运行(一个16位存放一个整数,一个是32位存放整数),这样往往需要对源程序作必要的修改,这样C语言程序的通用性比较低。但是我们可以使用条件编译来解决这个问题。例如;
#ifdef COMPUTER
#defineINTEGER_SIZE 16
#else
#defineINTEGER_SIZE 32
#endif
这样如果是16位处理器,则宏定COMPUTER即可,编译器编译#define INTEGER_SIZE 16
否则处理器为32位,编译器编译#define INTEGER_SIZE 32 。
还有这种条件编译比较灵活,不用加注释或者删除代码就可以随便进行选择代码参与编译。
第二种:
#ifndef 标识符
程序段
#endif
这种条件编译和第一种刚好相反,即若标识符未被定义过,则编译程序段,否则不编译。这种条件编译在处理头文件时经常用到,即文件被多个文件包含时使用这种形式可以避免重复编译。具体不举例,自己编程体验。
第三种:
#if 表达式
程序段1
#else
程序段2
#endif
这种条件编译的意思是:当表达式的值为真(非零)时编译程序段1,否则编译程序段2。可以事先给定一个条件,使程序在不同的条件下执行不同的功能。