關於指標 通俗易懂的從零入門篇
- 以夢為碼 -
談到指標,估計是很多人頭疼的一個點吧。
這篇文章儘量用通俗的話來解釋,從零從頭開始,但是無法保證到多麼高深的地方結束,日後遇到可能再來新增吧。
首先我想說的是該篇主要是我的個人理解,也參考了網上一些資料,不保證百分百正確,敬請指正~
另外想跳過一些碎碎念和基礎知識建議直接跳過標題一
目錄
關於指標 通俗易懂的從零入門篇
一、什麼是指標
指標,是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了(笑