學習目標
- 理解
char與字元編碼(ASCII)的關係:字元就是小整數。 - 掌握
getchar/putchar逐字元 I/O,會寫「讀到 EOF 為止」的標準迴圈。 - 理解 C 字串的本質:以
'\0'結尾的 char 陣列,沒有內建的字串型別。 - 重現 K&R 的四支經典程式:字元計數、行計數、單字計數(wc)、找最長行。
- 能自己實作
strlen、字串複製等基本操作,理解標準函式庫背後的原理。
對照 K&R 章節
- K&R §1.5 Character Input and Output(§1.5.1 檔案複製、§1.5.2 字元計數、§1.5.3 行計數、§1.5.4 單字計數)
- K&R §1.9 Character Arrays(最長行程式、getline/copy)
觀念講解
1. 字元就是小整數(ASCII)
char 存的是字元的編碼值。ASCII 是最通行的編碼:
| 字元 | 編碼 | 字元 | 編碼 |
|---|---|---|---|
'0'~'9' |
48~57 | 空格 ' ' |
32 |
'A'~'Z' |
65~90 | 換行 '\n' |
10 |
'a'~'z' |
97~122 | tab '\t' |
9 |
空字元 '\0' |
0 |
三個保證與慣用技巧:
'0'~'9'連續 →c - '0'得到數字值。- ASCII 中大小寫各自連續、相差 32 →
c - 'a' + 'A'轉大寫。 '\0'的值就是 0 → 判斷字串結尾可寫s[i] != '\0'或簡寫s[i]。
2. getchar / putchar 與 EOF
int c; /* 必須是 int,不是 char! */
while ((c = getchar()) != EOF) { /* K&R 最著名的一行 */
putchar(c);
}
getchar()每次讀入一個字元;輸入耗盡時回傳特殊值EOF(定義在<stdio.h>,通常是 -1)。c必須宣告為int:EOF 在所有合法字元值之外,若用char接,EOF 可能與某個字元混淆,或者(unsigned char 平台)永遠不等於 EOF 造成無窮迴圈。(c = getchar()) != EOF:先指定、再比較。括號必要,因為!=優先於=。- 終端機下手動送 EOF:macOS/Linux 按
Ctrl-D,Windows 按Ctrl-Z加 Enter。 - 用管線或重導測試最方便:
echo "hello" | ./prog、./prog < file.txt。
3. C 字串 = '\0' 結尾的 char 陣列
char s[] = "hi"; /* 編譯器自動加 '\0':{ 'h', 'i', '\0' },長度 3! */
| 索引 | 0 | 1 | 2 |
|---|---|---|---|
| 內容 | 'h' |
'i' |
'\0' |
- 字串常值
"hi"佔 3 個 byte:兩個字元加一個終止符。 '\0'是「哨兵」:所有字串函式靠它知道字串在哪裡結束。- 忘了
'\0',printf("%s")、strlen等會一路讀到記憶體深處——經典災難。 - 區分:
'a'是字元常數(int 值 97);"a"是字串(兩個 byte:'a'、'\0')。
4. K&R 四大經典程式的核心
字元計數(§1.5.2):
long nc = 0;
while (getchar() != EOF)
++nc;
行計數(§1.5.3):數換行字元就是數行數:
if (c == '\n')
++nl;
單字計數 wc(§1.5.4):狀態機的初體驗——用 IN/OUT 狀態記住「目前在單字內還是單字外」,
從外進內的瞬間就是「新單字開始」:
if (c == ' ' || c == '\n' || c == '\t')
state = OUT;
else if (state == OUT) {
state = IN;
++nw; /* 只在「進入單字」時 +1 */
}
最長行(§1.9):組合 getline(讀一行進陣列,回傳長度)與 copy(複製字串),
示範「函式 + 字元陣列」的協作。
5. 自己寫 strlen 與 copy
理解字串的最好方法是自己實作標準函式:
int my_strlen(const char s[])
{
int i = 0;
while (s[i] != '\0') /* 一路數到哨兵 */
++i;
return i; /* '\0' 不算在長度內 */
}
void my_strcpy(char to[], const char from[])
{
int i = 0;
while ((to[i] = from[i]) != '\0') /* 指定的值順便當條件:連 '\0' 一起搬 */
++i;
}
注意 my_strcpy 的巧妙:to[i] = from[i] 的值恰好是剛搬的字元,
搬到 '\0'(值為 0,即「假」)時迴圈自然結束——'\0' 已經複製完才停。
另外:目的陣列必須夠大,C 不會幫你檢查——這是緩衝區溢位的根源,永遠要心中有數。
範例程式
| 檔案 | 內容 | 編譯與執行 |
|---|---|---|
01_char_ascii.c |
字元與 ASCII:編碼表、大小寫轉換、數字字元轉數值 | cc -std=c11 -Wall -Wextra -o 01_char_ascii 01_char_ascii.c && ./01_char_ascii |
02_getchar_count.c |
getchar 讀 stdin 到 EOF:字元/行/單字計數(K&R wc),無輸入也能正常結束 | cc -std=c11 -Wall -Wextra -o 02_getchar_count 02_getchar_count.c && echo "hello world" \| ./02_getchar_count |
03_longest_line.c |
K&R §1.9 最長行程式:getline + copy,用寫死的多行文字示範 | cc -std=c11 -Wall -Wextra -o 03_longest_line 03_longest_line.c && ./03_longest_line |
04_my_string_funcs.c |
自製 strlen / strcpy / strcat / 比較 / 反轉,並與 <string.h> 對照驗證 |
cc -std=c11 -Wall -Wextra -o 04_my_string_funcs 04_my_string_funcs.c && ./04_my_string_funcs |
常見錯誤與陷阱
陷阱 1:用 char 接 getchar 的回傳值
char c; /* 錯:裝不下 EOF */
while ((c = getchar()) != EOF) ... /* 可能無窮迴圈或提早結束 */
修正:int c;。getchar 的回傳型別本來就是 int,正是為了容納 EOF。
陷阱 2:字元陣列忘了留 '\0' 的位置
char name[5] = "hello"; /* 錯!"hello" 需要 6 bytes(5 字元 + '\0') */
clang 會警告初始化字串放不下終止符。修正:char name[6] 或直接 char name[] = "hello";。
陷阱 3:用 == 比較字串
char a[] = "cat", b[] = "cat";
if (a == b) { ... } /* 恆假:比較的是兩個陣列的「位址」 */
修正:逐字元比較,或用 <string.h> 的 strcmp(相等時回傳 0:if (strcmp(a, b) == 0))。
陷阱 4:對字串常值動手腳
char *s = "hello";
s[0] = 'H'; /* 未定義行為:字串常值可能放在唯讀記憶體 */
修正:需要修改就用陣列 char s[] = "hello";(複製一份到可寫的陣列);
指向字串常值的指標一律宣告成 const char *s。
陷阱 5:目的緩衝區不夠大
char small[4];
my_strcpy(small, "a very long string"); /* 溢位:寫爆別人的記憶體 */
C 不做邊界檢查。修正:確保目的地夠大(來源長度 + 1),實務上使用有長度上限的版本
(strncpy、snprintf——中階單元詳談)。
練習題
- 基本題:印出
'A'~'Z'的 ASCII 對照表(字元與數值),每列 6 組。 - 大小寫轉換:不用
<ctype.h>,自己寫to_upper(char c)與to_lower(char c)(非字母的字元原樣回傳),對字串"Hello, World 123!"逐字元轉換印出。 - 回文判斷:寫
int is_palindrome(const char s[]),判斷"level"、"radar"、"hello"(提示:雙索引從頭尾夾擠,先用自製 strlen 求長度)。 - 移除空白(K&R 習題 1-18 精神):把字串中的連續空白壓縮成單一空白,如
"a b c"→"a b c"。 - 自製 atoi:寫
int my_atoi(const char s[]),把"1234"轉成整數 1234(提示:result = result * 10 + (s[i] - '0'));行有餘力處理前導空白與正負號。 - 挑戰題(單字直方圖,K&R 習題 1-13):統計一段寫死文字中每個「單字長度」的出現次數,畫出直方圖(需要 wc 的狀態機 + 陣列計數的組合)。
延伸閱讀
- K&R 2/e §1.5、§1.9;習題 1-8 ~ 1-13、1-16 ~ 1-19
man ascii:終端機裡直接查 ASCII 表<ctype.h>:isdigit、isalpha、toupper等標準分類函式(下個階段的工具)- 《The Practice of Programming》§3:狀態機與文字處理
- 下一單元預告:printf/scanf 完整格式化——把輸出入練到精通