第10讲:操作符详解

目录

  1. 操作符的分类

  2. 二进制和进制转换

  3. 原码、反码、补码

  4. 移位操作符

  5. 位操作符:&、|、^、~

  6. 单目操作符

  7. 逗号表达式

  8. 下标访问[]、函数调用()

  9. 结构成员访问操作符

  10. 操作符的属性:优先级、结合性

  11. 表达式求值

正文开始


操作符的分类

• 算术操作符:**+ 、- 、* 、/ 、%**

• 移位操作符:<< >> //移动的是二进制位

• 位操作符:& | ^ //位操作符是对二进制位进行计算

• 赋值操作符:**= 、+= 、 -= 、 = 、 /= 、%= 、<<= 、>>= 、&= 、|= 、^=*

• 单目操作符:!、++、–、&、*、+、-、~ 、sizeof、(类型)

• 关系操作符:> 、>= 、< 、<= 、 == 、^ !=

• 逻辑操作符:**&& 、||**

• 条件操作符:**? :**

• 逗号表达式:**,**

• 下标引用:**[]**

• 函数调用:**()**

• 结构成员访问:**. 、->**

上述的操作符,我们已经讲过算术操作符、赋值操作符、逻辑操作符、条件操作符和部分的单目操作符,今天继续介绍一部分,操作符中有一些操作符和二进制有关系,我们先铺垫一下二进制的和进制转换的知识。

二进制和进制转换

其实我们经常能听到2进制8 进制10 进制16 进制这样的讲法,那是什么意思呢?

其实 2 进制、 8 进制、 10 进制、 16 进制是数值的不同表示形式而已。

比如:数值 15 的各种进制的表示形式:

1
2
3
4
5
6
152 进制: 1111
158 进制: 17
1510 进制: 15
1516 进制:F
//16进制的数值之前写:0x
//8进制的数值之前写: 0

2进制的数字每一位都是0~1的数字组成
1111——>12^0 + 12^1 + 12^2 + 12^3 = 15

8进制的数字每一位是0~7的数字组成
1111——>18^0 + 18^1 + 18^2 + 18^3 = 17

10进制的数字每一位是0~9的数字组成
1111——>110^0 + 110^1 + 110^2 + 110^3 = 15

16进制的数字每一位是09,af的数字组成
1111——>116^0 + 116^1 + 116^2 + 116^3 = F

2进制 转 10进制

其实 10 进制的 123 表示的值是一百二十三,为什么是这个值呢?其实 10 进制的每一位是有 权重的, 10进制的数字从右向左是个位、十位、百位….,分别每一位的权重是10^0 ,10^1 ,10^2 …

如下图:
alt text

10进制123 每一位权重的理解

2 进制和 10 进制是类似的,只不过 2 进制的每一位的权重,从右向左是: 2^0 , 2^1 , 2^2 …
如果是 2 进制的 1101 ,该怎么理解呢?

alt text

2 进制 1101 每一位权重的理解

10进制转 2进制数字

alt text

10 进制转 2 进制

2 进制转 8 进制和 16 进制

2 进制转 8 进制

8 进制的数字每一位是07的,07的数字,各自写成 2 进制,最多有 3 个 2 进制位就足够了,比如 7 的二进制是 111 ,所以在 2 进制转 8 进制数的时候,从 2 进制序列中右边低位开始向左每 3 个 2 进制位会换算一个 8 进制位,剩余不够 3 个 2 进制位的直接换算。

如: 2进制的01101011,换成 8 进制:01530 开头的数字,会被当做 8 进制。

alt text

1
2
3
4
5
6
7
#include <stdio.h>
int main()
{
primntf("%d\n", 153);
primntf("%d\n", 0153);
return 0;
}

运行结果:

1
2
153
107

153的8进制:38^0 + 58^1 + 3*8^2 = 107

2 进制转 16 进制

16 进制的数字每一位是09,af的,09,af的数字,各自写成 2 进制,最多有 4 个 2 进制位就足够了,比如f的二进制是 1111 ,所以在 2 进制转 16 进制数的时候,从 2 进制序列中右边低位开始向左每 4 个 2 进制位会换算一个 16 进制位,剩余不够 4 个二进制位的直接换算。

如: 2 进制的 01101011 ,换成 16 进制:0x6b, 16 进制表示的时候前面加0x

alt text

原码、反码、补码

当你们要把一个数转换成2进制表示的时候,整数的2进制表示方法有三种,即原码、反码和补码

有符号整数 的三种表示方法均有 符号位数值位 两部分, 2 进制序列中,最高位的 1 位是被当做符号位剩余的都是数值位

alt text

符号位都是用 0 表示“正”,用 1 表示“负”。

正整数的原、反、补码都相同。

负整数的三种表示方法各不相同。

原码: 直接将数值按照正负数的形式翻译成二进制得到的就是原码。

反码: 将原码的符号位不变,其他位依次按位取反就可以得到反码。

补码: 反码+1就得到补码。
补码得到原码也是可以使用:取反,+1的操作。

图解:
alt text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <stdio.h>
int main()
{
int b = 10; //正整数的原、反、补码都相同。
//00000000000000000000000000001010-- 原码
//00000000000000000000000000001010-- 反码
//00000000000000000000000000001010-- 补码

int a = -10; //原码: 直接将数值按照正负数的形式翻译成二进制得到的就是原码。 反码: 将原码的符号位不变,其他位依次按位取反就可以得到反码。 补码: 反码+1就得到补码。
//10000000000000000000000000001010 -- 原码
//11111111111111111111111111110101 -- 反码
//11111111111111111111111111110110 -- 补码 //满2进1

//11111111111111111111111111110110 -- 补码
//10000000000000000000000000001001
//10000000000000000000000000001010 -- 原码

return 0;
}
//
//整数在内存中存储的是2进制的补码
//

int main()
{
// //1 - 1;
// //1 + (-1)
// //尝试用原码计算
// //000000000000000000000000000000001 1的原码
// //100000000000000000000000000000001 -1的原码
// //
// //100000000000000000000000000000010 -2 //err
//
// //尝试使用补码计算
// //000000000000000000000000000000001 1的补码
// //100000000000000000000000000000001 -1的原码
// //111111111111111111111111111111110
// //111111111111111111111111111111111 -1的补码
// //000000000000000000000000000000001
// //000000000000000000000000000000000 -- 0
// //
return 0;
}

对于整形来说:数据存放内存中其实存放的是补码

为什么呢?

在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理( CPU只有加法器 )此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

移位操作符

<<左移操作符

.>>右移操作符

注:移位操作符的操作数只能是整数。

左移操作符

移位规则:左边抛弃、右边补 0

整数例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int main()
{
int a = 10;
int b = a << 1;
//int b = a << 1;
//10
//00000000000000000000000000001010
//
printf("b = %d\n", b);
printf("a = %d\n", a);

return 0;
}

运行结果:

1
2
b=20
a=10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int main()
{
int a = 10;
a = a << 1; //a <<= 1;
//int b = a << 1;
//10
//00000000000000000000000000001010
//
//printf("b = %d\n", b);
printf("a = %d\n", a);

return 0;
}

运行结果:

1
a=20

alt text

负数例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<stdio.h>
int main()
{
int a = -1;
//10000000000000000000000000000001 //原码
//11111111111111111111111111111110 //反码
//11111111111111111111111111111111 //补码
//
//11111111111111111111111111111110 //b的补码
//10000000000000000000000000000001 //反码
//10000000000000000000000000000010 //原码
int b = a << 1;

printf("a=%d\n", b);//-2
printf("b=%d\n", a);//-1

return 0;
}

运行结果

1
2
a=-1
b=-2

左移操作符演示

alt text

右移操作符

移位规则:首先右移运算分两种

  1. 逻辑右移:左边用 0 填充,右边丢弃

  2. 算术右移:左边用原该值的符号位填充,右边丢弃

右移到底采用算术右移还是逻辑右移?

  1. 取决于编译器

  2. 通常采用算术右移

alt text

注意:右移操作符的操作数只能是整数,不能是浮点数

alt text

举例1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

int main()
{
int a = -10;

//10000000000000000000000000001010 //原码
//11111111111111111111111111110101 //反码
//11111111111111111111111111110110 //补码
//
int b = a >> 1; //算术右移
//11111111111111111111111111111011 //b的补码
//10000000000000000000000000000100 //反码
//10000000000000000000000000000101 //原码
//
printf("a = %d\n", b);
printf("b = %d\n", a);

return 0;
}

运行结果

1
2
a = -10
b = -5

举例2

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main()
{
int num = 10 ;
int n = num>> 1 ;
printf("n= %d\n", n);
printf("num= %d\n", num);
return 0 ;
}

alt text

逻辑右移 1 位演示

alt text

算术右移 1 位演示

警告⚠:对于移位运算符,不要移动负数位,这个是标准未定义的

例如

1
2
3
int num = 10 ;

num>>-1;//error

位操作符:&、|、^、~

位操作符有

1
2
3
4
& //按位与
| //按位或
^ //按位异或
~ //按位取反

他们的操作数必须是整数。

**举例**:

按位与

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>

int main()
{
int a = 6;
//00000000000000000000000000000110 ——> 6的补码
//原码和反码与补码相同

int b = -7;
//10000000000000000000000000000111 //b的原码
//11111111111111111111111111111000 //b的反码
//11111111111111111111111111111001 ——> -7的补码

int c = a & b; //按位与: a和b的补码的二进制位进行运算
//对应的二进制位规则:有0则为0,两个同时为1,才为1
//
//00000000000000000000000000000110 ——> 6的补码
//11111111111111111111111111111001 ——> -7的补码
//00000000000000000000000000000000 ——> c的补码, 结果为 0,0的原码和补码与补码相同
printf("%d\n",c);

return 0;
}

运行结果

1
0

按位或

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>

int main()
{
int a = 6;
//00000000000000000000000000000110 ——> 6的补码
//原码和反码与补码相同

int b = -7;
//10000000000000000000000000000111 //b的原码
//11111111111111111111111111111000 //b的反码
//11111111111111111111111111111001 ——> -7的补码
//
int c = a | b; //按位或: a和b的补码的二进制位进行运算
//规则:只要有1,就是1,两个同时为0,才为0
//
//00000000000000000000000000000110 ——> 6的补码
//11111111111111111111111111111001 ——> -7的补码
//11111111111111111111111111111111 ——> c的补码,结果为 -1
//10000000000000000000000000000000 ——> -1的反码
//10000000000000000000000000000001 ——> -1的原码
//
printf("%d\n",c);

return 0;
}

运行结果

1
-1

按位异或

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>

int main()
{
int a = 6;
//0000000000000000000000000000110 ——> 6的补码
//原码和反码与补码相同

int b = -7;
//10000000000000000000000000000111 //b的原码
//11111111111111111111111111111000 //b的反码
//11111111111111111111111111111001 ——> -7的补码
//
int c = a ^ b; //按位异或: a和b的补码的二进制位进行运算
//规则:对应的二进制位,相同为0,相异为1
//
//00000000000000000000000000000110 ——> 6的补码
//11111111111111111111111111111001 ——> -7的补码
//11111111111111111111111111111111 ——> c的补码,结果为 -1
//10000000000000000000000000000000 ——> -1的反码
//10000000000000000000000000000001 ——> -1的原码
//
printf("%d\n",c);//-1

return 0;
}

运行结果

1
-1

按位取反

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int main()
{
int a = 0;
printf("%d\n",~a); //-1
//~ 按(2进制)位取反
//00000000000000000000000000000000 ——> 0的补码
//11111111111111111111111111111111 ——> -1的补码

//10000000000000000000000000000000 ——> -1的反码
//10000000000000000000000000000001 ——> -1的原码

return 0;
}

运行结果

1
-1

直接上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <stdio.h>
int main()
{
int num1 = -3;
int num2 = 5 ;
printf("%d\n", num1 & num2);
printf("%d\n", num1 | num2);
printf("%d\n", num1 ^ num2);
printf("%d\n", ~ 0 );
return 0 ;
}

//10000000000000000000000000000011 ——> -3的原码
//11111111111111111111111111111100 ——> -3的反码
//11111111111111111111111111111101 ——> -3的补码

//00000000000000000000000000000101 ——> 5的原码,反码,补码


//num1 & num2 ——> 按位与: a和b的补码的二进制位进行运算
//规则:有0则为0,两个同时为1,才为1
//11111111111111111111111111111101 ——> -3的补码
//00000000000000000000000000000101 ——> 5的补码

//00000000000000000000000000000101 ——> 结果为5 ——> 补码,反码,补码相同


//num1 | num2 ——> 按位或: a和b的补码的二进制位进行运算
//规则:只要有1,就是1,两个同时为0,才为0
//11111111111111111111111111111101 ——> -3的补码
//00000000000000000000000000000101 ——> 5的补码
//11111111111111111111111111111101 补码

//11111111111111111111111111111100 反码
//10000000000000000000000000000011 原码,结果为 -3

//num1 ^ num2——> 按位异或: a和b的补码的二进制位进行运算
//规则:相同为0,相异为1
//11111111111111111111111111111101 ——> -3的补码
//00000000000000000000000000000101 ——> 5的补码
//11111111111111111111111111111000 补码

//11111111111111111111111111110111 反码
//10000000000000000000000000001000 原码,结果为8


//~ 按(2进制)位取反
//00000000000000000000000000000000 ——> 0的补码
//11111111111111111111111111111111 ——> -1的补码

//10000000000000000000000000000000 ——> -1的反码
//10000000000000000000000000000001 ——> -1的原码

运行结果

1
2
3
4
5
-3
-8
-1

一道变态的面试题

不能创建临时变量(第三个变量),实现两个整数的交换

举例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main()
{
int a = 3;
int b = 5;

int c = 0;//空瓶

printf("交换前:a=%d b=%d\n", a, b);
c = a;
a = b;
b = c;
printf("交换后:a=%d b=%d\n", a, b);

return 0;
}

运行结果

1
2
交换前:a=3 b=5
交换后:a=5 b=3

举例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main()
{
int a = 3;
int b = 5;

printf("交换前:a=%d b=%d\n", a, b);

a = a + b;
b = a - b;
a = a - b;

printf("交换后:a=%d b=%d\n", a, b);

return 0;
}

运行结果

1
2
交换前:a=3 b=5
交换后:a=5 b=3

为什么不能创建临时变量?

因为临时变量的生命周期只在当前语句块内,而在当前语句块结束后,临时变量就会被销毁,导致结果错误

使用异或运算符实现两个整数的交换

理解使用异或运算符实现两个整数的交换的用法

alt text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

int main()
{
int a = 3;
int b = 5;


printf("交换前:a=%d b=%d\n", a, b);

a = a ^ b;
b = a ^ b;
a = a ^ b;

printf("交换后:a=%d b=%d\n", a, b);

return 0;
}

运行结果

1
2
交换前:a=3 b=5
交换后:a=5 b=3

练习 1 : 编写代码实现:求一个整数存储在内存中的二进制中 1 的个数。

举例1

alt text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>

int count_bit_one(unsigned int n)//unsigned int 无符号整型
{
int count = 0;
while (n)//当 n 非 0 时,执行循环
{
if ((n % 2) == 1)
count++;

n = n / 2;
}

return count;
}
//00000000000000000000000000001111
//10000000000000000000000000000001
//11111111111111111111111111111110
//11111111111111111111111111111111

int main()
{
int num = 0;
scanf("%d", &num);//-1
int ret = count_bit_one(num);
printf("%d\n", ret);

return 0;
}

运行结果

1
2
15 //——>输入
4

举例2

alt text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>

int count_bit_one(int n)
{
int i = 0;
int count = 0;
for (i = 0; i < 32; i++)
{
if (((n >> i) & 1) == 1)
{
count++;
}
}
return count;
}

int main()
{
int num = 0;
scanf("%d", &num);//-1
int ret = count_bit_one(num);
printf("%d\n", ret);

return 0;
}

运行结果

1
2
15 //——>输入
4

举例3

alt text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>

int count_bit_one(int n)
{
int count = 0;
while (n)
{
count++;
n = n & (n - 1);
}
return count;
}

int main()
{
int num = 0;
scanf("%d", &num);//-1
int ret = count_bit_one(num);
printf("%d\n", ret);

return 0;
}

运行结果

1
2
15 //——>输入
4

分析

  • 方法 1:循环求余,判断余数是否为 1 ,是则计数器加 1 ,然后除以 2 ,直到 n 为 0 。

  • 方法 2:循环 32 次,判断第 i 位是否为 1 ,是则计数器加 1 。

  • 方法 3:循环求余,判断余数是否为 1 ,是则计数器加 1 ,然后与 n 进行位与运算,去掉最后一位,直到 n 为 0 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
参考代码:
// 方法 1
// 主函数,计算一个整数的二进制表示中 1 的个数
#include <stdio.h>

int main()
{
int num = 10 ;
int count= 0 ; // 计数
while(num)
{
if(num % 2 == 1 )
count++;
num = num / 2 ;
}
printf("二进制中 1 的个数 = %d\n", count);
return 0 ;
}

// 思考这样的实现方式有没有问题?

// 方法 2
// 主函数,计算一个整数的二进制表示中 1 的个数
#include <stdio.h>

int main()
{
int num = -1;
int i = 0 ;
int count = 0 ; // 计数
for(i= 0 ; i< 32 ; i++)
{
if( num & ( 1 << i) )
count++;
}
printf("二进制中 1 的个数 = %d\n",count);
return 0 ;
}

// 思考还能不能更加优化,这里必须循环 32 次的。

// 方法 3
// 主函数,计算一个整数的二进制表示中 1 的个数
#include <stdio.h>

int main()
{
int num = -1;
int i = 0 ;
int count = 0 ; // 计数
while(num)
{
count++;
num = num & (num - 1);
}
printf("二进制中 1 的个数 = %d\n",count);
return 0 ;
}
// 这种方式是不是很好?达到了优化的效果,但是难以想到。

练习 2 : 二进制位置 0 或者置 1

编写代码将 13 二进制序列的第 5 位修改为 1 ,然后再改回 0

1
2
3
132 进制序列: 00000000000000000000000000001101
将第 5 位置为 1 后: 00000000000000000000000000011101
将第 5 位再置为 000000000000000000000000000001101

参考代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>

int main()
{
int a = 13;
//00000000000000000000000000001101
//00000000000000000000000000010000
//00000000000000000000000000000001
//
//00000000000000000000000000011101
//11111111111111111111111111101111
//
//00000000000000000000000000000001
int n = 5;
a = a | (1 << (n - 1));
printf("%d\n", a);

a &= ~(a << (n - 1));
printf("%d\n", a);

return 0;
}

// 改进版:

#include <stdio.h>
int main()
{
int a = 13 ;
//00000000000000000000000000001101
//00000000000000000000000000010000
//00000000000000000000000000000001
//
//00000000000000000000000000011101
//11111111111111111111111111101111
//
//00000000000000000000000000000001

a = a | ( 1 << 4 );
printf("a = %d\n", a);

a = a & ~( 1 << 4 );
printf("a = %d\n", a);

return 0 ;
}

单目操作符

单目操作符有这些:

1
!、++、--、&、*、+、-、~ 、sizeof、(类型)

alt text

单目操作符的特点是只有一个操作数,在单目操作符中只有 **&**和 * 没有介绍,这 2 个操作符,我们放在学习指针的时候学习。

逗号表达式

1
exp1, exp2, exp3, ...expN

逗号表达式,就是用逗号隔开的多个表达式。

逗号表达式,从左向右依次执行整个表达式的结果是最后一个表达式的结果

注意:逗号表达式,一定要从左向右依次执行,因为前面的表达式的计算,可能会影响后面的表达式的计算。

代码1:

定义变量 a、b 并使用逗号表达式给变量 c 赋值
逗号表达式中依次执行 a>b、a=b+10、a、b=a+1,最后 c 被赋值为 b 的值,即 13

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main()
{
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, a, b = a + 1);//逗号表达式,一定要从左向右依次执行
// 执行 a > b, 1 > 2,表达式为假,结果为0
// 执行 a = b + 10, a = 12
// 执行 a, 结果为12
// 执行 b = a + 1, b = 13
// 最后 c = 13

printf("%d\n", c);

return 0;
}

运行结果:

1
13

代码2:

使用逗号表达式在条件语句中赋值,但应注意逗号表达式的返回值为最后一个表达式的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>

int main()
{
// 主函数:程序入口

int a = 0;
int b = 0;
int c = 0;
int d = 0;

if (a = b + 1, c = a / 2, d > 0)//从左向右依次执行,但主要看最后一个表达式的结果
// 执行 a = b + 1, a = 1
// 执行 c = a / 2, c = 0
// 执行 d > 0, 表达式为假,结果为0
// 最后结果为 0
{
// 如果条件成立则执行以下代码块
}

return 0;
}

代码3:

调用 get_val() 函数获取 a 的值,并进行业务处理直到 a 不大于 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <stdio.h>
// 主函数,程序入口
int main()
{
// 获取初始数值
a = get_val();
// 统计数值
count_val(a);

// 当数值大于 0 时执行循环
while (a > 0)
{
// 业务处理
// ...

// 获取新的数值
a = get_val();
// 统计数值
count_val(a);
}
return 0;
}

//改进版:

#include <stdio.h>
// 主函数,程序入口
int main()
{
// 获取初始数值
a = get_val();
// 统计数值
count_val(a);

// 使用逗号表达式改写循环条件
// 当数值大于 0 时执行循环
while (a = get_val(), count_val(a), a>0)//从左向右依次执行,但主要看最后一个表达式的结果
{
// 业务处理
}
return 0;
}

下标访问[]、函数调用()

[ ]下标引用操作符

操作数:一个数组名 + 一个索引值(下标)

举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main()
{
int arr[10] = { 1,2,3,4,5 };

int m = arr[4];//数组中下标是4的元素
//[] 下标引用操作符 - 操作数是:arr,4
//3+5, + 是操作符,3和5是操作数
//
printf("%d\n", m);

return 0;
}

解读:

1
2
3
int arr[ 10 ];//创建数组
arr[ 4 ] = 10 ;//实用下标引用操作符。
[ ]的两个操作数是arr和 4 ,其中arr是数组名,4是索引值。

函数调用操作符()

接受一个或者多个操作数:第一个操作数函数名剩余的操作数就是传递给函数的参数

举例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

int Add(int x, int y)
{
return x + y;
}

int main()
{
printf("hehe\n");//() 就是函数调用操作符,操作数是:
printf("%d\n", 100);
int ret = Add(3, 5);//Add 3 5
//函数调用操作符最少有几个操作数?

return 0;
}

举例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

// 定义第一个测试函数,用于打印 "hehe"
void test1()
{
printf("hehe\n");
}

// 定义第二个测试函数,用于打印传入的字符串参数
void test2(const char *str)
{
printf("%s\n", str);
}

// 主函数
int main()
{
test1(); // 这里的()就是作为函数调用操作符。
test2("hello bit."); // 这里的()就是函数调用操作符。
return 0;
}

结构成员访问操作符

结构体

C语言已经提供了内置类型,如:char、short、int、long、float、double等,但是只有这些内置类型还是不够的,假设我想描述学生,描述一本书,这时单一的内置类型是不行的。

描述一个学生需要名字、年龄、学号、身高、体重等;

描述一本书需要作者、出版社、定价等。C语言为了解决这个问题,增加了结构体这种自定义的数据类
型,让程序员可以自己创造适合的类型。

📌结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量,如:
标量数组指针,甚至是其他结构体

结构的声明

1
2
3
4
struct tag         //struct ——> 结构体关键字, tag ——> 结构体的名称(自定义)
{
member-list;//成员列表
}variable-list; //变量列表

描述一个学生

1
2
3
4
5
6
7
struct Stu
{
char name[ 20 ];//名字
int age;//年龄
char sex[ 5 ];//性别
char id[ 20 ];//学号
}; //分号不能丢

结构体变量的定义和初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//学生类型
struct Student
{
//成员变量
char name[20];//名字
int age;//年龄
float score;//成绩
}s4, s5, s6;//全局变量

struct Student s3 = {"王五", 25, 88.8};//全局变量

struct Point//坐标类型
{
int x;
int y;
};

struct S
{
char ch;//字符型变量
struct Point p;//坐标变量
int arr[10];//数组变量
double d;//浮点型变量
};

int main()
{
int a;

struct Student s1 = {"翠花", 20, 98.0};//局部变量
struct Student s2 = {"旺财", 18, 69.8};
struct Point p = { 10, 20 };
struct S s = { 'a',{4,5}, {1,2,3,4,5,6,7},3.14 };

printf("%c\n", s.ch);//打印S的结构体变量ch成员的值
printf("坐标是:%d %d\n", s.p.x, s.p.y);
printf("%d\n", s.arr[0]);
printf("%lf\n", s.d);

//结构体变量.结构体成员名
//-> 这个是依赖指针的,所以放在后期给大家介绍
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 代码 1 :变量的定义
// 结构体类型定义,包括两个整型成员x和y
struct Point
{
int x;
int y;
} p1; //声明类型的同时定义变量p
struct Point p2; //定义结构体变量p

// 代码2:初始化。
// 结构体类型Stu包括名字和年龄两个成员
struct Stu //类型声明
{
char name[15];//名字
int age; //年龄
};
// 初始化结构体变量s1,名字为zhangsan,年龄为20
struct Stu s1 = {"zhangsan", 20 };//初始化
// 按照指定顺序初始化结构体变量s2,年龄为20,名字为lisi
struct Stu s2 = {.age=20, .name="lisi"};//指定顺序初始化

// 代码 3
// 结构体类型Node包括整型成员data,Point类型成员p和指向Node类型的指针成员next
struct Node
{
int data;
struct Point p;
struct Node* next;
} n1 = {10, {4, 5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化

结构成员访问操作符

结构体成员的直接访问

结构体成员直接访问是通过点操作符(.)访问的。点操作符接受两个操作数。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

// 定义一个结构体 Point,包括两个整型成员 x 和 y
struct Point
{
int x; // 横坐标
int y; // 纵坐标
} p = { 1 , 2 }; // 创建一个结构体变量 p,并初始化 x 和 y

int main()
{
// 打印 p 的 x 和 y 值
printf("x: %d y: %d\n", p.x, p.y);
return 0; // 返回 0,表示程序执行成功
}

使用方式结构体变量.成员名

结构体成员的间接访问

有时候我们得到的不是一个结构体变量,而是得到了一个指向结构体的指针。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>

// 定义结构体Point,包含整型变量x和y
struct Point
{
int x;
int y;
};

int main()
{
// 创建Point类型的结构体变量p,并初始化x和y的值为3和4
struct Point p = { 3 , 4 };
// 创建指向Point类型结构体变量p的指针ptr
struct Point *ptr = &p;
// 通过指针ptr修改p的成员变量x和y的值为10和20
ptr->x = 10 ;
ptr->y = 20 ;
// 打印修改后的x和y的值
printf("x = %d y = %d\n", ptr->x, ptr->y);
// 返回0表示程序正常结束
return 0 ;
}

使用方式结构体指针->成员名

综合举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 定义一个结构体Stu,包含名字和年龄两个成员变量
struct Stu
{
char name[ 15 ]; // 名字
int age; // 年龄
};

// 打印结构体Stu变量的名字和年龄
void print_stu(struct Stu s)
{
printf("%s %d\n", s.name, s.age);
}

// 设置结构体Stu变量的名字为"李四",年龄为28
void set_stu(struct Stu* ps)
{
strcpy(ps->name, "李四");
ps->age = 28 ;
}

// 主函数
int main()
{
// 创建一个Stu结构体变量s,并初始化为"张三"和20
struct Stu s = { "张三", 20 };
// 打印s结构体变量的名字和年龄
print_stu(s);
// 调用set_stu函数设置s的名字和年龄
set_stu(&s);
// 再次打印s结构体变量的名字和年龄
print_stu(s);
return 0 ;
}

更多关于结构体的知识,后期在《第 19 讲:自定义类型:结构体》中讲解。

操作符的属性:优先级、结合性

C语言的操作符有 2 个重要的属性:优先级结合性,这两个属性决定了表达式求值的计算顺序

优先级

优先级指的是,如果一个表达式包含多个运算符,哪个运算符应该优先执行。各种运算符的优先级是不一样的。

相邻的运算符,优先级由高到低依次是

1
2
3
4
5
6
#include <stdio.h>

int main()
{
int r = (3+4) * 5; // 先计算加法,再计算乘法
}
1
3 + 4 * 5 ;// 先计算乘法,再计算加法

上面示例中,表达式3 + 4 * 5里面既有加法运算符(+),又有乘法运算符(*)。由于乘法的优先级高于加法,所以会先计算4 * 5,而不是先计算3 + 4

结合性

**如果两个运算符优先级相同,优先级没办法确定先计算哪个了,这时候就看结合性了,则根据运算符是左结合,还是右结合,决定执行顺序。大部分运算符是左结合(从左到右执行),少数运算符是右结合(从右到左执行)比如赋值运算符(=)**。

1
5 * 6 / 2 ;

上面示例中,*和/的优先级相同,它们都是左结合运算符,所以从左到右执行,先计算5 * 6,再计算/ 2

运算符的优先级顺序很多,下面是部分运算符的优先级顺序(按照优先级从高到低排列),建议大概记住这些操作符的优先级就行,其他操作符在使用的时候查看下面表格就可以了。

  • 圆括号(())
  • 自增运算符(++),自减运算符(–)
  • 单目运算符(+和-)
  • 乘法(*),除法(/)
  • 加法(+),减法(-)
  • 关系运算符(<、>等)
  • 赋值运算符(=)

由于圆括号的优先级最高,可以使用它改变其他运算符的优先级。

alt text

参考:https://zh.cppreference.com/w/c/language/operator_precedence

表达式求值

整型提升

C语言中整型算术运算总是至少以缺省(默认)整型类型的精度来进行的。

为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换普通整型,这种转换称为 整型提升

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main()
{
//int a = 10;
//int b = 20;
//int c = a + b;

char a = 20;
char b = 130;

char c = a +b; // 整型提升,a和b被转换为int类型

printf("%d\n", c);

return 0;
}

整型提升的意义

表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节⻓度一般就是int的字节⻓度,同时也是CPU的通用寄存器的⻓度。

因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准⻓度。

通用CPU(general-purposeCPU)是难以直接实现两个 8 比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种⻓度可能小于int⻓度的整型值,都必须先转换为int或unsignedint,然后才能送入CPU去执行运算。

1
2
3
4
//实例 1
char a,b,c;
***
a = b + c;

b和c的值被提升为普通整型,然后再执行加法运算

加法运算完成之后,结果将被截断,然后再存储于a中

如何进行整体提升呢?

  1. 有符号整数提升是按照变量的数据类型的符号位来提升的

  2. 无符号整数提升,高位补 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//负数的整形提升
char c1 = -1;
变量c1的二进制位(补码)中只有 8 个比特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为 1
提升之后的结果是:
11111111111111111111111111111111

//正数的整形提升
char c2 = 1 ;
变量c2的二进制位(补码)中只有 8 个比特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为 0
提升之后的结果是:
00000000000000000000000000000001

//无符号整形提升,高位补 0

算术转换

如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换

1
2
3
4
5
6
7
long double
double
float
unsigned long int
long int
unsigned int
int

如果某个操作数的类型在上面这个列表中排名靠后,那么首先要转换为另外一个操作数的类型后执行运算。

问题表达式解析

表达式 1

1
2
3
//表达式的求值部分由操作符的优先级决定。
//表达式 1
a*b + c*d + e*f

表达式 1 在计算的时候,由于比+的优先级高,只能保证,的计算是比+早,但是优先级并不能决定第三个*比第一个+早执行。

所以表达式的计算机顺序就可能是:

1
2
3
4
5
a*b
c*d
a*b + c*d
e*f
a*b + c*d + e*f

或者

1
2
3
4
5
a*b
c*d
e*f
a*b + c*d
a*b + c*d + e*f

表达式 2

1
2
//表达式 2
c + --c;

#同上,操作符的优先级只能决定自减–的运算在+的运算的前面,但是我们并没有办法得知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。

表达式 3

1
2
3
4
5
6
7
8
9
#include <stdio.h>
//表达式 3
int main()
{
int i = 10 ;
i = i-- - --i * ( i = -3 ) * i++ + ++i;
printf("i = %d\n", i);
return 0 ;
}

表达式 3 在不同编译器中测试结果:非法表达式程序的结果

alt text

表达式 4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#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 ;
}

这个代码有没有实际的问题?有问题!

虽然在大多数的编译器上求得结果都是相同的。

但是上述代码answer = fun() - fun() * fun();中我们只能通过操作符的优先级得知:先
算乘法,再算减法。

函数的调用先后顺序无法通过操作符的优先级确定。

表达式 5 :

1
2
3
4
5
6
7
8
9
10
11
//表达式 5
#include <stdio.h>
int main()
{
int i = 1 ;
int ret = (++i) + (++i) + (++i);
printf("%d\n", ret);
printf("%d\n", i);
return 0 ;
}
//尝试在linux 环境gcc编译器,VS2013环境下都执行,看结果。

gcc编译器执行结果:

alt text

VS2022运行结果:

alt text

看看同样的代码产生了不同的结果,这是为什么?

简单看一下汇编代码,就可以分析清楚.

这段代码中的第一个+在执行的时候,第三个++是否执行,这个是不确定的,因为依靠操作符的优先级和结合性是无法决定第一个+和第三个前置++的先后顺序。

总结

即使有了操作符的优先级和结合性,我们写出的表达式依然有可能不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在潜在⻛险的,建议不要写出特别复杂的表达式。