C语言实现单向链表

首先声明:

笔者刚刚接触C语言,发现C语言与C#相比,太麻烦了。大部分功能都要自己去实现。最明显的例子就是关于可变长度的数组的问题。于是各种百度,找到这个问题的解决方案,就是用链表来实现。关于C语言实现链表,很多教科书上都有示例代码。笔者大致了解了一下基本思想,然后就自己实验出了本文中的代码。

所以,这篇blog不能算是研究成果吧,只能算是我自己的一点学习笔记。有不足的地方,还请各位指教。


1、开篇:

在C#中,如果要实现一个可变长度的数组,随时向数组中添加元素、删除元素,可以使用list类、hashtable类等集合类型即可。但是在C语言中,数组的长度在定义的时候就必须确定,那么数组长度怎么才能实现可变呢?

在数据结构一书中提到了一种数据类型:链表。它的指导思想是在第一个数据块中保存第二个数据块的地址,第二个数据块中又保存第三个数据块的地址。通过层层引用,直到最后一个数据块。如此一来,只要知道第一个数据块的地址,然后就可以通过各数据块中保存的下一个数据库的地址,来实现类似于长度可变得数组的功能。


既然是数据块,那么就必须要保存一种类型的数据。然后它还需要保存下一个数据块的地址,这样的一来,就需要在一种类型中保存两种类型的数据。因此,结构体,是用来做链表的理想类型。因为在结构体中,可以保存任意多种数据类型,当然包括了基本类型,复杂类型等,也包含了自身指针类型。


2、实例:

在本例中,我假设保存学生的信息(暂时只保存“名字”和“年龄”两个信息)。对于一个特定的班级,学生数量是一定的。可以用数组来实现。但是这样写代码,通用行不强,因为同一个学校中有很多个班级,每个班级中学生的数量也是不同的。如果使用数组,就必须把数组定义的足够大,能容纳下学生数量最多的班级的学生信息,但是这样也会造成内存的严重浪费。因此,使用“链表”,可以很方便的随时扩充“数组的元素”。

如此一来,我们就需要一个“学生”类型的结构体,一个指向第一个元素的指针head,一个能向下移动的指针p_student。

3、代码实现:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define Student  struct STU     //将struct STU 用STU代替
#define Volume sizeof(struct STU)

//定义学生信息结构体
struct STU{
    char name[16];
    int age;
    Student *Next;      //指向同类型的下一个数据;
};

int main()
{
    char identify[16] = {0};                  //接收用户输入的姓名
    int old = 0;                              //接收用户输入的年龄
    int no=0;                                 //用于指示现在接收的第几个数据

    Student *head = NULL;                     //<span style="font-family: Arial, Helvetica, sans-serif;">定义链表的头指针head</span>

    Student *p_student = NULL;                //<span style="font-family: Arial, Helvetica, sans-serif;">定义接收数据时候,新申请内存单元的指针</span>

    //输入数据
    do{
        printf("please input name: ---->less than 15 bytes ---->press Enter to quit\n");
        gets(identify);

        //判断输入的姓名是否正常,如果输入的是enter键,则退出
        if(identify[0] == '\0'){
            break;
        }else{
            printf("please input age: ---->less than 15 bytes\n");
            scanf("%d%*c",&old);

            //指示第几次获取用户输入
            no++;
            //如果是第一次申请内存,则先将申请到的内存首地址赋值给p_student,并将链表的首地址赋值给head;
            //否则将申请到的新内存地址,先赋值给p_student->Next,然后再把这个地址赋值给p_student;
            if(no ==1){
                p_student = malloc(Volume);
                head = p_student;
            }else{
                p_student = p_student->Next = malloc(Volume);
            }
            strcpy(p_student->name,identify);
            p_student->age = old;
            p_student->Next = NULL;
        }
    }while(1);

    printf("you have ended input\n");
    printf("in total, you have entered %d students\n\n",no);

    printf("starting readding all informations\n");
    if(head ==NULL){                                //如果head不为空,则表示这不是一个空链表
        printf("there is no data in the linker.");
    }else{
        p_student = head;                           //重置位置指针
        no = 0;
        do{
            no++;
            printf("No: %-5d\tName:%-15s\tAge:%-5d\n",no,p_student->name,p_student->age);
            if(p_student->Next ==NULL){         //判断元素的.Next成员是否为NULL。如果是,则表示已经读到最后一个元素,如果不为NULL,则表示后面还有元素。
                break;
            }else{
                p_student= p_student->Next;
            }
        }while(1);
        printf("All informations have been displayed here .enjoy yourself.\n");
    }
    printf("Program is ending...\nProgram has ended.\n");
    return 0;
}

4、代码陷阱分析:

4.1  -->  gets(identify);
这句代码的作用是从键盘获取输入的字符串。需要注意的是,如果不输入字符,而直接按下回车键的时候,是不会读取任何东西。不知道是我理解有误还是编译器的问题,实际实验的效果就是这样的。所以在下一条语句中,首先判断identity[0]是不是'\0';如果是'\0',就表示输入结束。那么就退出do循环。

4.2  -->  scanf("%d%*c",&old);
在句代码的作用是从键盘获取输入的数字,存放到变量old中。但是一定要注意:在输入完以后我们需要在按下回车键,以只是scanf()函数开始读取。这个时候,回车符‘\n’,保存在了输入缓冲区。而scanf()不会自动吸收掉这个回车符。等到下一次读取的时候,又会将这个回车符自动读取,然后造成gets(identify)语句读取错误。所以在这里,使用临时数据格式字符串%*来吸收掉多输入的‘\n’。

4.3 -->  if(no ==1)代码块
这里的想法是,在使用malloc()函数申请内存空间的时候,先把申请到的内存的地址赋值给上一个元素的.Next成员,然后把.Next的地址赋值给p_student,这样就可以让上一个元素指向下一个元素,又能很方便的使用一级指针访问新申请的内存空间。
但是问题就出现在第一个元素上。因为在输入循环第一次执行的时候,p_student的值为NULL,即不指向任何内存空间,也就是说没有没有任何内存空间来保存下一元素的地址。所以要先判断一下,是否是第一个元素。如果是,则先保存p_student的地址,如果不是,则就可以放心的把新申请的地址赋值给上一个元素的Next成员了。

4.5 -->  if(p_student->Next ==NULL) 代码块
这个代码的功能是判断当前这个元素是否还有下一个元素,如果有,则继续读下一个元素,如果没有,则表示已经读取完成,退出循环。

5、遗留问题:

程序写到这,可以正常运行了。但是先不要高兴的太早。因为这段代码中有一个很容易忽略的问题,那就是当我们使用完这些数据后,比如写入到文件,内存空间中这些数据其实就没用了。但是由于这些内存是我们手动申请的,操作系统是不会帮我们管理的。所以如果在最后我们不去手动释放这些内存的话,这些我们申请的内存空间,就回保持被占用的状态,而操作系统无法使用它。这样一来,内存就泄露了。所以,这个地方千万要注意,释放内存。

至于释放内存的方法,那就是是根据指针,去一个一个释放了。可以用循环来做,定义一个临时的指针来保存下一个元素的地址,然后使用free()函数释放掉上一个元素的内存。

这部分代码,就留个希望学习的朋友自己研究吧。

  • 1
    点赞
  • 0
    收藏
  • 打赏
    打赏
  • 0
    评论
二.内核链表 内核链表是一种链表,Linux内核中的链表都是用这种形式实现的 1.特性 内核链表是一种双向循环链表,内核链表的节点节点结构中只有指针域 使用内核链表的时候,将内核链表作为一个成员放入到一个结构体中使用 我们在链表中找到内核链表结构的地址,通过这个地址就可以找到外部大结构体的地址,通过大结构体就可以访问其中的成员 优势: 内核链表突破了保存数据的限制,可以用内核链表来保存任何数据(使用一种链表表示各种类型的数据,通用性很强) 内核链表中只有指针域,维护起来更加方便,效率更高 2.使用 内核链表在内核中已经被实现,我们只需要调用其接口直接使用即可 内核链表实现代码在内核源代码的list.h文件中 3.源代码分析 (1)节点结构: struct list_head { struct list_head *next, *prev;//前置指针 后置指针 }; (2)初始化 #define INIT_LIST_HEAD(ptr) do { \ (ptr)->next = (ptr); (ptr)->prev = (ptr); \ } while (0) (3)插入 //从头部插入 static inline void list_add(struct list_head *new, struct list_head *head)//传入要插入的节点和要插入的链表 { __list_add(new, head, head->next); } //从尾部插入 static inline void list_add_tail(struct list_head *new, struct list_head *head) { __list_add(new, head->prev, head); } (4)通过节点找到外部结构体的地址 //返回外部结构体的地址,第一个参数是节点地址,第二个参数是外部结构体的类型名,第三个参数是节点在外部结构体中的成员名 #define list_entry(ptr, type, member) ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member))) (5)遍历内核链表 //遍历内核链表 #define list_for_each(pos, head) \ for (pos = (head)->next; pos != (head); \ pos = pos->next) //安全遍历内核链表 #define list_for_each_safe(pos, n, head) \ for (pos = (head)->next, n = pos->next; pos != (head); \ pos = n, n = pos->next) 二.内核链表 内核链表是一种链表,Linux内核中的链表都是用这种形式实现的 1.特性 内核链表是一种双向循环链表,内核链表的节点节点结构中只有指针域 使用内核链表的时候,将内核链表作为一个成员放入到一个结构体中使用 我们在链表中找到内核链表结构的地址,通过这个地址就可以找到外部大结构体的地址,通过大结构体就可以访问其中的成员 优势: 内核链表突破了保存数据的限制,可以用内核链表来保存任何数据(使用一种链表表示各种类型的数据,通用性很强) 内核链表中只有指针域,维护起来更加方便,效率更高 2.使用 内核链表在内核中已经被实现,我们只需要调用其接口直接使用即可 内核链表实现代码在内核源代码的list.h文件中 3.源代码分析 (1)节点结构: struct list_head { struct list_head *next, *prev;//前置指针 后置指针 }; (2)初始化 #define INIT_LIST_HEAD(ptr) do { \ (ptr)->next = (ptr); (ptr)->prev = (ptr); \ } while (0) (3)插入 //从头部插入 static inline void list_add(struct list_head *new, struct list_head *head)//传入要插入的节点和要插入的链表 { __list_add(new, head, head->next); } //从尾部插入 static inline void list_add_tail(struct list_head *new, struct list_head *head) { __list_add(new, head->prev, head); } (4)通过节点找到外部结构体的地址 //返回外部结构体的地址,第一个参数是节点地址,第二个参数是外部结构体的类型名,第三个参数是节点在外部结构体中的成员名 #define list_entry(ptr, type, member) ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member))) (5)遍历内核链表 //遍历内核链表 #define list_for_each(pos, head) \ for (pos = (head)->next; pos != (head); \ pos = pos->next) //安全遍历内核链表 #define list_for_each_safe(pos, n, head) \ for (pos = (head)->next, n = pos->next; pos != (head); \ pos = n, n = pos->next) 二.内核链表 内核链表是一种链表,Linux内核中的链表都是用这种形式实现的 1.特性 内核链表是一种双向循环链表,内核链表的节点节点结构中只有指针域 使用内核链表的时候,将内核链表作为一个成员放入到一个结构体中使用 我们在链表中找到内核链表结构的地址,通过这个地址就可以找到外部大结构体的地址,通过大结构体就可以访问其中的成员 优势: 内核链表突破了保存数据的限制,可以用内核链表来保存任何数据(使用一种链表表示各种类型的数据,通用性很强) 内核链表中只有指针域,维护起来更加方便,效率更高 2.使用 内核链表在内核中已经被实现,我们只需要调用其接口直接使用即可 内核链表实现代码在内核源代码的list.h文件中 3.源代码分析 (1)节点结构: struct list_head { struct list_head *next, *prev;//前置指针 后置指针 }; (2)初始化 #define INIT_LIST_HEAD(ptr) do { \ (ptr)->next = (ptr); (ptr)->prev = (ptr); \ } while (0) (3)插入 //从头部插入 static inline void list_add(struct list_head *new, struct list_head *head)//传入要插入的节点和要插入的链表 { __list_add(new, head, head->next); } //从尾部插入 static inline void list_add_tail(struct list_head *new, struct list_head *head) { __list_add(new, head->prev, head); } (4)通过节点找到外部结构体的地址 //返回外部结构体的地址,第一个参数是节点地址,第二个参数是外部结构体的类型名,第三个参数是节点在外部结构体中的成员名 #define list_entry(ptr, type, member) ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member))) (5)遍历内核链表 //遍历内核链表 #define list_for_each(pos, head) \ for (pos = (head)->next; pos != (head); \ pos = pos->next) //安全遍历内核链表 #define list_for_each_safe(pos, n, head) \ for (pos = (head)->next, n = pos->next; pos != (head); \ pos = n, n = pos->next) C语言下的单链表,可以增加,删除,查找,销毁节点。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论

打赏作者

raynadofan

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值