知识点 11 · 动态内存管理¶
本页最后更新于 ,内容可能过时
易错点总结 · 链表 + 动态内存的三大高频 bug¶
这一节是"先记住再做题"的说明。这一组坑在真题里出现过 4 次。
BUG 1:sizeof 加了 * —— 出现 2 次!
/* ❌ 错 */ struct Node* p = (struct Node*)malloc(sizeof(struct Node*));
/* ✅ 对 */ struct Node* p = (struct Node*)malloc(sizeof(struct Node));
- 错在申请了指针的大小(64 位下 8 字节),而结点需要 16 字节(
int data+ 8 字节指针,含对齐)。 - 症状:编译不报错、小数据量下常"碰巧能跑",数据一多就崩溃——最难查的一类 bug。
写
sizeof时要想清楚:装的是"要多大",不是"指针多大"。
BUG 2:删除结点时先 free 后取 next
/* ❌ 错:free 之后 middle->next 已经是野内存 */
free(middle);
node = middle->next;
/* ✅ 对:先用临时变量保住后继,再释放 */
node = middle->next; // 或者 next = middle->next;
free(middle);
middle = node;
链表操作里,任何"改指针"的动作都必须先保住你要用的下一个结点——删除、反转、排序里全都适用。
正面样例:
next_node = prev_node->next;就写在free(del_node);之前——顺序完全正确。
BUG 3:temp = newNode; 而不是 temp->next = newNode;
这是"插入到末尾"最经典的错误:
/* ❌ 错:只改了局部变量 temp,函数返回后一切归零 */
temp = newNode;
/* ✅ 对:改的是链表里最后一个结点的 next 字段 */
temp->next = newNode;
理解本质:
temp是局部变量(函数栈上的一个格子),temp->next是堆上结点的一个字段。 只有改堆上的东西才能留下痕迹。
1 · malloc / calloc / free 的用法¶
下列关于动态内存分配的说法错误的是?
- A.
malloc函数返回的指针需要强制转换为目标类型 - B. 使用
free函数释放动态分配的内存 - C. 动态分配的内存不会自动初始化
- D.
calloc函数会将分配的内存初始化为零
答案
A
解析
在 C 语言中,malloc 返回的 void * 可以隐式转换为任何对象指针类型,不需要强制转换。
强制转换是 C++ 的要求。(虽然写成 (int *)malloc(...) 也能通过编译,但"需要"这个说法是错的。)
| 函数 | 原型 | 是否初始化 | 说明 |
|---|---|---|---|
malloc(size) |
void *malloc(size_t) |
❌ 内容为垃圾值 | 只申请,不清零 |
calloc(n, size) |
void *calloc(size_t, size_t) |
✅ 全部置 0 | 申请 n 个元素并清零 |
realloc(p, size) |
void *realloc(void *, size_t) |
— | 调整已分配块的大小 |
free(p) |
void free(void *) |
— | 释放;只能释放 malloc/calloc/realloc 返回的指针 |
易错:加分号写成
int *p = (int *)malloc(n * sizeof(int));在 C 里是合法但不必要的; 真正必须强制转换的是 C++(以及用 C++ 编译器编译.c文件时)。记忆:
malloc不初始化、calloc清零——c 开头 = clear(清零)。
2 · 动态数组 · 分配 + 判空¶
输入两组整型数组 A1 和 A2,每次从 A1 和 A2 中各取出一个数 x 和 y,判断 x 和 y 中是否有且仅有一个为负数。如果满足条件,则按照规定格式输出这两个数以及判断结果。请写出应填入____处的字句。
#include <stdio.h>
#include <stdlib.h>
int *input(int *n) {
int i, *a;
scanf( 4.1.1 );
if( 4.1.2 )
exit(0);
for( 4.1.3 )
scanf("%d", &a[i]);
return a;
}
int main(){
int n1, *a1, n2, *a2, k1, k2, z;
a1 = input(&n1);
a2 = input(&n2);
for(k1 = 0; k1 < n1; k1++) {
for(k2 = 0; k2 < n2; k2++) {
if((z = 4.1.4 ){
printf("%d ⊙ %d = %d\n", a1[k1], a2[k2], z);
}
}
}
free(a1);
free(a2);
return 0;
}
答案
| 空 | 答案 |
|---|---|
| 4.1.1 | "%d", n |
| 4.1.2 | (a = (int *)malloc((*n) * sizeof(int))) == NULL |
| 4.1.3 | i = 0; i < *n; i++ |
| 4.1.4 | (a1[k1] < 0) ^ (a2[k2] < 0) |
解析
- 4.1.1:
n本身就是int *(形参),scanf需要地址,所以直接写n,不要写&n。 - 4.1.2 是本题与本节考点(动态内存)关系最紧的一空:这里同时完成了两件事——动态分配数组
a,并检查分配是否失败。这是这类题最标准的惯用法:记住这个"分配 + 判空"的搭配。
malloc失败会返回NULL,不判空就直接a[i] = ...会解引用空指针崩溃。 - 4.1.3:循环读入,下界 0、上界
*n。 - 4.1.4:"有且仅有一个"就是异或。C 里关系表达式的值正好是 0/1,所以可以直接用
^: 真值表:一正一负 →0 ^ 1 = 1或1 ^ 0 = 1✓;同正 →0^0=0;同负 →1^1=0。
等价写法:
(a1[k1] < 0) != (a2[k2] < 0)也对,而且可读性更好。题干里
if((z = ... ){处印有两个左括号一个右括号,属印刷瑕疵;按逻辑应为if ((z = (a1[k1]<0) ^ (a2[k2]<0)) != 0)或直接if (z = ...)(赋值表达式的值即z)。
本题的三条易错
易错:
- 动态分配的数组必须自己
free(程序末尾的free(a1); free(a2);就是配套动作)——main里return 0之后操作系统会回收,但做题时该写就得写,这是评分点。 malloc的参数是字节数,要写n * sizeof(int),不是n。input函数返回的是堆上的数组首地址——这正是"函数里造数组、返回到外面用"的唯一正解(局部数组int a[n];返回后即失效)。
3 · 动态数组 · 分配 + 判空(升序插入)¶
以下程序的功能是输入一组参数,根据其数量 n 建立一个整数数组,并且根据参数进行升序排列。其中参数值小于或等于 0 的器件不插入数组。最后输出所有的参数值,请写出应填入空缺处的字句。
#include <stdio.h>
#include <stdlib.h>
void InsDev(int *d, int *n, int para)
{
int k;
if(para <=0)
return;
for(____(24.1)____; k>=0; k--) /* 定位 */
{
if(____(24.2)____)
break;
d[k+1] = d[k]; /* 移动 */
}
d[++k] = para; /* 插入 */
____(24.3)____;
}
int main()
{
int *device, n, para, k=0;
scanf("%d", &n); /* 读入的参数量 */
if(____(24.4)____) /* 创建整数数组 */
exit(0);
while(k<n)
{
scanf("%d", ¶);
InsDev(____(24.5)____);
}
for(k=0; k<n; k++) /*输出所有的参数值 */
printf("%d\n", device[k]);
return 0;
}
答案
| 空 | 答案 |
|---|---|
| (24.1) | k = *n - 1 |
| (24.2) | d[k] <= para(等价写法 para >= d[k]) |
| (24.3) | (*n)++ |
| (24.4) | (device = (int *)malloc(n * sizeof(int))) == NULL |
| (24.5) | device, &k, para |
解析
- (24.4) 就是本节考点:"分配 + 判空"的惯用法——
本节的通用模板:
指针 = (类型 *)malloc(个数 * sizeof(类型))→ 立刻和NULL比 → 失败就退出/返回。 差别只在"个数"藏在哪:个数是形参int *n时要写(*n) * sizeof(int);本题的n是普通int,直接写n。 - (24.5):
InsDev的形参是int *d, int *n, int para,所以依次传数组名device、计数变量的地址&k、本次参数para。关键理解:这里传的不是
main里的n,而是k——因为main的while(k<n)循环体里根本没有k++!k只能靠InsDev里的 (24.3) 经指针加上去。哪一次para <= 0,函数就提前return,k不动,循环就再读一个数——这恰好实现了"小于等于 0 的器件不插入数组"(输出个数仍等于n,由for(k=0;k<n;k++)保证)。 - (24.3):
(*n)++;—— 插入成功后把计数加 1。括号不能省:
*n++等价于*(n++),动的是指针而不是计数。这道题把它放在这里考,用意很明显。 - (24.1):既然 (24.2) 是"从后往前"扫,
k的初值必须是当前最后一个元素的下标,即*n - 1。 - (24.2):升序插入 = "从后往前,比
para大的统统往后挪"。所以一旦遇到d[k] <= para(不再比para大)就break,插入点就是k+1——正好由下一句d[++k] = para;完成。边界自检:若
para比数组里所有元素都小,循环会一路跑到k = -1,此时d[++k]即d[0] = para,天然正确,不需要特判。这正是这种写法的漂亮之处。
材料里的程序没有 free(device)
真机验证(gcc 15.2.0,-Wall -Wextra 零警告):
- 输入
4+3 -1 5 2 4→ 输出2 3 4 5(-1被跳过,所以多读了 1 个数才凑够 4 个) - 输入
5+-2 9 1 8 0 3 7 2→ 输出1 3 7 8 9
这份材料里的程序从头到尾没有 free(device)。考试按题面作答即可,但自己写代码时要记得释放。
malloc 出来的内存要配对 free,这是动态内存题的评分点;照抄本题时请自己补上 free(device);。
4 · 动态内存分配的四个说法辨析¶
下列关于动态内存分配的描述中错误的是?
- A. 使用
malloc必须释放分配的内存 - B. 使用
calloc分配的内存会初始化为 0 - C. 如果动态内存分配失败,返回 NULL
- D. 动态分配的内存可以自动释放
答案
D
解析
堆上的内存永远不会自动回收——malloc/calloc 拿到的那块,只有显式 free 才还回去,否则要等整个程序结束才由操作系统统一收走。这正是"内存泄漏"的根源,所以 D 错。
A、B、C 都是对的:calloc 的 c 开头 = clear(清零),而 malloc 不清零;malloc/calloc 分配失败时统一返回 NULL(所以用完要判空)。
栈上变量自己消失,堆上内存必须手动 free,malloc 与 free 一一配对。
说明¶
- 材料里这一题没有附答案,
(24.1)–(24.5)五处均由推导确定,并用 gcc 15.2.0 真机跑过两组输入(见解析末尾),输出符合"升序 + 跳过非正数"的要求。
(24.2) 也可以写成 para >= d[k] 等价的边界形式,判定逻辑相同。