`strtok` 是 C 標準庫字符串分割函數,但因**內部靜態指針保存狀態**,導致**非線程安全**、**不可遞歸/嵌套調用**。本文詳細對比 `strtok` 缺陷與 `strtok_r`(可重入版)優勢,演示多層分割(如 CSV 解析)的正確姿勢,並解析字符串字面量不可修改的坑點。
char *strtok(char *str, const char *delim);
// 首次調用傳 str,後續調用傳 NULL
// 返回下一個 token 指針,修改原串插入 '\0'
示例:`"Fred,John,Ann"` 分割 `","` → `"Fred"` `"John"` `"Ann"`。
內部靜態指針記錄「下次掃描起始位置」。導致:
解析 `"Fred male 25,John male 62,Anna female 16"`:
外層按 `","` 分割 → `"Fred male 25"`、`"John male 62"`...
內層按 `" "` 分割每段 → 姓名、性別、年齡。
使用 `strtok` 時,內層循環將外層的靜態指針改寫,導致外層無法繼續,只能得到第一段數據。
char *strtok_r(char *str, const char *delim, char **saveptr);
// saveptr:用戶提供的狀態指針,替代內部靜態指針
// 首次調用傳 str 地址,後續傳 NULL,saveptr 需保持同一變量
char buffer[] = "Fred male 25,John male 62,Anna female 16";
char *outer_ptr = NULL;
char *inner_ptr = NULL;
char *token;
int idx = 0;
char *tokens[20];
// 外層按逗號分割
while ((token = strtok_r(buffer, ",", &outer_ptr)) != NULL) {
// 內層按空格分割
char *field = strtok_r(token, " ", &inner_ptr);
while (field != NULL) {
tokens[idx++] = field;
field = strtok_r(NULL, " ", &inner_ptr);
}
tokens[idx++] = "***"; // 分隔標記
buffer = NULL; // 後續外層調用傳 NULL
}
// 輸出驗證
for (int i = 0; i < idx; i++) printf(">%s<\n", tokens[i]);
輸出:`>Fred< >male< >25< >***< >John< >male< >62< >***< >Anna< >female< >16< >***<` —— 完美解析所有字段。
| 特性 | strtok | strtok_r |
|---|---|---|
| 線程安全 | ❌ 靜態指針共享 | ✅ 用戶管理 saveptr |
| 嵌套調用 | ❌ 相互干擾 | ✅ 獨立 saveptr 隔離 |
| 標準 | C89/C99 | POSIX (Linux/Unix),Windows 用 `strtok_s` |
| 原串修改 | ✅ 插入 '\0' | ✅ 插入 '\0' |
char *s = "a,b,c"; // s 指向只讀段 .rodata
strtok(s, ","); // 嘗試寫入 '\0' → 段錯誤 (SIGSEGV)
// 正確:用數組在棧上分配可修改內存
char s[] = "a,b,c"; // 編譯器複製字面量到棧數組
strtok(s, ","); // 安全
**鐵律**:傳給 `strtok`/`strtok_r` 的第一個參數**必須是可寫內存**(棧數組、`malloc` 堆內存),**絕不能是字符串字面量**。
// MSVC: char *strtok_s(char *str, const char *delim, char **context);
// 用法同 strtok_r,參數順序略異
#ifdef _WIN32
#define strtok_r strtok_s
#endif
`strtok` 因歷史包袱存在嚴重缺陷,**生產代碼禁用**。**`strtok_r` 是正統替代者**,通過用戶態 `saveptr` 實現線程安全與嵌套分割。記住三鐵律:1. 用可寫緩衝區 2. 用 `strtok_r` 3. 多層分割各層獨立 `saveptr`。理解其「修改原串插 `\0`」的破壞性行為,避免在需要保留原串場景誤用。