跳轉到

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)    /* 正確:每次迴圈都真的從裝置讀取 */
    ;

相關條目#