数据在内存中的存储
- 一、数据类型
-
- 1.内置类型及意义
- 2.类型的基本分类
-
- 2.1.整型
- 2.2.浮点型
- 2.3.构造类型
- 2.4.指针类型
- 2.5.空类型
- 二、整型在内存中的存储
-
- 1.原码、反码、补码
- 2.补码的意义
- 3.大小端字节序
- 4.练习题(营养鸡汤)
- 三、浮点型在内存中的存储
-
- 1.浮点数表示规定
- 2.浮点数存储规定
- 3.浮点数取出规定
一、数据类型 1.内置类型及意义 前面我们已经学习了基本的内置类型:
C语言数据类型字节数(32位系统环境下)
char1个字节
short2个字节
int4个字节
long4个字节(64位系统中是8个字节)
long long 8个字节
float4个字节
double8个字节
类型的意义:
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表)。
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只有加法器,简化运算器的结构、提高运算速度)。1.CPU只有加法器,计算
2.使用补码,可以将符号位和数值域位一处理。
3.补码与原码相互转换,其运算逻辑是相同的,不需要额外的硬件电路。
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
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
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
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
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
int main()
{
char a = 128;
printf("%u\n", a);
//4294967168
return 0;
}
做法和鸡汤2一样,不做多解析,可以自己尝试写
鸡汤4:
#include
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
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
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]3.浮点数取出规定 取出和存放是反过来的逻辑,即:
如果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
)
S照常取出,E取出后再减去中间值(127或1023),M取出后再前面"加"个指数E从内存中取出可以分成三种情况:1.
。
- 一般情况
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,说明是一个非常大的数,极限等于∞ 。表示± ∞的情况。
老规矩,举例子,上图:
文章图片
看到这里,我们都阔然开朗了!
这时,我们再回头解决最开始的引例:
文章图片
分析:
以整型视角放,以浮点型视角取:
文章图片
以浮点型视角放,以整理的视角取:
文章图片
内容比较多,一定要好好消化~
【C语言进阶|人人都看得懂的C语言进阶系列之数据存储】最后,各位老铁看了记得点赞关注评论哦,你的支持是我坚持的动力~
文章图片
推荐阅读
- C语言基础|【Visual Studio 2019】 实用调试技巧,学会了都说好
- C语言基础|【C语言入门必看】指针
- C语言基础|【C语言入门必看】结构体
- C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
- 《C陷阱与缺陷》读书笔记--第一章语法陷阱1
- C语言|C语言两种方法计算一个数所有位上的数的总和
- C语言|C语言实现简单计算器
- Kubernetes|Kubernetes 中的服务发现与负载均衡
- 蓝桥杯|蓝桥杯第十届C语言b组——B: 年号字串