第八章 数据的存储
从本章开始就正式进入了C语言进阶篇的学习,我们将从更为底层的角度思考C语言中更为复杂的机制和语法。
第一节
基本数据类型
C语言中的基本数据类型在初阶篇已经探讨过了,分为整形,浮点型,字符型与构造类型几个大类,每个类型都有着不同的容量,所占的内存大小也不尽相同,有的甚至在不同系统下都有着不同的大小因此必须在实核情境下选择合适的数据类型进行使用。1
2
3
4
5
6
7char //字符数据类型,占1字节
short //短整型,占2字节
int //整形,占4字节
long //长整型,占4字节
long long //更长的整形,占8字节
float //单精度浮点数,占4字节
double //双精度浮点数,占8字节
其中整形数组类型分为有符号和无符号类型,无符号类型不能表示负数,否则会导致很严重的数据出错问题,关于为什么出错与数据在内存中的存储有很大的关系,之后我们会进行解释。
整形在内存中的存储
大端字节序、小端字节序
数据在内存中的存储形式都是以二进制数的方式所表示的,比如说我们要将int
型8
这个十进制数存入内存,编译器会帮助我们将其转换为二进制形式00000000 00000000 00000000 00001000
,一共32位,这里我们先不探讨数据在内存中是怎么转换为二进制形式进行存储的。这32位二进制一共是4个字节,系统会以每个字节为一个单位继续宁内存存储,因此为了方便起见,我们往往将内存中的数据以十六进制进行表示,因为1位十六进制数等于4位二进制数,也就是一个字节原本需要8位二进制表示,现在只需要用2位十六进制进行表示即可,通常我们在内存监视器中所看到的数据存储也大多是以十六进制进行表示的。将这么一串二进制转换为八进制即可表示为00 00 00 08
。但是实际在内存中是以二进制进行存储的,在这里用十六进制是为了方便我们进行观察。
之后就要将这么一串数据存入内存中了,但是就在这里问题出现了,我们是要将最高位1个字节的数据首先存储在低地址区,还是要将最低位1个字节的数据首先存储在低地址区呢?其实这两种存储方式都得到了实施,而具体的存储方式与我们电脑的CPU密切相关(其实也并不仅有CPU决定,具体来说是和环境也就是平台密切相关,但CPU占大头),因此在当前的市场就有了大小端字节序之称,专门用来用来区分这两种不同的数据在内存中的存储方式。
1、大端字节序:低位存储在高地址上,依然以以上的例子为例,则在内存中以这样的方式进行存储:00 00 00 08
。
2、小端字节序:低位存储在低地址上,依然以以上的例子为例,则在内存中以这样的方式进行存储:08 00 00 00
。
注意:大小端字节序仅仅决定数据在内存中的存储,并不会因此改变数据本身的值!!!
有了以上的原理,我们即可使用代码验证我们当前的环境是大端还是小端了。1
2
3
4
5
6
7
8
9
10
11
12
13
14#include <stdio.h>
int main()
{
int a = 0x11223344;
char* p = (char*)&a;
if(*p == 0x11)
{
printf("大端!\n");
}
else
{
printf("小端!\n");
}
}
通过以上的代码,我们将内存中的数据取头一个字节的数据进行验证的方法即可判断是大端还是小端。
整形在内存中的存储
接下来就是数据是如何转换为二进制并且在内存中进行储存的问题了。首先是整型是如何在内存中进行存储的呢?这就要牵扯到原码,补码,反码三个紧密联系的二进制码了。
计算机在内存中都是以补码的形式进行存储的,而我们直接将一个数转换成二进制的所形成的最多只能被称为原码其重要进行两步转换。
有符号整形
这里我们先以有符号进行演示,之后再讨论无符号。这次我们以(int)-8
为例,在有符号的情况下,我们的数据会将第一位二进制留出来作为符号位,0
表示正数,1
表示负数,这也是为什么32位的整形在有符号的情况下最大值只能到21亿的原因,因为有一位被保留出来做了符号位。除过第一位我们将数据原本的值转换为二进制即可表示为10000000 00000000 00000000 00001000
,这段代码即被称为原码。之后我们想要转换为补码还得先将其转换为反码,就是在原码的基础上,除过符号位外全部按位取反即可变为11111111 11111111 11111111 11110111
这就是反码。之后我们只需再将反码 +1即可变为补码11111111 11111111 11111111 11111000
这就是实际上有符号整形在内存中的存储形式了。
而以上的转换过程只对负整数适用,而正整数的原反补码都相同。
无符号整形
无符号整形与有符号整形基本类似,只是其省略了最高位的符号位而已,因此使得无符号的最大值比有符号整形最大值大了一倍,但也因此使得无符号整型无法表示负数。
但是在这里值得强调的一点是,一个数据存储在内存中进行存储,尽管存储的形式完全一致,内容也完全一致,但是我们使用不同的理解方法去理解,就会有完全不同的结果。这点我们从以下这个例子就可以看出来。1
2
3
4
5
6
7
8
9#include <stdio.h>
int main()
{
unsigned int a = -1;
printf("%u\n", a);
}
[misaki@localhost test]$ ./Main
4294967295
从执行结果就可以看出其中的问题,在存储数据的时候我们虽然定义了一个无符号的整形,但是当我们存储负数时编译器也并不会报错,此使-1会按照有符号负整数的方式进行存储在内存中,11111111 11111111 11111111 11111111
,但是当我们使用无符号将其进行打印时,编译器又会将此数据以无符号正整数的方式将其编译转换成十进制就得出了我们最终的结果,因此可以看出同一个数据有多种理解方式因此得出的结果也不尽相同,并且无符号数据类型同时也是十分危险的,十分容易出错,因此我并不推荐经常使用它们。
但是说了这么多我们为什么要如此大费周章使用补码存储数据呢?这又与我们电脑的cpu密切相关
了,由于我们的cpu只有加法器,我们想要更为方便与简单的处理减法以及表示负数我们就需要想出一种最为合理和简单方式,因此补码诞生了,这使得cpu可以将符号域与数值域共同参与运算,大大提高了运算效率,因此将数据存储为补码是必要的。
浮点型在内存中的存储
关于浮点型在内存中的存储十分复杂,在这里只作简单讲解。
首先浮点型在内存中的存储与整形可以说是天差地别。根据国际标准IEEE的规定,任意一个二进制浮点数可以表示为以下的形式(-1) ^ S * M * 2 ^ E
,然而其中E和M的大小共同决定着这个数据的精度,而double
之所以比float
进度更高也是因为其在数据存储域的划分上有着更多的存储空间,关于其中的细节,还有着十分严密甚至是严苛的规定和标准,具体请参考IEEE 754。