本檔收錄本單元的練習題、完整解題思路與參考解答。
強烈建議先自己動手做:看完題目就闔上這份檔案,寫出你的版本、編譯、執行、卡住了再回來看「解題思路」;
真的做完了,再對照「參考解答」與「程式碼講解」。每題的完整程式碼也放在 exercises/ 資料夾,可直接編譯:
cc -std=c11 -Wall -Wextra -o /tmp/out exercises/ex1_count_words.c && /tmp/out
練習 1:count_words —— 計算單字數(難度:基礎)
題目
寫函式 int count_words(const char *s):用 isspace 計算字串中的單字數
(單字 = 連續的非空白字元)。
測試至少涵蓋:一般句子、前後與中間有多餘空白、單一單字、空字串、全空白字串。
範例:count_words(" leading and trailing ") 應回傳 3。
解題思路
- 先定義清楚「一個單字」:一段連續的非空白字元。 數單字不能數「空白的個數 + 1」——連續空白、開頭結尾的空白都會讓這招失效。
- 正確的觀察:每個單字恰好有一個「起點」—— 也就是「前一格是空白(或字串開頭)、這一格不是空白」的轉折點。 數轉折點就等於數單字。
- 用狀態機實作(這是 K&R §1.6 字數統計 wc 的核心想法):
- 狀態變數
in_word:目前是否走在單字裡面; - 掃描每個字元:- 是空白 →
in_word = 0(離開單字); - 不是空白且
in_word == 0→ 新單字開始!count++,並設in_word = 1; - 不是空白且已在單字內 → 什麼都不做。
- 是空白 →
" hi you "
^ ^ ^
| hi 的起點(+1)
| you 的起點(+1) -> count = 2
- ctype 的安全慣例:
isspace的引數必須是unsigned char值或EOF。char可能是有號的,遇到值為負的位元組(如 UTF-8 中文位元組)直接傳入是未定義行為, 所以一律寫isspace((unsigned char)*s)。
參考解答
/*
* ex1_count_words.c
* -----------------
* 練習 1:count_words —— 用 isspace 計算字串中的單字數(難度:基礎)
*
* 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex1_count_words.c
* 執行:/tmp/out
*/
#include <stdio.h>
#include <ctype.h>
/*
* 單字 = 連續的非空白字元。
* 狀態機想法(K&R §1.6 wc 的縮影):
* 記住「前一個字元是否在單字內」,遇到「空白 -> 非空白」的轉折就 +1。
*/
int count_words(const char *s)
{
int count = 0;
int in_word = 0; /* 0:目前在空白區;1:目前在單字內 */
for (; *s != '\0'; s++) {
if (isspace((unsigned char)*s)) {
in_word = 0; /* 離開單字 */
} else if (!in_word) {
in_word = 1; /* 空白 -> 非空白:新單字開始 */
count++;
}
}
return count;
}
int main(void)
{
const char *tests[] = {
"hello world", /* 2 */
" leading and trailing ", /* 3 */
"one", /* 1 */
"", /* 0 */
" \t \n ", /* 0:全空白 */
"a b c d e", /* 5 */
};
int n = (int)(sizeof tests / sizeof tests[0]);
for (int i = 0; i < n; i++)
printf("count_words(\"%s\") = %d\n", tests[i], count_words(tests[i]));
return 0;
}
程式碼講解
- 狀態機只有兩個狀態(
in_word0/1),卻正確處理了所有空白怪象: 多重空白只觸發一次「離開」、開頭空白不會誤加、結尾空白無影響。 isspace涵蓋空格、\t、\n、\v、\f、\r——比手寫*s == ' '完整得多。- 空字串:迴圈不執行,回 0;全空白:永遠走不到「轉折」分支,回 0。邊界自動正確。
- 易錯處一:忘記
(unsigned char)轉型——ASCII 純英文測資看不出問題, 遇到非 ASCII 位元組就可能出事,屬於「測試蓋不到的未定義行為」。 - 易錯處二:想在「單字結束」時計數——字串結尾的單字後面沒有空白,會漏數最後一個。 在起點計數就沒有這個問題。
範例輸出
count_words("hello world") = 2
count_words(" leading and trailing ") = 3
count_words("one") = 1
count_words("") = 0
count_words("
") = 0
count_words("a b c d e") = 5
(第五行的測試字串含 tab 與換行,印出來會看到跳格與斷行,屬正常現象。)
練習 2:to_title_case —— 標題化大小寫(難度:基礎)
題目
寫函式 void to_title_case(char *s):就地修改字串,
讓每個單字的首字母大寫、其餘字母一律小寫(用 <ctype.h>)。
非字母字元保持原樣。例如 "tHE c PROGRAMMING lANGUAGE" → "The C Programming Language"。
解題思路
- 沿用練習 1 的狀態機:判斷「單字的第一個字元」的邏輯完全相同——
差別只在動作從「計數」變成「改字」:
- 單字的第一個字元 →
toupper; - 單字的其餘字元 →tolower; - 空白 → 重設狀態。 toupper/tolower的貼心設計:非字母的字元原樣回傳, 所以"123abc"的'1'丟給toupper不會出錯——狀態機不需要特別處理數字開頭的「單字」。- 回傳值轉型:
toupper回傳int,指派回char加明確轉型(char), 在-Wextra下才乾淨。 - 測試資料必須用陣列:
char t1[] = "hello world";把字面值複製進可修改的陣列。 寫成char *t1 = "hello world";再就地修改是未定義行為(macOS 直接 crash)—— 這正是上上單元「char *vschar []」的重點。
參考解答
/*
* ex2_title_case.c
* ----------------
* 練習 2:to_title_case —— 每個單字首字母大寫、其餘小寫(難度:基礎)
*
* 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex2_title_case.c
* 執行:/tmp/out
*/
#include <stdio.h>
#include <ctype.h>
/*
* 就地修改字串:單字的第一個字母 -> 大寫,其餘字母 -> 小寫。
* 和 count_words 一樣用「是否在單字內」的狀態機,
* 差別在這次要「改字」而不是「數數」。
*/
void to_title_case(char *s)
{
int in_word = 0;
for (; *s != '\0'; s++) {
if (isspace((unsigned char)*s)) {
in_word = 0;
} else if (!in_word) {
in_word = 1;
*s = (char)toupper((unsigned char)*s); /* 單字第一個字元 */
} else {
*s = (char)tolower((unsigned char)*s); /* 單字其餘字元 */
}
}
}
int main(void)
{
/* 必須用可修改的陣列(字串字面值不可就地修改) */
char t1[] = "hello world";
char t2[] = "tHE c PROGRAMMING lANGUAGE";
char t3[] = " multiple spaces kept ";
char t4[] = "123abc mixed42cases";
to_title_case(t1);
to_title_case(t2);
to_title_case(t3);
to_title_case(t4);
printf("\"%s\"\n", t1);
printf("\"%s\"\n", t2);
printf("\"%s\"\n", t3);
printf("\"%s\"\n", t4);
return 0;
}
程式碼講解
- 參數是
char *s(不加 const):這個函式的本職就是修改字串。 函式簽名的 const 與否傳達了「會不會動你的資料」的契約。 "tHE c PROGRAMMING lANGUAGE"的測試涵蓋「全大寫要壓小寫」與「單一字母的單字」;"123abc"展示toupper('1') == '1'的原樣回傳特性——首「字元」是數字時單字照樣成立, 後面的a因為已在單字內所以壓成小寫。- 空白的數量與位置完全不動——狀態機只在乎「是否空白」,不重排版面。
- 易錯處:對字面值指標呼叫本函式。編譯器不會攔(參數型別就是
char *), 執行期才爆炸。養成習慣:要修改的字串一律存在char buf[]。
範例輸出
"Hello World"
"The C Programming Language"
" Multiple Spaces Kept "
"123abc Mixed42cases"
練習 3:starts_with —— 判斷字串開頭(難度:基礎)
題目
用 strncmp 實作 int starts_with(const char *s, const char *prefix):
s 以 prefix 開頭回傳 1,否則回傳 0。
邊界情況:prefix 比 s 長、兩者完全相等、空 prefix、空 s。
另外示範一個實際應用:從檔名清單中過濾出以 "img_" 開頭的檔案。
解題思路
- 把題目翻譯成庫函式的語言:「
s的前 n 個字元 ==prefix的前 n 個字元」, 其中 n =strlen(prefix)。這正是strncmp(s, prefix, n) == 0。 - 一行搞定:
return strncmp(s, prefix, strlen(prefix)) == 0; - 為什麼邊界都自動正確?
-
prefix比s長:比到s的'\0'時兩邊不等('\0'vs 其他字元),回 0 ✓ - 完全相等:n 個字元全同,回 1 ✓ - 空prefix:n = 0,strncmp規定比 0 個字元一律回傳 0(相等),所以回 1 ✓ (「空字串是任何字串的前綴」在數學上也是合理定義) - 空s、非空prefix:第一個字元就是'\0'vs 非'\0',回 0 ✓ strncmp與strcmp的分工:strcmp比整串,strncmp只比前 n 個—— 「開頭是否為…」正是strncmp的招牌用途。
參考解答
/*
* ex3_starts_with.c
* -----------------
* 練習 3:starts_with —— 用 strncmp 判斷字串開頭(難度:基礎)
*
* 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex3_starts_with.c
* 執行:/tmp/out
*/
#include <stdio.h>
#include <string.h>
/*
* 若 s 以 prefix 開頭回傳 1,否則回傳 0。
* strncmp(s, prefix, n) 只比較前 n 個字元——
* 把 n 設成 prefix 的長度,正好就是「開頭是否為 prefix」。
*/
int starts_with(const char *s, const char *prefix)
{
return strncmp(s, prefix, strlen(prefix)) == 0;
}
static void test(const char *s, const char *prefix)
{
printf("starts_with(\"%s\", \"%s\") = %d\n",
s, prefix, starts_with(s, prefix));
}
int main(void)
{
test("https://example.com", "https://"); /* 1 */
test("http://example.com", "https://"); /* 0 */
test("hello", "hello world"); /* 0:prefix 比 s 長 */
test("hello", "hello"); /* 1:完全相等 */
test("hello", ""); /* 1:空前綴恆成立 */
test("", "a"); /* 0 */
/* 實際應用:過濾出以 "img_" 開頭的檔名 */
const char *files[] = { "img_001.png", "doc.txt", "img_002.png", "note.md" };
printf("以 img_ 開頭的檔案:");
for (int i = 0; i < 4; i++)
if (starts_with(files[i], "img_"))
printf("%s ", files[i]);
printf("\n");
return 0;
}
程式碼講解
strncmp(...) == 0:strncmp回傳的是三態比較結果(<0 / 0 / >0), 不是布林值。判斷相等一定要== 0;直接if (strncmp(...))的語意剛好相反, 是新手最常踩的雷。strlen(prefix)每次呼叫都會重算長度。若同一個 prefix 要比對上萬個字串, 可以先算好長度傳進去;日常使用這樣寫最清晰。- 過濾檔名的迴圈展示了這個小工具的真實用途——URL scheme 檢查、副檔名/前綴過濾、
指令解析(
if (starts_with(line, "GET ")))都是同一招。
範例輸出
starts_with("https://example.com", "https://") = 1
starts_with("http://example.com", "https://") = 0
starts_with("hello", "hello world") = 0
starts_with("hello", "hello") = 1
starts_with("hello", "") = 1
starts_with("", "a") = 0
以 img_ 開頭的檔案:img_001.png img_002.png
練習 4:my_strstr —— 自己實作子字串搜尋(難度:進階)
題目
不使用任何庫函式,實作 char *my_strstr(const char *hay, const char *needle):
回傳 needle 在 hay 中第一次出現位置的指標;找不到回傳 NULL;
空 needle 依標準慣例回傳 hay 本身。
測試需涵蓋:一般命中、部分相符後需回頭重試(如在 "ababc" 找 "abc")、
整串相等、找不到、空 needle、needle 比 hay 長。
解題思路
- 樸素演算法(brute force):把
needle依序「對齊」到hay的每個起點試比:
hay: a b a b c needle: a b c
對齊 0: a b c
✓ ✓ ✗ 第 3 個字元 a != c,失敗
對齊 1: b a b
✗ 第 1 個字元就不符,失敗
對齊 2: a b c
✓ ✓ ✓ needle 比完了 -> 命中,回傳 hay+2
- 雙層迴圈的結構:
- 外層:
hay逐字前進,代表每個「對齊起點」; - 內層:從對齊點開始用兩個游標h、n同步前進, 比到needle結束(成功)或字元不符(失敗換下一個起點)。 - 內層迴圈的結束條件是精華:
while (*h == *n && *n != '\0')—— 兩個條件的順序也有意義:先確認相等再看needle是否走完。 離開迴圈後只要檢查*n == '\0':是 → needle 每個字元都對上了。 - 為什麼「部分相符」的測試重要? 在
"ababc"找"abc"時, 對齊 0 會匹配到一半才失敗——錯誤的實作(例如比對失敗後hay忘了回頭)會漏掉真正的答案。 本實作的外層hay++天然「回頭」:內層用的是另一組游標,不動外層變數。 - 空 needle 的特例放在最前面:標準規定
strstr(s, "")回傳s。 - 複雜度 O(len(hay) × len(needle))。教科書級的 KMP/Boyer-Moore 可到 O(n+m), 但實務上樸素版對短字串反而快——標準庫多半也是最佳化過的樸素版。
參考解答
/*
* ex4_my_strstr.c
* ---------------
* 練習 4:my_strstr —— 不用庫函式實作子字串搜尋(難度:進階)
*
* 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex4_my_strstr.c
* 執行:/tmp/out
*/
#include <stdio.h>
#include <stddef.h>
/*
* 在 hay 中找 needle 第一次出現的位置;找不到回傳 NULL。
* 空 needle 依標準慣例回傳 hay 本身。
*
* 樸素演算法:把 needle 依序「對齊」到 hay 的每個位置試比。
* hay: a b a b c
* 試位 0: a b c (第 3 字元 a!=c,失敗)
* 試位 1: b a b (第 1 字元 b!=a... 失敗)
* 試位 2: a b c (全部相符 -> 回傳 hay+2)
*/
char *my_strstr(const char *hay, const char *needle)
{
if (*needle == '\0') /* 空 needle:立即命中 */
return (char *)hay;
for (; *hay != '\0'; hay++) { /* 對齊到 hay 的每個位置 */
const char *h = hay; /* 從對齊點開始的游標 */
const char *n = needle;
while (*h == *n && *n != '\0') { /* 逐字比對到 needle 結束或不符 */
h++;
n++;
}
if (*n == '\0') /* needle 全部比完 -> 命中 */
return (char *)hay;
}
return NULL; /* hay 掃完仍無 -> 不存在 */
}
static void test(const char *hay, const char *needle)
{
char *p = my_strstr(hay, needle);
if (p != NULL)
printf("my_strstr(\"%s\", \"%s\") -> 索引 %td(\"%s\")\n",
hay, needle, p - hay, p);
else
printf("my_strstr(\"%s\", \"%s\") -> NULL\n", hay, needle);
}
int main(void)
{
test("hello, world", "world"); /* 索引 7 */
test("ababc", "abc"); /* 索引 2:部分相符後回頭重試 */
test("hello", "hello"); /* 索引 0:整串相等 */
test("hello", "xyz"); /* NULL */
test("hello", ""); /* 索引 0:空 needle */
test("aaa", "aaaa"); /* NULL:needle 比 hay 長 */
return 0;
}
程式碼講解
- 內層游標
h、n與外層的hay、needle分離,是「回頭重試」能運作的關鍵: 失敗時外層只前進一步,一切重來。 while (*h == *n && *n != '\0')巧妙處理了「hay 先走完」的情況:*h是'\0'而*n不是時,*h == *n不成立,自動失敗—— 所以"aaa"找"aaaa"不會越界(h停在'\0'上,從不跨過去)。(char *)hay的轉型:同strchr的說明,照抄標準庫介面的歷史包袱。- 易錯處一:內層直接動
hay/needle本體——失敗後回不了頭。 - 易錯處二:判斷成功用
*h == '\0'——那是「hay 走完」,不是「needle 走完」; 正確條件是*n == '\0'。
範例輸出
my_strstr("hello, world", "world") -> 索引 7("world")
my_strstr("ababc", "abc") -> 索引 2("abc")
my_strstr("hello", "hello") -> 索引 0("hello")
my_strstr("hello", "xyz") -> NULL
my_strstr("hello", "") -> 索引 0("hello")
my_strstr("aaa", "aaaa") -> NULL
練習 5:my_atoi_strict —— 嚴格版字串轉整數(難度:進階)
題目
寫函式 int my_atoi_strict(const char *s, int *out),比 atoi 更嚴格:
- 允許前導空白、至多一個正負號、之後至少一位數字;
- 數字之後必須直接到字串結尾——出現任何其他字元都算失敗;
- 超出
int範圍算失敗; - 成功回傳 1 並把結果寫入
*out;失敗回傳 0 且*out保持不動。
測試需涵蓋:一般正負數、"0"、"12abc"(atoi 會靜靜回 12)、"abc"、空字串、
只有符號的 "-"、超出範圍的 "99999999999"。
解題思路
- 先想清楚 atoi 哪裡不好:
atoi("abc")回 0、atoi("0")也回 0—— 呼叫端無法分辨「轉換失敗」與「值就是 0」。解法是把狀態與值分開: 回傳值報告成敗,結果經由指標參數送出(第 01 單元的模式再現)。 - 把格式規則翻成流水線,每一站驗一件事:
while (isspace(...)) s++;—— 跳過前導空白;- 看到
+/-就記下符號並前進一格(至多一個); - 此刻必須是數字:
if (!isdigit(...)) return 0;—— 擋掉""、"-"、"abc"; - 逐位累積:
val = val * 10 + (*s - '0');('7' - '0' == 7,字元轉數字的老招); - 迴圈結束後
*s必須是'\0':擋掉"12abc"。 - 溢位檢查:用
long(在 macOS 上 64 位元)累積,每步檢查sign * val是否超出int範圍 [-2147483648, 2147483647]。 注意檢查要在帶符號之後做——-2147483648合法但+2147483648不合法。 - 「失敗時 *out 不動」的實作:把
*out = ...放在所有檢查之後的最後一行。 任何一站失敗都在寫出之前 return——呼叫端的變數保持原值(用哨兵值 -999 驗證)。
參考解答
/*
* ex5_atoi_strict.c
* -----------------
* 練習 5:my_atoi_strict —— 嚴格版字串轉整數(難度:進階)
*
* 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex5_atoi_strict.c
* 執行:/tmp/out
*/
#include <stdio.h>
#include <ctype.h>
/*
* 比 atoi 嚴格的轉換:
* - 允許前導空白、一個正負號、至少一位數字
* - 數字之後必須直接到字串結尾,出現任何其他字元都算失敗
* 成功:回傳 1,結果經由 *out 寫出;失敗:回傳 0,*out 不動。
* (atoi 對 "12abc" 會靜靜回 12、對 "abc" 回 0——錯誤完全吃掉,
* 嚴格版把「格式錯誤」與「值為 0」區分開來。)
*/
int my_atoi_strict(const char *s, int *out)
{
while (isspace((unsigned char)*s)) /* 1. 跳過前導空白 */
s++;
int sign = 1;
if (*s == '+' || *s == '-') { /* 2. 至多一個正負號 */
if (*s == '-')
sign = -1;
s++;
}
if (!isdigit((unsigned char)*s)) /* 3. 至少要有一位數字 */
return 0;
long val = 0; /* 用 long 累積以便檢查溢位 */
while (isdigit((unsigned char)*s)) {
val = val * 10 + (*s - '0');
if (sign * val < -2147483648L || sign * val > 2147483647L)
return 0; /* 4. 超出 int 範圍算失敗 */
s++;
}
if (*s != '\0') /* 5. 數字後有雜訊 -> 失敗 */
return 0;
*out = (int)(sign * val); /* 全部檢查通過才寫出結果 */
return 1;
}
static void test(const char *s)
{
int value = -999; /* 哨兵值:失敗時應保持不動 */
int ok = my_atoi_strict(s, &value);
if (ok)
printf("\"%s\" -> 成功,值 = %d\n", s, value);
else
printf("\"%s\" -> 失敗(value 未被改動,仍是 %d)\n", s, value);
}
int main(void)
{
test("123"); /* 成功:123 */
test(" -456"); /* 成功:前導空白 + 負號 */
test("+789"); /* 成功:正號 */
test("0"); /* 成功:0(與「失敗」明確區分) */
test("12abc"); /* 失敗:數字後有雜訊(atoi 會回 12) */
test("abc"); /* 失敗:完全不是數字(atoi 會回 0) */
test(""); /* 失敗:空字串 */
test("-"); /* 失敗:只有符號沒有數字 */
test("99999999999");/* 失敗:超出 int 範圍 */
return 0;
}
程式碼講解
- 「回傳成敗 + 指標送值」是 C 錯誤處理的標準介面(
fscanf的回傳值、 BSD 的strtonum都是這個路數),比魔術回傳值(-1?0?)健壯得多。 - 第 3 站的「至少一位數字」檢查一石三鳥:空字串、只有空白、只有符號全部在這裡擋下。
*s - '0':C 保證'0'..'9'的編碼連續,字元減'0'就是數位值。- 溢位檢查用
long(macOS 上 64 位元)當緩衝墊,累積過程不會先溢位再檢查。 嚴格可攜的寫法會用<limits.h>的INT_MIN/INT_MAX與LONG_MAX邊界技巧, 這裡為了聚焦流程用了明確字面值。 - 哨兵值
-999的測試手法:驗證「失敗路徑真的沒碰*out」——這種契約要靠測試保護。
範例輸出
"123" -> 成功,值 = 123
" -456" -> 成功,值 = -456
"+789" -> 成功,值 = 789
"0" -> 成功,值 = 0
"12abc" -> 失敗(value 未被改動,仍是 -999)
"abc" -> 失敗(value 未被改動,仍是 -999)
"" -> 失敗(value 未被改動,仍是 -999)
"-" -> 失敗(value 未被改動,仍是 -999)
"99999999999" -> 失敗(value 未被改動,仍是 -999)
練習 6:my_strlcpy —— BSD 風格的安全複製(難度:挑戰)
題目
實作 size_t my_strlcpy(char *dst, const char *src, size_t size)(BSD 的 strlcpy 規格):
- 至多寫入
size - 1個字元到dst,永遠補'\0'(size > 0時); - 回傳
src的完整長度(不含'\0')——呼叫端用回傳值 >= size即可偵測截斷。
測試:空間充足、剛剛好、太小(截斷)、size == 1、空來源字串,
並示範「檢查回傳值偵測截斷」的典型使用模式。
解題思路
- 先搞懂 strlcpy 解決了 strncpy 的哪兩個毛病:
-
strncpy在 src 太長時不補'\0'——dst 變成沒有結尾的地雷; -strncpy在 src 較短時會把剩餘空間全部填 0——無謂的浪費。strlcpy的契約:dst 永遠是合法字串 + 回傳值告訴你有沒有截斷。 - 實作三步:
- 先取得
srclen = strlen(src)(這就是要回傳的值); - 計算實際可複製的字元數:n = min(srclen, size - 1)—— 取「來源長度」與「容量減一(留給'\0')」的較小者; -memcpy(dst, src, n); dst[n] = '\0';——複製後無條件補結尾。 size == 0的防衛:連'\0'都放不下,什麼都不能寫(整段包在if (size > 0))。- 截斷偵測的原理:回傳的是「本來需要」的長度。
回傳值 >= size⟺srclen > size - 1⟺ 放不下 ⟺ 發生截斷。 呼叫端一行搞定:if (my_strlcpy(buf, src, sizeof buf) >= sizeof buf) 處理截斷; - 測試手法:先用
memset(buf, '#', ...)填滿哨兵字元, 複製後若 printf 正常停在複製的內容,就證明'\0'有補上(沒有就會一路印到#)。
參考解答
/*
* ex6_strlcpy.c
* -------------
* 練習 6:my_strlcpy —— BSD 風格的安全字串複製(難度:挑戰)
*
* 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex6_strlcpy.c
* 執行:/tmp/out
*/
#include <stdio.h>
#include <string.h>
/*
* 把 src 複製進 dst(容量 size bytes):
* - 至多寫入 size-1 個字元,永遠補 '\0'(size > 0 時)
* - 回傳 src 的完整長度:呼叫端用「回傳值 >= size」即可偵測截斷
* 對照 strncpy 的兩大缺陷:可能不補 '\0'、且會把剩餘空間全部填 0(浪費)。
*/
size_t my_strlcpy(char *dst, const char *src, size_t size)
{
size_t srclen = strlen(src);
if (size > 0) {
size_t n = (srclen < size - 1) ? srclen : size - 1; /* 實際可複製數 */
memcpy(dst, src, n);
dst[n] = '\0'; /* 無論如何補結尾 */
}
return srclen; /* 回傳「本來需要」的長度 */
}
static void test(const char *src, size_t size)
{
char buf[16];
/* 先填滿哨兵字元,觀察函式實際動到哪些位置 */
memset(buf, '#', sizeof buf);
size_t need = my_strlcpy(buf, src, size);
printf("src=\"%s\", size=%zu -> dst=\"%s\", 回傳=%zu%s\n",
src, size, buf, need,
(need >= size) ? "(發生截斷!)" : "");
}
int main(void)
{
test("hello", 16); /* 空間充足:完整複製 */
test("hello", 6); /* 剛剛好:5 字元 + '\0' */
test("hello", 4); /* 太小:截斷成 "hel",回傳 5 提醒 */
test("hello", 1); /* 只放得下 '\0':dst 變空字串 */
test("", 8); /* 空字串來源 */
/* 典型使用模式:檢查回傳值偵測截斷 */
char small[8];
const char *long_path = "/usr/local/lib/libfoo.dylib";
if (my_strlcpy(small, long_path, sizeof small) >= sizeof small)
printf("警告:\"%s\" 被截斷為 \"%s\"\n", long_path, small);
return 0;
}
程式碼講解
n = (srclen < size - 1) ? srclen : size - 1;:一行的 min。 注意size - 1在size == 0時會下溢成極大值(size_t無號)—— 這就是為什麼整段要包在if (size > 0)裡,順序不能倒。memcpy比逐字元迴圈快(大區塊時走 SIMD),且這裡長度已知,正是它的主場。size == 1的行為:複製 0 個字元、dst[0] = '\0'——dst 是合法的空字串。 對照strncpy(dst, src, 1)會留下 1 個字元且沒有結尾。- 回傳值設計的巧思:不回傳「實際複製了幾個」而回傳「本來需要幾個」, 讓截斷偵測變成一個比較運算,也讓呼叫端知道該配多大的緩衝區重試。
- macOS 系統其實內建
strlcpy(BSD 家族),但它不在 C 標準中; glibc(Linux)長年拒收,直到 POSIX 2024 才納入——自己會寫一個仍然實用。
範例輸出
src="hello", size=16 -> dst="hello", 回傳=5
src="hello", size=6 -> dst="hello", 回傳=5
src="hello", size=4 -> dst="hel", 回傳=5(發生截斷!)
src="hello", size=1 -> dst="", 回傳=5(發生截斷!)
src="", size=8 -> dst="", 回傳=0
警告:"/usr/local/lib/libfoo.dylib" 被截斷為 "/usr/lo"
常見錯誤解法對照
| 錯誤寫法 | 症狀 | 正確做法 |
|---|---|---|
isspace(*s)(沒轉 unsigned char) |
非 ASCII 位元組觸發未定義行為 | isspace((unsigned char)*s) |
| 數單字用「空白數 + 1」 | 連續/前後空白全部數錯 | 狀態機數「空白→非空白」轉折 |
if (strncmp(s, p, n)) 判斷「相等」 |
邏輯剛好相反 | strncmp(...) == 0 |
my_strstr 內層直接動 hay |
部分相符失敗後回不了頭 | 內層用獨立游標 h、n |
用 atoi 卻想偵測錯誤 |
"abc" 與 "0" 都回 0,無法區分 |
回傳成敗 + 指標送值(或用 strtol) |
strncpy(dst, src, sizeof dst) 就收工 |
src 過長時 dst 沒有 '\0' |
手動補結尾,或改用 strlcpy/snprintf |