C 語言使得程式設計者能以結構化且有條理的方法來設計程式 本書將簡單介紹 C 程式的設計, 並舉出數個例子來說明 C 語言的一些重要特性 第三和第四章將會介紹 C 的結構化程式設計 (structured programming)
我們從一個簡單的 C 程式開始 第一個例子是列印一行文字
第 1 行和第 2 行 /* Fig. 2.1: fig02_01.c A first program in C */ 以 /* 開頭而以 */ 結尾, 代表這是兩行註解 (comment) 你可以在程式中加入一些註解 (document programs), 使程式較容易閱讀 當程式在執行的時候, 電腦會跳過註解的部分
註解會被 C 編譯器略過, 因此也不會為註解產生任何機器語言目的碼 註解有助於其他人來了解你的程式
C99 也引入了 C++ 的 // 單行註解 (single-line comments), 從 // 開始到這行的最末端都會被視為註解 這可以是單獨一行的註解, 或是某一行的右半段為註解的情況 有些程式設計師比較喜歡 // 註解, 這是因為這種註解比較短, 而且不會產生一些 /* */ 註解常帶來的程式設計錯誤 第 3 行 #include <stdio.h> 是個 C 前置處理器 (C preprocessor) 指令
在程式編譯之前, 前置處理器會處理以 # 開頭的每一行 第 3 行告訴前置處理器將標準輸入 / 輸出標頭檔 (standard input/output header) stdio.h 引入到程式裡面 這個前置檔含有編譯器在編譯標準輸入 / 輸出函式庫函式 ( 如 printf) 時, 所用到的資訊 第 6 行 int main( void ) 是每個 C 程式都有的部份 在 main 之後的小括號表示 main 是程式的一個建構區塊, 稱為函式 (function)
C 程式中可含有一個或多個函式, 不過其中一個必須是 main 每個 C 程式都是從 main 函式開始執行 位於 main 左邊的關鍵字 int, 表示 main 函式會 回傳 一個整數值 在第五章中, 我們將介紹如何建構你自己的函式, 將會解釋函式的 回傳值 是什麼意思
目前我們只要在每個程式的 main 左邊擺個 int 關鍵字就行了 當函式被呼叫執行時, 也可以接收資訊 括號內的 void 表示 main 沒有接收任何資訊
每個函式本體 (body) 以左大括號開始 右大括號 (right brace)} ( 第 11 行 ) 表示每個函式的結束位置 這兩個括號以及之間的程式稱為一個區塊 (block) 第 8 行 printf( "Welcome to C!\n" ); 指示電腦執行一個動作 (action), 將雙引號內的字串 (string) 顯示在螢幕上 字串有時稱做字元字串 (character string) 訊息 (message), 或是字面常數 (literal)
這一整行, 包括 printf 括號 括號裡的引數 (argument) 及分號 (semicolon ; ) 等, 稱為一道敘述式 (statement) 每行敘述須以分號 ( 也叫敘述結束符號 ) 做結束 當前述的 printf 敘述式執行時, 它會在螢幕上印出 Welcome to C! 通常螢幕上所顯示的結果會和 printf 敘述式中雙引號所括起來的內容一樣 然而, 請注意字元 \n 不會顯示在螢幕上 反斜線符號 (\) 稱為跳脫字元 (escape character) 它表示 printf 應印出一些特殊的字元
當字串中出現反斜線時 ( 也就是跳脫字元 ), 編譯器就會找尋下一個字元, 與反斜線結合成跳脫序列 (escape sequence) 跳脫序列 \n 表示新增一行 (newline) 當換行字元出現在 printf 輸出的字串當中, 換行字元會將螢幕上的游標移到下一行的起始位置 其他一些常見的跳脫序列請參閱圖 2.2
因為反斜線在字串中具有特殊的意義, 例如, 編譯器將它視為跳脫字元, 所以我們若想顯示 \ 時, 就必須以二個 \ 來表示 此外, 想要列印雙引號 (") 也會帶來一些問題, 因為它被視為是所列印字串的分界符號, 因此雙引號不會被列印出來 因此若想用 printf 顯示雙引號, 則必須在字串中以跳脫序列 \" 來表示
第 10 行 return 0; /* indicate that program ended successfully */ 位在每一個 main 函式的結束處 關鍵字 return 是我們用來離開函式 (exit a function) 的方法之一 如上所示, 在 main 函式的結束處使用 return 敘述時, 若值為 0, 表示該程式成功結束 右大括號 (right brace)} ( 第 12 行 ) 表示 main 函式的結束位置
我們說 printf 使編譯器執行一個動作 (action) 當任何程式在執行時, 它會進行各種不同的動作以及決策的判斷 (decisions) 而在第三章裡, 我們將更進一步地介紹有關程式設計的動作 / 判斷模式 (action/decision model)
標準函式庫的函式 ( 如 printf 或 scanf) 並不是 C 語言的一部分 所以編譯器無法發現他們是否拼字錯誤 當編譯器在編譯 printf 敘述式時, 它只是在目的碼中空出一塊空間來呼叫函式庫函式 編譯器不知道函式庫函式在哪裡 但是聯結器知道 一直要到連結器進行連結的時候, 才會找出函式庫函式的位置, 然後在目的碼中插入對函式庫函式正確的呼叫
此時, 目的碼便是完整且可執行的了 因此, 已經聯結的程式通常稱為可執行 (executable) 的程式 如果將函式名稱拼錯, 連結器將會發現這個錯誤, 因為它無法將 C 程式中的這個錯誤名稱正確地配對到函式庫中任何現存的函式
printf 函式能以數種不同的方式印出 Welcome to C! 舉例來說, 圖 2.3 的程式就可以產生與圖 2.1 程式相同的輸出 這是因為每個 printf 都會從上一個 printf 結束列印的地方開始列印 第一個 printf ( 第 8 行 ) 印出 Welcome 及一個空格, 而第二個 printf ( 第 9 行 ) 便從同一行的空格之後開始列印
同一個 printf 可利用 newline 字元印出數行文字 ( 如圖 2.4) 每次碰到 \n (newline) 跳脫序列時,printf 便移至下一行的起頭位置
我們的下一個程式使用了標準函式庫函式 scanf, 來讀進使用者鍵入的兩個整數, 然後計算這兩個值的和, 再以 printf 將結果印出來 [ 在圖 2.5 的輸入 / 輸出對話中, 我們把使用者輸入數字的部份以粗體表示 ]
第 8-10 行 int integer1; /* first number to be input by user */ int integer2; /* second number to be input by user */ int sum; /* variable in which sum will be stored */ 都是定義 (definitions) integer1 integer2 和 sum 是變數 (variables) 名稱 變數就是電腦記憶體中的某個位置, 它可以存放數值供程式使用 這些定義指定 integer1 integer2 和 sum 為 int 型別的變數, 意思是這三個變數所存放的將會是整數 (integer) 值, 像 7-11 0 31914 這些數就是整數
所有的變數使用之前, 均需在 main 本體開始的左大括號之後宣告 前述的定義也可以合併成為單一定義敘述式, 如下 : int integer1, integer2, sum; 但這樣就無法用相對應的註解清楚地描述每個變數的功用, 就像我們在第 8-10 行所做的那樣 C 語言當中變數的名稱可以是任意合法的識別字 (identifier) 識別字是由字母 數字和底線 (_) 成的一連串字元, 但第一個字元不可是數字
識別字的長度沒有限制, 不過 C 標準中規定編譯器只需辨認前 31 個字元 C 會區分大小寫 (case sensitive), 所以 a1 與 A1 是不同的識別字.
宣告必須放在函式的左大括號以及任何可執行的敘述式之間 當編譯器無法辨識某一敘述式時, 便會產生語法錯誤 編譯器通常會發出錯誤訊息, 幫助你找出並修改錯誤的敘述 語法錯誤違反語言規則 語法錯誤也稱為編譯錯誤 (compile errors), 或編譯時期錯誤 (compile-time errors)
第 12 行 printf( "Enter first integer\n" ); /* prompt */ 會在螢幕上印出字面常數 Enter first integer, 並將游標移至下一行的開頭 這個訊息就叫作提示 (prompt), 因為它指示使用者進行某特定動作 下一個敘述式 scanf( "%d", &integer1 ); /* read an integer */ 使用 scanf 從使用者處讀取一個數值 scanf 函式由標準輸入 ( 通常是鍵盤 ) 讀取輸入值
這一個 scanf 有兩個引數 %d 和 & integer1 第一個引數是格式控制字串 (format control string), 它顯示出使用者應該輸入的資料型別 %d 這個轉換指定詞 (conversion specifier) 表示資料應該是整數 ( 字母 d 代表十進制整數 ) 這裡的 % 被 scanf (printf 亦然 ) 視為一個跳脫字元, 作為轉換指定詞的開端 scanf 的第二個引數以 &(ampersand) 為開頭, 稱為位址運算子 (address operator), 後面接一個變數名稱
& 加上變數名稱, 是用來告訴 scanf, 變數 integer1 存放在記憶體中的什麼地方 ( 位址 ) 然後電腦就會將 integer1 的值儲存在那個地方 & 的使用通常讓新手或沒有用過這個符號的其他語言程式設計師感到困惑 現在你只要記住在 scanf 敘述式的每個變數前, 都要加 & 即可
當電腦執行前述的 scanf 時, 它會等待使用者輸入變數 integer1 的值 使用者鍵入一個整數並按 Enter 鍵 (Enter key) 將數值傳給電腦 電腦便將此數字, 或值 (value) 設定給變數 integer1 在接下來的程式中, 任何對 integer1 的參考都將使用這一個數值 printf 和 scanf 函式構成了電腦與使用者之間的交談 因為這種互動像是一般的對話, 因此稱為對話操作 (conversational computing) 或互動式操作 (interactive computing)
第 15 行 printf( "Enter second integer\n" ); /* prompt */ 在螢幕上印出 Enter second integer 這個訊息, 然後將游標移到下一行的開頭 這個 printf 也是用來提示使用者進行某項動作 敘述式 scanf( "%d", &integer2 ); /* read an integer */ 則從使用者的輸入得到變數 integer2 的值
第 18 行的指定敘述句 (assignment statement) sum = integer1 + integer2; /* assign total to sum */ 計算了變數 integer1 和 integer2 的和, 並使用指定運算子 = (assignment operator) 將結果設定給變數 sum 這道敘述式應讀成 sum 得到 integer1 + integer2 的值 大部份的計算都是用指定敘述式來執行 運算子 = 和 + 稱為二元運算子 (binary operator), 因為它們具有二個運算元 (operands) + 運算子的兩個運算元是 integer1 和 integer2 而 = 運算子的兩個運算元則是 sum 和運算式 integer1 + integer2 的值
第 20 行 printf( "Sum is %d\n", sum ); /* print sum */ 呼叫函式 printf, 在螢幕上印出字面常數 Sum is 以及變數 sum 的值 這個 printf 函式有兩個引數 _Sum is %d\n 和 sum 第一個引數是所謂的格式控制字串 它包含了一些待印的字面常數, 以及轉換指定詞 %d, 表示有個整數將要被列印 第二個引數則指明了要被列印的數值 請注意整數的轉換指定詞在 printf 和 scanf 裡是相同的
上面兩道敘述式可以合併成 printf( "Sum is %d\n", integer1 + integer2 ); 第 22 行 return 0; /* indicate that program ended successfully */ 將數值 0 傳回執行程式的作業系統環境 這個值是用來告訴作業系統, 程式已經成功地執行了 至於如何報告程式的錯誤, 請參考你所使用作業系統環境的手冊 程式的最後一行 ( 第 24 行 ) 是個右大括號 (}), 它代表了 main 函式的結束
像 integer1 integer2 和 sum 這樣的變數名稱都會對應到電腦裡的記憶體位置 (locations) 每個變數都具有一個名稱 (name) 一個型別 (type) 及一個數值 (value) 在圖 2.5 的加法程式中, 當敘述式 ( 第 13 行 ) scanf( "%d", &integer1 ); /* read an integer */ 執行時, 使用者鍵入的數值會被放在 integer1 所被指定的記憶體位置 假設使用者輸入數字 45 作為變數 integer1 的值 則電腦會將 45 放到變數 integer1 的位置, 如圖 2.6 所示
當數值放入記憶體位置時, 會蓋掉該位置原本的值, 因此, 將新數值放到記憶體位置是有破壞性的 回到我們的加法程式, 當下列敘述式 ( 第 16 行 ) scanf( "%d", &integer2 ); /* read an integer */ 執行時, 假設使用者輸入 72, 這個數值就會存到 integer2 的位置, 其記憶體配置如圖 2.7 所示 這些位置不一定是記憶體中相鄰的位置
在程式讀取 integer1 和 integer2 的值之後, 它會將這兩個值相加, 然後將其和放到變數 sum 敘述式 ( 第 18 行 ) sum = integer1 + integer2; /* assign total to sum */ 會執行加法, 並取代目前 sum 的值
這發生在將 integer1 和 integer2 相加後的結果放到 sum 的位置 ( 破壞了 sum 中可能已有的數值 ) 計算 sum 之後, 記憶體就如圖 2.8 所示 我們可看到 integer1 和 integer2 的值和他們被相加之前是一樣的
電腦執行計算時, 只是利用這些數值而已, 不會破壞它們 因此, 從記憶體讀取數值時, 其過程不具破壞性 (nondestructive, 不會改變數值的內容 )
圖 2.9 列出了 C 的算術運算子 (arithmetic operators) 請注意, 在這裡用到了幾個代數中並未使用的特殊符號 *(asterisk) 代表乘, 而 %(percent sign) 代表模數運算子, 我們將會在下面介紹 在代數裡面, 如果我們想將 a 乘以 b, 只要寫成 ab 即可 不過我們若在 C 裡寫出 ab, 代表的是一個有兩個字元的識別字 因此 C 需要用 * 運算子來明確地表示相乘, 如 a*b.
算術運算子都是二元運算子 例如, 運算式 3+7 包含了二元運算子 + 和運算元 3 及 7 整數除法 (Integer division) 得到的結果會是個整數 如 7/4 會等於 1, 而 17/5 會等於 3 C 還提供了模數運算子 %, 它的運算結果是整數相除後的餘數 模數運算子是個整數運算子, 它的運算元必須是整數 運算式 x % y 會產生 x 除以 y 的餘數 因此 7%4 等於 3, 而 17%5 等於 2
在 C 運算式中, 小括號的用法跟代數運算的用法很像 例如, 要將 a 乘以 b + c 的值, 就寫成 a * ( b + c )
C 在算術運算式中的運算子用法, 依照下述的 運算子優先權規則 (rules of operator precedence) 決定運算的順序, 一般而言與代數中的規則相同 : 在同一對小括號中的運算子先進行計算 因此, 你可以依自己的需要, 用小括號決定運算的順序 小括號具有 最高優先權 若為巢狀 (nested), 或稱嵌入 (embedded) 的小括號 (parentheses), 如 ( ( a + b ) + c ) 最裡面那對小括號中的運算子會先計算
乘法 除法和模數運算會先處理 若運算式有多個乘法 除法和模數運算, 則從左到右進行計算 因此乘法 除法和模數運算具有相同的優先權層級 接下來進行加和減的運算 若運算式有多個加減法運算, 則從左到右進行計算 加和減的優先順序等級是相同的, 但是優先次序低於乘法 除法以及模數運算子
運算子優先順序的規則是讓 C 能以指定的順序計算運算式 此外, 當我們講到由左至右地運算時, 指的是運算子的結合性 (associativity) 往後我們將會看到有些運算子的結合性是由右至左 圖 2.10 列出上述的運算子優先順序規則
圖 2.11 說明了運算子執行的順序
跟代數運算一樣, 在運算式中加入非必要的小括號, 可讓運算式的計算順序更清楚 這些括號稱為多餘括號 (redundant parentheses)
可執行的 C 敘述式若不是執行某一項動作 (actions) ( 如計算或資料的輸入輸出 ), 便是進行某項判斷 (decisions) ( 我們很快會見到幾個範例 ) 例如我們會在程式裡判斷某位學生的月考成績是否大於等於 60 分, 如果是的話, 便印出 Congratulations! You passed. 本節將介紹 C 的 if 敘述式的簡單版本, 這種版本可讓程式根據某個稱為條件式 (condition) 之敘述式的真偽, 來進行判斷
若條件吻合, 也就是條件式為真 (true), 則程式會執行 if 本體中的敘述式 若條件不吻合, 也就是條件是偽 (false), 則程式不會執行本體中的敘述 不論本體內敘述式是否執行, 當 if 敘述式完成後, 接下來執行的是 if 敘述式之後的下一個敘述式 在 if 敘述中的條件式, 通常是由等號運算子 (equality operator) 和關係運算子 (relational operator) 所組成
所有關係運算子都擁有相同的優先權, 並從左到右進行結合運算 等號運算子的優先順序要比關係運算子低, 他們也是由左至右地結合 C 的條件式可以是任何為零 ( 偽 ) 或非零 ( 真 ) 的運算式
要避免困擾, 等號運算子應該讀成 雙等號, 而指定運算子應該讀成 取得值 或是 設定為值 我們即將會看到, 混用 == 和 = 這兩個運算子並不一定會造成明顯的語法錯誤, 卻有可能引發極嚴重的邏輯錯誤
圖 2.13 的例子用了 6 個 if 敘述式來比較使用者輸入的兩個數 如果任何一個 if 敘述式的條件滿足的話, 相關的 printf 敘述式便會執行
程式利用 scanf 來輸入兩個數值 ( 第 15 行 ) 每個轉換指定詞都有其相對應用來存放數值的引數 第一個 %d 將數值轉換儲存到變數 num1, 第二個 %d 則將數值轉換儲存到變數 num2 將每個 if 敘述式的本體縮排, 並在 if 敘述式的前後留一行空白, 將可增進程式的可讀性
每個函式本體 (body) 以左大括號開始 每個 if 敘述式的本體皆以對應的右大括號作為結束 ( 如 19 行 ) if 敘述式的本體可以置入任意數量的敘述式 圖 2.13 的註解 ( 第 1-3 行 ) 分成三行 在 C 程式裡, 空白字元 (white space, 如定位 換行 空格等等 ) 一般都是被忽略的 因此, 敘述式和註解可以分成數行來撰寫 不過將識別字拆成分屬於不同行則是不正確的
圖 2.14 列出本章所介紹運算子的運算優先順序 運算子的優先權是從上到下遞減 注意等號也是個運算子 除了指定運算子 = 之外, 其他所有運算子的結合性都是由左至右 指定運算子 (=) 是從右至左結合
本章的 C 程式中, 有些字 ( 特別是 int, return 及 if) 是 C 語言的關鍵字 (keywords) 或保留字 (reserved words) 圖 2.15 包含了 C 的關鍵字 這些字對 C 編譯器來說都具有特殊的意義, 因此程式設計師必須小心不可以將他們用來做為識別字 ( 如變數名稱 )