搞清楚C語言指標

SD!LTF發表於2020-06-06

Part 0:為什麼要寫這篇文章

C語言中的指標是C語言的精髓,也是C語言的重難點之一。
然而,很少有教程能把指標講的初學者能聽懂,還不會引起歧義。
本文章會嘗試做到這一點,如有錯誤,請指出。

Part 1:地址和&

我們先拋開指標不談,來講一個小故事:

一天,小L準備去找小S玩。但是小L不知道小S的家住在哪裡,正當他著急的時候,他看到了一個路牌,上面寫著:小S的家在神仙小區403

哦,真的是要素過多。為什麼這麼說?

  1. 小L和小S:我們可以看做是兩個變數/常量。
  2. 小S的家:這裡可以看做是變數/常量小S的地址。
    我們要搞清楚,每個變數/常量都和我們一樣:我們每個人都有自己的家,正如變數也有自己的地址。通俗的理解,地址是給變數/常量來存放值的地點
  3. 路牌:注意注意注意!這裡就指出了變數/常量小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;

這麼寫當然沒問題,但是對於初學者來說,有兩個問題:

  1. 有的初學者會把*p當做是指標名
  2. 有的初學者會把定義時出現的*p取值時出現的*p弄混

指標他有沒有值?有!我們會在下一節給他賦值。
既然他的定義方式和變數一樣,他也有值,他為什麼不是變數呢?


Part 3:與指標相關的幾個符號

與指標相關的符號有兩個,一個是&,一個是*
先來聊聊&
&我們上面講過,他是來取地址的。舉個例子:

#include <stdio.h>
int main(){
    int a = 10;
    float b = 10.3;
    printf("%p,%p",&a,&b);
}

%p用來輸出地址,當然,你也可以寫成%d或者%x。先不管這個,我們來看看他會輸出什麼:

那麼也就是說,變數ab的地址是000000000062FE1C000000000062FE18
那麼我們怎麼把這個地址給指標呢?很簡單: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:總結

本文乾貨全部在這裡了:

  1. 指標是個變數,他的型別是資料型別+*,他的值是一個地址,他自身也有地址
  2. 指標有兩個專屬運算子:&*
  3. 指標可以操作變數,不能操作常量
  4. 指標可以表示字串
  5. 請注意野指標的問題

本文沒有講到的:

  1. char[],char,const char的區別與聯絡
  2. const修飾指標會怎麼樣?
  3. void*指標的運用
  4. 多級指標的運用
  5. NULL到底是什麼
  6. malloc函式的運用

感謝觀看!

相關文章