基本运算符
赋值运算符: =
在C语言中,=并不意味着“相等”,而是一个赋值运算符。下 面的赋值表达式语句:
bmw = 2002;
把值2002赋给变量bmw。也就是说,=号左侧是一个变量名,右侧是赋给该变量的值。符号=被称为赋值运算符。
另外,上面的语句不读作“bmw等于2002”,而读作“把值2002赋给变量bmw”。赋值行为从右往左进行。
也许变量名和变量值的区别看上去微乎其微,但是,考虑下面这条常用的语句:
i=i+l;对数学而言,这完全行不通。如果给-一个有限的数加上1,它不可能“等于”原来的数。但是,在计算机赋值表达式语句中,这很合理。该语句的意思是:找出变量i的值,把该值加1,然后把新值赋值变量i 。
在C语言中,类似这样的语句没有意义(实际上是无效的): 2002 = bmw;
因为在这种情况下,2002被称为右值(rvale), 只能是字面常量。不能给常量赋值,常量本身就是它的值。因此,在编写代码时要记住,=号左侧的项必须是-一个变量名。实际上,赋值运算符左侧必须引用一个存储位置。最简单的方法就是使用变量名。不过,后面章节还会介绍“指针”,可用于指向一个存储位置。
概括地说,C使用可修改的左值(modifable lvalue) 标记那些可赋值的实体。也许“可修改的左值”不太好懂,我们再来看一-些定义。
几个术语:数据对象、左值、右值和运算符
赋值表达式语句的目的是把值储存到内存位置上。用于储存值的数据存储区域统称为数据对象(dataobject)。C标准只有在提到这个概念时才会用到对象这个术语。使用变量名是标识对象的一-种方法。除此之外,还有其他方法,但是要在后面的章节中才学到。例如,可以指定数组的元素、结构的成员,或者使用指针表达式(指针中储存的是它所指向对象的地址)。左值(lvalue) 是C语言的术语,用于标识特定数据对象的名称或表达式。因此,对象指的是实际的数据存储,而左值是用于标识或定位存储位置的标签。
对于早期的C语言,提到左值意味着:
1.它指定一个对象,所以引用内存中的地址;
2.它可用在赋值运算符的左侧,左值(Ivalue) 中的I源自left.
但是后来,标准中新增了const限定符。用const创建的变量不可修改。因此,const 标识符满足上面的第I项,但是不满足第2项。-方面C继续把标识对象的表达式定义为左值,一方面某些左值却不能放在赋值运算符的左侧。有些左值不能用于赋值运算符的左侧。此时,标准对左值的定义已经不能满足当前的状况。
为此,C标准新增了一个术语:可修改的左值(modifiable lvalue),用于标识可修改的对象。所以,赋值运算符的左侧应该是可修改的左值。当前标准建议,使用术语对象定位值(object locator value)更好。
右值(rvalue)指的是能赋值给可修改左值的量,且本身不是左值。 例如,考虑下面的语句: .
bmw = 2002;
加法运算符: +
加法运算符(addition operator)用于加法运算,使其两侧的值相加。例如,语句:
printf (“&d”, 4 + 20) ;
打印的是24,而不是表达式
4+20
相加的值(运算对象)可以是变量,也可以是常量。因此,执行下面的语句:
income = salary + bribes;
计算机会查看加法运算符右侧的两个变量,把它们相加,然后把和赋给变量income。,
在此提醒读者注意,income、salary和bribes都是可修改的左值。因为每个变量都标识了一个可
被赋值的数据对象。但是,表达式salary + brives 是一一个右值。
减法运算符: -
减法运算符(subtraction operator)用于减法运算,使其左侧的数减去右侧的数。例如,下 面的语句把
200.0赋给takehome:
takehome = 224.00 - 24.00; “
+和-运算符都被称为二元运算符(binaryoperator),即这些运算符需要两个运算对象才能完成操作。
符号运算符: -和+
减号还可用于标明或改变-一个值的代数符号。例如,执行下面的语句后,smokey 的值为12:
rocky = -12;
smokey = - rocky;
以这种方式使用的负号被称为一元运算符(unary operator)。-元运算符只需要一一个运算对象(见图5.2)。
C90标准新增了- - 元+运算符,它不会改变运算对象的值或符号,只能这样使用:
dozen = +12;
编译器不会报错。但是在以前,这样做是不允许的。
5.2.5乘法运算符: *
符号表示乘法。下面的语句用2.54乘以inch,并将结果赋给cm:
cm = 2.54 inch;
C没有平方函数,如果要打印一个平方表,怎么办?如程序清单5.4所示,可以使用乘法来计算平方。
/ squares.c —计算1~20的平方/
#include
int main (void)
{
int num = 1;
while (num < 21)
printf (“号4d各6d\n”, num,num num) ;
num=num+1;
return 0;
}
5.2.6
除法运算符: /
C使用符号/来表示除法。/左侧的值是被除数,右侧的值是除数。例如,下面 four的值是4.0:
four = 12.0/3.0;
整数除法和浮点数除法不同。浮点数除法的结果是浮点数,而整数除法的结果是整数。整数是没有小
数部分的数。这使得5除以3很让人头痛,因为实际结果有小数部分。在C语言中,整数除法结果的小数
部分被丢弃,这- -过程被称为截断(truncation)。
运行程序清单5.6中的程序,看看截断的情况,体会整数除法和浮点数除法的区别。
程序清单5.6 divide.c 程序
/ divide.c —演示除法*/
#include
int main (void)
{
printf (“integer division:
5/4
is8d\n”,5/4);
printf (“integer division: 6/3
is8d\n”,6/3);
printf (“integer division:
7/4
is号d\n”,7/4);
printf (“floating division: 7./4. is 81.2f \n”, 7. / 4.);
printf (“mixed division:
7./4 is 81.2f \n”,7. / 4) ;
return 0;
}
运算符优先级
//可补充
sizeof 运算符
sizeof运算符和size t类型
读者在第3章就见过sizeof运算符。回顾一-下,sizeof 运算符以字节为单位返回运算对象的大小
(在C中,1字节定义为char类型占用的空间大小。过去,1字节通常是8位,但是一些字符集可能使用
更大的字节)。运算对象可以是具体的数据对象(如,变量名)或类型。如果运算对象是类型(如,float),
则必须用圆括号将其括起来。程序清单5.8演示了这两种用法。
程序清单5.8 sizeof.c 程序
// sizeof.c —使用sizeof运算符
//使用C99新增的号zd转换说明—如果编译器不支持8zd,请将其改成8u或各lu
#include
int main (void)
{
intn=0;
size t intsize;
intsize = sizeof
(int) ;
printf(“n =号d,n has号zd bytes; all ints have各zd bytes. \n”,
n,sizeof n, intsize) ;
return 0;
}
C语言规定,sizeof返回size t类型的值。这是-一个无符号整数类型,但它不是新类型。前面介
绍过,size t是语言定义的标准类型。C有一个typedef机制(第14章再详细介绍),允许程序员为现
有类型创建别名。例如,
typedef double real;
这样,real 就是double的别名。现在,可以声明一个real类型的变量:
real deal; // 使用typedef
编译器查看real时会发现,在typedef声明中real已成为double的别名,于是把deal创建为
double类型的变量。类似地,C头文件系统可以使用typedef 把size t作为unsigned int或
unsigned long的别名。这样,在使用size_ t类型时,编译器会根据不同的系统替换标准类型。
C99做了进一步调整,新增了号zd转换说明用于printf()显示size t类型的值。如果系统不支,
持8zd,可使用8u或81u代替8zd。
取余运算符(求模运算符)
求模运算符( modulus operator) 用于整数运算。求模运算符给出其左侧整数除以右侧整数的余数
(remainder)。例如,13各5 (读作“13求模5”)得3,因为13比5的两倍多3,即13除以5的余数
是3。求模运算符只能用于整数,不能用于浮点数。
递增运算符与递减运算符
include
#include
int main(void)
{
int a = 12;
int b = 12;
int c = a++; //取12的值;再参与运算,把12赋值给了c此时的c是12;运算后再+1;(注意打印的时候也是12)
int d = ++b; //自加1再取值,取13再运算,把13赋值给b;
int q = 10;
int w = 10;
int e = q—; //取10的值,再参与运算,把10赋值给了e此时的e是10,运算后再-1。(注意打印的时候也是10)
int r = —w; //先运算,10自减后变成9,再运算,把9赋值给了r。
#include
int main(void)
{
int a = 12;
int b = 12;
int c = a++; //取12的值;再参与运算,把12赋值给了c此时的c是12;运算后再+1;(注意打印的时候也是12)
int d = ++b; //自加1再取值,取13再运算,把13赋值给b;
int q = 10;
int w = 10;
int e = q—; //取10的值,再参与运算,把10赋值给了e此时的e是10,运算后再-1。(注意打印的时候也是10)
int r = —w; //先运算,10自减后变成9,再运算,把9赋值给了r。
//a++ ;a取值后,a的值再自增1;先取值再运算。 注:运算就是是赋值(赋值运算符)<br /> //++a ;a自增1后,再取值;先运算,再取值<br /> //a-- ;a取值后,a的值再自减1;先取值再运算。<br /> //--a ;a自减1后,再取值;先运算,再取值
printf(" %d\n %d\n", c, d); //后置++比前置++的优先级要来的高<br /> printf(" %d\n %d\n", e, r);
system("pause");<br /> return 0;<br />}
表达式
表达式(expression)由运算符和运算对象组成(前面介绍过,运算对象是运算符操作的对象)。最简单
的表达式是一个单独的运算对象,以此为基础可以建立复杂的表达式。下面是一 些表达式:
4
-6
4+21
a(b + c/d) /20
q=52
x=++q各3
q>3
如你所见,运算对象可以是常量、变量或二者的组合。- -些 表达式由子表达式(subexpression)组成(子
表达式即较小的表达式)。例如,c/d 是上面例子中a(b + c/d) /20的子表达式。
每个表达式都有一个值
C表达式的一个最重要的特性是,每个表达式都有一个值。要获得这个值,必须根据运算符优先级规
定的顺序来执行操作。在上面我们列出的表达式中,前几个都很清晰明了。但是,有赋值运算符(=)的表
达式的值是什么?这些表达式的值与赋值运算符左侧变量的值相同。因此,表达式q = 52 作为一个整体
的值是10。那么,表达式q > 3的值是多少?这种关系表达式的值不是0就是1,如果条件为真,表达
式的值为1;如果条件为假,表达式的值为0。表5.2列出了一些表达式及其值:
虽然最后一个表达式看上去很奇怪,但是在C中完全合法(但不建议使用),因为它是两个子表达式的
和,每个子表达式都有-一个值。
语句
语句(statement) 是C程序的基本构建块。一条语句相当于一条完整的计算机指令。在C中,大部分
语句都以分号结尾。因此,
legs = 4
120
5.4
表达式和语句
只是一个表达式(它可能是一一个较大表达式的一部分),而下面的代码则是一条语句:
legs = 4;
最简单的语句是空语句:
//空语句.
C把末尾加上一个分号的表达式都看作是一- 条语句(即,表达式语句)。因此,像下面这样写也没问题:
8;
3+4;
但是,这些语句在程序中什么也不做,不算是真正有用的语句。更确切地说,语句可以改变值或调用
函数:
X=25;
++x;
Yy = sqrt(x) ;
虽然一条语句(或者至少是一- 条有用的语句)相当于一条完整的指令,但并不是所有的指令都是语句。
考虑下面的语句:
x=6+(y=5);
该语句中的子表达式y = 5是一条完整的指令,但是它只是语句的一部分。因为- .条完整的指令不一
定是一条语句,所以分号用于识别在这种情况下的语句(即,简单语句)。
到目前为止,读者已经见过多种语句(不包括空语句)。程序清单5.13演示了一些常见的语句。
下面我们讨论程序清单5.13。 到目前为止,相信读者已经很熟悉声明了。尽管如此,我们还是要提醒
读者:声明创建了名称和类型,并为其分配内存位置。注意,声明不是表达式语句。也就是说,如果删除
声明后面的分号,剩下的部分不是一个表达式,也没有值:
int port /不是表达式,没有值/
赋值表达式语句在程序中很常用:它为变量分配-一个值。赋值表达式语句的结构是,- 一个变量名,后
面是一个赋值运算符,再跟着一个表达式,最后以分号结尾。注意,在while循环中有一个赋值表达式语
句。赋值表达式语句是表达式语句的一一个示例。
1根据C 标准,声明不是语句。这与C++有所不同。一译者注
2
在C语言中,赋值和函数调用都是表达式。没有所谓的“赋值语句”和“函数调用语句”,这些语句实际上都是表达
式语句。本书将“assignment statement”均译为“赋值表达式语句”,以提醒读者注意。一译者注
复合语句
复合语句(compound statement)是用花括号括起来的一条或多条语句,复合语句也称为块( block)。
shoes2.c程序使用块让while语句包含多条语句。比较下面两个程序段:
/程序段1/
index = 0;
while (index++ < 10)
sam=10index+2;
printf(“sam = 8d\n”, sam) ;
/程序段2/
index = 0;
while (index++ < 10)
{
sam=10index+2
printf (“sam = 8d\n”, sam) ;
}
程序段I, while循环中只有- - 条赋值表达式语句。没有花括号,while语句从while这行运行至下
一个分号。循环结束后,printf() 函数只会被调用-一次。
程序段2,花括号确保两条语句都是while循环的一部分,每执行—次循环就调用-次printf()函数。
根据while语句的结构,整个复合语句被视为一条语句(见图5.7)。
总结表达式和语句
表达式:
表达式由运算符和运算对象组成。最简单的表达式是不带运算符的一个常量或变量(如,22或
beebop)。更复杂的例子是55 + 22 和vap = 2 (vip + (vup = 4)).
语句:
到目前为止,读者接触到的语句可分为简单语句和复合语句。简单语句以一个分号结尾。如
下所示:
赋值表达式语句:
toes = 12;
函数表达式语句:
printf (“号d\n”, toes) ;
空语句:
/什么也不做/ .
复合语句( 或块)由花括号括起来的一条或多条语句组成。如下面的while语句所示:
while (years < 100)
{
wisdom = wisdom 1.05;
printf (“8d 8d\n”,years, wisdom) ;
years = years + 1;
}
总结 c的一些运算符
下面是我们学过的一些运算符。
赋值运算符:
= 将其右侧的值赋给左侧的变量
== 运算符是c的相等运算符 注 别把=与==混淆
算术运算符:
- + 将其左侧的值与右侧的值相加
- - 将其左侧的值减去右侧的值
- - 作为一元运算符,改变其右侧值的符号
- * 将其左侧的值乘以右侧的值
- / 将其左侧的值除以右侧的值,如果两数都是整数,计算结果将被截断
- % 当其左侧的值除以右侧的值时,取其余数(只能应用于整数)
- ++ 对其右侧的值加1 (前缀模式),或对其左侧的值加1 (后缀模式)
- — 对其右侧的值减1 (前缀模式),或对其左侧的值减1 (后缀模式)
其他运算符:
sizeof
获得其右侧运算对象的大小(以字节为单位),运算对象可以是一个被圆括号括
起来的类型说明符,如sizeof(float),或者是一个具体的变量名、数组名等,
如sizeof foo
(类型名)
强制类型转换运算符将其右侧的值转换成圆括号中指定的类型,如(float)9把
整数9转换成浮点数9.0
关键概念
C通过运算符提供多种操作。每个运算符的特性包括运算对象的数量、优先级和结合律。当两个运算符共享一个运算对象时,优先级和结合律决定了先进行哪项运算。每个C表达式都有一个值。如果不了解运算符的优先级和结合律,写出的表达式可能不合法或者表达式的值与预期不符。这会影响你成为一名优秀的程序员。虽然C允许编写混合数值类型的表达式,但是算术运算要求运算对象都是相同的类型。因此,C会进行自动类型转换。尽管如此,不要养成依赖自动类型转换的习惯,应该显式选择合适的类型或使用强制类型转换。这样,就不用担心出现不必要的自动类型转换。
小结
C语言有许多运算符,如本章讨论的赋值运算符和算术运算符。- -般而言,运算符需要-一个或多个运算对象才能完成运算生成-一个值。只需要一个运算对象的运算符(如负号和sizeof)称为一元运算符,需要两个运算对象的运算符(如加法运算符和乘法运算符)称为二元运算符。
表达式由运算符和运算对象组成。在C语言中,每个表达式都有一个值,包括赋值表达式和比较表达式。运算符优先级规则决定了表达式中各项的求值顺序。当两个运算符共享-一个运算对象时,先进行优先级高的运算。如果运算符的优先级相等,由结合律(从左往右或从右往左)决定求值顺序。
类型转换
通常,在语句和表达式中应使用类型相同的变量和常量。但是,如果使用混合类型,C不会像Pascal那样停在那里死掉,而是采用一套规则进行自动类型转换。虽然这很便利,但是有- -定的危险性,尤其是在无意间混合使用类型的情况下(许多UNIX系统都使用lint 程序检查类型“冲突”。如果选择更高错误级别,许多非UNIXC编译器也可能报告类型问题)。最好先了解一些基本的类型转换规则。
1.当类型转换出现在表达式时,无论是unsigned还是signed的char和short都会被自动转换成int,如有必要会被转换成unsigned int (如果short与int的大小相同,unsigned short就比int大。这种情况下,unsigned short会被转换成unsigned int)。在K&R那时的C中,float会被自动转换成double(目前的C不是这样)。由于都是从较小类型转换为较大类型,所以这些转换被称为升级(promotion)。
2.涉及两种类型的运算,两个值会被分别转换成两种类型的更高级别。
3.类型的级别从高至低依次是long double、 double、 float、 unsignedlong long、long
long、unsigned long、long、 unsigned int、int。例外的情况是,当long和int的大小相同
时,unsigned int比long的级别高。之所以short和char类型没有列出,是因为它们已经被升级到
int或unsigned int。
4.在赋值表达式语句中,计算的最终结果会被转换成被赋值变量的类型。这个过程可能导致类型升级
或降级(demotion)。 所谓降级,是指把一种类型转换成更低级别的类型。
5.当作为函数参数传递时,char和short被转换成int, float被转换成double。第9章将介绍,
函数原型会覆盖自动升级。
类型升级通常都不会有什么问题,但是类型降级会导致真正的麻烦。原因很简单:较低类型可能放不
下整个数字。例如,一个8位的char类型变量储存整数101没问题,但是存不下22334。
如果待转换的值与目标类型不匹配怎么办?这取决于转换涉及的类型。待赋值的值与目标类型不匹配
时,规则如下。
1.目标类型是无符号整型,且待赋的值是整数时,额外的位将被忽略。例如,如果目标类型是8位
unsigned char,待赋的值是原始值求模256。
2.如果目标类型是一个有符号整型,且待赋的值是整数,结果因实现而异。
3.如果目标类型是-一个整型,且待赋的值是浮点数,该行为是未定义的。
如果把—个浮点值转换成整数类型会怎样?当浮点类型被降级为整数类型时,原来的浮点值会被截断。
例如,23.12 和23.99都会被截断为23,-23.5 会被截断为-23。
强制类型转换运算符
通常,应该避免自动类型转换,尤其是类型降级。但是如果能小心使用,类型转换也很方便。我们前面讨论的类型转换都是自动完成的。然而,有时需要进行精确的类型转换,或者在程序中表明类型转换的意图。这种情况下要用到强制类型转换(cast), 即在某个量的前面放置用圆括号括起来的类型名,该类型名即是希望转换成的目标类型。圆括号和它括起来的类型名构成了强制类型转换运算符(castoperator),
其通用形式是:(type)
用实际需要的类型(如,long)替换type即可。
考虑下面两行代码,其中mice是int类型的变量。第2行包含两次int强制类型转换。
mice = 1.6 + 1.7;
mice = (int)1.6 + (int) 1.7;
第1行使用自动类型转换。首先,1.6和1.7相加得3.3。然后,为了匹配int 类型的变量,3.3被类型转换截断为整数3。第2行,1.6和1.7在相加之前都被转换成整数(1),所以把1+1的和赋给变量mice。
本质上,两种类型转换都好不到哪里去,要考虑程序的具体情况再做取舍。
一般而言,不应该混合使用类型( 因此有些语言直接不允许这样做),但是偶尔这样做也是有用的。C语言的原则是避免给程序员设置障碍,但是程序员必须承担使用的风险和责任。