inline
inline
关键字用于建议编译器将函数的调用替换为函数体本身,从而减少函数调用的开销,提高程序执行效率。
但 inline
只是建议,现代编译器会根据优化策略决定是否真正内联。
示例:
#include <iostream>
inline int square(int x) { // 建议内联展开
return x * x;
}
int main() {
std::cout << square(5) << '\n'; // 可能被展开为 std::cout << (5 * 5) << '\n';
}
可能被编译成:
std::cout << (5 * 5) << '\n';
👉 省去函数调用过程,提高运行速度。
适用于短小频繁调用的函数
适合代码少、执行频繁的小函数,比如:
数学计算(max(a, b)、square(x))
getter/setter
运算符重载
❌ 不适合大函数,因为内联会导致代码膨胀(Code Bloat)。
方法 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
inline 在头文件 | 避免链接错误,全局共享 | 可能增加编译时间(函数体重复处理) | 小型高频使用的函数 |
声明在 .h ,定义在 .cpp | 减少编译依赖,隐藏实现细节 | 模板无法使用 | 普通函数,大型项目 |
static 在头文件 | 避免链接错误 | 每 TU 一份副本,可能浪费空间,无法用于 跨文件共享全局状态(因为 static 使函数仅在当前 TU 可见)。 | 极少使用,C 风格遗留代码 |
换句话说:将所有函数在头文件中用 inline
声明并定义是可行的,但会带来一些潜在问题,需要根据具体场景权衡利弊。
普通函数 vs. inline
函数的本质区别
1️⃣ 普通函数的情况
代码示例:
// math.h
#ifndef MATH_H
#define MATH_H
int add(int a, int b); // 只声明
#endif
// math.cpp
#include "math.h"
int add(int a, int b) { // 在 math.cpp 中定义
return a + b;
}
// main.cpp
#include "math.h"
#include <iostream>
int main() {
std::cout << add(2, 3) << std::endl; // ✅ 这样不会报错
}
🔹 编译过程:
- 编译
math.cpp
- 发现
add()
的定义,并在目标文件 (math.o
) 里生成符号add
。
- 发现
- 编译
main.cpp
- 只看到
add()
的声明,不会生成add()
的实现。
- 只看到
- 链接阶段
- 链接器在
math.o
里找到了add()
的实现,并将其正确链接。
- 链接器在
✅ 所以,普通函数在 .cpp
里定义是没问题的,因为编译器知道它是一个“外部符号”,链接器最终会找到它的实现。
2️⃣ inline
函数的情况
错误示例:
// math.h
#ifndef MATH_H
#define MATH_H
inline int add(int a, int b); // ❌ 仅声明,不在头文件定义
#endif
// math.cpp
#include "math.h"
inline int add(int a, int b) { // ❌ 只在 math.cpp 里定义
return a + b;
}
// main.cpp
#include "math.h"
#include <iostream>
int main() {
std::cout << add(2, 3) << std::endl; // ❌ 链接错误:undefined reference to `add(int, int)`
}
💥 问题:在 main.cpp
里,add()
的实现找不到!
🔹 发生了什么?
- 编译
math.cpp
inline int add(int a, int b)
被定义,可能会被内联,也可能不会。
- 编译
main.cpp
- 只看到
inline int add(int a, int b);
声明,但编译器需要看到完整的定义,否则无法展开。 - 由于
add()
是inline
,编译器不会默认把它当成外部符号,也不会生成add()
的符号表项。
- 只看到
- 链接阶段
main.o
里找不到add()
的实现,导致 undefined reference 错误。
❌ 根本原因:
- 普通函数的声明是外部声明,意味着编译器知道它的定义会在其他编译单元(
.cpp
文件)中找到。 inline
函数的本质是每个.cpp
里都需要看到完整的定义,才能决定是否展开它。- 但
main.cpp
里没有add()
的定义,导致链接失败。
所以inline 函数无论是全局函数、类的 public 还是 private 成员函数,都应该在头文件中同时声明和实现。
constexpr
和常量表达式
常量表达式(const expression)
是指值不会改变并且在编译过程就能得到计算结果的表达式。显然,字面值属于常量表达式,用常量表达式初始化的const
对象也是常量表达式。后面将会提到,C++
语言中有几种情况下是要用到常量表达式的。
我们先在global.h中声明一个全局函数返回固定大小
extern int GetSize();
在global.cpp中实现
int GetSize(){
return 20;
}
然后我们用const定义一些常量表达式
一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定,例如
{
//max_files是一个常量表达式
const int max_files = 20;
//limit是一个常量表达式
const int limit = max_files + 10;
//staff_size不是常量表达式,无const声明
int staff_size = 20;
//sz不是常量表达式,运行时计算才得知
const int sz = GetSize();
}
尽管staff_size
的初始值是个字面值常量,但由于它的数据类型只是一个普通int
而非const int
,所以它不属于常量表达式。
另一方面,尽管sz
本身是一个常量,但它的具体值直到运行时才能获取到,所以也不是常量表达式。
在一个复杂系统中,很难(几乎肯定不能)分辨一个初始值到底是不是常量表达式。
当然可以定义一个const
变量并把它的初始值设为我们认为的某个常量表达式,但在实际使用时,尽管要求如此却常常发现初始值并非常量表达式的情况。
C++11新标准
C++11新标准规定,允许将变量声明为constexpr
类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr
的变量一定是一个常量,而且必须用常量表达式初始化:
//20是一个常量表达式
constexpr int mf = 20;
//mf+1是一个常量表达式
constexpr int limit = mf + 10;
//错误,GetSize()不是一个常量表达式,需要运行才能返回
//constexpr int sz = GetSize();
尽管不能使用普通函数作为constexpr
变量的初始值,新标准允许定义一种特殊的constexpr
函数。
这种函数应该足够简单以使得编译时就可以计算其结果,这样就能用constexpr
函数去初始化constexpr
变量了。
我们在global.h
中定义一个constexpr
函数
inline constexpr int GetSizeConst() {
return 1;
}
为了避免在多个源文件中包含同一个头文件而导致的多重定义错误,可以将 constexpr
函数声明为 inline
。
inline
关键字允许在多个翻译单元中定义同一个函数,而不会引起链接错误。
接下来在定义一个constexpr
变量就行了
constexpr int sz = GetSizeConst();
指针和constexpr
必须明确一点,在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关:
//p是一个指向整形常量的指针
const int * p = nullptr;
//q是一个指向整数的常量指针
constexpr int *q = nullptr;
一个constexpr
指针的初始值必须是nullptr
或者0,或者是存储于某个固定地址中的对象。
函数体内定义的变量一般来说并非存放在固定地址中,因此constexpr
指针不能指向这样的变量。
定义于所有函数体之外的对象其地址固定不变,能用来初始化constexpr
指针
global_i是一个全局变量
//constexpr指针只能绑定固定地址
//constexpr int *p = &mvalue;
constexpr int *p = nullptr;
//可以绑定全局变量,全局变量地址固定
constexpr int *cp = &global_i;
可以修改constexpr
指向的内容
constexpr int *p = &global_i;
//修改p指向的内容数据
*p = 1024;
问题
global_i是一个全局变量,下面这个指针是什么类型?能否修改cp
指向的数据的内容(*cp = 200
)?
constexpr const int * cp = &global_i;
如果 constexpr double PI_CONST = 3.14159;
需要在多个 .cpp
文件中使用,正确的做法是在头文件中声明和定义,这样每个包含这个头文件的 .cpp
文件都可以直接使用它。
✅ constexpr
变量不需要 extern
,可以直接在头文件定义,因为它默认 inline
,不会引发链接错误。
✅ const
变量需要 extern
处理,否则会在多个 .cpp
文件中重复定义,引发链接错误。
C++17 及以后,推荐用 constexpr
代替 const
进行全局常量声明!