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 個程序多。

C 程序解剖

圖 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 所示。

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

圖 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() 函數用參數來做什么?該函數會查看雙引號中的內容,並將其打印在屏幕上。

帶實參的 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 語言概述

(完)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM