天天看點

C語言:指針

沒學指針就是沒學c語言!指針是c語言的精華,也是c語言的難點,破解c語言指針,會讓你的c語言水準突飛猛進。

所謂指針,也就是記憶體的位址;所謂指針變量,也就是儲存了記憶體位址的變量。不過,人們往往不會區分兩者的概念,而是混淆在一起使用,在必要的情況下,大家也要注意區分

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

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

C語言:指針

 00000000   ffffffff

我們将記憶體中位元組的編号稱為位址(address)或指針(pointer)。位址從 0 開始依次增加,對于 32 位環境,程式能夠使用的記憶體為 4gb,最小的位址為 0,最大的位址為 0xffffffff。

下面的代碼示範了如何輸出一個位址:

#include <stdio.h>

int main(){

int a = 100;

char str[20] = "c.biancheng.net";

printf("%#x, %#x\n", &a, str);

return 0;

}

運作結果:

0x28ff3c, 0x28ff10

<code>%#x</code>表示以十六進制形式輸出,并附帶字首<code>0x</code>。a 是一個變量,用來存放整數,需要在前面加<code>&amp;</code>來獲得它的位址;str 本身就表示字元串的首位址,不需要加<code>&amp;</code>。

c語言中有一個控制符<code>%p</code>,專門用來以十六進制形式輸出位址,不過 %p 的輸出格式并不統一,有的編譯器帶<code>0x</code>字首,有的不帶,是以此處我們并沒有采用。

c語言用變量來存儲資料,用函數來定義一段可以重複使用的代碼,它們最終都要放到記憶體中才能供 cpu 使用。

資料和代碼都以二進制的形式存儲在記憶體中,計算機無法從格式上區分某塊記憶體到底存儲的是資料還是代碼。當程式被加載到記憶體後,作業系統會給不同的記憶體塊指定不同的權限,擁有讀取和執行權限的記憶體塊就是代碼,而擁有讀取和寫入權限(也可能隻有讀取權限)的記憶體塊就是資料。

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

cpu 通路記憶體時需要的是位址,而不是變量名和函數名!變量名和函數名隻是位址的一種助記符,當源檔案被編譯和連結成可執行程式後,它們都會被替換成位址。編譯和連結過程的一項重要任務就是找到這些名稱所對應的位址。

假設變量 a、b、c 在記憶體中的位址分别是 0x1000、0x2000、0x3000,那麼加法運算<code>c = a + b;</code>将會被轉換成類似下面的形式:

0x3000 = (0x1000) + (0x2000);

<code>( )</code>表示取值操作,整個表達式的意思是,取出位址 0x1000 和 0x2000 上的值,将它們相加,把相加的結果指派給位址為 0x3000 的記憶體

變量名和函數名為我們提供了友善,讓我們在編寫代碼的過程中可以使用易于閱讀和了解的英文字元串,不用直接面對二進制位址,那場景簡直讓人崩潰。

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

資料在記憶體中的位址也稱為指針,如果一個變量存儲了一份資料的指針,我們就稱它為指針變量。

在c語言中,允許用一個變量來存放指針,這種變量稱為指針變量。指針變量的值就是某份資料的位址,這樣的一份資料可以是數組、字元串、函數,也可以是另外的一個普通變量或指針變量。

現在假設有一個 char 類型的變量 c,它存儲了字元 'k'(ascii碼為十進制數 75),并占用了位址為 0x11a 的記憶體(位址通常用十六進制表示)。另外有一個指針變量 p,它的值為 0x11a,正好等于變量 c 的位址,這種情況我們就稱 p 指向了 c,或者說 p 是指向變量 c 的指針。

C語言:指針

定義指針變量與定義普通變量非常類似,不過要在變量名前面加星号<code>*</code>,格式為:

datatype *name;

或者

datatype *name = value;

<code>*</code>表示這是一個指針變量,<code>datatype</code>表示該指針變量所指向的資料的類型 。例如:

p1 是一個指向 int 類型資料的指針變量,至于 p1 究竟指向哪一份資料,應該由賦予它的值決定。再如:

在定義指針變量 p_a 的同時對它進行初始化,并将變量 a 的位址賦予它,此時 p_a 就指向了 a。值得注意的是,p_a 需要的一個位址,a 前面必須要加取位址符<code>&amp;</code>,否則是不對的。

和普通變量一樣,指針變量也可以被多次寫入,隻要你想,随時都能夠改變指針變量的值,請看下面的代碼:

<code>*</code>是一個特殊符号,表明一個變量是指針變量,定義 p1、p2 時必須帶<code>*</code>。而給 p1、p2 指派時,因為已經知道了它是一個指針變量,就沒必要多此一舉再帶上<code>*</code>,後邊可以像使用普通變量一樣來使用指針變量。也就是說,定義指針變量時必須帶<code>*</code>,給指針變量指派時不能帶<code>*</code>。

假設變量 a、b、c、d 的位址分别為 0x1000、0x1004、0x2000、0x2004,下面的示意圖很好地反映了 p1、p2 指向的變化:

C語言:指針

需要強調的是,p1、p2 的類型分别是<code>float*</code>和<code>char*</code>,而不是<code>float</code>和<code>char</code>,它們是完全不同的資料類型,讀者要引起注意。

指針變量也可以連續定義,例如:

注意每個變量前面都要帶<code>*</code>。如果寫成下面的形式,那麼隻有 a 是指針變量,b、c 都是類型為 int 的普通變量:

指針變量存儲了資料的位址,通過指針變量能夠獲得該位址上的資料,格式為:

*pointer;

這裡的<code>*</code>稱為指針運算符,用來取得某個位址上的資料,請看下面的例子:

15, 15

假設 a 的位址是 0x1000,p 指向 a 後,p 本身的值也會變為 0x1000,*p 表示擷取位址 0x1000 上的資料,也即變量 a 的值。從運作結果看,*p 和 a 是等價的。

上節我們說過,cpu 讀寫資料必須要知道資料在記憶體中的位址,普通變量和指針變量都是位址的助記符,雖然通過 *p 和 a 擷取到的資料一樣,但它們的運作過程稍有不同:a 隻需要一次運算就能夠取得資料,而 *p 要經過兩次運算,多了一層“間接”。

假設變量 a、p 的位址分别為 0x1000、0xf0a0,它們的指向關系如下圖所示:

C語言:指針

程式被編譯和連結後,a、p 被替換成相應的位址。使用 *p 的話,要先通過位址 0xf0a0 取得變量 p 本身的值,這個值是變量 a 的位址,然後再通過這個值取得變量 a 的資料,前後共有兩次運算;而使用 a 的話,可以通過位址 0x1000 直接取得它的資料,隻需要一步運算。

也就是說,使用指針是間接擷取資料,使用變量名是直接擷取資料,前者比後者的代價要高。

指針除了可以擷取記憶體上的資料,也可以修改記憶體上的資料,例如:

99, 99, 99, 99

*p 代表的是 a 中的資料,它等價于 a,可以将另外的一份資料指派給它,也可以将它指派給另外的一個變量。

<code>*</code>在不同的場景下有不同的作用:<code>*</code>可以用在指針變量的定義中,表明這是一個指針變量,以和普通變量區分開;使用指針變量時在前面加<code>*</code>表示擷取指針指向的資料,或者說表示的是指針指向的資料本身。

也就是說,定義指針變量時的<code>*</code>和使用指針變量時的<code>*</code>意義完全不同。以下面的語句為例:

第1行代碼中<code>*</code>用來指明 p 是一個指針變量,第2行代碼中<code>*</code>用來擷取指針指向的資料。

需要注意的是,給指針變量本身指派時不能加<code>*</code>。修改上面的語句:

第2行代碼中的 p 前面就不能加<code>*</code>。

指針變量也可以出現在普通變量能出現的任何表達式中,例如:

【示例】通過指針交換兩個變量的值。

a=100, b=999

a=999, b=100

從運作結果可以看出,a、b 的值已經發生了交換。需要注意的是臨時變量 temp,它的作用特别重要,因為執行<code>*pa = *pb;</code>語句後 a 的值會被 b 的值覆寫,如果不先将 a 的值儲存起來以後就找不到了。

假設有一個 int 類型的變量 a,pa 是指向它的指針,那麼<code>*&amp;a</code>和<code>&amp;*pa</code>分别是什麼意思呢?

<code>*&amp;a</code>可以了解為<code>*(&amp;a)</code>,<code>&amp;a</code>表示取變量 a 的位址(等價于 pa),<code>*(&amp;a)</code>表示取這個位址上的資料(等價于 *pa),繞來繞去,又回到了原點,<code>*&amp;a</code>仍然等價于 a。

<code>&amp;*pa</code>可以了解為<code>&amp;(*pa)</code>,<code>*pa</code>表示取得 pa 指向的資料(等價于 a),<code>&amp;(*pa)</code>表示資料的位址(等價于 &amp;a),是以<code>&amp;*pa</code>等價于 pa。

在我們目前所學到的文法中,星号<code>*</code>主要有三種用途:

表示乘法,例如<code>int a = 3, b = 5, c;  c = a * b;</code>,這是最容易了解的。

表示定義一個指針變量,以和普通變量區分開,例如<code>int a = 100;  int *p = &amp;a;</code>。

表示擷取指針指向的資料,是一種間接操作,例如<code>int a, b, *p = &amp;a;  *p = 100;  b = *p;</code>。

繼續閱讀