当前位置: 首页 > article >正文

内联函数和模版函数的常见错误、以及其为什么可以将其声明和定义在头文件实现并被多个源文件引用

内联函数和模版函数的常见错误、以及其为什么可以将其声明和定义在头文件实现并被多个源文件引用

    • 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;
}

连接错误:具体来说,当编译器编译一个包含模板函数调用的源文件时,它会根据需要对模板进行实例化,并生成相应的代码。如果模板函数的定义没有被包含在当前的编译单元中,那么编译器就无法生成相应的代码,从而导致链接错误。

在这里插入图片描述


http://www.kler.cn/a/1654.html

相关文章:

  • 微信小程序中使用 TypeScript 定义组件时,Component 函数确实需要多个类型参数
  • touch详讲
  • 如何打开/处理大型dat文件?二进制格式.dat文件如何打开?Python读取.dat文件
  • 资源分享:gpts、kaggle、paperswithcode
  • 测试用例颗粒度说明
  • 连接Milvus
  • 我在字节的这两年
  • 基于鲸鱼算法的极限学习机(ELM)分类算法-附代码
  • 艹,终于在8226上把灯点亮了
  • python学习——【第一弹】
  • vue模板语法
  • 盘点Python那些简单实用的第三方库
  • 基础入门 HTTP数据包Postman构造请求方法请求头修改状态码判断
  • 字符串函数的模拟实现
  • 手撕数据结构与算法——树(三指针描述一棵树)
  • 【C++初阶】4. Date类的实现
  • 【剑指offer】10~11.斐波那契数列(C# 实现)
  • AAAI顶会行人重识别算法源码解读——Relation Network for Person Re-identification
  • 百元降噪耳机推荐,适合学生党入手的四款降噪蓝牙耳机
  • AI风暴 :文心一言 VS GPT-4
  • C#大型HIS医院LIS管理系统源码
  • 【测试开发篇3】软件测试的常用概念
  • 【6】核心易中期刊推荐——图像与信号处理
  • Android开发自定义搜索框实现源码详解
  • 【Linux】基本指令介绍
  • Vue3(递归组件) + 原生Table 实现树结构复杂表格