前言
大家好呀,我是Humble,今天要给大家分享的是操作符的知识
操作符可能有的人觉得很简单,没什么重要的内容,但是实际上呢,背后却有着许多地方值得我们去学习推敲。
通过操作符,我们能学到很多知识,对于学习C语言有着重大的意义。
废话不多说,我们来看一下吧!
一.操作符分类
下面是操作符的分类,来看看有没有你了解的吧
1.算数操作符
2.赋值操作符
3.单目操作符
4.关系操作符
5.逻辑操作符
6.条件操作符
7.移位操作符
8.位操作符
9.逗号操作符(逗号表达式)
10下标引用,函数调用
11.结构成员访问
其中的算数操作符,赋值操作符和单目操作符的一部分都是我们在前面的博文中分享过的(即本专栏的chapter3:C语言数据类型和变量下),而关系操作符,逻辑操作符和条件操作符是我们在本专栏的chapter4:分支和循环上 中讲过的
今天我们要讲的就是单目操作符剩余的部分,还有之前没说的移位操作符,位操作符,逗号操作符
下标引用,函数调用和结构成员访问
不过在将这些操作符前,我们要先学习一些前置的知识帮助我们理解今天要讲的操作符
二.二进制和进制转换
下面的内容可能比较偏理论,没有那么多的代码,但我希望大家可以耐心看下去
注:对二进制以及进制转换已经掌握的小伙伴可以直接跳转到三哦
那么先来介绍一下二进制
1.二进制
其实我们经常能听到2进制、8进制、10进制、16进制这样的讲法,那是什么意思呢?
其实2进制,8进 制,10进制、16进制是数值的不同表示形式而已。
比如:数值15的各种进制的表示形式:
15的2进制:1111
15的8进制:17
15的10进制:15
15的16进制:F
下面重点介绍一下二进制: 不过我们还是得从10进制讲起,因为10进制是我们日常经常使用的,我们已经对它有了较深的感知
1.10进制中满10进1
2.10进制的数字每⼀位都是0~9的数字组成
通过十进制,我们来类比二进制,其实道理是一样的
1. 2进制中满2进1
2.进制的数字每⼀位都是0~1的数字组成
下面说一下进制转换
2.进制转换
a.2进制转10进制 10进制转2进制
2进制转10进制
我们先从十进制引入吧。比如10进制的123为什么是这个值呢?
其实10进制的每⼀位是权重的,10进制的数字从右向左是个位、十位、百位,分别每一位的权重是 10的0次,10的1次,10的2次
2进制和10进制是类似的,只不过2进制的每⼀位的权重,从右向左是: 2 的0次,2 的1次,2 的2次
举个例子
如果是2进制的1101,该怎么理解呢
2 0*1+2 1*0+2 2*1+2 3*1=13
10进制转2进制
方法:先取余再倒序相加
举个例子
十进制 的15如果我想变成二进制,该怎么转换呢
其实方法就是先取余再倒序相加,由下往上依次得到的余数就是我们想要的15的二进制数1111
b.2进制转8进制和16进制
先来看一下2进制转8进制
8进制的数字每一位是0~7的,写成2进制,最多有3个2进制位就足够了
比如7的二进制是111,所以在2进制转8进制数的时候,从2进制序列中右边低位开始向左每3个2进制位会换算一个8进制位,剩余不够3个2进制位的直接换算。
如:2进制的01101011,换成8进制:0153 (注:0开头的数字,会被当做8进制)
2进制转16进制
8421法:16进制的数字每一位是0~15的,写成2进制,最多有4个2进制位
而10~15的数我们用a~f来表示
2进制转16进制数的时候,从2进制序列中右边低位开始向左每4个2进制位会转换成1个16进制位,剩余不够4个二进制位的直接换算。
如:2进制的01101011,换成16进制:0x6b
(注:0x开头的数字,会被当做16进制)
三.原码,反码和补码
整数的2进制表⽰⽅法有三种, 即原码、反码和补码(是二进制哦)
有符号整数的三种表示方法均有符号位和数值位两部分。2进制序列中,最高位的1位是被当做符号 位,剩余的都是数值位。 符号位都是用0表示“正”,用1表示“负”
整数原、反、补码很简单,因为正整数的原、反、补码都相同。
但负整数的三种表示方法各不相同
原码:直接将数值按照正负数的形式翻译成二进制得到的就是原码。
反码:将原码的符号位不变,其他位依次按位取反就可以得到反码。
补码:反码+1就得到补码
原码得到反码取反(将原码的符号位不变,其他位依次按位取反)+1的操作
反码得到原码除了-1取反之外也是可以使用:取反(将原码的符号位不变,其他位依次按位取反)+1的操作
还有一个结论:对于整形来说,数据存放内存中其实存放的是补码,计算时用的也是补码(这个很重要!)
好,有了上面的知识,剩下所有的操作符我们就可以讲了
四.操作符
移位操作符 << >>
<<左移操作符
>> 右移操作符
注:移位操作符的操作数只能是整数
因为整数的2进制表示方法有:原码、反码和补码,这与下面的移位规则有关、
1.<<左移操作符
移位规则:左边抛弃、右边补0(对于补码来说)
对负数也是一样的。最高位该丢就丢,规则是不变的
举个例子:
下面是运行结果:
为什么是20呢?结合今天我们讲的知识,就可以解释了
num的原码是 00000000000000000000000000001010(因为int是4个字节,所以有32个位)
又因为是正整数,所以补码也是00000000000000000000000000001010
然后将这个数左移,由移位规则:左边抛弃、右边补0,我们得到n的补码为
00000000000000000000000000010100
因为是正数(符号位为0),所以它的原码也是00000000000000000000000000010100
再根据进制转换,我们得到n的值就是20
2.>> 右移操作符
右移操作符相对来说比较复杂,分为两种移位:
- 逻辑移位左边用0填充,右边丢弃
- 算术移位左边用原该值的符号位填充,右边丢弃
究竟是逻辑移位还是算术移位是看编译器的
在VS中是采用算术移位的即左边用原该值的符号位填充,右边丢弃
运行结果大家可以由左移操作符自己先算一下呀
运行结果如下
最后提醒一点⚠️:对于移位运算符,不要移动负数位,这个是标准未定义的
例如:
位操作符 & | ^ ~
这里的位是二进制位的意思,根据这个,我们就可以知道这个操作符也是用补码来算的
而且它们的操作数也得是整数
下面是位操作符的分类
按位与&
按位或 |
按位异或^
按位取反~
直接上代码~~
#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;
}
这里给出代码及运算结果,其实方法跟上面一样,是用二进制的补码进行运算的,大家可以自己去求一下哦
那我们说了这么多,接下来看几道题来感受一下这几个操作符的作用吧
一道变态的题:不能创建临时变量(第三个变量),实现两个数的交换(注意用刚刚学的知识哦)
下面是代码的一个示例
练习:编写代码实现:求一个整数存储在内存中的二进制中1的个数
方法一
方法二
练习:而进制位置0或者置1
编写代码将13二进制序列的第5位修改为1,然后再改回0
比如:13的2进制序列: 00000000000000000000000000001101
将第5位置为1后:00000000000000000000000000011101
将第5位再置为0:00000000000000000000000000001101
下面是示例代码,写完了可以对一下哦
单目操作符
单目操作符汇总:++、--、+、-, !、&、*,~ 、sizeof、(类型)
在chapter3中我们讲过++,--,+,-,sizeof,(类型)
chapter4我们讲了!(逻辑操作符)
今天我们讲了~
而&(取地址)和*(解引用)因为指针还没学,我们先放着留到指针(下一篇博客)再讲
逗号操作符
逗号表达式,就是用逗号隔开的多个表达式
逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果
下面举个例子
下标引用[ ],函数调用 ( )
1.下标引用[ ]
操作数:一个数组名 + 一个索引值 int arr[10];
比如:创建数组 arr[9] = 10;下标引用操作符 [ ]的两个操作数是arr和9
2.函数调用 ( )
接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数
这个其实我们在前面的学习中一直在使用,只是我们不知道而已,如图
结构成员访问操作符
1.结构体
C语言已经提供了内置类型,如:char、short、int、long、float、double等,但是只有这些内置类型还是不够的:
假设我想描述学生,描述一本书,这时单一的内置类型是不行的。描述一个学生需要名字、年龄、学号、身高、体重等;描述一本书需要作者、出版社、定价等。C语言为了解决这个问题,增加了结构体这种自定义的数据类型,让程序员可以自己创造适合的类型。
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量,如: 标量、数组、指针,甚至是其他结构体
2.结构的声明
下面是声明的格式:
struct tag
{
member-list;
}variable-list;
那么我们就以上面学生的描述举一个例子:
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}; //分号不能丢!!!
3.结构体变量的定义和初始化
//代码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"};//指定顺序初始化!!!
//代码3//结构体变量的嵌套初始化
struct Node
{
int data;
struct Point p;
}n1 = {10, {4,5}}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}};//结构体嵌套初始化
4.结构成员访问操作符
a.结构体成员的直接访问
结构体成员的直接访问是通过点操作符(.)访问的
点操作符接受两个操作数
使用方法:结构体变量.成员名
示例代码如下
struct Point
{
int x;
int y;
}p = {1,2};
int main()
{
printf("x: %d y: %d\n", p.x, p.y);
return 0;
}
b.结构体成员的间接访问
有时候我们得到的不是一个结构体变量,而是得到了一个指向结构体的指针
那么我们可以使用结构体成员的间接访问
使用方法为:结构体指针变量->成员名
如下:
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;
}
这块内容有点超纲,建议初步了解指针后再次观看。而且我们在讲完指针后会在专门讲一次结构体的知识,这里只是给大家见识一下~
我们前面学了这么多的操作符是为了干什么呢?
其实我们学习操作符的目的是应用到表达式中的求值的
在这之前,我们还得将一个东西:操作符的属性
来,我们接着往下看
五.操作符的属性:优先级,结合性
C语言的操作符有2个重要的属性:优先级,结合性 ,这2个属性决定了表达式求值的运算顺序
先来看一下优先级
1.优先级
这个其实很好理解,因为我们从小到大一直与它打交道
来看一个我们熟悉的例子,这里运算的优先级大家应该瞬间就反应出来了吧
3+4*5
所以优先级指的是,如果一个表达式包含多个运算符,哪个运算符应该被优先执行
2.结合性
如果两个运算符的优先级向他们,优先级没法 确定先计算哪个,这时候就看结合性
根据运算符是左结合(从左到右执行)还是右结合(从右到左执行)来决定执行顺序
大部分运算符是左结合,
少数是右结合,比如赋值运算符(=)
运算符的优先级顺序很多,下面是部分运算符的优先级顺序(按照优先级从高到低排列)
建议大概记住这些操作符的优先级就行
其他操作符在使用的时候查看下面表格就可以了
1.圆括号( () )
2.自增运算符( ++ ),自减运算符( -- )
3. 单目运算符( + 和 - )
4.乘法( * ),除法( / )
5. 加法( + ),减法( - )
6.关系运算符( < 、 > 等)
7. 赋值运算符( = )
(注:由于圆括号的优先级最高,所以可以使用它改变其他运算符的优先级)
六.表达式求值
好,当我们知道一个表达式应该先算谁后算谁的时候,下面我们来讲一个非常重要的知识:表达式求值
我们先将整型提升
1.整型提升
C语言中整型算术运算总是至少以缺省(默认)整型(int)类型的精度来进行的
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升
我们下面来看一个例子
这个表达式的结果是多少呢?
这里就用到了整形提升
那么如何进行整体提升呢?
1. 有符号整数提升是按照变量的数据类型的符号位来提升的
2. 无符号整数提升,高位补0
所以上面这个结果,应该是按照下面的过程得到的:
char类型是占用一个字节的,它的取值范围为:-128~127
所以c1能放下
因为125是个整数(int),它的二进制为01111101
然后补0使它有32个位
00000000000000000000000001111101
而c1去存的时候只存01111101,这里发生了截断
c2存的方式同理,它只存了00001010
然后来到c3,它让c1,c2相加,这时两个变量相加就要发生整形提升了
根据上面的规则 有符号整数提升是按照变量的数据类型的符号位来提升的
所以在c1和c2在计算前都按最高位0补上前面空缺的位,再进行计算
然后呢,我们将结果进行截断,得到10000111
我们最后%d要打印的是有符号的整数,
所以这里又要发生整形提升,只有提升到整形,char类型的c3才能被打印
因为它的符号位为1,所以是前面补1,得到补码,
对于负数,我们要得到原码得取反+1
所以最后截断打印出的数二进制为01111001 十进制就是121,因为是负数,所以是-121,也就是上面打印的结果
2.算术转换
刚刚讲的类型是<=整形的
下面讲>=整形的情况该怎么处理,也就是算数转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换
如果某个操作数的类型在上面这个列表中排名靠后,那么首先要转换为另外一个排名靠前操作数的类型后执行运算
举个例子:
我们给了 int a; float f;
然后对a 和 f 进行相加,这个时候必须把a的类型转换成float类型(因为float类型在上面这个列表中排名靠前)
讲到这,大家已经掌握了操作符的优先级,结合性,整形提升,算数转换,这个时候是不是可以进行任意的表达式求值了呢?
但是接下来要给大家泼一盆冷水
就是即使我们掌握了上面提到的知识,我们在求表达式的时候依然会出现一些问题
下面我们就来看一些问题表达式吧
3.问题表达式解析
问题表达式1
//表达式的求值部分由操作符的优先级决定。
//表达式1
a*b + c*d + e*f
表达式1在计算的时候,由于 * 比 + 的优先级高
只能保证, * 的计算是比 + 早,
但是优先级并不 能决定第三个 * 比第一个 + 早执行
所以表达式的执行顺序就有很多,不具有唯一性
问题表达式2
//表达式2
c + --c;
同上,操作符的优先级只能决定自减 -- 的运算在 + 的运算前面,但是我们并没有办法得知, + 操 作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的
问题表达式3
//表达式3
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(); 中我们只能通过操作符的优先级得知:先算乘法,再算减法。 函数的调用先后顺序无法通过操作符的优先级确定
问题表达式4
//表达式4
#include <stdio.h>
int main()
{
int i = 1;
int ret = (++i) + (++i) + (++i);
printf("%d\n", ret);
printf("%d\n", i);
return 0;
}
//尝试在gcc编译器,VS环境下都执⾏,看执行的结果
gcc编译器的结果:
VS2022运行结果:
看看同样的代码产生了不同的结果,这是为什么?
这段代码中的第一个 + 在执行的时候,第三个++是否执行,这个是不确定的
因为依靠操作符的优先 级和结合性是无法决定第一个 + 和第三个前置 ++ 的先后顺序
总结:即使有了操作符的优先级和结合性,我们写出的表达式依然有可能不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在潜在风险的,建议不要写出特别复杂的表达式
结语:
结语
好了,今天的分享就到这里了
最后,希望大家点个赞或者关注吧(感谢感谢)
让我们在接下来的时间里一起成长,一起进步吧!