Brian Kernighan & Dennis Ritchie:c 程序设计语言 (第2版)

以下是從提供的資料中提取並詳盡解釋的主要論點:

C 語言的程式結構與基本概念

  • 程式由函數組成: C 程式的核心是由一個或多個函數構成,其中 main 函數是程式執行時的入口點。程式從 main 函數的第一個敘述開始執行。函數是程式碼組織的基本單位,負責完成特定任務。
  • 編譯與執行流程: C 程式通常需要經過編譯器將原始程式碼(例如 .c 檔案)轉換為機器碼組成的執行檔(例如在 UNIX 上是 a.out)。編譯過程包括預處理、編譯、組譯和連結。連結器會將程式所需的標準庫函數(如 printf)與程式碼合併。執行檔可以直接在作業系統上執行。
  • 基本輸入/輸出: 程式與外部世界互動的最基本方式是透過標準輸入(鍵盤或其他輸入來源)和標準輸出(螢幕或其他輸出目的地)。getchar() 函數用於從標準輸入讀取一個字元,putchar(c) 函數用於將字元 c 寫入標準輸出。printf() 函數則提供了更強大的格式化輸出功能,可以輸出各種型別的資料,並控制輸出的格式(如寬度、精度等)。\n 字元用於表示換行。
  • 變數與資料型別: 變數用於儲存資料,必須先宣告後使用。C 語言提供了基本的資料型別,包括整數型別 (int, short, long, char,以及它們的 signedunsigned 版本) 和浮點數型別 (float, double, long double)。不同的型別佔用不同的記憶體空間,並有不同的數值範圍和精度。int 通常反映了機器處理整數的自然大小,但其具體範圍可能因系統而異。char 用於儲存字元,在記憶體中通常以小整數表示(如 ASCII 值)。
  • 常數: 程式中可以直接使用的固定值,包括整數常數(如 123, 0L, 123U),浮點數常數(如 123.4, 1e-2, 123.4F),字元常數(如 'a', '\n', '\013', '\x7'),字串常數(如 "hello, world")。字元常數是單個字元的值,而字串常數是一個以 \0 結束的字元序列。列舉常數 (enum) 提供了另一種定義具名整數常數的方式,提高了程式碼的可讀性。
  • 宣告與初始化: 變數可以在宣告時進行初始化,例如 int i = 0;char esc = '\\';。外部變數和靜態變數如果沒有顯式初始化,會被自動初始化為零。自動變數如果沒有顯式初始化,其值是不確定的。const 關鍵字用於宣告常數變數,其值在初始化後不能被改變。
  • 基本運算子: 包括算術運算子 (+, -, *, /, % 取餘),關係運算子 (>, <, >=, <=, == 等於, != 不等於),邏輯運算子 (&& 邏輯且, || 邏輯或, ! 邏輯非)。關係運算子和邏輯運算子的結果是整數,表示真(非零)或假(零)。
  • 型別轉換: 在運算式中,不同型別的運算元會發生隱式型別轉換(例如,將 int 轉換為 float 以進行浮點數運算)。C 語言有一套明確的轉換規則,稱為「常用算術轉換」,旨在保留精度。也可以使用強制轉換運算子顯式進行型別轉換,例如 (double) n
  • 遞增與遞減運算子: ++-- 運算子用於將變數加一或減一。它們有前置形式(++i, --i,先改變值再使用)和後置形式(i++, i--,先使用值再改變)。
  • 複合賦值運算子: 形式如 op=,例如 i += 2 等價於 i = i + 2。這種形式更簡潔,且在某些情況下可能更有效率。
  • 條件運算子: expr1 ? expr2 : expr3,如果 expr1 為真,則整個運算式的值是 expr2,否則為 expr3。這提供了編寫簡單條件表達式的緊湊方式。
  • 逗號運算子: , 運算子按順序計算多個運算式,整個運算式的值是最後一個運算式的值。它常在 for 迴圈的初始化和迭代部分使用。
  • 運算子優先順序和結合性: 不同的運算子有不同的優先順序和結合性規則,決定了複雜運算式中各部分的求值順序。可以使用括號來覆蓋這些規則。

程式控制流程

  • 敘述與區塊: C 程式由敘述組成,敘述以分號 ; 結束。一對大括號 {} 及其中的內容構成一個複合敘述(或稱區塊),可以在需要單個敘述的地方使用。變數可以在任何區塊的開頭宣告,其作用域限定在該區塊內。
  • if-else 敘述: 根據條件的真假執行不同的程式碼區塊。else 部分是可選的。巢狀的 if-else 中,else 與最近的未配對的 if 結合。可以使用 else-if 結構處理多個互斥的條件。
  • switch 敘述: 根據一個整數表達式的值,從多個 case 標籤中選擇一個程式碼區塊執行。default 標籤是可選的,用於處理不匹配任何 case 的情況。break 敘述用於跳出 switch 敘述,避免執行後續的 case 程式碼。
  • while 迴圈: 在條件為真時重複執行程式碼區塊。先檢查條件,後執行迴圈體。
  • for 迴圈: 一個更通用的迴圈結構,包含初始化、條件檢查和迭代表達式。形式為 for (initialization; condition; iteration) statement;。任何部分都可以省略。for (;;) 是一個無限迴圈。
  • do-while 迴圈: 先執行迴圈體一次,然後在條件為真時重複執行。至少執行一次迴圈體。
  • breakcontinue break 敘述用於立即跳出最內層的 while, for, do-whileswitch 敘述。continue 敘述用於跳過當前迴圈迭代的剩餘部分,並繼續下一次迭代的條件檢查(while, do-while, for)。
  • goto 敘述: 允許無條件跳轉到程式中標有特定標籤的位置。雖然 C 語言支援 goto,但通常建議避免使用,以免產生難以理解和維護的「麵條式程式碼」。在某些特定情況下,如從深層巢狀結構中跳出到錯誤處理程式碼,goto 可能會簡化程式碼。

函數與程式結構

  • 函數定義: 函數定義包括回傳型別、函數名、參數列表(包含參數型別和名稱)和函數體。void 型別用於表示函數不回傳值。
  • 函數原型: 在函數使用(呼叫)之前,應提供函數原型(聲明),告知編譯器函數的名稱、回傳型別和參數型別。ANSI C 推薦使用帶參數型別的函數原型(例如 int power(int, int);),這使得編譯器可以檢查函數呼叫時參數的型別是否正確。
  • 參數傳遞: C 語言使用「傳值呼叫」方式傳遞函數參數。函數接收的是參數值的副本,對參數副本的修改不會影響原始參數。要修改原始參數,必須傳遞指向原始參數的指標。
  • 回傳值: 函數使用 return 敘述回傳一個值(型別應與函數的回傳型別相符)並結束執行。沒有回傳值的函數(或回傳型別為 void 的函數)也可以使用 return; 結束執行。
  • 作用域規則: 變數的作用域決定了它在程式中哪些地方可以被訪問。自動變數(在函數或區塊內部宣告)具有區塊作用域。函數參數具有函數作用域。外部變數(在所有函數外部宣告)和靜態變數具有檔案作用域或區塊作用域,具體取決於宣告位置。
  • 連結性 (externstatic): 外部變數和函數預設具有外部連結性,可以在程式的多個原始檔案中共享。extern 關鍵字用於在其他檔案中聲明一個外部變數或函數。static 關鍵字用於限制變數或函數的作用域或連結性:用於外部變數或函數時,限制其檔案作用域,使其不能被其他檔案訪問;用於函數或區塊內部的自動變數時,使其在函數呼叫結束後仍保留其值(靜態儲存期)。
  • 多檔案程式: 大型程式通常分割成多個原始檔案。不同的檔案可以透過 extern 聲明共享外部變數和函數。編譯時需要將所有相關的原始檔案或目標檔案連結起來。
  • 標頭檔: 標頭檔(.h 檔案)用於存放函數原型、巨集定義 (#define)、型別定義 (typedef) 和外部變數聲明。程式中需要使用這些定義時,透過 #include 指示字將標頭檔包含進來。#include <filename> 通常用於包含標準庫標頭檔,#include "filename" 用於包含使用者自訂標頭檔。
  • 預處理器: C 預處理器在編譯之前處理原始程式碼。主要的預處理器指示字包括:
    • #define: 定義巨集,可以定義常數或帶參數的巨集函數。巨集在預處理階段進行文本替換。
    • #undef: 取消已定義的巨集。
    • 條件式編譯 (#if, #ifdef, #ifndef, #elif, #else, #endif): 根據條件包含或排除程式碼段,常用於處理不同平台或編譯選項。
    • #include: 包含其他檔案的內容。
    • #line, #error, #pragma: 其他用於控制編譯器或產生錯誤的指示字。
  • 遞迴: 函數可以直接或間接地呼叫自身。遞迴在處理樹狀結構或需要分解為相似子問題的問題時非常有用(如快速排序)。

陣列與指標

  • 陣列: 陣列是儲存相同型別元素的連續記憶體區域。陣列元素透過索引存取(從 0 開始)。陣列名本身在大多數情況下代表陣列第一個元素的位址。
  • 指標: 指標是一個變數,其值是另一個變數的位址。宣告指標使用 *(如 int *ip;)。& 運算子用於取一個變數的位址(如 ip = &x;)。* 運算子用於間接存取(解引用)指標指向的值(如 y = *ip;)。
  • 指標與陣列的關係: 在 C 語言中,指標和陣列有著非常緊密的關係。陣列名 a 在大多數情況下等價於 &a[0](陣列第一個元素的位址)。a[i] 等價於 *(a+i)。這意味著指標可以像陣列一樣使用索引來存取元素(例如 pa[i] 等價於 *(pa+i)),反之,陣列名也可以用於指標運算(但在大多數情況下,陣列名是一個常數位址,不能被修改,如 a++ 是非法的)。
  • 指標算術: 指標可以進行算術運算。例如,p+i 表示指向 p 所指元素之後的第 i 個元素的位址。p++ 使指標指向下一個元素。兩個指向同一陣列元素的指標可以相減,結果是它們之間元素的個數(單位是元素的大小)。
  • 字元指標與字串: C 語言中的字串是字元陣列,以 \0(空字元)結束。字串常數(如 "hello")儲存在程式的靜態儲存區,其型別是 char 陣列。字元指標常被用於操作字串,例如 char *p = "hello"; 宣告一個字元指標並指向字串常數的開始位址。標準函式庫 <string.h> 提供了處理字串的常用函數(如 strcpy, strlen, strcmp)。
  • 指標作為函數參數: 為了讓函數能夠修改呼叫者函數中的變數,可以將變數的指標傳遞給函數。例如,swap 函數需要接收兩個整數的指標才能交換它們的值。
  • 指向函數的指標: 函數名在運算式中可以表示函數的位址。可以宣告指向函數的指標,並透過指標呼叫函數。這在實現泛型演算法(如 qsort 函數可以接受一個指向比較函數的指標)時非常有用。
  • 命令列參數: main 函數可以接受兩個參數:argc (argument count) 表示命令列參數的個數,argv (argument vector) 是一個字元指標陣列,其中 argv[0] 是程式名,argv[1]argv[argc-1] 是實際的參數字串。
  • 複雜宣告的解讀: C 語言的宣告語法可能很複雜(如指向函式的指標陣列)。理解宣告的核心是找到被宣告的名稱,然後從內往外、按優先順序(主要看括號 () 和方括號 [])解讀其型別。typedef 可以用於簡化複雜的宣告。

結構、聯合與位元欄位

  • 結構 (struct): 結構允許將不同型別的變數組合在一起,形成一個單一的邏輯實體。結構的成員透過 . 運算子存取。
  • 指向結構的指標: 可以宣告指向結構的指標。透過指標存取結構成員時,使用 -> 運算子(例如 p->member 等價於 (*p).member)。
  • 結構的陣列: 可以宣告結構型別的陣列。
  • 巢狀結構: 結構的成員可以是另一個結構。
  • 遞迴結構: 結構可以包含指向同型別結構的指標,這是實現鏈結串列、樹等資料結構的基礎。
  • 聯合 (union): 聯合允許在同一塊記憶體空間中儲存不同型別的資料,但只能在任何時候儲存其中一種型別。聯合的大小由其最大成員的大小決定。
  • 位元欄位 (bit-fields): 在結構中,可以指定整數成員的位元寬度,以緊湊地儲存多個旗標或小整數。
  • typedef typedef 用於為現有型別創建新的名稱(別名),包括基本型別、陣列、指標、結構、聯合和函數指標等。這可以提高程式碼的可讀性和可移植性。

標準函式庫與系統介面

  • 標準 I/O 庫 (<stdio.h>): 提供了一組用於處理檔案和標準 I/O 流的函數。
    • 檔案操作:fopen 開啟檔案,fclose 關閉檔案,freopen 重新定向流。
    • 字元 I/O:getc, putc, getchar, putchar, ungetc (將字元退回輸入流)。
    • 行 I/O:fgets 從流讀取一行,fputs 向流寫入一行。
    • 格式化 I/O:printf, scanf (標準流),fprintf, fscanf (指定流),sprintf, sscanf (字串)。
    • 檔案狀態和錯誤:feof (檢查是否到達檔案尾), ferror (檢查是否發生錯誤), perror (列印系統錯誤訊息)。
  • 標準函式庫的其他部分: ANSI C 標準庫提供了豐富的功能:
    • <ctype.h>: 字元分類和轉換函數 (如 isalpha, isdigit, tolower, toupper)。
    • <string.h>: 字串處理函數 (如 strcpy, strlen, strcmp, strstr)。
    • <stdlib.h>: 通用工具函數 (如 atof, atoi, atol, rand, srand, malloc, free, exit, system, bsearch, qsort, abs, div)。
    • <math.h>: 數學函數 (如 sin, cos, sqrt, pow, fabs)。
    • <time.h>: 時間和日期函數。
    • <assert.h>: 斷言巨集,用於程式調試。
    • <stdarg.h>: 支援可變參數列表的函數。
  • 低階 I/O (UNIX 介面示例): 提供了比標準 I/O 更接近作業系統的介面,通常使用檔案描述符 (integers) 而非 FILE 指標。
    • read(fd, buf, n): 從檔案描述符 fd 讀取最多 n 個位元組到緩衝區 buf
    • write(fd, buf, n): 將緩衝區 buf 中的 n 個位元組寫入檔案描述符 fd
    • open(name, flags, perms): 開啟檔案並回傳檔案描述符。
    • creat(name, perms): 建立或截斷檔案並回傳檔案描述符。
    • close(fd): 關閉檔案描述符。
    • unlink(name): 刪除檔案。
    • lseek(fd, offset, origin): 在檔案中定位讀寫位置。
  • 檔案系統資訊: stat(name, &stbuf)fstat(fd, &stbuf) 函數用於獲取檔案的資訊(如大小、型別、權限等),資訊儲存在 struct stat 結構中。
  • 目錄讀取: 在 UNIX 系統中,可以使用 opendir 開啟目錄,readdir 讀取目錄中的條目,closedir 關閉目錄。
  • 動態記憶體分配: malloc(size) 從堆中分配指定大小的記憶體區塊,並回傳指向該區塊的指標。calloc(nobj, size) 分配一個能容納 nobj 個大小為 size 的元素的空間,並將其初始化為零。free(p) 釋放 malloccalloc 分配的記憶體。這些函數定義在 <stdlib.h> 中。提供的資料中包含了一個簡化的 mallocfree 實現示例,展示了如何管理可用記憶體區塊的自由列表。

C 語言特性與設計哲學

  • 精簡的核心語言: C 語言本身相對小巧,不包含許多在其他語言中內建的功能(如 I/O、字串操作、記憶體管理)。這些功能透過標準函式庫提供,這使得 C 語言的核心更易於學習和實現,同時提供了很大的靈活性。
  • 貼近硬體: C 語言提供了指標和位元運算等低階功能,可以直接操作記憶體和位元層級的資料,這使得它適合編寫作業系統、嵌入式系統程式和對效能要求高的應用程式。
  • 注重效率: C 語言的設計考慮了生成高效的機器碼。其控制結構和資料型別都旨在與典型的電腦硬體架構緊密對應。
  • 可移植性: 雖然 C 提供了低階介面,但透過標準函式庫和對不同平台特性的抽象(如 sizeof 運算子),C 程式可以在不同的硬體和作業系統平台上編譯和執行,具有良好的可移植性。ANSI C 標準的出現進一步增強了 C 語言的可移植性。

這些論點共同描繪了 C 語言作為一種強大、靈活且高效的通用程式語言的特性,特別適合系統程式設計和對資源控制要求嚴格的應用場景。