C语言中的原码、反码和补码及数据的大小端存储模式

本文章使用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列。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值