400 likes | 549 Views
编译预处理. 编译预处理. 掌握宏定义的使用方法; 掌握文件包含的使用方法; 了解条件编译的使用方法。. C 语言的编译系统分为编译预处理和正式编译,这是 C 语言的一大特点,其中编译预处理是它和其他高级语言的一个重要区别。编译 C 语言程序时,编译系统中首先是编译预处理模块根据预处理命令对源程序进行适当的处理,然后才是对源程序的正式编译:对加工过的 C 源程序进行语法检查和语义处理,最后将源程序转换为目标程序。
E N D
编译预处理 • 掌握宏定义的使用方法; • 掌握文件包含的使用方法; • 了解条件编译的使用方法。
C语言的编译系统分为编译预处理和正式编译,这是C 语言的一大特点,其中编译预处理是它和其他高级语言的一个重要区别。编译C语言程序时,编译系统中首先是编译预处理模块根据预处理命令对源程序进行适当的处理,然后才是对源程序的正式编译:对加工过的C源程序进行语法检查和语义处理,最后将源程序转换为目标程序。 • 预处理命令均以符号“#”开头,并且一行只能写一条预处理命令,结束时不能使用语句结束符,若预处理命令太长,可以使用续行标志“\”后续写在下一行,一般将预处理命令写在源程序的开头。
宏定义 • C语言有两种宏定义命令: • 不带参数的宏定义(或符号常量定义); • 带参数的宏定义。
不带参数的宏定义 • 不带参数的宏定义通常用来定义符号常量,即用一指定的宏名(即标识符)来代表一个字符串,一般形式为: • #define<宏名> <替换序列> • 其中宏名常用大写字母表示,宏名与替换序列(即字符序列)之间用空格符分隔。在程序中,经编译预处理后,就进行宏展开,凡是宏名出现的地方被替换为它所对应的替换序列。
从键盘连续输入字符,统计其中的小写字母的个数,直到按ESC键结束。从键盘连续输入字符,统计其中的小写字母的个数,直到按ESC键结束。 • #include “stdio.h” • #define ESC 0x1B • main() • { • int count=0; • char c; • while(( c=getch( ))!=ESC) • if(c>=‘a’ && c<=‘z’) • count++; • printf(“count=%d”,count); • }(宏不进行类型检验) 其中,预处理程序将此程序中凡是出现ESC的地方都用常量0x1B替换。如果ESC的编码值有所变化,只需修改宏定义语句即可,这样有助于程序的调试和移植。
对于宏定义的使用,作以下几点说明: (1)预处理模块只是用宏名作简单的替换,不作语法检查,若字符串有错误,只有在正式编译时才能检查出来。 (2)没有特殊的需要,一般在预处理语句的行末不必加分号,若加了分号,则连同分号一起替换。如: #define ESC 0x1B; … … while((c=getch( ))!=ESC) … … 经过宏展开后,while语句变为: while((c=getch( ))!=0x1B;) 显然有错误,即表达式变为了语句。
(3)使用宏定义可以减少程序中重复书写字符串的工作量,提高程序的可移植性。例如,定义数组的大小:(3)使用宏定义可以减少程序中重复书写字符串的工作量,提高程序的可移植性。例如,定义数组的大小: #define arr_size 100 int array[arr_size]; 这时数组的大小为100,若改变数组大小,则: #define arr_size 200
PI的有效范围 (4)宏定义命令一般写在文件开头、函数之前,作为文件的一部分,宏名的有效范围为宏定义之后到本源文件结束。如果要强制终止宏定义的作用域,可以使用#undef命令。如: PI的有效范围 #define PI 3.14159 main( ) { …… } #undef PI fly1( ) … … 这样就可以灵活控制宏定义的作用范围。
(5)进行宏定义时可以引用已定义的宏名,宏展开是层层替换。例如:(5)进行宏定义时可以引用已定义的宏名,宏展开是层层替换。例如: #define PI 3.14159 #define R 4.0 #define L 2* PI* R #define S PI* R* R main( ) { printf(“L=%f\nS=%f\n”,L,S); } 经过宏展开后,printf函数中的输出项L、S展开如下: L -- 2* 3.14159* 4.0 S -- 3.14159* 4.0* 4.0 printf函数被展开成: printf((“L=%f\nS=%f\n”,2* 3.14159* 4.0, 3.14159* 4.0* 4.0)
(6)程序中出现用双引号括起来的字符串中的字符,若与宏名同名,不进行替换。例如第(5)点的例子中printf函数内有两个S字符,一个在双引号内的S不被替换,而另一个在双引号外的S将被替换。(6)程序中出现用双引号括起来的字符串中的字符,若与宏名同名,不进行替换。例如第(5)点的例子中printf函数内有两个S字符,一个在双引号内的S不被替换,而另一个在双引号外的S将被替换。
带参数的宏定义 • 带参数的宏定义不仅要进行字符串的替换,而且还要进行参数替换,一般形式为: • #define <宏名>(<参数表>)<带参数的替换序列> • 其中,参数为形参。 • 宏展开过程:程序中若有带实参的宏,则按#define指定的替换序列从左至右进行替换。若宏定义中包含有形参,则用程序中相应的实参替换形参,其中实参可以是常量、变量或表达式;
从键盘输入两个数,输出较小的数。 #include “stdio.h” #define MIN(a,b) ((a)<(b)?(a):(b)) main() { int x,y; printf (“输入两个数”); scanf (“%d,%d ”,&x,&y); printf (“MIN=%d”,MIN(x,y)); } 以上程序执行时,用序列((x)<(y)?(x):(y))来替换MIN(x,y)。所以,可以输出两个数中的较小者。
对使用带参数的宏定义需要说明几点 (1)对用宏定义定义的字符序列中的参数要用圆括号括起来,而且最好把整个字符串也用括号括起来,以保证在任何替换情况下都把宏定义作为一个整体,并且可以有一个合理的计算顺序,否则宏展开后,可能会出现意想不到的错误。如: #define S(r) 3.14159* r* r … area=S(a+b); 经过宏展开变为: area=3.14159* a+b* a+b; 显然是由于在进行宏定义时,对r没有加括号造成与设计的原意不符。那么,为了得到形如: area=3.14159* (a+b) * (a+b); 就应该在宏定义时给字符序列中的形参加上括号,如; #define S(r) 3.14159* (r) * (r)
(2)宏定义时,不要在宏名与带参数的括号之间加空格,否则会将空格后的字符都作为替换序列的一部分。如:(2)宏定义时,不要在宏名与带参数的括号之间加空格,否则会将空格后的字符都作为替换序列的一部分。如: #define S (a,b) a* b 如果程序中有 mul= S(x,y) 则被展开为: mul=(a,b)a* b(x,y)
(3)把函数和带参数的宏要区分开,虽然它们有相似之处,但它们是不同的,其区别见表所示。(3)把函数和带参数的宏要区分开,虽然它们有相似之处,但它们是不同的,其区别见表所示。
文件包含 • 文件包含是指一个源文件可以将另外一个源文件的全部内容包含进来,即将另一个C语言的源程序文件嵌入正在进行预处理的源程序中相应位置,一般形式为: • #include <文件名>或#include “文件名” • 其中“文件名”指被嵌入的源程序文件中的文件名,必须用尖括号或双引号括起来。通过使用不同的括号使查找嵌入文件时可采用不同的查找策略。 • 尖括号<>:预处理程序在规定的磁盘目录(通常为include子目录)查找文件。在Turbo C中,是由集成环境中参数设置子菜单中的目录选项(Option:Directores)中的Include目录项规定是在哪一个目录,如设为\TC\INCLUDE,则就在此子目录中查找。一般包含C的库函数常用这种方式。 • 双引号“”:预处理程序首先在当前目录中查找文件,若没有找到则再去由操作系统的path命令设置的各目录中查找,若还没有找到,最后才去Include子目录中查找。
图表示了“文件包含”的含意,原来的源程序文件mypro.c和用文件包含命令嵌入的源程序文件file1.c在逻辑上被看成同一文件,经过编译以后生成一个目标文件mypro.obj。图表示了“文件包含”的含意,原来的源程序文件mypro.c和用文件包含命令嵌入的源程序文件file1.c在逻辑上被看成同一文件,经过编译以后生成一个目标文件mypro.obj。 mypro.c: 源程序文件 #include “file1.c” file1.c: 被嵌入的文件 mypro.obj: 目标文件 编译
(2) 文件file.c #include “stdio.h” #include “pformat.h” main( ) { int a,b,c,d; char string[ ]=“STUDENT”; a=1;b=2;c=3;d=4; PR(D1,a); PR(D2,a,b); PR(D3,a,b,c); PR(D4,a,b,c,d); PR(S,string); } • (1) 文件pformat.h • #define PR printf • #define NL “\n” • #define D “%d” • #define D1 D NL • #define D2 D D NL • #define D3 D D D NL • #define D4 D D D D NL • #define S “%s” 注意在编译时,这两个文件不是用link命令实现联接,而是作为一个源程序进行编译,并得到一个相应的目标文件(.obj)。因此被包含的文件应该是源文件。
使用#include语句要注意以下几点: (1)一个#include语句只能指定一个被包含文件,若包含n个则需n个#include语句。 (2)若#include语句指定的文件内容发生变化,则应该对包含此文件的所有源文件重新编译处理。 (3)文件包含命令可以嵌套使用,即一个被包含的文件中可以再使用#include语句包含另一个文件,而在该文件中还可以再包含其它文件,通常允许嵌套10层以上。
条件编译 • C语言的编译预处理程序还提供了条件编译能力,使得可以对源程序的一部分内容进行编译,即不同的编译条件产生不同的目标代码。条件编译命令有以下几种形式 1. #ifdef 标识符 程序段1 #else 程序段2 #endif 其作用:若标识符已经被定义过(一般用#define命令定义),那么程序段1参加编译,否则程序段2参加编译,其中#else部分可以省略,即: #ifdef 标识符 程序段1 #endif
#ifdef DEBUG • printf(“x=%d,y=%d\n”,x,y); • #endif • 若DEBUG被定义过,即: • #define DEBUG • 则在程序运行时输出x,y的值,以便调试时用于分析;若删去#define DEBUG,则此处的printf语句就不参加编译。 • 注意条件编译与if语句有区别,即不参加编译的程序段在目标程序中没有与之对应的代码。如果是if语句,则不管表达式是否为真,if语句中的所有语句都产生目标代码。
2. #ifndef标识符 • 程序段1 • #else • 程序段 2 • #endif • 其作用:若标识符没有定义,程序段1参加编译,否则程序段2参加编译,其中#else部分可以省略,即: • #ifndef标识符 • 程序段1 • #endif
#ifndefDEBUG • printf(“x=%d,y=%d\n”,x,y); • #endif • 若DEBUG没有定义,则在程序运行时输出x,y的值;若用#define定义了DEBUG,则此处的printf语句就不参加编译。
3. #if表达式 • 程序段1 • #else • 程序段 2 • #endif • 其作用:若表达式为“真”(非0),程序段1参加编译,否则程序段2参加编译,其中#else部分可以省略。
#define FLAG 1 • #if FLAG • a=1; • #else • b=0; • #endif • 若FLAG为非0,则编译语句“a=1;”,否则编译语句“b=0;”。 • • 注意#if预处理语句中的表达式是在编译阶段计算值的,因而此处的表达式不能是变量,必须是常量或用#define定义的标识符。
4. #undef 标识符 • 其作用:将已定义的标识符变为未定义的。例如: • #undef DEBUG • 则语句: • #ifdef DEBUG • 为假(0), • 而语句: • #ifndef DEBUG • 为真(非0)。
常用技巧 • 1,防止一个头文件被重复包含 • #ifndef COMDEF_H • #define COMDEF_H • //头文件内容 • #endif
2,得到指定地址上的一个字节或字 • #define MEM_B( x ) ( *( (byte *) (x) ) ) • #define MEM_W( x ) ( *( (word *) (x) ) )
3,求最大值和最小值 • #define MAX( x, y ) ( ((x) > (y)) ? (x) : (y) ) • #define MIN( x, y ) ( ((x) < (y)) ? (x) : (y) )
4,得到一个field在结构体(struct)中的偏移量 #define FPOS( type, field ) \ ( (dword) &(( type *) 0)-> field ) 例如: #define FPOS(type,field) ( (dword) &(( type *) 0)-> field ) typedef unsigned long dword; typedef struct student { int num; char * name; float score; } S; main() { int a=FPOS(S, score); printf("%d",a); }
5得到一个结构体中field所占用的字节数 • #define FSIZ( type, field ) \ • sizeof( ((type *) 0)->field )
6,返回一个比X大的最接近的8的倍数 • #define RND8( x ) ((((x) + 7) / 8 ) * 8 )
7,将一个字母转换为大写 • #define UPCASE( c ) \ • ( ((c) >= 'a' && (c) <= 'z') ? ((c) - 0x20) : (c) )
8,返回数组元素的个数 • #define ARR_SIZE( a ) ( sizeof( (a) ) / sizeof( (a[0]) ) )
20,宏定义防止使用是错误 • 用小括号包含。 • 例如:#define ADD(a,b) (a+b) • 用do{}while(0)语句包含多语句防止错误 • 例如:#difne DO(a,b) a+b;\ • a++; • 应用时:if(….) • DO(a,b); //产生错误 • else • 解决方法: #difne DO(a,b) do{a+b;\ • a++;}while(0)
位操作(1) • 基本位操作符 • <<,>> • 如 (0x01) << 4 =0x10; (0x80)>> 4 = 0x08 • 位操作应用举例 • 乘法、除法,例如a*8可以写成a<<3; • 地址计算,例如一个32位的地址装在元素类型是char的数组a[4]中(假设按照从低到高存放),那么计算这个地址的方法为((a[3]<<24)|(a[2]<<16)|(a[1]<<8)|a[0] • 容易出现的问题 • 尽量使用无符号变量,避免产生符号错误; • 注意避免发生数据溢出;
位操作(2) • 位运算符例子分析 请分析下面问题的含义。 #define unsigned char UINT1#define unsigned short UINT2#define unsigned long UINT4 问题1: #define MC_GET_CHAR(__data__) \ (*((UINT1*)(__data__))) 问题2: #define MC_GET_SHORT(__data__) \ ((UINT2)((((UINT2)(*((UINT1*)(__data__))))<<8 )\ |((UINT2)(*((UINT1*)(__data__)+1)))&0x00ff)))
位操作(2) • 位运算符例子分析 请分析下面问题的含义。 #define unsigned char UINT1#define unsigned short UINT2#define unsigned long UINT4 问题1: #define MC_GET_CHAR(__data__) \ (* ( (UINT1*)(__data__) ) ) 问题2: #define MC_GET_SHORT(__data__) \ ( (UINT2) (( ((UINT2)( *((UINT1*)(__data__) )))<<8 )\ |((UINT2)(*((UINT1*)(__data__)+1)))&0x00ff) ) )
位操作(3) 问题3: #define MC_GET_LONG(__data__) \ ((((UINT4)MC_GET_SHORT((__data__)))<<16 )\ |(((UINT4)MC_GET_SHORT(__data__+2))&0x0000ffff)) 问题4: #define MC_GET_3BN(__data__) \ ((((UINT4)MC_GET_CHAR((__data__)))<<16 )\ |(((UINT4)MC_GET_SHORT(__data__+1))&0x0000ffff))