說明
相比Linux內核鏈表宿主結構可有多個鏈表結構的優點,本函數集側重封裝性和易用性,而靈活性和效率有所降低。
可基於該函數集方便地構造棧或隊列集。
本函數集暫未考慮並發保護。
一 概念
鏈表是一種物理存儲單元上非連續、非順序的存儲結構,數據元素的邏輯順序通過鏈表中的指針鏈接次序實現。鏈表由一系列存儲結點組成,結點可在運行時動態生成。每個結點均由兩部分組成,即存儲數據元素的數據域和存儲相鄰結點地址的指針域。當進行插入或刪除操作時,鏈表只需修改相關結點的指針域即可,因此相比線性表順序結構更加方便省時。
鏈表可分為單鏈表(Singly Linked List)、雙向鏈表(Doubly Linked List)和循環鏈表(Circular List)等。一個單鏈表結點僅包含一個指向其直接后繼結點的指針,因此當需要操作某個結點的直接前驅結點時,必須從單鏈表表頭開始查找。
雙向鏈表和循環鏈表均為單鏈表的變體。通常創建雙向循環鏈表以綜合利用兩者的優點。
1.1 雙向鏈表
雙向鏈表的每個結點除含有數據域外,還有兩個指針域,分別指向直接前驅結點和直接后繼結點。因此,從雙向鏈表中的任一結點開始,均可方便地訪問其前驅結點和后繼結點。雙向鏈表的結點結構示意圖如下所示:
圖1 雙向鏈表的結點結構
其中,Data為結點存儲的數據元素,prev指針指向該結點的前驅結點,next指針指向該結點的后繼結點。雙向鏈表通常含有一個表頭結點,亦稱哨兵結點(Sentinel Node),用於簡化插入和刪除等操作。帶頭結點的非空雙向鏈表如下圖所示:
圖2 帶頭結點的非空雙向鏈表
圖中,表頭指針dhead指向表頭結點Head,該結點的前驅指針為空;結點C為表尾結點,其后繼指針為空。除表頭結點和表尾結點外,對指向雙向鏈表任一結點的指針p,滿足下面的關系:
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即可。
1.2 循環鏈表
將單鏈表尾結點的指針域指向第一個結點或表頭結點,即構成單向循環鏈表,簡稱循環鏈表。從循環鏈表中任一結點單向出發,均可找到鏈表中其他結點。
借助表頭結點可統一空表和非空表的運算,因此循環鏈表中往往加入表頭結點。帶頭結點的循環鏈表如下圖所示:
圖5 帶頭結點的循環鏈表(頭指針)
循環鏈表的操作算法與普通單鏈表基本相同,只是對表尾的判斷有所改變。在循環鏈表chead中,判斷表尾結點p的條件是p->next == chead,即當結點的后繼指針指向表頭結點時,說明已到表尾。
注意,創建循環鏈表時必須使其尾結點的后繼指針指向表頭結點,尤其是在尾結點后插入新結點時。
棄用頭指針而采用尾指針,可方便地找到循環鏈表的開始結點和終端結點。如下圖所示:
圖6 帶頭結點的循環鏈表(尾指針)
1.3 雙向循環鏈表
雙向鏈表通常采用帶表頭結點的循環鏈表形式,即雙向循環鏈表。雙向循環鏈表在雙向鏈表的基礎上,將表頭結點的前驅指針指向尾結點,尾結點的后驅指針指向頭結點,首尾相連形成一個雙向環。雙向循環鏈表可方便地獲取當前結點的前驅結點,不必像單向循環鏈表那樣從頭開始遍歷;而其循環的特性又可方便地從任一結點出發單向遍歷整個鏈表,不必像雙向鏈表那樣根據方向而使用不同的指針域。
帶頭結點的雙向循環鏈表如下圖所示:
圖7 帶頭結點的雙向循環鏈表
二 實現
本節將采用C語言實現一個通用雙向循環鏈表的創建及操作函數集。
文中“OMCI_”和“Omci”前綴為代碼所在模塊名信息,使用接口時可按需修改這些前綴。
2.1 數據結構
定義雙向循環鏈表單元結構示意如下:
圖8 雙向循環鏈表單元結構示意圖
其中,根結點的pHead字段指向鏈表頭結點,pTail字段指向鏈表尾結點。頭結點的pPrev字段指向尾結點,尾結點的pNext字段指向頭結點。若鏈表為空(僅含頭結點),則pHead和pTail字段均指向頭結點。
鏈表結點定義如下:
1 typedef struct T_OMCI_LIST_NODE{ 2 struct T_OMCI_LIST_NODE *pPrev; /* 指向鏈表直接前驅結點的指針 */ 3 struct T_OMCI_LIST_NODE *pNext; /* 指向鏈表直接后繼結點的指針 */ 4 VOID *pvNodeData; /* 指向鏈表數據的指針。獲取具體數據時需顯式轉換該指針類型為目標類型 */ 5 }T_OMCI_LIST_NODE;
相應地,鏈表定義如下:
1 typedef struct{ 2 T_OMCI_LIST_NODE *pHead; /* 指向鏈表頭結點的指針 */ 3 T_OMCI_LIST_NODE *pTail; /* 指向鏈表尾結點的指針 */ 4 INT32U dwNodeNum; /* 鏈表結點數目 */ 5 INT32U dwNodeDataSize; /* 鏈表結點保存的數據字節數 */ 6 }T_OMCI_LIST;
為支持不同的數據類型和數據結構(通用性),鏈表結點數據域定義為VOID *pvNodeData指針。變量dwNodeDataSize指示數據域的數據寬度(字節數)。也可將數據寬度信息存儲於頭結點數據域內,從而不必定義變量dwNodeDataSize。通過遍歷鏈表並計數可得結點數目,故變量dwNodeNum也並非必要。因此,dwNodeDataSize和dwNodeNum意在簡化邏輯,也是“空間換時間”思想的體現。
除此之外,還定義以下狀態值,以使鏈表內部狀態透明化:
1 //鏈表函數返回狀態枚舉值 2 typedef enum{ 3 OMCI_LIST_OK = (INT8U)0, 4 OMCI_LIST_ERROR = (INT8U)1 5 }LIST_STATUS; 6 7 //鏈表結點空閑情況枚舉值 8 typedef enum{ 9 OMCI_LIST_OCCUPIED = (INT8U)0, 10 OMCI_LIST_EMPTY = (INT8U)1, 11 OMCI_LIST_NULL = (INT8U)2 12 }LIST_OCCUPATION; 13 14 //BOOL型常量,適用於'Is'前綴函數 15 #define OMCI_LIST_TRUE (BOOL)1 16 #define OMCI_LIST_FALSE (BOOL)0
2.2 宏代碼
為確保安全性,鏈表操作中需要進行大量的指針校驗。因此,定義幾個校驗空指針的宏,以簡化代碼篇幅:
1 #define FUNC_NAME __FUNCTION__ //(__func__) 2 3 /* 指針校驗宏 */ 4 //若無返回值則retVal置RETURN_VOID 5 #define RETURN_VOID 6 #define CHECK_SINGLE_POINTER(ptr1, retVal) do{\ 7 if(NULL == (ptr1)) 8 { \ 9 printf("[%s(%d)]Null Pointer: "#ptr1"!\n\r", FUNC_NAME, __LINE__); \ 10 return retVal; \ 11 } \ 12 }while(0) 13 #define CHECK_DOUBLE_POINTER(ptr1, ptr2, retVal) do{\ 14 if((NULL == (ptr1)) || (NULL == (ptr2))) \ 15 { \ 16 printf("[%s(%d)]Null Pointer: "#ptr1"(%p), "#ptr2"(%p)!\n\r", FUNC_NAME, __LINE__, ptr1, ptr2); \ 17 return retVal; \ 18 } \ 19 }while(0) 20 #define CHECK_TRIPLE_POINTER(ptr1, ptr2, ptr3, retVal) do{\ 21 if((NULL == (ptr1)) || (NULL == (ptr2)) || (NULL == (ptr3))) \ 22 { \ 23 printf("[%s(%d)]Null Pointer: "#ptr1"(%p), "#ptr2"(%p), "#ptr3"(%p)!\n\r", FUNC_NAME, __LINE__, ptr1, ptr2, ptr3); \ 24 return retVal; \ 25 } \ 26 }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節函數接口中,以保證其安全性。使用者若能在外部杜絕空指針引用,則可添加條件編譯開關“剔除”這些校驗宏,以提高代碼執行效率。
對於鏈表結點的操作比較固定,因此也用宏定義加以封裝:
1 //創建結點為作為鏈表頭以生成雙向循環空鏈表 2 #define OMCI_INIT_NODE(pNode) do{ \ 3 (pNode)->pNext = (pNode)->pPrev = (pNode); \ 4 }while(0) 5 //"孤立"鏈表結點,避免通過該結點訪問其前驅和后繼結點(進而遍歷鏈表) 6 #define OMCI_ISOL_NODE(pNode) do{ \ 7 (pNode)->pNext = (pNode)->pPrev = NULL; \ 8 }while(0) 9 //判斷鏈表是否僅含頭結點 10 #define OMCI_LIST_WITH_HEAD(pHeadNode) do{ \ 11 (((pHeadNode)->pPrev == (pHeadNode)) && ((pHeadNode->pNext == pHeadNode))); \ 12 }while(0) 13 14 //插入鏈表結點 15 #define OMCI_INSERT_NODE(prevNode, insertNode) do{ \ 16 (insertNode)->pNext = (prevNode)->pNext; \ 17 (insertNode)->pPrev = (prevNode); \ 18 (prevNode)->pNext->pPrev = (insertNode); \ 19 (prevNode)->pNext = (insertNode); \ 20 }while(0) 21 //刪除鏈表結點 22 #define OMCI_REMOVE_NODE(removeNode) do{ \ 23 (removeNode)->pPrev->pNext = (removeNode)->pNext; \ 24 (removeNode)->pNext->pPrev = (removeNode)->pPrev; \ 25 }while(0) 26 27 //獲取鏈表結點及其數據(不做安全性檢查) 28 #define GET_NODE_NUM(pList) ((pList)->dwNodeNum) 29 #define GET_HEAD_NODE(pList) ((pList)->pHead) 30 #define GET_TAIL_NODE(pList) ((pList)->pTail) 31 #define GET_PREV_NODE(pNode) ((pNode)->pPrev) 32 #define GET_NEXT_NODE(pNode) ((pNode)->pNext) 33 #define GET_NODE_DATA(pNode) ((pNode)->pvNodeData) 34 35 //雙向循環鏈表遍歷校驗宏 36 #define LIST_ITER_CHECK(pList, retVal) do{\ 37 CHECK_SINGLE_POINTER((pList), retVal); \ 38 CHECK_SINGLE_POINTER((pList)->pHead, retVal); \ 39 CHECK_SINGLE_POINTER((pList)->pHead->pNext, retVal); \ 40 }while(0) 41 //雙向循環鏈表遍歷宏 42 //pList: 鏈表指針;pLoopNode: 鏈表結點,用作循環計數器; 43 //pTmpNode: 鏈表結點,用作刪除pLoopNode時臨時保存pLoopNode->pNext 44 //某些情況下采用遍歷宏代替OmciLocateListNode或OmciTraverseListNode函數可提高執行效率。 45 //如外部數據和結點數據需按共同的規則轉換時,采用遍歷宏可使外部數據不必重復轉換。 46 #define LIST_ITER_LOOP(pList, pLoopNode) \ 47 for(pLoopNode = (pList)->pHead->pNext; \ 48 pLoopNode != (pList)->pHead; \ 49 pLoopNode = pLoopNode->pNext) 50 #define LIST_ITER_LOOP_SAFE(pList, pLoopNode, pTmpNode) \ 51 for(pLoopNode = (pList)->pHead->pNext, pTmpNode = pLoopNode->pNext; \ 52 pLoopNode != (pList)->pHead; \ 53 pLoopNode = pTmpNode, pTmpNode = pLoopNode->pNext)
結點的插入和刪除操作可參考1.1節雙向鏈表的圖例。GET_HEAD_NODE等宏可高效(但不安全)地獲取鏈表結點及其數據,后續將給出其函數版本。LIST_ITER_LOOP宏旨在給使用者提供一定程度的自由度,某些情況下可提高執行效率。
2.3 函數接口
首先定義一組私有函數,主要是創建、刪除和銷毀鏈表結點。這些內部使用的函數已盡可能保證參數安全性,故省去參數校驗處理。
為簡便起見,下文中“XX指針”均表示指向XX的指針。
創建新的鏈表結點:
1 /********************************************************************** 2 * 函數名稱: CreateListNode 3 * 功能描述: 創建新的鏈表結點 4 * 輸入參數: T_OMCI_LIST *pList :鏈表指針 5 * VOID *pvNodeData :待插入的鏈表結點數據指針 6 * 輸出參數: NA 7 * 返 回 值: T_OMCI_LIST_NODE* 8 ***********************************************************************/ 9 static T_OMCI_LIST_NODE *CreateListNode(T_OMCI_LIST *pList, VOID *pvNodeData) 10 { 11 T_OMCI_LIST_NODE *pInsertNode = (T_OMCI_LIST_NODE*)calloc((sizeof(T_OMCI_LIST_NODE)+pList->dwNodeDataSize), 1); 12 if(NULL == pInsertNode) 13 { 14 printf("[%s]pList(%p) failed to alloc for pInsertNode!\n", FUNC_NAME, pList); 15 return NULL; 16 } 17 18 pInsertNode->pvNodeData = (INT8U *)pInsertNode + sizeof(T_OMCI_LIST_NODE); 19 if(NULL != pvNodeData) 20 { //創建非頭結點時 21 memmove(pInsertNode->pvNodeData, pvNodeData, pList->dwNodeDataSize); 22 } 23 24 return pInsertNode; 25 }
刪除指定的鏈表結點:
1 /********************************************************************** 2 * 函數名稱: RemoveListNode 3 * 功能描述: 刪除指定的鏈表結點(釋放結點內存並置其前驅后繼指針為NULL) 4 * 輸入參數: T_OMCI_LIST *pList :鏈表指針 5 * VOID *pvNode :待刪除的鏈表結點指針 6 * 輸出參數: NA 7 * 返 回 值: LIST_STATUS 8 * 注意事項: 本函數未置待刪除結點指針為NULL,請避免訪問已刪除結點 9 ***********************************************************************/ 10 static LIST_STATUS RemoveListNode(T_OMCI_LIST *pList, T_OMCI_LIST_NODE *pNode) 11 { 12 OMCI_ISOL_NODE(pNode); 13 free(pNode); //釋放鏈表結點 14 15 return OMCI_LIST_OK; 16 }
OMCI_ISOL_NODE 宏用於"孤立"待刪除的鏈表結點,避免通過該結點訪問其前驅和后繼結點(進而遍歷鏈表)。因為RemoveListNode函數無法將結點指針置空(C語言值傳遞特性),故調用者需注意避免再次使用已刪除的結點。若要達到結點指針置空的目的,可調用銷毀結點的接口函數:
1 /********************************************************************** 2 * 函數名稱: DestroyListNode 3 * 功能描述: 銷毀指定的鏈表結點(釋放結點內存並置結點指針為NULL) 4 * 輸入參數: T_OMCI_LIST *pList :鏈表指針 5 * VOID **pNode :待銷毀的鏈表結點指針的指針 6 * 輸出參數: NA 7 * 返 回 值: LIST_STATUS 8 * 注意事項: 當指向待銷毀結點的指針存在多份拷貝且散布程序各處時(尤其當 9 * 調用鏈未能保證**pNode指向原始結點時),無法徹底銷毀該結點 10 ***********************************************************************/ 11 static LIST_STATUS DestroyListNode(T_OMCI_LIST *pList, T_OMCI_LIST_NODE **pNode) 12 { 13 free(*pNode); //釋放鏈表結點 14 *pNode = NULL; 15 16 return OMCI_LIST_OK; 17 }
DestroyListNode函數會釋放指定結點的內存並將結點指針置空。但當代碼中存在該結點指針的其他副本時,該函數顯然無法將這些指針副本置空。
至於RemoveListNode和DestroyListNode函數孰優孰劣,可參考附注中對“迷途指針”的討論。
有時可能需要獲知鏈表的確切占用情況(通常沒有必要),如不含任何結點、僅含頭結點或者還包含其他有用結點。GetListOccupation函數可滿足這一“吹毛求疵”的需求,其他情況應使用下文將要給出的判空函數OmciIsListEmpty。OmciIsListEmpty將不含任何結點和僅含頭結點均視為空鏈表,以隱藏內部細節。
1 /********************************************************************** 2 * 函數名稱: GetListOccupation 3 * 功能描述: 獲取鏈表占用情況 4 * 輸入參數: T_OMCI_LIST *pList :鏈表指針 5 * 輸出參數: NA 6 * 返 回 值: LIST_OCCUPATION 7 * 注意事項: 本函數僅用於內部測試。 8 ***********************************************************************/ 9 static LIST_OCCUPATION GetListOccupation(T_OMCI_LIST *pList) 10 { 11 CHECK_SINGLE_POINTER(pList, OMCI_LIST_NULL); 12 CHECK_SINGLE_POINTER(pList->pHead, OMCI_LIST_NULL); 13 14 return (0 == pList->dwNodeNum) ? OMCI_LIST_EMPTY : OMCI_LIST_OCCUPIED; 15 }
基於上述私有函數,可進一步構建鏈表及其結點的基本操作接口。
2.3.1 鏈表操作
使用鏈表前,必須對其初始化。初始化時將創建頭結點,並確定后續將要鏈接的結點數據寬度。
1 /********************************************************************** 2 * 函數名稱: OmciInitList 3 * 功能描述: 鏈表初始化,產生空的雙向循環鏈表 4 * 輸入參數: T_OMCI_LIST *pList :鏈表指針 5 * INT32U dwNodeDataSize :鏈表結點保存的數據字節數 6 * 輸出參數: NA 7 * 返 回 值: LIST_STATUS 8 ***********************************************************************/ 9 LIST_STATUS OmciInitList(T_OMCI_LIST *pList, INT32U dwNodeDataSize) 10 { 11 CHECK_SINGLE_POINTER(pList, OMCI_LIST_ERROR); 12 13 if(0 == dwNodeDataSize) 14 { 15 printf("[%s]pList=%p, dwNodeDataSize=%uBytes, undesired initialization!\n", 16 FUNC_NAME, pList, dwNodeDataSize); 17 return OMCI_LIST_ERROR; 18 } 19 pList->dwNodeDataSize = dwNodeDataSize; //給予重新修改結點數據大小的機會 20 21 if(NULL != pList->pHead) 22 { 23 printf("[%s]pList(%p) has been initialized!\n", FUNC_NAME, pList); 24 return OMCI_LIST_OK; 25 } 26 27 T_OMCI_LIST_NODE *pHeadNode = CreateListNode(pList, NULL); 28 if(NULL == pHeadNode) 29 { 30 printf("[%s]pList(%p) failed to create pHeadNode!\n", FUNC_NAME, pList); 31 return OMCI_LIST_ERROR; 32 } 33 34 OMCI_INIT_NODE(pHeadNode); 35 pList->pHead = pList->pTail = pHeadNode; 36 pList->dwNodeNum = 0; 37 38 return OMCI_LIST_OK; 39 }
通常不會中途修改dwNodeDataSize。僅當使用者確知數據寬度的變化邊界(如確知前N個結點數據為四字節,其后為八字節)時,中途修改dwNodeDataSize才有意義。當然,也可新增一個OmciResizeList接口。
調用OmciInitList接口后,將創建一張僅含頭結點的空雙向循環鏈表。此后可向鏈表中插入結點。
暫時不需要當前鏈表時,可清空鏈表除頭結點外的結點。這樣再次使用時無需初始化鏈表,直接插入結點即可。若確定不再需要當前鏈表時,可銷毀鏈表的所有結點。OmciClearList和OmciDestroyList函數分別完成鏈表的清空和銷毀。
1 /********************************************************************** 2 * 函數名稱: OmciClearList 3 * 功能描述: 清空雙向循環鏈表除頭結點外的結點 4 * 輸入參數: T_OMCI_LIST *pList :鏈表指針 5 * 輸出參數: NA 6 * 返 回 值: LIST_STATUS 7 * 注意事項: 清空鏈表結點后,再次插入結點時不需要初始化鏈表。 8 ***********************************************************************/ 9 LIST_STATUS OmciClearList(T_OMCI_LIST *pList) 10 { 11 LIST_ITER_CHECK(pList, OMCI_LIST_ERROR); 12 13 T_OMCI_LIST_NODE *pNextNode, *pListNode = pList->pHead->pNext; 14 while(pListNode != pList->pHead) 15 { 16 pNextNode = pListNode->pNext; 17 RemoveListNode(pList, pListNode); 18 pListNode = pNextNode; 19 } 20 21 OMCI_INIT_NODE(pList->pHead); 22 pList->pTail = pList->pHead; 23 pList->dwNodeNum = 0; 24 25 return OMCI_LIST_OK; 26 } 27 /********************************************************************** 28 * 函數名稱: OmciDestroyList 29 * 功能描述: 銷毀雙向循環鏈表,包括頭結點 30 * 輸入參數: T_OMCI_LIST *pList :鏈表指針 31 * 輸出參數: NA 32 * 返 回 值: LIST_STATUS 33 * 注意事項: 銷毀鏈表后,再次插入結點時需要初始化鏈表。 34 ***********************************************************************/ 35 LIST_STATUS OmciDestroyList(T_OMCI_LIST *pList) 36 { 37 LIST_ITER_CHECK(pList, OMCI_LIST_ERROR); 38 39 T_OMCI_LIST_NODE *pNextNode, *pListNode = pList->pHead->pNext; 40 while(pListNode != pList->pHead) 41 { 42 pNextNode = pListNode->pNext; 43 DestroyListNode(pList, &pListNode); 44 pListNode = pNextNode; 45 } 46 47 DestroyListNode(pList, &(pList->pHead)); //銷毀頭結點 48 pList->pTail = NULL; //尾結點指針置空 49 pList->dwNodeNum = 0; 50 pList->dwNodeDataSize = 0; 51 52 return OMCI_LIST_OK; 53 }
清空或銷毀鏈表后,OmciIsListEmpty函數的返回值將為邏輯真,表明當前鏈表為空。
1 /********************************************************************** 2 * 函數名稱: OmciIsListEmpty 3 * 功能描述: 判斷鏈表是否為空(僅含頭結點或不含任何結點) 4 * 輸入參數: T_OMCI_LIST *pList :鏈表指針 5 * 輸出參數: NA 6 * 返 回 值: BOOL 7 ***********************************************************************/ 8 BOOL OmciIsListEmpty(T_OMCI_LIST *pList) 9 { 10 CHECK_SINGLE_POINTER(pList, OMCI_LIST_TRUE); 11 CHECK_SINGLE_POINTER(pList->pHead, OMCI_LIST_TRUE); 12 13 T_OMCI_LIST_NODE *pHeadNode = pList->pHead; 14 if((0 == pList->dwNodeNum) && 15 (pHeadNode->pPrev == pHeadNode) && //冗余校驗以加強安全性 16 (pHeadNode->pNext == pHeadNode)) 17 { 18 return OMCI_LIST_TRUE; 19 } 20 else 21 { 22 return OMCI_LIST_FALSE; 23 } 24 }
此處為加強安全性對頭結點進行檢驗,但並非必要。若剔除冗余校驗,則OmciIsListEmpty函數的實現會更為簡潔高效。
2.3.2 結點操作
鏈表初始化后,可在鏈表頭結點后逆序或順序依次插入新的結點。當從頭結點向后繼方向遍歷時,逆序插入的行為類似於棧,而順序插入的行為類似於隊列。
1 /********************************************************************** 2 * 函數名稱: OmciPrependListNode 3 * 功能描述: 在鏈表頭結點后逆序增加結點,尾結點恆為頭結點 4 * 在頭結點指針pHead所指向結點和pHead->pNext所指向結點 5 * 之間插入新結點,先插入的結點向右移動。遍歷鏈表時 6 * 從pHead開始向右依次訪問至最先插入的結點,類似於棧。 7 * 輸入參數: T_OMCI_LIST *pList :鏈表指針 8 * VOID *pvNodeData :待插入的鏈表結點數據指針 9 * 輸出參數: NA 10 * 返 回 值: LIST_STATUS 11 ***********************************************************************/ 12 LIST_STATUS OmciPrependListNode(T_OMCI_LIST *pList, VOID *pvNodeData) 13 { 14 CHECK_DOUBLE_POINTER(pList, pvNodeData, OMCI_LIST_ERROR); 15 16 if(0 == pList->dwNodeDataSize) 17 { 18 printf("[%s]pList=%p, dwNodeDataSize=0Bytes, probably uninitialized or initialized improperly. See 'OmciInitList'!\n", 19 FUNC_NAME, pList); 20 return OMCI_LIST_ERROR; 21 } 22 T_OMCI_LIST_NODE *pInsertNode = CreateListNode(pList, pvNodeData); 23 if(NULL == pInsertNode) 24 { 25 printf("[%s]pList(%p) failed to create pInsertNode!\n", FUNC_NAME, pList); 26 return OMCI_LIST_ERROR; 27 } 28 29 OMCI_INSERT_NODE(pList->pHead, pInsertNode); //在鏈表頭結點后增加一個結點 30 31 pList->dwNodeNum++; 32 33 return OMCI_LIST_OK; 34 } 35 36 /********************************************************************** 37 * 函數名稱: OmciAppendListNode 38 * 功能描述: 在鏈表頭結點后順序增加結點,新結點作為尾結點 39 * 在頭結點指針pHead所指向結點前(即尾結點后)插入新結點, 40 * 先插入的結點向左移動。遍歷鏈表時從pHead開始向右依次 41 * 訪問至最后插入的結點,類似於隊列。 42 * 雙向循環鏈表已保證pList->pTail(即pHead->pPrev)非空。 43 * 輸入參數: T_OMCI_LIST *pList :鏈表指針 44 * VOID *pvNodeData :待插入的鏈表結點數據指針 45 * 輸出參數: NA 46 * 返 回 值: LIST_STATUS 47 ***********************************************************************/ 48 LIST_STATUS OmciAppendListNode(T_OMCI_LIST *pList, VOID *pvNodeData) 49 { 50 CHECK_DOUBLE_POINTER(pList, pvNodeData, OMCI_LIST_ERROR); 51 52 if(0 == pList->dwNodeDataSize) 53 { 54 printf("[%s]pList=%p, dwNodeDataSize=0Bytes, probably uninitialized or initialized improperly. See 'OmciInitList'!\n", 55 FUNC_NAME, pList); 56 return OMCI_LIST_ERROR; 57 } 58 59 T_OMCI_LIST_NODE *pInsertNode = CreateListNode(pList, pvNodeData); 60 if(NULL == pInsertNode) 61 { 62 printf("[%s]pList(%p) failed to create pInsertNode!\n", FUNC_NAME, pList); 63 return OMCI_LIST_ERROR; 64 } 65 66 OMCI_INSERT_NODE(pList->pTail, pInsertNode); //在鏈表尾結點后增加一個結點 67 pList->pTail = pInsertNode; //新的尾結點指向當前添加的結點 68 69 pList->dwNodeNum++; 70 71 return OMCI_LIST_OK; 72 }
對dwNodeDataSize 的校驗用於指示鏈表未初始化或未正確初始化的錯誤。將該校驗置於私有函數CreateListNode中可簡化Prepend和Append代碼。但FUNC_NAME信息將暴露內部函數,從而給使用者造成疑惑,故該校驗予以保留。
有時需要在鏈表中任意位置插入結點,此時可使用OmciInsertListNode接口。
1 /********************************************************************** 2 * 函數名稱: OmciInsertListNode 3 * 功能描述: 在鏈表中任意位置插入結點 4 * 輸入參數: T_OMCI_LIST *pList :鏈表指針 5 * T_OMCI_LIST_NODE *pPrevNode :待插入結點的前驅結點指針 6 * VOID *pvNodeData :待插入結點的數據域指針 7 * 輸出參數: NA 8 * 返 回 值: LIST_STATUS 9 * 注意事項: 若pPrevNode恆為頭結點或尾結點,請使用OmciPrependListNode 10 * 或OmciAppendListNode函數 11 ***********************************************************************/ 12 LIST_STATUS OmciInsertListNode(T_OMCI_LIST *pList, T_OMCI_LIST_NODE *pPrevNode, VOID *pvNodeData) 13 { 14 CHECK_TRIPLE_POINTER(pList, pPrevNode, pvNodeData, OMCI_LIST_ERROR); 15 16 if(0 == pList->dwNodeDataSize) 17 { 18 printf("[%s]pList=%p, dwNodeDataSize=0Bytes, probably uninitialized or initialized improperly. See 'OmciInitList'!\n", 19 FUNC_NAME, pList); 20 return OMCI_LIST_ERROR; 21 } 22 23 T_OMCI_LIST_NODE *pInsertNode = CreateListNode(pList, pvNodeData); 24 if(NULL == pInsertNode) 25 { 26 printf("[%s]pList(%p) failed to create pInsertNode!\n", FUNC_NAME, pList); 27 return OMCI_LIST_ERROR; 28 } 29 30 OMCI_INSERT_NODE(pPrevNode, pInsertNode); 31 if(pPrevNode == pList->pTail) 32 pList->pTail = pInsertNode; 33 34 pList->dwNodeNum++; 35 36 return OMCI_LIST_OK; 37 }
當pPrevNode恆為頭結點時,OmciInsertListNode接口等效於OmciPrependListNode;當pPrevNode恆為尾結點時,OmciInsertListNode接口等效於OmciAppendListNode。這兩種情況建議使用Prepend或Append接口(畢竟減少一個參數)。
插入若干結點后,就可刪除或銷毀鏈表中除頭結點外的任一結點。
1 /********************************************************************** 2 * 函數名稱: OmciRemoveListNode 3 * 功能描述: 刪除雙向循環鏈表中除頭結點外的某一結點 4 * 輸入參數: T_OMCI_LIST *pList :鏈表指針 5 * T_OMCI_LIST_NODE *pNode :待刪除的鏈表結點指針 6 * 輸出參數: NA 7 * 返 回 值: LIST_STATUS 8 ***********************************************************************/ 9 LIST_STATUS OmciRemoveListNode(T_OMCI_LIST *pList, T_OMCI_LIST_NODE *pNode) 10 { 11 CHECK_DOUBLE_POINTER(pList, pNode, OMCI_LIST_ERROR); 12 CHECK_DOUBLE_POINTER(pNode->pPrev, pNode->pNext, OMCI_LIST_ERROR); 13 14 if(0 == pList->dwNodeNum) 15 { 16 printf("[%s]pList(%p) has no node to be Removed!\n", FUNC_NAME, pList); 17 return OMCI_LIST_ERROR; 18 } 19 20 OMCI_REMOVE_NODE(pNode); 21 if(pNode->pNext == pList->pHead) 22 { 23 pList->pTail = pNode->pPrev; //刪除尾結點 24 } 25 26 RemoveListNode(pList, pNode); 27 pList->dwNodeNum--; 28 29 return OMCI_LIST_OK; 30 } 31 /********************************************************************** 32 * 函數名稱: OmciDestroyListNode 33 * 功能描述: 銷毀雙向循環鏈表中除頭結點外的某一結點 34 * 輸入參數: T_OMCI_LIST *pList :鏈表指針 35 * T_OMCI_LIST_NODE **pNode :待銷毀的鏈表結點二級指針 36 * 輸出參數: NA 37 * 返 回 值: LIST_STATUS 38 ***********************************************************************/ 39 LIST_STATUS OmciDestroyListNode(T_OMCI_LIST *pList, T_OMCI_LIST_NODE **pNode) 40 { 41 CHECK_DOUBLE_POINTER(pList, pNode, OMCI_LIST_ERROR); 42 CHECK_SINGLE_POINTER(*pNode, OMCI_LIST_ERROR); 43 44 if(0 == pList->dwNodeNum) 45 { 46 printf("[%s]pList(%p) has no node to be Removed!\n", FUNC_NAME, pList); 47 return OMCI_LIST_ERROR; 48 } 49 50 OMCI_REMOVE_NODE(*pNode); 51 if((*pNode)->pNext == pList->pHead) 52 { 53 pList->pTail = (*pNode)->pPrev; //刪除尾結點 54 } 55 56 DestroyListNode(pList, pNode); 57 pList->dwNodeNum--; 58 59 return OMCI_LIST_OK; 60 }
然而,要刪除或銷毀鏈表結點,必須先定位到該結點。
在鏈表中“定位”某個結點有兩種手段:一是通過結點編號查找,如OmciGetListNodeByIndex;二是通過某種給定條件匹配,如OmciLocateListNode(查找首個滿足給定條件的結點)。
1 /********************************************************************** 2 * 函數名稱: OmciGetListNodeByIndex 3 * 功能描述: 獲取鏈表中指定序號的結點(按頭結點后繼方向排序) 4 * 輸入參數: T_OMCI_LIST* pList :鏈表指針 5 * INT32U dwNodeIndex :結點序號(從1開始) 6 * 輸出參數: NA 7 * 返 回 值: T_OMCI_LIST_NODE* 鏈表結點指針(空表返回NULL) 8 ***********************************************************************/ 9 T_OMCI_LIST_NODE* OmciGetListNodeByIndex(T_OMCI_LIST *pList, INT32U dwNodeIndex) 10 { 11 CHECK_SINGLE_POINTER(pList, NULL); 12 13 if(0 == dwNodeIndex) 14 return pList->pHead; //也可返回NULL 15 if(dwNodeIndex >= pList->dwNodeNum) 16 return pList->pTail; 17 18 INT32U dwNodeIdx = 1; 19 T_OMCI_LIST_NODE *pListNode = pList->pHead; 20 for(; dwNodeIdx <= dwNodeIndex; dwNodeIdx++) 21 pListNode = pListNode->pNext; 22 23 return pListNode; 24 } 25 /********************************************************************** 26 * 函數名稱: OmciLocateListNode 27 * 功能描述: 查找鏈表中首個與pData滿足函數fpCompareNode判定關系的結點 28 * 輸入參數: T_OMCI_LIST* pList :鏈表指針 29 * VOID* pvData :待比較數據指針 30 * CompareNodeFunc fpCompareNode :比較回調函數指針 31 * 輸出參數: NA 32 * 返 回 值: T_OMCI_LIST_NODE* 鏈表結點指針(未找到時返回NULL) 33 ***********************************************************************/ 34 /* 比較回調函數原型,用來自定義鏈表節點比較 */ 35 typedef INT8U (*CompareNodeFunc)(VOID *pvNodeData, VOID *pvData, INT32U dwNodeDataSize); 36 T_OMCI_LIST_NODE* OmciLocateListNode(T_OMCI_LIST *pList, VOID *pvData, CompareNodeFunc fpCompareNode) 37 { 38 CHECK_TRIPLE_POINTER(pList, pvData, fpCompareNode, NULL); 39 CHECK_SINGLE_POINTER(pList->pHead, NULL); 40 CHECK_SINGLE_POINTER(pList->pHead->pNext, NULL); 41 42 T_OMCI_LIST_NODE *pListNode = pList->pHead->pNext; 43 while(pListNode != pList->pHead) 44 { 45 if(0 == fpCompareNode(pListNode->pvNodeData, pvData, pList->dwNodeDataSize)) 46 return pListNode; 47 48 pListNode = pListNode->pNext; 49 } 50 51 return NULL; 52 }
可見,OmciLocateListNode接口本質上就是“遍歷+匹配”。要進行單純而強大的遍歷操作,可使用OmciTraverseListNode接口。
1 /********************************************************************** 2 * 函數名稱: OmciTraverseListNode 3 * 功能描述: 鏈表結點遍歷函數,遍歷操作由fpTravNode指定 4 * 輸入參數: T_OMCI_LIST* pList :鏈表指針 5 * VOID* pvTravInfo :遍歷操作回調函數所需信息 6 * 也可為空,取決於回調函數具體實現 7 * TravNodeFunc fpTravNode :遍歷操作回調函數指針 8 * 輸出參數: NA 9 * 返 回 值: LIST_STATUS 10 * 注意事項: 本函數可間接實現Print等操作,但不建議代替后者。 11 * fpTravNode返回非0(OMCI_LIST_OK)值時中止遍歷 12 ***********************************************************************/ 13 typedef LIST_STATUS (*TravNodeFunc)(VOID *pvNode, VOID *pvTravInfo, INT32U dwNodeDataSize); 14 LIST_STATUS OmciTraverseListNode(T_OMCI_LIST *pList, VOID *pvTravInfo, TravNodeFunc fpTravNode) 15 { 16 CHECK_DOUBLE_POINTER(pList, fpTravNode, OMCI_LIST_ERROR); 17 CHECK_SINGLE_POINTER(pList->pHead, OMCI_LIST_ERROR); 18 CHECK_SINGLE_POINTER(pList->pHead->pNext, OMCI_LIST_ERROR); 19 20 T_OMCI_LIST_NODE *pListNode = pList->pHead->pNext; 21 while(pListNode != pList->pHead) 22 { 23 T_OMCI_LIST_NODE *pTmpNode = pListNode->pNext; //fpTravNode內可能會銷毀結點pListNode 24 if(OMCI_LIST_OK != fpTravNode(pListNode, pvTravInfo, pList->dwNodeDataSize)) 25 break; 26 27 pListNode = pTmpNode; 28 } 29 30 return OMCI_LIST_OK; 31 }
因為OmciAppendListNode和OmciPrependListNode已暗含“正序”和“逆序”的意思,故僅提供OmciTraverseListNode函數,而無需再增加逆序遍歷的接口(除非需要同時雙序遍歷)。
常常需要打印輸出鏈表結點的數據域內容,而OmciTraverseListNode接口稍顯笨重。此時可使用專門的打印接口OmciPrintListNode。
1 /********************************************************************** 2 * 函數名稱: OmciPrintListNode 3 * 功能描述: 打印輸出鏈表結點的數據域內容 4 * 輸入參數: T_OMCI_LIST* pList :鏈表指針 5 * PrintListFunc fpPrintList :打印回調函數指針 6 * 輸出參數: NA 7 * 返 回 值: LIST_STATUS 8 ***********************************************************************/ 9 /* 打印回調函數原型,用來自定義鏈表內容打印 */ 10 typedef VOID (*PrintListFunc)(VOID *pNodeData, INT32U dwNodeNum); 11 LIST_STATUS OmciPrintListNode(T_OMCI_LIST *pList, PrintListFunc fpPrintList) 12 { 13 CHECK_DOUBLE_POINTER(pList, fpPrintList, OMCI_LIST_ERROR); 14 CHECK_SINGLE_POINTER(pList->pHead, OMCI_LIST_ERROR); 15 CHECK_SINGLE_POINTER(pList->pHead->pNext, OMCI_LIST_ERROR); 16 17 T_OMCI_LIST_NODE *pListNode = pList->pHead->pNext; 18 while(pListNode != pList->pHead) 19 { 20 //具體打印格式交給回調函數靈活處理(可直接打印也可拷貝至本地處理后打印) 21 fpPrintList(pListNode->pvNodeData, pList->dwNodeNum); 22 pListNode = pListNode->pNext; 23 } 24 printf("\n"); 25 26 return OMCI_LIST_OK; 27 }
對於CompareNodeFunc 和PrintListFunc,以下給出兩個范例:
1 /********************************************************************** 2 * 函數名稱: CompareNodeGeneric 3 * 功能描述: 通用鏈表結點內存比較 4 * 輸入參數: VOID *pvNodeData :鏈表結點數據指針 5 * VOID *pvData :待比較外部數據指針 6 * INT32U dwNodeDataSize :鏈表結點數據大小 7 * 輸出參數: NA 8 * 返 回 值: 0:Equal; !0:Unequal 9 * 注意事項: 比較長度為結點數據字節數,即默認與外部數據大小一致 10 ***********************************************************************/ 11 INT8U CompareNodeGeneric(VOID *pvNodeData, VOID *pvData, INT32U dwNodeDataSize) 12 { 13 CHECK_DOUBLE_POINTER(pvNodeData, pvData, 1); 14 return memcmp(pvNodeData, pvData, dwNodeDataSize); 15 } 16 /********************************************************************** 17 * 函數名稱: PrintListWord 18 * 功能描述: 打印鏈表結點,結點數據域為兩字節整數 19 * 輸入參數: VOID *pvNodeData :鏈表節點數據指針 20 * INT32U dwNodeNum :鏈表節點數目 21 * 輸出參數: NA 22 * 返 回 值: VOID 23 * 注意事項: 僅作示例,未考慮字節序等問題。 24 ***********************************************************************/ 25 VOID PrintListWord(VOID *pvNodeData, INT32U dwNodeNum) 26 { 27 CHECK_SINGLE_POINTER(pvNodeData, RETURN_VOID); 28 printf("%d ", *((INT16U *)pvNodeData)); 29 }
最后,給出獲取鏈表結點及其數據的安全接口:
1 /********************************************************************** 2 * 函數名稱: OmciGetListNodeNum 3 * 功能描述: 獲取鏈表結點數目 4 * 輸入參數: T_OMCI_LIST *pList :鏈表指針 5 * 輸出參數: NA 6 * 返 回 值: INT32U 鏈表結點數目 7 ***********************************************************************/ 8 INT32U OmciGetListNodeNum(T_OMCI_LIST *pList) 9 { 10 CHECK_SINGLE_POINTER(pList, 0); 11 return (pList->dwNodeNum); 12 } 13 14 /********************************************************************** 15 * 函數名稱: OmciGetListHead/OmciGetListTail 16 * 功能描述: 獲取鏈表頭結點/尾結點指針 17 * 輸入參數: T_OMCI_LIST *pList :鏈表指針 18 ***********************************************************************/ 19 T_OMCI_LIST_NODE* OmciGetListHead(T_OMCI_LIST *pList) 20 { 21 CHECK_SINGLE_POINTER(pList, NULL); 22 return (pList->pHead); 23 } 24 T_OMCI_LIST_NODE* OmciGetListTail(T_OMCI_LIST *pList) 25 { 26 CHECK_SINGLE_POINTER(pList, NULL); 27 return (pList->pTail); 28 } 29 30 /********************************************************************** 31 * 函數名稱: OmciGetPrevNode/OmciGetNextNode 32 * 功能描述: 獲取鏈表指定結點的前驅結點/后繼結點指針 33 * 輸入參數: T_OMCI_LIST_NODE *pNode :指定結點的指針 34 ***********************************************************************/ 35 T_OMCI_LIST_NODE* OmciGetPrevNode(T_OMCI_LIST_NODE *pNode) 36 { 37 CHECK_SINGLE_POINTER(pNode, NULL); 38 return (pNode->pPrev); 39 } 40 T_OMCI_LIST_NODE* OmciGetNextNode(T_OMCI_LIST_NODE *pNode) 41 { 42 CHECK_SINGLE_POINTER(pNode, NULL); 43 return (pNode->pNext); 44 } 45 46 /********************************************************************** 47 * 函數名稱: OmciGetNodeData 48 * 功能描述: 獲取鏈表指定結點的數據域 49 * 輸入參數: T_OMCI_LIST_NODE *pNode :指定結點的指針 50 ***********************************************************************/ 51 VOID* OmciGetNodeData(T_OMCI_LIST_NODE *pNode) 52 { 53 CHECK_DOUBLE_POINTER(pNode, pNode->pvNodeData, NULL); 54 return (pNode->pvNodeData); 55 }
三 測試
本節將對上文實現的鏈表操作接口進行測試,測試函數兼作使用示例。
1 #ifdef TEST_AND_EXAMPLE 2 3 static LIST_STATUS TravPrintWord(VOID *pvNode, VOID *pvTravInfo, INT32U dwNodeDataSize) 4 { 5 CHECK_SINGLE_POINTER(pvNode, OMCI_LIST_ERROR); 6 T_OMCI_LIST_NODE *pNode = (T_OMCI_LIST_NODE *)pvNode; 7 printf("%d ", *((INT16U *)GET_NODE_DATA(pNode))); 8 return OMCI_LIST_OK; 9 } 10 11 T_OMCI_LIST gExampleList = {0}; 12 VOID ListTestExample(VOID) 13 { //本函數並非嚴格意義上的測試函數,主要用作示例,且示例並非最佳用法。 14 INT8U ucTestIndex = 1; 15 INT16U aTestListData[] = {11, 22, 33, 44, 55, 66}; 16 17 printf("\n<Test Case %u>: Initialization!\n", ucTestIndex++); 18 OmciInitList(&gExampleList, sizeof(INT16U)); 19 printf("gExampleList=%p, pHead=%p, pTail=%p\n", &gExampleList, 20 OmciGetListHead(&gExampleList), OmciGetListTail(&gExampleList)); 21 22 printf("\n<Test Case %u>: Append Node to List!\n", ucTestIndex++); 23 OmciAppendListNode(&gExampleList, &aTestListData[0]); 24 OmciAppendListNode(&gExampleList, &aTestListData[1]); 25 OmciAppendListNode(&gExampleList, &aTestListData[2]); 26 printf("OmciIsListEmpty=%u(0-Occupied; 1-Empty)\n", OmciIsListEmpty(&gExampleList)); 27 printf("gExampleList NodeNum=%u\n", OmciGetListNodeNum(&gExampleList)); 28 OmciPrintListNode(&gExampleList, PrintListWord); 29 30 printf("\n<Test Case %u>: Insert Node to List!\n", ucTestIndex++); 31 T_OMCI_LIST_NODE *pPrevNode = OmciGetListNodeByIndex(&gExampleList, 2); 32 printf("NodeData2=%d\n", *((INT16U *)OmciGetNodeData(pPrevNode))); 33 OmciInsertListNode(&gExampleList, pPrevNode, &aTestListData[4]); 34 printf("gExampleList NodeNum=%u\n", OmciGetListNodeNum(&gExampleList)); 35 OmciPrintListNode(&gExampleList, PrintListWord); 36 37 printf("\n<Test Case %u>: Remove Node from List!\n", ucTestIndex++); 38 T_OMCI_LIST_NODE *pDeleteNode = OmciLocateListNode(&gExampleList, &aTestListData[1], CompareNodeGeneric); 39 OmciRemoveListNode(&gExampleList, pDeleteNode); 40 printf("gExampleList NodeNum=%u\n", OmciGetListNodeNum(&gExampleList)); 41 OmciPrintListNode(&gExampleList, PrintListWord); 42 43 printf("\n<Test Case %u>: Clear List!\n", ucTestIndex++); 44 OmciClearList(&gExampleList); 45 printf("gExampleList=%p, pHead=%p, pTail=%p\n", &gExampleList, 46 GET_HEAD_NODE(&gExampleList), GET_TAIL_NODE(&gExampleList)); 47 printf("OmciIsListEmpty=%u(0-Occupied; 1-Empty)\n", OmciIsListEmpty(&gExampleList)); 48 printf("gExampleList NodeNum=%u\n", OmciGetListNodeNum(&gExampleList)); 49 50 printf("\n<Test Case %u>: Prepend Node to List!\n", ucTestIndex++); 51 OmciPrependListNode(&gExampleList, &aTestListData[3]); 52 OmciPrependListNode(&gExampleList, &aTestListData[4]); 53 OmciPrependListNode(&gExampleList, &aTestListData[5]); 54 printf("OmciIsListEmpty=%u(0-Occupied; 1-Empty)\n", OmciIsListEmpty(&gExampleList)); 55 printf("gExampleList NodeNum=%u\n", OmciGetListNodeNum(&gExampleList)); 56 OmciPrintListNode(&gExampleList, PrintListWord); 57 58 T_OMCI_LIST_NODE *pListNode = NULL; 59 LIST_ITER_LOOP(&gExampleList, pListNode) 60 { 61 printf("%d ", *((INT16U *)GET_NODE_DATA(pListNode))); 62 } 63 printf("\n"); 64 65 OmciTraverseListNode(&gExampleList, NULL, TravPrintWord); 66 printf("\n"); 67 68 printf("\n<Test Case %u>: Destory List!\n", ucTestIndex++); 69 OmciDestroyList(&gExampleList); 70 printf("gExampleList=%p, pHead=%p, pTail=%p\n", &gExampleList, 71 GET_HEAD_NODE(&gExampleList), GET_TAIL_NODE(&gExampleList)); 72 printf("gExampleList NodeNum=%u\n", OmciGetListNodeNum(&gExampleList)); 73 printf("GetListOccupation=%u(0-Occupied; 1-Empty; 2-Null)\n", GetListOccupation(&gExampleList)); 74 return; 75 } 76 77 #endif
在上述測試代碼中,Prepend或Append結點的代碼若用OmciInsertListNode實現,如下:
1 OmciInsertListNode(&gExampleList, OmciGetListHead(&gExampleList), &aTestListData[3]); 2 OmciInsertListNode(&gExampleList, OmciGetListHead(&gExampleList), &aTestListData[4]); 3 OmciInsertListNode(&gExampleList, OmciGetListHead(&gExampleList), &aTestListData[5]); 4 //Or 5 OmciInsertListNode(&gExampleList, OmciGetListTail(&gExampleList), &aTestListData[1]); 6 OmciInsertListNode(&gExampleList, OmciGetListTail(&gExampleList), &aTestListData[2]); 7 OmciInsertListNode(&gExampleList, OmciGetListTail(&gExampleList), &aTestListData[3]);
測試結果如下所示:
1 <Test Case 1>: Initialization! 2 gExampleList=0x804bc8c, pHead=0x8b39010, pTail=0x8b39010 3 4 <Test Case 2>: Append Node to List! 5 OmciIsListEmpty=0(0-Occupied; 1-Empty) 6 gExampleList NodeNum=3 7 11 22 33 8 9 <Test Case 3>: Insert Node to List! 10 NodeData2=22 11 gExampleList NodeNum=4 12 11 22 55 33 13 14 <Test Case 4>: Remove Node from List! 15 gExampleList NodeNum=3 16 11 55 33 17 18 <Test Case 5>: Clear List! 19 gExampleList=0x804bc8c, pHead=0x8b39010, pTail=0x8b39010 20 OmciIsListEmpty=1(0-Occupied; 1-Empty) 21 gExampleList NodeNum=0 22 23 <Test Case 6>: Prepend Node to List! 24 OmciIsListEmpty=0(0-Occupied; 1-Empty) 25 gExampleList NodeNum=3 26 66 55 44 27 66 55 44 28 66 55 44 29 30 <Test Case 7>: Destory List! 31 gExampleList=0x804bc8c, pHead=(nil), pTail=(nil) 32 gExampleList NodeNum=0 33 [GetListOccupation(140)]Null Pointer: pList->pHead! 34 GetListOccupation=2(0-Occupied; 1-Empty; 2-Null)
四 附注
迷途指針(Dangling pointer,亦稱懸垂指針)和野指針(Wild pointer)
- 迷途指針:所指向的對象被釋放或收回,但該指針仍指向原對象的內存地址(想象被強拆后無家可歸的人...)。
- 野指針:指針在使用之前未進行必要的初始化(未顯式初始化的靜態指針不是野指針)。
可見,迷途指針和野指針均指向不合法的對象,應禁止讀寫其指向的內存。野指針簡單且易於處理,以下主要討論迷途指針。
在C語言中,當指針所指向的動態內存被顯式地釋放(free)后,該指針就成為迷途指針。若通過迷途指針訪問或修改已釋放的動態分配內存,則可能引發難以排查的故障(尤其當原對象內存分配作他用時)。若指針是函數內的自動變量,函數退出時會被自動銷毀;否則,最好在釋放動態內存后將該指針置空(NULL)。雖然將迷途指針重新置空的做法可能隱藏諸如double free之類的邏輯問題,但卻使得對它的讀寫錯誤更容易暴露(尤其是在多線程環境中)。
在C語言中,可通過下述兩種free替代版本來盡可能避免迷途指針錯誤:
1 #define SAFE_FREE((pointer)) do{ \ 2 if(pointer != NULL){ \ 3 free(pointer); \ 4 pointer = NULL; \ 5 }while(0); 6 7 void SafeFree(void **pointer) 8 { 9 if(pointer != NULL) 10 { 11 free(*pointer); 12 *pointer = NULL; 13 } 14 }
然而,當指向動態分配內存的指針存在多個副本且散布程序各處時,該技術不會置空其他指針變量,從而導致釋放后指針行為的不一致。因此,編碼者應保證每個指針都有其明確的用途和生存期。
注意,因為C語言的值傳遞特性,現有的free庫函數內不可能將入參指針置空。若要達到置空的目的,必須傳入二級指針,如SafeFree。但SafeFree必然與其內存分配版本(如SafeAlloc)的入參類型不一致,這會增加使用者出錯的機率。而由上面的討論可知,即使置空當前入參指針,也無法清除其副本。因此,最好由調用者自行決定如何置空。至於作者傾向於free還是SafeFree,可參考《關於Linux系統basename函數缺陷的思考》一文,或者試想下逐級釋放的順序性。
另一種常見的迷途指針產生於試圖返回棧上分配的局部變量的地址。詳見《已釋放的棧內存》一文。