一、根本区别
宏是无脑的文本替换:
- 在预处理阶段机械替换,不经过编译器语义检查
inline是有脑的编译器建议:
- 本质是带类型检查的函数,但最终是否内联展开,由编译器根据复杂、优化级别决定
二、四大核心区别
|----------|------------------|------------------|
| 对比 | 宏函数#define | inline函数 |
| 处理阶段 | 预处理阶段(纯文本替换,不编译) | 编译阶段(编译器会尝试内联展开) |
| 类型检查 | 无 | 严格的类型检查 |
| 参数求值 | 会多次求值(存在副作用) | 只求值一次 |
| 调试能力 | 无法调试 | 可打断点调试 |
| 作用域 | 全局污染 | 遵循C++作用域规则 |
- 代码展示多次求值的副作用
cpp
#define SQUARE(x) ((x) * (x))
inline int square(int x) { return x * x; }
int main() {
int a = 3;
int b = 3;
int r1 = SQUARE(++a); // 展开: ((++a) * (++a)) => a 变成了 5,结果是 5*5=25(UB,结果取决于编译器)
int r2 = square(++b); // 先计算 ++b(b=4),传进去,结果为 16。安全!
}
三、inline失效的四大场景
1)、编译器认为函数太复杂,存在代码膨胀情况而进行拒绝
- 场景:函数体包含循环(for/while),递归,switch分支过多,或静态变量声明时,编译器通常会自动忽略inline请求,把它当做普通函数进行处理
cpp
inline int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1); // ❌ 递归,编译器基本不会内联
}
inline int complex_calc() {
int sum = 0;
for (int i = 0; i < 1000; ++i) sum += i; // 循环太长,通常不会内联
return sum;
}
2)、函数通过函数指针调用
- 场景:当把inline函数赋值给函数指针,或者通过函数指针回调时,编译器必须生成该函数的实际地址(可执行实体),此时内联无法展开
cpp
inline void printHello() { std::cout << "Hello"; }
void invoke(void (*func)()) { func(); }
int main() {
void (*ptr)() = printHello; // 获取了地址,编译器被迫生成真实的函数体
invoke(ptr); // 这里调用的是函数指针,无法内联
}
3)、虚函数调用(动态多态)
- 场景:通过基类指针或引用调用virtual inline函数时,由于调用目标在运行时才能确定(查虚表),编译器无法再编译期展开内联函数
cpp
class Base { public: virtual inline void foo() {} };
class Derived : public Base { public: void foo() override {} };
void test(Base* b) {
b->foo(); // 运行时多态,无法内联(除非编译器能确定b的确切类型并去虚拟化)
}
4)、构造、析构函数
- 构造函数在头文件的类体内,如果变量很多的话,编译器会拒绝内联
四、inline的底层本质
- inline对于编译器而言,只是建议,不是命令,会根据情况,有可能不会展开
五、现代C++替代宏函数
-
替代普通宏函数:使用 inline函数+模板
-
替代类型通用宏: 使用auto + 模板
-
编译期常量计算宏: 使用constepr