第10讲:操作符详解
C 语言知识点大总结
目录
操作符的分类
二进制和进制转换
原码、反码、补码
移位操作符
位操作符:&、|、^、~
单目操作符
逗号表达式
下标访问[]、函数调用()
结构成员访问操作符
操作符的属性:优先级、结合性
表达式求值
正文开始
操作符的分类
• 算术操作符:+ 、- 、* 、/ 、%
• 移位操作符:<< >> //移动的是二进制位
• 位操作符:& | ^ //位操作符是对二进制位进行计算
• 赋值操作符:= 、+= 、 -= 、 *= 、 /= 、%= 、<<= 、>>= 、&= 、|= 、^=
• 单目操作符:!、++、—、&、*、+、-、~ 、sizeof、(类型)
• 关系操作符:> 、>= 、< 、<= 、 == 、^ !=
• 逻辑操作符:&& 、||
• 条件操作符:? :
• 逗号表达式:,
• 下标引用:[]
• 函数调用:()
• 结构成员访问:. 、->
上述的操作符,我们已经讲过算术操作符、赋值操作符、逻辑操作符、条件操作符和部分的单目操作符,今天继续介绍一部分,操作符中有一些操作符和二进制有关系,我们先铺垫一下二进制的和进制转换的知识。
二进制和进制转换
其实我们经常能听到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
2进制的数字每一位都是0~1的数字组成
1111——>12^0 + 12^1 + 12^2 + 12^3 = 158进制的数字每一位是0~7的数字组成
1111——>18^0 + 18^1 + 18^2 + 18^3 = 1710进制的数字每一位是0~9的数字组成
1111——>110^0 + 110^1 + 110^2 + 110^3 = 1516进制的数字每一位是0~9,a~f的数字组成
1111——>116^0 + 116^1 + 116^2 + 116^3 = F
2进制 转 10进制
其实 10 进制的 123 表示的值是一百二十三,为什么是这个值呢?其实 10 进制的每一位是有 权重的, 10进制的数字从右向左是个位、十位、百位….,分别每一位的权重是10^0 ,10^1 ,10^2 …
如下图:
10进制123 每一位权重的理解
2 进制和 10 进制是类似的,只不过 2 进制的每一位的权重,从右向左是: 2^0 , 2^1 , 2^2 …
如果是 2 进制的 1101 ,该怎么理解呢?
2 进制 1101 每一位权重的理解
10进制转 2进制数字
10 进制转 2 进制
2 进制转 8 进制和 16 进制
2 进制转 8 进制
8 进制的数字每一位是0~7的,0~7的数字,各自写成 2 进制,最多有 3 个 2 进制位就足够了,比如 7 的二进制是 111 ,所以在 2 进制转 8 进制数的时候,从 2 进制序列中右边低位开始向左每 3 个 2 进制位会换算一个 8 进制位,剩余不够 3 个 2 进制位的直接换算。
如: 2进制的01101011,换成 8 进制:0153 , 0 开头的数字,会被当做 8 进制。
|
运行结果:153
107
153的8进制:38^0 + 58^1 + 3*8^2 = 107
2 进制转 16 进制
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
原码、反码、补码
当你们要把一个数转换成2进制表示的时候,整数的2进制表示方法有三种,即原码、反码和补码
有符号整数 的三种表示方法均有 符号位 和 数值位 两部分, 2 进制序列中,最高位的 1 位是被当做符号位,剩余的都是数值位。
符号位都是用 0 表示“正”,用 1 表示“负”。
正整数的原、反、补码都相同。
负整数的三种表示方法各不相同。
原码: 直接将数值按照正负数的形式翻译成二进制得到的就是原码。
反码: 将原码的符号位不变,其他位依次按位取反就可以得到反码。
补码: 反码+1就得到补码。
补码得到原码也是可以使用:取反,+1的操作。
图解:
|
对于整形来说:数据存放内存中其实存放的是补码。
为什么呢?
在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理( CPU只有加法器 )此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
移位操作符
<<左移操作符
.>>右移操作符
注:移位操作符的操作数只能是整数。
左移操作符
移位规则:左边抛弃、右边补 0
整数例子
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;
}
运行结果:b=20
a=10
|
运行结果:a=20
负数例子
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;
}
运行结果a=-1
b=-2
左移操作符演示
右移操作符
移位规则:首先右移运算分两种
:
逻辑右移:
左边用 0 填充,右边丢弃
算术右移:
左边用原该值的符号位填充,右边丢弃
右移到底采用算术右移还是逻辑右移?
取决于编译器
通常采用算术右移
注意
:右移操作符的操作数只能是整数,不能是浮点数。
举例1
:
|
运行结果a = -10
b = -5
举例2
:
|
逻辑右移 1 位演示
算术右移 1 位演示
警告⚠:对于移位运算符,不要移动负数位,这个是标准未定义的。
例如
:int num = 10 ;
num>>-1;//error
位操作符:&、|、^、~
位操作符有:
& //按位与 |
注
:他们的操作数必须是整数。
举例
:
按位与
:
|
运行结果:0
按位或
:
|
运行结果:-1
按位异或
:
|
运行结果:-1
按位取反
:
|
运行结果:-1
直接上代码
:
|
运行结果:5
-3
-8
-1
一道变态的面试题
:
不能创建临时变量(第三个变量),实现两个整数的交换
。
举例1
:
|
运行结果:交换前:a=3 b=5
交换后:a=5 b=3
举例2
:
|
运行结果:交换前:a=3 b=5
交换后:a=5 b=3
为什么不能创建临时变量?
因为临时变量的生命周期只在当前语句块内,而在当前语句块结束后,临时变量就会被销毁,导致结果错误
。
使用异或运算符实现两个整数的交换:
理解使用异或运算符实现两个整数的交换的用法
:
|
运行结果:交换前:a=3 b=5
交换后:a=5 b=3
练习 1 : 编写代码实现:求一个整数存储在内存中的二进制中 1 的个数。
举例1
:
|
运行结果:15 //——>输入
4
举例2
:
|
运行结果:15 //——>输入
4
举例3
:
|
运行结果:15 //——>输入
4
分析
:
方法 1:循环求余,判断余数是否为 1 ,是则计数器加 1 ,然后除以 2 ,直到 n 为 0 。
方法 2:循环 32 次,判断第 i 位是否为 1 ,是则计数器加 1 。
方法 3:循环求余,判断余数是否为 1 ,是则计数器加 1 ,然后与 n 进行位与运算,去掉最后一位,直到 n 为 0 。
参考代码: |
练习 2 : 二进制位置 0 或者置 1
编写代码将 13 二进制序列的第 5 位修改为 1 ,然后再改回 0
13 的 2 进制序列: 00000000000000000000000000001101 |
参考代码:
|
单目操作符
单目操作符有这些:
!、++、--、&、*、+、-、~ 、sizeof、(类型) |
单目操作符的特点是
只有一个操作数
,在单目操作符中只有 &和 * 没有介绍,这 2 个操作符,我们放在学习指针的时候学习。
逗号表达式
exp1, exp2, exp3, ...expN |
逗号表达式,就是用逗号隔开的多个表达式。
逗号表达式,
从左向右
依次执行。整个表达式的结果是最后一个表达式的结果
。
注意
:逗号表达式,一定要从左向右依次执行,因为前面的表达式的计算,可能会影响后面的表达式的计算。
代码1
:
定义变量 a、b 并使用逗号表达式给变量 c 赋值
逗号表达式中依次执行 a>b、a=b+10、a、b=a+1,最后 c 被赋值为 b 的值,即 13
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;
}
运行结果:
13
代码2
:
使用逗号表达式在条件语句中赋值,但应注意逗号表达式的返回值为最后一个表达式的值
|
代码3
:
调用 get_val() 函数获取 a 的值,并进行业务处理直到 a 不大于 0
|
下标访问[]、函数调用()
[ ]下标引用操作符
操作数:一个数组名 + 一个索引值(下标
)
举例
:
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;
}解读
:int arr[ 10 ];//创建数组
arr[ 4 ] = 10 ;//实用下标引用操作符。
[ ]的两个操作数是arr和 4 ,其中arr是数组名,4是索引值。
函数调用操作符()
接受一个或者多个
操作数:第一个操作数
是函数名
,剩余的操作数
就是传递给函数的参数
。
举例
1:
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:
// 定义第一个测试函数,用于打印 "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语言为了解决这个问题,增加了结构体这种自定义的数据类
型,让程序员可以自己创造适合的类型。
📌结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量,如:标量
、数组
、指针
,甚至是其他结构体
。
结构的声明
struct tag //struct ——> 结构体关键字, tag ——> 结构体的名称(自定义) |
描述一个学生
:struct Stu
{
char name[ 20 ];//名字
int age;//年龄
char sex[ 5 ];//性别
char id[ 20 ];//学号
}; //分号不能丢
结构体变量的定义和初始化
//学生类型 |
// 代码 1 :变量的定义 |
结构成员访问操作符
结构体成员的直接访问
结构体成员
的直接访问
是通过点操作符(.
)访问的。点操作符接受两个操作数。如下所示:
|
使用方式: 结构体变量.成员名
结构体成员的间接访问
有时候我们得到的不是一个结构体变量,而是得到了一个指向结构体的指针。如下所示:
|
使用方式: 结构体指针->成员名
综合举例:
// 定义一个结构体Stu,包含名字和年龄两个成员变量 |
更多关于结构体的知识,后期在《第 19 讲:自定义类型:结构体》中讲解。
操作符的属性:优先级、结合性
C语言的操作符有 2 个重要的属性:优先级
、结合性
,这两个属性决定了表达式求值的计算顺序
。
优先级
优先级指的是,如果一个表达式包含多个运算符,哪个运算符应该优先执行。各种运算符的优先级是不一样的。
相邻的运算符,优先级由高到低
依次是:
int main()
{
int r = (3+4) * 5; // 先计算加法,再计算乘法
}
3 + 4 * 5 ;// 先计算乘法,再计算加法 |
上面示例中,表达式3 + 4 * 5里面既有加法运算符(+),又有乘法运算符(*)。由于乘法的优先级高于加法,所以会先计算4 * 5,而不是先计算3 + 4
。
结合性
如果两个运算符优先级相同
,优先级没办法确定先计算哪个了,这时候就看结合性了,则根据运算符是左结合
,还是右结合
,决定执行顺序。大部分运算符是左结合(从左到右执行)
,少数运算符是右结合(从右到左执行)
,比如赋值运算符(=)
。
5 * 6 / 2 ; |
上面示例中,*和/的优先级相同,它们都是左结合运算符,所以从左到右执行,先计算5 * 6,再计算/ 2
。
运算符的优先级顺序很多,下面是部分运算符的优先级顺序(按照优先级从高到低排列),建议大概记住这些操作符的优先级就行,其他操作符在使用的时候查看下面表格就可以了。
- 圆括号(())
- 自增运算符(++),自减运算符(—)
- 单目运算符(+和-)
- 乘法(*),除法(/)
- 加法(+),减法(-)
- 关系运算符(<、>等)
- 赋值运算符(=)
由于圆括号的优先级最高,可以使用它改变其他运算符的优先级。
参考:https://zh.cppreference.com/w/c/language/operator_precedence
表达式求值
整型提升
C语言中整型算术运算总是至少以缺省(默认)整型类型的精度
来进行的。
为了获得这个精度,表达式中的字符和短整型
操作数在使用之前被转换为普通整型
,这种转换称为 整型提升。
|
整型提升的意义 :
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节⻓度一般就是int的字节⻓度,同时也是CPU的通用寄存器的⻓度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准⻓度。
通用CPU(general-purposeCPU)是难以直接实现两个 8 比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种⻓度可能小于int⻓度的整型值,都必须先转换为int或unsignedint,然后才能送入CPU去执行运算。
//实例 1 |
b和c的值被提升为普通整型,然后再执行加法运算
。
加法运算完成之后,结果将被截断,然后再存储于a中
。
如何进行整体提升呢?
有符号整数提升是按照变量的数据类型的符号位来提升的
无符号整数提升,高位补 0
//负数的整形提升 |
算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
long double |
如果某个操作数的类型在上面这个列表中排名靠后,那么首先要转换为另外一个操作数的类型后执行运算。
问题表达式解析
表达式 1
//表达式的求值部分由操作符的优先级决定。 |
表达式 1 在计算的时候,由于比+的优先级高,只能保证,的计算是比+早,但是优先级并不能决定第三个*比第一个+早执行。
所以表达式的计算机顺序就可能是:
a*b |
或者
a*b |
表达式 2
//表达式 2 |
同上,操作符的优先级只能决定自减—的运算在+的运算的前面,但是我们并没有办法得知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。
表达式 3
|
表达式 3 在不同编译器中测试结果:非法表达式程序的结果
表达式 4
|
这个代码有没有实际的问题?有问题!
虽然在大多数的编译器上求得结果都是相同的。
但是上述代码answer = fun() - fun() * fun();中我们只能通过操作符的优先级得知:先
算乘法,再算减法。
函数的调用先后顺序无法通过操作符的优先级确定。
表达式 5 :
//表达式 5 |
gcc编译器执行结果:
VS2022运行结果:
看看同样的代码产生了不同的结果,这是为什么?
简单看一下汇编代码,就可以分析清楚.
这段代码中的第一个+在执行的时候,第三个++是否执行,这个是不确定的,因为依靠操作符的优先级和结合性是无法决定第一个+和第三个前置++的先后顺序。
总结
即使有了操作符的优先级和结合性,我们写出的表达式依然有可能不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在潜在⻛险的,建议不要写出特别复杂的表达式。
完