🔷 第十章 类和对象
📄 一句话概括

直接写在类里面的成员函数,会被编译器自动当作内联函数(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()(比如在循环里调一万次)再比,你会发现内联版可能稍大一点。这个"稍大的体积"就是它换到的"稍快的速度"的代价。(差距常常很小——这也是"别过度纠结内联"的理由。)

六、常见误区

  1. "类里面写的函数必须加 inline"——不用。写在类里面就自动内联了,加不加 inline 一样(可以加,图个明白)。
  2. "内联函数一定更快"——不一定。函数一大,内联会让代码膨胀,反而拖慢(尤其影响 CPU 指令缓存)。只对"小而热"的函数有实际意义。
  3. "写了 inline 编译器就一定会展开"——不一定。它只是建议(6.6)。编译器会根据函数大小、是否递归、是否被取地址等自行决定。
  4. "内联函数的定义也要放 .cpp 里"——不行。内联的定义必须在头文件(或者和调用者在同一个编译单元里),否则编译器看不到代码、没法展开。内联是"头文件里放定义"这条禁令的唯一例外。
  5. "为了性能,把所有函数都写成内联"——这是典型的过早优化。先让代码写对、写清楚;真遇到性能瓶颈(用 6.6、6.7.3 的思路去测),再针对"热函数"做优化。盲目内联往往既没变快,又把代码改难看了。
自测三问(点开对答案) 1. 写在类里面的成员函数,需要写 `inline` 吗?——**不需要**,**自动就是内联**。写在**类外面**时才需要显式写 `inline`(不写就退化成普通函数)。 2. 内联函数的**定义**为什么必须放在头文件里?——因为内联是"**就地展开**",**编译器必须能看到函数代码**。放在 `.cpp` 里,别的文件编译时看不到,就没法展开。**这也是 10.1.3「头文件不放定义」的例外**:内联函数允许各编译单元各有一份。 3. 什么样的函数适合内联?——**"小而热"**:**函数体短**(一两行、几行)**且被频繁调用**(如 `getX()`、`area()`)。**函数体大、递归、只调用一次**的都不适合。而且 `inline` 只是**建议**,编译器可以不采纳。

相关笔记