空指針常量
一個表示0值的整數(shù)常量,叫做空指針常量。例如:0、0L、1-1(它們都是值為0的整數(shù)常量表達式)以及(void*)0、void* NULL 都是空指針常量,空指針常量可以賦值給任何指針類型,因為它是變體類型(void*)。但是我們更傾向于使用NULL表示這個空指針常量。對于其它方式(比如0)來表示空指針常量雖然不會產(chǎn)生任何問題,但是在根本意義上并不符合空指針常量的定義。因為空指針常量的存在意義還在強調(diào)它并不指向任何對象(后面會講細節(jié))。
空指針
空指針不指向任何實際的對象或者函數(shù)。反過來說,任何對象或者函數(shù)的地址都不可能是空指針。
空指針是一個特殊的指針,因為這個指針不指向任何地方。這意味任何一個有效的指針如果和空指針進行相等的比較運算時,結(jié)果都是false。
在程序中,得到一個空指針最直接的方法就是運用預定義的NULL,這個值在多個頭文件中都有定義。
如果要初始化一個空指針,我們可以這樣,
校驗一個指針是否為一個有效指針時,我們更傾向于使用這種方式
為什么有人會用if(ip)這種方式校驗一個指針非空,而且在C++中不會出現(xiàn)錯誤呢?而且現(xiàn)在很多人都會這樣寫。 原因是這樣的,
- // Define NULL pointer value
- #ifndef NULL
- # ifdef __cplusplus
- # define NULL 0
- # else
- # define NULL ((void *)0)
- # endif
- #endif // NULL
在現(xiàn)在的C/C++中定義的NULL即為0,而C++中的true為≠0,所以此時的if(ip)和if(ip != NULL)是等效的。
NULL指針
NULL是一個標準規(guī)定的宏定義,用來表示空指針常量。在C++里面被直接定義成了整數(shù)立即數(shù)的0,而在沒有__cplusplus定義的前提下,就被定義成一個值是0的 void* 類型的指針常量
零指針
零值指針,是值為0的指針,可以是任何一種類型的指針,可以是通用變體類型 void*,也可以是 char*, int* 等等。
在C++里面,任何一個概念都以一種語言內(nèi)存公認的形式表現(xiàn)出來,例如std::vector會提供一個empty()子函數(shù)來返回容器是否為空,然而對于一個基本數(shù)值類型(或者說只是一個類似整數(shù)類型的類型)我們不可能將其抽象成一個類(當然除了auto_ptr等智能指針)來提供其詳細的狀態(tài)說明,所以我們需要一個特殊值來做為這種狀態(tài)的表現(xiàn)。
C++標準規(guī)定,當一個指針類型的數(shù)值是0時,認為這個指針是空的。(我們在其它的標準下或許可以使用其它的特殊值來定義我們需要的NULL實現(xiàn),可以是1,可以是2,是隨實現(xiàn)要求而定的,但是在標準C++下面我們用0來實現(xiàn)NULL指針)
空指針指向內(nèi)存的什么地方
標準并沒有對空指針指向內(nèi)存中的什么地方這一問題作出規(guī)定,也就是說用哪個具體地址值表示空指針取決于系統(tǒng)實現(xiàn)。我們常見的空指針一般指向0地址,即空指針的內(nèi)部用全0來表示(zero null pointer,零空指針);也有一些系統(tǒng)用一些特殊的地址值或者特殊的方式表示空指針(nonzero null pointer,非零空指針),具體參見 C FAQ。
在實現(xiàn)編程中不需要了解在我們的系統(tǒng)上空指針到底是一個zero null pointer還是 nonzero null pointer,我們只需要了解一個指針是否是空指針就可以了——編譯器會自動實現(xiàn)其中的轉(zhuǎn)換,為我們屏蔽其中的實現(xiàn)細節(jié)。注意:不要把空指針的內(nèi)部實現(xiàn)表示等同于整數(shù)0的對象表示——如上所述,有時它們是不同的。
對空指針實現(xiàn)的保護政策
邏輯地址和物理地址
既然我們選擇了0作為空的概念。在非法訪問空的時候我們需要保護以及報錯。因此,編譯器和系統(tǒng)提供了很好的政策。
我們程序中的指針其實是windows內(nèi)存段偏移后的地址,而不是實際的物理地址,所以不同的地址中的零值指針指向的同一個0地址,其實在內(nèi)存中都不是物理內(nèi)存的開端的0,而是分段內(nèi)存的開端,這里我們需要簡單介紹一下windows下的內(nèi)存分配和管理制度:
windows下,執(zhí)行文件(PE文件)在被調(diào)用后,系統(tǒng)會分配給它一個額定大小的內(nèi)存段用于映射這個程序的所有內(nèi)容(就是磁盤上的內(nèi)容)并且為這個段進行新的偏移計算,也就是說我們的程序中訪問的所有near指針都是在我們“自家”的段里面的,當我們需要訪問far指針的時候,我們其實是跳出了“自家的院子”到了他人的地方,我們需要一個段偏移資質(zhì)來完成新的偏移(人家家里的偏移)所以我們的指針可能是OE02:0045就是告訴我們要訪問0E02個內(nèi)存段的0045號偏移,然后windows會自動給我們找到0E02段的開始偏移,然后為我們計算真實的物理地址。
所以程序A中的零值指針和程序B中的零值指針指向的地方可能是完全不同的。
空指針賦值分區(qū)
這一分區(qū)是進程的地址空間中從0x00000000 到 0x0000FFFF 的閉區(qū)間(64K 的內(nèi)存大小),這 64K 的內(nèi)存是一塊保留內(nèi)存,不能被程序動態(tài)內(nèi)存分配器分配,不能訪問,也不能使用,保留該分區(qū)的目的是為了幫助程序員捕獲對空指針的賦值。如果進程中的線程試圖讀取或者寫入位于這一分區(qū)內(nèi)的內(nèi)存地址,就會引發(fā)訪問違規(guī)。
為什么空指針訪問會出現(xiàn)異常
歸根結(jié)底,程序中所使用的數(shù)據(jù)都需要從物理設(shè)備上獲取,即程序中的數(shù)據(jù)需要從一個真實的物理地址中讀取或者寫入。所以當一個指針的邏輯地址可以通過計算能夠準確無誤的映射到一個正確的物理地址上時,這時候數(shù)據(jù)的訪問就是正確的,程序的執(zhí)行也沒有任何問題。如果一個指針為空指針,那么該指針所指向的邏輯地址空間位于空指針賦值分區(qū)的區(qū)間上??罩羔樫x值分區(qū)上的邏輯地址沒有物理存儲器與之對應(yīng),因而訪問時就會產(chǎn)生違規(guī)訪問的異常。
野指針
野指針不是空指針,是一個指向垃圾內(nèi)存的指針。
形成原因
1.指針變量沒有被初始化。
任何指針變量被剛創(chuàng)建時不會被自動初始化為NULL指針,它的缺省值是隨機的。所以,指針變量在創(chuàng)建的同時應(yīng)當被初始化,要么將指針設(shè)置為NULL,要么讓它指向合法的內(nèi)存。例如:
- char* p = NULL;
- char* str = (char*)malloc(1024);
2.指針被free或者delete之后,沒有設(shè)置為NULL,讓人誤以為這是一個合法指針。
free和delete只是把指針所指向的內(nèi)存給釋放掉,但并沒有把指針本身給清理掉。這時候的指針依然指向原來的位置,只不過這個位置的內(nèi)存數(shù)據(jù)已經(jīng)被毀尸滅跡,此時的這個指針指向的內(nèi)存就是一個垃圾內(nèi)存。但是此時的指針由于并不是一個NULL指針(在沒有置為NULL的前提下),在做如下指針校驗的時候會逃過校驗,此時的p不是一個NULL指針,也不指向一個合法的內(nèi)存塊,造成會面程序中指針訪問的失敗。
3.指針操作超越了變量的作用范圍。
由于C/C++中指針有++操作,因而在執(zhí)行該操作的時候,稍有不慎,就容易指針訪問越界,訪問了一個不該訪問的內(nèi)存,結(jié)果程序崩潰
另一種情況是指針指向一個臨時變量的引用,當該變量被釋放時,此時的指針就變成了一個野指針,如下
- A *p; // A為一個自定義對象
- {
- A a;
- p = &a; // 注意 a 的生命期 ,只在這個程序塊中(花括號里面的兩行),而不是整個test函數(shù)
- }
- p->Func(); // p是“野指針”
注:本文參照點擊打開鏈接