系统性学习C语言-第十讲-操作符详讲
系统性学习C语言-第十讲-操作符详讲
- 1. 操作符的分类
- 2. 二进制和进制转换
- 2.1 二进制转十进制
- 2.1.1 十进制转二进制数字
- 2.2 二进制转八进制和十六进制
- 2.2.1 二进制转八进制
- 2.2.2 二进制转十六进制
- 3. 原码,反码,补码
- 4. 移位操作符
- 4.1 左移操作符
- 4.2 右移操作符
- 5. 位操作符:& 、| 、^ 、~
- 5.1 & 按位与操作符
- 5.2 | 按位或操作符
- 5.3 ^ 按位异或操作符
- 5.4 ~ 按位取反操作符
- 5.5 练习一
- 5.5.1 原理解析
- 5.6 练习二
- 5.7 练习三
- 6. 单目操作符
- 7. 逗号表达式
- 8. 下标访问 [ ]、函数调用 ( )
- 8.1 [ ]下标引用操作符
- 8.2 函数调用操作符
- 9. 结构成员访问操作符
- 9.1 结构体
- 9.1.1 结构的声明
- 9.1.2 结构体变量的定义和初始化
- 9.2 结构成员访问操作符
- 9.2.1 结构体成员的直接访问
- 9.2.2 结构体成员的间接访问
- 10. 操作符的属性:优先级、结合性
- 10.1 优先级
- 10.2 结合性
- 11. 表达式求值
- 11.1 整型提升
- 11.2 算术转换
- 11.3 问题表达式解析
- 11.3.1 表达式 1
- 11.3.2 表达式 2
- 11.3.3 表达式 3
- 11.3.4 表达式 4
- 11.3.5 表达式 5
1. 操作符的分类
-
算术操作符: + 、- 、* 、/ 、%
-
移位操作符: << >>
-
位操作符: & | ^
-
赋值操作符: = 、+= 、 -= 、 *= 、 /= 、%= 、<<= 、>>= 、&= 、|= 、^=
-
单⽬操作符: !、++、–、&、*、+、-、~ 、sizeof、(类型)
-
关系操作符: > 、>= 、< 、<= 、 == 、 !=
-
逻辑操作符: && 、||
-
条件操作符: ? :
-
逗号表达式: ,
-
下标引⽤: []
-
函数调⽤: ()
-
结构成员访问: . 、->
上述的操作符,我们已经讲过算术操作符、赋值操作符、逻辑操作符、条件操作符和部分的单⽬操作符,今天继续介绍⼀部分,
操作符中有⼀些操作符和⼆进制有关系,我们先铺垫⼀下⼆进制的和进制转换的知识。
2. 二进制和进制转换
其实我们经常能听到 2 进制、8 进制、10 进制、16 进制 这样的讲法,那是什么意思呢?
其实2进制、8进制、10进制、16进制是数值的不同表⽰形式⽽已。
⽐如:数值15的各种进制的表⽰形式:
15的2进制:1111
15的8进制:17
15的10进制:15
15的16进制:F
//16进制的数值之前写:0x
//8进制的数值之前写:0
我们重点介绍⼀下⼆进制:
⾸先我们还是得从10进制讲起,其实10进制是我们⽣活中经常使⽤的,我们已经形成了很多常识:
-
10进制中满10进1
-
10进制的数字每⼀位都是0~9的数字组成
其实⼆进制也是⼀样的
-
2进制中满2进1
-
2进制的数字每⼀位都是0~1的数字组成
那么 1101 就是⼆进制所代表的一个数字了。
2.1 二进制转十进制
其实10进制的123表⽰的值是⼀百⼆⼗三,为什么是这个值呢?其实10进制的每⼀位是有权重的,
10进制的数字从右向左是个位、⼗位、百位…,分别每⼀位的权重是 10 ^ 0(十的零次方),10 ^ 1,10 ^ 2 ……
如下图:
2进制和10进制是类似的,只不过2进制的每⼀位的权重,从右向左是: 2 ^ 0,2 ^ 1,2 ^ 2 ……
如果是2进制的1101,该怎么理解呢?
2.1.1 十进制转二进制数字
2.2 二进制转八进制和十六进制
2.2.1 二进制转八进制
8 进制的数字每⼀位是 0 ~ 7 的,0 ~ 7 的数字,各⾃写成 2 进制,最多有 3 个 2 进制位就⾜够了,⽐如 7 的⼆进制是111 ,
所以在 2 进制转 8 进制数的时候,从 2 进制序列中右边低位开始向左,每 3 个 2 进制位会换算⼀个 8 进制位,
剩余不够 3 个 2 进制位的直接换算。
如:2 进制的 01101011,换成 8 进制:0153,0 开头的数字,会被当做 8 进制。
2.2.2 二进制转十六进制
16 进制的数字每⼀位是 0 ~ 9, a ~ f 的,0 ~ 9, a ~ f 的数字,各⾃写成 2 进制,最多有 4 个 2 进制位就⾜够了,
⽐如 f 的⼆进制是 1111,所以在 2 进制转 16 进制数的时候,从 2 进制序列中右边低位开始向左,每4个2进制位换算⼀个16进制位,
剩余不够 4 个⼆进制位的直接换算。
如:2 进制的 01101011,换成16进制:0x6b,16进制表⽰的时候前⾯加 0x
3. 原码,反码,补码
整数的 2 进制表⽰方法有三种,即原码、反码、补码
有符号整数的三种表示⽅法均有符号位和数值位两部分,2 进制序列中,最⾼位的 1 位是被当做符号位,剩余的都是数值位。
符号位都是⽤ 0 表⽰“正”,⽤ 1 表⽰“负”。
正整数的原、反、补码都相同。
负数与正数的三种表示方法各不相同。
原码:直接将数值按照正负数的形式翻译成⼆进制得到的就是原码。
反码:将原码的符号位不变,其他位依次按位取反就可以得到反码。
补码:反码+1就得到补码。
补码得到原码也是可以使⽤:取反,+ 1 的操作。
对于整形来说:数据存放内存中其实存放的是补码。
原因如下:
在计算机系统中,数值⼀律⽤补码来表⽰和存储。原因在于,使⽤补码,可以将符号位和数值域统⼀处理;
同时,加法和减法也可以统⼀处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
4. 移位操作符
左移操作符: <<
右移操作符: >>
注意:
移位操作符的操作数只能是整数
4.1 左移操作符
移位规则:左边抛弃、右边补 0 。
#include <stdio.h>
int main()
{int num = 10;int n = num<<1;printf("n= %d\n", n);printf("num= %d\n", num);return 0;
}
4.2 右移操作符
移位规则:首先右移运算分两种
-
逻辑右移:左边⽤0填充,右边丢弃
-
算术右移:左边⽤原该值的符号位填充,右边丢
#include <stdio.h>
int main()
{int num = 10;int n = num>>1;printf("n= %d\n", n);printf("num= %d\n", num);return 0;}
逻辑右移:
算数右移:
当操作数为有符号数时:
在 C/C++ 标准中规定,对有符号数的右移由编译器决定,但主流编译器默认采用算数右移以保持数学意义,
当我们的操作数为有符号数时,正数高位补 0 ,负数高位补 1 ,低位丢弃。
当操作数为无符号数时:
在 C/C++ 标准中强制要求无符号数的右移必须为逻辑右移,高位补 0 ,低位丢弃。
警告⚠️:
对于移位运算符,不要移动负数位,这个是标准未定义的。
例如:
int num = 10;
num >> -1;
5. 位操作符:& 、| 、^ 、~
位操作符有:
& //按位与
| //按位或
^ //按位异或
~ //按位取反
注意:
他们的操作数必须是整数。
5.1 & 按位与操作符
对于这个符号,其实我们之前见过相似的 &&
,这是逻辑操作符中的与。在逻辑判断中,当左右表达式同时为真,整个表达式才为真
一旦有表达式为假,则整个表达式为假,总结为:全真才真,一假则假。
&
按位与操作符有同样的性质,其本身也为双目操作符,拥有左操作数与右操作数,但是在操作数的二进制补码层面上进行操作的,
在两个操作数同样权重的二进制位上,如果同时为 1 ,那么最终的表达式中同样的权重位也为 1 ,若有一方为 0 ,
那么最终表达式中同样的权重位也为 0 ,总结为 :全 1 为 1 ,有 0 为 0 。
下面我们通过一个例子具体理解一下 &
按位与操作符。
#include <stdio.h>
int main()
{int num1 = 3;int num2 = -5;printf("%d\n", num1 & num2);return 0;
}
3 的原码:00000000 00000000 00000000 00000011
3 的反码:00000000 00000000 00000000 00000011 (正数的原码、反码、补码一致)
3 的补码:00000000 00000000 00000000 00000011
-5 的原码:10000000 00000000 00000000 00000101
-5 的反码:11111111 11111111 11111111 11111010 (负数的反码是在原码基础上,符号位不变,其余取反)
-5 的补码:11111111 11111111 11111111 11111011 (负数的补码是在反码的基础上 + 1 )
在将操作数的二进制形式都表示出来后,我们取补码进行最终的按位与步骤。
00000000 00000000 00000000 00000011( 3 的补码)
11111111 11111111 11111111 11111011( -5 的补码)
我们按照符号的性质,同权重处全 1 为 1 ,有 0 为 0 ,分析出最终结果的补码为:
00000000 00000000 00000000 00000011(结果的补码)
经分析,符号位不为 1 ,说明结果数为正数,原、反、补码相同,再将其由二进制变为十进制数,结果为 3 。
我们观察一下代码的运行结果来验证一下我们的分析过程:
5.2 | 按位或操作符
对于这个符号,其实我们之前见过相似的 ||
,这是逻辑操作符中的或。在逻辑判断中,当左右表达式有一方为真,
则整个表达式就为真,只有当左右表达式全为假时,则整个表达式为假,总结为:一真则真,全假才假。
|
按位与操作符也有同样的性质,其本身也为双目操作符,拥有左操作数与右操作数,但是在操作数的二进制补码层面上进行操作的,
在两个操作数同样权重的二进制位上,如果有一方为 1 ,那么最终的表达式中同样的权重位也为 1 ,
若全部为 0 ,那么最终表达式中同样的权重位也为 0 ,总结为 :全 1 为 1 ,有 0 为 0 。
下面我们通过一个例子具体理解一下 |
按位与操作符。
#include <stdio.h>
int main()
{int num1 = 3;int num2 = -5;printf("%d\n", num1 | num2);return 0;
}
对于原码、反码、补码的分析部分,我们不再进行,详细看 &
按位与操作符部分的分析过程,这里我们直接进行运算。
00000000 00000000 00000000 00000011( 3 的补码)
11111111 11111111 11111111 11111011( -5 的补码)
我们按照符号的性质,同权重处有 1 则 1 ,全 0 为 0 ,分析出最终结果的补码为:
11111111 11111111 11111111 11111011 (最终结果的补码)
经分析,符号处为 1 ,说明结果为负数,我们要对补码转换为原码最终才可以转换为十进制数。
11111111 11111111 11111111 11111011 (最终结果的补码)
11111111 11111111 11111111 11111010 (最终结果的反码,负数补码 - 1 即为反码)
00000000 00000000 00000000 00000101 (最终结果的原码,负数反码符号位不变,其余取反即为原码)
我们得出原码后,将二进制数转化为十进制数为 -5 。
我们观察一下代码的运行结果,验证一下我们的分析过程。
5.3 ^ 按位异或操作符
按位异或操作符为双目操作符,拥有左操作数和右操作数,同样是在操作数的补码层面上进行操作,
此操作符的性质是:在两个操作数同样权重的二进制位上,相同为 1 ,不相同为 0 。
总结为:相同为 0 ,不相同为 1 。
我们还是通过相同的代码例子,来学习该操作符。
#include <stdio.h>
int main()
{int num1 = 3;int num2 = -5;printf("%d\n", num1 ^ num2);return 0;
}
00000000 00000000 00000000 00000011( 3 的补码)
11111111 11111111 11111111 11111011( -5 的补码)
我们按照符号的性质,**相同为 0 ,不相同为 1 ** ,分析出最终结果的补码为:
11111111 11111111 11111111 11111000(最终结果的补码)
经分析,符号处为 1 ,说明最终的结果为负数,要把补码转换为原码才能从二进制数转化为十进制数 。
11111111 11111111 11111111 11110111(最终结果的反码)
10000000 00000000 00000000 00001000(最终结果的原码)
最终转化为十进制的结果为 -8 。
我们观察一下代码的运行结果,验证一下我们的分析过程。
5.4 ~ 按位取反操作符
~ 按位取反操作符不同上面讲的几个操作符,它为单目操作符,只有一个操作数,
它的功能就是对操作数补码的每个二进制位取反,实际上在上面的推理求反码的过程中,我们已经使用了和他相似的功能。
我们还是通过相同的代码例子,来学习该操作符。
#include <stdio.h>
int main()
{int num1 = 3;printf("%d\n", ~num1);return 0;
}
00000000 00000000 00000000 00000011( 3 的补码)
我们按照符号的性质,每个补码二进制位取反 ,分析出最终结果的补码为:
11111111 11111111 11111111 11111100(结果的补码)
经分析,补码的符号位为 1 ,为负数,补码要转换为原码,才可以转换为十进制数。
11111111 11111111 11111111 11111011(结果的反码,补码 -1 为反码)
10000000 00000000 00000000 00000100(结果的原码,除符号位外其余取反)
最终结果转换为十进制为,-4
我们观察一下代码的运行结果,验证一下我们的分析过程。
(此处缺图)
5.5 练习一
多余上面的操作符我们学习过后,我们来通过练习巩固一下。
题目要求:
不能创建临时变量(第三个变量),实现两个整数的交换。
参考代码:
#include <stdio.h>
int main()
{int a = 10; //原 a 变量int b = 20; //原 b 变量a = a^b; //a 变量b = a^b; //b 变量a = a^b; //最终变量printf("a = %d b = %d\n", a, b);return 0;
}
这里变量的交换不能通过创建临时变量来实现,且要与我们学习的位操作符联系起来,这里我们使用了三次按位异或的操作来实现,
00000000 00000000 00000000 00001010(10 的原码、反码、补码)
00000000 00000000 00000000 00010100(20 的原码、反码、补码)
在第一次按位异或后,观察 a 变量
储存的情况。
00000000 00000000 00000000 00011110(第一次按位异或操作,a 变量
储存情况)
在第二次按位异或后,观察 b 变量
储存情况,此时是将已经按位异或一次后的 a 变量
与 b 变量
进行异或操作。
00000000 00000000 00000000 00001010(第二次按位异或操作,b 变量
存储情况)
观察第三次按位异或后,最终变量储存情况。
00000000 00000000 00000000 00010100(第三次按位异或操作, 最终变量
促存情况)
将最终 a、b 储存的数据与 10、20 的原码进行比较,就会发现变量成功地交换了,那么究竟是为什么呢?
5.5.1 原理解析
第一次异或:
第一次按位异或结果中 原 a 变量
与 原 b 变量
二进制一样的位置标记 0 ,不一样的位置标记 1 ,最终得出一串二进制数。
这串二进制记录了 原 a 变量
与 原 b 变量
哪些二进制位相同,哪些不同。
第二次异或:
在第二次按位异或中,这串二进制数与 原 b 变量
按位异或的过程,可以分为几种情况:
1 . 原 a 变量中,相同权重二进制位为 0
此情况,先分析与 原 b 变量
同权重位一样的情况,那此时 原 b 变量
中同权重位为 0 。因为 原 a 变量
与 原 b 变量
同权重位均为 0
所以 a 变量
中同位为 0 ,此时 a 变量
与 原 b 变量
进行异或操作,因为相同权重处都为 0 ,所以结果为 0 ,与 原 a 变量
处一致。
现在来分析与 原 b 变量
不一样的情况,此时 原 b 变量
中相同权重位为 1 。因为 原 a 变量
与 原 b 变量
同权重位不同,
所以 a 变量
中同位为 1 ,此时 a 变量
与 b 变量
进行异或操作,因为相同权重处均为 1 ,最终结果为 0,与原 a 变量处一致。
2 . 原 a 变量中,相同权重二进制位为 1
分析的思路与相同权重二进制位为 0 时是一样的, 原 a 变量
中相同权重二进制位进行变化时,与 b 变量
一样或不一样的情况中,
原 b 变量
相同权重的二进制位也会进行变化,最终不会使结果的二进制位与 原 a 变量
产生不同。
可见无论原 b 变量
中储存的为何值,通过异或操作都可以将 原 a 变量
中的数值还原,只要提供原 a 变量与原 b 变量异或的数值。
第三次异或:
第三次异或的目的其实与第二次大致相同,只不过现在最终要还原的是原 b 变量
,通过 b 变量
中储存的原 a 变量
,
与a 变量
中储存的原 a 变量
与原 b 变量
异或后的值求得。分析过程与第二次异或的过程是一样的。
这里读者们可能看的有些晕头转向,这里我更为通俗易懂的讲解一下。
第一次异或操作所得的数表示的是:两个数哪些二进制位相同,哪些二进制位不同。
然后将第一次异或的数,与第一次异或任意的一个操作数,进行异或操作就可以还原处另一个操作数。
因为第一次异或所得的数会告诉它,另一个原变量哪些二进制位与自己一样,哪些不一样,最终就能还原出来。
就像填色游戏一样,只要我一直告诉要填的颜色是否正确,最后整幅画就可以被完美复现。
5.6 练习二
题目要求:编写程序求一个整数储存在内存中的二进制中 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 ,取余数,再除以 2 ,
最后将所有余数颠倒顺序,就可最终将十进制数转换位二进制。在这道题中,我们参考这样的思路,不断模 2 ,取余数,再除以 2 ,
求出目标数二进制的每一位,然而现在我们只需对求出的每一位判断是否为 1 即可,每次循环中都求出一位二进制位,
求出后直接判断,若为 1 ,则记录个数的变量自增。
然而这种方法有一个缺点,只能对正数进行操作,对负数这种方法是失效的。因为内存中存储的是补码,然而负数的原、反、补,
不是一样的,所以无法对负数进行求解,那么我们需要一种更加普适的方法。
方法二 参考代码:
#include <stdio.h>
int main()
{int num = -1;int i = 0;int count = 0;//计数for(i=0; i<32; i++){if( (num >> i) & 1)count++;}printf("⼆进制中1的个数 = %d\n",count);return 0;
}
思路解析:
当一个数与 1 进行按位与操作时,1 转换为二进制只有最低位的 1 ,当二进制中与 1 进行按位与操作时,只有相同位同样为 1 时,
最终结果才为 1 ,因为 1 的二进制只有最低的一位,所以我们可以利用这个性质来让 num
的二进制位一位一位进行判断,
根据我们判断的次数,我们要对 num
右移我们的判断次数,因为 1 的二进制只有最低的一位,所以我们要将需要判断的位次,
右移到最低位上。int
变量占 4 字节,一个字节中 8 个比特位,所以我们一共需要判断 32 个二进制位,所以循环要进行 32 次。
在这个方法中,整数都可以进行操作,优化了方法一中不能对负数进行操作的缺点。
但在方法二中,循环一定会进行 32 次,有时我们的 num
没那么大,不需要进行那么多次。基于这个缺点,我们再对代码进行改善。
方法三 参考代码:
#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;
}
思路解析:
这里我们求一个二进制中 1 的个数的方法变成了 num = num & (num-1)
,再代码中我们并没有 if
判断某个条件让count++
,
而是进入循环一次让 count++
一次,说明只要进入了循环就代表 num
中包含一个 1 ,我们来分析一下其原理。
现在假设 num
为 100,它的二进制补码表示为:
00000000 00000000 00000000 01100100
减去 1 :
00000000 00000000 00000000 01100011
将两者进行按位与操作:
00000000 00000000 00000000 01100000
可以看到在操作后,num
二进制中最右边的 1 被我们消除掉了,所以能进一次循环,就代表有一个 1 在二进制补码中,
所以无需 if
条件进行判断,就可对 count
变量进行自增。
当 num
变量中不再有 1 ,即为 0 时,循环就结束,此时 count
变量中存储的数值即为 num
变量中二进制 1 的个数。
方法三优化了方法一、二的缺点,现在我们可以对整数进行操作,循环也不用一定要运行 32 次。
5.7 练习三
练习要求:
编写代码将 13 ⼆进制序列的第 5 位修改为 1 ,然后再改回 0 。
13的2进制序列: 00000000000000000000000000001101
将第5位置为1后:00000000000000000000000000011101
将第5位再置为0:00000000000000000000000000001101
参考代码:
#include <stdio.h>
int main()
{int a = 13;a = a | (1<<4);printf("a = %d\n", a);a = a & ~(1<<4);printf("a = %d\n", a);return 0;
}
思路解析:
若要将二进制中某位改为 1 ,其余为保持不动,应该使那个特定位 与 1 按位或,其他为与 0 按位或,
按位或的性质是:有 1 为 1,全 0 为 0 ;这样那个需要特定变动的点位则一定会变成 1 ,而其他位置是1 的还是 1 ,是 0 的还是 0 ,
我们达成了将特定点位改为 1 的目的,并使其他位保持不变。
若要将二进制中某位改为 0 ,其余为保持不动,应该使那个特定位 与 0 按位与,其他为与 1 按位与,
按位与的性质使:全 1 为 1,有 0 为 0 ;这样那个需要特定变动的点位一定会变成 1 ,而其他位置是 1 的还是 1 ,是 0 的还是 0 ,
我们达成了将特定点位改为 0 的目的,并使其他位保持不变。
在分析清楚如何将特定为置 0 或置 1 后,接下来我们分析,如何实现那个只有特定位是 1 或 0 ,其余为全为 0 或 1 的数。
对于特定位是 1 ,其余位是 0 的数其实很好实现,我们只需要对 1 进行改造,1 只有二进制最低位为 1 ,其余全为 0,
我们只需对 1 进行左移操作就可以实现特定点位为 1 ,其余位全为 0 的操作。
特定位置是 0 ,其余位是 1 ,只需要在左移后进行取反操作即可。
然后根据思路实现代码逻辑即可。
6. 单目操作符
单目操作符有这些:
!、++、--、&、*、+、-、~ 、sizeof、(类型)
单⽬操作符的特点是只有⼀个操作数,在单⽬操作符中只有 &
和 *
没有介绍,这2个操作符,我们放在学习指针的时候学习。
7. 逗号表达式
exp1, exp2, exp3, …expN
逗号表达式,就是⽤逗号隔开的多个表达式。
逗号表达式,从左向右依次执行。整个表达式的结果是最后⼀个表达式的结果。
下面我们通过几个例子来理解一下:
代码一:
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, a, b = a + 1);//逗号表达式
c是多少?
我们根据逗号表达式的性质,从左向右依次求值,第一个表达式为 a > b
,判断位假,最终结果为 1 ,
第二个表达式为 a = b + 10
,最终值为 11 ,同时也存储到了 a
变量中,第三个表达式 a
,最终值为 11,
最后一个表达式 b = a + 1
,a
值现在为 11 ,最终值为 12 ,存储到了 b
变量中。
由于是最后一个表达式,所以最后整个表达式的结果为 12 ,结果储存到了 c
变量中,所有 c
的值为 12 。
代码二:
if (a = b + 1, c=a / 2, d > 0)
在最后一个表达式中,只有变量 d
,前面表达式中均与变量 d
无关,所以我们无需对前面的表达式进行分析,
直接返回最后一个表达式,所以最终的代码等同于。
if (d > 0)
代码三:
a = get_val();
count_val(a);
while (a > 0)
{//业务处理 //...a = get_val();count_val(a);
}
这里我们尝试将循环中的业务处理,使用逗号表达式融入到循环的条件判断中。
如果使⽤逗号表达式,改写:
while (a = get_val(), count_val(a), a>0)
{//业务处理
}
这样在循环条件判断中,在逗号表达式中的表达式都执行完后,我们就已经完成了业务处理,返回的最后一个表达式 a > 0
,
作为循环的判断条件。
8. 下标访问 [ ]、函数调用 ( )
8.1 [ ]下标引用操作符
语法:⼀个数组名 + ⼀个索引值(下标)
int arr[10];//创建数组
arr[9] = 10;//实⽤下标引⽤操作符。
[ ]的两个操作数是 arr 和 9 。
8.2 函数调用操作符
接受⼀个或者多个操作数:第⼀个操作数是函数名,剩余的操作数就是传递给函数的参数。
#include <stdio.h>
void test1()
{printf("hehe\n");
}
void test2(const char *str)
{printf("%s\n", str);
}
int main()
{test1(); //这⾥的()就是作为函数调用操作符。 test2("hello bit.");//这⾥的()就是函数调用操作符。 return 0;
}
主函数中对于调用test1
与 test2
的 ( ) 就是函数调用操作符
9. 结构成员访问操作符
9.1 结构体
C语言 已经提供了内置类型,如:char、short、int、long、float、double
等,但是只有这些内置类型还是不够的,
假设我想描述学生,描述一本书,这时单一的内置类型是不行的。描述⼀个学生需要名字、年龄、学号、身高、体重等;
描述⼀本书需要作者、出版社、定价等。C语⾔为了解决这个问题,增加了结构体这种自定义的数据类型,
让程序员可以自己创造适合的类型。
结构是⼀些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量,如:标量、数组、指针,甚⾄是其他结构体。
9.1.1 结构的声明
struct tag
{member-list; //成员列表
} variable-list; //变量列表(可有可无)
创建一个结构题来描述学生:
struct Stu
{char name[20];//名字 int age;//年龄 char sex[5];//性别 char id[20];//学号
}; //分号不能丢
9.1.2 结构体变量的定义和初始化
1 . 结构体变量的定义:
//代码1:变量的定义
struct Point
{int x;int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
对于变量的定义我们既可以在声明结构体后在大括号与分号之间直接进行定义,也可以普通定义。
这里我们需要注意一下结构体声明的位置,如若在函数外声明结构体,则为全局结构体,在变量列表处定义的也为全局结构体,
在函数内部声明则为局部结构体,在变量列表处定义的也为局部结构体
2 . 结构体变量的初始化:
struct Point p3 = {10, 20};
struct Stu //类型声明
{char name[15];//名字 int age; //年龄
};
struct Stu s1 = {"zhangsan", 20};//初始化
struct Stu s2 = {.age=20, .name="lisi"};//指定顺序初始化
在初始化时,我们既可以根据定义结构体成员的顺序来初始化变量,也可以通过点操作符 .
来直接访问结构体的成员变量进行初始化,
struct Node
{int data;struct Point p;struct Node* next;
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化
在结构体进行嵌套时,我们如若要对结构体成员中的结构体进行初始化,则要带上大括号。
9.2 结构成员访问操作符
9.2.1 结构体成员的直接访问
结构体成员的直接访问是通过点操作符 .
访问的,点操作符接受两个操作数。如下所示:
#include <stdio.h>
struct Point
{int x;int y;
}p = {1,2};
int main()
{printf("x: %d y: %d\n", p.x, p.y);return 0;
}
使用方式:结构体变量.
成员名
9.2.2 结构体成员的间接访问
有时候我们得到的不是⼀个结构体变量,而是得到了⼀个指向结构体的指针。如下所示:
#include <stdio.h>
struct Point
{int x;int y;
};
int main()
{struct Point p = {3, 4};struct Point *ptr = &p;ptr->x = 10;ptr->y = 20;printf("x = %d y = %d\n", ptr->x, ptr->y);return 0;
}
使用方式:结构体指针 ->
成员名
综合举例:
#include <stdio.h>
#include <string.h>
struct Stu
{char name[15];//名字 int age; //年龄
};
void print_stu(struct Stu s)
{printf("%s %d\n", s.name, s.age);
}
void set_stu(struct Stu* ps)
{strcpy(ps->name, "李四");ps->age = 28;
}
int main()
{struct Stu s = { "张三", 20 };print_stu(s);set_stu(&s);print_stu(s);return 0;
}
关于更多,更详细的结构体知识,会在后期专门出一片文章进行讲解,这里我们主要时了解一下操作符。
10. 操作符的属性:优先级、结合性
C语言的操作符有 2 个重要的属性:优先级、结合性,这两个属性决定了表达式求值的计算顺序。
10.1 优先级
优先级指的是,如果⼀个表达式包含多个运算符,哪个运算符应该优先执行,各种运算符的优先级是不⼀样的。
3 + 4 * 5;
上面示例中,表达式 3 + 4 * 5
里面既有加法运算符 +
,⼜有乘法运算符 *
。由于乘法的优先级高于加法,所以会先计算 4 * 5
,
⽽不是先计算 3 + 4
。
10.2 结合性
如果两个运算符优先级相同,优先级没办法确定先计算哪个了,这时候就看结合性了,则根据运算符是左结合,还是右结合,
决定执行顺序。大部分运算符是左结合(从左到右执行),少数运算符是右结合(从右到左执行),比如赋值运算符 =
。
5 * 6 / 2;
上面示例中,*
和 /
的优先级相同,它们都是左结合运算符,所以从左到右执⾏,先计算 5 * 6
,再计算 / 2
。
运算符的优先级顺序很多,下面是部分运算符的优先级顺序(按照优先级从高到低排列),建议大概记住这些操作符的优先级就行,
其他操作符在使用的时候查看下面表格就可以了。
由于圆括号的优先级最高,可以使用它改变其他运算符的优先级。
参考:C++官网符号优先级
11. 表达式求值
11.1 整型提升
C语言 中整型算术运算总是至少以缺省(默认)整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
整型提升的意义:
表达式的整型运算要在 CPU 的相应运算器件内执行,CPU 内整型运算器( ALU )的操作数的字节长度⼀般就是 int
的字节⻓度,
同时也是 CPU 的通用寄存器的长度。
因此,即使两个 char
类型的相加,在 CPU 执行时实际上也要先转换为 CPU 内整型操作数的标准长度。
通用 CPU 是难以直接实现两个 8 比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。
所以,表达式中各种长度可能小于 int 长度的整型值,都必须先转换为 int
或 unsigned int
,然后才能送⼊ CPU 去执行运算。
//实例1
char a,b,c;
...
a = b + c;
b
和 c
的值被提升为普通整型,然后再执行加法运算。
加法运算完成之后,结果将被截断,然后再存储于 a
中。
如何进行整体提升呢?
-
有符号整数提升是按照变量的数据类型的符号位来提升的
-
无符号整数提升,高位补 0
//负数的整形提升
char c1 = -1;
变量c1的⼆进制位(补码)中只有8个⽐特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候,⾼位补充符号位,即为1
提升之后的结果是:
11111111111111111111111111111111
//正数的整形提升
char c2 = 1;
变量c2的⼆进制位(补码)中只有8个⽐特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,⾼位补充符号位,即为0
提升之后的结果是:
00000000000000000000000000000001
//⽆符号整形提升,⾼位补0
11.2 算术转换
如果某个操作符的各个操作数属于不同的类型,那么除⾮其中⼀个操作数的转换为另⼀个操作数的类型,否则操作就无法进行。
下面的层次体系称为寻常算术转换。
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上⾯这个列表中排名靠后,那么首先要转换为另外⼀个操作数的类型后执行运算。
11.3 问题表达式解析
11.3.1 表达式 1
//表达式的求值部分由操作符的优先级决定。
//表达式1
a*b + c*d + e*f
表达式1 在计算的时候,由于 *
⽐ +
的优先级高,只能保证,*
的计算是比 +
早,
但是优先级并不能决定第三个 *
⽐第⼀个 +
早执行。所以表达式的计算机顺序就可能是:
a*b
c*d
a*b + c*d
e*f
a*b + c*d + e*f
或者:
a*b
c*d
e*f
a*b + c*d
a*b + c*d + e*f
11.3.2 表达式 2
c + --c;
同上,操作符的优先级只能决定自减 --
的运算在 +
的运算的前面,但是我们并没有办法得知,
+
操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。
11.3.3 表达式 3
int main()
{int i = 10;i = i-- - --i * ( i = -3 ) * i++ + ++i;printf("i = %d\n", i);return 0;
}
表达式 3 在不同编译器中测试结果:
11.3.4 表达式 4
#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(); 中我们只能通过操作符的优先级得知:先算乘法,再算减法。
函数的调用先后顺序无法通过操作符的优先级确定。
11.3.5 表达式 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 编译器执行结果:
VS2022 运行结果:
看看同样的代码产⽣了不同的结果,这是为什么?
这段代码中的第⼀个 +
在执⾏的时候,第三个 ++
是否执行,这个是不确定的,
因为依靠操作符的优先级和结合性是无法决定第⼀个 +
和第三个前置 ++
的先后顺序。
到此,第十讲-操作符详讲部分的内容到此结束
如对文章有更好的意见与建议,一定要告知作者,读者的反馈对于我十分重要,希望读者们勤勉励学,精益求精,
我们下篇文章再见👋。