透过模板实现和声明的分离,理解编译链接的原理
很多时候,我们会将函数的声明和实现分离。函数的声明通常声明在.h文件中,函数的实现通常实现在.cpp文件中。比如,我们想要定义两个数的加法运算,就可以这样写:
在add.h中声明函数
#pragma once
int add(int a, int b);
double add(double a, double b);
在add.cpp中实现函数
#include"add.h"
int add(int a, int b)
{
return a + b;
}
double add(double a, double b)
{
return a + b;
}
我们知道,这样肯定没问题。但是,我们了解了模板,掌握了泛型的概念。可以用模板来优化函数重载,于是我们可能就会这样改进上述代码:
在add.h中声明模板
#pragma once
template <class T>
T add(T a, T b);
在add.cpp中实现模板
#include"add.h"
template <class T>
T add(T a, T b)
{
return a + b;
}
此时,我们想要使用该模板函数,只需要在使用文件中引入add.h即可。比如,我们在main.cpp中使用:
#include<iostream>
#include"add.h" // 引入头文件
using namespace std;
int main()
{
cout << add<int>(1, 2) << endl;
cout << add<double>(1.1, 2.2) << endl;
getchar();
return 0;
}
但是,这种情况却会报错:无法解析这两个函数,也就意味着编译器找不到这两个函数
编译链接过程
我们先了解一下正常的函数实现和分离的编译链接过程(即不用模板的情况下)。三个文件,一个包含了add的实现分离,一个调用函数的main
add.h文件
add.cpp文件
mian.cpp文件
那么,这三个文件在编译器中是如何工作的呢?
首先,我们需要知道的是.h文件是不被C++编译器单独编译的,编译器只会编译cpp文件,而且是单独编译!!!!也就意味着main.cpp和add.cpp是单独编译的,也就意味着main.cpp和add.cpp在编译时,互相不知道对方的存在,只是老老实实的将代码编译器为汇编代码。
下图,展示了整个编译链接过程:
着重说明:调用函数的时候(无论是在哪调用函数,该历程是在mian.cpp中调用),会编译为call函数地址的汇编代码,但是这个代码地址是不准确的,链接的时候会修复这个地址,从而找到真正实现的地方,达到正确调用的目的。
编译器为什么不单独编译.h文件呢?
因为.h文件的作用就是起到声明的作用,规范项目整洁,增强代码可读性。因此,在任何文件中包含某个.h文件,就会将该.h文件里的所有东西,原封不动的复制进来,跟着cpp文件一起编译,所以不会单独编译.h文件。
也就是说,main.cpp中,其实是这样的代码。直接把两个函数的声明放在这,这样的话调用函数的时候,就知道是个函数,所以就会编译通过,不会报错。但是,函数的实现在哪,这里是不知道的,因此call 函数地址时,这个地址基本上都是错的。
从编译链接的原理解释,为什么模板不可以声明和实现分离
add.h
声明模板
#pragma once
template <class T>
T add(T a, T b);
add.cpp
中实现模板
#include"add.h"
template <class T>
T add(T a, T b)
{
return a + b;
}
此时,我们想要使用该模板函数,只需要在使用文件中引入add.h即可。比如,我们在main.cpp中使用:
#include<iostream>
#include"add.h" // 引入头文件
//template <class T>
//T add(T a, T b);
using namespace std;
int main()
{
cout << add<int>(1, 2) << endl;
cout << add<double>(1.1, 2.2) << endl;
getchar();
return 0;
}
我们了解了编译链接原理->对每一个cpp文件单独编译。所以我们在编译add.cpp时,就相当于编译下面的代码。由于没有调用具体的函数,因此在add.cpp中不会产生任何具体的函数。
template <class T>
T add(T a, T b);
template <class T>
T add(T a, T b)
{
return a + b;
}
也就是说,add.obj是空的,没有任何函数的汇编代码。
我们在模板中学过,只要没有具体的调用函数,模板就不会自动生成任何函数,因为模板不知道该生成什么类型的函数。
因此,在链接的时候,main函数中call 函数地址,就找不到具体实现的地方,因此就会链接失败。所以就会提示:无法解析add函数,这是因为编译器找不到add函数的具体调用地址。
可以看出,错误是LNK,也就是链接错误。正与我们分析的一模一样。
所以,模板想要生成具体的函数实现,就必须满足单独编译cpp文件时,在当前cpp文件内出现了该模板的调用,才会生成具体的函数实现,否则不会产生任何动作。总之:想要使用模板,就不要将模板的声明和实现分离。
模板的声明和实现一般用来放在.hpp文件中
hpp一般用来存放,声明和实现不分离的文件。到时候,直接在调用处包含该hpp文件即可。
hpp也不会参与编译,跟h文件是一样的。哪个文件包含hpp文件,就会将hpp文件的所有代码复制到该文件。