[基礎概念] 指標 (1):認識一下指標是什麼樣的東西

聽說指標是很多人在學 C 或 C++ 的痛,但我覺得好像跟初次教你這個概念的老師有關。我非常受不了有些老師在教某個大魔王單元之前會先恐嚇學生說:這超難喔不認真會死掉。我相信很多人數學(或是任一科目)都毀在這種老師手裡,希望各位老師明白教學方式和吸收成果因人而異,打這種預防針只會有反效果。在此寫下我理解的指標概念,希望可以教教未來金魚腦的我XDD

為什麼要用指標

傳遞陣列或字串更有效率、較複雜的資料結構需用指標 linked list

指標的概念

指標可以看成是一種特殊的變數

指標(pointer)宣告方式為:

資料型別 *變數名稱
ex: int *ptr,意思是宣告指向整數的指標變數。

或是

資料型別* 變數名稱
ex: int* ptr,意思是宣告型態為「指向整數之指標」的變數 ptr

指標簡單來說可以想像成很多東西,例如以隨便的地址為例子好了。
住址:建國路 1 號
屋子裡面的人:Alex

如果我宣告了一個指標變數 *p,那大概會是這樣:
p = 住址:建國路 1 號
*p = 屋子裡面的人:Alex

以比較專業一點的講法,如果我寫了 int *p,表示我宣告了一個指標變數:
p 代表一個記憶體的位址
*p 代表這個記憶體位址的資料內容,並且這個資料內容的型態是 int

位址列印示範

& 是位址符號,以上述例子來說,&a 表示取得該變數的位址,也就是「建國路 1 號」。記憶體位置和指標運作息息相關,我們可以藉由下方程式碼實現列印實際存放位置,並觀察變化:

#include <stdio.h>
#include <stdlib.h>

int main(void){
    int a;
    int b=5;
    double c=2.78;
    
    printf("a=%4d, 佔據 %d 個位元組, 位址為%p \n", a, sizeof(a), &a);
    printf("b=%4d, 佔據 %d 個位元組, 位址為%p \n", b, sizeof(b), &b);
    printf("c=%4.2f, 佔據 %d 個位元組, 位址為%p \n", c, sizeof(c), &c);
}

output
a= 0, 佔據 4 個位元組, 位址為0x7ffe92729cec
b= 5, 佔據 4 個位元組, 位址為0x7ffe92729ce8
c=2.78, 佔據 8 個位元組, 位址為0x7ffe92729ce0

再度說明符號

&:位址運算子,用來取得變數的位址。
*:依位址取值運算子,依照位址取得指向的變數內容。

例如我現在宣告 int a = 123,在電腦內表示如下,&a 就是取出 a 的位址 0x7ffe

例外我們也能把這個位址儲存在一個變數(指標變數)內:

int a = 123
int *ptr = &a

上述意思一樣是宣告一個型態為 inta 變數,並且把 a 的位址存進去 ptr 內。之後使用 *ptr 可以取出 a 的內容,也就是 123

當然,指標變數也會有自己的位址:

#include <stdio.h>
#include <stdlib.h>

int main(void){
    int *ptr, n = 20;
    ptr = &n;
    
    printf("n=%d, &n=%p \n", n, &n);
    printf("*ptr=%d, ptr=%p, &ptr", *ptr, ptr, &ptr);
    
    return 0;
}

output
n=20, &n=0x7fffa9ce246c
*ptr=20, ptr=0x7fffa9ce246c, &ptr=0x7fffa9ce2470

也可以在過程中更改指標變數指向的對象,例如下方程式碼原先將 ptr 指向 a,後來改成指向 b,可以觀察一下兩者變化:

#include <stdio.h>
#include <stdlib.h>

int main(void){
    int a=11, b = 999999, *ptr;
    
    ptr = &a;
    printf("&a=%p, &ptr=%p, ptr=%p, *ptr=%d \n", &a, &ptr, ptr, *ptr);
    
    ptr = &b;
    printf("&b=%p, &ptr=%p, ptr=%p, *ptr=%d \n", &b, &ptr, ptr, *ptr);
    
    return 0;
}

output
&a=0x7ffedb27e45c, &ptr=0x7ffedb27e450, ptr=0x7ffedb27e45c, *ptr=11
&b=0x7ffedb27e458, &ptr=0x7ffedb27e450, ptr=0x7ffedb27e458, *ptr=999999

指標、函數與一些操作

上述都算是簡單宣告的例子,而指標也很常用在函數當中。下方是寫一個列印出位址和內容的函數:

#include <stdio.h>
#include <stdlib.h>

void show(int *);

int main(void){
    int a = 5;
    int *ptr = &a;
    
    show(&a);
    show(ptr);
    
    return 0;
}

void show(int *p){
    printf("在位址%p內,內容為%d \n", p, *p);
}

這裡要注意的是,函數中的變數是用位址。

output
在位址0x7ffc4a740854內,內容為5
在位址0x7ffc4a740854內,內容為5

傳遞指標

這邊就可以很明顯看到直接以位址取得變數,並直接改變該變數的樣子:

#include <stdio.h>
#include <stdlib.h>

void add1(int *);

int main(void){
    int a = 99;
    printf("呼叫之前 a=%d \n", a);
    add1(&a);
    printf("呼叫之後 a=%d \n", a);
    
    return 0;
}

void add10(int *p){
    *p = *p + 10;
}

output

呼叫之前 a=99
呼叫之後 a=109

交換

#include <stdio.h>
#include <stdlib.h>

void swap(int *, int *);

int main(void){
    int a = 99, b = 1;
    printf("swap之前 a=%d, b=%d \n", a, b);
    swap(&a, &b);
    printf("swap之後 a=%d, b=%d \n", a, b);
    
    return 0;
}

void swap(int *p1, int *p2){
    int tmp = *p1;
    *p1 = *p2;
    *p2 = tmp;
}

output
swap之前 a=99, b=1
swap之後 a=1, b=99

下方程式也是差不多的例子,利用

#include <stdio.h>
#include <stdlib.h>

void rec(int, int, int *, int *);

int main(void){
    int a = 2, b = 7;
    int area, peri;
    
    rec(a, b, &area, &peri);
    printf("area=%d, length=%d", area, peri);
    
    return 0;
}

void rec(int x, int y, int *area, int *peri){
    *area = x*y;
    *peri = 2*(x + y);
}

output
area=14, length=18

Call by value & Call by reference

也不怕大家笑,我面試的時候真的被這題經典題問倒XD

Call by value

直接拿程式碼當作例子。我們寫了一個函式 add,希望能夠把 a+b 的結果存進去 a

#include <stdio.h>
#include <stdlib.h>

void add(int a, int b) {
    a = a + b;
}

int main() {
    int a = 99;
    int b = 1;
    
    add(a, b);
    printf("a = %d", a);
    
    return 0;
}

原本我們預期會列印出 100,但是因為是傳值進去,因此只有改變當時的數值,並不會改變變數 a 本身。

這算是一開始學程式時,最常運用到的方式。但若在大型程式中,使用結束後沒有刪除未使用的記憶體空間,很可能有記憶體不足的狀況,嚴重一些會導致程式崩潰。

另外如果使用超大陣列,例如宣告了 int array [9999999],會花時間在複製資料,效能會比較低落。

Call by reference

有了上述錯誤的教訓,我們試圖將程式碼改成正確的:

#include <stdio.h>
#include <stdlib.h>

void add(int *a, int *b) {
    *a = *a + *b;
}

int main() {
    int a = 99;
    int b = 1;
    
    add(&a, &b);
    printf("a = %d", a);
    
    return 0;
}

因為引用了變數本身(而不是數值),所以結果成功地輸出 100 了。

另外嚴格上來說 C 語言沒有 call by reference,它在運作上算是把位址當作 value 使用進行 call by value。而為了理解方便,我們會將它叫做 call by address 或是 call by pointer,嚴格來說這不是一個正統的說法,但為了容易理解,所以有這個名稱。

那麼實際上的 call by reference 要怎麼操作呢?一樣以上述例子,改用 C++ 來寫:

#include <stdio.h>
#include <stdlib.h>

void add(int &a, int &b) {
    a = a + b;
}

int main() {
    int a = 99;
    int b = 1;
    
    add(a, b);
    printf("a = %d", a);
    
    return 0;
}

結論

其實指標的概念真的不難,但非常建議要實際操作看看。

讓我知道你在想什麼!