为什么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 <iostream>

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 <typename T> class TestTemplate {
public:
  TestTemplate(const T &t) : ele_(t) {}
  T get_ele();

private:
  T ele_;
};

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

///main.cpp
#include "test_template.h"
#include <iostream>

int main(int argc, char const *argv[]) {
  TestTemplate<int> 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<int>::get_ele()”, referenced from: _main in template_main-7858f0.o, 这个编译错误已经很明显的告诉我们,链接器没找到TestTemplate<int>::get_ele()的定义,那么我们来看看test_template.o里面是否有TestTemplate<int>::get_ele()的定义

g++ -c test_template.cpp

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

nm test_template.o

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

换种方式

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

#include "test_template.h"

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

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

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

总结

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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++之程序结构,头⽂件,源代码⽂件 程序组织策略 程序组织策略 C++中建⽴代码⼯程时,通常包括三个部分:头⽂件,函数源代码⽂件,主程序源代码(即main()函数,可以调⽤函数源代码,完成程 序的整体流程与功能)。 头⽂件( 头⽂件(#include)常包含的内容: )常包含的内容: 函数原型 eg:void a(int temp); 使⽤ #define 或 const 定义的符号常量 结构声明声明 模板声明 内联函数 说明: 1. 通常情况下,多个函数可能同时包括上述某些相同内容。如果每个函数中都对该内容进⾏声明,想要修改该内容就需要同时修改上述全部 声明,对后⾯的维护造成不必要的⿇烦。为避免上述问题,可以将相关的内容放在 头⽂件中,这样,要修改内容时,只需要在头⽂件中做 ⼀次修改就可以。 2. 函数定义,变量声明不能放在头⽂件中。例如,如果⼀个头⽂件中包含某⼀函数定义,然后在其他两个⽂件(属于同⼀个程序)中包含该 头⽂件,则同⼀个程序将包含同⼀个函数的两个定义(内联函数是特例),发⽣冲突。变量声明同函数定义。 3. 结构声明(struct),类声明(class)本⾝并不创建变量,⽽只是在源代码⽂件中声明变量时,告诉编译器如何创建该结构变量。同 理,模板声明指⽰编译器如何⽣成与源代码中函数调⽤相匹配的函数定义。 4. const 变量和内联函数有特殊的链接属性,可以放在头⽂件中,不会引起问题。 函数源代码⽂件( 函数源代码⽂件(.cpp或 或.cc): ): 函数源代码⽂件中,主要存放头⽂件中函数原型的具体实现。 使⽤⽅法:编写程序的时候,如果需要使⽤这些函数,则只需包含头⽂件,并将函数源代码⽂件添加到项⽬列表或make列表中 (Linux)。 注:在IDE中不要将头⽂件加⼊到项⽬列表中,也不要在源代码⽂件中使⽤#include来包含其他源代码⽂件,这样将会导致多重声明
是的,C++模板的定义和实现可以写在同一个文件中。事实上,C++编译器在编译过程中需要看到模板的定义和实现,所以将它们放在一个文件中是常见且合理的做法。 C++模板通常包含两部分:头文件(.h或.hpp)和源文件(.cpp)。头文件中包含模板声明和定义,而源文件中包含模板实现。在使用模板的地方,编译器会根据需要将模板实例化为具体类型的代码。 将模板定义和实现放在一个文件中有以下几个优点: 1. 可读性和维护性:将模板定义和实现放在一个文件中,可以更方便地查看和理解模板的完整实现。这样也更便于进行代码的维护和修改。 2. 编译效率:将模板定义和实现放在一个文件中可以避免分离编译带来的额外开销。编译器在编译时可以直接看到完整的模板定义,从而生成正确的实例化代码,提高编译效率。 3. 依赖管理:将模板定义和实现放在一个文件中可以简化依赖管理。当其他源文件需要使用该模板时,只需包含头文件即可,不需要额外处理源文件的依赖关系。 需要注意的是,模板的定义和实现通常都应该放在文件中,并通过`#include`语句引入到其他源文件中。这样可以确保模板的定义在使用之前可见,以便编译器进行模板实例化。 总而言之,C++模板的定义和实现可以写在同一个文件中,这样有助于代码的可读性、维护性和编译效率。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值