C 語言的指針機制靈活強大,但「多級指針」常讓初學者望而生畏。本文從 main 的 argv 切入,層層遞進至字串分割函式 makeargv 的三級指針 char ***argvp 設計,透過記憶體圖解與完整程式碼,徹底釐清多級指針的實際運作邏輯與應用場景。
char **argv:命令列參數的標準映射int main(int argc, char **argv) { ... }
// 等價於
int main(int argc, char *argv[]) { ... }
當執行 prog -c -v 200 時,記憶體佈局如下:
argv (char **) ──► +----+ +----------+
| *0 |──► | "prog\0" |
+----+ +----------+
| *1 |──► | "-c\0" |
+----+ +----------+
| *2 |──► | "-v\0" |
+----+ +----------+
| *3 |──► | "200\0" |
+----+ +----------+
| *4 | = NULL (哨兵)
+----+
argv[0] ~ argv[argc-1] 指向各參數字串(長度不一)。argv[argc] == NULL 為標準哨兵,方便迴圈遍歷。char ***argvp:動態建立參數向量若要編寫一個函式,將單一字串 "mine -c 10 2.0" 依分隔符 " \t" 分割成類 argv 結構,並回傳給呼叫者,簽名必須為:
int makeargv(const char *s, const char *delimiters, char ***argvp);
為何需要三級指針?
char **my_argv 變數(二級指針)。char ***。*argvp = malloc(...) 寫入新陣列位址,呼叫者即可透過 my_argv 存取結果。makeargv 完整實作與逐行解析#include <stdlib.h>
#include <string.h>
#include <errno.h>
int makeargv(const char *s, const char *delimiters, char ***argvp) {
if (!s || !delimiters || !argvp) { errno = EINVAL; return -1; }
*argvp = NULL; // 1. 先歸零,失敗時安全
char *t = malloc(strlen(s) + 1); // 2. 複製字串,因 strtok 會破壞原串
if (!t) return -1;
strcpy(t, s);
// 3. 先數 token 數量
int numtokens = 0;
if (strtok(t, delimiters)) // 第一次呼叫
for (numtokens = 1; strtok(NULL, delimiters); numtokens++);
// 4. 配置指標陣列 (+1 給尾端 NULL)
*argvp = malloc((numtokens + 1) * sizeof(char *));
if (!*argvp) { free(t); return -1; }
if (numtokens == 0) { free(t); return 0; }
// 5. 重新分割並填入指標
strcpy(t, s); // 重置 t
*(*argvp) = strtok(t, delimiters); // argvp[0]
for (int i = 1; i < numtokens; i++)
*(*argvp + i) = strtok(NULL, delimiters); // argvp[i]
*(*argvp + numtokens) = NULL; // 哨兵
return numtokens;
}
關鍵指針操作解析:
*argvp:解參考一次,得到呼叫者的 char ** 變數位址。*(*argvp + i):等價 (*argvp)[i],寫入第 i 個 char *。strtok 回傳的指標指向 t 內部,故 t 必須存活到呼叫者釋放 *argvp 為止。int main(void) {
char *test = "mine -c 10 2.0";
char delim[] = " \t";
char **argvp = NULL;
int n = makeargv(test, delim, &argvp);
if (n == -1) { perror("makeargv"); return 1; }
for (int i = 0; i < n; i++)
printf("%d: %s\n", i, argvp[i]);
// 釋放順序:先釋放指標陣列指向的字串緩衝 (argvp[0] 所屬的 t),再釋放指標陣列
free(argvp[0]); // 實際上指向 makeargv 內部的 t
free(argvp);
return 0;
}
注意:上述實作將所有 token 指向同一塊緩衝區 t,釋放時只需 free(argvp[0]);若改為各自 strdup,則需逐一釋放。
char ****?若 Shell 需支援「多行命令歷史」,每行又是一個 char **,則需 char ***history;若歷史本身可動態增減,函式參數便成 char ****。雖語法合法,但實務上建議封裝結構體:
typedef struct { int argc; char **argv; } Command;
Command *history = malloc(max_history * sizeof(Command));
以結構體取代多級指針,可大幅降低認知負擔與除錯難度。
| 層級 | 型別 | 典型用途 | 記憶體語意 |
|---|---|---|---|
| 1 | char * | 單一字串 | 指向字元陣列 |
| 2 | char ** | 字串陣列 (argv) | 指向指標陣列,每元素指向字串 |
| 3 | char *** | 輸出參數:建立字串陣列 | 指向呼叫者的 char ** 變數 |
掌握「誰配置、誰釋放」與「指標層級對應修改權」兩大原則,多級指針不再是噩夢,而是構建動態資料結構的利器。