深入理解void型別
1.空指標
一般來說,程式的起始地址是從“程式碼區”的0地址開始存放的(注:如果插入一個記憶體分佈圖,則更能說明問題,此處省略),但實際上現代作業系統並非如此,卻保留了從0開始的一塊記憶體。至於這塊記憶體到底有有多大,與具體的作業系統有關。如果程式試圖訪問這塊記憶體,則系統提示異常。
為什麼作業系統不是保留一個位元組呢?由於記憶體管理是按頁來進行的,因此無法做到單獨保留一個位元組。儘管如此,但還是有極少數系統設定RAM區從0地址開始,但指向有效變數的指標不會指向0地址。即使“程式碼區”從0地址開始,但在任何情況下,0地址都不是C語言中任何函式的起始地址,因此指向有效函式地址的指標也不會指向
☛ 課外知識延伸
雖然 80C51微控制器XDATA區(外部RAM)是從0地址開始的,但只要對儲存在0地址中的變數不進行取地址操作(&操作),即可有效地保證指標不會指向0地址。
與此同時,雖然32位ARM7微控制器也是從0地址開始的,但這塊記憶體僅用於存放中斷向量程式碼,而不是程式中的有效變數地址,因此即便用空指標來判斷指標的有效性,其仍然是可行的。
基於此,於是將空指標定義為指向0地址的指標。毫無疑問,任何一種指標型別都有一個特殊的指標值,即空指標。它既不會指向任何物件或函式,也不是任何物件或函式的地址。而未初始化的指標,則完全可能指向任何地方。
由此可見,空指標與未初始化的指標是完全不同的兩個概念。那麼,將如何在程式中獲得一個空指標呢?
2. 空指標常量與NULL
標準C規定,在初始化、賦值或比較時,如果一邊是變數或指標型別的表示式,則編譯器可以確定另一邊的常數0為空指標,並生成正確的空指標值。即在指標上下文中“值為0的整型常量表達式”在編譯時轉換為空指標。
為了讓程式中的空指標使用更加明確,標準C專門定義了一個標準預處理巨集NULL,其值為“空指標常量”,通常為0或(void *)0,即在指標上下文中NULL與0是等價的,而未加修飾的0也是完全可以接受的。由於void *指標的特殊賦值屬性,比如:
#define NULL ((void *)0)
當NULL定義為((void *)0)時,即NULL是可以賦值給任何型別指標的值,它的型別為void*,而不是整數0,因此初始化“FILE *fp = NULL;”是完全合法的。
而為了區分整數0和空指標0,當需要其它型別的0的時候,即使可能工作,但也不能使用NULL,如果這樣處理其格式是錯誤的,這在非指標上下文中是不能工作的。特別地,不能在需要ASCII空字元(NUL)的地方使用NULL。如果確實需要,則可以自定義為:
#define NUL ''
由此可見,常數0是一個空指標常量,而NULL僅僅是它的一個別名。
3. 空指標的用途
一般來說,未初始化是不能使用的非法指標,因為它完全有可能指向任何地方,從而導致程式無法判斷它為非法指標。因此,不管指標變數是全域性的還是區域性的、靜態的還是非靜態的,都應該在宣告它的同時進行初始化,要麼賦予一個有效的地址,要麼賦予NULL。
標準C規定,全域性指標變數的預設值為NULL,而對於區域性指標變數則必須明確地指定其初值。因此,void通常用於指標變數的初始化,用來判斷一個指標的有效性。比如:
unsigned char *pucBuf=(void *)0;// 定義pucBuf為unsigned char型別指標並初始化為空指標
如果後續的程式碼忘記初始化指標而直接使用的話,則可能造成程式失敗。雖然空指標也是非法指標,但可以通過程式判斷並告訴程式設計師程式碼可能有問題。也就是說,如果一開始就將指標初始化為空指標,則可避免程式異常。比如:
if(pucBuf==0){
return error; // 如果pucBuf為空指標,則返回引數錯誤
}
由於void型別指標的不確定性,因此它可以指向任意型別的資料,那麼只要在使用時做一個簡單的強制型別轉換就可以了。比如:
unsignned char*pcData = NULL;// 定義pcData為unsigned char型別指標
void*pvData;// 定義pvData為void型別指標
pvData = pcData;// 無需進行強制型別轉換
pcData = (unsigned char*) pvData;// 將pvData強制轉換為unsigned char型別指標
顯然不存在void型別的物件,也就是說,當物件為空型別時,其大小為0位元組;當物件未確定型別時,那麼它的大小也是未確定的,因此不能宣告void型別變數。比如:
void a; // 非法宣告
既然上述宣告是非法的,那麼,也就不能將sizeof運算子用於void型別。也就意味著,編譯器不知道所指物件的大小,由於指標的算術運算總是基於所指物件的大小的,因此不允許對void指標進行算術運算。
總之,在指標宣告中,void *表示通用指標的型別。如果void作為函式的返回型別,則表示不返回任何值。如果void位於引數列表中,則表示沒有引數。
4. 用無型別指標作為函式引數
由於C語言中最小長度的變數為char型別(包括unsigned char、signed char等),其sizeof(char)的結果為1,而其它任何變數的長度都是它的整數倍。比如,如果使用SDCC51編譯器,其sizeof(int)為2。因為通用swap函式函式不知道需要交換的變數的型別,所以需要一個引數給出相應的指示。由於C語言的變數型別多種多樣,因此不可能為每一種變數型別編號,而且swap並不關心變數的真正型別,所以可以用變數的長度代替變數型別。通用swap函式的原型為:
void swap(void *pvData1, void *pvData2, int iDataSize)
將a,b兩個變數(變數型別必須一樣)的值交換的程式碼如下:
swap(&a, &b, sizeof(a));
通用swap排序函式的參考程式碼見程式清單1.1。
程式清單1.1 通用swap排序函式
1 void swap (void *pvData1, void *pvData2, int iDataSize)
2 {
3 unsigned char *pcData1 = NULL;
4 unsigned char *pcData2 = NULL;
5 unsigned char ucTmp1;
6
7 pcData1 = (unsigned char *)pvData1;
8 pcData2 = (unsigned char *)pvData2;
9
10 do {
11 ucTmp1 = *pcData1;
12 *pcData1 = *pcData2;
13 *pcData2 = ucTmp1;
14 pcData1++;
15 pcData2++;
16 } while (--iDataSize > 0);
17 }