1. 程式人生 > 其它 >C 語言概述

C 語言概述

目錄

C 程式是什麼樣子的?初見 C 程式會覺得有些古怪,程式中有許多 {cp->tort*ptr++ 這樣的符號。然而,在學習 C 的過程中,對這些符號和 C 語言特有的其他符號會越來越熟悉,甚至會喜歡上它們。如果熟悉與 C 相關的其他語言,會對 C 語言有似曾相識的感覺。本文,我們從演示一個簡單的程式示例開始,解釋該程式的功能。同時,強調一些 C 語言的基本特性。

一、簡單的 C 程式示例

我們來看一個簡單的 C 程式,如程式清單 1 所示。該程式演示了用 C 語言程式設計的一些基本特性。請先通讀程式清單 1,看看自己是否能明白該程式的用途,再認真閱讀後面的解釋。

程式清單 1 first.c 程式

#include <stdio.h>
int main(void)                    /* 一個簡單的C程式 */
{
     int num;                     /* 定義一個名為num的變數 */
     num = 1;                     /* 為num賦一個值 */

     printf("I am a simple ");    /* 使用printf()函式 */
     printf("computer.\n");
     printf("My favorite number is %d because it is first.\n",num);

     return 0;
}

如果你認為該程式會在螢幕上列印一些內容,那就對了!光看程式也許並不知道列印的具體內容,所以,執行該程式,並檢視結果。首先,用你熟悉的編輯器(或者編譯器提供的編輯器)建立一個包含程式清單 1 中所有內容的檔案。給該檔案命名,並以 .c 作為副檔名,以滿足當前系統對檔名的要求。例如,可以使用 first.c。現在,編譯並執行該程式。如果一切執行正常,該程式的輸出應該是:

I am a simple computer.
My favorite number is 1 because it is first.

總而言之,結果在意料之中,但是程式中的 \n%d 是什麼?程式中有幾行程式碼看起來有點奇怪。接下來,我們逐行解釋這個程式。

程式調整

程式的輸出是否在螢幕上一閃而過?某些視窗環境會在單獨的視窗執行程式,然後在程式執行結束後自動關閉視窗。如果遇到這種情況,可以在程式中新增額外的程式碼,讓視窗等待使用者按下一個鍵後才關閉。一種方法是,在程式的 return 語句前新增一行程式碼:

getchar();

這行程式碼會讓程式等待擊鍵,視窗會在使用者按下一個鍵後才關閉。

二、示例解釋

我們會把程式清單 1 的程式分析兩遍。第 1 遍(快速概要)概述程式中每行程式碼的作用,幫助讀者初步瞭解程式。第 2 遍(程式細節)詳細分析程式碼的具體含義,幫助讀者深入理解程式。

圖 1 總結了組成 C 程式的幾個部分,圖中包含的元素比第 1 個程式多。

圖 1 C 程式解剖

2.1 第 1 遍:快速概要

本節簡述程式中的每行程式碼的作用。下一節詳細討論程式碼的含義。

#include<stdio.h>        ←包含另一個檔案

該行告訴編譯器把 stdio.h 中的內容包含在當前程式中。stdio.h 是 C 編譯器軟體包的標準部分,它提供鍵盤輸入和螢幕輸出的支援。

int main(void)        ←函式名

C 程式包含一個或多個函式,它們是 C 程式的基本模組。程式清單 1 的程式中有一個名為 main() 的函式。圓括號表明 main() 是一個函式名。int 表明 main() 函式返回一個整數,void 表明 main() 不帶任何引數。這些內容我們稍後詳述。現在,只需記住 int 和 void 是標準 ANSI C 定義 main() 的一部分(如果使用 ANSI C 之前的編譯器,請省略 void;考慮到相容的問題,請儘量使用較新的 C 編譯器)。

/* 一個簡單的C程式 */         ←註釋

註釋在 /**/ 兩個符號之間,這些註釋能提高程式的可讀性。注意,註釋只是為了幫助讀者理解程式,編譯器會忽略它們。

{         ←函式體開始

左花括號表示函式定義開始,右花括號(})表示函式定義結束。

int num;         ←宣告

該宣告表明,將使用一個名為 num 的變數,而且 num 是 int(整數)型別。

num = 1;         ←賦值表示式語句

語句 num = 1; 把值 1 賦給名為 num 的變數。

printf("I am a simple ");    ←呼叫一個函式

該語句使用 printf() 函式,在螢幕上顯示 I am a simple,游標停在同一行。printf() 是標準的 C 庫函式。在程式中使用函式叫作呼叫函式。

printf("computer.\n");     ←呼叫另一個函式

接下來呼叫的這個 printf() 函式在上條語句打印出來的內容後面加上“computer”。程式碼 \n 告訴計算機另起一行,即把游標移至下一行。

printf("My favorite number is %d because it is first.\n", num);

最後呼叫的 printf() 把 num 的值(1)內嵌在用雙引號括起來的內容中一併列印。%d 告訴計算機以何種形式輸出 num 的值,列印在何處。

return 0;     ←return語句

C 函式可以給呼叫方提供(或返回)一個數。目前,可暫時把該行看作是結束 main() 函式的要求。

}        ←結束

必須以右花括號表示程式結束。

2.2 第 2 遍 :程式細節

瀏覽完程式清單 1 後,我們來仔細分析這個程式。再次強調,本節將逐行分析程式中的程式碼,以每行程式碼為出發點,深入分析程式碼背後的細節,為更全面地學習 C 語言程式設計的特性夯實基礎。

1.#include指令和標頭檔案

#include<stdio.h>

這是程式的第 1 行。#include <stdio.h> 的作用相當於把 stdio.h 檔案中的所有內容都輸入該行所在的位置。實際上,這是一種“拷貝-貼上”的操作。include 檔案提供了一種方便的途徑共享許多程式共有的資訊。

#include 這行程式碼是一條 C 前處理器指令(preprocessor directive)。通常,C 編譯器在編譯前會對原始碼做一些準備工作,即預處理(preprocessing)。

所有的 C 編譯器軟體包都提供 stdio.h 檔案。該檔案中包含了供編譯器使用的輸入和輸出函式(如,printf())資訊。該檔名的含義是標準輸入/輸出標頭檔案。通常,在 C 程式頂部的資訊集合被稱為標頭檔案(header)。

在大多數情況下,標頭檔案包含了編譯器建立最終可執行程式要用到的資訊。例如,標頭檔案中可以定義一些常量,或者指明函式名以及如何使用它們。但是,函式的實際程式碼在一個預編譯程式碼的庫檔案中。簡而言之,標頭檔案幫助編譯器把你的程式正確地組合在一起。

ANSI/ISO C 規定了 C 編譯器必須提供哪些標頭檔案。有些程式要包含 stdio.h,而有些不用。特定 C 實現的文件中應該包含對 C 庫函式的說明。這些說明確定了使用哪些函式需要包含哪些標頭檔案。例如,要使用 printf() 函式,必須包含 stdio.h 標頭檔案。省略必要的標頭檔案可能不會影響某一特定程式,但是最好不要這樣做。本書每次用到庫函式,都會用 #include 指令包含 ANSI/ISO 標準指定的標頭檔案。

注意 為何不內建輸入和輸出

讀者一定很好奇,為何不把輸入和輸出這些基本功能內建在語言中。原因之一是,並非所有的程式都會用到 I/O(輸入/輸出)包。輕裝上陣表現了 C 語言的哲學。正是這種經濟使用資源的原則,使得 C 語言成為流行的嵌入式程式語言(例如,編寫控制汽車自動燃油系統或藍光播放機晶片的程式碼)。#include 中的 # 符號表明,C 前處理器在編譯器接手之前處理這條指令。

2.main()函式

int main(void)

程式清單 1 中的第 2 行表明該函式名為 main。的確,main 是一個極其普通的名稱,但是這是唯一的選擇。C 程式一定從 main() 函式開始執行(目前不必考慮例外的情況)。除了 main() 函式,你可以任意命名其他函式,而且 main() 函式必須是開始的函式。圓括號有什麼功能?用於識別 main() 是一個函式。很快你將學到更多的函式。就目前而言,只需記住函式是 C 程式的基本模組。

int 是 main() 函式的返回型別。這表明 main() 函式返回的值是整數。返回到哪裡?返回給作業系統。

通常,函式名後面的圓括號中包含一些傳入函式的資訊。該例中沒有傳遞任何資訊。因此,圓括號內是單詞 void。

如果瀏覽舊式的 C 程式碼,會發現程式以如下形式開始:

main()

C90 標準勉強接受這種形式,但是 C99 和 C11 標準不允許這樣寫。因此,即使你使用的編譯器允許,也不要這樣寫。

你還會看到下面這種形式:

void main()

一些編譯器允許這樣寫,但是所有的標準都未認可這種寫法。因此,編譯器不必接受這種形式,而且許多編譯器都不能這樣寫。需要強調的是,只要堅持使用標準形式,把程式從一個編譯器移至另一個編譯器時就不會出什麼問題。

3.註釋

/*一個簡單的程式*/

在程式中,被 /* */ 兩個符號括起來的部分是程式的註釋。寫註釋能讓他人(包括自己)更容易明白你所寫的程式。C 語言註釋的好處之一是,可將註釋放在任意的地方,甚至是與要解釋的內容在同一行。較長的註釋可單獨放一行或多行。在 /**/ 之間的內容都會被編譯器忽略。下面列出了一些有效和無效的註釋形式:

/* 這是一條C註釋。 */
/* 這也是一條註釋,
   被分成兩行。*/
/*
   也可以這樣寫註釋。
*/

/* 這條註釋無效,因為缺少了結束標記。

C99 新增了另一種風格的註釋,普遍用於 C++ 和 Java。這種新風格使用 // 符號建立註釋,僅限於單行。

// 這種註釋只能寫成一行。
int rigue; // 這種註釋也可置於此。

因為一行末尾就標誌著註釋的結束,所以這種風格的註釋只需在註釋開始處標明 // 符號即可。

這種新形式的註釋是為了解決舊形式註釋存在的潛在問題。假設有下面的程式碼:

/*
    希望能執行。
*/
x = 100;
y = 200;
/* 其他內容已省略。 */

接下來,假設你決定刪除第 4 行,但不小心刪掉了第 3 行(*/)。程式碼如下所示:

/*
    希望能執行。
y = 200;
/*其他內容已省略。 */

現在,編譯器把第 1 行的 /* 和第 4 行的 */ 配對,導致 4 行程式碼全都成了註釋(包括應作為程式碼的那一行)。而 // 形式的註釋只對單行有效,不會導致這種“消失程式碼”的問題。

一些編譯器可能不支援這一特性。還有一些編譯器需要更改設定,才能支援 C99 或 C11 的特性。

考慮到只用一種註釋風格過於死板乏味,本文在示例中採用兩種風格的註釋。

4.花括號、函式體和塊

{
    ...
}

程式清單 1 中,花括號把 main() 函式括起來。一般而言,所有的 C 函式都使用花括號標記函式體的開始和結束。這是規定,不能省略。只有花括號({})能起這種作用,圓括號(())和方括號([])都不行。

花括號還可用於把函式中的多條語句合併為一個單元或塊。如果讀者熟悉 Pascal、ADA、Modula-2 或者 Algol,就會明白花括號在 C 語言中的作用類似於這些語言中的 begin 和 end。

5.宣告

int num;

程式清單 1 中,這行程式碼叫作宣告(declaration)。宣告是 C 語言最重要的特性之一。在該例中,宣告完成了兩件事。其一,在函式中有一個名為 num 的變數(variable)。其二,int 表明 num 是一個整數(即,沒有小數點或小數部分的數)。int 是一種資料型別。編譯器使用這些資訊為 num 變數在記憶體中分配儲存空間。分號在 C 語言中是大部分語句和宣告的一部分,不像在 Pascal 中只是語句間的分隔符。

int 是 C 語言的一個關鍵字(keyword),表示一種基本的 C 語言資料型別。關鍵字是語言定義的單詞,不能做其他用途。例如,不能用 int 作為函式名和變數名。但是,這些關鍵字在該語言以外不起作用,所以把一隻貓或一個可愛的小孩叫 int 是可以的(儘管某些地方的當地習俗或法律可能不允許)。

示例中的 num 是一個識別符號(identifier),也就是一個變數、函式或其他實體的名稱。因此,宣告把特定識別符號與計算機記憶體中的特定位置聯絡起來,同時也確定了儲存在某位置的資訊型別或資料型別。

在 C 語言中,所有變數都必須先宣告才能使用。這意味著必須列出程式中用到的所有變數名及其型別。

以前的 C 語言,還要求把變數宣告在塊的頂部,其他語句不能在任何宣告的前面。也就是說,main() 函式體如下所示:

int main() //舊規則
{
     int doors;
     int dogs;
     doors = 5;
     dogs = 3;
     // 其他語句
}

C99 和 C11 遵循 C++ 的慣例,可以把宣告放在塊中的任何位置。儘管如此,首次使用變數之前一定要先宣告它。因此,如果編譯器支援這一新特性,可以這樣編寫上面的程式碼:

int main()            // 目前的C規則
{
     // 一些語句
     int doors;
     doors = 5; // 第1次使用doors
     // 其他語句
     int dogs;
     dogs = 3; // 第1次使用dogs
     // 其他語句
}

為了與舊系統更好地相容,本文沿用最初的規則(即,把變數宣告都寫在塊的頂部)。

現在,讀者可能有 3 個問題:什麼是資料型別?如何命名?為何要宣告變數?請往下看。

資料型別

C 語言可以處理多種型別的資料,如整數、字元和浮點數。把變數宣告為整型或字元型別,計算機才能正確地儲存、讀取和解釋資料。

命名

給變數命名時要使用有意義的變數名或識別符號(如,程式中需要一個變數數羊,該變數名應該是 sheep_count 而不是 x3)。如果變數名無法清楚地表達自身的用途,可在註釋中進一步說明。這是一種良好的程式設計習慣和程式設計技巧。

C99 和 C11 允許使用更長的識別符號名,但是編譯器只識別前 63 個字元。對於外部識別符號,只允許使用 31 個字元。〔以前 C90 只允許 6 個字元,這是一個很大的進步。舊式編譯器通常最多隻允許使用 8 個字元。〕實際上,你可以使用更長的字元,但是編譯器會忽略超出的字元。也就是說,如果有兩個識別符號名都有 63 個字元,只有一個字元不同,那麼編譯器會識別這是兩個不同的名稱。如果兩個識別符號都是 64 個字元,只有最後一個字元不同,那麼編譯器可能將其視為同一個名稱,也可能不會。標準並未定義在這種情況下會發生什麼。

可以用小寫字母、大寫字母、數字和下劃線(_)來命名。而且,名稱的第 1 個字元必須是字母或下劃線,不能是數字。表 1 給出了一些示例。

表 1 有效和無效的名稱

有效的名稱 無效的名稱
wiggles $Z]**
cat2 2cat
Hot_Tub Hot-Tub
taxRate tax rate
_kcab don’t

作業系統和C庫經常使用以一個或兩個下劃線字元開始的識別符號(如,_kcab),因此最好避免在自己的程式中使用這種名稱。標準標籤都以一個或兩個下劃線字元開始,如庫識別符號。這樣的識別符號都是保留的。這意味著,雖然使用它們沒有語法錯誤,但是會導致名稱衝突。

C 語言的名稱區分大小寫,即把一個字母的大寫和小寫視為兩個不同的字元。因此,stars 和 Stars、STARS 都不同。

為了讓 C 語言更加國際化,C99 和 C11 根據通用字元名(即 UCN)機制添加了擴充套件字符集。其中包含了除英文字母以外的部分字元。

宣告變數的 4 個理由

一些更老的語言(如,FORTRAN 和 BASIC 的最初形式)都允許直接使用變數,不必先宣告。為何 C 語言不採用這種簡單易行的方法?原因如下。

  • 把所有的變數放在一處,方便讀者查詢和理解程式的用途。如果變數名都是有意義的(如,taxrate 而不是 r),這樣做效果很好。如果變數名無法表述清楚,在註釋中解釋變數的含義。這種方法讓程式的可讀性更高。
  • 宣告變數會促使你在編寫程式之前做一些計劃。程式在開始時要獲得哪些資訊?希望程式如何輸出?表示資料最好的方式是什麼?
  • 宣告變數有助於發現隱藏在程式中的小錯誤,如變數名拼寫錯誤。例如,假設在某些不需要宣告就可以直接使用變數的語言中,編寫如下語句:
RADIUS1 = 20.4;

在後面的程式中,誤寫成:

CIRCUM = 6.28 * RADIUSl;

你不小心把數字 1 打成小寫字母 l。這些語言會建立一個新的變數 RADIUSl,並使用該變數中的值(也許是 0,也許是垃圾值),導致賦給 CIRCUM 的值是錯誤值。你可能要花很久時間才能查出原因。這樣的錯誤在 C 語言中不會發生(除非你很不明智地聲明瞭兩個極其相似的變數),因為編譯器在發現未宣告的 RADIUSl 時會報錯。

  • 如果事先未宣告變數,C 程式將無法通過編譯。如果前幾個理由還不足以說服你,這個理由總可以讓你認真考慮一下了。

如果要宣告變數,應該宣告在何處?前面提到過,C99 之前的標準要求把宣告都置於塊的頂部,這樣規定的好處是:把宣告放在一起更容易理解程式的用途。C99 允許在需要時才宣告變數,這樣做的好處是:在給變數賦值之前宣告變數,就不會忘記給變數賦值。但是實際上,許多編譯器都還不支援 C99。

6.賦值

num = 1;

程式清單中的這行程式碼是賦值表示式語句。賦值是 C 語言的基本操作之一。該行程式碼的意思是“把值1賦給變數 num”。在執行 int num; 宣告時,編譯器在計算機記憶體中為變數 num 預留了空間,然後在執行這行賦值表示式語句時,把值儲存在之前預留的位置。可以給 num 賦不同的值,這就是 num 之所以被稱為變數(variable)的原因。注意,該賦值表示式語句從右側把值賦到左側。另外,該語句以分號結尾,如圖 2 所示。

圖 2 賦值是 C 語言中的基本操作之一

7.printf() 函式

printf("I am a simple ");
printf("computer.\n");
printf("My favorite number is %d because it is first.\n", num);

這 3 行都使用了 C 語言的一個標準函式:printf()。圓括號表明 printf 是一個函式名。圓括號中的內容是從 main() 函式傳遞給 printf() 函式的資訊。例如,上面的第 1 行把 I am a simple 傳遞給 printf() 函式。該資訊被稱為引數,或者更確切地說,是函式的實際引數(actual argument),如圖 3 所示。〔在 C 語言中,實際引數(簡稱實參)是傳遞給函式的特定值,形式引數(簡稱形參)是函式中用於儲存值的變數。〕printf() 函式用引數來做什麼?該函式會檢視雙引號中的內容,並將其列印在螢幕上。

圖 3 帶實參的 printf() 函式

第 1 行 printf() 演示了在 C 語言中如何呼叫函式。只需輸入函式名,把所需的引數填入圓括號即可。當程式執行到這一行時,控制權被轉給已命名的函式(該例中是 printf())。函式執行結束後,控制權被返回至主調函式(calling function),該例中是 main()。

第 2 行 printf() 函式的雙引號中的 \n 字元並未輸出。這是為什麼?\n 的意思是換行。\n 組合(依次輸入這兩個字元)代表一個換行符(newline character)。對於 printf() 而言,它的意思是“在下一行的最左邊開始新的一行”。也就是說,列印換行符的效果與在鍵盤按下 Enter 鍵相同。既然如此,為何不在鍵入 printf() 引數時直接使用 Enter 鍵?因為編輯器可能認為這是直接的命令,而不是儲存在原始碼中的指令。換句話說,如果直接按下 Enter 鍵,編輯器會退出當前行並開始新的一行。但是,換行符僅會影響程式輸出的顯示格式。

換行符是一個轉義序列(escape sequence)。轉義序列用於代表難以表示或無法輸入的字元。如,\t 代表 Tab 鍵,\b 代表 Backspace 鍵(退格鍵)。每個轉義序列都以反斜槓字元(\)開始。

這樣,就解釋了為什麼 3 行 printf() 語句只打印出兩行:第 1 個 printf() 列印的內容中不含換行符,但是第 2 和第 3 個 printf() 中都有換行符。

第 3 個 printf() 還有一些不明之處:引數中的 %d 在列印時有什麼作用?先來看該函式的輸出:

My favorite number is 1 because it is first.

對比發現,引數中的 %d 被數字 1 代替了,而 1 就是變數 num 的值。%d 相當於是一個佔位符,其作用是指明輸出 num 值的位置。該行和下面的 BASIC 語句很像:

PRINT "My favorite number is "; num; " because it is first."

實際上,C 語言的 printf() 比 BASIC 的這條語句做的事情多一些。% 提醒程式,要在該處列印一個變數,d 表明把變數作為十進位制整數列印。printf() 函式名中的 f 提醒使用者,這是一種格式化列印函式。printf() 函式有多種列印變數的格式,包括小數和十六進位制整數。

8.return 語句

return 0;

return 語句是程式清單 1 的最後一條語句。int main(void) 中的 int 表明 main() 函式應返回一個整數。C 標準要求 main() 這樣做。有返回值的 C 函式要有 return 語句。該語句以 return 關鍵字開始,後面是待返回的值,並以分號結尾。如果遺漏 main() 函式中的 return 語句,程式在執行至最外面的右花括號(})時會返回 0。因此,可以省略 main() 函式末尾的 return 語句。但是,不要在其他有返回值的函式中漏掉它。因此,強烈建議讀者養成在 main( )函式中保留 return 語句的好習慣。在這種情況下,可將其看作是統一程式碼風格。但對於某些作業系統(包括 Linux 和 UNIX),return 語句有實際的用途。

三、簡單程式的結構

在看過一個具體的程式示例後,我們來了解一下 C 程式的基本結構。程式由一個或多個函式組成,必須有 main() 函式。函式由函式頭和函式體組成。函式頭包括函式名、傳入該函式的資訊型別和函式的返回型別。通過函式名後的圓括號可識別出函式,圓括號裡可能為空,可能有引數。函式體被花括號括起來,由一系列語句、宣告組成,如圖 4 所示。本文的程式示例中有一條宣告,聲明瞭程式使用的變數名和型別。然後是一條賦值表示式語句,變數被賦予一個值。接下來是 1 條 printf() 語句,呼叫 printf() 函式1次。最後,main() 以 return 語句結束。

圖 4 函式包含函式頭和函式體

簡而言之,一個簡單的 C 程式的格式如下:

#include <stdio.h>
int main(void)
{
     語句
     return 0;
}

(大部分語句都以分號結尾。)

四、提高程式可讀性的技巧

編寫可讀性高的程式是良好的程式設計習慣。可讀性高的程式更容易理解,以後也更容易修改和更正。提高程式的可讀性還有助於你理清程式設計思路。

前面介紹過兩種提高程式可讀性的技巧:選擇有意義的函式名和寫註釋。注意,使用這兩種技巧時應相得益彰,避免重複囉嗦。如果變數名是 width,就不必寫註釋說明該變量表示寬度,但是如果變數名是 video_routine_4,就要解釋一下該變數名的含義。

提高程式可讀性的第 3 個技巧是:在函式中用空行分隔概念上的多個部分。例如,程式清單 1 中用空行把宣告部分和程式的其他部分割槽分開來。C 語言並未規定一定要使用空行,但是多使用空行能提高程式的可讀性。

提高程式可讀性的第 4 個技巧是:每條語句各佔一行。同樣,這也不是 C 語言的要求。C 語言的格式比較自由,可以把多條語句放在一行,也可以每條語句獨佔一行。下面的語句都沒問題,但是不好看:

int main( void ) { int four; four
=
4
;
printf(
        "%d\n",
four); return 0;}

分號告訴編譯器一條語句在哪裡結束、下一條語句在哪裡開始。如果按照本文示例的約定來編寫程式碼(見圖 5),程式的邏輯會更清晰。

圖 5 提高程式的可讀性

五、進一步使用 C

本文的第 1 個程式相當簡單,下面的程式清單 2 也不太難。

程式清單 2 fathm_ft.c 程式

// fathm_ft.c -- 把2英尋轉換成英尺
#include <stdio.h>
int main(void)
{
     int feet, fathoms;

     fathoms = 2;
     feet = 6 * fathoms;
     printf("There are %d feet in %d fathoms!\n", feet, fathoms);
     printf("Yes, I said %d feet!\n", 6 * fathoms);

     return 0;
}

與程式清單 1 相比,以上程式碼有什麼新內容?這段程式碼提供了程式描述,聲明瞭多個變數,進行了乘法運算,並列印了兩個變數的值。下面我們更詳細地分析這些內容。

5.1 程式說明

程式在開始處有一條註釋(使用新的註釋風格),給出了檔名和程式的目的。寫這種程式說明很簡單、不費時,而且在以後瀏覽或列印程式時很有幫助。

5.2 多條宣告

接下來,程式在一條宣告中聲明瞭兩個變數,而不是一個變數。為此,要在宣告中用逗號隔開兩個變數(feet 和 fathoms)。也就是說,

int feet, fathoms;

int feet;
int fathoms;

等價。

5.3 乘法

然後,程式進行了乘法運算。利用計算機強大的計算能力來計算 6 乘以 2。C 語言和許多其他語言一樣,用 * 表示乘法。因此,語句

feet = 6 * fathoms;

的意思是“查詢變數 fathoms 的值,用 6 乘以該值,並把計算結果賦給變數 feet”。

5.4 列印多個值

最後,程式以新的方式使用 printf() 函式。如果編譯並執行該程式,輸出應該是這樣:

There are 12 feet in 2 fathoms!
Yes, I said 12 feet!

程式的第 1 個 printf() 中進行了兩次替換。雙引號後面的第 1 個變數(feet)替換了雙引號中的第 1 個 %d;雙引號後面的第 2 個變數(fathoms)替換了雙引號中的第 2 個 %d。注意,待輸出的變數列於雙引號的後面。還要注意,變數之間要用逗號隔開。

第 2 個 printf() 函式說明待列印的值不一定是變數,只要可求值得出合適型別值的項即可,如 6 * fathoms

該程式涉及的範圍有限,但它是把英尋轉換成英尺程式的核心部分。我們還需要把其他值通過互動的方式賦給 feet,其方法將在後面文章中介紹。

六、多個函式

到目前為止,介紹的幾個程式都只使用了 printf() 函式。程式清單 3 演示了除 main() 以外,如何把自己的函式加入程式中。

程式清單 3 two_func.c 程式

/* two_func.c -- 一個檔案中包含兩個函式 */
#include <stdio.h>
void butler(void); /* ANSI/ISO C函式原型 */
int main(void)
{
     printf("I will summon the butler function.\n");
     butler();
     printf("Yes. Bring me some tea and writeable DVDs.\n");

     return 0;
}
void butler(void) /* 函式定義開始 */
{
     printf("You rang, sir?\n");
}

該程式的輸出如下:

I will summon the butler function.
You rang, sir?
Yes. Bring me some tea and writeable DVDs.

butler() 函式在程式中出現了 3 次。第 1 次是函式原型(prototype),告知編譯器在程式中要使用該函式;第 2 次以函式呼叫(function call)的形式出現在 main() 中;最後一次出現在函式定義(function definition)中,函式定義即是函式本身的原始碼。下面逐一分析。

C90 標準新增了函式原型,舊式的編譯器可能無法識別(稍後我們將介紹,如果使用這種編譯器應該怎麼做)。函式原型是一種宣告形式,告知編譯器正在使用某函式,因此函式原型也被稱為函式宣告(function declaration)。函式原型還指明瞭函式的屬性。例如,butler() 函式原型中的第 1 個 void 表明,butler() 函式沒有返回值(通常,被調函式會向主調函式返回一個值,但是 butler() 函式沒有)。第 2 個 void(butler(void) 中的 void)的意思是 butler() 函式不帶引數。因此,當編譯器執行至此,會檢查 butler() 是否使用得當。注意,void 在這裡的意思是“空的”,而不是“無效”。

早期的 C 語言支援一種更簡單的函式宣告,只需指定返回型別,不用描述引數:

void butler();

早期的 C 程式碼中的函式宣告就類似上面這樣,不是現在的函式原型。C90、C99 和 C11 標準都承認舊版本的形式,但是也表明了會逐漸淘汰這種過時的寫法。如果要使用以前寫的 C 程式碼,就需要把舊式宣告轉換成函式原型。

接下來我們繼續分析程式。在 main() 中呼叫 butler() 很簡單,寫出函式名和圓括號即可。當 butler() 執行完畢後,程式會繼續執行 main() 中的下一條語句。

程式的最後部分是 butler() 函式的定義,其形式和 main() 相同,都包含函式頭和用花括號括起來的函式體。函式頭重述了函式原型的資訊:butler() 不帶任何引數,且沒有返回值。如果使用老式編譯器,請去掉圓括號中的 void。

這裡要注意,何時執行 butler() 函式取決於它在 main() 中被呼叫的位置,而不是 butler() 的定義在檔案中的位置。例如,把 butler() 函式的定義放在 main() 定義之前,不會改變程式的執行順序,butler() 函式仍然在兩次 printf() 呼叫之間被呼叫。記住,無論 main() 在程式檔案中處於什麼位置,所有的 C 程式都從 main() 開始執行。但是,C 的慣例是把 main() 放在開頭,因為它提供了程式的基本框架。

C 標準建議,要為程式中用到的所有函式提供函式原型。標準 include 檔案(包含檔案)為標準庫函式提供了函式原型。例如,在 C 標準中,stdio.h 檔案包含了 printf() 的函式原型。

七、除錯程式

現在,你可以編寫一個簡單的 C 程式,但是可能會犯一些簡單的錯誤。程式的錯誤通常叫作 bug,找出並修正錯誤的過程叫作除錯(debug)。程式清單 4 是一個有錯誤的程式,看看你能找出幾處。

程式清單 4 nogood.c 程式

/* nogood.c -- 有錯誤的程式 */
#include <stdio.h>
int main(void)
(
     int n, int n2, int n3;

     /* 該程式有多處錯誤
     n = 5;
     n2 = n * n;
     n3 = n2 * n2;
     printf("n = %d, n squared = %d, n cubed = %d\n", n, n2, n3)

     return 0;
)

7.1 語法錯誤

程式清單 4 中有多處語法錯誤。如果不遵循 C 語言的規則就會犯語法錯誤。這類似於英文中的語法錯誤。例如,看看這個句子:Bugs frustrate be can。該句子中的英文單詞都是有效的單詞(即,拼寫正確),但是並未按照正確的順序組織句子,而且用詞也不妥。C 語言的語法錯誤指的是,把有效的 C 符號放在錯誤的地方。

nogood.c 程式中有哪些錯誤?其一,main() 函式體使用圓括號來代替花括號。這就是把 C 符號用錯了地方。其二,變數宣告應該這樣寫:

int n, n2, n3;

或者,這樣寫:

int n;
int n2;
int n3;

其三,main() 中的註釋末尾漏掉了 */(另一種修改方案是,用 // 替換 /*)。最後,printf() 語句末尾漏掉了分號。

如何發現程式的語法錯誤?首先,在編譯之前,瀏覽原始碼看是否能發現一些明顯的錯誤。接下來,檢視編譯器是否發現錯誤,檢查程式的語法錯誤是它的工作之一。在編譯程式時,編譯器發現錯誤會報告錯誤資訊,指出每一處錯誤的性質和具體位置。

儘管如此,編譯器也有出錯的時候。也許某處隱藏的語法錯誤會導致編譯器誤判。例如,由於 nogood.c 程式未正確宣告 n2 和 n3,會導致編譯器在使用這些變數時發現更多問題。實際上,有時不用把編譯器報告的所有錯誤逐一修正,僅修正第1條或前幾處錯誤後,錯誤資訊就會少很多。繼續這樣做,直到編譯器不再報錯。編譯器另一個常見的毛病是,報錯的位置比真正的錯誤位置滯後一行。例如,編譯器在編譯下一行時才會發現上一行缺少分號。因此,如果編譯器報錯某行缺少分號,請檢查上一行。

7.2 語義錯誤

語義錯誤是指意思上的錯誤。例如,考慮這個句子:Scornful derivatives sing greenly(輕蔑的衍生物不熟練地唱歌)。句中的形容詞、名詞、動詞和副詞都在正確的位置上,所以語法正確。但是,卻讓人不知所云。在 C 語言中,如果遵循 了C 規則,但是結果不正確,那就是犯了語義錯誤。程式示例中有這樣的錯誤:

n3 = n2 * n2;

此處,n3 原意表示 n 的 3 次方,但是程式碼中的 n3 被設定成 n 的 4 次方(n2 = n * n)。

編譯器無法檢測語義錯誤,因為這類錯誤並未違反 C 語言的規則。編譯器無法瞭解你的真正意圖,所以你只能自己找出這些錯誤。例如,假設你修正了程式的語法錯誤,程式應該如程式清單 5 所示:

程式清單 5 stillbad.c 程式

/* stillbad.c -- 修復了語法錯誤的程式 */
#include <stdio.h>
int main(void)
{
     int n, n2, n3;

     /* 該程式有一個語義錯誤 */
     n = 5;
     n2 = n * n;
     n3 = n2 * n2;
     printf("n = %d, n squared = %d, n cubed = %d\n", n, n2, n3);

     return 0;
}

該程式的輸出如下:

n = 5, n squared = 25, n cubed = 625

如果對簡單的立方比較熟悉,就會注意到 625 不對。下一步是跟蹤程式的執行步驟,找出程式如何得出這個答案。對於本例,通過檢視程式碼就會發現其中的錯誤,但是,還應該學習更系統的方法。方法之一是,把自己想象成計算機,跟著程式的步驟一步一步地執行。下面,我們來試試這種方法。

main() 函式體一開始就聲明瞭 3 個變數:n、n2、n3。你可以畫出 3 個盒子並把變數名寫在盒子上來模擬這種情況(見圖 6)。接下來,程式把 5 賦給變數 n。你可以在標籤為 n 的盒子裡寫上 5。接著,程式把 n 和 n 相乘,並把乘積賦給 n2。因此,檢視標籤為 n 的盒子,其值是 5,5 乘以 5 得 25,於是把 25 放進標籤為 n2 的盒子裡。為了模擬下一條語句(n3 = n2 * n2),檢視 n2 盒子,發現其值是 25。25 乘以 25 得 625,把 625 放進標籤為 n3 的盒子。原來如此!程式中計算的是 n2 的平方,不是用 n2 乘以 n 得到 n 的 3 次方。

圖 6 跟蹤程式的執行步驟

對於上面的程式示例,檢查程式的過程可能過於繁瑣。但是,用這種方法一步一步檢視程式的執行情況,通常是發現程式問題所在的良方。

7.3 程式狀態

通過逐步跟蹤程式的執行步驟,並記錄每個變數,便可監視程式的狀態。程式狀態(program state)是在程式的執行過程中,某給定點上所有變數值的集合。它是計算機當前狀態的一個快照。

我們剛剛討論了一種跟蹤程式狀態的方法:自己模擬計算機逐步執行程式。但是,如果程式中有 10000 次迴圈,這種方法恐怕行不通。不過,你可以跟蹤一小部分迴圈,看看程式是否按照預期的方式執行。另外,還要考慮一種情況:你很可能按照自己所想去執行程式,而不是根據實際寫出來的程式碼去執行。因此,要儘量忠實於程式碼來模擬。

定位語義錯誤的另一種方法是:在程式中的關鍵點插入額外的 printf() 語句,以監視指定變數值的變化。通過檢視值的變化可以瞭解程式的執行情況。對程式的執行滿意後,便可刪除額外的 printf() 語句,然後重新編譯。

檢測程式狀態的第3種方法是使用偵錯程式。偵錯程式(debugger)是一種程式,讓你一步一步執行另一個程式,並檢查該程式變數的值。偵錯程式有不同的使用難度和複雜度。較高階的偵錯程式會顯示正在執行的原始碼行號。這在檢查有多條執行路徑的程式時很方便,因為很容易知道正在執行哪條路徑。如果你的編譯器自帶偵錯程式,現在可以花點時間學會怎麼使用它。例如,試著除錯一下程式清單 4。

八、關鍵字和保留識別符號

關鍵字是 C 語言的詞彙。它們對 C 而言比較特殊,不能用它們作為識別符號(如,變數名)。許多關鍵字用於指定不同的型別,如 int。還有一些關鍵字(如,if)用於控制程式中語句的執行順序。在表 2 中所列的 C 語言關鍵字中,粗體表示的是 C90 標準新增的關鍵字,斜體表示的 C99 標準新增的關鍵字,粗斜體表示的是 C11 標準新增的關鍵字。

表 2 ISO C 關鍵字

auto extern short while
break float signed _Alignas
case for sizeof _Alignof
char goto static _Atomic
const if struct _Bool
continue inline switch _Complex
default int typedef _Generic
do long union _Imaginary
double register unsigned _Noreturn
else restrict void _Static_assert
enum return volatile _Thread_local

如果使用關鍵字不當(如,用關鍵字作為變數名),編譯器會將其視為語法錯誤。還有一些保留識別符號(reserved identifier),C 語言已經指定了它們的用途或保留它們的使用權,如果你使用這些識別符號來表示其他意思會導致一些問題。因此,儘管它們也是有效的名稱,不會引起語法錯誤,也不能隨便使用。保留識別符號包括那些以下劃線字元開頭的識別符號和標準庫函式名,如 printf()。

九、關鍵概念

程式設計是一件富有挑戰性的事情。程式設計師要具備抽象和邏輯的思維,並謹慎地處理細節問題(編譯器會強迫你注意細節問題)。平時和朋友交流時,可能用錯幾個單詞,犯一兩個語法錯誤,或者說幾句不完整的句子,但是對方能明白你想說什麼。而編譯器不允許這樣,對它而言,幾乎正確仍然是錯誤。

編譯器不會在下面講到的概念性問題上幫助你。因此,本文介紹一些關鍵概念幫助讀者彌補這部分的內容。

在本文中,讀者的目標應該是理解什麼是 C 程式。可以把程式看作是你希望計算機如何完成任務的描述。編譯器負責處理一些細節工作,例如把你要計算機完成的任務轉換成底層的機器語言(如果從量化方面來解釋編譯器所做的工作,它可以把 1KB 的原始檔建立成 60KB 的可執行檔案;即使是一個很簡單的 C 程式也要用大量的機器語言來表示)。由於編譯器不具有真正的智慧,所以你必須用編譯器能理解的術語表達你的意圖,這些術語就是 C 語言標準規定的形式規則(儘管有些約束,但總比直接用機器語言方便得多)。

編譯器希望接收到特定格式的指令,我們在本文已經介紹過。作為程式設計師的任務是,在符合 C 標準的編譯器框架中,表達你希望程式應該如何完成任務的想法。

十、小結

C 程式由一個或多個 C 函式組成。每個 C 程式必須包含一個 main() 函式,這是 C 程式要呼叫的第 1 個函式。簡單的函式由函式頭和後面的一對花括號組成,花括號中是由宣告、語句組成的函式體。

在 C 語言中,大部分語句都以分號結尾。宣告語句為變數指定變數名,並標識該變數中儲存的資料型別。變數名是一種識別符號。賦值表示式語句把值賦給變數,或者更一般地說,把值賦給儲存空間。函式表示式語句用於呼叫指定的已命名函式。呼叫函式執行完畢後,程式會返回到函式呼叫後面的語句繼續執行。

printf() 函式用於輸出想要表達的內容和變數的值。

一門語言的語法是一套規則,用於管理語言中各有效語句組合在一起的方式。語句的語義是語句要表達的意思。編譯器可以檢測出語法錯誤,但是程式裡的語義錯誤只有在編譯完之後才能從程式的行為中表現出來。檢查程式是否有語義錯誤要跟蹤程式的狀態,即檢查程式每執行一步後所有變數的值。

最後,關鍵字是 C 語言的詞彙。

原文:C 語言概述

(完)