第十章预处理和位运算.docx
- 文档编号:5903021
- 上传时间:2023-01-02
- 格式:DOCX
- 页数:14
- 大小:37.92KB
第十章预处理和位运算.docx
《第十章预处理和位运算.docx》由会员分享,可在线阅读,更多相关《第十章预处理和位运算.docx(14页珍藏版)》请在冰豆网上搜索。
第十章预处理和位运算
第十章预处理和位运算
10.1预处理
10.1.1概述
C语言的编译系统分为编译预处理和正式编译,这是C语言的一大特点,其中编译预处理是它和其他高级语言的一个重要区别。
编译C语言程序时,编译系统中首先是编译预处理模块根据预处理命令对源程序进行适当的处理,然后才是对源程序的正式编译:
对加工过的C源程序进行语法检查和语义处理,最后将源程序转换为目标程序。
预处理命令均以符号“#”开头,并且一行只能写一条预处理命令,结束时不能使用分号,一般将预处理命令写在源程序的开头。
如果能正确使用预处理命令,就能编写出易于调试、易于移植的程序,并为结构化程序设计提供帮助。
C语言提供三种编译预处理命令:
1.宏定义;
2.文件包含;
3.条件编译。
10.1.2宏定义
C语言有两种宏定义命令:
不带参数的宏定义(或符号常量定义)和带参数的宏定义。
一、不带参数的宏定义
不带参数的宏定义通常用来定义符号常量,即用一指定的宏名(即标识符)来代表一个字符串,一般形式为:
#define<宏名><替换序列>
其中宏名常用大写字母表示,宏名与替换序列(即字符序列)之间用空格符分隔。
在程序中,经编译预处理后,就进行宏展开,凡是宏名出现的地方被替换为它所对应的替换序列。
对于宏定义的使用,作以下几点说明:
⑴预处理模块只是用宏名作简单的替换,不作语法检查,若字符串有错误,只有在正式编译时才能检查出来。
⑵没有特殊的需要,一般在预处理语句的行末不必加分号,若加了分号,则连同分号一起替换。
如:
#definePI3.1415926;
……
area=PI*r*r;
经过宏展开后,该语句为:
area=3.1415926;*r*r;显然有错误。
⑶使用宏定义可以减少程序中重复书写字符串的工作量,提高程序的可移植性。
例如,定义数组的大小:
#defineN100
inta[N];
这时数组的大小为100,若改变数组大小,则:
#defineN200
⑷宏定义命令一般写在文件开头、函数之前,作为文件的一部分,宏名的有效范围为宏定义之后到本源文件结束。
如果要强制终止宏定义的作用域,可以使用#undef命令。
如:
这样就可以灵活控制宏定义的作用范围。
⑸进行宏定义时可以引用已定义的宏名,宏展开是层层替换。
例如:
#definePI3.1415926
#defineR4.0
#defineL2*PI*R
#defineSPI*R*R
main()
{
printf("L=%f\nS=%f\n",L,S);
}
经过宏展开后,printf函数中的输出项L、S展开如下:
L:
2*3.1415926*4.0
S:
3.1415926*4.0*4.0
printf函数被展开成:
printf(("L=%f\nS=%f\n",2*3.1415926*4.0,3.1415926*4.0*4.0)
二、带参数的宏定义
带参数的宏定义不仅要进行字符串的替换,而且还要进行参数替换,一般形式为:
#define<宏名>(<参数表>)<带参数的替换序列>
其中,宏定义中的参数为形参。
程序中使用带参数的宏时,程序中的参数为实参,实参可以是常量、变量或表达式。
宏展开时,将替换序列中的形参用相应位置的实参替换;若宏定义的替换序列中的字符不是形参,则在替换时保留。
如:
#defineS(a,b)a*b
area=S(2,3);
其中a和b称为形参,2和3称为实参,在宏展开时,把2、3分别代替宏定义中的a、b,a*b中的*号保留,因此宏展开后语句为:
area=2*3;。
【例10.1】从键盘输入两个数,输出较小的数。
#include"stdio.h"
#defineMIN(a,b)((a)<(b)?
(a):
(b))
main()
{
intx,y;
printf("Pleaseinputtwointegers:
");
scanf("%d%d",&x,&y);
printf("MIN=%d",MIN(x,y));
}
以上程序执行时,用序列((x)<(y)?
(x):
(y))来替换MIN(x,y)。
所以,可以输出两个数中的较小者。
对使用带参数的宏定义需要说明几点:
⑴对用宏定义定义的字符序列中的参数要用圆括号括起来,而且最好把整个字符串也用括号括起来,以保证在任何替换情况下都把宏定义作为一个整体,并且可以有一个合理的计算顺序,否则宏展开后,可能会出现意想不到的错误。
如:
#defineS(r)3.14159*r*r
……
area=S(a+b);
经过宏展开变为:
area=3.14159*a+b*a+b;
显然是由于在进行宏定义时,对r没有加括号造成与设计的原意不符。
那么,为了得到形如:
area=3.14159*(a+b)*(a+b);
就应该在宏定义时给字符序列中的形参加上括号,如;
#defineS(r)3.14159*(r)*(r)
⑵宏定义时,不要在宏名与带参数的括号之间加空格,否则会将空格后的字符都作为替换序列的一部分。
如:
#defineS(a,b)a*b
如果程序中有
mul=S(x,y)
则被展开为:
mul=(a,b)a*b(x,y)
⑶把函数和带参数的宏要区分开,虽然它们有相似之处,但它们是不同的,其区别见表10-1所示。
表10-1函数和带参数之宏的区别
区别
类型
函数
带参数的宏
是否计算实参的值
先计算出实参表达式的值,然后代替形参
不计算实参表达式的值,直接用实参进行简单的替换
何时进行处理、分配内存单元
在程序运行时进行值的处理、分配临时的内存单元,
编译时进行宏展开,不分配内存单元,不进行值的处理
类型要求
实参和形参要定义类型,且类型一致
不存在类型问题,只是一个符号表示,可以为任何类型
调用情况
函数的代码只作为一个拷贝存在,对程序较大、调用次数多的较合算,但调用函数时有一定数量的处理开销
在源代码中遇到宏定义时,都将其扩展为代码,程序调用几次宏就扩展为代码几次,但调用宏时没有处理的开销
10.1.3文件包含
文件包含是指一个源文件可以将另外一个源文件的全部内容包含进来,即将另一个C语言的源程序文件嵌入正在进行预处理的源程序中相应位置,一般形式为:
#include<文件名>
或
#include“文件名”
其中“文件名”指被嵌入的源程序文件中的文件名,必须用尖括号或双引号括起来。
通过使用不同的括号使查找嵌入文件时可采用不同的查找策略。
尖括号<>:
预处理程序在规定的磁盘目录(通常为include子目录)查找文件。
在TurboC中,是由集成环境中参数设置子菜单中的目录选项(Option:
Directores)中的Include目录项规定是在哪一个目录,如设为\TC\INCLUDE,则就在此子目录中查找。
一般包含C的库函数常用这种方式。
双引号“”:
预处理程序首先在当前目录中查找文件,若没有找到则再去由操作系统的path命令设置的各目录中查找,若还没有找到,最后才去Include子目录中查找。
图10-1表示了“文件包含”的含意,原来的源程序文件mypro.c和用文件包含命令嵌入的源程序文件file1.c在逻辑上被看成同一文件,经过编译以后生成一个目标文件mypro.obj。
在C语言的编译系统中有许多以.h(h为head的缩写)为扩展名的文件,被称为“头文件”。
在使用C语言的编译系统提供的库函数进行程序设计时,通常需要在源文件的开始部分包含进来相应的头文件。
这些头文件都是由C语言提供的源程序文件,其中主要内容是使用相应库函数时所需要的函数原型说明、变量说明、类型定义及宏定义等。
例如,在程序中要使用输入、输出类库函数(如putchar()等),就要在程序中加入“#include
因此若能正确使用#include语句,就可以减少不必要的重复工作,提高工作效率。
使用#include语句要注意以下几点:
⑴一个#include语句只能指定一个被包含文件,若包含n个则需n个#include语句。
⑵若#include语句指定的文件内容发生变化,则应该对包含此文件的所有源文件重新编译处理。
⑶文件包含命令可以嵌套使用,即一个被包含的文件中可以再使用#include语句包含另一个文件,而在该文件中还可以再包含其它文件,通常允许嵌套10层以上。
10.1.4条件编译
C语言的编译预处理程序还提供了条件编译能力,使得可以对源程序的一部分内容进行编译,即不同的编译条件产生不同的目标代码。
条件编译命令有以下几种形式:
1.#ifdef标识符
程序段1
#else
程序段2
#endif
其作用:
若标识符已经被定义过(一般用#define命令定义),那么程序段1参加编译,否则程序段2参加编译,其中#else部分可以省略,即:
#ifdef标识符
程序段1
#endif
例:
#ifdefDEBUG
printf("x=%d,y=%d\n",x,y);
#endif
若DEBUG被定义过,即:
#defineDEBUG
则在程序运行时输出x,y的值,以便调试时用于分析;若删去#defineDEBUG,则此处的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部分可以省略。
例:
#defineFLAG1
#ifFLAG
a=1;
#else
b=0;
#endi
若FLAG为非0,则编译语句“a=1;”,否则编译语句“b=0;”。
注意#if预处理语句中的表达式是在编译阶段计算值的,因而此处的表达式不能是变量,必须是常量或用#define定义的标识符。
4.#undef标识符
其作用:
将已定义的标识符变为未定义的。
例如:
#undefDEBUG
则语句:
#ifdefDEBUG
为假(0),而语句:
#ifndefDEBUG
为真(非0)。
10.2位运算
所谓位运算是指进行二进制位的运算。
在系统软件中,常要处理二进制位的问题。
例如,将一个存储单元中的各二进制位左移或右移一位,两个数按位相加等。
C语言中对一个整数(包括无符号整数、字符型、长整数)可以进行如表10-2所示的位操作。
表10-2位运算符及其含义
运算符
含义
&
按位与
|
按位或
^
按位异或
~
按位取反
<<
左移
>>
右移
说明:
⑴位运算符除~为一元运算外,其余都为二元运算,例如x&y,x^y,x<
⑵操作数只能是整型或字符型数据,不允许是实型数据。
下面对各种位操作进行介绍。
1.按位与、按位或、按位异或运算符
与操作:
0&0=00&1=01&0=01&1=1
或操纵:
0|0=00|1=11|0=11|1=1
异或操作:
0^0=00^1=11^0=11^1=0
这里的0和1均指一个二进制位。
下面举例说明异或操作:
intx=62;
x=x^43690;
printf("%u,%x\n",x,x);
结果:
43668,aa94
0000000000111110………62
^1010101010101010………43690
1010101010010100………43668
从以上操作可以看出完成的操作是逐位进行的。
【例10.2】设变量n为一正整数,试写表达式,判断n是否为奇数。
在整数n的二进制表示中,如果“个位”数为0,则n为偶数,如果“个位”数为1,则n为奇数,计算二进制“个数”数的式子为:
n&1
设n分别取100和25,则上式的运算结果为
0000000001100100100000000000001100125
&00000000000000011&00000000000000011
0000000000000000000000000000000011
整数n与1按位与的结果为整数n二进制表示的最低位,当n取100时,n的二进制最低位为0,则n为偶数,表达式n&1的结果为0,当n取25时,n的二进制最低位为1,则n为奇数。
因此,n是否为奇数的判断式子为:
n&1!
=0或n&1(式1)
而常规的判断是否奇数的式子为:
n%2!
=0或n%2(式2)
由于按位与是各个位分别进行运算,并且各个位之间并没有进位或借位关系,理论上位运算的执行速度可以比加减运算还要快,而求余运算是除法运算,所以,式1的运算速度超过式2。
2.按位取反运算符(~)
按位取反,即各位变反。
0变1,1变0。
如:
intx=1;
printf("~x=%x\n",~x);
结果为:
~x=fffe
3.左移运算符(<<)
把一个数中的所有二进制位向左移指定位数,右边空位补0。
如:
2570<<22为左移两位
移前:
0000101000001010
移后:
000010100000101000
补2位0
即,移前为0a0a(2570)
移后为2828(10280)/*2828为10280的十六进制形式*/
可见移位的过程中只要无1移出(上述移出最左边两个0),则结果总等于原来无符号数乘2n(n为移出的位数,这里n=2)。
如:
2570*22=10280。
因为移位操作比乘法来的快,所以乘2可用左移一位来实现。
4.右移运算符(>>)
把一个数中的所有二进制位向右移指定的位数,移到右端的低位被舍弃,对有符号数,左边空位补原来的符号位(最高位);对无符号数,左边空位补0。
我们仍用0a0a(2570)这个数进行说明,如:
2570>>22为右移两位
移前:
0000101000001010
移后:
000000101000001010
补2位0
即,移前为0a0a(2570)
移后为0282(642)
因为2570÷22=642.5
可见移后相当于移前这个整数除以2n取整(n为右移的位数,这里n=2)。
这个规律对负数也完全符合。
如:
-1248>>2
结果应为-312。
【例10.3】能否使用16位二进制表示一天中的时、分、秒等时间信息。
时间中的时、分和秒为3个整数,一般情况下,每个整数分别使用16位二进制表示,现在要求将这3个信息“挤”在一个16位二进制数中,问应如何组织?
时的取值范围为0至23,共24个小时,由于25=32,故最多使用5位二进制即可表示小时信息;分的取值范围为0至59,每小时共60分钟,由于26=64,故最多使用6位二进制即可表示分钟信息;秒的取值范围也为0至59,每分钟共60秒,由于26=64,故最多使用6位二进制即可表示秒信息。
总计共需17位二进制以精确表示时分秒信息。
计算机一般使用16位或32位表示信息,如果最多只能使用16位二进制,则必须牺牲某个信息量的精度,即秒最多只能取5位,对应0至59中的30个偶数秒,而奇数秒无法表示。
例如为了表示时间13:
20:
30。
小时为13,对应5位二进制为01101;分钟为20,对应6位二进制为010100;秒为30,对应6位二进制为011110,去掉其中的最低位,则秒使用5位二进制01111表示。
则该时间的16位二进制表示为:
0110101010001111
显然上式的16位二进制不能简单地转换成十进制数并理解成27279,它是由多个信息拼接而成的。
设时为h,分为m,秒为s,拼接的16位二进制数为d,则d的二进制表示形式为:
hhhhhmmmmmmsssss(式3)
而小时的二进制表示为:
00000000000hhhhh(式4)
将式4中的结果左移11位就得到式3中的最高5位,表达式为d<<11。
同样地,将分钟m左移5位(m<<5)得到分钟,秒右移一位(s>>1)可以得到5位二进制表示的秒,用于表示0、2、4等偶数秒。
则计算d的完整表达式为:
d=(h<<11)|(m<<5)|(s>>1);(式5)
或
d=(h<<11)+(m<<5)+(s>>1);/*使用加运算*/
当变量h、m和s的值有可能超界时,通过按位与可以限定位的宽度,如下式:
d=((h&0x1F)<<11)|((m&0x3F)<<5)|(s>>1)&0x1F;(式6)
式子(h&0x1F)<<11可以将变量h的高11位清零,只保留低5位,然后再左移11位。
式子(s>>1)&0x1F先将s右移一位(相当于除2),然后只保留5位。
式3的也可以使用以下数学计算公式:
d=h*64*32+m*32+s/2
或
d=(h*64+m)*32+s/2(式7)
同样当变量h、m和s的值有可能超界时,先限定位的宽度,如下式:
d=(h%24*64+m%60)*32+(s%60)/2(式8)
上述各式中,式5至式8是等价的,式5式6的效率比式7式8高。
反过来,如果已知d,求解其中所包含的时间信息,使用以下运算:
h=(d>>11)&0x1F;
m=(d>>5)&0x3F;
s=(d&0x1F)<<1;
在系统软件、压缩软件或硬件相关的程序中,每个二进制位都是非常宝贵的资源,二进制位的重组或拆分是常见的,因此位运算是非常普遍和必不可少的。
下面再举几个位运算的例子,具体过程读者可自行分析。
1.写出表达式(0x1234&0xFF)<<8的16进制结果。
答案为0x3400
2.分别计算3&5、3|5、3^5的十进制结果。
答案分别为1,7,6
3.分别计算下列表达式的十进制值:
(~3&15),~10^~12,(200>>4)&15
答案分别为12,6,12
习题
1.C语言的编译系统对宏命令的处理是。
A.在程序运行时进行
B.在程序连接时进行
C.和C程序中的其它语句同时进行编译的
D.在对源程序中其它成份正式编译之前进行的
2.阅读程序
#include
#defineMUL(x,y)(x)*y
main()
{
inta=3,b=4,c;
c=MUL(a++,b++);
printf("%d\n",c);
}
上面程序的输出结果是。
A.12B.15C.20D.16
3.以下正确的描述是。
A.C语言的预处理功能是指完成宏替换和包含文件的调用
B.预处理命令只能位于C源程序文件的首部
C.凡是C源程序中行首以“#”标识的控制行都是预处理命令
D.C语言的编译预处理就是对源程序进行初步的语法检查
4.设有以下宏定义:
#defineWIDTH80
#defineLENGTHWIDTH+40
则执行赋值语句:
v=LENGTH*20;(v为int型变量)后,v的值是。
5.设有以下程序,为使之正确运行,请填入应包含的命令行。
main()
{
doublex=2,y=3;
printf("%lf\n",pow(x,y));
}
6.试定义一个带参数的宏swap(x,y),以实现两个整数之间的交换,并利用它将一维数组a和b的值进行交换。
7.分别用函数和带参数的宏编程实现从三个数中找出最小数。
8.用1010101010101010这个二进制对00ff这个十六进制数分别进行与、或、异或操作。
9.已知整数x、y的值分别为0x1532、0xabcd,编程将整数x的高字节作为整数z的高字节,整数y的高字节作为整数z的低字节,并输出整数z。
10.分别写出下列表达式的十进制结果:
⑴200&15
⑵(200>>4)&15
⑶200^15
⑷~10^~12
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 第十 预处理 运算