前言
指標 (pointer) 是 C 語言的衍生型別之一。指標的值並非資料本身,而是另一塊記憶體的虛擬位址 (address)。我們可利用指標間接存該指標所指向的記憶體的值。在 C 語言中,有些和記憶體相關的操作必需使用指標,實作動態資料結構時也會用到指標,所以我們學 C 時無法避開指標。
許多 C 語言教材將指標放在整本書的後半段,集中在一章到兩章來講。但我們很早就介紹指標,並在日後介紹其他 C 語言特性時,順便提到指標相關的內容。這樣的編排,是希望讀者能儘早習慣指標的使用方式。
記憶體階層 (Memory Layout)
使用指標,不必然要手動管理記憶體。記憶體管理的方式,得看資料在 C 程式中的記憶體階層而定。C 語言有三種記憶體階層:
- 靜態記憶體配置 (static memory allocation)
- 自動記憶體配置 (automatic memory allocation)
- 動態記憶體配置 (dynamic memory allocation)
靜態記憶體儲存程式本身和全域變數,會自動配置和釋放,但容量有限。
自動記憶體儲存函式內的局部變數,會自動配置和釋放,在函式結束時自動釋放,容量有限。
動態記憶體儲存函式內的變數,需手動配置和釋放,可跨越函式的生命週期,可於執行期動態決定記憶體容量,可用容量約略等於系統的記憶體量。
雖然靜態記憶體和自動記憶體可自動配置,但各自受到一些限制,故仍要會用動態記憶體。
靜態記憶體配置 (Static Memory Allocation)
我們來看一個使用靜態記憶體配置的簡短範例:
#include <assert.h> /* 1 */
/* DON'T DO THIS IN PRODUCTION. */ /* 2 */
int i = 3; /* 3 */
int main(void) /* 4 */
{ /* 5 */
int *i_p = &i; /* 6 */
assert(*i_p == 3); /* 7 */
*i_p = 4; /* 8 */
assert(i == 4); /* 9 */
return 0; /* 10 */
} /* 11 */
在本範例的第 3 行,我們宣告變數 i
為 3
時,C 程式自動為我們配置記憶體。由於該變數屬於全域變數,會自動配置記憶體,不需人為介入。
在範例程式的第 6 行中,我們宣告了指向 int
的指標 i_p
。
int *
表示指向 int
型別的指標。C 語言需要知道指標所指向的型別,才能預知記憶體的大小。
&i
代表回傳變數 i
的虛擬記憶體位址。因為指標的值是記憶體位址,在本例中,我們的指標 i_p
指向一塊已經配置好的記憶體,即為變數 i
所在的位址。
接著,我們在範例的第 7 行確認確認指標 i_p
所指向的值的確是 3
。在這行敘述中,*i_p
代表解址,解址後會取出該位址的值。在本例中即取回 3
。
接著,我們在第 8 行藉由修改指標 i_p
所指向的記憶體間接修改變數 i
的值。
最後,我們在第 9 行藉由 assert(i == 4);
敘述確認指標 i_p
確實間接修改到變數 i
。代表兩者的確指向同一塊記憶體。
如果讀者用 Valgrind 或其他記憶體檢查軟體去檢查此程式,可發現此範例程式沒有配置動態記憶體,也沒有記憶體洩露的問題。代表使用指標不必然要手動管理記憶體。
附帶一提,在本範例中,我們使用了全域變數。這在撰碼上是不良的習慣,因全域變數很容易在不經意的情形下誤改。我們的程式只是為了展示靜態記憶體的特性,不建議在實務上使用全域變數。
自動記憶體配置 (Automatic Memory Allocation)
接著,我們來看一個使用自動記憶體配置的實例:
#include <assert.h> /* 1 */
int main(void) /* 2 */
{ /* 3 */
int i = 3; /* 4 */
int *i_p = &i; /* 5 */
assert(*i_p == 3); /* 6 */
*i_p = 4; /* 7 */
assert(i == 4); /* 8 */
return 0; /* 9 */
} /* 10 */
在本例的第 4 行中,當我們宣告變數 i
的值為 3
時,同樣會自動配置記憶體。由於變數 i
存在於主函式中,故使用自動記憶體配置。
在本例的第 5 行中,我們宣告了指向變數 i
的指標 i_p
。這時候 i
的記憶體已經配置好了。
這個範例除了記憶體配置的方式外,其他的指令和前一節的範例雷同,故不再詳細說明,請讀者自行閱讀。
如果讀者用 Valgrind 或其他記憶體檢查軟體檢查此範例程式,同樣可發現此程式沒有配置動態記憶體,也沒有記憶體洩露的問題。
動態記憶體配置 (Dynamic Memory Allocation)
我們來看一個動態記憶體配置的實例:
#include <assert.h> /* 1 */
#include <stdio.h> /* 2 */
#include <stdlib.h> /* 3 */
int main(void) /* 4 */
{ /* 5 */
int *i_p = (int *) malloc(sizeof(int)); /* 6 */
if (!i_p) { /* 7 */
fprintf(stderr, "Failed to allocate memory\n"); /* 8 */
goto ERROR; /* 9 */
} /* 10 */
*i_p = 3; /* 11 */
if (!(*i_p == 3)) { /* 12 */
fprintf(stderr, "Wrong value: %d\n", *i_p); /* 13 */
goto ERROR; /* 14 */
} /* 15 */
free(i_p); /* 16 */
return 0; /* 17 */
ERROR: /* 18 */
if (i_p) /* 19 */
free(i_p); /* 20 */
return 1; /* 21 */
} /* 22 */
在第 6 行中,我們配置一塊大小為 int
的記憶體。
C 標準函式庫中配置記憶體的函式為 malloc()
,該函式接收的參數為記憶體的大小。我們甚少手動寫死記憶體的大小,而會使用 sizeof
直接取得特定資料型別的大小。這幾乎是固定的手法了。
malloc()
回傳的型別是 void *
,即為大小未定的指標。我們會自行手動轉型為指向特定型別的指標。有些 C 編譯器會自動幫我們轉型,就不用自行轉型。
由於配置記憶體是有可能失敗的動作,我們在第 7 行至第 10 行檢查是否成功地配置記憶體。
當 malloc()
失敗時,會回傳 NULL
。而 NULL
在布林語境中會視為偽,故 !i_p
在 i_p
的值為 NULL
時會變為真。
若 !i_p
為真,代表 malloc()
未成功配置記憶體,這時候我們會中止一般的流程,改走錯誤處理流程。我們先在標準錯誤印出錯誤訊息,然後用 goto
跳到標籤 ERROR
所在的地方。由於 C 沒有內建的錯誤處理機制,使用 goto
跳離一般程式流程算是窮人版的例外 (exception)。
fprintf()
敘述用到了標準輸出入和巨集的概念,稍微超出現在的範圍。先知道該敘述會在標準錯誤印出訊息所在的檔案名稱和行數即可。
如果 malloc()
成功地配置記憶體,我們就繼續一般的程式流程。我們在第 11 行將 i_p
指向的記憶體賦值為 3
,然後在第 12 行至第 15 行檢查是否正確地賦值。一般來說,這行敘述是不需檢查的。這裡僅是展示這項特性。
由於 i_p
所指向的記憶體是手動配置的,我們在第 16 行釋放 i_p
所占用的記憶體。
基本上,malloc()
和 free()
應成對出現。每配置一次記憶體就要在確定不使用時釋放回去。由於這個範例相當短,這似乎顯而易見。但是在撰寫動態資料結構時,會跨越多個函式,比這個範例複雜得多,就有可能會忘了釋放記憶體。
當程式發成錯誤時,我們會改走錯誤處理的流程。在本例中,錯誤流程在第 18 行至第 21 行。在進行錯誤處理時,我們同樣會釋放記憶體,但程式最後會回傳非零值 1
,代表程式異常結束。
由於在錯誤發生時,我們無法確認 i_p
是否已配置記憶體,所以要用 if
敘述來確認。請讀者再回頭把整個程式運行的流程看一次,即可了解。
空指標 (Null Pointer)
當 C 程式試圖去存取系統資源時,該指令有可能失敗,故要撰寫錯誤處理相關的程式碼。以下範例程式試圖打開一個文字檔案 file.txt :
#include <stdio.h> /* 1 */
int main(void) /* 2 */
{ /* 3 */
FILE *fp; /* 4 */
fp = fopen("file.txt", "r"); /* 5 */
if (!fp) { /* 6 */
fprintf(stderr, "Failed to open the file\n"); /* 7 */
return 1; /* 8 */
} /* 9 */
fclose(fp); /* 10 */
return 0; /* 11 */
} /* 12 */
在第 5 行中,我們試圖用 fopen()
函式以讀取模式開啟 file.txt 。當檔案無法開啟時,會回傳空指標 NULL
。所以我們在第 6 行至第 9 行檢查指標 fp
是否為空。
當 fp
為空指標時,!fp
會負負得正,這時候程式會中止一般流程,改走錯誤處理流程。在這個範例中,我們在終端機印出錯誤訊息,並回傳非零值 1
代表程式異常結束。
以下是另一種檢查空指標的寫法:
fp = fopen("file.txt", "r");
if (fp == NULL) {
fprintf(stderr, "Failed to open the file\n");
return 1;
}
基本上,兩者皆可使用,這僅是風格上的差異。
比較指標是否相等
當我們使用指標時,會處理兩個值,一個是指標所指向的位址,另一個是指標所指向的值。我們用以下範例來看兩者的差別:
#include <stdio.h> /* 1 */
int main(void) /* 2 */
{ /* 3 */
int a = 3; /* 4 */
int b = 3; /* 5 */
int *p = &a; /* 6 */
int *q = &a; /* 7 */
int *r = &b; /* 8 */
if (!(p == q)) { /* 9 */
fprintf(stderr, "Wrong addresses: %p %p\n", p, q); /* 10 */
goto ERROR; /* 11 */
} /* 12 */
if (p == r) { /* 13 */
fprintf(stderr, "Wrong addresses: %p %p\n", p, r); /* 14 */
goto ERROR; /* 15 */
} /* 16 */
if (!(*p == *q)) { /* 17 */
fprintf(stderr, "Wrong values: %d %d\n", *p, *q); /* 18 */
goto ERROR; /* 19 */
} /* 20 */
if (!(*p == *r)) { /* 21 */
fprintf(stderr, "Wrong values: %d %d\n", *p, *r); /* 22 */
goto ERROR; /* 23 */
} /* 24 */
return 0; /* 25 */
ERROR: /* 26 */
return 1; /* 27 */
} /* 28 */
一開始,我們分別在第 4 行及第 5 行配置兩塊自動記憶體。雖然變數 a
和變數 b
的值是相同的,但兩者存在於不同的記憶體區塊。
接著,我們第 6 行至第 8 行宣告三個指標,分別指向這兩個變數。
我們可以預期 p
和 q
同時會指向變數 a
所在的記憶體,而 r
則指向變數 b
所在的記憶體。但 p
、 q
、 r
所指向的值皆相等。從範例程式中即可確認這樣的狀態,讀者可自行閱讀一下。
野指標 (Wild Pointer)
若我們宣告了指標但未對指標賦值,這時候指標的位址是未定義的,由各家 C 編譯器自行決定其行為。宣告但未賦值的指標稱為野指標,這時候指標的值視為無意義的垃圾值,不應依賴其結果。
我們來看一個野指標的範例程式:
#include <stdio.h> /* 1 */
#include <stdlib.h> /* 2 */
#define PUTS(format, ...) { \
fprintf(stdout, "(%s:%d) " format "\n", \
__FILE__, __LINE__, ##__VA_ARGS__); \
}
#define PUTERR(format, ...) { \
fprintf(stderr, "(%s:%d) " format "\n", \
__FILE__, __LINE__, ##__VA_ARGS__); \
}
int main(void) /* 3 */
{ /* 4 */
int *i_p; /* Wild pointer. */ /* 5 */
if (i_p == NULL) { /* 6 */
PUTS("i_p is NULL"); /* 7 */
} /* 8 */
if (!i_p) { /* 9 */
PUTS("i_p is EMPTY"); /* 10 */
} /* 11 */
i_p = NULL; /* NULL pointer. */ /* 12 */
if (i_p == NULL) { /* 13 */
PUTS("i_p is NULL"); /* 14 */
} /* 15 */
if (!i_p) { /* 16 */
PUTS("i_p is EMPTY"); /* 17 */
} /* 18 */
i_p = (int *) malloc(sizeof(int)); /* 19 */
if (!i_p) { /* 20 */
PUTERR("Failed to allocate memory"); /* 21 */
goto ERROR; /* 22 */
} /* 23 */
if (i_p == NULL) { /* 24 */
PUTS("i_p is NULL"); /* 25 */
} /* 26 */
if (!i_p) { /* 27 */
PUTS("i_p is EMPTY"); /* 28 */
} /* 29 */
free(i_p); /* 30 */
return 0; /* 31 */
ERROR: /* 32 */
if (i_p) /* 33 */
free(i_p); /* 34 */
return 1; /* 35 */
} /* 36 */
在開頭的地方,我們宣告了兩個巨集 PUTS
和 PUTERR
。若讀者跟著我們這個系列的文章讀下來,到目前為止我們還沒講過巨集。我們這裡使用巨集是為了節省版面。請讀者暫時把巨集當成另類函式即可,後文會再說明。
在第 5 行時,指標 i_p
尚未賦值,其值為垃圾值。使用不同 C 編譯器編譯此範例程式時,會得到不同的結果。
在第 12 行時,我們將 i_p
以 NULL
賦值,這時候 i_p
就不再是野指標,轉為空指標。
在第 19 行時,我們手動配置了一塊記憶體,這時候 i_p
就是一般的指標。
由這個例子可知,我們在宣告指標時,若未馬上配置記憶體或其他系統資源時,應該立即以 NULL
賦值,讓該指標成為空指標。
迷途指標 (Dangling Pointer)
原本指向某塊記憶體的指標,當該記憶體中途消失時,該指標所指向的位址不再合法,這時候的指標就成為迷途指標。如同野指標,迷途指標所指向的值視為垃圾值,不應依賴其結果。
以下是一個迷途指標的範例程式:
#include <stdio.h> /* 1 */
int main(void) /* 2 */
{ /* 3 */
int *i_p = NULL; /* 4 */
{ /* 5 */
int i = 3; /* 6 */
i_p = &i; /* 7 */
} /* 8 */
/* i_p is a dangling pointer now. */ /* 9 */
if (*i_p == 3) { /* 10 */
fprintf(stderr, "It should not be 3\n"); /* 11 */
return 1; /* 12 */
} /* 13 */
return 0; /* 14 */
} /* 15 */
在第 7 行時,指標 i_p
指向 i
。但在第 8 行時,該區塊結束,i
的記憶體會自動釋放掉,這時候 i_p
所指向的記憶體不再合法,i_p
變成迷途指標。之後的運算基本上是無意義且不可靠的。
我們再來看另一個迷途指標的例子:
#include <stdio.h> /* 1 */
#include <stdlib.h> /* 2 */
#define PUTERR(format, ...) { \
fprintf(stderr, "(%s:%d) " format "\n", \
__FILE__, __LINE__, ##__VA_ARGS__); \
}
int main(void) /* 3 */
{ /* 4 */
int *i_p = (int *) malloc(sizeof(int)); /* 5 */
if (!i_p) { /* 6 */
PUTERR("Failed to allocate int"); /* 7 */
goto ERROR; /* 8 */
} /* 9 */
free(i_p); /* 10 */
/* i_p is a dangling pointer now. */ /* 11 */
*i_p = 3; /* 12 */
if (*i_p == 3) { /* 13 */
PUTERR("It should not be 3"); /* 14 */
goto ERROR; /* 15 */
} /* 16 */
return 0; /* 17 */
ERROR: /* 18 */
if (i_p) /* 19 */
free(i_p); /* 20 */
return 1; /* 21 */
} /* 22 */
在第 5 行時,我們配置一塊記憶體到 i_p
。在第 10 行時這塊記憶體釋放掉了,這時候 i_p
所指的位址不再合法,故 i_p
成為迷途指標。即使之後的運算能夠成功,那也只是一時僥倖而已。
結語
在本文中,我們介紹了數個指標的基本用法。我們把指標放在前半部,是為了要讓大家及早適應指標。在後續介紹各種 C 語言的特性時,我們會再加入指標相關的使用方式。