首先聲明:這里說(shuō)的概念都是c語(yǔ)言中的,跟c++會(huì)有些許不一樣西设。
預(yù)備知識(shí)
1.空指針常量(null pointer constant)
An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant.
這里即是說(shuō)明:值為0的整型常量表達(dá)式瓣铣,或強(qiáng)制(轉(zhuǎn)換)為 void * 類(lèi)型的此類(lèi)表達(dá)式,稱(chēng)為 空指針常量 贷揽。
如0棠笑、0L、3-3禽绪、'\0'蓖救、017、(void)0等都屬于空指針常量印屁。
至于系統(tǒng)會(huì)采用哪種形式來(lái)作為空指針常量使用循捺,則是和具體實(shí)現(xiàn)相關(guān)。一般的C系統(tǒng)采用 (void *)0 或者 0 的居多雄人,也有個(gè)別采用的 0L 从橘;至于C++系統(tǒng),由于存在嚴(yán)格的類(lèi)型轉(zhuǎn)換要求础钠, void * 不能像C中自由轉(zhuǎn)換成其他指針類(lèi)型恰力,所以通常選擇 0 作為空指針常量。
把空指針常量賦給指針類(lèi)型的變量p旗吁,p就成為了一個(gè)空指針踩萎。
2.NULL值
The macro NULL is defined in <stddef.h> (and other headers) as a null pointer constant.
即NULL是一個(gè)標(biāo)準(zhǔn)規(guī)定的宏定義,用來(lái)表示空指針常量很钓。
我們找到 stddef.h 中的該宏定義:
#define NULL ((void *)0)
毫無(wú)疑問(wèn)香府,NULL就是一種空指針常量董栽。
那有個(gè)問(wèn)題,我們可以自定義NULL值嗎企孩?
實(shí)際上NULL是標(biāo)準(zhǔn)庫(kù)中的一個(gè) reserved identifier (保留標(biāo)識(shí)符) 锭碳,所以如果包含了相應(yīng)的標(biāo)準(zhǔn)頭文件引入了NULL的話(huà),再在程序中重新定義NULL為其他值(比如把NULL定義為3)就是非法的柠硕。
一工禾、空指針(null pointer)
1.空指針定義
If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer to any object or function.
通過(guò)預(yù)備知識(shí)中對(duì)于空指針常量和NULL值的講解,我們可以知道:
只要將空指針常量賦給指針類(lèi)型變量,該指針變量就是空指針坯约。
int *p;
p = 0;
p = 0L;
p = '\0';
p = 3 - 3;
p = 0 * 17;
p = (void*)0;
p = NULL;
如上所示代碼蝌蹂,經(jīng)過(guò)其中任何一種賦值操作后,p就是一個(gè)空指針肥败。而且,由系統(tǒng)保證空指針不指向任何實(shí)際的對(duì)象或函數(shù)。反過(guò)來(lái)說(shuō)就是:任何對(duì)象或者函數(shù)的地址都不可能是空指針胁编。
2.空指針的內(nèi)存指向
標(biāo)準(zhǔn)并沒(méi)有對(duì)空指針指向內(nèi)存中什么地方這一問(wèn)題做出規(guī)定。也就是說(shuō)鳞尔,具體使用 0x0地址 還是其他地址來(lái)表示空指針嬉橙,都依賴(lài)于具體系統(tǒng)的實(shí)現(xiàn)。兩種實(shí)現(xiàn)如下:
(1)零空指針(zero null pointer)
這是我們常見(jiàn)的一種實(shí)現(xiàn)寥假,即空指針的內(nèi)部用全0來(lái)表示市框。
(2)非零空指針(nonzero null pointer)
也有一些系統(tǒng)用一些特殊的地址值或者特殊的方式來(lái)表示空指針。
我們?cè)趯?shí)際編程中不需要了解我們系統(tǒng)空指針的實(shí)現(xiàn)和內(nèi)存指向糕韧,我們只需要了解一個(gè)指針是否是空指針就可以了——編譯器會(huì)自動(dòng)實(shí)現(xiàn)其中的轉(zhuǎn)換枫振,為我們屏蔽掉其中的實(shí)現(xiàn)細(xì)節(jié)。
3.空指針的使用
空指針的使用萤彩,主要就是防止野指針和防止懸垂指針粪滤。
防止野指針
int *p = NULL;
防止懸垂指針
int *p = malloc(sizeof(int));
free(p);
p = NULL; // 置空
詳細(xì)見(jiàn)下文野指針和懸垂指針。
二雀扶、野指針(wild pointer)
1.野指針概念
野指針:沒(méi)有初始化的指針
2.野指針產(chǎn)生原因
指針變量未初始化
如下程序:
int main()
{
int *p;
printf(%p", p);
return 0;
}
這里的p未被初始化杖小,它的缺省值是隨機(jī)的。
因此我們?cè)诼暶饕粋€(gè)指針變量的時(shí)候愚墓,為了防止出現(xiàn)野指針的問(wèn)題予权,可以將其初始化為NULL,即設(shè)為空指針转绷;也可以咋初始化時(shí)就將指針確定指向伟件。
如下所示:
int a = 3;
int *p = &a;
// 或者
int *p = NULL;
int *p = 0;
其中int *p = 0和int *p = NULL都比較常用。
三议经、懸垂指針(dangling pointer)
1.懸垂指針概念
懸垂指針:指向已經(jīng)被釋放的自由區(qū)內(nèi)存(free store)的指針斧账。
它和野指針的區(qū)別就在于:懸垂指針曾經(jīng)有效過(guò)谴返,現(xiàn)在失效了;但是野指針從未有效過(guò)咧织。
2.懸垂指針產(chǎn)生原因
(1)指針指向的內(nèi)存釋放之后未置空
指針指向的內(nèi)存被free或者delete釋放后嗓袱,指針的值仍然為剛剛被釋放的那塊內(nèi)存的首地址,但是此時(shí)指針已經(jīng)失去了對(duì)那塊內(nèi)存的合法訪(fǎng)問(wèn)權(quán)限习绢。
如下程序所示:
int main()
{
int *p = NULL;
p = malloc(sizeof(int));
*p = 3;
printf("Before free, p = %p, *p = %d\n", p, *p);
free(p);
/* 注意渠抹,此時(shí)p和p指向的內(nèi)容并沒(méi)有發(fā)生變化,但是free內(nèi)存后已經(jīng)失去了
對(duì)堆上那塊內(nèi)存的合法操作性 */
printf("After free, p = %p, *p = %d\n", p, *p);
return 0;
}
程序輸出:
Before free, p = 0xe7a010, *p = 3
After free, p = 0xe7a010, *p = 0
在程序執(zhí)行free(p)之后闪萄,p就是一個(gè)野指針梧却。為了避免野指針可能引起的問(wèn)題,我們應(yīng)該在free(p)之后加上:
p = NULL;
這個(gè)操作就稱(chēng)為置空败去。
(2)指向同一塊內(nèi)存多個(gè)指針之一被釋放
這種情況嚴(yán)格來(lái)講跟第一種情況是一回事兒放航,示例代碼如下:
int *p = malloc(sizeof(int));
*p = 3;
int *pd = p;
/* 當(dāng)前p和pd指向的是同一塊內(nèi)存 */
free(p);
p = NULL;
/* 釋放掉p所指向的內(nèi)存,并將p置0 */
從上述代碼可以看出圆裕,若有兩個(gè)指針指向同一個(gè)內(nèi)存广鳍,其中一個(gè)指針被free且被置空后,另一個(gè)指針卻仍然指向那塊被釋放了的內(nèi)存空間吓妆,這就成了一個(gè)懸垂指針赊时。
這是懸垂指針產(chǎn)生的一個(gè)典型示例,常見(jiàn)于實(shí)際生產(chǎn)環(huán)境中行拢。
(3)指針操作超出變量生命周期
不要返回指向棧內(nèi)存的指針或引用祖秒,因?yàn)闂?nèi)存在函數(shù)結(jié)束時(shí)會(huì)被釋放。
示例代碼如下:
// 定義一個(gè)函數(shù)
char *getString()
{
char *p = "Hello World!";
return p;
}
在函數(shù)內(nèi)部定義的字符串指針p剂陡,其指向的內(nèi)容存放在棧上狈涮,當(dāng)這個(gè)函數(shù)執(zhí)行完后退出后,這部分空間就會(huì)被釋放鸭栖,返回過(guò)去的p指針就成了懸垂指針歌馍。
四、void指針(void pointer)
1.void指針概念
void的意思是“無(wú)類(lèi)型”晕鹊,所以void指針又被稱(chēng)為“無(wú)類(lèi)型指針”松却,void指針可以指向任何類(lèi)型的數(shù)據(jù),所以void指針一般被稱(chēng)為通用指針或者泛指針溅话,也被叫做萬(wàn)能指針晓锻。
2.void指針的使用
(1)void指針變量p所指向的內(nèi)容不能通過(guò)*p去訪(fǎng)問(wèn)
如果要通過(guò)void指針去獲取它所指向的變量值時(shí)候,需要先將void指針強(qiáng)制類(lèi)型轉(zhuǎn)換成和變量名類(lèi)型想匹配的數(shù)據(jù)類(lèi)型指針后再進(jìn)行操作飞几,如下所示:
int a = 5;
void *p = &a;
printf("*p=%d\n", *p);
編譯器會(huì)報(bào)錯(cuò)砚哆,提示你:
incomplete type is not allowed.
意思就是這個(gè)類(lèi)型不完整,沒(méi)有辦法去獲取它所指向的變量值屑墨。改成:
printf("*p = %d\n", *(int *)p);
就可以了躁锁。
(2)void指針賦給其他類(lèi)型的指針
一個(gè)常見(jiàn)的使用場(chǎng)景就是:動(dòng)態(tài)內(nèi)存申請(qǐng)與釋放纷铣。
首先要明確一件事,c語(yǔ)言和c++在一些語(yǔ)法實(shí)現(xiàn)上有區(qū)別战转,這里我們說(shuō)的是c語(yǔ)言搜立。
C語(yǔ)言中,void指針賦給其他任意類(lèi)型的指針(除開(kāi)函數(shù)指針槐秧,void指針賦給函數(shù)指針下面討論)啄踊,是天經(jīng)地義的,無(wú)需手動(dòng)強(qiáng)轉(zhuǎn)刁标;其他任意類(lèi)型的指針賦給void指針颠通,也是天經(jīng)地義的。
例如:
typedef struct {
...
...
} complex_struct;
// c語(yǔ)言中的正確寫(xiě)法:
complex_struct = malloc(sizeof(complex_struct));
// c語(yǔ)言中多此一舉的寫(xiě)法:
// complex_struct = (complex_struct)malloc(sizeof(complex_struct));
如果你發(fā)現(xiàn)不手動(dòng)強(qiáng)轉(zhuǎn)命雀,編譯器出現(xiàn)warning蒜哀,請(qǐng)注意一下自己是不是忘了加malloc函數(shù)的頭文件:
#include <stdlib.h>
因?yàn)樵赾語(yǔ)言中,編譯器會(huì)把任何未定義的函數(shù)默認(rèn)返回int吏砂,因此如果我們不加stdlib.h這個(gè)頭文件,malloc函數(shù)默認(rèn)返回int乘客,所以我們看到編譯器警告狐血,如果看得不仔細(xì)的話(huà)會(huì)以為是因?yàn)闆](méi)有強(qiáng)制類(lèi)型轉(zhuǎn)換導(dǎo)致兩邊類(lèi)型不匹配,其實(shí)編譯器提示的是int *和int類(lèi)型不匹配易核,而不是int *和void *類(lèi)型不匹配匈织!(3)void指針賦給函數(shù)指針
void指針可以賦給函數(shù)指針以外的其他所有指針,malloc函數(shù)就是一個(gè)例子牡直。至于函數(shù)指針的問(wèn)題缀匕,這里來(lái)討論。
linux中有個(gè)dlsym函數(shù)碰逸,函數(shù)聲明如下:
void *dlsym(void *handle, const char *symbol);
man手冊(cè)中有個(gè)示例代碼:
int main()
{
...
void *handle;
double (*cosine)(double);
...
*(void **) (&cosine) = dlsym(handle, "cos");
...
}
這里面它想用函數(shù)指針去接收dlsym函數(shù)返回的void指針乡小,卻沒(méi)有用以下兩種更自然的、好理解的形式:
cosine = dlsym(handle, "cos");
cosine = (double (*)(double)) dlsym(handle, "cos");
這是因?yàn)镃99標(biāo)準(zhǔn)的遺漏饵史,導(dǎo)致了void指針無(wú)法直接轉(zhuǎn)換成函數(shù)指針满钟。所以它用了下面這種夸張的轉(zhuǎn)換:
*(void **) (&cosine) = dlsym(handle, "cos");
先把consine取地址變成二級(jí)指針,然后將這個(gè)強(qiáng)轉(zhuǎn)成(void **)這個(gè)void二級(jí)指針胳喷,再經(jīng)過(guò)指針運(yùn)算符*變成void一級(jí)指針湃番,這樣左右兩邊類(lèi)型就匹配了。
(4)其他類(lèi)型的指針賦給void指針
void指針可以用作泛型吭露,接收任意數(shù)據(jù)類(lèi)型指針吠撮。
void指針用于指向特定地址,而無(wú)需關(guān)心這個(gè)地址上存放著什么類(lèi)型的數(shù)據(jù)讲竿。例如常見(jiàn)的memcpy等函數(shù)就用到void*泥兰,函數(shù)原型如下:
void *memcpy(void *des, void *src, size_t n);
此處的void *des和void *src可以接收任意類(lèi)型的數(shù)據(jù)類(lèi)型指針择浊,既然是內(nèi)存拷貝,入?yún)⒕筒粦?yīng)該限制傳入什么類(lèi)型的指針逾条,邏輯上十分合理琢岩。