数据在内存中的存储
一、数据类型
1.内置类型及意义
前面我们已经学习了基本的内置类型:
C语言数据类型字节数(32位系统环境下)
char 1个字节
short 2个字节
int 4个字节
long 4个字节(64位系统中是8个字节)
long long 8个字节
float 4个字节
double 8个字节
类型的意义:
1.清楚类型开辟内存空间的大小(int a;
变量a
开辟4个字节的空间)
2.由类型明确存的是什么(如int
存的是整数,float
存的是浮点数)
2.类型的基本分类
2.1.整型
char
signed char
unsigned char
short
signed short [int]
unsigned short [int]
int
signed int
unsigned int
long
signed long [int]
unsigned long [int]
注意:
[]
的内容可省略不写。signed int
和int
效果一样。所以不写signed
是默认为有符号的。特殊的是,char
类型并未规定是有符号还是无符号,但常见的编译器都会默认有符号char
被划分到整型。其内存占一个字节,signed char
的范围-128~127 ,unsigned char
的范围0 ~ 255(ASCII表)。
2.2.浮点型
float
double
2.3.构造类型
数组类型:
int[5]
char[10]
结构体类型:
struct type
{
v1;
v2;
};
枚举类型:
enum type { v1, v2, ...};
联合类型:
union type
{
v1;
v2;
};
数组也是有类型的,去掉数组名就是类型。不信我们来验证:
其他构造类型我们到自定义类型部分再来学习(关注我不迷路哦!)
2.4.指针类型
int *pi;
char *pc;
float* pf;
void* pv;
2.5.空类型
viod test(); //函数返回类型
viod test(viod); //函数参数
viod* p; //指针
二、整型在内存中的存储
一个变量的创建是要在内存中开辟空间。空间的大小由变量类型决定。那数据在所开辟内存中是如何存储的?
比如:
int a = 10;
int b = -20;
我们知道为a
开辟了4个字节的空间,那如何存储?
这就得学习下面原码、反码、补码的概念:
1.原码、反码、补码
- 注意:整数的存储涉及到原码、反码、补码的概念,这里只讨论整数的存储。
- 计算机中的整数有三种表示方法,即原码、反码和补码。
- 三种表示方法均有符号位和数值位两部分。
- 符号位(最高位):0表示正,1表示负
- 数值位(其他位):
下面是负整数的三种表示方法:
- 原码:按数据的数值写出二进制序列
- 反码:原码的符号位不变,其他位取反(0变为1,1变为0,攻受反转哈哈哈)
- 补码:反码+1
对于负整数, 反码、补码表示方式是人脑无法直观看出其数值的,通常需要转换成原码在计算其数值。
对于正数和无符号整数来说
- 原码、反码和补码相同。
干说没用,我们拿个例子来验证一下:
int a = -1;
10000000 00000000 00000000 00000001 - 原码
11111111 11111111 11111111 11111110 - 反码
11111111 11111111 11111111 11111111 - 补码
二进制1111
= 十进制15
=十六进制f
,即是 ff ff ff ff
到此,我们验证成功并发现整数在内存中存放的是补码
2.补码的意义
为什么在计算机系统中,整数数值一律用补码来表示和存储,原因有:
1.加法和减法也可以统一处理(CPU只有加法器,简化运算器的结构、提高运算速度)。
2.使用补码,可以将符号位和数值域位一处理。
3.补码与原码相互转换,其运算逻辑是相同的,不需要额外的硬件电路。
1.CPU只有加法器,计算1-1
时可转化为`1+(-1)
若以原码存储并计算
00000000 00000000 00000000 00000001 - 1的原码
10000000 00000000 00000000 00000001 - -1的原码
10000000 00000000 00000000 00000010 - -2的原码
小学生都知道1+(-1)=0,-2明显不符合结果
若以补码存储并计算
00000000 00000000 00000000 00000001 - 1的补码
11111111 11111111 11111111 11111111 - -1的补码
100000000000000000000000000000000000 - 0的补码 超过32位发生截断
正确
2.上述以补码存储并计算可以看出 符号位也参与计算,当作数值位统一处理。
3.这点用一张图就能明白:原码《==》补码
3.大小端字节序
管你什么大小端,来了都得一锅端,上图:
咱们可以看到十六进制数字 0x123456
在内存中恰好"反"过来,为什么会这样呢?这里就要说到大小端了
- 大小端字节序:以字节为单位,两种不同的计算机存储顺序。
大端字节序存储:
当一个数据的低位放到高地址处,数据的高位放到低地址处;
小端字节序存储:
当一个数据的低位放到低地址处,数据的高位放到高地址处。
这样的定义看着十分枯燥,老规矩,一张图搞定:
当我们先定好地址的顺序(如上图,从左往右,地址由低到高),大端模式是按照数字的书写顺序进行存储的,而小端模式是颠倒书写顺序进行存储的。 这样是不是就清晰很多啦!
啊哈哈哈哈哈哈鸡汤来咯!
下面是一道出自百度的面试题:
百度2015年系统工程师笔试题(10分):
请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。
分析:
- 我们先固定好 地址是由低到高,并定义一个变量
a = 1
- 若机器字节序为大端,变量a在内存中是
00 00 00 01
- 若机器字节序为小端,变量a在内存中是
01 00 00 00
- 不同点是低地址的字节,一个是
00
,一个是01
- 不同点是低地址的字节,一个是
- 如果获取到低地址字节的数据,问题就解决了。流氓做法:将变量a的地址强制类型转换成
char*
类型,再解引用,达到访问一个字节的目的
#include<stdio.h>
int check_sys()
{
int a = 1; //0x00 00 00 01
return (*(char*)&a);//返回0,大端;返回1,小端
}
int main()
{
int ret = check_sys();
if(0 == ret)
{
printf("大端\n");
}
else if(1 == ret)
{
printf("小端\n");
}
return 0;
}
哎呀,啧啧啧啧,不咸不淡,味道真是好极了!
4.练习题(营养鸡汤)
鸡汤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);//a=-1,b=-1,c=255
return 0;
}
解毒:
char a = -1;
11111111 11111111 11111111 11111111 - -1的补码
11111111 - 截断存入a
11111111 11111111 11111111 11111111 - 以%d打印 有符号数的整型提升 -1的补码
11111111 11111111 11111111 11111110 - -1的反码
10000000 00000000 00000000 00000001 - -1的原码
signed char b=-1;
- 同上
unsigned char c=-1;
11111111 11111111 11111111 11111111 -1的补码
11111111 - 截断存入a
00000000 00000000 00000000 11111111 以%d打印 无符号数的整型提升 255的补码
00000000 00000000 00000000 11111111 - 255的原码
存入时:
往a,b,c
存入的都是-1的补码,a,b,c
都是char
类型,只有一个字节的空间,则会发生截断,只存入了11111111
取出时:
1.a,b
是signed char
有符号数;c
是unsigned char
无符号数
2. 在%d(输出有符号整数)打印之前,因为a,b,c
都是char
类型,只有1个字节,达不到整型的4个字节,被迫整型提升。
- 整型提升:
- 有符号数:
- 最高位是1,补1(负数)
- 最高位是0,补0(正数)
- 无符号数:无论最高位是0或1,都是补0(无符号数是正数)
3. %d和%u有自己的原则:将得到的二进制序列按自己的格式输出
- %d输出十进制有符号整数:将二进制序列当成有符号数来打印
- %u输出十进制无符号整数:将二进制序列当成无符号数来打印
整型提升在操作符那章有讲到👉传送门👈
变式:
#include <stdio.h>
int main()
{
char a= -1;
signed char b=-1;
unsigned char c=-1;
printf("a=%u,b=%u,c=%u",a,b,c);//a=2^32-1,b=2^32-1,c=255
return 0;
}
解析:
char a= -1;
11111111 - 截断存入
11111111 11111111 11111111 11111111 - 整型提升并以%u打印 - 2^32-1
signed char b=-1;
11111111 - 截断存入
11111111 11111111 11111111 11111111 - 整型提升并以%u打印 - 2^32-1
unsigned char c=-1;
11111111 - 截断存入
00000000 00000000 00000000 11111111 - 整型提升并以%u打印 - 255
说白了就是,a,b,c
上刀山下火海,但%d
和%u
却只在乎你的结果(整型提升后的二进制序列),并按自己的原则做事,这也太现实了😂
类型只能决定字节大小和有无符号数,而%d,%u决定了如何使用该数据。
鸡汤2:
#include <stdio.h>
int main()
{
char a = -128;
printf("%u\n", a);
return 0;
}
解毒:
char a = -128;
10000000 00000000 00000000 10000000 - 原码
11111111 11111111 11111111 01111111 - 反码
11111111 11111111 11111111 10000000 - 补码
10000000 - 截断存入
11111111 11111111 11111111 10000000 - 整型提升并以%u打印
printf("%u\n", a);//4294967168
鸡汤3:
#include <stdio.h>
int main()
{
char a = 128;
printf("%u\n", a);//4294967168
return 0;
}
做法和鸡汤2一样,不做多解析,可以自己尝试写
鸡汤4:
#include<stdio.h>
int main()
{
int i = -20;
unsigned j = 10;
printf("%d\n", i + j);//输出-10
return 0;
}
解毒:
int i = -20;
10000000 00000000 00000000 00010010 - -20的原码
11111111 11111111 11111111 11101101 - -20的反码
11111111 11111111 11111111 11101110 - -20的补码
unsigned j = 10;
00000000 00000000 00000000 00001010 - 10的原反补
i + j 用补码运算,,结果还是补码
11111111 11111111 11111111 11110110 - i + j的补码
11111111 11111111 11111111 11110101 - i + j的反码
10000000 00000000 00000000 00001010 - i + j的原码 //-10
鸡汤5:
int main()
{
unsigned int i;//i任何值都是 >= 0的
for (i = 9; i >= 0; i--)//循环不会终止
{
printf("%u\n", i);
}
return 0;
}
解毒:
当 i = 0 时,i--后,i = -1
以%u打印-1,就是4294967295
鸡汤6:
int main()
{
char a[1000];
int i;
for (i = 0; i < 1000; i++)
{
a[i] = -1 - i;
}
printf("%d", strlen(a));//255
return 0;
}
解毒:
strlen()计算字符串从长度,直到空结束字符(即'\0'),但不包括空结束字符
而'\0'的ASCII值为0,即直到字符数组元素为0停止
我们知道signed char 类型是1个字节,8个bit,范围是[-128,127]
从上述循环中的a[i] = -1 - i; 可知数组元素依次为-1, -2, -3,...,-128,...,
-128之后是什么呢?是-129吗?并不是
为了更好的理解,继续上图:
10000000 - -128的补码 00000000 - 0的补码
补码-1 ↓ 补码-1 ↓
01111111 - 127的补码 11111111 - -1的补码
这便从负数回到正数了 这便从正数回到负数了
如此形成一个signed char 补码轮回图
按照上面做法,我们也可以推出signed short的补码轮回图。
鸡汤7:
#include <stdio.h>
unsigned char i = 0;
int main()
{
for (i = 0; i <= 255; i++)
{
printf("hello world\n");
}
return 0;
}
解毒:
unsigned char 类型变量范围是[0,255],255+1的补码会变成0的补码,也是一个轮回。
因此循环条件恒成立,是个死循环
到此,相信大家都喝个饱了,那就继续下一个内容。
三、浮点型在内存中的存储
浮点数如:3.14159,1E10等,在内存中也是以原反补的二进制序列的形式存储的吗?我们先看一个例子:
#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;
}
学习了上面的内容,我们可以做出分析:
但运行代码后,我了个乖乖!这是发什么甚么事啦
经过对比发现:
正确的是:(以整型视角放,以整型视角取)和(以浮点型视角放,以浮点型视角取)
错误的是:(以整型视角放,以浮点型视角取)和(以浮点型视角放,以整理的视角取)
如果放和取的视角不同便会出错,这说明了整型和浮点型的存储机制是不兼容的,是有区别的。 |
要理解这个结果,一定要搞懂浮点数在计算机内部的表示方法
经查阅资料知道:
1.浮点数表示规定
根据国际标准IEEE(电气和电子工程协会)754,任意一个二进制数V可以表示成以下形式
(-1)^S * M * 2^E
- 符号位
(-1)^S
:S=0时,V为正数,S=1时,V为负数。
S=0,(-1)^0^ =1
,表示正数
S=1,(-1)^1^ =-1
,表示负数
这点类似整数的表示
- 有效数位M:范围:[1,2)
类似科学计数法的有效部分
计算机是二进制机器,所以逢二进一
小数点前是2的正次方,小数点后是2的负次方
- 2^E表示指数位
类似科学计数法的指数部分
十进制进位是×10x
二进制进位是× 2E
概念比较复杂,我们举个例子就明白了:
比如十进制浮点数5.5,转化为二进制便是101.1
将有效部分和符号位带上,就是(-1)0 × 101.1 × 22 ,即S=0,M=1.011,E=2
2.浮点数存储规定
我们为什么要定义一套表示浮点数的逻辑呢?
通过这套逻辑,我们只需往内存放进S,M,E三个量即可,在有限的位数中尽量保存有用的值,利于扩大精度,也方便存储。
那这三个部分,分别占多少个比特位呢
IEEE 754规定:
对于32位浮点数float,最高的1位是符号位S,接着的8位是指数E,剩下的23位为有效数字M
对于64位的浮点数double,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M
最大化利用了内存并提高了精度
为了最大化的利用存储空间,S,M,E在存入时十分灵活:
- S:只有(-1)0 和(-1)1 两种情况,那存0或1就行
按照相同的逻辑取出就可,其他的位留给E和M用,更能提高精度。
- M的范围:[1,2),所以M必然等于
1.xxxx
,可只存小数部分xxxx
舍弃前面的1,只存小数点后面的
xxxx
,最大化的利用内存。
因为规定在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面xxxx
部分,比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省一位有效数字。(以32位浮点数为例,留给M的只有23位,将第1位的1舍去后,等于可以保存24位有效数字)
- E:E是一个无符号整数
如果E为 8位,它的取值范围:[0,255]
如果E为11位,它的取值范围:[0,2047]
我们知道,科学计数法中的E是可以出现负数的,如: 十进制的0.5要转换成二进制的0.1,再写成科学计数法就是1.0 × 2-1,则这里S = 0;M = 1.0;E = -1
所以规定,存入内存时E的真实值必须再加上一个中间数
对于 8位的E,这个中间数是127
对于11位的E,这个中间数是1023。
(比如:2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001
)
3.浮点数取出规定
取出和存放是反过来的逻辑,即:
S照常取出,E取出后再减去中间值(127或1023),M取出后再前面"加"个
1.
。
指数E从内存中取出可以分成三种情况:
-
一般情况
1.E不全为0或不全为1: E照常减去127或1023就行。
-
特殊情况(不要求深入了解)
- E为全0:
当E为全0(-127)时,说明E的真实值为-127(-127 + 127 =0)
一个数乘以2-127 说明是一个非常小的数,极限等于0。此时,直接取出M,在前面加上0.,得到0.x...xx
。
- E为全1:
当E为全1(255)时,说明E的真实值为128(128 + 127 =255)
一个数乘以2128,说明是一个非常大的数,极限等于∞ 。表示± ∞的情况。
说了这么多,相信你都已经蒙圈了,别问为什么,因为我就是这么过来的😂
老规矩,举例子,上图:
看到这里,我们都阔然开朗了!
这时,我们再回头解决最开始的引例:
分析:
以整型视角放,以浮点型视角取:
以浮点型视角放,以整理的视角取:
内容比较多,一定要好好消化~
最后,各位老铁看了记得点赞关注评论哦,你的支持是我坚持的动力~