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。