1. 程式人生 > 其它 >1分鐘徹底理解C語言指標的概念

1分鐘徹底理解C語言指標的概念

計算機中所有的資料都必須放在記憶體中,不同型別的資料佔用的位元組數不一樣,例如 int 佔用4個位元組,char 佔用1個位元組。為了正確地訪問這些資料,必須為每個位元組都編上號碼,就像門牌號、身份證號一樣,每個位元組的編號是唯一的,根據編號可以準確地找到某個位元組。

下圖是 4G 記憶體中每個位元組的編號(以十六進位制表示):

我們將記憶體中位元組的編號稱為地址(Address)或指標(Pointer)。地址從 0 開始依次增加,對於 32 位環境,程式能夠使用的記憶體為 4GB,最小的地址為 0,最大的地址為 0XFFFFFFFF。 下面的程式碼演示瞭如何輸出一個地址:

#include <stdio.h>int main(){int a = 100;char str[20] = "c.biancheng.net";printf("%#X, %#Xn", &a, str);return 0;}

執行結果: 0X28FF3C, 0X28FF10 %#X表示以十六進位制形式輸出,並附帶字首0X。a 是一個變數,用來存放整數,需要在前面加&來獲得它的地址;str 本身就表示字串的首地址,不需要加&

一切都是地址

C語言用變數來儲存資料,用函式來定義一段可以重複使用的程式碼,它們最終都要放到記憶體中才能供 CPU 使用。 資料和程式碼都以二進位制的形式儲存在記憶體中,計算機無法從格式上區分某塊記憶體到底儲存的是資料還是程式碼。當程式被載入到記憶體後,作業系統會給不同的記憶體塊指定不同的許可權,擁有讀取和執行許可權的記憶體塊就是程式碼,而擁有讀取和寫入許可權(也可能只有讀取許可權)的記憶體塊就是資料。

CPU 只能通過地址來取得記憶體中的程式碼和資料,程式在執行過程中會告知 CPU 要執行的程式碼以及要讀寫的資料的地址。如果程式不小心出錯,或者開發者有意為之,在 CPU 要寫入資料時給它一個程式碼區域的地址,就會發生記憶體訪問錯誤。這種記憶體訪問錯誤會被硬體和作業系統攔截,強制程式崩潰,程式設計師沒有挽救的機會。

CPU 訪問記憶體時需要的是地址,而不是變數名和函式名!變數名和函式名只是地址的一種助記符,當原始檔被編譯和連結成可執行程式後,它們都會被替換成地址。編譯和連結過程的一項重要任務就是找到這些名稱所對應的地址。 假設變數 a、b、c 在記憶體中的地址分別是 0X1000、0X2000、0X3000,那麼加法運算c = a + b;

將會被轉換成類似下面的形式:

0X3000 = (0X1000) + (0X2000);

( )表示取值操作,整個表示式的意思是,取出地址 0X1000 和 0X2000 上的值,將它們相加,把相加的結果賦值給地址為 0X3000 的記憶體。 變數名和函式名為我們提供了方便,讓我們在編寫程式碼的過程中可以使用易於閱讀和理解的英文字串,不用直接面對二進位制地址,那場景簡直讓人崩潰。

需要注意的是,雖然變數名、函式名、字串名和陣列名在本質上是一樣的,它們都是地址的助記符,但在編寫程式碼的過程中,我們認為變數名錶示的是資料本身,而函式名、字串名和陣列名錶示的是程式碼塊或資料塊的首地址。

C語言指標變數的運算

指標變數儲存的是地址,本質上是一個整數,可以進行部分運算,例如加法、減法、比較等,請看下面的程式碼:

#include <stdio.h>int main(){int    a = 10,   *pa = &a, *paa = &a;double b = 99.9, *pb = &b;char   c = '@',  *pc = &c;//最初的值printf("&a=%#X, &b=%#X, &c=%#Xn", &a, &b, &c);printf("pa=%#X, pb=%#X, pc=%#Xn", pa, pb, pc);//加法運算pa++; pb++; pc++;printf("pa=%#X, pb=%#X, pc=%#Xn", pa, pb, pc);//減法運算pa -= 2; pb -= 2; pc -= 2;printf("pa=%#X, pb=%#X, pc=%#Xn", pa, pb, pc);//比較運算if(pa == paa){printf("%dn", *paa);}else{printf("%dn", *pa);}return 0;}

執行結果:

&a=0X28FF44, &b=0X28FF30, &c=0X28FF2B
pa=0X28FF44, pb=0X28FF30, pc=0X28FF2B
pa=0X28FF48, pb=0X28FF38, pc=0X28FF2C
pa=0X28FF40, pb=0X28FF28, pc=0X28FF2A2686784

從運算結果可以看出:pa、pb、pc 每次加 1,它們的地址分別增加 4、8、1,正好是 int、double、char 型別的長度;減 2 時,地址分別減少 8、16、2,正好是 int、double、char 型別長度的 2 倍。 這很奇怪,指標變數加減運算的結果跟資料型別的長度有關,而不是簡單地加 1 或減 1,這是為什麼呢?

以 a 和 pa 為例,a 的型別為 int,佔用 4 個位元組,pa 是指向 a 的指標,如下圖所示:

剛開始的時候,pa 指向 a 的開頭,通過 *pa 讀取資料時,從 pa 指向的位置向後移動 4 個位元組,把這 4 個位元組的內容作為要獲取的資料,這 4 個位元組也正好是變數 a 佔用的記憶體。 如果pa++;使得地址加 1 的話,就會變成如下圖所示的指向關係:

這個時候 pa 指向整數 a 的中間,*pa 使用的是紅色虛線畫出的 4 個位元組,其中前 3 個是變數 a 的,後面 1 個是其它資料的,把它們“攪和”在一起顯然沒有實際的意義,取得的資料也會非常怪異。 如果pa++;使得地址加 4 的話,正好能夠完全跳過整數 a,指向它後面的記憶體,如下圖所示:

我們知道,陣列中的所有元素在記憶體中是連續排列的,如果一個指標指向了陣列中的某個元素,那麼加 1 就表示指向下一個元素,減 1 就表示指向上一個元素,這樣指標的加減運算就具有了現實的意義。 不過C語言並沒有規定變數的儲存方式,如果連續定義多個變數,它們有可能是挨著的,也有可能是分散的,這取決於變數的型別、編譯器的實現以及具體的編譯模式,所以對於指向普通變數的指標,我們往往不進行加減運算,雖然編譯器並不會報錯,但這樣做沒有意義,因為不知道它後面指向的是什麼資料。

下面的例子是一個反面教材,警告小夥伴們不要嘗試通過指標獲取下一個變數的地址:

#include <stdio.h>int main(){int a = 1, b = 2, c = 3;int *p = &c;int i;for(i=0; i<8; i++){printf("%d, ", *(p+i) );}return 0;}

在 VS2010 Debug 模式下的執行結果為:

3, -858993460, -858993460, 2, -858993460, -858993460, 1, -858993460,

可以發現,變數 a、b、c 並不挨著,它們中間還參雜了別的輔助資料。 指標變數除了可以參與加減運算,還可以參與比較運算。當對指標變數進行比較運算時,比較的是指標變數本身的值,也就是資料的地址。如果地址相等,那麼兩個指標就指向同一份資料,否則就指向不同的資料。

上面的程式碼(第一個例子)在比較 pa 和 paa 的值時,pa 已經指向了 a 的上一份資料,所以它們不相等。而 a 的上一份資料又不知道是什麼,所以會導致 printf() 輸出一個沒有意義的數,這正好印證了上面的觀點,不要對指向普通變數的指標進行加減運算。 另外需要說明的是,不能對指標變數進行乘法、除法、取餘等其他運算,除了會發生語法錯誤,也沒有實際的含義。

“有一天我們都會死去,追求智慧的道路還會有人在走著。死掉以後的事我看不到,但在我活著的時候,想到這件事,心裡就高興。”——王小波

今天是20年前中國早期程式設計師王小波離開的日子。

喜歡讀書的人,對王小波都不陌生,他是中國最富創造性的作家之一,他是中國近半世紀的苦難和荒謬所結晶出來的天才,他英年早逝。然而王小波除了作家的身份外,還是一名程式設計師,並且是一名很牛的程式設計師。

在90年代初的時候因為國內應用軟體缺乏,愛搗鼓東西的王小波利用閒暇時間學習了彙編和C語言,編了中文編輯器和輸入法。

中文編輯器和輸入法任何一個都是大牛級的GEEK才會去嘗試的東西。王小波通過賣軟體還掙了些錢,當時很多中觀村的老闆要拉他入夥,當然寫程式碼這種來錢快的活對屌絲王小波還是有吸引力的,所幸他還是拒絕了人家。

在做出中文編輯器和輸入法,解決了自己的需求之後,王小波對寫軟體的興趣已經沒有多少了。因為他覺得寫軟體可以賺錢,寫小說同樣也可以賺錢。出於一個程式設計師的修養,王小波還是不斷地從資料結構和演算法來優化這兩個軟體。

其他具體他的光輝事蹟老九君就不在這裡贅述了。

王小波幹過很多事情,下過鄉,考過大學,出過國,學過經濟,寫過程式碼,成了小說家,去世的那一年完成了心願做了編劇。總之不管他幹了什麼,他身上讓我們尊重的還是獨立之精神,自由之思想。