1989:ANSI C(C89)——第一個正式標準#
事件#
1989 年,美國國家標準協會(ANSI)通過第一個 C 語言正式標準 ANSI X3.159-1989(俗稱 C89),1990 年被 ISO 採納為 ISO/IEC 9899:1990(C90),內容相同。從此「C 語言」有了跨廠商的法律定義。
為何重要#
- 函式原型(function prototypes):本標準最重要的新語法。
- void、const、volatile 等新關鍵字。
- 標準函式庫定型:
stdio.h、stdlib.h、string.h、math.h等,讓程式碼跨廠商可編譯。
理論與實用原因(各新語法為何加入)#
- 函式原型
int f(int x, char c);: - 彌補的缺陷:K&R 舊式宣告不檢查參數型別與個數,
f(1)傳錯參數要到執行期才炸,是 C 最大的錯誤來源之一。 - 理論原因:型別檢查理論成熟(源自 Algol 68、Pascal 的完整原型傳統),讓編譯器在呼叫端檢查每個參數。
- 實用原因:大型專案(UNIX 上百萬行)需要介面與實作分離,標頭檔中的原型成為模組契約。
void:- 彌補的缺陷:K&R C 的函式不回傳值時語法曖昧,通用指標只能用
char *假裝。 - 實用原因:
void f()明確表達「無回傳值」,void *成為真正的通用指標,配malloc使用。 const:- 彌補的缺陷:C 沒有任何「唯讀」標記,字串字面值被誤改、介面參數被誤寫的錯誤叢生。
- 理論原因:呼應模組化設計——介面應明示「哪些資料不會被修改」,讓編譯器強制執行。
- 實用原因:
const char *s讓 API 契約可被編譯器檢查;編譯器也能據此最佳化。 volatile:- 彌補的缺陷:編譯器最佳化日益聰明,會把「看似無用的記憶體讀寫」刪掉,但記憶體映射 I/O 與中斷處理常式的讀寫有其副作用。
- 實用原因:OS 核心與裝置驅動程式需要告訴編譯器「這個變數每次都要真的讀寫」。
彌補了什麼缺陷#
C89 彌補了 K&R C「無標準、方言叢生、函式不檢查參數、無唯讀標記、無通用指標」的缺陷,讓 C 真正成為跨平台大型工程可用的語言。
程式範例#
函式原型——C89 最重要新語法,彌補「K&R 不檢查參數」的缺陷:
/* ===== K&R 舊式(C89 之前):不檢查參數 ===== */
int add(); /* 括號內沒有參數型別 */
int add(a, b)
int a, b; /* 型別寫在函式下方 */
{ return a + b; }
add(1, 2, 3); /* 傳 3 個參數——編譯器不警告,執行期行為未定義! */
add('A', 1L); /* 傳錯型別——默默出錯 */
/* ===== ANSI C89 函式原型:呼叫端即時檢查 ===== */
int add(int a, int b); /* 原型放在標頭檔,成為模組契約 */
add(1, 2, 3); /* 編譯錯誤:too many arguments */
add('A', 1); /* 'A' 自動轉為 int,型別明確可檢查 */
void——明確的「無回傳值」與真正的通用指標:
/* K&R:函式不回傳值時語法曖昧,通用指標只能用 char * 假裝 */
setup() /* 回傳什麼?沒人知道 */
char *tmp = malloc(100); /* char * 當通用指標,轉型處處都是 */
/* C89: */
void setup(void) /* 明確:無參數、無回傳值 */
void *tmp = malloc(100); /* void * 真正的通用指標 */
int *p = tmp; /* void * 可隱式轉型,不必寫 (int *) */
const——讓 API 契約可被編譯器檢查:
/* K&R:字串被誤改,沒有任何保護 */
int len(char *s) { /* 介面沒說 s 會不會被改 */
s[0] = 'X'; /* 誤改呼叫端的字串,默默發生 */
return strlen(s);
}
/* C89: */
int len(const char *s) { /* const 明示「不會修改」 */
s[0] = 'X'; /* 編譯錯誤:assignment of read-only */
return strlen(s);
}
len("hello"); /* 字串字面值也可以安全傳入 */
volatile——記憶體映射 I/O 的必備語法:
/* 沒有 volatile:最佳化把「看似無用」的讀寫刪掉,驅動程式就壞了 */
unsigned int *status = (unsigned int *)0xFFFF0000;
while (*status == BUSY) /* 編譯器:*status 沒人改,只讀一次,死迴圈! */
;
/* volatile:告訴編譯器「每次都要真的讀」 */
volatile unsigned int *status = (volatile unsigned int *)0xFFFF0000;
while (*status == BUSY) /* 正確:每次迴圈都真的從裝置讀取 */
;