跳转至

知识点 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 中各取出一个数 xy,判断 xy 中是否有且仅有一个为负数。如果满足条件,则按照规定格式输出这两个数以及判断结果。请写出应填入____处的字句。

#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,并检查分配是否失败。这是这类题最标准的惯用法:
    if((a = (int *)malloc((*n) * sizeof(int))) == NULL) exit(0);
    

    记住这个"分配 + 判空"的搭配。 malloc 失败会返回 NULL,不判空就直接 a[i] = ... 会解引用空指针崩溃。

  • 4.1.3:循环读入,下界 0、上界 *n
  • 4.1.4:"有且仅有一个"就是异或。C 里关系表达式的值正好是 0/1,所以可以直接用 ^
    (a1[k1] < 0) ^ (a2[k2] < 0)
    
    真值表:一正一负 → 0 ^ 1 = 11 ^ 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)。

本题的三条易错

易错:

  1. 动态分配的数组必须自己 free(程序末尾的 free(a1); free(a2); 就是配套动作)——mainreturn 0 之后操作系统会回收,但做题时该写就得写,这是评分点。
  2. malloc 的参数是字节数,要写 n * sizeof(int),不是 n
  3. 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", &para);
        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) 就是本节考点:"分配 + 判空"的惯用法——
    if((device = (int *)malloc(n * sizeof(int))) == NULL) exit(0);
    

    本节的通用模板:指针 = (类型 *)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——因为 mainwhile(k<n) 循环体里根本没有 k++k 只能靠 InsDev 里的 (24.3) 经指针加上去。哪一次 para <= 0,函数就提前 returnk 不动,循环就再读一个数——这恰好实现了"小于等于 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(所以用完要判空)。

栈上变量自己消失,堆上内存必须手动 freemallocfree 一一配对。


说明

  • 材料里这一题没有附答案,(24.1)(24.5) 五处均由推导确定,并用 gcc 15.2.0 真机跑过两组输入(见解析末尾),输出符合"升序 + 跳过非正数"的要求。

(24.2) 也可以写成 para >= d[k] 等价的边界形式,判定逻辑相同。