很多情况下,程序员可能突然产生这样的问题:为什么非得在文件的首位置#include "... .h"? 我include源文件行不行。
其实任何后缀的文本文件(忽视编码情况)都可以被#include,但是你去#include 源文件会不会出问题呢?我们看下面测试代码:
//Single.cpp
#ifndef __SINGLE__CPP_
#define __SINGLE__CPP_
#include <stdio.h>
void Fun()
{
printf("Fun...\n");
}
#endif/*__SINGLE__CPP_*/
//main.cpp
#include"Single.cpp"
void main()
{
Fun();
}
VC2008下编译,出现如下问题:
为什么会出现这样的情况呢?
其实C语言的编译方式是分离式的,分为两步(简单起见我们假设只有一个生成目标):
1、将一个或多个源文件编译成可重定位的目标文件,其中每个文件是分别编译的。
2、将1步生成的可重定位目标文件链接成一个可执行目标文件或者共享目标文件。
因此,假设foo1.c中用include指令包含了foo2.c,foo2.c中定义了函数function且没有用static修饰。那么经过预处理,foo1.c中也将包含function的定义。于是foo1.c生成的可重定位目标文件(假设为foo1.o)和foo2.c(假设为foo2.o)都有function这个符号,这样上面所说的第二步就会因为冲突而失败。
事实上,在没有头文件可被包含的情况下(事实上初期,是最早最早的时候),我们可以采用如下的方式:
//main.cpp
extern Fun();
void main()
{
Fun();
}
程序编译运行,是我们想要的结果。其实这个就是一个前置声明。事实上,我们不加"extern"关键字,编译也正常:
//main.cpp
void Fun();
void main()
{
Fun();
}
这是因为:函数具有定义和声明,定义的时候用extern,说明这个函数是可以被外部引用的,声明的时候用extern说明这是一个声明。但由于函数的定义和声明是有区别的,定义函数要有函数体,声明函数没有函数体,所以函数定义和声明时都可以将extern省略掉,反正其他文件也是知道这个函数是在其他地方定义的,所以不加extern也行。两者如此不同,所以省略了extern也不会有问题。
我们再看变量的声明和定义:
extern int a; //声明一个全局变量a
int a; //定义一个全局变量a
extern int a =0 ; //定义一个全局变量a 并给初值。
int a =0; //定义一个全局变量a,并给初值,
第四个等于第三个,都是定义一个可以被外部使用的全局变量,并给初值。但是定义只能出现在一处。也就是说,不管是int a;还是extern int a=0;还是int a=0;都只能出现一次,而那个extern int a可以出现很多次。当你要引用一个全局变量的时候,你就要声明,extern int a;这时候extern不能省略,因为省略了,就变成int a;这是一个定义,不是声明。
因此,访问者模块时仍要对变量进行声明,而这时候是必须要加"extern"关键字的。
这样我们出现一个问题?既然extern+前置声明都能实现这些功能?为什么还得将声明放在头文件中并被包含呢?我觉得下面这段话说得很清楚:
在C语言家族程序中,头文件被大量使用。一般而言,每个C++/C程序通常由头文件(header files)和定义文件(definition files)组成。头文件作为一种包含功能函数、数据接口声明的载体文件,用于保存程序的声明(declaration),而定义文件用于保存程序的实现 (implementation)。
C++/C程序的头文件以“.h”为后缀。以下是假设名称为 graphics.h的头文件:
#ifndef GRAPHICS_H (作用:防止graphics.h被重复引用)
#define GRAPHICS_H
#include.... (作用:引用标准库的头文件)
...
#include... (作用:引用非标准库的头文件)
...
void Function1(...); (作用:全局函数声明)
...
class Box (作用:类结构声明)
{
...
};
#endif
从以上例子可以看出,头文件一般由三部分内容组成:(1)头文件开头处的版权和版本声明;(2)预处理块;(3)函数和类结构声明等。在头文件中,用 ifndef/define/endif结构产生预处理块,用 #include 格式来引用库的头文件。头文件的这种结构,是利用C语言进行开发软件所通常具备的,属于公有知识。
一般在一个应用开发体系中,功能的真正逻辑实现是以硬件层为基础,在驱动程序、功能层程序以及用户的应用程序中完成的。根据以上示例,可以发现头文件的主要作用在于调用库功能,对各个被调用函数给出一个描述,其本身不包含程序的逻辑实现代码,它只起描述性作用,告诉应用程序通过相应途径寻找相应功能函数的真正逻辑实现代码。用户程序只需要按照头文件中的接口声明来调用库功能,编译器会从库中提取相应的代码。
从以上结构图来看,头文件是用户应用程序和函数库之间的桥梁和纽带。在整个软件中,头文件不是最重要的部分,但它是C语言家族中不可缺少的组成部分。做一个不算很恰当的比喻,头文件就像是一本书中的目录,读者(用户程序)通过目录,可以很方便就查阅其需要的内容(函数库)。在一本书中,目录固然重要,但绝对不是一本书的核心的、最重要的部分。
你既然写出了一个模块并且要提供给其它模块使用的功能,你总不能让其它模块去“猜”你模块的哪些接口吧。所以这时候干脆再带个索引一样的头文件。当然如果你只提供了索引不提供内容,则会发生编译时没问题,链接时出问题。