1、C/C+程序设计 C/C+程序设计程序设计第第7章章 编译预处理和位编译预处理和位运算运算第第7章章 编译预处理和位运算编译预处理和位运算 引言引言 编译预处理是C语言编译系统的一个组成部分。所谓编译预处理是指在对源程序进行编译之前,先对源程序中的编译预处理命令进行处理,然后再将处理的结果和源程序一起进行编译,以得到目标代码,预处理的实现方法是通过几种特殊的命令,在进行程序的编译之前,先对这些命令进行处理。这些预处理命令的介入,可以改进程序的设计环境,提高编程效率。编译预处理命令均以符号“#”开头,并且规定一行只能写一条预处理命令,命令结束不能使用分号,通常预处理命令都放在源程序的开头。在前面
2、几章的程序中,我们用到的#define、#include等命令都是编译预处理的应用。C语言提供了语言提供了3种预处理命令:种预处理命令:宏定义宏定义文件包含文件包含条件编译条件编译第第7章章 编译预处理和位运算编译预处理和位运算 C语言有两种宏定义命令,一种是不带参数的宏定义(也称符号常量定义),另一种是带参数的宏定义。7.1 编译预处理编译预处理-宏定义宏定义1.不带参数的宏定义不带参数的宏定义一般格式为一般格式为:#define 标识符标识符 字符串字符串说明:说明:1)通常宏名用大写字母表示,以示与普通变量区别。2)宏名与字符替换序列之间用空格符分隔。3)编译预处理时,在程序中进行宏替换
3、(也称宏展开),凡是 宏名出现的地方均被替换为它所对应的字符替换序列。4)编译预处理时只做简单的替换,不进行语法检查更不会做运 算,若字符串有错误,只有在正式编译时才会进行检查。5)没有特殊的需要,一般在预处理语句的行末不必加分号,若 加了分号,则连同分号一起替换。6)使用宏定义可以减少程序中重复书写字符串的工作量,提高 程序的可移植性。#define PI 3.14159f;area=PI*r*r;经过宏替换后:area=3.14159f;*r*r;,显然正式编译时会报告语法错误。通常当问题规模事先不能确定时,可使用宏定义来定义一个表示问题规模的符号常量。如:#define N 1007)宏
4、定义命令通常放在文件开头或者函数定义之前,宏名的作用域通常从定义开始到所在源文件结束。但使用#undef命令可提前强制终止某个宏的作用域。#define PI 3.14159fvoid main()#undef PI /结束宏名PI的作用域fun()PI的有效范围 这里,#undef PI 之后的代码范围,符号常量PI的宏定义就不再起作用了。8)进行宏定义时,在字符替换序列中可以引用已定义的其它宏名,宏展开时会进行层层替换。例如:#include#define PI 3.1415926f#define R 4.0f#define L 2*PI*R#define S PI*R*Rvoid mai
5、n()printf(L=%f,S=%f n,L,S);经过宏展开后,printf()函数调用语句被宏替换为:printf(L=%f n,S=%f n,2*3.14159f*4.0f,3.14159f*4.0 f*4.0f);2.带参数的宏定义带参数的宏定义带参数的宏定义不仅要进行简单的字符串替换,而且还要进行参数替换。一般形式为一般形式为:#define ()说明:说明:1)宏定义时,宏名与左括号之间不要出现空格,否则会将空格连同后面的所有替换字符序列都作为替换内容进行替换。例如:#define S (a,b)a*b 如果程序中有 y=S(x,y),则被展开为:y=(a,b)a*b(x,y)显
6、然这不是想要的结果。2)宏定义中的参数称为形参。程序中使用带参数的宏时,程序中的参数为实参,实参可以是常量、变量或表达式。宏展开时,将替换序列中的形参用相应位置的实参替换;若宏定义的替换序列中的字符不是形参,则在替换时保留。例如:#define S(a,b)a*barea=S(2,3);其中a和b称为形参,2和3称为实参,在宏展开时,把2、3分别代替宏定义中的a、b,a*b中的“*”号保留,因此宏展开后语句为“area=2*3;”。3)宏定义字符序列中的参数要用圆括号括起来,而且最好把整个字符串也用圆括号括起来,以保证在任何替换情况下都把宏定义作为一个整体,并且可以有一个合理的计算顺序,否则宏
7、展开后,可能会出现意想不到的错误。例如:#define S(r)3.14159*r*rarea=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)【例例7-1】从键盘输入两个数,输出较小的数。#include#define MIN(a,b)(a)(b)?(a):(b)void main()int x,y;printf(Please inpu
8、t two integers:);scanf(%d%d,&x,&y);printf(MIN=%d n,MIN(x,y);类型类型区别区别函数函数带参数的宏带参数的宏是否计算是否计算实实参参的值的值先计算出实参表达式的值,然后传递给形参变量。不计算实参表达式的值,直接用实参原样进行简单的替换。何时进行处理、何时进行处理、分配内存单元分配内存单元在程序运行时进行值的处理、调用函数时分配临时的内存单元。预编译时进行宏展开,不分配内存单元,不进行值的处理。类型要求类型要求实参和形参要有类型声明,且二者类型要匹配。不存在类型问题,只是一个符号表示。调用情况调用情况函数的代码仅存在一个拷贝,对函数较大、调
9、用次数较多时比较合算,但调用函数时会产生时间和空间的开销。在程序源代码中只要遇到宏符号,都将其进行宏替换,调用宏时没有时间空间的开销。但调用次数过多时会使程序代码加长很多。参数传递方式参数传递方式有按值传递和按址传递方式对宏不存方式问题,就是简单替换。3.带参数的宏与函数的区别带参数的宏与函数的区别7.1.2 文件包含文件包含文件包含是指一个源文件可以将另外一个源文件的全部内容包含进来,即将另一个C语言的源程序文件嵌入正在进行预处理的源程序中相应位置,一般形式为:#include 或者或者#include 文件名文件名 说明:1)“文件名”必须用一对尖括号或一对双引号括起来。2)使用尖括号和使
10、用双引号对应着不同的路径查找策略。尖括号尖括号:系统直接在规定的磁盘目录(通常为软件安装目 录下的Include子目录)查找文件。双引号双引号:首先在当前文件所在目录中查找文件,若没有找 到,再在操作系统的path命令设置的各目录中查找,若还没有找到,最后才在Include子目录中查找。3)一个#include命令只能指定一个被包含文件。7.1.2 文件包含文件包含4)若#include命令指定的文件内容发生变化,则应该对包含此文件的所有源文件重新编译处理。5)文件包含命令可以嵌套使用,即一个被包含的文件中可以再使用#include命令包含另一个文件,而在该文件中还可以再包含其它文件,通常允许
11、嵌套10层以上。【例例7-2】分析图7-1所示的几个C源文件之间的包含关系。#include file2.c file1.c中其余代码#include file3.c file2.c其余代码file3.代码cfile1.cfile2.cfile3.c分析:分析:通过#include“file3.c”命令,使file3.c的代码被加入到文件file2.c中,而#include “file2.c”又使file2.c被加入到 file1.c中,因此,通过预处理后,在文件“file1.c”中既有“file2.c”的代码也有“file3.c”的代码。两种形式:指定表达式真假值指定表达式真假值 或者:或者
12、:指定某种符号是否定义指定某种符号是否定义7.1.3 条件编译条件编译 通常C源文件代码都会参与编译。但是,有时希望依据一定条件只对其中一部分代码进行编译,这就是条件编译。1.指定某种符号已有定义的条件编译命令指定某种符号已有定义的条件编译命令格式格式:#ifdef 标识符标识符 程序段程序段1#else 程序段程序段2#endif 说明说明:若标识符已经被定义过(一般用#define命令定义),那么编译程序段1,否则编译程序段2,其中#else部分为可选项。7.1.3 条件编译条件编译【例例7-3】分析下列条件编译代码分析下列条件编译代码。#ifdef DEBUG printf(x=%d,y
13、=%dn,x,y);#endif 分析分析:若标识符DEBUG被定义过,即程序中有宏定义“#define DEBUG”,则编译程序就会编译语句“printf(x=%d,y=%dn,x,y);”,从而在程序运行时输出x,y的值。若没有宏定义“#define DEBUG”,则此处的printf语句就不参加编译,当然也不会被执行。注意注意条件编译与条件编译与if语句有区别,即不参加编译的程序段在语句有区别,即不参加编译的程序段在目标程序中没有与之对应的代码。如果是目标程序中没有与之对应的代码。如果是if语句,则不管表达语句,则不管表达式是否为真,式是否为真,if语句中的所有语句都产生相应的目标代码。
14、语句中的所有语句都产生相应的目标代码。7.1.3 条件编译条件编译7.1.3 条件编译条件编译2.指定某种符号没有定义的条件编译命令指定某种符号没有定义的条件编译命令格式:#ifndef 标识符标识符 程序段程序段1#else 程序段程序段2#endif 说明说明:其意义与#ifdef的意义恰好相反。若标识符没有定义,程序段1参加编译,否则程序段2参加编译。#else部分为可选项。如:#ifndef DEBUG printf(x=%d,y=%dn,x,y);#endif 若标识符DEBUG没有定义,则编译printf语句,在程序运行时输出x,y的值;若有标识符DEBUG的宏定义,则此处的pri
15、ntf语句就不参加编译,也不被执行。7.1.3 条件编译条件编译3.指定表达式真假值的条件编译命令指定表达式真假值的条件编译命令格式格式:#if 表达式表达式 程序段程序段1#else 程序段程序段 2#endif 说明说明:若表达式为“真”(非0),编译程序段1,否则编译程序段2。其中#else部分可以省略。例如:#include#define FLAG 1void main()int a=1,b=0;#if FLAG a+;printf(a=%d,a);#else b-;printf(b=%d,b);#endif 此时FLAG为非0,则编译语句“a+;printf(a=%d,a);”。注意
16、注意:#if预处理语句中的表达式是在编译阶段计算值的,因而此处的表达式中不能出现变量,必须是常量或用#define定义的标识符。7.1.3 条件编译条件编译4.#undef 标识符标识符格式格式:#undef 标识符标识符 功能:将已定义的标识符变为未定义的。例如:#include#define FLAG 1void main()int a=1,b=0;#undef FLAG#ifdef FLAG a+;printf(a=%d,a);#else b-;printf(b=%d,b);#endif虽然前面已经定义标识符FLAG,但程序中“#undef FLAG”命令后,标识符FLAG已经变为未定义
17、,故下面程序段只编译语句“b-;printf(b=%d,b);”,程序运行结果是b=-1。程序中的所有数据在计算机内存中都是以二进制的形式储存的。在很多系统的程序开发中,都要求有对二进制的位(bit)进行一些运算和处理。位运算和位处理通常由低级语言来提供,而作为高级语言的C语言也提供了位运算的功能。7.2 位运算位运算7.2 位运算位运算7.2.1 位运算的概念和位运算符 位运算指对存储单元中的数据按二进制位进行的运算和处理。C语言提供了语言提供了6种位运算符:种位运算符:1)&(按位与)(按位与)2)|(按位或)(按位或)3)(按位异或)(按位异或)4)(按位取反)(按位取反)5)(右移)(
18、右移)说明:说明:1)位运算符中除了“”以外,均为二目运算符。2)运算对象只能是整型或字符型,不能为实型。3)运算对象均以二进制补码的形式进行运算。4)位运算符的优先级顺序为:(高于)(高于)&(高于)(高于)|。5)可以与赋值运算符结合成为复合赋值运算符。7.2 位运算位运算7.2.2 不同位运算的运算规则1.“按位与按位与”运算(运算(&)将两个操作数的对应二进制位进行逻辑与运算。运算规则:将对象的二进制数补码低位对齐,当对应位都为1该位的运算结果为1,否则为0。例如:6&5。应先把6和5以补码表示,再进行按位与运算。运算过程如下:6的补码:0000 01105的补码:0000 0101&
19、运算:0000 0100再如:-6&5-6的补码:111110105的补码:00000101&运算:0000 0000(注意,运算结果也是补码,故6&5的结果是4)(注意,运算结果也是补码,故-6&5的结果是0)7.2 位运算位运算说明说明:1)按位与运算的结果也是补码形式。2)“按位与”运算通常用于对一个数中的某些位清0,即需要清0的位与“0”相与。这样不需清0的位仍保持原值不变。【例例7-4】变量n为一正整数,写出判断n是奇数的表达式。分析:分析:判断一个整数是否为的常规表达式为:n%2!=0 或 n%2,在此给出另一种判断方法为:n&1!=0 或 n&1 思考一下,为什么?7.2 位运算
20、位运算2.“按位或按位或”运算()运算()将两个操作数的对应二进制位进行或运算。运算规则:如果两个相应位有一个为1,则该位的结果为1;如果两个相应位都为0,则该位的结果为0。例如:0 x65|0 x35。运算过程如下:0 x65的补码:0110 01010 x35的补码:0011 0101|运算:0111 0101说明说明:“按位或”运算通常用于将一个数中的某些二进制位设置成1,即需要置1的那些位与“1”相或。(注意:结果是补码。故,0 x65|0 x35的结果是0 x75)【例例7-5】设变量n为一正整数,请将该数最低位字节中的偶数位全部置为1,试写出能实现该功能的表达式。分析:分析:与二进
21、制数“10101010”进行位或运算,可实现。表达式为:n=n|1707.2 位运算位运算3.“按位异或按位异或”运算(运算()将两个操作数的对应二进制位进行异或运算。运算规则:如果两个相应位的值不同,则该位的结果为1,如果两个相应位的值相同,则该位的结果为0。例如:0125 0017(以0开头的数表示八进制数)。运算过程如下:0125的补码:0101 01010017的补码:0000 1111 运算:0101 1010说明:“按位异或”运算通常用于对一个数中的某些位取反,即需要取反的位与“1”相异或。(注意,结果是补码,故0125 0017的结果是0 x5a)7.2 位运算位运算4.“按位取
22、反按位取反”运算(运算()将操作数的各二进制位按位取反。即将0取为1,将1取为0。例如:0125(以0开头的数表示八进制数)。运算过程如下:0125的补码:0101 0101 运算:1010 1010(注意,结果是补码,故结果是十进制数:-86)5.“左移左移”运算(运算()操作数的对应二进制位依次左移若干位。高位丢弃,低位补0。例如:设有“int a=3,b;”,则执行了“b=a)操作数的对应二进制位依次右移若干位。操作数若为无符号数,移位后左端补0。对于有符号数,正数移位后左端补0;负数移位后左端补1。例如:设有“int a=128,b;”,则执行了“b=a 2;”后,b的值是32,a的值不变。说明:说明:右移右移1位,相当于除位,相当于除2。