【C++】007、宏与inline的区别

一、根本区别

宏是无脑的文本替换:

  • 在预处理阶段机械替换,不经过编译器语义检查

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