• 指针应用-----链表一


    在于C语言指针的相关知识点算是已经学得差不多了,当然,语言的学习是一个终生的,所以还需慢慢去学习,今天就以一个非常经典,也是体现指针应用的一个例子,来操作练一下所学的指针相关的知识点-----链表

    对于链表,我想学过编程的应该都对它有比较清楚的了解,下面简单对它进行回顾一下:

    链表的基本操作:

    下面自己动手利用指针的知识一点一点来实现链表,同时学习一下C语言多文件的编译风格:

    第一步:搭建好基础开发框架:

    首先需要定义一个结构体,来代表一个结点,结点里面的数据域指针域两个构成,将其定义放到头文件(.h)中【至于放到.h头中的好处,请参考http://www.cnblogs.com/webor2006/p/3460345.html博文】如下:

    list.h:

    #ifndef _LIST_H_
    #define _LIST_H_
    
    typedef struct node
    {
        int data;
        struct node* next;
    } node_t;
    
    #endif /* _LIST_H_ */

    然后定义一个它的实现list:c,这里只去包含list.h文件,目前啥都不做,之后会慢慢去填充的:

    list:c:

    最后,再定义一个主入口文件,它会去包含list.h,也就是把具体实现放在list.c中,main.c只关心主干流程,目前也啥都不做:

    main.c:

    对于这个程序,由两个.c文件和一个.h文件组成,为了更方便去编译程序,这时需要用到Makefile【关于它的编写,会有专门篇幅来学习它,目前先简单理解下】如下:

    Makefile:

    好了,框架已经搭建完毕,下面进行编译,看能否正常生成可执行文件main

    第二步:实现链表的插入方法:

    首先定义一个头节点:

    main.c:

    然后定义一个插入方法:

    list.h:

    #ifndef _LIST_H_
    #define _LIST_H_
    
    typedef struct node
    {
        int data;
        struct node* next;
    } node_t;
    
    node_t* list_insert_front(node_t* head, int data);
    
    #endif /* _LIST_H_ */

    提示:对于这个函数,其实还有另外一种实现方法,可以不返回指针,直接用指针的指针去改为head的指针地址,这个之后会有实现。

    插入方法具体实现【关于链表的插入的基本概念这里就不多说了,就是将新的元素链接到前一个元素的next上】:

    list.c:

    #include "list.h"
    #include <stdlib.h>
    
    node_t* list_insert_front(node_t* head, int data)
    {
        node_t* n = (node_t*)malloc(sizeof(node_t));
        assert(n != NULL);
        n->data = data;
        n->next = NULL;
    
        if (head == NULL)
            head = n;
        else
        {
            n->next = head;
            head = n;
        }
    
        return head;
    }

    注意:这里是采用的头插法。

    然后这时多插入几个节点:

    main.c:

    #include "list.h"
    #include <stdio.h>
    
    int main(void){
        
        node_t* head = NULL;
        head = list_insert_front(head, 30);
        head = list_insert_front(head, 20);
        head = list_insert_front(head, 10);
    
        return 0;
    }

    对于上面的插入流程,用一个图例来解释一下这个插入方法的实现原理:

    第一次插入:head = list_insert_front(head, 30);

    第二次插入:head = list_insert_front(head, 20);

    第三次插入:head = list_insert_front(head, 10);

    接下来,为了验证结点是否插入正常,再实现第三步的方法。

    第三步:实现链表的遍历方法:

    首先定义遍历的方法,这里为了更好的实现,采用函数指针来实现,如下:

    list.h:

    #ifndef _LIST_H_
    #define _LIST_H_
    
    typedef struct node
    {
        int data;
        struct node* next;
    } node_t;
    
    typedef void (*FUNC)(node_t*);//函数指针,它专门是打印结点的
    
    node_t* list_insert_front(node_t* head, int data);
    
    void list_for_each(node_t* head, FUNC f);//最终这里面遍历到结点之后,回调打印函数,而不用将打印实现也放到这个遍历函数中,代码上更加整洁
    
    #endif /* _LIST_H_ */

    接着,我们来实现这个遍历的方法

    list.c:

    #include "list.h"
    #include <stdlib.h>
    
    node_t* list_insert_front(node_t* head, int data)
    {
        node_t* n = (node_t*)malloc(sizeof(node_t));
        assert(n != NULL);
        n->data = data;
        n->next = NULL;
    
        if (head == NULL)
            head = n;
        else
        {
            n->next = head;
            head = n;
        }
    
        return head;
    }
    //遍历链表
    void list_for_each(node_t* head, FUNC f)
    {
        while (head)
        {
            f(head);
            head = head->next;
        }
    }

    遍历方法中可能我们会这样来写:

    void list_for_each(node_t* head, FUNC f)
    {
      node_t* tempPoint = head;//定义一个临时变量去遍历,我们知道指针作为参数传递实际上是值传递,所以不用担心直接赋值会修改实参指针的指向,完全不需要这个临时变量
    while (tempPoint) { f(head); tempPoint = tempPoint->next; } }

    这种写法虽然也是可以的,但是有点多此一举,从另外一面来讲,是指针理解得不够透,所以避勉这样的写法!

    注意:我们将具体的打印函数放到main.c中,而不用写在list.c中,因为,这个函数最终是在main调用传递过去的。

    main.c:

    #include "list.h"
    #include <stdio.h>
    
    void print_node(node_t* n)
    {
        printf("data=%d ", n->data);
    }
    
    int main(void){
        
        node_t* head = NULL;
        head = list_insert_front(head, 30);
        head = list_insert_front(head, 20);
        head = list_insert_front(head, 10);
    
        list_for_each(head, print_node);//开始遍历
        putchar('
    ');
    
        return 0;
    }

    好了,遍历方法也已经写好了,接着编译运行来验证一下我们插入的结点是否生效了:

    于是在list.h中加入头文件:

    list.h:

    再次make:

    第四步:实现链表的销毁方法:

    接着,我们来实现链表的销毁方法,由于每个链表都是在堆上申请的,所以最后用完了肯定是需要销毁的,还是老规距,在头文件中定义接口:

    list.h:

    #ifndef _LIST_H_
    #define _LIST_H_
    
    typedef struct node
    {
        int data;
        struct node* next;
    } node_t;
    
    typedef void (*FUNC)(node_t*);
    
    node_t* list_insert_front(node_t* head, int data);
    
    void list_for_each(node_t* head, FUNC f);
    
    void list_free(node_t* head);//销毁链表
    
    #endif /* _LIST_H_ */

    具体实现,当然还是在list.c文件中:

    #include "list.h"
    #include <stdlib.h>
    #include <assert.h>
    
    node_t* list_insert_front(node_t* head, int data)
    {
        node_t* n = (node_t*)malloc(sizeof(node_t));
        assert(n != NULL);
        n->data = data;
        n->next = NULL;
    
        if (head == NULL)
            head = n;
        else
        {
            n->next = head;
            head = n;
        }
    
        return head;
    }
    
    void list_for_each(node_t* head, FUNC f)
    {
        while (head)
        {
            f(head);
            head = head->next;
        }
    }
    
    void list_free(node_t* head)
    {
        node_t* tmp = head;
        while (head)
        {
            head = head->next;
            free(tmp);
            tmp = head;
        }
    }

    释放方法也是需要遍历,但这次需要借助临时变量,其实现过程用简单的图来描述如下:

    这时,main.c调用之:

    #include "list.h"
    #include <stdio.h>
    
    void print_node(node_t* n)
    {
        printf("data=%d ", n->data);
    }
    
    int main(void){
        
        node_t* head = NULL;
        head = list_insert_front(head, 30);
        head = list_insert_front(head, 20);
        head = list_insert_front(head, 10);
    
        list_for_each(head, print_node);
        putchar('
    ');
    
        list_free(head);
        assert(head == NULL);//这里断言一下,看是否真正释放了
    
        return 0;
    }

    编译:

    如图上所示,在main.c中用到了assert,需要包含assert.h,我们知道main.c中包含了list.h文件,而list.c中已经包含了assert.h:

    这时,我们不应该在main.c中又再次包含assert.h,而应该将这个头文件由list.c中的包含放到list.h中,这样main.c又包含了list.h,所以就可以共用了:

    list.h:

    #ifndef _LIST_H_
    #define _LIST_H_
    
    #include <assert.h>//将list.c中的移到头文件中来,以便在main.c中可以共用
    
    typedef struct node
    {
        int data;
        struct node* next;
    } node_t;
    
    typedef void (*FUNC)(node_t*);
    
    node_t* list_insert_front(node_t* head, int data);
    
    void list_for_each(node_t* head, FUNC f);
    
    void list_free(node_t* head);
    
    #endif /* _LIST_H_ */

    这时,再次make:

    其实原因还是出在:指针作为参数传递是值传递

    看main.c,将head传递到list_free之后,由于list_free不会改变head的指向(当然如果是二级指针,那就没这个问题了),因为它是一级指针,所以,应该list_free最后需将head传回给main.c,然后再赋值给main.c中的head既可:

    list.h:

    #ifndef _LIST_H_
    #define _LIST_H_
    
    #include <assert.h>
    
    typedef struct node
    {
        int data;
        struct node* next;
    } node_t;
    
    typedef void (*FUNC)(node_t*);
    
    node_t* list_insert_front(node_t* head, int data);
    
    void list_for_each(node_t* head, FUNC f);
    
    node_t* list_free(node_t* head);//添加一个返回值
    
    #endif /* _LIST_H_ */

    list.c:

    node_t* list_free(node_t* head)
    {
        node_t* tmp = head;
        while (head)
        {
            head = head->next;
            free(tmp);
            tmp = head;
        }
        return head;//最终遍历完之后head会指向NULL
    }

    main.c:

    #include "list.h"
    #include <stdio.h>
    
    void print_node(node_t* n)
    {
        printf("data=%d ", n->data);
    }
    
    int main(void)
    {
        node_t* head = NULL;
        head = list_insert_front(head, 30);
        head = list_insert_front(head, 20);
        head = list_insert_front(head, 10);
    
        list_for_each(head, print_node);
        putchar('
    ');
    
        head = list_free(head);//由于一级指针的原因,需将head重新赋值才能改变它的指向,之后可用二级指针解决
        assert(head == NULL);
    
    
        return 0;
    }

    再次编译,运行:

    实际上,对于销毁方法的实现,还可以更精简,如下:

    node_t* list_free(node_t* head)
    {
        node_t* tmp;//这里不需要初始化
        while (head)
        {
            tmp = head;//里面的赋值也只要一句话既可
            head = head->next;
            free(tmp);
        }
        return head;
    }

    对于一个功能的实现,能用最精简的方法实现是最好的,能不多一行就不多一行代码,这也是我写代码一直追求的,好了,关于链表其它的操作,下回再分解,再见!

  • 相关阅读:
    【codecombat】 试玩全攻略 第二章 边远地区的森林 一步错
    【codecombat】 试玩全攻略 第十八关 最后的kithman族
    【codecombat】 试玩全攻略 第二章 边远地区的森林 woodlang cubbies
    【codecombat】 试玩全攻略 第二章 边远地区的森林 羊肠小道
    【codecombat】 试玩全攻略 第十七关 混乱的梦境
    【codecombat】 试玩全攻略 第二章 边远地区的森林 林中的死亡回避
    【codecombat】 试玩全攻略 特别关:kithguard斗殴
    【codecombat】 试玩全攻略 第二章 边远地区的森林 森林保卫战
    【codecombat】 试玩全攻略 第二章 边远地区的森林
    实验3 类和对象||
  • 原文地址:https://www.cnblogs.com/webor2006/p/3481080.html
Copyright © 2020-2023  润新知