NULL 指標、零指標、野指標

發表於2016-10-19

1. 空指標、NULL指標、零指標

1.1什麼是空指標常量

0、0L、”、3 – 3、0 * 17 (它們都是“integer constant expression”)以及 (void*)0 (我覺得(void*)0應該算是一個空指標吧,更恰當一點)等都是空指標常量(注意 (char*) 0 不叫空指標常量,只是一個空指標值)。至於系統選取哪種形式作為空指標常量使用,則是實現相關的。一般的 C 系統選擇 (void*)0 或者 0 的居多(也有個別的選擇 0L);至於 C++ 系統,由於存在嚴格的型別轉化的要求,void* 不能象 C 中那樣自由轉換為其它指標型別,所以通常選 0 作為空指標常量(C++標準推薦),而不選擇 (void*)0。

1.2 什麼是空指標

如果 p 是一個指標變數,則 p = 0; p = 0L; p = ”; p = 3 – 3; p = 0 * 17; 中的任何一種賦值操作之後(對於 C 來說還可以是 p = (void*)0;), p 都成為一個空指標,由系統保證空指標不指向任何實際的物件或者函式。反過來說,任何物件或者函式的地址都不可能是空指標。(比如這裡的(void*)0就是一個空指標。把它理解為null pointer還是null pointer constant會有微秒的不同,當然也不是緊要了)。其實空指標只是一種程式設計概念,就如一個容器可能有空和非空兩種基本狀態。

1.3 NULL 指標

NULL 是一個標準規定的巨集定義,用來表示空指標常量。因此,除了上面的各種賦值方式之外,還可以用 p = NULL; 來使 p 成為一個空指標。

(很多系統中的實現:#define NULL (void*)0,與這裡的“a null pointer constant”並不是完全一致的)

C++標準庫定義的NULL指標

NULL是一個巨集,在C++裡面被直接被定義成了整數立即數型別的0,而在沒有__cplusplus定義的前提下,就被定義成一個值是0的void   *型別指標常量。

1.4 零指標

零值指標,是值為0的指標,可以是任何一種指標型別,可以是通用變體型別void*,也可以是char*,int*等等。

在C++裡面,任何一個概念都要以一種語言記憶體公認的形式表現出來,例如std::vector會提供一個empty()子函式來返回容器是否為空,然而對於一個基本數值型別(或者說只是一個類似整數型別的型別)我們不可能將其抽象成一個類(當然除了auto_ptr等只能指標)來提供其詳細的狀態說明,所以我們需要一個特殊值來做為這種狀態的表現。
C++標準規定,當一個指標型別的數值是0時,認為這個指標是空的。(我們在其他的標準下或許可以使用其他的特殊值來定義我們需要的NULL實現,可以是1,可以是2,是隨實現要求而定的,但是在標準C++下面我們用0來實現NULL指標)

1.5 空指標向了記憶體的什麼地方(空指標的內部實現)?

標準並沒有對空指標指向記憶體中的什麼地方這一個問題作出規定,也就是說用哪個具體的地址值(0x0 地址還是某一特定地址)表示空指標取決於系統的實現。我們常見的空指標一般指向 0 地址,即空指標的內部用全 0 來表示(zero null pointer,零空指標);也有一些系統用一些特殊的地址值或者特殊的方式表示空指標(nonzero null pointer,非零空指標),具體請參見C FAQ。

在實際程式設計中不需要了解在我們的系統上空指標到底是一個 zero null pointer 還是 nonzero null pointer,我們只需要瞭解一個指標是否是空指標就可以了——編譯器會自動實現其中的轉換,為我們遮蔽其中的實現細節。注意:不要把空指標的內部表示等同於整數 0 的物件表示——如上所述,有時它們是不同的。

1.6 對空指標實現的保護政策

既然我們選擇了0作為空的概念,在非法訪問空的時候我們需要保護以及報錯。因此,編譯器和系統提供了很好的政策。

我們程式中的指標其實是WINDOWS記憶體段偏移後的地址,而不是實際的實體地址,所以不同的程式中的零值指標指向的同一個0地址,其實在記憶體中都不是實體記憶體的開端的0,而是分段的記憶體的開端,這裡我們需要簡單介紹一下WINDOWS下的記憶體分配和管理制度:

WINDOWS下,執行檔案(PE檔案)在被呼叫後,系統會分配給它一個額定大小的記憶體段用於對映這個程式的所有內容(就是磁碟上的內容)並且為這個段進行新的偏移計算,也就是說我們的程式中訪問的所有NEAR指標都是在我們“自家”的段裡面的,當我們要訪問FAR指標的時候,我們其實是跳出了“自家的院子”到了他人的地方,我們需要一個段偏移地址來完成新的偏移(人家家裡的偏移)所以我們的指標可能是OE02:0045就是告訴系統我們要訪問0E02個記憶體段的0045好偏移,然後WINDOWS會自動給我們找到0E02段的開始偏移,然後為我們計算真實的實體地址。

所以程式A中的零值指標和程式B中的零值指標指向的地方可能是完全不同的。

保護政策:

我們的程式在使用的是系統給定的一個段,程式中的零值指標指向這個段的開端,為了保證NULL概念,系統為我們這個段的開頭64K記憶體做了苛刻的規定,根據虛擬記憶體訪問許可權控制,我們程式中(低訪問許可權)訪問要求高訪問許可權的這64K記憶體被視作是不容許的,所以會必然引發Access Volitation 錯誤,而這高許可權的64K記憶體是一塊保留記憶體(即不能被程式動態記憶體分配器分配,不能被訪問,也不能被使用),就是簡單的保留,不作任何使用。

我們在直接定義一個指標後並不知道這個指標指向何處(而不是有些程式設計師認為的如同JAVA等語言會自動零值初始化),所以我們一旦非法地直接訪問這些未知地內容時,極其有可能會觸碰到程式所不能觸碰地記憶體(這時類似64K限制地保護政策又會起效,就如同你不僅隨意闖入了陌生人的家(野指標),而且拿著刀子要問他要錢(訪問),警察(WINDOWS記憶體訪問保護政策)當然請你去警察局(報錯)談談),所以養成良好的指標初始化(賦值為NULL)以及使用FREE(或者時DELETE)之後立即再初始化為空是十分必要的!  

1.7 為什麼通過空指標讀寫的時候就會出現異常?

NULL指標分配的分割槽:其範圍是從 0x00000000到0x0000FFFF。這段空間是空閒的,對於空閒的空間而言,沒有相應的物理儲存器與之相對應,所以對這段空間來說,任何讀寫操作都是會引起異常的。空指標是程式無論在何時都沒有物理儲存器與之對應的地址。為了保障“無論何時”這個條件,需要人為劃分一個空指標的區域,固有上面NULL指標分割槽。

1.8 是否可以定義自己的 NULL 的實現?

NULL 是標準庫中的一個reserved identifier (保留識別符號)。所以,如果包含了相應的標準標頭檔案而引入了 NULL 的話,則再在程式中重新定義 NULL 為不同的內容是非法的,其行為是未定義的。也就是說,如果是符合標準的程式,其 NULL 的值只能是 0,不可能是除 0 之外的其它值,比如 1、2、3 等。

1.9 malloc 函式在分配記憶體失敗時返回 0 還是 NULL?

malloc 函式是標準 C 規定的庫函式。在標準中明確規定了在其記憶體分配失敗時返回的是一個 “null pointer”(空指標)。對於空指標值,一般的文件(比如 man)中傾向於用 NULL 表示,而沒有直接說成 0。但是我們應該清楚:對於指標型別來說,返回 NULL 和 返回 0 是完全等價的,因為 NULL 和 0 都表示 “null pointer”(空指標)。(一般系統中手冊中都返回NULL)

C++裡面的NEW再記憶體失敗是會丟擲一個BAD_ALLOC異常。

2. 野指標

“野指標”不是NULL指標,是指向“垃圾”記憶體的指標。

2.1 “野指標”的成因主要有兩種

1)指標變數沒有被初始化。任何指標變數剛被建立時不會自動成為NULL指標,它的預設值是隨機的,它會亂指一氣。所以,指標變數在建立的同時應當被初始化,要麼將指標設定為NULL,要麼讓它指向合法的記憶體。例如:

2)指標p被free或者delete之後,沒有置為NULL,讓人誤以為p是個合法的指標。

free和delete只是把指標所指的記憶體給釋放掉,但並沒有把指標本身幹掉。free以後其地址仍然不變(非NULL),只是該地址對應的記憶體是垃圾,p成了“野指標”。如果此時不把p設定為NULL,會讓人誤以為p是個合法的指標。如果程式比較長,我們有時記不住p所指的記憶體是否已經被釋放,在繼續使用p之前,通常會用語句if (p != NULL)進行防錯處理。很遺憾,此時if語句起不到防錯作用,因為即便p不是NULL指標,它也不指向合法的記憶體塊。

3)指標操作超越了變數的作用範圍。這種情況讓人防不勝防,示例程式如下:

函式Test在執行語句p->Func()時,物件a已經消失,而p是指向a的,所以p就成了“野指標”。

相關文章