程序执行前的操作
一个编译单元是指一个.cpp文件及其它所#include的所有.h文件。.h文件的代码将会被扩展到.cpp文件中,然后编译器编译.cpp文件会生成一个.obj文件(Win),当编译器将所有的.cpp文件编译完后,再用链接器将所有.obj文件链接起来,形成.exe文件。才可以执行
\\ ------- in Test.h start
#ifndef TEST_H
#define TEST_H
void testOutput();
#endif // TEST_H
\\ ------- in Test.h end
\\ ------- in Test.cpp start
#include "Test.h"
#include <QDebug>
void testOutput()
{
qDebug() << "This is a test output.";
}
\\ ------- in Test.cpp end
\\ ------- in main.cpp start
#include <QCoreApplication>
#include "ZDS/Test.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
testOutput();
return a.exec();
}
\\ ------- in main.cpp end
在以上代码中,Test.cpp 和main.cpp 各自被编译成不同的 .obj 文件(假设命名为 Test.obj 和 main.obj),在 main.cpp 中,调用了 testOutput 函数,然而当编译器编译 main.cpp 时,它所仅仅知道的只是 main.cpp 中所包含的 Test.h 文件中的一个关于 void testOutput(); 的声明,所以,编译器将这里的 testOutput 看作外部连接类型,即认为它的函数实现代码在另一个 .obj 文件(Test.obj)中,也就是说,main.obj 中实际没有关于 testOutput 函数的哪怕一行二进制代码,而这些代码实际存在于 Test.cpp 所编译成的 Test.obj 中。在 main.obj 中对 testOutput 的调用只会生成一行 call 指令
call testOutput
在编译时,这个 call 指令显然是错误的,因为 main.obj 中并无一行 testOutput 的实现代码。此时需要链接器出马,链接器负责在其他.obj中寻找testOutput的实现代码,找到以后将call testOutput这个指令的调用地址换成实际的testOutput函数进入点的地址。需要注意的是:连接器实际上将工程里的 .obj “连接” 成了一个 .exe 文件,而它最关键的任务就是上面说的,寻找一个外部连接符号在另一个 .obj 中的地址,然后替换原来的“虚假”地址。
总结下来:
1、编译main.cpp时,编译器不知道testOutput的实现,所以对这个函数的调用只是给出一个指示,指示链接器去寻找testOutput的实体。
2、编译Test.cpp时,编译器知道了testOutput的实现,在Test.obj中出现了testOutput的二进制代码
3、链接器在寻找testOutput,在Test.obj中找到了代码的地址,将main.obj中悬着的call XXX换成了call testOutput的实际地址
了解原理之后,来看看模板为什么不一样?
我们知道模板有一个特点,就是只有在实例化调用的时候才可以被编译成二进制代码。因为不调用时没人知道这个模板会变成什么类型的,是int还是double还是其他的?
\\ ------- in Test.h start
#ifndef TEST_H
#define TEST_H
#include <QDebug>
template<typename T>
void testOutput(T test) {
qDebug() << "This is a test output: " << test;
}
#endif // TEST_H
\\ ------- in Test.h end
\\ ------- in main.h start
#include <QCoreApplication>
#include "ZDS/Test.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
testOutput("haha");
return a.exec();
}
\\ ------- in main.h end
如果你在 main.cpp 文件中没有调用过 testOutput,testOutput 也就得不到实例化,从而 main.obj 中也就没有关于 testOutput 的任意一行二进制代码!如果你这样调用了
f(10); // f<int>得以实例化出来
f(10.0); // f<double>得以实例化出来
这样 main.obj 中也就有了 testOutput<int>,testOutput<double> 两个函数的二进制代码段。
然而实例化要求编译器知道模板的定义(注意不是声明!!)
下面是模板的声明和定义分开的版本
\\ ------- in Test.h start
#ifndef TEST_H
#define TEST_H
template <typename T>
class Test
{
public:
Test(T v)
: t (v)
{}
void testOutput();
private:
T t;
};
#endif // TEST_H
\\ ------- in Test.h end
\\ ------- in Test.cpp start
#include "Test.h"
#include <QDebug>
template<typename T>
void Test<T>::testOutput()
{
qDebug() << "This is a test output: " << t;
}
\\ ------- in Test.cpp end
\\ ------- in main.cpp start
#include <QCoreApplication>
#include "ZDS/Test.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
Test<int> test(13);
test.testOutput(); // #1
return a.exec();
}
\\ ------- in main.cpp end
在vs中,会报错:
main.obj:-1: error: LNK2019: 无法解析的外部符号 "public: void __thiscall Test<int>::testOutput(void)" (?testOutput@?$Test@H@@QAEXXZ),该符号在函数 _main 中被引用
编译器在#1处并不知道 Test<int>::testOutput 的定义,因为它不在 Test.h 里面,于是编译器只好寄希望于连接器,希望它能够在其他 .obj 里面找到 Test<int>::testOutput 的实例,在本例中就是 Test.obj,然而,后者中真有 Test<int>::testOutput 的二进制代码吗?NO!!!因为 C++ 标准明确表示,当一个模板不被用到的时侯它就不该被实例化出来,Test.cpp 中用到了 Test<int>::testOutput 了吗?没有!!所以实际上 Test.cpp 编译出来的 Test.obj 文件中关于 Test<int>::testOutput 一行二进制代码也没有,于是连接器就傻眼了,只好给出一个连接错误。
但如果在Test.cpp中写一个函数调用Test<int>::testOutput,编译器就会在Test.cpp文件中实例化出该函数,这样Test.obj中就有了Test<int>::testOutput的地址,链接器才能够连接上。
总结:在分离编译的过程中,编译器编译某个.cpp文件时并不知道另一个.cpp文件的存在,也不回去查找(寄希望于链接器)。但模板仅在真正调用的时候才会实例化出来,所以当链接器看到模板的声明时,并不能实例化出来模板,只能创建一个外部链接符号并期待链接器能找到。当实现该模板的.cpp文件中并没有用到该模板的实例,所以编译器也不会实例化模板,所以整个工程找不到一行模板实例化的二进制代码,于是链接器会报错。
所以一定要把模板声明和定义都放在.h文件中