【C 语言系统入门教程】第 19 讲:数据在内存中的存储 | 零基础学习笔记
前言
数据在内存中的存储方式是 C 语言最核心的底层知识之一,也是笔试面试的高频考点。
本讲深入讲解整数的原反补码存储、大小端字节序、浮点数的 IEEE 754 标准,通过经典笔试题解析,帮你彻底搞懂数据在内存中的二进制表示,解决 "为什么同样的二进制数据,不同类型解读结果天差地别" 的核心问题。
🎯 本讲学习目标
-
掌握整数的原码、反码、补码表示,理解为什么用补码存储。
-
理解大小端字节序的概念,能独立编写程序判断机器字节序。
-
掌握char/signed char/unsigned char的取值范围与溢出问题。
-
理解IEEE 754 浮点数存储标准,掌握 float 和 double 的内存结构。
-
能独立解析整数和浮点数存储相关的经典笔试题。
-
建立 "数据类型决定内存解读方式" 的核心思维。
📝 核心学习内容
1. 整数在内存中的存储
1.1 原码、反码、补码
有符号整数的二进制表示有三种形式:
-
原码:符号位 + 数值位,最高位 0 表示正,1 表示负。
-
反码:原码符号位不变,其余位按位取反。
-
补码:反码 + 1。
核心规则:
-
正整数:原码 = 反码 = 补码
-
负整数:原码≠反码≠补码
-
整数在内存中存储的是补码
1.2 为什么用补码存储?
-
统一符号位和数值位:符号位直接参与运算,无需额外硬件。
-
统一加法和减法:CPU 只有加法器,减法可以转换为补码加法。
-
补码与原码相互转换过程相同:取反 + 1,无需额外电路。
示例:计算 1-1
TypeScript
1-1 = 1+(-1)
1的补码:00000000 00000000 00000000 00000001
-1的补码:11111111 11111111 11111111 11111111
相加结果:00000000 00000000 00000000 00000000(正确)
2. 大小端字节序和字节序判断
2.1 什么是大小端?
超过 1 字节的数据在内存中存储时,存在字节顺序问题:
-
大端模式 :数据的高位字节 存放在内存的低地址,低位字节存放在高地址。
-
小端模式 :数据的低位字节 存放在内存的低地址,高位字节存放在高地址。
示例 :int a = 0x11223344
| 内存地址 | 大端模式 | 小端模式 |
|---|---|---|
| 低地址 | 0x11 | 0x44 |
| 0x22 | 0x33 | |
| 0x33 | 0x22 | |
| 高地址 | 0x44 | 0x11 |
大小端字节序存储
2.2 为什么有大小端?
-
硬件设计差异:不同处理器架构采用不同的字节序。
-
常见架构:X86 是小端模式,ARM、DSP 多为小端,KEIL C51 是大端。
2.3 判断当前机器字节序(百度笔试题)
方法 1:指针法
cpp
#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;
}
方法 2:联合体法
cpp
int check_sys()
{
union
{
int i;
char c;
}un;
un.i = 1;
return un.c;
}
2.4 经典整数存储笔试题解析
练习 1:char 类型取值范围
cpp
#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=-1, b=-1, c=255
解析:
-
char默认是signed char,取值范围 - 128~127。 -
-1的补码是11111111,unsigned char解读为 255。
练习 2:char 溢出问题
cpp
#include <stdio.h>
int main()
{
char a = -128;
printf("%u\n", a); // 输出4294967168
return 0;
}
解析:
-
-128的补码是10000000(char 类型)。 -
提升为 int 时,符号位扩展为
11111111 11111111 11111111 10000000。 -
按
%u(无符号)解读,结果为 4294967168。
练习 3:strlen 陷阱
cpp
#include <stdio.h>
#include <string.h>
int main()
{
char a[1000];
int i;
for(i=0; i<1000; i++)
{
a[i] = -1 - i;
}
printf("%d", strlen(a)); // 输出255
return 0;
}
解析:
-
char取值范围 - 128~127,当i=127时,a[127] = -128。 -
i=128时,a[128] = 127,直到i=255时,a[255] = 0。 -
strlen遇到'\0'(即 0)停止,所以长度为 255。
练习 4:unsigned char 死循环
cpp
#include <stdio.h>
int main()
{
unsigned char i = 0;
for(i=0; i<=255; i++)
{
printf("hello world\n");
}
return 0;
}
解析:
unsigned char取值范围 0~255,当i=255时,i++变为 0,循环永远成立。
3. 浮点数在内存中的存储
3.1 浮点数存储的疑问
cpp
#include <stdio.h>
int main()
{
int n = 9;
float *pFloat = (float *)&n;
printf("n的值为:%d\n", n); // 9
printf("*pFloat的值为:%f\n", *pFloat); // 0.000000
*pFloat = 9.0;
printf("n的值为:%d\n", n); // 1091567616
printf("*pFloat的值为:%f\n", *pFloat); // 9.000000
return 0;
}
为什么同样的二进制数据,整数和浮点数解读结果完全不同?
3.2 IEEE 754 浮点数标准
任意二进制浮点数 V 可以表示为:
V=(−1)S×M×2E
-
(−1)S:符号位,S=0 表示正数,S=1 表示负数。
-
M:有效数字,1≤M<2。
-
2E:指数位。
内存结构:
| 类型 | 符号位 S | 指数位 E | 有效数字位 M | 总位数 |
|---|---|---|---|---|
| float | 1 位 | 8 位 | 23 位 | 32 位 |
| double | 1 位 | 11 位 | 52 位 | 64 位 |
3.3 浮点数存储过程
-
有效数字 M:
-
标准形式为1.xxxxxx,存储时只保存小数部分
xxxxxx,省略整数部分的 1。 -
这样可以节省 1 位,提高精度。
-
-
指数 E:
-
E 是无符号整数,但科学计数法中 E 可以是负数。
-
存储时需要加上中间值:float 加 127,double 加 1023。
-
例如:E=3,存储为 3+127=130(二进制 10000010)。
-
3.4 浮点数读取过程
指数 E 分三种情况:
-
E 不全为 0 且不全为 1(常规情况):
-
真实指数 = 存储的 E - 127(或 1023)。
-
有效数字 M = 1 + 存储的小数部分。
-
-
E 全为 0:
-
真实指数 = 1 - 127(或 1023)。
-
有效数字 M = 0 + 存储的小数部分(不再加 1)。
-
用于表示 ±0 和接近 0 的极小值。
-
-
E 全为 1:
-
如果 M 全为 0,表示 ± 无穷大。
-
如果 M 不全为 0,表示 NaN(非数字)。
-
3.5 例题解析
问题 1:为什么 int 9 按 float 解读是 0.000000?
-
9 的二进制补码:
00000000 00000000 00000000 00001001 -
按 float 解读:
-
S=0,E=00000000,M=00000000000000000001001
-
E 全为 0,真实指数 = 1-127=-126
-
M=0.00000000000000000001001
-
V = (−1)0×0.00000000000000000001001×2−126≈1.001×2−146
-
这是一个极小的数,用 % f 打印显示为 0.000000。
-
问题 2:为什么 float 9.0 按 int 解读是 1091567616?
-
9.0 的二进制:
1001.0= 1.001×23 -
S=0,M=00100000000000000000000(省略 1),E=3+127=130(10000010)
-
二进制表示:
0 10000010 00100000000000000000000 -
按 int 解读:这个二进制数的十进制值就是 1091567616。
float类型浮点数内存分配
double类型浮点数内存分配
📝 课后习题
一、选择题
-
整数在内存中存储的是()
A. 原码
B. 反码
C. 补码
D. ASCII 码
-
小端模式下,
int a = 0x12345678,第一个字节的值是()A. 0x12
B. 0x34
C. 0x56
D. 0x78
-
signed char的取值范围是()A. 0~255
B. -127~127
C. -128~127
D. -256~255
-
float 类型中,指数位占多少位()
A. 1 位
B. 8 位
C. 23 位
D. 32 位
-
浮点数 9.0 的二进制科学计数法表示是()
A. 1.001×23
B. 10.01×22
C. 0.1001×24
D. 1.001×22
二、判断题
-
正整数的原码、反码、补码都相同。()
-
大端模式是低位字节存放在低地址。()
-
unsigned char i=255; i++;后 i 的值是 0。() -
浮点数可以精确表示所有小数。()
-
float 类型的有效数字精度比 double 高。()
三、编程题
-
编写程序,判断当前机器是大端还是小端模式。
-
分析以下代码的输出结果,并解释原因。
cpp
#include <stdio.h>
int main()
{
unsigned int a = 10;
int b = -20;
if(a + b > 0)
printf("a+b>0\n");
else
printf("a+b<=0\n");
return 0;
}
- 解释为什么
0.1 + 0.2 != 0.3。
📝 参考答案
一、选择题
-
C
-
D
-
C
-
B
-
A
二、判断题
-
√
-
×(小端模式)
-
√
-
×(部分小数无法精确表示)
-
×(double 精度更高)
三、编程题
1. 判断大小端(见正文)
2. 代码输出
cpp
a+b>0
解析:
-
a是 unsigned int,b是 int,运算时b会被转换为 unsigned int。 -
-20的补码作为 unsigned int 解读是一个很大的正数,10 + 4294967276 = 4294967286 > 0。
3. 0.1+0.2 问题
-
0.1 和 0.2 的二进制表示都是无限循环小数,无法精确存储。
-
浮点数存储时会截断,导致精度损失,所以相加结果不等于 0.3。
📝 本讲总结
-
整数存储:以补码形式存储,统一符号位和加减法运算。
-
大小端字节序:小端低位存低地址,大端高位存低地址,可通过指针或联合体判断。
-
char 类型:注意有符号和无符号的取值范围,溢出会导致循环。
-
浮点数存储:遵循 IEEE 754 标准,分为符号位、指数位、有效数字位。
-
核心思想 :内存中存储的都是二进制数据,数据类型决定了如何解读这些二进制。
-
本讲是 C 语言底层核心,彻底掌握才能写出健壮、高效的代码,应对各类笔试面试。
📌 版权说明
本文为 C 语言系统学习原创笔记,基于标准课件体系整理,纯干货零基础友好,未经允许禁止转载,如有错误欢迎评论区指正~
大小端字节序存储
float类型浮点数内存分配
double类型浮点数内存分配