Skip to content

指標 (Pointers)

指標是 C/C++ 中較獨特的概念,也是讓它成為難以取代的語言的主要原因,當然也是最難的概念之一。

指標的概念

可以想像我們在程式中宣告的一切變數、函數,都會儲存在記憶體中,所以每一個變數都有他對應的一塊記憶體,這些區塊都有他對應的「地址」,也就是記憶體位置。

這些「地址」在 C 中可以被變數儲存起來,這個變數的類別叫做「指標(pointers)」,因為它存著一個「地址」,指標就可以找到在那個位址的變數,找到這個變數的過程稱為「指向(point)」,找到的變數會有各種型別與大小,但本質上是一段記憶體區塊,一段 0 與 1,因此可以用各種方式解讀,用 int 解讀就叫 int pointer;用 char 解讀就叫 char pointer。

使用指標

  • int *ptr: 宣告指向 int 的指標
  • ptr = &num: 給指標一個地址
  • ptr: 記憶體位置
  • *ptr: 記憶體位置中儲存的數值
c
int *ptr; // 宣告一個指標,用 int 方式解讀記憶體
int num = 123; // 4 bytes
float fnum = 0.123; // 4 bytes
ptr = # // 取得 num 的記憶體位址,讓 ptr 指向他
printf("%p: %d\n", ptr, *ptr); // 印出 ptr 指向的位置與 ptr 解讀出的內容
ptr = (int *) &fnum; // 取得 fnum 的記憶體位址,並強制編譯器以 int 型別理解這塊位置
printf("%p: %d\n", ptr, *ptr); // 印出 位置: 1039918957

不同的 pointer 之間可以互相轉型 (type casting),只要指向的內容不會超出記憶體範圍,像是 char pointer 指向 int 或 int pointer 指向 float,在很多時候能夠更靈活的操作。

型別轉換

這是前面為了簡單沒有細說的部分,大致上來說就是用不同的方式去解讀同一個數值,float int char 都是不同的解讀方式。

c
int a = 90.25; // float -> int 捨棄小數點
float b = (float) a / 2; // int -> float 通常沒事
char c = a; // int -> char 可能會因為 int 數值比 char 大導致 overflow

不同的 pointer 間也可以互相轉換,但是如果直接讓 int pointer 指向 float 位置,編譯器會覺得你是不是做錯了,必須讓編譯器明確的知道需要轉換位置的類別。

更多型別與指標

指標的指標

有時候會需要指標的指標來達成類似二維陣列的效果,就是指向指標的指標,具體用途會在指標(下)

c
int **ptr;

所有用中括號宣告的陣列宣告本質上都是指標,不是指標的指標

常數指標

傳遞變數有時候是很大的性能開銷,使用 pointer 可以有效解決問題,但又不想讓函數能夠隨意更改位置或裡面的值,這時候就會使用 常數指標

陣列名稱可以看做 type const*

c
// 不能改變裡面的值
void func1(const int *ptr);
// 不能改變 pointer 指向的位址
void func2(int const *ptr);
// 都不能改變
void func3(const int const *ptr);

理解方式

為了解讀這個複雜的情況,我使用一套優秀的解讀方案

  • const 跟後面的型別
  • 公式: type (const) *ptr ptr 是指向 type 的 (常數)指標,type可以是 const int、int *

字串指標

字串是一個比較複雜的部份,之前有說過宣告時可以使用 arr[] = "ciallo"複製一段字串到陣列中,但如果只是單純要儲存一個字串的話,其實也可以使用 pointer。

指標指向常數:*ptr = "ciallo"

這樣寫時,其實並沒有創造一個陣列,只是在指向一段已經存在於記憶體中的「唯讀」資料。

  • 唯讀性:這段字串存在於唯讀資料區(Read-only Data),你不能寫 ptr[0] = 'b';,這會導致程式崩潰。
  • 字串池 (Interning):編譯器很聰明,如果程式中出現兩次一樣的 "ciallo",它只會存一份。

陣列作法:arr[] = "ciallo"

這是在 Stack 上開闢一塊空間,然後把 "ciallo" 複製(寫)進來。

  • 可修改性:因為這份資料是專屬的副本,可以隨意修改它:arr[0] = 'b';

  • 獨立性:即使內容一樣,它的地址也絕對跟字串常數不同。

c
char *ciallo = "ciallo";
char *hi = "ciallo";
if (hi == ciallo) {
    printf("same\n");
}
hi = "ciallo (∠·ω )⌒★";
if (hi != ciallo) {
    printf("not the same after change\n");
}
char arr_ciallo[] = "ciallo";
if (arr_ciallo != ciallo) {
    printf("unique memory for arr string\n");    
}
進階: string literal 記憶體位置

如果是 ptr = "ciallo" 那麼字串本身是存在 不可變 data 段;如果是用 arr[] = "ciallo" 的實作方式則取決於 C 語言實作本身,可能是從 data 段移過來,也可能是直接在 stack 做一個。