跳转至

知识点 11 · 动态内存管理

本页最后更新于 ,内容可能过时

易错点 · 链表与动态内存的常见错误

这一节先列出结论,后面的题目再逐条展开。这几种错误在真题里出现过 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 字节指针,含对齐)。
  • 症状:编译不报错,小数据量下可能正常运行,数据量增大后崩溃,这类错误较难排查。

写 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 清零。


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,并检查分配是否失败。这是这类题常用的惯用法:
    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 = 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)。

本题的三条易错

易错:

  1. 动态分配的数组必须自己 free(程序末尾的 free(a1); free(a2); 就是配套动作)——main 里 return 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——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 会将分配的内存清零,而 malloc 不清零;malloc/calloc 分配失败时统一返回 NULL(所以用完要判空)。

栈上变量随作用域结束自动释放,堆上内存必须手动 free,malloc 与 free 一一配对。


说明

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

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