4. 第一個程序

通常一本教編程的書中第一個例子都是打印“Hello, World.”,這個傳統源自[K&R],用C語言寫這個程序可以這樣寫:

例 1.1. Hello World

#include <stdio.h>

/* main: generate some simple output */

int main(void)
{
	printf("Hello, world.\n");
	return 0;
}

將這個程序保存成main.c,然後編譯執行:

$ gcc main.c
$ ./a.out
Hello, world.

gcc是Linux平台的C編譯器,編譯後在當前目錄下生成執行檔a.out,直接在命令行輸入這個執行檔的路徑就可以執行它。如果不想把檔案名叫a.out,可以用gcc-o參數自己指定檔案名:

$ gcc main.c -o main
$ ./main
Hello, world.

雖然這只是一個很小的程序,但我們目前暫時還不具備相關的知識來完全理解這個程序,比如程序的第一行,還有程序主體的int main(void){...return 0;}結構,這些部分我們暫時不詳細解釋,讀者現在只需要把它們看成是每個程序按慣例必須要寫的部分(Boilerplate)。但要注意main是一個特殊的名字,C程序總是從main裡面的第一條語句開始執行的,在這個程序中是指printf這條語句。

第3行的/* ... */結構是一個註釋(Comment),其中可以寫一些描述性的話,解釋這段程序在做什麼。註釋只是寫給程序員看的,編譯器會忽略從/**/的所有字元,所以寫註釋沒有語法規則,愛怎麼寫就怎麼寫,並且不管寫多少都不會被編譯進執行檔中。

printf語句的作用是把消息打印到屏幕。注意語句的末尾以;號(Semicolon)結束,下一條語句return 0;也是如此。

C語言用{}括號(Brace或Curly Brace)把語法結構分成組,在上面的程序中printfreturn語句套在main的{}括號中,表示它們屬於main的定義之中。我們看到這兩句相比main那一行都縮進(Indent)了一些,在代碼中可以用若干個空格(Blank)和Tab字元來縮進,縮進不是必須的,但這樣使我們更容易看出這兩行是屬於main的定義之中的,要寫出漂亮的程序必須有整齊的縮進,第 1 節 “縮進和空白”將介紹推薦的縮進寫法。

正如前面所說,編譯器對於語法錯誤是毫不留情的,如果你的程序有一點拼寫錯誤,例如第一行寫成了stdoi.h,在編譯時會得到錯誤提示:

$ gcc main.c
main.c:1:19: error: stdoi.h: No such file or directory
...

這個錯誤提示非常緊湊,初學者往往不容易看明白出了什麼錯誤,即使知道這個錯誤提示說的是第1行有錯誤,很多初學者對照着書看好幾遍也看不出自己這一行哪裡有錯誤,因為他們對符號和拼寫不敏感(尤其是英文較差的初學者),他們還不知道這些符號是什麼意思又如何能記住正確的拼寫?對於初學者來說,最想看到的錯誤提示其實是這樣的:“main.c程序第1行的第19列,您試圖包含一個叫做stdoi.h的檔案,可惜我沒有找到這個檔案,但我卻找到了一個叫做stdio.h的檔案,我猜這個才是您想要的,對嗎?”可惜沒有任何編譯器會友善到這個程度,大多數時候你所得到的錯誤提示並不能直接指出誰是犯人,而只是一個線索,你需要根據這個線索做一些偵探和推理。

有些時候編譯器的提示信息不是error而是warning,例如把上例中的printf("Hello, world.\n");改成printf(1);然後編譯運行:

$ gcc main.c
main.c: In function ‘main’:
main.c:7: warning: passing argument 1 of ‘printf’ makes pointer from integer without a cast
$ ./a.out 
Segmentation fault

這個警告信息是說類型不匹配,但勉強還能配得上。警告信息不是致命錯誤,編譯仍然可以繼續,如果整個編譯過程只有警告信息而沒有錯誤信息,仍然可以生成執行檔。但是,警告信息也是不容忽視的。出警告信息說明你的程序寫得不夠規範,可能有Bug,雖然能編譯生成執行檔,但程序的運行結果往往是不正確的,例如上面的程序運行時出了一個段錯誤,這屬於運行時錯誤。各種警告信息的嚴重程度不同,像上面這種警告几乎一定表明程序中有Bug,而另外一些警告只表明程序寫得不夠規範,一般還是能正確運行的,有些不重要的警告信息gcc預設是不提示的,但這些警告信息也有可能表明程序中有Bug。一個好的習慣是打開gcc-Wall選項,也就是讓gcc提示所有的警告信息,不管是嚴重的還是不嚴重的,然後把這些問題從代碼中全部消滅。比如把上例中的printf("Hello, world.\n");改成printf(0);然後編譯運行:

$ gcc main.c
$ ./a.out

編譯既不報錯也不報警告,一切正常,但是運行程序什麼也不打印。如果打開-Wall選項編譯就會報警告了:

$ gcc -Wall main.c
main.c: In function ‘main’:
main.c:7: warning: null argument where non-null required (argument 1)

如果printf中的0是你不小心寫上去的(例如錯誤地使用了編輯器的查找替換功能),這個警告就能幫助你發現錯誤。雖然本書的命令行為了突出重點通常省略-Wall選項,但是強烈建議你寫每一個編譯命令時都加上-Wall選項。

習題

1、儘管編譯器的錯誤提示不夠友好,但仍然是學習過程中一個很有用的工具。你可以像上面那樣,從一個正確的程序開始每次改動一小點,然後編譯看是什麼結果,如果出錯了,就儘量記住編譯器給出的錯誤提示並把改動還原。因為錯誤是你改出來的,你已經知道錯誤原因是什麼了,所以能很容易地把錯誤原因和錯誤提示信息對應起來記住,這樣下次你在毫無防備的情況下撞到這個錯誤提示時就會很容易想到錯誤原因是什麼了。這樣反覆練習,有了一定的經驗積累之後面對編譯器的錯誤提示就會從容得多了。