第3章 基本运算与顺序结构 3.1 概 述 借鉴数学中的运算符和表达式概念,C语言引入运算符,并为之定义运算规则。例如 使用+、-、*、/运算符实现加、减、乘、除四则运算。数学中不同类别的运算有先后顺序,C 语言同样定义了运算符的优先级,用于确定表达式中运算符的运算顺序。例如“先乘除、 后加减”。同时,C语言还规定了运算符与运算对象的结合性,例如-1/3代表-1除以 3。解决问题的数学计算公式则通过C语言的表达式实现计算,表达式由若干常量、变 量、函数和运算符构成。C语言的基本运算包括算术运算、字符运算、位运算、赋值运算、 逗号运算、复数运算、关系运算、逻辑运算、条件运算、数组运算、字符串运算、指针运算及 结构体运算等。由于计算机使用整数存储字符数据,因此字符数据也支持算术运算。本 章重点介绍赋值运算、算术运算、字符运算、位运算、逗号运算、类型转换运算以及标准输 入输出库函数和数学库函数的使用,其他运算参见后续相关章节。 计算机程序运行时,装载到内存的程序按照顺序自动执行。C语言定义相应函数实 现特定功能,其函数功能由构成函数的语句完成,语句执行计算、调用、控制等操作。通过 计算机输入设备(如键盘、鼠标等)获得输入信息,通过计算机输出设备(如显示器、打印机 等)实现运算结果的输出。由于输入输出设备种类繁多,为保证C程序的可移植性,并没 有定义专门的C语言指令实现输入和输出,而是通过调用输入输出函数(在不同设备上 定义具体的函数)实现。对于文本型界面的输入输出,C语言通过标准输入输出库函数实 现键盘输入信息和屏幕输出信息。对于图形化的输入输出(如Windows桌面程序和三维 游戏),则通过调用GDI、OpenGL或EGE等图形化函数库实现。 3.2 运算符与表达式 C语言提供了丰富的运算符参与数据的运算处理。运算符与运算对象共同构成了表 达式,运算对象又称为操作数。使用运算符时应注意以下几点。 48 程序设计基础(C 语言)(第4 版) 1.运算符的目 运算符能够链接运算对象的个数称为运算符的目,从这个角度可将运算符分为3类。 (1)单目运算符,只能链接一个运算对象,如++、--、&、[]、!等。 (2)双目运算符,可以链接两个运算对象,如+、-、*、/、>、<、&& 等。 (3)三目运算符,可以链接3个运算对象。C语言只有条件运算符(?:)一个三目运算符。 2.运算符的优先级 优先级是指使用不同运算符进行计算时执行的先后次序。当一个运算对象两侧的运算 符优先级不同时,应遵循优先处理优先级高的运算符的原则。例如,在算术运算中,乘除运 算符的优先级高于加减运算符的优先级。C语言所有运算符的优先级顺序参见附录B。 3.运算符的结合性 结合性是指运算符与运算对象的组合规定,又称为运算符的结合方向。当一个运算 对象两侧连接同一优先级的两个运算符时,需要根据运算符的结合性进行处理。运算符 的结合性分为左结合性和右结合性。如果先结合左边的运算符,称为自左至右结合方向 (左结合性),如算术运算符、关系运算符、逻辑运算符等;如果先结合右边的运算符,称为 自右至左结合方向(右结合性),如赋值运算符、条件运算符及所有单目运算符。 表达式是一个可以计算的算式,其计算过程按照运算符的优先级高低和结合性的方 向顺序进行,同时还要考虑运算对象是否具有相同的数据类型以及是否需要进行类型转 换。每个表达式代表一个确定的值和确定的数据类型。根据运算符,C语言的基本表达 式有以下几种。 . 单目运算表达式:由自增(++)/自减(--)、内存大小运算(sizeof)、取地址运算(&)、 取值运算(*)、正数运算(+)、负数运算(-)、位反运算(~)等运算符构成的表达式。 . 算术运算表达式:由加(+)、减(-)、乘(*)、除(/)和取余(% )等运算符构成的表达式。 . 关系运算表达式:由小于(<)、大于(>)、小于或等于(<=)、大于或等于(>=)、等于 (==)、不等于(!=)等运算符构成的表达式。 . 逻辑运算表达式:由逻辑与(&&)、或(‖)、非(!,也是单目运算符)等运算符构成 的逻辑关系表达式。 . 位移运算表达式:由左移(<<)、右移(>>)等运算符构成的表达式。 . 位逻辑运算表达式:由位与(&)、位或(|)、位异或(^)、位取反(~,也是单目运算 符)等运算符构成的表达式。 . 条件表达式:由运算符(?:)构成的表达式。 . 赋值表达式:由运算符(=)构成的表达式。 . 逗号表达式:由运算符(,)构成的表达式。 此外,C语言的表达式还包括由数组下标运算符[]构成的表达式、指针运算表达式以 及动态数组和动态结构运算表达式等。 当一个表达式中包含多个运算符时,计算时优先计算高级别的运算符。例如,计算表 达式x+y&&z,由于算术运算符优先级高于逻辑运算符优先级,因此先计算x+y,再将x+y 第3 章 基本运算与顺序结构 49 的计算结果与z进行&& 运算。如果希望x与y&&z的结果相加,则需要写成x+(y&&z), 通过小括号提升y&&z运算的优先级。基本表达式一般完成简单的运算,复杂的运算可 以调用函数库实现,常用的函数库包括数学库(math)、复数库(complex)、标准库 (stdlib)、字符串库(string)、字符库(ctype)、宽字节字符库(wchar)等。 特殊地,一个常量、一个变量、一个函数都可以看作一个独立的表达式。例如,printf ("hello!")是一个函数调用表达式。 3.3 赋值运算 C语言中赋值运算符为“=”,赋值表达式由赋值运算符链接表达式(右侧)和变量(左 侧)构成。 语法格式: <变量名>=<表达式> 赋值运算用于将赋值运算符右侧表达式的结果值赋予左侧的变量,表达式可以是常 量、变量、表达式或另外一个赋值表达式。赋值运算符的优先级较低,结合方向为自右向 左。例如: . 表达式a=1表示将常量1赋给变量a。 . 表达式i=i+1表示将变量i中的值加1后重新赋给变量i。 . 表达式x=a*b表示先计算a*b的值,再进行赋值运算,将a*b结果值赋给变量x。 . 表达式a=b=c=4相当于由c=4、b=c和a=b这3个赋值表达式组合而成。变量c 的值为4(c=4),b的值为变量c的值(4),a的值为变量b的值(4),整个表达式的 值为变量a的值(4)。 . 表达式a=(b=1)+(c=5),由于赋值运算优先级低于算术运算优先级,利用加括号 的方式改变了运算次序。首先计算b=1,将1赋值给b,结果值为1;其次计算c=5, 将5赋值给c,结果值为5;最后进行加法运算并将运算结果值赋给a,a的值为6。 进行赋值运算时,应尽量保证赋值运算符两侧的数据类型一致。若不一致,赋值时会 自动将右侧表达式的值转换为与左侧变量相同的类型。 赋值表达式中的“=”不是数学中的等号,它表示将其右侧的值赋予左侧的变量中(左 侧只允许是变量,不能是表达式)。例如表达式x+5=y是一个错误的赋值表达式。 例3-1 编写程序,将键盘输入的两个整数进行交换。 #include int main() { int a,b,temp; /*声明变量,a、b 存放从键盘输入的整数;temp 变量存 储中间结果*/ scanf("%d%d",&a,&b); /*输入两个整数*/ temp=a; /*交换,将整数a 存放在temp 中*/ a=b; b=temp; 50 程序设计基础(C 语言)(第4 版) printf("%d,%d\n",a,b); /*输出交换后的两个整数*/ return 0; } 3.4 算术运算 所有的整型数据、实型数据、字符型数据都支持算术运算,进行算术运算的常用运算 符包括括号、加法(+)、减法(-)、乘法(*)、除法(/)、取余(%)、前置自增(++)/自减(--)、 后置自增(++)/自减(--),以及正运算(+)和负运算(-)。此外,位运算中的左移(<<)与 右移(>>)可以实现乘以2的乘法和除以2的除法操作,复合赋值运算同样支持算术运 算。这些运算符优先级从高到低的顺序如下: (1)括号,结合方向是自左至右,用于提高表达式的优先级。 (2)前置自增(++)/自减(--)、后置自增(+ +)/自减(- -)、正运算(+)、负运算(-), 结合方向为由右向左。 (3)乘法(*)、除法(/)和取余(%),结合方向为自左至右。 (4)加法(+)、减法(-),结合方向为自左至右。 (5)左移(<<)、右移(>>),结合方向为自左至右。 (6)复合赋值运算。 3.4.1 基本算术运算 基本算术运算主要是数学意义上的计算,由算术运算符+、-、*、/、% 和运算对象组 成,运算对象可以是常量、变量或表达式。 语法格式: <运算对象>算术运算符<运算对象> 可以利用括号提高表达式的优先级,例如,“intx=3,y=4,z=5;”,算术表达式x*y+(x +z)/y的计算过程如图3-1所示。遵循C语言对算术表达式计算顺序的运算符优先级规 则,首先计算括号内的值,结果为8;然后按自左向右的结合方向进行乘法和除法运算,结 图3-1 表达式计算顺序 果分别是12和2;最后进行加法运算。最终表达式计算结果 为14。 例3-2 编写程序,计算蛋白质相对分子质量。 蛋白质相对分子质量的计算公式mpr=n*a-18(n-m ),其 中,mpr代表蛋白质的相对分子质量,n 代表构成蛋白质的氨基 酸数,a 为氨基酸的平均相对分子质量,m 为构成蛋白质的肽链条数。 #include int main() { long a,m,n,mpr; scanf("%ld%ld%ld",&a,&n,&m); 第3 章 基本运算与顺序结构 51 mpr=n*a-18*(n-m); printf("mpr=%ld \n ",mpr); return 0; } C语言还提供了语法意义上的正运算(+)和负运算(-)。 语法格式: +表达式 -表达式 正运算(+)不会改变表达式的值,运算符仅用于标明某整数或实数为正数,通常省略 +,例如+100 和100 等同。负运算(-)计算表达式的相反值,例如,“intx=200,y= -300;”,则表达式-(x+y)的值为100。 3.4.2 自增或自减运算 前缀自增(++)/自减(--)、后缀自增(++)/自减(--)运算可以实现整型、实型、字符 型变量以及指针类型变量的自增1或自减1。 语法格式: ++变量名 (前缀) --变量名(前缀) 变量名++ (后缀) 变量名-- (后缀) 自增/自减运算符放置于变量前(左侧)称为前缀运算。前缀运算执行先运算后使用 规则,即将变量先加/减1,然后将其结果值作为表达式值。自增/自减运算符放置于变量 后(右侧)称为后缀运算。后缀运算执行先使用后运算规则,即将变量当前的值参与到表 达式的处理中,然后再对变量值加/减1。例如: int a=10, b=10; x=++a; /*变量a 的当前值10 先加1 变成11 后,将新值11 赋给x(x 值为11)*/ x=b++; /*变量b 的当前值10 赋给x(x 值为10)后,变量b 再加1 变为11*/ 当出现难以区分的若干+或-组成的运算符串时,应按照运算规则从左至右取尽可能 多的符号组成运算符。例如: int x=5, y=5; y=x+++y; /*表达式x+++y 应理解为(x++)+y,先进行x+y 操作,将结果10 赋予y,x 自 增变为6*/ 例3-3 分析程序的输出结果。 #include int main() { int a=1,b=1,c=1; a=a+++b+++c++; /*等价于a=a+b+c;a++;b++;c++*/ printf("%d,%d,%d",a,++b,c++); return 0; } 变量a、b、c以初始化的方式被赋值为1。语句“a=a+++b+++c++;”相当于“a=(a++)+ 52 程序设计基础(C 语言)(第4 版) (b++)+(c++);”语句,先执行加法后再执行赋值操作,a=a+b+c完成后所有变量的值加 1。执行此语句后a=4、b=2、c=2。当执行printf输出函数调用语句时,变量b为前缀运 算,其值自增1变为3后输出,变量c为后缀运算,输出其值2后自增1变为3。 使用自增/自减运算符时应注意几个问题。 . 自增/自减运算只能作用于变量,不允许对常量和表达式进行自增/自减运算。例 如,1++,--(x+y)都是非法表达式。 . 当自增/自减运算独立构成一条语句时,前缀运算和后缀运算的效果相同。例如, “++x;”等价于“x++;”。 . 如果一个表达式中对同一个变量多次进行自增/自减运算,例如语句“a=++a+++ a+++a;”,不仅表达式的可读性差,而且不同编译器对表达式的处理方式不尽相 同,可能导致运算结果不一致。因此,建议自增/自减运算尽可能简单,仅仅用于 单个变量的自增或自减表达式中。可将“a=a+++b+++c++;”改写为“a=a+b+c; a++;b++;c++;”。 3.4.3 整数运算 整数域内所有运算的结果依然为整数。如3/2的结果为1。取余运算是两个整数相 除的余数,如12%5的结果为2。取余运算仅适用于整数运算。 C语言定义的整数类型仅仅涵盖整数域的某一子集,由于不同的数据类型规定了不 同的机内表示长度,决定了对应数据量的变化范围,当某一整数超出此范围时,计算机将 其截取为表示范围内允许的数,这种情况称为溢出处理。假设int型长度为4B,表示int 型数据使用范围应限制为-2147483648~2147483647,可以正确表示-15、0、 2147483647等数据,当表示整数2147483650时发生溢出错误。 例3-4 整型数据的溢出分析。 int main() { short int x, y; x=32767; y=x+1; printf("%d,%d\n",x,y); return 0; } 程序中的x、y都是shortint型变量,计算结果存储在变量y中,如图3-2所示,按 照计算机对整数的编码,最高位为1 时表示负数,说明此数为-32768,造成溢出 错误。 图3-2 计算溢出 第3 章 基本运算与顺序结构 53 本例解决溢出的方法是将变量y声明为整型(int)。但int型同样存在溢出错误,当 需要处理的数据达到10亿数量级时就应该十分小心。 C语言在头文件limits.h中定义了整数的相关边界常量,在stdint.h中补充定义了整 型数据的边界常量,使用时通过预编译指令#include包含相应的头文件。 #include 或 #include "stdint.h" #include 或 #include "limits.h" 常用的整数边界常量如表3-1所示。 表3-1 整数边界常量 类 型最大边界常量最大值最小边界常量最小值 signedchar SCHAR_MAX +127 SCHAR_MIN -127 unsignedchar UCHAR_MAX 255 0 char CHAR_MAX +127 CHAR_MIN -127 shortint SHRT_MAX 215-1 SHRT_MIN -(215-1) unsignedshortint USHRT_MAX 216-1 0 int INT_MAX 231-1 INT_MIN -(231-1) unsignedint UINT_MAX 232-1 0 longint LONG_MAX 231-1 LONG_MIN -(231-1) unsignedlongint ULONG_MAX 232-1 0 longlongint LLONG_MAX 263-1 LLONG_MIN -(263-1) unsignedlonglongint ULLONG_MAX 264-1 0 INTn* INTn_MAX 2n-1-1 INTn_MIN -(2n -1) UINTn UINTn_MAX 2n-1 0 INT_LEASTn INT_LEASTn_MAX 2n-1-1 INT_LEASTn_MIN -(2n -1) UINT_LEASTn UINT_LEASTn_MAX 2n-1 0 INTMAX_MAX 263-1 INTMAX_MIN -(263-1) *n 的取值为8、16、32或64,余同。 3.4.4 实数运算 实数类型可以表示小数。由于实数只能存储有限数字位,因此实数运算时通常存在 舍入误差。实数的精度取决于尾数部分的位数,尾数部分的位数越多,能够表示的有效数 字位越多。例如,单精度数的尾数用23 位存储,加上默认的小数点前1 位,223+1 = 16777216,因为107<16777216<108,所以单精度浮点数的有效位数是7位;双精度的尾 数用52位存储,252+1=9007199254740992,因为1016<9007199254740992<1017,所 以双精度数的有效位数为16位。 例3-5 分析程序中实数的舍入误差。 54 程序设计基础(C 语言)(第4 版) #include int main() { float a,b; double c,d; a=123456.789e5; b=a+20; c=123456.789e5; d=c+20; printf("%f\n%f\n",a,b); printf("%lf\n%lf\n",c,d); return 0; } 运行结果: 12345678848.000000 12345678848.000000 12345678900.000000 12345678920.000000 程序中的a和b为单精度浮点(float)型,有效位数只有7位,其余位相当于无效位, 超出有效位数的加减运算没有实际意义;c和d是双精度型,有效位为16位,双精度 (double)型相对单精度型更接近原数据。 C语言在头文件float.h中定义了各种实数边界常量,常用的符号常量如表3-2所示。 表3-2 实数边界常量 类型最大边界常量最 大 值最小边界常量最 小 值 float FLT_MAX 3.40282347E+38F FLT_MIN 1.17549435E-38F double DBL_MAX 1.7976931348623157E+308 DBL_MIN 2.2250738585072014E-308 使用时通过预编译指令#include包含头文件float.h。 #include 或 #include "float.h" 3.4.5 复合赋值运算 C语言允许在赋值运算符之前加上其他运算符以构成复合的赋值运算符。双目运算 符都可以和赋值运算符一起组合成复合的赋值运算符。复合赋值算术运算符包括+ =、 -=、*=、/=、%=等。利用复合赋值运算符将一个变量和一个表达式链接起来构成复合赋 值表达式。 语法格式: <变量名>复合赋值运算符<表达式> 复合赋值运算的作用等价于 <变量名>=<变量名>运算符<表达式> 例如: a+=5; /*等价于a=a+5;*/ a*=b+5; /*等价于a=a*(b+5);*/ a+=a-=a*a; /*等价于“a=a+(a= a-(a*a));”,假设a 为5,先计算表达式a*a 的结果 (25),再计算表达式a=a-(a*a)的结果(a= 5- 25= - 20),最后计算表达 式a=a+(-20)的结果(-40)*/ 第3 章 基本运算与顺序结构 55 3.5 字符运算 无论表现形式是ASCII字符、宽字节字符还是统一编码字符,字符的本质是使用一 个整数标识一个字符。 3.5.1 算术运算 字符数据作为一个整数,可以参与所有整型数据的运算。例如: char c='A'; int n=0; n=c*100; /*c 作为整数参与c*100 运算*/ printf("\n%d",n); /*输出6500*/ c++; /*参与c++运算*/ printf("\n%c",c); /*输出B*/ 需要注意字符数据参与运算后可能超出其使用范围而产生溢出错误。例如: c=c*100; /*结果值为6500,超出ASCII 范围*/ 例3-6 编写程序,读入一个数字字符,将其转换为对应的整数(例如将字符'1'转换为 整数1)。 #include int main() /*利用字符'1'和字符'0'的ASCII 码相差1 的特点,将两 个字符相减获得整数1*/ { char c; int n=0; c=getchar(); /*读入数字字符*/ n=c-'0'; printf("%d\n",n); /*输出对应整数*/ return 0; } 3.5.2 字符分类 C语言的字符处理函数库提供了判定当前字符类型的相关函数。有关ASCII字符 的分类函数如表3-3所示。使用时通过预编译指令#include包含头文件ctype.h: #include 或 #include "ctype.h" 表3-3 ASCII字符分类函数 函 数 名用 途函 数 名用 途 intisalnum(intc) 是否为英文字母及数字字符intislower(intc) 是否为小写字母 intisalpha(intc) 是否为英文字母intisprint(intc) 是否为可打印字符 intisblank(intc) 是否为空白字符intispunct(intc) 是否为除了英文字母、数字 和空格外的可打印字符 续表 56 程序设计基础(C 语言)(第4 版) 函 数 名用 途函 数 名用 途 intiscntrl(intc) 是否为控制字符intisspace(intc) 是否为空格 intisdigit(intc) 是否为数字字符intisupper(intc) 是否为大写字母 intisgraph(intc) 是否为除空格外的可打印字符intisxdigit(intc) 是否为十六进制字符 例3-7 编写程序,如果读入的字符是英文字母或数字字符,输出数字1,否则输出数 字0。 #include #include int main() { char c=; int n=0; c=getchar(); printf("%d\n",isalnum(c)); /*如果输入字符'A',则输出1;如果输入字符'&',则输出0*/ return 0; } 宽字节字符分类函数的功能及用法与表3-3所示的函数相对应,但是函数名不同,例 如isalpha函数对应的宽字节字符分类函数为iswalpha。使用宽字节字符分类函数时,通 过预编译指令#include包含头文件wctype.h。统一字符编码字符属于宽字节字符的一 种,因此使用宽字节字符的分类函数。 3.5.3 字符转换 字符转换包括字符大小写之间的转换,转换过程既可以利用字符算术运算实现,也可 以通过调用相关库函数实现。针对ASCII码字符转换的库函数为tolower和toupper,对 应宽字节字符函数为towlower和towupper。算术运算则利用小写字母和对应大写字母 之间ASCII码值相差32的特点,在原字符基础上加/减32,获得对应字母。 例3-8 编写程序,将读入的一个大写英文字母转换为一个小写英文字母。 /*方法一: 算术计算实现*/ #include int main() { char c= ; int n=0; c=getchar(); /*读入大写英文字母*/ n=c+32; printf("%c\n",n); /*输出小写英文字母*/ return 0; } /*方法二: 转换函数调用实现*/ #include int main() { char c=; int n=0; c=getchar(); n=tolower(c); printf("%c\n",n); return 0; } 宽字节字符与ASCII字符的转换处理涉及字符串的处理(参见6.6节)。 第3 章 基本运算与顺序结构 57 3.6 位 运 算 位运算是C语言的一种特殊运算,包括逻辑运算和移位运算。位运算以单独的二进 制位作为运算对象,只能对整型或字符型数据进行。 3.6.1 位逻辑运算 位逻辑运算符包括&(按位与运算符)、|(按位或运算符)、^(按位异或运算符)、~(按 位取反运算符)。其中,&、|、^是双目运算符,优先级介于关系运算符与逻辑运算符之间, 结合方向为自左至右;~为单目运算符,优先级高于算术运算符,结合方向为自右至左。 语法格式: <运算对象>位逻辑运算<运算对象> (对于&、|、^) ~<运算对象> 1.按位与运算 按位与运算是将参与运算的两个数据按对应的二进制数逐位进行逻辑与运算。当两 个操作对象二进制数的相同位均为1时,结果数值的相应位为1,否则为0。 例如,int型常量4和7进行按位与运算表示为4&7,运算过程为(仅取数据的后两个 字节分析,下同): 4 (0000000000000100) & 7 (0000000000000111) 4 (0000000000000100) int型常量-4&7的运算过程为: -4 (1111111111111100) & 7 (0000000000000111) 4 (0000000000000100) 按位与运算通常用于对一个数据的某些位清零或保留某些位。例如,保留变量x的 低8位(高8位清零),可以执行运算x&255(255的二进制数为0000000011111111)。 2.按位或运算 按位或运算是将参与运算的两个数据按对应的二进制数逐位进行逻辑或运算。当两 个运算对象二进制数的相同位均为0时,结果数值的相应位为0,否则为1。 例如,int型常量4和7进行按位或运算表示为4|7,运算过程如下: 4 (0000000000000100) | 7 (0000000000000111) 7 (0000000000000111) 按位或运算一般用于将一个数据的某些位置1,数据的其余位保持不变。例如,将变 58 程序设计基础(C 语言)(第4 版) 量x的最低位置1,可以执行运算x|1(1的二进制数为0000000000000001)。 3.按位异或运算 按位异或运算是将参与运算的两个数据按对应的二进制数逐位进行逻辑异或运算。 当两个二进制数的相同位互不相同的时候,对应位的结果为1,否则为0。 例如,int型常量4和7进行按位异或运算表示为4^7,运算过程如下: 4 (0000000000000100) ^ 7 (0000000000000111) 3 (0000000000000011) 按位异或运算可以将一个数的某些位翻转(即原来为1的位变为0,为0的位变为 1),其余位不变。例如,将变量x的后4位取反,可以执行运算x&15(15的二进制数为 0000000000001111)。 4.按位取反运算 按位取反运算是将参与运算的数据按对应的二进制数逐位进行求反运算,即原来为 1的位变成0,原来为0的位变成1。 例如,int型常量7进行按位取反运算表示为~7,其运算过程如下: ~ 7 (0000000000000111) -8 (1111111111111000) 3.6.2 位移运算 位移运算包括左移运算和右移运算,可以实现二进制数值的移位(乘/除)处理。左移 运算符<<和右移运算符>>是双目运算符,其优先级介于算术运算与关系运算之间,结合 方向为自左至右。 语法格式: <运算对象>位移运算符<移动位数> 运算对象可以是整型或字符型常量、具有确定值的变量或表达式。移动位数表示可 以左移或右移的具体位数。 1.左移运算 左移运算的功能是将<<左侧运算对象的二进制数值逐位左移若干位,左移的位数由 <<右侧的数值指定。左侧被移出的位将被舍弃,右侧空出的位补0。例如: int a=5,b; b=a<<2; 由于(a)10=(5)10= (0000000000000101)2,a左移2位后的结果(000000000001 0100)2=(20)10,b的结果为(20)10,a左移两位相当于扩大4倍:b/a=20/5=4=22。 左移会引起数据的变化,左移一位相当于原数据乘以2,左移n 位相当于原数据乘以 2n 。由于位移运算速度比乘法运算速度快很多,处理数据乘法运算时可以采用左移运算。 第3 章 基本运算与顺序结构 59 2.右移运算 右移运算的功能是将>> 左侧运算对象的二进制值逐位右移若干位,右移的位数由 >>右侧的数值指定。右侧被移出的位将被舍弃。例如: int a=5,b; b=a>>2; /*由于(a)10=(5)10=(0000 0000 0000 0101)2,所以b=(0000 0000 0000 0001)2=(1)10*/ 右移同样会引起数据的变化,右移一位相当于原数据除以2,右移n 位则相当于原数 据除以2n 。右移运算时,如果当前的数据为无符号数,左边补0。如果当前的数据为有符 号数,符号位为0(正数)时,左边补0;符号位为1(负数)时,则取决于所使用的系统:补0 称为“逻辑右移”,补1称为“算术右移”。 3.6.3 复合位运算及补位原则 C语言提供的复合位运算符包括&=、!=、> > =、< < = 和^ =。按位取反不存在复合 运算。语 法格式: <变量>复合位运算符<表达式> 例如: a&=0x11; /*等价于a=a&0x11;*/ a>>=2; /*等价于a=a>>2;*/ 按照右端对齐的原则进行不同长度的数据之间的位运算,即按长度最大的数据进行 处理,将数据长度小的数据左端补0或1。例如,chara与intb进行按位运算时,需将字 符a先转化为int型数据,左端补0后再进行位运算。补位原则如下: . 对于有符号数据:如果为正数,则左端补0;如果为负数,则左端补1。 . 对于无符号数据,左端补0。 3.7 逗号运算 逗号也是C语言的一种运算符,称为逗号运算符。逗号表达式由逗号运算符及两个 以上的表达式链接而成。 语法格式: <表达式1>,<表达式2>,…,<表达式n > 逗号运算符的运算对象可以是任何类型的表达式。在所有运算符中,逗号运算符的 优先级最低,结合方向为自左至右。逗号表达式的计算过程是:依次计算< 表达式1>, <表达式2>,…,<表达式n>的值,并将<表达式n> 的值作为整个表达式的结果值,因此 逗号运算又称为顺序求值运算。例如: int a=2,b,c; float x=5.2; 60 程序设计基础(C 语言)(第4 版) b=a, 2*a, 2*x; /*先计算b=a,a 赋值给b(b=2),再计算2*a(其值为4),最后计算 2*x(其值为10.4),表达式结果为10.4(最后一个表达式的值)*/ c=(a=10,b=5,a+b); /*依次计算括号内的逗号表达式a=10、b=5、10+5,并将10+5 赋予 变量c*/ 多数情况下,使用逗号表达式的目的不是获得逗号表达式的最终结果值,而是为了按 顺序分别计算每个表达式的结果值,这种计算在循环结构中经常使用。 3.8 强制类型转换 不同类型的数据在进行混合运算时需要进行类型转换,即将不同类型的数据转换成相 同类型的数据后再进行运算。C语言提供隐式转换和显式转换两种转换方式。隐式转换不 需要人工干预,由编译器自动完成,显式转换则通过强制类型转换运算符明确转换类型。 3.8.1 算术运算中的隐式转换 当不同类型的数据混合进行算术运算时,系统自动转换为同一种数据类型后进行运 图3-3 混合运算的数据类型转换规则 算。自动转换依据“类型提升”的原则,如图3-3 所示。按 照数据类型由低向高的方向转换,首先将 较低类型的数据提升为较高的类型,使二者的数 据类型一致(但数值不变),然后进行计算,其结果 数据的类型是较高类型,以保证不降低精度。数 据类型的高低则是根据类型所占存储空间的大小 和存储范围判定,占用存储空间越大表示数据的 类型越高。算术运算的自动转换规则如下: . 单精度实型数据(float)自动转换成双精度实型数据(double)。 . 字符型数据(char)和短整型数据(short)自动转换成整型数据(int)。 . 整型数据(int)与无符号型数据(unsigned)自动转换成无符号型数据,整型数据 (int)、无符号型数据(unsigned)与长整型数据(long)运算时自动转换成长整型数 据(long)。 . 整型数据(int)、无符号型数据(unsigned)、长整型数据(long)与实型数据(float/ double)都转换成实型数据(double)。 假设x是int型,y是double型,表达式x+a' '+y的计算过程为:首先将字符常量a转 换为int型;按照算术运算规则从左向右计算x+'a',计算结果为整型,将x+'a'的计算结果 自动转换为double型,与y进行加法计算,表达式结果为double型。 3.8.2 赋值运算中的隐式转换 执行赋值运算时,如果赋值运算符两侧的数据类型不同,赋值运算符右侧表达式类型 第3 章 基本运算与顺序结构 61 的数据将转换为左侧变量的类型。即计算出赋值运算符右侧表达式的值后,转换为左侧 变量所属的类型,再赋值给左侧的变量。赋值运算的自动转换规则如下: . 将实型数据赋给int型变量时,舍弃小数部分。例如: int a; a=15.5; /*结果为a=15(数据截取)*/ 将int型数据赋给实型变量时,数值不变,将以实数的形式(在整数后添上小数点及 若干个0)存储到变量中。例如: float a; a=10; /*结果为a=10.0(数据填充)*/ . 将double型数据赋给float变量时,截取其前面7位有效数字存入float变量的存 储单元中。相反,将float型数据赋给double变量时,数值不变,有效位扩展到 16位。 . 将char型数据赋给int型变量时,由于char型数据在运算时根据其ASCII值自 动转换为int型数据,因此只需要将char型数据的ASCII值存储到int型变量低 8位中,高位补0。将int型数据赋给char型变量时,只将其低8位存入char型变 量中。 . 将unsignedint型数据赋给longint型变量时,只需要将高位补0。将unsigned int型数据赋给字节数相同的int型变量时,将unsigned型变量在内存中的内容 原样放入int型变量的内存中。将int型数据赋给longint型变量时,只将int型 数据放入longint型变量的低字节中,高字节全部补充为int型数据的符号位。 不同类型的数据进行赋值运算时,如果右侧数据的类型高于左侧,可能造成一部分数 据丢失,降低数据的精度;也可能发生数据溢出,导致结果错误。 3.8.3 显式转换 一般情况下,编译器自动处理数据的类型转换,称为隐式转换。而利用强制类型转换 运算符将某一类型的数据强制转换为另外一种类型,则称为显式转换。 语法格式: (类型名)<表达式> 显式转换用于强行将表达式的值转换成类型名所表示的数据类型。例如,(int)4.2 的结果是4。显式转换的目的是使表达式值的数据类型发生改变,从而使不同类型数据 之间的运算能够进行下去。 如果表达式仅是单个常量或变量,则常量或变量不必用圆括号括起来;但是如果是含 有运算符的表达式,则必须用括号将其括起来,否则容易发生歧义。例如: (int)a /*表示将变量a 的值强制转换为int 型*/ (int)(a+b) /*表示将表达式a+b 的计算结果强制转换为int 型*/ (int)a+b /*表示将变量a 的值强制转换成int 型后,再与b 进行加运算*/ 显式转换后仅产生一个临时的、类型不同的数据继续参与运算,其常量、变量或表达 式的原有类型以及原来的数据值均不改变。例如: 62 程序设计基础(C 语言)(第4 版) int x=5; float y; y=(float)x/2; /*x 值被强制转换为float 型(5.0)参与运算,运算结果为2.5,但x 的 数据类型仍为int*/ 由于类型转换占用系统时间,过多的转换将降低程序的运行效率。因此设计程序时 应尽量选择适当的数据类型,以减少不必要的类型转换。 3.9 sizeof运算 sizeof运算符用于获取指定数据类型所需要的存储空间大小,其优先级高于双目运算符。 语法格式: sizeof(类型名) 或 sizeof(变量名) sizeof运算的结果是无符号整数,表示存储属于类型名或变量名的值所需要的字节数。 当不了解系统中各种数据类型所占存储单元的字节数时,可利用sizeof运算符获 取。例如,16位机的sizeof(int)的值通常为2(int型数据占用2B存储空间),32位机的 sizeof(int)值为4(int型数据占用4B的存储空间)。sizeof运算还适用于常量、变量或表 达式。例如,float型变量x=5.0,其sizeof(x)的值为4(float型数据占用4B存储空间)。 此外,sizeof运算符也可以与其他运算符共同构成复杂表达式,例如i*sizeof(int)。尽 管sizeof()的写法与库函数调用写法类似,但是sizeof是一个运算符而不是函数。 3.10 标准设备输入输出函数库 计算机发展到今天,麦克风、摄像头、投影仪、体感传感器等输入输出设备层出不穷, 然而键盘、鼠标和显示器仍然是最常用的设备。尽管图形化的输入输出方式已经成为主 流,但是文本型的输入输出方式仍然保留。C语言为文本型的输入输出提供了标准输入 输出函数库,将键盘命名为标准输入设备,用常量stdin代表;将显示器命名为标准输出 设备,用常量stdout代表;实现从键盘输入文本信息和向显示器屏幕输出文本信息。调 用标准输入输出库函数时需要在源程序的开始处使用预编译指令#include包含头文件 stdio.h。 #include 或 #include "stdio.h" 3.10.1 字符输入输出函数 1.ASCII字符输入输出 getchar函数用于单个字符输入,其功能是从标准输入设备(键盘)上读入一个且仅一 个字符,并将该字符作为getchar函数的返回值。 调用格式: 第3 章 基本运算与顺序结构 63 getchar() getchar函数只能接收一个字符而非一串字符。如果输入"abcde",getchar函数也只 接收第一个字符a' '。例如: char ch; ch =getchar(); /*从键盘读入一个字符并将它赋给ch*/ getchar函数得到的字符可以赋给char型变量或int型变量,也可以不赋给任何变量 而作为表达式的一部分。getchar函数不能显示输入的数据,如果希望显示该数据,必须 调用相应的输出函数实现。 putchar用于单个字符输出,其功能是将指定表达式的值(输出项)所对应的字符输出 到标准输出设备(显示器)上,每次只能输出一个字符。 调用格式: putchar(输出项) putchar函数必须带输出项,输出项可以是char型或int型常量、变量、表达式。 例如: char ch; ch=getchar(); putchar(ch); 输出字符常量时必须用单引号括起来,如'\n'、'*'等;输出表达式值时,可以是'a'+32 等代表一个确定字符的形式,不能是表达式的计算值超出字符存储范围的形式,也不能是 字符串形式,a' bc'或"abc"都是错误形式。 2.宽字节字符输入输出 与getchar函数对应的getwchar函数用于读入宽字节字符,与putchar函数对应的 putwchar函数用于输出宽字节字符。使用时通过预编译指令#include包含头文件 wchar.h。例如: wchar_t ch; ch=getwchar(); /*读入宽字节字符*/ putwchar(ch); /*输出宽字节字符*/ 3.10.2 格式化输出函数 printf函数为ASCII字符格式化输出函数,其功能是按用户指定的格式将指定的数 据输出到标准输出设备上。 调用格式: printf("格式控制字符串",输出项列表) 由双引号括起来的格式控制字符串用于指定数据的输出格式,由格式控制字符(格式 转换说明符、标志、域宽、精度)和普通字符组成。格式转换说明符和百分号(%)同时使 用,用以说明输出数据的数据类型,标志、宽度和精度为可选项;普通字符则原样输出。 输出项列表指出输出数据,当有多个输出项时,各输出项之间用逗号分隔。输出项可 64 程序设计基础(C 语言)(第4 版) 以是常量、变量和表达式。例如: printf ("%d,%f\n",a,x+1); /*双引号中的%d 和%f 为格式说明符,逗号和\n 为普通字 符,输出项为a 和x+1*/ 输出项与格式控制字符在类型和数量上必须一一对应。例如,输出项a与%d对应, 表示按int型输出变量a的值;输出项x+1与%f对应,表示按float型输出表达式x+1 的值。当 printf函数没有输出项,而格式控制字符串中只有普通字符时,函数完成的功能是 将双引号中的字符串输出。例如: printf("Hello C programming!"); /*输出字符串"Hello C programming!"*/ printf函数不会自动换行,如果希望“HelloCprogramming!”分行输出,则需要使用 换行符\n。例如: printf("Hello\nC programming!"); 或 printf("Hello\n"); printf("C programming!"); 输出结果为两行信息: Hello C programming! 换行符'\n'作为输出控制字符,其作用是执行printf函数时,其后的输出结果从新一行输 出。如果字符串中忘记使用'\n',即使多次调用printf函数,输出的结果也不能换行。例如: printf("Hello"); printf("C programming!"); 输出结果为一行字符: Hello C programming! 由于双引号、单引号、反斜线等在C语言中有特殊用途,因此如果输出结果中需要包 含这些字符,则必须使用转义字符形式输出。例如: printf("\"Hello C programming!\""); 输出结果为: "Hello C programming!" 当输出程序的计算结果时,需要使用由%开头,后跟若干格式转换说明符的格式控制 字符串,用以说明数据输出的类型、长度、位数等。 语法格式: % [修饰符]格式转换说明符 修饰符为可选项,包括标志修饰符、宽度修饰符、精度修饰符、长度修饰符,用于确定 输出数据的宽度、精度、对齐方式等,以产生更加规范、整齐、美观的数据输出形式。没有 修饰符时,按系统默认设置输出。 1.格式转换说明符 格式转换说明符规定了对应输出项的输出类型,即输出的数据转换为指定的格式输 第3 章 基本运算与顺序结构 65 出。该项不能省略。常用的格式转换说明符及其含义如表3-4所示。 表3-4 格式转换说明符及其含义 格式转换说明符含 义 c 按字符形式输出单个字符 d,i 按十进制整数形式输出带符号整数(正数不输出符号) u 按十进制整数形式输出无符号整数 o 按八进制整数形式输出无符号整数(不输出前缀0) x,X 按十六进制整数形式输出无符号整数(不输出前缀0x) f,F 按十进制小数形式输出单、双精度实数 e,E 按科学记数法输出单、双精度实数,如1.2E1 g,G 根据精度设置输出双精度实数 a,A 按照十六进制形式输出实数,格式为[-]0xh.hhhhp±d,例如9.0输出为0x9p+0 p 输出内存地址,详见第8章 s 按字符串形式输出 % 输出% 格式转换说明符必须与%结合使用,表3-4中的字符只有放在%后面才能作为输出的 格式转换说明符。如果%后的字符未在表3-4中列出,则输出行为不确定。例如: int d=15; printf("d=%d", d); 格式控制字符串d=%d中的第一个d是一个普通字符而不是格式转换说明符,需要 按原字符形式输出,放在%后的第二个d是格式转换说明符,说明在此位置以十进制整数 形式输出变量d的值15。 例3-9 编写程序,用格式转换说明符控制输出格式。 #include int main() { int a1=+400,a2=-400; float b=3.1415926,e=31415.26535898; float g=3.140000; char c='a'; double d=3.1415926535898; printf("a1=%d\n",a1); printf("a1=%o\n",a1); printf("a1=%x\n",a1); printf("a1=%u\n",a1); printf("a2=%d\n",a2); printf("a2=%u\n",a2); printf("b=%f\n",b); printf("e=%e\n",e); printf("g=%g\n",g); printf("d=%f\n",d); printf("c=%c\n",c); /*与putchar 函数功能相当*/ printf("s=%s\n", "Cprogram"); return 0; } 程序输出结果: a1=400 a1=620 a1=190 a1=400 a2=-400 a2=4294966896 b=3.141593 e=3.141527e+004 g=3.14 d=3.141593 c=a s=Cprogram 从输出结果可以看出: 66 程序设计基础(C 语言)(第4 版) . 只有负号(-)会输出,正号(+)不会输出。 . 使用%u格式控制字符串输出正整数时,该数不发生变化;输出负整数时,将该负 整数转换为对应的无符号整数后输出。 . 使用%f、%e、%E输出的值保留6位小数,如果不够6位,则在尾部添0补位。 . 使用%e和%E输出实数时,在指数前输出字母e或E,同时小数点左侧的数字仅保 留一位。 . 双精度数可以用%f格式输出,但仅输出6位小数。 . %g不输出小数部分尾部的0。 2.宽度修饰符和精度修饰符 宽度修饰符的作用是控制输出数据的宽度,用一个位于% 与格式转换说明符之间的 十进制整数表示输出数据的位数,也称为域宽。printf函数中也可以指定输出数据的精度, 以小数点开始,后跟一个十进制整数,放在%与格式转换说明符之间。宽度和精度可以同时 使用,使用形式是“宽度.精度”。常见的宽度修饰符与精度修饰符格式及其含义如表3-5所示。 表3-5 常见的宽度修饰符与精度的修饰符 修饰符及说明格式含 义 %md 以宽度m 输出int型数,不足m 位数时左侧补以空格 %0md 以宽度m 输出int型数,不足m 位数时左侧补以0 %m.nf 以宽度m 输出实型数,小数位数为n位 %ms 以宽度m 输出字符串,不足m 位数时左侧补以空格 %m.ns 以宽度m 输出字符串左侧的n个字符,不足m 位数时左侧补以空格 例3-10 编写程序,按宽度修饰符和精度修饰符要求输出数据。 #include int main() { printf("%3d\n",1); printf("%3d\n",10); printf("%3d\n",100); printf("%3d\n",1000); printf("%0.3d\n",1); printf("%0.3d\n",10); printf("%0.3d\n",100); printf("%0.3d\n",1000); printf("%.3d\n",1); printf("%.3d\n",10); printf("%.3d\n",100); printf("%.3d\n",1000); printf("%7.2f\n",123.4567); printf("%5.2f\n",123.4567); printf("%2.7f\n",123.4567); printf("%5s\n","Cprogram"); printf("%7.3s\n","Cprogram"); printf("%2.6s\n","Cprogram"); return 0; } 程序输出结果: 1 10 100 1000 001 010 100 1000 001 010 100 1000 123.46 123.46 123.4567000 Cprogram Cpr Cprogr