C++的内联函数

目录

前言

内联函数

为什么声明和定义分离

为什么声明和定义分离后不出错 

为什么内联函数不支持声明和定义分离 

为什么内联函数支持声明和定义不分离

坚持声明和定义不分离的解决方法

static修饰函数

inline修饰函数

结论

声明和定义不分离的应用场景


前言

        在C语言中可能会遇到频繁调用100次函数的情况,这就意味这需要频繁的创建和销毁栈帧一百次,无疑这会增加程序运行时的开销,对此C语言使用了宏来解决这一问题。但是宏是一个很容易犯错的概念,其中主要有以下几个方面:

  • 宏不是函数,宏函数不需要参数参数
#define ADD(a,b) ((a)+(b))  //right#define ADD(int a,int b) ((a)+(b)) //wrong
  • 宏定义的结尾不需要分号
#define ADD(a,b) ((a)+(b))  //right#define ADD(a,b) ((a)+(b));//wrong
  • 要用括号控制优先级(运算符的优先级问题)
#define ADD(a,b) ((a)+(b))  //right//不加括号导致出错:
ADD(x | y,x & y);  ——> (x | y + x & y),原本想要计算x|y与x&y之和,但是由于+的优先级高所以就变成了计算x | (y + x) & y

此外,宏也存在一些缺点:

  • 语法复杂,坑很多,不容易控制(上面说过了)
  • 不能调试
  • 没有类型检查

关于宏的具体内容请查看:C语言关于预处理命令的理解

内联函数

问题描述:由于宏在处理频繁调用函数的问题中可能会出现各种各样的问题,C++摒弃了宏而采用inline关键字来解决该问题

内联展开:当使用 inline 关键字修饰函数时,编译器会尝试在每个调用该函数的地方直接将函数的代码插入到调用位置,而不是像普通函数那样通过跳转到另一个内存地址执行完后再返回

展开详解:当编译器遇到一个被声明为 inline 的函数并进行调用时,在生成最终的可执行程序时,并不会像普通函数一样创建一个新的栈帧、跳转到对应代码段执行完后再返回。相反,编译器会尽可能地将整个被标记为 inline 的函数体复制粘贴至每次调用该 inline 函数处。

优点:有助于减少因频繁调用小型简单操作而产生的额外开销(如参数传递、栈帧创建等),从而提高程序运行效率

无inline关键字修饰函数:

#include <iostream>
using namespace std;int Add(int a, int b)
{return a + b;
}int main()
{int ret1 = Add(1, 2) * 3;int x = 1, y = 2;int ret2 = Add(x | y, x & y);return 0;
}

有inline关键字修饰函数:

#include <iostream>
using namespace std;inline int Add(int a, int b)
{return a + b;
}int main()
{int ret1 = Add(1, 2) * 3;int x = 1, y = 2;int ret2 = Add(x | y, x & y);return 0;
}

1和2分别放入寄存器,接着将寄存器会将 eax 中的值与 3 相乘,并将结果放回到 eax 寄存器

注意事项:

1、并非所有被声明为 inline 的函数都一定会被内联展开,在实际情况中编译器可能基于复杂性和其他因素做出选择性忽略某些内联请求:

  • 在release模式下,可以直接查看编译器生成的汇编代码中是否存在call Add
  • 在debug模式下,需要对编译器进行设置否则不会展开(debug模式下,编译器默认不会对代码进行优化)

2、内联是一种以空间(编译好的可执行程序的大小)换时间的做法,如果所有函数都是内联函数,这可能会使目标文件变大

        对于一个100行且需要调用1w次的func函数,如果采用内联的方法,可执行程序的代码增100*1w行,最终的可执行程序代码量剧增,代码的编译速度降低影响用户体验,如果不采用内联的办法,每次调用时都会跳转到该段代码运行,这就导致只有一份 func 函数代码存在于程序中,可执行程序实际上只会增加一百行的代码:

3、inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议是将函数规模较小(即函数不是很大)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性,《C++prime》第五版关于inline的建议如下:

4、内联 不建议声明和定义分离,否则链接错误(展开后没有函数地址 ,链接会找不到)

为什么声明和定义分离

        我们现在有一个Stack.h文件和Test.cpp文件和一个空的Stack.cpp文件,当我们直接在Stack.h中声明和定义一个函数,程序可以正常运行:

Stack.h文件

#pragma once
#include <iostream>
using namespace std;int Add(int a, int b)
{cout << "int Add(int a,int b)" << endl;return a + b;
}

Test.cpp文件

#include "Stack.h"int main()
{Add(1, 2);return 0;
}

但是当我们在空的Stack.cpp文件中也包含Stack.h文件,就会出现一个重定义的链接错误:

Stack.cpp文件

#include "Stack.h"

        这是因为Stack.cpp和Test.cpp两个文件都包含了Add函数,且在编译的阶段检查时不满足函数重载的条件(两个文件中的Add函数的返回值类型、参数类型、顺序一致),当两个.cpp文件经过编译、链接和汇编形成的.o文件进行链接时,链接器就会检测出函数重定义的错误

#pragma once解决的问题是一个源文件同时包含多个一样的头文件

#include "Stack.h"
#include "Stack.h" //wrongint main()
{int i = 0;return 0;
}

结论:如果将函数或变量的实现直接写在头文件中(声明和定义不分离),并且多个源文件包含了该头文件,在链接时就会出现多重定义错误

为什么声明和定义分离后不出错 

        采用函数声明与定义分离,即Stack.h文件中只有Add函数的声明,Stack.cpp文件中只有函数的定义,Test.cpp文件中只有调用函数,当程序进行编译时,.h文件会分别在两个.cpp文件中展开,Stack.cpp文件中既有函数的声明和定义,并且Add的函数名会被符号表统计,Test.cpp文件中只有函数的声明和调用,Test.cpp在链接前调用函数会生成类似call Add(?)的汇编指令(Test.cpp文件中没有函数的定义),所以链接器在链接阶段会去符号表依据函数名寻找该函数的地址,当找到该函数的地址后会将两个文件进行链接,此时汇编指令就变成了call Add(函数地址)

  1. 编译阶段

    • 头文件(Stack.h)被包含到两个源文件(Stack.cpp 和 Test.cpp)中
    • Stack.cpp 中包含了 Add 函数的实现部分
    • Test.cpp 中只包含了对 Add 函数的调用
  2. 链接阶段

    • 在编译过程中生成目标代码时,在 Test.cpp 文件里对 Add 函数进行调用时会生成类似 call Add(?) 的汇编指令。因为此时并没有找到该函数具体实现所在地址,所以使用一个占位符“?”来表示需要填充该地址。
  3. 符号解析和链接

    • 链接器会根据符号表查找具体实现位置,并将所有目标代码整合成可执行程序。
    • 链接器遇到 call Add(?) 这样的指令时,在符号表找到相应函数名,并替换为正确地址后完成最终连接

为什么内联函数不支持声明和定义分离 

原因:被inline修饰的函数在编译阶段不会被统计进符号表中(前提是明确规定该函数可以展开)

函数声明和定义分离:      

        如果将inline修饰的函数放入Stack.cpp文件中且包含.h文件,Stack.h文件中只有函数的声明,Test.cpp文件中只有函数的调用且包含.h文件,在编译后会出现如下情况:

为什么内联函数支持声明和定义不分离

原因:被inline修饰的函数在编译阶段不会被统计进符号表中(前提是明确规定该函数可以展开)

函数声明和定义未分离:        
        如果将inline修饰的函数放入Stack.h文件中,Stack.cpp文件只有包含.h文件的#include "Stack.h"一行代码,Test.cpp文件中有函数的调用且包含.h文件,在编译后会出现如下情况:

坚持声明和定义不分离的解决方法

static修饰函数

基本概念:使用static修饰的函数,则该函数只能被当前文件访问,无法被其他文件调用

底层原理:编译阶段被static修饰的函数不会被统计进符号表中(其它的用不到)

#pragma once
#include <iostream>
using namespace std;static int Add(int a, int b)
{cout << "int Add(int a,int b)" << endl;return a + b;
}

        当Stack.cpp文件只有#include ”Stack.h“,Test.cpp文件有函数调用和#include ”Stack.h“时,在编译后会产生如下情况:

inline修饰函数

不做解释

结论

  • 要么在写文件的时候直接选择函数声明和定义分离的方式
  • 如果实在想要用函数声明和定义不分离,那么大函数用static修饰,小函数用inline修饰

必须声明和定义不分离的应用场景

有时候声明和定义必须不分离的情况可能包括以下几点:

  1. 模板函数或类对于模板函数或类,通常需要将其声明和定义放在同一个文件中。因为编译器需要在实例化时能够看到完整的函数或类定义,以便生成相应的代码

  2. inline 函数:为处理频繁调用小型函数而导致的开销问题,而采用inline修饰函数时

  3. 静态成员变量:对于某些静态成员变量,在 C++ 中如果希望确保只有一个实例存在,则通常会将其声明和定义结合起来,并且还可能需要特殊处理以确保正确初始化

  4. 特定平台下链接问题:一些特定平台或库要求某些符号必须在同一源文件中进行声明和定义,并且不能被其他模块引用。这种情况下也需要将声明与实现结合起来

~over~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/505816.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

二次元风格地址发布页源码

二次元风格地址发布页源码&#xff0c;源码由HTMLCSSJS组成&#xff0c;记事本打开源码文件可以进行内容文字之类的修改&#xff0c;双击html文件可以本地运行效果&#xff0c;也可以上传到服务器里面&#xff0c;重定向这个界面 下载地址 https://www.qqmu.com/2347.html

C3_W2_Collaborative_RecSys_Assignment_吴恩达_中英_Pytorch

Practice lab: Collaborative Filtering Recommender Systems(实践实验室:协同过滤推荐系统) In this exercise, you will implement collaborative filtering to build a recommender system for movies. 在本次实验中&#xff0c;你将实现协同过滤来构建一个电影推荐系统。 …

在什么时候企业档案才会发生调整

档案在企业中通常会调整在以下几个时刻&#xff1a; 1. 入职时&#xff1a;员工入职时&#xff0c;企业会要求员工提供个人档案&#xff0c;包括身份证件、学历证明、工作经历等相关文件。 2. 离职时&#xff1a;员工离职时&#xff0c;企业会整理员工的离职档案&#xff0c;包…

Spring重点记录

文章目录 1.Spring的组成2.Spring优点3.IOC理论推导4.IOC本质5.IOC实现&#xff1a;xml或者注解或者自动装配&#xff08;零配置&#xff09;。6.hellospring6.1beans.xml的结构为&#xff1a;6.2.Spring容器6.3对象的创建和控制反转 7.IOC创建对象方式7.1以有参构造的方式创建…

SpringCloud-MQ消息队列

一、消息队列介绍 MQ (MessageQueue) &#xff0c;中文是消息队列&#xff0c;字面来看就是存放消息的队列。也就是事件驱动架构中的Broker。消息队列是一种基于生产者-消费者模型的通信方式&#xff0c;通过在消息队列中存放和传递消息&#xff0c;实现了不同组件、服务或系统…

鸿蒙Harmony应用开发—ArkTS声明式开发(通用属性:尺寸设置)

用于设置组件的宽高、边距。 说明&#xff1a; 从API Version 7开始支持。后续版本如有新增内容&#xff0c;则采用上角标单独标记该内容的起始版本。 width width(value: Length) 设置组件自身的宽度&#xff0c;缺省时使用元素自身内容需要的宽度。若子组件的宽大于父组件的…

电子电气架构——汽车以太网诊断路由汇总

电子电气架构——汽车以太网诊断路由汇总 我是穿拖鞋的汉子,魔都中坚持长期主义的工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 人们会在生活中不断攻击你。他们的主要武器是向你灌输对自己的怀疑:你的价值、你的能力、你的潜力。他们往往会将…

代码随想录刷题笔记 DAY 38 | 不同路径 No.62 | 不同路径II No.63

文章目录 Day 3801. 不同路径&#xff08;No. 62&#xff09;<1> 题目<2> 笔记<3> 代码 02. 不同路径&#xff08;No. 63&#xff09;<1> 题目<2> 笔记<3> 代码 Day 38 01. 不同路径&#xff08;No. 62&#xff09; 题目链接 代码随想录…

Redis安全加固策略:服务账号管理 开启redis密码认证 开启防护模式

Redis安全加固策略&#xff1a;服务账号管理 & 开启redis密码认证 & 开启防护模式 1.1 服务账号管理1.1.1 检测方法1.1.2 加固参考配置操作 1.2 开启redis密码认证1.2.1 检测方法1.2.2 加固参考配置操作 1.3 开启防护模式1.3.1 检测方法1.3.2 加固参考配置操作 &#x…

Ubuntu进入python时报错:找不到命令 “python”,“python3” 命令来自 Debian 软件包 python3

一、错误描述 二、解决办法 进入”/usr/bin”目录下&#xff0c;查看/usr/bin目录中所有与python相关的文件和链接&#xff1a; cd /usr/bin ls -l | grep python 可以看到Python3指向的是Python3.10&#xff0c;而并无指向python3的软连接 只需要在python与python3之间手动…

【C++提高编程】

C提高编程 C提高编程1 模板1.1 模板的概念1.2 函数模板1.2.1 函数模板语法1.2.2 函数模板注意事项1.2.3 函数模板案例1.2.4 普通函数与函数模板的区别1.2.5 普通函数与函数模板的调用规则1.2.6 模板的局限性 1.3 类模板1.3.1 类模板语法1.3.2 类模板与函数模板区别1.3.3 类模板…

信息检索(七):Transformer Memory as a Differentiable Search Index

Transformer Memory as a Differentiable Search Index 摘要1. 引言2. 相关工作3. 可微搜索索引3.1 索引策略3.1.1 索引方法3.1.2 文档表示策略 3.2 用于检索的 Docids 表示3.3 训练和优化 4. 实验4.1 基线4.2 实验结果 5. 结论参考资料 原文链接&#xff1a;https://proceedin…