内联函数与变量

内联函数与变量

Alex 2025 年 1 月 18 日

当需要编写完成某项离散任务的代码(例如读取用户输入、向文件输出或计算某个数值)时,通常有两种实现方式:

  1. 将代码直接写入现有函数内部(即“就地”或“内联”编写);
  2. 创建一个新函数(并可能再划分子函数)来专门处理该任务。

将代码放入独立的新函数可带来诸多潜在好处:

  • 小函数在整体程序中更易阅读与理解;
  • 函数天然模块化,便于复用;
  • 仅需在一处修改,维护更轻松。

然而,使用新函数的缺点是每次调用都会产生一定的性能开销。示例:

#include <iostream>

int min(int x, int y)
{
    return (x < y) ? x : y;
}

int main()
{
    std::cout << min(5, 6) << '\n';
    std::cout << min(3, 2) << '\n';
    return 0;
}

每当遇到 min() 调用时,CPU 必须:

  • 保存当前指令地址与寄存器状态;
  • 实例化并初始化形参 xy
  • 跳转到 min() 的代码;
  • 函数返回后,再跳转回原位置,并复制返回值。
    这些额外工作统称为开销

对于大型或复杂函数,调用开销相对实际执行时间微不足道;但对小型函数(如上述 min()),开销可能超过函数体本身的执行时间。若此类小函数被频繁调用,则使用函数会导致显著性能损失。

内联展开(Inline Expansion)

编译器通过内联展开避免上述开销:将函数调用处替换为函数体的实际代码。
对示例进行内联展开后,代码等价于:

#include <iostream>

int main()
{
    std::cout << ((5 < 6) ? 5 : 6) << '\n';
    std::cout << ((3 < 2) ? 3 : 2) << '\n';
    return 0;
}

两处 min() 调用被直接替换为函数体,既消除了调用开销,又保留结果。

内联代码的性能与权衡

除移除调用开销外,内联展开还能让编译器进一步优化,例如:

  • 表达式 ((5 < 6) ? 5 : 6) 为编译期常量,编译器可将第一条语句优化为 std::cout << 5 << '\n';

然而,内联亦有代价:若函数体指令多于调用指令,展开会使可执行文件增大。更大的二进制文件可能降低缓存效率,反而减慢运行速度。

因此,是否适合内联需综合考虑:函数调用成本、函数大小及后续优化空间。
内联最适合短小、简单(通常仅数条语句)的函数,尤其是循环中多次调用的场景。

何时发生内联展开

每个函数调用属于以下两类之一:

  • 可能展开(大多数函数);
  • 无法展开

现代编译器对“可能”类函数逐调用点评估是否内联;可能全部、部分或完全不展开。
最常见的“无法展开”情形是:函数定义位于另一翻译单元,编译器无法获取其定义。

历史上的 inline 关键字

早期编译器缺乏或拙于判断内联收益,C++ 引入 inline 关键字,作为提示

#include <iostream>

inline int min(int x, int y)
{
    return (x < y) ? x : y;
}

int main()
{
    std::cout << min(5, 6) << '\n';
    std::cout << min(3, 2) << '\n';
    return 0;
}

在现代 C++ 中,inline 关键字不再用于请求内联展开,原因如下:

  • 属于过早优化,误用反而损害性能;
  • 仅为提示,编译器可完全忽略;
  • 粒度不当——关键字作用于函数定义,而展开决策针对每次调用;
  • 现代优化器通常比人类更擅长判断内联时机。

最佳实践:
不要使用 inline 关键字请求函数内联。

现代语义:inline 表示“允许多重定义”

若函数(默认具有外部链接)的定义放在头文件中,且被多个 .cpp 包含,将违反一次定义规则(ODR)。
现代 C++ 中,inline 的语义演变为“允许多重定义”。内联函数可在多个翻译单元定义,只要:

  • 编译器在每个使用翻译单元可见完全相同的定义;
  • 每个翻译单元仅含一份定义;
  • 所有定义完全一致,否则导致未定义行为。

示例:

main.cpp

#include <iostream>

double circumference(double radius); // 前向声明

inline double pi() { return 3.14159; }

int main()
{
    std::cout << pi() << '\n';
    std::cout << circumference(2.0) << '\n';
    return 0;
}

math.cpp

inline double pi() { return 3.14159; }

double circumference(double radius)
{
    return 2.0 * pi() * radius;
}

两文件均定义 pi(),因标记 inline,链接器会合并为单一定义;若移除 inline,则触发 ODR 违规。

头文件中的内联函数

内联函数常置于头文件,通过 #include 供各翻译单元使用,确保定义一致。该做法尤其适用于仅头文件库,无需额外源文件或链接步骤,只需 #include 即可使用。

示例头文件:

pi.h

#ifndef PI_H
#define PI_H

inline double pi() { return 3.14159; }

#endif

main.cpp & math.cpp#include "pi.h",链接器自动去重。

高级内容:隐式内联

以下函数 隐式内联(无需 inline 关键字):

  • 类/结构体/联合体内部定义的成员函数;
  • constexpr / consteval 函数;
  • 函数模板隐式实例化。

最佳实践

仅在必要场景(如头文件定义函数/变量)使用 inline,避免滥用。

为何不把全部函数放头文件并内联?

  1. 编译时间剧增:头文件改动会导致所有包含者重编译;
  2. 重复编译:同一内联函数在 N 个翻译单元被编译 N 次;
  3. 大型项目级联重编译影响巨大。

C++17 内联变量

背景:若需跨文件共享常量,函数式写法(如 pi())显得冗余。
C++17 引入 inline 变量,允许变量在多个文件定义,规则与内联函数一致。

常见用途见 7.10 课《使用 inline 变量共享全局常量》。

相关补充

  • 隐式内联变量:类内 static constexpr 数据成员。
  • 默认情况下,constexpr 变量 内联(除上述特例)。

速览

场景是否需 inline
头文件定义函数/变量需显式 inline(除非已隐式)
源文件定义函数无需
类内成员函数隐式
constexpr / consteval 函数隐式
函数模板实例隐式

结论

仅在头文件定义需跨翻译单元共享的函数或变量时使用 inline,其余情况交由编译器优化。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"