标题:国庆前第二波:学C语言再谈链表学习,果断送分帖。
只看楼主
hellovfp
Rank: 16Rank: 16Rank: 16Rank: 16
等 级:禁止访问
威 望:30
帖 子:2976
专家分:7697
注 册:2009-7-21
结帖率:98.48%
已结贴  问题点数:100 回复次数:48 
国庆前第二波:学C语言再谈链表学习,果断送分帖。
学C语言再谈链表学习:

  相信学习过C语言和数据结构的人,都会学习链表。N多的同学都会做链表练习。不过都是基于现成数据结构一书提示的代码,不过它们并不通用,表现在一个程序中你不能使用包含多数固定格式结构的链表。现在,让我们百尺竽头,更进一步学习C中的链表,让我们一起来看看C中的通用链表的一种做法:

  一、用面象对象的思想重新设计我们的链表
  试想想链表中哪些成为我们的设计要素呢?观察一下链表,链表的元素个数是链表的一个组成部分,链表的节点指针也是链表的一个组成部分,链表中的节点数据中包含我们的真实数据,从这些现象,我们上升到一个层次来看刚才所说有这三点,元素个数、节点指针、节点数据成了我们关注的对象,抽象的提炼就是这么来的,所以根据这些抽象的结果,我们可以这样用C来定义节点,放入list.h中:

/**
 *  链表结点定义
 */
typedef struct tag_node
{
    void *data;             //结点数据指针
    struct tag_node *next;  //结点指针
}node, *Node;

  二、void指针在链表中的应用
  我们学习过C语言指针的同学都知道,在C指针中有一个特殊的东西,void*,这个东西可以保存任意类型数据的地址,所以它就是我们通用链表的核心部件,void指针在没有附值初始化前,是指向没有意义的数据的,另外我们也要注意的是,使用void*做为我们的结点数据指针所带来的一个问题,如何保证客户添加进链表中的元素是同一类元素?这个问题我先提在这里,大家思考一下。

  三、数据隐藏与封装
  链表中的一些元素,我们不希望用户进行修改,怎么办?把它们隐藏进来!如元素个数,这是表示链表元素的属性,不能让用户随意修改,节点指针,我们也不希望用户直接访问,所以我们用一个如下的结构来进行数据隐藏,本例中我们把它放入一个叫list_impl.h的头文件中,并不提供给最终的用户,所以用户其实不知道我们的链表有些什么属性,我们只提供公用接口函数用于访问链表的各种功能。
------------------------
/**
 *  链表私有数据,对用户隐藏
 */
typedef struct list_private
{
    Node head;                      //头节点
    Node tail;                      //尾节点

    unsigned int size;              //元素个数
    unsigned int data_size;         //元素数据大小

} lpri, *Lpri;

----------------------------
为了让我们的链表知道结点指针所指向的数据,它的大小是多少,需要一个新的成员来描述,也就是上面的data_size。

  四、将上面两个部分集合进来,型成我们的通用链表,放入list.h中
请看下面的结构定义:
typedef struct tag_list
{
    void *lprivate;          //私有数据
}clist, *List;

看起来是不是怪怪的?呵呵,对于用户来说,只需要使用这个自定义类型List既可。它可以做什么?可以让你的程序使用多个带数据的结构体,所有的链表操作如创建、添加、删除、排序这些过程在代码中都是相用的,但你没有写重复代码就实现了,这就是程序设计的思考所带来的 效果。下面让我们看一看链表是如何创建和销毁的。

#include "list.h"
#include "list_impl.h"


/**
 *  功能:链表创建
 *  参数:data_size -- 元素数据大小,必须大于0
 *        destructor -- 元素析构函数指针,用于释放动态元素分配.
 *  返回:已初始化链表指针
 *  备注:链表本身动态分配,由list_destroy函数管理释放, 在元素
 *        内数据为非动态分配情况下,destructor 可为空指针,
 */
List list_create(unsigned int data_size, list_destructor destructor)
{
    List list = NULL;

    assert(data_size > 0);
    list = NEWC(1, clist);

    if(list != NULL)
    {
        Lpri lp;                                //链表私有数据
        Node head;                              //空的头节点

        lp   = NEWC(1, lpri);
        head = NEWC(1, node);

        if( head && lp )                        //头节点和私有数据内存分配成功, 初始化私有数据
        {
            lp->head = head;
            lp->tail = head;
            lp->data_size     = data_size;
            lp->destruct_func = destructor;
            list->lprivate    = lp;

            //set_error(LIST_OK);
            return list;
        }
    }

    //set_error(LIST_MEM_FAILE);
    return list;
}

-----------------------------------------------------------------
/**
 *  功能:释放链表占用的内存
 *  参数:list -- 链表指针
 *  返回:空链表指针。
 */
List list_destroy(List list)
{
    assert(list);

    if(list != NULL)
    {
        Node next;
        Lpri lp = (Lpri) list->lprivate;
        assert(lp);

        while( lp->head )
        {
            next = lp->head->next;

            if( lp->head->data )                //如果是头节点就不释放数据空间
            {
                if( lp->destruct_func )
                {
                    lp->destruct_func( lp->head->data );    //调用元素析构函数
                }
                free(lp->head->data);                //先释放节点数据
            }

            free(lp->head);                    //再释放节点
            lp->head = next;
        }

        free(list->lprivate);                                //释放私有数据空间,及链表本身
        free(list);
        list = NULL;

        set_error(LIST_OK);
        return list;
    }
    set_error(LIST_IS_NULL);
    return list;
}

  五、回调函数与链表
  由于使用的是void指针,所以所指向的数据有可能是使用malloc动态分配的,为了避免链表释放时产生内存泄露问题,我们使用了回调函数来解决这个问题,看下面的定义:
/**
 *  回调函数定义
 *      compare         -- 数据比较函数指针
 *      foreach_func    -- 遍历时处理节点数据函数指针
 *
 *  参数:ndata  -- 节点数据指针
 *         param -- list_for_each函数附加参数指针
 */
typedef int  (*compare)(const void *ndata, const void *data);
typedef void (*list_destructor)(void *ndata);
typedef void (*foreach_func)(void *ndata, void *param);

通过typedef void (*list_destructor)(void *ndata);这句指名一个函数指针,让用户自定义一个函数用于释放动态分配的数据,这个指针也可以指向空,表示用户的数据结构中不含有动态分配的数据,注意看list_destroy是如何处理这个问题的,相当于C++的析构函数。

  六、插入元素与遍历示例代码
/**
 *  功能:添加链表结点 (后插法)
 *  参数:list -- 链表指针,data -- 链表数据指针,任意数据类型
 *  返回:int值,0表示成功,1表示失败
 *  备注:不能添加不同类型的元素,否则链表操作会崩溃
 */
int    list_add_back(List list, const void *data)
{
    assert(list);
    assert(data);

    if( list && data )
    {
        Lpri lp = (Lpri) list->lprivate;
        assert(lp);

        if( lp )
        {
            Node new_node = NEWC(1, node);

            if( new_node )
            {
                new_node->data = NEW(lp->data_size, char);          //分配节点数据

                if( new_node->data )
                {
                    memcpy(new_node->data, data, lp->data_size);    //拷贝数据

                    lp->tail->next = new_node;                //添加节点
                    lp->tail = new_node;                //记录尾节点位置
                    lp->size ++;                    //链表元素总数加1

                    set_error(LIST_OK);
                    return 0;
                }
            }
            set_error(LIST_MEM_FAILE);
            return 1;
        }
    }
    set_error(LIST_IS_NULL);
    return 1;
}

注意上面memcpy的这行,是如何拷贝结点数据到结点中的,如果你真的理解了这行,表示你明白"对象"在C中是如何表现的。

---------------------------------------------------------------------------------------
/**
 *  函数:list_for_each
 *  功能:遍历链表元素
 *  参数:list -- 链表指针,pfunc为指向遍历处理数据的函数指针,
 *        param -- 附加参数指针。可以为空指针
 *  返回:int值,0表示成功,1表示失败
 *  备注:doit申明为void (*foreach_func)(void *ndata, void *param)原型
 *         param参数传到处理函数中。
 */
int list_for_each(List list, foreach_func pfunc, void *param)
{
    assert(list);

    if( list )
    {
        Lpri lp = (Lpri) list->lprivate;
        assert(lp);

        if( lp )
        {
            Node i;

            for(i = lp->head->next; i; i = i->next)
            {
                pfunc(i->data, param);
            }
            set_error(LIST_OK);
            return 0;
        }
    }
    set_error(LIST_IS_NULL);
    return 1;
}

详细代码请见附件。

总结:通过本通用链表一些知识学习,我们可以学习到C语言接口与实现的分开,面象对象的抽象思维方式,丰富我们的程序视野,学习代码背后的思想,是每个程序员应该去思考的事情,不能光看代码的表面。最后说一下第二点的思考,使用void指针创建的通用链表可能存在的问题 ,如果用户添加进链表中的元素不是同一类的结构体,那么程序将可能出错,例如一个4字节的结构体的链表,添加了一个32位指针元素进来, 我们可以在链表中添加一个属性int type_id,在创建和添加链表中进行验证,强制用户提供元素必须是同一类型的元素,这样就解决了void指 针所带有的这个问题。OK,感谢你观看本文,如果你能从文章中学到一些什么你感兴趣的东西,本人将不慎荣幸。


Genral_list.rar (8.2 KB)


搜索更多相关主题的帖子: 学习 C语言 通用 元素 
2011-09-30 13:21
小鱼儿c
Rank: 9Rank: 9Rank: 9
等 级:贵宾
威 望:14
帖 子:852
专家分:1317
注 册:2011-4-1
得分:20 
一定要定起 。果断‘结婚’

用心做一件事情就这么简单
2011-09-30 13:30
BlueGuy
Rank: 16Rank: 16Rank: 16Rank: 16
等 级:版主
威 望:29
帖 子:4476
专家分:4055
注 册:2009-4-18
得分:20 
模板不是 面向对象 的一部分,属于通用编程方式..

list_add_back(List list, const void *data)
// 这一句是不是多余了,memcpy(new_node->data, data, lp->data_size);    //拷贝数据
// 直接 new_node->data = data, 不就完了?

[ 本帖最后由 BlueGuy 于 2011-9-30 14:15 编辑 ]

我就是真命天子,顺我者生,逆我者死!
2011-09-30 13:46
hellovfp
Rank: 16Rank: 16Rank: 16Rank: 16
等 级:禁止访问
威 望:30
帖 子:2976
专家分:7697
注 册:2009-7-21
得分:0 
回复 3楼 BlueGuy
思考一下,这么说吧,如果用户在一个函数体内声名的结构变量,假如使用new_node->data = data这句,出了函数体后,数据还存在么?
所以memcpy(new_node->data, data, lp->data_size);    //拷贝数据
这行拷贝数据(或称作对象复制)是必须的。


[ 本帖最后由 hellovfp 于 2011-9-30 14:50 编辑 ]
收到的鲜花
  • 小鱼儿c2011-09-30 15:06 送鲜花  5朵   附言:呵呵,这个我以前明白了。这个我学到了

我们都在路上。。。。。
2011-09-30 14:32
TonyDeng
Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20
等 级:贵宾
威 望:304
帖 子:25859
专家分:48889
注 册:2011-6-22
得分:20 
我来接点儿分,分享一下。

授人以渔,不授人以鱼。
2011-09-30 14:42
刘定邦
Rank: 10Rank: 10Rank: 10
等 级:青峰侠
帖 子:687
专家分:1570
注 册:2010-9-21
得分:20 
学习了。。。
2011-09-30 14:49
BlueGuy
Rank: 16Rank: 16Rank: 16Rank: 16
等 级:版主
威 望:29
帖 子:4476
专家分:4055
注 册:2009-4-18
得分:0 
以下是引用hellovfp在2011-9-30 14:32:47的发言:

思考一下,这么说吧,如果用户在一个函数体内声名的结构变量,假如使用new_node->data = data这句,出了函数体后,数据还存在么?
所以memcpy(new_node->data, data, lp->data_size);    //拷贝数据
这行拷贝数据(或称作对象复制)是必须的。

你说的也有道理,
不过,函数体内声明的变量,出了函数体不一定不存在,
比如:
static int b;
int* b = malloc(sizeof(int));

[ 本帖最后由 BlueGuy 于 2011-9-30 15:08 编辑 ]

我就是真命天子,顺我者生,逆我者死!
2011-09-30 15:05
hellovfp
Rank: 16Rank: 16Rank: 16Rank: 16
等 级:禁止访问
威 望:30
帖 子:2976
专家分:7697
注 册:2009-7-21
得分:0 
以下是引用BlueGuy在2011-9-30 15:05:23的发言:

 
你说的也有道理,
不过结构通常都是以指针的方式使用的, 函数体内声明的变量,出了函数体不一定不存在,  
比如:static int b;
      int* b = malloc(sizeof(int));
static这种情况,我也想过,更是有必要,因为添加进链表中的对象本身就是不同的个体,如果同一个对象两次添加进链表,使用你说的那句,我们进行释放时,基本上可以说,程序崩溃。当然,那句拷贝代码我也是从国外的代码中学习到的,学习的时候也思考了一段时候,理解了对象的本质。另外还要向Blue版主学习的地方太多了,自己努力吧。

[ 本帖最后由 hellovfp 于 2011-9-30 15:18 编辑 ]

我们都在路上。。。。。
2011-09-30 15:11
hellovfp
Rank: 16Rank: 16Rank: 16Rank: 16
等 级:禁止访问
威 望:30
帖 子:2976
专家分:7697
注 册:2009-7-21
得分:0 
马上下机了,还有同学接分不?

我们都在路上。。。。。
2011-09-30 15:20
TonyDeng
Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20
等 级:贵宾
威 望:304
帖 子:25859
专家分:48889
注 册:2011-6-22
得分:0 
快,得分我就升级了。
收到的鲜花
  • hellovfp2011-09-30 15:24 送鲜花  10朵   附言:Tong大侠还缺分,不是吧?哈哈。

授人以渔,不授人以鱼。
2011-09-30 15:22



参与讨论请移步原网站贴子:https://bbs.bccn.net/thread-351347-1-1.html




关于我们 | 广告合作 | 编程中国 | 清除Cookies | TOP | 手机版

编程中国 版权所有,并保留所有权利。
Powered by Discuz, Processed in 0.504116 second(s), 9 queries.
Copyright©2004-2024, BCCN.NET, All Rights Reserved