一文让你彻底搞懂:计算机如何做减法?

好多人工作了几年,都不明白什么是补码,原码,反码.

在开篇我们先介绍一下二进制的一些知识,毕竟在计算机的运算中全部都是使用的二进制。

二进制分为有符号数和无符号数。

  • 有符号数,用来在计算机中表示正数和负数,最高位是符号位,0表示正数,1表示负数。

比如1010 0010就表示一个负数。

  • 无符号数,最高位不是符号位,而是二进制数字的一部分。比如10100010就表示一个正数162.

Java中所有的数字均为有符号数,最高位是符号位。

         比如int为32位,byte为8位,short为16位,long为64位,他们的取值范围分别是:

类型

取值范围

描述

Byte

-128~127

占用1个字节,-2的7次方到2的7次方减1

Short

-32768~32767

占用2个字节,-2的15次方到2的15次方-1

Int

-2147483648~2147483647

占用4个字节,-2的31次方到2的31次方-1

Long

-9223372036854774808~

9223372036854774807

占用8个字节(-2的63次方到2的63次方-1)

其实,就拿最简单的4位bit(如图)来说,这个数表示的十进制数就是2^4-1.即2^3+2^2+2^1+1=8+4+2+1=15=2^4-1,这个是在不考虑符号位的情况下 计算出的结果。

                                                      

1

1

1

1

由此,可以知道JAVA中32位int的最大值是除了最高位0之外的31位1的数字,即:

0

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

 4个1是2^4-1,那么31个1就是2^31-1,这就是int型的最大值。                          

那java中int型最小值怎么表示呢?

        我们再拿四个bit位(如图)来举例子,即符号位为0,其余三位为1的值0111,换算为十进制就是2^2+2^1+1=7,这是四个bit位的最大值,表示为2^3-1,即7.

0

1

1

1

那么四个bit位的最小值是多少?其实就是最大值加1,结果是:

1

0

0

0

这个数其实就是四位bit位所能表示的最小值,表示为:-2^3,即-8.

所以四位bit位表示的范围是[-2^3, 2^3-1],即[-8,7]

那这里继续引申一下:

对于n位bit的整数类型?

  1. 符号位是1,其余n-1位是0,我们表示为-2^(n-1),而不是2^(n-1).,表示为最小值
  2. 符号位为0,其余n-1位是1,我们表示为2^(n-1)-1,表示为最大值

所以n位bit位的整数,所能表示的范围是[-2^(n-1), 2^(n-1)-1]

         一旦数字超过了这个限定,就会发生溢出,超过上限,叫做上溢出。超过下限,叫做下溢出。上溢出之后,又从下限开始:最大的数加1,就变成了最小的数。下溢出之后又从上限开始,最小的数减去1就变成了最大的数,如此循环往复。

 public static void main(String[] args) {           
	      
	      byte c = 127;
	      byte d = 1;
//	      byte f = c + d;//编译错误,越界
//	      System.out.println(f);
	      System.out.println((byte)(c + d));//-128,127+1,即最大值加1,超过最大值范围,上溢出为最小值

	      byte g = -128;
	      byte h = -1;
	      System.out.println((byte)(g+h));//127,-128-1,得到一个byte类型的最大值127,即超过最小值范围,下溢出为最大值
	   }

这有点像对一个数取模的概念

        比如当取模的除数为5时,任意一个数对5取模,得到的都是[0,4]区间内的一个数。而巧的是区间上限减去区间下限再加1,刚好就是取模的除数。比如4-0+1=5.

所以对于二进制的溢出也是一样的,[-2^(n-1), 2^(n-1)-1]的上限减去下限再加1,的值就是取模的除数,这个值是:

                                                     2^(n-1)-1-(-2^(n-1))+1=2^n=2^n-1+1

在[-2^(n-1), 2^(n-1)-1]区间内的任意数字加上这个值得到的永远是[-2^(n-1), 2^(n-1)-1]内的值。

再比如今天是周一,加上七天后、14天后、21天后…7*n天之后的那天都是周一一样。因为每周只有周一到周日这7天的区间[1,7],那么根据我们的公式:7-1+1=7,就是取模的除数。任意一天加上这个区间内的任意一个值,得到的都是周一到周日。

 

接着我们介绍原码、反码、补码的概念:

原码:就是我们看到的二进制的原始表示。最高位为符号位。其余位数表示该数字绝对值的二进制。

我们是否可以直接用负数的原码来进行减法计算呢?

我们来看一个例子3+(-2),假如我们使用的是java语言中的int型表示3和-2.

那么-2的二进制原码表示:

1

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

1

0

最高位1,低两位10,其余位0.

3的二进制原码表示:

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

1

1

最高位0,低两位11,其余位0.

3和-2的二进制原码相加得到:

1

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

1

0

1

但这个表示的是几呢?-5的原码。显然3-2等于1。

所以负数的原码不能直接用来做减法。其实CPU的运算器中只有加法器,减法是由加法换算而来的。

 

假设i-j,其中j为正数。如果i-j加上取模的除数2^n-1+1,那么就会形成溢出可以得到i-j的值:

                                            i-j=(i-j)+(2^n-1+1)=i+(2^n-1-j+1)

我这里为什么一直在强调2^n-1+1这个写法?

2^n-1所表示的二进制有n1(就跟2^4-1=15表示1111一样)。在不考虑符号位的情况下有n-11,那么2^n-1-2的结果:

                                                                     

从结果可以看出,2^n-1-j相当于对正数j的二进制原码除了符号位外按位取反。-jj的原码,除了符号位外都是相同的,所以2^n-1-j相当于对负数-j的二进制原码除了符号位外按位取反。我们把2^n-1-j所对应的编码称为负数-j的反码。所以:

                                               i-j=(i-j)+(2^n-1+1)=i+(2^n-1-j+1)

就等于i的原码+(-j的反码+1),也就是i的原码+(-j的补码).如果i也是正数,那么结果等于

i-j= i的补码+-j)的补码

因为正数的原码、反码、补码都相同。

 

补充知识:二进制的位操作

向左移位:表示为<<,二进制110101(十进制的53)向左移动一位,就是在末尾添加一位0.变为1101010(十进制的106.    

                                                         

左移一位后,53变成了106,结论是:二进制左移一位,其实是将数字翻倍。

向右移位:表示为>>>.二进制110101(十进制53)向右移动一位,就是去除末尾的那一位,变成了10010(十进制26),而26也是53除以2取整的值。因此二进制右移一位,就是数字除以2取整后的值。

                                                            

问题:左移是<<.为啥右移就是>>>

先看下5332位表示:

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

1

1

0

1

0

1

再看下-5332位表示

1

1

1

1

1

1

1

1

1

 

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

0

0

1

0

1

1

那么-53右移操作,符号位1我们是否需要右移呢?

抛开这个问题不论,再来看JAVA中定义的两种右移:

逻辑右移:符号>>>,逻辑右移1位,左边补0即可。

0

1

1

1

1

1

1

1

1

 

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

0

0

1

0

1

算术右移:符号>>,算术右移1位,符号位保持不变,其他一律右移一位,并且补符号位1,补的1仍然在符号位之后。

                              

按位或

表示为|

参与操作的位中,只要有一个位为1,那么最终结果是1,也就是真

比如:110101和100011每一位对齐进行按位或,结果是110111

 

按位与

表示为&

参与操作的位中必须全部是1,那么最终结果才是1,否则为0.

比如:110101和100011每一位对齐进行按位与,结果是100001

应用:使用按位与来判断一个数的奇偶性。

偶数的二进制最后一位总是0;奇数的二进制最后一位总是1,那么对于给定数值,可以让其二进制与数字1进行按位与操作,然后取得这个数的最后1位,如果是1说明是奇数,否则说明是偶数。

 

 

按位异或

表示为^

参与操作的位相同,结果为0,否则为1.所以要想得到1,必须参与的操作位是不同的,这就是异的含义。比如:110101和100011每一位对齐进行按位异或,结果是010010

根据按位异或的性质,所有的数值跟自身异或操作后,结果是0.而且想得到0,两个数值也必须是相同的。这可以作为判断两个变量是否相等的条件。而且任意一个数字跟0按位异或结果都是这个数本身。比如:x^0=x

 

 

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Python面向对象编程(Object-Oriented Programming,简称OOP)是一种程序设计方法,它将数据和操作数据的方法组合成对象,通过定义类(class)来创建对象。下面是一些概念和原则,可以帮助你更好地理解Python面向对象编程。 1. 类和对象: - 类是一种抽象的数据类型,它定义了对象的属性和方法。 - 对象是类的实例,它具有类定义的属性和方法。 2. 属性和方法: - 属性是对象的数据,可以是整数、字符串、列表等。 - 方法是对象的行为,可以是函数或过程。 3. 封装: - 封装是将数据和对数据的操作封装在一起,以创建一个独立的实体。 - 使用类来封装数据和方法,可以隐藏实现细节,提高代码的可读性和可维护性。 4. 继承: - 继承是一种机制,允许一个类继承另一个类的属性和方法。 - 子类可以重用父类的代码,并且可以添加新的属性和方法。 5. 多态: - 多态是指同一个方法可以在不同的类中具有不同的实现方式。 - 多态可以提高代码的灵活性和可扩展性。 下面是一个简单的例子,展示了如何定义一个类、创建对象并调用对象的方法: ```python class Person: def __init__(self, name, age): self.name = name self.age = age def say_hello(self): print(f"Hello, my name is {self.name} and I'm {self.age} years old.") # 创建对象 person = Person("Alice", 25) # 调用对象的方法 person.say_hello() ``` 这个例子定义了一个名为`Person`的类,它有两个属性(`name`和`age`)和一个方法(`say_hello`)。我们通过`Person`类创建了一个名为`person`的对象,并调用了它的`say_hello`方法。 希望这个简单的例子能帮助你更好地理解Python面向对象编程。如果你有其他问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jeff.sheng

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值