讀古今文學網 > CPrimerPlus(第6版)(中文版) > 第2章 C語言概述 >

第2章 C語言概述

本章介紹以下內容:

運算符:=

函數:main、printf

編寫一個簡單的C程序

創建整型變量,為其賦值並在屏幕上顯示其值

換行字符

如何在程序中寫註釋,創建包含多個函數的程序,發現程序的錯誤

什麼是關鍵字

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

2.1 簡單的C程序示例

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

程序清單2.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;

}

如果你認為該程序會在屏幕上打印一些內容,那就對了!光看程序也許並不知道打印的具體內容,所以,運行該程序,並查看結果。首先,用你熟悉的編輯器(或者編譯器提供的編輯器)創建一個包含程序清單2.1 中所有內容的文件。給該文件命名,並以.c作為擴展名,以滿足當前系統對文件名的要求。例如,可以使用first.c。現在,編譯並運行該程序(查看第1章,複習該步驟的具體內容)。如果一切運行正常,該程序的輸出應該是:

I am a simple computer.

My favorite number is 1 because it is first.

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

程序調整

程序的輸出是否在屏幕上一閃而過?某些窗口環境會在單獨的窗口運行程序,然後在程序運行結束後自動關閉窗口。如果遇到這種情況,可以在程序中添加額外的代碼,讓窗口等待用戶按下一個鍵後才關閉。一種方法是,在程序的return語句前添加一行代碼:

getchar;

這行代碼會讓程序等待擊鍵,窗口會在用戶按下一個鍵後才關閉。在第 8 章中會詳細介紹 getchar的內容。

2.2 示例解釋

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

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

圖2.1 C程序解剖

2.2.1 第1遍:快速概要

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

#include<stdio.h> ←包含另一個文件

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

int main(void) ←函數名

C程序包含一個或多個函數,它們是C程序的基本模塊。程序清單2.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 第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預處理器在編譯器接手之前處理這條指令。本書後面章節中會介紹更多預處理器指令的示例,第16章將更詳細地討論相關內容。

2.main函數

int main(void);

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

int是main函數的返回類型。這表明main函數返回的值是整數。返回到哪裡?返回給操作系統。我們將在第6章中再來探討這個問題。

通常,函數名後面的圓括號中包含一些傳入函數的信息。該例中沒有傳遞任何信息。因此,圓括號內是單詞void(第11章將介紹把信息從main函數傳回操作系統的另一種形式)。

如果瀏覽舊式的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.花括號、函數體和塊

{

...

}

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

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

5.聲明

int num;

程序清單2.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 語言可以處理多種類型的數據,如整數、字符和浮點數。把變量聲明為整型或字符類型,計算機才能正確地儲存、讀取和解釋數據。下一章將詳細介紹C語言中的各種數據類型。

命名

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

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

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

表2.1 有效和無效的名稱

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

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

為了讓C語言更加國際化,C99和C11根據通用字符名(即UCN)機制添加了擴展字符集。其中包含了除英文字母以外的部分字符。欲瞭解詳細內容,請參閱附錄B的「參考資料VII:擴展字符支持」。

聲明變量的4個理由

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

把所有的變量放在一處,方便讀者查找和理解程序的用途。如果變量名都是有意義的(如,taxtate而不是 r),這樣做效果很好。如果變量名無法表述清楚,在註釋中解釋變量的含義。這種方法讓程序的可讀性更高。

聲明變量會促使你在編寫程序之前做一些計劃。程序在開始時要獲得哪些信息?希望程序如何輸出?表示數據最好的方式是什麼?

聲明變量有助於發現隱藏在程序中的小錯誤,如變量名拼寫錯誤。例如,假設在某些不需要聲明就可以直接使用變量的語言中,編寫如下語句:

RADIUS1 = 20.4;

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

CIRCUM = 6.28 * RADIUSl;

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

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

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

6.賦值

num = 1;

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

圖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),如圖2.3所示。〔在C語言中,實際參數(簡稱實參)是傳遞給函數的特定值,形式參數(簡稱形參)是函數中用於儲存值的變量。第5章中將詳述相關內容。〕printf函數用參數來做什麼?該函數會查看雙引號中的內容,並將其打印在屏幕上。

圖2.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章中再來探討相關內容。

這樣,就解釋了為什麼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語句[3]是程序清單2.1的最後一條語句。int main(void)中的int表明main函數應返回一個整數。C標準要求main這樣做。有返回值的C函數要有return語句。該語句以return關鍵字開始,後面是待返回的值,並以分號結尾。如果遺漏 main函數中的 return 語句,程序在運行至最外面的右花括號(})時會返回0。因此,可以省略main函數末尾的return語句。但是,不要在其他有返回值的函數中漏掉它。因此,強烈建議讀者養成在 main函數中保留 return 語句的好習慣。在這種情況下,可將其看作是統一代碼風格。但對於某些操作系統(包括Linux和UNIX),return語句有實際的用途。第11章再詳述這個主題。

2.3 簡單程序的結構

在看過一個具體的程序示例後,我們來瞭解一下C程序的基本結構。程序由一個或多個函數組成,必須有 main函數。函數由函數頭和函數體組成。函數頭包括函數名、傳入該函數的信息類型和函數的返回類型。通過函數名後的圓括號可識別出函數,圓括號裡可能為空,可能有參數。函數體被花括號括起來,由一系列語句、聲明組成,如圖2.4所示。本章的程序示例中有一條聲明,聲明了程序使用的變量名和類型。然後是一條賦值表達式語句,變量被賦給一個值。接下來是3條printf語句[4],調用printf函數3次。最後,main以return語句結束。

圖2.4 函數包含函數頭和函數體

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

#include <stdio.h>

int main(void)

{

語句

return 0;

}

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

2.4 提高程序可讀性的技巧

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

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

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

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

int main( void ) { int four; four

=

4

;

printf(

"%d\n",

four); return 0;}

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

圖2.5 提高程序的可讀性

2.5 進一步使用C

本章的第1個程序相當簡單,下面的程序清單2.2也不太難。

程序清單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;

}

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

2.5.1 程序說明

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

2.5.2 多條聲明

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

int feet, fathoms;

int feet;

int fathoms;

等價。

2.5.3 乘法

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

feet = 6 * fathoms;

的意思是「查找變量fathoms的值,用6乘以該值,並把計算結果賦給變量feet」。

2.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。

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

2.6 多個函數

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

程序清單2.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函數沒有返回值(通常,被調函數會向主調函數返回一個值,但是 bulter函數沒有)。第 2 個 void (butler(void)中的 void)的意思是 butler函數不帶參數。因此,當編譯器運行至此,會檢查butler是否使用得當。注意,void在這裡的意思是「空的」,而不是「無效」。

早期的C語言支持一種更簡單的函數聲明,只需指定返回類型,不用描述參數:

void butler;

早期的C代碼中的函數聲明就類似上面這樣,不是現在的函數原型。C90、C99 和C11 標準都承認舊版本的形式,但是也表明了會逐漸淘汰這種過時的寫法。如果要使用以前寫的 C代碼,就需要把舊式聲明轉換成函數原型。本書在後面的章節會繼續介紹函數原型的相關內容。

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

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

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

C標準建議,要為程序中用到的所有函數提供函數原型。標準include文件(包含文件)為標準庫函數提供可函數原型。例如,在C標準中,stdio.h文件包含了printf的函數原型。第6章最後一個示例演示了如何使用帶返回值的函數,第9章將詳細全面地介紹函數。

2.7 調試程序

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

程序清單2.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;

)

2.7.1 語法錯誤

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

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

int n, n2, n3;

或者,這樣寫:

int n;

int n2;

int n3;

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

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

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

2.7.2 語義錯誤

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

n3 = n2 * n2;

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

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

程序清單2.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個盒子並把變量名寫在盒子上來模擬這種情況(見圖2.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次方。

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

圖2.6 跟蹤程序的執行步驟

2.7.3 程序狀態

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

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

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

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

2.8 關鍵字和保留標識符

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

表2.2 ISO C關鍵字

續表

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

2.9 關鍵概念

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

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

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

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

2.10 本章小結

C程序由一個或多個C函數組成。每個C程序必須包含一個main函數,這是C程序要調用的第1個函數。簡單的函數由函數頭和後面的一對花括號組成,花括號中是由聲明、語句組成的函數體。

在C語言中,大部分語句都以分號結尾。聲明為變量創建變量名和標識該變量中儲存的數據類型。變量名是一種標識符。賦值表達式語句把值賦給變量,或者更一般地說,把值賦給存儲空間。函數表達式語句用於調用指定的已命名函數。調用函數執行完畢後,程序會返回到函數調用後面的語句繼續執行。

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

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

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

2.11 複習題

複習題的參考答案在附錄A中。

1.C語言的基本模塊是什麼?

2.什麼是語法錯誤?寫出一個英語例子和C語言例子。

3.什麼是語義錯誤?寫出一個英語例子和C語言例子。

4.Indiana Sloth編寫了下面的程序,並徵求你的意見。請幫助他評定。

include studio.h

int main{void} /* 該程序打印一年有多少周 /*

(

int s

s := 56;

print(There are s weeks in a year.);

return 0;

5.假設下面的4個例子都是完整程序中的一部分,它們都輸出什麼結果?

a. printf("Baa Baa Black Sheep.");

printf("Have you any wool?\n");

b. printf("Begone!\nO creature of lard!\n");

c.printf("What?\nNo/nfish?\n");

d.int num;

num = 2;

printf("%d + %d = %d", num, num, num + num);

6.在main、int、function、char、=中,哪些是C語言的關鍵字?

7.如何以下面的格式輸出變量words和lines的值(這裡,3020和350代表兩個變量的值)?

There were 3020 words and 350 lines.

8.考慮下面的程序:

#include <stdio.h>

int main(void)

{

int a, b;

a = 5;

b = 2; /* 第7行 */

b = a; /* 第8行 */

a = b; /* 第9行 */

printf("%d %d\n", b, a);

return 0;

}

請問,在執行完第7、第8、第9行後,程序的狀態分別是什麼?

9.考慮下面的程序:

#include <stdio.h>

int main(void)

{

int x, y;

x = 10;

y = 5; /* 第7行 */

y = x + y; /*第8行*/

x = x*y;  /*第9行*/

printf("%d %d\n", x, y);

return 0;

}

請問,在執行完第7、第8、第9行後,程序的狀態分別是什麼?

2.12 編程練習

紙上得來終覺淺,絕知此事要躬行。讀者應該試著編寫一兩個簡單的程序,體會一下編寫程序是否和閱讀本章介紹的這樣輕鬆。題目中會給出一些建議,但是應該盡量自己思考這些問題。一些編程答案練習的答案可在出版商網站獲取。

1.編寫一個程序,調用一次 printf函數,把你的姓名打印在一行。再調用一次 printf函數,把你的姓名分別打印在兩行。然後,再調用兩次printf函數,把你的姓名打印在一行。輸出應如下所示(當然要把示例的內容換成你的姓名):

2.編寫一個程序,打印你的姓名和地址。

3.編寫一個程序把你的年齡轉換成天數,並顯示這兩個值。這裡不用考慮閏年的問題。

4.編寫一個程序,生成以下輸出:

For he's a jolly good fellow!

For he's a jolly good fellow!

For he's a jolly good fellow!

Which nobody can deny!

除了 main函數以外,該程序還要調用兩個自定義函數:一個名為 jolly,用於打印前 3 條消息,調用一次打印一條;另一個函數名為deny,打印最後一條消息。

5.編寫一個程序,生成以下輸出:

Brazil, Russia, India, China

India, China,

Brazil, Russia

除了main以外,該程序還要調用兩個自定義函數:一個名為br,調用一次打印一次「Brazil, Russia」;另一個名為ic,調用一次打印一次「India, China」。其他內容在main函數中完成。

6.編寫一個程序,創建一個整型變量toes,並將toes設置為10。程序中還要計算toes的兩倍和toes的平方。該程序應打印3個值,並分別描述以示區分。

7.許多研究表明,微笑益處多多。編寫一個程序,生成以下格式的輸出:

Smile!Smile!Smile!

Smile!Smile!

Smile!

該程序要定義一個函數,該函數被調用一次打印一次「Smile!」,根據程序的需要使用該函數。

8.在C語言中,函數可以調用另一個函數。編寫一個程序,調用一個名為one_three的函數。該函數在一行打印單詞「one」,再調用第2個函數two,然後在另一行打印單詞「three」。two函數在一行顯示單詞「two」。main函數在調用 one_three函數前要打印短語「starting now:」,並在調用完畢後顯示短語「done!」。因此,該程序的輸出應如下所示:

starting now:

one

two

three

done!

[1].原書圖中敘述有誤。根據C11標準,C語言有6種語句,已在圖中更正。——譯者注

[2].C語言是通過賦值運算符而不是賦值語句完成賦值操作。根據C標準,C語言並沒有所謂的「賦值語句」,本書及一些其他書籍中提到的「賦值語句」實際上是表達式語句(C語言的6種基本語句之一)。本書把「賦值語句」均譯為「賦值表達式語句」,以提醒初學者注意。——譯者注

[3].在C語言中,return語句是一種跳轉語句。——譯者注

[4].市面上許多書籍(包括本書)都把這種語句叫作「函數調用語句」,但是歷年的C標準中從來沒有函數調用語句!值得一提的是,函數調用本身是一個表達式,圓括號是運算符,圓括號左邊的函數名是運算對象。在C11標準中,這樣的表達式是一種後綴表達式。在表達式末尾加上分號,就成了表達式語句。請初學者注意,這樣的「函數調用語句」實質是表達式語句。本書的錯誤之處已在翻譯過程中更正。——譯者注

[5].音尋,也稱為尋。航海用的深度單位,1英尋=6英尺=1.8米,通常用在海圖上測量水深。——譯者注

[6].要理解該句子存在語法錯誤,需要具備基本的英文語法知識。——譯者注