当前位置: 首页 > news >正文

【C语言】操作符

操作符

    • 1. 前言
    • 2. 算术操作符
      • 2.1 加法、减法、乘法操作符
      • 2.2 除法操作符
      • 2.3 取模操作符
    • 3. 移位操作符
      • 3.1 左移操作符
      • 3.2 右移操作符
    • 4. 位操作符
      • 4.1 按位与操作符
      • 4.2 按位或操作符
      • 4.3 按位异或操作符
    • 5. 赋值操作符
      • 5.1 单等号赋值操作符
      • 5.2 复合赋值操作符
    • 6. 单目操作符
      • 6.1 逻辑取反操作符
      • 6.2 负值和正值操作符
      • 6.3 取地址操作符
      • 6.4 解引用操作符(间接访问操作符)
      • 6.5 sizeof操作符
      • 6.6 按位取反操作符
      • 6.7 前置(后置)自增(自减)操作符
      • 6.8 强制类型转换操作符
    • 7. 关系操作符
    • 8. 逻辑操作符
      • 8.1 逻辑与操作符(逻辑并且操作符)
      • 8.2 逻辑或操作符
    • 9. 条件操作符(三目操作符)
    • 10. 逗号表达式
    • 11. 下标引用操作符
    • 12. 函数调用操作符
    • 13. 结构成员访问操作符
    • 14. 表达式求值
      • 14.1 隐式类型转换
      • 14.2 算术转换
      • 14.3 操作符的属性

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); // 运行结果:a = 1, b = 1.500000

	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); // 运行结果:a = 1

	return 0;
}

注意:

  • %的两端必须都是整数
  • n%m之后的结果是有范围的,即0到m-1。

3. 移位操作符

移位操作符操作的是二进制位。要想学会移位操作符,就得先了解整数在内存中是如何存储的。
整数的二进制表示有3种形式,分别是原码,反码和补码。

  • 正整数的原码,反码和补码是相同的。
  • 负整数的原码,反码和补码是需要计算的。

假设我们写int a = 5;,5的二进制序列就是101。
这个101是如何得到的呢?在二进制中,从右往左的权重分别是2的0次方,2的1次方,2的2次方,2的3次方等等。那么,对于101,最右边的1的权重就是2的0次方,即1,左边的1的权重就是2的2次方,即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
这个原码再转换成10进制,即把二进制的1010转换成10进制,左边的1表示2的3次方,即8,右边的1表示2的1次方,即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. 整数的二进制序列有三种形式,分别是原码,反码和补码(后面简称原反补)。
  2. 整数在内存中以补码的形式存储。
  3. 正整数的原反补相同,负整数的原反补需要计算。
  4. 负整数的原反补的计算方式:
  • 最高位(符号位)为1表示负数,直接转换出来的二进制序列是原码。
  • 对原码符号位不变,其他位按位取反得到反码。
  • 反码+1得到补码
  • 补码-1得到反码
  • 反码的符号位不变,其他位按位取反得到原码。
  • 补码的符号位不变,其他位按位取反,得到的二进制序列再+1也可以直接得到原码。(也就是说,通过原码得到补码的方式,把这种方式用在补码上,也能反过来得到原码,类似负负得正)。

对于最后一点,举个例子:已知-5的补码。
-5的补码:11111111111111111111111111111011
先取反。
取反前:11111111111111111111111111111011
取反后:10000000000000000000000000000100
再+1:10000000000000000000000000000101
而这就是-5的原码。

3.2 右移操作符

学会了左移,接下来讲讲右移。
右移分为两种情况:

  1. 算术右移:右边丢弃,左边补原符号位
  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; // error

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),老生常谈的方法是:(如果看不懂的话,仔细对照注释,反复看)

// a = 3, b = 5
int tmp = a;
// a = 3, b = 5, tmp = 3
a = b;
// a = 5, b = 5, tmp = 3
b = tmp;
// a = 5, b = 3, tmp = 3

你可能想到可以用加减。

// a = 3, b = 5
a = a + b;
// a = 3+5, b = 5
b = a - b;
// a = 3+5, b = 3+5-5 = 3
a = a - b;
// a = 3+5-3 = 5, b = 3

用加减的话确实没有创建临时变量,但是有一个问题:如果a和b比较大,不过没有大到超过int的最大存储范围,但是加起来就超出int的最大存储范围了,算出来的结果就是错的。
所以,我们的主角登场了!用异或!

// a = 3, b = 5
a = a ^ b;
// a = 3^5, b = 5
b = a ^ b;
// a = 3^5, b = (3^5)^5 = 3
a = a ^ b;
// a = (3^5)^3 = 5, b = 3

我第一次看到这种方法,也直呼妙哉!
看明白了这三种方法,我有以下思考:
首先,要交换两个数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,取值有truefalsetrue表示真(本质上是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; // 对pa解引用,相当于把a改成了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 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。

// 在初始化时把arr[3]置成5,把arr[7]置成9
int arr[10] = {[3]=5, [7]=9};

12. 函数调用操作符

括号()为函数调用操作符,接受1个以上的操作数,分别是函数名和函数参数。
比如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类型的变量,在计算时会整型提升。如何提升呢?

整形提升是按照变量的数据类型的符号位来提升的。

  1. 有符号整型提升:高位补充符号位。
  2. 无符号整形提升:高位补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是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 操作符的属性

复杂表达式的求值有三个影响的因素。

  1. 操作符的优先级。
  2. 操作符的结合性。
  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;
}

这玩意,真是神仙来了都不知道怎么算呀!
总结:我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。

相关文章:

  • 图跃网站建设/seo外链怎么做
  • 以下是b2b电子商务网站/章鱼磁力链接引擎
  • 北京市海淀区市政府网站建设/世界十大网站排名
  • 如何做印刷报价网站/免费注册网站有哪些
  • 保定网站建设哪家好/网站建设方案优化
  • 网站可以做第三方检测报告/咨询网络服务商
  • python题库刷题训练软件
  • 【设计模式】【第九章】【设计模式小结】
  • 基于图搜索的规划算法之 A* 家族(十一):总结
  • RocketMQ 消费者Rebalance算法 解析——图解、源码级解析
  • 【0126】Latch中self-pipe trick的应用机制
  • 《gitlab从零到壹》出现问题:代码合并,源分支会被删除解决方案
  • 模拟电路中的“基础积木”是什么?
  • MySQL查询慢,除了索引,还有什么原因?
  • 行内元素和块级元素的区别
  • Spring 注解开发中bean作用范围与生命周期管理
  • 数据通信基础
  • 目标检测SSD学习笔记