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),C语言规定每条语句末尾都要有一个;号,printf的下一条语句也是如此。

C语言用{}号(Brace或Curly Brace)把语法结构分成组,在上面的程序中printfreturn语句套在main的{}号中,表示它们属于main的定义之中。我们看到这两句相比main那一行都缩进(Indent)了一些,在代码中可以用若干个空格(Blank)和Tab字符来缩进,缩进不是必须的,但这样使我们更容易看出这两行是属于main的定义之中的,要写出漂亮的程序必须要有整齐的缩进。

正如前面所说,编译器对于语法错误是毫不留情的,如果你的程序有一点拼写错误,例如第一行写成了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、尽管编译器的错误提示不够友好,但仍然是学习过程中的一个很有用的工具。你可以像上面那样,从一个正确的程序开始每次改动一小点,然后编译看是什么结果,如果出错了,就尽量记住编译器给出的错误提示并把改动还原。因为错误是你改出来的,你已经知道错误原因是什么了,就可以很容易地把错误原因和错误提示信息对应起来记住,这样下次你在毫无防备的情况下撞到这个错误提示就会很容易想到错误原因是什么了。这样反复练习,有了一定的经验积累之后面对编译器的错误提示就会从容得多了。