C语言基础:(十九)数据在内存中的存储
目录
前言
一、整数在内存中的存储
二、大小端字节序和字节序判断
2.1 什么是大小端?
2.2 为什么会有大小端?
2.3 练习题
2.3.1 练习1
2.3.2 练习2
2.3.3 练习3
三、浮点数在内存中的存储
3.1 练习题
3.2 浮点数的存储
3.2.1 浮点数存的过程
3.2.2 浮点数取的过程
(1)E不全为0或不全为1(常规情况)
(2)E全为0
(3)E全为1
3.3 题目解析
总结
前言
数据在内存中的存储是计算机科学中至关重要的基础概念,直接影响程序的性能、效率和正确性。从变量、数组到复杂的数据结构,每种类型的数据在内存中都有其独特的布局方式,涉及字节序、对齐、堆栈分配等底层机制。理解这些机制不仅能帮助开发者优化代码,还能避免内存泄漏、越界访问等常见问题。随着现代计算机体系结构的发展,内存管理技术也在不断演进,从静态分配到动态分配,再到虚拟内存和缓存优化,每一层抽象背后都隐藏着精妙的设计。本文将对数据在内存中的存储原理进行探讨,揭示不同类型数据的内存表示方式。下面就让我们正式开始吧!
一、整数在内存中的存储
在讲解操作符的时候,我们曾经介绍过以下内容:
整数的二进制表示方法有三种,即原码、反码、补码;
对于有符号的整数,这三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,最高位的一位是被当作符号位,剩余的都是数值位。
正整数的原码反码补码都相同,而负整数的三种形式各不相同。
原码:直接将数值按照正负数的形式翻译成⼆进制得到的就是原码。
反码:将原码的符号位不变,其他位依次按位取反就可以得到反码。
补码:反码+1就得到补码。
对于整型来说:数据存放在内存中其实存放的是二进制的补码。
为什么呢?
在计算机系统中,数值一律用补码来表示和存储。原因是,使用补码可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理(CPU只有加法器),此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
二、大小端字节序和字节序判断
当我们了解了整数在内存中的存储之后,我们来调试看一个细节:
#include <stdio.h>
int main()
{int a = 0x11223344;return 0;
}
调试的时候,我们可以看到在a中的0x11223344这个数字是按照字节为单位,倒着存储的,如下图所示。这是为什么呢?这就要涉及到我们先前粗略介绍过的知识 —— 大小端。
2.1 什么是大小端?
其实超过一个字节的数据在内存中存储的时候,就有存储顺序的问题,按照不同的存储顺序,我们可以分为大端字节序存储和小端字节序存储,下面是具体的概念:
- 大端(存储)模式:是指数据的低位字节内容保存在内存地址的高地址处,而数据的高位字节内容,保存在内存的低地址处。
- 小端(存储)模式:是指数据的低位字节内容保存在内存的低地址处,而数据的高位字节内容,保存在内存的高地址处。
我们需要牢记上述的概念,便于我们分辨大小端。
2.2 为什么会有大小端?
为什么会有大小端模式的区别呢?
这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8bit位,但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度是大于一个字节的,那么就必然存在着一个如何将多个字节安排的问题。因此就会导致大端存储模式和小端存储模式。
我们来举个例子:一个16bit的short型x,在内存中的地址位0x0010,x的值为0x1122,那么0x11为高字节,0x22为低字节。对于大端模式,就将0x11放在低地址,即0x0010中,0x22放在高地址,即0x0011中。对于小端模式则刚好相反,我们常用的x86结构就是小端模式,而KETL C51为大端模式。很多的ARM、DSP都为小端模式,有些ARM处理器甚至还可以由硬件来选择是大端模式还是小端模式。
2.3 练习题
2.3.1 练习1
请简述大端字节序和小端字节序的概念,并设计一个程序来判断当前机器的字节序。(百度笔试题)
我们先定义一个整型变量,如下图所示,将其转化为字符指针类型变量之后再解引用,如果当前机器为小端字节序,那么第一个字节存储的就是01,返回值就是1;如果为大端字节序,那么第一个字节存储的就是00,返回值就是0。
那么我们就可以设计出如下的程序:
//代码1
#include <stdio.h>
int check_sys()
{int i = 1;return (*(char *)&i);
} int main()
{int ret = check_sys();if(ret == 1){printf("⼩端\n");} else{printf("⼤端\n");} return 0;
}
我们也可以使用联合体union来设计程序,如下所示:
//代码2
int check_sys()
{union{int i;char c;}un;un.i = 1;return un.c;
}
2.3.2 练习2
#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;
}
首先我们来观察一下变量的类型:a虽然类型为char,但实际上默认为signed char;b的类型为signed char;c的类型为unsigned char。
那么signed char 和 unsigned char的取值范围是啥?查询资料后可以知道,signed char 的取值范围是 -128~127 ,unsigned char 的取值范围 是 0~255。
由于unsigned char 的范围是0到255,因此c被赋值-1会隐式转换为255(-1的补码表示0xFF被直接解释为无符号数255)。
由于printf中以%d形式输出,所以在输出时,char 和 signed char 会被提升为 int 类型,保持原值。因此最后程序的输出结果如下所示:
2.3.3 练习3
#include <stdio.h>
//X86环境 ⼩端字节序
int main()
{int a[4] = { 1, 2, 3, 4 };int *ptr1 = (int *)(&a + 1);int *ptr2 = (int *)((int)a + 1);printf("%x,%x", ptr1[-1], *ptr2);return 0;
}
注:在上述代码的printf中,%x表示以16进制的形式打印数据。
我们首先来分析一下数组a的内存布局,int占四个字节,且为小端字节序(数据的低字节存储在低地址)如下所示:
a[0] (1): 0x01 0x00 0x00 0x00 (低地址 -> 高地址)
a[1] (2): 0x02 0x00 0x00 0x00
a[2] (3): 0x03 0x00 0x00 0x00
a[3] (4): 0x04 0x00 0x00 0x00
接下来分析ptr1的计算:
&a表示整个数组的地址,其类型为 int(*)[4];&a + 1 会跳过整个数组(即4 * sizeof(int) = 16字节),它指向的是a[4](数组末尾的下一个位置);因此ptr1[-1]就等价于*(ptr1-1),即a[3],其值为4。
然后是ptr2的计算:
(int)a首先将数组首地址强制转化为int类型(假设为0x1000);(int)a + 1即为0x1001(即为a[0]的第2个字节);那么(int*)((int)a + 1)就将0x1001强制转换为 int* ,此时ptr2指向的就是a[0]的第2个字节;*ptr2会读取0x1001开始的4个字节(但由于ptr2未对齐,其行为是未定义的,但实际上在小端机器上可能得到特定值)。
如果我们假设a的起始地址是0x1000,那么对*ptr2的具体值分析如下:
- a[0]的存储:0x01 0x00 0x00 0x00(地址为0x1000-0x1003)。
- a[1]的存储:0x02 0x00 0x00 0x00(地址为0x1004-0x1007)。
- ptr2指向的是0x1001,读取四个字节:0x00 0x00 0x00 0x02(小端序解释为0x02000000)。
因此本题的输出值为:
4,2000000
三、浮点数在内存中的存储
我们常见的浮点数例如:3.14159、1E10等,实际上浮点数家族是一个大家族,它包括了float、double、long double类型。而浮点数表示的范围在 float .h 头文件中声明。
3.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("n的值为:%d\n",n);printf("*pFloat的值为:%f\n",*pFloat);return 0;
}
大家来想想上面的代码输出的结果会是什么?
结果如下:
3.2 浮点数的存储
在上面的代码中,n和*pFloat在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别如此之大?
要理解这个结果,我们就必须搞懂浮点数在计算机内部的表示方法。
根据国际标准IEEE(电气和电子工程协会)754,任意一个二进制浮点数V可以表示成下面的形式:
其中:
我们举例来说:
十进制的5.0,写成二进制为101.0,相当于是1.01×2^2。
那么按照上面V的格式,我们可以得出S=0,M=1.01,E=2。
十进制的-5.0,写成二进制是-101.0,相当于是-1.01×2^2.那么,S=1,M=1.01,E=2。
IEEE 754规定:
对于32位的浮点数(float),最高的1位为存储符号S,接着的8位为存储指数E,剩下的23位为存储有效数字M,内存分配图如下:
对于64位的浮点数(double),最高的1位为存储符号位S,接着的11位为存储指数E,剩下的52位为存储有效数字M,内存分配图如下:
表示符号位,当S=0时,V为整数;当S=1时,V为负数。
- M 表示有效数字,M是大于等于1,且小于等于2的数。
表示指数位。
3.2.1 浮点数存的过程
IEEE 754 对于有效数字M和指数E,还有一些特殊规定:
前面我们提到过,1≤M<2,这也就是说,M可以写成 1.xxxxxx 的形式,其中xxxxxx表示小数部分。
而IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。比如在保存1.01的时候,只保存01,等到读取的时候,就再把第一位的1加上去。这样做的目的,是为了节省1位有效数字。以32位浮点数为例,留给M的只有32位,我们将第一位的1舍去以后,等于就可以保存24位的有效数字了。
至于指数E,情况就比较复杂了:
首先,E为一个无符号整数(unsigned int)。这就意味着,如果E是8位,那么它的取值范围就是0~255;如果E是11位,那么它的取值范围就是0~2047。但是,我们知道,科学计数法中的E是可以出现负数的 ,所以IEEE 754就规定,在存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;而对于11位的E,这个中间数就是1023。比如,2^10的E是10,所以保存成32位的浮点数时,必须保存成10 + 127 = 137,即10001001。
3.2.2 浮点数取的过程
指数E从 内存中取出还可以再分成三种情况:
(1)E不全为0或不全为1(常规情况)
这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第一位的1。
比如:0.5的二进制形式为0.1,由于规定正数部分必须为1,即将小数点右移1位,则为1.0*2^(-1),其阶码为-1 + 127(中间值)= 126,表示为01111110,而尾数1.0去掉整数部分为0,补齐0到23位,即00000000000000000000000,则其二进制表示形式为:
0 01111110 00000000000000000000000
(2)E全为0
这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。二进制表示形式如下所示:
0 00000000 00100000000000000000000
(3)E全为1
这时,如果有效数字M全为0,那么表示 ±∞ (政府取决于符号位S)。二进制表示形式如下所示:
0 11111111 00010000000000000000000
3.3 题目解析
下面就让我们回到一开始的练习题。
先来看第一个环节,为什么 9 还原成浮点数,就成了0.000000?
9以整型的形式存储在内存中,得到如下二进制序列:
0000 0000 0000 0000 0000 0000 0000 1001
首先,将9的二进制序列按照浮点数的形式拆分,得到第一位符号位S=0,后面8位的指数E=00000000,最后23位的有效数字M=000 0000 0000 0000 0000 1001。
由于指数E全为0,所以符合E全为0的情况。因此浮点数V就写成:
V=(-1)^0 × 0.00000000000000000001001×2^(-126)=1.001×2^(-146)
显然V是一个很小的接近于0的正数,所以用十进制小数表示就是0.000000。
我们再来看第二环节,浮点数为9.0,为什么整数打印是1091567616?
首先,浮点数9.0等于二进制的1001.0,即换算成科学计数法为:1.001×2^3。所以:,那么,第一位的符号位S=0,有效数字M等于001后面再加上20个0,凑满23位,指数E等于3 + 127 = 130,即10000010。
所以,写成二进制形式,应该是S + E + M,即:
0 10000010 001 0000 0000 0000 0000 0000
这个32位的二进制数,被当作整数用来解析的时候,就是整数在内存中的补码,原码正是1091567616。
总结
以上就是本期关于数据在内存中的存储的全部内容了,希望本文能帮助大家更深入地理解数据存储的相关过程和概念。我们下期见!