2025年4月17日

技术教程 为何C++需要头文件源文件分离,以及链接器的多重定义报错

作者 TheWhiteDog9487

我在刚开始学C++的时候,就一直有个疑问。
不管是教学还是看的其他教程文章,采用的都是定义扔头文件实现放源文件,而且include的永远是头文件。
那我就寻思着,不要定义直接实现源文件并且include源文件怎么你了?
啥都不知道嘛,不信邪就去试试,你还真别说,还真就不行。
直接上示例代码:

// a.cpp

#include <iostream>

#include "include.cpp"

using namespace std;

int main(){
    cout << "Hello World!" << endl;
    IncludeFunc();
    return 0;
}
// include.cpp

#include <iostream>

using namespace std;

void IncludeFunc(){
    cout << "IncludeFunc" << endl;}
// include.hpp

void IncludeFunc();

而把a.cpp里的#include "include.cpp"改成#include "include.hpp"之后,立马就没事了。

这是怎么回事呢?


首先明确一下,#include干了什么。
#include只会对文件内容进行复制粘贴。

就比如a.cpp里的#include <iostream>,预处理阶段预处理器会打开iostream这么个文件,把里面所有的内容复制出来替换到#include <iostream>所在位置。
由于是复制粘贴,搞的和宏定义一样,因此你甚至可以在变量名称之类的地方使用#include,这点我这里不做演示。
如果想看演示的话可以看这个:
C++分文件为什么要include .h文件,include .cpp会怎么样?_哔哩哔哩_bilibili 空降07:15

解释清楚这个之后,我们就可以继续向后看了。
注意我上面MSVC的命令,我把a.cpp和include.cpp都编译了。
而a.cpp里面有一个include "include.cpp",那么include.cpp里的全局函数void IncludeFunc()就会被复制到a.cpp里面再编译一次。
发现问题没有?此时被编译的a.cpp和include.cpp里面都有一个签名是void IncludeFunc()的函数,并且位于同一作用域。
而他俩返回值类型和参数列表都是一样的,函数名称也一致,这就导致了无法发生重载,事实上就相当于是下面这样:

#include <iostream>

using namespace std;

void IncludeFunc(){
    cout << "IncludeFunc" << endl;}

void IncludeFunc(){
    cout << "IncludeFunc" << endl;}

int main(){
    cout << "Hello World!" << endl;
    IncludeFunc();
    return 0;
}

这个就很容易看懂了嘛,一眼重定义。

那为什么包含头文件就没事呢?
因为头文件里放的东西都不一样啊。
include.cpp里面是函数签名+实现,而include.hpp里面只有一个签名,没了。
这种只有签名没有实现的结构叫函数声明。
如果函数经过声明并且实现不在本文件内,那么链接器就会去其他文件里面查找实现。
所以,只需要include头文件+编译实现cpp文件即可解决问题。

Loading