C 沒有內建字串型別——字串只是「以 '\0' 結尾的字元陣列」。
標準函式庫 <string.h> 與 <ctype.h> 提供了操作字串的工具箱,
但也埋著緩衝區溢位等安全地雷。本單元完整導覽這些函式,並自己動手實作幾個經典函式。
學習目標
- 熟悉
<string.h>主要函式:長度、複製、串接、比較、搜尋、切割 - 熟悉
<ctype.h>字元分類與轉換函式 - 理解緩衝區溢位的成因,知道為什麼
gets被 C11 移除 - 會用
snprintf、strncpy等有界版本寫出安全的字串程式 - 能自己實作
atoi、itoa、reverse、trim
對照 K&R 章節
| 本單元主題 | K&R 2nd Edition |
|---|---|
| 字元指標與字串函式 | §5.5 Character Pointers and Functions |
| string.h 函式總表 | Appendix B3: String Functions |
| ctype.h | Appendix B2: Character Class Tests |
| atoi/itoa/reverse | §2.7, §3.6(原始實作出處) |
觀念講解
1. <string.h> 函式總表
| 函式 | 原型(簡化) | 功能 | 注意事項 |
|---|---|---|---|
strlen |
size_t strlen(const char *s) |
長度(不含 '\0') |
回傳 size_t(無號) |
strcpy |
char *strcpy(char *dst, const char *src) |
複製 | 不檢查空間,dst 必須夠大 |
strncpy |
char *strncpy(char *dst, const char *src, size_t n) |
至多複製 n 字元 | src 太長時不補 '\0'! |
strcat |
char *strcat(char *dst, const char *src) |
串接到 dst 尾 | 同樣不檢查空間 |
strncat |
char *strncat(char *dst, const char *src, size_t n) |
至多接 n 字元 | 一定補 '\0'(比 strncpy 友善) |
strcmp |
int strcmp(const char *a, const char *b) |
字典序比較 | 回傳 <0 / 0 / >0,不是 bool |
strncmp |
int strncmp(const char *a, const char *b, size_t n) |
只比前 n 字元 | 常用於「開頭是否為…」 |
strchr |
char *strchr(const char *s, int c) |
找 c 第一次出現 | 找不到回傳 NULL |
strrchr |
char *strrchr(const char *s, int c) |
找 c 最後一次出現 | 取副檔名、basename 常用 |
strstr |
char *strstr(const char *hay, const char *needle) |
找子字串 | 找不到回傳 NULL |
strtok |
char *strtok(char *s, const char *delim) |
切割字串 | 會改寫原字串、有內部狀態 |
2. <ctype.h> 字元函式表
| 函式 | 判斷/轉換 | 範例 |
|---|---|---|
isalpha(c) |
字母? | 'a'→真 |
isdigit(c) |
數字? | '7'→真 |
isalnum(c) |
字母或數字? | |
isspace(c) |
空白(空格/tab/換行…)? | trim 的核心 |
isupper(c) / islower(c) |
大寫/小寫? | |
ispunct(c) |
標點? | |
toupper(c) / tolower(c) |
轉大寫/小寫 | 非字母原樣回傳 |
注意:引數應是 unsigned char 值或 EOF;
處理 char 時慣例寫 isalpha((unsigned char)c),避免負值造成未定義行為。
3. 緩衝區溢位與 gets 之死
char buf[8];
gets(buf); /* 使用者輸入超過 7 字元就溢位! */
gets 無法限制讀入長度,攻擊者可藉由超長輸入覆寫返回位址(stack smashing),
是史上最著名的安全漏洞來源之一(1988 年 Morris Worm 即利用此類漏洞)。
因此 C11 直接把 gets 從標準中移除。替代方案:
char buf[8];
if (fgets(buf, sizeof buf, stdin) != NULL) {
buf[strcspn(buf, "\n")] = '\0'; /* 去掉換行 */
}
4. 安全組字串:snprintf
sprintf 與 strcpy/strcat 一樣不檢查空間;snprintf 是首選:
char path[64];
int n = snprintf(path, sizeof path, "/tmp/%s_%d.txt", name, id);
if (n < 0 || (size_t)n >= sizeof path) {
/* 被截斷或出錯:n 是「本來需要」的長度 */
}
snprintf 保證不越界且一定補 '\0',回傳值可檢查是否被截斷。
5. strncpy 的陷阱
char dst[4];
strncpy(dst, "abcdef", sizeof dst); /* dst = "abcd",沒有 '\0'! */
src 長度 ≥ n 時 strncpy 不會補結尾,之後對 dst 做 strlen/printf 都會越界。
安全慣例:
strncpy(dst, src, sizeof dst - 1);
dst[sizeof dst - 1] = '\0'; /* 手動補上 */
6. strtok 的特性
strtok 會把分隔字元改寫成 '\0' 並在內部記住位置:
char line[] = "alice,30,taipei"; /* 必須可修改,不能是字面值 */
char *tok = strtok(line, ",");
while (tok != NULL) {
printf("%s\n", tok);
tok = strtok(NULL, ","); /* 之後每次傳 NULL 繼續切 */
}
因為有隱藏的內部狀態,strtok 不可重入、不能巢狀使用。
範例程式
| 檔案 | 內容 |
|---|---|
01_string_h_tour.c |
string.h 主要函式逐一示範:複製/串接/比較/搜尋/strtok 切割 CSV |
02_ctype_and_safety.c |
ctype.h 分類統計、大小寫轉換;緩衝區安全:strncpy 陷阱、snprintf、fgets 慣例 |
03_implement_classics.c |
自己實作 atoi / itoa / reverse / trim,含負數與邊界處理 |
編譯與執行(以第一個為例):
cc -std=c11 -Wall -Wextra -o /tmp/demo 01_string_h_tour.c
/tmp/demo
常見錯誤與陷阱
1. 用 == 比較字串
if (name == "alice") { ... } /* 比較的是位址,不是內容! */
修正:
if (strcmp(name, "alice") == 0) { ... }
2. 目的緩衝區太小(溢位)
char dst[5];
strcpy(dst, "hello world"); /* 溢位:需要 12 bytes */
修正:確保空間足夠,或使用 snprintf(dst, sizeof dst, "%s", src)。
3. strncpy 沒補 '\0'
char dst[4];
strncpy(dst, "abcdef", 4);
printf("%s\n", dst); /* 越界讀:dst 沒有結尾 */
修正:dst[3] = '\0'; 或改用 snprintf。
4. 對字串字面值使用 strtok
char *s = "a,b,c";
strtok(s, ","); /* 未定義行為:strtok 要改寫字串 */
修正:先複製到可修改的陣列 char buf[] = "a,b,c";。
5. 忘記 strlen 回傳無號數
if (strlen(s) - 1 >= 0) { ... } /* 永遠為真:size_t 不會是負的 */
空字串時 strlen(s) - 1 會下溢成極大值。先檢查 strlen(s) > 0。
練習題
- 寫
int count_words(const char *s):用isspace計算字串中的單字數。 - 寫
void to_title_case(char *s):每個單字首字母大寫、其餘小寫(用 ctype.h)。 - 寫
int starts_with(const char *s, const char *prefix):用strncmp實作。 - 寫
char *my_strstr(const char *hay, const char *needle):不用庫函式自己實作子字串搜尋。 - 寫
int my_atoi_strict(const char *s, int *out):比 atoi 更嚴格, 遇到非法字元回傳 0(失敗),成功回傳 1 並經由指標寫出結果。 - (進階)實作
size_t my_strlcpy(char *dst, const char *src, size_t size): BSD 風格的安全複製——永遠補'\0',回傳 src 的完整長度以便檢查截斷。
延伸閱讀
- K&R Appendix B2、B3:ctype.h 與 string.h 完整規格
- CERT C Coding Standard,STR 章節(字串安全規則)
- CWE-120 / CWE-121:緩衝區溢位漏洞分類
man 3 string、man 3 ctype(macOS 內建文件)- 下一單元
05-structures:把相關資料打包成結構