C++ 内联函数

Posted by KalosAner on March 18, 2025

一、引言

内联(inline)函数的主要作用就是在编译阶段把内联函数的代码直接复制到调用该代码的地方。这样做可以省去调用函数的开销,提高效率。但是当调用该函数的地方太多时,会导致汇编代码过长,而且会增长编译时间。此外,内联函数不可以是递归函数,并且通常建议 inline 函数不超过 10 行代码。

inline 必须在函数定义时才生效,仅在函数声明时添加无效。

二、C++ 17 新特性

在 C++ 17 的更新中 inline 是不生效的,是否内联由编译器决定。也就是必须通过优化来进行内联,即使用 g++ -S inlinefunc.cpp -O2 进行编译。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

inline int func(int x) {
    int y = x + 1;
    ++ y;
    return y + rand();
}

int main() {
    std::cout << func(11) << std::endl;
    return 0;
}

无论代码的第 3 行加不加 inline 关键字的编译结果都是一样的。

不启用 O2 优化:g++ -S inlinefunc.cppmain 函数中就会调用 func 函数。

Snipaste_2025-03-18_21-58-00

启用 O2 优化:g++ -S inlinefunc.cpp -O2main 函数就不会调用 func 函数,而是直接调用 rand 函数。

Snipaste_2025-03-18_21-57-05

所以在新版本中 inline 的作用就是允许被多次包含。

三、多次包含

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// config.h
inline double PI = 3.14159; // C++17 允许在头文件中定义内联变量
double funcA();
double funcB();

// a.cpp
#include "config.h"
void funcA() {
    double radius = 2.0;
    double area = PI * radius * radius; // 使用 PI
}

// b.cpp
#include "config.h"
void funcB() {
    double circumference = 2 * PI * 5.0; // 使用 PI
}

// mian.cpp 随便写个 main 函数,不然会报错
#include <iostream>
#include "config.h"

using namespace std;

int main() {
    cout << funcA() << endl;
    cout << funcB() << endl;
    return 0;
}

如上代码,如果 config.h 中不对 PI 进行 inline 修饰在编译链接时就会报错。并且使用 inline 修饰的变量是全局共享的,在任意位置改变 PI 的值,其他地方使用到的 PI 也会同步变化。

inline 变量会被编译器标记为“弱符号”(weak symbol),链接时若发现多个相同定义(不同定义会报错),会选择其中一个并忽略其他重复项。

如果对 PI 进行 const 修饰的话,即使不加 inline 也不会报错。因为每个包含该头文件的 .cpp 文件会生成自己的 const 变量实例,但不会暴露给其他编译单元。

四、多次定义

以下代码使用 g++ a.cpp b.cpp main.cpp -std=c++17 进行编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// a.cpp
inline double PI = 3.14;

// b.cpp
inline double PI = 3.1415;

// main.cpp
#include <iostream>
using namespace std;
// extern double PI;

int main() {
    // cout << PI << endl;
    return 0;
}

编译通过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// a.cpp
inline double PI = 3.14;

// b.cpp
double PI = 3.1415;

// main.cpp
#include <iostream>
using namespace std;
extern double PI;

int main() {
    cout << PI << endl;
    return 0;
}

编译通过,输出 3.1415。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// a.cpp
double PI = 3.14;

// b.cpp
double PI = 3.1415;

// main.cpp
#include <iostream>
using namespace std;
extern double PI;

int main() {
    cout << PI << endl;
    return 0;
}

编译失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
// a.cpp
double PI = 3.14;

// b.cpp
double PI = 3.1415;

// main.cpp
#include <iostream>
using namespace std;

int main() {
    return 0;
}

编译失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
// a.cpp
inline double PI = 3.14;

// b.cpp
inline double PI = 3.1415;

// main.cpp
#include <iostream>
using namespace std;

int main() {
    return 0;
}

编译通过。

五、inlinestatic

static 修饰的变量或者函数也可以在多个 .cpp 文件中定义的,但是 static 修饰是有作用域的,仅限定本文件进行访问。

对于如下代码,使用 static 修饰 PI 变量则 27 代码不会影响 funcAfuncB 的输出,但是使用 inline 修饰就会改变 funcAfuncB 的输出。这表示 static 修饰的变量的作用域只在本地,而 inline 修饰的变量是所有文件共享的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// config.h
inline double PI = 3.14159; // C++17 允许在头文件中定义内联变量
double funcA();
double funcB();

// a.cpp
#include "config.h"
void funcA() {
    double radius = 2.0;
    double area = PI * radius * radius; // 使用 PI
}

// b.cpp
#include "config.h"
void funcB() {
    double circumference = 2 * PI * 5.0; // 使用 PI
}

// mian.cpp 随便写个 main 函数,不然会报错
#include <iostream>
#include "config.h"

using namespace std;

int main() {
    cout << PI << endl;
    PI = 3.1415;
    cout << funcA() << endl;
    cout << funcB() << endl;
    return 0;
}

六、O2 优化

O0:无优化,代码直接翻译,便于调试但效率最低。

O1:基础优化(如删除未使用变量、简化分支),编译速度较快。

O2:性能优化,优化通过减少指令数量、优化寄存器使用、简化控制流等方式加速代码执行。

O3:激进优化(如更深度循环展开、伪寄存器网络),可能导致代码膨胀或兼容性问题。

O2 优化的具体技术

关键优化选项

函数内联:对简单函数直接内联调用代码(-finline-functions),减少跳转开销。 • 寄存器分配:优先将频繁使用的变量放入寄存器(-fregmove),加速数据访问。 • 分支预测优化:调整条件分支顺序以匹配 CPU 流水线特性(-fguess-branch-probability)。 • 全局子表达式消除:跨函数分析并合并重复计算(-fgcse),减少内存访问。

对代码结构的影响

指令重排:编译器可能改变代码执行顺序以优化流水线效率,但可能破坏依赖内存顺序的逻辑。 • 代码膨胀:内联和循环展开会增加代码体积,可能影响 CPU 缓存命中率。

典型应用场景

算法竞赛:用于“卡常”(避免时间超限),例如洛谷等 OJ 平台允许开启 O2 以通过部分暴力解法。 • 高性能计算:加速数值计算密集的代码(如矩阵运算、物理模拟)。 • 嵌入式系统:优化实时性要求高的代码段,但需谨慎避免资源超限。

潜在风险

调试困难:优化后代码与源码差异大,断点和变量值可能无法准确对应。 • 未定义行为:优化可能暴露代码中的隐藏错误(如未初始化变量、越界访问)。 • 平台依赖:某些优化可能在不同架构(如 x86 与 ARM)上表现不一致。

使用时在代码开头添加以下指令(以 GCC 为例):

1
2
3
#pragma GCC optimize("O2")
// 或开启更激进优化组合
#pragma GCC optimize("Ofast,inline")

或在编译命令中指定 -O2 选项。