内联函数

在C++语言的设计中,内联函数的引入可以说完全是为了性能的考虑。因此在编写对性能要求比较高的C++程序时,非常有必要仔细考量内联函数的使用。 所谓"内联",即将被调用函数的函数体代码直接地整个插入到该函数被调用处,而不是通过call语句进行。当然,编译器在真正进行"内联"时,因为考虑到被内联函数的传入参数、自己的局部变量,以及返回值的因素,不仅仅只是进行简单的代码拷贝,还需要做很多细致的工作,但大致思路如此。

开发人员可以有两种方式告诉编译器需要内联哪些类成员函数,一种是在类的定义体外;一种是在类的定义体内。

(1)当在类的定义体外时,需要在该成员函数的定义前面加"inline"关键字,显式地告诉编译器该函数在调用时需要"内联"处理,如:

class Student
{
public:
String GetName();
int GetAge();
void SetAge(int ag);
……
private:
String name;
int age;
……
}; inline String GetName()
{
return name;
} inline int GetAge()
{
return age;
} inline void SetAge(int ag)
{
age = ag;
}

(2)当在类的定义体内且声明该成员函数时,同时提供该成员函数的实现体。此时,"inline"关键字并不是必需的,如:

class Student
{
public:
String GetName() { return name; }
int GetAge() { return age; }
void SetAge(int ag) { age = ag; }
……
private:
String name;
int age;
……
};

当普通函数(非类成员函数)需要被内联时,则只需要在函数的定义时前面加上"inline"关键字,如:

    inline int DoSomeMagic(int a, int b)
{
return a * 13 + b % 4 + 3;
}

因为C++是以"编译单元"为单位编译的,而一个编译单元往往大致等于一个".cpp"文件。在实际编译前,预处理器会将"#include"的各头文件的内容(可能会有递归头文件展开)完整地拷贝到cpp文件对应位置处(另外还会进行宏展开等操作)。预处理器处理后,编译真正开始。一旦C++编译器开始编译,它不会意识到其他cpp文件的存在。因此并不会参考其他cpp文件的内容信息。联想到内联的工作是由编译器完成的,且内联的意思是将被调用内联函数的函数体代码直接代替对该内联函数的调用。这也就意味着,在编译某个编译单元时,如果该编译单元会调用到某个内联函数,那么该内联函数的函数定义(即函数体)必须也包含在该编译单元内。因为编译器使用内联函数体代码替代内联函数调用时,必须知道该内联函数的函数体代码,而且不能通过参考其他编译单元信息来获得这一信息。

如果有多个编译单元会调用到某同一个内联函数,C++规范要求在这多个编译单元中该内联函数的定义必须是完全一致的,这就是"ODR"(one-definition rule)原则。考虑到代码的可维护性,最好将内联函数的定义放在一个头文件中,用到该内联函数的各个编译单元只需#include该头文件即可。进一步考虑,如果该内联函数是一个类的成员函数,这个头文件正好可以是该成员函数所属类的声明所在的头文件。这样看来,类成员内联函数的两种声明可以看成是几乎一样的,虽然一个是在类外,一个在类内。但是两个都在同一个头文件中,编译器都能在#include该头文件后直接取得内联函数的函数体代码。讨论完如何声明一个内联函数,来查看编译器如何内联的。继续上面的例子,假设有个foo函数:

#include "student.h"
... void foo()
{
...
Student abc;
abc.SetAge(12);
cout << abc.GetAge();
...
}

foo函数进入foo函数时,从其栈帧中开辟了放置abc对象的空间。进入函数体后,首先对该处空间执行Student的默认构造函数构造abc对象。然后将常数12压栈,调用abc的SetAge函数(开辟SetAge函数自己的栈帧,返回时回退销毁此栈帧)。紧跟着执行abc的GetAge函数,并将返回值压栈。最后调用cout的<<操作符操作压栈的结果,即输出。

#include "student.h"
... void foo()
{
...
Student abc;
{
abc.age = 12;
}
int tmp = abc.age;
cout << tmp;
...
}

这时,函数调用时的参数压栈、栈帧开辟与销毁等操作不再需要,而且在结合这些代码后,编译器能进一步优化为如下结果:

#include "student.h"
... void foo()
{
...
cout << 12;
...
}

这显然是最好的优化结果;相反,考虑原始版本。如果SetAge/GetAge没有被内联,因为非内联函数一般不会在头文件中定义,这两个函数可能在这个编译单元之外的其他编译单元中定义。即foo函数所在编译单元看不到SetAge/GetAge,不知道函数体代码信息,那么编译器传入12给SetAge,然后用GetAge输出。在这一过程中,编译器不能确信最后GetAge的输出。因为编译这个编译单元时,不知道这两个函数的函数体代码,因而也就不能做出最终版本的优化。

从上述分析中,可以看到使用内联函数至少有如下两个优点。

(1)减少因为函数调用引起开销,主要是参数压栈、栈帧开辟与回收,以及寄存器保存与恢复等。

(2)内联后编译器在处理调用内联函数的函数(如上例中的foo()函数)时,因为可供分析的代码更多,因此它能做的优化更深入彻底。前一条优点对于开发人员来说往往更显而易见一些,但往往这条优点对最终代码的优化可能贡献更大。

C++ 内联函数 摘自 C++ 应用程序性能优化的更多相关文章

  1. 内联函数 inline

    (一)inline函数(摘自C++ Primer的第三版) 在函数声明或定义中函数返回类型前加上关键字inline即把min()指定为内联. inline int min(int first, int ...

  2. c++内联函数与静态函数

    不能是虚函数的成员函数有:静态成员函数,内联成员函数,构造函数.没有什么函数需要硬性规定为虚函数,一般析构函数会被定义为虚函数其他就是在继承类中可能需要override的类成员函数应该定义为虚函数 h ...

  3. 你好,C++(28)用空间换时间 5.2 内联函数 5.3 重载函数

    5.2  内联函数 通过5.1节的学习我们知道,系统为了实现函数调用会做很多额外的幕后工作:保存现场.对参数进行赋值.恢复现场等等.如果函数在程序内被多次调用,且其本身比较短小,可以很快执行完毕,那么 ...

  4. 内联函数(Inline Functions)

    影响性能的一个重要因素是内联技巧.内联函数也可称为内嵌函数. 在C++中,函数调用需要建立栈环境,进行参数复制,保护调用现场,返回时,还要进行返回值复制,恢复调用现场.这些工作都是与完成特定任务的操作 ...

  5. c++ 内联函数 (讲解的TM真好)

    1.  内联函数 在C++中我们通常定义以下函数来求两个整数的最大值: 复制代码 代码如下: int max(int a, int b) {  return a > b ? a : b; } 为 ...

  6. C++ 内联函数inline

    http://blog.csdn.net/u011327981/article/details/50601800 1.  内联函数 在C++中我们通常定义以下函数来求两个整数的最大值: 复制代码 代码 ...

  7. C++内联函数

    在C语言中,我们使用宏定义函数这种借助编译器的优化技术来减少程序的执行时间,那么在C++中有没有相同的技术或者更好的实现方法呢?答案是有的,那就是内联函数.内联函数作为编译器优化手段的一种技术,在降低 ...

  8. C++ 内联函数笔记

    要使用内联函数,必须采取下述措施之一: +在函数声明前加上关键字inline: +在函数定义前加上关键字inline. 通常的做法是省略原型,将整个定义(即函数头和所有函数代码)放在本应提供原型的地方 ...

  9. 我的c++学习(6)默认参数和内联函数

    默认参数 一般情况下,函数调用时实参个数应与形参相同,但为了更方便地使用函数,C++也允许定义具有默认参数的函数,这种函数调用时实参个数可以与形参不相同.“默认参数”指在定义或声明函数时为形参指定默认 ...

随机推荐

  1. CORS,jsonp解决跨域问题

    同源和跨域 同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响.可以说Web是构建在同源策略基础之上 ...

  2. Docker 面试题

    Docker 面试题 Docker? Docker是一个容器化平台,它以容器的形式将您的应用程序及其所有依赖项打包在一起,以确保您的应用程序在任何环境中无缝运行. CI(持续集成)服务器的功能是什么? ...

  3. Android MVC MVP MVVM (一)

    示例效果 一共三个控件,EditText,Button,TextView 成功显示账号信息,查询失败显示错误信息. <?xml version="1.0" encoding= ...

  4. MySQL: 1006 - Can't create database '***' (errno: 13) 错误 解决方法

    原文连接:https://blog.csdn.net/kexiaoling/article/details/50259569 如果使用root账号登录到数据库create database时提错错误: ...

  5. [loj#539][LibreOJ NOIP Round #1]旅游路线_倍增_dp

    「LibreOJ NOIP Round #1」旅游路线 题目链接:https://loj.ac/problem/539 题解: 这个题就很神奇 首先大力$dp$很好想,因为可以把一维放到状态里以取消后 ...

  6. IDEA安装Git

    1.下载Git 官方地址为:https://git-scm.com/download/win 2.下载完之后,双击安装 3.选择安装目录 4.选择组件 5.开始菜单目录名设置 6.选择使用命令行环境 ...

  7. Springboot问题解决记录

    本随笔只为了方便查阅 如何将SpringBoot项目地打成一个war包: 传送门:https://blog.csdn.net/zhoucheng05_13/article/details/779152 ...

  8. ubuntu上安装及使用docker

    关于Docker: Docker 是一个开源项目,为开发者和系统管理员提供了一个开放的平台,在任何地方通过打包和运行应用程序作为一个轻量级的容器.Docker 在软件容器内自动部署应用程序.Docke ...

  9. 剑指offer2:C++实现的替换空格(字符中的空格替换为“%20”)

    1. 题目描述 请实现一个函数,将一个字符串中的空格替换成“%20”.例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy. 2. 思路和方法: 2.1 ...

  10. 使用python连接mysql数据库——pymysql模块的使用

    安装pymysql pip install pymysql 使用pymysql 使用数据查询语句 查询一条数据fetchone() from pymysql import * conn = conne ...