相比Linux内核链表宿主结构可有多个链表结构的优势,本函数集侧重封装性和易用性,而灵活性和效率有所下降。
可基于该函数集方便地构造栈或队列集。
本函数集暂未考虑并发保护。html
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序经过链表中的指针连接次序实现。链表由一系列存储结点组成,结点可在运行时动态生成。每一个结点均由两部分组成,即存储数据元素的数据域和存储相邻结点地址的指针域。当进行插入或删除操做时,链表只需修改相关结点的指针域便可,所以相比线性表顺序结构更加方便省时。node
链表可分为单链表(Singly Linked List)、双向链表(Doubly Linked List)和循环链表(Circular List)等。一个单链表结点仅包含一个指向其直接后继结点的指针,所以当须要操做某个结点的直接前驱结点时,必须从单链表表头开始查找。算法
双向链表和循环链表均为单链表的变体。一般建立双向循环链表以综合利用二者的优势。安全
双向链表的每一个结点除含有数据域外,还有两个指针域,分别指向直接前驱结点和直接后继结点。所以,从双向链表中的任一结点开始,都可方便地访问其前驱结点和后继结点。双向链表的结点结构示意图以下所示:数据结构
图1 双向链表的结点结构多线程
其中,Data为结点存储的数据元素,prev指针指向该结点的前驱结点,next指针指向该结点的后继结点。双向链表一般含有一个表头结点,亦称哨兵结点(Sentinel Node),用于简化插入和删除等操做。带头结点的非空双向链表以下图所示:并发
图2 带头结点的非空双向链表函数
图中,表头指针dhead指向表头结点Head,该结点的前驱指针为空;结点C为表尾结点,其后继指针为空。除表头结点和表尾结点外,对指向双向链表任一结点的指针p,知足下面的关系:oop
p = p->prev->next = p->next->prev测试
即当前结点前驱的后继是自身,其后继的前驱也是自身。
链表有查找、插入和删除三种基本操做。双向链表也不例外。
1) 查找操做
在带表头的双向链表中查找数据域为一特定值的某个结点时,可从表头结点开始向后依次匹配各结点数据域的值,若与特定值相同则返回指向该结点的指针,不然继续日后遍历直至表尾。
2) 插入操做
假设指针p和q指向双向链表中的两个先后相邻结点,将某个新结点(指针为s)插到p和q之间,其过程及C语言描述以下图所示:
图3 在双向链表中插入结点的过程
注意,结点前驱后继指针的操做顺序并不是惟一,但必须保证最后才对p->next或q->prev赋值(操做➃),不然会“丢失”p的后继结点或q的前驱结点。
可见,若相邻结点指针p、q均已知,则在p和q之间插入新结点s时,只需依次将s的前驱指针指向p,s的后继指针指向q,p的后继指针指向s,q的前驱指针指向s。即:
① s->prev = p; ② s->next = q; ③ p->next = s; ④ q->prev = s; |
双向链表中p和q->prev指向同一结点,所以上述步骤等效于图3中q“视角”的第二种插入顺序。为便于记忆,可想象孩子(s)前后去拉爸爸(p)和妈妈(q)的手,爸爸(p)妈妈(q)再前后拉住孩子(s)的手。
3) 删除操做
删除某个结点,其实就是插入某个结点的逆操做。仍是对于双向循环链表,要在连续的三个结点s,p,q中删除p结点,只需把s的右链域指针指向q,q的左链域指针指向s,并收回p结点便可。
假设指针p、s和q指向双向链表中的三个先后相邻结点,删除结点s的过程及C语言描述以下图所示:
图4 在双向链表中删除结点的过程
可见,删除时只需将p的后继指针指向q,q的前驱指针指向p,并回收结点s便可。
将单链表尾结点的指针域指向第一个结点或表头结点,即构成单向循环链表,简称循环链表。从循环链表中任一结点单向出发,都可找到链表中其余结点。
借助表头结点可统一空表和非空表的运算,所以循环链表中每每加入表头结点。带头结点的循环链表以下图所示:
图5 带头结点的循环链表(头指针)
循环链表的操做算法与普通单链表基本相同,只是对表尾的判断有所改变。在循环链表chead中,判断表尾结点p的条件是p->next == chead,即当结点的后继指针指向表头结点时,说明已到表尾。
注意,建立循环链表时必须使其尾结点的后继指针指向表头结点,尤为是在尾结点后插入新结点时。
弃用头指针而采用尾指针,可方便地找到循环链表的开始结点和终端结点。以下图所示:
图6 带头结点的循环链表(尾指针)
双向链表一般采用带表头结点的循环链表形式,即双向循环链表。双向循环链表在双向链表的基础上,将表头结点的前驱指针指向尾结点,尾结点的后驱指针指向头结点,首尾相连造成一个双向环。双向循环链表可方便地获取当前结点的前驱结点,没必要像单向循环链表那样从头开始遍历;而其循环的特性又可方便地从任一结点出发单向遍历整个链表,没必要像双向链表那样根据方向而使用不一样的指针域。
带头结点的双向循环链表以下图所示:
图7 带头结点的双向循环链表
本节将采用C语言实现一个通用双向循环链表的建立及操做函数集。
文中“OMCI_”和“Omci”前缀为代码所在模块名信息,使用接口时可按需修改这些前缀。
定义双向循环链表单元结构示意以下:
图8 双向循环链表单元结构示意图
其中,根结点的pHead字段指向链表头结点,pTail字段指向链表尾结点。头结点的pPrev字段指向尾结点,尾结点的pNext字段指向头结点。若链表为空(仅含头结点),则pHead和pTail字段均指向头结点。
链表结点定义以下:
typedef struct T_OMCI_LIST_NODE{ struct T_OMCI_LIST_NODE *pPrev; /* 指向链表直接前驱结点的指针 */ struct T_OMCI_LIST_NODE *pNext; /* 指向链表直接后继结点的指针 */ VOID *pvNodeData; /* 指向链表数据的指针。获取具体数据时需显式转换该指针类型为目标类型 */ }T_OMCI_LIST_NODE;
相应地,链表定义以下:
typedef struct{ T_OMCI_LIST_NODE *pHead; /* 指向链表头结点的指针 */ T_OMCI_LIST_NODE *pTail; /* 指向链表尾结点的指针 */ INT32U dwNodeNum; /* 链表结点数目 */ INT32U dwNodeDataSize; /* 链表结点保存的数据字节数 */ }T_OMCI_LIST;
为支持不一样的数据类型和数据结构(通用性),链表结点数据域定义为VOID *pvNodeData指针。变量dwNodeDataSize指示数据域的数据宽度(字节数)。也可将数据宽度信息存储于头结点数据域内,从而没必要定义变量dwNodeDataSize。经过遍历链表并计数可得结点数目,故变量dwNodeNum也并不是必要。所以,dwNodeDataSize和dwNodeNum意在简化逻辑,也是“空间换时间”思想的体现。
除此以外,还定义如下状态值,以使链表内部状态透明化:
//链表函数返回状态枚举值 typedef enum{ OMCI_LIST_OK = (INT8U)0, OMCI_LIST_ERROR = (INT8U)1 }LIST_STATUS; //链表结点空闲状况枚举值 typedef enum{ OMCI_LIST_OCCUPIED = (INT8U)0, OMCI_LIST_EMPTY = (INT8U)1, OMCI_LIST_NULL = (INT8U)2 }LIST_OCCUPATION; //BOOL型常量,适用于'Is'前缀函数 #define OMCI_LIST_TRUE (BOOL)1 #define OMCI_LIST_FALSE (BOOL)0
为确保安全性,链表操做中须要进行大量的指针校验。所以,定义几个校验空指针的宏,以简化代码篇幅:
#define FUNC_NAME __FUNCTION__ //(__func__) /* 指针校验宏 */ //若无返回值则retVal置RETURN_VOID #define RETURN_VOID #define CHECK_SINGLE_POINTER(ptr1, retVal) do{\ if(NULL == (ptr1)) { \ printf("[%s(%d)]Null Pointer: "#ptr1"!\n\r", FUNC_NAME, __LINE__); \ return retVal; \ } \ }while(0) #define CHECK_DOUBLE_POINTER(ptr1, ptr2, retVal) do{\ if((NULL == (ptr1)) || (NULL == (ptr2))) \ { \ printf("[%s(%d)]Null Pointer: "#ptr1"(%p), "#ptr2"(%p)!\n\r", FUNC_NAME, __LINE__, ptr1, ptr2); \ return retVal; \ } \ }while(0) #define CHECK_TRIPLE_POINTER(ptr1, ptr2, ptr3, retVal) do{\ if((NULL == (ptr1)) || (NULL == (ptr2)) || (NULL == (ptr3))) \ { \ printf("[%s(%d)]Null Pointer: "#ptr1"(%p), "#ptr2"(%p), "#ptr3"(%p)!\n\r", FUNC_NAME, __LINE__, ptr1, ptr2, ptr3); \ return retVal; \ } \ }while(0)
若待检查的指针中至少有一个指针为空时,校验宏打印全部待检查的指针值并退出。但其实现使得下面的语句在pList为空时崩溃(打印时试图访问pList->pHead等):
CHECK_TRIPLE_POINTER(pList, pList->pHead, pList->pHead->pNext, OMCI_LIST_ERROR);
所以必须使用下面的分级校验以免多级指针前级为NULL时访问本级出错:
CHECK_SINGLE_POINTER(pList, OMCI_LIST_ERROR);
CHECK_SINGLE_POINTER(pList->pHead, OMCI_LIST_ERROR);
CHECK_SINGLE_POINTER(pList->pHead->pNext, OMCI_LIST_ERROR);
若不打印各指针的值,则逻辑或(||)的运算顺序足矣保证CHECK_TRIPLE_POINTER写法的安全性。
注意,这些指针校验宏大量应用于2.3节函数接口中,以保证其安全性。使用者若能在外部杜绝空指针引用,则可添加条件编译开关“剔除”这些校验宏,以提升代码执行效率。
对于链表结点的操做比较固定,所以也用宏定义加以封装:
//建立结点为做为链表头以生成双向循环空链表 #define OMCI_INIT_NODE(pNode) do{ \ (pNode)->pNext = (pNode)->pPrev = (pNode); \ }while(0) //"孤立"链表结点,避免经过该结点访问其前驱和后继结点(进而遍历链表) #define OMCI_ISOL_NODE(pNode) do{ \ (pNode)->pNext = (pNode)->pPrev = NULL; \ }while(0) //判断链表是否仅含头结点 #define OMCI_LIST_WITH_HEAD(pHeadNode) do{ \ (((pHeadNode)->pPrev == (pHeadNode)) && ((pHeadNode->pNext == pHeadNode))); \ }while(0) //插入链表结点 #define OMCI_INSERT_NODE(prevNode, insertNode) do{ \ (insertNode)->pNext = (prevNode)->pNext; \ (insertNode)->pPrev = (prevNode); \ (prevNode)->pNext->pPrev = (insertNode); \ (prevNode)->pNext = (insertNode); \ }while(0) //删除链表结点 #define OMCI_REMOVE_NODE(removeNode) do{ \ (removeNode)->pPrev->pNext = (removeNode)->pNext; \ (removeNode)->pNext->pPrev = (removeNode)->pPrev; \ }while(0) //获取链表结点及其数据(不作安全性检查) #define GET_NODE_NUM(pList) ((pList)->dwNodeNum) #define GET_HEAD_NODE(pList) ((pList)->pHead) #define GET_TAIL_NODE(pList) ((pList)->pTail) #define GET_PREV_NODE(pNode) ((pNode)->pPrev) #define GET_NEXT_NODE(pNode) ((pNode)->pNext) #define GET_NODE_DATA(pNode) ((pNode)->pvNodeData) //双向循环链表遍历校验宏 #define LIST_ITER_CHECK(pList, retVal) do{\ CHECK_SINGLE_POINTER((pList), retVal); \ CHECK_SINGLE_POINTER((pList)->pHead, retVal); \ CHECK_SINGLE_POINTER((pList)->pHead->pNext, retVal); \ }while(0) //双向循环链表遍历宏 //pList: 链表指针;pLoopNode: 链表结点,用做循环计数器; //pTmpNode: 链表结点,用做删除pLoopNode时临时保存pLoopNode->pNext //某些状况下采用遍历宏代替OmciLocateListNode或OmciTraverseListNode函数可提升执行效率。 //如外部数据和结点数据需按共同的规则转换时,采用遍历宏可以使外部数据没必要重复转换。 #define LIST_ITER_LOOP(pList, pLoopNode) \ for(pLoopNode = (pList)->pHead->pNext; \ pLoopNode != (pList)->pHead; \ pLoopNode = pLoopNode->pNext) #define LIST_ITER_LOOP_SAFE(pList, pLoopNode, pTmpNode) \ for(pLoopNode = (pList)->pHead->pNext, pTmpNode = pLoopNode->pNext; \ pLoopNode != (pList)->pHead; \ pLoopNode = pTmpNode, pTmpNode = pLoopNode->pNext)
结点的插入和删除操做可参考1.1节双向链表的图例。GET_HEAD_NODE等宏可高效(但不安全)地获取链表结点及其数据,后续将给出其函数版本。LIST_ITER_LOOP宏旨在给使用者提供必定程度的自由度,某些状况下可提升执行效率。
首先定义一组私有函数,主要是建立、删除和销毁链表结点。这些内部使用的函数已尽量保证参数安全性,故省去参数校验处理。
为简便起见,下文中“XX指针”均表示指向XX的指针。
建立新的链表结点:
/********************************************************************** * 函数名称: CreateListNode * 功能描述: 建立新的链表结点 * 输入参数: T_OMCI_LIST *pList :链表指针 * VOID *pvNodeData :待插入的链表结点数据指针 * 输出参数: NA * 返 回 值: T_OMCI_LIST_NODE* ***********************************************************************/ static T_OMCI_LIST_NODE *CreateListNode(T_OMCI_LIST *pList, VOID *pvNodeData) { T_OMCI_LIST_NODE *pInsertNode = (T_OMCI_LIST_NODE*)calloc((sizeof(T_OMCI_LIST_NODE)+pList->dwNodeDataSize), 1); if(NULL == pInsertNode) { printf("[%s]pList(%p) failed to alloc for pInsertNode!\n", FUNC_NAME, pList); return NULL; } pInsertNode->pvNodeData = (INT8U *)pInsertNode + sizeof(T_OMCI_LIST_NODE); if(NULL != pvNodeData) { //建立非头结点时 memmove(pInsertNode->pvNodeData, pvNodeData, pList->dwNodeDataSize); } return pInsertNode; }
删除指定的链表结点:
/********************************************************************** * 函数名称: RemoveListNode * 功能描述: 删除指定的链表结点(释放结点内存并置其前驱后继指针为NULL) * 输入参数: T_OMCI_LIST *pList :链表指针 * VOID *pvNode :待删除的链表结点指针 * 输出参数: NA * 返 回 值: LIST_STATUS * 注意事项: 本函数未置待删除结点指针为NULL,请避免访问已删除结点 ***********************************************************************/ static LIST_STATUS RemoveListNode(T_OMCI_LIST *pList, T_OMCI_LIST_NODE *pNode) { OMCI_ISOL_NODE(pNode); free(pNode); //释放链表结点 return OMCI_LIST_OK; }
OMCI_ISOL_NODE 宏用于"孤立"待删除的链表结点,避免经过该结点访问其前驱和后继结点(进而遍历链表)。由于RemoveListNode函数没法将结点指针置空(C语言值传递特性),故调用者需注意避免再次使用已删除的结点。若要达到结点指针置空的目的,可调用销毁结点的接口函数:
/********************************************************************** * 函数名称: DestroyListNode * 功能描述: 销毁指定的链表结点(释放结点内存并置结点指针为NULL) * 输入参数: T_OMCI_LIST *pList :链表指针 * VOID **pNode :待销毁的链表结点指针的指针 * 输出参数: NA * 返 回 值: LIST_STATUS * 注意事项: 当指向待销毁结点的指针存在多份拷贝且散布程序各处时(尤为当 * 调用链未能保证**pNode指向原始结点时),没法完全销毁该结点 ***********************************************************************/ static LIST_STATUS DestroyListNode(T_OMCI_LIST *pList, T_OMCI_LIST_NODE **pNode) { free(*pNode); //释放链表结点 *pNode = NULL; return OMCI_LIST_OK; }
DestroyListNode函数会释放指定结点的内存并将结点指针置空。但当代码中存在该结点指针的其余副本时,该函数显然没法将这些指针副本置空。
至于RemoveListNode和DestroyListNode函数孰优孰劣,可参考附注中对“迷途指针”的讨论。
有时可能须要获知链表的确切占用状况(一般没有必要),如不含任何结点、仅含头结点或者还包含其余有用结点。GetListOccupation函数可知足这一“吹毛求疵”的需求,其余状况应使用下文将要给出的判空函数OmciIsListEmpty。OmciIsListEmpty将不含任何结点和仅含头结点均视为空链表,以隐藏内部细节。
/********************************************************************** * 函数名称: GetListOccupation * 功能描述: 获取链表占用状况 * 输入参数: T_OMCI_LIST *pList :链表指针 * 输出参数: NA * 返 回 值: LIST_OCCUPATION * 注意事项: 本函数仅用于内部测试。 ***********************************************************************/ static LIST_OCCUPATION GetListOccupation(T_OMCI_LIST *pList) { CHECK_SINGLE_POINTER(pList, OMCI_LIST_NULL); CHECK_SINGLE_POINTER(pList->pHead, OMCI_LIST_NULL); return (0 == pList->dwNodeNum) ? OMCI_LIST_EMPTY : OMCI_LIST_OCCUPIED; }
基于上述私有函数,可进一步构建链表及其结点的基本操做接口。
使用链表前,必须对其初始化。初始化时将建立头结点,并肯定后续将要连接的结点数据宽度。
/********************************************************************** * 函数名称: OmciInitList * 功能描述: 链表初始化,产生空的双向循环链表 * 输入参数: T_OMCI_LIST *pList :链表指针 * INT32U dwNodeDataSize :链表结点保存的数据字节数 * 输出参数: NA * 返 回 值: LIST_STATUS ***********************************************************************/ LIST_STATUS OmciInitList(T_OMCI_LIST *pList, INT32U dwNodeDataSize) { CHECK_SINGLE_POINTER(pList, OMCI_LIST_ERROR); if(0 == dwNodeDataSize) { printf("[%s]pList=%p, dwNodeDataSize=%uBytes, undesired initialization!\n", FUNC_NAME, pList, dwNodeDataSize); return OMCI_LIST_ERROR; } pList->dwNodeDataSize = dwNodeDataSize; //给予从新修改结点数据大小的机会 if(NULL != pList->pHead) { printf("[%s]pList(%p) has been initialized!\n", FUNC_NAME, pList); return OMCI_LIST_OK; } T_OMCI_LIST_NODE *pHeadNode = CreateListNode(pList, NULL); if(NULL == pHeadNode) { printf("[%s]pList(%p) failed to create pHeadNode!\n", FUNC_NAME, pList); return OMCI_LIST_ERROR; } OMCI_INIT_NODE(pHeadNode); pList->pHead = pList->pTail = pHeadNode; pList->dwNodeNum = 0; return OMCI_LIST_OK; }
一般不会中途修改dwNodeDataSize。仅当使用者确知数据宽度的变化边界(如确知前N个结点数据为四字节,其后为八字节)时,中途修改dwNodeDataSize才有意义。固然,也可新增一个OmciResizeList接口。
调用OmciInitList接口后,将建立一张仅含头结点的空双向循环链表。此后可向链表中插入结点。
暂时不须要当前链表时,可清空链表除头结点外的结点。这样再次使用时无需初始化链表,直接插入结点便可。若肯定再也不须要当前链表时,可销毁链表的全部结点。OmciClearList和OmciDestroyList函数分别完成链表的清空和销毁。
/********************************************************************** * 函数名称: OmciClearList * 功能描述: 清空双向循环链表除头结点外的结点 * 输入参数: T_OMCI_LIST *pList :链表指针 * 输出参数: NA * 返 回 值: LIST_STATUS * 注意事项: 清空链表结点后,再次插入结点时不须要初始化链表。 ***********************************************************************/ LIST_STATUS OmciClearList(T_OMCI_LIST *pList) { LIST_ITER_CHECK(pList, OMCI_LIST_ERROR); T_OMCI_LIST_NODE *pNextNode, *pListNode = pList->pHead->pNext; while(pListNode != pList->pHead) { pNextNode = pListNode->pNext; RemoveListNode(pList, pListNode); pListNode = pNextNode; } OMCI_INIT_NODE(pList->pHead); pList->pTail = pList->pHead; pList->dwNodeNum = 0; return OMCI_LIST_OK; } /********************************************************************** * 函数名称: OmciDestroyList * 功能描述: 销毁双向循环链表,包括头结点 * 输入参数: T_OMCI_LIST *pList :链表指针 * 输出参数: NA * 返 回 值: LIST_STATUS * 注意事项: 销毁链表后,再次插入结点时须要初始化链表。 ***********************************************************************/ LIST_STATUS OmciDestroyList(T_OMCI_LIST *pList) { LIST_ITER_CHECK(pList, OMCI_LIST_ERROR); T_OMCI_LIST_NODE *pNextNode, *pListNode = pList->pHead->pNext; while(pListNode != pList->pHead) { pNextNode = pListNode->pNext; DestroyListNode(pList, &pListNode); pListNode = pNextNode; } DestroyListNode(pList, &(pList->pHead)); //销毁头结点 pList->pTail = NULL; //尾结点指针置空 pList->dwNodeNum = 0; pList->dwNodeDataSize = 0; return OMCI_LIST_OK; }
清空或销毁链表后,OmciIsListEmpty函数的返回值将为逻辑真,代表当前链表为空。
/********************************************************************** * 函数名称: OmciIsListEmpty * 功能描述: 判断链表是否为空(仅含头结点或不含任何结点) * 输入参数: T_OMCI_LIST *pList :链表指针 * 输出参数: NA * 返 回 值: BOOL ***********************************************************************/ BOOL OmciIsListEmpty(T_OMCI_LIST *pList) { CHECK_SINGLE_POINTER(pList, OMCI_LIST_TRUE); CHECK_SINGLE_POINTER(pList->pHead, OMCI_LIST_TRUE); T_OMCI_LIST_NODE *pHeadNode = pList->pHead; if((0 == pList->dwNodeNum) && (pHeadNode->pPrev == pHeadNode) && //冗余校验以增强安全性 (pHeadNode->pNext == pHeadNode)) { return OMCI_LIST_TRUE; } else { return OMCI_LIST_FALSE; } }
此处为增强安全性对头结点进行检验,但并不是必要。若剔除冗余校验,则OmciIsListEmpty函数的实现会更为简洁高效。
链表初始化后,可在链表头结点后逆序或顺序依次插入新的结点。当从头结点向后继方向遍历时,逆序插入的行为相似于栈,而顺序插入的行为相似于队列。
/********************************************************************** * 函数名称: OmciPrependListNode * 功能描述: 在链表头结点后逆序增长结点,尾结点恒为头结点 * 在头结点指针pHead所指向结点和pHead->pNext所指向结点 * 之间插入新结点,先插入的结点向右移动。遍历链表时 * 从pHead开始向右依次访问至最早插入的结点,相似于栈。 * 输入参数: T_OMCI_LIST *pList :链表指针 * VOID *pvNodeData :待插入的链表结点数据指针 * 输出参数: NA * 返 回 值: LIST_STATUS ***********************************************************************/ LIST_STATUS OmciPrependListNode(T_OMCI_LIST *pList, VOID *pvNodeData) { CHECK_DOUBLE_POINTER(pList, pvNodeData, OMCI_LIST_ERROR); if(0 == pList->dwNodeDataSize) { printf("[%s]pList=%p, dwNodeDataSize=0Bytes, probably uninitialized or initialized improperly. See 'OmciInitList'!\n", FUNC_NAME, pList); return OMCI_LIST_ERROR; } T_OMCI_LIST_NODE *pInsertNode = CreateListNode(pList, pvNodeData); if(NULL == pInsertNode) { printf("[%s]pList(%p) failed to create pInsertNode!\n", FUNC_NAME, pList); return OMCI_LIST_ERROR; } OMCI_INSERT_NODE(pList->pHead, pInsertNode); //在链表头结点后增长一个结点 pList->dwNodeNum++; return OMCI_LIST_OK; } /********************************************************************** * 函数名称: OmciAppendListNode * 功能描述: 在链表头结点后顺序增长结点,新结点做为尾结点 * 在头结点指针pHead所指向结点前(即尾结点后)插入新结点, * 先插入的结点向左移动。遍历链表时从pHead开始向右依次 * 访问至最后插入的结点,相似于队列。 * 双向循环链表已保证pList->pTail(即pHead->pPrev)非空。 * 输入参数: T_OMCI_LIST *pList :链表指针 * VOID *pvNodeData :待插入的链表结点数据指针 * 输出参数: NA * 返 回 值: LIST_STATUS ***********************************************************************/ LIST_STATUS OmciAppendListNode(T_OMCI_LIST *pList, VOID *pvNodeData) { CHECK_DOUBLE_POINTER(pList, pvNodeData, OMCI_LIST_ERROR); if(0 == pList->dwNodeDataSize) { printf("[%s]pList=%p, dwNodeDataSize=0Bytes, probably uninitialized or initialized improperly. See 'OmciInitList'!\n", FUNC_NAME, pList); return OMCI_LIST_ERROR; } T_OMCI_LIST_NODE *pInsertNode = CreateListNode(pList, pvNodeData); if(NULL == pInsertNode) { printf("[%s]pList(%p) failed to create pInsertNode!\n", FUNC_NAME, pList); return OMCI_LIST_ERROR; } OMCI_INSERT_NODE(pList->pTail, pInsertNode); //在链表尾结点后增长一个结点 pList->pTail = pInsertNode; //新的尾结点指向当前添加的结点 pList->dwNodeNum++; return OMCI_LIST_OK; }
对dwNodeDataSize 的校验用于指示链表未初始化或未正确初始化的错误。将该校验置于私有函数CreateListNode中可简化Prepend和Append代码。但FUNC_NAME信息将暴露内部函数,从而给使用者形成疑惑,故该校验予以保留。
有时须要在链表中任意位置插入结点,此时可以使用OmciInsertListNode接口。
/********************************************************************** * 函数名称: OmciInsertListNode * 功能描述: 在链表中任意位置插入结点 * 输入参数: T_OMCI_LIST *pList :链表指针 * T_OMCI_LIST_NODE *pPrevNode :待插入结点的前驱结点指针 * VOID *pvNodeData :待插入结点的数据域指针 * 输出参数: NA * 返 回 值: LIST_STATUS * 注意事项: 若pPrevNode恒为头结点或尾结点,请使用OmciPrependListNode * 或OmciAppendListNode函数 ***********************************************************************/ LIST_STATUS OmciInsertListNode(T_OMCI_LIST *pList, T_OMCI_LIST_NODE *pPrevNode, VOID *pvNodeData) { CHECK_TRIPLE_POINTER(pList, pPrevNode, pvNodeData, OMCI_LIST_ERROR); if(0 == pList->dwNodeDataSize) { printf("[%s]pList=%p, dwNodeDataSize=0Bytes, probably uninitialized or initialized improperly. See 'OmciInitList'!\n", FUNC_NAME, pList); return OMCI_LIST_ERROR; } T_OMCI_LIST_NODE *pInsertNode = CreateListNode(pList, pvNodeData); if(NULL == pInsertNode) { printf("[%s]pList(%p) failed to create pInsertNode!\n", FUNC_NAME, pList); return OMCI_LIST_ERROR; } OMCI_INSERT_NODE(pPrevNode, pInsertNode); if(pPrevNode == pList->pTail) pList->pTail = pInsertNode; pList->dwNodeNum++; return OMCI_LIST_OK; }
当pPrevNode恒为头结点时,OmciInsertListNode接口等效于OmciPrependListNode;当pPrevNode恒为尾结点时,OmciInsertListNode接口等效于OmciAppendListNode。这两种状况建议使用Prepend或Append接口(毕竟减小一个参数)。
插入若干结点后,就可删除或销毁链表中除头结点外的任一结点。
/********************************************************************** * 函数名称: OmciRemoveListNode * 功能描述: 删除双向循环链表中除头结点外的某一结点 * 输入参数: T_OMCI_LIST *pList :链表指针 * T_OMCI_LIST_NODE *pNode :待删除的链表结点指针 * 输出参数: NA * 返 回 值: LIST_STATUS ***********************************************************************/ LIST_STATUS OmciRemoveListNode(T_OMCI_LIST *pList, T_OMCI_LIST_NODE *pNode) { CHECK_DOUBLE_POINTER(pList, pNode, OMCI_LIST_ERROR); CHECK_DOUBLE_POINTER(pNode->pPrev, pNode->pNext, OMCI_LIST_ERROR); if(0 == pList->dwNodeNum) { printf("[%s]pList(%p) has no node to be Removed!\n", FUNC_NAME, pList); return OMCI_LIST_ERROR; } OMCI_REMOVE_NODE(pNode); if(pNode->pNext == pList->pHead) { pList->pTail = pNode->pPrev; //删除尾结点 } RemoveListNode(pList, pNode); pList->dwNodeNum--; return OMCI_LIST_OK; } /********************************************************************** * 函数名称: OmciDestroyListNode * 功能描述: 销毁双向循环链表中除头结点外的某一结点 * 输入参数: T_OMCI_LIST *pList :链表指针 * T_OMCI_LIST_NODE **pNode :待销毁的链表结点二级指针 * 输出参数: NA * 返 回 值: LIST_STATUS ***********************************************************************/ LIST_STATUS OmciDestroyListNode(T_OMCI_LIST *pList, T_OMCI_LIST_NODE **pNode) { CHECK_DOUBLE_POINTER(pList, pNode, OMCI_LIST_ERROR); CHECK_SINGLE_POINTER(*pNode, OMCI_LIST_ERROR); if(0 == pList->dwNodeNum) { printf("[%s]pList(%p) has no node to be Removed!\n", FUNC_NAME, pList); return OMCI_LIST_ERROR; } OMCI_REMOVE_NODE(*pNode); if((*pNode)->pNext == pList->pHead) { pList->pTail = (*pNode)->pPrev; //删除尾结点 } DestroyListNode(pList, pNode); pList->dwNodeNum--; return OMCI_LIST_OK; }
然而,要删除或销毁链表结点,必须先定位到该结点。
在链表中“定位”某个结点有两种手段:一是经过结点编号查找,如OmciGetListNodeByIndex;二是经过某种给定条件匹配,如OmciLocateListNode(查找首个知足给定条件的结点)。
/********************************************************************** * 函数名称: OmciGetListNodeByIndex * 功能描述: 获取链表中指定序号的结点(按头结点后继方向排序) * 输入参数: T_OMCI_LIST* pList :链表指针 * INT32U dwNodeIndex :结点序号(从1开始) * 输出参数: NA * 返 回 值: T_OMCI_LIST_NODE* 链表结点指针(空表返回NULL) ***********************************************************************/ T_OMCI_LIST_NODE* OmciGetListNodeByIndex(T_OMCI_LIST *pList, INT32U dwNodeIndex) { CHECK_SINGLE_POINTER(pList, NULL); if(0 == dwNodeIndex) return pList->pHead; //也可返回NULL if(dwNodeIndex >= pList->dwNodeNum) return pList->pTail; INT32U dwNodeIdx = 1; T_OMCI_LIST_NODE *pListNode = pList->pHead; for(; dwNodeIdx <= dwNodeIndex; dwNodeIdx++) pListNode = pListNode->pNext; return pListNode; } /********************************************************************** * 函数名称: OmciLocateListNode * 功能描述: 查找链表中首个与pData知足函数fpCompareNode断定关系的结点 * 输入参数: T_OMCI_LIST* pList :链表指针 * VOID* pvData :待比较数据指针 * CompareNodeFunc fpCompareNode :比较回调函数指针 * 输出参数: NA * 返 回 值: T_OMCI_LIST_NODE* 链表结点指针(未找到时返回NULL) ***********************************************************************/ /* 比较回调函数原型,用来自定义链表节点比较 */ typedef INT8U (*CompareNodeFunc)(VOID *pvNodeData, VOID *pvData, INT32U dwNodeDataSize); T_OMCI_LIST_NODE* OmciLocateListNode(T_OMCI_LIST *pList, VOID *pvData, CompareNodeFunc fpCompareNode) { CHECK_TRIPLE_POINTER(pList, pvData, fpCompareNode, NULL); CHECK_SINGLE_POINTER(pList->pHead, NULL); CHECK_SINGLE_POINTER(pList->pHead->pNext, NULL); T_OMCI_LIST_NODE *pListNode = pList->pHead->pNext; while(pListNode != pList->pHead) { if(0 == fpCompareNode(pListNode->pvNodeData, pvData, pList->dwNodeDataSize)) return pListNode; pListNode = pListNode->pNext; } return NULL; }
可见,OmciLocateListNode接口本质上就是“遍历+匹配”。要进行单纯而强大的遍历操做,可以使用OmciTraverseListNode接口。
/********************************************************************** * 函数名称: OmciTraverseListNode * 功能描述: 链表结点遍历函数,遍历操做由fpTravNode指定 * 输入参数: T_OMCI_LIST* pList :链表指针 * VOID* pvTravInfo :遍历操做回调函数所需信息 * 也可为空,取决于回调函数具体实现 * TravNodeFunc fpTravNode :遍历操做回调函数指针 * 输出参数: NA * 返 回 值: LIST_STATUS * 注意事项: 本函数可间接实现Print等操做,但不建议代替后者。 * fpTravNode返回非0(OMCI_LIST_OK)值时停止遍历 ***********************************************************************/ typedef LIST_STATUS (*TravNodeFunc)(VOID *pvNode, VOID *pvTravInfo, INT32U dwNodeDataSize); LIST_STATUS OmciTraverseListNode(T_OMCI_LIST *pList, VOID *pvTravInfo, TravNodeFunc fpTravNode) { CHECK_DOUBLE_POINTER(pList, fpTravNode, OMCI_LIST_ERROR); CHECK_SINGLE_POINTER(pList->pHead, OMCI_LIST_ERROR); CHECK_SINGLE_POINTER(pList->pHead->pNext, OMCI_LIST_ERROR); T_OMCI_LIST_NODE *pListNode = pList->pHead->pNext; while(pListNode != pList->pHead) { T_OMCI_LIST_NODE *pTmpNode = pListNode->pNext; //fpTravNode内可能会销毁结点pListNode if(OMCI_LIST_OK != fpTravNode(pListNode, pvTravInfo, pList->dwNodeDataSize)) break; pListNode = pTmpNode; } return OMCI_LIST_OK; }
由于OmciAppendListNode和OmciPrependListNode已暗含“正序”和“逆序”的意思,故仅提供OmciTraverseListNode函数,而无需再增长逆序遍历的接口(除非须要同时双序遍历)。
经常须要打印输出链表结点的数据域内容,而OmciTraverseListNode接口稍显笨重。此时可以使用专门的打印接口OmciPrintListNode。
/********************************************************************** * 函数名称: OmciPrintListNode * 功能描述: 打印输出链表结点的数据域内容 * 输入参数: T_OMCI_LIST* pList :链表指针 * PrintListFunc fpPrintList :打印回调函数指针 * 输出参数: NA * 返 回 值: LIST_STATUS ***********************************************************************/ /* 打印回调函数原型,用来自定义链表内容打印 */ typedef VOID (*PrintListFunc)(VOID *pNodeData, INT32U dwNodeNum); LIST_STATUS OmciPrintListNode(T_OMCI_LIST *pList, PrintListFunc fpPrintList) { CHECK_DOUBLE_POINTER(pList, fpPrintList, OMCI_LIST_ERROR); CHECK_SINGLE_POINTER(pList->pHead, OMCI_LIST_ERROR); CHECK_SINGLE_POINTER(pList->pHead->pNext, OMCI_LIST_ERROR); T_OMCI_LIST_NODE *pListNode = pList->pHead->pNext; while(pListNode != pList->pHead) { //具体打印格式交给回调函数灵活处理(可直接打印也可拷贝至本地处理后打印) fpPrintList(pListNode->pvNodeData, pList->dwNodeNum); pListNode = pListNode->pNext; } printf("\n"); return OMCI_LIST_OK; }
对于CompareNodeFunc 和PrintListFunc,如下给出两个范例:
/********************************************************************** * 函数名称: CompareNodeGeneric * 功能描述: 通用链表结点内存比较 * 输入参数: VOID *pvNodeData :链表结点数据指针 * VOID *pvData :待比较外部数据指针 * INT32U dwNodeDataSize :链表结点数据大小 * 输出参数: NA * 返 回 值: 0:Equal; !0:Unequal * 注意事项: 比较长度为结点数据字节数,即默认与外部数据大小一致 ***********************************************************************/ INT8U CompareNodeGeneric(VOID *pvNodeData, VOID *pvData, INT32U dwNodeDataSize) { CHECK_DOUBLE_POINTER(pvNodeData, pvData, 1); return memcmp(pvNodeData, pvData, dwNodeDataSize); } /********************************************************************** * 函数名称: PrintListWord * 功能描述: 打印链表结点,结点数据域为两字节整数 * 输入参数: VOID *pvNodeData :链表节点数据指针 * INT32U dwNodeNum :链表节点数目 * 输出参数: NA * 返 回 值: VOID * 注意事项: 仅做示例,未考虑字节序等问题。 ***********************************************************************/ VOID PrintListWord(VOID *pvNodeData, INT32U dwNodeNum) { CHECK_SINGLE_POINTER(pvNodeData, RETURN_VOID); printf("%d ", *((INT16U *)pvNodeData)); }
最后,给出获取链表结点及其数据的安全接口:
/********************************************************************** * 函数名称: OmciGetListNodeNum * 功能描述: 获取链表结点数目 * 输入参数: T_OMCI_LIST *pList :链表指针 * 输出参数: NA * 返 回 值: INT32U 链表结点数目 ***********************************************************************/ INT32U OmciGetListNodeNum(T_OMCI_LIST *pList) { CHECK_SINGLE_POINTER(pList, 0); return (pList->dwNodeNum); } /********************************************************************** * 函数名称: OmciGetListHead/OmciGetListTail * 功能描述: 获取链表头结点/尾结点指针 * 输入参数: T_OMCI_LIST *pList :链表指针 ***********************************************************************/ T_OMCI_LIST_NODE* OmciGetListHead(T_OMCI_LIST *pList) { CHECK_SINGLE_POINTER(pList, NULL); return (pList->pHead); } T_OMCI_LIST_NODE* OmciGetListTail(T_OMCI_LIST *pList) { CHECK_SINGLE_POINTER(pList, NULL); return (pList->pTail); } /********************************************************************** * 函数名称: OmciGetPrevNode/OmciGetNextNode * 功能描述: 获取链表指定结点的前驱结点/后继结点指针 * 输入参数: T_OMCI_LIST_NODE *pNode :指定结点的指针 ***********************************************************************/ T_OMCI_LIST_NODE* OmciGetPrevNode(T_OMCI_LIST_NODE *pNode) { CHECK_SINGLE_POINTER(pNode, NULL); return (pNode->pPrev); } T_OMCI_LIST_NODE* OmciGetNextNode(T_OMCI_LIST_NODE *pNode) { CHECK_SINGLE_POINTER(pNode, NULL); return (pNode->pNext); } /********************************************************************** * 函数名称: OmciGetNodeData * 功能描述: 获取链表指定结点的数据域 * 输入参数: T_OMCI_LIST_NODE *pNode :指定结点的指针 ***********************************************************************/ VOID* OmciGetNodeData(T_OMCI_LIST_NODE *pNode) { CHECK_DOUBLE_POINTER(pNode, pNode->pvNodeData, NULL); return (pNode->pvNodeData); }
本节将对上文实现的链表操做接口进行测试,测试函数兼做使用示例。
#ifdef TEST_AND_EXAMPLE static LIST_STATUS TravPrintWord(VOID *pvNode, VOID *pvTravInfo, INT32U dwNodeDataSize) { CHECK_SINGLE_POINTER(pvNode, OMCI_LIST_ERROR); T_OMCI_LIST_NODE *pNode = (T_OMCI_LIST_NODE *)pvNode; printf("%d ", *((INT16U *)GET_NODE_DATA(pNode))); return OMCI_LIST_OK; } T_OMCI_LIST gExampleList = {0}; VOID ListTestExample(VOID) { //本函数并不是严格意义上的测试函数,主要用做示例,且示例并不是最佳用法。 INT8U ucTestIndex = 1; INT16U aTestListData[] = {11, 22, 33, 44, 55, 66}; printf("\n<Test Case %u>: Initialization!\n", ucTestIndex++); OmciInitList(&gExampleList, sizeof(INT16U)); printf("gExampleList=%p, pHead=%p, pTail=%p\n", &gExampleList, OmciGetListHead(&gExampleList), OmciGetListTail(&gExampleList)); printf("\n<Test Case %u>: Append Node to List!\n", ucTestIndex++); OmciAppendListNode(&gExampleList, &aTestListData[0]); OmciAppendListNode(&gExampleList, &aTestListData[1]); OmciAppendListNode(&gExampleList, &aTestListData[2]); printf("OmciIsListEmpty=%u(0-Occupied; 1-Empty)\n", OmciIsListEmpty(&gExampleList)); printf("gExampleList NodeNum=%u\n", OmciGetListNodeNum(&gExampleList)); OmciPrintListNode(&gExampleList, PrintListWord); printf("\n<Test Case %u>: Insert Node to List!\n", ucTestIndex++); T_OMCI_LIST_NODE *pPrevNode = OmciGetListNodeByIndex(&gExampleList, 2); printf("NodeData2=%d\n", *((INT16U *)OmciGetNodeData(pPrevNode))); OmciInsertListNode(&gExampleList, pPrevNode, &aTestListData[4]); printf("gExampleList NodeNum=%u\n", OmciGetListNodeNum(&gExampleList)); OmciPrintListNode(&gExampleList, PrintListWord); printf("\n<Test Case %u>: Remove Node from List!\n", ucTestIndex++); T_OMCI_LIST_NODE *pDeleteNode = OmciLocateListNode(&gExampleList, &aTestListData[1], CompareNodeGeneric); OmciRemoveListNode(&gExampleList, pDeleteNode); printf("gExampleList NodeNum=%u\n", OmciGetListNodeNum(&gExampleList)); OmciPrintListNode(&gExampleList, PrintListWord); printf("\n<Test Case %u>: Clear List!\n", ucTestIndex++); OmciClearList(&gExampleList); printf("gExampleList=%p, pHead=%p, pTail=%p\n", &gExampleList, GET_HEAD_NODE(&gExampleList), GET_TAIL_NODE(&gExampleList)); printf("OmciIsListEmpty=%u(0-Occupied; 1-Empty)\n", OmciIsListEmpty(&gExampleList)); printf("gExampleList NodeNum=%u\n", OmciGetListNodeNum(&gExampleList)); printf("\n<Test Case %u>: Prepend Node to List!\n", ucTestIndex++); OmciPrependListNode(&gExampleList, &aTestListData[3]); OmciPrependListNode(&gExampleList, &aTestListData[4]); OmciPrependListNode(&gExampleList, &aTestListData[5]); printf("OmciIsListEmpty=%u(0-Occupied; 1-Empty)\n", OmciIsListEmpty(&gExampleList)); printf("gExampleList NodeNum=%u\n", OmciGetListNodeNum(&gExampleList)); OmciPrintListNode(&gExampleList, PrintListWord); T_OMCI_LIST_NODE *pListNode = NULL; LIST_ITER_LOOP(&gExampleList, pListNode) { printf("%d ", *((INT16U *)GET_NODE_DATA(pListNode))); } printf("\n"); OmciTraverseListNode(&gExampleList, NULL, TravPrintWord); printf("\n"); printf("\n<Test Case %u>: Destory List!\n", ucTestIndex++); OmciDestroyList(&gExampleList); printf("gExampleList=%p, pHead=%p, pTail=%p\n", &gExampleList, GET_HEAD_NODE(&gExampleList), GET_TAIL_NODE(&gExampleList)); printf("gExampleList NodeNum=%u\n", OmciGetListNodeNum(&gExampleList)); printf("GetListOccupation=%u(0-Occupied; 1-Empty; 2-Null)\n", GetListOccupation(&gExampleList)); return; } #endif
在上述测试代码中,Prepend或Append结点的代码若用OmciInsertListNode实现,以下:
OmciInsertListNode(&gExampleList, OmciGetListHead(&gExampleList), &aTestListData[3]); OmciInsertListNode(&gExampleList, OmciGetListHead(&gExampleList), &aTestListData[4]); OmciInsertListNode(&gExampleList, OmciGetListHead(&gExampleList), &aTestListData[5]); //Or OmciInsertListNode(&gExampleList, OmciGetListTail(&gExampleList), &aTestListData[1]); OmciInsertListNode(&gExampleList, OmciGetListTail(&gExampleList), &aTestListData[2]); OmciInsertListNode(&gExampleList, OmciGetListTail(&gExampleList), &aTestListData[3]);
测试结果以下所示:
<Test Case 1>: Initialization! gExampleList=0x804bc8c, pHead=0x8b39010, pTail=0x8b39010 <Test Case 2>: Append Node to List! OmciIsListEmpty=0(0-Occupied; 1-Empty) gExampleList NodeNum=3 11 22 33 <Test Case 3>: Insert Node to List! NodeData2=22 gExampleList NodeNum=4 11 22 55 33 <Test Case 4>: Remove Node from List! gExampleList NodeNum=3 11 55 33 <Test Case 5>: Clear List! gExampleList=0x804bc8c, pHead=0x8b39010, pTail=0x8b39010 OmciIsListEmpty=1(0-Occupied; 1-Empty) gExampleList NodeNum=0 <Test Case 6>: Prepend Node to List! OmciIsListEmpty=0(0-Occupied; 1-Empty) gExampleList NodeNum=3 66 55 44 66 55 44 66 55 44 <Test Case 7>: Destory List! gExampleList=0x804bc8c, pHead=(nil), pTail=(nil) gExampleList NodeNum=0 [GetListOccupation(140)]Null Pointer: pList->pHead! GetListOccupation=2(0-Occupied; 1-Empty; 2-Null)
迷途指针(Dangling pointer,亦称悬垂指针)和野指针(Wild pointer)
可见,迷途指针和野指针均指向不合法的对象,应禁止读写其指向的内存。野指针简单且易于处理,如下主要讨论迷途指针。
在C语言中,当指针所指向的动态内存被显式地释放(free)后,该指针就成为迷途指针。若经过迷途指针访问或修改已释放的动态分配内存,则可能引起难以排查的故障(尤为当原对象内存分配做他用时)。若指针是函数内的自动变量,函数退出时会被自动销毁;不然,最好在释放动态内存后将该指针置空(NULL)。虽然将迷途指针从新置空的作法可能隐藏诸如double free之类的逻辑问题,但却使得对它的读写错误更容易暴露(尤为是在多线程环境中)。
在C语言中,可经过下述两种free替代版原本尽量避免迷途指针错误:
#define SAFE_FREE((pointer)) do{ \ if(pointer != NULL){ \ free(pointer); \ pointer = NULL; \ }while(0); void SafeFree(void **pointer) { if(pointer != NULL) { free(*pointer); *pointer = NULL; } }
然而,当指向动态分配内存的指针存在多个副本且散布程序各处时,该技术不会置空其余指针变量,从而致使释放后指针行为的不一致。所以,编码者应保证每一个指针都有其明确的用途和生存期。
注意,由于C语言的值传递特性,现有的free库函数内不可能将入参指针置空。若要达到置空的目的,必须传入二级指针,如SafeFree。但SafeFree必然与其内存分配版本(如SafeAlloc)的入参类型不一致,这会增长使用者出错的机率。而由上面的讨论可知,即便置空当前入参指针,也没法清除其副本。所以,最好由调用者自行决定如何置空。至于做者倾向于free仍是SafeFree,可参考《关于Linux系统basename函数缺陷的思考》一文,或者试想下逐级释放的顺序性。
另外一种常见的迷途指针产生于试图返回栈上分配的局部变量的地址。详见《已释放的栈内存》一文。