本文章使用VS2022运行及测试代码。
一、什么是原码、反码和补码?
计算机中的整数有三种2进制表示方法,即原码、反码和补码。
三种表示方法均有符号位和数值位两部分。正数的符号位用0表示,负数的符号位用1表示。
如果直接将整数1换算成二进制位就是整数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 0 1 --- 这就是1的原码。
C语言中的整型是4Byte大小,所以有32个bit位,即32个二进制位。其中第一位就是符号位,其余的31位是数值位。
对于正数来说,原码、反码和补码是一样的。所以上述原码也是整数1的反码和补码。
但是负数的三码是不同的,需要通过一定的规则换算出。
负数-1的原码为10000000000000000000000000000001,因为是负数所以符号位是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 1 1 1 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 1 1 1 1 1 --- 这就是-1的补码。
在计算机中,所有的运算都是基于补码进行的。
那为什么要使用补码呢?请见下文。
二、计算机中数据的存储
1.补码可以统一加减法
在计算机中,所有的数据都是使用补码存储,原因在于补码可以统一符号位和数值位两部分。这意味计算机只使用加法就可以计算两个数据的加减。实际上,CPU也只有加法器。
使用以下代码演示计算机如何只使用加法完成数据的相减。
#include <stdio.h>
int main(void)
{
int num1 = 10;
//num1的原码 00000000000000000000000000001010
//num1的反码 00000000000000000000000000001010
//num1的补码 00000000000000000000000000001010
int num2 = -10;
//num2的原码 10000000000000000000000000001010
//num2的反码 11111111111111111111111111110101
//num2的补码 11111111111111111111111111110110
int sum = num1 + num2;
return 0;
}
通过调用VS的内存窗口可以观察到num1和num2在内存中的存储内容。

VS内存窗口的表现形式是十六进制,1个十六进制可以转化成4个二进制位。一个整型是4个字节,即32个二进制位,所以可以用8个十六进制位表示。
num1在内存中是0a 00 00 00,因为数据在VS中是倒着存储的,所以实际上是00 00 00 0a,转化成二进制是00000000000000000000000000001010,即num1的补码,也是原码。
num2在内存中是f6 ff ff ff,即ff ff ff f6转化成二进制是11111111111111111111111111110110,即是num2的补码。
num1的补码直接加上num2的补码就是num1加num2的结果。
00000000000000000000000000001010 + 11111111111111111111111111110110 = 00000000000000000000000000000000
sum的二进制位为00000000000000000000000000000000,换算成十进制即是0。

2.补码与原码的相互转化
原码和补码都可以通过取反再+1得到对方。
如-10的原码是: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 1 0 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 0 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 0 1 1 0
反码也可以通过取反再加1得到原码:
-10的补码是 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 0 1 1 0
取反得到 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 1 0 0 1
再加1得到 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 1 0 1 0
如此可以补码与原码的相互转换,其运算过程是相同的,不需要额外的硬件电路。
三、大端与小端
1.什么是大端模式和小端模式?
上文提到num1在在VS中是倒着存储的,这是因为存储模式是小端。一般来说,有两种存储模式,大端(存储)模式和小端(存储)模式,都是以字节为单位的数据存储顺序。
大端(存储)模式:是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
小端(存储)模式:是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中。
可以用下列代码判断具体模式:
#include <stdio.h>
int main()
{
int a = 1;//需要一个非零整型
if (*(char*)&a == 1)
printf("小端\n");
else
printf("大端\n");
return 0;
}
因为char*只有一个字节,所以用char*进行强制类型转换,将a转换成char*类型。因为char*是指针,所以用*进行解引用。
2.小端模式导致的数据在内存中的存储顺序变化
因为目前计算机基本都是小端存储,因此本文章仅展现小端存储。
#include <stdio.h>
int main(void)
{
unsigned int a = 0x12345678;
unsigned char b = *(unsigned char*)&a;
printf("%x", b);
return 0;
}
在上述代码中向a赋值十六进制的12345678,因为vs的存储模式是小端存储,所以低地址放在低地址,高地址放在高地址,并且VS内存中的地址是由小到大,所以a中的数据是倒着存储的。
由于一个字节可以表示2个十六进制数字,所以顺序的颠倒也是以2个数字为一个单位。
| a 的值 | 12 34 56 78 |
| a 在内存中的存储形式 | 78 56 34 12 |

通过VS中的内存窗口可以观察到,a的储存形式确实是倒着存储:78 56 34 12
虽然是以两个数字为一个单位倒着存储,但实际上7和8中,8仍是低位地址,在一个字节的表示中,右边数字的地址比左边低;以字节为单位,则左边的字节壁右边的字节地址更低。这一知识点会在3.3中再做解释。
我们也可以通过输出结果来判断a是怎么存储的。
首先&a,这样a就是用32位的地址表示(即一个int*),再通过强制类型转换将将&a转变为unsigned char* 类型,因为unsigned char*只有一个字节大小,所以只会有最低地址的一个字节内容保留。
以下是输出结果:

可以观察到只有78被打印出来,表明12 34 56 78是倒着存储在a中的。
3.数据存储默认在所开辟空间中的最低位
一个整型默认会开辟4字节的空间,如果存储内容无法填满空间,则默认保存在最低位。
以下代码为例:
#include <stdio.h>
int main()
{
unsigned int a = 0x1234;
int b = 5;
return 0;
}
a的存储形式如下:

0x 12 34可以视为0x 00 00 12 34,在前面补0,倒着存储在a中即34 12 00 00。
b的存储形式如下:

可以看到在最低地址的字节中,是以05形式存储的,说明在一个字节之中,右侧的地址比左侧低,在字节之间的比较中,左侧字节的地址更低。
如果这样看不习惯,可以将VS的内存窗口的列数调为1列。

1万+

被折叠的 条评论
为什么被折叠?



