返回上一级
1531 字
8 分钟
链表(二)C 实现与头部插入

p5(13 分钟)、p6(12 分钟)

前置知识#

需要实用层面的 C/C++ 指针知识 + 动态内存分配概念。

建一个节点#

这一段的链表逻辑视图:3 个节点,地址分别是 200 / 100 / 300。

分配给每个节点的内存块地址完全是随机的,彼此没有关系,不能保证按升序降序排列或相邻,正因如此才需要保留链接。

NULL 是什么#

NULL 是「地址零的同义词或宏定义」,零是无效地址。

一个等于零或空值的指针变量,意味着该指针变量不指向有效的内存位置。

C 结构体定义#

struct node {
int data;
struct node *link; /* 也可叫 next */
};

C 风格必须写 struct node *;C++ 直接写 node *。C++ 的写法看起来更干净。

链表的标识#

始终持有一个「指向节点的指针」变量,存第一个节点(头节点)地址。示例变量名 A。

这个指向头节点或第一个节点的特定指针变量的名称,也可理解为链表的名称。

建空表两步#

struct node *A; /* 声明一个指向节点的指针 */
A = NULL; /* 未指向任何地方 */

创建并插入第一个节点(顺序必须对)#

/* ① malloc 创建内存块(运行期分配的动态内存) */
temp = (struct node *)malloc(sizeof(struct node));
/* ② 填数据、调链接 */
(*temp).data = 2;
(*temp).link = NULL;
/* ③ 最后一步:让头指针指向它 */
A = temp;

几个要点:

  • malloc 返回的是 void 指针,必须做类型转换(因为 temp 是 node *)
  • 处理动态内存的唯一方式是通过指针来引用此内存位置
  • temp 的作用是暂时存储节点的地址,直到我们恰当地固定所有的链接;之后 temp 可以挪作他用

箭头语法#

(*temp).data 等价于 temp->data

箭头 = 一个连字符 + 一个右尖括号。箭头写法更为常见。

C++ 建节点#

node *temp = new node; /* 也可用 malloc */

new 操作符总是比 malloc 更受青睐,用 C++ 推荐 new。

遍历到链表末尾的通用逻辑#

这是全课出现频率最高的一段代码:

temp1 = head;
while (temp1->link != NULL) /* 对最后一个节点,link == NULL 为假 → 退出 */
temp1 = temp1->link;
temp1->link = temp; /* 挂上新节点 */

打印元素只需把循环体换成 printf("%d ", temp1->data);。

一条核心纪律#

我们没有直接用变量 A 本身去遍历链表,因为如果修改 A 就会丢失头节点的地址。

A 永远不会被修改,无论哪个变量存储着头节点的地址都绝不会被修改,只有这些临时变量会被修改以便遍历链表。

所有链表代码都遵守这条纪律。

头部插入#

这一段是一个可运行的完整 C 程序。

节点定义与全局 head#

struct node {
int data;
struct node *link;
};
struct node *head; /* 声明在所有函数之外(全局) */

main 流程#

head = NULL;
打印「多少个数字」→ 读入 N
循环 N 次:
读入 X
insert(X)
print()

insert 实现顺序#

/* ① malloc 创建节点,用 temp 接收(假定地址 100) */
temp = (struct node *)malloc(sizeof(struct node));
/* ② 填数据 */
temp->data = x;
/* ③ 链接字段先置空 */
temp->next = NULL;
/* ④ 分两种情况 */
if (head == NULL) {
head = temp; /* 链表为空:头指针指向新节点 */
} else {
temp->next = head; /* 先让新节点指向原头节点 */
head = temp; /* 再让头指针指向新节点 */
}

一个重要简化#

第 ③ 行的 temp->next = NULL 只有当链表为空时才用得到。

因为空表时 head 本身就是 NULL,所以只写 temp->next = head 这一条语句就已经涵盖了链表为空的情形,从而避免写两条语句。

/* 简化版:一条语句覆盖两种情况 */
temp->next = head;
head = temp;
void print() {
struct node *temp; /* C 里必须写 struct node * */
temp = head;
while (temp != NULL) {
printf("%d ", temp->data);
temp = temp->next;
}
}

为什么要用临时变量:不想修改头指针,否则会丢失第一个节点的引用。

在 C 里老是容易漏掉 struct node * 的写法。

运行结果#

输入 5 个数,依次插入 2、5、8、1、10,每次插入后打印:

插入列表
22
55 2
88 5 2
11 8 5 2
1010 1 8 5 2

这似乎能行得通。

head 不是全局时会怎样#

若 head 在 main 内声明为局部变量,其他函数访问不到 head,必须把首节点地址作为参数传进去。

参数命名为 head(也可以是 A、temp 等)。

这个参数 head 是 print 的局部变量,与 main 里的 head 是两个不同的变量;main 调用时是把值复制过去。

因此在 print 里可以不用临时变量,直接用这个参数 head 遍历——这里并没有修改这个 head。

insert 函数#

head 参数只是一份副本,但修改链表后 main 里的 head 也应当被修改。两种处理办法:

路线一:返回指针

struct node *insert(struct node *head, int x) {
...
return head; /* 返回修改后的 head */
}
/* main 里: */
head = insert(head, x);

路线二:按引用传递(双重指针)

void insert(struct node **head, int x) {
...
}
/* main 里: */
insert(&head, x);

因为 head 本身就是「指向节点的指针」,所以插入函数要接收指向指针节点的双重指针(struct node **head),函数内到处要写解引用,返回类型为 void。

两条路线都实际跑通并展示过。

坑#

  • 绝不修改 head / A 本身,只用临时变量遍历;丢了头指针整条链就找不回来了。
  • malloc 必须做类型转换(C 里),否则编译器警告;C 里声明指针必须写 struct node *。
  • temp->next = NULL 在链表非空时是多余的,只写 temp->next = head 就能覆盖两种情况。
  • head 放全局还是传参要提前想清楚;传参就必须用「返回指针」或「双重指针」之一。
  • 传参时 head 是副本,函数内改了不影响 main 里的;NULL 就是地址 0,不是随便一个特殊值。
链表(二)C 实现与头部插入
https://me.622168.xyz/posts/data-structure/03-linked-list-c-head-insert/
作者
Shaw 的小屋
发布于
2026-09-18
许可协议
CC BY-NC-SA 4.0