文章

inline关键字

内联函数减少调用开销,提升效率;constexpr函数在编译期求值,提高性能和常量表达能力。

inline关键字

inline 关键字

内联函数

C++ 中的 内联函数(inline function) 是一种用于提升小函数执行效率的技术,通过在编译阶段将函数调用处替换为函数体代码,从而避免函数调用带来的开销(如压栈、跳转等)。

1
2
3
inline 返回类型 函数名(参数列表) {
    // 函数体
}
  • 减少函数调用开销(尤其适用于频繁调用的短小函数)

  • 增强可读性,像宏那样替代代码块,但具备类型检查与作用域管理

示例:

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

inline int add(int a, int b) {
    return a + b;
}

int main() {
    int x = 3, y = 5;
    cout << "Sum: " << add(x, y) << endl;
    return 0;
}

经过内联优化后,add(x, y) 会被直接替换为其函数体 x + y,所以主函数大致会被编译器重写为这样:

1
2
3
4
5
int main() {
    int x = 3, y = 5;
    std::cout << "Sum: " << (x + y) << std::endl;
    return 0;
}

这只是逻辑上的等价替换,编译器最终生成的机器码可能远比这个复杂,也会受编译选项影响(如 -O2, -O3 优化级别)

ODR(One Definition Rule)

C++ 要求每个函数或变量在整个程序中只能有一个定义。如果把函数定义(非声明)放在头文件中,并在多个 .cpp 文件中 #include 了这个头文件,就相当于在多个地方都定义了一遍同一个函数。这就会导致 链接错误(multiple definition)

1
error: multiple definition of 'xxx'; first defined here...

将函数标记为 inline,就告诉编译器和链接器:“这个函数即使在多个编译单元中定义了,只要定义内容相同,是可以共存的”。

这是 C++ 标准中特意为支持头文件中定义函数而设计的机制。

类与内联函数

类中直接定义的函数默认是内联的,示例:

1
2
3
4
5
6
class MyClass {
public:
    int getX() const { return x; }  // 在类定义中写了函数体,就是内联函数
private:
    int x = 10;
};

这个 getX() 函数是一个内联成员函数,等价于写在类外并加上 inline

1
2
3
4
5
6
7
8
9
10
class MyClass {
public:
    int getX() const;
private:
    int x = 10;
};

inline int MyClass::getX() const {
    return x;
}

三种写法都可以让函数成为“内联函数”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 写法一:类内定义(自动内联)
class A {
    int get() const { return 1; }
};

// 写法二:类外定义 + inline
class B {
    int get() const;
};
inline int B::get() const { return 2; }

// 写法三:类内声明 + inline 关键字(可选)
class C {
    inline int get() const { return 3; } // inline 是可加可不加
};

non-inline 成员函数

代码唯一实例,所有对象共享:每一个 non-inline 的成员函数只会诞生一个函数实例。

C++ 类里面的成员函数本质上和普通函数差不多,只是编译器在调用时会隐式加一个 this 指针参数(即 ClassName* this)。

  • inline 成员函数 通常写在类定义里面的成员函数会被默认当作 inline,编译器可能在调用点直接展开,可能生成多个实例(每个翻译单元里都可能有拷贝,靠链接时合并)。
  • non-inline 成员函数 也就是在类外定义、没有 inline 修饰的普通成员函数。

“只会诞生一个函数实例”的意思指的是:

  • 无论有多少个对象,甚至无论有多少个不同的翻译单元(源文件)引用这个类,该非内联成员函数在最终可执行文件中只会保留一个实体
  • 每个对象在调用成员函数时,实质上只是把自身的 this 指针传入同一个函数体。

也就是说,成员函数的代码不会随着对象数量的增加而重复生成。

举个例子:

1
2
3
4
5
6
7
8
9
class Foo {
public:
    void bar();   // non-inline 成员函数
};

void Foo::bar() {
    // 一段普通函数逻辑
    // 编译器背后其实是: void bar(Foo* this)
}

无论写:

1
2
3
4
Foo f1, f2, f3;
f1.bar();
f2.bar();
f3.bar();
  • 函数体只有一份(在可执行文件里就是一段机器指令)。
  • 调用 f1.bar() 实际上是 bar(&f1)f2.bar()bar(&f2)

成员函数的实现和普通函数一样,编译器在最终程序里只保留一个函数体,所有对象在调用时只是传入不同的 this 指针,不会因为对象多而产生多个副本。

注意事项

  1. 适合短小函数
    • 太复杂的函数可能不会被编译器内联(即使你写了 inline
  2. 编译器决定是否内联
    • inline 只是建议,现代编译器可能根据优化策略选择是否真正内联
  3. 避免在头文件中滥用
    • 多处定义同一函数时应使用 inline 以避免链接冲突(特别在头文件中定义函数时)
  4. 不能递归内联
    • 编译器通常不会对递归函数内联

与 constexpr 的关系

  • constexpr 函数自动隐式为 inline,所有 constexpr 函数都会自动视为 inline 函数,这样可以放心地在头文件中定义它们同时支持编译期求值与多文件包含,避免链接冲突。
  • inline 函数并不能保证编译期求值,它只影响链接与调用方式

与 宏的关系

宏是简单的文本替换,没有类型检查、没有作用域控制、容易出错;而 inline / constexpr 函数是类型安全、作用域明确、支持调试和优化的现代写法,几乎在所有场景下都优于宏。

特性/维度#defineinline / constexpr 函数
编译阶段预处理器阶段(字符串替换)编译器阶段(语义完整,类型检查)
是否有作用域没有作用域有作用域,像正常函数一样
类型检查没有类型检查有完整类型检查
调试支持不易调试(无法设置断点)可调试,可单步进入
多次定义是否报错不报错,但可能出现副作用inline/constexpr 函数合法
函数特性支持(递归等)不支持constexpr 可递归(C++14 起)
命名空间支持不支持支持命名空间和类作用域
是否有副作用风险高风险:参数可能重复求值无副作用:参数求值一次
推荐程度尽量避免推荐使用

宏有副作用

1
2
3
4
5
#define SQUARE(x) ((x) * (x))

int a = SQUARE(5);     // 结果是 25
int b = SQUARE(1 + 2); // 结果是 ((1 + 2) * (1 + 2)) = 9(正确)
int c = SQUARE(i++);   // 有副作用!i 会被自增两次

宏没有类型检查

1
2
3
4
5
6
7
8
#define MAX(x, y) ((x) > (y) ? (x) : (y))

constexpr int max(int x, int y) {
    return x > y ? x : y;
}

int x = MAX("hello", 5);  // 编译不报错,但运行出错
int y = max("hello", 5);  // 编译报错:类型不兼容
本文由作者按照 CC BY 4.0 进行授权