在解释这个问题之前,我们要先回顾一下一个C++程序编译链接的过程。
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函数的具体定义,所以会报错。
这就是为什么模板函数需要将声明和定义都写在头文件的原因,这样才可以让编译器顺利地生成特定类型的函数的定义,而不是将希望寄托在其他文件中寻找函数定义。这个道理同样也适用于类模板。