| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535 |
- /**
- * ============================================================================
- * 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 <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- /* ---------------------------------------------------------------------------
- * 工具:带错误检查的分配包装
- * ---------------------------------------------------------------------------
- * 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;
- }
|