【转载】为什么不常见include .c文件
备:对于#include <filename.h> ,编译器从标准库路径开始搜索 filename.h
对于#include “filename.h” ,编译器从用户的工作路径开始搜索 filename.h
今天有人问我: #include能不能include一个(多个.c文件)?
偶的回答是:从理论上讲可以,但是不推荐。
为什么经常见到include .h文件而不是include .c文件?或者说include是不是就是为包含.h文件设定的语法?这个问题的答案偶不知道,没有见有文档记载、说明这个问题。不过从语法角度讲,include的意思就是从当前位置包含另外一个文件,就象宏替换一样把当前行用另外一个文件的整个内容替换掉。
从这点讲,include .c文件是可行的,c编译器完全能够正常处理。但是为什么不常见include .c文件?从设计角度上讲,源代码区分为.h和.c文件,是为了接口与实现的分离,实际上两者没什么本质的差别。.h文件提供接口,.c文件提供具体的实现,两者可以一一对应,也可以不一一对应,没有强制要求。一个.c文件做为一个模块的实现,有可能要跟其他的模块打交道,这个时候就需要include其他模块的接口(其他模块的.h文件);而包含其他模块的实现(.c文件)是没有意义的、危险的。
所以,我们不应该在项目中include .c文件,这样使用者出于直觉很难想到这里会有问题,增加了排错的难度。前几天偶移植一个国际知名大公司的代码就遇到了这个问题,耗费了半天的时间查看了全部的源码和makefile才发现了这个不常见编译现象。当然,那个公司的代码之所以这么做,是他认为这些代码已经很成熟了,不需要修改和反复重新编译。但它的做法确实对我的调试造成了很大的障碍。
----- GNU Make Document 中的相关章节 -----
4.12 自动生成依赖
在为一个程序编写的makefile文件中,常常需要写许多仅仅是说明一些OBJ文件依靠头文件的规则。例如,如果‘main.c’通过一条#include语句使用‘defs.h’,您需要写入下的规则:
main.o: defs.h
您需要这条规则让make知道如果‘defs.h’一旦改变必须重新构造‘main.o’。由此您可以明白对于一个较大的程序您需要在makefile文件中写很多这样的规则。而且一旦添加或去掉一条#include语句您必须十分小心地更改makefile文件。
为避免这种烦恼,现代C编译器根据原程序中的#include语句可以为您编写这些规则。如果需要使用这种功能,通常可在编译源程序时加入‘-M’开关,例如,下面的命令:
cc -M main.c
产生如下输出:
main.o : main.c defs.h
这样您就不必再亲自写这些规则,编译器可以为您完成这些工作。
注意,由于在makefile文件中提及构造‘main.o’,因此‘main.o’将永远不会被隐含规则认为是中间文件而进行搜寻,这同时意味着make不会在使用它之后自动删除它;参阅隐含规则链。
对于旧版的make程序,通过一个请求命令,如‘make depend’,利用编译器的特点生成依赖是传统的习惯。这些命令将产生一个‘depend’文件,该文件包含所有自动生成的依赖;然后makefile 文件可以使用include命令将它们读入(参阅包含其它makefile文件)。
在GNU make中,重新构造makefile文件的特点使这个惯例成为了过时的东西――您永远不必具体告诉make重新生成依赖,因为GNU make总是重新构造任何过时的makefile文件。参阅Makefile文件的重新生成的过程。
我们推荐使用自动生成依赖的习惯是把makefile文件和源程序文件一一对应起来。如,对每一个源程序文件‘name.c’有一名为‘name.d’的 makefile文件和它对应,该makefile文件中列出了名为‘name.o’的OBJ文件所依赖的文件。这种方式的优点是仅在源程序文件改变的情况下才有必要重新扫描生成新的依赖。
这里有一个根据C语言源程序‘name.c’生成名为‘name.d’依赖文件的格式规则:
%.d: %.c
set -e; $(CC) -M $(CPPFLAGS) $< \
| sed 's/\($*\)\.o[ :]*/\1.o $@ : /g' > $@; \
[ -s $@ ] || rm -f $@
关于定义格式规则的信息参阅定义与重新定义格式规则。‘-e’开关是告诉shell如果$(CC)命令运行失败(非零状态退出)立即退出。正常情况下,shell退出时带有最后一个命令在管道中的状态(sed),因此make不能注意到编译器产生的非零状态。
对于GNU C编译器您可以使用‘-MM’开关代替‘-M’,这是省略了有关系统头文件的依赖。详细内容参阅《GNU CC使用手册》中控制预处理选项。
命令Sed的作用是翻译(例如):
main.o : main.c defs.h
到:
main.o main.d : main.c defs.h
这使每一个‘.d’文件和与之对应的‘.o’文件依靠相同的源程序文件和头文件,据此,Make可以知道如果任一个源程序文件和头文件发生变化,则必须重新构造依赖文件。
一旦您定义了重新构造‘.d’文件的规则,您可以使用使用include命令直接将它们读入,(参阅包含其它makefile文件),例如:
sources = foo.c bar.c
include $(sources:.c=.d)
(这个例子中使用一个代替变量参照从源程序文件列表‘foo.c bar.c'翻译到依赖文件列表‘foo.d bar.d'。详细内容参阅替换引用。)所以,‘.d’的makefile文件和其它makefile文件一样,即使没用您的任何进一步的指令,make 同样会在必要的时候重新构建它们。参阅Makefile文件的重新生成过程。
GNU Make手册的开头就说出了很多人不知道的知识——它不仅仅用于编译的:p
----- GNU Make Document 中的相关章节 -----
GNU Make符合IEEE Standard 1003.2-1992 (POSIX.2) 6.2章节的规定。
因为C语言程序更具有代表性,所以我们的例子基于C语言程序,但Make并不是仅仅能够处理C语言程序,它可以处理那些编译器能够在Shell命令下运行的的各种语言的程序。事实上,GNU Make不仅仅限于程序,它可以适用于任何如果一些文件变化导致另外一些文件必须更新的任务。
如果要使用Make,必须先写一个称为Makefile的文件,该文件描述程序中各个文件之间的相互关系,并且提供每一个文件的更新命令。在一个程序中,可执行程序文件的更新依靠OBJ文件,而OBJ文件是由源文件编译得来的。
一旦合适的Makefile文件存在,每次更改一些源文件,在shell命令下简单的键入:
make
就能执行所有的必要的重新编译任务。Make程序根据Makefile文件中的数据和每个文件更改的时间戳决定哪些文件需要更新。对于这些需要更新的文件,Make基于Makefile文件发布命令进行更新,进行更新的方式由提供的命令行参数控制。
这属于滥用#include.
为何要分为头文件和源文件?本来就是为了传递如下信息:
类型定义
外部函数原型
外部变量
宏
这部分不产生任何实际代码的东西。
c各文件之间的代码是通过#include来引入的吗?这属于连接器的工作!
怀疑使用这个的人,也许是因为源文件的代码太长了,就滥用#include,将源代码分到几个文件上。
我估计将实现这种招数的办法是将源代码划分为一个主多个从的关系,主的引入所有从文件。
他们的内容有如下规定:
主文件,包括所有外部连接的代码(公共)
从文件,所有元素具备内部连接(私有)
对于客户端来说,只有主文件才是他们真正去要连接的。
而因为从文件都是内部连接,相关的.obj将毫无疑义的被丢弃,因为主文件有着一模一样的拷贝。
当然更蹩脚的办法是将这些#include了的源文件排除出项目定义文件。