为什么模板声明定义不能分离?

前言

模板函数一般不能声明定义分开,但是普通函数函数就可以,为什么呢?那就要先介绍一下在软件开发过程中,从源代码到可执行程序,通常会经历预处理、编译、汇编、链接的这四个主要步骤。

过渡过程干了什么

  1. 预处理(Preprocessing)

    • 任务:预处理器的任务是处理源代码中的预处理指令,如宏定义#define)、文件包含#include)、条件编译#ifdef#ifndef#endif等)指令。它会删除所有注释,展开所有宏定义,处理条件编译指令,并插入包含文件的内容。
    • 生成文件:预处理后的文件通常以 .i 或 .ii 结尾(在C/C++中),这是预处理后的文本文件,仍然保持高级语言的形式。
  2. 编译(Compilation)

    • 任务:编译器将预处理后的源代码转换成汇编语言。这个过程包括词法分析、语法分析、语义分析、中间代码生成、代码优化等步骤。编译器检查源代码中的错误,如语法错误、类型错误等,并将源代码转换成汇编指令
    • 生成文件:编译阶段生成的文件通常称为目标文件(Object File),以 .s结尾。这个文件包含机器代码,但是它还不能直接执行(计算机无法识别),因为它可能包含未解析的符号引用。
  3. 汇编(Assembly)

    • 任务:汇编器将汇编语言转换成机器语言。它将汇编指令转换成对应的二进制代码,并处理与特定硬件平台相关的指令。
    • 生成文件:汇编阶段生成的是机器码,它通常被直接存储在目标文件中,所以这个步骤可能不会生成新的文件,而是更新之前编译阶段生成的 .o 或 .obj 文件
  4. 链接(Linking)

    • 任务:链接器将一个或多个目标文件以及所需的库文件组合成一个完整的可执行程序。这个过程包括地址和空间分配、符号决议、重定位等。链接器确保所有外部引用的函数和变量都有正确的地址,并解决不同目标文件之间的依赖关系。
    • 生成文件:链接阶段生成的最终文件是可执行文件,在Unix/Linux系统中通常以 .out 或 .exe 结尾,在Windows系统中通常以 .exe 结尾。

普通函数为什么支持分离定义

编译过程

  1. 编译时只看声明: 当编译器编译一个源文件(比如 main.cpp)时,它只需要知道函数的声明,不需要知道函数的实现。因此,编译器可以生成对 myFunction 的调用指令,而无需查看 my_function.cpp 中的实际代码。

链接过程

  1. 符号引用: 编译器在编译源文件时,会为函数调用生成一个符号引用(通常是函数名)。这个符号引用将被放在生成的目标文件(.o 或 .obj 文件)中。

  2. 符号定义: 链接器在链接阶段会查找这些符号引用对应的符号定义。如果 myFunction 的定义在 my_function.cpp 中,链接器会在链接时找到这个定义,并将调用指令与实际函数代码连接起来。

而正常函数编译阶段只需要看到声明;链接时,才回去寻找定义!这样可以大大提高编译效率!

为什么模板函数不支持分离

这主要与模板函数的编译、链接过程与普通函数的差异性有关。

  1. 实例化发生在编译时: 对于模板函数,实例化是在编译时(也就是头文件展开之后)进行的而不是在链接时。编译器需要模板函数的定义来生成特定类型的函数实例。

  2. 实例化的多样性: 由于模板可以针对任意类型进行实例化,编译器无法在编译时预先知道所有可能的实例化。因此,它不能像普通函数那样在链接时查找定义。

  3. 编译器优化: 编译器在编译时对模板函数进行实例化,允许它针对特定类型进行优化。如果模板函数的定义在 .cpp 文件中,编译器在编译时看不到这些定义,因此无法进行优化。

通俗的说

模板函数是一个模板,只有实例化时,才能得到函数的定义。假如我们将声明放在头文件,将定义放在源文件。这就会出现链接错误。

对于编译器而言,他需要在头文件展开之后(也就是编译阶段)需要找到模板函数的定义,然后才能在链接阶段进行绑定,完成程序。如果找不到模板函数的定义,就可能出现编译阶段的错误,但是最终还是体现在了链接部分。

解决方式

1.显式实例化

将模板函数在源文件中定义之后,需要在源文件额外进行模板函数的显式实例化。

template <typename T>
void swap(T& a, T& b) {
    T temp = a;
    a = b;
    b = temp;
}

// 显式实例化模板函数swap用于int类型
template void swap<int>(int& a, int& b);

编译器就只会为int类型的swap函数生成代码,而不会为其他类型生成。这可以减少编译时间和可执行文件的大小。

当我们调用swap函数时,如果我们只交换int类型的变量,编译器就会使用上面显式实例化的版本:

int x = 10, y = 20;
swap(x, y); // 使用显式实例化的swap<int>函数
显式实例化之后,意味着实例化了模板,这样就可以在编译阶段
确保了编译器在编译阶段生成特定类型的函数代码,从而使得链接器在链接阶段能够找到这些实例化的函数实现
显式实例化在编译阶段起到了模板函数定义的作用。在显式实例化的情况下,不需要模板函数的定义在编译时可见,因为编译器已经根据显式实例化指令(在另一个.cpp文件)创建了函数的实现。这与普通函数不同,普通函数需要在链接时找到其定义( 显式实例化之后,就相当于生成了一份模板函数定义的代码(对应着特定的类型),在链接阶段就可以找到定义)
方法2:定义在同一个头文件中
这样在头文件展开之后,可以直接将定义展开,就可以找到定义。通常这样的头文件被叫做.hpp或者.h

模板的优缺点

优点:

1.模板复用了代码,节省资源,更快的选代开发,C++的标准模板库(STL)因此而产生
2.增强了代码的灵活性

缺点:
1.模板会导致代码膨胀问题,也会导致编译时间变长(每种实例化都会产生额外的函数代码)
2.出现模板编译错误时,错误信息非常凌乱,不易定位错误
  • 9
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值