C++の函数——内联函数&函数指针
- 2020 年 1 月 14 日
- 筆記
C++の函数
—— 内联函数&函数指针
今天我们继续讨论C++函数部分,剩下两个点,一个是内联函数,另一个是函数指针。
内联函数
我们先看一下内联函数。内联函数也是C++中的一个重要特性。所谓内联函数,其实本质上也是一种函数,在形式上的表现就是在普通函数前面加上关键字"inline",然后相对于普通函数来说,它也比较短小。C++中"inline"的作用其实是为了优化代码的运行,降低代码的执行时间,就像在C语言中的宏函数一样,作用也是为了降低代码的执行时间。
当内联函数被调用时,并不会向普通函数一样从主函数跳转到函数,而是直接将内联函数中的代码逻辑替换进主函数,提高运行效率。而这个过程是在代码编译的过程即完成的,当我们将一个函数定义为内联函数时,编译器识别到内联函数的特征后,就将函数的定义替换到函数的调用。那么我们怎么定义内联函数呢?
如何定义内联函数
定义内联函数就要在函数的前面使用“inline”关键字。像下面这样:
inline int add(int a, int b); //(1) inline int add(int a, int b) //(2) { return (a + b); } void test() //test func { int i = 4; int j = 6; add(i, j); } void test_f() //equal to test() { int i = 4; int j = 6; (i + j); }
我们看到上面有两个部分,一个是add函数的声明,一个是add函数的定义,并且每个函数前都有“inline”,我们便将“add”函数定义为内联函数,那么在代码中调用时就是将add函数的定义替换为调用部分的代码,如上面的test(),在编译的时候就会自动转为test_f()。
注意:有一点需要注意,并不是每一个用inline标明的函数都会被编译器转为内联,内联的根本目的是优化程序的运行,因此对于使用较为频繁的短小的函数,才有明显的效果,如果函数较为庞大,编译器也会忽略掉函数前面的“inline”将其变为普通函数。
为什么要用内联函数
我们在代码中经常会用到一些小函数,它们逻辑简单,代码量少,但是如果考虑到这些函数被调用者调用的时候,我们会发现大部分的时间都耗费在调用这个过程,也就是程序从主函数跳转到被调用函数的过程,而不是在我们写的这个小函数中。实际上正常的函数调用指令时,程序立即在函数调用语句之后存储指令的内存地址,将被调用的函数加载到内存中复制参数值,跳转到被调用函数的内存位置,执行函数代码,存储函数的返回值,然后跳转回执行被调用函数之前保存的指令地址。这样会导致程序的运行时间开销太大。
而C++的内联函数则提供了一种替代的方法,使用inline关键字,编译器用函数代码本身替换函数调用语句,然后再编译整个代码。因此,对于内联函数,编译器不必跳到另一个位置去执行函数,然后再跳回去,因为被调用程序的代码已经是调用程序中代码的一部分了。
下面我们列举一下内联函数的优缺点:
优点:
1、内联函数通过避免函数调用开销从而加速了我们的程序
2、当函数调用发生时,内联函数节省了堆栈上变量push/pop的开销
3、内联函数节省了从函数返回调用开销
4、内联函数通过使用指令缓存来增加引用的局部性
5、通过将其标记为内联,您可以将函数定义放入头文件中
缺点:
1、由于代码扩展,它增加了可执行文件的大小
2、c++内联在编译时解决。这意味着如果您更改内联函数的代码,您将需要使用它重新编译所有代码,以确保它将被更新
3、当在头文件中使用时,它会使头文件变大,包含用户不关心的信息
4、如上所述,它增加了可执行文件的大小,这可能会导致内存抖动。更多的页面错误会降低程序性能
5、有时并不有用,例如在嵌入式系统中,由于内存限制,大的可执行文件大小根本不是首选
什么时候使用内联函数
函数可以根据程序员的需要进行内嵌,那么我们什么时候使用呢?
1、当性能优先时,应该使用内联函数
2、在宏上使用内嵌函数
3、优先在函数定义中使用类外的inline关键字来隐藏实现细节
函数指针
所谓函数指针,其实本质上还是指针,但是不同于我们之前提到的指针,函数指针是指向函数的指针。根据前面的文章,我们很容易声明一个函数,如下我们声明一个比较两个字符串长度的函数:
bool lengthCompare(const string &, const string &);
现在,我们再来看一下函数指针的声明方式吧:
bool (*pf)(const string &, const string &);
从上面我们可以看到pf前面有个*,因此pf是一个指针,右面是形参列表,所以pf是指向函数的指针,从前面的bool可以看出这个函数的返回值类型是bool类型。
注意: *pf两边的()是必须的,因为这代表*pf是一个整体,pf是一个指针,如果不加括号,就表示bool* 是一个整体,pf就成了函数名,那么它的含义就变成了返回值为bool类型指针的函数了,这样是不是很好理解?
如何使用函数指针
其实同数组一样,函数名就代表了函数入口的首地址,也就是我们说的函数指针。对于上面两个例子来说,由于他们具有相同的参数列表,因此我们可以得到下面的等价式:
pf = lengthCompare; pf = &lengthCompare;
可以看到,函数名就是函数的首地址,也表示函数本身。
因此,我们也会有下面的调用方式:
bool b1 = pf("leoay", "learn C++"); bool b2 = (*pf)("leoay", "learn C++!"); bool b3 = lengthCompare("leoay", "learn C++!!");
可以看到,我们并不需要对函数指针进行解引用就能直接调用它,因为我们在调用函数的时候其实就是找函数在程序中的首地址,然后将参数传进去。
重载函数的指针
前面我们说到了函数的重载,就是说在同一个源文件中函数具有相同的名字,但是具有不同的参数列表时的情况,因此我们很容易延伸到函数指针里面,就是这里要说的重载函数指针。我们先来看一下怎么声明重载函数指针:
void ff(int*); void ff(unsigned int); void (*pf1)(unsigned int) = ff;
从上面的代码,我们可以看出想要使用重载函数指针,我们就要先声明重载函数,然后我们在定义一个函数指针时,将重载函数的地址赋值给这个函数指针,这里有一点我们需要注意,既然重载函数有不同的列表,那么我们在定义重载函数指针时该怎么选择呢?当然是与我们想要使用的那个重载函数保持一致。就是说我们想用哪个重载函数定义函数指针,函数指针的参数列表就应该与哪个重载函数保持一致。
把函数指针当做参数
到这里,我们发现函数指针并没有什么神奇的地方,我们完全可以把它当做一个指针看待,只不过具备函数的一些特征。但是,回归根本,它还是一枚可爱的指针。因此,它应该具备指针的一些特征。比如,我们可以把它当做参数传递给其他的参数。以后我们会讲到,C++中常见的回调函数就是这样使用的。
下面我们就用代码演示一下这种骚操作吧:
void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &)); void useBigger(const string &s1, const string &s2, bool (*pf)(const string &, const string &));
可以看到上面的代码中两个函数的参数中分别有下面这两个参数:
bool pf(const string &, const string &) bool (*pf)(const string &, const string &)
上面是一个函数,下面是一个函数指针。但是在这里实际上他们是等价的,当函数被作为参数传递给另一个参数的时候,是等价于函数指针的。所以上面两个声明其实是等价的。
对于函数指针与内联函数的说明这篇文章就到这里,由于是基础系列文章,先不详细展开,还有一些知识需要用实例和练习加以说明。以后如果出高级系列再详细展开讨论。