背景
当程序调用一个函数时,实际上有一些额外的开销。函数调用会使程序跳转到另一个地址(函数的地址),并在函数结束时返回,如以下的函数示例。简单函数示例
假如上述函数编译后的汇编代码如下:简单函数对应的汇编代码
其中"sp"指的是堆栈指针,每个程序运行时,都有一个独立的堆栈,堆栈里放本地变量和返回地址(当调用一个函数时,那个函数的返回地址要放在堆栈里)。上述函数的执行过程具有以下几步:
(1)sp+8,即在堆栈里留出8个字节的空间,用于存放a和b两个变量;
(2)将4装入ax中,ax是一个主要用来做运算的寄存器;
(3)将ax里的值移到sp[-8]的地址处,即相当于源程序中的a=4;
(4)将sp[-8]的值移动到寄存器ax中,因为在函数fun里用到了a的值(把a赋值给value),这个步骤其实可以优化;
(5)将ax压入堆栈,相当于把函数的参数value压入堆栈,即在调用函数之前,需要将其参数压入堆栈,因此,函数的参数和本地变量的地位是一样的;
(6)调用函数,call是一条指令,首先将程序下一条指令的地址压入堆栈(以便返回),其次是转到_fun_int的位置开始执行;
(7)执行函数fun()的内容,@sp[-8]为函数的参数值,即value=4,将返回值装入ax中;
(8)找到返回的地址,将返回值赋给变量b
(9)把之前压入的ax(也就是临时变量value)弹出堆栈。函数的大致执行过程
因此,调用函数时,程序需要将送给函数的参数、返回地址放入堆栈,如果函数有临时变量,还需要将临时变量都放入堆栈,接着准备函数的返回值,函数返回时,需要将之前压入(push)堆栈的东西全部弹出(pop),可见,一个简单的函数过程需要许多额外的步骤。整个过程类似于我们在阅读文章时停下来看脚注,在阅读完脚注后返回到以前阅读的地方,来回跳跃并记录就需要额外的开销。
内联函数(inline)
内联函数与常规函数之间的主要区别不在于编写的方式,而在于C++编译器如何将它们组合到程序中。对于内联函数,编译器将使用相应的代码替换函数调用,也就是将函数体中的代码嵌入到被调用的地方去。这样处理,使得程序无需跳转到另一个位置执行代码,再跳回来,从而加快运行速度。
因此将原来的代码修改成:修改后的代码
而实际生成的代码是:实际生成的代码
编译结果里面并不会出现那个函数,内联函数仍旧保持了函数的独立性(函数有自己的空间,有自己的变量,函数调用时需要类型检查),只是不去真正地调用函数而已,函数类型检查这个步骤依然存在(由编译器完成),因而不会增加程序运行时的负担。
注意事项
对于一般的函数,其声明放在.h文件中,实现放在.cpp里,在其他文件里使用该函数时,只需要包含.h文件即可。inline函数可以这样做吗?不可以,编译会报错。如下:内联函数错误示例
为什么会报错,因为在编译主函数时,根据头文件知道有一个函数是inline,但是不知道这个函数的body,因此无法按照inline的规定将其函数体的代码插入到主函数中,就将这个函数当成了普通的函数。而在编译该内联函数的.cpp时,由于编译器发现是一个内联函数,就不会生成相应的代码。因此链接器在链接两个源文件时报错,main需要调用函数,而那个函数却不存在。
因此内联函数的body应该直接写在头文件里(不需要对应的.cpp),然后被包含到其他的源文件,此时的内联函数不再是定义,其实就是个声明。
要不要用内联函数内联函数的代码会被插入到调用处,如果程序有好几处调用该函数,程序就会变长,因此内联函数牺牲了代码的空间,降低额外运行开销,加快了程序运行的速度。
内联函数比C语言的宏定义好,如 :
#define SQUARE(X) X*X
a=SQUARE(5.0) 即为a=5.0*5.0,这并不是通过值传递实现的,而是通过文本替换实现的,假如输入的参数不是5.0,而是一个字符串呢?这种宏定义不能进行类型检查,而内联函数可以,因此更安全 。
如果编译器发现函数很长,会自动拒绝该函数作为内联函数。
递归不能作为内联函数,因为递归一定需要借助堆栈的压入和弹出实现功能。
对于C++类的成员函数,如果在函数声明的地方就给出了函数体,这些函数默认就是内联函数,如下:类里面默认的内联函数
类的成员函数“getColor()”和“setColor()”是内联函数,它们的调用相当于直接访问属性“color”,运行效率较高,但是通过函数去访问类的属性,符合面向对象编程的思想,函数使得类的属性与外界产生隔绝。
6. 如果函数很小,又在程序中多次被调用(如一个循环中),编译器会自动将其作为inline函数。