文章目录
- 前言
- 一、二进制和进制转换
- 2进制转10进制
- 10进制转2进制
- 2进制转8进制
- 2进制转16进制
- 二、原码、反码、补码
- 三、移位操作符
- 左移操作符
- 右移操作符
- 四、位操作符
- &
- ^
- ~
- 一道奇葩的面试题
- 一道练习题
- 再来一个练习题
- 五、逗号表达式
- 六、结构成员访问操作符
- 结构体
- 结构的声明
- 结构体变量的初始化
- 结构体成员的访问
- 直接访问
- 间接访问
- 七、优先级和结合性
- 优先级
- 结合性
- 八、表达式求值
- 整型提升
- 算术转换
- 九、几个问题表达式解析
- 表达式1
- 表达式2
- 表达式3
- 表达式4
- 总结
前言
其实之前我们也有过操作符的讲解,今天我们再来深入地了解一下它
一、二进制和进制转换
生活中经常能看到2进制、8进制、10进制和16进制,说到底不过是数值的不同表示形式而已
举例来说,数值15的各种进制的表示形式:
但在这里我还是想重点介绍一下二进制,10进制中数字满10进1,10进制的数字每一位都是由0~9的数字组成
二进制也是如此,2进制满2进1,每一位数字也都是由0~1来组成
2进制转10进制
10进制的123表示的值是一百二十三,为什么是这个值呢?其实10进制的每一位是权重的,10进制的数字从右向左依次是个位、十位、百位,每位的权重是100,101,102
同样的,对于2进制的1101,我们该怎么理解呢?
这就是2进制转10进制的方法
10进制转2进制
一个数,比如说123,除以2余1,因为二进制上每一位的权重都是偶数,所以说必然最右位为1,这时候,我们把这个数的二进制位往右移动一位(其实就是/2),就又可以判断新的数的最后一位(即原来数的倒数第二位)是1还是0,依次类推
如图所示
比如说来个数字17,二进制位表示是10001,我们把它%2,为1,因此最右边一位为1,这时候把17给/2,得到8,再%2,得到0…如此,我们按顺序得到了10001,我们得到的其实是从右往左的,所以我们倒着来看,结果是10001,验证一下,1 * 16 + 1 * 1 = 17,没错
2进制转8进制
8是2的三次方,而8进制的数字每一位是0~7的,如果把这八个数字写成二进制数,有三位就足够了,比如7的二进制是111,所以在2进制转8进制的时候,从2进制序列中右边低位开始向左每3个二进制位会换算成一个8进制位的,剩余不够3个2进制位的直接换算
来个具体例子:
0153,0开头的数字会被认为是8进制
2进制转16进制
16是2的四次方,16进制的数字每一位是0~9,a-f的,把它们各自写成2进制,最多有4个2进制位就足够了,比如f的二进制是个1111,所以在2进制转16进制的时候,从2进制序列中右边低位开始向左每4个2进制位会换算成一个2进制位,剩余不够4个二进制位的直接换算
来个具体例子:
0x6b,0x开头的数字会被认为是16进制
二、原码、反码、补码
整数的2进制表示方法有三种,即原码、补码、反码
有符号整数的表示方法均有符号位和数值位两部分,2进制序列中,最高位的1位是被当作符号位,剩余的都是数值位
其中,符号位都是用0表示“正”,用1表示“负”
正整数的原、反、补码都相同
负整数的三种表示方法各不相同
原码:直接将数值按照正负数的形式翻译成二进制得到的就是原码
反码:将原码的符号位不变,其他位依次按位取反就可以得到反码
补码:反码+1就得到补码
其实,补码得到原码也可以取反 ,+1
对于整型来说,数据存放内存中其实存放的是补码
我们要思考,这是为什么呢?
其实,在计算机系统中,数值一律用补码来表示和存储,原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路
比如说1 + (-1)用原码的话,得到-2,这显然错误
反之,如果选择了补码(最高位1进制进出去了,哈哈):
其实可以想到,早期计算机科学家想到补码得花多大精力,敬佩!
三、移位操作符
左移操作符
规则:左边抛弃、右边补0
结果就像这样:
其实二进制往左移动一位,是不是相当于每一位上的权重加了一个量级,比如说原先最左边的1,表示1个8,往左移动,就变成了1个16,其他位也如是,所以整体来看,往右移动就相当于是* 2,请记下这个结论!
右移操作符
规则:
逻辑右移:左边用0填充,右边抛弃
算术右移:左边用该值的符号位填充,右边丢弃
到底是哪一种移法,取决于编译器的实现,大部分是算术右移,VS也是
也好理解,逻辑右移有点太粗暴了,可能一开始是负数,移动一下,变成了一个超大正数
另外,对于移位运算符,不要移动负数位,这个是标准未定义的
四、位操作符
位操作符的操作数必须是整数,且操作的都是二进制位
&
规则是按照二进制位与
只要有0就是0,两个同时为1才是1
我们来求 3 & (-5)
显然最后得出的是3,二进制位如下,经过程度打印即可验证
^
规则是按照二进制位异或
相同为0,相异为1
显然,最后得出的结果:
我们把它取反再+1变成原码,得到以下结果-8:
~
首先,我们要再次强化一个认识,就是数据在内存中是以补码的形式进行存储的!
我们现在来两个变量a,b,并且假设a为1,b = ~a
那么a,b在内存中的存储是:
那么b实际上是多少?我们来取反+1求原码
答案是-2
再来个例子,~0打印出来是多少?
哈哈,答案是-1,请自行验证吧!
一道奇葩的面试题
不能创建临时变量(第三个变量),实现两个整数的交换
讲解之前,请先了解异或的两个性质,它们很好证明,你第一次见的话,可以把它记下来
a ^ 0 = a
a ^ a = 0
于是,我们可以利用整体代换思想,写出以下语句:
a = a ^ b;
b = a ^ b;
a = a ^ b;
我们把第一句的a替换到第二局,那么a ^ b ^ b(都是最开始的值)的值就赋给b,显然,b就成了原来a,a最后也变成了原来b
其实还有一种方式如下
但是这种写法的缺陷是,a和b如果非常大,求和后的结果超过了整型的最大值
一道练习题
编写代码实现:求一个整数存储在内存中的二进制中1的个数
我们发现,a & 1 == 1 ; a & 1 == 0,分别说明a的二进制中最低位是1,最高位是0
因此,我们确保循环条件num > 0,每次都取最后一个数来判断,并右移一位
代码如下:
但是我们也需要考虑正负数的区别,假如我们把把求二进制位1的个数封装成一个函数,输入-1,结果求出来是0(-1 & 2永远不会等于1),这时候,我们怎么办呢?
答案是将这个负数看成是一个无符号整数,传递过去的数字视为unsigned int,即:
另外,请注意n = n & (n - 1)这个操作,它的意义是去掉n最右边的一个1
因此,这种也是一个好思路,且不用再传递无符号整数了
再来一个练习题
结合前文的n = n & (n - 1)这个结论,请你判断一个数是否是2的次方数
其实,也就是看一个数的二进制位是否只有一个1,那么我们来一次去掉1操作,判断是否为0不就可以了?
确实是这样:
?
我们不禁也思考,二进制位置0或者置1怎么办?
如下,原理大家自行分析
请注意,二进制的相关操作在单片机和嵌入式里面相当常见
五、逗号表达式
exp1, exp2, exp3,…expN
逗号表达式,就是用逗号隔开的多个表达式,逗号表达式就是从左向右执行,整个表达式的结果是最后一个表达式的结果
可用来减少冗余代码,下面是具体例子,解决了必须先操作一次的问题
六、结构成员访问操作符
结构体
C语言已经提供了内置类型,如:char、short、int、long等,但是只有这些内置类型还是不够的,假设我想描述学生,描述一本书,这时单一的内置类型是不行的,C语言为了解决这个问题,增加了结构体这种自定义的数据类型,让程序员可以自己创造适合的类型
结构是一些值的集合,这些值称为成员变量,结构的每个成员可以是不同类型的变量,如:标量、数组、指针,甚至是其他结构体
结构的声明
假设要描述一个学生:
注意,这时候类型是struct Stu而不是Stu
申请一个变量的时候,要struct Stu s1;
结构体变量的初始化
我们可以用大括号来初始化结构体变量,如下:
以下可以让你明白结构体的一些知识:
结构体成员的访问
直接访问
使用结构成员访问操作符.
格式为结构体变量.成员名
间接访问
有时候我们得到的不是一个结构体变量,而是得到了一个指向结构体的指针
按道理来说,我们这时候需要(*结构体指针).成员名,可是,我们有一个符号->,可以直接访问
使用方式为结构体指针->成员名
七、优先级和结合性
这两个属性决定了表达式求值的计算顺序
优先级
指的是一个表达式包含多个运算符,哪个运算符应该优先执行,各种运算符的优先级是不一样的
结合性
如果两个运算符优先级相同,优先级没办法确定先执行哪一个了,这时候就看结合性了,则根据运算符是左结合,还是右结合,决定执行顺序,大部分运算符是左结合(从左到右执行),少数运算符是右结合(从右到左执行),比如说赋值运算符=
下面有个表达,大家有需要自行查询即可
具体可参考链接:
C语言运算符优先级
其实,我的原则一直都是肝踏马的优先级,直接加括号就完事儿了,乐~
八、表达式求值
整型提升
C语言中整型算术运算总是至少以缺省整型类型的精度来进行的
为了获得这个精度,表达式中的字符合短整型操作数在使用之前被转换成普通整型,这被称为整型提升
即char 和 short也要先转换为整型操作数的标准长度
我们要思考整型提升的意义是什么?
其实,表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度
因此,即使是两个char类型的相加,在CPU执行时实际上也是先转换为CPU内整型操作数的标准长度
通用CPU是难以直接实现两个8比特字节直接相加运算(虽然说机器指令可能有这种字节相加指令),所以,表达式中各种长度可能小于int长度的整型值,都必须先转化为int或者是unsigned int,然后才能送入CPU去执行运算
软硬件相互成就,也相互限制
我们不妨来个具体例子
char a,b,c;
…
a = b + c;
b和c的值被提升为普通整型,然后再执行加法运算
那么如何进行整数提升?
1.有符号整数提升是按照变量的数据类型的符号位提升的
2.无符号整数提升,高位补0
对于负数:
对于正数:
char能存多少就存多少,其他发生截断,提升的时候就看最高位,把它当作符号位(signed char),至于无符号,直接全部补0
我们假设char c1 = 125, c2 = 10, c3 = c1 + c2;
那么我们现在打印有符号整数c3,结果是多少
分析如下,答案是-121
另外,如果直接直接打印c1 + c2,而不是赋给c3呢?
答案是135,至于理由自行分析!
注意此时不发生截断,只有整型提升
算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行,下面的层次体系为寻常算术转换
如果某个操作数的类型在上面这个列表中排名靠后,那么首先要转换成另外一个操作数的类型后执行运算
九、几个问题表达式解析
表达式1
到底哪个运算顺序对?可是运算优先级判断的是相邻操作符的优先级,可本题确不属于该情况
因此优先级无法保证第三个*比第一个+早执行
所以计算机计算该表达式的顺序就可能为以下两种情况:
表达式2
同上,操作符的优先级只能保证前置–的运算在+前面,但是我们没办法得知,+操作符的左操作数的获取在右操作数之前还是之后求值,这是未定义的,也就是说,4 + 4还是5 + 4这是不确定的
表达式3
谭浩强高徒,同学同事老师谁这么写,墙壁!
这是未定义行为,具体取决于编译器,如下:
表达式4
的确,根据优先级,我们会先算后面两个fun(),可问题是,函数的调用先后顺序无法由操作符的优先级确定
所以说, 4 - 2 * 3还是 3 - 2 * 4这是不确定的
总而言之,不要写出特别复杂的表达式,尽量拆成多个语句,因为我们虽然有优先级和结合性,依然不能通过操作符的属性来确定唯一的计算路径,有潜在风险
总结
操作符的学习,说实话是有些枯燥的,感觉动脑的还是比较少,但是别急
我们接下来就要迎来指针的学习了,这很重要,你必须深刻掌握它,于是我先预告一下