定义类模板时,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 秒实验(把三种报错各看一遍):
- 删掉
void Point<T>::show上面那行template <typename T>→ 报'T' was not declared in this scope。 - 加回来,把
Point<T>::show改成Point::show→ 报invalid use of template-name 'Point' without an argument list。 - 加回来,把构造函数写成
template <typename T> Point::Point(T a, T b) : x(a), y(b) { }→ 报invalid use of template-name 'Point' without an argument list(和上一条同名,但位置不同)。 - 再去
main里把Point<int> a(1, 2)改成Point a(1, 2)→ 报class template argument required for 'class Point'。 - 四个错各看一遍,以后见到就知道往哪儿找。
六、常见误区
- "定义类模板时要写
class Point<T>"——不用,而且是错的。<T>只在"使用/特化/类外成员定义"时出现,定义模具本身时不写。 - "类外定义成员函数不用再写模板头"——必须写,而且每个函数都要写一遍。这是"
T的作用域只到紧跟的那个定义为止"(12.1.1)带来的后果。 - "类名写
Point就够了"——要写Point<T>。漏了报invalid use of template-name ... without an argument list。 - "构造函数两边都要带
<T>"——::前带、::后不带(构造函数的名字就是类名,Point::Point)。 - "类模板可以把成员函数定义放
.cpp"——不行,会报undefined reference。整个模板(类 + 全部成员函数)写在一个.h里。 - "
T随便换什么类型都能正常工作"——它只保证"能编译"。上面Point<string>::move用+=追加字符串,技术上跑得通、语义上是荒谬的。模板是"检查语法",不是"检查语义"(这也是 12.2.5 要"有界"的原因——把规则写进代码里)。
自测三问(点开对答案)
1. 定义类模板时,类名后面要不要写 `- ☐ 12.2.2 简单类模板(下一节)