数据在内存中的存储
- 前言
- 一、数据类型介绍
- (一)基本概念
- (二)类型的基本归类
- 1.整型家族
- 2.浮点型家族
- 3.构造类型
- 4.指针类型
- 5.空类型
- 二、整形在内存中的存储
- (一)原码、反码、补码
- 1.概念
- 2.为什么内存中存的是补码
- (1)原因
- (2)例子
- (3)引例解释
- (二)大小端介绍
- 1.什么是大段小端
- 2.为什么有大端和小端
- 3.笔试练习题
- (1)题目描述
- (2)解题思路
- (3)代码
- (三)练习
- 1.signed char 和 unsigned char
- 2.打印%u
- 3.signed+unsigned
- 4.无符号死循环打印
- 5.strlen找字符串长度
- 6.unsigned范围
- 三、浮点型在内存中的存储
- (一)引例
- (二)浮点数存储规则
- 1.概念
- 2.举例
- 3.IEEE 754
- (1)通项
- (2)特别规定
- (i)对于存储M
- (ii)对于存储E
- (iii)对于取出E
- (3)综合举例
- (三)引例解释
- 1.第一部分解释
- 2.第二部分解释
- 总结
前言
当大家学完C语言的初阶,那肯定要去学习学习C语言进阶,在学习C语言进阶的时候,免不了要先了解一下数据在内存中是怎么存储的,在之前的博客中我也简单讲解了一下关于数据在内存中的存储,相信大家也已经了解了很多关于数据在内存中的存储,那接下来,我将细致地讲解一下关于数据在内存中的存储。
一、数据类型介绍
(一)基本概念
关于数据类型,我们已经学习了内置的学习类型如下:
类型的意义:
- 使用这个类型开辟内存空间的大小(大小决定了使用范围)。
- 如何看待内存空间的视角。
(二)类型的基本归类
1.整型家族
大家可以看,char也属于整型家族,因为我们之前说到过了,char在计算机内存中存储的是ASCII码的值,而ASCII码值为整数,所以char也属于整型家族。另外,除了char其他所有的整型家族的类型前面都是可以忽略signed的,因为这两者是相等的,而char是取决于编译器的,但大多数编译器都是char num; == signed char num;这是十分重要的。
2.浮点型家族
3.构造类型
也叫做自定义类型。可以根据程序员改变而改变的。
4.指针类型
5.空类型
二、整形在内存中的存储
在进行了解整型在内存中的存储中,先引一个例子:
这串代码中的a和b在计算机内存是如何存储的呢?我们继续往下看。
(一)原码、反码、补码
1.概念
计算机中的整数有三种2进制表示方法,即原码、反码和补码。
三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位
正数的原、反、补码都相同。
负整数的三种表示方法各不相同。
原码
直接将数值按照正负数的形式翻译成二进制就可以得到原码。
反码
将原码的符号位不变,其他位依次按位取反就可以得到反码。
补码
反码+1就得到补码。
大家也可以先去看看下面放的这篇博客,里面详细地讲解了原码、补码、反码的知识。
【C初阶】详解操作符
2.为什么内存中存的是补码
(1)原因
在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
(2)例子
根据原码、反码、补码的知识用以计算1-1的值:
(3)引例解释
我们可以看到对于a和b分别存储的是补码,那我们经过一系列的操作发现这怎么按照补码来看十六进制是0x00000014和0xfffffff6,而在内存中存的是倒过来的呢?这就涉及到了大端和小端的概念了。
(二)大小端介绍
1.什么是大段小端
大端(存储)模式:是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址
中;
小端(存储)模式:是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地
址中。
解释:当你有100块的时候,恰巧碰到一个机会,能够改变1 0 0这三位数字中的任何一位,那我们肯定想改变的是最左边的那位的数字,因为这位数字在最高位,所以从左往右是高字节到低字节。同样,大家也可以理解成我们通常所说的高八位和低八位的区别,高八位就是左边的八位数字,而低八位就是右边的数字。
我们看看VS2022编译器里是什么端存储的吧!
原来VS2022IDE编译器是小端存储呀!!!
接下来大家可以看一下函数的栈帧的创建与销毁,能更好的理解为什么VS2022IDE编译器是小端存储的,因为我们在栈区是需要进行压栈和出栈的。
函数栈帧创建与销毁
当我们进行压栈的时候,是把低字节先压进去,再压高字节;当进行出栈的时候,是把高字节拿出去,再拿低字节,这是由于栈区底下是封死的,而顶上是开放的,出栈的时候必须先拿出高地址的数据,再把低地址的数据拿出去。
小端存储和大端存储没有优劣之分,两者都是好把手!
2.为什么有大端和小端
这是因为在计算机系统中,我们是以字节为单位的,每个地址单元
都对应着一个字节,一个字节为8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。
例如:一个 16bit 的 short 型 x ,在内存中的地址为 0x0010 ,x 的值为 0x1122 ,那么 0x11为高字节, 0x22为低字节。对于大端模式,就将 0x11 放在低地址中,即 0x0010 中, 0x22 放在高地址中,即 0x0011(往后增加了一位地址,因为0x1122中的0x11和0x22正好差8个比特位,正好是一个字节) 中。小端模式,刚好相反。我们常用的 X86 结构是小端模式,而 KEIL C51 则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。
3.笔试练习题
(1)题目描述
请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。
(2)解题思路
概念很简单,就是前面所说的大小端的概念,大端是高字节存放在低地址处,低字节存放在高地址处;小端则相反,而难的是写一个代码判断当前机器的字节序。
那我们想的是利用内存去判断一下,那我们用1来试一下,因为1在内存中用16进制显示的是0x00 00 00 01,我们可以看出1的高字节和低字节是不一样的,那我们如果用小端的方式第一个字节输出的是0x01,而如果是大端的方式存储的话第一个字节输出的是0x00,如下图,那可是怎么取出第一个字节呢?很简单,取地址a,我们先取出int类型的a的首地址,然后再强制类型转换成加一只能跳过一个字节的char类型,然后再存放在一个char*的指针里面即可。
(3)代码
#include<stdio.h>
int check_sys(void) {
int a = 1;
char* p = (char*)&a;
if (*p == 1)//*p==0x01
return 1;
else //*p==0x00
return 0;
}
int main() {
int ret = check_sys();
if (ret == 1) {
printf("小端\n");
}
else {
printf("大端\n");
}
return 0;
}
(三)练习
大家在进行练习之前,强烈建议大家去看一看我之前的博客关于操作符的详解,我会放到下面链接。
1.signed char 和 unsigned char
【C初阶】详解操作符
int main()
{
char a = -1;
//-1 是整数 32bit位
//10000000000000000000000000000001 - 原码
//11111111111111111111111111111110 - 反码
//11111111111111111111111111111111 - 补码
//因为char只占8个bit位,所以发生截断,只保留低8位
//11111111 - a - 截断
//因为打印的是%d,是有符号的整数,所以要发生整型提升,按照符号位提升
//11111111111111111111111111111111 - 补码 - 提升
//10000000000000000000000000000001 - 原码
//-1
signed char b = -1;
//signed char 与 char是一样的
unsigned char c = -1;
//10000000000000000000000000000001 - 原码
//11111111111111111111111111111110 - 反码
//11111111111111111111111111111111 - 补码
//11111111 - c - 截断
//因为是unsigned char,最高的一位是看做有效位来进行计算的,不是符号位
//00000000000000000000000011111111 - 补码 - 整型提升
//00000000000000000000000011111111 - 原码
//255
printf("a=%d b=%d c=%d", a, b, c); //-1 -1 255
//%d是打印有符号的整数
return 0;
}
这题的注意事项是在进行完截断以后,我们进行整型提升的时候是要看原本这个数的类型进行整型提升的。
2.打印%u
#include <stdio.h>
int main()
{
char a = -128;
//10000000000000000000000010000000 - 原码
//11111111111111111111111101111111 - 反码
//11111111111111111111111110000000 - 补码
//截断,char只能保留8个比特位
//10000000 - a
//看a本身的类型是signed char,是有符号的char,那么就按照符号位进行整型提升
//11111111111111111111111110000000 - 整型提升
//因为%u打印的是无符号的整形,那么就不会把原本的补码看做是一个有符号的整型,而是看做一个无符号的整型,直接打印
//4294967168
printf("%u\n", a);
//%u打印的是无符号整形
char b = 128;
//00000000000000000000000010000000 - 原码、反码、补码
//因为char b只存8个bit位,所以只存低八位
//100000000 - 截断
//char 为有符号整型,所以根据符号位进行整型提升
//11111111111111111111111110000000 - 整型提升
//因为%u打印的是无符号的整形,那么就不会把原本的补码看做是一个有符号的整型,而是看做一个无符号的整型,直接打印
//4294967168
printf("%u\n", b);
//%u打印的是无符号整形
return 0;
}
这里要注意的是当我们看到要打印%u的时候,它是会打印一个无符号的整型,所以当我们进行整型提升的时候是不用看符号位的,直接根据它原本的值进行打印,而大家会有疑惑了,为什么不用看符号位直接打印,那它的补码形式怎么办?其实,在计算机内部它存的确实是二进制的补码,但是当我们要打印的是%u这个无符号整型的时候,是直接把原本的补码看做一个无符号的数了,那也就在%u打印的逻辑下不存在补码这回事,它仅仅是一个无符号的整型,而发声整型提升是要看这个数原本的类型的,如果是有符号,则根据符号进行整型提升,如果是无符号,则前面全部补0。
3.signed+unsigned
#include<stdio.h>
int main() {
int i = -20;
//10000000000000000000000000010100 - 原码
//11111111111111111111111111101011 - 反码
//11111111111111111111111111101100 - 补码
unsigned int j = 10;
//00000000000000000000000000001010 - 原码、补码、反码
printf("%d\n", i + j);
//%d是打印有符号整型
//i + j
//11111111111111111111111111110110 - 补码
//11111111111111111111111111110101 - -1
//10000000000000000000000000001010 - 原码
//-10
return 0;
}
4.无符号死循环打印
#include<stdio.h>
#include<windows.h>
int main() {
unsigned int i;//无符号整数,i永远大于等于0
for (i = 9; i >= 0; i--) //结束循环的条件为i>=0,而unsigned是永远大于等于0
{
printf("%u\n", i);
Sleep(1000);
}
return 0;
}
5.strlen找字符串长度
#include<stdio.h>
int main()
{
char a[1000];
int i;
for (i = 0; i < 1000; i++)
{
a[i] = -1 - i;
}
//在字符0之前已经相加等于0,128+127=255
//-1 -2 -3 -4 -5 …… -128 127 …… 5 4 3 2 1 0 -1 -2 ……
printf("%d", strlen(a));//strlen找'\0'才停止,而'\0'的ASCII码值就为0
return 0;
}
6.unsigned范围
#include <stdio.h>
unsigned char i = 0; //全局变量unsigned无符号,取值范围为0~255
int main()
{
for (i = 0; i <= 255; i++)//跳出循环条件恒成立
{
printf("hello world\n");
}
return 0;
}
三、浮点型在内存中的存储
常见的浮点数:
3.14159
1E10 = 1.0*10^10
浮点数家族包括: float、double、long double 类型。
浮点数表示的范围:float.h中定义
对于整型的取值范围:limits.h可以看到。
对于浮点型的取值范围:float.h可以看到。
大家可以打开everything并用VS code去查看,也可以看我下面的演示图片:
(一)引例
num 和 *pFloat 在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这么大?要理解这个结果,一定要搞懂浮点数在计算机内部的表示方法这数字有点不符合我们之前猜的数,这说明浮点型的存储方式与整型的存储方式是不一样的!那我们接下来继续往下看吧!
(二)浮点数存储规则
1.概念
2.举例
3.IEEE 754
(1)通项
IEEE 754规定:
对于32位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M。
对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。
(2)特别规定
(i)对于存储M
因为 1≤M<2 ,也就是说,M可以写成 1.xxxxxxx的形式,其中xxxxxxx表示小数部分。IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxxx部分。比如保存1.01的时候,只需要保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字。相当于节省了一定的空间。
(ii)对于存储E
首先,E为一个无符号整数(unsigned int)。
这意味着,如果E为8位,它的取值范围为0-255;如果E为11位,它的取值范围为0~2047。但是,我们知道,科学计数法中的E是可以出现负数的,所以IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127(2的7次-1);对于11位的E,这个中间数是1023(2的10次-1)。比如,2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。
(iii)对于取出E
(3)综合举例
(三)引例解释
#include<stdio.h>
int main()
{
int n = 9;
//00000000000000000000000000001001 - 原码、补码、反码
float* pFloat = (float*)&n;
//在*pFloat视角下是浮点型存储方式
//0 00000000 00000000000000000001001
//s e m
//v = (-1)^0 * 0.00000000000000000001001 * 2^(-126)
//0.000000
printf("n的值为:%d\n", n);//9
printf("*pFloat的值为:%f\n", *pFloat);//0.000000
*pFloat = 9.0;
//1001.0
//v = (-1)^0 * 1.001 * 2^3
//s=0
//e=3 3+127=130(十进制)=10000010
//m=1.001
//0 10000010 00100000000000000000000
//在n的视角下,上式为01000001000100000000000000000000为补码,正数原码补码反码一样
//所以n为1091567616
//在*pFloat的视角下:还原以后就是9.0
printf("num的值为:%d\n", n);//1091567616
printf("*pFloat的值为:%f\n", *pFloat);//9.0
return 0;
}
1.第一部分解释
根据E全为0的那个解释,可以知道这个数是一个很小的数,接近于0的数。
2.第二部分解释
n的视角下它是一个有符号的整型,是利用整型的存储模式进行计算的。
总结
关于数据在内存中的存储似乎不是那么难,我们讲解了关于数据类型的基本归类,也涉及到了大小端的概念,此外还讲解了关于浮点型在内存中的存储,这些都是未来能够进行更好的编译的一个很牢固的基础。