1、指針的內存佈局
先看下面的例子:
int *p;
你們都知道這裏定義了一個指針p。可是p 究竟是什麼東西呢?還記得第一章裏說過,「任何一種數據類型咱們均可以把它當一個模子」嗎?p,毫無疑問,是某個模子咔出來的。
咱們也討論過,任何模子都必須有其特定的大小,這樣才能用來「咔咔咔」。那咔出p 的這個模子究竟是什麼樣子呢?它佔多大的空間呢?如今用sizeof 測試一下(32 位系統):sizeof(p)的值爲4。嗯,這說明咔出p 的這個模子大小爲4 個byte。顯然,這個模子不是「int」,雖然它大小也爲4。既然不是「int」那就必定是「int *」了。好,那如今咱們能夠這麼理解這個定義:
一個「int *」類型的模子在內存上咔出了4 個字節的空間,而後把這個4 個字節大小的空間命名爲p,同時限定這4 個字節的空間裏面只能存儲某個內存地址,即便你存入別的任何數據,都將被看成地址處理,並且這個內存地址開始的連續4 個字節上只能存儲某個int類型的數據。
這是一段咬文嚼字的說明,咱們仍是用圖來解析一下:
如上圖所示,咱們把p 稱爲指針變量,p 裏存儲的內存地址處的內存稱爲p 所指向的內存。
指針變量p 裏存儲的任何數據都將被看成地址來處理。
咱們能夠簡單的這麼理解:一個基本的數據類型(包括結構體等自定義類型)加上「*」號就構成了一個指針類型的模子。這個模子的大小是必定的,與「*」號前面的數據類型無關。「*」號前面的數據類型只是說明指針所指向的內存裏存儲的數據類型。因此,在32 位系統下,無論什麼樣的指針類型,其大小都爲4byte。能夠測試一下sizeof(void *)。
2、「*」與防盜門的鑰匙
這裏這個「*」號怎麼理解呢?舉個例子:當你回到家門口時,你想進屋第一件事就是拿出鑰匙來開鎖。那你想一想防盜門的鎖芯是否是很像這個「*」號?你要進屋必需要用鑰匙,那你去讀寫一塊內存是否是也要一把鑰匙呢?這個「*」號就是否是就是咱們最好的鑰匙?
使用指針的時候,沒有它,你是不可能讀寫某塊內存的。
3、int *p = NULL 和*p = NULL 有什麼區別?
不少初學者都沒法分清這二者之間的區別。咱們先看下面的代碼:
int *p = NULL;
這時候咱們能夠經過編譯器查看p 的值爲0x00000000。這句代碼的意思是:定義一個指針變量p,其指向的內存裏面保存的是int 類型的數據;在定義變量p 的同時把p 的值設置爲0x00000000,而不是把*p 的值設置爲0x00000000。這個過程叫作初始化,是在編譯的時候進行的。
明白了什麼是初始化以後,再看下面的代碼:
int *p;
*p = NULL;
一樣,咱們能夠在編譯器上調試這兩行代碼。第一行代碼,定義了一個指針變量p,其指向的內存裏面保存的是int 類型的數據;可是這時候變量p 自己的值是多少不得而知,也就是說如今變量p 保存的有多是一個非法的地址。第二行代碼,給*p 賦值爲NULL,即給p指向的內存賦值爲NULL;可是因爲p 指向的內存多是非法的,因此調試的時候編譯器可能會報告一個內存訪問錯誤。這樣的話,咱們能夠把上面的代碼改寫改寫,使p 指向一塊合法的內存:
int i = 10;
int *p = &i;
*p = NULL;
在編譯器上調試一下,咱們發現p 指向的內存由原來的10 變爲0 了;而p 自己的值, 即內存地址並無改變。
通過上面的分析,相信你已經明白它們之間的區別了。不過這裏還有一個問題須要注意,也就是這個NULL。初學者每每在這裏犯錯誤。
注意NULL 就是NULL,它被宏定義爲0:
#define NULL 0
不少系統下除了有NULL外,還有NUL(Visual C++ 6.0 上提示說不認識NUL)。NUL 是ASCII碼錶的第一個字符,表示的是空字符,其ASCII 碼值爲0。其值雖然都爲0,但表示的意思徹底不同。一樣,NULL 和0 表示的意思也徹底不同。必定不要混淆。
另外還有初學者在使用NULL 的時候誤寫成null 或Null 等。這些都是不正確的,C 語言對大小寫十分敏感啊。固然,也確實有系統也定義了null,其意思也與NULL 沒有區別,可是你千萬不用使用null,這會影響你代碼的移植性。
4、如何將數值存儲到指定的內存地址
假設如今須要往內存0x12ff7c 地址上存入一個整型數0x100。咱們怎麼才能作到呢?咱們知道能夠經過一個指針向其指向的內存地址寫入數據,那麼這裏的內存地址0x12ff7c 其本質不就是一個指針嘛。因此咱們能夠用下面的方法:
int *p = (int *)0x12ff7c;
*p = 0x100;
須要注意的是將地址0x12ff7c 賦值給指針變量p 的時候必須強制轉換。至於這裏爲何選擇內存地址0x12ff7c,而不選擇別的地址,好比0xff00 等。這僅僅是爲了方便在VisualC++ 6.0 上測試而已。若是你選擇0xff00,也許在執行*p = 0x100;這條語句的時候,編譯器會報告一個內存訪問的錯誤,由於地址0xff00 處的內存你可能並無權力去訪問。既然這樣,咱們怎麼知道一個內存地址是能夠合法的被訪問呢?也就是說你怎麼知道地址0x12ff7c處的內存是能夠被訪問的呢?其實這很簡單,咱們能夠先定義一個變量i,好比:
int i = 0;
變量i 所處的內存確定是能夠被訪問的。而後在編譯器的watch 窗口上觀察&i 的值不就知道其內存地址了麼?這裏我獲得的地址是0x12ff7c,僅此而已(不一樣的編譯器可能每次給變量i 分配的內存地址不同,而恰好Visual C++ 6.0 每次都同樣)。你徹底能夠給任意一個能夠被合法訪問的地址賦值。獲得這個地址後再把「int i = 0;」這句代碼刪除。一切「罪證」銷燬得一乾二淨,簡直是作得完美無缺。
除了這樣就沒有別的辦法了嗎?未必。咱們甚至能夠直接這麼寫代碼:
*(int *)0x12ff7c = 0x100;
這行代碼其實和上面的兩行代碼沒有本質的區別。先將地址0x12ff7c 強制轉換,告訴編譯器這個地址上將存儲一個int 類型的數據;而後經過鑰匙「*」向這塊內存寫入一個數據。
上面討論了這麼多,其實其表達形式並不重要,重要的是這種思惟方式。也就是說咱們徹底有辦法給指定的某個內存地址寫入數據的。
5、編譯器的bug?
另一個有意思的現象,在Visual C++ 6.0 調試以下代碼的時候卻又發現一個古怪的問題:
int *p = (int *)0x12ff7c;
*p = NULL;
p = NULL;
在執行完第二條代碼以後,發現p 的值變爲0x00000000 了。按照我麼上一節的解釋,應該p的值不變,只是p 指向的內存被賦值爲0。難道咱們講錯了嗎?別急,再試試以下代碼:
int i = 10;
int *p = (int *)0x12ff7c;
*p = NULL;
p = NULL;
經過調試,發現這樣子的話,p 的值沒有變,而p 指向的內存的值變爲0 了。這與咱們前面講解的徹底一致。固然這裏的i 的地址恰好是0x12ff7c,但這並不能改變「*p = NULL;」這行代碼的功能。
爲了再次測試這個問題,我又調試了以下代碼:
int i = 10;
int j = 100;
int *p = (int *)0x12ff78;
*p = NULL;
p = NULL;
這裏0x12ff78 恰好就是變量j 的地址。這樣的話一切正常,可是若是把「int j = 100;」這行代碼刪除的話,又出現上述的問題了。測試到這裏我仍是不甘心,編譯器怎麼能犯這種低級錯誤呢?因而又接着進行了以下測試:
unsigned int i = 10;
//unsigned int j = 100;
unsigned int *p = (unsigned int *)0x12ff78;
*p = NULL;
p = NULL;
獲得的結果與上面徹底同樣。固然,我仍是沒有死心,又進行了以下測試:
char ch = 10;
char *p = (char *)0x12ff7c;
*p = NULL;
p = NULL;
這樣子的話,徹底正常。但當我刪除掉第一行代碼後再測試,這裏的p 的值並未變成0x00000000,而是變成了0x0012ff00,同時*p 的值變成了0。這又是怎麼回事呢?初學者是否定爲這是編譯器「良心發現」,把*p 的值改寫爲0 了。
若是你真這麼認爲,那就大錯特錯了。這裏的*p 仍是地址0x12ff7c 上的內容嗎?顯然不是,而是地址0x0012ff00 上的內容。至於0x12ff7c 爲何變成0x0012ff00,則是由於編譯器認爲這是把NULL 賦值給char 類型的內存,因此只是把指針變量p 的低地址上的一個字節賦值爲0。至於爲何是低地址,請參看前面講解過大小端模式相關內容。
測試到這裏,已經基本能夠確定這是Visual C++ 6.0 的一個bug。因此平時必定不要迷信某個編譯器,要相信本身的判斷。固然,後面還會提到一個我認爲的Visual C++ 6.0 的一個bug。還有,這個小小的例子,你是否能夠在多個編譯器上測試測試呢?
6、如何達到手中無劍、胸中也無劍的地步
噢,上面的討論一不當心就這麼多了。這裏我爲何要把這個小小的問題放到這裏長篇大論呢?我是想告訴讀者:研究問題必定要肯鑽研。千萬不要小看某一個簡單的事情,簡單的事情可能富含着不少祕密。通過這樣一番深究,相信你也有很多收穫。平時學習工做也是如此,不要小瞧任何一件簡單的事情,把簡單的事情作好也是一種偉大。勞模許振超開了幾十年的吊車,技術精到指哪打哪的地步。達到這種程度是須要花苦功夫的,幾十年如一日每天重複這件看似很簡單的事情,這不是通常人能作到的。一樣的,在《天龍八部》中,蕭峯血戰聚賢莊的時候,一套平平凡凡的太祖長拳打得虎虎生威,在場的英雄無不佩服至極,這也是其苦練的結果。咱們學習工做一樣如此,要肯下苦功夫鑽研,不要怕鑽得深,只怕鑽得不深。其實這也就是爲何同一個班的學生,水平會相差很是大的最關鍵之處。 學得好的,每每是那些捨得鑽研的學生。我平時上課教學生的毫不僅僅是知識點,更多的時候我在教他們學習和解決問題的方法。有時候這個過程遠比結論要重要的多。後面的內容,你也應該能看出來,我很是注重過程的分析,只有你真正明白了這些思考問題、解決問題的方法和過程,你才能真正立於不敗之地。全部的問題對你來講都是一個樣,沒有本質的區別。解決任何問題的辦法都一致,那就是把沒見過的、不會的問題想法設法轉換成你見過的、你會的問題;至於怎麼去轉換那就要靠你的苦學苦練了。也就是說你要達到手中無劍,胸中也無劍的地步。 固然這些只是我我的的領悟,寫在這裏但願能與君共勉。