T. W. Burger (twburger@bigfoot.com), 老闆, Thomas Wolfgang Burger Consulting 公司
2000 年 9 月 01 日
您是否做過這樣一個項目,它要求您在記憶體中儲存數目不定的若干不同對象?對於某些情況,二叉樹是最佳選擇,但在通常情況下,更簡單的鏈表是顯而易見的選擇。
一個簡化的問題樣本
鏈表的痛點在於必須複製鏈表處理函數來處理不同的對象,即便邏輯是完全相同的。例如:
兩個結構類似的鏈表
struct Struct_Object_A { int a; int b; Struct_Object_A *next; } OBJECT_A; typedef struct Struct_Object_B { int a; int b; int c; Struct_Object_B *next; } OBJECT_B;
|
上面定義的兩個結構只有很小的一點差別。OBJECT_B 和 OBJECT_A 之間只差一個整型變數。但是,在編譯器看來,它們仍然是非常不同的。必須為儲存在鏈表中的每個對象複製用來添加、刪除和搜尋鏈表的函數。為瞭解決這個問題,可以使用具有全部三個變數的一個聯合或結構,其中整數 c 並不是在所有的情況下都要使用。這可能變得非常複雜,並會形成不良的編程風格。
C 代碼解決方案:虛擬鏈表
此問題更好的解決方案之一是虛擬鏈表。虛擬鏈表是只包含鏈表指標的鏈表。Object Storage Service在鏈表結構背後。這一點是這樣實現的,首先為鏈表節點分配記憶體,接著為對象分配記憶體,然後將這塊記憶體配置給鏈表節點指標,如下所示:
虛擬鏈表結構的一種實現
typedef struct liststruct { liststruct *next; } LIST, *pLIST; pLIST Head = NULL; pLIST AddToList( pLIST Head, void * data, size_t datasize ) { pLIST newlist=NULL; void *p; // 分配節點記憶體和資料記憶體 newlist = (pLIST) malloc( datasize + sizeof( LIST ) ); // 為這塊資料緩衝區指定一個指標 p = (void *)( newlist + 1 ); // 複製資料 memcpy( p, data, datasize ); // 將這個節點指定給鏈表的表頭 if( Head ) { newlist->next = Head; } else newlist->next = NULL; Head = newlist; return Head; }
|
鏈表節點現在建立在資料值副本的基本之上。這個版本能很好地處理標量值,但不能處理帶有用 malloc 或 new 分配的元素的對象。要處理這些對象,LIST 結構需要包含一個一般的解除函數指標,這個指標可用來在將節點從鏈表中刪除並解除它之前釋放記憶體(或者關閉檔案,或者調用關閉方法)。
一個帶有解除函數的鏈表
typedef void (*ListNodeDestructor)( void * ); typedef struct liststruct { ListNodeDestructor DestructFunc; liststruct *next; } LIST, *pLIST; pLIST AddToList( pLIST Head, void * data, size_t datasize, ListNodeDestructor Destructor ) { pLIST newlist=NULL; void *p; // 分配節點記憶體和資料記憶體 newlist = (pLIST) malloc( datasize + sizeof( LIST ) ); // 為這塊資料緩衝區指定一個指標 p = (void *)( newlist + 1 ); // 複製資料 memcpy( p, data, datasize ); newlist->DestructFunc = Destructor;
// 將這個節點指定給鏈表的表頭 if( Head ) { newlist->next = Head; } else newlist->next = NULL; Head = newlist; return Head; } void DeleteList( pLIST Head ) { pLIST Next; while( Head ) { Next = Head->next; Head->DestructFunc( (void *) Head ); free( Head ); Head = Next; } } typedef struct ListDataStruct { LPSTR p; } LIST_DATA, *pLIST_DATA; void ListDataDestructor( void *p ) { // 對節點指標進行類型轉換 pLIST pl = (pLIST)p; // 對資料指標進行類型轉換 pLIST_DATA pLD = (pLIST_DATA) ( pl + 1 ); delete pLD->p; } pLIST Head = NULL; void TestList() { pLIST_DATA d = new LIST_DATA; d->p = new char[24]; strcpy( d->p, "Hello" ); Head = AddToList( Head, (void *) d, sizeof( pLIST_DATA ), ListDataDestructor ); // 該對象已被複製,現在刪除原來的對象 delete d; d = new LIST_DATA; d->p = new char[24]; strcpy( d->p, "World" ); Head = AddToList( Head, (void *) d, sizeof( pLIST_DATA ), ListDataDestructor ); delete d; // 釋放鏈表 DeleteList( Head ); }
|
在每個鏈表節點中包含同一個解除函數的同一個指標似乎是浪費記憶體空間。確實如此,但只有鏈表始終包含相同的對象才屬於這種情況。按這種方式編寫鏈表允許您 將任何對象放在鏈表中的任何位置。大多數鏈表函數要求對象總是相同的類型或類。虛擬鏈表則無此要求。它所需要的只是將對象彼此區分開的一種方法。要實現這 一點,您既可以檢測解除函數指標的值,也可以在鏈表中所用的全部結構前添加一個類型值並對它進行檢測。當然,如果要將鏈表編寫為一個 C++ 類,則對指向解除函數的指標的設定和儲存只能進行一次。
C++ 解決方案:類鏈表
本解決方案將 CList 類定義為從 LIST 結構匯出的一個類,它通過儲存解除函數的單個值來處理單個儲存類型。請注意添加的 GetCurrentData() 函數,該函數完成從鏈表節點指標到資料位移指標的數學轉換。
一個虛擬鏈表對象
// 定義解除函數指標 typedef void (*ListNodeDestructor)( void * ); // 未添加解除函數指標的鏈表 typedef struct ndliststruct { ndliststruct *next; } ND_LIST, *pND_LIST; // 定義處理一種資料類型的鏈表類 class CList : public ND_LIST { public: CList(ListNodeDestructor); ~CList(); pND_LIST AddToList( void * data, size_t datasize ); void *GetCurrentData(); void DeleteList( pND_LIST Head ); private: pND_LIST m_HeadOfList; pND_LIST m_CurrentNode; ListNodeDestructor m_DestructFunc; }; // 用正確的起始值構造這個鏈表對象 CList::CList(ListNodeDestructor Destructor) : m_HeadOfList(NULL), m_CurrentNode(NULL) { m_DestructFunc = Destructor; } // 在解除對象以後刪除鏈表 CList::~CList() { DeleteList(m_HeadOfList); } // 向鏈表中添加一個新節點 pND_LIST CList::AddToList( void * data, size_t datasize ) { pND_LIST newlist=NULL; void *p; // 分配節點記憶體和資料記憶體 newlist = (pND_LIST) malloc( datasize + sizeof( ND_LIST ) ); // 為這塊資料緩衝區指定一個指標 p = (void *)( newlist + 1 ); // 複製資料 memcpy( p, data, datasize ); // 將這個節點指定給鏈表的表頭 if( m_HeadOfList ) { newlist->next = m_HeadOfList; } else newlist->next = NULL; m_HeadOfList = newlist; return m_HeadOfList; } // 將當前的節點資料作為 void 類型返回,以便調用函數能夠將它轉換為任何類型 void * CList::GetCurrentData() { return (void *)(m_CurrentNode+1); } // 刪除已指派的鏈表 void CList::DeleteList( pND_LIST Head ) { pND_LIST Next; while( Head ) { Next = Head->next; m_DestructFunc( (void *) Head ); free( Head ); Head = Next; } } // 建立一個要在鏈表中建立和儲存的結構 typedef struct ListDataStruct { LPSTR p; } LIST_DATA, *pND_LIST_DATA; // 定義標準解除函數 void ClassListDataDestructor( void *p ) { // 對節點指標進行類型轉換 pND_LIST pl = (pND_LIST)p; // 對資料指標進行類型轉換 pND_LIST_DATA pLD = (pND_LIST_DATA) ( pl + 1 ); delete pLD->p; } // 測試上面的代碼 void MyCListClassTest() { // 建立鏈表類 CList* pA_List_of_Data = new CList(ClassListDataDestructor); // 建立資料對象
pND_LIST_DATA d = new LIST_DATA; d->p = new char[24]; strcpy( d->p, "Hello" ); // 建立指向鏈表頂部的局部指標 pND_LIST Head = NULL; //向鏈表中添加一些資料 Head = pA_List_of_Data->AddToList( (void *) d, sizeof( pND_LIST_DATA ) ); // 該對象已被複製,現在刪除原來的對象 delete d; // 確認它已被儲存 char * p = ((pND_LIST_DATA) pA_List_of_Data->GetCurrentData())->p; d = new LIST_DATA; d->p = new char[24]; strcpy( d->p, "World" ); Head = pA_List_of_Data->AddToList( (void *) d, sizeof( pND_LIST_DATA ) ); // 該對象已被複製,現在刪除原來的對象 delete d; // 確認它已被儲存 p = ((pND_LIST_DATA) pA_List_of_Data->GetCurrentData())->p; // 刪除鏈表類,解構函式將刪除鏈表 delete pA_List_of_Data; }
|
小結
從前面的討論來看,似乎僅編寫一個簡單的鏈表就要做大量的工作,但這隻須進行一次。很容易將這段代碼擴充為一個處理排序、搜尋以及各種其他任務的 C++ 類,並且這個類可以處理任何資料對象或類(在一個項目中,它處理大約二十個不同的對象)。您永遠不必重新編寫這段代碼。