内联函数和模版函数的常见错误、以及其为什么可以将其声明和定义在头文件实现并被多个源文件引用
内联函数和模版函数的常见错误、以及其为什么可以将其声明和定义在头文件实现并被多个源文件引用
- 1 编译器对内联函数的处理
- 1.1 编译阶段
- 1.2 汇编阶段
- 1.3 链接阶段(编译器不对一个内联函数进行内联展开时不出现重定义问题的原因)
- 2 内联函数的声明和定义
- 2.1 内联函数声明和定义分离的错误
- 2.1.1 内联展开的情况
- 2.1.2 内联未展开的情况
- 3 编译器对模版函数的处理
- 3.1 编译阶段
- 3.2 汇编阶段
- 3.3 链接阶段(编译器实例化模版函数不发生重定义问题的原因)
- 4 模版函数的声明和定义
- 4.1 声明和定义分离后的链接错误
1 编译器对内联函数的处理
所有的问题都得在了解了编译器在编译、汇编、链接阶段对内联函数的处理,才能引刃而解,因此我们先了解编译器在各个阶段对内联函数的处理。
1.1 编译阶段
在编译阶段,编译器会将inline函数的定义放入符号表中,并在需要使用该函数的地方直接展开其代码。同时,编译器还会对内联函数进行一些静态检查和优化,比如进行常量折叠、循环展开等操作。
1.2 汇编阶段
如果inline函数已经被展开,那么它的代码片段将被嵌入到调用点,否则编译器将插入一条跳转指令以调用该函数call(函数地址)
。
1.3 链接阶段(编译器不对一个内联函数进行内联展开时不出现重定义问题的原因)
在链接阶段,链接器将所有目标文件和库文件合并成一个可执行文件或共享库。如果inline函数没有被展开,那么它将被视为一个独立的函数,并在符号表中创建一个单独的函数符号(也就是编译器不对一个内联函数进行内联展开时不出现重定义错误的原因)。如果函数被展开了,那么它的代码将直接嵌入到调用点,并且不会创建单独的函数符号。
综上,inline 函数的处理是在编译、汇编和链接的不同阶段进行的,其中编译阶段是最核心的一个阶段,也是 inline 函数实现的关键。
2 内联函数的声明和定义
如果将内联函数像普通函数一样,声明和定义分离,可能会出现链接错误。
2.1 内联函数声明和定义分离的错误
2.1.1 内联展开的情况
声明和定义分离后:
//inline.h
#include <iostream>
inline int add(int a, int b);
//inline.cpp
#include "inline.h"
inline int add(int a, int b)
{
return a + b;
}
//test.cpp
#include "inline.h"
using std::cout;
using std::cin;
using std::endl;
int main()
{
int result = add(1, 2);
return 0;
}
报错:
由于函数被展开了,那么它的代码将直接嵌入到调用点,并且不会创建单独的函数符号,因此链接器就无法找到该函数的符号,从而报告链接错误。
2.1.2 内联未展开的情况
如果inline函数因为太长没有被展开,那么在链接时会生成一个独立的函数符号,并将其放入符号表中。如果存在其他包含该inline函数定义的源文件或库文件,那么在链接时可以正确地解析该函数的实现并将其链接到目标文件中,从而避免了链接错误。
但是,如果所有的源文件或库文件都没有包含该inline函数的定义,那么在链接时就会出现"未定义符号"的链接错误,导致程序无法正常运行。
3 编译器对模版函数的处理
C++ 中的模板函数在编译、汇编和链接时的处理与普通函数不同。由于模板函数是一种泛型编程技术,模板参数的具体值在编译期间才能确定。因此,模板函数需要进行两次编译:一次是模板定义的编译,另一次是模板实例化的编译。
3.1 编译阶段
对于模板函数的每个使用,编译器都会对其进行模板实例化,即根据传入的实参具体化出一个函数实例。这个过程就叫做 “模板实例化”(Template Instantiation)。编译器会将每个实例化后的函数大小确定下来,并生成相应的汇编代码。
3.2 汇编阶段
每个被实例化的函数都会被转换为相应的汇编代码,并与源文件中的其他汇编代码一起生成目标文件。
3.3 链接阶段(编译器实例化模版函数不发生重定义问题的原因)
当多个源文件包含相同的模板实例化时,编译器会分别为它们生成独立的实现,并将它们存储在各自的目标文件中。
跟未进行内联展开的内联函数类似:链接时,链接器会将这些目标文件合并成为一个可执行文件或库,并在符号表中保留每个实例化的唯一名称和地址信息,从而避免了符号重定义的问题。
4 模版函数的声明和定义
4.1 声明和定义分离后的链接错误
//template.h 声明
#include <iostream>
template<class T>
void swap(T& left, T& right);
//template.c 定义
#include "template.h"
template<class T>
void swap(T& left, T& right)
{
int tmp = left;
left = right;
right = tmp;
}
//test.c 调用
#include "template.h"
using std::cout;
using std::cin;
using std::endl;
int main()
{
int a = 0;
int b = 10;
swap(a, b);
cout << a << b << endl;
return 0;
}
连接错误:具体来说,当编译器编译一个包含模板函数调用的源文件时,它会根据需要对模板进行实例化,并生成相应的代码。如果模板函数的定义没有被包含在当前的编译单元中,那么编译器就无法生成相应的代码,从而导致链接错误。