2016年6月1日 星期三

給新手的C++教學 (上冊) - 12. 指標 (Pointer)

回到「給新手的C++教學 (上冊)」

上一章

這一章,我們將更深入地探討電腦的運作原理
進而更完善的掌控電腦的記憶體

你會發現,你將會有能力控制更多平常想不到的東西!

你有想過嗎?
當我們宣告一個變數的時候,電腦會撥出一些記憶體來讓程式儲存變數的資訊
但是,電腦的記憶體是有限的,總不可能每一塊記憶體都只有使用一次吧?
這樣的話每一台電腦的記憶體大概都只夠用10分鐘了XD
因此,允許記憶體被重複使用的機制是必要的

為了讓記憶體能夠被重複使用,我們必須確認用過的記憶體中,哪些現在還在使用中、哪些已經使用完畢了
那麼,我們的程式當然只能使用那些已經使用完畢的記憶體,否則修改到其他程式正在使用的記憶體,進而導致其他程式出錯

觀念簡單,但是要有效率地做到這件事 (避免每次找記憶體前都要老老實實的把每一塊記憶體都確認一遍),需要許多先進的演算法知識,而且程式碼寫起來非常的麻煩

放心,這件事連我都不知道怎麼做
所謂「站在巨人的肩膀上」,這種事情不用再由我們自己處理了!
在先人的努力之下,「C++」這個偉大的程式語言,已經讓電腦可以只依據簡單的幾行程式碼,就可以執行許多複雜卻需要經常執行的工作
換句話說
我們在撰寫C++程式碼的時候,只需要告訴電腦「需要使用多少記憶體」和「哪些記憶體已經使用完畢」就好了!

我們在宣告變數的時候,就等於是告訴電腦「恩,我需要這麼多記憶體來儲存這一個變數的資訊」,然後電腦就會把你要的記憶體給你

但是,我們有告訴過電腦「哪些記憶體已經使用完畢」嗎?
有的!

還記得在第十章的時候,我們有提到「每一個變數都有它自己的作用範圍」嗎?
那麼,當正在執行的程式碼位於一個變數的作用範圍外,這個變數所使用的記憶體該怎麼辦呢?
這時我們不會需要用這個變數來儲存任何資訊
也就是,這個變數目前不需要使用記憶體來儲存資訊

電腦學過C++,想當然會利用這個「作用範圍外記憶體就不需要了」的特性
不管是甚麼變數,只要程式執行到它的「作用範圍」之外
電腦就會「自動」把這個變數使用的記憶體當作「使用完畢」
也就是說,現在,這塊記憶體可能是閒置的,或者被其他的程式拿去使用了
如果我們再去修改這塊記憶體,會造成不可預知的錯誤
當然,你還沒開始學習本章的內容,不會知道怎麼在作用範圍外修改這塊記憶體

在作用範圍外修改記憶體?
有需要嗎?
是的,有時候會很需要,請看本章範例
感覺很危險?
是的,使用不當會造成其他程式的錯誤,甚至讓整台電腦當機,必須將電腦插頭拔掉再重新開機

因此,在學習本章前,請務必將重要資料存檔,以避免造成不必要的損失

嘿嘿嘿~
現在
我們開始吧XD



你知道怎麼「交換兩個變數的資訊」嗎?
我們就來試試看吧~

#include<cstdio>
int main()
{
    int a=3,b=7;
    printf("交換前a=%d, b=%d\n",a,b);
    int t=a;
    a=b;
    b=t;
    printf("交換後a=%d, b=%d\n",a,b);
    return 0;
}

我們宣告的兩個變數a、b,它們的資訊被交換了!
(為了方便理解,建議將變數「c改名為「t)
你玩過猜球遊戲嗎?
魔術師會把三個相同的杯子蓋起來,並把一顆球藏進中間那個杯子
然後用迅雷不及掩耳的速度多次快速隨機交換其中兩個杯子
最後要你猜球會在哪個杯子
↓猜球遊戲↓ (警告:影片有聲音)
現在,請你寫一個程式來模擬這個過程
一開始魔術師有三個杯子,球在中間那個杯子裡面
接著會有$n$次交換的動作
請你找出最後球在哪個杯子

輸入格式:
首先會有一個整數$n$,代表有幾次操作
接下來會有$n$個數字,代表每一次操作的種類
$1$代表交換左邊和中間的杯子
$2$代表交換左邊和右邊的杯子
$3$代表交換中間和右邊的杯子

輸出格式:
輸出「左邊」、「中間」或「右邊」,代表球最後會在哪一個杯子裡面

例如:
輸入「5 3 2 1 2 3」,會輸出「右邊」
輸入「6 2 3 1 1 3 1」,會輸出「左邊」
輸入「7 1 1 1 3 2 2 1」,會輸出「中間」

趕快自己做做看吧~

提示:可以利用剛剛學的「交換兩個變數的資訊」的技巧

範例程式碼:

#include<cstdio>
int main()
{
    int n;
    scanf("%d",&n);
    int a=0,b=1,c=0;
    int i=0;
    while(i<n)
    {
        int type;
        scanf("%d",&type);
        if(type==1)
        {
            int t=a;
            a=b;
            b=t;
        }
        else if(type==2)
        {
            int t=a;
            a=c;
            c=t;
        }
        else if(type==3)
        {
            int t=b;
            b=c;
            c=t;
        }
        else
        {
            printf("你輸入錯誤了XD");
        }
        i=i+1;
    }
    if(a==1) printf("左邊\n");
    else if(b==1) printf("中間\n");
    else if(c==1) printf("右邊\n");
    else printf("怎麼可能?一定是程式寫錯了Orz\n");
    return 0;
}

對於輸入「5 3 2 1 2 3」,程式正確的輸出「右邊
對於輸入「6 2 3 1 1 3 1」,程式正確的輸出「左邊」
對於輸入「7 1 1 1 3 2 2 1」,程式正確的輸出「中間」

你可能會發現,程式碼好冗長啊~
我們的程式碼重複做了三遍「交換兩個變數的資訊」的動作
就寫成一個函式吧~
對了,如果一個函式不需要回傳任何東西,應該用「void (無值)」這個型別:

#include<cstdio>
void Exchange(int x,int y)
{
    int t=x;
    x=y;
    y=t;
}
int main()
{
    int n;
    scanf("%d",&n);
    int a=0,b=1,c=0;
    int i=0;
    while(i<n)
    {
        int type;
        scanf("%d",&type);
        if(type==1)
        {
            Exchange(a,b);
        }
        else if(type==2)
        {
            Exchange(a,c);
        }
        else if(type==3)
        {
            Exchange(b,c);
        }
        else
        {
            printf("你輸入錯誤了XD");
        }
        i=i+1;
    }
    if(a==1) printf("左邊\n");
    else if(b==1) printf("中間\n");
    else if(c==1) printf("右邊\n");
    else printf("怎麼可能?一定是程式寫錯了Orz\n");
    return 0;
}

好吧,我承認程式碼還是41行
不過,至少程式碼的邏輯清楚多了,對吧?
測試一下,確定程式輸出的結果是正確的!

對於輸入「5 3 2 1 2 3」,程式錯誤的輸出「中間」
對於輸入「6 2 3 1 1 3 1」,程式錯誤的輸出「中間」
對於輸入「7 1 1 1 3 2 2 1」,程式正確的輸出「中間」

等等,怎麼三種情況都輸出「中間」?
根本形同沒有交換過嘛!

仔細想想,當我們執行「Exchange(a,b);」的時候
實際上效果會等同於以下四行程式碼:

int x=a,y=b;
int t=x;
x=y;
y=t;

雖然剛開始$x=a$、$y=b$
可是當我們交換了$x$和$y$之後,原本真正想要交換的$a$和$b$根本不受影響啊!
我們交換了$x$和$y$後,$a$和$b$還是不變,形同做白工


難道這種東西就不能寫成一個函式嗎?
當然可以!

注意到,「函式」其實是在$a$和$b$的「作用範圍」之外的
(這時候電腦不會把$a$和$b$的記憶體當作「使用完畢」,因為「Exchange(a,b);」這一行程式碼還是「執行中」的狀態,而且位於$a$和$b$的「作用範圍」裡面)
因此,我們必須想辦法在「函式的位置」直接修改「$a$和$b$這兩個變數所使用的記憶體」

方法是使用「指標 (Pointer)」

其實呢,我也不知道為甚麼這個東西叫做指標
反正原理就是把電腦的每一塊記憶體都指定一個編號
然後程式就可以依照一個編號去找到要存取的那塊記憶體了
這種「記憶體編號」就是所謂的「指標」

要怎麼取得一個變數的「指標」(就是這個變數使用的記憶體的編號) 呢?
假設你的變數名稱叫做「a」
那麼它的指標就是「&a」
也就是在前面加一個「&」就好了
簡單吧!

但是請注意,雖然「指標」是一種「編號」,不過它的「型別」並不是我們常用來儲存整數的「int」
更正確地來講,因為每台電腦的配備都不同,「指標」的儲存方式也會不同!
對於32位元的電腦來說,一個「指標」是一個「32位元」的整數 (也就是我們熟悉的『int』)
對於64位元的電腦來說,一個「指標」是一個「64位元」的整數 (也就是使用64位元的記憶體來儲存資料的『進階版int』--『long long』)

總之,你不能把「指標」當作「int」或「long long」
那麼,它的型別是甚麼呢?

其實很簡單
假如你是從一個「int」變數來取得指標,它的型別就是「int*」
假如你是從一個「float」變數來取得指標,它的型別就是「float*」
假如你是從一個「char」變數來取得指標,它的型別就是「char*」

舉個例子:

int a;
int* b=&a;

這樣一來,「b」就是「a的指標」

需要特別注意的是,當你想要「一次宣告多個指標」的時候
你應該在每個指標名稱前面都加上「*」
我也覺得滿奇怪的,不知道為甚麼這樣規定 (感謝學長補充,見註8)
因此,個人不建議上面的寫法,我會習慣寫成以下的形式:

int a,b,c;
int *d=&a,*e=&b,*f=&c;

也就是說,把每一個「*」都緊鄰著指標名稱
這樣一來,「d」就是「a的指標」、「e」就是「b的指標」、「f」就是「c的指標」
個人認為這樣的程式碼會工整許多,而且還附帶提醒用途 (提醒你宣告指標時,每個指標名稱前面都要有「*」)

好了,我們現在可以想出「交換兩個變數的資訊」的函式的大致架構了:

void Exchange(int *x,int *y)
{
    想辦法交換「x」和「y」所代表的兩塊記憶體的資訊
}

然後函式的使用方式就是:

Exchange(&a,&b);

現在,假設你有了「int *b=&a;」,要怎麼透過「b」來修改「a的記憶體」呢?
方法是在「b」前面加上「*」

注意,這和「宣告指標」時「int *b=&a;」的作用不同
在「b」前面加上「*」之後
你就可以把「*b」直接當作一個「int變數」來存取或修改了!

你看你看~~~
「*」是不是很像射飛鏢在用的「箭靶」呢?

這是「箭靶」,有三支「飛鏢」射中箭靶的中心
圖片來源:http://img3.redocn.com/20130429/Redocn_2013042413460991.jpg

「指標」就像一支「飛鏢」一樣
「指標」儲存的資訊 (記憶體編號) 就是這支「飛鏢」指向哪裡
當你寫下「int *b=&a;」的時候,「b」這支「飛鏢」就指向了「a」這個「箭靶」
當你寫下「*b」的時候,「*b」就代表「b指向的箭靶」了,也就是「a」!

具體使用方式請參考以下程式碼:

#include<cstdio>
int main()
{
    int a=2;
    printf("修改前a=%d\n",a);
    int *b=&a;
    *b=4;
    printf("修改後a=%d\n",a);
    return 0;
}

我們透過「a」的指標「b」,成功修改「a」的數值了!
所以,我們就在「*b=4;」這一行程式碼中
把「b指向的箭靶」修改成「4」了!

現在,你知道要怎麼寫出「交換兩個變數的資訊」的函式了嗎?

如果還想不出來,以下提供參考,希望你會恍然大悟 (如果還是想不通,請務必讓我知道):

void Exchange(int *x,int *y)
{
    int t=*x;
    *x=*y;
    *y=t;
}



現在,我們來嘗試寫一個「函式版」的「猜球遊戲模擬程式」吧!

#include<cstdio>
void Exchange(int *x,int *y)
{
    int t=*x;
    *x=*y;
    *y=t;
}
int main()
{
    int n;
    scanf("%d",&n);
    int a=0,b=1,c=0;
    int i=0;
    while(i<n)
    {
        int type;
        scanf("%d",&type);
        if(type==1)
        {
            Exchange(&a,&b);
        }
        else if(type==2)
        {
            Exchange(&a,&c);
        }
        else if(type==3)
        {
            Exchange(&b,&c);
        }
        else
        {
            printf("你輸入錯誤了XD");
        }
        i=i+1;
    }
    if(a==1) printf("左邊\n");
    else if(b==1) printf("中間\n");
    else if(c==1) printf("右邊\n");
    else printf("怎麼可能?一定是程式寫錯了Orz\n");
    return 0;
}

試試看,確認程式的輸出是正確的!

對於輸入「5 3 2 1 2 3」,程式正確的輸出「右邊」
對於輸入「6 2 3 1 1 3 1」,程式正確的輸出「左邊」
對於輸入「7 1 1 1 3 2 2 1」,程式正確的輸出「中間」

耶!完全正確!
我們成功了!\(^o^)/

最後,來個小提醒
不管你玩C++玩得多深
千萬不要寫出類似下面危險的程式碼:

#include<cstdio>
int main()
{
    int *a=(int*)32405;
    *a=911;
    return 0;
}

其中,「(int*)」代表強制用後面的「32405」產生一個型別為「int*」的變數
誰知道編號為「32405」的記憶體是幹嘛用的啦
你把這塊記憶體的資料修改成「911」
可能沒事
可能「IE」的名字被顯示成「JE」
可能你正在玩的鑽礦遊戲Digging Game 2掛惹
可能半小時後才出事
可能整台電腦大當機

好啦,你硬要試試看
我只能說你很有實驗精神
但是
請先將重要資料存檔
並隨時做好「必須將電腦插頭拔掉才能重新開機」的心理準備

下一章 (這還不是最後一章)

感謝:李旺陽學長
(版權所有 All copyright reserved)

26 則留言:

  1. 所以如果真的當機重開後就會好嗎?

    回覆刪除
  2. 是不是有一種比較簡單的方式是傳參考呢?

    回覆刪除
    回覆
    1. 是的沒錯,只是指標可以做到傳參考做不到的事哦~

      刪除
    2. 像是哪些事是傳參考做不到的呢?

      刪除
  3. 想請問一下
    # define A 3; 跟
    const int A = 3;
    有甚麼差別嗎?
    會建議使用哪一個呢?

    回覆刪除
    回覆
    1. 是有差的
      define會把所有符合的字串置換掉,例如#define push_back PB之後s.push_back(v)就可以寫成s.PB(v)
      而const int只是宣告一個不能修改的變數而已
      因為define的影響範圍廣傷維護性,小莫自己的原則是能用const就用const

      刪除
  4. 你好呀,本人嘗試了以下編碼,想大大給點意見 :
    #include
    int main()
    {
    while(2>1)
    {
    int n;
    scanf("%d",&n);
    if(n==1)
    {
    printf("Left\n");
    }
    else if(n==2)
    {
    printf("Center\n");
    }
    else if(n==3)
    {
    printf("Right\n");
    }
    else if(n>3)
    {
    printf("Wrong input\n");
    }
    }
    }

    回覆刪除
    回覆
    1. 請問您想要甚麼樣子的建議呢?小莫真的不知道耶XD
      然後code的部分可以參考這篇來修正排版哦~

      刪除
  5. 現在系統不是都有虛擬記憶體嗎?
    電腦會當機嗎?!

    回覆刪除
    回覆
    1. 會不會因為指標存取錯誤導致電腦當機應該跟有沒有虛擬記憶體無關吧?
      不過在Windows底下好像記憶體會用權限保護的樣子,如果程式沒有用系統管理員權限執行的話通常不會造成這種悲劇

      刪除
  6. 想問傳參考是不是c++獨有的呢?
    c是不是沒有?
    建議使用傳參考還是傳指標呢?

    回覆刪除
    回覆
    1. 是的,傳參考是C++獨有的哦~
      建議能用參考就用參考
      但是,有些事情是參考做不到的,所以指標也很重要哦~^_^

      刪除
    2. 可是這篇好像都只有教傳指標(?

      刪除
    3. 哈哈,原本的確是有計畫要撰寫關於「參考(參照)」的教學哦~
      可以在這裡找找看(?

      刪除
  7. 回覆
    1. C和C++的確是很像的語言~
      但請仔細看,它是C++哦XD

      刪除
  8. 真的超級感謝你><
    我終於看懂指標是甚麼了

    回覆刪除

歡迎留言或問問題~
若您的留言中包含程式碼,請參考這篇
如果留言不見了請別慌,那是因為被google誤判成垃圾留言,小莫會盡快將其手動還原

注意:只有此網誌的成員可以留言。