🔷 第十二章 模板
📄 一句话概括

定义类模板时,template <typename T> 只写在类的外面,类名后面绝对不能写 <T>(class Point<T> { 是错的)。而成员函数一旦搬到类外定义,就有三处不能漏:① 每个函数前都要重新写 template <typename T>;② 类名要写成 Point<T>::;③ 返回类型前面也要有模板头。漏任何一处都报错,而且报错信息各不相同。另外记住:类模板的声明与定义必须放在同一个头文件里(原因和 12.1.1 一样)。

📝 本节和前面几节的关系

12.2 讲了"类模板是什么",本节讲"怎么写不出错"——全是细节,但每一条都对应一个具体的报错。它和 10.1.2 类的声明与定义是一对:那里学的是普通类的"类内定义 vs 类外实现(类名::)",本节是它的"模板版",难点全在"要在 类名:: 前面再加一行模板头"。

一、语法拆解:类模板本身怎么写

template <typename T>            // ① 模板头:写在类的外面
class Point {                    // ② ★ 类名后面"不写" <T>!
private:
    T x, y;                      // ③ 成员的类型可以是 T

public:
    Point(T a = T(), T b = T())  // ④ 构造函数:参数类型是 T
        : x(a), y(b) { }         //    初始化列表:把 T 的值喂给成员(10.2.1)

    T getX() const { return x; } // ⑤ 类内定义:自动内联(10.4.2)
    void show() const;           // ⑥ 类内声明,定义放类外(下一节)
};

三个最容易搞混的点:

位置 写什么 例子
类外 的模板头 要写 template <typename T>
类定义 的名字 只写类名,不带 <T> class Point { ✅ / class Point<T> { ❌
用的时候 必须带 <具体类型> Point<int> p; ✅ / Point p; ❌

一句话记牢:"定义类模板时不写 <T>,使用它时必须写。" 中间那个"类内声明、类外定义"的成员函数,才是下面要小心的地方。

二、成员函数在类外定义:三处不能漏

这是本节的核心,也是模板语法里报错最多的地方。

template <typename T>                          // ★ ① 每个成员函数前都要重新写模板头
void Point<T>::show() const {                  // ★ ② 类名写 Point<T>    ③ 写完整返回类型
    cout << "(" << x << ", " << y << ")" << endl;
}

对比一下普通类(10.1.2):

普通类 类模板
模板头 没有 每个函数定义前都要有一行 template <...>
类名限定 Point::show Point<T>::show(多一个 <T>)
返回类型 直接写 直接写(但前面必须有模板头)

三处漏掉的后果各不相同(认准这三种报错):

漏了什么 报错(GCC/Dev-C++ 的典型措辞) 意思
漏了 template <typename T> error: 'T' was not declared in this scope 编译器不认识 T 了——因为你没声明它是模板参数
类名漏了 <T>(写成 Point::show) error: invalid use of template-name 'Point' without an argument list 它知道 Point 是个模板名,但你没给模板实参
构造函数的类名写错(如 Point::Point<T>) error: expected initializer before '<' token 构造函数名后面不能带 <T>(<T> 只跟类名走)

构造函数的特例:类名在 :: 前面,不带 <T> 的只有"构造函数的名字"那半边:

template <typename T>
Point<T>::Point(T a, T b) : x(a), y(b) { }      // ★ 前面 Point<T> 带,后面 Point 不带
//    ↑ 带 <T>          ↑ 不带 <T>

记法::: 前面是"哪个类"(要带 <T>),:: 后面是"哪个函数"(构造函数的名字就是类名,不带 <T>)。

三、多参数模板:每处都要写全

template <typename K, typename V>          // ★ 两个参数
class Dict {
private:
    K key;
    V val;
public:
    Dict(K k, V v) : key(k), val(v) { }
    V getValue() const { return val; }
};

template <typename K, typename V>          // ★ 类外定义也要写两个
V Dict<K, V>::getValue2() const { ... }     // ★〈K, V〉一起写,顺序别换

要点:模板参数表要"原样重写"——参数个数不同、顺序不同,都会报错(顺序换了甚至会报奇怪的 no matching function)。

四、铁律:类模板的声明与定义必须在一起

12.1.1 讲过函数模板为什么不能拆 .h / .cpp——类模板同理,而且更容易踩,因为类模板通常"类内声明、类外定义",很容易顺手把类外定义挪到别的文件里。

// Point.h
template <typename T>
class Point {
    T x, y;
public:
    Point(T a, T b);
    void show() const;
};

// Point.cpp            ← ★ 把这两个定义放这里
template <typename T>
Point<T>::Point(T a, T b) : x(a), y(b) { }

template <typename T>
void Point<T>::show() const { ... }

// main.cpp
#include "Point.h"
int main() { Point<int> p(1, 2); p.show(); }     // ★ 链接时报 undefined reference

报错:

undefined reference to `Point<int>::show() const'
undefined reference to `Point<int>::Point(int, int)'

原因(和 12.1.1 完全一样):main.cpp 里实例化 Point<int> 时,编译器必须看到成员函数的定义——它在 Point.cpp 里,main.cpp 看不见。

所以规矩是:

类模板的类定义 + 全部成员函数定义,一律写在同一个 .h 里。 把"类内声明、类外定义"理解成"写在同一个文件里的两段",而不是"两个文件"。

注意:这和 10.4.2 内联成员函数是同一个道理(那里的例外也是"定义要进头文件")。标准库的头文件之所以"又长又全是代码",就是因为这条规则。

五、动手 Demo:Point<T>

#include <iostream>
#include <string>
using namespace std;

template <typename T>
class Point {
private:
    T x, y;

public:
    // 类内定义(自动内联)
    Point(T a = T(), T b = T()) : x(a), y(b) { }
    T    getX() const { return x; }
    T    getY() const { return y; }
    void move(T dx, T dy) { x += dx; y += dy; }

    // 类内声明,类外定义
    void show() const;
};

// ★ 类外定义:模板头 + Point<T>::
template <typename T>
void Point<T>::show() const {
    cout << "(" << x << ", " << y << ")" << endl;
}

int main() {
    Point<int>    a(1, 2);
    Point<double> b(1.5, 2.5);
    Point<string> c("左", "右");           // ★ T 直接换成 string 也行

    a.show();
    b.show();
    c.show();

    a.move(10, 20);                        // ★ move 的参数也是 T
    a.show();

    return 0;
}

看到什么算成功:

(1, 2)
(1.5, 2.5)
(左, 右)
(11, 22)

读结果:

现象 说明
三个不同类型的 Point 都能用 编译器造出了 Point<int>、Point<double>、Point<string> 三个类
Point<string> c("左", "右") 能编过 T 换成 string 后,x += dx 会走 string 自己的 +=(追加)——move 在这里的语义是"把两个字符串各追加一份",这就是模板的边界:它只保证"能编译",不保证"语义合理"
a.move(10, 20) 得 (11, 22) T 是 int,x += dx 就是加法

30 秒实验(把三种报错各看一遍):

  1. 删掉 void Point<T>::show 上面那行 template <typename T> → 报 'T' was not declared in this scope。
  2. 加回来,把 Point<T>::show 改成 Point::show → 报 invalid use of template-name 'Point' without an argument list。
  3. 加回来,把构造函数写成 template <typename T> Point::Point(T a, T b) : x(a), y(b) { } → 报 invalid use of template-name 'Point' without an argument list(和上一条同名,但位置不同)。
  4. 再去 main 里把 Point<int> a(1, 2) 改成 Point a(1, 2) → 报 class template argument required for 'class Point'。
  5. 四个错各看一遍,以后见到就知道往哪儿找。

六、常见误区

  1. "定义类模板时要写 class Point<T>"——不用,而且是错的。<T> 只在"使用/特化/类外成员定义"时出现,定义模具本身时不写。
  2. "类外定义成员函数不用再写模板头"——必须写,而且每个函数都要写一遍。这是"T 的作用域只到紧跟的那个定义为止"(12.1.1)带来的后果。
  3. "类名写 Point 就够了"——要写 Point<T>。漏了报 invalid use of template-name ... without an argument list。
  4. "构造函数两边都要带 <T>"——:: 前带、:: 后不带(构造函数的名字就是类名,Point::Point)。
  5. "类模板可以把成员函数定义放 .cpp"——不行,会报 undefined reference。整个模板(类 + 全部成员函数)写在一个 .h 里。
  6. "T 随便换什么类型都能正常工作"——它只保证"能编译"。上面 Point<string>::move 用 += 追加字符串,技术上跑得通、语义上是荒谬的。模板是"检查语法",不是"检查语义"(这也是 12.2.5 要"有界"的原因——把规则写进代码里)。
自测三问(点开对答案) 1. 定义类模板时,类名后面要不要写 ``?什么时候必须写?——**定义模具时不写**(`class Point {`);**使用它时必须写**(`Point p;`);**类外定义成员函数时要写**(`void Point::show()`)。 2. 成员函数在类外定义时,哪三处不能漏?——**① 前面要有 `template `;② 类名写成 `Point::`;③ 返回类型要写全(模板头在它前面)。** 构造函数则记成"**`::` 前带 ``、`::` 后不带**"。 3. 为什么类模板的成员函数定义不能放进 `.cpp`?——因为**实例化点在别的 `.cpp` 里**,编译器在那儿**必须看到成员函数的定义**才能生成代码;看不到就只能在链接期报 **`undefined reference to 'Point::show() const'`**。

相关笔记