本書的程式全部以 ANSI C 撰寫。這份附錄說明它與較傳統的 K&R C 有哪些差異,以及把書中程式移植到舊編譯器時,編譯器實際會抱怨些什麼。
「在此附錄中我們要討論 American National Standard X3.159-1989 版本的 C 語言(ANSI C)與 Kernighan 和 Ritchie 在其著作 "The C Programming Language", Prentice Hall, 1978 中所提出的一種較傳統的版本之 C 語言(K&R C)間的多個不同點。」
「在本書中,我們採用標準的 ANSI C。我們這樣做有兩個原因。
「雖然如此,仍有許多不支援 ANSI C 的 C 編譯程式存在。其中一個著名的例子是 SUN Sparc stations,它執行 Berkeley UNIX。本書中的所有程式都可以成功地轉換成 K&R C 程式,並可在 SUN Sparcstation 上執行。」
1989 年的 ANSI C(即 C89 / C90)在今天已是所有編譯器的最低標準,K&R C 幾乎絕跡。但這份附錄仍然有用:它解釋了為什麼本書正文中那些看似囉嗦的寫法(明確的 void、完整的函數原型、明確的型態轉換)是必要的 —— 那些正是第 4 章 4.1.1 節「指標可能是危險的」三條規則的語言層面根據。
| 項目 | 說明 |
|---|---|
| 行接續 | 「程式行可以連接在一起,只要在第一行之後加上倒斜線 \ 即可。」 |
| 三字圖 trigraph | 「ANSI C 提供一種『三字圖』,即三個字元順序,例如 ??/ 可用來表示連續到下一行。」 |
| 多位元組字元 | 「ANSI C 提供多位元組的概念和更大的字元集。它們的目的在於適用於國際字元,它們之中有許多需要以一個以上的位元組來表示。一個寬的字元可以用 wchar_t 來宣告,它定義在 stddef.h 中。多位元組字元是一個寬字元的外部表示法。它以一般的 C 之字之字串表示。」 |
「const 表示不能改變其值的物件。」
const int i = 47; /* i cannot be changed */
int * const cptr; /* cptr is a pointer that cannot
be changed */
const int *ptrc; /* pointer to constant data can be
assigned to but not
the object it points to */
i = 10; /* this is illegal */
i++; /* this is illegal */
| 宣告 | 誰不能改 |
|---|---|
const int i | i 本身不能改 |
int * const cptr | 指標 cptr 不能改(不能指向別處),但 *cptr 可以改 |
const int *ptrc | *ptrc 不能改(不能透過它修改資料),但 ptrc 本身可以改指向別處 |
這三行是 C 語言最常被搞混的宣告之一。訣竅:從變數名往外讀。cptr is a const pointer to int;ptrc is a pointer to const int。
「volatile 用於一個物件可能經常地被修改,而我們想要編譯程式不要對它執行最佳化時。」—— 典型用途是記憶體對映的硬體暫存器,或會被中斷處理常式修改的變數。
「ANSI C 主要的擴充之一為函數原型(prototype)。在 K&R C 中,函數的定義以函數名稱與代表參數的一連串名稱開始。有關參數的事實則出現於後。在 ANSI C 中,參數及其型態一起出現在函數標頭中。一個函數原型與函數標頭類似,除了其中有參數型態出現以外。函數原型通常靠近 #define 和 #include 等 C 語言假指令(directives),並容許 C 編譯程式對函數呼叫執行型態檢查。」
small(x,y) /* K&R C function heading */
small(int, int) /* ANSI C function prototype */
small(int x, int y) /* ANSI C function heading */
「即使函數沒有參數,即 f(),ANSI C 提供了格式 void f(void) 作為一種原型。」
在 K&R C 裡 f() 的意思是「參數未指定」而不是「沒有參數」—— 這也是為什麼本書所有主程式都寫成 void main(void) 而非 main()。
| 傳統 C(無原型) | ANSI C(有原型) | |
|---|---|---|
| (i) | 引數以標準預設值轉換 | 引數轉換成型式參數所宣告的型態,就好像做了一個指派一般 |
| (ii) | 不執行型態檢查,也不檢查引數的個數 | 引數的個數和型態與原型比對,如果有不一致時,即發生錯誤 |
| (iii) | 任何函數都可以有可變的引數個數 | 需要可變的引數個數之函數必須先經過宣告 |
第 (ii) 條就是「改善程式的可靠性」的全部內容 —— 沒有原型時,把 float 傳給一個期待 int 的函數,編譯器不會警告,程式就默默地算出垃圾。
「ANSI C 提出在語言中設定變數初值的方法。初值寫在大括號中。有些編譯程式會為 0 設定固定的儲位,但這不是語言定義的一部份,因此不可依靠它。」
int numbers [] [MAX_NUMBERS] = {{1, 2, 3}, {4, 5, 6},
{7, 8, 9}}
「設定一個維陣列 numbers,其中 numbers[0][0]=1,numbers[0][1]=2,number[0][2]=3,number[1][0]=4,……」
課本特別警告:不要假設未明確初始化的元素會是 0。雖然多數編譯器對靜態儲存期的變數會清零,這不是語言保證的行為。這和第 4 章 4.1.1 節「把沒有實際指向物件的所有指標設定為 NULL」是同一種紀律:不要依賴未定義的行為。
「明白 ANSI C 如何組織型態是很有用的,這顯示於圖 A.1。它要求型態 int 不小於 short,且 long 不小於 int。」
| 型態 | 許多實作的位元數 |
|---|---|
char | 8 位元 |
short | 16 位元 |
int | 16 或 32 位元 |
long | 32 位元 |
「ANSI C 要求至少要使用這樣的寬度。」注意它規定的是最小值與相對大小,不是確切的位元數。
「ANSI C 要求每一編譯程式實作要對整數的範圍加以說明,它們定義在標頭檔案 limits.h 中。在此檔案中的數值包括:CHAR_BIT、INT_MIN、CHAR_MIN、CHAR_MAX 及其他的數值。」
本書多處用到這些常數:第 1 章的 INT_MAX/INT_MIN(Natural_Number ADT 的上下界)、第 6 章明白警告不要用 INT_MAX 當最短路徑的 $\infty$(會溢位)、第 9 章與第 10 章用 INT_MAX 當「這個欄位沒有元素」的標記。
「C 規定 /* 為註解的開始,*/ 為註解的結束。它不容許巢狀的註解,但有許多 C 實作則可以。如果有必要對一大段可能含有註解的 C 程式碼加上註解時,應該使用 C 的前處理指令:」
#if 0
...
#endif
| 項目 | K&R C | ANSI C |
|---|---|---|
| 複合運算元 | 「如 += 或 -= 視為兩個符號,而且其中可以有空白」 | 「視其為單一符號」 |
| 識別字長度 | 「規定識別字僅以它們的前八個字元來區別。所以,識別字 looknice 和 looknice2 可能被看成是相同的」 | 「容許至少以 31 個字元來判斷識別字是否相同,因此,鼓勵我們使用更有意義的名稱」 |
# 的位置 | 「預期 # 在第一格,命令直接寫在它的後面」 | 「容許在 # 前後有空白」 |
register | 「非 ANSI C 編譯程式通常限制 register 只能和純量型態一起使用」 | 「容許關鍵字暫存器可以和其他任何型式的變數或參數一起使用」 |
「然而,在 C 中的外部識別字必須以除錯程式和鏈結程式來處理。這些工具通常限制比較多,所以較長的名稱還是不受到鼓勵的。」—— 也就是說:函數名與全域變數名(外部連結)仍應保守,長名字留給區域變數。
int。在 ANSI C 中這被視為不好的程式設計實務。ANSI C 提供特別的型態 void 來指示傳回的函數值可以忽略。」int 相同,預設的 int 傳回型態稍後可能被解釋為一個指標,「在有些電腦上,這已經證實是危險的作法」。)「要在一個附錄中包含全部的 K&R C 和 ANSI C 的差異是不可能的。有興趣的讀者請參考 Harbison 和 Steele 所著的書籍 "C: A Reference Manual",以便對差異性作完整的探討。」
(Harbison 和 Steele 也是第 2 章字串函數與第 3 章 C 優先權等級的參考來源。)
「要將本書中的程式由 ANSI C 轉成你所用的 C 語言是很容易的。在此我們提供一個完整的實例,使程式轉換後可以在 SUN Sparcstation 上執行。在程式 A.1,你可發現這是由本書第 9 章所取出的一個範例程式。它是一個『完整的』程式,它是可執行的。它示範了一個雜湊表的維護。在某些指令的右邊我們加上相當的 K&R C 指令以作為比較。在每一指令的左邊加上了行號,以幫助你找到當我們將此程式在 SUN Sparcstation 上執行時所產生的編譯錯誤。」
1 /* file name: Hash1.c */
2 #include <stdio.h>
3 #include <string.h>
4 #include <stdlib.h>
5 #define MAX_CHAR 10
6 #define TABLE_SIZE 13
7
8 typedef struct {
9 char key[MAX_CHAR];
10 } element;
11 /* K&R C correct form */
12 element hash_table[TABLE_SIZE];
13 int transform(char *); /* int transform(); */
14 int hash(char *); /* int hash(); */
15 void init_table(element []); /* void init_table(); */
16 void linear_insert(element, element []); /* void linear_insert();*/
17 int linear_search(element, element []); /* int linear_search(); */
18 void print_table(element []); /* void print_table(); */
19
20 void main(void) /* void main() */
21 {
22 char key[MAX_CHAR];
23 element info;
24 int position;
程式 A.1:ANSI C 程式與其 K&R C 對照(節錄開頭)—— 注意右欄:K&R C 的宣告只寫傳回型態與函數名,括號內完全空白。
| ANSI C | K&R C |
|---|---|
int transform(char *); | int transform(); |
void init_table(element []); | void init_table(); |
void linear_insert(element, element []); | void linear_insert(); |
void main(void) | void main() |
規則只有一條:把原型括號裡的所有型態拿掉。函數定義的部分則要改成 K&R 的兩段式寫法(參數名稱在括號內、型態宣告在括號後、大括號前)。
"hash1.c", line 18: element declared as parameter to non-function
"hash1.c", line 20: syntax error at or near type word "void"
"hash1.c", line 68: redeclaration of linear_insert
"hash1.c", line 74: item undefined
"hash1.c", line 77: ht undefined
"hash1.c", line 87: syntax error at or near variable name "item"
"hash1.c", line 87: redeclaration of formal parameter, element
"hash1.c", line 94: item undefined
"hash1.c", line 97: ht undefined
"hash1.c", line 109: syntax error at or near variable name "ht"
"hash1.c", line 113: ht undefined
"hash1.c", line 116: syntax error at or near variable name "ht"
"hash1.c", line 116: fatal error: too many errors
注意最後一行:fatal error: too many errors —— 編譯器直接放棄了。這是移植舊程式時最典型的經驗:一個語法差異會引發雪崩式的連鎖錯誤。
"hash1.c", line 27: warning: & before array or function: ignored
"hash1.c", line 33: warning: & before array or function: ignored
"hash1.c", line 36: warning: & before array or function: ignored
"hash1.c", line 44: warning: & before array or function: ignored
"hash1.c", line 48: syntax error at or near type word "char"
"hash1.c", line 54: key undefined
"hash1.c", line 60: syntax error at or near type word "char"
"hash1.c", line 64: key undefined
"hash1.c", line 67: syntax error at or near variable name "item"
"hash1.c", line 67: redeclaration of formal parameter, element
"hash1.c", line 74: item undefined
"hash1.c", line 77: ht undefined
"hash1.c", line 87: syntax error at or near variable name "item"
"hash1.c", line 87: redeclaration of formal parameter, element
"hash1.c", line 94: item undefined
"hash1.c", line 97: ht undefined
"hash1.c", line 109: syntax error at or near variable name "ht"
"hash1.c", line 113: ht undefined
"hash1.c", line 116: syntax error at or near variable name "ht"
"hash1.c", line 121: ht undefined
| 錯誤訊息 | 真正的原因與修法 |
|---|---|
element declared as parameter to non-function | K&R C 看不懂原型裡的型態。把原型括號清空。 |
syntax error at or near type word "void" | void main(void) 的第二個 void。改成 void main()。 |
syntax error at or near type word "char" | 函數定義的標頭仍是 ANSI 形式。改成 K&R 兩段式。 |
redeclaration of formal parameter | 同上 —— 參數在括號內與括號後被宣告了兩次。 |
key undefined / item undefined / ht undefined | 因為上一個語法錯誤,整個函數標頭沒被正確解析,所有參數名都變成未定義。修好標頭,這些就一起消失。 |
warning: & before array or function: ignored | 對陣列名稱取位址。在 K&R C 中陣列名稱本身就是位址,& 是多餘的(ANSI C 則明確定義 &arr 的型態)。把 & 拿掉即可。 |
絕大多數的錯誤都是「假的」 —— 它們是少數幾個真正的語法差異(原型、void、函數標頭形式)所引發的連鎖反應。移植的正確做法是由上而下、一次修一類,然後重新編譯,而不是照著錯誤清單逐行修。這與第 1 章 1.1 節談除錯時說的「一個修正過的錯誤可能產生數個新的錯誤」是同一個經驗。
| 項目 | K&R C | ANSI C |
|---|---|---|
| 函數原型 | 只有名稱,無型態;不做型態檢查 | 參數型態寫在括號內;檢查個數與型態 |
| 無參數函數 | f() = 參數未指定 | f(void) = 確實沒有參數 |
| 可變引數 | 任何函數都可以 | 必須先宣告 |
const / volatile | 無 | 有 |
| 識別字長度 | 前 8 個字元 | 至少 31 個字元 |
| 複合運算元 | + = 可有空白 | 單一符號,不可分開 |
# 位置 | 必須在第一格 | 前後可有空白 |
register | 只能用於純量型態 | 任何型式的變數或參數 |
| 初值設定 | 受限 | 大括號形式,但不可假設未初始化的元素為 0 |
| 整數範圍 | — | 定義在 limits.h;short $\le$ int $\le$ long |
void main(void) → void main()。&。INT_MAX/INT_MIN(Natural_Number ADT);除錯的連鎖錯誤。malloc 在 ANSI C 傳回 void*。INT_MAX 當 $\infty$(會溢位)。INT_MAX 標記「此欄位無元素」。K&R C 已成歷史,但這份附錄提出的紀律仍然完全適用:寫明確的原型、寫明確的傳回型態、不依賴未定義的初值、不依賴編譯器的寬容。今天的對應物是開啟 -Wall -Wextra -Werror、用 const 標示不變的東西、不假設未定義行為 —— 目的與 1989 年制定 ANSI C 時完全一樣:讓編譯器替你抓錯,而不是讓使用者替你抓錯。