C语言自学--数据在内存中的存储
目录
1、整数在内存中的存储
2. 大小端字节序和字节序判断
2.1、 什么是大小端?
2.2 、大小端模式的起源
2.3 练习
2.3.1 、练习1
2.3.2、练习2
2.3.3、练习3
2.3.4 、练习4
2.3.5、练习5
3. 浮点数在内存中的存储
3.1、练习1
3.2 浮点数的存储
3.2.1 浮点数存储过程
3.2.2 浮点数的取过程
1、整数在内存中的存储
关于整数的二进制表示方法,我们之前在学习操作符时已经介绍过三种形式:原码、反码和补码。这三种表示方法都具有以下共同特点:
- 表示结构由符号位和数值位组成
- 符号位统一规定:0表示"正",1表示"负"
- 数值位的最高位作为符号位,其余为实际数值位
具体特性如下:
- 正整数:原码、反码和补码表示完全相同
- 负整数:三种表示方法各有不同:
- 原码:直接将数值按正负数形式转换为二进制
- 反码:保持符号位不变,其余位按位取反
- 补码:在反码基础上加1得到
在计算机系统中,整形数据以补码形式存储在内存中,主要原因如下:
- 补码表示法能够统一处理符号位和数值域;
- 使用补码可以简化CPU设计,使加减法运算都能通过加法器实现;
- 补码与原码之间的转换运算过程相同,无需额外的硬件电路支持。
这种设计既优化了计算效率,又降低了硬件实现的复杂度。
2. 大小端字节序和字节序判断
在掌握了整数在内存中的存储方式后,我们来深入探讨一个调试细节:
#include <stdio.h>
int main()
{int a = 0x11223344;return 0;
}
在调试过程中,我们可以观察到变量a中存储的0x11223344这个数值是以字节为单位进行倒序存储的。
2.1、 什么是大小端?
当数据超过一个字节时,在内存中的存储顺序就变得重要。根据不同的存储顺序,可分为大端字节序和小端字节序两种模式:
- 大端存储模式:数据的高位字节存储在内存的低地址处,低位字节存储在内存的高地址处
- 小端存储模式:数据的低位字节存储在内存的低地址处,高位字节存储在内存的高地址处
掌握这两个概念有助于准确区分大小端模式。
2.2 、大小端模式的起源
大小端模式的出现源于计算机系统的基本存储特性。计算机以字节(8位)为基本寻址单位,但编程语言中存在多种数据宽度(如16位的short型、32位的long型)。当处理器位宽超过8位时(如16位或32位处理器),就必须解决多字节数据的存储顺序问题,这就产生了大小端两种存储模式。
举例说明: 假设一个16位的short型变量x,其内存地址从0x0010开始,高字节为0x11,低字节为0x22。在大端模式中,高字节0x11存储在低地址0x0010,低字节0x22存储在高地址0x0011,整体表示为0x1122。小端模式则完全相反。
典型应用:
- X86架构采用小端模式
- Keil C51编译器使用大端模式
- 多数ARM和DSP处理器采用小端模式
- 部分ARM处理器支持通过硬件配置选择大小端模式
2.3 练习
2.3.1 、练习1
题目:简述大端字节序和小端字节序的概念,并设计一个程序来判断当前机器的字节序。(10分)- 百度笔试题
解答:
-
概念:
- 大端字节序:数据的高字节存储在低地址,低字节存储在高地址。
- 小端字节序:数据的低字节存储在低地址,高字节存储在高地址。
-
判断程序(C语言实现):
#include <stdio.h>int check_endian() {int num = 1;char *ptr = (char *)#return (*ptr == 1) ? 0 : 1; // 0表示小端,1表示大端 }int main() {if (check_endian()) {printf("Big Endian\n");} else {printf("Little Endian\n");}return 0; }
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;}
2.3.3、练习3
#include <stdio.h>int main(){char a = -128;printf("%u\n",a);return 0;}
#include <stdio.h>int main(){char a = 128;printf("%u\n",a);return 0;}
这两段代码的输出结果相同,都会打印 4294967168
。以下是详细分析:
原因分析
char
类型在大多数系统中默认是 signed char
,范围是 -128
到 127
。当赋值为 128
或 -128
时,存储的二进制值相同(10000000
),但解释方式不同。
赋值 char a = 128
时,128
超出 signed char
范围,发生溢出,实际存储的是 -128
(二进制补码为 10000000
)。
赋值 char a = -128
时,直接存储 -128
的补码 10000000
。
输出解析
printf
使用 %u
格式化输出时,a
会被提升为 unsigned int
。对于 signed char
类型:
- 若
a
为负数(如-128
),提升为unsigned int
时会进行符号扩展,高位补1
。-128
的补码10000000
扩展为11111111 11111111 11111111 10000000
,即4294967168
。 - 若
a
为正数,高位补0
,但此例中a
实际为-128
,因此结果相同。
关键点
- 二进制补码存储:
128
和-128
在char
中存储的二进制相同。 - 整数提升:
char
在参与表达式或格式化输出时会被提升为int
(或unsigned int
)。 - 符号扩展:负数提升时会填充高位
1
,导致最终数值巨大。
2.3.4 、练习4
#include <stdio.h>
int main()
{char a[1000];int i;for (i = 0; i < 1000; i++){a[i] = -1 - i;}printf("%d", strlen(a));return 0;
}
字符数组赋值
char a[1000];
声明了一个长度为1000的字符数组。char
类型在大多数系统中占用1字节(8位),取值范围为-128到127(有符号)或0到255(无符号)。此处假设为有符号类型。
循环中,a[i] = -1 - i;
对数组进行赋值:
- 当
i=0
时,a[0] = -1
; - 当
i=1
时,a[1] = -2
; - 依此类推。
当i=127
时,a[127] = -128
(char
的最小值)。
当i=128
时,a[128] = -129
,超过了char
的表示范围,发生溢出。根据补码规则,-129的二进制表示为11111111 01111111
(假设16位),截取低8位为01111111
,即127。
继续递增时,a[129] = -130
,截断为126,依此类推,直到a[255] = -256
,截断为0。
字符串终止符
strlen
函数计算字符串长度时,从起始地址开始扫描,直到遇到第一个'\0'
(ASCII值为0)为止。根据上述赋值逻辑:
a[255] = 0
,因此strlen(a)
会在a[255]
处停止。- 前面的有效字符为
a[0]
到a[254]
,共255个字符。
输出结果
最终输出strlen(a)
的结果为255。这是因为strlen
统计的是'\0'
之前的字符数量。
关键点总结
char
类型的溢出行为导致赋值从-1递减到0。strlen
依赖'\0'
作为终止符,因此长度为255。
2.3.5、练习5
#include <stdio.h>
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;
}
代码分析
该代码涉及指针运算和数组内存布局,需要逐步拆解其行为。
数组初始化
int a[4] = { 1, 2, 3, 4 };
数组a
在内存中的布局(假设小端存储,int
为4字节):
地址偏移 | 字节内容(十六进制)
0x00 | 01 00 00 00 // a[0] = 1
0x04 | 02 00 00 00 // a[1] = 2
0x08 | 03 00 00 00 // a[2] = 3
0x0C | 04 00 00 00 // a[3] = 4
指针运算解析
int *ptr1 = (int *)(&a + 1);
&a
是数组指针,类型为int(*)[4]
。&a + 1
会跳过整个数组(16字节),指向数组末尾后的地址。ptr1[-1]
等价于*(ptr1 - 1)
,即从ptr1
回退4字节,指向a[3]
的地址,值为4
。
int *ptr2 = (int *)((int)a + 1);
(int)a
将数组首地址转为整数值。(int)a + 1
使地址增加1字节(非4字节),指向a[0]
的第二个字节。- 转换为
int*
后,*ptr2
会从该地址读取4字节(可能引发未对齐访问,实际行为依赖平台)。
假设小端机器: 从a[0]
的第二个字节开始读取4字节,会组合a[0]
的后3字节和a[1]
的第1字节:
原内存布局:01 00 00 00 02 00 00 00...
读取的4字节:00 00 00 02
小端解析为:0x02000000(十进制33554432)
输出结果
printf("%x,%x", ptr1[-1], *ptr2);
ptr1[-1]
输出4
的十六进制形式:4
。*ptr2
输出0x02000000
的十六进制形式:2000000
(具体值依赖字节序)。
最终答案
在小端机器上,程序输出为:
4,2000000
注意:若平台禁止未对齐访问或使用大端序,结果可能不同。
3. 浮点数在内存中的存储
常见的浮点数包括:3.14159、1E10等。
浮点数家族的具体表示范围可在标准头文件 float.h 中查看定义。
3.1、练习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("num的值为:% d\n",n);printf("*pFloat的值为:% f\n",*pFloat);
}
代码功能分析
该代码演示了C语言中指针类型转换对数据解释的影响,通过整型与浮点型的指针转换,展示同一内存空间在不同类型下的二进制解释差异。
关键代码解析
int n = 9;
float* pFloat = (float*)&n;
将整型变量n
的地址强制转换为浮点型指针,使同一内存空间可通过两种类型访问。由于整型和浮点型的二进制存储格式不同,会导致数据解释差异。
第一次输出结果
printf("n的值为:%d\n",n); // 输出整型值9
printf("*pFloat的值为:%f\n",*pFloat); // 输出将整型9的二进制解释为浮点数的结果
整型9
的二进制表示为00000000 00000000 00000000 00001001
,按IEEE 754浮点数标准解释时,会得到一个极小的非规格化数(约1.4e-45)。
第二次输出结果
*pFloat = 9.0;
printf("num的值为:%d\n",n); // 输出将浮点9.0的二进制解释为整数的结果
printf("*pFloat的值为:%f\n",*pFloat); // 正常输出浮点值9.0
浮点数9.0
的二进制表示为0 10000010 00100000 00000000 00000000
(十六进制0x41100000
),转为十进制整数值为1091567616
。
核心原理
- 类型双关(Type Punning):通过不同类型指针访问同一内存区域
- IEEE 754标准:浮点数采用符号位+指数位+尾数位的存储格式
- 整型存储:直接采用补码形式存储
典型运行结果:
n的值为:9
*pFloat的值为:0.000000
num的值为:1091567616
*pFloat的值为:9.000000
3.2 浮点数的存储
在上面的代码中,虽然num
和*pFloat
指向的是内存中的同一个数,但为何浮点数与整数的解读结果会有如此大的差异?要理解这一现象,关键在于掌握浮点数在计算机内部的表示方法。
根据国际标准IEEE 754,任意一个二进制浮点数V可以表示为以下形式:
V = (-1)^S × M × 2^E
其中:
- S表示符号位:当S=0时,V为正数;当S=1时,V为负数
- M表示有效数字,其取值范围为1 ≤ M < 2
- E表示指数位
举例说明:
- 十进制数5.0的二进制表示为101.0,相当于1.01×2^2。根据上述公式,可得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位浮点数:
- 最高1位存储符号位S
- 接下来的8位存储指数E
- 剩余的23位存储有效数字M
- 对于64位浮点数:
- 最高1位存储符号位S
- 接下来的11位存储指数E
- 剩余的52位存储有效数字M
3.2.1 浮点数存储过程
IEEE 754标准对有效数字M和指数E有特殊规定:
-
有效数字M的处理
根据规定,1≤M<2,因此M可以表示为1.xxxxxx的形式(xxxxxx为小数部分)。存储时,默认第一位总是1,故只需保存后面的xxxxxx部分。例如存储1.01时,仅保存01,读取时再补回第一位。这种设计可节省一位存储空间:以32位浮点数为例,实际23位存储空间通过此法可保存24位有效数字。 -
指数E的处理
E作为无符号整数,其存储需要特殊处理:- 8位E的取值范围:0~255
- 11位E的取值范围:0~2047
由于科学计数法允许负指数,IEEE 754规定存储时需将真实E值加上固定偏移量:
- 8位E的偏移量:127
- 11位E的偏移量:1023
例如:2^10的E=10,在32位浮点数中存储为10+127=137(二进制10001001)。
3.2.2 浮点数的取过程
指数E的提取可分为三种情况:
- 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,补齐23个0得到00000000000000000000000 最终二进制表示为:0 01111110 00000000000000000000000
0 01111110 00000000000000000000000
当E全为0时,浮点数的指数E实际值为1-127(或1-1023)。此时有效数字M不再需要补上首位的1,而是直接表示为0.xxxxxx的小数形式。这种设计是为了准确表示±0以及接近零的极小数值。
0 11111111 00010000000000000000000
3.3 题目解析
现在,让我们回到最初的练习题。
首先看第一个问题:为什么将整数9转换为浮点数后,结果却显示为0.000000?
这是由于整数9在内存中以整型形式存储时,对应的二进制序列为:
0000 0000 0000 0000 0000 0000 0000 1001
首先,将9的二进制序列按照浮点数格式进行拆分:
- 符号位s=0(1位)
- 指数E=00000000(8位)
- 有效数字M=00000000000000000001001(23位)
由于指数E全为0,符合特殊情况。因此浮点数V的计算公式为: V = (-1)^0 × 0.00000000000000000001001 × 2^(-126) = 1.001 × 2^(-146) 显然,V是一个极小的正数,接近于0,故十进制表示为0.000000。
接下来分析浮点数9.0:
- 二进制表示为1001.0
- 科学计数法形式:1.001 × 2^3
因此: 9.0 = (-1)^0 × (1.001) × 2^3
各组成部分:
- 符号位S=0
- 有效数字M=001(补全至23位)
- 指数E=3+127=130(二进制10000010)
最终二进制表示为S+E+M的组合。
0 10000010 001 0000 0000 0000 0000 0000
这个32位二进制数作为整数解析时,以补码形式存储在内存中,其原码对应值为1091567616。