目录
计算机只认识0和1,因此任何数都需要表示成二进制的形式。计算机系统规定,最高位用于表示整数的符号位,0表示正数,1表示负数,这是为了方便高级语言标识有符号数和无符号数。在处理器指令层面,指令在计算时不区分整数是否是有符号,还是无符号,统一按有符号数处理,至于应当明符号,还是无符号,由程序员去决定(你认为这是有符号数,就当成有符号数去解析,你认为是无符号数,就当成无符号数去解析)。
数的运算包括加减乘除,人类首先发明的是加法器,因此,加法问题最为简单,首先得到解决;乘法问题可以看成是数的累加,即移位,逻辑判断,累加结合,则解决了加法问题,乘法问题很容易解决。除法问题可以看成是减法的移位,逻辑判断,累减,所以核问题是解决整数的减法问题。因为减去一个数,可以看成是加上一个我负数,因此,核心问题就成了如何表示这个负数。
如果一个正数,和一个与它绝对值相同的负数,这两个数各自在计算器中的表示,通过逻辑电路的加法运算法则,恰好产生了溢出,从而其有效位全部是0,则这两个数互为对方的某种形式,则减法问题得到解决。
为了便于描述,我们先提出原码:某个整数,其绝对值的二进制表示形式。注意,原码是描述补码的基础,计算机中的整数不是用原码表示,为了统一,我们说计算机中的数是用补码表示。
因为找到一个数与其相反数相加为0,直接操作并不容易,要产生进位,计算电路的设计更为复杂,而且没有办法通用一套电路。于是,找到一个数与其相加等于1(如果是一个字节,则为11111111,我们以这个为例),再在这个数基础上加1,产生溢出,则结果就为0了。而找到相加等于1的值,很容易,在原码的基础上按位取反即可,即反码。
取得了反码,在这个基础上加1,正好产生溢出为0,这就满足了我们转换减法为加法的目的,在电路实现上,用一套电路即可,计算时,统统当成有符号数进行运算。以-1为例,我们考虑一个字节的运算:
原码形式:00000001
反码形式(按位取反):11111110 (与原码相加正好为11111111)
补码形式(补码加1): 11111110 + 00000001 = 11111111(0xFF)
因为我们以正整数为基础进行的补码运算,而为了统一称乎,我们称计算机中的整数为用补码表示。因此,正整数的补码就是其本身,正数不需要转换,只需要转换负数。而负数只需要按照这种规则进行转码,就能与其对应的正整数通过电路的逻辑相加运算结果为0,完美地解决了转减法为加法的问题,并统一使用一套电路。转换编码的时候不考虑符号,因为负数补码的概念已经将符号编码在其中了,因为它与对应的正数相加恰好为0。比如,我们计算1-3=1+(-3):
1的补码表示:00000001
-3的补码形式:求原码00000011,求反码11111100,求补码11111101
1-3=1+(-3) = 00000001+11111101=11111110(-2)
因此,计算机中的计算结果值就是11111110,对于这个结果值,你要把它解析成正数还是负数,那就是程序员的责任了,比如,在使用C++编程时,你将它转换成有符号数int,因为最高位是负号,意味着这是负数,这个编码值应当看成是负数的补码表示,在显示时,应当转换为对应的原码值加上符号,这个工作由C++编译器替程序员完成了,这时候我们在调试时显示的值是-2,但是我们显示时内存值是11111110。注意,计算机加减运算时不区分有符号还是无符号,最高位也是当成整值参与计算。
当我们在编程进行四则运算的时候,我们应当遵循数据类型一致的原则,因为你用一个int变量与一个unsigned int变量相加,计算机也会给你内部转换成一致类型再进行运算,而且写出这样的表示也没有意义。下面举几个例子,看看编译器是如何隐式处理的。以VS2022编译环境为例,编写X64代码。
示例1:1字节有符号数加法运算
void Test()
{
signed char a = 1;
signed char b = -3;
signed char total = (signed char)(a + b);
}
查看反汇编译代码:
void Test()
{
00007FF6FCC12220 sub rsp,18h
signed char a = 1;
00007FF6FCC12224 mov byte ptr [rsp],1
signed char b = -3;
00007FF6FCC12228 mov byte ptr [b],0FDh
signed char total = (signed char)(a + b);
00007FF6FCC1222D movsx eax,byte ptr [rsp]
00007FF6FCC12231 movsx ecx,byte ptr [b]
00007FF6FCC12236 add eax,ecx
00007FF6FCC12238 mov byte ptr [total],al
}
00007FF6FCC12228 mov byte ptr [b],0FDh
这一句,-3被转换成了补码形式0FDh(前缀0,后缀h表示数的十六进制表示)。
00007FF6FCC1222D movsx eax,byte ptr [rsp]
00007FF6FCC12231 movsx ecx,byte ptr [b]
这两句,编译器将以字节表示的数符号扩展到32位,这是编译器作的优化,这样计算效率高。
00007FF6FCC12236 add eax,ecx
这是汇编语言加法指令,运算时按逻辑加运算,不管什么符号位。
00007FF6FCC12238 mov byte ptr [total],al
这一句是我们使用的强制转换方法。
这个例子可以看出两点:编译器在计算字节数据类型时,会将其转换成32位的整数再进行运算;运算指令不管符号位,把符号位也当成正常数据参与运算,符号只体现在编码中,负数已经表示成了补码形式,而高级语言在声明有符号无符号时,实际上是告诉编译器,应当将数据按何种形式进行编码。
示例2:1字节有符号数和4字节有符号数加法运算
void Test()
{
signed char a = 1;
int b = -3;
auto total = a + b;
}
查看反汇编译代码:
void Test()
{
00007FF7DC942220 sub rsp,18h
signed char a = 1;
00007FF7DC942224 mov byte ptr [rsp],1
int b = -3;
00007FF7DC942228 mov dword ptr [b],0FFFFFFFDh
auto total = a + b;
00007FF7DC942230 movsx eax,byte ptr [rsp]
00007FF7DC942234 add eax,dword ptr [b]
00007FF7DC942238 mov dword ptr [total],eax
}
00007FF7DC942230 movsx eax,byte ptr [rsp]
编译器将以字节表示的数符号扩展到32位。
这个例子可以看出一点:两个类型大小不一的整数相加,编译器会先将字节小的数作符号扩展成与类型大的数的类型一致,再参与运算。
示例3:有符号数与无符号数混全加法运算
void Test()
{
unsigned int a = 1;
int b = -3;
auto total = a + b;
}
查看反汇编译代码:
void Test()
{
00007FF62B5F2220 sub rsp,18h
unsigned int a = 1;
00007FF62B5F2224 mov dword ptr [a],1
int b = -3;
00007FF62B5F222C mov dword ptr [rsp],0FFFFFFFDh
auto total = a + b;
00007FF62B5F2233 mov eax,dword ptr [rsp]
00007FF62B5F2236 mov ecx,dword ptr [a]
00007FF62B5F223A add ecx,eax
00007FF62B5F223C mov eax,ecx
00007FF62B5F223E mov dword ptr [total],eax
}
这个例子可以看出一点:编译器不管啥有符号无符号,你标识为有符号,如果输入的是负值,会把它转换为负数的被码形式,再参与运算,至于计算结果正确与否,那是程序员去判断的事。不同的编译器可能对计算值处理不一样,VS2022会将其默认为和为unsigned int型,如果值为负数,则明显结果错误,因此,何必要写这种代码去考验编译器呢?