1. 内存概述
1.1 什么是内存?
内存(Memory)是计算机的重要组成部分,用于存储正在运行的程序和数据。C语言中,每个变量都占用一定大小的内存空间,理解数据在内存中的存储方式对于写出高效、正确的程序至关重要。
1.2 内存的基本单位
单位 大小 说明
位(bit) 0或1 最小的存储单位
字节(byte) 8 bits 基本寻址单位
字(word) 4或8 bytes CPU处理的基本单位
2. 整数在内存中的存储
2.1 原码、反码、补码
计算机中整数以补码形式存储。理解三种编码方式至关重要:
#include <stdio.h>
* 以 8 位整数为例(char类型)
*
* 正数 5:
* 原码:0000 0101
* 反码:0000 0101(正数的反码与原码相同)
* 补码:0000 0101(正数的补码与原码相同)
*
* 负数 -5:
* 原码:1000 0101(最高位为符号位,1表示负数)
* 反码:1111 1010(符号位不变,其余位取反)
* 补码:1111 1011(反码+1)
*/
void printBinary(unsigned char num) {
for (int i = 7; i >= 0; i--) {
printf("%d", (num >> i) & 1);
if (i == 4) printf(" ");
}
}
int main() {
printf("===== 整数存储形式详解 =====\n\n");
// 正数存储
signed char positive = 5;
printf("正数 %d 的存储形式:\n", positive);
printf(" 二进制:");
printBinary(positive);
printf("\n 说明:正数的原码、反码、补码相同\n\n");
// 负数存储
signed char negative = -5;
printf("负数 %d 的存储形式:\n", negative);
printf(" 二进制:");
printBinary(negative);
printf("\n 这是 -5 的补码形式\n");
printf(" 原码:1000 0101\n");
printf(" 反码:1111 1010\n");
printf(" 补码:1111 1011\n\n");
// 验证:补码的补码是原码
printf("验证:补码的补码是原码\n");
unsigned char complement = 0xFB; // 1111 1011
printf(" 补码 1111 1011 表示:%d\n", (signed char)complement);
return 0;
}
2.2 不同整数类型的存储范围
#include <stdio.h>
#include <limits.h>
int main() {
printf("===== 整数类型存储范围 =====\n\n");
// char 类型
printf("char 类型(1字节,8位):\n");
printf(" unsigned char: 0 ~ %u\n", UCHAR_MAX);
printf(" signed char: %d ~ %d\n", SCHAR_MIN, SCHAR_MAX);
printf(" char: 取决于编译器\n\n");
// short 类型
printf("short 类型(2字节,16位):\n");
printf(" unsigned short: 0 ~ %u\n", USHRT_MAX);
printf(" signed short: %d ~ %d\n\n", SHRT_MIN, SHRT_MAX);
// int 类型
printf("int 类型(4字节,32位):\n");
printf(" unsigned int: 0 ~ %u\n", UINT_MAX);
printf(" signed int: %d ~ %d\n\n", INT_MIN, INT_MAX);
// 演示溢出
printf("===== 整数溢出演示 =====\n\n");
unsigned char uc = 255;
printf("unsigned char uc = 255\n");
printf("uc + 1 = %d (溢出回绕到0)\n", uc + 1);
signed char sc = 127;
printf("\nsigned char sc = 127\n");
printf("sc + 1 = %d (溢出到负数)\n", sc + 1);
return 0;
}
2.3 大小端字节序
#include <stdio.h>
/*
* 大端模式(Big-endian):高位字节存储在低地址
* 小端模式(Little-endian):低位字节存储在低地址
*
* 例如整数 0x12345678 在内存中的存储:
*
* 地址: 0x100 0x101 0x102 0x103
* 大端: 12 34 56 78
* 小端: 78 56 34 12
*/
// 检测当前系统的大小端模式
void checkEndianness() {
int num = 0x12345678;
unsigned char *ptr = (unsigned char*)#
printf("整数 0x12345678 在内存中的存储:\n");
printf("地址\t\t值\n");
for (int i = 0; i < sizeof(num); i++) {
printf("%p\t0x%02x\n", ptr + i, ptr[i]);
}
if (ptr[0] == 0x78) {
printf("\n当前系统是小端模式(Little-endian)\n");
} else {
printf("\n当前系统是大端模式(Big-endian)\n");
}
}
// 大小端转换
unsigned int swapEndian(unsigned int num) {
return ((num >> 24) & 0xFF) | // 最高字节移到最低
((num >> 8) & 0xFF00) | // 次高字节移到次低
((num << 8) & 0xFF0000) | // 次低字节移到次高
((num << 24) & 0xFF000000); // 最低字节移到最高
}
int main() {
printf("===== 大小端字节序详解 =====\n\n");
checkEndianness();
printf("\n===== 大小端转换演示 =====\n\n");
unsigned int original = 0x12345678;
unsigned int converted = swapEndian(original);
printf("原始值:0x%08x\n", original);
printf("转换后:0x%08x\n", converted);
return 0;
}
3. 浮点数在内存中的存储
3.1 IEEE 754 标准
浮点数遵循 IEEE 754 标准,以科学计数法形式存储:
#include <stdio.h>
/*
* 单精度浮点数(float,32位):
* | 符号位(1) | 指数位(8) | 尾数位(23) |
*
* 双精度浮点数(double,64位):
* | 符号位(1) | 指数位(11) | 尾数位(52) |
*
* 计算公式:(-1)^S × (1.M) × 2^(E-127)
*/
// 打印 float 的二进制表示
void printFloatBinary(float f) {
unsigned int *ptr = (unsigned int*)&f;
unsigned int num = *ptr;
// 符号位
int sign = (num >> 31) & 1;
// 指数位
int exponent = (num >> 23) & 0xFF;
// 尾数位
unsigned int mantissa = num & 0x7FFFFF;
printf("符号位:%d\n", sign);
printf("指数位:%d (0x%02X)\n", exponent, exponent);
printf("尾数位:0x%06X\n", mantissa);
printf("\n完整二进制:");
for (int i = 31; i >= 0; i--) {
printf("%d", (num >> i) & 1);
if (i == 31 || i == 23) printf(" ");
}
printf("\n");
}
int main() {
printf("===== 浮点数存储详解 =====\n\n");
float f = 3.14f;
printf("浮点数 %.2f 的内存表示:\n", f);
printFloatBinary(f);
// 特殊值
printf("\n===== 特殊浮点数 =====\n\n");
float infinity = 1.0f / 0.0f;
printf("正无穷大:%f\n", infinity);
float nan = 0.0f / 0.0f;
printf("NaN(非数字):%f\n", nan);
// 精度问题演示
printf("\n===== 浮点数精度问题 =====\n\n");
float a = 0.1f;
float b = 0.2f;
float c = a + b;
printf("0.1 + 0.2 = %.20f\n", c);
printf("理论上应该是 0.3,但由于二进制表示不精确,产生了微小误差\n");
// 比较浮点数
if (c == 0.3f) {
printf("相等\n");
} else {
printf("不相等!误差:%.20f\n", c - 0.3f);
}
// 正确的浮点数比较方法
float epsilon = 1e-6;
if (fabs(c - 0.3f) < epsilon) {
printf("使用容差比较:相等\n");
}
return 0;
}
3.2 浮点数与整数的转换
#include <stdio.h>
#include <string.h>
// 查看浮点数的十六进制表示
void inspectFloat(float f) {
unsigned int hex;
memcpy(&hex, &f, sizeof(float));
printf("%f 的十六进制:0x%08X\n", f, hex);
}
int main() {
printf("===== 浮点数与整数转换 =====\n\n");
float f1 = 1.0f;
float f2 = 2.0f;
float f3 = 0.5f;
printf("常见浮点数的内存表示:\n");
inspectFloat(f1);
inspectFloat(f2);
inspectFloat(f3);
// 整数和浮点数的位模式对比
printf("\n===== 整数和浮点数的位模式对比 =====\n\n");
int i = 1;
float f = 1.0f;
printf("整数 1 的位模式:");
for (int j = 31; j >= 0; j--) {
printf("%d", (i >> j) & 1);
if (j == 31 || j == 23) printf(" ");
}
printf("\n");
printf("浮点数 1.0 的位模式:");
unsigned int *ptr = (unsigned int*)&f;
for (int j = 31; j >= 0; j--) {
printf("%d", (*ptr >> j) & 1);
if (j == 31 || j == 23) printf(" ");
}
printf("\n");
printf("说明:即使数值相同,整数和浮点数的二进制表示完全不同!\n");
return 0;
}
4. 指针与内存地址
4.1 指针的本质
指针是一个变量,存储的是内存地址。
#include <stdio.h>
int main() {
printf("===== 指针与内存地址 =====\n\n");
int a = 10;
int *p = &a;
printf("变量 a 的值:%d\n", a);
printf("变量 a 的地址:%p\n", &a);
printf("指针 p 的值(存储的地址):%p\n", p);
printf("指针 p 的地址:%p\n", &p);
printf("通过指针访问的值:%d\n", *p);
printf("\n===== 不同类型指针的大小 =====\n\n");
printf("char* 大小:%zu 字节\n", sizeof(char*));
printf("int* 大小:%zu 字节\n", sizeof(int*));
printf("double* 大小:%zu 字节\n", sizeof(double*));
printf("void* 大小:%zu 字节\n", sizeof(void*));
printf("\n所有指针类型在相同平台下大小相同\n");
return 0;
}
4.2 指针运算与内存布局
#include <stdio.h>
int main() {
printf("===== 指针运算与内存布局 =====\n\n");
int arr[] = {10, 20, 30, 40, 50};
int *p = arr;
printf("数组地址:\n");
for (int i = 0; i < 5; i++) {
printf("&arr[%d] = %p, arr[%d] = %d\n", i, &arr[i], i, arr[i]);
}
printf("\n指针运算:\n");
printf("p = %p, *p = %d\n", p, *p);
printf("p + 1 = %p, *(p + 1) = %d\n", p + 1, *(p + 1));
printf("p + 2 = %p, *(p + 2) = %d\n", p + 2, *(p + 2));
printf("\n注意:p + 1 跳过了 %d 字节(一个int的大小)\n", sizeof(int));
return 0;
}
5. 结构体的内存对齐
5.1 内存对齐规则
#include <stdio.h>
#include <stddef.h>
/*
* 内存对齐规则:
* 1. 第一个成员在偏移量为0的位置
* 2. 每个成员要对齐到其类型大小的整数倍
* 3. 结构体总大小是最大成员大小的整数倍
*/
// 演示不同定义顺序导致的内存大小差异
struct StructA {
char c; // 1字节
int i; // 4字节,需要对齐到4的倍数
short s; // 2字节
};
// 内存布局:c(1) + 填充(3) + i(4) + s(2) + 填充(2) = 12字节
struct StructB {
int i; // 4字节
char c; // 1字节
short s; // 2字节,需要对齐到2的倍数
};
// 内存布局:i(4) + c(1) + 填充(1) + s(2) = 8字节
struct StructC {
char c1; // 1字节
short s; // 2字节,需要对齐到2的倍数
int i; // 4字节,需要对齐到4的倍数
char c2; // 1字节
};
// 内存布局:c1(1) + 填充(1) + s(2) + i(4) + c2(1) + 填充(3) = 12字节
int main() {
printf("===== 结构体内存对齐 =====\n\n");
printf("struct A 大小:%zu 字节\n", sizeof(struct StructA));
printf("struct B 大小:%zu 字节\n", sizeof(struct StructB));
printf("struct C 大小:%zu 字节\n", sizeof(struct StructC));
printf("\n说明:相同成员,不同顺序,占用内存不同!\n");
printf("建议:将占用空间大的成员放在前面,节省内存\n");
printf("\n===== 查看成员偏移量 =====\n\n");
printf("struct A:\n");
printf(" c 偏移量:%zu\n", offsetof(struct StructA, c));
printf(" i 偏移量:%zu\n", offsetof(struct StructA, i));
printf(" s 偏移量:%zu\n", offsetof(struct StructA, s));
printf("\nstruct B:\n");
printf(" i 偏移量:%zu\n", offsetof(struct StructB, i));
printf(" c 偏移量:%zu\n", offsetof(struct StructB, c));
printf(" s 偏移量:%zu\n", offsetof(struct StructB, s));
return 0;
}
5.2 强制对齐和紧凑结构
#include <stdio.h>
// 使用 pragma pack 强制紧凑排列
#pragma pack(push, 1) // 按1字节对齐
struct PackedStruct {
char c;
int i;
short s;
};
#pragma pack(pop)
// 使用 attribute 指定对齐(GCC)
#ifdef __GNUC__
struct AlignedStruct {
char c;
int i;
short s;
} __attribute__((aligned(8))); // 8字节对齐
#endif
int main() {
printf("===== 对齐控制 =====\n\n");
printf("正常结构体大小:%zu 字节\n", sizeof(struct StructA));
printf("紧凑结构体大小:%zu 字节(使用 #pragma pack(1))\n",
sizeof(struct PackedStruct));
#ifdef __GNUC__
printf("指定8字节对齐:%zu 字节\n", sizeof(struct AlignedStruct));
#endif
printf("\n紧凑结构体的成员布局:\n");
printf("c 偏移量:%zu\n", offsetof(struct PackedStruct, c));
printf("i 偏移量:%zu\n", offsetof(struct PackedStruct, i));
printf("s 偏移量:%zu\n", offsetof(struct PackedStruct, s));
return 0;
}
6. 动态内存分配
6.1 堆内存的分配与释放
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
printf("===== 动态内存分配 =====\n\n");
// malloc - 分配内存(不初始化)
int *p1 = (int*)malloc(5 * sizeof(int));
if (p1 == NULL) {
printf("内存分配失败\n");
return 1;
}
printf("malloc: 分配了 5 个 int 的空间,地址:%p\n", p1);
// calloc - 分配并初始化为0
int *p2 = (int*)calloc(5, sizeof(int));
printf("calloc: 分配并清零,前几个值:%d, %d, %d\n", p2[0], p2[1], p2[2]);
// realloc - 重新调整大小
int *p3 = (int*)realloc(p1, 10 * sizeof(int));
if (p3 != NULL) {
printf("realloc: 扩展到 10 个 int,新地址:%p\n", p3);
p1 = p3;
}
// 释放内存
free(p1);
free(p2);
printf("\n内存已释放\n");
return 0;
}
6.2 常见错误示例
#include <stdio.h>
#include <stdlib.h>
// 错误1:内存泄漏
void memoryLeak() {
int *p = (int*)malloc(sizeof(int));
*p = 10;
// 忘记调用 free(p) - 内存泄漏
}
// 错误2:悬空指针
void danglingPointer() {
int *p = (int*)malloc(sizeof(int));
*p = 10;
free(p);
// *p = 20; // 错误!使用已释放的内存
}
// 错误3:重复释放
void doubleFree() {
int *p = (int*)malloc(sizeof(int));
free(p);
// free(p); // 错误!重复释放
}
// 错误4:缓冲区溢出
void bufferOverflow() {
int *p = (int*)malloc(5 * sizeof(int));
// p[5] = 100; // 错误!越界访问
free(p);
}
int main() {
printf("===== 动态内存常见错误 =====\n\n");
printf("1. 内存泄漏:分配后忘记释放\n");
printf("2. 悬空指针:使用已释放的内存\n");
printf("3. 重复释放:多次释放同一内存\n");
printf("4. 缓冲区溢出:访问越界\n");
printf("\n使用 valgrind 等工具可以检测这些问题\n");
return 0;
}
7. 栈内存与堆内存对比
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 栈内存分配
void stackAllocation() {
int stackArr[100000]; // 栈上分配,自动释放
printf("栈内存分配成功\n");
}
// 堆内存分配
void heapAllocation() {
int *heapArr = (int*)malloc(100000 * sizeof(int));
if (heapArr != NULL) {
printf("堆内存分配成功\n");
free(heapArr);
}
}
int main() {
printf("===== 栈内存 vs 堆内存 =====\n\n");
printf("栈内存特点:\n");
printf(" - 自动分配和释放\n");
printf(" - 大小固定(通常几MB)\n");
printf(" - 访问速度快\n");
printf(" - 存在栈溢出风险\n\n");
printf("堆内存特点:\n");
printf(" - 手动分配和释放\n");
printf(" - 大小灵活(受系统内存限制)\n");
printf(" - 访问速度相对慢\n");
printf(" - 需要手动管理,容易内存泄漏\n\n");
printf("内存布局(从高地址到低地址):\n");
printf(" +-----------------+ 高地址\n");
printf(" | 栈(Stack) |\n");
printf(" +-----------------+\n");
printf(" | ↓ |\n");
printf(" | ↑ |\n");
printf(" +-----------------+\n");
printf(" | 堆(Heap) |\n");
printf(" +-----------------+\n");
printf(" | BSS 段 | 未初始化数据\n");
printf(" +-----------------+\n");
printf(" | 数据段 | 初始化数据\n");
printf(" +-----------------+\n");
printf(" | 代码段 | 可执行代码\n");
printf(" +-----------------+ 低地址\n");
return 0;
}
8. 实战案例:实现内存池
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define POOL_SIZE 1024
#define ALIGNMENT 8
// 简单的内存池实现
typedef struct MemoryPool {
unsigned char buffer[POOL_SIZE];
size_t offset;
} MemoryPool;
// 初始化内存池
void initPool(MemoryPool *pool) {
memset(pool->buffer, 0, POOL_SIZE);
pool->offset = 0;
}
// 对齐分配
void* poolAlloc(MemoryPool *pool, size_t size) {
// 对齐到 ALIGNMENT 的倍数
size_t alignedSize = (size + ALIGNMENT - 1) & ~(ALIGNMENT - 1);
if (pool->offset + alignedSize > POOL_SIZE) {
printf("内存池空间不足!\n");
return NULL;
}
void *ptr = pool->buffer + pool->offset;
pool->offset += alignedSize;
return ptr;
}
// 重置内存池
void poolReset(MemoryPool *pool) {
pool->offset = 0;
}
// 获取剩余空间
size_t poolRemaining(MemoryPool *pool) {
return POOL_SIZE - pool->offset;
}
int main() {
printf("===== 内存池实现 =====\n\n");
MemoryPool pool;
initPool(&pool);
printf("内存池初始化完成,大小:%d 字节\n", POOL_SIZE);
printf("剩余空间:%zu 字节\n\n", poolRemaining(&pool));
// 分配内存
int *a = (int*)poolAlloc(&pool, sizeof(int));
double *b = (double*)poolAlloc(&pool, sizeof(double));
char *str = (char*)poolAlloc(&pool, 100);
if (a && b && str) {
*a = 42;
*b = 3.14159;
strcpy(str, "Hello, Memory Pool!");
printf("分配成功:\n");
printf(" int: %d\n", *a);
printf(" double: %.5f\n", *b);
printf(" string: %s\n", str);
printf(" 剩余空间:%zu 字节\n", poolRemaining(&pool));
}
printf("\n重置内存池...\n");
poolReset(&pool);
printf("重置后剩余空间:%zu 字节\n", poolRemaining(&pool));
return 0;
}
9. 调试技巧
9.1 打印内存内容
#include <stdio.h>
// 以十六进制打印内存区域
void printMemory(const void *ptr, size_t size) {
const unsigned char *bytes = (const unsigned char*)ptr;
printf("内存地址:%p\n", ptr);
printf("字节内容:");
for (size_t i = 0; i < size; i++) {
printf("%02X ", bytes[i]);
if ((i + 1) % 8 == 0 && i + 1 < size) {
printf("\n ");
}
}
printf("\n");
}
int main() {
printf("===== 内存调试工具 =====\n\n");
int arr[] = {0x12345678, 0x90ABCDEF};
printf("数组内容:\n");
printMemory(arr, sizeof(arr));
struct {
int a;
char b;
short c;
} s = {0x12345678, 'A', 0x1234};
printf("\n结构体内容:\n");
printMemory(&s, sizeof(s));
return 0;
}
10. 总结
10.1 核心知识点回顾
数据类型 存储方式 关键点
整数 补码 注意溢出和符号位
浮点数 IEEE 754 精度问题,特殊值
指针 地址 大小固定,类型决定步长
结构体 内存对齐 顺序影响大小
数组 连续内存 数组名即首地址
10.2 最佳实践
整数运算:注意溢出,使用适当类型
浮点数:不要直接比较相等,使用容差
指针:初始化,检查NULL,避免悬空
动态内存:配对 malloc/free,防止泄漏
结构体:合理安排成员顺序,节省内存
字节序:网络传输时注意转换
10.3 常见陷阱
· ❌ 使用未初始化的内存
· ❌ 越界访问数组
· ❌ 忘记释放动态内存
· ❌ 使用已释放的内存
· ❌ 忽略内存对齐
· ❌ 混淆大小端字节序
记住:理解内存是C语言的核心,只有真正理解数据如何存储,才能写出高效、可靠的程序!