C语言宏定义实现常用数据结构——双向循环链表
一、背景:普通C语言的链表存在的问题
C语言中没有多态,如果在一个工程中,你想同时使用整形链表,浮点型链表,就要定义两个结构体:List_int和List_float ,然后同样的插入、删除节点的代码要写两遍。
举一个例子:假设你要在一个整型链表的一个节点node后面插入一个整数,这个插入函数的接口可能是下面这样的:
typedef struct Node_
{
int data;
struct Node_* next;
}Node;
bool insert_next(Node* node,int data){ ... }
假设你要在一个浮点型链表的一个节点node后面插入一个浮点数,这个插入函数的接口可能是下面这样的:
typedef struct Node_
{
float data;
struct Node_* next;
}Node;
bool insert_next(Node* node,float data){ ... }
在《算法精解C语言描述》里面,是使用void*指针来实现多态的,如下所示
typedef struct Node_
{
void* data;
struct Node_* next;
}Node;
bool insert_next(Node* node,void*data){ ... }
节点Node只保存数据的指针,通过强制类型转换,data可以是任意数据类型的指针。但这样做有风险,你不知道data的数据到底保存在哪里,如果你一不小心使用类似下面的代码:
typedef struct Node_
{
void* data;
struct Node_* next;
}Node;
bool insert_next(Node* node,void*data){ ... }
void insert_PI(Node* node)
{
float pi=3.141592f;
insert_next(node, &pi);
}
就悲剧了。因为变量(float pi)在函数退出后,就被出栈了。后面可能会被其他函数修改地址(&pi)的值,这时链表里保存的数据就出错了。这时只能使用下面的方式:
typedef struct Node_
{
void* data;
struct Node_* next;
}Node;
bool insert_next(Node* node,void*data){ ... }
void insert_PI(Node* node)
{
float* pi=(float*)malloc(sizeof(float));
* pi=3.141592f;
insert_next(node, &pi);
}
用malloc函数分配一个内存,这个内存不会放到函数的栈区域。在函数退出之后,(* pi)也不会被其他函数无意修改。
这样的代码虽然可以用,但使用的时候容易出错。
二、前言
为了避免重复劳动,我写了通用的数据结构的库函数。
为了实现多态的特性,在这个库函数中使用了大量的指针强制类型转换。
为了提高运行效率,和避免在一些单片机(如STC系列单片机)中出现函数重入的警告,所有的功能函数都用宏函数的方式来描述。
这篇文章先介绍链表。链表有很多种,单向链表,双向链表,单向循环链表,双向循环链表。为了提高代码的适用范围,这里选择实现双向循环链表。
数据结构的知识建议看《算法精解C语言实现 kyle Loudon著》,这里就不赘述了。
三、内存管理
C语言还有一个很麻烦的事:内存管理。我不喜欢用malloc函数动态分配内存。因为我看不到这个函数的源代码,不放心。而且这个函数效率也不高。
为了提高内存分配的效率,减少内存碎片产生,我们自己设计一个动态内存分配的函数。这里参考了《数据结构、算法与应用C++语言描述Sartaj Sahni著》3.6模拟指针的内容。
#include <iostream>
//为了让这个数据结构可以用在C语言的不同数据类型上,将数据操作都以指针的形式表现出来。
//函数只输入,返回指针。
#define TypeSize_int 4 //定义int占用的数据长度。STC51单片机是2字节,一般的32位机是4字节
#define TypeSize_point 4 //定义指针占用的数据长度。STC51单片机是2字节,32位机是4字节
#define TypeSize_SpaceNode 8 //定义下面数据结构SpaceNode的长度。SpaceNode里面有两个指针型变量,共占8字节。
//空间节点,节点前面几个字节是两个指针,用来保存前后两个节点的地址。
typedef struct SpaceNode_
{
struct SpaceNode_* prev;
struct SpaceNode_* next;
}SpaceNode;
//节点的空间,这里的节点都至少包含两个元素:next,prev。
typedef struct NodeSpace_
{
char* buffer_head; //内存空间的头地址
char* buffer_tail; //内存空间的尾地址
int nodeSize; //节点大小。节点的前面是prev和next的指针,后面跟着节点的数据。
int numberOfNodes; //内存空间能容纳节点的数量
//下面维护两个表来记录空白节点
SpaceNode* first_1; //表1记录未用过的空白节点头指针。
SpaceNode* first_2; //表2记录使用过,但被释放了的空白节点头指针。表2是一个双向链表。
SpaceNode first_2_tail; //表2的尾节点。避免first_2是空表时没有元素。
}NodeSpace;
//初始化节点空间。
//dataTypesize:节点的数据类型的尺寸。如:float 4字节,char 1字节。
//char* buf:内存空间的数组。
//int buf_len :buf数组的长度。
void init_NodeSpace(NodeSpace* ns, int dataTypesize, char* buf, int buf_len)
{
buf = ((int)buf % 4 == 0) ? buf : (buf + 4 - (int)buf % 4); //为了在32位机上能提高效率,头部向下4字节对齐。
buf_len = ((int)buf % 4 == 0) ? buf_len : (buf_len - 4 + (int)buf % 4);
ns->nodeSize = dataTypesize + TypeSize_SpaceNode;
ns->numberOfNodes = buf_len / ns->nodeSize;
ns->buffer_head = buf;
ns->buffer_tail = ns->buffer_head + ns->numberOfNodes * ns->nodeSize;
ns->first_1 = (SpaceNode*)(ns->buffer_head); //初始化表1
ns->first_2 = &(ns->first_2_tail); //初始化表2
ns->first_2_tail.prev = NULL;
ns->first_2_tail.next = NULL;
}
//从(nodeSpace)的first_1里分配一个节点,返回节点的指针。前提是first_1非空
#define allocate_from_first_1(nodeSpace) \
(void*)((nodeSpace).first_1 = (SpaceNode*)((char*)((nodeSpace).first_1) + (nodeSpace).nodeSize), (char*)((nodeSpace).first_1) - (nodeSpace).nodeSize)
//从(nodeSpace)的first_2里分配一个节点,返回节点的指针。前提是first_2非空
#define allocate_from_first_2(nodeSpace) (void*)((nodeSpace).first_2 = (SpaceNode*)(*(nodeSpace).first_2).next, (*(nodeSpace).first_2).prev)
//(nodeSpace)的表first_1是否是空的
#define is_first_1_empty(nodeSpace) ((nodeSpace).first_1 == (SpaceNode*)(nodeSpace).buffer_tail)
//(nodeSpace)的表first_2是否是空的
#define is_first_2_empty(nodeSpace) ((nodeSpace).first_2 == (&(nodeSpace).first_2_tail))
//从(nodeSpace)里分配一个节点,返回节点的指针。 先从表first_2中找,然后从表first_1中找,如果都找不到,就返回null
#define new_node(nodeSpace) \
(void*)(is_first_2_empty(nodeSpace) \
? (is_first_1_empty(nodeSpace) \
? (NULL) \
: allocate_from_first_1(nodeSpace)) \
: (allocate_from_first_2(nodeSpace)))
//Delete的节点指针node必须是由New分配的,不然可能会出错。 将节点插进first_2的双向链表里
#define delete_node(nodeSpace, nodepoint) \
((*(SpaceNode*)nodepoint).next = (nodeSpace).first_2, \
(nodeSpace).first_2->prev = (SpaceNode*)nodepoint, \
(nodeSpace).first_2 = (SpaceNode*)nodepoint)
NodeSpace 测试代码 /
typedef struct MyStruct
{
void* prev;
void* next;
int a;
} st;
void print_NodeSpace(NodeSpace* ns)
{
std::cout << "输出nodespace \r\n";
if (ns != NULL) {
std::cout << "buffer_head地址:" << (void*)ns->buffer_head << "\r\n";
std::cout << "buffer_tail地址:" << (void*)ns->buffer_tail << "\r\n";
std::cout << "nodeSize :" << ns->nodeSize << "\r\n";
std::cout << "numberOfNodes :" << ns->numberOfNodes << "\r\n";
std::cout << "first_1地址:" << (void*)ns->first_1 << "\r\n";
std::cout << "first_2地址:" << (void*)ns->first_2 << "\r\n";
}
}
void print_st(st* s)
{
std::cout << "地址:";
if (s != NULL) {
std::cout << s << " , 前后指针:" << s->prev << "," << s->next << " 值 " << s->a << "\r\n";
}
}
void test_NodeSpace()
{
char u[60];
NodeSpace ns;
st* sn;
st* sn1;
st* sn2;
st* sn3;
st* sn4;
st* sn5;
init_NodeSpace(&ns, 4, u, 60);
sn = (st*)new_node(ns); sn->a = 11; print_NodeSpace(&ns);
sn1 = (st*)new_node(ns); sn1->a = 12; print_NodeSpace(&ns);
sn2 = (st*)new_node(ns); sn2->a = 13; print_NodeSpace(&ns);
delete_node(ns, sn);
sn3 = (st*)new_node(ns); sn3->a = 14; print_NodeSpace(&ns);
sn4 = (st*)new_node(ns); sn4->a = 15; print_NodeSpace(&ns);
sn5 = (st*)new_node(ns); print_NodeSpace(&ns);
sn5->a = 16;
print_st(sn);
print_st(sn1);
print_st(sn2);
print_st(sn3);
print_st(sn4);
print_st(sn5);
}
四、双向循环链表
代码如下:
//双向链表结点
typedef struct DListNode_
{
struct DListNode_* prev;
struct DListNode_* next;
// void* data;
} DListNode;
//双向链表
typedef struct DList_
{
NodeSpace* nodeSpace; //节点的空间
int size; //链表元素个数。
DListNode* head; //链表的头节点。
DListNode* current; //链表的当前节点。插入新节点,或者遍历元素,都用它作为中间变量。
} DList;
//初始化双向链表,NodeSpace是链表节点存储的空间。
void dlist_init(DList* list, NodeSpace* ns)
{
/// 初始化链表
list->nodeSpace = ns;
list->size = 0;
list->head = NULL;
list->current = NULL;
}
//下面的所有宏函数,参数node都是指针形式。
//判断元素 node 是否为指定双向链表 list 的头节点
#define dlist_is_head(list,node) ((void*)(node) == (void*)(list).head)
//从空链表中插入节点,这个节点成为头节点。返回待初始化的新节点指针。
#define dlist_insert_empty(list) \
(void*)((list).head = (DListNode*)new_node(*(list).nodeSpace), \
(list).head == NULL ? NULL : ( \
(list).head->prev = (list).head, \
(list).head->next = (list).head, \
(list).size = 1,\
(char*)((list).head) ) )
//从node后面插入新节点,返回待初始化的新节点指针。
#define dlist_insert_next(list,node) \
(void*)((list).current = (DListNode*)new_node(*(list).nodeSpace), \
(list).current == NULL ? NULL : ( \
(list).current->prev = node, \
(list).current->next = node->next, \
node->next->prev = (list).current,\
node->next = (list).current,\
(list).size++, \
(char*)((list).current)) )
//从node前面插入新节点,返回待初始化的新节点指针。
#define dlist_insert_prev(list,node) \
(void*)((list).current = (DListNode*)new_node(*(list).nodeSpace), \
(list).current == NULL ? NULL : ( \
(list).current->prev = node->prev, \
(list).current->next = node, \
node->prev->next = (list).current,\
node->prev = (list).current,\
(list).size++, \
(char*)((list).current) ) )
//移除node节点。如果node是头节点,先把头节点移动到下一个节点,再做删除。
#define dlist_remove_node(list,node) \
if((list).size>1){ \
if (dlist_is_head(list, node)) { \
(list).head = (list).head->next; \
} \
node->prev->next = node->next; \
node->next->prev = node->prev; \
delete_node(*(list).nodeSpace,node); \
(list).size--; \
} \
else if ((list).size == 1) { \
delete_node(*(list).nodeSpace, node); \
(list).size = 0; \
(list).head = NULL; \
}
//销毁链表,释放内存。
void dlist_destroy(DList* list)
{
// 删除每一个元素
while ((*list).size > 0) {
dlist_remove_node((*list), (*list).head);
}
}
DList 测试代码 /
typedef struct dnode
{
void* prev;
void* next;
int a;
} Dnode; //双向链表节点
void print_DList(DList* dl)
{
std::cout << "输出DList ,";
if (dl != NULL && dl->size > 0) {
std::cout << "头节点 :" << ((Dnode*)dl->head)->a << " , ";
std::cout << "节点数量:" << dl->size << "\r\n";
dl->current = dl->head;
std::cout << " " << ((Dnode*)dl->current)->a;
dl->current = dl->current->next;
while (dl->current != dl->head)
{
std::cout << " " << ((Dnode*)dl->current)->a;
dl->current = dl->current->next;
}
std::cout << " \r\n";
}
else
{
std::cout << " 空表 \r\n";
}
}
void print_Dnode(Dnode* s)
{
std::cout << "地址:";
if (s != NULL) {
std::cout << s << " , 前后指针:" << s->prev << "," << s->next << " 值 " << s->a << "\r\n";
}
}
void test_dlist()
{
const int len = 80;
char buf[len];
NodeSpace ns;
init_NodeSpace(&ns, 4, buf, len);
DList list;
dlist_init(&list, &ns);
((Dnode*)dlist_insert_empty(list))->a = 5;
((Dnode*)dlist_insert_next(list, list.head))->a = 6;
((Dnode*)dlist_insert_next(list, list.head))->a = 7;
print_DList(&list);
((Dnode*)dlist_insert_next(list, list.head))->a = 8;
((Dnode*)dlist_insert_prev(list, list.head))->a = 3;
((Dnode*)dlist_insert_prev(list, list.head))->a = 4;
print_DList(&list);
list.current = list.head->prev;
dlist_remove_node(list, list.current);
list.current = list.head->next;
dlist_remove_node(list, list.current);
list.current = list.head->next;
dlist_remove_node(list, list.current);
print_DList(&list);
((Dnode*)dlist_insert_next(list, list.head))->a = 8;
((Dnode*)dlist_insert_prev(list, list.head))->a = 3;
((Dnode*)dlist_insert_prev(list, list.head))->a = 4;
print_DList(&list);
// dlist_remove_node(list, list.head); 这样会出错。list.head在宏展开后会转移失败。不能直接用list.head这个变量名。
std::cout << " 结束\r\n";
}
解析:
1、dlist_insert_next(list,node)返回的是新插入节点的指针,这个新节点从(list).nodeSpace中分配。宏函数dlist_insert_next只初始化新节点的指针。至于节点的数据区域,需要用户自己去初始化。
2、用户在使用这个库函数的时候,要自己定义节点的结构体类型。自己定义的结构体的第一、第二个数据必须是 prev、 next 的指针。后面再根据使用情况定义数据类型。
typedef struct dnode
{
void* prev;
void* next;
int a;
} Dnode; //双向链表节点
3、上面自己定义的节点数据的长度(上面的例子中,data就是int a)必须和init_NodeSpace函数的dataTypesize一致,不然会出错。
//初始化节点空间。
//dataTypesize:节点的数据类型的尺寸。如:float 4字节,char 1字节。
//char* buf:内存空间的数组。
//int buf_len :buf数组的长度。
void init_NodeSpace(NodeSpace* ns, int dataTypesize, char* buf, int buf_len)
注意事项:
1、在VisualStudio上编译这个代码,注意把解决方案平台从x64换成x86,不然会出错。
因为这个代码是为32位机准备的。如果是64位机,需要自己修改下面两个宏定义
#define TypeSize_point 4 //定义指针占用的数据长度。STC51单片机是2字节,32位机是4字节,64位机是8字节。
#define TypeSize_SpaceNode 8 //定义下面数据结构SpaceNode的长度。SpaceNode里面有两个指针型。如果是32位机,就是8字节;如果是64位机,就是16字节。
2、如果不是对自己的水平很自信,不要轻易去动上面的宏函数。宏函数的debug也是需要一些小技巧的。
最后,设计了这个代码,我还是挺得意的,嘻嘻。