链表删除与插入节点行为差异:为何delete_from_list可修改头指针?
我正在学习King所著《C语言程序设计:现代方法》(第二版,2008)中的链表章节,对删除和插入操作的行为差异有疑惑。作者在第429页提到:add_to_list不会修改传入的链表指针,而是返回新创建节点的指针,要让add_to_list直接更新first指针难度很大。但我发现,删除首节点时不会改动原链表,删除中间或末尾节点却会修改原链表;而且delete_from_list同样复制了first指针,为什么它能修改first的指向,add_to_list却做不到?我忽略了什么细节?
以下是相关代码示例:
#include <stdio.h> #include <stdlib.h> struct node { int value; struct node *next; }; struct node *delete_from_list(struct node *, int); struct node *add_to_list(struct node *, int n); int main(int argc, char **argv) { // setup a linked list // list's head struct node *first= NULL; // first node struct node *new_node= malloc(sizeof(struct node)); new_node->value= 10; new_node->next= first; first= new_node; //second node new_node = malloc(sizeof(struct node)); new_node->value= 20; new_node->next= first; first= new_node; //third node new_node= malloc(sizeof(struct node)); new_node->value= 40; new_node->next= first; first= new_node; //fourth node new_node= malloc(sizeof(struct node)); new_node->value= 30; new_node->next= first; first= new_node; int i; struct node *head= first; printf("\n------------------------\n"); printf(" Original nodes: "); for(i=0; head!= NULL; head= head->next, i++) printf("\n%d-ith value: %d ", i, head->value); printf("\n------------------------\n"); struct node *first_no20= delete_from_list(first, 20); struct node *head_no20= first_no20; printf("\n------------------------\n"); printf(" Nodes without 20: "); for(i=0; head_no20!= NULL; head_no20= head_no20->next, i++) printf("\n%d-ith value: %d ", i,head_no20->value); printf("\n------------------------\n"); printf("\n------------------------\n"); head=first; printf(" Original nodes: "); for(i=0; head!= NULL; head= head->next, i++) printf("\n%d-ith value: %d ", i, head->value); printf("\n------------------------\n"); struct node *first_no30= delete_from_list(first, 30); struct node *head_no30= first_no30; printf("\n------------------------\n"); printf(" Nodes without 30: "); for(i=0; head_no30!= NULL; head_no30= head_no30->next, i++) printf("\n%d-ith value: %d ", i,head_no30->value); printf("\n------------------------\n"); printf("\n------------------------\n"); printf(" Original nodes: "); head=first; for(i=0; head!= NULL; head= head->next, i++) printf("\n%d-ith value: %d ", i, head->value); printf("\n------------------------\n"); return 0; } struct node *delete_from_list(struct node *list, int n) { struct node *cur, *prev; for(cur=list, prev=NULL; cur != NULL && cur->value !=n; prev= cur, cur= cur->next) ; if(cur == NULL) return list; if(prev== NULL) list= list->next; else prev->next= cur->next; free(cur); return list; } struct node *add_to_list(struct node *list, int n) { struct node *new_node; new_node= malloc(sizeof(struct node)); if(new_node == NULL) { printf("Error: malloc failed in add_to_list\n"); exit(EXIT_FAILURE); } new_node->value = n; new_node->next= list; return new_node; }
核心原因:C语言的值传递机制
C语言中所有函数参数都是值传递,函数拿到的是传入参数的副本,而非原变量本身。这是理解两者行为差异的关键。
1. add_to_list的行为本质
add_to_list接收的list是原first指针的副本。函数内部创建新节点后,让新节点的next指向该副本的指向,但无法修改原first变量的内容——因为副本和原变量是完全独立的两个指针。因此必须返回新节点的地址,由调用者手动将原first赋值为这个返回值,才能更新链表头。
比如直接调用add_to_list(first, 5)不会改变原first,必须写first = add_to_list(first, 5)才会生效。
2. delete_from_list的两种操作场景
delete_from_list同样接收list作为原first的副本,但操作分为两种情况:
- 删除中间/末尾节点:此时修改的是
prev->next——这是链表中某个节点的成员指针,并非函数参数list本身。prev指向原链表中的节点(指针副本指向同一块内存区域),所以修改prev->next会直接改动原链表的结构。 - 删除首节点:函数内部将
list(副本)指向list->next,但这个修改仅在函数内部有效,原first不会自动更新。因此函数必须返回修改后的list,调用者需要手动将原first赋值为返回值(如first = delete_from_list(first, 30)),否则原first仍指向已被释放的节点,会出现野指针问题。
你代码中删除20后原链表被修改,是因为删除的是中间节点,改动的是链表节点内部的next指针;而删除30(首节点)后,原first仍指向已释放的内存,这属于未定义行为——正确做法是将返回值赋值给first,让原指针指向新的链表头。
你忽略的关键细节
- 无论是
add_to_list还是delete_from_list,都无法直接修改原first变量,必须通过返回值让调用者手动更新。 - 删除中间/末尾节点时,修改的是链表节点的成员(而非函数参数指针本身),因此会影响原链表;而插入操作是创建新节点,需要更新的是链表头指针本身,只能通过返回值传递。
- 你代码中删除首节点后未更新原
first,会导致野指针,这是错误的用法。
内容的提问来源于stack exchange,提问作者utobi

