目录
概要:本文主要讲述了数据在内存中的存储位置,存储方法以及如何读取内存中的数据。如果你对其有困惑,不妨好好阅读,也许会有新的体会和感悟。
数据在内存中的存储
1.数据类型介绍
(1)内置类型
整型家族
疑惑点:为什么char类型属于整型家族?
解释:这是因为char类型的数据在存储的时候,是以它的ASCLL值进行存储的,本质是一个整型。
浮点型家族
注意:浮点型都是有符号的。
(2)自定义类型
(3)指针类型
(4)空类型(void)
常用于表示函数不需要参数
例:
#include<stdio.h>
int main(void) {
printf("hello,world!");
return 0;
}
运行结果:
当然,这常适用于非主函数。
2.数据在内存中的存储
符号位:对于一个有符号的的数来说,最高位就是符号位,c语言中规定1表示负,0表示正。
《1》整型数据在内存中的存储
原码 ,反码 ,补码
原码
把一个十进制数直接转化为它的二进制,就是这个数的原码。
反码
正数:还是原码
负数:符号位不变,其他位按位取反
补码
正数:还是原码
负数:反码+1
我们要注意:
【1】
对于有符号整型数据:只有屏幕上打印的是原码,而在内存中进行存储和表示的都是补码。为什么?
序号 | 原因 |
---|---|
1. | 使用补码,可以将符号位和数值域统一处理 |
2. | 加减法也可以统一用加法处理(CPU只有加法器) |
凭什么这么说?
我们可以用一个简单的例子佐证我们的说法
例:写一个代码打印-1+1的结果
代码:
#include<stdio.h>
int main() {
printf("%d", -1 + 1);
return 0;
}
运行结果:
思考 | 讨论 |
---|---|
到这里,你可能会想:这不就理应如此吗? | 但事实上,这是我们c语言设计好的用补码进行运算的结果。不信?你看:如果我们用原码进行计算,我们会惊奇的发现发现:最后的打印结果应为-2。但是这就与常理违背了,-1 + 1 == -2 , 离之大谱! |
所以通过这个例子,我们就不难发现:对于有符号整型数据来说,只有屏幕上打印的是原码,而在内存中进行存储和表示的都是补码的智慧和原因。
【2】
序号 | 注意 |
---|---|
1. | 原码,反码,补码的概念主要针对有符号的整型家族(signed int或int)类型的数据,而对于其他类型的数据,有其相应的存储方式 |
2. | 特殊地:我们把无符号的整型家族可以看做正数(有符号的数)进行处理,只不过这个正数没有符号位 |
《2》整型数据的运算与打印
但是,掌握了以上的知识点之后,我们其实还并不能完全预见和理解整型数据运算屏幕上打印的结果,还得掌握一些占位符的作用,整型提升,数据截断,数据范围:
【1】
占位符
占位符 | 作用 |
---|---|
%d | 打印有符号整型数据 |
%u、%zd | 打印无符号整型数据 |
例:
#include<stdio.h>
int main() {
int a = 2147483647;
int b = 1;
printf("%d %u", a + b, a + b);
return 0;
}
运行结果:
思考:为什么同样是a + b,打印的结果确一正一负呢?
这里其实涉及了后文要讲的数据范围,但我们通过这个例子依旧是能够感受到占位符不同所带来的不同打印结果,该题我们在后文再进一步剖析。
【2】
数据范围
我们知道:
1.每个数据类型都有其对应的字节长度,1字节 == 8bit 位
详见:
数据类型 | 字节 |
---|---|
char | 1 |
short(int) | 2 |
(long) int | 4 |
float | 4 |
long long | 8 |
double | 8 |
那这些bit位是用来干嘛的呢?
2.实际上,这些bit位就是用来存放数字1或0来表示数据大小的。由此我们可以想到:那既然表示数据大小的位数是有限的,那每个类型的数据大小也应该是在一个范围内的。
详见:
数据类型 | 范围 |
---|---|
char | -128 ~ 127, 0 ~ 255 |
short(int) | - 2^15 ~ 2^15 - 1, 0 ~ 2^16 - 1 |
(long) int | - 2^31 ~ 2^31 - 1, 0 ~ 2^32 - 1 |
注:有负号的为有符号数据类型数据的范围
那如果一个数据超过了其范围,怎么计算?
3.我们可以用圆环的思想去考虑:
每一个无符号的数据类型从大到小都是bit位全0–>bit位全1,而到了最大的时候,也就是bit位全1,再加1就会发生数据越位,所有bit位变成全0,然后再由bit全0–>bit位全1,构成了一个头尾相交的圆环。
以unsigned char为例:
等价于:
而每一个符号的数据类型从大到小都是bit位全0–>bit位除了首位全1,而到了最大的时候,也就是bit位除了首位全1,再加1就会变成bit位除了首位全0,然后再变成bit位全1,再加1就会发生数据越位,所有bit位变成全0,然后再由bit全0–>bit位全1,构成了一个头尾相交的圆环。
但是和无符号数据类型不同的是:
有符号的数它的二进制位比无符号的数少了一位数值位,并且我们规定:1000…0000000表示的是最小的负数。
但我们应注意:二进制数存储和表示无论是正数还是负数都是采用补码的方式,而只是因为我们把无符号数据看做正数,原码和补码一样,且多了一位数值位才导致了表示和存储方式看起来不同,实际都是一样用补码的形式进行存储和表示
无符号和有符号数的异同 | 详述 |
---|---|
不同 | 无符号数多了一位数值位 |
相同 | 实际都是一样用补码的形式进行存储和表示 |
重要规定(有符号的数) | 1000…0000000表示的是最小的负数 |
以signed char为例:
等价于:
了解这些,我们就可以把上面的例子剖析
例子:
#include<stdio.h>
int main() {
int a = 2147483647;
int b = 1;
printf("%d %u", a + b, a + b);
return 0;
}
运行结果:
疑惑 | 剖析 |
---|---|
为什么用占位符%d打印的结果是负数? | (1)十进制:2147483648–>二进制(补码):01111111111111111111111111111111;十进制:1–>二进制(补码):00000000000000000000000000000001;相加得:10000000000000000000000000000000(补码)(2)又用%d进行打印,%d是用来打印int类型数据的,所以我们要把相加得的结果转换为原码打印出来(这里要用重要规定) |
为什么用占位符%u打印的结果是正数? | (1)同上;(2)又用%u进行打印,%u是用来打印无符号整数的,无符号整数的特别之处——就在于我们是把它看作有符号整数中的正数,就导致:它的原码和反码是一样的,所以打印在屏幕上的数就是相加结果的十进制数 |
对于占位符的思考 | 占位符给了我们一个理解和读取二进制数(补码)的视角和方式 |
【3】
整型提升
问题 | 答案 |
---|---|
什么叫整型提升? | c语言的整型算术运算总是至少缺省(默认)整型类型的精度进行计算。为了获得这个精度,表达式中字符型(char)和短整型(short)在使用之前就被系统自动转换为普通整型(int),这种转换就叫整型提升。 |
整型提升的意义是什么? | 表达式的整型运算要在CPU的加法器和其他运算器件中进行,而该运算器的操作数的字节长度就为int的字节长度。 |
数据提升的条件和对象 | 发生在小于整型类型的数据类型上。 |
提升规则
本质就是看原码的最高位是1还是0,是1补1,是0补0。
【4】
数据截断
问题 | 答案 |
---|---|
什么是数据截断? | 就是一个字节长度较小的数据类型在接收一个字节长度较大的数据类型时只会从字节长度较大的数据的数据低位开始接收,直至填满自己的所有二进制位 |
会有什么结果? | 可能会导致数据的丢失,一定会导致字节的减少 |
熟练掌握这些,我们就基本能预见和理解整型数据运算屏幕上打印的结果,下面就以一些例子来加深和巩固对以上知识的理解
例:
#include<stdio.h>
int main() {
char a = 3;
//00000011
char b = 127;
//01111111
char c = a + b;
//因a和b参加运算,所以要发生整型提升
//整型提升规则:高位补符号位(在大部分编译器中,char等价于signed char,即有符号,最高位为符号位,范围为:-128~127;无符号的char类型数据范围为:0~255;
//整型提升:
//00000000000000000000000000000011
//00000000000000000000000001111111
//结果00000000000000000000000010000000
//截断(发生在高字节类型转化为低字节):
//10000000 补码
//10000011
printf("%d", c);
//打印结果为:-126
return 0;
}
//也可以用圆环秒解:0 1 2 ~ 126 127 ~-1 -2 -3 ~ -127 -128 ~ 0 1 2 ~~
注释的解析很全,不用再过多解释,对照上面的知识点进行理解
《3》大小端介绍
(1)来历
这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8bit位。但是在c语言中除了8bit的char之外,还有16bit位的short型,32位的long型(取决于具体编译器);另外,对于位数大于8位的处理器(16bit或32bit),由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排进内存中存储的问题。由此产生了大端和小端。
(2)模式介绍及效果
模式(全称) | 效果 |
---|---|
大端字节序存储 | 数据的低位字节存放在高地址 |
小端字节序存储 | 数据的低位字节存放在低地址 |
实际效果:
自己模拟
VS2022上的内存窗口
(3)模式判断
我们可以借助一个2015年百度的笔试题观察大小端的区别
题目:写一个代码,实现大小端的判断
代码:
#include<stdio.h>
int main() {
int a = 1;
char* p = (char*)&a;
if (*p == 1) {
printf("是小端\n");
} else {
printf("是大端\n");
}
return 0;
}
思路:
序号 | 步骤 |
---|---|
1 | 用char*类型的指针一个一个字节地访问int类型变量a的各个字节 |
2 | 我们可以把a赋值为只有第一个字节有值的整数,这里赋值为1 |
3 | 前提:我们知道指针访问内存的习惯是从低地址到高地址 |
4 | 如果*p == 1,说明低地址存放的字节是低位字节,也就是小端存储 |
5 | 如果*p == 0,说明低地址存放的字节是高位字节,也就是大端存储 |
《4》浮点型数据在内存中的存储
引导:
我们先来一个题目激一激
题目——读出下列代码运行的结果
代码:
#include<stdio.h>
int main() {
int n = 9;
float* p = (float*) &n;
printf("%d\n", n);
printf("%f\n", *p);
*p = 9.0;
printf("%f\n", *p);
printf("%d\n", n);
return 0;
}
建议思考写下自己的答案再往后进行
运行结果:
在学习浮点型在内存中的存储之前,你是否只能准确预见第一行和第三行的输出情况?别担心,跟紧步伐,我们来学习浮点型在内存中的存储。
正文:
(1)十进制浮点数转换为标准存储的二进制浮点数
第一步:十进制浮点数转换为二进制浮点数
部分 | 转换方法 |
---|---|
整数部分 | 同十进制整数转换为二进制整数一样 |
小数部分 | 根据权重进行配凑,从小数点后第一位2^-1开始 |
本质 | 都是根据权重的大小不同进行配凑 |
第二步:二进制浮点数转换为标准储存的二进制浮点数
根据国际标准IEE(电气与电子工程协会)754,任意一个二进制浮点数可以表示成这样的形式:(-1)^S * M * (2)^E
元素 | 含义 |
---|---|
S | 表示符号位,为0则为正数,为1则为负数 |
M | 表示有效数字,大于等于1,小于2 |
E | 表示二进制下的指数位 |
例:
(2)标准存储的二进制浮点数的存储规则
第一部分:存储位置
元素 | 存储位置 |
---|---|
float | 32bit位 |
S | 存放于第1个bit位 |
M | 存放于第2个bit位~第9个bit位,8个bit位 |
E | 存放于第10个bit位~第32个bit位,23个bit位 |
double | 64bit位 |
S | 存放于第1个bit位 |
M | 存放于第2个bit位~第12个bit位,11个bit位 |
E | 存放于第个13bit位~第64个bit位,52个bit位 |
实际效果:
第二部分:存储的方式
元素 | 存储方式 |
---|---|
S | 无特殊,正常存 |
M | 因为总是满足1<=M<2,所以我们进行存储的时候,就不用把1存入,这样就节省了一位bit位,可存的M的范围就更大了 |
E | 较复杂,分问题和解决方案两个模块进行讲解 |
名称 | 内容 |
---|---|
问题 | 首先,E在内存中存储时是一个无符号整数,只有浮点数整体才是有符号的,但是我们知道在科学记数法中,E存在有负数的可能 |
解决方案 | 所以我们规定:给每个实际的E值加127(对于double是1023)再存入bit位中 |
以float为例:
通过以上的两个知识点,我们就能了解够浮点型在内存中是如何存储的,但存进去了,又该怎么读取呢?接下来就来介绍读取的方法
(3)浮点数的读取规则
第一部分:占位符
占位符 | 作用 |
---|---|
%f | 把一个数据的二进制位以单精度浮点数(float)的读取规则进行读取 |
%1f | 把一个数据的二进制位以双精度浮点数(double)的读取规则进行读取 |
第二部分:读取规则
以E的值划分为三种情况 | 读取规则 |
---|---|
E不为全0或不为全1 | (1)先把标准存储的二进制浮点数的形式读出来:S:是0真实值符号位就为1(正数),是1真实值符号位就为-1(负数);E:先把对应区域的二进制转换成十进制数,再-127(double类型则-1023)。M:直接把对应区域的二进制转换成十进制数,再在前面加上1和小数点;(2)最后:根据十进制浮点数转换为标准存储的二进制浮点数逆推回去得到真实值(十进制浮点数) |
E全为1 | 无穷 |
E全为0 | 无限趋近于0 |
学完了这些,我们再回头看看引导的那道题
题目——读出下列代码运行的结果
代码:
#include<stdio.h>
int main() {
int n = 9;
float* p = (float*) &n;
printf("%d\n", n);
printf("%f\n", *p);
*p = 9.0;
printf("%f\n", *p);
printf("%d\n", n);
return 0;
}
序号 | 分析 |
---|---|
1 int n = 9; | 首先我们创建了一个整型变量n,并为它分配了4个字节的内存空间 |
2 | 这4个字节,也就是32个bit位存放的二进制序列为:000000000000000000000000000001001(原码)000000000000000000000000000001001(补码) |
3 float* p = (float*) &n | 然后我们创建了一个浮点型指针变量float* p |
4 printf(“%d\n”, n); | 接着用了%d的占位符对整型变量n的二进制序列(补码)进行访问和打印:%d是将一个二进制数列(补码)当成有符号的整型变量,并转换为原码的十进制打印。类比到这里,就是把00000000000000000000000000001001(补码)按照有符号整型的变换规则变成00000000000000000000000000001001(原码),再转换(十进制)9进行打印 |
5 printf(“%f\n”, *p); | 再接着用了%f的占位符对整型变量n的二进制序列(补码)进行访问和打印:%f是将一个二进制数列(补码)当成单精度浮点数,并转换为原码的十进制打印。类比到这里,就是把00000000000000000000000000001001(补码)按照单精度浮点数的读取规则变成(-1)^0 * 1.00000000 * 2^(-118)(二进制浮点数的标准存储形式),再转换(十进制)0.0000000……进行打印 |
6 *p = 9.0; | 再然后使用了解引用操作符*,对n的地址中原存的二进制序列00000000000000000000000000001001(补码)以单精度存储规则进行了重新存储:先把我们要存的9.0(十进制浮点数)变成(-1)^0 * 1.001 * 2 ^ 2 (二进制浮点数的标准存储形式),然后再转换为00010000000000000000000000000010(补码) |
7 printf(“%f\n”, *p); | 再接着用了%f的占位符对现在的n中存储的二进制(上述补码)进行访问和打印:%f是将一个二进制数列(补码)当成单精度浮点数,并转换为原码的十进制打印。类比到这里,就是把00010000000000000000000000000010(补码)按照单精度浮点数的读取规则变成(-1)^0 * 1.001 * 2 ^ 2 (二进制浮点数的标准存储形式),,再转换(十进制)9.00000000……进行打印 |
8 printf(“%d\n”, n); | 最后用了%d的占位符对现在的n中存储的二进制(上述补码)进行访问和打印:%d是将一个二进制数列(补码)当成有符号的整型变量,并转换为原码的十进制打印。类比到这里,就是把00010000000000000000000000000010(补码)按照有符号整型的变换规则变成00010000000000000000000000000010(原码),再转换(十进制)1096517616进行打印 |
这样我们就得到了
总结
本文到这里就结束了,内容较多,看完一定要好好练习,多加体会!
如有错误,还请指正!感谢观看!