现在程序开发使用各种IDE越来越多,很多程序员在日常工作中都接触不到Makefile、编译、链接这些东西了,甚至有些程序员代码写的挺好,却不知道编译、链接其实是两个步骤,只知道按照某种约定,把函数的声明放在头文件,实现放在相应的源文件里面,剩下的事情IDE都可以帮你一键搞定。
在搞清楚“为什么头文件里面只能放声明”之前,我们先想一想,头文件是用来做什么的?一个头文件是被其它头文件或源文件引用的,也就是我们经常看到的每个文件前面几行的"#include xxxx"。那么一个文件在"#include"了一个头文件之后,究竟发生了什么事儿呢?众所周知,"#include"是一个预编译指令,我们把一个引用了某个头文件的源文件预编译一下,看看究竟发生了什么~~
假设有三个文件func0.h, func1.h, func1.c,内容如下:
//func0.h
int func0() {
return 0;
}
//func1.h
int func1();
//func1.c
#include "func0.h"
#include "func1.h"
int func1() {
func0();
return 1;
}
对上面的文件"func1.c"执行预编译:gcc -E func1.c,输出结果如下(注释已删除):
int func0() {
return 0;
}
int func1();
int func1() {
func0();
return 1;
}
从上面的输出结果可以看出来,去除注释的内容,"#include"头文件其实就是在源文件中把头文件的内容复制了一份。所以,当一个头文件被多个文件引用的时候,这个头文件的内容也就被复制到了多个文件当中。比如头文件"func0.h"又被文件"func2.c"引用,"func2.c"预编译的结果如下:
int func0() {
return 0;
}
int func2();
int func2() {
func0();
return 2;
}
从上面例子可以看出,由于"func0"的实现被放在头文件"func0.h"中,导致在"func1.c"和"func2.c"中都存在函数"func0"的实现。这时单独编译"func1.c"和"func2.c"是没有问题的,因为这时每个文件都是独立编译、还没有出现函数名的冲突。当一个文件比如"main.c"需要同时调用函数"func1"和"func2",代码如下:
//func2.h
int func2.h();
//main.c
#include "func1.h"
#include "func2.h"
int main() {
func1();
func2();
return 0;
}
这时编译"main.c"也不会出错,因为"func1.h"和"func2.h"中只有函数"func1"和"func2"的声明,但是当需要链接函数"func1"和"func2"的实现时,就会报告符号重复定义错误了:
$gcc func1.o func2.o main.o -o main
func2.o: In function `func0':
func2.c:(.text+0x0): multiple definition of `func0'
func1.o:func1.c:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status
这是因为"func1.o"和"func2.o"里面都有函数"func0"的实现,链接器不知道该使用哪一个"func0"的实现了。问题的解决办法也是显而易见的,只要把函数"func0"的实现从"func0.h"中移到"func0.c"中,然后在头文件"func0.h"中只保留函数"func0"的声明,然后在链接的时候指定"func0.o"即可:
$gcc func0.o func1.o func2.o main.o -o main
总结,从上面例子可以看出,由于"#include xxx"其实就是把某个头文件的内容在相应文件中拷贝了一份,如果头文件中放置了某个函数的实现(或全局变量的定义),当这个头文件被其它文件引用的时候,这些函数的实现或变量的定义就会出现在多个文件当中,那么当这些文件编译生成的目标文件需要被链接到一起的时候,就会出现符号重复定义的问题。
当然,如果你可以保证不需要把有重复符号的目标文件链接到一起,把函数实现或变量定义放在头文件也不会出错,但是当然也不建议这么做,这会给代码后期的维护带来诸多不便~~