天天看點

C語言指針

先看下面的例子:

   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類型的資料。

這是一段咬文嚼字的說明,我們還是用圖來解析一下:

C語言指針

如上圖所示,我們把p 稱為指針變量,p 裡存儲的記憶體位址處的記憶體稱為p 所指向的記憶體。

指針變量p 裡存儲的任何資料都将被當作位址來處理。

我們可以簡單的這麼了解:一個基本的資料類型(包括結構體等自定義類型)加上“*”号就構成了一個指針類型的模子。這個模子的大小是一定的,與“*”号前面的資料類型無關。“*”号前面的資料類型隻是說明指針所指向的記憶體裡存儲的資料類型。是以,在32 位系統下,不管什麼樣的指針類型,其大小都為4byte。可以測試一下sizeof(void *)。

這裡這個“*”号怎麼了解呢?舉個例子:當你回到家門口時,你想進屋第一件事就是拿出鑰匙來開鎖。那你想想防盜門的鎖芯是不是很像這個“*”号?你要進屋必須要用鑰匙,那你去讀寫一塊記憶體是不是也要一把鑰匙呢?這個“*”号就是不是就是我們最好的鑰匙?

使用指針的時候,沒有它,你是不可能讀寫某塊記憶體的。

很多初學者都無法厘清這兩者之間的差別。我們先看下面的代碼:

   int *p = null;

這時候我們可以通過編譯器檢視p 的值為0x00000000。這句代碼的意思是:定義一個指針變量p,其指向的記憶體裡面儲存的是int 類型的資料;在定義變量p 的同時把p 的值設定為0x00000000,而不是把*p 的值設定為0x00000000。這個過程叫做初始化,是在編譯的時候進行的。

明白了什麼是初始化之後,再看下面的代碼:

   *p = null;

同樣,我們可以在編譯器上調試這兩行代碼。第一行代碼,定義了一個指針變量p,其指向的記憶體裡面儲存的是int 類型的資料;但是這時候變量p 本身的值是多少不得而知,也就是說現在變量p 儲存的有可能是一個非法的位址。第二行代碼,給*p 指派為null,即給p指向的記憶體指派為null;但是由于p 指向的記憶體可能是非法的,是以調試的時候編譯器可能會報告一個記憶體通路錯誤。這樣的話,我們可以把上面的代碼改寫改寫,使p 指向一塊合法的記憶體:

   int i = 10;

   int *p = &i;

在編譯器上調試一下,我們發現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,這會影響你代碼的移植性。

假設現在需要往記憶體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 類型的資料;然後通過鑰匙“*”向這塊記憶體寫入一個資料。

上面讨論了這麼多,其實其表達形式并不重要,重要的是這種思維方式。也就是說我們完全有辦法給指定的某個記憶體位址寫入資料的。

另外一個有意思的現象,在visual c++ 6.0 調試如下代碼的時候卻又發現一個古怪的問題:

   p = null;

在執行完第二條代碼之後,發現p 的值變為0x00000000 了。按照我麼上一節的解釋,應該p的值不變,隻是p 指向的記憶體被指派為0。難道我們講錯了嗎?别急,再試試如下代碼:

通過調試,發現這樣子的話,p 的值沒有變,而p 指向的記憶體的值變為0 了。這與我們前面講解的完全一緻。當然這裡的i 的位址剛好是0x12ff7c,但這并不能改變“*p = null;”這行代碼的功能。

為了再次測試這個問題,我又調試了如下代碼:

   int j = 100;

   int *p = (int *)0x12ff78;

這裡0x12ff78 剛好就是變量j 的位址。這樣的話一切正常,但是如果把“int j = 100;”這行代碼删除的話,又出現上述的問題了。測試到這裡我還是不甘心,編譯器怎麼能犯這種低級錯誤呢?于是又接着進行了如下測試:

   unsigned int i = 10;

   //unsigned int j = 100;

   unsigned int *p = (unsigned int *)0x12ff78;

得到的結果與上面完全一樣。當然,我還是沒有死心,又進行了如下測試:

   char ch = 10;

   char *p = (char *)0x12ff7c;

這樣子的話,完全正常。但當我删除掉第一行代碼後再測試,這裡的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。還有,這個小小的例子,你是否可以在多個編譯器上測試測試呢?

噢,上面的讨論一不小心就這麼多了。這裡我為什麼要把這個小小的問題放到這裡長篇大論呢?我是想告訴讀者:研究問題一定要肯鑽研。千萬不要小看某一個簡單的事情,簡單的事情可能富含着很多秘密。經過這樣一番深究,相信你也有不少收獲。平時學習工作也是如此,不要小瞧任何一件簡單的事情,把簡單的事情做好也是一種偉大。勞模許振超開了幾十年的吊車,技術精到指哪打哪的地步。達到這種程度是需要花苦功夫的,幾十年如一日天天重複這件看似很簡單的事情,這不是一般人能做到的。同樣的,在《天龍八部》中,蕭峰血戰聚賢莊的時候,一套平平凡凡的太祖長拳打得虎虎生威,在場的英雄無不佩服至極,這也是其苦練的結果。我們學習工作同樣如此,要肯下苦功夫鑽研,不要怕鑽得深,隻怕鑽得不深。其實這也就是為什麼同一個班的學生,水準會相差非常大的最關鍵之處。

學得好的,往往是那些舍得鑽研的學生。我平時上課教學生的絕不僅僅是知識點,更多的時候我在教他們學習和解決問題的方法。有時候這個過程遠比結論要重要的多。後面的内容,你也應該能看出來,我非常注重過程的分析,隻有你真正明白了這些思考問題、解決問題的方法和過程,你才能真正立于不敗之地。所有的問題對你來說都是一個樣,沒有本質的差別。解決任何問題的辦法都一緻,那就是把沒見過的、不會的問題想法設法轉換成你見過的、你會的問題;至于怎麼去轉換那就要靠你的苦學苦練了。也就是說你要達到手中無劍,胸中也無劍的地步。

當然這些隻是我個人的領悟,寫在這裡希望能與君共勉。