【C++】模板声明与定义不分离

2023-05-16

一般在写C++相关代码的时候,我们总习惯于将类声明和类实现进行分离。也就是说,类的声明一般写在.h文件中,而它的实现一般写在.cpp文件中。但是,在模板类中,这个习惯却要恰恰相反。即:要求模板类的类声明和类实现要都放在头文件,而不能分离

本文就对模板的这个奇特习惯进行分析。


分离式编译模式

在进行模板特性的讲解之前,首先需要了解一下C++的分离式编译模式

所谓分离编译模式,就是指:一个程序或者项目由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件连接起来形成单一的可执行文件的过程

C/C++组织源代码和生成可执行文件的方式就是分离式编译模式。

简单粗暴的理解就是,一个C++项目分为若干个cpp文件和h文件,每个cpp文件单独编译成每个的目标文件,最终将每个cpp文件连接在一起组成最后的单一的可执行文件。这里最重要的点就是:编译是相对于每个cpp文件而言的

接下去的问题就是,对于编译每个cpp文件的时候,是否都需要每个类的实现?

如果都需要每个类的实现,那么就只能将每个类的实现也都写到h文件中,这样在cpp文件中引入的h文件中,才会有每个类的实现;如果不需要每个类的实现,那么就没有必要将每个类的实现写到h文件中。

C/C++所采用的方法是:只要给出类的声明,就可以在本源文件中使用该类。由于每个源文件都是独立的编译单元,在当前源文件中使用但未在此类的实现,就假设在其他的源文件中实现好了。


模板声明与定义

声明定义不分离

但是,分离式编译模式却驯不服模板。

C++标准要求编译器在实例化模板时,必须在上下文中可以查看到其实现;而反过来,在看到实例化模板之前,编译器对模板的实现是不处理的。原因很简单,编译器怎么会预先知道typename实参是什么呢?因此模板的实例化与实现必须放到同一文件中。

《C++编程思想》说明了原因:

模板定义很特殊。由template<…> 处理的任何东西都意味着编译器在当时不为它分配存储空间,它一直处于等待状态直到被一个模板实例告知。在编译器和连接器的某一处,有一机制能去掉指定模板的多重定义。所以为了容易使用,几乎总是在头文件中放置全部的模板声明和定义。

简单来说:只有模板实例化时,编译器才会得知T实参是什么。而编译器在处理模板实例化时,不仅仅要看到模板的定义式,还需要模版的实现体

为什么需要这样呢?

比如说存在类Rect, 其类定义式写在test.h,类的实现体写在test.cpp中。对于模板来说,编译器在处理test.cpp文件时,编译器无法预知T的实参是什么,所以编译器对其实现是不作处理的。

紧接着在main.cpp中用到了Rect,这个时候会实例化。也就是说,在test.h中会实例出对Rect进行类的声明。但是,由于分离式编译模式,在编译的时候只需要类的声明即可,因此编译是没有任何问题的

但是在链接的过程中,需要找到Rect的实现部分。但是上面也说了,编译是相对于每个cpp文件而言的。在test.cpp的编译的时候,由于不知道T的实参是什么,并没有对其进行处理。因此,Rect的实现自然并没有被编译,链接也就自然而然地因找不到而出错

也就是说,模板如果将类声明和类实现进行分离,那么分离式编译模式会导致在链接的时候出现问题。

例子

解释清楚了,接下来可以看一个例子:

在test.h文件中,定义模板类Rect:

#include <iostream>

template<typename T>
class Rect {
  public:
    Rect(T l = 0.0f, T t = 0.0f, T r = 0.0f, T b = 0.0f) :
      left_(l), top_(t), right_(r), bottom_(b) {}

    void display();

    T left_;
    T top_;
    T right_;
    T bottom_;
};

在test.cpp文件中,定义模板类Rect方法的实现:

#include "test.h"

template<typename T>
void Rect<T>::display() {
  std::cout << left_ << " " << top_ << " " << right_
    << " " << bottom_ << std::endl;
}

最终在main.cpp文件中,使用改模板类:

#include <iostream>
#include "test.h"

int main() {
  Rect<float> rect(1.1f, 2.2f, 3.3f, 4.4f);
  rect.display();

  return 0;
}

对这三个文件进行编译,最终会报错,报错的内容为:

yngzmiao@yngzmiao-virtual-machine:~/test/build$ cmake .. && make
-- The C compiler identification is GNU 4.8.4
-- The CXX compiler identification is GNU 4.8.4
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/yngzmiao/test/build
Scanning dependencies of target test
[ 25%] Building CXX object CMakeFiles/test.dir/test.cpp.o
[ 50%] Linking CXX static library libtest.a
[ 50%] Built target test
Scanning dependencies of target main
[ 75%] Building CXX object CMakeFiles/main.dir/main.cpp.o
[100%] Linking CXX executable main
CMakeFiles/main.dir/main.cpp.o:在函数‘main’中:
main.cpp:(.text+0x3c):对‘Rect<float>::display()’未定义的引用
collect2: error: ld returned 1 exit status
make[2]: *** [main] 错误 1
make[1]: *** [CMakeFiles/main.dir/all] 错误 2
make: *** [all] 错误 2

可以看出,改代码在ld的过程中出现了错误,即链接的时候没有找到实现而出错。

如果将模板类的声明和实现不分离,都写在.h文件中。即如下:

#include <iostream>

template<typename T>
class Rect {
  public:
    Rect(T l = 0.0f, T t = 0.0f, T r = 0.0f, T b = 0.0f) :
      left_(l), top_(t), right_(r), bottom_(b) {}

    void display() {
      std::cout << left_ << " " << top_ << " " << right_
        << " " << bottom_ << std::endl;
    }

    T left_;
    T top_;
    T right_;
    T bottom_;
};

最终编译运行没有问题。

总结

在分离式编译的环境下,编译器编译某一个cpp文件时并不知道另一个cpp文件的存在,也不会去查找(当遇到未决符号时它会寄希望于链接器)。

这种模式在没有模板的情况下运行良好,但遇到模板时就傻眼了,因为模板仅在需要的时候才会实例化出来。所以,当编译器只看到模板的声明时,它不能实例化该模板,只能创建一个具有外部链接的符号并期待链接器能够将符号的地址决议出来。

然而当实现该模板的cpp文件中没有用到模板的实例时,编译器懒得去实例化,所以,整个工程中就找不到一行模板实例的二进制代码,于是链接器也黔驴技穷了。


相关阅读

  • c++ 模板类 声明和定义都放在.h文件的原因
  • C++为什么要求把类声明和类实现进行分离?又为什么要求模板类的类声明和类实现要都放在头文件而不能分离?
  • 为什么模板函数应该定义在头文件内
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【C++】模板声明与定义不分离 的相关文章

随机推荐

  • kingbase迁移-bit字符串的长度(2)与bit类型(1)不匹配.

    今天做mysql转kingbase8 迁移完成后 xff0c 出现报错如下 Caused by com kingbase8 util KSQLException 错误 bit字符串的长度 2 与bit类型 1 不匹配 Where COPY
  • idea maven打包编译报错 java.lang.AssertionError: input.getType

    今天使用idea打包编译maven项目 xff0c 出现如下报错 构建报错时 xff0c 最先显示的是这个报错 查了一圈下来 xff0c 我的配置是没有问题的 Failed to execute goal org apache maven
  • gitee仓库人员上限如何一键删人

    我碰到情况是 xff0c 自己的gitee码云 xff0c 新建了很多仓库 xff0c 拉了谁都不知道 今天做了一个项目 xff0c 新建了仓库 xff0c 发了邀请链接 xff0c 想拉一下研发进仓库 xff0c 结果就提示人员上限了 我
  • docker启动pg出现报错Connection matched pg_hba.conf line 89: “local all postgres peer“

    今天在公司使用docker启动pg xff0c 查看日志出现以下报错 provided user name postgres and authenticated user name root do not match Peer authen
  • Deep Learning 最优化方法之Adam

    本文是Deep Learning 之 最优化方法系列文章的Adam方法 主要参考Deep Learning 一书 整个优化系列文章列表 xff1a Deep Learning 之 最优化方法 Deep Learning 最优化方法之SGD
  • 【CMake】CMakeLists.txt的超傻瓜手把手教程(附实例源码)

    新手写CMakeLists txt简直就是实力劝退 xff0c 各种命令让很多人头大 xff0c 如何写一个最基础的CMakeLists txt呢 xff1f 本文从一个实例出发 xff0c 教你编写的基本流程 CMakeLists txt
  • 【Gradle】Groovy的语法详解(下篇)

    上文介绍了Groovy的一般省略规则 xff0c 以及字符串 列表 Map的使用方式 xff0c 然而在Groovy中 xff0c 闭包是其中的一大杀器 xff0c 可以对代码的结构和逻辑带来的优化显而易见 本博文主要对Groovy闭包的使
  • 【OpenCV】OpenCV图像/视频的读取与写入(C++版)

    使用C 43 43 开发图像处理算法时 xff0c 最基础的就是利用OpenCV完成图像文件的输入 输出以及自动内存管理 重点 所以 xff0c 只要需要掌握一些简单的OpenCV的操作即可 本博文就对这些基础内容进行讲解 图像操作 图像读
  • 【OpenCV】OpenCV常用函数(C++版)

    俗话说 xff1a 好记性不如烂笔头 在使用OpenCV的过程中 xff0c 时常会用到很多函数 xff0c 而且往往可能会一时记不起这个函数的具体参数怎么设置 xff0c 故在此将常用函数做一汇总 图像缩放与放大 对图像的各项操作中 xf
  • 【Python】python曲线拟合

    python作为一款可以简单方便地进行科学计算的语言 xff0c 进行曲线拟合自然是必备的功能之一了 本文就如何进行曲线拟合进行讲解 本文需要进行拟合的数据为 xff1a x 61 np arange 1 31 1 y 61 np arra
  • 【C++】NULL和nullptr的关联与差别

    在写代码的过程中 xff0c 有时候需要将指针赋值为空指针 xff0c 以防止野指针 在C中 xff0c 都是使用NULL来实现的 xff1b 在C 43 43 中 xff0c 除了NULL之外 xff0c 还提供了nullptr来进行定义
  • 【C++】C++11可变参数模板(函数模板、类模板)

    在C 43 43 11之前 xff0c 类模板和函数模板只能含有固定数量的模板参数 C 43 43 11增强了模板功能 xff0c 允许模板定义中包含0到任意个模板参数 xff0c 这就是可变参数模板 可变参数模板的加入使得C 43 43
  • 【C++】C++11统一初始化(initializer_list<T>源码分析)

    C 43 43 11之前的初始化语法很乱 xff0c 有四种初始化方式 xff0c 而且每种之前甚至不能相互转换 让人有种剪不断 xff0c 理还乱的感觉 因此 xff0c C 43 43 11添加了统一初始化的方式 xff0c 本文将对统
  • 【C++】右值引用、移动语义、完美转发(上篇)

    在C 43 43 11 xff0c 引入了右值引用的概念 xff0c 在此基础上的移动语义在STL容器中使用非常广泛 简单来说 xff0c move语义使得你可以用廉价的move赋值替代昂贵的copy赋值 xff0c 完美转发使得可以将传来
  • 【C++】右值引用、移动语义、完美转发(下篇)

    上篇中 xff0c 主要讲解了右值引用和移动语义的具体定义和用法 在C 43 43 11中几乎所有的容器都实现了移动语义 xff0c 以方便性能优化 本文以C 43 43 11容器中的insert方法为例 xff0c 详细讲解在容器中移动语
  • AI==喜茶??

    2017年7月10日 xff0c 上海 xff0c 雨 刚从某CV方向的公司下班 xff0c 骑着小黄车朝着浦东某郊区租了一个月的床位行驶着 xff0c 雨打在脸上 xff0c 有点生疼 我不禁在思考 xff0c 这一切到底为了什么 xff
  • 【C++】unique_ptr独占型智能指针详解

    指针是C C 43 43 区别于其他语言的最强大的语法特性 xff0c 借助指针 xff0c C C 43 43 可以直接操纵内存内容 但是 xff0c 指针的引入也带来了一些使用上的困难 xff0c 这要求程序员自己必须手动地对分配申请的
  • 【C++】shared_ptr共享型智能指针详解

    指针是C C 43 43 区别于其他语言的最强大的语法特性 xff0c 借助指针 xff0c C C 43 43 可以直接操纵内存内容 但是 xff0c 指针的引入也带来了一些使用上的困难 xff0c 这要求程序员自己必须手动地对分配申请的
  • 【C++】weak_ptr弱引用智能指针详解

    weak ptr这个指针天生一副小弟的模样 xff0c 也是在C 43 43 11的时候引入的标准库 xff0c 它的出现完全是为了弥补它老大shared ptr天生有缺陷的问题 相比于上一代的智能指针auto ptr来说 xff0c 新进
  • 【C++】模板声明与定义不分离

    一般在写C 43 43 相关代码的时候 xff0c 我们总习惯于将类声明和类实现进行分离 也就是说 xff0c 类的声明一般写在 h文件中 xff0c 而它的实现一般写在 cpp文件中 但是 xff0c 在模板类中 xff0c 这个习惯却要