C 语言实现泛型的单链表--支持任意元素类型

/ 前言 /

在之前的文章分享 Linux 内核源码实现的循环双链表也是一种泛型的思想(不了解的请戳这里:Linux内核源码剖析(一)--不同寻常的双向链表),利用用户自定义的结构体包含 Linux 内核双链表节点,通过结构体元素偏移找到用户结构体起始位置,实现一种“泛型”链表。不过今天小 C 分享的是另外一种实现思路,请往下看~

/ 设计 /

主要依据两个点来设计实现的:指定元素内存,通过宏定义推迟元素定义。

1.指定元素内存

常规的链表定义如下,定义一个节点指向下一个节点,其他就是元素,这种结构体在内存上的布局如下图所示:

struct list

{
    struct list *next;
    int age;
    int sex;
    float height;
    char name[32];
};

图片

list结构体内存分布图

看了上图结构体元素在内存中的分布后,我们将结构体拆为如下两部分,但是申请内存时候还是一起申请,如下图所示。这样我们可以只定义一个 list 链表结构体,human 结构体由用户定义,想包含什么数据就包含什么数据。但是这样有一个问题:我们我们创建节点时候要把链表指针和元素的内存都申请了,元素是用户自定义的,而我们不知道元素有多大?请往下看。

struct list
{
    struct list *next;
}

struct human
{
    int age;
    int sex;
    float height;
    char name[32];
};

图片

将两个结构体的内存同时申请,创建的内存连接在一起。

   

2.通过宏定义推迟元素定义

前面我们通过将一个常规的结构体给拆开,实现用户自定义链表中的元素,降低了耦合,也迎来了一个问题:那就是用户自定义的结构体有多大,这关乎我们创建链表节点时候申请内存的大小。怎么解决?下面这样定义?

struct list* create_node()
{
    struct list* node = (struct list*)malloc(sizeof(struct list) + sizeof(struct human));
    return node;
}

这样定义创建节点,的确可以解决,但是如果用户不想让链表的元素名字叫 struct human 咋办?

答案:采用宏定义的方式推迟了对自定义链表元素名字的要求,如下所示。这个 type 是用户自定义链表节点元素的名字,例如 struct human,也可以定义其他的名字,完全看个人意愿。同理,其他的链表操作也采用宏定义的方式,这样还减少了函数调用的栈开销,一定程度上提升了性能。

#define slist_new_node(_type) (struct list*)malloc(sizeof(struct list)+sizeof(_type))

/ 实现 /

C 语言泛型单链表实现源码如下:


#ifndef __SLIST_H__
#define __SLIST_H__

#include <stdio.h>
#include <stdlib.h>

/**
 * @brief 实现C语言泛型单链表
 * @author young  【微信公众号: Linux编程用C】
 * @mail estyoung71@gmail.com
*/


/**
 * @brief 定义链表节点
*/
typedef struct slist_s
{
    struct slist_s *next;
}slist_t;

/**
 * @brief 创建一个链表节点
 * @param _type 链表中元素的类型
*/
#define slist_new_node(_type) (slist_t*)malloc(sizeof(slist_t)+sizeof(_type))

/**
 * @brief 传入链表节点,获取节点的元素
 * @param _node 传入的链表节点指针
 * @param _type 链表元素的类型
 * @return 返回节点元素的指针
*/
#define slist_get_elem(_node, _type) ( (_type*)(((char*)_node)+sizeof(slist_t)) )

/**
 * @brief 在指定节点位置前插入一个节点
 * @param _pos 节点位置指针
 * @param _node 要插入的节点指针
*/
#define slist_insert_front(_pos, _node) ({ _node->next = _pos; _pos = _node; })

/**
 * @brief 在指定节点位置后插入一个节点
 * @param _pos 节点位置指针
 * @param _node 要插入的节点指针
*/
#define slist_insert_back(_pos, _node) ({ _node->next = _pos->next; _pos->next = _node; })

/**
 * @brief 删除链表节点
 * @param _prev 要删除的前一个节点
*/
#define slist_erase_node(_prev) ({ slist_t* tmp = _prev->next; _prev->next = tmp->next; tmp; })

/**
 * @brief 删除节点后重新复位
 * @param _prev 被删除节点前一个节点指针
 * @param _node 要删除节点的指针
*/
#define slist_erase_reset(_prev, _node) ({ _node = _prev; })

/**
 * @brief 遍历链表
 * @param head 链表头节点
 * @param node 当前节点指针
*/
#define slist_foreach(head, node) \
    for(node=head; node; node=node->next)

/**
 * @brief 遍历并删除节点
 * @param head 链表头节点
 * @param prev 保存节点的前一个节点
 * @param node 当前节点
*/
#define slist_foreach_safe(head, prev, node) \
    for(prev=head, node=prev->next; node; prev=node, node=node->next)

#endif /* end slist */

/ 验证 /

编写测试程序,测试单链表:struct info_t这个结构体是用户自定义的,测试创建、插入、遍历、删除等功能。

#include <string.h>
#include "slist.h"

typedef struct info_t
{
    int age;
    char name[32];
}info_t;

int main(void)
{
    slist_t *node, *tmp, *prev, *head = slist_new_node(info_t);
    slist_get_elem(head, info_t)->age = -1;
    snprintf(slist_get_elem(head, info_t)->name, 32, "tom");

    for(int i=0; i<10; ++i){
        node = slist_new_node(info_t); //创建节点
        
        slist_get_elem(node, info_t)->age = 10+i; //给元素赋值
        snprintf(slist_get_elem(node, info_t)->name, 32, "tom%d", i);

        slist_insert_back(head, node); //插入到头节点后面
    }

    printf("----------split-------------\n");
    slist_foreach(head, node){ //遍历
        printf("name=%s age=%d\n", slist_get_elem(node, info_t)->name, slist_get_elem(node, info_t)->age);
    }

    printf("----------split-------------\n");
    slist_foreach_safe(head, prev, node){ //遍历删除
        if(slist_get_elem(node, info_t)->age<15){
            tmp = slist_erase_node(prev); //删除
            
            free(tmp); //释放内存
            tmp = NULL;

            slist_erase_reset(prev, node); //连续删除需要给node赋值
        }
    }

    printf("----------split-------------\n");
    slist_foreach(head, node){ //删除后在遍历打印
        printf("name=%s age=%d\n", slist_get_elem(node, info_t)->name, slist_get_elem(node, info_t)->age);
    }

    return 0;
}

测试效果:插入10个元素,遍历打印,删除5个,遍历打印。

图片

    欢迎大家一起交流讨论,如果本篇文章对你有用的,请点赞、再看、转发吧~~,蟹蟹你~。

    我把代码传入GitHub:https://github.com/young-1-code/data_structure.git,在上面也有完整的slits代码和测试代码,取代码时候帮忙点个star吧~,蟹蟹你~。

图片

  • 12
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值