嘿嘿,家人们,今天咱们来深度剖析数据类型在内存中的存储,好啦,废话多不讲,开干!
1.:数据类型介绍
在前面呢,博主已经介绍了基本的数据类型:
char //字符数据类型 ---->占据1个字节short //短整型 ---->占据2个字节int //整形 ---->占据4个字节long //长整型 ---->占据4个字节long long //更长的整形 ---->占据8个字节float //单精度浮点数 ---->占据4个字节double //双精度浮点数 ---->占据8个字节
- 使用这个类型开辟内存空间大小(大小则决定了使用的数值范围).
- 为学习者提供了一个看待内存空间的视角.
1.1:类型的归类
1.1.1:整型家族
char
unsigned char
signed char
PS:字符型的数据在内存中是以ascii码值的方式存储的,因此属于整型家族.
short
unsigned short int(无符号短整型)
signed short int(有符号短整型)
int
unsigned int(无符号短整型)
signed int(有符号短整型)
long
unsigned lont int(无符号长整型)
signed int(有符号长整型)
1.1.2:浮点数家族
float 浮点型
double 双精度浮点型
1.1.3:构造类型
数组类型
结构体类型 struct
枚举类型 enum
联合类型 union
1.1.4:空类型
void 表示空类型(无类型).
通常应用于函数的返回类型、函数的参数、指针类型.
2:整数在内存中的存储
在操作符的时候,博主有讲解到原码,反码,补码的相关知识,这里带着uu们简单回顾下.
整数的2进制表⽰⽅法有三种,即 原码、反码和补码三种表⽰⽅法均有 符号位和数值位 两部分,符号位都是⽤ 0表⽰“正” , ⽤1表⽰“负 ”, 最⾼位的⼀位是被当做符号位 ,剩余的都是数值位. 正整数的原、反、补码都相同. 负整数的三种表⽰⽅法各不相同。(1): 原码:直接将数值按照正负数的形式翻译成⼆进制得到的就是原码。
(2): 反码: 符号位不变,其他位 依次按位取反就可以得到反码。
(3): 补码:反码+1就得到补码。
(1):在计算机系统中,数值⼀律⽤补码来表⽰和存储。原因在于: 使⽤补码,可以将符号位和数值域统⼀处理.(2): 同时,加法和减法也可以统⼀处理( CPU只有加法器 )此外,补码与原码相互转换,其运算过程是 相同的,不需要额外的硬件电路.这里我们举个几个简单的例子
3:大小端字节序和字节序判断
当我们了解了整数在内存中的存储方式后,接下来我们来通过调试看一个小细节.
通过调试,我们能够清晰地看到,在a中的 0x12345678 这个数字是按照字节为单位,倒着存储的。这是为什么呢?这里就要涉及到大小端存储啦
3.1什么是大小端
- 大端存储模式:是指数据的低位字节内容保存在内存的高地址处,而数据的高位字节内容,保存在内存的低地址处.
- 小端存储模式:是指数据的低位字节内容保存在内存的低地址处,而数据的高位字节内容,保存在内存的低地址处.
3.2:为什么会有大小端模式之分呢
- 在计算机系统中,是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8bit.但是在C语言中除了8bit的char类型数据外,还有16bit的short,32bit的int,32bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题.因此就导致了大端存储模式与小端存储模式.
-
一个 16bit 的 short 型 x , 在内存中的地址为 0x0010 , x 的值为 0x1122 ,那么 0x11 为高字节, 0x22 为低字节。对于 大端模式,就将 0x11 放在低地址 中,即 0x0010 中, 0x22 放在高地址 中,即 0x0011 中。 小端模式,刚好相反 。我们常用的 X86 结构是小端模式 ,而 KEIL C51 则为大端模式。很多的ARM , DSP 都为小端模式。有些 ARM 处理器还可以由硬件来选择是大端模式还是小端模式。
3.3:判断一个机器的字节序
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int check_sys()
{
int value = 1;
return (*(char*)&value & 1);
}
int main()
{
int result = check_sys();
if(result == 0)
{
printf("大端存储\n");
}
else
{
printf("小端存储\n");
}
}
4:相关练习
4.1:练习1
#define _CRT_SECURE_NO_WARNINGS
#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;
}
4.2:练习2
#include <stdio.h>
int main()
{
char a = -128;
printf("%u\n",a);
return 0;
}
4.3:练习3
#include <stdio.h>
int main()
{
char a = 128;
printf("%u\n",a);
return 0;
}
4.4:练习4
#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", strlen(a));
return 0;
}
4.5:练习5
#include <stdio.h>
unsigned char i = 0;
int main()
{
for(i = 0;i<=255;i++)
{
printf("hello world\n");
}
return 0;
}
上述代码呢,很明显,会发生死循环,那么是为什么呢?
4.6:练习6
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
unsigned int i;
for (i = 9; i >= 0; i--)
{
printf("%u\n", i);
}
return 0;
}
通过观察上面的结果,我们可以看到,此代码也会发生死循环,那么是为什么呢,有的uu会很奇怪,当i ==0时,此时再对其进行--,不应该是到 -1了吗,那么这是为啥呢?
5:浮点数在内存中的存储
常见的浮点数:3.14159、1E10等,浮点数家族包括:float、double、long double 类型.那么浮点数在内存中是如何存储的呢,我们首先来看下面这段代码.
5.1:代码1
#include <stdio.h>
int main()
{
int n = 9;
float* pFloat = (float*)&n;
printf("n的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
*pFloat = 9.0;
printf("num的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
return 0;
}
num和*pFloat明明在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这么大,这是为什么呢?这里就涉及到了浮点数在内存中的存储方式了.
5.2:浮点数存储规则
根据国际标准IEE754标准,任何一个二进制浮点数V可以表示成下面的形式.
- V = (-1)^S * M * 2 ^ E
- (-1)^S表示符号位,当S为0时,V为整数,当S为1时,V为负数.
- M表示有效数字,M是大于等于1,小于2的.
- 2^E表示指数位.
了解了浮点数的存储规则后,我们来看几个例子.
5.2.1:例子1
5.2.2:例子2
5.2.3:IEE754规定
对于32位的浮点数,最高的1位存储符号位S,接着的8位存储指数E,剩下的23位存储有效数字M.
5.3:浮点数存储的过程
IEE754对有效数字M和指数M,有一些特别的规定.
在上面有提到过,1 <= M < 2,那么也就是说,M可以写成1.xxxxxxx的形式,其中xxxxxxx表示小数部分.
IEE754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分.比如保存1.01的时候,只保存01,等到要对其进行读取的时候,再把第一位的加上去.这样子做的目的是为了节省一位有效数字.以64位浮点数为例,留给M的只有52位,将第一位的1加进来以后,等于可以保存53位有效数字.
至于指数E,情况就有些复杂.
首先,E为一个无符号整数(unsigned int)
这就意味着,若E为8位,那么它的取值范围为0~255;如果E为11位,它的取值范围为0~2047.但是,我们知道,科学计数法中的E是可以出现负数的,所以IEE754规定,存入内存E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023.例如,2^10的E是10,所以保存为32位浮点数时,必须保存成10 + 127 = 137.即10001001.
5.4:浮点数取的过程
指数E从内存中取出还可以再分成三种情况
5.4.1:E不全为0或不全为1
这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(32位浮点数)或1023(64位浮点数),得到真实值,再将有效数字M前加上第一位的1.
比如:0.5的二进制形式为0.1,由于规定正数部分必须为1,即将小数点右移1位,则为1.0*2^(-1),其阶码为-1 + 127 = 126,表示为01111110,而尾数1.0去掉整数部分为0,补齐0到23位,那么二级制表示形式则为
0 01111110 00000000000000000000000.
5.4.2:E为全0
这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第⼀位的1,而是还原为0.xxxxxx的⼩数。这样做是为了表示±0,以及接近于0的很小的数字。
5.4.3:E为全1
这个时候,如果有效数字M全为0,表示±无穷大.
6:题目解析
了解了浮点数的存储过程后,我们再回到最初的代码
#include <stdio.h>
int main()
{
int n = 9;
float* pFloat = (float*)&n;
printf("n的值为:%d\n", n);
9
/*
补码:000000000000000000000000000001001
从浮点数存储的角度出发
0(S) 00000000(E) 000000000000000000001001(M)
由于E为全0,那么此时E的真实值是1 - 127(32位) = -126
M = 0.000000000000000000001001
S = 0
因此V = (-1)^0 * 0.000000000000000000001001 * 2^-126;*/
printf("*pFloat的值为:%f\n", *pFloat);//无限接近于0
*pFloat = 9.0;
/*二进制序列:1001.0
V = (-1)^0 * 1.001 * 2^3;
S = 0, M = 1.001 , E = 3;
将其还原为二进制序列时,
首先E + 127(32位) = 130------->10000010
M去掉整数位后为001,然后后面补0,
因此二进制序列是
0(S) 10000010 (3 + 127---->E)001 0000 0000 0000 0000 0000*/
printf("num的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
return 0;
}
9
补码:000000000000000000000000000001001
从浮点数存储的角度出发
0(S) 00000000(E) 000000000000000000001001(M)
由于E为全0,那么此时E的真实值是1 - 127(32位) = -126
M = 0.000000000000000000001001
S = 0
因此V = (-1)^0 * 0.000000000000000000001001 * 2^-126-------->无线接近于0
9.0
二进制序列:1001.0
V = (-1)^0 * 1.001 * 2^3;
S = 0, M = 1.001 , E = 3;
将其还原为二进制序列时,
- 首先E + 127(32位) = 130------->10000010
- M去掉整数位后为001,然后后面补0,
因此二进制序列是
0(S) 10000010 (3 + 127---->E)001 0000 0000 0000 0000 0000
好啦,uu们,数据类型在内存中的存储这部分滴详细内容博主就讲到这里啦,如果uu们觉得博主讲的不错的话,请动动你们滴小手给博主点点赞,你们滴鼓励将成为博主源源不断滴动力,同时也欢迎大家来指正博主滴错误~