1
0

memory_allocation.c 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. /**
  2. * ============================================================================
  3. * C 语言动态内存分配 · 学习示例
  4. * ============================================================================
  5. *
  6. * 内容大纲:
  7. * 第 1 节 malloc 基本内存分配
  8. * 第 2 节 calloc 分配并初始化为 0
  9. * 第 3 节 realloc 调整内存大小(扩容 / 缩容)
  10. * 第 4 节 free 释放内存,防野指针
  11. * 第 5 节 动态数组(自动扩容的 vector)
  12. * 第 6 节 二维矩阵(指针数组 / 连续内存块 两种方式)
  13. * 第 7 节 字符串数组(char** + strdup)
  14. * 第 8 节 结构体数组(先数组、后成员的两层分配)
  15. * 第 9 节 柔性数组成员(Flexible Array Member, C99)
  16. * 第 10 节 常见错误、内存跟踪与良好习惯
  17. *
  18. * 编译运行(任选其一):
  19. * gcc memory_allocation.c -o memory_allocation.exe -Wall -Wextra -g
  20. * cl memory_allocation.c /Fe:memory_allocation.exe
  21. *
  22. * 说明:本程序按 C99 标准书写,注释使用中文,运行时请保证控制台
  23. * 使用 UTF-8 编码(Windows 下可用 `chcp 65001`)。
  24. * ============================================================================
  25. */
  26. #include <stdio.h>
  27. #include <stdlib.h>
  28. #include <string.h>
  29. /* ---------------------------------------------------------------------------
  30. * 工具:带错误检查的分配包装
  31. * ---------------------------------------------------------------------------
  32. * malloc 失败时会返回 NULL。如果拿到 NULL 还继续使用,就是
  33. * “空指针解引用”(NULL pointer dereference),程序直接崩溃。
  34. * 下面的 xmalloc / xcalloc / xrealloc 把“分配 + 检查 + 报错退出”
  35. * 封装起来,后面所有示例都用它们,保证代码既简洁又安全。
  36. * ------------------------------------------------------------------------- */
  37. #define XMALLOC(size) xmalloc(size, __FILE__, __LINE__)
  38. #define XCALLOC(count, size) xcalloc(count, size, __FILE__, __LINE__)
  39. #define XREALLOC(ptr, size) xrealloc(ptr, size, __FILE__, __LINE__)
  40. static void *xmalloc(size_t size, const char *file, int line)
  41. {
  42. void *p = malloc(size);
  43. if (p == NULL) {
  44. fprintf(stderr, "[内存分配失败] %s:%d 无法分配 %zu 字节\n",
  45. file, line, size);
  46. exit(EXIT_FAILURE);
  47. }
  48. return p;
  49. }
  50. static void *xcalloc(size_t count, size_t size, const char *file, int line)
  51. {
  52. void *p = calloc(count, size);
  53. if (p == NULL) {
  54. fprintf(stderr, "[内存分配失败] %s:%d 无法分配 %zu x %zu 字节\n",
  55. file, line, count, size);
  56. exit(EXIT_FAILURE);
  57. }
  58. return p;
  59. }
  60. static void *xrealloc(void *ptr, size_t size, const char *file, int line)
  61. {
  62. void *p = realloc(ptr, size); /* 注意:realloc 可能返回新地址 */
  63. if (p == NULL) {
  64. fprintf(stderr, "[内存重分配失败] %s:%d 无法调整到 %zu 字节\n",
  65. file, line, size);
  66. exit(EXIT_FAILURE);
  67. }
  68. return p;
  69. }
  70. /* 打印分隔标题,方便看清每个示例的输出 */
  71. static void section(const char *title)
  72. {
  73. printf("\n================ %s ================\n", title);
  74. }
  75. /* ============================================================================
  76. * 第 1 节 malloc —— 基本内存分配
  77. * ----------------------------------------------------------------------------
  78. * void *malloc(size_t size);
  79. * - 在堆上分配 size 个字节的连续内存;
  80. * - 返回 void*(无类型指针),使用时要强制转换成目标类型;
  81. * - 分配的内存【内容不确定】,必须自己初始化后才能读取;
  82. * - 分配失败返回 NULL。
  83. * ==========================================================================*/
  84. static void demo_malloc(void)
  85. {
  86. section("1. malloc 基本分配");
  87. int *arr; /* 指向堆上 int 数组的指针 */
  88. int i;
  89. /* 分配 5 个 int 需要的空间 */
  90. arr = XMALLOC(5 * sizeof(int));
  91. printf("成功分配 5 个 int(共 %zu 字节)\n", 5 * sizeof(int));
  92. /*
  93. * malloc 不会清零!真正未初始化时内容是无法预测的“垃圾值”。
  94. * 直接读取未初始化的内存属于未定义行为,为了安全演示,
  95. * 这里用 memset 把内存填成 0xAA,再把它当成 int 打印,
  96. * 就能直观看到“这块内存里原来有未知的数据”。
  97. */
  98. memset(arr, 0xAA, 5 * sizeof(int));
  99. printf("malloc 后未初始化(本处用 0xAA 模拟垃圾值):\n");
  100. for (i = 0; i < 5; i++)
  101. printf(" arr[%d] = 0x%08X\n", i, (unsigned)arr[i]);
  102. /* 必须自己赋值后才能正常使用 */
  103. for (i = 0; i < 5; i++)
  104. arr[i] = i * i;
  105. printf("手动初始化后:");
  106. for (i = 0; i < 5; i++)
  107. printf("%d ", arr[i]);
  108. printf("\n");
  109. free(arr);
  110. }
  111. /* ============================================================================
  112. * 第 2 节 calloc —— 分配并清零
  113. * ----------------------------------------------------------------------------
  114. * void *calloc(size_t count, size_t size);
  115. * - 分配 count * size 字节,并把所有字节置为 0;
  116. * - 参数写成“个数 x 单个大小”,比 malloc(count*size) 更不容易写错、
  117. * 还能避免整数溢出;
  118. * - 同样要检查返回值是否为 NULL。
  119. * ==========================================================================*/
  120. static void demo_calloc(void)
  121. {
  122. section("2. calloc 分配并清零");
  123. int *arr;
  124. int i;
  125. arr = XCALLOC(5, sizeof(int));
  126. printf("calloc 分配的 5 个 int 全部为 0:\n");
  127. for (i = 0; i < 5; i++)
  128. printf(" arr[%d] = %d\n", i, arr[i]);
  129. free(arr);
  130. }
  131. /* ============================================================================
  132. * 第 3 节 realloc —— 调整内存大小
  133. * ----------------------------------------------------------------------------
  134. * void *realloc(void *ptr, size_t new_size);
  135. * - 把 ptr 指向的内存块调整到 new_size 字节;
  136. * - 扩容时原有内容会被完整保留,新增部分【未初始化】;
  137. * - 返回值可能是新的地址(原内存可能被搬走),所以一定要:
  138. * ptr = realloc(ptr, new_size); // 重新赋给 ptr
  139. * 绝不要写成:
  140. * realloc(ptr, new_size); // 丢弃返回值 → 泄漏或悬垂
  141. * - 传入 NULL 等价于 malloc;new_size 为 0 等价于 free(不推荐依赖)。
  142. * ==========================================================================*/
  143. static void demo_realloc(void)
  144. {
  145. section("3. realloc 扩容与缩容");
  146. int *arr;
  147. int i;
  148. arr = XMALLOC(4 * sizeof(int));
  149. for (i = 0; i < 4; i++)
  150. arr[i] = i + 1;
  151. printf("初始 4 个元素:");
  152. for (i = 0; i < 4; i++) printf("%d ", arr[i]);
  153. printf("\n");
  154. /* 扩容到 8 个 int:旧数据保留,新空间未初始化 */
  155. arr = XREALLOC(arr, 8 * sizeof(int));
  156. printf("扩容后前 4 个(保留的原数据):");
  157. for (i = 0; i < 4; i++) printf("%d ", arr[i]);
  158. printf("\n");
  159. /* 新空间必须初始化后才能使用 */
  160. for (i = 4; i < 8; i++)
  161. arr[i] = i * 100;
  162. printf("初始化后全部 8 个:");
  163. for (i = 0; i < 8; i++) printf("%d ", arr[i]);
  164. printf("\n");
  165. /* 缩容到 3 个 int:多余的元素被丢弃 */
  166. arr = XREALLOC(arr, 3 * sizeof(int));
  167. printf("缩容后只剩 3 个:");
  168. for (i = 0; i < 3; i++) printf("%d ", arr[i]);
  169. printf("\n");
  170. free(arr);
  171. }
  172. /* ============================================================================
  173. * 第 4 节 free —— 释放内存,并防止野指针
  174. * ----------------------------------------------------------------------------
  175. * void free(void *ptr);
  176. * - 只能释放 malloc / calloc / realloc 返回的指针;
  177. * - 释放后该指针仍是原来的地址,但内存已归还系统,
  178. * 这样的指针叫“野指针 / 悬垂指针”(dangling pointer),必须立刻置 NULL;
  179. * - 对 NULL 调用 free 是安全的(什么都不做);
  180. * - 同一块内存只能释放一次(重复释放 = double free,会崩溃)。
  181. * ==========================================================================*/
  182. static void demo_free(void)
  183. {
  184. section("4. free 与野指针");
  185. int *p = XMALLOC(sizeof(int));
  186. *p = 42;
  187. printf("释放前 *p = %d\n", *p);
  188. free(p);
  189. /* 释放后 p 仍“指向”旧地址,继续读写它是未定义行为,必须置空 */
  190. p = NULL;
  191. if (p == NULL) {
  192. printf("已释放并置空。对 NULL 调用 free 是安全的:\n");
  193. free(p); /* 什么都不做,不会出错 */
  194. }
  195. }
  196. /* ============================================================================
  197. * 第 5 节 动态数组(可自动扩容的 int vector)
  198. * ----------------------------------------------------------------------------
  199. * 最常见的用法:用一个结构体记录“数据指针 + 元素个数 + 容量”,
  200. * 满了就用 realloc 把容量翻倍。翻倍扩容后平均每个元素只被拷贝 O(1) 次,
  201. * 所以 push 的均摊时间复杂度是 O(1)。
  202. * ==========================================================================*/
  203. typedef struct {
  204. int *data; /* 指向堆上数据 */
  205. int len; /* 当前元素个数 */
  206. int cap; /* 当前容量(已分配) */
  207. } IntVec;
  208. static void vec_init(IntVec *v)
  209. {
  210. v->data = NULL;
  211. v->len = 0;
  212. v->cap = 0;
  213. }
  214. static void vec_push(IntVec *v, int value)
  215. {
  216. if (v->len == v->cap) {
  217. int new_cap = (v->cap == 0) ? 4 : v->cap * 2; /* 容量翻倍 */
  218. v->data = XREALLOC(v->data, (size_t)new_cap * sizeof(int));
  219. v->cap = new_cap;
  220. }
  221. v->data[v->len++] = value;
  222. }
  223. static void vec_free(IntVec *v)
  224. {
  225. free(v->data);
  226. v->data = NULL; /* 好习惯:释放后置空 */
  227. v->len = 0;
  228. v->cap = 0;
  229. }
  230. static void demo_vector(void)
  231. {
  232. section("5. 动态数组 vector");
  233. IntVec v;
  234. int i;
  235. vec_init(&v);
  236. for (i = 0; i < 10; i++)
  237. vec_push(&v, i * 10);
  238. printf("共 %d 个元素(容量 %d):\n", v.len, v.cap);
  239. for (i = 0; i < v.len; i++)
  240. printf(" v[%d] = %d\n", i, v.data[i]);
  241. vec_free(&v);
  242. }
  243. /* ============================================================================
  244. * 第 6 节 二维矩阵的两种分配方式
  245. * ----------------------------------------------------------------------------
  246. * 方式 A:指针数组 —— 每一行单独 malloc。写法直观、可以每行长度不同,
  247. * 但各行之间地址不连续,对缓存不友好。
  248. * 方式 B:连续内存块 —— 一次 malloc 出 rows*cols 个元素,再用一个
  249. * 指针数组把大块内存“切”成一行一行。内存完全连续,性能更好,
  250. * 释放也更简单。
  251. * ==========================================================================*/
  252. static void demo_matrix(void)
  253. {
  254. const int rows = 3, cols = 4;
  255. int **m_a, **m_b, *storage;
  256. int r, c;
  257. /* ---- 方式 A:指针数组(每行单独分配) ---- */
  258. m_a = XMALLOC((size_t)rows * sizeof(int *));
  259. for (r = 0; r < rows; r++)
  260. m_a[r] = XMALLOC((size_t)cols * sizeof(int));
  261. for (r = 0; r < rows; r++)
  262. for (c = 0; c < cols; c++)
  263. m_a[r][c] = r * 10 + c;
  264. printf("方式 A(每行单独 malloc):\n");
  265. for (r = 0; r < rows; r++) {
  266. printf(" ");
  267. for (c = 0; c < cols; c++)
  268. printf("%4d", m_a[r][c]);
  269. printf("\n");
  270. }
  271. /* 释放顺序:先释放每一行,再释放行指针数组(顺序不能反) */
  272. for (r = 0; r < rows; r++)
  273. free(m_a[r]);
  274. free(m_a);
  275. /* ---- 方式 B:连续内存块 ---- */
  276. storage = XMALLOC((size_t)(rows * cols) * sizeof(int));
  277. m_b = XMALLOC((size_t)rows * sizeof(int *));
  278. for (r = 0; r < rows; r++)
  279. m_b[r] = storage + r * cols; /* 把大块内存划分成行 */
  280. for (r = 0; r < rows; r++)
  281. for (c = 0; c < cols; c++)
  282. m_b[r][c] = r * 10 + c;
  283. printf("方式 B(连续内存块):\n");
  284. for (r = 0; r < rows; r++) {
  285. printf(" ");
  286. for (c = 0; c < cols; c++)
  287. printf("%4d", m_b[r][c]);
  288. printf("\n");
  289. }
  290. /* 方式 B 只需释放两次:先指针数组,再数据块 */
  291. free(m_b);
  292. free(storage);
  293. }
  294. /* ============================================================================
  295. * 第 7 节 字符串数组
  296. * ----------------------------------------------------------------------------
  297. * 用 char** 存放多个字符串。字符串字面量不能随意修改,所以要把它们
  298. * 复制到堆上(strdup:malloc + strcpy)。MSVC 里 strdup 叫 _strdup,
  299. * 下面自己写一个 safe_strdup 保证跨平台可移植。
  300. * ==========================================================================*/
  301. static char *safe_strdup(const char *s)
  302. {
  303. size_t n = strlen(s) + 1; /* 别忘了末尾的 '\0' */
  304. char *copy = XMALLOC(n);
  305. memcpy(copy, s, n);
  306. return copy;
  307. }
  308. static void demo_strings(void)
  309. {
  310. section("7. 字符串数组");
  311. const char *names[] = { "C", "Java", "Python", "Rust" };
  312. int count = (int)(sizeof(names) / sizeof(names[0]));
  313. char **strs = XMALLOC((size_t)count * sizeof(char *));
  314. int i;
  315. /* 每个字符串单独复制到堆上 */
  316. for (i = 0; i < count; i++)
  317. strs[i] = safe_strdup(names[i]);
  318. printf("共 %d 个字符串:\n", count);
  319. for (i = 0; i < count; i++)
  320. printf(" strs[%d] = \"%s\"(长度 %zu)\n", i, strs[i], strlen(strs[i]));
  321. /* 释放顺序:先每个字符串,再字符串指针数组 */
  322. for (i = 0; i < count; i++)
  323. free(strs[i]);
  324. free(strs);
  325. }
  326. /* ============================================================================
  327. * 第 8 节 结构体数组(两层分配)
  328. * ----------------------------------------------------------------------------
  329. * 结构体里含指针时,内存要分两层分配:
  330. * 1. 给“结构体数组”分配内存;
  331. * 2. 给每个结构体内部的指针成员分配内存。
  332. * 释放顺序与分配顺序相反:先释放每个成员的指针,再释放数组本身。
  333. * ==========================================================================*/
  334. typedef struct {
  335. char *name; /* 指向堆上的字符串 */
  336. int score;
  337. } Student;
  338. static void demo_struct_array(void)
  339. {
  340. section("8. 结构体数组");
  341. const char *stu_names[] = { "Alice", "Bob", "Carol" };
  342. int count = (int)(sizeof(stu_names) / sizeof(stu_names[0]));
  343. Student *list = XMALLOC((size_t)count * sizeof(Student));
  344. int i;
  345. for (i = 0; i < count; i++) {
  346. list[i].name = safe_strdup(stu_names[i]);
  347. list[i].score = 60 + i * 10;
  348. }
  349. printf("学生列表:\n");
  350. for (i = 0; i < count; i++)
  351. printf(" %-6s 得分 %d\n", list[i].name, list[i].score);
  352. /* 释放:先释放每个成员指针,再释放结构体数组 */
  353. for (i = 0; i < count; i++)
  354. free(list[i].name);
  355. free(list);
  356. }
  357. /* ============================================================================
  358. * 第 9 节 柔性数组成员(Flexible Array Member, C99)
  359. * ----------------------------------------------------------------------------
  360. * 结构体的最后一个成员可以写成不指定长度的数组 items[],
  361. * 它不占结构体本身的空间。这样“结构体头 + 数据”可以用一次 malloc
  362. * 分配在同一块连续内存里,访问时 items 紧跟在头后面,释放也只需 free 一次。
  363. * ==========================================================================*/
  364. typedef struct {
  365. size_t len; /* 元素个数 */
  366. int items[]; /* 柔性数组成员,sizeof(Pack) 不含它 */
  367. } Pack;
  368. static void demo_flexible_array(void)
  369. {
  370. section("9. 柔性数组成员");
  371. size_t n = 6;
  372. Pack *pack = XMALLOC(sizeof(Pack) + n * sizeof(int));
  373. size_t i;
  374. pack->len = n;
  375. for (i = 0; i < n; i++)
  376. pack->items[i] = (int)i * 2;
  377. printf("sizeof(Pack) = %zu(只包含 len,不含 items)\n", sizeof(Pack));
  378. printf("共 %zu 个元素:", pack->len);
  379. for (i = 0; i < pack->len; i++)
  380. printf("%d ", pack->items[i]);
  381. printf("\n");
  382. printf("一次 malloc、一次 free:数据与结构体头在同一个内存块里。\n");
  383. free(pack);
  384. }
  385. /* ============================================================================
  386. * 第 10 节 常见错误、内存跟踪与良好习惯
  387. * ----------------------------------------------------------------------------
  388. * 常见错误速查:
  389. * 1. 不检查 malloc 的返回值 → 见 xmalloc 包装
  390. * 2. 分配了却忘记 free → 内存泄漏(memory leak)
  391. * 3. 越界写(分配 5 个却写第 6 个) → 堆损坏,难排查
  392. * 4. free 之后继续使用(野指针) → 见第 4 节
  393. * 5. 同一指针重复 free(double free)→ 崩溃
  394. * 6. 释放与分配方式不匹配 → C 中统一用 free
  395. *
  396. * 下面的跟踪计数器用于演示“谁分配、谁释放”,借此直观看出泄漏:
  397. * 分配次数 - 释放次数 > 0 就说明有内存没还回去。
  398. * ==========================================================================*/
  399. static int g_alloc_count = 0;
  400. static int g_free_count = 0;
  401. #define TRACK_MALLOC(size) track_malloc(size, __FILE__, __LINE__)
  402. #define TRACK_FREE(ptr) do { track_free(ptr, __FILE__, __LINE__); ptr = NULL; } while (0)
  403. static void *track_malloc(size_t size, const char *file, int line)
  404. {
  405. void *p = xmalloc(size, file, line);
  406. g_alloc_count++;
  407. printf(" [跟踪] 分配 0x%p(%zu 字节)@ %s:%d\n", p, size, file, line);
  408. return p;
  409. }
  410. static void track_free(void *p, const char *file, int line)
  411. {
  412. if (p != NULL) {
  413. g_free_count++;
  414. printf(" [跟踪] 释放 0x%p @ %s:%d\n", p, file, line);
  415. }
  416. free(p);
  417. }
  418. static void demo_pitfalls(void)
  419. {
  420. section("10. 内存跟踪:谁分配、谁释放");
  421. /* 故意“分配后不释放”,制造一块泄漏内存(学习观察用) */
  422. int *leak = TRACK_MALLOC(100 * sizeof(int));
  423. (void)leak; /* 避免“未使用变量”警告 */
  424. /* 正确的一对:分配后释放 */
  425. int *ok = TRACK_MALLOC(10 * sizeof(int));
  426. TRACK_FREE(ok);
  427. printf("\n 已分配 %d 次,已释放 %d 次,差值 %d(>0 表示有泄漏)\n",
  428. g_alloc_count, g_free_count, g_alloc_count - g_free_count);
  429. if (g_alloc_count != g_free_count)
  430. printf(" >> 发现 %d 块内存未释放(人为制造的泄漏,仅供学习)<<\n",
  431. g_alloc_count - g_free_count);
  432. printf("\n 提示:真实项目中可用 Valgrind(Linux)或 AddressSanitizer\n"
  433. " (gcc -fsanitize=address)自动检测泄漏与越界。\n");
  434. }
  435. /* ============================================================================
  436. * 程序入口:依次运行所有示例
  437. * ==========================================================================*/
  438. int main(void)
  439. {
  440. /* 关闭 stdout 缓冲,保证 Windows 控制台也能即时显示中文输出 */
  441. setvbuf(stdout, NULL, _IONBF, 0);
  442. demo_malloc();
  443. demo_calloc();
  444. demo_realloc();
  445. demo_free();
  446. demo_vector();
  447. demo_matrix();
  448. demo_strings();
  449. demo_struct_array();
  450. demo_flexible_array();
  451. demo_pitfalls();
  452. printf("\n全部示例运行完毕!\n");
  453. return 0;
  454. }