所有的问题都得在了解了编译器在编译、汇编、链接阶段对内联函数的处理,才能引刃而解,因此我们先了解编译器在各个阶段对内联函数的处理。
编译器会将 inline 函数体嵌入到函数调用处,以避免函数调用的开销。这个过程就叫做 “内联展开”(Inline Expansion)。如果编译器决定不对一个函数进行内联展开,那么该函数就会被当做普通函数来处理,就好像没有加上 inline 一样。
已经进行了内联展开的 inline 函数体将被转换为相应的汇编代码,并与源文件中的其他汇编代码一起生成目标文件。
在链接阶段,由于 inline 函数的定义通常是写在头文件中的,如果编译器在编译阶段对函数进行内联展开,就不会形成符号表;如果编译器决定不对一个函数进行内联展开,多个源文件可能包含同一个 inline 函数的实现。为了避免重复定义的问题,在链接阶段必须将这些实现合并成为一个函数。如果出现多个 inline 函数的实现无法合并的情况,链接器就会报重复定义错误。
综上,inline 函数的处理是在编译、汇编和链接的不同阶段进行的,其中编译阶段是最核心的一个阶段,也是 inline 函数实现的关键。
内联函数建议声明和定义都在头文件中实现,否则会出现链接错误
声明和定义分离后:
//inline.h
#include
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;
}
报错:
当我们将内联函数的定义放在源文件中时,编译器会将其视为普通函数,并根据需要在目标文件中生成相应的符号。但是如果某个源文件需要调用该内联函数,但它没有包含该函数的实现,则链接器就无法找到该函数的符号,从而报告链接错误。
//inline.h
#include
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;
inline int add(int a, int b)
{return a - b;
}
int main()
{int result = add(1, 2);cout << result << endl;return 0;
}
在链接期间,由于存在两个不同的 add 函数实现,链接器无法确定要选择哪个实现,从而报告符号重复定义的错误。
在某些编译器(如vs2019)下,我们发现其并没有报错:
需要注意的是,尽管某些编译器可能支持这种行为,但在 C++ 标准中,多个源文件中包含相同名称但实现不同的内联函数是不被允许的,并且可能会导致不可预测的行为和不可移植性问题。因此,我们应该遵循 C++ 标准并避免在多个源文件中定义相同名称但实现不同的内联函数。
C++ 中的模板函数在编译、汇编和链接时的处理与普通函数不同。由于模板函数是一种泛型编程技术,模板参数的具体值在编译期间才能确定。因此,模板函数需要进行两次编译:一次是模板定义的编译,另一次是模板实例化的编译。
对于模板函数的每个使用,编译器都会对其进行模板实例化,即根据传入的实参具体化出一个函数实例。这个过程就叫做 “模板实例化”(Template Instantiation)。编译器会将每个实例化后的函数大小确定下来,并生成相应的汇编代码。
每个被实例化的函数都会被转换为相应的汇编代码,并与源文件中的其他汇编代码一起生成目标文件。
当多个源文件包含相同的模板实例化时,编译器会分别为它们生成独立的实现,并将它们存储在各自的目标文件中。
跟未进行内联展开的内联函数类似:链接时,链接器会将这些目标文件合并成为一个可执行文件或库,并在符号表中保留每个实例化的唯一名称和地址信息,从而避免了符号重定义的问题。
//template.h 声明
#include
template
void swap(T& left, T& right);//template.c 定义
#include "template.h"
template
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;
}
连接错误:具体来说,当编译器编译一个包含模板函数调用的源文件时,它会根据需要对模板进行实例化,并生成相应的代码。如果模板函数的定义没有被包含在当前的编译单元中,那么编译器就无法生成相应的代码,从而导致链接错误。
那么如果在test.cpp
文件中也写下一个函数定义呢?
这里我选择在test.cpp
中故意写一个不同的函数实现
//template.h
#include
template
void swap(T& left, T& right);//template.cpp
#include "template.h"
template
void swap(T& left, T& right)
{int tmp = left;left = right;right = tmp;
}//test.cpp
#include "template.h"
using std::cout;
using std::cin;
using std::endl;template
void swap(T& left, T& right)
{left = 0;right = 0;
}
int main()
{int a = 0;int b = 10;swap(a, b);cout << a << ' ' << b << endl;return 0;
}
上述情况下:在链接期间,由于存在两个不同实现的 swap 函数模板,链接器无法确定要选择哪个实现,从而报告符号重复定义的错误。
但是在某些编译器(如vs2019)下,可能这种写法也不会报错:
需要注意的是,尽管一些编译器可能支持这种行为,但在 C++ 标准中,多个源文件中包含相同名称但实现不同的模板函数仍然是未定义的行为,并且可能导致不可预测的结果和不可移植性问题。因此,我们应该遵循 C++ 标准并避免在多个源文件中定义相同名称但实现不同的模板函数。