1
七、算术复合赋值表达式八、自增与自减运算符表达式九、逗号运算符表达式十、表达式的副作用十一、位操作运算符
2
七、算术复合赋值表达式算术复合赋值运算是算术运算与赋值运算的结合,操作数与相应算术运算符的操作数类似,但复合赋值要求左操作数是左值,其运算的结果为左值表达式,结果类型为左操作数的数据类型。
结果为左值的含义是指整个表达式可以作为一个操作数出现在需要左值的地方。
赋值运算是右结合的,算术复合赋值运算的语法形式为:
左值 运算符 表达式 lvalue@=expre
3
复合赋值表达式,lvalue @=expre 一般可理解为:
lvalue = lvalue @ expre
例如,lvalue- =expre
一般可理解为,lvalue= lvalue - expre
但复合赋值运算简洁优美,运算直截了当。复合赋值运算求出右操作数的值,然后将结果直接作用到左操作数上,左操作数 lvalue仅计算一次,有特别快的运行效率;
而 lvalue=lvalue- expre中 lvalue被求值两次。
两者并不完全等价。在程序设计中优先使用
lvalue-=expre,少用 lvalue = lvalue-expre形式。
4
下面具体予以介绍
直接加赋值,左值 += 表达式 lvalue+=expre
将右操作数的值直接加到左操作数上。 例如:
int x=3,y=4;
x+=y;
//在 x既有的基础上加上 y,结果 x=7就在 x中
[例 ] (x+=2)+=1中的 (x+=2)仅求值一次,(x+=2)=(x+=2)+1中的 (x+=2)被求值 2次。
#include <stdio.h>
void main (void)
{ int x=0; (x+=2)+=1; printf ("%d\n",x);
x=0; (x+=2)= (x+=2)+1; printf ("%d",x);
}
5
直接减赋值,左值 - = 表达式 lvalue-=expre
将左操作数直接减去右操作数的值,差直接存入左操作数中。 例如,int x=3,y=4; y-=x;
对于同类型数据 x,y和相同的初始值,表达式 y-x和 y-=x
具有相同的值。但表达式 y-x是右值,y值不变,而表达式 y-
=x是左值,运算后 y值发生变化。加减赋值运算的两个操作数可为算术类型。但如果 lvalue为指针,则 expre是整型。
直接乘赋值,左值 *=算术表达式 lvalue*=arithmetic
两个操作数为算术类型,将右操作数直接乘左操作数,
积立即送入左操作数中。 如,x*=y+z
6
直接除赋值,左值 /=算术表达式 lvalue/=arithmetic
两个操作数为算术类型,将右操作数直接除左操作数,
商立即送入左操作数中。 如,z/=x*y
直接取模赋值,整型左值 %=整型表达式
intLvalue%=integer
两个操作数都是整型,将右操作数直接除左操作数,两者的余数立即送入左操作数中。
复合赋值 x+=e与简单赋值 x=e的共同点是运算都是右结合的,但复合赋值要求预先给定左值的初始值,运算具有累积的性质。简单赋值适宜于变量的初始化且变量具有新的开始。
7
右值表达式 x*e 和左值复合赋值表达式 x*=e执行相同的运算,表达式 x*e 不改变 x,e的值,表达式 x*=e改变 x的值。
表达式 x-=e的类型是左操作数 x的类型,表达式 x-e的类型是 x和 e中保值精度更强的类型。
例如,x是 long类型,e是 double类型,x-e的类型是
double类型,而 x-=e的类型是 long类型。
设 {long x=9;double e=2.8;},则 x/e的结果为 3.214286
而 x/=e的结果为 3。此时有 sizeof(x/e)=8,sizeof(x/=e)=4。
8
八、自增与自减运算符表达式自增与自减运算符是单目运算符,操作数要求是左值。
不同的是后自增运算和后自减运算的结果为右值表达式,先自增运算和先自减运算的结果为左值表达式。
后自增与后自减运算符亦称为后置运算符。
先自增与先自减运算符亦称为前置运算符。
前置运算与后置运算要求的左值操作数可以是算术型的数据或指针型的数据,对于算术型的数据系统简单的在原先的值上加 1或 1.0,对于指针类型增加一个类型步长。
9
先自增与先自减运算符的语法格式为 (前置运算的运算符
++,--前置于操作数 ):
++lvalue(先自增) --lvalue(先自减)
//前置运算的结果是变化后的左值
++lvalue相当于 lvalue+=1,--lvalue相当于 lvalue-=1。
先自增运算与先自减运算直接对变量进行增 1减 1,运算的结果是改变后的左值。期间无须临时变量。
前置运算 --++lvalue 没有太特殊的地方,++lvalue相当于 lvalue+=1 但是 ++lvalue运行效率更快。前置运算是和谐的,是连贯的。 例如 语句序列 {y=4; x=++y; } 等价于 {y=4;
++y; x=y; },此时 x=5,y=5,语句序列 {y=4; x=-++y; }等价于 {y=4; ++y; x=-y; },结果为 x=-5,y=5。
10
后自增与后自减运算符的语法格式为:
lvalue++(后自增) lvalue- - (后自减 )
//后置运算的结果是变化前的右值
lvalue++相当于 tmp=lvalue,然后 lvalue+=1,表达式
lvalue++的结果值是 tmp的值。
lvalue- -相当于 tmp=lvalue,然后 lvalue-=1,表达式
lvalue--的结果值是 tmp的值。
后自增与后自减运算先使用变量 lvalue的值,然后将变量 lvalue的值增减 1个单位。
例如 语句序列 {y=4 ; x=y++;} {y=4; x=y; y++;},
此时 x的值为 4,y的值为 5。
语句序列 { y=4 ; x=-y++; } { y=4 ; x=-y;y++; }
即 x的值为 -4,y的值为 5。
11
对于表达式,z+++y在 C/C++编译器中后置运算符的运算优先级高于前置运算符。编译器根据运算的优先级别与后置运算符的特点理解为 (z++)+y或 z++ + y,即表达式的结果为:
z+y //z是变化前的值
z+=1;
例如 语句块 {int z=1;int y=z+++2; printf ("%d,%d\n",y,z);}
输出结果 3,2。
后置运算 x++与前置运算 ++x性质上是不同的,前置运算符是右结合的存在 ++++x。而表达式 ++x++不能通过左右值的适应性要求。后置运算的优先级高,++x++处理为
++(x++),x++结果为右值,前置运算符需要左值操作数。
12
类似地不存在 x++++。存在 ++x=3,但 x++=3是错误的,
因为 x++的结果是右值,而赋值运算符要求左值操作数。可以用空格明确分隔运算符如,a++ +b表示表达式 a++加上 b
的值,a+ ++b表示 a加上表达式 ++b的值,类似地 --b+ ++a
表示 --b加上 ++a的值,而 --b+ + +a表示 --b加上 +a的值。
C++是根据后置运算符命名的,先有 C,先使用 C,然后才是 C++。后置运算表达式的结果是变化前的值,结果为右值表达式,然后左值操作数的值变动 1个单位。
自增与自减运算改变操作数的值,这也是在各种运算中特别改变左值的地方,值得高度警惕,表达式求值的副作用亦常来源于自增与自减运算,特别是后置运算。
13
九、逗号运算符表达式逗号运算符与操作数或函数调用相连构成逗号表达式,
其语法格式为:
操作数 1,操作数 2,...,操作数 n e1,e2,…,en
逗号运算符严格地从左到右依次顺序计算操作数 1,操作数 2,...,操作数 n。每一个操作数依次完成相应的副作用。
逗号表达式的类型和结果是操作数 n的类型和值。
例如,对于定义 {int x;float y;double z;},表达式
(x=5,y=7,z=9)的结果就是 double型的变量 z,值为 9,表达式 (y=4,x=5)的值是 5,类型为 int。 (y=7,y*=6,y+=10)具有值
52,类型为 float。
14
逗号运算 (e1,e2,…,en) 相当于 {e1;e2;…; en;} 。逗号运算的各操作数应导致内存数据改变才有意义。
例如,x=(3+5,5*x,x+4)中的操作数 3+5,5*x对于结果
x=x+4是无意义的。而表达式 (x=3+5,5*x,x+4)的值是 12,
中间的操作数 5*x对于结果 12是无意义的空运算。
逗号运算符的优先级别是最低的,两边的圆括号一般不宜省略。 例如:
int k=1,j=2,n=3;
等价于 int k=1;
int j=2;
int n=3;
int s= ( j+=k,n+=j);
15
由于逗号运算顺序求值,因此上面定义语句包含的逗号表达式可以分解为:
j+=k; n+=j; int s=n;
省去圆括号的语句 {int k=1,j=2,n=3;int s= j+=k,n+=j;}
系统理解为:
int k=1;int j=2;
int n=3; int s= j+=k ; int n+=j;
在一个程序段为一个变量定义了两次,因而是语法错误。在 C++中如果逗号表达式中最后的表达式是左值,则逗号表达式的结果是左值。 例如:
int a,b,c; //定义 int型变量 a,b,c
(a=1,b=a+1,c)=6; //等价于 a=1;b=a+1;c=6;
16
逗号运算中的操作数可以是 void型的函数调用。 例如 下面的代码输出 2,3:
int x=0,y=1; /*定义全局变量 x,y*/
void f (int n) { x=n; }
void main (void)
{( f (2),y)+=2; printf ("%d,%d",x,y); }
在函数调用实参列表中的逗号用于分隔实参,不是逗号运算符。以下函数调用是不等价的,
void func (int,int);
void func (int); //同名函数原型
func (k,j); //调用双参数函数 func(int,int)
func ((k,j)); // 调用单参数函数 func(int)
17
逗号运算符常用于 for语句的循环中:
for (j=k=0; j+k<100; j+=k,k++ ) {;...;}
在上例中逗号表达式 j+=k,k++中的 j+=k首先求值,然后计算 k++。
18
十、表达式的副作用表达式由于求值次序不同而结果不同的现象称为表达式严格意义上的副作用,本书将表达式求值的结果导致左值改变的情况也称为表达式的副作用。
表达式的副作用是由于简单赋值、复合赋值、自增自减运算和函数调用的不确定性引起的。
表达式的求值遵循运算符优先级和结合性所规定的次序,优先级高的先完成求值计算。
运算符构成的复杂表达式必须分解成一个一个简单的操作指令序列,对于这种分解的具体次序 C/C++语言没有规定其先后。因此不可逆料的副作用乘虚而入。
19
例如 表达式语句:
(++y) *= 3+(++y) ;
在 C/C++中没有规定复合赋值两边作为基本表达式 (++y)
的求值次序,在转换到汇编语言的时候存在多种分解的运算序列,下面是可能的两种,(设初始值 int y=0)
1.?++y;?++y;? int tmp=3+y;? y*= tmp;
此时具体地有,(vc6.0输出结果 )y=10
2.?++y;? int tmp=3+y;?++y;? y*= tmp;
此时为,(Builder5.0输出 ) y=8
编译器分解的次序不同导致 y具体值的不同,这就是副作用。
20
同样表达式:
x=(z+=a)*k+j*(z+=b);
其结果也是不确定的。对于下面两种可能的分解序列将导致不同的结果:
1.?z+=a;? t1=z*k;?z+=b;? t2=j*z;?x=t1+t2;
2.?z+=a;?z+=b;?t1=z*k;? t2=j*z;?x=t1+t2;
表达式 z+=b和 t1=z*k等是简单表达式,而 (z+=a)*k+j*(z+=b)
是复杂表达式。
编译器拥有自由以适当次序分解复杂表达式成为简单表达式,以便一步一步地得出各表达式的值。
21
函数实参表达式的求值次序根据习惯是从右到左,这种习惯不是语言的规定。编译器对于相同的表达式可以进行优化统一处理。下面例题的输出结果说明这一点。
[例 ] 一个副作用的运算
# include<stdio.h>
void main()
{ int z=1;
printf ("%d,%d,%d\n",z++,-z++,z++); // 1,-1,1 [3,-2,1]
printf ("%d,%d,%d\n",z++,z,-z++); // 4,4,-4 [5,5,-4]
z=1; printf ("%d\n",(z++)+(z++)+(z++)); // 3 [6 ]
z=1; printf ("%d,%d,%d\n",++z,++z,++z); // 4,3,2 [4,3,2]
int x=4; printf("%d\n",(++x)+(++x)+(++x)); // 19 [18] }
22
C/C++不规定也不宜硬性规定复杂表达式幕后还原的具体先后初始求值次序。
这就给编译器一定的自由空间,在首先遵循运算符优先级和结合性的大局控制下,对操作数的细节求值计算,编译器可以适当地进行必要的优化安排。因此编程时应把握如下几点:
1,内嵌左值的运算符 (++,--,=,+=,-=)的表达式可产生副作用。
2,右值不必建立变量保存,仅须右值操作数的运算符不直接产生副作用。
3,表达式和函数调用在分号之前完成所有的副作用。
4,确保基本表达式求初值的结果不依赖于左右次序。
23
十一、位操作运算符位操作运算符有一个单目运算符 ~,其余是双目运算符 ;
位运算是汇编语言中用得比较多的运算,位操作运算是对操作数以二进制位 (bit)为单位进行处理的。
参加位运算的操作数为整型数据。
双目位运算符存在两种形式:
a,IntL @IntR
例如,4<<5; 8>>2; 10&2; 2^4; 10|20;
b,Lvalue@= IntR
例如,n<<=2; i>>=2; m&=1; k^=4; m|=5;
24
表达式 IntL@IntR的结果是右值 。 @表示 <<,>>,&,^,|
位运算符。
Lvalue@= IntR第一个操作数必须是整型的左值,运算的结果是左值。
Lvalue@= IntR将两个操作数进行位操作,然后把结果直接赋予第一个操作数。
1.按位求反单目运算符 ~
按位求反运算符将二进制数按位由 0变 1,由 1变 0。
例如:
~0xd8 11011000
0x27 00100111
printf ("%hx,%x\n",~0xd8,~0x27); //输出,ff27,ffffffd8
注意,0xd8相当于 32位的数 0x000000d8 。
这样 ~0xd8 =0xfffffff27。
25
2.移位运算符移位操作有逻辑移位和算术移位,逻辑移位常用于操作无符号整数,算术移位一般操作有符号整数。移位运算符有,l 向右移位运算符 >> 2向左移位运算符 << 。
这两个双目运算符具有从左到右的结合性。位移运算符的语法格式为:
左移表达式
m<<n 结果右值 m*2n m<<=n 结果左值 m= m*2n
右移表达式
m>>n 结果右值 m/2n m>>=n 结果左值 m= m/2n
移位运算符的两个操作数都必须是整型,整型转换按照算术类型转换中指定的规则进行。结果的类型与左操作数 m类型相同。
26
右移表达式 m>>n的值为 ;左移表达式 m<<n的值为。右操作数 n指定移位次数,如果右操作数为负数或大于等于左操作数的位数,则结果是未定义的。
左移运算符使得 m的位模式向左移动 n位,右边空出的位数用 0填充。
右移运算符使得 m的位模式向右移动 n位数。如果 m是一个无符号量,则移位是一个逻辑移位,逻辑移位是在左边空出的位上填充 0。
d5 d4 d3 d2 d1 d0 0 0 d7 d6 d5 d4 d3 d2 d1 d0m<<2
d7 d6 d5 d4 d3 d2 d1 d0 0 0 0 d7 d6 d5 d4 d3m>>3
27
向右移动有符号的整数时一般进行算术移位,算术移位用符号位填充空出的位,但也有对左边空出的部分添 0最高位保留符号。右移符号数存在测不准关系 (s=1)如下,
s d6 d5 d4 d3 d2 d1 d0 s s s d6 d5 d4 d3 d2
1 1 1 d6 d5 d4 d3 d2
m>>2
s d6 d5 d4 d3 d2 d1 d0 s 0 0 d6 d5 d4 d3 d2
1 0 0 d6 d5 d4 d3 d2
m>>2
28
s是符号,0表示正,1表示负。
因此右移带符号的整数其空位的填补具体决定于编译器的实现,编程时请慎用右移带符号的整数,最好不右移符号数。
左移是简单的右进 0左出的 bit移动,计算溢出表示有价值的 1进到高位而舍弃。
29
3.按位与运算符按位与运算对应的二进制位同为 1时结果为 1,其余为 0。
按位与用于屏蔽 (设置某位为 0)左操作数的特定位,右操作数
IntR常是通过宏指定的立即数,屏蔽 Lvalue的 k位为 0,只需将 IntR的 k位预先设位 0即可。
例如,对于 8为二进制的按位与运算:
Lvalue&=IntR格式 IntL&IntR格式
Lvalue 11100101 0xbf 10111111
0x7f 01111111 0xc0 11000000
Lvalue &=0x7f 01100101 0xbf&0xc0 10000000
30
4.按位异或运算符按位异或运算对应的二进制位相同清 0,相异置 1 。两个数相等仅在异或它们 IntL^IntR的结果为 0。异或运算可用于数据的加密解密和计算机图形的处理,数据的还原等。
例如:
IntL,1101 0101 IntL,1101 1010
IntR,0000 1111 IntR,1111 1111
IntL^IntR,1101 1010 IntL^IntR,0010 0101
从上可见 IntR 中的 0保持操作数 IntL原位不变,IntR 中 1
将 IntL原位 1切换 0,0切换 1。
31
对一个变量用同一数异或两次复原。
例如:
Lvalue,0110 1010 Lvalue,1010 1101
IntR,1100 0111 IntR,1100 0111
Lvalue ^=IntR:1010 1101 Lvalue ^= IntR:01101010
32
5.按位或运算符按位或运算对应的二进制位同为 0时结果位是 0,其余为 1。按位或运算主要用于设置左操作数的特定位为 1,右操作数 IntR常是通过宏指定的立即数,设置 Lvalue的 k位为 1,只需将 IntR的 k位预先设位 1即可。
例如:
IntL,1101 0101 Lvalue,1001 1100
IntR,0000 1111 IntR,1111 0000
IntL|IntR,1101 1111 Lvalue|=IntR,1111 1100
33
位或运算可用于进行位的组合或压缩处理,例如,
WINDOWS系统中大量的宏定义精心地具有值 1,2,4,8,16等,
其特点是数据只有一位为 1其余为 0,如此可以进行消息的处理或数据的解包还原。 1,2,4,8四位二进制的分解形式为:
0001,0010,0100,1000,将它们或在一起
1|2|4|8展开形式为,0001|0010|0100|1000=1111
结果就浓缩在 15中。通过与运算可以对原来的值予以还原,即 (IntL|IntR) & IntL= IntL。
Lvalue,1101 1111 ( 设 IntL=1101 0101 IntR=0000 1111)
IntL,1101 0101 ( 令 Lvalue= IntL|IntR )
Lvalue&=IntL,1101 0101
34