C語言下的容器及泛型編程

1 引言

衆所周知,C++語言提供了大名鼎鼎的標準模板庫(STL)作爲C++語言下的編程利器受到無數青睞,而C語言下並沒有提供類似的工具,使得C語言的開發變得尤爲困難和專業化。Nesty框架的NCollection容器爲C語言提供了一整套標準的、豐富的模板工具,用以彌補C語言在結構化編程上的弱勢。之前發表的一篇文章是關於如何在C語言下進行面向對象編程的,主要介紹了NOOC的諸多特性。然而,NOOC真正的作用是爲NCollection建立基礎。本文還是以一些簡單的代碼作爲例子,介紹NCollection的使用方法,通過閱讀本節的內容,你將瞭解爲什麼NCollection可以大大地加快C語言下開發的效率。

2 迭代模型

作者所指的迭代模型,是指對謀一同類數據進行遍歷時所使用的方法,從設計模式的角度來看,迭代模式甚至包含了訪問者模式。以Nesty的開發爲例,NCollection整套框架的設計都是圍繞其迭代模型展開的,因爲迭代模型涉及到是否能爲所有容器提供一套統一的訪問標準。在Nesty的容器框架下,迭代模型通常包含了兩種基本形式:(1)基於索引的;(2)基於位置(Position)的。基於索引的迭代模式比較常用和易於理解,我們通常利用索引來遍歷數組;基於位置的迭代模式會更加抽象一些,其工作方式類似於數組索引,在Nesty中通過定義一個Position數據結構來維持元素在各自容器中的某個“位置”。爲了方便對比,下例例舉STL容器中的迭代器,以及Nesty容器定義的索引迭代模式及Position迭代模式的代碼:

示例(1),索引迭代模式:
// 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) MatchCompareHash等操作會執行相應的內存比較和哈希:
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的進展。

20 結束語

NCollection容器集以其豐富的陣容構成了Nesty框架中相對獨立的模塊,其作用是爲C語言提供一套規範的動態數據結構,以簡化在C下從事算法開發的難度。NCollection容器涵蓋的內容十分龐大,本文僅從實用角度篩選了部分比較常用的,且具有理論性質的內容加以講解,爲的是讓對此感興趣的朋友能夠了解Nesty容器的設計思想及理念。相信從事過算法開發的朋友都知道,要想在C語言環境下開發動態數據結構是相對困難且專業的工作,大多數情況下,程序員需要單獨去管理各個元素的內存,這不但增加了編程的難度,也容易引發錯誤。Nesty容器通過void*類型及TypeClass協議泛化了所有類型的操作,因此能夠達“到同一套編程接口對所有類型都適用”的目的。因此Nesty容器的代碼可以高度地被重用,程序員只需要根據容器所持有類型的特性,實現相應的TypeClass操作。而Nesty容器基於NOOC對象模型實現了部分面向對象的功能,因此才能使容器基於接口編程;而更令人激動的是,通過在容器的接口層,引入Position迭代模式,統一了容器對元素實施遍歷,插入,查找等的操作,爲實現泛型算法打下良好的基礎。容器接口的定義是精簡的,有好的,通過閱讀本文的代碼,你將發現,在C語言下面使用Nesty容器將不比在C++中使用模板容器需要編寫過多的代碼。然而,Nesty容器依然有其侷限性,由於C語言泛型不像C++模板能夠在編譯時實例化模板類,C泛型更多依賴對void*及函數指針的操作,因此引發了類型識別及性能等問題;但是,只要從事開發的程序員正確使用,Nesty容器就C語言開發來說依然是相當高效且易用的。另外,本文僅僅是介紹少部分的功能,更多的功能需要感興趣的朋友從代碼及實例中去探討,Nesty的工程有大量測試代碼來供你學習和研究。如果你對此感興趣並有所想法,請不妨留下你的寶貴意見,作者熱切盼望得到你的反饋。Nesty是跨平臺的,開源的軟件,其下載站點爲點擊打開鏈接




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章