直接写在类里面的成员函数,会被编译器自动当作内联函数(6.6 那个 inline 的"就地展开")——省掉一次函数调用的开销,代价是代码体积变大。想在类外定义还保持内联,要显式写 inline,而且必须把定义放在头文件里(因为内联函数要在每个用到它的文件里可见)。记住两句话:内联只是"建议"、编译器可以不理会;适合内联的永远是"小而热"的函数。
这是 6.6「内联函数」在类里的版本——概念完全一样,只是"在哪儿写"有了新规矩。本节还会解决 10.1.3 留下的一个悬念:那时说"头文件里不该放定义(会导致多重定义)",当时留了一句"内联函数是例外"——本节把这句话解释清楚。
一、内联是什么(复习 6.6)
普通函数的调用过程(6.3 的"调用五步"):记返回地址 → 复制实参 → 跳进去 → 交结果 → 跳回来。
内联的做法:不跳了——把函数的代码直接"塞"到调用处,像把内容抄过去一样。
inline int square(int x) { return x * x; }
int main() {
int r = square(4); // 编译后,这一行大致变成 int r = (4 * 4);
}
| 普通调用 | 内联 | |
|---|---|---|
| 有没有"跳" | 有(进出函数的开销) | 没有 |
| 速度 | 稍慢 | 稍快 |
| 代码体积 | 一份 | 每个调用处都复制一份(变大) |
| 谁决定 | —— | 编译器(内联只是建议,6.6) |
一句话:内联是"空间换时间"——用更大的可执行文件,换更少的调用开销。
二、类里函数的两种内联情况
情况 1:定义写在类里面 → 自动内联(不用写 inline)
class Point {
private:
int x, y;
public:
void set(int a, int b) { x = a; y = b; } // ← 自动内联
int getX() const { return x; } // ← 自动内联
};
你没写 inline,但编译器已经当它是内联了。 这是类的一个便利。
情况 2:定义写在类外面 → 要显式写 inline
class Point {
public:
int getX() const; // 类里只声明
};
inline int Point::getX() const { // ★ 类外定义要写 inline
return x;
}
漏了 inline 会怎样:它就变成普通函数,不内联——功能上没错,只是少了优化。
三、一个必须知道的位置规矩:内联的定义要放头文件
为什么:内联是"就地展开",编译器必须能看见函数的代码才能展开。如果 .h 里只留声明、实现藏在 .cpp 里,别的 .cpp 在编译时根本看不到实现,也就无从展开。
所以工程上的正确做法是:
| 放哪儿 | 内容 |
|---|---|
.h 头文件 |
类定义 + 内联函数的定义(包括类内定义的、和类外标了 inline 的) |
.cpp 源文件 |
普通成员函数的实现(不内联的那些) |
顺带解决 10.1.3 的悬念:
10.1.3 说"头文件里不该放定义,会导致
multiple definition"。内联函数是例外——因为标准规定:内联函数允许多个编译单元里各有一份,链接时把它们合成一份。所以类内的短函数写在头文件里完全没问题——这恰恰是它们该待的地方。
四、什么时候该内联,什么时候不该
| 情形 | 建议 | 原因 |
|---|---|---|
getX()、setX() 这种一两行的存取函数 |
✅ 内联(写在类里) | 又短又好用,省下的调用开销很实在 |
短的判断/计算(isEmpty()、area() 两三行) |
✅ 内联 | 同上 |
| 几十行的复杂函数 | ❌ 不要内联(写到 .cpp 里) |
每个调用处都塞一大段,可执行文件会鼓起来,反而更慢(缓存命中率下降) |
| 递归函数 | ❌ 内联没意义 | 递归的深度编译期不知道,展开不了 |
| 只有一次调用的函数 | ❌ 没必要 | 展开和调用差不多,还让代码变乱 |
判据一句话:"小而热"就内联——函数短(小)+ 被频繁调用(热)。两条都满足才值得。
关于"编译器可以不理会"(6.6 的重点,这里再强调):
| 你写的 | 编译器可能 |
|---|---|
标了 inline |
照样不展开(函数太大、递归、地址被取走等) |
没标 inline |
也可能帮你展开(优化级别高时) |
所以 inline 是"建议",不是"命令"。 而且——写不写 inline,程序功能完全一样,差别只在性能(有时根本看不出差别)。别为了内联把代码写得难读。
五、动手 Demo:同一段逻辑,三种写法
#include <iostream>
using namespace std;
// ── 写法 A:短函数写在类里(自动内联,推荐)──
class RectA {
private:
double w, h;
public:
RectA(double a, double b) : w(a), h(b) { }
double area() const { return w * h; } // 自动内联
bool isSquare() const { return w == h; } // 自动内联
};
// ── 写法 B:类外定义并显式 inline(效果同上)──
class RectB {
private:
double w, h;
public:
RectB(double a, double b) : w(a), h(b) { }
double area() const; // 声明
bool isSquare() const; // 声明
};
inline double RectB::area() const { return w * h; } // ★ 显式 inline
inline bool RectB::isSquare() const { return w == h; }
// ── 写法 C:不内联(类外、不加 inline),功能一样 ──
class RectC {
private:
double w, h;
public:
RectC(double a, double b) : w(a), h(b) { }
double area() const; // 声明,不内联
};
double RectC::area() const { return w * h; } // 普通函数
int main() {
RectA a(4, 4);
RectB b(3, 5);
RectC c(6, 6);
cout << "A 面积 " << a.area() << ",是正方形?" << (a.isSquare() ? "是" : "否") << endl;
cout << "B 面积 " << b.area() << ",是正方形?" << (b.isSquare() ? "是" : "否") << endl;
cout << "C 面积 " << c.area() << endl;
return 0;
}
看到什么算成功:
A 面积 16,是正方形?是
B 面积 15,是正方形?否
C 面积 36
读结果:三个类的运行结果看不出任何差别——这就对了。内联与否不影响功能,只影响编译出来的机器码(省不省那几次函数调用)。所以:内联是"性能开关",不是"功能开关"。
30 秒实验(看得见差别):在 Dev-C++ 里对写法 A 和 C 分别按 F11 编译,然后比较生成 .exe 的文件大小。多调几次 area()(比如在循环里调一万次)再比,你会发现内联版可能稍大一点。这个"稍大的体积"就是它换到的"稍快的速度"的代价。(差距常常很小——这也是"别过度纠结内联"的理由。)
六、常见误区
- "类里面写的函数必须加
inline"——不用。写在类里面就自动内联了,加不加inline一样(可以加,图个明白)。 - "内联函数一定更快"——不一定。函数一大,内联会让代码膨胀,反而拖慢(尤其影响 CPU 指令缓存)。只对"小而热"的函数有实际意义。
- "写了
inline编译器就一定会展开"——不一定。它只是建议(6.6)。编译器会根据函数大小、是否递归、是否被取地址等自行决定。 - "内联函数的定义也要放
.cpp里"——不行。内联的定义必须在头文件(或者和调用者在同一个编译单元里),否则编译器看不到代码、没法展开。内联是"头文件里放定义"这条禁令的唯一例外。 - "为了性能,把所有函数都写成内联"——这是典型的过早优化。先让代码写对、写清楚;真遇到性能瓶颈(用 6.6、6.7.3 的思路去测),再针对"热函数"做优化。盲目内联往往既没变快,又把代码改难看了。
自测三问(点开对答案)
1. 写在类里面的成员函数,需要写 `inline` 吗?——**不需要**,**自动就是内联**。写在**类外面**时才需要显式写 `inline`(不写就退化成普通函数)。 2. 内联函数的**定义**为什么必须放在头文件里?——因为内联是"**就地展开**",**编译器必须能看到函数代码**。放在 `.cpp` 里,别的文件编译时看不到,就没法展开。**这也是 10.1.3「头文件不放定义」的例外**:内联函数允许各编译单元各有一份。 3. 什么样的函数适合内联?——**"小而热"**:**函数体短**(一两行、几行)**且被频繁调用**(如 `getX()`、`area()`)。**函数体大、递归、只调用一次**的都不适合。而且 `inline` 只是**建议**,编译器可以不采纳。- ☐ 10.4.3 静态类成员(下一节)
相关笔记
- 访问类成员 —— 上一节:三种访问视角
- 静态类成员 —— 下一节:属于类不属于对象的成员
- 内联函数 —— 内联的原始解释(本节是它在类里的版本)
- 类的实现 —— "头文件不放定义"与它的例外
- 🔷 C++基础 · 索引 —— 全课路线图