1. 程式人生 > 其它 >關於指標 通俗易懂的從零入門篇

關於指標 通俗易懂的從零入門篇

技術標籤:c語言指標

- 以夢為碼 -

談到指標,估計是很多人頭疼的一個點吧。

這篇文章儘量用通俗的話來解釋,從零從頭開始,但是無法保證到多麼高深的地方結束,日後遇到可能再來新增吧。

首先我想說的是該篇主要是我的個人理解,也參考了網上一些資料,不保證百分百正確,敬請指正~

另外想跳過一些碎碎念和基礎知識建議直接跳過標題一

目錄

關於指標 從零開始的探討

一、什麼是指標

二、一級指標

三、二級指標


關於指標 通俗易懂的從零入門篇

一、什麼是指標

指標,是C語言中的一個重要概念及其特點,也是掌握C語言比較困難的部分。指標也就是記憶體地址,指標變數是用來存放記憶體地址的變數,不同型別的指標變數所佔用的儲存單元長度是相同的,而存放資料的變數因資料的型別不同,所佔用的

儲存空間長度也不同。有了指標以後,不僅可以對資料本身,也可以對儲存資料的變數地址進行操作。

摘自《指標》百度百科,https://baike.baidu.com/item/%E6%8C%87%E9%92%88/2878304?fr=aladdin

每次最惱火的地方就是看到這些抽象至極的語言了,總而言之,有兩個重要的點:

1.指標是變數

2.指標存放的內容是地址

有了上面兩個認識,開展下面的工作就簡單多了

既然是從零開始,首先在程式碼裡寫個return 0沒毛病吧(開個玩笑

int main(){

    //...

    return 0;
}

好吧不多廢話,我們先從普通變數研究起來,看一下一個int型變數的地址

void main(){
        int a = 0;
        printf("&a: %x\n", &a);
}

(在vim裡寫的,縮排自動8格請勿介意)

這裡有個可以注意的點,在編譯時gcc會返回這麼幾句:

$ gcc -o test test.c
test.c: In function ‘main’:
test.c:5:2: warning: format ‘%x’ expects argument of type ‘unsigned int’, but argument 2 has type ‘int *’ [-Wformat=]
  printf("&a: %x\n", &a); 
  ^

[Warning],%x期望的變數型別是unsigned int,但是第二個變數(第一個是引號部分,第二個是&a)的型別是int*,可以發現&a的型別變成了int*;

暫且標記一下,因為是warning所以不管他也沒事。

輸出結果:

$ ./test
&a: 3fdf143c

'&'是取地址符應該不用解釋吧,所以a在系統記憶體中被儲存的地址就是0x3fdf143c

順道扯一點作業系統的知識

$ uname -a
Linux ubuntu 4.4.0-31-generic #50~14.04.1-Ubuntu SMP Wed Jul 13 01:07:32 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux

這裡可以通過上面的指令檢視一下自己linux的位數,我這裡是64位

0x3fdf143c是八位十六進位制,換算下來正好就是64位(一個十六進位制位是八個二進位制位,八八六十四)

因此,確實是地址沒錯。

二、一級指標

1.基礎知識點

直接上程式碼

void main(){
        int a = 0;
        int* p = &a;
        printf("a: %d, &a: %x\n", a, &a);
        printf("*p: %d, p: %x, &p: %x\n", *p, p, &p);
}

為了簡化大家看程式碼的時間,這裡簡單寫一下:

第一行,輸出a、&a的內容

第二行,輸出*p、p、&p的內容

$ ./test
a: 0, &a: 453e0d44
*p: 0, p: 453e0d44, &p: 453e0d48

觀察仔細的小夥伴可能發現了,p==&a,沒錯,這也是我為什麼要在程式碼中把int和*放一塊的原因,(int*) p = &a

我只是想表達,p的內容就是a的地址,至於大家常說的誰指向誰,其實我也沒明白,姑且認為p指向a好了

還有一個常識性的問題,*p和p,誰叫指標?個人傾向於認為p是指標,不然也不會有p是一級指標或是二級指標的說法了

上段程式碼中我們還輸出了p的地址,這個0x453e0d48是什麼?

首先,指標也是變數,既然是變數記憶體必定有一塊儲存空間是留給他的

不懂的話我畫個圖

我畫圖實在太醜請見諒…

於是可以發現,p就是a的地址,也即p==&a

那麼*p操作無非就是跑到p裡儲存的那個地址,然後取一下里面的值

其實這樣看來,'*'和'&'兩個運算子互為逆運算

到此一級指標的內容就介紹完了,還有一點小的知識點(講課拉下的後遺症,事無鉅細

0x453e0d48-0x453e0d44=?,答案是4,恰巧=sizeof(int),真的是恰巧嗎?我們再嘗試一段程式碼

void main(){
        char a = 'd';
        char* p = &a;
        printf("a: %c, &a: %x\n", a, &a);
        printf("*p: %c, p: %x, &p: %x\n", *p, p, &p);
}
$ ./test
a: d, &a: e0bd5007
*p: d, p: e0bd5007, &p: e0bd5008

0xe0bd5008-0xe0bd5007=?,答案是1,恰巧=sizeof(char),看來不是恰巧呢~

主要是我倆變數捱得太近了,所以直接用兩塊連續的空間存了

2.應用

一級指標的應用其實不難,用心去分析,實在不行就寫寫,自己捋順瞭然後趕緊找個紙記下來

老生常談的一段程式碼,功能是交換a和b

void swap(int x, int y){
        int temp;
        temp = x;
        x = y;
        y = temp;
}


void main(){
        int a = 1;
        int b = 2;
        printf("before swap: a=%d, b=%d;\n", a, b);
        swap(a, b);
        printf(" after swap: a=%d, b=%d;\n", a, b);
}

執行結果:

$ ./test1
before swap: a=1, b=2;
 after swap: a=1, b=2;

沒有交換成功,為什麼呢?

原因很簡單,當實參a和b傳進函式時,其實系統做了幾件事:

1.int x, y;

2.x=a, y=b;

需要明確的是,a和b有自己的一塊地址,這塊地址既然已經被分配給a或b,就不會再分配給其他變量了

所以,建立x和y這兩個形參時,使用的必然是不同於a和b的兩塊地址空間,你x和y交換與我a和b何干,這兩組變數八竿子也打不著一塊去,唯一的共同點也就是值一樣了

因此,這麼更換是不行的

要想更換成功需要引入指標

void swap(int *x, int *y){//變了這
        int temp;
        temp = *x;//這
        *x = *y;//這
        *y = temp;//這
}


void main(){
        int a = 1;
        int b = 2;
        printf("before swap: a=%d, b=%d;\n", a, b);
        swap(&a, &b);//還有這
        printf(" after swap: a=%d, b=%d;\n", a, b);
}
$ ./test1
before swap: a=1, b=2;
 after swap: a=2, b=1;

結果OK,為什麼OK呢,我們不妨來思考一下

先前我們是直接傳了兩個變數,但是實參和形參佔用的是不同的地址空間

這裡傳入的時候一樣是執行了建立變數和賦值兩個操作,為了解答為何呼叫函式時使用&a和&b,這裡省略成一步寫

int* x = &a, int*y = &b

結合前面說過的指標賦值,一下就清楚了

於是,在函式體內部使用*x和*y時,實際上使用的是*(&a)和*(&b)也就是a和b

所以交換*x和*y的操作實際上就是在交換a和b

一級指標到二級指標的引子:

前面編譯這段程式碼時有幾個warning被我跳過了

void main(){
        char a = 'd';
        char* p = &a;
        printf("a: %c, &a: %x\n", a, &a);
        printf("*p: %c, p: %x, &p: %x\n", *p, p, &p);
}
$ gcc -o test test.c
test.c: In function ‘main’:
test.c:6:2: warning: format ‘%x’ expects argument of type ‘unsigned int’, but argument 3 has type ‘char *’ [-Wformat=]
  printf("a: %c, &a: %x\n", a, &a);
  ^
test.c:7:2: warning: format ‘%x’ expects argument of type ‘unsigned int’, but argument 3 has type ‘char *’ [-Wformat=]
  printf("*p: %c, p: %x, &p: %x\n", *p, p, &p); 
  ^
test.c:7:2: warning: format ‘%x’ expects argument of type ‘unsigned int’, but argument 4 has type ‘char **’ [-Wformat=]

看一下後兩個warning,p的型別是char*,&p的型別是char**

第一個warning寫的是&a的型別為char*

有點醍醐灌頂的感覺在了

三、二級指標

1.基礎知識點

二級指標按照定義來說就是指標的指標,一級指標指向普通變數,二級指標則指向一級指標(指標也是變數)

在上面程式碼的基礎上新增幾行

void main(){
        int a = 0;
        int* p = &a;
        int* p1 = p;//加了這
        int** p2 = &p;//這
        printf("a: %d,     &a: %x\n", a, &a);
        printf("  *p: %d,   p: %x,  &p: %x\n", *p, p, &p);
        printf(" *p1: %d,  p1: %x, &p1: %x\n", *p1, p1, &p1);//這
        printf("**p2: %d, *p2: %x,  p2: %x, &p2: %x\n", **p2, *p2, p2, &p2);//還有這
}

忽略一大堆的warning之後,看一下輸出結果

$ ./test
a: 0,     &a: fe4f3404
  *p: 0,   p: fe4f3404,  &p: fe4f3408
 *p1: 0,  p1: fe4f3404, &p1: fe4f3410
**p2: 0, *p2: fe4f3404,  p2: fe4f3408, &p2: fe4f3418

還是很整齊的,這裡p1只是p的一個拷貝而已,用處我們留到後面再說

這裡著重看p2,**p2==*p==a, *p2==p==&a, p2==&p

最後一個等式也側面揭示了為什麼定義時需要(int**) p2 = &p;

這裡p2是一個二級指標,p2的值是0xfe4f3408,也即儲存了一級指標p的地址空間(指0xfe4f3408,不指p2)

而一級指標p指向變數a

請結合下圖理解:

於是得出結論,p2指向p,p指向a

論證完畢

這裡有個小問題,&p-&a=4, &p1-&p=2, &p2-&p1=8

留待解決

2.應用

應用,這裡不舉太難的例子了

int a=1;
int b=2;

void change(int *n){
        n = &b;
}

void main(){
        int *p1 = &a;
        printf("before change: *p1 = %d\n", *p1);
        change(p1);
        printf(" after change: *p1 = %d\n", *p1);
}

這裡的change函式進行了一個簡單的賦值操作,使用的是一級指標,注意我們輸出的是*p1的值,來看一下結果:

$ ./test4
before change: *p1 = 1
 after change: *p1 = 1

好像沒有變,突然感覺一級指標教的東西瞬間崩塌了

不!並不是,還記得我們在一級指標中該怎麼寫這段程式碼嗎?

===只看二級指標的話請跳過這部分===

int a=1;
int b=2;

void change(int *n){
        //n = &b;
        *n = b;
}

void main(){
        int *p1 = &a;
        printf("before change: *p1 = %d\n", *p1);
        change(p1);
        printf(" after change: *p1 = %d\n", *p1);
}

註釋的部分是剛才的程式碼

$ ./test4
before change: *p1 = 1
 after change: *p1 = 2

細微的差別造成的不僅僅是結果上的不同,更是功能上的不同;

*n=b,正常來說應該給change傳的引數是&a,即change(&a),這一段程式碼的意思是“把a的值修改為b”

n=&b,也即我們上上段程式碼的寫法,意思是“修改一級指標p1的值為b”

當然以上純屬個人理解,歡迎指正

===跳過部分到此結束===

那麼如何才能修改一級指標p1的值呢?

通過以下程式碼:

int a=1;
int b=2;

void change(int **n){//注意這
        *n = &b;//這
}

void main(){
        int *p2 = &a;
        printf("before change: *p2 = %d\n", *p2);
        change(&p2);//還有這
        printf(" after change: *p2 = %d\n", *p2);
}

我們來分析一下,呼叫change(&p2)時,首先幹了一件事 int **n = &p2

在分析指標的時候,必須得走過彎彎繞繞最後才能達到結論,而且這些繁雜的過程是必須的,掌握指標之前絕不能跳過這些步驟,共勉。

n==&p2,這點應該毫無疑問了,換句話說,n的地址我們不知道(&n),但是n的內容我們是知道的,也就是一級指標p2的地址(&p2)

*n是什麼呢,*和&是逆運算,&取地址,*則順著地址找變數,已知n==&p2,則*n==*(&p2)==p2

那麼p2又是什麼呢?其實關心這個問題沒什麼必要,但為了理解,可以看出p2是a的地址(&a)

**n是什麼呢,是a

以上分析完之後,該看函式體內部了

*n=&b,把b的地址賦值給*n(p2),於是,p2==&b,p2的內容變成了b的地址,此時,指標p2指向變數b

那麼我們輸出*p2,不就是輸出p2指向的那個變數嗎?

因此,以上函式可以成立。

int a=1;
int b=2;

void change(int *n){
        n = &b;
}

void main(){
        int *p1 = &a;
        printf("before change: *p1 = %d\n", *p1);
        change(p1);
        printf(" after change: *p1 = %d\n", *p1);
}

這是我們剛剛的錯誤程式碼,他為什麼會錯?

按照上面的流程分析,呼叫函式時,首先進行int *n = p1

n是誰?n==p1,n的地址未知(&n),n的內容是p1,p1又是誰?p1的地址未知(&p1),p1的內容是a的地址&a

再看函式體內部,n=&b,也就是把n的內容修改為b的地址,然後便沒了下文

p1的地址沒變,a的地址不會變,變的僅僅只有n的內容而已,因為n的內容變了,所以n指向的不再是p1指向的a,而是b

但這又如何呢?僅僅是n指向的內容變了,n和p1沒有半毛錢關聯,p1壓根沒有被修改

關於二級指標的應用,上面舉的例子不是很貼切,之後更新學習筆記會雙向連結一下。

分割線

這一篇是邊學邊寫的,肯定會有很多瑕疵甚至矛盾的地方,敬請指正

學習這些複雜的東西讓我明白一件事,很多腦子裡想清楚的事情不一定真正能想清楚

暫時想清楚了不代表以後能想清楚

寫的過程中也有不少次調bug的經歷,甚至推翻了前面的論點

如果你是一個初學者,建議拿筆寫一寫,然後舉一反三寫程式碼論證一下

真的很有幫助

本來只打算貼三段程式碼,結果最後寫到test7.c了(笑