指針
我們通過指針,可以簡化一些 C 編程任務的執行,還有一些任務,如動態內存分配,沒有指針是無法執行的。所以,學習指針是很有必要的。
正如您所知道的,每一個變量都有一個內存位置,每一個內存位置都定義了可使用連字號(&)運算符訪問的地址,它表示了在內存中的一個地址。請看下面的實例,它將輸出定義的變量地址:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
當上面的代碼被編譯和執行時,它會產生下列結果:
- 1
- 2
- 1
- 2
通過上面的栗子,我們瞭解了什麼是內存地址以及如何訪問它。接下來讓我們看看什麼是指針吧!
什麼是指針?
指針是一個變量,其值爲另一個變量的地址,即內存位置的直接地址。就像其他變量或常量一樣,您必須在使用指針存儲其他變量地址之前,對其進行聲明。指針變量聲明的一般形式爲:
- 1
- 1
這裏的type 是指針的基類型,它必須是一個有效的 C 數據類型,name 是指針變量的名稱。用來聲明指針的星號 * 與乘法中使用的星號是相同的。但是,在這個語句中,星號是用來指定一個變量是指針。以下是有效的指針聲明:
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
所有指針的值的實際數據類型,不管是整型、浮點型、字符型,還是其他的數據類型,都是一樣的,都是一個代表內存地址的長的十六進制數。不同數據類型的指針之間唯一的不同是,指針所指向的變量或常量的數據類型不同。
如何使用指針?
- 使用指針時會頻繁進行以下幾個操作:定義一個指針變量、把變量地址賦值給指針、訪問指針變量中可用地址的值。這些是通過使用一元運算符 * 來返回位於操作數所指定地址的變量的值。下面的實例涉及到了這些操作:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
上面的代碼被編譯和執行時,運行的結果:
- 1
- 2
- 3
- 1
- 2
- 3
- 對指針存的地址指向的變量進行操作
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
上面代碼編譯執行結果如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
通過上面栗子,我們可以看出,指針所指向的變量值已經被更改了。
C中的NULL 指針
在變量聲明的時候,如果沒有確切的地址可以賦值,爲指針變量賦一個 NULL 值是一個良好的編程習慣。賦爲 NULL 值的指針被稱爲空指針。
NULL 指針是一個定義在標準庫中的值爲零的常量。請看下面的程序:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
運行結果爲:
- 1
- 1
在大多數的操作系統上,程序不允許訪問地址爲 0 的內存,因爲該內存是操作系統保留的。然而,內存地址 0 有特別重要的意義,它表明該指針不指向一個可訪問的內存位置。但按照慣例,如果指針包含空值(零值),則假定它不指向任何東西。
如需檢查一個空指針,您可以使用 if 語句,如下所示:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
這兩種方法都能判斷字符指針是否爲空,但推薦使用前者。”NULL” 的本質是個宏,並非是個常量,C99 中甚至可以自行定義,故儘量避免使用它去判斷,當 !s 與 s == NULL 表示同一含義的時候,使用前者吧!
C中的二級指針(多級指針)
指針可以指向一份普通類型的數據,例如 int、double、char 等,也可以指向一份指針類型的數據,例如 int 、double 、char * 等。
如果一個指針指向的是另外一個指針,我們就稱它爲二級指針,或者指向指針的指針。
假設有一個 int 類型的變量 a,p1是指向 a 的指針變量,p2 又是指向 p1 的指針變量,它們的關係如下圖所示:
用代碼表示爲:
- 1
- 2
- 3
- 1
- 2
- 3
指針變量也是一種變量,也會佔用存儲空間,也可以使用&獲取它的地址。C語言不限制指針的級數,每增加一級指針,在定義指針變量時就得增加一個星號 * 。p1 是一級指針,指向普通類型的數據,定義時有一個 * ;p2 是二級指針,指向一級指針 p1,定義時有兩個*。
如果再定義一個三級指針 p3,讓它指向 p2,那麼可以這樣寫:
- 1
- 1
四級指針:
- 1
- 1
等等,以此類推。。。
不過,經常使用的也就是一級指針和二級指針了。
在獲取指針指向的數據時,一級指針加一個 * ,二級指針加兩個 * ,三級指針加三個 *,以此類推,請看代碼:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
編譯並運行結果如下:
以三級指針 p3 爲例來分析上面的代碼。* p3等價於 * ( * (* p3))。* p3 得到的是 p2 的值,也即 p1 的地址;* ( * p3) 得到的是 p1 的值,也即 a 的地址;經過三次“取值”操作後,* ( *(p3)) 得到的纔是 a 的值。
假設 a、p1、p2、p3 的地址分別是 0X00A0、0X1000、0X2000、0X3000,它們之間的關係可以用下圖來描述:
方框裏面是變量本身的值,方框下面是變量的地址。
C中指針的運算
指針變量保存的是地址,本質上是一個整數,可以進行部分運算,例如加法、減法、比較等,請看下面的代碼:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
編譯並運行的結果如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
從上面的栗子來看,數組變量名:ids就是數組的首地址,指針的加法,p++就是向前移動了sizeof(數據類型)個字節。
我們知道,數組中的所有元素在內存中是連續排列的,如果一個指針指向了數組中的某個元素,那麼加 1 就表示指向下一個元素,減 1 就表示指向上一個元素,不過C語言並沒有規定變量的存儲方式,如果連續定義多個變量,它們有可能是挨着的,也有可能是分散的,這取決於變量的類型、編譯器的實現以及具體的編譯模式,所以對於指向普通變量的指針,我們往往不進行加減運算,雖然編譯器並不會報錯,但這樣做沒有意義,因爲不知道它後面指向的是什麼數據。
下面舉一個栗子,通過指針獲取下一個變量的地址:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
編譯並運行結果如下:
- 1
- 1
可以看出變量 a、b、c 並不挨着,它們中間還有其他的數據。
指針變量除了可以參與加減運算,還可以參與比較運算。當對指針變量進行比較運算時,比較的是指針變量本身的值,也就是數據的地址。如果地址相等,那麼兩個指針就指向同一份數據,否則就指向不同的數據。
所以總結出,不要對指向普通變量的指針進行加減運算;另外需要說明的是,不能對指針變量進行乘法、除法、取餘等其他運算,除了會發生語法錯誤,也沒有實際的含義。
C中的指針數組
如果一個數組中的所有元素保存的都是指針,那麼我們就稱它爲指針數組。指針數組的定義形式一般爲:
- 1
- 1
]的優先級高於*,該定義形式應該理解爲:
- 1
- 1
括號裏面說明arrayName是一個數組,包含了length個元素,括號外面說明每個元素的類型爲dataType *。
除了每個元素的數據類型不同,指針數組和普通數組在其他方面都是一樣的,下面是一個簡單的栗子:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
編譯並運行結果如下:
- 1
- 2
- 1
- 2
arr 是一個指針數組,它包含了 3 個元素,每個元素都是一個指針,在定義 arr 的同時,我們使用變量 a、b、c 的地址對它進行了初始化,這和普通數組是多麼地類似。
parr 是指向數組 arr 的指針,確切地說是指向 arr 第一個元素的指針,它的定義形式應該理解爲int * ( * parr),括號中的 * 表示 parr 是一個指針,括號外面的int * 表示 parr 指向的數據的類型。arr 第一個元素的類型爲 int *,
所以在定義 parr 時要加兩個 * (星號)(MD語法自動識別其他的格式了)。
第一個 printf() 語句中,arr[i] 表示獲取第 i 個元素的值,該元素是一個指針,還需要在前面增加一個 * 才能取得它指向的數據,也即 *arr[i] 的形式。
第二個 printf() 語句中,parr+i 表示第 i 個元素的地址,* (parr+i) 表示獲取第 i 個元素的值(該元素是一個指針),**(parr+i) 表示獲取第 i 個元素指向的數據。
通過指針給數組賦值,下面是一個簡單的栗子:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
C中的指針與二維數組
二維數組在概念上是二維的,有行和列,但在內存中所有的數組元素都是連續排列的,它們之間沒有“縫隙”。以下面的二維數組 a 爲例:
- 1
- 1
從概念上來理解,數組a 的分佈如下:
- 1
- 2
- 3
- 1
- 2
- 3
但在內存中,a 的分佈是一維線性的,整個數組佔用一塊連續的內存:
C語言中的二維數組是按行排列的,也就是先存放 a[0] 行,再存放 a[1] 行,最後存放 a[2] 行;每行中的 4 個元素也是依次存放。數組 a 爲 int 類型,每個元素佔用 4 個字節,整個數組共佔用 4×(3×4) = 48 個字節。
C語言允許把一個二維數組分解成多個一維數組來處理。對於數組 a,它可以分解成三個一維數組,即 a[0]、a[1]、a[2]。每一個一維數組又包含了 4 個元素,例如 a[0] 包含 a[0][0]、a[0][1]、a[0][2]、a[0][3]。
假設數組 a 中第 0 個元素的地址爲 1000,那麼每個一維數組的首地址如下圖所示:
爲了更好的理解指針和二維數組的關係,我們先來定義一個指向 a 的指針變量 p:
- 1
- 1
int *p[4]; //定義一個指針數組,該數組中每個元素是一個指針,每個指針指向哪裏就需要程序中後續再定義了。
int (*p)[4]; //定義一個數組指針,該指針指向含4個元素的一維數組(數組中每個元素是int型)。
- 1
- 2
- 3
- 1
- 2
- 3
對指針進行加法(減法)運算時,它前進(後退)的步長與它指向的數據類型有關,p 指向的數據類型是int [4],那麼p+1就前進 4×4 = 16 個字節,p-1就後退 16 個字節,這正好是數組 a 所包含的每個一維數組的長度。也就是說,p+1會使得指針指向二維數組的下一行,p-1會使得指針指向數組的上一行。
下面我們就來實現如何使用指針 p 來訪問二維數組中的每個元素。按照上面的定義:
- p指向數組 a 的開頭,也即第 0 行;p+1前進一行,指向第 1 行。
- *(p+1)表示取地址上的數據,也就是整個第 1 行數據。注意是一行數據,是多個數據,不是第 1 行中的第 0 個元素,下面的運行結果有力地證明了這一點:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
編譯並運行結果爲:
- 1
- 1
那麼,*(p+1)+1表示第 1 行第 1 個元素的地址。這個如何理解呢?(注意,這裏的小細節)
- 1
- 2
- 1
- 2
- 1
- 1
根據上面的推論,推出以下等價關係:
- 1
- 2
- 3
- 1
- 2
- 3
可能有點繞,不好理解,不過沒關係,多看幾遍,相信你是可以的。
栗子:
使用指針遍歷二維數組
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
編譯並運行的結果如下:
- 1
- 2
- 3
- 1
- 2
- 3
指針數組和二維數組指針的區別:
指針數組和二維數組指針在定義時非常相似,只是括號的位置不同:
- 1
- 2
- 1
- 2
指針數組和二維數組指針有着本質上的區別:指針數組是一個數組,只是每個元素保存的都是指針,以上面的 p1 爲例,在32位環境下它佔用 4×5 = 20 個字節的內存。二維數組指針是一個指針,它指向一個二維數組,以上面的 p2 爲例,它佔用 4 個字節的內存。
C中的函數指針
一個函數總是佔用一段連續的內存區域,函數名在表達式中有時也會被轉換爲該函數所在內存區域的首地址,這和數組名非常類似。我們可以把函數的這個首地址(或稱入口地址)賦予一個指針變量,使指針變量指向函數所在的內存區域,然後通過指針變量就可以找到並調用該函數。這種指針就是函數指針。
函數指針的定義形式爲:
- 1
- 1
returnType 爲函數返回值類型,pointerNmae 爲指針名稱,param list 爲函數參數列表。
注意:
( )的優先級高於*,第一個括號不能省略,如果寫作returnType *pointerName(paramlist);
就成了函數原型,它表明函數的返回值類型爲returnType *。
下面寫一個簡單的栗子:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
編譯並運行結果如下:
上面對msg函數進行了調用,fun_p是一個函數指針。
總結:
指針(Pointer)就是內存的地址,C語言允許用一個變量來存放指針,這種變量稱爲指針變量。指針變量可以存放基本類型數據的地址,也可以存放數組、函數以及其他指針變量的地址。
常見的指針含義: