阅读线索

先理解内存的分配和释放,再看指针如何组织数据,最后认识二级指针与函数指针。本文完整程序示例采用 C11 写法,不需要额外库。

01 动态存储分配

固定大小的数组适合容量已知的场景。程序需要根据运行时的数据量申请或调整空间时,可以使用 <stdlib.h> 中的内存管理函数。它们返回的 void * 可以在 C 中直接赋给相应的对象指针,无需强制类型转换。

四个常用的内存管理函数
函数用途使用前提
malloc分配指定字节数内容未初始化,读取前先赋值
calloc为一组元素分配空间将分配的存储空间按字节清零
realloc调整已有内存块的大小地址可能改变,新增长的部分未初始化
free释放已分配的内存传入有效的分配起始地址,或 NULL

内存分配可能失败。先检查返回值,再通过指针访问对象。malloc 不负责清零,但不能据此断定它在所有实现和场景中都比 calloc 更快。

C / C11
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *value = malloc(sizeof *value);

    // 分配失败时先处理错误,再使用指针。
    if (value == NULL) {
        fputs("无法为整数分配内存。\n", stderr);
        return EXIT_FAILURE;
    }

    *value = 42;
    printf("value = %d\n", *value);

    free(value);
    value = NULL;
    return EXIT_SUCCESS;
}

02 动态分配字符串

C 字符串以 '\0' 结尾。为长度为 n 的字符串分配空间时,要预留 n + 1 个字节;长度用 size_t 表示。拼接两个字符串时,还要检查长度相加是否溢出。

下面的 concat 不修改两个输入字符串,而是创建一个新字符串。发生输入或分配错误时返回 NULL,由调用者决定如何处理。

C / C11
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static char *concat(const char *left, const char *right) {
    if (left == NULL || right == NULL) {
        return NULL;
    }

    size_t left_len = strlen(left);
    size_t right_len = strlen(right);

    // 先检查长度相加是否溢出,并为结尾的 '\0' 留一个字节。
    if (left_len == SIZE_MAX ||
        right_len > SIZE_MAX - left_len - 1) {
        return NULL;
    }

    char *result = malloc(left_len + right_len + 1);
    if (result == NULL) {
        return NULL;
    }

    memcpy(result, left, left_len);
    memcpy(result + left_len, right, right_len + 1);
    return result;
}

int main(void) {
    char *message = concat("Hello, ", "C!");

    if (message == NULL) {
        fputs("字符串拼接失败。\n", stderr);
        return EXIT_FAILURE;
    }

    puts(message);
    // 返回的字符串归调用者所有,由调用者负责释放。
    free(message);
    return EXIT_SUCCESS;
}
指针数组与二维数组

char *reminders[50] 保存的是 50 个指针,每个元素可以指向单独分配的字符串。它与存储字符本身的二维数组不同。分配长度应按实际内容计算;用完后逐条释放已经成功分配的字符串。

03 动态数组与 realloc

数组申请的字节数是“元素数 × 单个元素大小”。使用 sizeof *values 可以让分配表达式跟随指针类型变化。乘法前检查范围,避免整数溢出导致实际分配空间过小。

realloc 扩容可能移动内存。用临时指针接收结果:当非零大小的分配失败时,原内存仍有效;成功后再更新原指针,并重新建立指向数组内部的指针。新增元素需要单独初始化。

C / C11
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    size_t count = 4;
    int *values = calloc(count, sizeof *values);

    if (values == NULL) {
        fputs("数组分配失败。\n", stderr);
        return EXIT_FAILURE;
    }

    values[0] = 10;
    size_t new_count = 8;

    if (new_count > SIZE_MAX / sizeof *values) {
        fputs("数组大小超出可分配范围。\n", stderr);
        free(values);
        return EXIT_FAILURE;
    }

    // 用临时指针接收结果,避免失败时丢失原来的内存地址。
    int *next = realloc(values, new_count * sizeof *values);
    if (next == NULL) {
        fputs("扩容失败,原数组仍然有效。\n", stderr);
        free(values);
        return EXIT_FAILURE;
    }

    values = next;
    // realloc 不初始化新增部分,使用前需要显式赋值。
    for (size_t i = count; i < new_count; ++i) {
        values[i] = 0;
    }
    count = new_count;

    for (size_t i = 0; i < count; ++i) {
        printf("%d%c", values[i], i + 1 == count ? '\n' : ' ');
    }

    free(values);
    return EXIT_SUCCESS;
}
需要释放时显式使用 free

不要依赖 realloc(ptr, 0) 释放内存,不同标准版本与实现的规则存在差异。需要释放时调用 free(ptr);realloc(NULL, 非零大小) 则可以用于申请新的内存。

04 释放存储空间与所有权

free 的函数原型是 void free(void *ptr);。分配得到的空间应有明确的所有者,并在最后一次使用后释放。丢失唯一的地址记录,内存就无法再由程序主动释放,形成内存泄漏。

  • 释放前保留需要使用的其他地址,例如链表中的下一个节点。
  • 不能释放栈上变量、字符串字面量、内存块内部地址或已经释放过的地址。
  • free(NULL) 不执行任何操作,可以用于失败路径的统一清理。
  • 释放后不再解引用旧地址,也不重复释放。必要时将持有者设为 NULL,让状态更清晰。
留意别名指针

把一个指针设为 NULL,不会同步改变其他指向同一内存的指针。其他别名也必须停止使用;否则仍可能访问已经释放的对象。

05 用指针组织链表

链表节点保存数据和下一个节点的地址。新增节点时按需申请空间,无需提前选择整个列表的容量,但每次分配仍有可能失败。

下面以按数值升序排列的链表为例,实现插入、查找和释放。插入时先寻找目标位置,再申请节点;退出前遍历释放全部节点。

C / C11
#include <stdio.h>
#include <stdlib.h>

typedef struct node {
    int value;
    struct node *next;  // 指向下一个节点;末尾节点为 NULL。
} Node;

static int insert_sorted(Node **head, int value) {
    if (head == NULL) {
        return 0;
    }

    // link 指向待更新的链接,统一处理头部、中间和尾部插入。
    Node **link = head;
    while (*link != NULL && (*link)->value < value) {
        link = &(*link)->next;
    }
    if (*link != NULL && (*link)->value == value) {
        return 0;  // 本示例不接收重复值。
    }

    Node *node = malloc(sizeof *node);
    if (node == NULL) {
        return 0;
    }

    node->value = value;
    node->next = *link;
    *link = node;
    return 1;
}

static const Node *find_node(const Node *head, int value) {
    while (head != NULL && head->value < value) {
        head = head->next;
    }
    return head != NULL && head->value == value ? head : NULL;
}

static void free_list(Node **head) {
    if (head == NULL) {
        return;
    }

    // 先保留下一个节点的地址,再释放当前节点。
    while (*head != NULL) {
        Node *next = (*head)->next;
        free(*head);
        *head = next;
    }
}

int main(void) {
    Node *head = NULL;
    const int numbers[] = {20, 5, 11};

    for (size_t i = 0; i < sizeof numbers / sizeof numbers[0]; ++i) {
        if (!insert_sorted(&head, numbers[i])) {
            fputs("插入失败:内存不足或数值重复。\n", stderr);
            free_list(&head);
            return EXIT_FAILURE;
        }
    }

    for (const Node *node = head; node != NULL; node = node->next) {
        printf("%d\n", node->value);
    }

    const Node *found = find_node(head, 11);
    if (found != NULL) {
        printf("找到节点:%d\n", found->value);
    }

    free_list(&head);
    return EXIT_SUCCESS;
}

在字符串提醒列表或零件记录中,也可以采用相同的组织方式。区别是节点的数据成员更多,需要为输入长度、重复编号和数量范围分别制定校验规则。

06 指向指针的指针

指针作为函数参数时,同样按值传递。函数接收 Node *head,可以修改它指向的节点,但只给局部变量 head 赋一个新地址,不会改变调用者的头指针。

要修改调用者保存的指针,就传递这个指针变量的地址。上例使用 Node **head,并以 insert_sorted(&head, value) 调用。

链表插入函数中三层表达式的含义
表达式指向或表示什么
head调用者头指针变量的地址
*head调用者当前保存的首节点地址
(*head)->value首节点的数据,需要先确认 *head 非空

Node **link 进一步把“头指针”和“某个节点的 next 指针”统一为待修改的链接。执行 *link = node,就能连接新节点,减少头部、中间与尾部插入的分支。

07 指向函数的指针

函数指针保存可调用的函数地址。它的返回类型和参数类型必须匹配目标函数,可用于回调和按条件选择处理方式。用 typedef 为函数指针类型命名,通常更容易阅读。

C / C11
#include <stdio.h>
#include <stdlib.h>

typedef int (*Operation)(int, int);

static int add(int left, int right) {
    return left + right;
}

static int subtract(int left, int right) {
    return left - right;
}

int main(void) {
    // 函数指针的参数和返回类型必须与目标函数匹配。
    Operation operation = add;
    printf("12 + 7 = %d\n", operation(12, 7));

    operation = subtract;
    printf("12 - 7 = %d\n", operation(12, 7));
    return EXIT_SUCCESS;
}

Operation 表示“接收两个 int 并返回 int 的函数指针”。上例中的数值计算不会超出 int 范围;用于外部输入时,还要为运算溢出制定处理策略。

SOURCE & REFERENCE

参考与整理

本文根据学习资料《指针的高级应用》整理,参考《C 语言程序设计:现代方法》。示例补充了分配失败处理、长度检查和资源释放,并修正了资料中的部分笔误。

延伸阅读:realloc 的接口规则
回到顶部