零基礎學C語言——指針

這是一個C語言系列文章,如果是初學者的話,建議先行閱讀之前的文章。筆者也會按照章節順序發佈。

指針的定義

有很多文章將C語言指針說得猶如蜀道之難一般,其實指針這個概念很容易。

我們依舊以 一文中介紹變量時使用的停車場為例。假設每一個停車位就是一個變量,我們可以對每一個停車位命名(例如用26個英文字母),同時每個停車位也有其自身的位置(例如經緯度或者其他人為編號)。

例如,有一輛奔馳停入了第十個車位,這個車位編號為9,車位名稱為碼哥車位,轉換成代碼就是類似:

<code>碼哥車位 = 奔馳/<code>

對應到C語言中有可能就是:

<code>int mage_bit = 1001; //加入1001代表奔馳/<code>

這時,如果碼哥我想要在停車場中找到剛才停入的奔馳車,那麼直觀的方式有,找碼哥車位在哪,或找編號為9的車位在哪。不那麼直觀的方式有哪些呢?比如,在第1個車位(編號為0的車位,例如名叫通用車位

)上放一個牌子,寫著碼哥的奔馳車(1001)在9號車位。

此時,0號車位(即名為通用車位)的區域所代表的C語言變量就是一個指針變量,它的內容指向另一個車位編號。

在C語言中,指針可以被看作為一個長整型變量,其內容是一個長整型的整數,這個整數是另一個變量的內存地址。上面的例子對應到C語言中,車位的編號就是車位變量的內存地址,車位的名字就是變量的名字。可以轉換成類似如下代碼:

<code>int mage_bit = 1001;
int *common = &mage_bit;/<code>

指針定義的一般形式

<code>數據類型 *指針變量名;

數據類型 *指針變量名 = 初始地址值;/<code>

這裡,數據類型不僅僅包含了 ,還包含了我們以後會介紹的自定義類型、結構體類型等。

在上例中common就是一個整型指針變量,它的初始值為整型變量mage_bit的內存地址。&為 ,在想要獲取其內存地址的變量名前加上&即可獲取其地址。

還有一種特殊的指針——空指針,其關鍵字為NULL。這個指針的含義是不指向任何地方。這個NULL也等價於整數0。

<code>數據類型 *指針變量名;/<code>

用來告知編譯器指針變量已被定義該如何尋找。

指針運算

指針是可以進行四則運算的,但是這些運算會受到指針的數據類型限制。舉個例子:

<code>int a[3], *p, *q; //定義整型數組a和整型指針p、q
q = p = a; //數組名字即為數組首地址,我們後面的小節中馬上要談到
//此時p和q中存放的數值是數組a的首地址,即數組第一個單元(下標為0的元素)所對應的內存地址
++q;//整型指針q自增1/<code>

此時,(unsigned long)q - (unsigned long)p等於多少呢?

答案是,4。為什麼自增1,但相減後等於4呢?

原因是,

指針每一次增1並不代表其存儲的地址數值加1,而是增長一個其所屬類型的字節長度,對於int來說就是4。

類似的,--操作,+、-等操作都是如此。同樣,不同數據類型,其字節跨度大小取決於類型所佔字節大小

如果非要將q只增加1字節,那麼可以將之強制類型轉換為 unisgned char型指針 然後加1。

指針常見的操作有:

  • --
  • ++
  • +=
  • -=
  • +
  • -

指針的用途顯然也不僅限於四則運算,畢竟徒有地址並無太大用途。我們可以通過指針所存儲的內存地址來獲取或修改該地址中存放的具體數據,例如:

<code>int a = 10;
int *b = &a;
*b = 1;/<code>

這裡,*為 ,*b在賦值運算前的值為10,即變量a中的值。賦值後,變量a的值則變為1。這就相當於上面停車場例子中,將奔馳直接換成拖拉機,但0號車位的牌子不變。

指針的指針

既然指針變量中存放的是一個變量的內存地址,而指針本身也是一個變量,那麼指針變量本身也會有對應的內存地址。因此,是否可以用指針記錄另一個指針的地址呢?

當然可以,我們來看一個例子:

<code>int a = 10;
int *b = &a; //整型指針b存儲變量a的內存地址
int **c = &b; //整型二級指針c存儲整型指針b的內存地址/<code>

這裡引入多級指針的概念,即一級指針的指針(即一級指針變量的地址)為二級指針,二級指針的指針為三級指針,以此類推。是幾級指針就在定義是加幾個*。例如:

<code>int ***d = &c;/<code>

一般三級以上指針在實際應用中極少見到,應用場景很少。

指針與const

在 一文中談到過,定義常量用const關鍵字,下面看幾個寫法:

<code>int a = 10;

const int *b = &a;
int const *c = &a;
int * const d = &a;
const int * const e = &a;/<code>

上面這幾種寫法有什麼區別呢?下面我們逐個給出:

指針b:可以給b賦新值,但是不可以用*b去修改a的值

指針c:與指針b作用一致

指針d:可以用*d修改a的值,但是不可以給d賦新的地址值

指針e:既不可以給e賦新地址值,也不可以用*e修改變量a的值

當然,也有繞過限制的方式,看其中一個例子:

<code>#include <stdio.h>

int main(void)
{
int a = 10, b = 1;
int const *p = &a;

int **q = &p;
*q = &b;
printf("%d\\n", *p);
return 0;
}/<stdio.h>/<code>

這裡使用了一個二級指針q指向受限的p,然後來修改p的指向。運行結果為1,即p的指向被成功修改。

本例編譯時,不排除一些較新版本的編譯器報錯,畢竟const是為安全性而設計的,不推薦這麼用。

指針與數組

談起指針就必然會牽扯到 ,因為編譯器會將數組名處理為數組的首地址。

<code>int a[10];
int *p = a;
int *q = &a[0];//取下標為0的元素的內存地址,&a[0]等價於&(a[0]),因為[]優先級高於&/<code>

這樣的寫法並不會報錯,因為這是符合語法也符合我們預期的。a是數組,但數組名a也是數組的首地址。數組是整型數組,因此其地址也要用整型指針存放。首地址與下標為0的元素的首地址是相等的。因此這裡p - q等於0。

在 一文中,其數據類型也包含了指針,因此指針也可以組成數組——指針數組。例如:

<code>int a = 10;
int *b[3];
b[0] = b[1] = b[2] = &a;/<code>

即,每一個數組元素都是一個指針

既然各種數據類型都有指針,那數組是否也有指針呢?當然有——數組指針。例如:

<code>int a[3] = {1, 2, 3};
int (*b)[3] = &a;/<code>

這個例子中,a指代的數組首地址和b這個數組指針中存放的地址是相同的。那麼這兩者有什麼差別呢?

<code>(unsigned long)(b+1) - (unsigned long)(a+1) //結果為8/<code>

為什麼兩個值差了那麼多字節呢?

還記得前面指針運算小節中,指針的四則運算會受到其數據類型的限制嗎?即運算跨度與數據類型佔的字節數有關。本例中,b是數組a的指針,而數組a的大小是多大呢?是12字節,因為它包含了3個int元素。而a的類型是什麼呢?是int,因此a每次操作的跨度都是以4字節為單位的,而b則是以12字節為單位的。

指針與函數

一文中函數參數小節有提及函數參數為數組的情況。在閱讀了本篇前幾小節後,大家也應該明白函數參數的值傳遞傳遞的其實就是數組的首地址,即一個指針。

下面來說說函數的返回值為數組的情況,看一個例子:

<code>int *foo(int *array)
{
return array;
}/<code>

這個例子非常簡單,參數是一個整型指針,返回值是一個整型指針,函數的功能就是把參數返回出去。

換言之,這個函數參數可以是一個整型數組,那麼返回的也就是整型數組。

最後,我們說一種特殊的指針——函數指針每一個函數,其名字就是這個函數的地址。這不奇怪,因為所有的代碼都是以二進制指令形式放在內存中運行的,因此函數也必然存在內存地址。也因為其存在內存地址,那麼必然可以將這個地址賦給一個符合這個函數類型的變量。但關於這部分的內容我將放在後續介紹typedef關鍵字的文章中說明。

一個綜合例子

<code>#include <stdio.h>

int *arr[2];

int **modify(void)
{
int ** const ptr = &arr[1];
**ptr = 1;
return arr;
}

int main(void)
{
int num = 10;
arr[0] = NULL;
arr[1] = #
int **ret = modify();
printf("%d %d %d %lx %lx\\n", (int)ret[0], num, *ret[1], (unsigned long)&num, (unsigned long)ret[1]);
return 0;
}/<stdio.h>/<code>

讀者可以自行編譯運行這段代碼,看看終端輸出內容是什麼。

代碼的含義很簡單,定義了一個指針數組arr,在main函數中將數組第一個元素置空指針,第二個元素設為整型變量num的地址,然後調用modify函數。在modify函數中,定義指針ptr,這個ptr可以修改其指向的地址單元中的值,但ptr存放的地址值不允許修改,然後利用**運算符修改arr第二個元素所指向的地址單元(即num)中的值,最後將arr作為返回值返回。

printf(以後詳細介紹)中,%d為格式輸出,即輸出int型數據的值,%lx為輸出十六進制表示的長整型整數值。可以看到,我分別打印出NULL的整數形式的值,num的值,指針數組第二個元素指向的內存單元中的值,num的內存地址,指針數組第二個元素的數值。


喜歡的小夥伴可以關注碼哥,也可以給碼哥留言評論,如有建議或者意見也歡迎私信碼哥,我會第一時間回覆。


分享到:


相關文章: