1. 前言
大家好,我是努力学习游泳的鱼。今天我来给大家讲解操作符的相关知识。C语言提供了丰富的操作符,有了它们,我们就可以随心所欲地让代码高质量完成工作,当然前提是我们能够掌握操作符的精髓。为了涵盖操作符诸多的细节,这篇文章的篇幅可能会有点长,希望大家耐心看完,并有所收获。感谢大家的支持!
2. 算术操作符
+ (加)
- (减)
* (乘)
/ (除)
% (取模)
2.1 加法、减法、乘法操作符
加减乘都和数学中的加减乘效果完全一样。比如3+5 ,10-6 ,2*8 得到的分别是8 ,4 ,16 。
2.2 除法操作符
但是“除”和数学中的除法还是有点区别的。/操作符两端如果都是整数,执行的是整数除法,得到的结果也是整数! 如3/2 不应该得到1.5 ,而应该这么想:由于/ 操作符两端的3 和2 都是整数,所以执行的是整数除法,3 除以2 ,商1 余1 ,最终的结果是那个商,即1 。 同理,10/3 应该这么想:由于/ 两端的10 和3 都是整数,所以执行的是整数除法,10 除以3 ,商3 余1 ,最终结果是哪个商,即3 。 那如何得到小数呢?只有当/ 操作符有一端是小数时,执行的是小数除法,最终的结果也是小数。如3.0/2 ,或者3/2.0 ,或者3.0/2.0 ,/ 操作符至少有一边的数是小数,执行的是小数除法,最终才是3 除以2 得到的1.5 。 我们可以写个程序来验证这一点:
#include <stdio.h>
int main()
{
int a = 3 / 2;
float b = 3.0 / 2;
printf("a = %d, b = %f\n", a, b);
return 0;
}
2.3 取模操作符
而% (取模)干的又是什么事情呢? 比如9%2 ,计算的是9 除以2 后,商4 余1 ,得到的是余数1 。所以% 得到的是两个整数相除后的余数。
#include <stdio.h>
int main()
{
int a = 9 % 2;
printf("a = %d\n", a);
return 0;
}
注意:
% 的两端必须都是整数!n%m 之后的结果是有范围的,即0 到m-1 。
3. 移位操作符
移位操作符操作的是二进制位。要想学会移位操作符,就得先了解整数在内存中是如何存储的。 整数的二进制表示有3种形式,分别是原码,反码和补码。
- 正整数的原码,反码和补码是相同的。
- 负整数的原码,反码和补码是需要计算的。
假设我们写int a = 5; ,5 的二进制序列就是101 。 这个101 是如何得到的呢?在二进制中,从右往左的权重分别是20,21,22,23等等。那么,对于101 ,最右边的1 的权重就是20,即1 ,左边的1 的权重就是22,即4 ,计算1+4 就得到5 了。 但是如果把5 放到int 类型的变量a 里面,只写101 是不够的!因为一个int 是4 个字节,也就是32 个比特位,所以应该写够32 位,不够的话要在左边补0 。 5 的原码:00000000000000000000000000000101 但是并不是所有的情况都要在左边全部补0 ,这涉及到符号位的知识。由于a 是个有符号的整数,以上的二进制序列中,最高位(也就是最左边)的0 就是符号位。对于符号位,0 表示正数,1 表示负数。正是由于5 是个正数,所以最高位的符号位才是0 。 言归正传,以上写出来的一长串二进制序列就是5 的原码。由于5 是正数,正数的原码,反码和补码是相同的,所以这一串二进制序列既是5 的原码,也是5 的反码,同时还是5 的补码。 5的反码:00000000000000000000000000000101 5的补码:00000000000000000000000000000101 以上就是正数的原反补的计算方式。那负数呢?假设我写int a = -5; 应该如何计算呢? 整数的最高位是符号位,符号位是1 表示负数。由于-5 是一个负数,所以-5 的符号位是1 ,这就是-5 和5 的区别。也就是说,把5 的二进制序列的最高位改成1 就是-5 的二进制序列,而这样直接写出来的二进制序列就是-5 的原码。 -5的原码:10000000000000000000000000000101 而负数的反码和补码是需要计算的。具体如何计算的呢? 原码的符号位不变,其他位按位取反(1 变成0 ,0 变成1 )就得到反码。为了方便对比,我把原码和补码放到一起。 -5的原码:10000000000000000000000000000101 -5的反码:11111111111111111111111111111010 而反码加1 就得到补码。 -5的补码:11111111111111111111111111111011 那整数在内存中是如何存储的呢? 记住:整数在内存中存储的是补码的二进制。 那么,什么是移位操作符呢?
<< (左移操作符)
>> (右移操作符)
移位操作符移动的是二进制位。
3.1 左移操作符
举个例子:下面的程序会输出什么呢?
#include <stdio.h>
int main()
{
int a = 5;
int b = a << 1;
printf("a = %d, b = %d\n", a, b);
return 0;
}
我们把5 存在变量a 里面,其实是把5 的补码放到了a 里。 5 的补码:00000000000000000000000000000101 而左移操作符干的事情是:左边丢弃,右边补0 所以最左边的0 就不要了,最右边放个0 。 左移前:00000000000000000000000000000101 左移后:00000000000000000000000000001010 对于左移后的二进制序列仍然要看成是补码,由于最高位是0 ,即符号位是0 ,所以该二进制序列是一个正数,正数的原反补相同,所以这个二进制序列也是左移后的数的原码。 左移后得到的补码:00000000000000000000000000001010 左移后得到的原码:00000000000000000000000000001010 这个原码再转换成十进制,即把二进制的1010 转换成十进制,左边的1 表示23,即8 ,右边的1 表示21,即2 ,计算8+2 ,得到的结果是10 。 综上所述,5<<1 得到的结果就是10 。对于上面的代码,a 是5 ,把a<<1 后的结果放到b 里,所以b 是10 。但是a 仍然是5 ,因为移位操作符不会改变操作的变量原来的值。 那如果a 是负数呢?
#include <stdio.h>
int main()
{
int a = -5;
int b = a << 1;
printf("a = %d, b = %d\n", a, b);
return 0;
}
同理,一开始我们把-5 的补码放到了a 里。 -5 的补码:11111111111111111111111111111011 左移操作符的效果是,左边丢弃,右边补0 ,所以-5<<1 的效果是:最左边的1 不要了,最右边补一个0 。 左移前:11111111111111111111111111111011 左移后:11111111111111111111111111110110 而左移前和左移后的二进制序列都是补码,我们实际在屏幕上打印的是原码,所以需要计算出该二进制序列的原码。 左移后得到的补码:11111111111111111111111111110110 由于最高位(符号位)是1 ,表示负数。负数的原反补不一定相同,是要计算的。 由于反码+1 得到补码,所以补码-1 就得到反码。 补码:11111111111111111111111111110110 反码:11111111111111111111111111110101 原码符号位不变,其他位按位取反得到反码。那反码符号位不变,其他位按位取反就能得到原码。 反码:11111111111111111111111111110101 原码:10000000000000000000000000001010 最高位(符号位)的1 表示负数,后面的1010 表示10 ,所以结果是-10 。 由于-5<<1 得到-10 ,放到了b 里,所以b 是-10 。而a 仍然是原来的值,即-5 ,因为移位操作符不会改变操作的变量的值。 其实,左移操作符有乘2 的效果,5 左移1 位后变成了10 ,-5 左移1 位后变成了-10 。 当然,左移不一定只左移一位,可以左移两位,三位等等,只要移动的位数是正整数就行。比如左移两位,就是左边丢弃两位,右边补两个0 。 总结:
- 整数的二进制序列有三种形式,分别是原码,反码和补码(后面简称原反补)。
- 整数在内存中以补码的形式存储。
- 正整数的原反补相同,负整数的原反补需要计算。
- 负整数的原反补的计算方式:
- 最高位(符号位)为
1 表示负数,直接转换出来的二进制序列是原码。 - 对原码符号位不变,其他位按位取反得到反码。
- 反码
+1 得到补码。 - 补码
-1 得到反码。 - 反码的符号位不变,其他位按位取反得到原码。
- 补码的符号位不变,其他位按位取反,得到的二进制序列再
+1 也可以直接得到原码。(也就是说,通过原码得到补码的方式,把这种方式用在补码上,也能反过来得到原码,类似负负得正)。
对于最后一点,举个例子:已知-5 的补码。 -5 的补码:11111111111111111111111111111011 先取反。 取反前:11111111111111111111111111111011 取反后:10000000000000000000000000000100 再+1 :10000000000000000000000000000101 而这就是-5 的原码。
3.2 右移操作符
学会了左移,接下来讲讲右移。 右移分为两种情况:
- 算术右移:右边丢弃,左边补原符号位。
- 逻辑右移:右边丢弃,左边补
0 。
如果右移一个正数,正数的符号位就是0 ,算术右移和逻辑右移没有区别。 如果右移一个负数,如果是算术右移,左边补的就是原符号位(即1 );如果是逻辑右移,左边补的就是0 。具体是哪种情况取决于编译器(比如VS采取的就是算术右移,我个人也觉得算术右移更合理,因为左边补0 的话,原来是个负数,右移后就变成正数了,有点别扭)。 举个例子:
#include <stdio.h>
int main()
{
int a = 5;
int b = a >> 1;
printf("a = %d, b = %d\n", a, b);
return 0;
}
如何计算5>>1 呢?5 是正数,原反补相同,都是: 00000000000000000000000000000101 正数的算术右移和逻辑右移效果相同,都是右边丢弃,左边补符号位(即0 )。 右移前:00000000000000000000000000000101 右移后:00000000000000000000000000000010 得到的是补码。由于符号位是0 ,是一个正数,原反补相同,所以结果就是10 作为二进制转换成十进制得到的2 。 再举个右移负数的例子。
#include <stdio.h>
int main()
{
int a = -5;
int b = a >> 1;
printf("a = %d, b = %d\n", a, b);
return 0;
}
-5>>1 如何计算呢?-5 的补码我们已经算过了。 -5 的补码:11111111111111111111111111111011 负数的算术右移和逻辑右移是不一样的。 如果是逻辑右移,右边丢弃,左边补0 。 逻辑右移前:11111111111111111111111111111011 逻辑右移后:01111111111111111111111111111101 最高位(符号位)是0 ,这是一个超级大的正数。 如果是算术右移,右边丢弃,左边补原符号位(即1 )。 算术右移前:11111111111111111111111111111011 算术右移后:11111111111111111111111111111101 由于最高位(符号位)是1 ,是负数,我们要把这个补码转换成原码。首先-1 得到反码。 补码:11111111111111111111111111111101 反码:11111111111111111111111111111100 反码符号位不变,其他位按位取反得到原码。 反码:11111111111111111111111111111100 原码:10000000000000000000000000000011 最高位(符号位)是1 表示负数,再把二进制的11 转换成十进制得到3 ,所以最终结果是-3 。 由于-5 右移1 位后得到的是-3 ,所以右移并不一定有除以2 的效果。 注意:对于移位操作符,不要移动负数位,这个是标准未定义的。 例如不要这么写:
int num = 10;
num >> -1;
4. 位操作符
这里先记住这三个操作符的名称。
& (按位与)
| (按位或)
^ (按位异或)
这里的位表示二进制位。
4.1 按位与操作符
先说按位与。
#include <stdio.h>
int main()
{
int a = 3;
int b = -5;
int c = a & b;
printf("c = %d\n", c);
return 0;
}
如何计算3&-5 呢? 整数在内存中都是以补码的方式存储的。前面已经详细讲解了如何计算整数的原反补,下面都省略计算的过程。 3的原码:00000000000000000000000000000011 3的反码:00000000000000000000000000000011 3的补码:00000000000000000000000000000011 -5的原码:10000000000000000000000000000101 -5的反码:11111111111111111111111111111010 -5的补码:11111111111111111111111111111011 按位与的规则是:对应的二进制位如果都是1 ,则结果的二进制位是1 ,否则结果的二进制位是0 。 下面前两行是3 和-5 的补码,第三行是按位与后的结果。 00000000000000000000000000000011 11111111111111111111111111111011 00000000000000000000000000000011 再把第三行的补码转换成原码,再转换成十进制得到3 。所以3&-5=3 。
4.2 按位或操作符
再来看看按位或。
#include <stdio.h>
int main()
{
int a = 3;
int b = -5;
int c = a | b;
printf("c = %d\n", c);
return 0;
}
按位或的规则是:对应的二进制位都是0 ,则结果的二进制位是0 ,否则结果的二进制位是1 。 下面前两行是3 和-5 的补码,第三行是按位或后的结果。 00000000000000000000000000000011 11111111111111111111111111111011 11111111111111111111111111111011 再把第三行的补码转换成原码: 补码:11111111111111111111111111111011 反码:11111111111111111111111111111010 原码:10000000000000000000000000000101 再把原码转换成十进制得到-5 。所以3|-5=-5 。
4.3 按位异或操作符
最后看按位异或。
#include <stdio.h>
int main()
{
int a = 3;
int b = -5;
int c = a ^ b;
printf("c = %d\n", c);
return 0;
}
按位异或的规则是:对应的二进制位不相同,则结果的二进制位是1 ,否则结果的二进制位是0 。即:相同为0 ,相异为1 。可以简单记为“找不同”。 下面前两行是3 和-5 的补码,第三行是按位或后的结果。 00000000000000000000000000000011 11111111111111111111111111111011 11111111111111111111111111111000 再把第三行的补码转换成原码。 补码:11111111111111111111111111111000 反码:11111111111111111111111111110111 原码:10000000000000000000000000001000 再把原码转换成十进制得到-8 。所以3^-5=-8 。 按位异或操作符,是一个技巧性很高的操作符。我们可以做一些总结:
- a^a=0,因为同一个数对应的二进制位都是相同的。
- 0^a=a,因为若
a 的二进制位是0 ,和0 相同,则结果也是0 ;若a 的二进制位是1 ,和0 相异,则结果也是1 。 - 异或是可以随意交换顺序的。如把
a ,b ,c 异或,或者把b ,c ,a 异或,或者把c ,b ,a 异或,结果都是一样的。
有一个经典的问题:找单身狗。假设一组数字,除了有一个数字只出现一次之外,其余每个数字都出现两次,请你找到那个只出现一次的数字(单身狗)。如[1 2 3 4 5 4 3 2 1] ,如何找到5 呢? 很简单,把这些数字全部异或到一起就行了。因为根据前面的总结,相同的数字异或结果是0 ,任何数字和0 异或得到它本身,异或又可以随意交换顺序,所以相当于先把相同的数字都异或得到0 ,再把这些0 一个一个跟单身狗异或,不管怎么异或得到的都是单身狗。 再来看一道题:不创建临时变量,交换两个整数。 如果我们要交换两个数(a 和b ,假设a 是3 ,b 是5 ),老生常谈的方法是:(如果看不懂的话,仔细对照注释,反复看)
int tmp = a;
a = b;
b = tmp;
你可能想到可以用加减。
a = a + b;
b = a - b;
a = a - b;
用加减的话确实没有创建临时变量,但是有一个问题:如果a 和b 比较大,不过没有大到超过int 的最大存储范围,但是加起来就超出int 的最大存储范围了,算出来的结果就是错的。 所以,我们的主角登场了!用异或!
a = a ^ b;
b = a ^ b;
a = a ^ b;
我第一次看到这种方法,也直呼妙哉! 看明白了这三种方法,我有以下思考: 首先,要交换两个数a 和b ,为什么不直接a=b ,b=a 呢? 你会说,那还用说?a=b 之后,a 的数据就被覆盖了,a 和b 就都是一开始b 的值了,a 的值就丢失了! 所以问题的关键,就是如何先保存a ,再把b 赋值给a 。 第一种方法,创建一个临时变量tmp ,先保存了a 的值,这样b 赋值给a 后,虽然a 被覆盖了,但并没有丢失a 的值。 第二种方法,先把两个数加起来,把和放到a 里,此时a 被覆盖了,但是我们有丢失a 的数据吗?没有。a 的数据看似丢失了,实际上一步就能找回来,因为a 里存储的是原来a 和b 的和,用“和”减去b 的值就能得到原来的a ,再把原来的a 放到b 里,此时b 的数据就被原来的a 的数据覆盖了。那b 的数据丢失了吗?也没有!因为a 里存储的仍然是a 和b 的和,用“和”减去此时b 里存储的原来的a 的值,就重新得到原来的b 的值了,再把这个值放到a 里。所以我们绕了个大弯,就是为了在覆盖掉一个变量后,不丢失这个变量原来的值。 第三种方法同理,把a 和b 异或的结果存储到a 中,虽然a 被覆盖了,但是并没有丢失a 的数据。因为一步就能把数据找回来,把a 和b 异或的结果再和原来的b 异或,就能得到原来的a ,放到b 里。同理此时虽然b 被覆盖了,但是b 的值也没有丢失。因为把a 和b 异或的结果再和原来的a (此时在b 里)异或,就重新得到b 了,再放到a 里,此时就把a 和b 交换了。 我们还可以换一种角度来理解第三种方法。假设a^b 得到的是一个密码,这个密码和原来的a 异或就能抵消掉a 从而得到b ,与原来的b 异或就能得到原来的a 。我们先a = a ^ b; 就是把密码放a 里。接着b = a ^ b; 就是把密码和原来的b 异或,得到原来的a ,再把原来的a 放到b 里。最后a = a ^ b; 就是把密码和原来的a 异或,得到原来的b ,再把原来的b 放到a 里。这样就实现了两个数的交换。 但是第三种方法有局限性。异或只能作用于整数,所以如果要交换两个浮点数,就不能使用这种方法了。实际写代码时,还是方法一最好。
5. 赋值操作符
5.1 单等号赋值操作符
一个等号= 是赋值操作符。如:
int weight = 120;
weight = 89;
double salary = 10000.0;
salary = 20000.0;
赋值操作符可以连续使用。
int a = 10;
int x = 0;
int y = 20;
a = x = y + 1;
但是不建议使用连续赋值,因为可读性不强而且不易调试。建议分开写,一行只干一件事。事实上,上面的连续赋值和下面的两行代码是等价的。
x = y + 1;
a = x;
5.2 复合赋值操作符
赋值操作符里还有复合操作符(@= ,@ 可以是加,减,乘,除,取模,左移,右移,按位与,按位或,按位异或等等操作符)。 简单来说,下面的代码两两之间是等价的。(即n@=m 等价于n=n@m )
a += 3;
a = a + 3;
b -= 6;
b = b - 6;
c *= 8;
c = c * 8;
d /= 9;
d = d / 9;
e %= 10;
e = e % 10;
f <<= 3;
f = f << 3;
g >> 5;
g = g >> 5;
h &= 11;
h = h & 11;
i |= 12;
i = i | 12;
j ^= 15;
j = j ^ 15;
复合赋值符更加简洁直观,我个人很喜欢使用。
6. 单目操作符
什么是单目操作符呢? 如果我们写a + b ,由于+ 有两个操作数,左操作数是a ,右操作数是b ,所以称+ 是双目操作符。而单目操作符就是只有一个操作数的操作符。
! (逻辑反操作)
= (负值)
+ (正值)
& (取地址)
sizeof (求操作数的类型大小,单位是字节)
~ (对一个数的二进制按位取反)
-- (前置、后置--)
++ (前置、后置++)
* (间接访问操作符/解引用操作符)
(类型) (强制类型转换)
6.1 逻辑取反操作符
首先看逻辑反操作,即一个感叹号! 。 在C语言中,0 表示假,非0 表示真。 而逻辑反操作会把真变成假,假变成真。 假设flag 为真,则!flag 为假,如果flag 为假,则!flag 为真。 这是如何实现的呢?如果flag 不是0 ,则!flag 的值就是0 。如果flag 已经是0 了,则!flag 的值就是1 。 实际使用时,为了表示如果flag 为真就做什么事,一般会写:
if (flag)
{
}
为了表示flag 为假就做什么事,一般会写:
if (!flag)
{
}
此时如果flag 是假,!flag 就是真,就会执行if 语句下面的大括号内的语句。 但是,“0 表示假,非0 表示真”这句话总感觉有点抽象,这是因为在C语言中,C99之前没有表示真假的类型。 C99引入了布尔类型。布尔类型为_Bool ,也可以写成bool ,取值有true 和false ,true 表示真(本质上是1 ),false 表示假(本质上是0 )。使用布尔类型要引用头文件stdbool.h 。 如果初始化一个变量为真,可以这么写:_Bool flag = true; ,当然也可以这么写bool flag = true; (_Bool 和bool 等价)。 如果初始化一个变量为假,可以这么写:_Bool flag = false; ,当然也可以这么写bool flag = false; (_Bool 和bool 等价)。 结合逻辑取反操作符,可以这么使用:
#include <stdio.h>
#include <stdbool.h>
int main()
{
bool flag1 = true;
if (flag1)
{
printf("true\n");
}
bool flag2 = false;
if (!flag2)
{
printf("false\n");
}
return 0;
}
6.2 负值和正值操作符
一个负号- ,可以把一个数变成它的相反数,比如a 是5 ,则-a 就是-5 ;b 是-7 ,则-b 就是7 (负负得正)。 一个正号+ ,就能得到一个数本身(这玩意真没啥用,一般会省略)。比如a 是5 ,+a 还是5 ;b 是-7 ,+b 还是-7 。
6.3 取地址操作符
& 操作符可以对一个对象取地址。比较常见的有: 取变量的地址:
int a = 10;
int *pa = &a;
取数组的地址:
int arr[10] = {0};
int (*parr)[10] = &arr;
取结构体的地址:
struct S
{
int i;
double d;
};
int main()
{
struct S s;
struct S *ps = &s;
return 0;
}
取指针的地址:
int a = 0;
int *pa = &a;
int** ppa = &pa;
int*** pppa = &ppa;
取函数的地址:
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*pAdd)(int, int) = &Add;
return 0;
}
6.4 解引用操作符(间接访问操作符)
一个星号* 是解引用操作符,又称间接访问操作符。 解引用操作符可以通过一个对象的地址找到这个对象。如:
int a = 10;
int *pa = &a;
*pa = 20;
6.5 sizeof操作符
sizeof 是用来计算类型创建的对象所占空间的大小的,单位是字节。 sizeof 可以计算变量的大小,本质上是计算变量类型的大小,此时括号可以省略。我们定义一个变量int a = 10; 此时sizeof(a) 就是a 变量所占空间的大小,本质上是一个int 类型的大小,即4 (字节)。当我们用sizeof 计算一个变量的大小时,括号可以省略,比如可以直接写sizeof a 。正是由于括号可以省略,所以sizeof 是一个操作符,而不是函数。 sizeof 还可以直接计算类型大小。比如sizeof(int) ,计算的是一个int 类型的大小,即4 (字节)。 sizeof 也可以计算一个数组的大小。如我们创建一个整型数组int arr[10] = {0}; 则可以使用sizeof(arr) 来计算它的大小。由于这个数组有10 个元素,每个元素是int ,所以总大小是10 个int ,即40 (字节)。根据这一点,我们就可以计算数组的元素个数。数组的元素个数=数组的总大小÷数组一个元素的大小 。所以数组arr 的元素个数就是sizeof(arr) / sizeof(arr[0]) 。 计算数组大小时,必须要在数组的局部范围内计算。如果使用函数,把数组作为参数传递时,传递的是数组首元素的地址,此时的sizeof(arr) 就不是数组的大小了,而是一个存储数组首元素的地址的指针变量的大小了。如32 位平台下,以下程序的输出结果是什么呢?
#include <stdio.h>
void test1(int arr[])
{
printf("%d\n", sizeof(arr));
}
void test2(char ch[])
{
printf("%d\n", sizeof(ch));
}
int main()
{
int arr[10] = { 0 };
char ch[10] = { 0 };
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(ch));
test1(arr);
test2(ch);
return 0;
}
在main 函数内部计算arr 的大小,由于直接把数组名放在sizeof 内部,数组名表示整个数组,计算的是整个数组的大小,单位是字节,所以结果是40 (字节)。同理接下来计算数组ch 的大小是10 (字节)。接着把数组arr 作为参数传递给test1 函数,此时数组名表示首元素地址,test1 函数会用一个整型指针来接收,所以计算sizeof(arr) 的结果是一个整型指针的大小,在32 位平台下是4 (字节)。同理test2 函数内部的sizeof(ch) 是一个字符指针的大小,在32 位平台下也是4 (字节)。 注意:sizeof() 中的表达式不参与计算,这是因为sizeof 是在编译期间处理的。
#include <stdio.h>
int main()
{
int a = 10;
short s = 0;
printf("%d\n", sizeof(s = a + 2));
printf("%d\n", s);
return 0;
}
以上程序,由于sizeof() 中的表达式不参与计算,所以并没有执行s = a + 2 ,而是直接看,a 是int ,加2 后还是int ,再赋值给short 类型的s ,由于空间不够,会发生截断,最终这个表达式的结果是short 类型的,所以算出它的大小是2 。此时s 仍然是0 。 最终程序输出:2 0
6.6 按位取反操作符
波浪号~ 可以对一个数的二进制按位取反。 比如:
#include <stdio.h>
int main()
{
int a = 0;
int b = ~a;
printf("a = %d, b = %d\n", a, b);
return 0;
}
先创建a ,并初始化成0 。由于a 是int 类型的,在内存中会以补码的形式存储(本篇文章前面详细讲解了原反补的计算,忘记的朋友可以回去看看),而0 的补码是: 00000000000000000000000000000000 对一个数按位取反,就是对每个二进制位,1 变成0 ,0 变成1 。 取反前:00000000000000000000000000000000 取反后:11111111111111111111111111111111 取反后得到的也是补码,再转换成原码。 补码:11111111111111111111111111111111 反码:11111111111111111111111111111110 原码:10000000000000000000000000000001 再把原码转换成十进制,得到-1 。所以对a 按位取反后得到-1 ,即b 是-1 。而按位取反操作符不会改变操作对象的值,所以a 仍然是0 。 这里可以记住一点:-1 的补码的所有二进制位都是1 。 那按位取反有什么用呢?来看一个例子: 假设int a = 11; ,11 的二进制序列是1011 (只写最右边4 位,因为左边都是0 ),如何把从右往左数第三位的0 改成1 呢?其实,只需要把11 的二进制序列按位或上0100 就行了。那如何产生0100 呢?只需要1<<2 。综上,完整的处理是a |= (1 << 2); 干得漂亮!接下来,我想你改回来,也就是把1111 重新改为1011 。这也很简单,只需要按位与上一个二进制序列,这个二进制序列的从右往左数第三位是0 ,其余位都是1 。那如何产生这样一个二进制位呢?这就需要用到按位取反了。一堆1 不好产生,但是你如果按位取反呢?那就是从右往左数第三位是1 ,其余位都是0 了。简单来说,就是1<<2 得到的二进制序列。所以,只需要~(1<<2) ,再按位与上1111 ,就能变回1011 了。完整的处理是a &= (~(1 << 2)); 如果还是不明白,我把完整的过程和完整的二进制序列列出来: 先计算1<<2 :00000000000000000000000000000100 再按位取反: 取反前:00000000000000000000000000000100 取反后:11111111111111111111111111111011 接下来的三行,分别是一开始a 的二进制序列1111 ,计算~(1<<2) 的二进制序列和计算a&=(~(1<<2)) 的二进制序列。 00000000000000000000000000001111 11111111111111111111111111111011 00000000000000000000000000001011 这就变回来了。 有没有发现,我们现在可以操控每一个二进制位了!如果熟练掌握这些操作符,就像庖丁解牛一般,非常灵活! 对于按位取反,还有一个使用场景。我们知道,scanf ,getchar 等函数在读取失败时会返回EOF ,所以判断是否读取成功我们就可以写if (scanf(...) != EOF) 。而EOF 的值是-1 ,~(-1)=0 ,所以也可以写if (~scanf(...)) 。
6.7 前置(后置)自增(自减)操作符
分别有前置++,后置++,前置--,后置-- 。 所谓自增,就是自己的值加1 ,注意这会改变原来的值,比如假设原来a 是5 ,++a; 之后a 就要变成6 。同理自减就是自己的值减1 ,也会改变原来的值。如b 原来是8 ,--b; 之后b 就要变成7 。注意:不管是前置还是后置,都会改变原来的值,++ 会使原来的值加1 ,-- 会使原来的值减1 。 那前置和后置有什么区别呢? 每个表达式都是有值的。比如a = 3 这个表达式不仅把3 赋值给a,而且整体作为一个表达式的值是a的值,即3 。所以当我们写b = a = 3; 时,本质上是把a = 3 这个表达式的值赋值给b ,所以b 也会变成3 。 同理,假设写++a 或者a++ 这样的表达式也是有值的。像++a 这样++ 放在a 的前面,所以称为前置++ ,像a++ 这样++ 放在a 的后面,称为后置++ 。前置-- 和后置-- 同理。 前置和后置的区别是:和变量构成的表达式的值是不一样的。 假设有一个变量a ,原来的值是7 ,那么++a 这个表达式的值是自增后的8 ,而a++ 的值是自增前的7 。所以如果写int b = ++a; ,那么b 就是8 ,如果写int b = a++; ,那么b 就是7 。 总结:前置++(--)与变量构成的表达式的值是自增(自减)后的值,后置++(--)与变量构成的表达式的值是自增(自减)前的值。 下面的程序会输出多少呢?
#include <stdio.h>
int main()
{
int a = 3;
int b = ++a;
printf("a = %d, b = %d\n", a, b);
return 0;
}
对于前置++ ,我们可以简单记为:先++ ,后使用。所以a 先++ 变成4 ,后使用4 的值,赋给b 。本质上int b = ++a; 就等价于a = a + 1; int b = a; 。输出:a 和b 的值都是4 。 再来看下面的程序:
#include <stdio.h>
int main()
{
int a = 3;
int b = a++;
printf("a = %d, b = %d\n", a, b);
return 0;
}
对于后置++ ,我们可以简单记为:先使用,后++ 。所以先使用a 的值(即3 ),赋值给b ,再让a++ ,a 自增变成4 。本质上int b = a++; 就等价于int b = a; a = a + 1; 。输出:a 是4 ,b 是3 。 对于前置-- 和后置-- 同理。
6.8 强制类型转换操作符
括号里放一个类型,就是强制类型转换。 如果写int a = 3.14; ,我们可以把3.14 这个浮点数赋值给整型变量a ,但是编译器会报一个警告,原因是类型不兼容。如何去掉这个警告呢?只需要把3.14 强制类型转换成整型,再赋值给a 就行了int a = (int)3.14; 。此时括号里放int 的效果就是把3.14 这个double 类型的数据强制类型转换成int 类型。 不推荐大家过多地使用强制类型转换。
7. 关系操作符
> (判断是否大于)
< (判断是否小于)
>= (判断是否大于或等于)
<= (判断是否小于或等于)
!= (判断是否不等于)
== (判断是否等于)
关系操作符比较简单,就是比较两个变量的关系。比如a 是3 ,b 是5 ,则a<b 为真。 注意:
- 一个等号
= 是赋值,两个等号== 才是判断相等。 - 不是所有变量都能使用关系操作符比较大小的。结构体,字符串等不能使用关系操作符比较大小。
8. 逻辑操作符
&& (逻辑与)
|| (逻辑或)
8.1 逻辑与操作符(逻辑并且操作符)
两端的操作数都为真,则结果为真,否则结果为假。
true && true = true
true && false = false
false && true = false
false && false = false
注意:对于逻辑与,若左操作数为假,则不再计算右操作数。 以下程序会输出多少?
#include <stdio.h>
int main()
{
int i = 0, a = 0, b = 2, c = 3, d = 4;
i = a++ && ++b && d++;
printf("a = %d, b = %d, c = %d, d = %d\n", a, b, c, d);
return 0;
}
由于表达式a++ 的值是自增前的值,即0 ,所以第一个逻辑与的左操作数是假,不再计算右操作数了,即a++ && ++b 的结果为假,第二个逻辑与的左操作数为假,不再计算右操作数。最后只有a 自增变成1 ,其余变量不变。
8.2 逻辑或操作符
两端的操作数都为假,则结果为假,否则结果为真。
true || true = true
true || false = true
false || true = true
false || false = false
注意:对于逻辑或操作符,若左操作数为真,则不再计算右操作数。
9. 条件操作符(三目操作符)
条件操作符是C语言里唯一的三目操作符,它有三个操作数。 exp1 ? exp2 : exp3 若exp1 为真,exp2 计算,exp3 不计算,最终表达式的结果是exp2 的值;若exp1 位假,exp2 不计算,exp3 计算,最终表达式的结果是exp3 的值。 其实这玩意就是个简化版的if else 语句。比如求两个数的较大值,既可以用if else 来写,也可以用条件操作符来写。
if (a > b)
max = a;
else
max = b;
max = ((a>b) ? a : b);
10. 逗号表达式
exp1, exp2, exp3, ..., expN 逗号表达式,就是用逗号隔开的多个表达式。 逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。 如:
int a = 1;
int b = 2;
int c = (a>b, a=b+10, a, b=a+1);
逗号表达式会从左向右依次执行。第一个表达式a>b 的值为假,即0 ;第二个表达式,b+10 为12 ,赋值给a ,a 变成12 ,表达式的结果是12 ;第三个表达式的值就是a 的值,即1 ;第四个表达式,a+1 为13 ,赋值给b ,b 的值变成13 ,表达式的结果是13 。逗号表达式的值是最后一个表达式的值,即13 ,赋值给c ,所以c 变成13 。
11. 下标引用操作符
即方括号。[] 操作数:数组名+索引值
int arr[10] = {0};
arr[9] = 10;
[]的两个操作数是arr和9
[] 的两个操作数是可以交换的,所以arr[5] 和5[arr] 是等价的。为什么呢?因为arr 是数组名,表示首元素地址,+5 后表示下标为5 的元素的地址,再解引用就是下标为5 的元素,而arr[5] 也表示下标为5 的元素,即arr[5] 等价于*(arr + 5) ,而加法有交换律,所以又等价于*(5 + arr) ,即等价于5[arr] 。你可以理解为,[] 的交换律是由于操作数有两个。 C99语法中,允许数组在创建时对指定下标的元素初始化,其余元素会被默认初始化为0 。
int arr[10] = {[3]=5, [7]=9};
12. 函数调用操作符
括号() 为函数调用操作符,接受一个以上的操作数,分别是函数名和函数参数。 比如printf("Hello, World!\n"); ,() 的操作数就是函数名printf ,字符串"Hello, World!\n" 。 若函数无参,则操作数只有一个,即函数名。
13. 结构成员访问操作符
结构成员访问操作符有. (点)和-> (箭头)。 使用方式:
结构体变量.结构体成员
结构体指针->结构体成员
如:
struct S
{
int i;
double d;
char ch;
};
int main()
{
struct S s;
s.i = 10;
s.d = 3.14;
s.ch = 'w';
struct S *ps = &s;
ps->i = 10;
ps->d = 3.14;
ps->ch = 'w';
return 0;
}
14. 表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性决定的。同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。
14.1 隐式类型转换
C的整型算术运算总是至少以缺省整型的精度来进行的。为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换被称为整型提升。 整形提升的意义:表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int 的字节长度,同时也是CPU的通用寄存器长度。因此,即使两个char 类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int 长度的整型值,都必须先转换为int 或unsigned int ,然后才能送入CPU去执行运算。 简单来说,char 类型和short 类型在参与运算时,会先被转换为int 类型,这种转换就是整形提升。 如:
char a, b, c;
a = b + c;
上面的程序中,b 和c 的值被提升为普通整型,然后再执行加法运算。加法运算完成之后,结果将被截断,然后再存储于a 中。 再举个例子:
#include <stdio.h>
int main()
{
char c1 = 3;
char c2 = 127;
char c3 = c1 + c2;
printf("%d\n", c3);
return 0;
}
程序是如何执行的呢?会输出多少呢? 首先char c1 = 3; 我们要把3 放到c1 里去。3 是一个整数,它的二进制序列有32 位。 3 的二进制序列(补码):00000000000000000000000000000011 而c1 是char 类型的变量,只能存储8 个比特位,所以会发生截断,最终只把3 的二进制序列的最低的8 个比特位存放到c1 中。 c1 里存放的数据:00000011 接着char c2 = 127; 我们要把127 放到c2 里去。127 也是一个整数,它的二进制序列有32 位。 127 的二进制序列(补码):00000000000000000000000001111111 而c2 也是char 类型的变量,只能存储8 个比特位,所以会发生截断,最终只把127 的二进制序列的最低的8 个比特位存放到c2 中。 c2 里存放的数据:01111111 然后char c3 = c1 + c2; ,先要计算c1 + c2 ,c1 和c2 都是char 类型的变量,在计算时会整型提升。如何提升呢?
整形提升是按照变量的数据类型的符号位来提升的。
- 有符号整型提升:高位补充符号位。
- 无符号整形提升:高位补
0 。
由于c1 是有符号的char ,所以高位补充符号位(即0 )。 整型提升前:00000011 整型提升后:00000000000000000000000000000011 由于c2 也是有符号的char ,所以高位补充符号位(即0 )。 整型提升前:01111111 整型提升后:00000000000000000000000001111111 接着相加。下面的前两行分别是c1 和c2 整型提升后的二进制序列,第三行是把前两行相加得到的二进制序列。 00000000000000000000000000000011 00000000000000000000000001111111 00000000000000000000000010000010 我们要把相加的结果放c3 里,而c3 也是char 类型的变量,只能存储8 个比特位,所以会发生截断,最终只把二进制序列的最低的8 个比特位存放到c3 中。 c3 里存放的数据:10000010 最后printf("%d\n", c3); ,c3 是char 类型,不够一个int ,又要整型提升。由于c3 也是有符号的char ,所以高位补充符号位(即1 )。 整型提升前:10000010 整型提升后:11111111111111111111111110000010 整型提升是对补码进行计算的,我们还要把它转换成原码。 补码:11111111111111111111111110000010 反码:11111111111111111111111110000001 原码:10000000000000000000000001111110 再转换成十进制,最终打印出来的结果是-126 。 再看下面的代码:
#include <stdio.h>
int main()
{
char a = 0xb6;
short b = 0xb600;
int c = 0xb6000000;
if (a == 0xb6)
printf("a");
if (b == 0xb600)
printf("b");
if (c == 0xb6000000)
printf("c");
return 0;
}
由于a 和b 都会发生整型提升,提升后就不是原来的值了,所以不会打印a 和b 。c 本身就是int 类型,不会发生整型提升,所以会打印。 再来看:
#include <stdio.h>
int main()
{
char c = 1;
printf("%u\n", sizeof(c));
printf("%u\n", sizeof(+c));
printf("%u\n", sizeof(-c));
return 0;
}
直接打印sizeof(c) ,由于c 是short 类型,是2 个字节。但是若打印sizeof(+c) 或sizeof(-c) ,由于c 参与运算,会发生整型提升,大小就是整型的大小,即4 个字节。
14.2 算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的类型转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
long double double float unsigned long int long int unsigned int int 这个列表可以这么记忆:浮点数>整数,精度高的类型>精度低的类型,无符号类型>有符号类型。
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换成另外一个操作数的类型后执行运算。 警告:但是算术转换要合理,要不然会有一些潜在的问题。
float f = 3.14;
int num = f;
总结:如果是精度低于整型的类型参与运算,会发生整型提升。如果是精度高于整型的类型参与运算,若类型不相同,会发生隐式类型转换。
14.3 操作符的属性
复杂表达式的求值有三个影响的因素。
- 操作符的优先级。
- 操作符的结合性。
- 是否控制求值顺序。
相邻的操作符需要考虑优先级。如乘法操作符优先级比加法操作符高,所以c = a + b * 5 会先计算b*5 再把结果与a 相加。 若相邻两个操作符的优先级一样,会考虑操作符的结合性。如a + b + c ,由于加法操作符是从左到右结合的,所以会先算a+b ,再把结果跟c 相加。 有些操作符会控制求值顺序。如逻辑与操作符,在左操作数为假时,就不计算右操作数了。 但是哪怕有了操作符的优先级,结合性,是否控制求值顺序等属性,我们依然无法确定一些表达式的值。这种表达式是问题表达式,我们在实际写代码中要避免写出这样的代码。 a*b + c*d + e*f 由于操作符的优先级只能决定相邻的乘法比加法运算要早,我们无法确定第三个* 和第一个+ 哪个先执行。 c + --c 同上,操作符的优先级只能决定自减-- 的运算在+ 的运算的前面,但是我们并没有办法得知,+ 操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。 再来看下一个:
#include <stdio.h>
int fun()
{
static int count = 1;
return ++count;
}
int main()
{
int answer;
answer = fun() - fun() * fun();
printf("%d\n", answer);
return 0;
}
由于我们无法确定函数的调用顺序,所以计算的可能是2-3*4 ,也可能是4-2*3 ,无法得到唯一的结果。 再来看一个:
#include <stdio.h>
int main()
{
int i = 1;
int ret = (++i) + (++i) + (++i);
printf("%d\n", ret);
printf("%d\n", i);
return 0;
}
这段代码中的第一个+ 在执行的时候,第三个++ 是否执行,这个是不确定的,因为依靠操作符的优先级和结合性是无法决定第一个+ 和第三个前置++ 的先后顺序。 我们再用一个超级壮观的表达式来收尾:
#include <stdio.h>
int main()
{
int i = 10;
i = i-- - --i * ( i = -3 ) * i++ + ++i;
printf("i = %d\n", i);
return 0;
}
这玩意,真是神仙来了都不知道怎么算呀! 总结:我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。
|