歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> NULL指針、零指針、野指針

NULL指針、零指針、野指針

日期:2017/3/1 9:37:23   编辑:Linux編程

1. 空指針、NULL指針、零指針

1.1什麼是空指針常量

0、0L、'\0'、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 = '\0'; 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指針

// Define NULL pointer value
#ifndef NULL
# ifdef __cplusplus
# define NULL 0
# else
# define NULL ((void *)0)
# endif
#endif // 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,要麼讓它指向合法的內存。例如:
char *p = NULL;
char *str = (char *) malloc(100);

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

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

char *p = (char *) malloc(100);
strcpy(p, “hello”);
free(p); // p 所指的內存被釋放,但是p所指的地址仍然不變

if(p != NULL) // 沒有起到防錯作用
{
strcpy(p, “world”); // 出錯
}

3)指針操作超越了變量的作用范圍。這種情況讓人防不勝防,示例程序如下:

class A
{
public:
void Func(void){ cout << “Func of class A” << endl; }
};
void Test(void)
{
A *p;
{
A a;
p = &a; // 注意 a 的生命期 ,只在這個程序塊中(花括號裡面的兩行),而不是整個test函數
}
p->Func(); // p是“野指針”
}

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

C++ 設計新思維》 下載見 http://www.linuxidc.com/Linux/2014-07/104850.htm

C++ Primer Plus 第6版 中文版 清晰有書簽PDF+源代碼 http://www.linuxidc.com/Linux/2014-05/101227.htm

讀C++ Primer 之構造函數陷阱 http://www.linuxidc.com/Linux/2011-08/40176.htm

讀C++ Primer 之智能指針 http://www.linuxidc.com/Linux/2011-08/40177.htm

讀C++ Primer 之句柄類 http://www.linuxidc.com/Linux/2011-08/40175.htm

將C語言梳理一下,分布在以下10個章節中:

  1. Linux-C成長之路(一):Linux下C編程概要 http://www.linuxidc.com/Linux/2014-05/101242.htm
  2. Linux-C成長之路(二):基本數據類型 http://www.linuxidc.com/Linux/2014-05/101242p2.htm
  3. Linux-C成長之路(三):基本IO函數操作 http://www.linuxidc.com/Linux/2014-05/101242p3.htm
  4. Linux-C成長之路(四):運算符 http://www.linuxidc.com/Linux/2014-05/101242p4.htm
  5. Linux-C成長之路(五):控制流 http://www.linuxidc.com/Linux/2014-05/101242p5.htm
  6. Linux-C成長之路(六):函數要義 http://www.linuxidc.com/Linux/2014-05/101242p6.htm
  7. Linux-C成長之路(七):數組與指針 http://www.linuxidc.com/Linux/2014-05/101242p7.htm
  8. Linux-C成長之路(八):存儲類,動態內存 http://www.linuxidc.com/Linux/2014-05/101242p8.htm
  9. Linux-C成長之路(九):復合數據類型 http://www.linuxidc.com/Linux/2014-05/101242p9.htm
  10. Linux-C成長之路(十):其他高級議題

Copyright © Linux教程網 All Rights Reserved