Outline — 點擊展開各節
前言
ANSI C 大於 K&R C
轉成 K&R C
回顧
Appendix · ANSI C and K&R C

附錄 ANSI C 與 K&R C

本書的程式全部以 ANSI C 撰寫。這份附錄說明它與較傳統的 K&R C 有哪些差異,以及把書中程式移植到舊編譯器時,編譯器實際會抱怨些什麼。

為何本書採用 ANSI 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。我們這樣做有兩個原因。

  1. 最重要的理由是 ANSI C 提供一個方法來改善程式的可讀性和可靠性。
  2. 第二個理由是愈來愈多的電腦廠商支援 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 節「指標可能是危險的」三條規則的語言層面根據。

ANSI C 大於 K&R C

小的修改與加入

項目說明
行接續「程式行可以連接在一起,只要在第一行之後加上倒斜線 \ 即可。」
三字圖 trigraph「ANSI C 提供一種『三字圖』,即三個字元順序,例如 ??/ 可用來表示連續到下一行。」
多位元組字元「ANSI C 提供多位元組的概念和更大的字元集。它們的目的在於適用於國際字元,它們之中有許多需要以一個以上的位元組來表示。一個寬的字元可以用 wchar_t 來宣告,它定義在 stddef.h 中。多位元組字元是一個寬字元的外部表示法。它以一般的 C 之字之字串表示。

兩種新的型態名稱:const 與 volatile

const

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 的差別 —— 讀法是「由右往左」
宣告誰不能改
const int ii 本身不能改
int * const cptr指標 cptr 不能改(不能指向別處),但 *cptr 可以改
const int *ptrc*ptrc 不能改(不能透過它修改資料),但 ptrc 本身可以改指向別處

這三行是 C 語言最常被搞混的宣告之一。訣竅:從變數名往外讀。cptr is a const pointer to intptrc is a pointer to const int

volatile

volatile 用於一個物件可能經常地被修改,而我們想要編譯程式不要對它執行最佳化時。」—— 典型用途是記憶體對映的硬體暫存器,或會被中斷處理常式修改的變數。

函數原型 (prototype) —— ANSI C 最主要的擴充

什麼是函數原型

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 */
void f(void) 的用意

即使函數沒有參數,即 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]=1numbers[0][1]=2number[0][2]=3number[1][0]=4,……」

「不可依靠它」是什麼意思

課本特別警告:不要假設未明確初始化的元素會是 0。雖然多數編譯器對靜態儲存期的變數會清零,這不是語言保證的行為。這和第 4 章 4.1.1 節「把沒有實際指向物件的所有指標設定為 NULL」是同一種紀律:不要依賴未定義的行為。

型態的組織與 limits.h

ANSI C 對整數寬度的要求

「明白 ANSI C 如何組織型態是很有用的,這顯示於圖 A.1。它要求型態 int 不小於 short,且 long 不小於 int

型態許多實作的位元數
char8 位元
short16 位元
int16 或 32 位元
long32 位元

ANSI C 要求至少要使用這樣的寬度。」注意它規定的是最小值與相對大小,不是確切的位元數。

limits.h

ANSI C 要求每一編譯程式實作要對整數的範圍加以說明,它們定義在標頭檔案 limits.h 中。在此檔案中的數值包括:CHAR_BITINT_MINCHAR_MINCHAR_MAX 及其他的數值。

本書多處用到這些常數:第 1 章的 INT_MAXINT_MIN(Natural_Number ADT 的上下界)、第 6 章明白警告不要INT_MAX 當最短路徑的 $\infty$(會溢位)、第 9 章與第 10 章用 INT_MAX 當「這個欄位沒有元素」的標記。

註解、識別字長度與前處理器

註解不能巢狀 —— 以及標準的解法

「C 規定 /* 為註解的開始,*/ 為註解的結束。它不容許巢狀的註解,但有許多 C 實作則可以。如果有必要對一大段可能含有註解的 C 程式碼加上註解時,應該使用 C 的前處理指令:

#if 0
...
#endif
項目K&R CANSI C
複合運算元「如 +=-= 視為兩個符號,而且其中可以有空白」「視其為單一符號
識別字長度「規定識別字僅以它們的前八個字元來區別。所以,識別字 looknicelooknice2 可能被看成是相同的」「容許至少以 31 個字元來判斷識別字是否相同,因此,鼓勵我們使用更有意義的名稱
# 的位置「預期 # 在第一格,命令直接寫在它的後面」「容許在 # 前後有空白」
register「非 ANSI C 編譯程式通常限制 register 只能和純量型態一起使用」「容許關鍵字暫存器可以和其他任何型式的變數或參數一起使用」
識別字長度的實務警告

然而,在 C 中的外部識別字必須以除錯程式和鏈結程式來處理。這些工具通常限制比較多,所以較長的名稱還是不受到鼓勵的。」—— 也就是說:函數名與全域變數名(外部連結)仍應保守,長名字留給區域變數。

兩個「應避免」的舊習慣
  1. 省略型態名稱。「C 容許型態名稱為一個變數或一個函數定義。在此情況下,預設值為 int在 ANSI C 中這被視為不好的程式設計實務。ANSI C 提供特別的型態 void 來指示傳回的函數值可以忽略。」
    (這正是第 4 章 4.1.1 節第 3 條規則「永遠為函數定義明確的傳回型態」的由來 —— 在許多系統中指標的大小和 int 相同,預設的 int 傳回型態稍後可能被解釋為一個指標,「在有些電腦上,這已經證實是危險的作法」。)
  2. 跳進複合指令中間。「C 容許無限制的分歧到複合指令的中間。這與 Ada、Modula-2 和 Pascal 等是非常的不同。這種實作也應避免。
課本自己說:這份清單不可能完整

要在一個附錄中包含全部的 K&R C 和 ANSI C 的差異是不可能的。有興趣的讀者請參考 Harbison 和 Steele 所著的書籍 "C: A Reference Manual",以便對差異性作完整的探討。

(Harbison 和 Steele 也是第 2 章字串函數與第 3 章 C 優先權等級的參考來源。)

將本書中的程式轉成 K&R 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 的宣告只寫傳回型態與函數名,括號內完全空白。

從這 24 行就能看出全部的轉換規則
ANSI CK&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 的兩段式寫法(參數名稱在括號內、型態宣告在括號後、大括號前)。

編譯器實際抱怨了什麼

圖 A.2:由 K&R C 編譯程式產生的錯誤訊息
"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 —— 編譯器直接放棄了。這是移植舊程式時最典型的經驗:一個語法差異會引發雪崩式的連鎖錯誤

圖 A.3:第二組錯誤訊息(修掉第一批之後)
"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-functionK&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 節談除錯時說的「一個修正過的錯誤可能產生數個新的錯誤」是同一個經驗。

本附錄重點回顧

ANSI C 相對於 K&R C 的主要差異
項目K&R CANSI C
函數原型只有名稱,無型態;不做型態檢查參數型態寫在括號內;檢查個數與型態
無參數函數f() = 參數未指定f(void) = 確實沒有參數
可變引數任何函數都可以必須先宣告
const / volatile
識別字長度8 個字元至少 31 個字元
複合運算元+ = 可有空白單一符號,不可分開
# 位置必須在第一格前後可有空白
register只能用於純量型態任何型式的變數或參數
初值設定受限大括號形式,但不可假設未初始化的元素為 0
整數範圍定義在 limits.hshort $\le$ int $\le$ long
移植到 K&R C 的四步驟
  1. 把所有函數原型的括號清空。
  2. void main(void)void main()
  3. 函數定義改成 K&R 兩段式標頭。
  4. 拿掉陣列名稱前多餘的 &
這份附錄與正文的連結
  • 第 1 章:INT_MAXINT_MIN(Natural_Number ADT);除錯的連鎖錯誤。
  • 第 2 章:ANSI C 允許結構指定,早期 C 不允許(2.2.1 節)。
  • 第 4 章:4.1.1 節三條指標規則的語言根據;malloc 在 ANSI C 傳回 void*
  • 第 6 章:為何不要INT_MAX 當 $\infty$(會溢位)。
  • 第 9、10 章:INT_MAX 標記「此欄位無元素」。
今天該怎麼看這份附錄

K&R C 已成歷史,但這份附錄提出的紀律仍然完全適用:寫明確的原型、寫明確的傳回型態、不依賴未定義的初值、不依賴編譯器的寬容。今天的對應物是開啟 -Wall -Wextra -Werror、用 const 標示不變的東西、不假設未定義行為 —— 目的與 1989 年制定 ANSI C 時完全一樣:讓編譯器替你抓錯,而不是讓使用者替你抓錯。