新年快乐~
通过设计一个通用循环双向链表,以实现对由不同类型节点组成的链表进行统一管理。实现此链表的关键是如何将不同类型的节点串联起来,并且能够访问节点内的数据。
1. 用途
设计的链表主要用于操作系统,所以代码的运行效率是首要考虑的问题还有函数参数类型传递的不确定性,在定义的过程中将不考虑普通函数的定义,选用内联函数和宏函数。
2. 思路
为了将节点链接起来,所有节点必须存在公共部分,公共部分中包含能够指向前置节点和后置节点的指针。通过公共部分在节点中的偏移量,以确定节点的地址,进而访问节点内的数据,如下图。
这里纠正一下,本来以为一个通用链表内应该可以是任意的节点类型,但实际写的过程中发现在访问的时候还是存在问题,虽然说可以通过要访问的数据在结构体中的偏移量,确定结构体地址,但前提还是要知道要访问结构体的数据类型,所以感觉通用链表的“通用”应该指的是通用的链表的操作,也就是说任意的链表所需要的插入删除等操作函数可以是同一个。所以通用链表结构更应该是下面这种形式。
3. 设计方法
3.1 公共部分设计
typedef struct entry entry_t;
struct entry
{
entry_t *pre;
entry_t *next;
};
3.2 结点类型
假设此链表中含有两种不同的数据类型,头节点struct HeadNode;
;其他节点类型struct Page;
,或者struct Paragraph;
。
3.3 链表操作
包括链表初始化,节点插入;节点删除;节点查找。
4. 整体设计
分文件编写。将通用的链表操作放置在头文件中,任何源文件需要时,包含一下。
4.1 试验思路
#include<stdio.h>
#include<stdlib.h>
#include"list.h"
//定义宏偏移函数
//#define offset(Page_t,node) unsigned (&(((Page_t*) 0) -> node))
#define offset(type,member) ((unsigned int) (&(((type*) 0) -> member))) //强制转换为十进制
//从偏移量获得结构体地址
//#define getAddr(type,ptr,member) ((type *)((char *)(ptr)-offset(type,member)))//可能太长了,拆开
#define getAddr(addr,node) getPage(addr,node,Page_t)
#define getPage(addr,member,type) ((type *)((char *)(addr)-offset(type,member)))
int main()
{
HeadNode_t hNode;
//先为头结点申请一块内存
HeadNode_t *p=(HeadNode_t *)malloc(sizeof(HeadNode_t));
//保留p,防止被改变
//关键一步,容易出错
//HeadNode_t *q;
p->num=0;
//手动将这三个类型的结点链接起来
/*----以前错误的思路--------
Page_t *q=(Page_t *)malloc(sizeof(Page_t));//不用这种方式创建
Page_t aNode;
q->node.next=&aNode.node;
q->num+=1;
q=q->node.next;
//创建bNode节点
Paragraph_t bNode;
q->node.next=&bNode.node;
q->num+=1;
q=q->node.next;
//头尾相连
q->node.next=p;
p->node.pre=q;
-------------------------*/
//-----------通过循环创建Page循环链表
for(int i=0;i<5;i++)
{
//Page_t aNode;//相当于重复定义了肯定不行
Page_t *temp=(Page_t *)malloc(sizeof(Page_t));
temp->test=i+1;
q->next=&temp->node;
p->num+=1;//头节点记录此时链表元素的个数
q=q->next;
}
//头尾相连
q->next=&p->node;
p->node.pre=q;
/*----------
**下面关键是如何访问test
**思路是这样的:首先我们获取公共部分node在结构体中的偏移量,因为node的地址是很轻松可以获得的,node的地址减去这个
**偏移量就是结构体的地址,知道这个地址我们就可以访问test了。
**调用获取偏移量函数
**使用传统函数,传入的类型是不能变化的,这也是使用宏函数的一个原因
**Page_t:是为了地址强转换的时候用;&aNode.node:减去偏移量的时候用;aNode.node:->member确定偏移量的时候用
*-----------*/
//Page_t *m=getAddr(Page_t,&aNode.node,aNode.node);//不得行,报错不知道为啥,说期待一个')'
//------------遍历输出链表元素
int i;q=&p->node;
for(i=0;i<3;i++) q=q->next;//定位到第三个元素
//输出第三个元素的test值
Page_t *m=getAddr(q,node);//q是第三个元素公共部分node地址,node在->member时确定偏移量的时候用
printf("第三个test值为%d",m->test);//第三个test值为3
return 0;
}
4.2 通用操作设计
#ifndef LIST_H
#define LIST_H
/*此文件将完成提供链表需要的数据类型,以及通用操作*/
//因为是面向操作系统的,所以为了代码的执行效率,将采用内联函数,为了避免内联函数重定义的发生,这里的内联函数
//声明为静态内联函数,只在本文件中定义。
//定义节点的公共部分
typedef struct Entry Entry_t;
typedef struct HeadNode HeadNode_t;
typedef struct Page Page_t;
typedef struct Paragraph Paragraph_t;
struct Entry
{
Entry_t *pre;
Entry_t *next;
};
struct HeadNode
{
int num;
Entry_t node;//还是和其他统一起来,这样后面对于链表操作可能会有便利
//Entry_t *pre;
//Entry_t *next;
};
struct Page
{
int test;
Entry_t node;
};
struct Paragraph
{
int test;
Entry_t node;
};
//链表操作
//链表初始化
static inline void list_init(Entry_t *q) __attribute__((always_inline));
//节点插入
static inline void list_insert(Entry_t *pre,Entry_t *next,Entry_t *elem) __attribute__((always_inline));
//节点删除
static inline void list_delete(Entry_t *pre,Entry_t *next) __attribute__((always_inline));
//节点查找
static inline Entry_t * list_find(Entry_t *p) __attribute__((always_inline));
//函数定义----内联函数的定义越简单越好
static inline
void list_init(Entry_t *q)
{
q->next=q;
q->pre=q;
//return q;//q是指针不用返回
}
static inline
void list_insert(Entry_t *pre,Entry_t *next,Entry_t *elem)
{
pre->next = next->pre = elem;
elem->next = next;
elem->pre = pre;
}
static inline
void list_delete(Entry_t *pre,Entry_t *next)
{
pre->next = next;
next->pre = pre;
}
static inline
Entry_t * list_find(Entry_t *p)
{
return p;
}
#endif
关键部分:宏函数定义加括号要清楚,保险起见确定连在一起的都加上括号;
在链表的创建时搞清楚什么是公共部分Entry_t *q=&p->node;
以保证遍历过程中的一致性。