C 语言内联函数简介

目录汇总:C 语言零基础入门教程

C99 及之后的函数声明中有一个 C89 中不存在的选项:可以包含关键字 inline。这个关键字是一个全新的声明指定符,不同于存储类型、类型限定符以及类型指定符。为了理解 inline 的作用,需要把 C 编译器在调用函数和从函数返回过程中产生的机器指令可视化。

在机器层面,调用函数之前可能需要预先执行一些指令。调用本身需要跳转到函数的第一条指令,函数本身可能也需要执行一些额外的指令来启动执行。如果函数有参数,参数需要被复制(因为 C 通过值传递参数)。从函数返回也需要被调用的函数和调用函数执行差不多的工作量。调用函数和从函数返回所需的工作量称为“额外开销”,因为我们并没有要求函数执行这些工作。尽管函数调用中的额外开销只是使程序稍许变慢,但在特定的情况下额外开销会产生累积效应。例如,在函数需要调用数百万次或数十亿次,使用老式的比较慢的处理器(例如在嵌套系统中),或者有着非常严格的时限要求(例如在实时系统中)时。

在 C89 中,避免函数额外开销的唯一方式是使用带参数的宏(C 语言宏定义简介)。但带参数的宏也有一些缺点。C99 提供了一种更好的解决方案:创建内联函数(inline function)。“内联”表明编译器把函数的每一次调用都用函数的机器指令来代替。这种方法虽然会使被编译程序的大小增加一些,但可以避免函数调用的常见额外开销。

不过,把函数声明为 inline 并不是强制编译器将代码内联编译,而只是建议编译器应该使函数调用尽可能地快,也许在函数调用时才执行内联展开。编译器可以忽略这一建议。从这方面来说,inline 类似于 registerrestrict 关键字,后两者也是用于提升程序性能的,但可以忽略。

一、内联定义

内联函数用关键字 inline 作为一个声明指定符:

1
2
3
4
inline double average(double a, double b)
{
  return (a + b) / 2;
}

下面考虑复杂一点的情形。average 有外部链接,所以在其他源文件中也可以调用 average。但编译器并没有考虑 average 的定义是外部定义(因其是内联定义),所以试图在别的文件中调用 average 将被当作错误。

有两种方法可以避免这一错误。一种方法是在函数定义中增加单词 static

1
2
3
4
static inline double average(double a, double b)
{
  return (a + b) / 2;
}

现在 average 具有内部链接了,所以其他文件不能调用它。其他文件可以定义自己的 average 函数,可以与这里的定义相同,也可以不同。

另一种方法是为 average 提供外部定义,从而可以在其他文件中调用。一种实现方式是将该函数重新写一遍(不使用 inline),并将这一函数定义放在另一个源文件中。这样做是合法的,但为同一个函数提供两个版本不太可取,因为我们不能保证对程序进行修改时它们仍然一致。

更好的实现方式是,首先将 average 的内联定义放入头文件(命名为 average.h)中:

1
2
3
4
5
6
7
8
9
#ifndef AVERAGE_H
#define AVERAGE_H

inline double average(double a, double b)
{
  return (a + b) / 2;
}

#endif

接下来再创建与之匹配的源文件 average.c:

1
2
3
#include "average.h"

extern double average(double a, double b);

现在,任何一个需要调用 average 函数的文件只需要简单地包含 average.h 就行了,该头文件包含了 average 的内联定义。average.c 文件包含了 average 的原型。由于使用了 extern 关键字,因此 average.h 中 average 的定义在 average.c 中被当作外部定义。

C99 中的一般法则是,如果特定文件中某个函数的所有顶层声明中都有 inline 但没有 extern,则该函数定义在该文件中是内联的。如果在程序的其他地方使用该函数(包含其内联定义的文件也算在内),则需要在另一个文件中为其提供外部定义。调用函数时,编译器可以选择进行正常调用(使用函数的外部定义)或者执行内联展开(使用函数的内联定义)。我们没有办法知道编译器会怎样选择,所以一定要确保这两处定义一致。刚刚讨论过的方式(使用 average.h 和 average.c)可以保证定义的一致性。

二、对内联函数的限制

因为内联函数的实现方式和一般函数大不一样,所以需要一些不同的规则和限制。对于具有外部链接的内联函数来说,具有静态存储期的变量是一个特别的问题。因此,C99 对具有外部链接的内联函数(未对具有内部链接的内联函数做约束)做了如下限制。

  • 函数中不能定义可改变的 static 变量。
  • 函数中不能引用具有内部链接的变量。

这样的函数可以定义同时为 staticconst 的变量,但每个内联定义都需要分别创建该变量的副本。

三、在 GCC 中使用内联函数

在 C99 标准之前,一些编译器(包括 GCC)已经可以支持内联函数了。因此,它们使用内联函数的规则可能与 C99 标准不一样。特别是前面描述的那种方案(使用 average.h 和 average.c 文件)在这些编译器中可能无效。

不论 GCC 的版本如何,被同时定义为 staticinline 的函数都可以工作得很好。这样做在 C99 中也是合法的,所以是最安全的。static inline 函数可以用于单个文件,也可以放在头文件中,然后在需要调用的源文件中包含进去。

还有一种方法可以在多个文件中共享内联函数。这种方法适用于旧版本的 GCC,但与 C99 相冲突。具体做法是将函数的定义放入头文件中,指明其为 externinline,然后在任何包含该函数调用的源文件中包含该头文件,并且在其中一个源文件中再次给出该函数的定义(不过这次没有 externinline 关键字)。这样即便编译器因为某种原因不能对函数进行“内联”,函数仍然有定义。

关于 GCC,最后需要注意的是,仅当通过 -O 命令行选项请求进行优化时,才会对函数进行“内联”。

请参阅

(完)

comments powered by Disqus