main函数的特殊之处在于执行程序时它自动被操作系统调用,操作系统就认准了main这个名字,除了名字特殊之外,main函数和别的函数没有区别。我们对照着main函数的定义来看语法规则:

    函数定义 → 返回值类型 函数名(参数列表) 函数体
    函数体 → { 语句列表 }
    语句列表 → 语句列表项 语句列表项 …
    语句列表项 → 语句
    语句列表项 → 变量声明、类型声明或非定义的函数声明
    非定义的函数声明 → 返回值类型 函数名(参数列表);

    我们稍后再详细解释“函数定义”和“非定义的函数声明”的区别。从开始我们才会看到类型声明,所以现在暂不讨论。

    给函数命名也要遵循上一章讲过的标识符命名规则。由于我们定义的main函数不带任何参数,参数列表应写成void。函数体可以由若干条语句和声明组成,C89要求所有声明写在所有语句之前(本书的示例代码都遵循这一规定),而C99的新特性允许语句和声明按任意顺序排列,只要每个标识符都遵循先声明后使用的原则就行。main函数的返回值是int型的,return 0;这个语句表示返回值是0,main函数的返回值是返回给操作系统看的,因为main函数是被操作系统调用的,通常程序执行成功就返回0,在执行过程中出错就返回一个非零值。比如我们将main函数中的return语句改为return 4;再执行它,执行结束后可以在Shell中看到它的退出状态(Exit Status):

    1. $ ./a.out
    2. 11 and 0 hours
    3. $ echo $?
    4. 4

    $?是Shell中的一个特殊变量,表示上一条命令的退出状态。关于main函数需要注意两点:

    1. [K&R]书上的main函数定义写成main(){...}的形式,不写返回值类型也不写参数列表,这是Old Style C的风格。Old Style C规定不写返回值类型就表示返回int型,不写参数列表就表示参数类型和个数没有明确指出。这种宽松的规定使编译器无法检查程序中可能存在的Bug,增加了调试难度,不幸的是现在的C标准为了兼容旧的代码仍然保留了这种语法,但读者绝不应该继续使用这种语法。

    2. 其实操作系统在调用main函数时是传参数的,main函数最标准的形式应该是int main(int argc, char *argv[]),在详细介绍。C标准也允许int main(void)这种写法,如果不使用系统传进来的两个参数也可以写成这种形式。但除了这两种形式之外,定义main函数的其它写法都是错误的或不可移植的。

    关于返回值和return语句我们将在第 1 节 “return语句”详细讨论,我们先从不带参数也没有返回值的函数开始学习定义和使用函数:

    例 3.2. 最简单的自定义函数

    1. #include <stdio.h>
    2.  
    3. void newline(void)
    4. {
    5. printf("\n");
    6. }
    7.  
    8. int main(void)
    9. {
    10. printf("First Line.\n");
    11. newline();
    12. printf("Second Line.\n");
    13. return 0;
    14. }

    我们定义了一个函数给main函数调用,它的作用是打印一个换行,所以执行结果中间多了一个空行。newline函数不仅不带参数,也没有返回值,返回值类型为void表示没有返回值[],这说明我们调用这个函数完全是为了利用它的Side Effect。如果我们想要多次插入空行就可以多次调用newline函数:

    1. int main(void)
    2. {
    3. printf("First Line.\n");
    4. newline();
    5. newline();
    6. newline();
    7. printf("Second Line.\n");
    8. return 0;
    9. }

    如果我们总需要三个三个地插入空行,我们可以再定义一个threeline函数每次插入三个空行:

    例 3.3. 较简单的自定义函数

    1. #include <stdio.h>
    2.  
    3. {
    4. printf("\n");
    5. }
    6.  
    7. void threeline(void)
    8. {
    9. newline();
    10. newline();
    11. newline();
    12. }
    13.  
    14. int main(void)
    15. {
    16. printf("Three lines:\n");
    17. threeline();
    18. printf("Another three lines.\n");
    19. threeline();
    20. return 0;
    21. }

    通过这个简单的例子可以体会到:

    1. 同一个函数可以被多次调用。

    2. 可以用一个函数调用另一个函数,后者再去调第三个函数。

    3. 通过自定义函数可以给一组复杂的操作起一个简单的名字,例如threeline。对于main函数来说,只需要通过threeline这个简单的名字来调用就行了,不必知道打印三个空行具体怎么做,所有的复杂操作都被隐藏在threeline这个名字后面。

    4. 使用自定义函数可以使代码更简洁,main函数在任何地方想打印三个空行只需调用一个简单的threeline(),而不必每次都写三个printf("\n")

    读代码和读文章不一样,按从上到下从左到右的顺序读代码未必是最好的。比如上面的例子,按源文件的顺序应该是先看newline再看threeline再看main。如果你换一个角度,按代码的执行顺序来读也许会更好:首先执行的是main函数中的语句,在一条printf之后调用了threeline,这时再去看threeline的定义,其中又调用了newline,这时再去看newline的定义,newline里面有一条printf,执行完成后返回threeline,这里还剩下两次newline调用,效果也都一样,执行完之后返回,接下来又是一条printf和一条threeline。如下图所示:

    读代码的过程就是模仿计算机执行程序的过程,我们不仅要记住当前读到了哪一行代码,还要记住现在读的代码是被哪个函数调用的,这段代码返回后应该从上一个函数的什么地方接着往下读。

    现在澄清一下函数声明、函数定义、函数原型(Prototype)这几个概念。比如void threeline(void)这一行,声明了一个函数的名字、参数类型和个数、返回值类型,这称为函数原型。在代码中可以单独写一个函数原型,后面加;号结束,而不写函数体,例如:

    这种写法只能叫函数声明而不能叫函数定义,只有带函数体的声明才叫定义。上一章讲过,只有分配存储空间的变量声明才叫变量定义,其实函数也是一样,编译器只有见到函数定义才会生成指令,而指令在程序运行时当然也要占存储空间。那么没有函数体的函数声明有什么用呢?它为编译器提供了有用的信息,编译器在翻译代码的过程中,只有见到函数原型(不管带不带函数体)之后才知道这个函数的名字、参数类型和返回值,这样碰到函数调用时才知道怎么生成相应的指令,所以函数原型必须出现在函数调用之前,这也是遵循“先声明后使用”的原则。

    在上面的例子中,main调用threelinethreeline再调用newline,要保证每个函数的原型出现在调用之前,就只能按先newlinethreelinemain的顺序定义了。如果使用不带函数体的声明,则可以改变函数的定义顺序:

    1. #include <stdio.h>
    2.  
    3. void newline(void);
    4. void threeline(void);
    5.  
    6. int main(void)
    7. {
    8. ...
    9.  
    10. void newline(void)
    11. {
    12. ...
    13. }
    14.  
    15. void threeline(void)
    16. {
    17. ...
    18. }

    这样仍然遵循了先声明后使用的原则。

    由于有Old Style C语法的存在,并非所有函数声明都包含完整的函数原型,例如void threeline();这个声明并没有明确指出参数类型和个数,所以不算函数原型,这个声明提供给编译器的信息只有函数名和返回值类型。如果在这样的声明之后调用函数,编译器不知道参数的类型和个数,就不会做语法检查,所以很容易引入Bug。读者需要了解这个知识点以便维护别人用Old Style C风格写的代码,但绝不应该按这种风格写新的代码。

    如果在调用函数之前没有声明会怎么样呢?有的读者也许碰到过这种情况,我可以解释一下,但绝不推荐这种写法。比如按上面的顺序定义这三个函数,但是把开头的两行声明去掉:

    1. #include <stdio.h>
    2.  
    3. int main(void)
    4. {
    5. printf("Three lines:\n");
    6. threeline();
    7. printf("Another three lines.\n");
    8. threeline();
    9. return 0;
    10. }
    11.  
    12. void newline(void)
    13. {
    14. printf("\n");
    15. }
    16.  
    17. void threeline(void)
    18. {
    19. newline();
    20. newline();
    21. newline();

    编译时会报警告:


    [4] 敏锐的读者可能会发现一个矛盾:如果函数newline没有返回值,那么表达式newline()不就没有值了吗?然而上一章讲过任何表达式都有值和类型两个基本属性。其实这正是设计void这么一个关键字的原因:首先从语法上规定没有返回值的函数调用表达式有一个void类型的值,这样任何表达式都有值,不必考虑特殊情况,编译器的语法解析比较容易实现;然后从语义上规定void类型的表达式不能参与运算,因此newline() + 1这样的表达式不能通过语义检查,从而兼顾了语法上的一致和语义上的不矛盾。