/** * ============================================================================ * C 语言动态内存分配 · 学习示例 * ============================================================================ * * 内容大纲: * 第 1 节 malloc 基本内存分配 * 第 2 节 calloc 分配并初始化为 0 * 第 3 节 realloc 调整内存大小(扩容 / 缩容) * 第 4 节 free 释放内存,防野指针 * 第 5 节 动态数组(自动扩容的 vector) * 第 6 节 二维矩阵(指针数组 / 连续内存块 两种方式) * 第 7 节 字符串数组(char** + strdup) * 第 8 节 结构体数组(先数组、后成员的两层分配) * 第 9 节 柔性数组成员(Flexible Array Member, C99) * 第 10 节 常见错误、内存跟踪与良好习惯 * * 编译运行(任选其一): * gcc memory_allocation.c -o memory_allocation.exe -Wall -Wextra -g * cl memory_allocation.c /Fe:memory_allocation.exe * * 说明:本程序按 C99 标准书写,注释使用中文,运行时请保证控制台 * 使用 UTF-8 编码(Windows 下可用 `chcp 65001`)。 * ============================================================================ */ #include #include #include /* --------------------------------------------------------------------------- * 工具:带错误检查的分配包装 * --------------------------------------------------------------------------- * malloc 失败时会返回 NULL。如果拿到 NULL 还继续使用,就是 * “空指针解引用”(NULL pointer dereference),程序直接崩溃。 * 下面的 xmalloc / xcalloc / xrealloc 把“分配 + 检查 + 报错退出” * 封装起来,后面所有示例都用它们,保证代码既简洁又安全。 * ------------------------------------------------------------------------- */ #define XMALLOC(size) xmalloc(size, __FILE__, __LINE__) #define XCALLOC(count, size) xcalloc(count, size, __FILE__, __LINE__) #define XREALLOC(ptr, size) xrealloc(ptr, size, __FILE__, __LINE__) static void *xmalloc(size_t size, const char *file, int line) { void *p = malloc(size); if (p == NULL) { fprintf(stderr, "[内存分配失败] %s:%d 无法分配 %zu 字节\n", file, line, size); exit(EXIT_FAILURE); } return p; } static void *xcalloc(size_t count, size_t size, const char *file, int line) { void *p = calloc(count, size); if (p == NULL) { fprintf(stderr, "[内存分配失败] %s:%d 无法分配 %zu x %zu 字节\n", file, line, count, size); exit(EXIT_FAILURE); } return p; } static void *xrealloc(void *ptr, size_t size, const char *file, int line) { void *p = realloc(ptr, size); /* 注意:realloc 可能返回新地址 */ if (p == NULL) { fprintf(stderr, "[内存重分配失败] %s:%d 无法调整到 %zu 字节\n", file, line, size); exit(EXIT_FAILURE); } return p; } /* 打印分隔标题,方便看清每个示例的输出 */ static void section(const char *title) { printf("\n================ %s ================\n", title); } /* ============================================================================ * 第 1 节 malloc —— 基本内存分配 * ---------------------------------------------------------------------------- * void *malloc(size_t size); * - 在堆上分配 size 个字节的连续内存; * - 返回 void*(无类型指针),使用时要强制转换成目标类型; * - 分配的内存【内容不确定】,必须自己初始化后才能读取; * - 分配失败返回 NULL。 * ==========================================================================*/ static void demo_malloc(void) { section("1. malloc 基本分配"); int *arr; /* 指向堆上 int 数组的指针 */ int i; /* 分配 5 个 int 需要的空间 */ arr = XMALLOC(5 * sizeof(int)); printf("成功分配 5 个 int(共 %zu 字节)\n", 5 * sizeof(int)); /* * malloc 不会清零!真正未初始化时内容是无法预测的“垃圾值”。 * 直接读取未初始化的内存属于未定义行为,为了安全演示, * 这里用 memset 把内存填成 0xAA,再把它当成 int 打印, * 就能直观看到“这块内存里原来有未知的数据”。 */ memset(arr, 0xAA, 5 * sizeof(int)); printf("malloc 后未初始化(本处用 0xAA 模拟垃圾值):\n"); for (i = 0; i < 5; i++) printf(" arr[%d] = 0x%08X\n", i, (unsigned)arr[i]); /* 必须自己赋值后才能正常使用 */ for (i = 0; i < 5; i++) arr[i] = i * i; printf("手动初始化后:"); for (i = 0; i < 5; i++) printf("%d ", arr[i]); printf("\n"); free(arr); } /* ============================================================================ * 第 2 节 calloc —— 分配并清零 * ---------------------------------------------------------------------------- * void *calloc(size_t count, size_t size); * - 分配 count * size 字节,并把所有字节置为 0; * - 参数写成“个数 x 单个大小”,比 malloc(count*size) 更不容易写错、 * 还能避免整数溢出; * - 同样要检查返回值是否为 NULL。 * ==========================================================================*/ static void demo_calloc(void) { section("2. calloc 分配并清零"); int *arr; int i; arr = XCALLOC(5, sizeof(int)); printf("calloc 分配的 5 个 int 全部为 0:\n"); for (i = 0; i < 5; i++) printf(" arr[%d] = %d\n", i, arr[i]); free(arr); } /* ============================================================================ * 第 3 节 realloc —— 调整内存大小 * ---------------------------------------------------------------------------- * void *realloc(void *ptr, size_t new_size); * - 把 ptr 指向的内存块调整到 new_size 字节; * - 扩容时原有内容会被完整保留,新增部分【未初始化】; * - 返回值可能是新的地址(原内存可能被搬走),所以一定要: * ptr = realloc(ptr, new_size); // 重新赋给 ptr * 绝不要写成: * realloc(ptr, new_size); // 丢弃返回值 → 泄漏或悬垂 * - 传入 NULL 等价于 malloc;new_size 为 0 等价于 free(不推荐依赖)。 * ==========================================================================*/ static void demo_realloc(void) { section("3. realloc 扩容与缩容"); int *arr; int i; arr = XMALLOC(4 * sizeof(int)); for (i = 0; i < 4; i++) arr[i] = i + 1; printf("初始 4 个元素:"); for (i = 0; i < 4; i++) printf("%d ", arr[i]); printf("\n"); /* 扩容到 8 个 int:旧数据保留,新空间未初始化 */ arr = XREALLOC(arr, 8 * sizeof(int)); printf("扩容后前 4 个(保留的原数据):"); for (i = 0; i < 4; i++) printf("%d ", arr[i]); printf("\n"); /* 新空间必须初始化后才能使用 */ for (i = 4; i < 8; i++) arr[i] = i * 100; printf("初始化后全部 8 个:"); for (i = 0; i < 8; i++) printf("%d ", arr[i]); printf("\n"); /* 缩容到 3 个 int:多余的元素被丢弃 */ arr = XREALLOC(arr, 3 * sizeof(int)); printf("缩容后只剩 3 个:"); for (i = 0; i < 3; i++) printf("%d ", arr[i]); printf("\n"); free(arr); } /* ============================================================================ * 第 4 节 free —— 释放内存,并防止野指针 * ---------------------------------------------------------------------------- * void free(void *ptr); * - 只能释放 malloc / calloc / realloc 返回的指针; * - 释放后该指针仍是原来的地址,但内存已归还系统, * 这样的指针叫“野指针 / 悬垂指针”(dangling pointer),必须立刻置 NULL; * - 对 NULL 调用 free 是安全的(什么都不做); * - 同一块内存只能释放一次(重复释放 = double free,会崩溃)。 * ==========================================================================*/ static void demo_free(void) { section("4. free 与野指针"); int *p = XMALLOC(sizeof(int)); *p = 42; printf("释放前 *p = %d\n", *p); free(p); /* 释放后 p 仍“指向”旧地址,继续读写它是未定义行为,必须置空 */ p = NULL; if (p == NULL) { printf("已释放并置空。对 NULL 调用 free 是安全的:\n"); free(p); /* 什么都不做,不会出错 */ } } /* ============================================================================ * 第 5 节 动态数组(可自动扩容的 int vector) * ---------------------------------------------------------------------------- * 最常见的用法:用一个结构体记录“数据指针 + 元素个数 + 容量”, * 满了就用 realloc 把容量翻倍。翻倍扩容后平均每个元素只被拷贝 O(1) 次, * 所以 push 的均摊时间复杂度是 O(1)。 * ==========================================================================*/ typedef struct { int *data; /* 指向堆上数据 */ int len; /* 当前元素个数 */ int cap; /* 当前容量(已分配) */ } IntVec; static void vec_init(IntVec *v) { v->data = NULL; v->len = 0; v->cap = 0; } static void vec_push(IntVec *v, int value) { if (v->len == v->cap) { int new_cap = (v->cap == 0) ? 4 : v->cap * 2; /* 容量翻倍 */ v->data = XREALLOC(v->data, (size_t)new_cap * sizeof(int)); v->cap = new_cap; } v->data[v->len++] = value; } static void vec_free(IntVec *v) { free(v->data); v->data = NULL; /* 好习惯:释放后置空 */ v->len = 0; v->cap = 0; } static void demo_vector(void) { section("5. 动态数组 vector"); IntVec v; int i; vec_init(&v); for (i = 0; i < 10; i++) vec_push(&v, i * 10); printf("共 %d 个元素(容量 %d):\n", v.len, v.cap); for (i = 0; i < v.len; i++) printf(" v[%d] = %d\n", i, v.data[i]); vec_free(&v); } /* ============================================================================ * 第 6 节 二维矩阵的两种分配方式 * ---------------------------------------------------------------------------- * 方式 A:指针数组 —— 每一行单独 malloc。写法直观、可以每行长度不同, * 但各行之间地址不连续,对缓存不友好。 * 方式 B:连续内存块 —— 一次 malloc 出 rows*cols 个元素,再用一个 * 指针数组把大块内存“切”成一行一行。内存完全连续,性能更好, * 释放也更简单。 * ==========================================================================*/ static void demo_matrix(void) { const int rows = 3, cols = 4; int **m_a, **m_b, *storage; int r, c; /* ---- 方式 A:指针数组(每行单独分配) ---- */ m_a = XMALLOC((size_t)rows * sizeof(int *)); for (r = 0; r < rows; r++) m_a[r] = XMALLOC((size_t)cols * sizeof(int)); for (r = 0; r < rows; r++) for (c = 0; c < cols; c++) m_a[r][c] = r * 10 + c; printf("方式 A(每行单独 malloc):\n"); for (r = 0; r < rows; r++) { printf(" "); for (c = 0; c < cols; c++) printf("%4d", m_a[r][c]); printf("\n"); } /* 释放顺序:先释放每一行,再释放行指针数组(顺序不能反) */ for (r = 0; r < rows; r++) free(m_a[r]); free(m_a); /* ---- 方式 B:连续内存块 ---- */ storage = XMALLOC((size_t)(rows * cols) * sizeof(int)); m_b = XMALLOC((size_t)rows * sizeof(int *)); for (r = 0; r < rows; r++) m_b[r] = storage + r * cols; /* 把大块内存划分成行 */ for (r = 0; r < rows; r++) for (c = 0; c < cols; c++) m_b[r][c] = r * 10 + c; printf("方式 B(连续内存块):\n"); for (r = 0; r < rows; r++) { printf(" "); for (c = 0; c < cols; c++) printf("%4d", m_b[r][c]); printf("\n"); } /* 方式 B 只需释放两次:先指针数组,再数据块 */ free(m_b); free(storage); } /* ============================================================================ * 第 7 节 字符串数组 * ---------------------------------------------------------------------------- * 用 char** 存放多个字符串。字符串字面量不能随意修改,所以要把它们 * 复制到堆上(strdup:malloc + strcpy)。MSVC 里 strdup 叫 _strdup, * 下面自己写一个 safe_strdup 保证跨平台可移植。 * ==========================================================================*/ static char *safe_strdup(const char *s) { size_t n = strlen(s) + 1; /* 别忘了末尾的 '\0' */ char *copy = XMALLOC(n); memcpy(copy, s, n); return copy; } static void demo_strings(void) { section("7. 字符串数组"); const char *names[] = { "C", "Java", "Python", "Rust" }; int count = (int)(sizeof(names) / sizeof(names[0])); char **strs = XMALLOC((size_t)count * sizeof(char *)); int i; /* 每个字符串单独复制到堆上 */ for (i = 0; i < count; i++) strs[i] = safe_strdup(names[i]); printf("共 %d 个字符串:\n", count); for (i = 0; i < count; i++) printf(" strs[%d] = \"%s\"(长度 %zu)\n", i, strs[i], strlen(strs[i])); /* 释放顺序:先每个字符串,再字符串指针数组 */ for (i = 0; i < count; i++) free(strs[i]); free(strs); } /* ============================================================================ * 第 8 节 结构体数组(两层分配) * ---------------------------------------------------------------------------- * 结构体里含指针时,内存要分两层分配: * 1. 给“结构体数组”分配内存; * 2. 给每个结构体内部的指针成员分配内存。 * 释放顺序与分配顺序相反:先释放每个成员的指针,再释放数组本身。 * ==========================================================================*/ typedef struct { char *name; /* 指向堆上的字符串 */ int score; } Student; static void demo_struct_array(void) { section("8. 结构体数组"); const char *stu_names[] = { "Alice", "Bob", "Carol" }; int count = (int)(sizeof(stu_names) / sizeof(stu_names[0])); Student *list = XMALLOC((size_t)count * sizeof(Student)); int i; for (i = 0; i < count; i++) { list[i].name = safe_strdup(stu_names[i]); list[i].score = 60 + i * 10; } printf("学生列表:\n"); for (i = 0; i < count; i++) printf(" %-6s 得分 %d\n", list[i].name, list[i].score); /* 释放:先释放每个成员指针,再释放结构体数组 */ for (i = 0; i < count; i++) free(list[i].name); free(list); } /* ============================================================================ * 第 9 节 柔性数组成员(Flexible Array Member, C99) * ---------------------------------------------------------------------------- * 结构体的最后一个成员可以写成不指定长度的数组 items[], * 它不占结构体本身的空间。这样“结构体头 + 数据”可以用一次 malloc * 分配在同一块连续内存里,访问时 items 紧跟在头后面,释放也只需 free 一次。 * ==========================================================================*/ typedef struct { size_t len; /* 元素个数 */ int items[]; /* 柔性数组成员,sizeof(Pack) 不含它 */ } Pack; static void demo_flexible_array(void) { section("9. 柔性数组成员"); size_t n = 6; Pack *pack = XMALLOC(sizeof(Pack) + n * sizeof(int)); size_t i; pack->len = n; for (i = 0; i < n; i++) pack->items[i] = (int)i * 2; printf("sizeof(Pack) = %zu(只包含 len,不含 items)\n", sizeof(Pack)); printf("共 %zu 个元素:", pack->len); for (i = 0; i < pack->len; i++) printf("%d ", pack->items[i]); printf("\n"); printf("一次 malloc、一次 free:数据与结构体头在同一个内存块里。\n"); free(pack); } /* ============================================================================ * 第 10 节 常见错误、内存跟踪与良好习惯 * ---------------------------------------------------------------------------- * 常见错误速查: * 1. 不检查 malloc 的返回值 → 见 xmalloc 包装 * 2. 分配了却忘记 free → 内存泄漏(memory leak) * 3. 越界写(分配 5 个却写第 6 个) → 堆损坏,难排查 * 4. free 之后继续使用(野指针) → 见第 4 节 * 5. 同一指针重复 free(double free)→ 崩溃 * 6. 释放与分配方式不匹配 → C 中统一用 free * * 下面的跟踪计数器用于演示“谁分配、谁释放”,借此直观看出泄漏: * 分配次数 - 释放次数 > 0 就说明有内存没还回去。 * ==========================================================================*/ static int g_alloc_count = 0; static int g_free_count = 0; #define TRACK_MALLOC(size) track_malloc(size, __FILE__, __LINE__) #define TRACK_FREE(ptr) do { track_free(ptr, __FILE__, __LINE__); ptr = NULL; } while (0) static void *track_malloc(size_t size, const char *file, int line) { void *p = xmalloc(size, file, line); g_alloc_count++; printf(" [跟踪] 分配 0x%p(%zu 字节)@ %s:%d\n", p, size, file, line); return p; } static void track_free(void *p, const char *file, int line) { if (p != NULL) { g_free_count++; printf(" [跟踪] 释放 0x%p @ %s:%d\n", p, file, line); } free(p); } static void demo_pitfalls(void) { section("10. 内存跟踪:谁分配、谁释放"); /* 故意“分配后不释放”,制造一块泄漏内存(学习观察用) */ int *leak = TRACK_MALLOC(100 * sizeof(int)); (void)leak; /* 避免“未使用变量”警告 */ /* 正确的一对:分配后释放 */ int *ok = TRACK_MALLOC(10 * sizeof(int)); TRACK_FREE(ok); printf("\n 已分配 %d 次,已释放 %d 次,差值 %d(>0 表示有泄漏)\n", g_alloc_count, g_free_count, g_alloc_count - g_free_count); if (g_alloc_count != g_free_count) printf(" >> 发现 %d 块内存未释放(人为制造的泄漏,仅供学习)<<\n", g_alloc_count - g_free_count); printf("\n 提示:真实项目中可用 Valgrind(Linux)或 AddressSanitizer\n" " (gcc -fsanitize=address)自动检测泄漏与越界。\n"); } /* ============================================================================ * 程序入口:依次运行所有示例 * ==========================================================================*/ int main(void) { /* 关闭 stdout 缓冲,保证 Windows 控制台也能即时显示中文输出 */ setvbuf(stdout, NULL, _IONBF, 0); demo_malloc(); demo_calloc(); demo_realloc(); demo_free(); demo_vector(); demo_matrix(); demo_strings(); demo_struct_array(); demo_flexible_array(); demo_pitfalls(); printf("\n全部示例运行完毕!\n"); return 0; }