<span class="bsf-rt-reading-time"><span class="bsf-rt-display-label" prefix="閱讀時間:"></span> <span class="bsf-rt-display-time" reading_time="6"></span> <span class="bsf-rt-display-postfix" postfix="mins"></span></span><!-- .bsf-rt-reading-time -->【C語言】指標 (Pointer) 是什麼?10個必懂觀念

【C語言】指標 (Pointer) 是什麼?10個必懂觀念

想當年,我大學時期在上程式設計課程的時候,遇到的第一個最難懂的概念沒有之一,就是我們今天要介紹的主角,不但在C語言當中扮演著舉足輕重的角色,更是C語言在硬體系統中的效能可以那麼強大的幕後推手 — 指標 (Pointer)

相信一定很多人對指標都是處於一個霧煞煞的狀態,沒關係,我當年第一次接觸的時候是霧到看不見的狀態,所以別氣餒,繼續看下去就會守得雲開見月明了!

那麼廢話不多說,開始來介紹指標吧!

指標 (pointer) 是什麼?

直白地來說,就是記憶體位址。

指標背後的實際原理究竟是?

讓我先來打個比喻!

每間房子都會有一個地址,而這個地址就會對應到某一戶人家,指標儲存的就是地址,然後我們會透過這個地址去找到住在這裡的那一戶人家,這就是指標的概念!

在C語言當中,我們主要是利用指標來間接存取指標所指向的記憶體位址的值。

還是有點抽象對不!

來看看下面這張圖:

指標

假設有一個名為 ptr 的指標,指向了 0x7554632 的記憶體位址(房屋地址),而那個位址所對應到的值是 34(某戶人家),這樣就可以透過 ptr 目前所指的記憶體位址,來存取當中的值了!

如何使用指標?

在這之前,我們先來聊聊2個重要的概念

概念1:變數會儲存在記憶體當中。

概念2:儲存在記憶體中的變數都會有一個專屬的記憶體位址。

大家還記得指標是用來儲存記憶體位址的嗎!

該如何取得變數的記憶體位址呢?

利用 & (AND)來取得記憶體的位址。

int number = 34; // 宣告整數變數 -> number

printf("%d\n", number); // 印出number的值
printf("%p\n, &number); // 印出number的記憶體位址(16進位)

輸出會像這樣:

34
0x7554632

那,取得記憶體位址之後呢?

當然就是去那個位址取值啦!

重頭戲來了,要怎麼取值?

利用 * (星號)來取得記憶體位址當中的值。

int number = 34; // 宣告整數變數 -> number

printf("%d\n", number); // 印出number的值
printf("%p\n, *&number); // 印出number的記憶體位址當中的值

大家覺得輸出會變成如何呢?

30
30

指標變數的宣告方式!

利用 * (星號)來宣告指標變數。

int number = 34;
int *ptr = 0;

ptr = &number; // 將ptr指標指向 number 陣列的記憶體位址
printf("%d\n", *ptr); // 取出ptr指向的記憶體位址(也就是number)的值
printf("%p\n", ptr); // 印出ptr的記憶體位址

輸出:

34
0x7554632

指標跟指標變數差別在哪?

指標:記憶體位址

指標變數:用來儲存指標的變數

宣告指標變數跟一般宣告變數是一樣的,只是資料型別 (Data type) 是指標。

以上面的例子來說,ptr 就是指標變數,而 0x7554632 就是指標。

這樣大家懂了嗎!

指標需要初始化嗎?該怎麼做?

那是一定要的!

為什麼一定要初始化指標?

當我們宣告了一個儲存未知記憶體位址的指標,而且沒有給予初始值,這時候如果去做存取的動作,就會發生 Segmentation fault(記憶體區段錯誤)。

初始化指標怎麼做?

情況#1:如果一開始不打算儲存任何位址,那麼可以:

int *ptr = 0;

int *ptr = NULL;

情況#2:有指定的位址

直接 assign 過去即可!

空指標 (NULL pointer) 是什麼?會指向哪裡?

空指標不指向任何位址。

空指標也是用來初始化指標的一種方式,以防指標儲存的是未知的位址。

初始化方式和上面一樣!

int *ptr = NULL;

*** 利用空指標來初始化是最常見的方式喔 ***

宣告時還不確定資料型態怎麼辦?

利用 void 來宣告指標。

Void pointer 一樣會佔有記憶體空間,只是還沒有確定資料型態。

基本上,Void pointer 可以變成任何型態,只要我們將它強制轉型過去即可!

來看個例子:

int number = 5;
float salary = 345.85;

void *ptr = NULL;

// void pointer 強制轉型成 int pointer
ptr = &number;
printf("%d\n", *( (int *) ptr) );

// void pointer 強制轉型成 float pointer
ptr = &salary;
printf("%f\n", *( (float *) ptr );

輸出:

5
345.85

*** 重要:還沒有轉型前的 Void pointer 是不能取值的哦 ***

陣列 (Array) 與指標 (Pointer) 之間的關係是?

在C語言裡面,陣列的背後就是用指標來實作的。

什麼意思呢?

pointer

假設我們宣告了一個陣列 number ,起始位址在 0x7554632,那麼利用 ptr 這個指標指向陣列的起始位址後,我們就可以透過 ptr 來存取 number 陣列的值了!

int number[6] = {34, 25, 76, 45, 98, 12};
int *ptr = &number[0]

printf("%d\n", *ptr);
printf("%d\n", number[0]);

ptr++;
printf("%d\n", *ptr);

輸出會是如何呢:

34
34
25

由於指標儲存的是記憶體位址,所以指標是可以拿來做運算的喔!

像上面範例中的 ptr++,就是一個很好的例子啦。

p.s. 指標做運算的時候,系統會根據指標的資料型態去對記憶體位址做增減,例如 int 的情況,ptr++之後,ptr 的記憶體位址就會+4 (int 大小為 4 bytes ),其餘以此類推!

指標只能用一層嗎?

想用幾層都可以,只是有沒有必要!

有些比較複雜的資料結構,設計起來可能會需要用到二層指標甚至三層,但是能不用到盡量還是不要,因為那會造成程式的複雜度過高,可讀性也會大大地降低!

還是來看看雙層指標 (Double pointer),概念是不變的~

int **ptr = NULL;
int *qtr = NULL;

int number[6] = {34, 25, 76, 45, 98, 12};

qtr = &number[0]; // 將 number 的起始位址存進 qtr
*ptr = qtr; // 再將 qtr 存進 *ptr 中

// 下面三個敘述代表的都是 number 陣列起始位址的值
printf("%d\n", number[0]);
printf("%d\n", *qtr);
printf("%d\n", **ptr);

這裏真的比較抽象,需要一點時間理解!

34
34
34

究竟為什麼要使用指標!

原因#1:透過指標可以動態配置/釋放記憶體,將記憶體的使用效率最大化。

原因#2:透過指標才能做到call by reference。

指標難道沒有缺點嗎?

怎麼可能沒有!一籮筐呢!

缺點#1:若指標內所存的位址錯誤,輕則是存取的值有問題,嚴重的話會導致程式直接 crash。

缺點#2:在程式中,將指標指向已經不能使用的記憶體位址(已釋放的記憶體),這一樣會造成 crash。

缺點#3:程式的可讀性會大幅地降低。

總結

以上就是 10 個不得不知的指標觀念:

  • 指標 (pointer) 是什麼?
  • 指標背後的實際原理!
  • 如何使用指標?
  • 指標需要初始化嗎?該怎麼做?
  • 空指標 (NULL pointer) 是什麼?會指向哪裡?
  • 宣告時還不確定資料型態怎麼辦?
  • 陣列 (Array) 與指標 (Pointer) 之間的關係是?
  • 指標只能用一層嗎?
  • 究竟為什麼要使用指標!
  • 指標難道沒有缺點嗎?

指標真的是比較抽象的概念,如果還有不懂的記得要再上去看懂哦!

對文章有什麼問題的話,都非常歡迎在下面留言告訴我!

By Hani

This Post Has 2 Comments

  1. tiop

    好酷,好意懂得教學,之前學半天就放棄了…

    1. Han I

      不要放棄,堅持下去就是你的!!

Leave a Reply