C语言 数据的存储

一、数据与进制之间的关系

我们都知道,计算机存储的数据单位是二进制,要么是 0,要么是 1. 实际上,计算机就是用这种二进制序列来表示某个数值。

但我们也要理解与电子信息数据相关的其他表示方法:十进制、十六进制、八进制。因为在 C语言 中,常常需要用到将这些进制进行一定的转换。

十进制:		(0 - 9)
二进制:		(0 1)
八进制:		(0 - 7)
十六进制:	(0 - 9 a b c d e f)

1. 十进制与二进制之间的转换

下图是数据为 11 的十进制与二进制之间的转换。此外,十进制与十六进制、十进制与八进制相互转换的过程也是同理。

1-1

2. 二进制与十六进制之间的转换

1 个字节 8 位 二进制,恰好可以用两个十六进制数据表示。

1-2

二、整型数据存储

1. 原、反、补码

计算机中的整数有三种表示方法,即原码、反码和补码。
三种表示方法均有符号位和数值位两部分。符号位 0 表示正,1 表示负;数值位就是正常的 0/1 序列。

原码:直接将原数据按照正负数转换成二进制。
反码:原码的符号位不变,其他位依次按位取反。
补码:反码 +1.

2. 整型数据在内存中的存储

int 类型的 10,与 int 类型的 -10 在内存中的存储如下:

1-3

从上面的图上看,我们可以得出结论:

① 整型数据存放内存中的是二进制补码。
② 正整数的原、反、补码是相同的;但负整数的原、反、补码则需要计算。
③ printf 格式化输出的是数据的原码。

3. 为什么整型数据存在内存中存储的是补码

注意: CPU只有加法器,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。这样一来,使用补码,则可以将符号位和数值域统一处理。

我们就拿 1 + (-1) = 0 来举例:

// 1 - 1 <==> 1 + (-1)

 00000000 00000000 00000000 00000001	-> 1的原、反、补码

 10000000 00000000 00000000 00000001	-> -1的原码
 11111111 11111111 11111111 11111110	-> -1的反码
 11111111 11111111 11111111 11111111	-> -1的补码
// 错误的算法(使用原码相加)

 00000000 00000000 00000000 00000001	-> 1的原码
 +
 10000000 00000000 00000000 00000001	-> -1的原码
 10000000 00000000 00000000 00000010	-> 数值为 -2

// 正确的算法(使用补码相加)

 00000000 00000000 00000000 00000001	-> 1的补码
 +
 11111111 11111111 11111111 11111111	-> -1的补码
100000000 00000000 00000000 00000000	-> 数值为 0(最前面的1 舍去)

从结果来看,CPU 加法器对原码直接运算产生的结果是错误的,而采用补码是正确的。

4. 有符号和无符号的数据类型

char
unsigned char
signed char

short <==> signed short		// 有符号短整型
unsigned short				// 无符号短整型

int <==> signed int			// 有符号整型
unsigned int				// 无符号整型

long <==> signed long 		// 有符号长整型
unsigned long				// 无符号长整型

注意事项:

① unsigned 代表无符号类型,signed 代表有符号类型。如果没有特殊说明,一般就表示有符号类型。( 例如:int 就等价于 signed int 类型,即有符号整型。short、long 也默认为是有符号类型。但 char 官方并没有说明默认是有符号类型,这取决编译器的实现。)

② 有符号类型的二进制最高位是符号位,无符号类型的二进制最高位依然是数据位。

有符号和无符号的存储范围

我们以 char 类型的有符号和无符号对比, char 类型是一个字节,即 8 个比特位。

1-4

通过上图分析,我们可以看到有符号 char 类型的数据存储范围:-128 ~ 127,而 无符号 char 类型的数据存储范围:0 ~ 255. 类比 short、int、long 类型的数据范围也是这么计算来的。

猜想

理解了上面的有符号和无符号原理后,如果我们将一个负数放进一个无符号类型中,那么结果会发生什么事情呢?

程序清单:

#include <stdio.h>

int main() {

	unsigned char a = -10;
	printf("%u\n", a); // %u 为无符号打印

	return 0;
}

// 输出结果:246

从输出结果来看,-10 依然是按照二进制补码 11110110 存储至内存中的,只是在最后输出的时候,程序将 -10 的补码视为 -10 的原码直接就打印出来了,因为无符号整型本身就是一个不存在负数的类型,所以程序就视为原、反、补码相同才输出的。

1-5

所以输出结果并不是 -10,而是直接将其视为无符号二进制计算出的结果。

1-6

5. 关于 char 类型

char 类型占用内存的大小为 1 个字节,即 8 个比特位。而我们一般普遍认为 char 是字符类型。但实际上字符类型在底层存储字符的时候,存储却是字符对应的 ASCII 码值,所以我们依然可以将 char 类型视为整型。

字符 ’ A ’ 在内存中存储的二进制补码如下所示:实际上 ’ A ’ 的 ASCII 码值为 65,系统再转换成对应的二进制序列放入了内存中,为了方便显示,以十六进制显示在我们的面前。

1-7

三、大小端存储

1-8

在上图,我们可以看到局部变量 a,在内存中存储的是倒序的字节数据。这是为什么呢?其实这是在 VS 底下的编译器,它采用的是小端存储模式。

注意: 计算机在内存中存储数据是二进制序列,但是 VS 编译器为了方便我们观察,采用了十六进制显示序列 ( 2个十六进制位对应 8个二进制位,对应1个字节 )

1. 两种存储方式的区别

大端存储方式:数据的低位字节保存在内存的高地址中,而数据的高位字节保存在内存的低地址中。

小端存储方式:数据的低位字节保存在内存的低地址中,而数据的高位字节保存在内存的高地址中。

1-9

2. 设计一个程序来判断当前编译器的字节序存储

设计思路:利用 char* 指针来找到整个数据的第一个字节,从而判断是否为对应的字节值即可。( 例如:0x 00 00 00 01,如果是小端存储,必然第一个字节取出的是低位 01;反之如果是大端存储,必然第一个字节取出的是高位 00. )

注意: 利用指针访问、解引用都是从低地址往高地址操作的。

1-10

程序清单:

#include <stdio.h>

int main() {

	int a = 0x00000001;
	char* p = (char*) & a;

	if (*p == 1) {
		printf("小端存储\n");
	}else {
		printf("大端存储\n");
	}

	return 0;
}

四、整型提升

由于表达式的整型运算需要在 CPU 的相应运算器件内执行,而 CPU 内整型运算器(ALU) 的操作数的字节长度一般是 int 类型的字节长度,同时也是 CPU 的通用寄存器的长度。因此,即使两个 char 类型的变量相加,在 CPU 执行时也要先转换为 CPU 内整型操作数的标准长度。所以,表达式中各种长度可能小于 int 长度的整型值,都必须先转换为 int 或 unsigned int,然后才能送入 CPU 去执行运算。

注意:

① 对于有符号类型,整形提升是按二进制最高位补全。
② 对于无符号类型,整型提升直接按 0 补全。

五、例题

程序清单1

#include <stdio.h>

int main()
{
	char a = -1;
	signed char b = -1;
	unsigned char c = -1;
	printf("a=%d, b=%d, c=%d", a, b, c);
	return 0;
}

// 输出结果:-1 -1 255

运算过程:

① 变量 a 的运算过程 (变量 b 也是同理)

// 1. 将 -1 数据放入变量 a 的内存中
10000000 00000000 00000000 00000001		-> -1 原码
11111111 11111111 11111111 11111110		-> -1 反码
11111111 11111111 11111111 11111111		-> -1 补码

11111111 // 截断成 char 类型,放入变量 a 中(补码)

// 2. 整型提升,由于变量 a 是有符号类型,所以按最高位补全
11111111 11111111 11111111 11111111		-> 新的补码

// 3. %d 打印,输出一个有符号的整型数据
11111111 11111111 11111111 11111111		-> 新的补码 
11111111 11111111 11111111 11111110		-> 新的反码 
10000000 00000000 00000000 00000001		-> 新的原码 (最终输出 -1)

② 变量 c 的运算过程

// 1. 将 -1 数据放入变量 c 的内存中
10000000 00000000 00000000 00000001		-> -1 原码
11111111 11111111 11111111 11111110		-> -1 反码
11111111 11111111 11111111 11111111		-> -1 补码

11111111 // 截断成 char 类型,放入变量 c 中(补码)

// 2. 整型提升,由于变量 c 是无符号类型,所以按 0 补全
00000000 00000000 00000000 11111111		-> 新的原、反、补码

// 3. %u 打印,输出一个无符号的整型数据
00000000 00000000 00000000 11111111		-> 不存在负数,直接输出 255

程序清单2

#include <stdio.h>

int main() {

	char a = 3;
	char b = 127;
	char c = a + b;
	printf("%d\n", c);

	return 0;
}

// 输出结果:-126

计算过程:

// 1. 将 a + b 的结果数据放入变量 c 的内存中
00000000 00000000 00000000 00000011		-> 3 的原、反、补码
00000011 // 截断成 char 类型,放入变量 a 中(补码)

00000000 00000000 00000000 01111111		-> 127 的原、反、补码
01111111 // 截断成 char 类型,放入变量 b 中(补码)

// 2. 整型提升,由于变量 a, b 是有符号类型,所以按最高位补全
00000000 00000000 00000000 00000011		-> 3 的补码
+
00000000 00000000 00000000 01111111		-> 127 的补码
=
00000000 00000000 00000000 10000010		-> 新的补码

10000010	// a + b 的结果,截断成 char 类型,放入变量 c 中(补码)

// 3. 整型提升,由于变量 c 是有符号类型,所以按最高位补全
11111111 11111111 11111111 10000010		-> 新的补码

// 4. %d 打印,输出一个有符号的整型数据
11111111 11111111 11111111 10000010		-> 新的补码
11111111 11111111 11111111 10000001		-> 新的反码
10000000 00000000 00000000 01111110		-> 新的原码 (最终输出 -126)

程序清单3

#include <stdio.h>

int main()
{
	char a = -128;
	char b = 128;
	printf("%u %u\n", a, b); 

	return 0;
}

// 输出结果:4294967168  4294967168

运算过程:变量 a

// 1. 将 -128 数据放入变量 a 的内存中
10000000 00000000 00000000 10000000		-> -128 原码
11111111 11111111 11111111 01111111		-> -128 反码
11111111 11111111 11111111 10000000		-> -128 补码

10000000 // 截断成 char 类型,放入变量 a 中(补码)

// 2. 整型提升,由于变量 a 是有符号类型,所以按最高位补全
11111111 11111111 11111111 10000000

// 3. %u 打印,输出一个无符号的整型数据
11111111 11111111 11111111 10000000		-> 不存在负数,直接输出 4294967168

运算过程:变量 b

// 1. 将 128 数据放入变量 b 的内存中
00000000 00000000 00000000 10000000		-> 128 原、反、补码

10000000 //截断成 char 类型,放入变量 b 中(补码)

// 2. 整型提升,由于变量 b 是有符号类型,所以按最高位补全
11111111 11111111 11111111 10000000		-> 新的补码

// 3. %u 打印,输出一个无符号的整型数据
11111111 11111111 11111111 10000000		-> 不存在负数,直接输出 4294967168

程序清单4

#include <stdio.h>

int main() {

	int i = -20;
	unsigned int j = 10;
	printf("%d\n", i + j);

	return 0;
}

// 输出结果:-10

运算过程:

10000000 00000000 00000000 00010100		-> -20 原码
11111111 11111111 11111111 11101011		-> -20 反码
11111111 11111111 11111111 11101100		-> -20 补码

00000000 00000000 00000000 00001010		-> 10 原、反、补码

// 1. i + j 的计算过程
11111111 11111111 11111111 11101100		-> -20 补码
+
00000000 00000000 00000000 00001010		-> 10 补码
=
11111111 11111111 11111111 11110110		-> 新的补码

// 2. %d 打印,输出一个有符号的整型数据
11111111 11111111 11111111 11110110		-> 新的补码
11111111 11111111 11111111 11110101		-> 新的反码
10000000 00000000 00000000 00001010 	-> 新的原码 (最终输出 -10)

程序清单5

#include <stdio.h>
#include <Windows.h>

int main() {

	unsigned int i;
	for (i = 9; i >= 0; i--)
	{
		printf("%u\n", i);
		Sleep(1000); // 休眠 1 秒
	}

	return 0;
}

// 输出结果:9  8  7  6  5  4  3  2  1  0  4294967295  4294967294...

运算过程:

00000000 00000000 00000000 00001001		-> 9 的原、反、补码
...
10000000 00000000 00000000 00000001		-> - 1 原码
11111111 11111111 11111111 11111110		-> - 1 反码
11111111 11111111 11111111 11111111		-> - 1 补码 (4,294,967,295)

// 当 -1 放入无符号变量 i 中时,此时程序就不将其视为负数了,
// 所以最终将其视为无符号直接输出,即所有的二进制补码序列全是数据位

程序清单6

#include <stdio.h>
#include <string.h>

int main()
{
	char a[1000];
	int i;
	for (i = 0; i < 1000; i++)
	{
		a[i] = -1 - i;
	}

	printf("%d\n", strlen(a));

	return 0;
}

// 输出结果:255

首先,我们得明白 strlen 是用来求字符串的长度的 ( ’ \0 ’ 之前的),而 ’ \0 ’ 的 ASCII 码也是 0. 所以对于上面的程序,当字符数组中间的元素出现 字符0 时, 就意味着 strlen 函数计算字符串长度已经到头了。

计算过程:

截断发生的过程:

// i = 0
10000000 00000000 00000000 00000001		-> -1 原码
11111111 11111111 11111111 11111110		-> -1 反码
11111111 11111111 11111111 11111111		-> -1 补码

11111111 // 截断放入 a[0] 中(补码)

// i = 1
10000000 00000000 00000000 00000010		-> -2 原码
11111111 11111111 11111111 11111101		-> -2 反码
11111111 11111111 11111111 11111110		-> -2 补码

11111110 // 截断放入 a[1] 中(补码)

// i = 2
10000000 00000000 00000000 00000011		-> -3 原码
11111111 11111111 11111111 11111100		-> -3 反码
11111111 11111111 11111111 11111101		-> -3 补码

11111101 // 截断放入 a[2] 中(补码)
...
...

经过上面的分析,可以总结出,字符数组中第 256 位被放入的是 0,所以在第 256 位之前,就是应该 strlen 所计算的字符串长度,即 255.

(从 00000000 到 11111111 总共有 2^8 种情况,即 256.)

// 字符数组中每个字符存储的二进制补码
11111111		-> -1 补码
11111110		-> -2 补码
11111101		-> -3 补码
...
...
00000001		
00000000 // 第 256 位补码

或者我们也可以如下分析:

无符号的 char 类型的范围:-128 ~ 127

2-1

程序清单7

#include <stdio.h>
#include <Windows.h>

unsigned char i = 0;
int main()
{
	for (i = 0; i <= 255; i++)
	{
		printf("hello world, %d\n", i);
		Sleep(100); // 休眠 0.1 秒
	}
	return 0;
}

// 输出结果:(死循环) 0 - 255, 0 - 255, 0 - 255 ...

运算过程:

有符号的 char 类型的范围:0 ~ 255

 00000000			-> 0 补码
 00000001			-> 1 补码
 ...
 ...
 11111111			-> 255 补码
100000000			-> 256 补码	-> 截断成 00000000
 00000001			-> 1 补码
 ...
 ...
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

十七ing

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

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

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

打赏作者

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

抵扣说明:

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

余额充值