C++ 类模板(Class Template)入门
类模板是 C++ 泛型编程的核心。上一篇讲了函数模板,本文接着讲类模板:如何定义、成员函数怎么写、全特化与部分特化、非类型参数、默认参数,以及模板模板参数。
一、为什么需要类模板
和函数模板同理:如果为 int、float、double 各写一个 Vector 类,逻辑完全相同,只是元素类型不同。类模板让我们写一份代码,编译器根据实例化时的类型自动生成对应版本。
二、基本定义语法
1 | template <typename T> |
使用时用 类名<具体类型> 实例化:
1 |
|
注意:类模板的名字(
Vec3)本身不是类型,必须带上参数Vec3<int>才是一个具体类型。这一点和函数模板不同——函数模板靠调用自动推导参数,类模板必须显式写出。
三、类外定义成员函数
成员函数在类内直接写很简单,但在类外定义时需要用 类名<模板参数>:: 限定,并重复 template 声明:
1 | template <typename T> |
两个要点:
- 类外定义必须以
template <typename T>开头,告诉编译器「这是一个模板的成员」; - 限定符写成
Vec3<T>::(带尖括号),不能只写Vec3::。
四、类模板特化(全特化)
和函数模板类似,类模板也能为特定类型单独写一份完整实现,这叫全特化。语法是 template <> 后跟 class 类名<具体类型>。
比如我们想给 Vec3<char> 换个实现:内部用 4 字符数组存 3 个字符 + 结尾 \0,并新增一个 asString() 把它当字符串输出:
1 | template <> |
全特化版本可以和通用模板完全不一样:成员变量、成员函数都能重新定义。
五、部分特化(partial specialization)
类模板比函数模板多一个能力:部分特化——只固定一部分模板参数,另一部分仍然泛化。
比如有个 MyPair<T1, T2>,当第一个参数是 int 时我们想走特殊实现,但第二个参数仍然任意:
1 | template <typename T1, typename T2> |
使用:
1 | MyPair<double, int> p1(1.5, 2); // 走通用模板 |
部分特化只能用于类模板,函数模板不支持部分特化(函数模板靠重载达到类似效果)。
六、非类型参数(non-type parameter)
模板参数不一定是类型,也可以是编译期常量(整型、枚举、指针等)。这种叫非类型参数。
经典用法:用整型常量指定数组大小:
1 | template <typename T, int N> |
使用:
1 | float values[5] = {1.2f, 1.3f, 1.4f, 1.5f, 1.6f}; // 注意是单 =,不是 == |
几个关键点:
- 第二个参数
5必须是编译期常量,不能是运行时变量; - 构造函数参数写成
T (&values)[N](数组的引用),会强制检查数组长度,长度不匹配直接编译失败,比裸指针安全; - 不同的
N值代表不同的类型:Array<float, 5>和Array<float, 6>是两个完全不同的类。
七、默认模板参数
和函数默认参数类似,模板参数也能给默认值:
1 | template <typename T = float, int N = 5> |
实例化时可以省略带默认值的参数:
1 | Array<> arr1(values); // 等价于 Array<float, 5> |
注意省略只能从右往左连续省略,不能跳着省。
八、模板模板参数(template template parameter)
模板参数本身也可以是另一个模板,这种叫「模板模板参数」。语法是 template <模板参数列表> class 名字。
一个典型场景:写一个 Wrapper 包装任意容器(std::vector、std::list 等):
1 |
|
使用:
1 | std::vector<int> v{1, 2, 3}; |
第一个参数 Container 接收的是「带一个类型参数的模板」(如 std::vector),而不是具体类型。这种写法比较少用,了解即可。
九、常见的坑
- 类模板的成员函数定义通常也要放头文件里。和函数模板同理,模板实例化时必须看到完整定义。只有声明会报「未定义的引用」。
- 类模板名不是类型。
Vec3不是类型,Vec3<int>才是。在函数参数、返回值里不能直接写Vec3,必须带参数(C++14 起类模板参数推导能省略一部分场景,但显式写更稳妥)。 - 部分特化不能改变参数个数。
MyPair<T1, T2>部分特化必须仍是两个参数,不能变成一个参数。 - 全特化后成员函数可以和通用模板完全不同,但部分特化一般保持接口一致,方便调用方代码不用改。
- 自己定义类名时避开
vector(小写)这种和标准库冲突的名字,用Vec3、Array、MyVector等。
十、小结
- 类模板用
template <typename T> class 类名定义,实例化时写类名<具体类型>; - 类外定义成员函数要重复
template声明,并用类名<T>::限定; - 全特化(
template <> class 类名<具体类型>)为特定类型整体重写; - 部分特化(
template <typename T2> class 类名<具体类型, T2>)只固定一部分参数,只有类模板支持; - 非类型参数用编译期常量(如
int N),不同常量值是不同类型; - 模板参数可以给默认值,从右往左连续省略;
- 模板模板参数接收另一个模板作为参数,写法是
template <typename> class 名字; - 模板实现一般放头文件;类名避开标准库名字。