C语言——数据在内存中的存储
在 C 语言中,理解“数据到底是怎么存在内存里的”是非常核心的一环。这直接影响到你对指针、内存对齐、字节序、大小端、结构体布局、联合体、位域等很多重要概念的理解。
下面从最基础开始,一步步把 C 语言中各种数据类型在内存中的真实存储方式讲清楚。
1. 内存的基本概念(先搞清楚这几点)
- 内存是以字节(byte)为单位编址的
- 每个字节有唯一的地址(通常用十六进制表示)
- 现代计算机基本都是8 bit = 1 byte
- 内存地址通常按从小到大增长(低地址 → 高地址)
- 大多数系统采用小端序(little-endian),少数采用大端序(big-endian)
2. 基本数据类型在内存中的存储(以 64 位系统为例)
| 类型 | 常见大小(字节) | 对齐要求(典型) | 取值范围(有符号) | 内存表示特点 |
|---|---|---|---|---|
| char / signed char | 1 | 1 | -128 ~ 127 | 直接存 ASCII 或数值 |
| unsigned char | 1 | 1 | 0 ~ 255 | — |
| short | 2 | 2 | -32768 ~ 32767 | 按小端/大端存 2 字节 |
| unsigned short | 2 | 2 | 0 ~ 65535 | — |
| int | 4 | 4 | -2^31 ~ 2^31-1 | 4 字节整数 |
| unsigned int | 4 | 4 | 0 ~ 2^32-1 | — |
| long | 8(64位系统) | 8 | -2^63 ~ 2^63-1 | 视平台(32位系统可能是 4 字节) |
| long long | 8 | 8 | -2^63 ~ 2^63-1 | 固定 8 字节 |
| float | 4 | 4 | ≈ ±1.18e-38 ~ ±3.4e38 | IEEE 754 单精度浮点 |
| double | 8 | 8 | ≈ ±2.23e-308 ~ ±1.79e308 | IEEE 754 双精度浮点 |
| long double | 12/16(视平台) | 8/16 | 更高精度 | 视编译器与平台(x86 常用 80 位扩展精度) |
| 指针(*) | 4(32位)/ 8(64位) | 4/8 | — | 存储内存地址 |
3. 小端序 vs 大端序(最容易混淆的部分)
小端序(Little-Endian):低位字节存低地址(目前绝大多数 x86、x86_64、ARM 都是小端)
大端序(Big-Endian):高位字节存低地址(一些网络协议、PowerPC、SPARC 常用)
例子:int x = 0x12345678;
在内存中的存储(假设地址从 0x1000 开始):
| 地址 | 小端序(主流) | 大端序 |
|---|---|---|
| 0x1000 | 78 | 12 |
| 0x1001 | 56 | 34 |
| 0x1002 | 34 | 56 |
| 0x1003 | 12 | 78 |
怎么判断当前系统是小端还是大端?
#include <stdio.h>
int is_little_endian() {
int num = 1;
return *(char*)&num == 1; // 如果低地址存的是 01,则小端
}
int main() {
printf("是小端序吗? %s\n", is_little_endian() ? "是" : "否");
return 0;
}
4. 结构体在内存中的存储(内存对齐)
C 语言为了提高 CPU 访问效率,会对结构体成员进行内存对齐(padding)。
规则(常见编译器默认):
- 结构体整体大小 = 最后一个成员偏移 + 最后一个成员大小 + 填充字节
- 每个成员的起始地址 = 该成员大小的倍数(或结构体最大成员大小的倍数)
- 结构体总大小必须是结构体最大成员大小的倍数(padding 到对齐边界)
示例:
struct A {
char a; // 1 字节
int b; // 4 字节
short c; // 2 字节
};
struct B {
char a; // 1
short c; // 2
int b; // 4
};
在 64 位系统 + 4 字节对齐下的布局:
- struct A:1 + 3(填充) + 4 + 2 + 2(填充) = 12 字节
- struct B:1 + 1(填充) + 2 + 4 = 8 字节
如何关闭/控制对齐?
#pragma pack(1) // 1 字节对齐(无填充)
#pragma pack() // 恢复默认
__attribute__((packed)) // gcc/clang 方式
5. 共用体(union)在内存中的存储
共用体所有成员共享同一块内存,大小等于最大成员的大小。
union Data {
int i; // 4 字节
float f; // 4 字节
char c[8]; // 8 字节
};
union Data d;
d.i = 0x12345678;
printf("%x\n", d.c[0]); // 小端下可能是 78
6. 位域(Bit-field)的内存布局
struct Flags {
unsigned int f1 : 1;
unsigned int f2 : 3;
unsigned int f3 : 4;
int f4 : 8;
};
位域会尽量打包到同一个整数中,但:
- 可能产生填充
- 跨越不同整数时容易出错
- 地址不可取(& 操作符对位域无效)
7. 快速记忆总结
- char → 1 字节,原样存
- short/int/float → 2/4 字节,按小端存(主流)
- double/long long → 8 字节
- 结构体 → 有对齐填充,成员按声明顺序排列
- 共用体 → 所有成员重叠,大小取最大
- 指针 → 存地址(32位4字节,64位8字节)
- 内存对齐 → 为了 CPU 访问效率,宁可浪费空间
8. 动手验证建议
写下面这段代码,打印每个成员的地址和内容,看看内存布局:
#include <stdio.h>
struct Test {
char a;
int b;
short c;
double d;
char e;
};
int main() {
struct Test t = { 'A', 0x12345678, 0xABCD, 3.1415926535, 'Z' };
printf("地址\t\t成员\t值\t\t十六进制\n");
printf("%p\t a\t%c\t\t%x\n", &t.a, t.a, t.a);
printf("%p\t b\t%d\t\t%x\n", &t.b, t.b, t.b);
printf("%p\t c\t%d\t\t%x\n", &t.c, t.c, t.c);
printf("%p\t d\t%.3f\t\t%llx\n", &t.d, t.d, *(unsigned long long*)&t.d);
printf("%p\t e\t%c\t\t%x\n", &t.e, t.e, t.e);
printf("\n结构体总大小:%zu 字节\n", sizeof(t));
return 0;
}
运行后观察地址差值,就能直观看到填充字节和对齐。
如果你想深入某个部分(比如 IEEE 754 浮点精确表示、位域跨字节、结构体在不同编译器下的布局差异、内存对齐对性能的影响等),随时告诉我,可以继续展开。