为什么cpp的声明和实现要放在一个文件

前言

本文首发于我的公众号:码农手札,主要介绍linux下c++开发的知识包括网络编程的知识同时也会介绍一些有趣的算法题,欢迎大家关注,利用碎片时间学习一些编程知识,冰冻三尺非一日之寒,让我们一起加油!

说实话,我个人是不怎么使用模板的,使用场景很少,因此在上一篇博客里面难得用了一次模板,反而出了点小问题,我遇到的问题就是把声明和实现分开在h文件和cpp文件,因此在编译的时候怎么都无法通过,简直懵逼了,不过好在上网搜了一下突然想起来好像之前看到过模板的声明和实现必须都写在一个头文件中,这里我们来简单研究下为什么cpp里面,模板和声明是必须放在一起的。

测试代码

让我们先写一份非模版实现的代码,然后再写一份使用模板实现的代码,使用一些辅助工具来看看有什么不同,下面先展示一份非模板实现的代码

///test.h
#pragma once

 class Test {
public:
  Test(const int &t) : ele_(t) {}
  int get_ele();

private:
  int ele_;
};

///test.cpp
#include "test.h"
int Test::get_ele() { return ele_; }

///test_main.cpp
#include "tetest_
#include 

int main(int argc, char const *argv[]) {
  Test test(5);
  std::cout << test.get_ele() << std::endl;
  return 0;
}

下面是使用模板实现的代码

///test_template.h
#pragma once

template  class TestTemplate {
public:
  TestTemplate(const T &t) : ele_(t) {}
  T get_ele();

private:
  T ele_;
};

///test_template.cpp
#include "test_template.h"
template  T TestTemplate::get_ele() { return ele_; }

///main.cpp
#include "test_template.h"
#include 

int main(int argc, char const *argv[]) {
  TestTemplate test_template(5);
  std::cout << test_template.get_ele() << std::endl;
  return 0;
}

非模板代码编译结果

很显然,按照c++的规则,前一种实现方式是完全没有问题的,但是后一种将模板声明和实现分开的方式编译是无法通过的,那么让我们试试看看到底是什么情况

g++ -c test.cpp

获得test.o文件,然后使用nm来查看里面有什么函数

nm test.o

在我电脑上,输出为0000000000000000 T __ZN4Test7get_eleE,这个时候,我们能够猜到这个就是get_ele这个函数的定义,其实这个新的名字是c++的一种给函数定义唯一标示的一种方式,这个是根据命名空间以及函数长度包括参数类型生成的,有一定的规则,不过这个其实没必要太关注(这里我多提一句,因为生成新名字的时候并不包括返回值的类型,这就是c++要求重载函数必须要求函数参数不同的原因)。不过当我们在遇到程序bug的时候,使用gdb也会看到这个东西,这个时候我们需要查看这个函数,这里我介绍一个辅助工具查看函数实际的名字

c++ __ZN4Test7get_eleE

这个命令会告诉我们那个名字对应的真实名字,我这里对应的是Test::get_ele(),不多提,也就是说Test::get_ele()的实现是在test.o文件里面的,当执行到编译的第四个阶段的时候也就是链接,test_main.o会告诉链接器,它需要Test::get_ele()的定义,这个时候链接器发现,哎,test.o文件里面,那正好,然后顺利生成可执行文件,到这里都是顺理成章,那么,看起来我们可以猜测将模板类的声明和实现分开的话,是否链接器无法找到它想要的东西呢,让我们继续下一步。

模板代码编译尝试

这里,毋庸置疑,直接编译肯定是会失败的,那么让我们来看看,到底是哪里出了问题,根据编译报错,Undefined symbols for architecture x86_64: "TestTemplate::get_ele()", referenced from: _main in template_main-7858f0.o, 这个编译错误已经很明显的告诉我们,链接器没找到TestTemplate::get_ele()的定义,那么我们来看看test_template.o里面是否有TestTemplate::get_ele()的定义

g++ -c test_template.cpp

获得test_template.o文件,然后我们用老办法,使用nm来看看test_template.o里面有没有我们想要的东西

nm test_template.o

终端什么都没输出,不过我们对于这个结果也不意外,因为要是有定义才奇怪了,这里我来解释一下,为什么没有定义呢,因为c++标准规定,如果当一个模板没有被用到的时候,它就不该被实现出来,这里由于test_template.cpp里面并没有任何地方用到了TestTemplate,所以自然也不会被实例化,这里可能有人会奇怪,但是main函数里面不是调用了吗,这里需要注意的是c++的编译方式,各个cpp文件是相互独立的,所以如果test_template.cpp里面没有用到,其他任何一个cpp文件用到,模板是都不会被实例化的,这也就是为什么我们使用nm命令查看test_template.o却什么也没看到的原因。

换种方式

我们将test_template.cpp改成下面这样

#include "test_template.h"

int test() {
  TestTemplate t(5);
  return t.get_ele();
}

template  T TestTemplate::get_ele() { return ele_; }

然后我们再编译,这个时候我们就会惊奇的发现,编译完全正常,这里我们仅仅添加了一个无法被外界调用的test函数(因为我们并没有在头文件种声明test函数),但是正是由于test函数中用到了TestTemplate,因此模板就被实例化了,如果我们将test函数中模板类型从int改成double,再次编译肯定是无法通过的,这个我就不解释了。

总结

到这里,文章就写的差不多了,这篇文章其实没啥很难的地方,本质上其实就是说c++要求模版声明和实现放在一个头文件,不过这里介绍了几个小命令,让我们熟悉一下c++的编译链接过程,以及一些规则。

你可能感兴趣的:(为什么cpp的声明和实现要放在一个文件)