inline关键字
内联函数减少调用开销,提升效率;constexpr函数在编译期求值,提高性能和常量表达能力。
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
指针,不会因为对象多而产生多个副本。
注意事项
- 适合短小函数:
- 太复杂的函数可能不会被编译器内联(即使你写了
inline
)
- 太复杂的函数可能不会被编译器内联(即使你写了
- 编译器决定是否内联:
inline
只是建议,现代编译器可能根据优化策略选择是否真正内联
- 避免在头文件中滥用:
- 多处定义同一函数时应使用
inline
以避免链接冲突(特别在头文件中定义函数时)
- 多处定义同一函数时应使用
- 不能递归内联:
- 编译器通常不会对递归函数内联
与 constexpr 的关系
constexpr
函数自动隐式为inline
,所有constexpr
函数都会自动视为 inline 函数,这样可以放心地在头文件中定义它们,同时支持编译期求值与多文件包含,避免链接冲突。- 但
inline
函数并不能保证编译期求值,它只影响链接与调用方式
与 宏的关系
宏是简单的文本替换,没有类型检查、没有作用域控制、容易出错;而 inline
/ constexpr
函数是类型安全、作用域明确、支持调试和优化的现代写法,几乎在所有场景下都优于宏。
特性/维度 | #define 宏 | inline / 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); // 编译报错:类型不兼容