数据在内存中的存储是编程的核心基础,本质是二进制位(bit)的有序排列------ 内存可视为 "字节数组",每个字节(8 bit)对应唯一的内存地址(如 0x00001000),数据存储本质是将其编码为二进制后,写入指定地址的字节中。
以下从「内存区域划分」「基本数据类型存储」「复合数据类型存储」「关键存储规则」四个维度,系统讲解数据在内存中的存储逻辑:
一、先明确:数据存在内存的哪个区域?
不同生命周期、不同类型的数据,会存储在内存的不同区域(以 C/C++ 为例,其他语言底层逻辑相通),核心区域划分如下:
| 内存区域 | 存储内容 | 生命周期 | 特点 |
|---|---|---|---|
| 栈(Stack) | 局部变量、函数参数、返回值 | 函数调用时创建,返回时销毁 | 自动分配释放,速度快,空间小(MB 级) |
| 堆(Heap) | 动态分配的数据(如 new/malloc) |
手动分配(new)/ 释放(delete) |
空间大(GB 级),需手动管理,易内存泄漏 |
| 静态区(全局区) | 全局变量、静态变量(static) |
程序启动时创建,退出时销毁 | 全局可见,默认初始化为 0 |
| 常量区(.rodata) | 字符串常量、const 常量 |
程序生命周期全程存在 | 只读(修改会触发崩溃) |
| 代码区(.text) | 程序指令(编译后的机器码) | 程序启动时加载 | 只读,按地址顺序执行 |
关键结论:数据的存储位置,直接决定其生命周期和访问权限(如常量区不可修改,栈数据自动释放)。
二、基本数据类型的存储(最基础,必须掌握)
基本数据类型(整型、浮点型、字符型)是 "值存储"------ 直接将数据的二进制编码写入内存,无需额外间接地址。
1. 整型存储:补码是核心(解决负数运算问题)
整型包括 char(1 字节)、short(2 字节)、int(4 字节)、long((32位)4/8(64位) 字节),分为有符号(signed) 和无符号(unsigned) 两类。
(1)无符号整型:直接存储二进制原码
无符号整型只有非负值,存储时直接将十进制数转为二进制原码,填充到对应字节中。
例:unsigned int a = 10(4 字节):
- 10 的二进制原码:
00000000 00000000 00000000 00001010 - 直接写入内存(地址从低到高或高到低,取决于大小端,后续详细解释大小端)。
(2)有符号整型:存储补码(核心!)
有符号整型需表示负数(最高位为符号位:0 = 正,1 = 负),为解决 "负数加法运算" 矛盾(如 1 + (-1) = 0),采用补码存储:
- 正数的补码 = 原码(与无符号一致);
- 负数的补码 = 反码 + 1(反码 = 原码符号位不变,其余位取反)。
例:int b = -10(4 字节):
- 原码:
10000000 00000000 00000000 00001010(符号位 1 表示负); - 反码:
11111111 11111111 11111111 11110101(符号位不变,其余位取反); - 补码:
11111111 11111111 11111111 11110110(反码 + 1);
- 最终内存中存储的是补码。(原因:在计算机系统中,数据一律用补码来表示和存储,原因在于,使用补码,可以将符号位和数值域统一处理,同时加法和减法也可以统一处理(cpu只有加法器),此外,补码与原码相互转换,其运算过程是相同的(解释:原码取反+1得到补码,补码取反+1得到原码),不需要额外的硬件电路)
(3)大小端问题:多字节数据的存储顺序
当数据占多个字节时(如 int 占 4 字节),会涉及 "字节在内存中的排列顺序",即大小端字节序:
- 大端(Big-endian):高位字节存低地址,低位字节存高地址(符合人类阅读习惯);
- 小端(Little-endian):低位字节存低地址,高位字节存高地址(x86 架构 CPU 主流)。
例:int c = 0x12345678(十六进制,对应 4 字节:0x12 高位,0x78 低位):
| 内存地址(低→高) | 小端存储(x86) | 大端存储(如 ARM 某些模式) |
|---|---|---|
| 0x00001000 | 0x78(低位) | 0x12(高位) |
| 0x00001001 | 0x56 | 0x34 |
| 0x00001002 | 0x34 | 0x56 |
| 0x00001003 | 0x12(高位) | 0x78(低位) |
判断大小端的代码技巧:用 union(共用体)或指针访问低地址字节,若为低位则是小端。
判断大小端的小程序:
cs
int s = 1;
if (*(char*)&s == 1) {
printf("小端\n");
}
else {
printf("大端\n");
}
代码解读:(我们暂时统一认为地址由低到高)
我们定义的s是int类型占据4个字节,我们知道1的16进制是0x00000001,其中高位是0x00,低位是0x01,设想一下:
小端架构:0x01,0x00,0x00,0x00
地址:低<------------------->高
大端架构:0x00,0x00,0x00,0x01
通过上述分析,我们就能知道在最第一个地址处大小端是不同的,所以只需要判断第一个地址是不是1就可以判断你的电脑中字节如何在内存中的排列顺序。
&s取的是低地址(第一个字节的地址);
(char*)&s指向这个低地址,解引用后获取的是第一个字节 ;
- 条件
*(char*)&s == 1成立 → 输出「小端」否则为「大端」。
这段代码的本质是:通过char*指针精准访问int变量的「首地址 1 字节」,判断该字节是否为int值的「低位字节」
它的优势在于:
- 高效:只涉及地址操作和 1 字节读取,无额外内存开销;
- 简洁:无需定义结构体 / 共用体(union),代码行数少;
- 通用:基于 C 语言的指针和强制类型转换特性,跨平台兼容(只要支持标准 C)。
补充:与共用体(union)判断法的对比
"用 union 判断大小端",原理和这段代码完全一致(都是访问首地址 1 字节),只是实现方式不同:
cs
// 共用体判断法(等价逻辑)
union EndianTest {
int i;
char c; // 共用体所有成员共享首地址,c对应首地址1字节
};
union EndianTest et;
et.i = 1;
if (et.c == 1) printf("小端\n");
else printf("大端\n");
两段代码的核心逻辑完全相同,只是前者用「指针强制转换」,后者用「共用体成员共享地址」,最终都是为了访问int变量的首地址 1 字节。