编译C程序有很多步骤,其中第一步为预处理(preprocessing)阶段
扫描二维码
随时随地手机看文章
一.前言
1.编译一个C程序涉及很多步骤。其中第一步骤称为预处理(preprocessing)阶段。C预处理器(preprocessor)在源代码编译之前对其进行文本性质的操作。
2.它的主要任务包括删除注释、插入被#include指令包含的内容、定义和替换由#define指令定义的符号以及确定代码的部分内容是否应该根据一些条件编译指令经行编译。
二.预定义符号
1.以下为预处理器定义的符号。它们的值或者是字符串常量,或者是十进制数字常量。
2.__FILE__和__LINE__在确认调试输出时很有用。__DATE__和__TIME__常常用于在被编译的程序中加入版本信息。
3.__STDC__用于那些在ANSI环境和非ANSI环境都必须进行编译的程序中结合条件编译。
注:
此处的前缀是两个下划线.
² __FILE__:用%s进行输出,输出结果为源程序名。
² __LINE__:用%d进行输出,输出结果为文件当前行号。
² __DATE__:用%s进行输出,输出结果为文件被编译的日期
² __STDC__:用%d进行输出,如果编译器遵循ANSIC,其数值为1。否则未定义。
三.#define
1.#define的用法:
#define name stuff
有了这条指令以后,每当有符号name出现在这条指令后面时,预处理器就会把它替换成stuff。
2.替换文本并不仅限于数值字面值常量。使用#define指令,可以把文本替换到程序中。
3.如果定义中的stuff非常长,可以将其分成几行,除了最后一行之外,每行的末尾都要加一个反斜杠。
Eg:
#define DEBUG_PRINT printf(“File %s line%d:” \
”x=%d,y=%d,z=%d”,\
__FILE__,__LINE__,\
x,y,z)
说明:此处利用了相邻的字符串常量被自动连接为一个字符串的这个特性。
4.在宏定义的末尾不要加上分号。如果加了则会出现一条空语句。
Eg:
DEBUG_PRINT;
此时,编译器替换后会都一条空语句.
1>有时候只允许出现一条语句,如果放入两条语句就会出现问题
Eg:
if(…)
DEBUG_PRINT;
else
…..
四.宏
1.#define机制包括了一个规定,只允许把参数替换到文本中,这种实现通常称为宏或定义宏(defined macro)
2.宏的声明方式:
#define name(parament-list) stuff
1>其中,parament-list(参数列表)是一个由逗号分隔的符号列表,它们可能出现在stuff中。参数列表的左括号必须与name紧邻。如果两者之间有任何空白存在,参数列表就会解释为stuff的一部分。
2>当宏被调用时,名字后面是一个由逗号分隔的列表,每个值都与宏定义中的一个参数相对应,整个列表用一对括号包围。但参数出现在程序中时,与每个参数对应的实际值都将被替换到stuff中。
eg:
#define SQUARE(x) ( (x)*(x))
如果在上述声明之后,调用
SQUARE(5)
预处理器就会用用下面这个表达式进行替换:
5*5。
说明:
在完整定义的参数宏中要加上括号,并且对宏定义中每个参数的两边也加上括号
3.#define替换
在程序中扩展#define定义符号和宏时,需要涉及几个步骤
1>在调用宏时,首先对参数进行检查,看看是否包含了任何由#define定义的符号。如果是,它们首先被替换
2>替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被它们的值所替代
3>最后,再次对结果文本进行扫描,看看它是否包含了任何由#define定义的符号。如果是,就崇光伏上述处理过程。
因此,宏参数和#define定义可以包含其他#define定义的符号。但是宏不可以出现递归。
说明:
1.当预处理器搜索#define定义的符号时,字符串常量的内容并不进行检查。如果想要把宏参数插入到字符串常量中,可以使用如下方法:
1>使用邻近字符串自动连接的特性,把一个字符串分成几段,每段实际上都是一个宏参数。
eg:
#include
#define PRINT(FORMAT,VALUE) \
printf(“thevalue is “ FORMAT “\n”,VALUE)
int main()
{
int x= 12;
PRINT(“%d”,x+3);
}
说明:
此技巧只有字符串常量作为宏参数给出时才能使用。
2>第二个技巧使用预处理器把一个宏参数转换为一个字符串。#argument这种结构被预处理器翻译为”argument”.
eg:
#define PRINT(FORMAT,VALUE) \
printf(“thevalue of #VALUE \
“ is “ FORMAT “\n”,VALUE)
int main()
{
int x= 12;
PRINT(“%d”,x+3);
}
输出结果为:
the value of x+3 is 15
3>## 结构则执行一种不同的任务。它把位于它两边的符号连接成一个符号。作为用途之一,它允许宏定义从分离的文本片段创建标识符。
下面的实例使用这种连接把一个值添加到几个变量之一:
#define ADD_TO_SUM(sum_number,value) \
sum ## sum_number += value
….
ADD_TO_SUM(5,25);
最后一条语句把值25加到变量sum5上。注意这种连接必须产生一个合法的标识符。否则,其结果就是未定的。
五.宏与函数
1.宏非常频繁地用于执行简单的计算,比如在两个表达式中寻找其中较大或较小的一个:
可以用:
#define MAX(a,b) ((a) > (b) ? (a) : (b) )
2此处不用函数的原因是:
1>首先用于调用和从函数返回的代码很可能比实际执行这个小型计算工作的代码更大
2>函数的参数必须声明为一种特定的类型,所以它只能在类型合适的表达式上使用。因此,上面的宏可以用于整型、长整型、单浮点型、双浮点型以及其他类型中。既:宏是与类型无关的。
3>使用宏的不好之处在于,一份宏定义代码的拷贝都将插入到程序中。除非宏非常短,否则使用宏可能会大幅度增加程序的长度。
4>还有一些任务根本无法用函数实现
Eg:#define MALLOC(n,type) \
((type *)malloc( (n)*sizeof(type) ) )
此宏中的第二个参数是一种类型,它无法作为函数参数进行传递。
5>宏参数具有副作用。
3.宏与函数的区别
1>代码长度:
² #define宏:每次使用时,宏代码都被插入到程序中。除了非常小的宏之外,程序的长度将大幅度增加
² 函数:函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码
2>执行速度:
² #define宏:更块
² 函数: 存在函数调用/返回的额外开销
3>操作符优先级
² #define宏参数的求值是在所有周围表达式的上下文环境里,除非它们加上括号,否则邻近操作符的优先级可能会产生不可预料的结果
² 函数:函数参数只在函数调用时求值一次,它的结果传递给函数。表达式的求值结果更容易预测。
4>参数求值
² #define宏:参数每次用于宏定义时,它们都将重新求值。由于多次求值,具有副作用的参数可能会产生不可预料的结果。
² 函数:参数在函数被调用前只求值一次。在函数中多次使用参数并不会导致多种求值过程。参数的副作用并不会造成任何特殊的问题。
5>参数类型
² #define宏:宏与类型无关。只要对参数的操作是合法的,它可以适用于任何参数类型
² 函数:函数的参数是与类型有关的。如果参数的类型不同,就需要使用不同的函数,即使它们执行的任务是相同的。
六.#undef
1.该预处理指令用于移除一个宏定义
#undef name
2.如果一个现存的名字需要被重新定义,那么它的旧定义首先必须用#undef移除。
七.命令行定义
1.许多C编译器都可以实现:允许在命令行中定义符号,用于启动编译过程。当同一个源文件被编译成一个程序的不同版本时,该特性很有用。
Eg:假定某个程序声明了一个某种长度的数组。如果某个机器的内存很有限,这个数组必须很小,但在另一个内存很多的机器上,可能希望数组能够大写。
1>定义数组为:
Int array[ARRAY_SIZE];
那么我们希望ARRY_SIZE在命令行中定义。
例如:
gcc -DARRY_SIZE=100 tiger.c。
即可实现在命令行中指定数组的大小为100。
2.在Linux编译器中,使用-D选项来完成该功能。
可以用两种方式使用该选项:
Ø -Dname
Ø -Dname=stuff
说明
1>此处的name即为程序中的标量名。
2>第一种形式定义了符号name,它的数值为。也可以用于条件编译中
3>第二中形式把该符号的值定义为等号后面的stuff。
3.提供符号命令行定义的编辑器通常也提供在命令行中去除符号的定义。在Linux编译器上,-U选项用于执行这项任务。指定-Uname将导致程序中符号name的定义被忽略。当它与条件编译结合使用时,该特性很有用。
八.条件编译
1.在编译一个程序时,如果可以选择某条语句或某组语句进行翻译或者被忽略,常常显得很方便。用于调试程序的语句就是一个明显的例子。它门不应该出现在程序的产品版本中,但是,如果以后做一些维护性修改时,又可能需要重新调试该语句。因此就需要条件编译。
2.条件编译(conditional compilation)用于实现该目的。使用条件编译,可以选择代码的一部分是被正常编译还是完全忽略。
3.用于支持条件编译的基本结构是#if指令和其匹配的#endif指令。
#if constant-expression
statements
#endif
1>其中constant-expression(常量表达式)由预处理器进行求值。如果它的值是非0值(真),那么statements部分被正常编译,否则预处理器就安静地删除它们。
2>所谓常量表达式,就是说它或者是字面值常量,或者是一个由#define定义的符号。如果变量在执行期间无法获得它们的值,那么它们如果出现在常量表达式中就是一非法的。因为它们的数值在编译时是不可预测的。
Eg:
Ø 将所有的调试代码都以下面的形式出现
#if DEBUG
printf(“x=%d ,y=%d\n”,x,y);
#endif
1>如果我们想编译这个代码,可以用下面的代码实现
#define DEBUG 1
2>如果想忽略,则只要把这个符号定义为0就可以了。
Ø 条件编译的另一个用途是在编译时选择不同的代码部分。为了支持该功能,#if指令还具有可选的#elif和#else字句。
1>语法功能是:
#if constant-expression
statements
#elif constant-expriession
other statements ….
#else
other statements
#endif
#elif字句出现的次数可以不限。每个constant-expression(常量表达式)只有当前面所有常量表达式的值都为假时才会被编译。#else子句中的语句只有当前面所有的常量表达式值都为假时才会被编译,在其他情况下它都会被编译。
4.是否被定义
1>测试一个符号石佛已经被定义是可能的。在条件编译中完成这个任务往往更为方便,因为程序如果并不需要控制编译的符号所控制的特性,它就不需要被定义。
Eg:
if defined(symbol)
#ifdef symbol
九.文件包含
1.函数库文件包含两种不同类型的#include文件包含:函数库文件和本地文件。
1>函数库头文件
Ø 函数库头文件使用的语法
#include
Ø 对于fiename,并不存在任何限制,标准库文件以一个.h后缀结尾。编译器在标准位置处查找函数头文件
Ø 编译器通过观察由编译器定义的“一系列标准位置”查找函数库头文件。在编译器的文档中应该说明这些位置是什么,以及怎样修改它们或者在列表中添加其他位置。
Ø Eg:在Linux系统上的C编译器在/user/include目录查找函数库头文件,该编译器有一个命令行选项,允许把其他目录添加到这个列表中,这样就可以创建自己的头文件函数库。
2>本地文件包含
Ø 语法格式:
#include “filename”
Ø 标准允许编译器自行决定是否把本地形式的#include和函数库形式的#include区别对待。
Ø 可以对本地头文件先使用一种特殊的处理方式,如果失败,编译器再按照函数库头文件的处理方式对它们进行处理。
Ø 处理本地头文件的一种常见策略就是在源文件所在的当前目录进行查找,如果该头文件并未找到,编译器就像查找函数库头文件一样在标准位置查找本地头文件。
Ø 可以在所有的#include语句中使用双括号而不是尖括号。但是,使用这种方法,有些编译器在查找函数库头文件时可能会浪费时间。
2.对函数库头文件使用尖括号的另一个较好的理由是可以给人们提示为函数头文件而不是本地文件。
3.UNIX系统和Borland C编译器也支持使用绝对路径名(absolute pathname),它不仅指定文件的名字,而且指定了文件的位置。
1>UNIX系统中的绝对路径名是以一个斜杠头开头,如下所示:
Eg:/home/fred/c/my_proj/declaration2.h
2>在MS-DOS系统中,它所使用的是反斜杠而不是斜杠。
3>如果一个绝对路径名出现在任何一种形式的#include,那么正常的目录查找就被跳过,因为这个路径名指定了头文件的位置。
4.嵌套文件包含
1>嵌套#include文件的一个不利之处在于它使得很难判断源文件之间的真正依赖关系。
2>嵌套#include文件的另一个不利之处在于一个头文件可能会被多次包含。
3>多重包含在绝大多数情况下出现于大型程序中,它往往需要使用很多头文件,因此要发现这种情况并不容易。要解决这个问题,可以使用条件编译,这样编写:
#ifndef _HEADERNAME_H
#define _HEADERNAME_H 1
/*
**All the stuff thatyou want in the header file
*/
#endif
那么,多重包含的危险就被消除了。当头文件第1次被包含时,它被正常处理,符号_HEADERNAME_H 被定义为1。如果头文件被再次包含,通过条件编译,它的所有内容被忽略。符号_HEADERNAME_H 按照被包含文件的文件名进行取名,以避免由于头文件使用相同的符号而引起的冲突。
说明:
前面的例子也可以改为
#define _HEADERNAME_H
使用该条语句,与前面的#define _HEADNAME_H 1效果是等同的。
说明:
1.当头文件被包含时,位于头文件内所有内容都要被编译。因此,每个头文件只应该包含一组函数或数据的声明。
2.使用几个头文件,每个头文件包含用于某个特定函数或模块的声明的做法会更好一些。
3.只把必要的声明包含于一个文件中,这样文件中的语句就不会意外的访问应该属于私有的函数或变量。
总结:
1.#argument结构由预处理器转换为字符串常量”argument”.
2.##操作符用于把它两边的文本粘切成同一个标识符。
3.有些任何既可以用宏也可以用函数实现。但是宏与类型无关。宏的执行速度快于函数,因为它存在函数调用/返回的开销。但是,使用宏通常会增加程序的长度,但函数确不会。
4.#include指令用于实现文件包含。它具有两种形式。
Ø 如果文件名位于一对尖括号中,编译器将在由编译器定义的标准位置查找这个文件,这种形式通常用于包含函数库头文件。
Ø 另一种形式,文件名出现在一对双括号内。不同的编译器可以用不同的方式处理这种形式。但是,如果用于处理本地头文件的任何特殊处理方式无法找到这个头文件,那么编译器接下来就使用标准查找过程来寻找它。这种形式通常用于包含自己编写的头文件。
5.文件包含可以嵌套,但很少需要进行超过一层或两层的文件包含嵌套。嵌套的包含文件将会增加多次包含同一个文件的危险,而且很难以确定某个特定的源文件依赖的究竟是那个头文件。
6.不要在一个宏定义的末尾加上分号,使其成为一条完整的语句。
7.头文件只应该包含一组函数函数和(或)数据的声明
8.把不同集合的声明分离到不同的头文件中可以改善信息隐蔽