1. 程式人生 > >C語言--從指標到二級指標(下)

C語言--從指標到二級指標(下)

通過上一篇博文,我們已經對指標有了一定的認識,在接下來的這一篇博文中,我們將進一步瞭解指標的精髓,包括指標的實質以及一些特殊的指標,同時還會涉及到一些C語言其他的語言特性。

一:指標的實質

指標作為C語言的精髓關鍵在於“指”,那這個“指”到底是什麼?C99中曾給出了指標的明確定義,歸納起來有十分重要的兩點:1. 指標是一種引用型別 2.指標本身的型別取決於它所引用的資料型別。怎麼理解呢?如果想要完全理解這兩點,那就說來話長了,甚至涉及到一些計算機的硬體結構,比如堆記憶體和棧記憶體,這有違本文主旨,其次這些我也不熟,姑且就不在此大談特談誤人子弟了。長話短說,在計算機中,每一個記憶體單元都是編址的,所以每一個記憶體單元理所應當的有一個獨一無二的地址編號,這個地址不過是個無符號整數,當計算機對記憶體進行寫入或讀取時就需要靠這個整數來定址。在C語言中,我們對這個用整數進行編號的地址進行了適當抽象,並賦予了一個名字,即為指標,這也就不難理解上一篇博文中為什麼我們打印出來的指標為什麼是一個無符號整數了,所有指標本身的值構成了一個集合,這個集合就是在當前系統進行定址時的定址範圍。簡單說來就是:指標本身是一個變數,只不過這個變數存放的內容是計算機中的資料的地址,我們通常把指標理解為“指向”其他變數的一種特殊資料型別。前段時間看到有關指標引發的爭論:指標究竟是地址還是引用?雙方各執一詞,似乎都有道理,其實這兩者並不矛盾。

回到C99中的那兩點,如果我們向一個記憶體單元中存入了一個int型別的資料,那麼這個資料對應的指標就是一個int*型別的引用型別。同理,如果存入一個double型別的資料,那指標就是一個double*型別的的引用型別。比如有一個int*型別的指標a,裡面放了一個int型整數5。那我們對指標進行間接定址運算,*a == 5輸出為true,所以我們說指標變數a引用了int型變數5。

此外,指標變數還支援加減運算,這和我們常見的加減有所不同,簡單說來對指標加一實質上是獲取了下一個儲存單元的地址,具體可以參考下面關於陣列與指標的討論。

下面我們來看看指標與陣列的關係,一旦我們在程式中聲明瞭一個數組,編譯系統就會在記憶體中分配一塊固定大小的儲存空間,這個空間是一塊連續的儲存區域,同時陣列名含有一個特殊的意義,它是存放數元素的連續儲存空間的首地址,即為指向了陣列中第一個元素的常量指標。看一個簡單例子:

#include <stdio.h>
#include <stdlib.h>
int main(void) {
	int a[] = { 1,2,3,4 };
	for (int i = 0; i < 4; i++) {
		printf(" the address of a[%d] is: %p \n", i, a + i);
		printf(" the value of a[%d] is: %4d \n" ,i, *(a + i));
	}
	system("pause");
	return 0;
}

執行結果如下:


很明顯,在這個程式中陣列名a居然被我們拿來像指標一樣使用了,同時我們發現,指標變數a每加1,a就指向了陣列中的下一個元素,細心觀察還可以發現指標加1,後,指標的值增大4,這是因為在計算機中每8位就是一個儲存單元,即每8為對應一個地址編號,而一個int型整數為32位,對應4個儲存單元,指標加1指向下一個陣列元素的過程中,指標“移動了”4個儲存單元(學名叫做指標偏移)。

如果我們把指標同二維陣列聯絡起來則將大大增加可玩性,但不過是一些花哨的奇技淫巧,比如下面4個其實是同一個東西:a[i][i], *(a[i] + j),*(*(a + i) + j), (*(a + i))[j], 感興趣的同學可以自行谷歌之,通常不建議將這種令人費解的程式碼寫入程式中,這是花式作死的成功典範。

二 特殊的指標

指標本身講來講去也就那些內容,而指標“指向”的內容則讓它變得高深莫測。常見的不過是“指向”一些常見資料型別的資料或者變數,再特殊一點“指向”一個數組,再高深一點就是一些“指向”另一個指標的指標,即二級指標,結構體指標,指標函式,函式指標之類的,對於這些,本文並不會一一俱到。接下來我們主要討論結構體指標和二級指標。

學習過C語言的同學對結構體都不會陌生,因為C語言自帶的資料型別顯然滿足不了我們的所有需求,這個時候我們就需要自定義資料型別了,也就是我們所說的結構體。關於結構體相信大家一定有所瞭解,我們就不展開來談了,下面我們來看看它和指標會碰撞出什麼樣的火花?看看下面這種情況會發生什麼?我們定義了一個結構體,這個結構體中的一個成員變數是一個指標,同時這個指標“指向”了這個結構體本身,不多說,直接看程式碼:

typedef struct stu {
	int ID;		//the ID of  student
	struct stu* nextStrudent;
}Student;		//define a student

這段程式碼無非是定義了一個代表學生結構體,比較特殊的是將結構體分成了兩個資料域,第一個資料域存放了學生的ID,第二個資料域存放了一個“指向”另一個學生的指標,這就實現了一種單向連結的關係,每一個學生相當於一個節點,效果就是他們實現了一種排隊的效果,特殊之處在於每一個人只知道他後面是誰,但對他前面的那個人卻一無所知(這就是我們在資料結構中所說的單向連結串列,如果這樣講下去,那就真的沒完沒了了,所以我們點到為止)。如圖所示:

最後值得一提的就是二級指標了,即“指向”指標的指標,如果你看完這兩篇博文已經對“指向”的實質有了深刻的認識,那麼二級指標自然也就難不倒你。

在上一篇博文中我們已經看到了C語言函式值傳遞的本質,通過程式碼的執行結果可以看出:如果不使用return或者指標,C語言函式呼叫結束後並不能改變呼叫函式時傳入的實參。同理,當呼叫函式時我們傳入的實參就是一個指標變數,如果我們希望在呼叫函式結束後該指標能夠“指向”其他資料或者變數,也得使用return或者該指標的指標(二級指標),我們在這裡只介紹二級指標,程式碼為證:

#include <stdio.h>
#include <stdlib.h>

void changeValue(int** pt, int b) {
	**pt = b;
}

int main(void) {
	int a = 3;
	int* p = &a;
	printf("  before change value, a = %d \n", a);
	changeValue(&p, 5);
	printf("  after change value, a = %d \n", a);
	system("pause");
	return 0;
}
執行結果如下:


雖然這段程式碼寫得很糟糕,只是強行拿來說事,我們還是來看圖吧:


可以看到最終pt指向了5,而pt是由&p賦值得來的,故&p最終也指向了5,而&p又是由a經過兩次取地址而得到的二級指標,故a的值最終為5。從中我們可以看到二級指標的奇妙之處。

以上就是有關指標的核心內容,感興趣的同學可以參考更多的文獻加以瞭解更多關於指標的知識!最後附上上一篇博文的連結:C語言---從指標到二重指標(上)