一篇带给你~0 == -1 问题全解

开发 后端
今天带大家复习一个容易遗忘的C语言知识点,负数的存储及其一个典型考察形式。

[[380692]]

 今天带大家复习一个容易遗忘的C语言知识点,负数的存储及其一个典型考察形式。

我们先看一个题目

  1. #include<stdio.h> 
  2.  
  3. int main() 
  4.  int a,b,c,d;   //或者   int a,b,c,d; 
  5.  a=0x8; 
  6.  b=a>>1; 
  7.  c=~(~0<<1); 
  8.  d=b&c; 
  9.  printf("c is %d\n",c); 
  10.  printf("d is %d\n",d); 

 

解析:

假设计算机是存储八位

  1. 0的存储是   0b 0000 0000 
  2. ~按位取反   0b 1111 1111 
  3. 左移一位    0b 1111 1110     空档处补0 
  4. 再按位取反  0b 0000 0001 
  5.  
  6. 所以c就等于1 

这里是一步一步推导过来的,你会忽略一个关键的过程,就是~0你算出来的,是0b 1111 1111,一个很大的负数,这和你以为的常理违背,我们下面讨论。

再次举例

  1. #include <stdio.h> 
  2. int main() 
  3.     printf("~0 == %d\n", ~0); 

 

  1. 0的存储是   0b 0000 0000 
  2. ~按位取反   0b 1111 1111 

这个题目比较干脆,直接是~0 == -1,有的时候我们不明白,为什么0b 1111 1111在内存中代表-1,因为他无论如何也是一个很大的负数才对。

可是实际上,负数在内存中是按照补码的形式存储的,也就是说0b 1111 1111是一个补码,那么它的反码就是0b 1111 1110,原码就是0b 1000 0001,也就是-1(注意负数求反码补码的时候符号位不变)

结论

  1. 0b 1111 1111 == -1 (~0) 
  2.  
  3. 0b 1111 1110 == -2 (~1) 
  4.  
  5. 0b 1111 1101 == -3 (~2) 

可以把它当做一个公式 ~a == -【a+1】

补充说明

为什么整数要在内存中按照补码储存。因为正数的原码反码补码都一样,所以我们主要讨论问什么负数在内存中按照补码方式存储。

采用补码的原因或好处如下,采用补码运算具有如下两个特征:

1)因为使用补码可以将符号位和其他位统一处理,同时,减法也可以按加法来处理,即如果是补码表示的数,不管是加减法都直接用加法运算即可实现。

2)两个用补码表示的数相加时,如果最高位(符号位)有进位,则进位被舍弃。

这样的运算有两个好处:

1)使符号位能与有效值部分一起参加运算,从而简化运算规则。从而可以简化运算器的结构,提高运算速度。(减法运算可以用加法运算表示出来)

2)加法运算比减法运算更易于实现。使减法运算转换为加法运算,进一步简化计算机中运算器的线路设计。

下图最能说明为什么用补码可以使整数的减法运算(即负数的运算)变为加法运算:


深入证明:

用带符号位的原码进行乘除运算时结果正确,而在加减运算的时候就出现了问题,如下:假设字长为8bits

  1. ( 1 ) - ( 1 ) = ( 1 ) + ( -1 ) = ( 0 ) 
  2. 可以表示为: 
  3. (00000001)原 + (10000001)原 = (10000010)原 = ( -2 ) 显然不正确.。 

因为在两个整数的加法运算中是没有问题的,于是就发现问题出现在带符号位的负数身上,对除符号位外的其余各位逐位取反就产生了反码。反码的取值空间和原码相同且一一对应。下面是反码的减法运算:

  1. ( 1 ) - ( 1 ) = ( 1 ) + ( -1 ) = ( 0 ) 
  2. 可以表示为: 
  3. (00000001) 反+ (11111110)反 = (11111111)反 = ( -0 ) 有问题。 
  4.  
  5. ( 1 ) - ( 2) = ( 1 ) + ( -2 ) = ( -1 ) 
  6. 可以表示为: 
  7. (00000001) 反+ (11111101)反 = (11111110)反 = ( -1 ) 正确 

问题出现在(+0)和(-0)上,在人们的计算概念中零是没有正负之分的。

于是就引入了补码概念。负数的补码就是对反码加一,而正数不变,正数的原码反码补码是一样的。在补码中用(-128)代替了(-0),所以补码的表示范围为:

(-128~127)共256个。

注意:(-128)没有相对应的原码和反码, (-128) = (10000000) 补码的加减运算如下:

  1. ( 1 ) - ( 1 ) = ( 1 ) + ( -1 ) = ( 0 ) 
  2. 可以表示为: 
  3. (00000001)补 + (11111111)补 = (00000000)补 = ( 0 ) 正确 
  4.  
  5. ( 1 ) - ( 2) = ( 1 ) + ( -2 ) = ( -1 ) 
  6. 可以表示为: 
  7. (00000001) 补+ (11111110) 补= (11111111)补 = ( -1 ) 正确 

采用补码表示还有另外一个原因,那就是为了防止0的机器数有两个编码。原码和反码表示的0有两种形式+0和-0,而我们知道,+0和-0是相同的。这样,8位的原码和反码表示的整数的范围就是-127-127(11111111-01111111),而采用补码表示的时候,00000000是+0,即0;10000000不再是-0,而是-128,这样,补码表示的数的范围就是-128~+127了,不但增加了一个数得表示范围,而且还保证了0编码的唯一性。

为什么正数的反码,补码和原码一样?这是规定或者说这是约定,没有多少道理,你算是算不出来的。

补码只是为负数想出来的办法,目的是减法变加法。是减法可以用加补码的方法实现。补码可用反码加1得来。于是道又有了负数的反码。

计算机里有硬件“加法器”,有了补码,减法也可以用加法器做了。计算机里运算速度,硬件远快于软件。这是弄出反码,补码和原码花样的原因。

形象说明

引进补码的作用是为了让计算机更方便做减法,比如说,按时间12个小时来算,现在的准确时间是4点,有一个表显示的是7点,要校准时间,我们可以将时针退7-4=3格,也可以向前拨12-3=9格,计算机做减法就可以转化成-3=+9,这样可以简化计算机的硬件设备去做复杂的减法。

 

责任编辑:姜华 来源: 嵌入式Linux系统开发
相关推荐

2022-04-18 08:57:32

React 18前端

2021-07-12 06:11:14

SkyWalking 仪表板UI篇

2021-06-21 14:36:46

Vite 前端工程化工具

2022-04-29 14:38:49

class文件结构分析

2021-04-08 11:00:56

CountDownLaJava进阶开发

2021-01-28 08:55:48

Elasticsear数据库数据存储

2023-03-29 07:45:58

VS编辑区编程工具

2021-04-14 14:16:58

HttpHttp协议网络协议

2021-04-01 10:51:55

MySQL锁机制数据库

2021-03-12 09:21:31

MySQL数据库逻辑架构

2021-07-21 09:48:20

etcd-wal模块解析数据库

2022-02-17 08:53:38

ElasticSea集群部署

2022-03-22 09:09:17

HookReact前端

2020-12-24 08:07:18

SpringBootSpring SecuWeb

2022-03-03 09:05:17

索引MySQL数据查询

2021-07-01 07:08:10

consul服务注册发现框 架

2023-02-27 10:17:05

EventBus观察者模式

2022-09-20 07:33:15

Jenkinshttps://mp

2020-12-21 08:10:01

Kubernetes实用技巧kubectl

2021-06-24 06:35:00

Go语言进程
点赞
收藏

51CTO技术栈公众号