Part 0:為什麼要寫這篇文章
C語言中的指標是C語言的精髓,也是C語言的重難點之一。
然而,很少有教程能把指標講的初學者能聽懂,還不會引起歧義。
本文章會嘗試做到這一點,如有錯誤,請指出。
Part 1:地址和&
我們先拋開指標不談,來講一個小故事:
一天,小L準備去找小S玩。但是小L不知道小S的家住在哪裡,正當他著急的時候,他看到了一個路牌,上面寫著:小S的家在神仙小區403
哦,真的是要素過多。為什麼這麼說?
- 小L和小S:我們可以看做是兩個變數/常量。
- 小S的家:這裡可以看做是變數/常量小S的地址。
我們要搞清楚,每個變數/常量都和我們一樣:我們每個人都有自己的家,正如變數也有自己的地址。通俗的理解,地址是給變數/常量來存放值的地點。 - 路牌:注意注意注意!這裡就指出了變數/常量小S的地址:神仙小區403
事實上,我們等會會講,輸出一個變數的地址其實是個16進位制的數字。
搞懂了上面,我們再來聊聊&
&
這個符號我們一個不陌生,你最初用到應該是在:scanf("%d",&a)
裡邊。
&
叫做取址符,用來獲取一個變數/常量的地址。
那麼我們為什麼要在scanf
裡邊用&
,不在printf
裡邊用呢?
一開始我也很疑惑,後來我看到了這個例子:
你是一個新生,你要進教室。
但是你並不知道教室在哪裡,這個時候你需要教室的地址。
下課了,你要出教室。
由於你已經在教室裡了,你就不需要獲取教室的地址就可以出去了。
Part 2:一定要記住的東西
一定要記住:指標就是個變數!
重要的事情說三次:
指標就是個變數!他儲存的是地址!他自己也有地址!
指標就是個變數!他儲存的是地址!他自己也有地址!
指標就是個變數!他儲存的是地址!他自己也有地址!
為什麼這麼說?我們從指標的定義開始:
指標的定義方法:<型別名+*> [名稱]
也就是說,指標的定義大概是這樣的:
int* ip; //型別是int*,名稱是ip
float* fp; //型別是float*,名稱是fp
double* dp; //型別是double*,名稱是dp
有的書上會這麼寫:
int *ip;
float *fp;
double *dp;
這麼寫當然沒問題,但是對於初學者來說,有兩個問題:
- 有的初學者會把
*p
當做是指標名 - 有的初學者會把定義時出現的
*p
和取值時出現的*p
弄混
指標他有沒有值?有!我們會在下一節給他賦值。
既然他的定義方式和變數一樣,他也有值,他為什麼不是變數呢?
Part 3:與指標相關的幾個符號
與指標相關的符號有兩個,一個是&
,一個是*
。
先來聊聊&
。
&
我們上面講過,他是來取地址的。舉個例子:
#include <stdio.h>
int main(){
int a = 10;
float b = 10.3;
printf("%p,%p",&a,&b);
}
%p
用來輸出地址,當然,你也可以寫成%d
或者%x
。先不管這個,我們來看看他會輸出什麼:
那麼也就是說,變數a
和b
的地址是000000000062FE1C
和000000000062FE18
那麼我們怎麼把這個地址給指標呢?很簡單:p = &a;
,舉個例子:
#include <stdio.h>
int main(){
int a = 10;
int* p;
p = &a;
printf("a的地址:%p\n",&a);
printf("指標p自身的地址:%p\n",&p);
printf("指標p指向的地址:%p",p);
}
得到輸出:
a的地址:000000000062FE1C
指標p自身的地址:000000000062FE10
指標p指向的地址:000000000062FE1C
你發現了嗎?如果我們有p = &a;
,我們發現:直接輸出p會輸出a的地址,輸出&p會輸出p的地址(這就是為什麼我一再強調p是個變數,他有自己的地址,正如路牌上有地址,路牌自身也有個地址一樣)。
請注意!如果你的指標為int*
,那麼你只能指向int
型別;如果是double*
型別,只能指向double
型別,以此類推
當然,void*
型別的指標可以轉化為任何一種不同的指標型別(如int*
,double*
等等)
那麼,我們來聊聊第二個符號*
*
有兩個用法。第一個在定義指標時用到,第二個則是取值,什麼意思?看下面這個例子:
#include <stdio.h>
int main(){
int a = 10;
int* p;
p = &a;
printf("a的地址:%p\n",&a);
printf("指標p自身的地址:%p\n",&p);
printf("指標p指向的地址:%p\n",p);
printf("指標p指向的地址的值:%d",*p);
}
得到輸出:
a的地址:000000000062FE1C
指標p自身的地址:000000000062FE10
指標p指向的地址:000000000062FE1C
指標p指向的地址的值:10
哈,我們得到了a的值!
也就是說,當我們有p = &a
,我們可以用*p
得到a的值。
那能不能操作呢?當然可以。
我們可以把*p
當做a的值,那麼,我們嘗試如下程式碼:
#include <stdio.h>
int main(){
int a = 10;
int* p;
p = &a;
printf("指標p指向的地址的值:%d\n",*p);
*p = 13;
printf("指標p指向的地址的值:%d\n",*p);
*p += 3;
printf("指標p指向的地址的值:%d\n",*p);
*p -= 3;
printf("指標p指向的地址的值:%d\n",*p);
*p *= 9;
printf("指標p指向的地址的值:%d\n",*p);
*p /= 3;
printf("指標p指向的地址的值:%d\n",*p);
*p %= 3;
printf("指標p指向的地址的值:%d\n",*p);
}
得到輸出:
指標p指向的地址的值:10
指標p指向的地址的值:13
指標p指向的地址的值:16
指標p指向的地址的值:13
指標p指向的地址的值:117
指標p指向的地址的值:39
指標p指向的地址的值:0
棒極了!我們可以用指標來操作變數了。
那麼,我們要這個幹什麼用呢?請看下一節:實現交換函式
Part 4:交換函式
交換函式是指標必學的一個東西。一般的交換我們會這麼寫:
t = a;
a = b;
b = t;
那麼我們把它塞到函式裡邊:
void swap(int a,int b){
int t;
t = a;
a = b;
b = t;
}
好,我們滿懷信心的呼叫他:
#include <stdio.h>
void swap(int a,int b){
int t;
t = a;
a = b;
b = t;
}
int main(){
int x = 5,y = 10;
printf("x=%d,y=%d\n",x,y);
swap(x,y);
printf("x=%d,y=%d",x,y);
}
於是乎,你得到了這個輸出:
x=5,y=10
x=5,y=10
啊啊啊啊啊啊啊啊,為什麼不行!!!
問題就在你的swap函式,我們來看看他們做了些啥:
swap(x,y); --->把x賦值給a,把y賦值給b
///進入函式體
int t; --->定義t
t = a; --->t賦值為a
a = b; --->a賦值為b
b = t; --->b賦值為t
各位同學,函式體內有任何一點談到了x和y嗎?
所謂的交換,交換的到底是a和b,還是x和y?
我相信你這時候你恍然大悟了,我們一直在交換a和b,並沒有操作x和y
那麼我們怎麼操作?指標!
因為x和y在整個程式中的地址一定是不變的,那麼我們通過上一節的指標運算可以得到,我們能夠經過指標操作變數的值。
那麼,我們改進一下這個函式
void swap(int* a,int* b){
int t;
t = *a;
*a = *b;
*b = t;
}
我們再來試試,然後你就會得到報錯資訊。
我想,你是這麼用的:swap(x,y)
。
問題就在這裡,我們看看swap需要怎樣的兩個變數?int*
和int*
型別。
怎麼辦?我告訴你一個小祕密:
任何一個變數加上&,此時就相當於在原本的型別加上了*
什麼意思?也就是說:
int a;
&a ---> int*;
double d;
&d ---> double*;
int* p;
&p ---> int**;//這是個二級指標,也就是說指向指標的指標
那麼,我們要這麼做:swap(&a,&b)
,把傳入的引數int
換為int*
再次嘗試,得到輸出:
x=5,y=10
x=10,y=5
累死了,總算是搞好了
Part 5:char*表示字串
char*這個神奇的型別可以表示個字串,舉個例子:
#include <stdio.h>
int main()
{
char* str;
str = "YOU AK IOI!";
printf("%s",str);
}
請注意:輸入和輸出字串的時候,都不能帶上*
和&
。
你可以用string.h
中的函式來進行操作
Part 6:野指標
有些同學他會這麼寫:
int* p;
printf("%p",p);
哦千萬不要這麼做!
當你沒有讓p指向某個地方的時候,你還把他用了!這個時候就會產生野指標。
野指標的危害是什麼?
第一種是指向不可訪問(作業系統不允許訪問的敏感地址,譬如核心空間)的地址,結果是觸發段錯誤,這種算是最好的情況了;
第二種是指向一個可用的、而且沒什麼特別意義的空間(譬如我們曾經使用過但是已經不用的棧空間或堆空間),這時候程式執行不會出錯,也不會對當前程式造成損害,這種情況下會掩蓋你的程式錯誤,讓你以為程式沒問題,其實是有問題的;
第三種情況就是指向了一個可用的空間,而且這個空間其實在程式中正在被使用(譬如說是程式的一個變數x),那麼野指標的解引用就會剛好修改這個變數x的值,導致這個變數莫名其妙的被改變,程式出現離奇的錯誤。一般最終都會導致程式崩潰,或者資料被損害。這種危害是最大的。
不論如何,我們都不希望看到這些發生。
於是,養成好習慣:變數先賦值。
指標你可以這麼做:int *p =NULL;
讓指標指向空
不論如何,他總算有個值了。
Part 7:總結
本文乾貨全部在這裡了:
- 指標是個變數,他的型別是
資料型別+*
,他的值是一個地址,他自身也有地址 - 指標有兩個專屬運算子:
&
和*
- 指標可以操作變數,不能操作常量
- 指標可以表示字串
- 請注意野指標的問題
本文沒有講到的:
- char[],char,const char的區別與聯絡
- const修飾指標會怎麼樣?
- void*指標的運用
- 多級指標的運用
- NULL到底是什麼
- malloc函式的運用
感謝觀看!