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。

練習題

  1. 寫 int count_words(const char *s):用 isspace 計算字串中的單字數。
  2. 寫 void to_title_case(char *s):每個單字首字母大寫、其餘小寫(用 ctype.h)。
  3. 寫 int starts_with(const char *s, const char *prefix):用 strncmp 實作。
  4. 寫 char *my_strstr(const char *hay, const char *needle):不用庫函式自己實作子字串搜尋。
  5. 寫 int my_atoi_strict(const char *s, int *out):比 atoi 更嚴格, 遇到非法字元回傳 0(失敗),成功回傳 1 並經由指標寫出結果。
  6. (進階)實作 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:把相關資料打包成結構