一.预处理的工作方式
1.1.预处理的功能
在集成开发环境中,编译,链接是同时完成的。其实,C语言编译器在对源代码编译之前,还需要进一步的处理:预编译。
所以,完整的步骤是:预编译 -> 编译 -> 链接
预编译的主要作用如下:
1
2
3
|
1 .将源文件中以”include”格式包含的文件复制到编译的源文件中。
2 .用实际值替换用“#define”定义的字符串。
3 .根据“# if ”后面的条件决定需要编译的代码。
|
1.2预处理的工作方式
预处理的行为是由指令控制的。这些指令是由#字符开头的一些命令。
#define指令定义了一个宏---用来代表其他东西的一个命令,通常是某一个类型的常量。预处理会通过将宏的名字和它的定义存储在一起来响应#define指令。当这个宏在后面的程序中使用到时,预处理器”扩展”了宏,将宏替换为它所定义的值。例如:下面这行命令:
1
|
#define PI 3.141592654
|
#include指令告诉预处理器打开一个特定的文件,将它的内容作为正在编译的文件的一部分“包含”进来。例如:下面这行命令:
1
|
#include
|
指示预处理器打开一个名字为stdio.h的文件,并将它的内容加到当前的程序中。
预处理器的输入是一个C语言程序,程序可能包含指令。预处理器会执行这些指令,并在处理过程中删除这些指令。预处理器的输出是另外一个程序:原程序的一个编辑后的版本,不再包含指令。预处理器的输出被直接交给编译器,编译器检查程序是否有错误,并经程序翻译为目标代码。
二.预处理指令
2.1.预处理指令
大多数预处理器指令属于下面3种类型:
1
2
3
|
宏定义:#define 指令定义一个宏,#undef指令删除一个宏定义。
文件包含:#include指令导致一个指定文件的内容被包含到程序中。
条件编译:# if ,#ifdef,#ifndef,#elif,# else 和#dendif指令可以根据编译器可以测试的条件来将一段文本包含到程序中或排除在程序之外。
|
剩下的#error,#line和#pragma指令更特殊的指令,较少用到。
2.2.指令规则
1
2
3
4
5
|
指令都是以#开始。#符号不需要在一行的行首,只要她之前有空白字符就行。在#后是指令名,接着是指令所需要的其他信息。
在指令的符号之间可以插入任意数量的空格或横向制表符。
指令总是第一个换行符处结束,除非明确地指明要继续。
指令可以出现在程序中任何地方。我们通常将#define和#include指令放在文件的开始,其他指令则放在后面,甚至在函数定义的中间。
注释可以与指令放在同一行。
|
三.宏定义命令----#define
使用#define命令并不是真正的定义符号常量,而是定义一个可以替换的宏。被定义为宏的标示符称为“宏名”。在编译预处理过程时,对程序中所有出现的“宏名”,都用宏定义中的字符串去代换,这称为“宏代换”或“宏展开”。
在C语言中,宏分为有参数和无参数两种。
3.1.无参数的宏
其定义格式如下:
1
|
#define 宏名 字符串
|
在以上宏定义语句中,各部分的含义如下:
1
2
3
4
|
#:表示这是一条预处理命令(凡是以“#”开始的均为预处理命令)。
define:关键字“define”为宏定义命令。
宏名:是一个标示符,必须符合C语言标示符的规定,一般以大写字母标示宏名。
字符串:可以是常数,表达式,格式串等。在前面使用的符号常量的定义就是一个无参数宏定义。
|
Notice:预处理命令语句后面一般不会添加分号,如果在#define最后有分号,在宏替换时分号也将替换到源代码中去。在宏名和字符串之间可以有任意个空格。
1
|
#define PI 3.14
|
在使用宏定义时,还需要注意以下几点:
1
2
3
4
5
6
7
8
9
|
宏定义是宏名来表示一个字符串,在宏展开时又以该字符串取代宏名。这只是一种简单的代换,字符串中可以含任何字符,可以是常数,也可以是表达式,预处理程序对它不作任何检查。如有错误,只能在编译已被宏展开后的源程序时发现。
宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。
宏名在源程序中若用引号括起来,则预处理程序不对其作宏替换。
宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名。在宏展开时由预处理程序层层替换。
习惯上宏名可用大写字母表示,以方便与变量区别。但也允许用小写字母。
|
3.2带参数的宏
1
|
#define命令定义宏时,还可以为宏设置参数。与函数中的参数类似,在宏定于中的参数为形式参数,在宏调用中的参数称为实际参数。对带参数的宏,在调用中,不仅要宏展开,还要用实参去代换形参。
|
带参宏定义的一般形式为:
1
|
#define 宏名(形参表) 字符串
|
在定义带参数的宏时,宏名和形参表之间不能有空格出现,否则,就将宏定义成为无参数形式,而导致程序出错。
1
|
#define ABS(x) (x)< 0 ?-(x):(x)
|
以上的宏定义中,如果x的值小于0,则使用一元运算符(-)对其取负,得到正数。
带参的宏和带参的函数相似,但其本质是不同的。使用带参宏时,在预处理时将程序源代码替换到相应的位置,编译时得到完整的目标代码,而不进行函数调用,因此程序执行效率要高些。而函数调用只需要编译一次函数,代码量较少,一般情况下,对于简单的功能,可使用宏替换的形式来使用。
3.3.预处理操作符#和##
3.3.1.操作符#
在使用#define定义宏时,可使用操作符#在字符串中输出实参。Eg:
1
|
#define AREA(x,y) printf(“长为“#x”,宽为“#y”的长方形的面积:%d\n”,(x)*(y));
|
3.3.2.操作符##
与操作符#类似,操作符##也可用在带参宏中替换部分内容。该操作符将宏中的两个部分连接成一个内容。例如,定义如下宏:
1
|
#define VAR(n) v##n
|
当使用一下方式引用宏:
1
|
VAR( 1 )
|
预处理时,将得到以下形式:
1
|
|
如果使用以下宏定义:
1
|
#define FUNC(n) oper##n
|
当实参为1时,预处理后得到一下形式:
1
|
oper1
|
四.文件包含------include
当一个C语言程序由多个文件模块组成时,主模块中一般包含main函数和一些当前程序专用的函数。程序从main函数开始执行,在执行过程中,可调用当前文件中的函数,也可调用其他文件模块中的函数。
如果在模块中要调用其他文件模块中的函数,首先必须在主模块中声明该函数原型。一般都是采用文件包含的方法,包含其他文件模块的头文件。
文件包含中指定的文件名即可以用引号括起来,也可以用尖括号括起来,格式如下:
1
2
3
|
#include< 文件名>
或
#include“文件名”
|
如果使用尖括号<>括起文件名,则编译程序将到C语言开发环境中设置好的 include文件中去找指定的文件。
因为C语言的标准头文件都存放在include文件夹中,所以一般对标准头文件采用尖括号;对编程自己编写的文件,则使用双引号。
如果自己编写的文件不是存放在当前工作文件夹,可以在#include命令后面加在路径。
#include命令的作用是把指定的文件模块内容插入到#include所在的位置,当程序编译链接时,系统会把所有#include指定的文件链接生成可执行代码。文件包含必须以#开头,表示这是编译预处理命令,行尾不能用分号结束。
#include所包含的文件,其扩展名可以是“.c”,表示包含普通C语言源程序。也可以是 “.h”,表示C语言程序的头文件。C语言系统中大量的定义与声明是以头文件形式提供的。 “.h”是接口文件,如果想理解C语言接口的写法,有必要琢磨一下 “.h”。
通过#define包含进来的文件模块中还可以再包含其他文件,这种用法称为嵌套包含。嵌套的层数与具体C语言系统有关,但是一般可以嵌套8层以上。
五.条件编译
预处理器还提供了条件编译功能。在预处理时,按照不同的条件去编译程序的不同部分,从而得到不同的目标代码。
使用条件编译,可方便地处理程序的调试版本和正式版本,也可使用条件编译使程序的移植更方便。
5.1使用#if
与C语言的条件分支语句类似,在预处理时,也可以使用分支,根据不同的情况编译不同的源代码段。
#if 的使用格式如下:
1
2
3
4
5
|
# if 常量表达式
程序段
# else
程序段
#endif
|
该条件编译命令的执行过程为:若常量表达式的值为真(非0),则对程序段1进行编译,否则对程序段2进行编译。因此可以使程序在不同条件下完成不同的功能。
举个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
#define DEBUG 1
int main()
{
int i,j;
char ch[ 26 ];
for (i= 'a' ;j= 0 ;i<= 'z' ;i++,j++)
{
ch[j]=i;
# if DEBUG
printf( "ch[%d]=%c\n" ,j,ch[j]);
#endif
}
for (j= 0 ;j< 26 ;j++)
{
printf( "%c" ,ch[j]);
}
return 0 ;
}
|
#if预编译命令还可使用多分支语句格式,具体格式如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
# if 常量表达式 1
程序段 1
#elif 常量表达式 2
程序段 2
… …
#elif 常量表达式 n
程序段 n
# else
程序段 m
#endif
|
关键字#elif与多分支if语句中的else if类似。
举个例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
#define os win
# if os=win
#include "win.h"
#elif os=linux
#include "linux.h"
#elif os=mac
#include "mac.h"
#endif
|
#if和#elif还可以进行嵌套,C89标准中,嵌套深度可以到达8层,而C99允许嵌套达到63层。在嵌套时,每个#endif,#else或#elif与最近的#if或#elif配对。
Eg:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
|
#define MAX 100
#define OLD - 1
int main()
{
int i;
# if MAX> 50
{
# if OLD> 3
{
i= 1 ;
{
#elif OLD> 0
{
i= 2 ;
}
# else
{
i= 3 ;
}
#endif
}
# else
{
# if OLD> 3
{
i= 4 ;
}
#elif OLD> 4
{
i= 5 ;
}
# else
{
i= 6 ;
}
#endif
}
#endif
return 0 ;
}
|
5.2使用#ifdef和#ifndef
在上面的#if条件编译命令中,需要判断符号常量定义的具体值。在很多情况下,其实不需要判断符号常量的值,只需要判断是否定义了该符号常量。这时,可不使用#if命令,而使用另外一个预编译命令———#ifdef.
1
2
3
4
5
6
7
8
9
|
#ifdef命令的使用格式如下:
#ifdef 标识符
程序段 1
# else
程序段 2
#endif
|
其意义是,如果#ifdef后面的标识符已被定义过,则对“程序段1”进行编译;如果没有定义标识符,则编译“程序段2”。一般不使用#else及后面的“程序2”。
而#ifndef的意义与#ifdef相反,其格式如下:
1
2
3
4
5
6
7
8
|
#ifndef 标识符
程序段 1
# else
程序段 2
#endif
|
其意义是:如果未定义标识符,则编译“程序段1”;否则编译“程序段2”。
5.3使用#defined和#undef
与#ifdef类似的,可以在#if命令中使用define来判断是否已定义指定的标识符。例如:
1
2
3
4
|
# if defined 标识符
程序段 1
#endif
|
与下面的标示方式意义相同。
1
2
3
4
|
#ifdef 标识符
程序段 1
#endif
|
也可使用逻辑运算符,对defined取反。例如:
1
2
3
4
|
# if ! define 标识符
程序段 1
#endif
|
与下面的标示方式意义相同。
1
2
3
4
|
#ifndef 标识符
程序段 1
#endif
|
在#ifdef和#ifndef命令后面的标识符是使用#define进行定义的。在程序中,还可以使用#undef取消对标识符的定义,其形式为:
1
|
#undef 标识符
|
举个例子:
1
2
3
4
5
|
#define MAX 100
……
#undef MAX
|
在以上代码中,首先使用#define定义标识符MAX,经过一段程序代码后,又可以使用#undef取消已定义的标识符。使用#undef命令后,再使用#ifdef max,将不会编译后的源代码,因为此时标识符MAX已经被取消定义了。
六.其他预处理命令
6.1.预定义的宏名
ANSI C标准预定义了五个宏名,每个宏名的前后均有两个下画线,避免与程序员定义相同的宏名(一般都不会定义前后有两个下划线的宏)。这5个宏名如下:
1
2
3
4
5
|
__DATE__:当前源程序的创建日期。
__FILE__:当前源程序的文件名称(包括盘符和路径)。
__LINE__:当前被编译代码的行号。
__STDC__:返回编译器是否位标准C,若其值为 1 表示符合标准C,否则不是标准C.
__TIME__:当前源程序的创建时间。
|
举个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
#include
int main()
{
int j;
printf( "日期:%s\n" ,__DATE__);
printf( "时间:%s\n" ,__TIME__};
printf( "文件名:%s\n" ,__FILE__);
printf( "这是第%d行代码\n" ,__LINE__);
printf( "本编译器%s标准C\n" ,(__STD__)? "符合" : "不符合" );
return 0 ;
}
|
6.2.重置行号和文件名命令------------#line
使用__LINE__预定义宏名赈灾编译的程序行号。使用#line命令可改变预定义宏__LINE__与__FILE__的内容,该命令的基本形如下:
#line number[“filename”]
其中的数字为一个正整数,可选的文件名为有效文件标识符。行号为源代码中当前行号,文件名为源文件的名字。命令为#line主要用于调试以及其他特殊应用。
举个例子:
1
2
3
4
5
6
7
8
9
10
|
1 :#include
2 :#include
4 :#line 1000
6 : int main()
7 :{
8 : printf( "当前行号:%d\n" ,__LINE__);
9 : return 0 ;
10 :}
|
在以上程序中,在第4行中使用#line定义的行号为从1000开始(不包括#line这行)。所以第5行的编号将为1000,第6行为1001,第7行为1002,第8行为1003.
6.3.修改编译器设置命令 ------------#pragma
#pragma命令的作用是设定编译器的状态,或者指示编译器完全一些特定的动作。#pragma命令对每个编译器给出了一个方法,在保持与C语言完全兼容的情况下,给出主机或者操作系统专有的特征。其格式一般为:
1
|
#pragma Para
|
其中,Para为参数,可使用的参数很多,下面列出常用的参数:
Message参数,该参数能够在编译信息输出窗口中输出对应的信息,这对于源代码信息的控制是非常重要的,其使用方法是:
1
|
#pragma message(消息文本)
|
当编译器遇到这条指令时,就在编译输出窗口中将消息文本显示出来。
另外一个使用比较多得pragma参数是code_seg.格式如:
1
|
#pragma code_seg([“section_name”[,section_class]])
|
它能够设置程序中函数代码存放的代码段,在开发驱动程序的时候就会使用到它。
参数once,可保证头文件被编译一次,其格式为:
1
|
#pragma once
|
只要在头文件的最开始加入这条指令就能够保证头文件被编译一次。
6.4.产生错误信息命令 ------------#error
#error命令强制编译器停止编译,并输出一个错误信息,主要用于程序调试。其使用如下:
1
|
#error 信息错误
|
注意,错误信息不用双括号括起来。当遇到#error命令时,错误信息将显示出来。
例如,以下编译预处理器命令判断预定义宏__STDC__,如果其值不为1,则显示一个错误信息,提示程序员该编译器不支持ANSI C标准。
1
2
3
4
|
# if __STDC__!= 1
#error NOT ANSI C
#endif
|
七.内联函数
在使用#define定义带参数宏时,在调用函数时,一般需要增加系统的开销,如参数传递,跳转控制,返回结果等额外操作需要系统内存和执行时间。而使用带参数宏时,通过宏替换可再编译前将函数代码展开导源代码中,使编译后的目标文件含有多段重复的代码。这样做,会增加程序的代码量,都可以减少执行时间。
在C99标准钟,还提供另外一种解决方法:使用内联函数。
在程序编译时,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体来进行替代。显然,这种做法不会产生转去转回得问题。都是由于在编译时将函数体中的代码被替代到程序中,因此会增加目标代码量,进而增加空间的开销,而在时间开销上不像函数调用时那么大,可见它是以增加目标代码为代码来换取时间的节省。
定义内联函数的方法很简单,只要在定义函数头的前面加上关键字inline即可。内联函数的定义与一般函数一样。例如,定于一个两个整数相加的函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
#include
#include
inline int add( int x, int y);
inline int add( int x, int y)
{
return x+y;
}
int main()
{
int i,j,k;
printf( "请输入两个整数的值:\n" );
scanf( "%d %d" ,&i,&j);
k=add(i,j);
printf( "k=%d\n" ,k);
return 0 ;
}
|
在程序中,调用函数add时,该函数在编译时会将以上代码复制过来,而不是像一般函数那样是运行时被调用。
内联函数具有一般函数的特性,它与一般函数所不同之处在于函数调用的处理。一般函数进行调用时,要讲程序执行权转导被调函数中,然后再返回到调用到它的函数中;而内联函数在调用时,是将调用表达式用内联函数体来替换。在使用内联函数时,应该注意如下几点:
1
2
|
在内联函数内部允许用循环语句和开关语句。但是,程序在经过编译之后,这个函数是不会作为内联函数进行调用的。
内联函数的定义必须出现在内联函数第一次被调用之前。
|
其实,在程序中声明一个函数为内联时,编译以后这个函数不一定是内联的,
即程序只是建议编译器使用内联函数,但是编译器会根据函数情况决定是否使用内联,所以如果编写的内联函数中出现循环或者开关语句,程序也不会提示出错,但那个函数已经不是内联函数了。
一般都是将一个小型函数作为内联函数。