前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C++:04---内联函数

C++:04---内联函数

作者头像
用户3479834
发布2021-02-03 15:38:17
1.2K0
发布2021-02-03 15:38:17
举报
文章被收录于专栏:游戏开发司机游戏开发司机

1.概念:

内联类似于宏定义,当程序执行到内联函数时,相当于复制了一份函数代码。牺牲代码空间,赢得了时间

内联说明只是向编译器发出一个请求,编译器可以选择忽略这个请求

2.关键字:inline

  • 声明时写了inline,定义时可省略。建议声明和定义都加上inline
代码语言:javascript
复制
inline int add(int a,int b)
{
return a+b;
}

3.内联的优、缺点:

  • 优点:避免了函数调用的开销,加快了代码的运行速度,避免频繁调用函数对栈内存带来的消耗
  • 缺点:浪费代码空间

4.使用内联的情况

  • 不宜使用内联的情况:递归函数、或函数中含有循环结构(浪费时间)、或函数代码过长(消耗内存)
  • 可使用内联的情况:内联一般在类中使用(函数内进行简单的赋值、或直接返回数据、或1~5条小语句)
  • 内联函数定义建议放在头文件中,但是不强制要求

总结:内联机制用于优化规模较小,流程直接,频繁调用的函数

5.显式内联、隐式内联

  • 隐式内联:结构体或类中的函数在结构体中声明并定义,并且如果这个函数不复杂,那么其是隐式内联的(编译器自动定义)
  • 显示内联:手动给出

6、内联函数和宏

1、宏容易出错;

2、宏不可调试;

3、宏无法操作类的私有对象;

4、内联函数可以更加深入的优化;

无论是《Effective C++》中的 “Prefer consts,enums,and inlines to #defines” 条款,还是《高质量程序设计指南——C++/C语言》中的“用函数内联取代宏”,宏在C++中基本是被废了,在书《高质量程序设计指南——C++/C语言》中这样解释到:

7. 将内联函数放入头文件

关键字 inline 必须与函数定义体放在一起才能使函数成为内联,仅将 inline 放在函数声明前面不起任何作用。

如下风格的函数 Foo 不能成为内联函数:

代码语言:javascript
复制
inline void Foo(int x, int y);   // inline 仅与函数声明放在一起   
void Foo(int x, int y)
{
    //...
}

而如下风格的函数 Foo 则成为内联函数:

代码语言:javascript
复制
void Foo(int x, int y);   
inline void Foo(int x, int y)   // inline 与函数定义体放在一起

所以说,C++ inline函数是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。一般地,用户可以阅读函数的声明,但是看不到函数的定义。尽管在大多数教科书中内联函数的声明、定义体前面都加了 inline 关键字,但我认为 inline 不应该出现在函数的声明中。这个细节虽然不会影响函数的功能,但是体现了高质量C++/C 程序设计风格的一个基本原则:声明与定义不可混为一谈,用户没有必要、也不应该知道函数是否需要内联。

定义在类声明之中的成员函数将自动地成为内联函数,例如:

代码语言:javascript
复制
class A
{  
public:
    void Foo(int x, int y) { ... }   // 自动地成为内联函数  
}

但是编译器是否将它真正内联则要看 Foo函数如何定义

内联函数应该在头文件中定义,这一点不同于其他函数。编译器在调用点内联展开函数的代码时,必须能够找到 inline 函数的定义才能将调用函数替换为函数代码,而对于在头文件中仅有函数声明是不够的。

当然内联函数定义也可以放在源文件中,但此时只有定义的那个源文件可以用它,而且必须为每个源文件拷贝一份定义(即每个源文件里的定义必须是完全相同的),当然即使是放在头文件中,也是对每个定义做一份拷贝,只不过是编译器替你完成这种拷贝罢了。但相比于放在源文件中,放在头文件中既能够确保调用函数是定义是相同的,又能够保证在调用点能够找到函数定义从而完成内联(替换)。

但是你会很奇怪,重复定义那么多次,不会产生链接错误?

我们来看一个例子:

代码语言:javascript
复制
// 文件A.h 代码如下:
代码语言:javascript
复制
class A
{
public:
    A(int a, int b) : a(a),b(b){}
    int max();
private:
    int a;
    int b;
};
代码语言:javascript
复制
// 文件A.cpp 代码如下:
代码语言:javascript
复制
#include "A.h"
inline int A::max()
{
    return a > b ? a : b;
}
代码语言:javascript
复制
// 文件Main.cpp 代码如下:
代码语言:javascript
复制
#include <iostream>
#include "A.h"
using namespace std;
inline int A::max()
{
    return a > b ? a : b;
}

int main()
{
    A a(3, 5);
    cout << a.max() << endl;
    return 0;
}

一切正常编译,输出结果:5

倘若你在Main.cpp中没有定义max内联函数,那么会出现链接错误: error LNK2001: unresolved external symbol "public: int __thiscall A::max(void)" (?max@A@@QAEHXZ)main.obj 找不到函数的定义,所以内联函数可以在程序中定义不止一次,只要 inline 函数的定义在某个源文件中只出现一次,而且在所有源文件中,其定义必须是完全相同的就可以。

在头文件中加入或修改 inline 函数时,使用了该头文件的所有源文件都必须重新编译。

8. 慎用内联

“如果所有的函数都是内联函数,还用得着“内联”这个关键字吗?

内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。以下情况不宜使用内联:

(1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。

(2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。

类的构造函数和析构函数容易让人误解成使用内联更有效。要当心构造函数和析构函数可能会隐藏一些行为,如“偷偷地”执行了基类或成员对象的构造函数和析构函数。所以不要随便地将构造函数和析构函数的定义放在类声明中。”

————《高质量程序设计指南——C++/C语言》 林锐

而在Google C++编码规范中则规定得更加明确和详细:

内联函数:
Tip:只有当函数只有 10 行甚至更少时才将其定义为内联函数.

定义: 当函数被声明为内联函数之后, 编译器会将其内联展开, 而不是按通常的函数调用机制进行调用. 优点: 当函数体比较小的时候, 内联该函数可以令目标代码更加高效. 对于存取函数以及其它函数体比较短, 性能关键的函数, 鼓励使用内联. 缺点: 滥用内联将导致程序变慢. 内联可能使目标代码量或增或减, 这取决于内联函数的大小. 内联非常短小的存取函数通常会减少代码大小, 但内联一个相当大的函数将戏剧性的增加代码大小. 现代处理器由于更好的利用了指令缓存, 小巧的代码往往执行更快。 结论: 一个较为合理的经验准则是, 不要内联超过 10 行的函数. 谨慎对待析构函数, 析构函数往往比其表面看起来要更长, 因为有隐含的成员和基类析构函数被调用! 另一个实用的经验准则: 内联那些包含循环或 switch 语句的函数常常是得不偿失 (除非在大多数情况下, 这些循环或 switch 语句从不被执行). 有些函数即使声明为内联的也不一定会被编译器内联, 这点很重要; 比如虚函数和递归函数就不会被正常内联. 通常, 递归函数不应该声明成内联函数.(递归调用堆栈的展开并不像循环那么简单, 比如递归层数在编译时可能是未知的, 大多数编译器都不支持内联递归函数). 虚函数内联的主要原因则是想把它的函数体放在类定义内, 为了图个方便, 抑或是当作文档描述其行为, 比如精短的存取函数.

-inl.h文件:

Tip:复杂的内联函数的定义, 应放在后缀名为 -inl.h 的头文件中.

内联函数的定义必须放在头文件中, 编译器才能在调用点内联展开定义. 然而, 实现代码理论上应该放在 .cc 文件中, 我们不希望 .h 文件中有太多实现代码, 除非在可读性和性能上有明显优势.

如果内联函数的定义比较短小, 逻辑比较简单, 实现代码放在 .h 文件里没有任何问题. 比如, 存取函数的实现理所当然都应该放在类定义内. 出于编写者和调用者的方便, 较复杂的内联函数也可以放到 .h 文件中, 如果你觉得这样会使头文件显得笨重, 也可以把它萃取到单独的 -inl.h 中. 这样把实现和类定义分离开来, 当需要时包含对应的 -inl.h 即可。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-11-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 游戏开发司机 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 6、内联函数和宏
    • 7. 将内联函数放入头文件
      • 8. 慎用内联
        • 内联函数:
        • Tip:只有当函数只有 10 行甚至更少时才将其定义为内联函数.
        • Tip:复杂的内联函数的定义, 应放在后缀名为 -inl.h 的头文件中.
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档