《程序设计与C语言》课件第10章_第1页
《程序设计与C语言》课件第10章_第2页
《程序设计与C语言》课件第10章_第3页
《程序设计与C语言》课件第10章_第4页
《程序设计与C语言》课件第10章_第5页
已阅读5页,还剩82页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第10章编译预处理与位运算10.1宏定义10.2文件包含10.3条件编译10.4位运算习题1010.1宏定义宏定义就是把一长串字符序列用一个简短的名字去代替,这个名字称为宏名,被代替的长串序列就称为宏值。宏定义的过程是通过#define命令实现的。其一般格式为: #define〈宏名〉〈宏值〉

〈宏名〉可以出现在程序中,在程序被编译以前,预处理程序会先把〈宏名〉原封不动地替换成〈宏值〉(这称为宏代换),然后再去进行编译。

〈宏名〉可以是一个标识符,这称为简单宏;也可以带有参数,这称为带参数的宏。

1.简单宏简单宏的定义如下: #define〈标识符〉〈字符序列〉例如: #definePI3.14159265则PI为宏名,3.14159265为宏值,如果程序中有语句:

area=PI*r*r;则预处理程序先把它代换成:

area=3.14159265*r*r;然后再交给编译器去编译。简单宏在定义时应注意以下几个问题:

(1)宏名往往用大写字母书写,以突出和其他标识符的区别,但小写也是可以的。

(2)如果宏值过长,可在换行前加一个续行符“\”,如:#defineLONG-STRING″thisisalong\

stringthatisusedintheprogram″

(3)当宏值为表达式时,最好用圆括号括起来,以避免引起误解。如: #defineA3+2程序员的原意是想让A代表3+2这个整体,但在宏代换中,比如:

e=5/A;就会被替换成:

e=5/3+2;根据算术运算符的优先级,得e=3而非e=1这个预期的正确结果。这不是预处理程序误解了你的意思,而是你误解了预处理程序。因为宏代换是机械地、没有智能地去做简单的替换工作,它不管替换后的语法和语义正确与否,因此保证宏代换正确性的责任必须由程序员承担。如果你熟知了宏代换的操作规程,那么在宏定义时对宏值加一圆括号,就不会出现这样的问题了。如定义成 #defineA(3+2)这样不管怎样使用A,都不会出现任何问题。

(4)宏定义可以嵌套,即前面已经定义过的宏名又可以用到下面的宏值中去,如 #definePI3.14159265

#defineTWOPI(2*PI)则对语句

c=TOWPI*r;预处理程序会替换成:

c=(2*3.14159265)*r;

(5)若宏名出现在字符串中,则字符串中的宏名不作代换。如: #defineYES1则语句

printf(“YES=%d\n”,YES);被处理成:

printf(“YES=%d\n”,1);即格式控制串中的YES不作代换。

(6)宏名的有效范围是从定义点起至其所在文件末尾,或遇到取消宏定义的指令#undef为止。对终止点以后的宏名还可以再定义其他宏值。如: #defineA100

#undefA

#defineA10

A代表100的有效范围

【例10-1】宏名的有效范围一。#include<stdio.h>

#defineA100main(){inti=2;printf(″i+A=%d\n″,i+A);#undefA#defineA10printf(″i+A=%d\n″,i+A);return0;}运行输出:

i+A=102i+A=12可以看出,A在不同的范围内被代换成不同的宏值。一个函数中定义的宏名只要不被取消命令取消,就仍然可以被它后面定义的函数体使用。该函数体中使用的宏名是作用范围直到文件尾的宏名,而不是在#undef截止范围内的宏名。

【例10-2】宏名的有效范围二。#include<stdio.h>

#defineA100mian(){inti=2,j=3;intmax(int,int);printf(″i+A=%d\n″,i+A);printf(″max=%d\n″,max(i,j));#undefA#defineA10printf(″i+A=%d/n″,i+A);printf(″max=%d\n″,max(i,j));return0;}

intmax(inta,intb){returna>b?a+A:b+A;}运行输出:

i+A=102max=13i+A=12max=13可见在max函数中用到的A是第二次定义的可作用到文件尾的A(A代表10),而第一个A未出main函数就被取消了。

(7)宏名常用在以下方面:①用一个有意义的名字去代替含义不清的一串数字。比如用PI代表圆周率,用PAGESIZE代表每页打印的行数等,如#definePI3.14159265358979

#definePAGESIZE66这样在程序中使用PI和PAGESIZE比用3.14159265358979和66的含义明确多了,既简单又清楚,既便于修改又可避免出错。如果圆周率的精度要变动或每页打印的行数要改变,只需在宏定义处修改即可,不必遍查程序去修改这两个数,这样就避免了如果在某一处忽略而造成大错的可能。②用一个短的名字去代替较长的名字。如: #defineSTUstructstudent则

STUstud1,stud2;即等价于

structstudentstud1,stud2;这与用typedef定义类型名的作用相似,如

typedefstructstudentSTU;但这两个其实是不相同的:宏名是在编译预处理阶段处理的,定义时不带分号;而新类型名是在编译阶段处理的,定义时后面有分号。

(8)程序设计中常见的错误是宏定义时在宏值的后面加分号。如: #definePI3.14159;则对

s=2*PI*r会替换成

s=2*3.14159;*r;这会产生编译错误。

2.带参数的宏宏定义的第二种情况是宏名后面可以带有参数,因而在宏值中也有相同的参数。带参数宏定义的一般格式为: #define〈宏名〉(〈参数表〉)〈含有参数的字符序列〉如 #defineS(x)5*x其中,S为宏名,x为参数,5*x是带参数的字符序列。对带有参数的宏的处理方法是:先用替换字符去替换参数,然后再把宏展开。比如S(5)会被处理成5*5(但不是25),即先用5去替换参数x,再把S(5)展开成5*5。定义和使用带参数的宏时应注意的问题有:

(1)为了不致引起误解,宏值中间的参数要用圆括号括起来。例如,定义一个求圆面积的带参数的宏:#definePI3.1416

#defineCIRCLE-AREA(x)(PI*(x)*(x))如果有语句:

area=CIRCLE-AREA(4)则会被预处理程序展开成:

area=(3.1416*(4)*(4));因为表达式只由常量组成,所以在编译时就能计算出该表达式的值并赋给area。用圆括号把参数括起来是为了在参数是表达式时迫使编译器以正确的顺序计算表达式的值。例如,

area=CIRCLE-AREA(3+c);则被展开成:

area=(3.1416*(3+c)*(3+c));其中圆括号使表达式以正确的顺序进行计算。如果去掉圆括号,宏会被展开成

area=3.1416*3+c*3+c;按算术运算符的优先级,右边的表达式将不正确地按下式计算:

area=(3.1416*3)+(c*3)+c;

(2)宏名和后面的圆括号之间不能有空格,如有 #defineCUBE(a)a*a*a则预处理程序会把CUBE作为无参的宏名,而把“(a)a*a*a”作为宏值。

(3)参数表中的参数和字符序列中的参数必须一一对应,即参数表中的参数必须全部在字符序列中出现;反过来,字符序列中的参数也都必须出现在参数表中。如 #defines(a,b)3*a+6

#definies(a)a+b都是错误的。第一种情况是参数表中的参数b在后面未用到,因此它在参数表中的出现毫无意义。第二种情况是字符序列中的b未在参数表中出现,运行时会出现错误。

(4)有副作用的表达式(如修改变量的值)不应该传递给宏,因为宏的参数可能会被计算多次而产生意想不到的结果。例如, #defineSQUARE(a)a*a如i=3,以++i代替参数a,则被展开成++i*++i,本来想求4的平方(16),结果却得到20。

(5)当一个宏名要在另一个宏定义的宏值中出现时,最好将它的宏值部分括起来,以避免发生意外。

【例10-3】用求两个数的最小值的宏来定义求三个数的最小值的宏。#definemin2(a,b)((a)<(b)?(a):(b))#definemin3(a,b)min2(a,b)<(c)?min2(a,b):(c)main(){inti,j,k,n;printf(″Input3integers:\n″);scanf(″%d%d%d″,&i,&j,&k);n=min3(i,j,k);printf(″Theminimumof(%d,%d,%d)is%d\n″,i,j,k,n);return0;}运行输出:Input3integers:564Theminimumof(5,6,4)is4宏定义min3(a,b,c)的宏值中用到了min2(a,b)。在对min3(a,b,c)求值时,并不是先计算出min2(a,b)的值,而是将min2(a,b)的宏值原封不动地进行替换,这样如果min2(a,b)的宏值中没有最外边的括号,则在求min3(a,b,c)的值时就会被替换成:

(a)<(b)?(a):(b)<(c)?(a)<(b)?(a):(b):(c)那么当输入为5,6,4时,输出的最小值就会是5而不是4。这就是程序设计中的漏洞。当将min2(a,b)的宏值整个地用括号括起来后,就不会出现这样的问题了。

(6)用带参的宏可以代替某些简单的带参函数。例如,可以用 #defineCIRCLE-AREA(x)(PI*(x)*(x))去代替函数:

floatcirclearea(floatx) {return3.14159*x*x;}既然两者都能完成同样的工作,那么带参宏和带参函数之间有何区别?它们各有哪些优缺点呢?下面具体说明。①函数调用时先对参数表达式进行计算,把计算的结果作为一个值去代替形参,而宏代换时不对参数求值,如

area=circlearea(5+8);会被处理成

area=circlearea(13);而

area=CIRCLE-AREA(5+8);则被处理成

area=PI*(5+8)*(5+8);②函数有形参和实参的类型匹配之说,而宏的参数没有类型。③函数调用是在运行时处理的,要占用运行时间,而宏是在编译预处理阶段处理的,不占用运行时间。④函数调用需要一些额外开销,而宏直接把代码插入到程序中,不会增加额外的开销,并保持了程序的可读性,因此对一些小的、调用次数频繁的函数,最好用带参的宏去代替。10.2文件包含如果在一个文件中用到了另一个文件中的某些内容,则不必把该文件全部重复输入到自己的文件中,只要用一个文件包含指令就可以了。预处理指令#include可以完成这一任务。#include指令的功能是用指定文件的一份拷贝来取代这条预处理指令。#include指令有如下两种格式:#include<文件名>

#include″文件名″这两种格式的差别在于预处理程序查找被包含文件的路径不同。如果用双引号括起文件名,则预处理程序就在当前正编译的程序所在的目录中查找被包含文件,该方法通常用来包含程序员定义的头文件;而对于用尖括号括起来的文件名(多用来查找标准库头文件),预处理程序就用与实现无关的方式查找被包含文件,通常是在预指定的目录中查找。 #include指令应出现在文件的开头。文件包含的语义如图10-1所示。即file1.c把file2.c的拷贝包含到自己的内部,成为一个大文件,然后再一块编译,所以被包含文件file2.c应当是源文件,不应当是目标文件(.obj)。头文件不能单独编译,它只能和C源程序文件一起进行编译。图10-1文件包含的语义使用文件包含指令时应注意以下几个问题:

(1)一个#include指令只能包含一个文件,要包含多个文件就要用多个#include指令。

(2)文件包含可以嵌套,比如文件file1中含有指令: #include″f2.c″而在文件f2.c中又有: #include<math.h>

#include″f3.c″则file1也把f2.c中包含的文件全部包含进来。

(3)被包含文件中的全局变量也是包含文件中的全局变量,因此在包含文件中对这些量不必再加extern说明即可加以引用。

(4)被包含文件的扩展名一般为.h(head),表示是在文件开头加进来的,其内容可以是程序文件或数据文件,也可以是宏定义、全局变量声明等。这些数据有相对的独立性,可被多个文件使用,不必在多个文件中都去定义,而只需在一个文件中定义,其他文件中包含这个定义文件即可。

【例10-4】利用随机数求圆周率π。编程思路:设有一个边长为1的正方形ABCD,则其面积为S=1×1=1。以A为圆心,以1为半径画弧,得一扇形ABD,也就是四分之一圆,如图10-2所示。扇形的面积S1=π×1×1/4=π/4。正方形面积S和该扇形面积S1相比:S/S1=4/π。现在在正方形中均匀地撒上M粒沙子,其中落在扇形区域内的有N粒,则沙粒数量之比应该和面积之比相当:M∶N≈S∶S1=4∶π可推出:π≈4×N/M利用随机数函数rand()产生沙粒的坐标(x,y),则0≤x,y<1。若,则沙粒落在扇形内。在产生了M个沙粒之后,就可以求出π的近似值。图10-2程序如下:#include<stdio.h>

#include<math.h>

#include<stdlib.h>

#defineMOD32768main(){intm,n=0,i;doublex,y,pi;printf(″Input:M=?\n″);scanf(″%d″,&m);for(i=0;i<m;i++)

{x=(double)rand()/MOD;y=(double)rand()/MOD;if(sqrt(x*x+y*y)<=1.0)n++;}pi=(double)4*n/m;printf(″n=%d,PI=%f/n″,n,pi);return0;}运行输出:

Input:M=?10000↙

n=7897,PI=3.158800再运行:

Input:M=?20000↙n=15776,PI=3.155200再运行:

Input:M=?30000↙n=23637,PI=3.151600由运行结果可见,随着测试数目的增加,落在1/4圆内的比例越来越向某个定值方向趋近,所求π的近似值越来越精确。程序中因用到标准输入/输出函数,所以应把头文件<stdio.h>包含进来;用到了数学函数sqrt,所以应把头文件<math.h>包含进来;而随机函数rand的原型说明在<stdlib.h>,所以也把这个头文件包含进来。10.3条件编译一个程序中的所有语句并不一定要全部编译执行,根据一定的条件可以对其中的一部分进行编译。能够控制编译范围的指令就是C语言提供的条件编译指令。条件编译指令的结构与if选择结构非常类似,例如,#if!define(NULL)

#defineNULL0

#endif这段描述的意思是:检查NULL是否被定义过,如没有被定义过,则表达式define(NULL)的结果为0,而!define(NULL)就是1,条件为真,从而执行下面的宏定义,把NULL定义为0;如果已作过NULL的定义,则条件为假,就不执行#define指令。每一个#if结构都是以#endif结束的。可以把#ifdefine()缩写为#ifdef,把#if!define()缩写为#ifndef。条件编译的形式主要有三种。

1.#if…#endif形式这种形式为:#if〈常整数表达式〉〈程序段〉

#endif其语义是:如〈常整数表达式〉的值为真,则编译〈程序段〉。在有多个部分的条件编译结构中可以用#elif和#else指令作为中间嵌套判断,从而形成如下结构:#if〈常整数表达式1〉 〈S1〉

#elif〈常整数表达式2〉 〈S2〉

#elif〈常整数表达式3〉 〈S3〉

#else 〈Sn〉

#endif预处理程序在处理上述条件编译命令时,先对#if之后的表达式求值,结果是一个逻辑值,然后再根据结果是否为0来决定对某段程序是否进行编译。elif即elseif的缩写。在上述各常整数表达式中不可包含强制转换运算符,因为预处理程序不理解这些属于核心语言的构件。常整数表达式中只能出现预定义的常量标识符,或整常数,或字符常数等。

2.#ifdef…#endif形式这种形式为:#ifdef〈标识符〉 〈程序段〉

#endif其语义是:如果〈标识符〉用#define命令定义过,则条件为真,接着编译〈程序段〉,如〈标识符〉未定义过,则忽略下面的〈程序段〉。

3.#ifndef…#endif形式这种形式为:#ifndef〈标识符〉〈程序段〉

#endif其语义是:如果〈标识符〉未被定义过,则条件为真,接下来编译〈程序段〉,否则忽略。如前面的例子可以写为:#ifndefNULL

#defineNULL0

#endif条件编译主要有以下用途:

(1)忽略程序的某一部分。在程序开发过程中,程序员经常发现需要把一大段代码变为注释,以防止编译器对这段代码进行编译。如果这段代码中已含有注释,就不能再用注释符号“/*”和“*/”来忽略这段代码了。这时可以使用如下预处理结构:#if0〈不编译的代码段〉

#endif因为0代表假,所以就不编译这段代码,如果想让编译器编译这段代码,把0改为1就可以了。

(2)帮助程序调试。调试主要是随时跟踪了解变量的取值情况,看是否有异常现象发生。虽然不少系统提供了调试程序(debugger),但通常难以理解和使用,所以常通过使用printf语句打印变量的值来测试控制流的正确性。这些printf语句可以放在条件编译指令间,在调试的时候编译这些语句。例如:#ifdefDEBUGprintf(″Variablex=%d\n″,x);

#endif若前面已有符号常量DEBUG的定义(即有指令#defineDEBUG),则编译执行printf语句,显示变量x当前的值。在调试完成后就不需再显示x的值了,此时只需去掉前面的#defineDEBUG指令就可以了,这样在编译过程中就会忽略为调试而加入的printf语句,不必在程序中一一地把这些调试语句去掉。在大型程序中可能要定义多个符号常量,用以控制对源文件中某些部分的条件编译。10.4位运算位运算是体现C语言具有低级语言功能的最重要的特点之一。计算机在用于检测和控制领域时都要用到位运算的知识,而且位运算也是计算机的基本操作。我们知道,计算机中的存储和运算都是以二进制方式进行的,而位运算正是提供了对二进制位进行操作的手段,因此合理地使用位运算可使程序的执行效率得到提高。10.4.1位运算符

C语言提供了6个位运算符:&、|、~、∧、、。它们的功能分别是:按位与、按位或、取反、按位异或、左移和右移。前4个运算符具有逻辑运算的特点,故也称为字位逻辑运算符;后两个具有移位的功能,故称为字位移位运算符。位运算符仅作用于非浮点类型。

1.字位逻辑运算符二进制的位只有两个值:0和1,字位逻辑运算符的操作对象也就是0或1,运算的结果也是0或1。另外,字位逻辑运算还有一个特点:它只对相关的两位二进制位进行运算,不产生进位,这是逻辑运算和算术运算的一个重要区别,因为算术运算有进位问题。

(1)按位与运算符&是双目运算符,其两个运算对象均是二进制位,结果也是二进制位。其功能为:

0&0=0,0&1=0,1&0=0,1&1=1即只有在两个运算对象全为1时,结果才为1,其余情况下结果均为0。

(2)按位或运算符|也是一个双目运算符,其功能为:

0|0=0,0|1=1,1|0=1,1|1=1即两个运算对象中只要有一个为1,结果就是1。

(3)按位取反运算符~是单目运算符,其功能为:

~0=1,~1=0即对原来的位值进行翻转。

(4)按位异或运算符∧是双目运算符,其功能是:

0∧0=0,0∧1=1,1∧0=1,1∧1=0即当两个运算对象的值相反时结果为1,相同时结果为0。它用来检测两个运算对象是否不相同,不相同时为真,相同时为假。

【例10-5】设a=(9)10=(1001)2,b=(10)10=(1010)2,求a&b、a|b及a∧b。

a&b: a|b: a∧b: 1001 1001 1001 &1010 |1010 ∧1010 1000 1011 0011结果为:a&b=1000,a|b=1011,a∧b=0011。

注意:①位运算符和逻辑运算符的区别。&和&&,|和||,虽然它们的真值表是相同的,但&和|是对两个数按二进制位进行操作,其结果是一个数,而逻辑运算符&&和||是把两个数作为整体进行操作的,其结果是逻辑值。例如:

9&10=(1000)2=(8)10而9&&10=1(真) 9|10=(1011)2=(11)10而9‖10=1(真)②任何一个数和它自身的异或永远是0,而任何一个数和0异或的结果是其自身。例如,若a=1010,则:

1010 1010 ∧1010 ∧0000 0000 1010即a∧a=0,a∧0=a。按位异或运算有个有趣的特性:对一个数据用另一个数据进行两次异或操作后,将恢复这个对象原来的值。即∧(∧(a,b),b)=a其中,∧(a,b)=a∧b,因此上式也就是(a∧b)∧ba∧(b∧b)=a∧0=a结合律例如,设a=1001,b=1010,则

1001 (a) 1001 (c) ∧1010 (b) ∧1010 (b) 0011 (c) 1001 (a)前一操作称为加密过程,将a用b进行异或运算,结果为c;后一操作称为解密过程,即将加密后的值c再用b进行异或运算,结果又恢复为a。利用这个特性,可以在不要第三变量的情况下将两个变量进行交换,这时只需进行三次异或操作即可:

a=a0∧b0;(1)b=a∧b0;(2)a=a∧b;(3)从形式上看,三个赋值表达式右边的表达式形式完全一样(下标0仅表示其初始值),但是随着赋值的进行,它们的内容却在发生着变化,前面赋值运算的结果用于后面的表达式中。可作如下推导:由(2)式推导:

b=a∧b0(a0∧b0)∧b0=a0∧(b0∧b0)=a0∧0=a0(4)由(3)式推导:a=a∧b(a0∧b0)∧b(a0∧b0)∧a0(a0∧a0)∧b0=0∧b0=b0即b的终值为a的初值a0,a的终值为b的初值b0。a0和a,b0和b都用同一内存单元,只是表示不同时间内的不同值而已。(1)式(1)式(4)式结合按位逻辑运算主要用在以下几个方面:①按位与运算&用于选取一个数的某些二进制位。这里又有两种情况:一是只取一个数的某几位,其他位都不要;二是只保留一个数中某几位的原始值,其他位全是0。要达到这样的目的,关键是设计一个“掩码”。对于第一种情况,掩码取所需要的位数,且全部置1;对于第二种情况,掩码和原来的数一样长,只在需要保留的位上置1,其余全置0。掩码确定之后,再与原数进行适当的按位与运算,即可得到所要的结果。在一个16位的系统中(即一个字由两个字节组成,共有16位二进制位),前8位和后8位全为1时表示的数的情况为:即:(11111111)2=(377)8 (1111111100000000)2=(177400)8因此要取一个数n的低8位,可取(377)8为掩码:(377)8 n=n&(0377)/*八进制数以0开头*/同理,要取其高8位,可用(177400)8为掩码:

n=n&(0177400)1111111111111111(377)8(177400)8

【例10-6】判断数的奇偶性。

编程思路:一般来说,要判断一个数的奇偶,只需用该数对2进行求模(%)运算,若结果为1则为奇数,若结果为0则为偶数,但如果用位运算则速度会更快。可让该数与数1进行与运算&。奇数的最低位为1,和1与的结果为1,代表奇数;偶数最低位为0,和1与的结果为0,代表偶数。由此可设计如下程序:

#include<stdio.h>main(){intn;printf(″Inputaninteger:\n″);scanf(″%d″,&n);if(n&1)printf(″%disodd!\n″,n);elseprintf(″%diseven!\n″,n);return0;}运行输出:

Inputaninteger: 9↙

9isaodd!②按位或运算|用于将一个数指定的位置成1,其他位保持不变。例如,将3244的第4至第7位置成1,先找出所需要的掩码(360)8(找的方法是在16位表示的整数中,令第4至第7位全为1,其余位全为0即为所求),再与3244作按位或运算:

n=n|03600000110010101100 (3244)|0000000011110000 (0360) 0000110011111100 (3324)

【例10-7】利用位运算实现大、小写字母的转换。编程思路:大、小写字母的ASCII码的差值为32=25=(100000)2,即′A′+32=′a′,′A′和′a′的编码分别是:

′A′:01000001′a′:01100001它们的差别在右起第6位,在大写字母的该位上置1即变为对应的小写字母,在小写字母的该位上置0则变为相应的大写字母。对于大写字母,可设计掩码为(00100000)2=(32)10,然后将其与大写字母的ASCII码进行按位或运算,即可得到对应的小写字母;对于小写字母,可设计掩码为(11011111)2=(df)16=0xdf,然后将其与小写字母的ASCII码进行按位与运算,即可得到对应的大写字母。

【例10-8】大、小写字母转换。#include<stdio.h>main{inticharc[80];printf(″Inputastring\n″);scanf(″%s″,c);for(i=0;i<80;i++){if(′A′<=c[i]&&c[i]<=′Z′)printf(″%c″,c[i]|32);elseif(′a′<=c[i]&c[i]<=′z′)printf(″%c″,c[i]&0xdf);if(c[i]==′\0′)break;}return0;}运行输出:

InputastringSTRINGstring↙

stringSTRING③按位异或运算∧可用于将一个数的指定位翻转,其他位不变。方法同样是先找出该数所需要的掩码,再将原数与掩码进行异或运算。掩码的设计方法是:在和需要翻转位相应的位置1,其余位置0。例如,对3244的低字节的高4位翻转。掩码为:0000000011110000(360)8,则n=n∧0360为:

0000110010101100 3244∧0000000011110000 (360)8

0000110001011100 3164④取反运算常用于掩码的设计。如欲设计一掩码,其最低位为0,其余位皆为1。对16位系统而言,其形式为:

1111111111111110对32位系统而言,其形式为:

11111111111111111111111111111110把它们用八进制表示,分别是0177776和037777777776。这种由二进制到八进制的转换过程,也需要一定的计算量,但如果用~1表示则相当简单。所以不管对16位还是32位系统,要对数值n的末位置0,都可以写成下面的形式:

n=n&~1注意:~1≠-1。如对16位系统而言:

~1=1111111111111110 -1=1111111111111111(补码表示)

~运算符的优先级高于算术运算符、逻辑运算符和其他的位运算符,如~b&a等价于(~b)&a。

2.字位移位运算符<<和>>这两个运算符的使用格式是:

〈数值〉<<〈左移位数〉〈数值〉>>〈右移位数〉例如:

a=a<<3;表示将a的二进制表示向左移3位。结果把a最左边的3位挤掉,右边补3个0。若a=16,其二进制为00010000,则a<<3之后变为10000000,即十进制的128,而128=16×23。由此可知,左移运算时,若移掉的位中无1,则左移n位后的值是原值的2n倍。运算 a>>3;是把a向右移3位,则移掉了a最右边的3位,左边补3个0,为00000010,即十进制的2,它等于16除以23。由此可知,若右移n位,则移动后的值是原值的1/2n。上述规则适用于正数及无符号数。若移动的数为负数,则在向左移时,符号位上的数字会发生变化,可能为1,也可能为0,因此结果是不确定的;而右移时,对符号位有不同的处理办法:若该位置成0则称逻辑右移,若该位置成1则称算术右移。例如:a:1001011111101101a>>1:0100101111110110 (逻辑右移)a>>1:1100101111110110 (算术右移)采用算术右移能保证数值的正负性不变。TurboC采用算术右移。10.4.2与位运算有关的复合赋值运算符位运算符和赋值运算符结合起来可构成如下的复合赋值运算符:

&=、|=、>>=、<<=、∧=复合赋值运算符的左边必须是变量,右边是表达式。例如:

a<<=2;等价于

a=a<<2;又如:

b|=c;等价于

b=b|c;对两个长度不等的数进行位运算时,右边对齐,短的数的左边或添加0(对正数或无符号数),或添加1(对负数),添加到两个数等长后再进行位运算。

【例10-9】取出一个整数从右边第m位开始的右n位。

编程思路:从右边第m位开始的右n位距最右端的距离是m-n,所以向右移m-n位后再与适当的掩码进行与运算,即可得到所需的位。这里的掩码应该是右边n位为1,其余位皆为0。实现的方法是把~0左移n位后再取反,即~(~0<<n)。因为:~0=1111111111111111~0<<n=1111111111110000(设n=4)~(~0<<n)=0000000000001111于是可编程如下:#include<stdio.h>main(){unsignedinta,m,n;printf(″Inputanunsignednumber\n″);scanf(″%u″,&a);printf(″m=?n=?\n″);scanf(″%d%d″,&m,&n);a=a>>(m-n)&~(~0<<n);printf(″result=%u\n″,a);return0;}运行输出:

Inputanunsignednumber57↙

m=?n=?63↙result=7

57的二进制表示为00111001,取从右端开始的第6位起的右3位,即求中间的111。

【例10-10】用位运算把十进制整数转换成二进制和八进制数。编程思路:首先把十进制数转换成二进制数,再在此基础上把二进制数转换成八进制数。如何把十进制数转换成二进制数呢?可以从最高位开始,不断求出二进制数的各位。欲求最高位,应找出一个合适的掩码使得只能取最高位,这个掩码可用1<<15来得到。在数值与该掩码进行一次按位与运算之后,即可求出最高位;然后把数值左移一位,使次高位处于最高位的位置,再与掩码进行与运算。如此循环,直到数的各位均被取出为止。如何转换成八进制数?可对得到的二进制数从右向左三位一组地进行分节,每节可转换成一个八进制数码。因一个整数用16位二进制表示,所以可分为6节,最左一节1位,其他各节均3位。可以考虑设计一个只取数中三位的掩码,让它和被转换数值的最右端的3位进行与运算,这就等于取出数值的低3位,转换成八进制数码;然后把数值右移3位,使下一节再与掩码进行与运算。每进行一次可得到一个八进制数码。掩码如何设计?因为要取3位,故可令掩码mask=(111)2=7。用一个有6个元素的数组把在转换过程中得到的八进制数码收集起来,由转换过程可知该数组中各元素的值是按八进制由低到高的顺序排列的,因此最先生成的是最低位。要想得到正确的八进制数,还必须把这个数组颠倒一下。另外应考虑,倒置以后的数组很可能由于数值不大致使高位为0,因此应去掉位于高端的无用0,直接输出有意义的八进制数码。程序如下:#include<stdio.h>voidconvtenTo2(unsigned);voidrev(unsigned[],int);main(){unsignedi,j,m,mm,a[6],mask=7;printf(″Inputanunsignedintegernumber:\n″);scanf(″%u″,&m);mm=m; /*保留输入值*/convtenTo2(m);for(j=0;i<=5;j++){a[j]=m&mask;m>>=3;}printf(″\n″);rev(a,6);printf(″The%u′soctalnumberis:″,mm);for(i=0;i<=5;i++)printf(″%o″,a[i]); /*输出6位八进制数*/printf(″Theequalis:\n(%u)10=″,mm);j=0;while(a[j]==0) /*去掉前导0*/j++;printf(″(″);for(i=j;i<=5;i++)printf(″%o″,a[i]);printf(″)8\n″);return0;}voidrev(unsignedb[],intn){inti,j=n/2,t;for(i=0;i<j;i++,n--){t=b[i];/*数组两端元素对调*/b[i]=b[n-1];

b[n-1]=t;

}}voidconvtenTo2(unsigneda){unsignedmask,i;mask=1<<15;printf(″\n%u′sbinarynumberis:\n(%u)10=″,a,a);printf(″(″);for(i=1;i<=16;i++){printf(″%c″,a&mask?′1′:′0′);a<<=1;if(i%8==0)putchar(′′);}printf(″)2″);putchar(′\n′);

}

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论