inline 函数由 inline 关键字定义,引入 inline 函数的主要原因是用它替代 C 中复杂易错不易维护的宏函数。
编译器在编译阶段完成对 inline 函数的处理,即对 inline 函数的调用替换为函数的本体。但 inline 关键字对编译器只是一种建议,编译器可以这样去做,也可以不去做。从逻辑上来说,编译器对 inline 函数的处理步骤一般如下:
(1)将 inline 函数体复制到inline函数调用处;
(2)为所用 inline 函数中的局部变量分配内存空间;
(3)将 inline 函数的的输入参数和返回值映射到调用方法的局部变量空间中;
(4)如果 inline 函数有多个返回点,将其转变为 inline 函数代码块末尾的分支(使用GOTO)。
比如如下代码:
// 求 0-9 的平方
inline int inlineFunc(int num) {
if(num>9||num<0) return -1;
return num*num;
}
int main(int argc,char* argv[]) {
int a=8;
int res=inlineFunc(a);
cout<<"res:"<<res<<endl;
}
inline 之后的 main 函数代码类似于如下形式:
int main(int argc,char* argv[]) {
int a = 8; {
int _temp_b=8;
int _temp;
if (_temp_q >9||_temp_q<0) _temp = -1;
else _temp =_temp*_temp;
b = _temp;
}
}
经过以上处理,可消除所有与调用相关的痕迹以及性能的损失。inline 通过消除调用开销来提升性能。
函数定义时,在返回类型前加上关键字 inline 即把函数指定为内联,函数申明时可加也可不加。但是建议函数申明的时候,也加上 inline,这样能够达到"代码即注释"的作用。
使用格式如下:
inline int functionName(int first, int secend,...) {
/****/};
inline如果只修饰函数的申明的部分,如下风格的函数foo不能成为内联函数:
inline void foo(int x, int y); // inline 仅与函数声明放在一起。
void foo(int x, int y){
}
而如下风格的函数foo 则成为内联函数:
void foo(int x, int y);
inline void foo(int x, int y){
} // inline 与函数定义体放在一起。
从上面可以知道,inline 函数相对宏函数有如下优点:
(1)内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度。
(2)内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。
例如宏函数和内联函数:
// 宏函数
#define MAX(a,b) ((a)>(b)?(a):(b))
// 内联函数
inline int MAX(int a,int b) {
return a>b?a:b;
}
使用宏函数时,其书写语法也较为苛刻,如果对宏函数出现如下错误的调用,MAX(a,"Hello");
宏函数会错误地比较int和字符串,没有参数类型检查,但是使用内联函数的时候,会出现类型不匹配的编译错误。
(3)在类中声明同时定义的成员函数,自动转化为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能。
(4)内联函数在运行时可调试,而宏定义不可以。
万事万物都有阴阳两面,内联函数也不外乎如此,使用inline函数,也要三思慎重。inline函数的缺点总结如下:
(1)代码膨胀。
inline函数带来的运行效率是典型的以空间换时间的做法。内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
(2)inline 函数无法随着函数库升级而升级。
如果f是函数库中的一个inline函数,使用它的用户会将f函数实体编译到他们的程序中。一旦函数库实现者改变f,所有用到f的程序都必须重新编译。如果f是non-inline的,用户程序只需重新连接即可。如果函数库采用的是动态连接,那这一升级的f函数可以不知不觉的被程序使用。
(3)是否内联,程序员不可控。
inline函数只是对编译器的建议,是否对函数内联,决定权在于编译器。编译器认为调用某函数的开销相对该函数本身的开销而言微不足道或者不足以为之承担代码膨胀的后果则没必要内联该函数,若函数出现递归,有些编译器则不支持将其内联。
了解了内联函数的优缺点,在使用内联函数时,我们也要注意以下几个事项和建议。
(1)使用函数指针调用内联函数将会导致内联失败。
也就是说,如果使用函数指针来调用内联函数,那么就需要获取inline函数的地址。如果要取得一个inline函数的地址,编译器就必须为此函数产生一个函数实体,那么就内联失败。
(2)如果函数体代码过长或者有多重循环语句,if或witch分支语句或递归时,不宜用内联。
(3)类的 constructors、destructors 和虚函数往往不是 inline 函数的最佳选择。
类的构造函数(constructors)可能需要调用父类的构造函数,析构函数同样可能需要调用父类的析构函数,二者背后隐藏着大量的代码,不适合作为inline函数。虚函数(destructors)往往是运行时确定的,而inline是在编译时进行的,所以内联虚函数往往无效。如果直接用类的对象来使用虚函数,那么对有的编译器而言,也可起到优化作用。
(4)至于内联函数是定义在头文件还是源文件的建议。
内联展开是在编译时进行的,只有链接的时候源文件之间才有关系。所以内联要想跨源文件必须把实现写在头文件里。如果一个 inline 函数会在多个源文件中被用到,那么必须把它定义在头文件中。
// base.h
class Base{
protected:void fun();};
// base.cpp
#include base.h
inline void Base::fun(){
}
// derived.h
#include base.h
class Derived: public Base{
public:void g();};
// derived.cpp
void Derived::g(){
fun();} // VC2010: error LNK2019: unresolved external symbol
上面这种错误,就是因为内联函数 fun() 定义在编译单元 base.cpp 中,那么其他编译单元中调用fun()的地方将无法解析该符号,因为在编译单元 base.cpp 生成目标文件 base.obj 后,内联函数fun()已经被替换掉,编译器不会为 fun() 生成函数实体,链接器自然无法解析。所以如果一个 inline 函数会在多个源文件中被用到,那么必须把它定义在头文件中。
这里有个问题,当在头文件中定义内联函数,那么被多个源文件包含时,如果编译器因为 inline 函数不适合被内联时,拒绝将inline函数进行内联处理,那么多个源文件在编译生成目标文件后都将各自保留一份inline函数的实体,这个时候程序在链接阶段会出现重定义错误吗?答案是不会,原因是,链接器在链接的过程中,会删除多余的 inline 函数实体,只保留一份,所以不会报重定义错误,因此我们不需要使用 static 关键字去多余地修饰 inline 函数,即不必像下面这样。
// test.h
static inline int max(int a,int b) {
return a>b?a:b;
}
(5)能否强制编译器进行内联操作?
也有人可能会觉得能否强制编译器进行函数内联,而不是建议编译器进行内联呢?很不幸的是目前还不能强制编译器进行函数内联,如果使用的是 MS VC++, 注意 __forceinline
如同 inine 一样,也是一个用词不当的表现,它只是对编译器的建议比inline更加强烈,并不能强制编译器进行 inline 操作。
(6)如何查看函数是否被内联处理了?
在 VS2017 中查看预处理后的.i文件,发现inline函数的内联处理不是在预处理阶段,而是在编译阶段。将源文件编译为汇编代码,或者将可执行文件反汇编生成汇编代码,在汇编代码中查看inline函数被调用处是否出现汇编的call指令,如果没有则说明inline函数在被调用处进行了函数体的替换操作,即内联处理。具体可以参考内联函数到底有没有被嵌入到调用处呢?。
(7)C++ 类成员函数定义在类体内为什么不会报重定义错误?
类成员函数定义在类体内,并随着类的定义放在头文件中,当被不同的源文件包含,那么每个源文件都应该包含了类成员函数的实体,为何在链接的过程中不会报函数的重定义错误呢?
原因是在类里定义时,这种函数会被编译器编译成内联函数,在类外定义的函数则不会。内联函数的好处是加快程序的运行速度,缺点是会增加程序的尺寸。比较推荐的写法是把一个经常要用的而且实现起来比较简单的小型函数放到类里去定义,大型函数最好还是放到类外定义。
可能存在疑问,类体内的成员函数被编译器内联处理,但并不是所有的成员函数都会被内联处理,比如包含递归的成员函数。但是实际测试,将包含递归的成员函数定义在类体内,被不同的源文件包含并不会报重定义错误,为什么会这样呢?请保持着疑问与好奇心,请继续往下看。
如果编译器发现被定义在类体内的成员函数无法被内联处理,那么在程序的链接过程中也不会出现函数重定义的错误。其原因是什么呢?其实很简单,类体内定义的成员函数即使不被内联处理,在链接时,链接器会对重复的成员函数实体进行冗余优化,只保留一份函数实体,也就不会出现函数重定义的错误了。
除了 inline 函数,C++编译器在很多时候都会产生重复的代码,比如模板(Templates)、虚函数表(Virtual Function Table)、类的默认成员函数(构造函数、析构函数和赋值运算符)等。以函数模板为例,在多个源文件中生成相同的实例,链接时不会出现函数重定义的错误,实际上是一个道理,因为链接器会对重复代码进行删除,只保留一份函数实体。
可以将内联理解为 C++ 中对于函数专有的宏,对于 C 的函数宏的一种改进。对于常量宏,C++ 提供 const 替代;而对于函数宏,C++ 提供的方案则是 inline。C++ 的内联机制,既具备宏代码的效率,又增加了安全性,还可以自由操作类的数据成员,算是一个比较完美的解决方案。
上面的结论和观点,缺乏实践和权威资料支撑,难免存在错误,仅供参考学习,如果大家发现错误和需要改进的地方,请大家留言给予宝贵的建议。
[1] inline函数
[2] 小问题大思考之C++里的inline函数
[3] 把inline函数的定义放在头文件中
[4] Inline Functions (C++)
[5] Can I selectively (force) inline a function?
[6] C语言inline详细讲解
[7] C++中的作用域与生命周期
[8] 内联函数到底有没有被嵌入到调用处呢?
[9] 余甲子,石凡,潘爱民.程序员的自我修养——链接、装载与库[M].北京:机械工业出版社,2009.C4.4.1重复代码消除.P113-P114
在使用MATLAB HDL Coder Toolbox的时候,每次新建一个Simulink模型都会重置Verilog代码生成的参数,默认生成VHDL代码,不生成Report,有时候也不需要clk_enable信号,所以需要有一个方便的办法自动批量更改一下。这个脚本的目的:在MATLAB命令行输入: sethdl(),会自动获取当前活动的Simulink模型,然后将输出代码更改为Verilog,生成可跟踪的Report等,读者可以根据自己的需求更改脚本,如果没有活动的Simulink模型则无操作。fu
转自:https://www.jianshu.com/p/86267fab4878/ 作者:张培跃prototype和__proto__属性每次看完又忘记,看到上述文章后,又想起来记下笔记参考这篇文章:讲的很详细:https://blog.csdn.net/cc18868876837/article/details/81211729class Box{ constructor(num1,num2){ this.num1 = num1; this.num
String StringBuffer Stringbuilder运行速度:StringBuilder>StringBuffer>StringString 是字符串常量,StringBuilder与StringBuffer是字符串变量,所以String对象一旦创建不可修改,后两者是变量,可以修改。StringBuilder是线程不安全的,StringBuffer线程安全,因为S...
package com.tianbo.maven_paChong_first;import java.io.BufferedWriter;import java.io.File;import java.io.FileOutputStream;import java.io.FileWriter;import java.io.IOException;import java.io.OutputStreamWriter;import java.io.PrintWriter;import java.
fragment加载方式1. 静态加载( 不行,没有 试出来)2. 动态加载1. Fragent使用效果图: Java代码 MainActivity:**************************************************************************************************************...
简述TCP/IP邮件,SMTP (简单邮件传输协议)、POP (邮局协议),IMAP (因特网消息访问协议)之间的含义和区别?1.TCP - 传输控制协议TCP 用于从应用程序到网络的数据传输控制。TCP 负责在数据传送之前将它们分割为 IP 包,然后在它们到达的时候将它们重组。IP - 网际协议(Internet Protocol)IP 负责计算机之间的通信。IP 负责在因特网上发送...
简介有的时候关于3d的散点和面片,直接用matlab 显示比较方便。codescatter3(x1,y1,z1,'k');hold on;scatter3(x2,y2,z2,'*');hold on;scatter3(x3,y3,z3,'*');hold on;line(x2,y2,z2);hold on;line(x3, y3, z3);hold on;% 交点 [-0.361271936335...
第一篇也不知道写什么,就把昨晚安装kali时遇见的事写一下吧!因为win10应用商店已经加入了kali,也省的我再去网上下载镜像,可下载后 wsl 未能设置为开发人员模式,这算是失误吧!步骤如下首先打开设置界面然后进入更新与安全再接着:开发者选项--->选中开发人员模式退出设置模式,然后win+Q搜索框打开控制面板,紧着着...
在传统的大型数据中心,网络通常是三层结构。 三层网络结构是采用层次化架构的三层网络,有三个层次:核心层(网络的高速交换主干)、汇聚层(提供基于策略的连接)、接入层(将工作站接入网络)核心层(Core Layer):核心层是网络的高速交换主干,对整个网络的连通起到至关重要的作用。核心层应该具有如下几个特性:可靠性、高效性、冗余性、容错性、可管理性、适应性、低延时性等。在核心层中,应该采用高带宽的千...
我忽略了要学习的最大的Android之一就是Loaders 。 看到我该学习它的时候了,也许我也可以帮助您。 我对Loader概念的主要兴趣是如何将它与经过实践检验的真实AsyncTask融合在一起,以及它是否真的更好。 异步任务 在了解Loader概念之前,重要的是要了解AsyncTask是什么以及它在Android中的用途。 如果您编写了用于Android的任何类型的应用程序,则很有...
定义一个链表存放10个学生的学号、姓名、成绩,从键盘输入数据。将链表的内容输出到屏幕上。按学号进行查找、插入、删除。将链表的内容输出到文件cx.txt#include &lt;stdio.h&gt; #include &lt;stdlib.h&gt; #define NULL 0#define LEN sizeof(struct student)struct student{ long num;...
架构师必须写代码——高效程序员的45个习惯之习惯39“我们的专家级架构师Fred会提供设计好的架构,供你编写代码。他经验丰富,拿的薪水很高,所以不要用一些愚蠢的问题或者实现上的难点,来浪费他的时间。” 软件开发业界中有许多挂着架构师称号的人。作为作者的我们,不喜欢这个称号,原因如下:架构师应该负责设计...