C語言下的容器及泛型程式設計
阿新 • • 發佈:2019-02-10
1 引言
眾所周知,C++語言提供了大名鼎鼎的標準模板庫(STL)作為C++語言下的程式設計利器受到無數青睞,而C語言下並沒有提供類似的工具,使得C語言的開發變得尤為困難和專業化。Nesty框架的NCollection容器為C語言提供了一整套標準的、豐富的模板工具,用以彌補C語言在結構化程式設計上的弱勢。之前發表的一篇文章是關於如何在C語言下進行面向物件程式設計的,主要介紹了NOOC的諸多特性。然而,NOOC真正的作用是為NCollection建立基礎。本文還是以一些簡單的程式碼作為例子,介紹NCollection的使用方法,通過閱讀本節的內容,你將瞭解為什麼NCollection可以大大地加快C語言下開發的效率。2 迭代模型
// incase MyList is a valid NList<NINT> instance
for (NINT Idx = 0; Idx < MyList.Len(); Idx++) {
// extract the element
NINT Val = MyList.GetIdx(Idx); // or MyList[Idx]
}
示例(2),Position迭代模式:
for (NPosition Pos = MyList.FirstPos(); Pos; Pos = MyList.Next(Pos)) { // extract the element NINT Val = MyList.GetPos(Pos); // or MyList(Pos) }
示例(3),STL迭代器模式:
// incase my_list is a valid std::list instance
for (std::list<int>::iterator it = my_list.begin(); it != my_list.end(); it++) {
// extract the element
int value = *it;
}
通過觀察上例,你會發覺Nesty容器所提供的Position迭代模式更加簡潔,而相比之下STL容器迭代器是一個子包含的物件,每次使用都需要從list型別中解壓,其語法是std::list<int>::iterator,而Position迭代模式通過呼叫容器的通用介面FirstPos返回一個位置資訊,並不斷通過Next及Prev來移動位置,並通過GetPos來從相應位置獲取資料。因此,Position僅代表一個抽象的位置,好比索引(Index)代表的是一個具有具體偏移值的位置一樣。 注意:為了方便闡述,上述程式碼使用了Nesty容器中的C++版本,C語言版本在編碼結構上與其一致,但程式碼略有不同。
3 NCollection容器框架
NCollection是在NOOC的基礎上開發的以面向物件為基礎的一套容器庫,提供約20種容器工具,其型別覆蓋所有通用資料結構,包括向量(NVector),列表(NList),集合(NSet),關聯表(NMap)等,以下是NCollection框架的全景UML圖,其中虛線框代表的是抽象類,實線框代表的是實現類:在上圖的整個框架中,有五個最為重要的容器介面:
NCollection
NCollection是框架中最頂層的基類,所有容器都派生自NCollection。對於外部而言,NCollection中所定義的操作是隻讀的,即只提供一個Push操作來單個插入元素;由於所有容器都有清空所有元素的操作,NCollection也提供了Empty操作用於批量刪除元素。NSeque
單詞Seque是Sequence的縮寫,代表序列。序列能很好地模擬先進先出(FIFO)及先進後出(FILO)等行為的資料結構。NSeque派生自介面NCollection並提供了一個Pop操作用於一次從容器中彈出一個元素,Pop操作會根據Seque的型別表現出不同的行為。例如,NSeque介面指向的是一個NQueue,則Pop表現為從佇列最前端彈出元素,如果NSeque指向的是一個NStack,則Pop表現為從棧最頂端彈出元素,以此類推。NSeque的實現類有NVector,NPriorQueue,NQueue,NStack。NList
NList是所有列表資料結構的抽象基類。列表具有佇列或棧的屬性,因此是從NSeque派生而來,但除此之外,列表通常還能以高效的速度(通常表現為常數級操作)從前/後、中間插入/刪除元素。列表是最為常用的資料結構,儘管有時候向量(NVector)同樣可以充當列表,但列表專門針對從內部插入/刪除元素作了優化。對於NSeque的介面行為,NList及其所有實現類都表現為佇列(FIFO)的屬性。NList的實現類有NArrayList,NDeList(雙端列表),NLinkedList,NStackList(棧表,行為和連結串列相同,但對記憶體碎片進行了優化)。NSet
NSet代表的是集合。如果僅從元素儲存來看,集合非常類似於列表,都是用於儲存單個元素,而集合卻對元素有快速查詢的能力。List的查詢總是基於遍歷的,其時間複雜度為O(N),而集合根據規則的不同,能夠提供比列表優越得多的查詢速度,例如對於雜湊而言,其平均查詢的效率通常為常數,而對於紅黑樹而言,其平均查詢時間通常為O(N Log N)。當然,集合在查詢速度上的優化是要付出代價的,例如其遍歷、插入/刪除速度不及列表,而且也無法維持列表元素的先後順序。NSet的實現類有NHashSet,NTreeSet,NArraySet,NLinkedSet,NStackSet。NMap
關聯表(Map)是以鍵(Key)和值(Value)構成的二元關係集合。如同NSet一樣,NMap對鍵擁有快速查詢的能力。為了保證容器在組成上的統一,NMap繼承了NCollection的介面,其介面表現為只對Key進行的操作。NMap的實現類包括:NHashMap,NTreeMap,NArrayMap,NLinkedMap,NStackMap。4 在C語言中使用Nesty容器
C語言是一門面向過程的程式語言,然而,通過對設計方法的規範化,依然可以在C語言使用C++中類似封裝,基於物件,甚至面向物件(NOOC)等的程式設計技術。物件包括了資料及方法的概念,因此在Nesty C的所有型別也按照類似的規則來定義。以NVector為例,NVector的資料是全大寫的物件NVECTOR,而NVector的操作是一組以NVector單詞開頭的函式的集合,如NVectorNew,NVectorAdd,NVectorDel等,以下程式碼片段演示如何在C語言下進行容器程式設計:// 建立NINT型別的容器物件
NVECTOR Vec = NVectorNew(NINT);
// 插入元素
NINT InsertValue = 3;
NVectorAdd(Vec, InsertValue);
// 刪除元素
NINT DeleteValue = 3;
NVectorDel(Vec, DeleteValue);
// 遍歷元素
NPOSITION Pos = NVectorFirstPos(Vec);
for (; Pos; Pos = NVectorNext(Vec, Pos)) {
NINT Val = NVectorGetPos(Vec, Pos, NINT);
}
// 不過對於向量,可以直接使用索引迭代模式
NINT Idx = 0;
for (; Idx < Vec->Len; Idx++) {
NINT Val = NvectorGetIdx(Vec, Pos, NINT);
}
5 C語言與泛型程式設計
泛型是從C++模板引入的概念,然而C語言下實現泛型可以利用無型別指標(void *)。而型別資料從二進位制的角度來探討,不外只是一塊有指定大小的記憶體塊;由於任何型別的指標的都可以隱式轉換為一個void *的地址,因此利用一個數據大小值,以及一個指向該資料頭部的無型別地址(void *)即可以表達任何型別的泛型。 但是定義一個型別光知道型別的大小是不行的,型別必須具有其獨特的行為,從型別資料的生存期來看,型別應該具備以下三個最為基本的行為(操作):建立(Create),拷貝(Copy),銷燬(Destroy)。以動態字串為例,字串的大小代表該字串佔用了多少個字元位元組。在字串建立時,需要將其初始化為指向一個有效字串的地址,當需要拷貝字串時,需要將字串資料進行逐字元拷貝,當字串不再被使用時,應該釋放其佔用的記憶體,並將字串指標初始化為0。 因此,在Nesty的框架中,型別的大小,建立,拷貝,銷燬這4個屬性構成了該型別的特徵簽名,這一概念和C++類的建構函式,複製拷貝操作符,解構函式等是對應的。6 泛型與Type Class
根據上一節的介紹,Nesty引入了Type Class的概念來支援C語言的泛型操作,在程式程式碼中由NTYPE_CLASS資料結構給出相關定義,Type Class指定了某型別相關的特性及操作,以下便是NTYPE_CLASS的定義:typedef struct tagNTYPE_CLASS
{
// type identifier
NINT TypeSize;
NPfnCreate FnCreate;
NPfnCopy FnCopy;
NPfnDestroy FnDestroy;
// type functions
NPfnMatch FnMatch;
NPfnCompare FnCompare;
NPfnHash FnHash;
NPfnPrint FnPrint;
// template functions
NPfnSwap FnSwap;
} NTYPE_CLASS;
定義那些以NPfn*開頭的定義是函式指標的定義,因此結構中的資料FnCreate,FnCopy,FnDestroy等是函式指標。當需要為型別建立容器時,需要為容器提供該型別的NTYPE_CLASS定義,以便告知容器根據這些操作來初始化/拷貝/刪除元素。以下例的自定義資料為例,為了使我們的容器能夠支援MYDATA,則需要為MYDATA的容器填充一個NTYPE_CLASS資料,並傳遞給容器的建立函式。
typedef tagMYDATA MYDATA;
struct tagMYDATA {
NINT Value;
};
// define Create behavior
void CreateMyData(NVOID * InThis, const NVOID * InOther) {
MYDATA * This = (MYDATA *)InThis;
MYDATA * Other = (MYDATA *)Other;
if (Other) {
This->Value = Other->Value;
}
else {
This->Value = 0;
}
}
// define Copy behavior
void CopyMyData(NVOID * InThis, const NVOID * InOther) {
MYDATA * This = (MYDATA *)InThis;
MYDATA * Other = (MYDATA *)Other;
NASSERT(Other); // source MUST not NULL!!!
This->Value = Other->Value;
}
// define Destroy behavior
void DestroyMyData(NVOID * InThis) {
MYDATA * This = (MYDATA *)InThis;
This->Value = 0;
}
為了方便闡述,以下先給出了NPfnCreate,NPfnCopy,及NPfnDestroy介面的定義:
typedef (*NPfnCreate)(NVOID * InThis, const NVOID * InOther);
typedef (*NPfnCopy)(NVOID * InThis, const NVOID * InOther);
typedef (*NPfnDestroy)(NVOID * InThis);
其中Create的介面能夠對型別進行預設構造和拷貝構造(當InOther為NULL)。當上例給出了MYDATA的這些行為函式,則可以利用他們來初始化/拷貝/銷燬該資料的例項:
MYDATA MyData, OtherData;
// create MYDATA by default construct
CreateMyData(&MyData, NULL);
// after create, MyData.Value will have the default value of 0
NASSERT(MyData.Value == 0);
// create MYDATA by copy construct, incase OtherData.Value == 3
CreateMyData(&MyData, &OtherData);
// after create, MyData.Value will have the initialized value of 3
NASSERT(MyData.Value == 3);
// copy MYDATA, incase OtherData.Value == 5
CopyMyData(&MyData, &OtherData);
// after copy, MyData.Value will have the updated value of 5
NASSERT(MyData.Value == 5);
// destruct MYDATA
DestroyMyData(&MyData);
// after destroy, MyData.Value will have the cleanup value of 0
NASSERT(MyData.Value == 0);
如果僅僅是為了初始化MyData根本不需要呼叫給出的這些函式,但這裡演示的是容器內部如何通過給定的這些操作來更新資料。另外,Type Class還定義了一些其他操作,如NPfnMatch用於比較兩個資料是否相等,相當過載C++的==操作符,NPfnCompare用於資料做大小比較,相當C++中過載<操作符,NPfnHash返回該型別雜湊值,NPfnPrint用於列印資料(主要為方便除錯),NPfnSwap用於交換資料(在對容器排序時用到,一般情況下使用者不需要提供NPfnSwap的定義,系統會自動建立)。以下給出了其餘介面的定義及根據本例比較常用的實現:
typedef (*NPfnCompare)(const NVOID * InThis, const NVOID * InOther);
typedef (*NPfnMatch)(const NVOID * InThis, const NVOID * InOther);
typedef (*NPfnHash)(const NVOID * InThis);
typedef (*NPfnPrint)(const NVOID * InThis, NCHAR * InBuf, NINT InLen);
typedef (*NPfnSwap)(NVOID * InThis, NVOID * InOther);
根據本例的MYDATA,以下是最為常用的實現:
NBOOL MatchMyData(const NVOID * InThis, const NVOID * InOther) {
MYDATA * This = (MYDATA *)InThis;
MYDATA * Other = (MYDATA *)Other;
return (NBOOL)(This->Value == Other->Value);
}
NBOOL CompareMyData(const NVOID * InThis, const NVOID * InOther) {
MYDATA * This = (MYDATA *)InThis;
MYDATA * Other = (MYDATA *)Other;
return (NBOOL)(This->Value < Other->Value);
}
NUINT HashMyData(const NVOID * InThis) {
MYDATA * This = (MYDATA *)InThis;
return (NUINT)This->Value;
}
NINT PrintMyData(const NVOID * InThis, NCHAR * InBuf, NINT InLen) {
MYDATA * This = (MYDATA *)InThis;
return NSnprintf(InBuf, InLen, _t("%d"), This->Value);
}
void SwapMyData(NVOID * InThis, NVOID * InOther) {
MYDATA * This = (MYDATA *)InThis;
MYDATA * Other = (MYDATA *)Other;
MYDATA Tmp = *This;
*This = *Other;
*Other = Tmp;
}
為了結束本小節,將以上面定義的操作為例填充一個NTYPE_CLASS資料結構,並建立相應的容器:NTYPE_CLASS TypeClass = { sizeof(MYDATA), CreateMyData, CopyMyData, DestroyMyData,
MatchMyData, CompareMyData, HashMyData, PrintMyData, SwapMyData };
NVECTOR Vec = NVectorNewCustom(&TypeClass, /* more parameters has been omitted */);
NINT Idx = 0;
for (; Idx < 8; Idx++) {
MYDATA Tmp;
Tmp.Value = Idx;
NVectorAdd(Vec, Tmp);
}
// print elements
NVectorPrint(Vec, NSystemOut());
// Outputs:
// [8](0, 1, 2, 3, 4, 5, 6, 7)
看到這裡你或許不禁要問,如果每使用一種型別都需要定義這麼多型別操作函式,還需要填充NTYPE_CLASS,在使用上豈不是相當麻煩?從表面上看,是的。然而,Nesty框架已經考慮到了這些問題,並且系統已經為大量常用型別預定義了Type Class,這些型別包括一些基本型別,如NINT, NUINT, NFLOAT, NDOUBLE等,大部分情況下,你不需要理會Type Class,只需要簡單地使用這些預定義的型別;然而,對於像MYDATA這種使用者自定義的資料結構,你還是需要提供一個Type Class,不過Nesty已經為你定製好了非常方便的工具,在最簡單的情況下,你只需要兩行程式碼即可以定義一個Type Class,之後會有單獨的小節介紹如何使用這個功能。
7 建立容器
有了上一節的內容作為基礎,本節將詳細介紹容器的建立方法。由於所有的容器型別都是NOBJECT物件,其建立使用的是new規則,即從堆上分配一塊記憶體並初始化,因此當結束使用時仍然需要呼叫NRELEASE介面釋放物件:NVECTOR Vec = NVectorNew(NINT);
// before exit
NRELEASE(Vec);
在上例中NVectorNew是一個巨集定義,接受一個型別作為引數,該巨集的實現會利用巨集的黏合操作獲取NINT預定義的TypeClass,其語法類似於:
NTYPE_CLASS * TypeClass = TypeClassNINT();
NVECTOR Vec = NVectorNewCustom(TypeClass, /* more paramter has been omitted */);
正如上一節所述,Nesty已經為各種常用的型別預定義了TypeClass,因此當你需要使用這些型別來建立容器時,是不需要再手動填充NTYPE_CLASS結構的,只需要像上例一樣給容器建立函式提供型別定義(如NINT)。NCollection中的所有容器都提供了類似NVectorNew這樣的介面,因此你完全不需要擔心建立容器會過於麻煩。另外,所有系統預定義的TypeClass都位於ntype_class.h標頭檔案中,在此不再一一列舉。因此,當你需要使用這些基本型別去建立容器時,應該優先考慮Nesty中定義的型別,例如你需要一個無符號整型的Vector,則應該使用NUINT去建立,而不應該應該使用unsigned
int,並且像NVectorNew(unsigned int)這樣的語法是非法的,如:
// create a unsigned int container
NVECTOR Vec = NVectorNew(NUINT);
// but, grammar like the following is invalid!
// NVectorNew(unsigned int);
對於Vector而言,NVectorNewCustom是最原始的介面,而NVectorNew及_NVectorNew是為了方便使用而進行的封裝,並且基本上能夠滿足需求;NCollection的所有容器都是基於這一規範設計的,因此當你建立容器時,儘量不要考慮NVectorNewCustom;但是,為了研究我們還是會細究NVectorNewCustom的各個引數,通過了解了NVectorNewCustom的介面,你將明白Nesty的容器到底是如何工作的;以下便是其定義:
NVECTOR NVectorNewCustom(const NTYPE_CLASS * InTypeClass, NBOOL InAutoShrinkable, const NALLOC_CLASS * InAllocClass);
在引數中,InTypeClass是容器元素型別的TypeClass描述(參考上節),InAutoShrinkable稍後介紹,InAllocClass用於告訴容器,其內部元素所使用的記憶體是如何分配和銷燬的,NALLOC_CLASS的定義如下:
typedef struct tagNALLOC_CLASS
{
NPfnMalloc FnMalloc;
NPfnRealloc FnRealloc;
NPfnFree FnFree;
NPfnCalculateStackSize FnCalculateStackSize;
} NALLOC_CLASS;
下面是NPfnMalloc,NPfnRealloc,NPfnFree及NPfnCalculateStackSize的定義:
typedef NVOID * (*NPfnMalloc) (NSIZE_T InSize);
typedef NVOID * (*NPfnRealloc) (NVOID * InPtr, NSIZE_T InSize);
typedef void (*NPfnFree) (NVOID * InPtr);
typedef NINT (*NPfnCalculateStackSize)(NINT InStackNum, NINT InStackMax);
與TypeClass的規則類似,TypeClass用於描述型別資料是如何建立和銷燬的,而AllocClass則描述容器內部的記憶體是如何被管理的;當容器內部需要為元素分配/釋放記憶體時,都會呼叫FnMalloc/FnRealloc/FnFree所指向的函式;這三者最為常見的實現是,直接呼叫作業系統的記憶體分配策略,如下所示:
NVOID * DefaultMalloc(NSIZE_T InSize) { return malloc(InSize); }
NVOID * DefaultAlloc(NVOID * InPtr, NSIZE_T InSize) { return realloc(InPtr, InSize); }
void DefaultFree(NVOID * InPtr) { free(InPtr); }
以上看來,為容器提供AllocClass看似是多餘的,其實不然,當我們需要對容器的分配策略進行優化,例如,將容器的分配重定向到使用者自定義的記憶體池,以便達到專一的目的時,NALLOC_CLASS作為一個有用介面將給容器定製提供充分便利。
接下來,需要討論的是容器的棧的屬性。棧(Stack)顧名思義,代表的是一堆緊靠的東西的意思;Vector的實現,使用的是動態陣列的策略,即Vector的內部會維護一個活動的記憶體堆疊,該堆疊總會預留一定數量的元素個數,當需要連續新增新元素時,會從預留的元素中分配,而不是重新分配一大塊記憶體;只有當元素個數超出了預留的空間時,才會進行重新分配;相反,當容器元素個數變得很稀少,而預留的空間又太大時,為了不浪費記憶體,活動棧也會執行重新分配,並釋放多餘的元素空間。總之,容器內部的活動棧會在容器執行插入/刪除操作時,根據當前元素的個數進行動態調整(擴張/收縮)。InAutoShrinkable引數預設為True,當為False時,容器在刪除元素時將不會根據收縮內部的活動棧。這一屬性對於某些靜態容器相當有用,例如,假設有一個容器專門用於儲存某些靜態物件,這些物件一旦建立將不會被銷燬,這時可以將容器的棧空間預設為經過統計得來的峰值,並進行一次分配,則可以省去了來回分配/釋放記憶體的麻煩。
NPfnCalculateStackSize用於控制活動棧的預留策略,活動棧包括兩個基本屬性,當前元素個數(StackNum),及最大元素個數(StackMax);一般情況下StackNum總是小於StackMax,當達到或者超過StackMax時,則表明棧空間不足,需要對棧重新分配,以容納更多元素。這時候容器內部會呼叫FnCalculateStackSize所指向的策略函式,重新計算棧的上限值(StackMax)。Nesty容器所提供的預設策略是預留約25%的元素個數,假設當前容器的元素個數是100個,並且已經達到了上限,需要重新分配,則重新計算後的StackMax值為125(預留25%);當然,使用者隨時可以修改該策略,例如總是預留約2倍的元素,可以通過修改FnCalculateStackSize所指向的策略來實現。值得注意的是,只有那些具有活動棧屬性的容器才會提供InAutoShrinkable標誌及FnCalculateStackSize才會發揮作用,對於那些無法提供棧屬性的容器(例如NLinkedList等),則上述的引數將被忽略掉。具有活動棧屬性的容器有:NVector,NPriorQueue,NArrayList,NDeList,NStackList,NArraySet,NStackSet,NArrayMap,NStackMap。
8 新增/刪除及訪問元素
往容器中新增元素,可以通過Push和Add方法,Push與Pop構成堆疊相互對應的操作;然而,當容器不作為堆疊使用時,為了區分這些操作的意思,大部分容器都定義了相關的Add及Del操作用於新增/刪除元素,實際上Push和Add的行為是一樣的,只不過其代表的意義不同而已。下面以NArrayList為例,演示瞭如何往容器新增元素:NARRAY_LIST List = NArrayListNew(NINT);
NINT Val = 0;
NArrayListAdd(List, Val);
Val = 1;
NArrayListAdd(List, Val);
NArrayListPrint(List, NSystemOut());
// Outputs:
// [2](0, 1)
觀察上面的例子,兩次對NArrayListAdd的呼叫,都需要初始化一個臨時變數Val,並將Val傳遞給NArrayListAdd,為什麼需要這樣做呢?原因是NArrayListAdd是一個巨集定義,而實際產生作用的是帶前下劃線的函式定義_NArrayListAdd,_NArrayListAdd接受的資料實際上是一個無符號的型別指標,其介面的宣告如下所示:
typedef void NELEMENT_T;
NPOSITION _NArrayListAdd(NARRAY_LIST InObj, const NELEMENT_T * InElement);
在前面的小節曾探討過C語言的泛型程式設計是通過無符號型別指標去泛化所有的型別,儘管之前在建立NArrayList的例項的時候,指明瞭使用NINT型別,但在新增元素的過程中,NArrayList實際上並不知道該型別,NArrayList唯一接受的是一個無符號的型別指標,其可能指向任何型別的資料,但該資料具體的行為是在建立NArrayList容器物件的時候通過傳遞進來的NTYPE_CLASS資料結構來描述的。因此,在插入元素時,容器收到的是一個無符號型別資料的地址,並通過其指定的TypeClass的Create操作來初始化資料;刪除元素時,容器依然會將儲存在內部的元素當做一個無符號的型別對待,並且通過呼叫TypeClass的Destroy操作來銷燬資料,以此類推。
Nesty泛型的概念是一個比較難以接受的東西,其工作原理完全不同於C++的模板;C++模板會在編譯階段根據實際型別“例項化”相關模板,並且不同型別都會產生一份程式碼的拷貝,而Nesty泛型所定義的操作永遠只有唯一一份,並且通過無型別void及Type Class來泛化型別的例項。當明白了Nesty泛型的工作原理後,你便了解為什麼需要預先初始化一份臨時資料並用於插入/刪除,因為這個臨時資料能夠產生一個有效的無符號地址,並將該地址的資料作為插入元素的資料傳遞給容器內部的Type Class的Create操作。為了便於理解這一概念,下面提供了一個分解的操作:
NINT Val = 0;
_NArrayListAdd(List, &Val);
因此,你無法將一個字面常量傳遞給NArrayListAdd,以下程式碼將會產生錯誤,提示字面量無法進行地址轉換:
// invalid grammar, you MUST have a initialized varable, but NOT a literal
// NArrayListAdd(List, 0);
刪除元素的操作與插入元素的原理是相同的,同樣需要初始化一個臨時資料:
NARRAY_LIST List = NArrayListNew(NINT);
NINT Idx = 0;
for (; Idx < 8; Idx++) {
NArrayListAdd(List, Idx);
}
NArrayListPrint(List, NSystemOut());
// Outputs:
// [8](0, 1, 2, 3, 4, 5, 6, 7)
NINT ValToDel = 3;
NArrayListDel(List, ValToDel);
NArrayListPrint(List, NSystemOut());
// Outputs:
// [7](0, 1, 2, 4, 5, 6, 7)
對元素的訪問也遵循同樣的原理,獲取元素資料的介面實際上返回的也是一個無符號型別的地址,使用者需要通過強制型別轉換將該地址轉換為實際型別的地址,以_NArrayListGetIdx為例,繼續上面的示例程式碼:
// definition of _NArrayListGetIdx
NELEMENT_T * _NArrayListGetIdx(NARRAY_LIST InObj, NINT InIdx)
// incase List is a valide instance of NARRAY_LIST of type NINT
NArrayListPrint(List, NSystemOut());
// Outputs:
// [8](0, 1, 2, 3, 4, 5, 6, 7)
NINT Val = *(NINT *)_NArrayListGetIdx(List, 3);
NASSERT(Val == 3);
然而,帶前置下劃線的_NArrayListGetIdx是一個不太好用的介面,因為每次呼叫都要執行型別轉換,因此NArrayList提供了更為方便的工具,如下例所示:
NINT Val = NArrayListGetIdx(List, 3, NINT);
NASSERT(Val == 3);
NINT * ValPtr = &NArrayListGetIdx(List, 6, NINT);
NASSERT(*ValPtr == 6);
不過,你需要注意的是,類似於NArrayListGetIdx / Pos等的介面的最後一個引數必須與你建立容器時所填寫的型別引數一致,否則將導致錯誤的轉換。
9 基於介面的容器程式設計
從前面介紹NCollection框架的小節我們瞭解到,NCollection提供了幾個通用的介面,這些介面和實現類是通過NOOC的繼承關係實現的,意味著我們可以使用任一繼承鏈上的介面的方法來操作實現類,下面的例子演示當我們需要使用NArrayList時,可以通過NList介面的方法,因為NArrayList是繼承自NList的,這樣可以增加很多靈活性;例如,我們可以在建立介面時將NArrayList更換為別的列表而程式的其他部分不受影響:NLIST List = (NLIST)NArrayListNew(NINT);
NINT Idx = 0;
for (; Idx < 8; Idx++) {
NListAdd(List, Idx);
}
NListPrint(List, NSystemOut());
// Outputs:
// [8](0, 1, 2, 3, 4, 5, 6, 7)
由於NCollection是所有容器的介面,通過NCollection可以實現某些泛型的演算法和功能,例如下列演算法,將接受任何型別的容器,並將元素單例化並逐個拷貝到另一個容器中:
void CopyUniqueInt(NCOLLECTION InDst, const NCOLLECTION InSrc) {
NPOSITION Pos = NCollectionFirstPos(InSrc);
for (; Pos; Pos = NCollectionNext(InSrc, Pos)) {
NINT Val = NCollectionGetPos(InSrc, Pos, NINT);
if (NCollectionFindPos(InDst, Val) == 0) {
NCollectionPush(InDst, Val);
}
}
}
10 遍歷元素
我們在第2小節的時候已經討論過了迭代模型,Position模式作為Nesty容器的標準模式為所有型別的容器提供了一致的的迭代介面,其意義在於為實現泛型演算法提供支援。如果你對Position模式並不適應,NList介面還提供了更為直觀的Index模式,利用Index模式進行迭代類似於遍歷陣列元素,但對某些型別的容器,Index模式在效率上會大打折扣。Position/Index模式
為了更好理解Position模式和Index模式,下面的例子用Position模式分別以前向及後向迭代列表元素:NLIST List = (NLIST)NLinkedListNew(NINT);
NPOSITION Pos = NULL;
NINT Idx = 0;
// push elements
for (; Idx < 8; Idx++) {
NListAdd(List, Idx);
}
// forward iteration
for (Pos = NListFirstPos(List); Pos; Pos = NListNext(List, Pos)) {
NINT Val = NListGetPos(List, Pos, NINT);
NPrintf(_t("%d, "), Val);
}
// Outputs:
// 0, 1, 2, 3, 4, 5, 6, 7,
// backward iteration
for (Pos = NListLastPos(List); Pos; Pos = NListPrev(List, Pos)) {
NINT Val = NListGetPos(List, Pos, NINT);
NPrintf(_t("%d, "), Val);
}
// Outputs:
// 7, 6, 5, 4, 3, 2, 1, 0,
// index iteration
for (Idx = 0; Idx < NListLen(List); Idx++) {
NINT Val = NListGetIdx(List, Idx, NINT);
NPrintf(_t("%d, "), Val);
}
// Outputs:
// 0, 1, 2, 3, 4, 5, 6, 7,
如果是NVector,NArrayList及NDeList,使用Index模式將更加效率,原因在於這些容器都是基於動態陣列的原理,即各個元素總是位於一塊連續的記憶體塊中;但是像NLinkedList及NStackList等其他一些容器,是基於連結串列的原理,即元素之間通過前向及後向指標進行連結的,各個元素在記憶體上的分佈是分散的;但即便如此,連結串列中的各個元素還是維持了先後順序,只不過在按Index迭代元素時,總是從列表的頭部/尾部元素開始,並逐個元素移動到相應的索引位置上。因此,上例按索引迭代的程式碼,其實質遠沒有你表面上看到的這樣簡潔;然而慶幸的是,NLinkedList及NStackList得到了快取的支援,如果僅僅是迭代元素,效能只是稍有損失;以上例中8個元素的連結串列為例,假設當前訪問的索引位置是3,在第一次訪問索引3時,連結串列依然要從頭部開始逐個向前移動到第三個元素,並將索引3和對應節點快取,假設下次訪問索引4,由於4和已經快取的索引3之間相差1,連結串列會直接從快取的節點開始向前移動一個元素的位置,以此類推。當連結串列重新插入/刪除元素時,快取的索引將被清除;因此,當連結串列按照索引進行迭代時,真正造成效能瓶頸的時候,是在你迭代過程中刪除/插入的時候。
迭代中刪除元素
迭代容器是按Position逐個迭代,刪除元素也是按Position逐個刪除;需要注意的是,像NListDelPos這種型別的介面會返回刪除後下一個元素的Position,其原型為:NPOSITION NListDelPos(NLIST InObj, NPOSITION InPos);
這一返回值是專為迭代中刪除元素而設計的,要解釋其原理需要牽扯到不容器內部較為複雜的實現,你需要記住的是,當從正向迭代刪除元素時,應該遵循以下的語法,如果觀察比較細緻的話,會發現C++的標準模板庫的迭代器也使用了類似的原理:
NPOSITION Pos = NULL;
NListPrint(List, NSystemOut());
// Outputs:
// [8](0, 1, 2, 3, 4, 5, 6, 7)
for (Pos = NListFirstPos(List); Pos; ) {
NINT Val = NListGetPos(List, Pos, NINT);
if (Val > 1 && Val < 6) {
Pos = NListDelPos(List, Pos);
}
else {
Pos = NListNext(List, Pos);
}
}
NListPrint(List, NSystemOut());
// [4](0, 1, 6, 7)
你需要注意的是NListDelPos和NListNext的使用方法;如果是以後向的方式迭代刪除元素,語法稍微簡單一些,如下所示:
NPOSITION Pos = NULL;
NListPrint(List, NSystemOut());
// Outputs:
// [8](0, 1, 2, 3, 4, 5, 6, 7)
for (Pos = NListLastPos(List); Pos;) {
NINT Val = NListGetPos(List, Pos, NINT);
NPOSITION Prev = NListPrev(List, Pos);
if (Val > 1 && Val < 6) {
NListDelPos(List, Pos);
}
Pos = Prev;
}
NListPrint(List, NSystemOut());
// [4](0, 1, 6, 7)
NCollection容器通過引入Position的概念,抽象並統一了各個容器訪問元素的規則,通過找到某一元素在容器中的Position並通過Next和Prev介面可以方便地前後移動移動元素,這種規則對於實現演算法是非常有幫助的;然而,Position模式在迭代過程中刪除元素時,規則複雜了些少,不過幸運的是,如果其他容器框架一樣,Nesty容器一樣提供了迭代器的介面,通過一下節的介紹,你將瞭解通過使用迭代器,可以大大簡化遍歷及刪除元素的步驟。
11 使用迭代器
迭代器的介面是由物件NITERATOR提供的,下面通過幾段簡短的程式碼來演示其用法:NLIST List = (NLIST)NLinkedListNew(NINT);
NITERATOR It = NULL;
NINT Vals[] = { 0, 1, 2, 3, 4, 5, 6, 7 };
NListAddNum(List, Vals, NARRAY_LEN(Vals));
It = NListIterator(List, NFALSE);
while (NIteratorHasNext(It)) {
NINT Val = NIteratorNext(It, NINT);
NPrintf(_t("%d, "), Val);
if (Val > 1 && Val < 6) {
NIteratorRemove(It);
}
}
// Outputs:
// 0, 1, 2, 3, 4, 5, 6, 7
NRELEASE(It);
NListPrint(List, NSystemOut());
// Outputs:
// [4](0, 1, 6, 7)
當你呼叫NListIterator方法時,將會返回一個新建立的NITERATOR物件,此時的Iterator處於一個“無效位置”的狀態,使用者需要通過NIteratorHasNext及NIteratorNext操作將迭代移動到一個有效位置並獲取資料。如果你曾經使用過Java則一定很清楚其迭代器介面的工作原理,這裡所使用的迭代器與Java是一樣的:
NLIST List = (NLIST)NLinkedListNew(NINT);
NITERATOR It = NListIterator(List, NFALSE);
NPOSITION Pos = NIteratorPosition(It);
NASSERT(Pos == NULL);
if (NIteratorHasNext(It)) {
NIteratorNext(It, NINT);
Pos = NIteratorPosition(It);
NASSERT(Pos != NULL);
}
由於NITERATOR是一個NOOC物件,在每次使用完後,都應該通過NRELEASE介面立即將其釋放,否則迭代器將長期持有其宿主容器的一個引用計數。迭代器的方法NIteratorHasNext及NIteratorNext/_NIteratorNext是配合使用的,只有通過NIteratorHasNext檢測存在下一個元素的時候,才能呼叫NIteratorNext/_NIteratorNext移動迭代器的位置,NIteratorNext/_NIteratorNext在移動下一位置的同時返回當前的值,使用者通過檢測這個值判斷是否要對當前元素執行刪除操作,其語法如下所示:
NITERATOR It = NListIterator(List, NFALSE);
while (NIteratorHasNext(It)) {
NINT Val = NIteratorNext(It, NINT);
NBOOL ShouldRemove = /* check current value */;
if (ShouldRemove) {
NIteratorRemove(It);
}
}
NRELEASE(It);
由於Remove操作總是發生在Next之後,因此你永遠不需要像Position一樣,關心Remove操作是否會影響下一個要遍歷的元素;其程式碼的方式將更加簡潔和易於理解。
迭代器同樣可以用於鍵值二元對的關聯表容器(NMap),當使用迭代器遍歷Map時,Next操作總是返回值(Value)而不是鍵(Key),但是迭代器提供了另外的操作GetKey來獲取當前遍歷元素對的鍵;當迭代器作用於像列表(NList)或集合(NSet)這種一元容器時,Next操作和GetKey操作返回的都是相同的數值,其程式碼示例如下:
// for list or other single element containers
NList List = (NLIST)NArrayListNew(NINT);
NITERATOR Items = NListIterator(List, NFALSE);
while (NIteratorHasNext(Items)) {
NINT Val = NIteratorNext(Items, NINT);
NINT Key = NIteratorGetKey(Items, NINT);
NASSERT(Val == Key);
}
// for map the key-value pair element containers
NMAP Map = (NMAP)NHashMapNewMulti(NINT, NCHARS);
NINT Key = 0;
NCHAR * Val = _t("ABCD");
NMapAdd(Map, Key, Val); // push more 0 and "ABCD"
NITERATOR Pairs = NMapIterator(Map, NFALSE);
while (NIteratorHasNext(Pairs)) {
NCHAR * Val = NIteratorNext(Pairs, NCHARS);
NINT Key = NIteratorGetKey(Pairs, NINT);
NASSERT(Key == 0);
NASSERT(NStrcmp(Val, _t("ABCD") == 0);
}
12 字串及容器
由於TypeClass介面所發揮的特殊作用,在Nesty容器介面中使用字串元素顯得十分方便,你只需要像建立其他型別一樣使用NCHARS型別,如下所示:NHASH_MAP Map = NHashMap(NCHARS, NINT);
NCHAR * Key = _t("nesty");
NINT Val = 0;
NHashMapAdd(Map, Key, Val);
// Add more key and value ...
通過字串去查詢容器的語法也一樣簡單:
NCHAR * Key = _t("nesty");
NPOSITION Pos = NHashMapFindPos(Map, Key);
NINT Val = NHashMapGetValue(Map, Pos, NINT);
NCHARS符號其實只是NCHAR *指標的另一個定義:
typedef const NCHAR * NCHARS;
在本節的第一個例子中,我們將臨時變數Key作為一個引數傳遞給NHashMapAdd,但NHashMap容器並不會儲存Key所指向的字串的地址,而是通過NCHARS型別的Create操作為鍵值分配字串大小的記憶體空間,並將Key字串的內容拷貝到新分配的記憶體空間中;因此容器儲存的是一個新分配的字串物件,而不是Key所指向的靜態字串的記憶體地址;當容器通過Empty操作刪除元素的時候,會再呼叫NCHARS的Destroy操作來釋放從堆上分配的記憶體,因此無需主動去為字串分配/釋放記憶體。通過下面的例子說明容器中的鍵值與作為臨時引數的Key指向的是不同的字串地址:NHASH_MAP Map = NHashMap(NCHARS, NINT);
NCHAR * Key = _t("nesty");
NINT Val = 0;
NHashMapAdd(Map, Key, Val);
NPOSITION Pos = NHashMapFindPos(Map, Key);
const NCHAR * KeyFromMap = NHashMapGetKey(Map, Pos, NCHARS);
NASSERT(Key != KeyFromMap);
如果你確實想自己去管理字串記憶體,則不要使用NCHARS作為容器的建立型別,例如你可以使用NVOIDP,即讓容器儲存一個記憶體地址,或者使用更直接的NCHARP;雖然NCHARS跟NCHARP都是NCHAR *的一個型別定義,但是其TypeClass的協議是不同的,使用NCHARS時容器會為你建立一個記憶體託管的字串型別,但當使用NCHARP是,則容器會把該字串視作一個純粹的字串指標,不會為你託管記憶體;下面是使用NCHARP建立容器時的程式碼,請注意其工作方式的不同:NMAP_MAP Map = NHashMapNew(NCHARP, NINT);
NCHAR * Key = (NCHAR *)NMalloc(sizeof(NCHAR *) * NStrlen(_t("nesty")));
NINT Val = 0;
NStrcpy(Key, _t("nesty"));
NHashMapAdd(Map, Key, Val);
// ...
由於容器使用的字串的記憶體是通過你呼叫堆分配函式NMalloc進行分配,並將指標儲存在容器的鍵元素中,因此當你清空容器元素之前,也需要從外部呼叫堆釋放函式NFree來回收記憶體(容器本身不會為你釋放記憶體)否則將引發洩漏:
NPOSITION Pos = NHashMapFirstPos(Map);
for (; Pos; Pos = NHashMapNext(Map, Pos)) {
const NCHAR * Key = NHashMapGetKey(Map, Pos);
// release string memory
NFree(Key);
}
NHashMapEmpty(Map);
// ...
程式設計師手動地去管理記憶體是一件痛苦的事情,因此一般情況下,你只需要使用NCHARS型別來建立容器便足夠了,NCHARS的TypeClass協議會為你管理字串的記憶體。
最後一種建立字串容器的方式是使用Nesty的NSTRING物件,NSTRING是Nesty框架提供的標準字串介面,通過NSTRING可以方便的實現字串連線,替換,查詢等操作,但由於NSTRING是一個NOOC物件,其物件的建立和銷燬遵循某些面向物件的規則,因此其過程是複雜且相對低效的;使用NSTRING來建立字串容器將使程式簡潔性和效能稍打折扣,一般情況下不推薦使用;不過如果你堅持使用,Nesty容器依然為NSTRING提供了相應的介面。另外,NSTRING容器使用的是NOOC物件的TypeClass規則,其步驟較為繁瑣,後面會有單獨的小節介紹如何建立NOOC物件容器:
NMAP_MAP Map = NHashMapNew(NSTRING, NINT);
NSTRING Str = NStringNew(_t("nesty"));
NINT Val = 0;
NHashMapAdd(Map, Str, Val);
NRELEASE(Str);
接下來示例如何在NSTRING容器中查詢鍵值:
NSTRING Tmp = NStringNew(_t("nesty"));
NPOSITION Pos = NHashMapFindPos(Map, Tmp);
// After done finding
NRELEASE(Tmp);
13 型別與Type Class
通過前面小節的介紹,Nesty已經預定義了部分常用資料型別的TypeClass,因此這些型別可以直接作為容器建立介面的引數,這些常用型別有: 整型:NBYTE,NUBYTE,NSHORT,NUSHORT,NINT,NUINT,NLONG,NULONG 浮點型:NFLOAT,NDOUBLE 字元型:NCHAR,NCHARP,NCHARS,NSTRING 及等等…… 然而,對於某些使用者自定義的型別(如第6節中的自定義型別MYDATA),為了能夠讓容器使用這些型別,程式設計師在定義資料的同時,也要定義相應的Type Class;Nesty的系統已經為你提供了非常方便的工具來達到目的。需要定義型別的TypeClass,需要兩個步驟,首先在宣告程式碼中(最好是與資料結構定義處於同一檔案中)使用巨集NTYPE_CLASS_DEC來宣告TypeClass,然後在實現程式碼中提供相應的協議函式(如前面介紹的Create,Copy,Destroy的操作的函式),並用NTYPE_CLASS_IMP巨集來繫結這些協議函式。建立普通型別的TypeClass
當前所指的普通型別,是指由C的關鍵字struct或者typedef定義的型別,不包括NOOC物件型別,以MYDATA為例:typedef tagMYDATA MYDATA;
struct tagMYDATA {
NINT Value;
};
// declaration part
NTYPE_CLASS_DEC(MYDATA);
// implementation part
void CreateMyData(NVOID * InThis, const NVOID * InOther) {...}
void CopyMyData(NVOID * InThis, const NVOID * InOther) {...}
void DestroyMyData(NVOID * InThis) { ... }
// ... and more actions, Match, Compare, Hash, Print
NTYPE_CLASS_IMP(MYDATA,
CreateMyData,
CopyMyData,
DestroyMyData,
MatchMyData,
CompareMyData,
HashMyData,
PrintMyData);
// After create the Type Class for MYDATA type,
// you can use 'MYDATA' as the parameter of containers
NLINKED_LIST List = NLinkedListNew(MyData);
// ....
事實上提供TypeClass協議函式是一件非常煩人的事情,而NTYPE_CLASS_IMP另一個更加強大的功能是允許你提供預設值,即傳遞NULL引數,這是NTYPE_CLASS_IMP將為你提供一個預設實現,至於預設實現的預設動作是什麼將會稍後討論。例如,如果你只想為MYDATA填寫Create,Copy,和Destroy操作,則你可以按如下方式填寫NTYPE_CLASS_IMP的引數:
NTYPE_CLASS_IMP(MYDATA,
CreateMyData,
CopyMyData,
DestroyMyData,
NULL,
NULL,
NULL,
NULL);
當然,你還可以為所有的TypeClass協議都提供預設值,讓系統為你提供預設實現:
NTYPE_CLASS_IMP(MYDATA,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL);
對於上述情況,你還可以使用另一個更加方便的巨集,NTYPE_CLASS_IMP_DEFAULT
NTYPE_CLASS_IMP_DEFAULT(MYDATA);
TypeClass協議的預設實現
當你使用巨集NTYPE_CLASS_IMP並給相應的協議操作傳遞NULL引數時,Nesty將為這些操作提供預設實現,下面列舉出這些預設實現的相關內容,繼續以MYDATA為例: 1) Create 會有條件地執行記憶體拷貝,當Create操作的InOther引數為空時,會通過NZeroMemory將記憶體清零,如:void CreateDefaultMyData(NVOID * InThis, const NVOID * InOther) {
if (InOther) {
NMemcpy(InThis, InOther, sizeof(MYDATA));
}
else {
NZeroMemory(InThis, sizeof(MYDATA));
}
}
2) Copy 執行簡單的記憶體拷貝,如:
void CopyDefaultMyData(NVOID * InThis, const NVOID * InOther) {
NMemcpy(InThis, InOther, sizeof(MYDATA));
}
3) Destroy 執行簡單的記憶體清0,如:
void DestroyDefaultMyData(NVOID * InThis, const NVOID * InOther) {
NZeroMemory(InThis, sizeof(MYDATA));
}
4) Match,Compare和Hash等操作會執行相應的記憶體比較和雜湊:
NBOOL MatchDefaultMyData(const NVOID * InThis, const NVOID * InOther) {
return (NBOOL)(return NMemcmp(InThis, InOther, sizeof(MYDATA)) == 0);
}
NBOOL CompareDefaultMyData(const NVOID * InThis, const NVOID * InOther) {
return (NBOOL)(return NMemcmp(InThis, InOther, sizeof(MYDATA)) < 0);
}
NUINT HashDefaultMyData(const NVOID * InThis) {
return NMemhash(InThis, sizeof(MYDATA));
}
5) Print操作僅簡單地列印This指標的地址:
NINT PrintDefaultMyData(const NVOID * InThis, NCHAR * InBuffer, NINT InLength) {
return NSnprintf(InBuffer, InLength, _t("%p"), InThis);
}
NOOC物件型別Type Class
建立NOOC的物件型別的Type Class相當簡單,只需要使用巨集NTYPE_CLASS_IMP_OBJECT,不需要實現及填寫任何的協議操作,由於NOOC物件型別的NOBJECT介面包含了Clone,Comp,Equal,HashCode及ToString等操作,NTYPE_CLASS_IMP_OBJECT實現會直接呼叫這些介面來實現TypeClass,因此NOOC物件是通過過載相應的介面來實現TypeClass的協議的;下面例子只簡單演示如何為NOOC物件MYOBJ建立TypeClass:NOBJECT_PRED(MYOBJ);
NOBJECT_DEC(MYOBJ, NOBJECT);
struct tagMYOBJ {
NOBJECT_BASE(NOBJECT);
NINT Val;
}
// declaration part
NTYPE_CLASS_DEC(MYOBJ);
// implementation part
NOBJECT MyObjClone(const MYOBJ InObj) { ... }
NBOOL MyObjEqual(const MYOBJ InObj, const NOBJECT InOther) { ... }
NBOOL MyObjComp(const MYOBJ InObj, const NOBJECT InOther) { ... }
NUINT MyObjHashCode(const MYOBJ InObj) { ... }
NSTRING MyObjToString(const MYOBJ InObj) { ... }
NOBJECT_IMP(MYOBJ, NOBJECT,
NCLONE_BIND(MyObjClone)
NEQUAL_BIND(MyObjEqual)
NHASHCODE_BIND(MyObjHashCode)
NCOMP_BIND(MyObjComp)
NTOSTRING_BIND(MyObjToString)
);
NTYPE_CLASS_IMP_OBJECT(MYOBJ);
NOOC物件的建立是一個相對複雜的過程,作者發表的另一篇文章《C語言下的面向物件程式設計技術》提供了一部分參考,其連線為點選開啟連結。
14 容器與持有物件
NOOC物件是一個引用計數物件,其通過引用計數來實現例項之間的共享及垃圾回收等操作,每一個NOOC物件在通過NNEW建立時其原始計數都為,因此當使用完畢後,必須通過NRELEASE介面來釋放計數,以便系統回收為其分配的動態記憶體。然而當該物件需要被共享(或者需要被其他物件所持有時),可以通過呼叫NACQUIRE介面來增加引用計數,以NSTRING物件為例:NSTRING Str = NStringNew(_t("hello Nesty"));
NASSERT(NCOUNTER(Str) == 1);
// Hold(Acquire) object reference
NSTRING NewRef = Str;
NACQUIRE(NewRef);
NASSERT(NCOUNTER(Str) == 2);
// After done working, you have to call equivalent numbers of NRELEASE to reclaim the object
NRELEASE(NewRef);
NRELEASE(Str);
通過引用計數可以很方便地實現物件間的共享,並節省記憶體;即當你需要在多個地方使用到同一物件時,只需要保有一個引用計數,並在退出時釋放該計數,而不用為每個應用單獨分配新的物件。
因此,容器對NOOC物件型別使用了同樣的規則,當你以物件的方式來建立容器時,新增元素並不會導致容器為每個元素都克隆(clone)一個拷貝,而僅僅是持有該物件的引用,並且容器在刪除元素時會自動將該引用釋放。為此,本節將解釋前面12(字串與容器)小節中,當使用NSTRING物件建立容器時,為何必須在新增/查詢元素後,將臨時NSTRING物件釋放:
NMAP_MAP Map = NHashMapNew(NSTRING, NINT);
NSTRING Str = NStringNew(_t("nesty"));
NINT Val = 0;
NHashMapAdd(Map, Str, Val);
NRELEASE(Str);
先回顧上面的這段程式碼,Str通過NStringNew來賦值時,該NSTRING物件的初始計數是1,當將該字串物件傳遞給NHashMapAdd後,容器僅僅是儲存了該物件地址的拷貝,並通過NACQUIRE持有該物件的一個計數;因此新增完畢後,Str物件的引用計數將變為2。而程式碼最後通過NRELEASE來將Str物件釋放,其目的是使它的計數回覆到1,即將該物件的持有權完全交給了HashMap,因為將來當你通過呼叫容器的Empty等操作來刪除該元素時,容器將會自動幫你回收該物件。假如你在新增元素之後不釋放Str物件的計數,即使將來HashMap刪除了該元素,也僅僅使其計數變為1,但物件不會被釋放,因此造成洩漏。
15 容器與元素分配
為了方便描述本節的內容,先回到之前MYDATA的定義,並在這裡稍作修改:typedef struct tagMYDATA MYDATA;
struct tagMYDATA {
NBYTE Data[32];
}
現在MYDATA變成了一個包含一塊32位元組大小的連續記憶體的資料結構,如果我們為其建立了TypeClass,然後建立容器並新增8個元素:
NVECTOR Vec = NVectorNew(MYDATA);
NINT Idx = 0;
for (; Idx < 8; Idx++) {
MYDATA Tmp;
NVectorAdd(Vec, Tmp);
}
則Vector會將這些元素的資料都儲存在一塊連續的記憶體中,且每個元素大小為sizeof(MYDATA) ,這與建立一個8個元素的靜態陣列的結構是類似的;容器會根據TypeClass中的TypeSize成員來分配並回收元素的記憶體。但有些時候,使用者如果想單獨管理各個元素的記憶體,則可以以指標方式來建立容器,如:
// NVOIDP is a typedef of void *
NVECTOR Vec = NVectorNew(NVOIDP);
MYDATA * Tmp = (MYDATA *)NMalloc(sizeof(MYDATA));
NVectorAdd(Vec, Tmp);
// Add more elements...
當你以這種方式來建立容器時,容器管理的只是元素的指標,但不會為你管理該指標所指向的記憶體,因此當你刪除元素時,還需要單獨去回收每個元素所佔用的記憶體:
NINT Idx = 0;
for (; Idx < Vec->Len; Idx++) {
NVOID * Data = NVectorGetIdx(Vec, Idx, NVOIDP);
NFree(Data);
}
NVectorEmpty(Vec);
16 列印及除錯容器
為了方便對容器進行除錯,容器提供了一套規範的字串化功能,但是字串花需要TypeClass協議中Print操作的支援,即前提是某型別已經在其TypeClass中定義了Print操作。所有的容器都提供了相關的Print介面,如NArrayListPrint,NSetPrint等,這些Print介面都接受一個NSTREAM_OUT的物件作為輸出物件,NSystemOut()代表輸出到系統的標準輸出介面,通常為命令列控制檯,但也可以輸出到Buffer。以NINT型別的容器為例:NARRAY_LIST List = NArrayListNew(NINT);
// incase List has elements: 0, 1, 2, 3, 4, 5, 6, 7
NArrayListPrint(List, NSystemOut());
則通過呼叫NArrayListPrint,你會在命令列下看到以下輸出:
[8]( 0 1 2 3 4 5 6 7 )
方括號[ ]中的數值代表容器元素的個數,圓括號( )中的數值是各個元素的字串化後的(列印)數值,以空格劃分。元素的列印數值取決於你如何去編寫相關型別的Print操作(詳見13小節關於自定義型別TypeClass的介紹)。
另外,你還可以列印輸出到一段Buffer,其做法是建立一個NSTRING_BUFFER_OUT的物件,NSTRING_BUFFER_OUT是NSTREAM_OUT的實現,因此可以將該對傳遞給NArrayListPrint的InStream引數:
// String Buffer example:
NSTRING_BUFFER Buffer = NStringBufferNew(4096);
NSTRING_BUFFER_OUT BufferOut = NStringBufferOutNew(Buffer);
NArrayListPrint(List, (NSTREAM_OUT)BufferOut);
// Process buffer
NPrintf(Buffer->Chars);
對於某些比較複雜的資料結構,例如Set和Map,除了提供Print這個基於序列方式列印元素的方法外,還另外了一個功能更加強大的State方法,用於窺探資料結構內部的元素組織情況,以方便使用者觀察資料,並調整相應的鍵值函式。例如:
NHASH_SET Set = NHashSetNew(NINT);
NINT Idx = 0;
for (; Idx < 12; Idx++) {
NHashSetAdd(Set, Idx);
}
NHashSetState(Set, NSystemOut());
NRELEASE(Set);
通過State方法,你將能看到Set各元素的雜湊分佈狀況:
----
State Map: Len = 12, HashSize = 8, Multiple = 0, Sorted = 0
----
State Hash:
[0]( 0 8 )[2]
[1]( 1 9 )[2]
[2]( 2 10 )[2]
[3]( 3 11 )[2]
[4]( 4 )[1]
[5]( 5 )[1]
[6]( 6 )[1]
[7]( 7 )[1]
----
Statistics:
Hash Chain = 8, Total Bucket = 12, Max Bucket = 2, Min Bucket = 1, Average Bucket = 1.500000
對於雜湊表來說,這個功能是相當有用的;例如當你看到某部分雜湊鏈上分佈的元素十分密集,而其他雜湊鏈的元素分佈十分稀疏,則證明你當前使用的雜湊函式很不均勻,在進行雜湊插入時產生了大量的“碰撞”,導致效率低下;一旦發現這種情況,你應該重新考慮元素Hash操作中用到的演算法, 或者考慮更換鍵的型別。另外,對於二叉樹資料結構,也同樣可以利用State方法來窺探其各元素在樹中的分配是否足夠平衡:
NTREE_SET Set = NTreeSetNew(NINT);
NINT Idx = 0;
for (; Idx < 12; Idx++) {
NTreeSetAdd(Set, Idx);
}
NTreeSetState(Set, NSystemOut());
NRELEASE(Set);
其輸出為:
----
State Map: Len = 12, Multiple = 0
----
State Tree:
3
.1
..0
..2
.7
..5
...4
...6
..9
...8
...10
....11
在上面的輸出中,點的數量代表的是當前葉節點的深度,該樹的遍歷採用的是先序遍歷方法,當前列印格式是文字格式,你還要通過自頂向下的方法,將文字格式還原為樹的檢視格式;例如,根據上面的輸出,還原出來的該樹的檢視為:
NTreeMap使用的是紅黑樹結構,由此可以觀察到,紅黑樹的平衡僅僅是子樹的區域性平衡。
17 泛型演算法
Nesty對泛型演算法的支援採取了兩種形式:(1)容器繫結的及(2)容器分離的。與容器繫結的演算法,主要是考慮到資料結構的性質,例如對於一般的排序演算法而言,像Vector,ArrayList等基於動態陣列的資料結構,最高效的排序演算法應當是快速排序,但是像LinkList等,其較為高效的排序則是歸併排序;再舉個例子,像列表旋轉演算法,基於陣列的和基於連結串列的資料結構之間的實現的方法及效率也大為不同。NCollection容器集的大部分資料結構都提供了類似Sort的方法,如NVectorSort,NLinkedSetSort,NArrayMapSort等,只有少部分在演算法上不允許排序的資料結構除外,例如NHashSet/Map,NTreeSet/Map等。另外,對於列表或向量來說,也提供了類似Rotate,Scroll及Reverse等等的演算法介面,如NVectorRotate,NLinkedListScroll,NArrayListReverse等等,由於這些演算法都與其容器的型別直接相關連,因此屬於容器繫結的演算法;儘管介面相同,但其內部實現會依據資料額結構的種類而有所/大為不同。下面以幾個清晰的例子來掩飾如何使用這些演算法。 對於排序而言,Sort方式將接受一個NPfnCompare的函式指標作為比較器,該函式的定義必須符合下面的格式(以NINT為例):// Compare two integer value with greater than comparation
NBOOL CompareNINT_GT(const NVOID * In1, const NVOID * In2) {
return (NBOOL)(*(const NINT *)In1 > *(const NINT *)In2);
}
比較器的引數必須是void *型別,因為之前已經就C語言泛型探討過,void * 可以作為一個通用介面來泛化所有型別,因此在函式實現部分,需要將void指標再強制轉換為其實際型別,獲取資料,比較並返回結果。一旦定義了比較器,則可以對列表進行常規排序:
NLINKED_LIST List = NLinkedListNew(NINT);
NINT Idx = 0;
for (; Idx < 8; Idx++) {
NLinkedListAdd(List, Idx);
}
NLinkedListSort(List, CompareNINT_GT);
NLinkedListPrint(List, NSystemOut());
// Outputs:
// [8](7, 6, 5, 4, 3, 2, 1, 0)
另外,你還可以對列表進行區域性排序,例如將上面的例子修改為:
// Sort elements between index 2 and index 2 + 4
NLinkedListSortNum(List, 2, 4, CompareNINT_GT);
NLinkedListPrint(List, NSystemOut());
// Outputs:
// [8](0, 1, 5, 4, 3, 2, 6, 7)
由於Nesty容器集是基於介面設計的,例如NCollection,NList,NSet,NMap等這些都是容器的相關介面,介面所定義的操作對於所有實現類其行為是相同的,因此還可以針對各個介面層提供其他泛型演算法,由於這些演算法不與特定的容器繫結,因此屬於容器分離的。容器分離演算法主要是基於介面間某些共通的操作而實現的,例如,由於NCollection介面支援容器的Position迭代模式,基於這一模式可以實現很多有用的操作,例如Copy,Find等,下面舉幾個簡單的例子。
拷貝,下面的例子在兩個在結構上完全無關的容器間實現拷貝,因為他們都實現了NCollection的介面:
NHASH_SET Set = NHashSetNew(NINT);
NVECTOR Vec = NVectorNew(NINT);
NCollections_Copy((NCOLLECTION)Set, (const NCOLLECTION)Vec);
新增,下面的例子按指定次數重複地往序列中新增元素:
NLIST List = (NLIST)NArrayListNew(NINT);
NINT Val = 0;
NCollections_PushFirst((NCOLLECTION)List, &Val, 20);
18 Nesty容器的侷限性
在C++的模板泛型中,模板的例項化是通過編譯器在編譯時進行,並且會為每個模板引數的型別編譯一個單獨的類,因此模板類具有靜態屬性;但是,由於Nesty容器是通過void*來對型別進行泛化的,然而任何型別的地址都可以自動轉換為void*,這將導致型別識別的問題;假設現在建立了下面兩個容器:NARRAY_LIST ListOfNINT = NArrayListNew(NINT);
NARRAY_LIST ListOfFLOAT = NArrayListNew(NFLOAT);
最荒謬的情況是,使用者如果故意將一個NINT型別的引數,傳遞給一個NFLOAT型別的容器時,編譯器根本無法識別這種錯誤:
NINT Val = 10;
NArrayListAdd(ListOfFLOAT, Val);
當然,int和float的長度的是相同的,因此在拷貝資料時頂多會引起資料錯誤,如果當連著的資料長度不一致時,極有可能引發崩潰,而這種錯誤只能通過檢查程式碼才能找到,因此在使用時必須十分謹慎。
另外,由於NCollection容器集是基於NOOC的物件實現的,而物件例項其實是一個指標定義,而在C中指標是可以任意轉換的,例如程式設計師可以惡作劇地將一個List容器物件強制轉換為一個Set/Map然後再傳遞給Set/Map的方法;然而這不能完全責怪作者,因為C語言本身就是一門比較靈活的(或者說有點肆無忌憚的)程式語言;因此,在使用Nesty進行C語言開發的時候程式設計師一定要相當小心謹慎。
Nesty容器在效能上落後於C++的模板容器,其中主要原因是,TypeClass的操作都是基於函式指標實現的,因此在呼叫一個函式指標的函式時,執行的是程式碼跳轉,而不像很多C++程式碼一樣直接通過內聯的方式來實施優化;但據作者測試比對來看,可以肯定的是,C與C++容器之間的差別僅僅是在程式碼內聯上,在演算法上不存在差異。
19 在C++中使用容器
對於大部分人來說(特別是那些習慣在面向物件的環境中開發的),C泛型及TypeClass是一個難以接受的東西,為此Nesty還針對C++模板進行了封裝,但其介面和定義和C語言的容器是一模一樣的,實際上C++的版本和C語言的版本都同屬一套容器。下面是一些簡單的例子:NArrayList<NINT> List;
List.Add(3);
for (NINT Idx = 0; Idx < List.Len(); Idx++) {
NINT Val = List[Idx];
}
List.Sort<NGreater<NINT> >();
由於C++的容器僅僅是對C語言的程式碼進行了封裝,並非重新開發,因此無法發揮C++語言的優勢;在測試過程中,其效率相對低於STL;但是作者已經意識到了這個問題,目前正著力於對C++版本的容器進行全面重構和重新開發,力圖在效能上能夠趕上STL。如果有對此感興趣的朋友,請多留意Nesty的進展。