内联函数(inline)总结

article/2025/9/17 3:49:35

1:定义: 
     它们看起来象函数,运作起来象函数,比宏(macro)要好得多,使用时还不需要承担函数调用的开销。当内联一个函数时,编译器可以对函数体执行特定环境下的优化工作。这样的优化对"正常"的函数调用是不可能的。

2:规则: 
     inline关键字必须和函数体定义放在一起才可以实现内联,仅仅将inline放在函数声明之前不起任何作用。inline是一个用于实现的关键字而不是一个用于声明的关键字。对于类方法,定义在类体内部的方法自动成为内联方法。

3:实现思想: 
      内联函数的基本思想在于将每个函数调用以它的代码体来替换,很可能会增加整个目标代码的体积过分地使用内联所产生的程序会因为有太大的体积而导致可用空间不够。即使可以使用虚拟内存,内联造成的代码膨胀也可能会导致不合理的页面调度行为(系统颠簸),这将使你的程序运行慢得象在爬,过多的内联还会降低指令高速缓存的命中率,从而使取指令的速度降低,因为从主存取指令当然比从缓存要慢。另一方面,如果内联函数体非常短,编译器为这个函数体生成的代码就会真的比为函数调用生成的代码要小许多。如果是这种情况,内联这个函数将会确实带来更小的目标代码和更高的缓存命中率! 
      inline指令就象register,它只是对编译器的一种提示,而不是命令。也就是说,只要编译器愿意,它就可以随意地忽略掉你的指令,事实上编译器常常会这么做。例如,大多数编译器拒绝内联"复杂"的函数(例如,包含循环和递归的函数);还有,即使是最简单的虚函数调用,编译器的内联处理程序对它也爱莫能助。(这一点也不奇怪。virtual的意思是"等到运行时再决定调用哪个函数",inline的意思是"在编译期间将调用之处用被调函数来代替",如果编译器甚至还不知道哪个函数将被调用,当然就不能责怪它拒绝生成内联调用了)。

4: 问题及对应的解决规则    

     假设写了某个函数f并声明为inline,如果出于什么原因,编译器决定不对它内联,那将会发生些什么呢?最明显的一个回答是将f作为一个非内联函数来处理:为f生成代码时就象它是一个普通的"外联"函数一样, 对f的调用也象对普通函数调用那样进行。

     理论上来说确实应该这样发生,但理论和现实往往会偏离,现在就属于这种情况。因为,这个方案对解决"被外联的内联"(outlined inline)这一问题确实非常理想,但它加入到C++标准中的时间相对较晚。较早的C++规范告诉编译器制造商去实现的是另外不同的行为,而且这一旧的行为在现在的编译器中还很普遍,所以必须理解它是怎么一回事。

      以上可以归结为:一个给定的内联函数是否真的被内联取决于所用的编译器的具体实现。幸运的是,大多数编译器都可以设置诊断级,当声明为内联的函数实际上没有被内联时,编译器就会为你发出警告信息。

      内联函数的定义实际上都是放在头文件中。这使得多个要编译的单元(源文件)可以包含同一个头文件,共享头文件内定义的内联函数所带来的益处。

复制代码
      // 文件example.h 
inline void f() { ... } // f的定义
...
// 文件source1.cpp
#include "example.h" // 包含f的定义
... // 包含对f的调用
// 文件source2.cpp
#include "example.h" // 也包含f的定义
... // 也调用f
复制代码

问题: 
假设现在采用旧的"被外联的内联"规则,而且假设f没有被内联,那么,当source1.cpp被编译时,生成的目标文件中将包含一个称为f的函数,就象f 没有被声明为inline一样。 
同样地,当source2.cpp被编译时,产生的目标文件也将包含一个称为f的函数。当想把两个目标文件链接在一起时,编译器会因为程序中有两个f的定义而报错。在两个cpp文件编译生成的.obj文件中都存在"被外联的内联"方法f。

为了防止这一问题,

旧规则规定: 
对于未被内联的内联函数,编译器把它当成被声明为static 那样处理,即,使它局限于当前被编译的文件。 
具体到刚才看到的例子中,遵循旧规则的编译器处理source1.cpp中的f时,就象f在 source1.cpp中是静态的一样;处理source2.cpp中的f时,也把它当成在source2.cpp中是静态的一样。 
这一策略消除了链接时的错误,但带来了开销:每个包含f的定义(以及调用f)的被编译单元都包含自己的f的静态拷贝。 
如果f自身定义了局部静态变量,那么,每个f的拷贝都有此局部变量的一份拷贝,这必然会让程序员大吃一惊,因为一般来说,函数中的"static"意味着"只有一份拷贝"。

新规则: 
将f作为一个非内联函数来处理:为f生成代码时就象它是一个普通的"外联"函数一样, 对f的调用也象对普通函数调用那样进行。

无论新规则还是旧规则,如果内联函数没被内联,每个调用内联函数的地方还是得承担函数调用的开销;如果是旧规则,还得忍受代码体积的增加,因为每个包含(或调用) f的被编译单元都有一份f的代码及其静态变量的拷贝!(更糟糕的是,每个f的拷贝以及每个f的静态变量的拷贝往往处于不同的虚拟内存页面,所以两个对f的不同拷贝进行调用有可能导致多个页面错误。)

更多问题: 
有时编译器即使很想内联一个函数,却不得不为这个内联函数生成一个函数体。特别是,如果程序中要取一个内联函数的地址,编译器就必须为此生成一个函数体。编译器怎么能产生一个指向不存在的函数的指针呢?

复制代码
inline void f() {...}            // 同上 
void (*pf)() = f; // pf指向f
int main() {
f(); // 对f的内联调用
pf(); // 通过pf对f的非内联调用
...
}
复制代码

这种情况似乎很荒谬:f的调用被内联了,

旧的规则: 
每个取f地址的被编译单元还是各自生成了此函数的静态拷贝。 
新规则下: 
不管涉及的被编译单元有多少,将只生成唯一一个f的外部拷贝.

即使你从来不使用函数指针,这类"没被内联的内联函数"也会找上你的门,因为不只是程序员会使用函数指针,有时编译器也这么做。特别是,编译器有时会生成构造函数和析构函数的外部拷贝,这样就可以通过得到那些函数的指针,方便地构造和析构类的对象数组。

实际上,随便一个测试就可以证明构造函数和析构函数常常不适合内联;甚至,情况比测试结果还糟。例如,看下面这个类Derived的构造函数:

复制代码
class Base { 
public:
...
private:
string bm1, bm2; // 基类成员
};
class Derived: public Base {
public:
Derived() {} // Derived的构造函数是空的,但,真的是空的吗?
private:
string dm1, dm2, dm3; // 派生类成员-3
};
复制代码

这个构造函数看起来的确象个内联的好材料,因为它没有代码。但外表常常欺骗人!仅仅因为它没有代码并不能说明它真的不含代码。实际上,它含有相当多的代码。

C++ 就对象创建和销毁时发生的事件有多方面的规定。当使用new时,动态创建的对象将自动地被它们的构造函数初始化,当使用 delete时析构函数怎样被调用。当创建一个对象时,对象的每个基类以及对象的每个数据成员会被自动地创建;当对象被销毁时,会自动地执行相反的过程(即析构)。

C++规定了哪些必须发生,但没规定"怎么"发生。"怎么发生"取决于编译器的实现者,但要弄清楚的是,这些事件不是凭空自己发生的。程序中必然有什么代码使得它们发生,特别是那些由编译器的实现者写的、在编译其间插入到你的程序中的代码,必然也藏身于某个地方。有时,它们就藏身于你的构造函数和析构函数。

所以,对于上面那个号称为空的Derived的构造函数,有些编译器会为它产生相当于下面的代码:

复制代码
// 一个Derived构造函数的可能的实现 
Derived:erived() {
// 如果在堆上创建对象,为其分配堆内存;
if (本对象在堆上)
this = :perator new(sizeof(Derived));
Base::Base(); // 初始化Base部分
dm1.string(); // 构造dm1
dm2.string(); // 构造dm2
dm3.string(); // 构造dm3
}
复制代码

      调用operator new(如果需要的话)的代码、构造基类部分的代码、构造数据成员的代码都会神不知鬼不觉地添加到你的构造函数中,从而增加构造函数的体积,使得构造函数不再适合内联。当然,同样的分析也适用于Base的构造函数,如果Base的构造函数被内联,添加到它里面的所有代码也会被添加到Derived的构造函数(Derived的构造函数会调用Base的构造函数)。如果string的构造函数恰巧也被内联,Derived的构造函数将得到其代码的5个拷贝,每个拷贝对应于Derived对象中5个string中的一个(2个继承而来,3个自己声明)。现在你应该明白,内联Derived的构造函数并非可以很简单就决定的!当然,类似的情况也适用于Derived的析构函数,无论如何都要清楚这一点:被Derived的构造函数初始化的所有对象都要被完全销毁。刚被销毁的对象以前可能占用了动态分配的内存,那么这些内存还需要释放。

5: 内联函数的使用规则 

      程序库的设计者必须预先估计到声明内联函数带来的负面影响:想对程序库中的内联函数进行二进制代码升级是不可能的。换句话说,如果f是库中的一个内联函数,用户会将f的函数体编译到自己的程序中。如果程序库的设计者后来要修改f,所有使用f的用户程序必须重新编译。相反,如果f是非内联函数,对f的修改仅需要用户重新链接,这就比需要重新编译大大减轻了负担;如果包含这个函数的程序库是被动态链接的,程序库的修改对用户来说完全是透明的。

      内联函数中的静态对象常常表现出违反直觉的行为。所以,如果函数中包含静态对象,通常要避免将它声明为内联函数

      一般来说,实际编程时最初的原则是不要内联任何函数,除非函数确实很小很简单,象下面这个age函数:

复制代码
      class Person { 
public:
int age() const { return personAge; }
...
private:
int personAge;
...
};
复制代码

      慎重地使用内联,不但给了调试器更多发挥作用的机会,还将内联的作用定位到了正确的位置:它是一个根据需要而使用的优化工具。不要忘了从无数经验得到的,一个程序往往花80%的时间来执行程序中20%的代码。这是一条很重要的定律,因为它提醒你,作为程序员的一个很重要的目标,就是找出这20%能够真正提高整个程序性能的代码。你可以选择内联你的函数,或者没必要就不内联,但这些选择只有作用在"正确"的函数上才有意义。 一旦找出了程序中那些重要的函数,以及那些内联后可以确实提高程序性能的函数(这些函数本身依赖于所在系统的体系结构),就要毫不犹豫地声明为inline。同时,要注意代码膨胀带来的问题,看看是否有内联函数没有被编译器内联。


http://chatgpt.dhexx.cn/article/eYRtcggv.shtml

相关文章

【C++】内联函数理解

内联函数 内联函数的使用是对于C语言中宏函数的一种改进,他继承了宏的优点并避免了宏的缺点。 宏的优点:a. 代码复用性高 b. 宏函数减少栈帧建立,提高效率 宏的缺点:a. 可读性差 b. 没有类型安全检查 c. 不方便调试 C基本不再建议…

在什么情况下方法调用会被内联?

写在前面 本文隶属于专栏《100个问题搞定Java虚拟机》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢! 本专栏目录结构和文献引用请见100个问题搞定Java虚拟机 解答 方法内联有许多规则。…

【C++】 内联函数详解(搞清内联的本质及用法)

目录 一.什么是内联函数 1.直观上定义: 2.更深入的思考: 二.为什么使用内联函数 1.为什么要代替部分宏定义 2.普通函数频繁调用的过程消耗栈空间 3.更深入的思考 三.内联函数和编译过程的相爱相杀 四.内联函数怎么用,在哪儿用&#…

[C++] 内联函数inline 以及 auto关键字 -- C++入门(4)

本篇文章主要包括内联函数和auto关键字。其中,内敛函数包括概念,特性等;auto关键字的使用规则,使用场景等。 目录 1.内敛函数 1.1问题引入: 1.2内联函数的概念 1.3内敛函数的特性 2.auto关键字 2.1auto简介 2.2 auto的…

Windows误删注册表恢复方法

昨天不小心把注册表给删了,期间一直找解决方法,因为没有usb等重装工具... 我把注册表的HKEY_LOCAL_MACHINE\software这个重要的东西给误删了 ---结果就是软件打不开.就连删除东西都没用,自带的cmd什么的系统工具打不开... 关机重启问题更严重了,直接蓝屏,但是还好,开机的时候…

Linux 误删文件恢复命令及方法

你知道的越多,不知道的就越多,业余的像一棵小草! 你来,我们一起精进!你不来,我和你的竞争对手一起精进! 编辑:业余草 http://r6d.cn/JzNf 推荐:https://www.xttblog.com/…

win10误删的注册表能还原吗_win10自带注册表恢复方法 win10注册表误删如何修复...

系统注册表,是计算机中一个很重要的部件,没有人可以保证自己能记住所有的注册表项,如果不小心让注册表被修改或者其他原因让注册表失效的时候,我们就可以通过cmd对注册表进行恢复,下面小编就为大家介绍win10自带注册表…

成功恢复 Linux 系统中已删除的文件

点击关注公众号,回复“1024”获取2TB学习资源! 当用户意外地删除了一个仍然需要的文件时,大多数情况下,是没有简便的方法可以重新找回或重建这个文件。不过,幸运的是文件是可以通过一些方法恢复的。当用户删除了一个文…

windows注册表操作——备份,还原注册表,清除注册表卸载残留信息

注册表包含在操作过程中Windows不断引用的信息,例如每个用户的配置文件、计算机上安装的应用程序以及每个用户可以创建的文档类型、文件夹和应用程序图标的 属性表 设置、系统上存在的硬件以及所使用的端口。 序言 注册表是Microsoft Windows中的一个重要的数据库…

Linux下达梦误删除文件后恢复步骤

目录 数据文件恢复1.测试环境准备2.删除数据文件3.恢复步骤4.使用限制5.官方资料 redo日志恢复1. 删除redo日志2.启动数据库3.查看建库参数4.初始化新实例5.拷贝redo文件6.修改db_magic值7.启动数据库8.错误示范9.迁移数据 官方社区 数据文件恢复 1.测试环境准备 1.1准备好测…

windows注册表恢复方法

如果可以进入安全模式,您可以在安全模式内调用命令提示符输入命令修复一下系统组件。 在管理员命令提示符下键入以下命令: Dism /Online /Cleanup-Image /ScanHealth 这条命令将扫描全部系统文件并和官方系统文件对比,扫描计算机中的不一致…

linux环境下恢复rm误删的文件

文章目录 前言rm之后还有救吗使用foremost找回文件使用extundelete找回文件 预防误删引发的事故总结 前言 一提到在 linux 环境下删除文件,那绝对离不开 sudo rm -rf /* 这个梗,每次看到这个命令,我都想到一幅恶搞的图片: 这个『…

如何恢复 Linux 系统下被删除的文件 ?

丢失数据是任何用户都可能经历的最令人不安和痛苦的经历之一。一旦珍贵数据被删除或丢失,就再也找不不回来通常会引发焦虑,让用户感到无助。值得庆幸的是,有几个工具可以用来恢复 Linux 机器上被删除的文件。我们尝试了一些数据恢复工具&…

入门版Linux上恢复误删除的文件

一、被删除的文件正在被进程使用: 当某个文件正在被某个程序使用时,linux针对该文件有回两个计数器: i_count计数器:该文件可能被多个进程使用,每一个进程使用该文件,i_count数值都会加1。反之&#xff0…

不小心误删注册表exe,所有exe程序无法运行

首先我陈述下误删的原因吧,与其说是误删不如说就是自己有 意删除的,电脑Windows7,我在安装CASS10.1的时候,由于一个补丁程序始终不能运行,弹框显示说CASS10.1.6补丁.exe不是有效的Win32应用程序。然而我去寻找解决办法…

如何恢复Linux中的误删文件

写在前面的话 在开始教程之前我有必要提醒大家,使用窗口管理器(GUI)删除文件和使用命令行工具(CLI)删除文件这两种方法之间是有区别的。 当我们使用窗口管理器来删除文件时,我们仅仅只是将文件从某个目录移…

Win11注册表编辑器误删了如何恢复?

​ 注册表编辑器是一个用来更改系统注册表设置的高级工具,与资源管理器的界面很类似。近期有用户将注册表编辑器误删了,那么应该如何恢复呢,下面小编就给大家分享一下详细的恢复方法。遇到同样问题的用户注意了。 更多重装系统教程尽在小白…

注册表中exe被删除后恢复

恢复注册表 如果我们不小心将注册表中的exe删除后,这个时候不管打开什么软件都需我们自己进行指定才能打开使用,这样是及其麻烦的,而且在删除掉.exe之后,原来能在“运行”中搜索的执行文件也都无法执行了,比如果原来我…

LINUX使用rm误删文件后恢复

最近使用centos执行了rm -rf删除了一个文件,后面想恢复。 先关闭selinux vim /etc/selinux/config SELINUXdisabled reboot 重启生效1、使用debugfs命令恢复(此命令为系统自带) 1-1、查看文件系统类型,以下2个命令都可查看&…

电脑注册表误删恢复办法:系统文件和设置还原法

一.起因:为了修改电脑字体一不小心把Control Panel整个注册表给删除了,导致电脑界面变的锯齿,界面变形等各种问题,网上找了许多方法都没成功或者难度较大,最终使用系统恢复还原点将系统变成几个小时前的各种设置&#…