C++模板类的声明和定义都要放在.h文件中

程序执行前的操作

一个编译单元是指一个.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文件中

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值