void 指標是一種特殊的指標,表示為“無型別指標”,在 ANSI C 中使用它來代替“char*”作為通用指標的型別。由於 void 指標沒有特定的型別,因此它可以指向任何型別的資料。也就是說,任何型別的指標都可以直接賦值給 void 指標,而無需進行其他相關的強制型別轉換,如下面的範例程式碼所示:
void *p1;
int *p2;
…
p1 = p2;
雖然如此,但這並不意味著可以無需任何強制型別轉換就將 void 指標直接賦給其他型別的指標,因為“空型別”可以包容“有型別”,而“有型別”則不能包容“空型別”。正如我們可以說“男人和女人都是人”,但不能說“人是男人”或者“人是女人”一樣。因此,下面的範例程式碼將編譯出錯,如果在 VC++2010 中,將提示“a value of type"void*"cannot be assigned to an entity of type"int*"”的錯誤資訊。
void *p1;
int *p2;
…
p2 = p1;
由此可見,要將 void 指標賦值給其他型別的指標,必須進行強制型別轉換。如下面的範例程式碼所示:
void *p1;
int *p2;
…
p2 = (int*)p1;
避免對void指標進行算術操作
ANSI C 標準規定,進行演算法操作的指標必須確定知道其指向資料型別大小,也就是說必須知道記憶體目的地址的確切值。如下面的範例程式碼所示:
char a[20]="qwertyuiopasdfghjkl";
int *p=(int *)a;
p++;
printf("%s", p);
在上面的範例程式碼中,指標變數 p 的型別是“int*”,指向的型別是 int,被初始化為指向整型變數 a。
在執行語句“p++”時,編譯器是這樣處理的:把指標 p 的值加上了“sizeof(int)”(由於在 32 位系統中,int 佔 4 位元組,所以這裡是被加上了 4),即 p 所指向的地址由原來的變數 a 的地址向高地址方向增加了 4 位元組。但又由於 char 型別的長度是一個位元組,所以語句“printf("%s",p)”將輸出“tyuiopasdfghjkl”。
而對於 void 指標,編譯器並不知道所指物件的大小,所以對 void 指標進行算術操作都是不合法的,如下面的範例程式碼所示:
void * p;
p++; // ANSI:錯誤
p+= 1; // ANSI:錯誤
上面的程式碼在 VC++2010 中將提示“expression must be a pointer to a complete object type”的錯誤資訊。
但值得注意的是,GNU 則不這麼認為,它指定“void*”的演算法操作與“char*”一致。因此下列語句在 GNU 編譯器中都是正確的:
void * p;
p++; // GUN:正確
p+=1; // GUN:正確
下面的範例程式碼演示了在 GCC 中執行對 void 指標的自增操作:
#include <stdio.h>
int main(void)
{
void * p="ILoveC";
p++;
printf("%sn", p);
}
執行結果為:
LoveC
由此可見,GNU 和 ANSI 還存在著一些區別,相比之下,GNU 較 ANSI 更“開放”,提供了對更多語法的支援。但是在真實的設計環境中,還是應該盡可能符合 ANSI 標準,盡量避免對 void 指標進行算術操作。
如果函數的引數可以是任意型別指標,應該將其引數宣告為 void*
前面提到,void 指標可以指向任意型別的資料,同時任何型別的指標都可以直接賦值給 void 指標,而無需進行其他相關的強制型別轉換。因此,在程式設計中,如果函數的引數可以是任意型別指標,那麼應該使用 void 指標作為函數的形參,這樣函數就可以接受任意資料型別的指標作為引數。
比較典型的函數有記憶體操作函數 memcpy 和 memset,如下面的程式碼所示:
void *memset(void *buffer, int b, size_t size)
{
assert(buffer!=NULL);
char* retAddr = (char*)buffer;
while (size--> 0)
{
*(retAddr++) = (char)b;
}
return retAddr;
}
void *memcpy (void *dst, const void *src, size_t size)
{
assert((dst!=NULL) && (src!=NULL));
char *temp_dest = (char *)dst;
char *temp_src = (char *)src;
char* retAddr = temp_dest;
size_t i = 0;
/* 解決資料區重疊問題*/
if ((retAddr>temp_src) && (retAddr<(temp_src+size)))
{
for (i=size-1; i>=0; i--)
{
*(temp_dest++) = *(temp_src++);
}
}
else
{
for (i=0; i<size; i++)
{
*(temp_dest++) = *(temp_src++);
}
}
*(retAddr+size)='';
return retAddr;
}
這樣,任何型別的指標都可以傳入 memcpy 函數和 memset 函數中,這也真實地體現了記憶體操作函數的意義,因為它操作的物件僅僅是一片記憶體,而不論這片記憶體是什麼型別。memcpy 函數的呼叫範例如下面的程式碼所示:
char buf[]="abcdefg";
// buf+2(從c開始,長度3個,即cde)
memcpy(buf, buf+2 ,3);
printf("%sn", buf);
或者進行如下形式的呼叫:
int dst[100];
int src[100];
memcpy(dst, src, 100*sizeof(int));
因為引數型別是 void*,所以上面的呼叫都是正確的。現在假設 memcpy 函數的引數型別不是 void*,而是 char*,如下面的程式碼所示:
char *memcpy(char* dst, const char* src, size_t size)
{
assert((dst !=NULL) && (src != NULL));
char *retAddr = dst;
size_t i = 0;
if ((retAddr>src) && (retAddr<(src+size)))
{
for (i=size-1; i>=0; i--)
{
*(dst++)= *(src++);
}
}
else
{
for (i=0; i<size; i++)
{
*(dst++) = *(src++);
}
}
*(retAddr+size)='';
return retAddr;
}
現在繼續執行如下形式的呼叫:
int dst[100];
int src[100];
memcpy(dst, src, 100*sizeof(int));
由於型別不匹配,編譯器就會報錯,如圖 1 所示。
圖 1