目錄
- 一、簡單的 C 程式示例
- 二、示例解釋
- 2.1 第 1 遍:快速概要
- 2.2 第 2 遍 :程式細節
- 三、簡單程式的結構
- 五、進一步使用 C
- 5.1 程式說明
- 5.2 多條宣告
- 5.3 乘法
- 5.4 列印多個值
- 六、多個函式
- 七、除錯程式
- 7.1 語法錯誤
- 7.2 語意錯誤
- 7.3 程式狀態
- 八、關鍵字和保留識別符號
- 九、關鍵概念
- 十、小結
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 語言概述
(完)
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/287986.html
標籤:其他
上一篇:初識 C 語言
下一篇:Nginx一些基本配置
