文章目录
Data Storage
本文将介绍数据是如何存储在内存中的,以怎样的形式存储在内存中,又是以怎样的形式向外拿取的。
1. 数据类型
在c语言中,仅存在4中基本数据类型——整型、浮点型、指针、构造类型。而其它的类型都是从这四种基本类型的某种组合衍生而来的。
1.1 整型家族
整型家族包括:字符型、短整型、整型、长整型。他们都分别分为有符号(signed)和无符号(unsigned)两种。
char
signed char
unsigned char
short
signed short
unsigned short
int
signed int
unsigned int
long [int]
signed long [int]
unsigned long [int]
长整形至少应该和整型一样长,而整型至少应该和短整型一样长。
在c语言标准中,并没有规定长整型必须比短整型长,只是规定不得比短整型短。ANSI标准加入了一个规范,说明了各种整型值的最小允许范围。
类型 | 最小允许范围 |
---|---|
signed char | -128~127 |
unsigned char | 0~255 |
signed short | -32768~32767 |
unsigned short | 0~65535 |
signed int | -32767~32767 |
unsigned int | 0~65535 |
signed long | -2147483647~2147483647 |
unsigned long | 0~4294967295 |
short int
至少为16位,long int至少为32位。而int则需要根据编译器的设计者决定。在头文件
limits.h
中说明了各种不同的整数类型的特点。
也许你会有疑问,为什么字符型也属于整型家族?
字符型变量是将其对应的
ASCII编码
存放至内存,而不是字符本身。
1.2 浮点数家族
浮点数家族包括:float、double和long double类型。这些类型分别表示为单精度、双精度和在某些支持扩展精度的机器上的扩展精度。
ANS标准仅仅规定了long double至少和double一样长,而double至少和float一样长。标准同时规定了一个最小范围:所有的浮点类型至少能够容纳10-37~1037之间的任何值。
在头文件
float.h
中说明了各种不同的浮点类型的特点。
浮点数常量值在缺省的情况下都是double类型的。如果后面跟一个L或者l来表示它是一个long double类型的值,或者跟一个F或f来表示它是一个float类型的值。
1.3 指针类型
指针类型建立在整型、浮点型、空类型、构造类型的基础之上。
变量的值存储于计算机的内存中,每个变量都占据一个特定的位置。每个内存位置都由地址唯一去定并引用。指针只是的地址的另一个名字。指针变量就是一个其值为另外一个内存地址的变量。
你可以将计算机的内存想象成一条长街上的一间间房子,这些房子是连着的,每间房子都用一个唯一的号码来进行标识。这个号码就是这间房子的位置(地址),每个位置可以用来存放一个值。
1.4 构造类型
构造类型分为:数组类型、结构体类型(struct)、枚举类型(enum)、联合类型(union)
特别的:
空类型
void 表示空类型(无类型)
通常应用在函数的返回类型、函数的参数、指针类型。
2. 整型在内存中的存储
2.1 二进制表示形式
计算机中整数的二进制表示形式有三种:原码、反码、补码。其中补码是数据存放在内存中的形式。
原码、反码、补码的三种表是形式均存在符号位和数值位两部分。
符号位:位于数据的二进制形式的第一位,由0表示正,1表示负
数值位:除去第一位的符号位,其余位都是数值位
2.1.1 正数
正数的原码、反码、补码都相同。即将一个正整数换算为二进制形式,即得到其原码、反码、补码。
int a = 10;
00000000 00000000 00000000 00001010 ——原码
00000000 00000000 00000000 00001010 ——反码
00000000 00000000 00000000 00001010 ——补码
2.1.2 负数
负数的原码、反码、补码各不相同。
原码
直接将负数按照正负数的形式翻译为二进制,就可以得到原码
负数的符号位应为1
反码
将得到的原码符号位不改变,其余位按位取反,得到反码
即:将原码的符号位不改变,除去符号位,其余所有位都改为相反值(0改1,1改0)
补码
将得到的反码加1,即可得到补码
int a = -10;
10000000 00000000 00000000 00001010 ——原码
11111111 11111111 11111111 11110101 ——反码
11111111 11111111 11111111 11111110 ——补码
2.2 存储形式
在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值位统一处理。同时,因为CPU只有加法器,所以加法和减法也可以统一处理。
补码转换为原码,与,原码转换为补码的方式是想通的,不需要额外的硬件电路。
2.2.1 加法运算
int a = 10;
int b = 5;
int c = a + b;
10:
原码/反码/补码:00000000 00000000 00000000 00001010
5:
原码/反码/补码:00000000 00000000 00000000 00000101
10+5:
10的补码:00000000 00000000 00000000 00001010 0x0000000a
5的补码:00000000 00000000 00000000 00000101 0x00000005
15的补码:00000000 00000000 00000000 00001111 0x0000000f
两个正整数相加,即两个数的补码进行相加,相加得到的二进制形式即为两数相加的结果的补码
我们可以通过vs调试功能证明这一点。为了更方便观看,以十六进制显示。
2.2.2 减法运算
我们可以把10-5
这一减法转换为10+(-5)
,然后再通过加法器进行计算
int a = 10;
int b = -5;
int c = a + b;
因为-5
为负数,所以其原码、反码、补码各不相同
10:
原码/反码/补码:00000000 00000000 00000000 00001010
-5:
原码:10000000 00000000 00000000 00000101
反码:11111111 11111111 11111111 11111010
补码:11111111 11111111 11111111 11111011
10+(-5):
10的补码: 00000000 00000000 00000000 00001010 0x0000000a
-5的补码: 11111111 11111111 11111111 11111011 0xfffffffb
1 00000000 00000000 00000000 00000101
因为整型只能存放4个字节的内容,所以发生截断最后结果为:
00000000 00000000 00000000 00000101
通过上述调试截图,我们发现在内存中存储的数据是倒着存放的,这是为什么呢?
2.3 大小端
我们都已经知道,数据是在内存中存储的,并且是以补码的形式存放,那么这些二进制数字在内存中是怎么存放的呢?
内存空间相当于一个停车位,我们可以正着开进去,也可以倒进去,或者可以练一手侧方停车(自从考完驾照,侧方忘完了,每次停车最烦侧方)。
如图,当我们把车子停到停车位里面之后,我们之后肯定还会再用它,再要把它取出来,我想直接直进直出的肯定比左打方向右打方向的一点点挪出来要方便的多吧。
同理,我们把数据存储到内存中,是为了之后使用,那么怎么放进去的就要怎么拿出来,顺序存放只需要按照顺序查找就能取出数据,乱序存放不仅需要取出数据,还需要把数据重新组合排列为原来的数据,麻烦倍增。所以最后只留下的两种方式,正序和逆序,也称为大端存储模式和小端存储模式。
**大端存储模式:**数据的低位保存在内存的高地址中,数据的高位保存在内存的低地址中
**小端存储模式:**数据的低位保存在内存的低地址中,数据的高位保存在内存的高地址中
在计算机系统中,我们以字节为单位,每个地址单元都对应一个字节,一个字节为8bit。但是在C语言中除了8bit的char以外,还有16bit的short,32bit的long(由编译器决定位数)。再者,对于位数大于8位的处理器,如:16、32、64位处理器,由于寄存器宽度大于1个字节,那么必然存在如何将多个字节安排的问题。
不同机器的存储模式不同,由机器决定存储模式。
2.4 百度笔试题
请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。
大端字节序与小端字节序在内存中的存储不同,即数据存储的顺序不同,想要得到一个机器的字节序只需要对一个数字进行存储之后,取出这个数字的第一个字节的内容,即可得知此机器的字节序。
首先这个用于判断的数字,在存储时高位和低位需要有鲜明的区别,例如数字 1
1的二进制形式为:00000000 00000000 00000000 00000001
1的十六进制形式为:0x 00 00 00 01
最高位为0最低位为1,只需要判断存储空间的低位也就是第一个字节的内容是否为1即可得知此机器的字节序。
代码实现
//版本1:
#include<stdio.h>
int main()
{
int n = 1;
if (*((char*)&n) == 1)
{
printf("小端字节序\n");
}
else
{
printf("大端字节序\n");
}
return 0;
}
//版本2:
#include<stdio.h>
int ByteOrder()
{
int n = 1;
return *((char*)&n);
}
int main()
{
int ret = 0;
ret = ByteOrder();
if (ret == 1)
{
printf("小端字节序\n");
}
else
{
printf("大端字节序\n");
}
return 0;
}
3. 浮点型在内存中的存储
常见的浮点数形式:
3.1415926
1E-10
3.1 科学记数法
科学记数法是一种记数的方法。把一个数表示成a与10的n次幂相乘的形式
科学记数法的形式是由两个数的乘积组成的。表示为a*10^b
或者`aEb。其中a的取值范围1≤|a|<10。
例如:
0.00001 == 1*10^-5 == 1E-5
3.2 浮点数换算
将一个小数转换为二进制形式,与将一个整数转换为二进制形式是完全不同的。
实例讲解:
4.75转换为二进制形式
B —— 二进制
O —— 八进制
D —— 十进制
H —— 十六进制
1. 先将整数 4 转换为二进制形式
00000100
2.再将小数部分 0.75 转换为二进制形式
对于二进制小数部分,0.1(B) == 2^-1 = 0.5(D)
0.01(B) == 2^-2 = 0.25(D)
故小数部分0.75的二进制形式为:0.11
4.75的二进制形式为:0100.11
3.3 浮点数存储规则
3.3.1 表示形式(-1)^S * M * 2^E
根据国际标准IEEE(电气和电子工程协会)754,任意一个二进制浮点数V
可以表示成下面的形式:
(-1)^S * M * 2^E
(-1)^S表示符号位,当S=0时,V为正数;当S=1时,V为负数
M表示有效数字,大于等于1,小于2;(1≤M≤2)
2^E表示指数位
举例讲述:
还是4.75
转换为二进制形式为:100.11。写成二进制的科学记数法形式为:1.0011 * 2^2
按照公式 (-1)^S * M * 2^E进行拆分
1.0011为正数,所以S==0,即:(-1)^0
M表示有效数字,即:M==1.0011
E为指数位,即:E=2
- 综合上述得:(-1)^0 * 1.0011 * 2^2
3.3.2 存储模型
IEEE754规定:对于32位浮点数和64位的浮点数分别采用不同的存储模型
32位浮点数(单精度浮点数)存储模型:
最高的1位是符号位S,之后的8位是指数E,剩下的23位为有效数字M。
64位浮点数(双精度浮点数)存储模型:
最高的1位是符号位S,之后的11位是指数E,剩下的52位为有效数字M。
3.3.3 特别规定
IEEE754对有效数字M和指数E,设有一些特别
3.3.3.1 有效数字M
在将一个二进制数字V,写成IEEE754规定的形式(-1)^S * M * 2^E
时,对于有效数字M而言,1≤M≤2
,M需要写成1.xxxxxx
的形式,其中的xxxxxx
表示小数部分。
对此,IEEE754规定,在计算机内部保存M时,默认这个数字的第一位总是1,因此可以被舍去,只保存后面的xxxxxx
部分。比如在保存1.01
时,只保存01
,等到读取的时候,再把小数点前面的1加上。这样做的目的是为了节省1位有效数字。
以32位浮点数存储模型为例,留给有效数字M的空间为23位。如果第一位的1不再保存,等于可以保存24位有效数字。(23位小数点后数字+1位默认存在舍去的1)
3.3.3.2 指数E
E为一个无符号的整数(unsigned int),这说明,如果E为8位,它的取值范围为0255;如果E为11位,它的取值范围为02047。但是,科学记数法是可以出现负数的。
所以IEEE754规定:存入内存时,E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E这个中间数是1023。
例如:2^2的E是2,在保存成32位浮点数时,必须保存成2+127=129
,即:1000 0001
;然后存入到模型中E的位置。
特殊情况
对于指数E从内存中取出还可以分为三种情况:
E不为全0或者E不为全1
当E不为全0或者E不为全1的情况时,浮点数采用下面的规则表示:指数E的计算值减去127(1023),得到真实值,再将有效数字M前加上第一位的1。
E为全0
当E为全0时,浮点数的指数E等
1-127
(或者1-1023
)即为真实值,此时有效数字M不在加上第一位的1,而是还原成0.xxxxxx
的小数形式。这样做是为了表示±0,以及接近于0的很小的数字。
E为全1
这时,如果有效数字M全为0,表示±无穷大(政府取决于符号位S)
3.4 练习
对于下面这段代码的输出结果为多少?
int main()
{
int n = 9;
float *pFloat = (float*)&n;
printf("n的值为: %d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
*pFloat = 9.0;
printf("n的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
return 0;
}
一部分:
9的补码为:00000000 00000000 00000000 00001001 == 0x 00 00 00 09
int n在内存中开辟一块4个字节大小的内存空间
&n取出n所指向的内存地址,强制类型转换为(float* ),将这块内存空间的地址赋值给了,指针类型为float*的pFloat。
以%d的形式打印n时,其值并未遭到改变,以整型的形式存入,再以整型的形式取出打印,所以输出为9
以%f的形式打印时,以整型的形式存入,以浮点数的形式取出
内存中补码为:00000000 00000000 00000000 00001001 将其按照补码的形式进行取的时候,即按照32位浮点数模型进行拆分
得到:S=0
E=00000000
M=0000000 00000000 00001001上文说到,当指数E全为0时,指数E等于1-127(1-1023)即为真实值。有效数字M不再加上第一位的1,而是还原成0.xxxxxx的小数。
此时指数E全为0,则浮点数V写成:
V=(-1)^0 * 0.00000000000000000001001 * 2^(1-127)
=1.001 * 2^(-126)
此时V是一个很小的无限接近于0的正数,所以用十进制小数表示为0.000000
二部分
*pFloat = 9.0;将9.0以浮点数的形式存入到 *pFloat所指向的内存空间
9.0转换为1001.0,科学记数法为 1.001*23,转换为**(-1)S * M * 2^E**的形式得到
(-1)^0 * 1.001 * 2^3
得到 S=0,E=3+127=130,M=1.001E=130 转换为二进制为10000010
M去除1存储为001,后面补齐20个0
最终浮点数9.0在内存中存储为:0 10000010 00100000000000000000000
以浮点数的形式存储,以整型的形式往外取,则直接将01000001000100000000000000000000看成一个整型的存储方式,换算为十进制为1091567616
以浮点数的形式存储,以浮点数的形式往外取,则为9.000000