目录
1.编译链接的过程
我们编程的时候习惯将函数的声明文件写在.h文件中,然后将函数的定义写在.cpp文件中,在需要调用别的文件中的某个函数的时候就会将该函数所在的头文件include到调用处的文件头部。在C++程序预编译过程中,所有头文件都会展开,在之后的过程中所有的头文件都会消失,只剩下头部被展开了#include的源文件被转为.obj文件。因为展开了头文件的缘故,这些.cpp文件中会有许多的函数声明。而这些函数声明的真正定义往往就集中在某一个.cpp源文件中(在没有编译错误的前提下,这时候的源文件已经被编译为.obj文件)。然后由链接器负责将各个.obj文件链接起来,让每个函数调用都准确找到对应定义的地址,生成最终的.exe文件。
头文件声明的作用,就是让编译器知道,这个函数的定义应该在其他文件中,就不会因为暂时找不到函数的定义而报错。至于找到对应的定义,就是链接器需要干的事情。
2.函数模板
但是函数模板是个例外。我们不能将函数模板和类模板的声明和定义分散在.h文件和.cpp文件中。如果我们这样写了会发生什么呢?看这个例子:
//func.h文件
#pragma once
//函数模板的声明
template<typename T> void Swap(T& a, T& b);
//func.cpp文件
#include "func.h"
//函数模板的定义
template<typename T> void Swap(T& a, T& b)
{
T temp = a;
a = b;
b = temp;
}
//main.cpp文件
#include <iostream>
#include "func.h"
using namespace std;
int main()
{
int n1 = 10, n2 = 20;
Swap(n1, n2);
return 0;
}
运行会报一个链接错误,提示找不到void Swap(int &, int &)这个函数的具体定义:
究其原因,是因为函数模板实际上并不是一个真正的函数。它仅仅是编译器用来生成函数或类的一张“图纸”。模板不会占用内存,最终生成的函数才会占用内存。函数模板只是用来告诉编译器,在遇到某个函数调用的时候需要生成什么样子的函数。在编译过程中,编译器会参考函数模板,然后生成对应的真正的函数。 例如:
#include <iostream>
#include <string>
#include "func.h"
using namespace std;
template<typename T> void fun(T a)
{
cout << a << endl;
}
int main()
{
string str = "hello world!";
fun(str);
return 0;
}
在这段代码中,编译器在执行main的时候遇到了一个func(string )的调用,此时编译器就会参考函数的模板,生成一个特定类型string的函数定义:
void func(string a)
{
cout<< a << endl;
}
这才是真正的函数的定义。而且,编译器在哪里遇到了函数,就会在当前文件中寻找函数模板的定义,然后在当前文件中生成这种真正的函数定义。
回到第一个例子,当编译器在main.cpp中遇到了Swap(n1,n2)的时候,就会寻找函数模板的定义,尝试生成Swap(int,int)的函数定义。但是在展开的头文件func.h中,只发现了 template void Swap(T& a, T& b)的模板声明,找不到具体的模板定义,因此就地生成函数定义的尝试失败。此时编译器不会报错,而是对该函数的调用做一个记录,希望等到链接程序时在其他目标文件(.obj 文件或 .o 文件)中找到该函数的定义。但不幸的是,在func.cpp文件中,只有这个函数模板的具体定义,而没有这个特定于int类型的swap函数的具体定义,所以会报错。这就是为什么模板函数需要将声明和定义都写在头文件的原因,这样才可以让编译器顺利地生成特定类型的函数的定义,而不是将希望寄托在其他文件中寻找函数定义。这个道理同样也适用于类模板。
当然可以采用模板定义的位置显式实例化,如下面代码那样,但这种方法不实用,不推荐使用。
// a.h
template<class T>
T Add(const T& left, const T& right);
// a.cpp
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
// 函数显示实例化,将类型为int、double的函数定义,放在a.cpp中
template
int Add<int>(const int& left,const int& right);
template
double Add<double>(const double& left,const double& right);
// main.cpp
#include"a.h"
int main()
{
Add(1, 2);
Add(1.0, 2.0);
return 0;
}
这种方法是直接在函数或类定义的文件中实例化,等调用的时候就可以找到该地址了,因为链接时,它们已经实例化了,有了地址进入了符号表,在符号表中就能找到它们的地址,从而不会报错。但这种方法的弊端很明显,每次换一个类型就得自己显示实例化一次,这样就显得有些呆板和麻烦了,所以不推荐这种方法。
3.总结
通常情况下,你会在.h文件中声明函数和类,而将它们的定义放置在一个单独的.cpp文件中。但是在使用模板时,这种习惯性做法将变得不再有用,因为当实例化一个模板时,编译器必须看到模板确切的定义,而不仅仅是它的声明。因此,最好的办法就是将模板的声明和定义都放置在同一个.h文件中。这就是为什么所有的STL头文件都包含模板定义的原因。
标准要求编译器在实例化模板时必须在上下文中可以查看到其定义实体;而反过来,在看到实例化模板之前,编译器对模板的定义体是不处理的——原因很简单,编译器怎么会预先知道 typename 实参是什么呢?因此模板的实例化与定义体必须放到同一翻译单元中。
对C++编译器而言,当调用函数的时候,编译器只需要看到函数的声明。当定义类类型的对象时,编译器只需要知道类的定义,而不需要知道类的实现代码。因此,因该将类的定义和函数声明放在头文件中,而普通函数和类成员函数的定义放在源文件中。
但在处理模板函数和类模板时,问题发生了变化。要进行实例化模板函数和类模板,要求编译器在实例化模板时必须在上下文中可以查看到其定义实体;而反过来,在看到实例化模板之前,编译器对模板的定义体是不处理的——原因很简单,编译器怎么会预先知道 typename 实参是什么呢?因此模板的实例化与定义体必须放到同一翻译单元中。
原文链接:https://blog.csdn.net/Currybeefer/article/details/125705223