3.5 #include预处理器指示符

源代码最终是需要被编译器处理的。编译器编译的过程比较复杂,但一般需要经历好几步,第一步是预处理。所谓预处理,就是在编译前先进行一些预先处理,如代替源代码中需要代替的部分。#include就是这么一个预处理指示指令。

为了弄清楚#include的作用,现在请读者思考一个问题:编译器如何知道有printf这个函数?

3.5.1 函数声明及其作用

上节中留给读者的试验,修改printf为其他单词,如print_format。在编译的时候,编译器会返回以下错误:

Warning h \cbook \src\2\2 2-helloword.c:5  missing prototype for print_format
Error :\cbook \src\2\2.2helloworldc 5 undefined reference to _print_format
编译和连接          耗时   :  3.3秒  返回代码        : 1

“Warning h\cbook\src\2\2 2-helloworl.c:5 missing prototype for print_format”这句话表明,缺了print_format的函数原型。这仅仅是一个警告。“Error:\cbook\src\2\2.2helloworldc 5 undefined reference to_print_format”这句话表明,出现一个错误,调用了一个没有定义的函数print_format。

简单解释一下函数原型(prototype)的概念。回顾上节提到过的函数定义,函数定义由4部分组成:返回类型、函数名、参数表、函数体。前面的3部分合起来称为函数原型。如下:

返回类型 函数名(参数表)

函数在被调用之前,一定要让编译器知道函数原型,这样编译器才知道有哪些函数名,该函数需要些什么样类型的参数,返回什么样类型的值。告诉编译器函数原型的动作称为函数声明。如下:

返回类型 函数名(参数表);

注意 函数声明是一条语句,要用分号表示结束。

函数声明和函数定义中的返回值类型、参数表、函数名都要一致。虽然C语言提供了很多库函数,但是对于编译器来说还是不确定库函数的位置。所以即使使用的是C语言系统的库函数,也必须向编译器声明。

因为在本实验中print_format函数并没有向编译器声明过其函数原型,编译器就提出抗议——一条警告。这条警告只是提醒程序员而已,如果程序员忘记了向编译器声明函数原型,编译器会自己生成一个默认的函数声明。然而代码中实际上调用了一个根本不存在也就是没有定义的函数,编译器当然就要罢工了——一条错误提示。

3.5.2 试验寻找#include的作用

代码3-1中,函数printf的声明在哪里呢?请读者再做一个试验:将代码3-1中的第一行代码删除掉。就是去掉“#include <stdio.h>”,再编译看出现什么现象。整个文件代码如下:

01      void main(void)                                                         /*主函数,入口点*/
02      {                                                                                       /*函数开始*/
03              printf("\nHello World!");                                       /*打印字符串*/
04              getchar();                                                              /*等待用户敲入回车*/
05      }

【代码解析】是不是编译器又提示缺少函数原型了呢?

Warning d:\cbook\src\2\2.2-helloworld.c: 3  missing prototype for printf
Warning d:\cbook\src\2\2.2-helloworld.c: 4  missing prototype for getchar
编译和连接 耗时:0.3秒 返回代码:0

可以推测printf和getchar两个函数的声明一定在stdio.h文件里。

没错,在CodeBlocks的安装目录下,有一个include文件夹。读者可以在Windows的文件浏览器中定位到CodeBlocks的安装文件夹中,去看看include下有些什么文件。是不是在include文件夹下可以搜索到stdio.h文件?用记事本或者任意一个文本编辑器打开该文件,截取该文件中的一部分如下:

int getchar(void);
char * gets(char *);
int _getw(FILE *);
int _pclose(FILE *);
#define pclose _pclose
FILE * popen(const char *, const char *);
#define _popen popen
int printf(const char *, ...);
int dprintf(const char *, ...);
int putc(int, FILE *);

看见了“int getchar(void);”和“int printf(const char *,...);”两行吗?它们就是这两个函数的声明。

请读者再做一个试验:修改代码3-1如代码3-2所示。

代码3-2 去掉#include语句自行添加函数声明DeclareSelf

<-----------------------------文件名:DeclareSelf.c ---------------------------->
01      int getchar(void);
02      int printf(const char *, ...);
03
04      void main(void)                                                         /*主函数,入口点*/
05      {                                                                                       /*函数开始*/
06              printf("\nHello World!");                                       /*打印字符串*/
07              getchar();                                                              /*等待用户按回车键*/
08      }                                                                                       /*函数结束*/

【代码解析】此时编译,顺利通过。还记得初中时学过的等价交换吗?#include和什么等价?

3.5.3 #include的作用

本节来解释#include这行代码的作用。

#include是C语言预处理器指示符。#和include之间可以有多个空格。#也不一定要顶格,但是一定是第一个非空白字符。#include的作用是告诉编译器,在编译前要做些预先处理:将后面< >中的文件内容包含到当前文件内。所谓包含,是指将< >中列出的文件的内容复制到当前文件里。

注意 #一定要是第一个非空白字符,否则编译器会提示错误,并且错误信息和出错原因完全不匹配。

因为getchar和printf两个函数的声明位于stdio.h文件中,所以用#include把stdio.h文件包含进来,自然就把getchar和printf两个函数的声明包含进来了。

说明 函数声明只是向编译器登记有这么一个函数,声明了函数而不调用这个函数是被容许的。这就是为什么包含了整个stdio.h文件(里面声明了很多其他函数),但实际没有使用这些函数而编译器又不提示的原因。

读者可能要问,stdio.h文件是个什么文件呢?std是标准(standard)的缩写,io是Input/Output的缩写,联合起来就是“标准输入输出”的意思,一般就是与屏幕输出和键盘输入相关的内容。“.h”是C语言头文件扩展名。所谓头文件,就是该文件都是些函数的声明、变量的声明等内容,“.c”文件是C语言实现文件,是真正做事情的文件。

为了使读者对“包含”的意思有个更明确的概念,再做一个试验:

修改代码3-1为代码3-3,主要的修改就是把main函数中的“printf("\nHello Wolrd")”:删除,但是把它移到文件string.txt中。

代码3-3 #include的试验AboutInclude

<----------------------------文件名:AboutInclude.c--------------------------->
01      #include <stdio.h>                                                /*包含该头文件的目的是使用了函数printf*/
02                                                                                      /*空行,主要是为了分隔,编译器忽略*/
03      void main(void)                                                 /*主函数,入口点*/
04      {                                                                               /*函数开始*/
05              #include "string.txt"                                   /*将.txt文件包含到程序中来*/
06              getchar();                                                      /*等待用户按回车键*/
07      }                                                                               /*函数结束*/

【代码解析】第5行代码将一个.txt文件包含到程序中来。读者大概可以想到,里面包含了一些函数。在AboutInclude.c同一个文件夹下面,新建一个.txt文件:string.txt。文件内容如下:

printf("\nHello World");

编译代码3-3,代码顺利通过,运行效果同代码3-1。