前言
在笔者学习《大话数据结构》一书,编写C语言代码实现相应的数据结构、完成对数据的增删改查等功能时,对文章中C语言中指针(*和&符号)的使用缺少透彻的认识和理解。因此笔者在本篇文章中,将单链表的实现过程作为案例,对单链表实现过程中C语言指针的使用进行详细的讲解和分析,以此加深对C语言中指针使用的实践理解。
一、 声明单链表存储结构
在对单链表进行初始化前,需要对单链表的存储结构给出定义,已知单链表由两部分组成:存储数据元素信息的数据域+存储直接后继位置的指针域组成。线性表的单链表存储结构代码定义如下:
typedef int ElemType;
typedef struct Node
{
ElemType data;
struct Node *next;
}Node;
typedef struct Node *LinkList;
在C语言中,指针是一种特殊的变量,指针存储变量的地址而不是变量的值。
而一元运算操作符*
是间接的、非关联的操作运算符。可以使用*
来声明变量的指针类型。
当在指针变量前使用*
时,可以得到指针变量所指向的变量的值。
示例代码中:*LinkList
即申明了LinkList
是指针类型的变量,其存储的变量的数据结构是Node
类型,Node
的数据结构即为单链表的结点结构由数据域和指针域组成。
申明指针变量时,以下三种申明方式都能达到相同的效果:
typedef struct Node* LinkList;
typedef struct Node *LinkList;
typedef struct Node * LinkList;
三、单链表初始化
申明好单链表的存储结构后,对单链表进行初始化操作。单链表初始化代码如下:
int InitLinkList(LinkList *L){
*L = (LinkList)malloc(sizeof(Node));
if (!(*L)) {
return 0;
}
(*L)->next = NULL;
return -1;
}
主程序调用初始化操作如下:
int main(int argc, const char * argv[]) {
LinkList L;
ret = InitLinkList(&L);
}
使用一元操作符
&
能够返回对象在存储空间的存储地址
示例代码中:
&L
实际上是将指针变量LinkList
的存储地址(注意:这里传入的是指针在内存空间的地址,而不是其存储的变量在存储空间的地址)
传入InitLinkList
函数后,使用*L
可通过指针的地址,访问到指针变量存储的结点Node
的地址。
由于指针变量LinkList
只进行了变量声明,未指向任何Node
数据结构的变量即其存储的变量地址为空。
因此需要在初始化函数中,使用malloc
函数,从内存空间中划分了一段Node
大小的存储空间,并将该存储空间的地址赋值给指针。使其指向一个Node
类型的变量。
malloc()函数能够为指针变量保留指定字节数的内存块,并返回指定指针类型的指针。而指针变量则保留分配内存中的第一个字节的地址。
二、单链表的插入
单链表的插入操作,如图所示:只需要:
- p的后继结点改成s的后继结点
- 再把结点s变成p的后继结点
《大话数据结构》一书中,单链表的插入代码如下:
int LinkListInsert(LinkList *L, int i, ElemType e)
{
int j;
LinkList p,s;
p = *L;
j = 1;
while (p && j < i) { /*遍历寻找第i-1个结点*/
p = p->next;
++j;
}
if (!p || j > i) {
return 0; /*第i个结点不存在*/
}
s = (LinkList)malloc(sizeof(Node)); /*生成新结点(C标准函数)*/
s->data = e;
s->next = p->next; /*将p的后继结点赋值给s的后继*/
p->next = s; /*将s赋值给p的后继*/
return 1;
}
主程序调用初始化操作如下:
int main(int argc, const char * argv[]) {
LinkList L;
int e;
e = 77;
// 在线性表第一个位置插入元素
i = 1;
ret = LinkListInsert(&L, i, e);
}
在插入代码中:
语句LinkList p, s
申明了指针p
和指针s
,让p
等于单链表的头结点,再给指针s
指向的变量分配一个节点的内存空间,即s
为新的需要插入的结点。已知结点由:数据域+指针域组成,s->data
即结点的数据域,s->next
即结点的指针域,用来保存下一个结点在存储空间中的地址。
三、单链表的读取
获取链表第i个数据的算法思路:
- 声明一个指针p指向链表第一个结点,初始化j从1开始
- 当j<i时,遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1
- 若到链表末尾p为空,则说明第i个结点不存在
- 否则查找成功,返回结点p的数据
《大话数据结构》一书中,单链表的查找代码如下:
int GetLinkListElem(LinkList L, int i, ElemType *e){
int j;
LinkList p; /*声明一指针p*/
p = L->next; /*让p指向链表的头结点*/
j = 1; /*j为计数器*/
while (p && j < i) {
p = p->next;
++j;
}
if (!p || j>i) {
return 0; /*第i个结点不存在*/
}
*e = p->data; /*取第i个结点的数据*/
return 1;
}
这一段代码中,在对C语言指针的了解不是很透彻的时候,让笔者感到困惑的是,在函数LinkListInsert
代码中传入函数的是&L
,而在GetLinkListElem
只传入了L
,这两种指针的用法的区别是什么?
两者用法实际上能够实现相同的效果:
当完成InitLinkList
后,此时指针L
已经指向了存在在内存空间中的结点,即指针变量L
的值为结点在内存空间的地址,通过结点地址可获取结点的数据域与指针域
而*&L
也能实现相同的效果(获取结点的地址):
- 通过
&L
得到的是指针在内存空间的地址 - 使用
*&L
可获取指针存储的其他变量的地址,即结点在内存空间的地址
而在实际的应用中,当需要改变链表的结构如增加结点、删除结点时,可以将指针的地址传入函数中,加以应用。
而当只需查询链表信息时不会改变链表结构时,可只将指针指向的变量地址传入函数中,进行查询操作,从而避免代码发生错误时导致指针地址变化,丢失原有的指针信息。
四、单链表的删除
删除单链表结点算法思路:
- 将p的后继结点指向q的后继结点
int LinkListDelete(LinkList *L, int i, ElemType *e)
{
int j;
LinkList p, q;
p = *L;
j = 1;
while (p->next && j < i) { /*遍历寻找第i-1个结点*/
p = p->next;
j++;
}
if (!(p->next) || j > i) {
return 0; /*第i个结点不存在*/
}
q = p->next;
p->next = q->next; /*将q的后继赋值给p的后继*/
*e = q->data; /*将q结点中的数据给e*/
free(q); /*让系统回收此结点, 释放内存*/
return 1;
}
单链表的删除中与指针相关与上文重复的知识内容,笔者在这里就不再赘述,而其中值得注意的语句free(q)
实现了释放指针指向的内存地址即结点的分配空间,即回收了结点在内存空间的地址,释放了内存。
free()函数释放指针指向的内存中分配的空间。
总结
本文仅供大家参考,有不对的地方还请读者指出,一起学习共同进步
参考文献
大话数据结构
Programiz:Learn C Programming.
The C Programming Language