类模板基础:定义、成员与实例化
说到类模板,我得先坦白一件事——我刚开始学C++那会儿,总觉得模板是种“黑魔法”。明明写一个类就能搞定的事,为什么要搞出个模板来?直到我在一个项目中需要同时支持int、float和自定义的BigNumber类型……嗯,那次我老老实实把类模板啃完了。
说白了,类模板就是“类的模具”。你给它一个类型参数,它帮你生成一个具体的类。这跟函数模板的思路一脉相承,但类模板要处理的东西更复杂——成员变量、成员函数、继承关系,全都要跟着类型参数走。
类模板的定义
先看一个最朴素的例子。假设我们要写一个简单的栈,支持任意类型:
template <typename T>
class Stack {
private:
std::vector<T> elements;
public:
void push(const T& value);
void pop();
T top() const;
bool empty() const { return elements.empty(); }
};
注意这个 template <typename T> 前缀。它告诉编译器:接下来这个类是个模板,T是类型参数。你可以把T理解成一个占位符,等真正用的时候再填上具体类型。
关键点:类模板的声明和定义通常要放在同一个头文件中。为什么?因为实例化时编译器需要看到完整的定义。我见过不少新手把声明放.h、定义放.cpp,结果链接报错——这就是所谓的“模板编译模型”问题。
你可能会问:能不能用class代替typename?当然可以。我个人习惯用typename,因为更直观——它明确表示“这是一个类型”。class容易让人误以为只能传类类型,其实int、double这些内置类型也行。
成员函数的模板化
类模板的成员函数,定义时也要带上template前缀。看代码:
template <typename T>
void Stack<T>::push(const T& value) {
elements.push_back(value);
}
template <typename T>
void Stack<T>::pop() {
if (empty()) {
throw std::out_of_range("Stack<T>::pop(): empty stack");
}
elements.pop_back();
}
template <typename T>
T Stack<T>::top() const {
if (empty()) {
throw std::out_of_range("Stack<T>::top(): empty stack");
}
return elements.back();
}
注意写法:template <typename T> 后面跟着 void Stack<T>::push(...)。这个 Stack<T> 表明我们正在定义类模板Stack的成员,T是模板参数。
小技巧:如果成员函数体很短,比如上面的empty(),可以直接在类内部定义。这样编译器会隐式地把它当作inline函数,省去外部定义的麻烦。
我曾经在一个项目中把所有成员函数都写在类外部,结果头文件变得又长又乱。后来我学乖了:短函数放类内,长函数放类外。代码可读性提升不少。
类模板的实例化
实例化,就是给模板参数填上具体类型。编译器会为你生成一个真正的类。
int main() {
Stack<int> intStack; // 实例化为 int 版本
Stack<std::string> strStack; // 实例化为 string 版本
intStack.push(42);
strStack.push("hello");
std::cout << intStack.top() << std::endl; // 输出 42
std::cout << strStack.top() << std::endl; // 输出 hello
return 0;
}
这里 Stack<int> 和 Stack<std::string> 是两个完全不同的类。它们各自拥有独立的成员变量和成员函数。你想想看,intStack和strStack之间没有任何关系——除了它们都来自同一个模板。
注意:类模板的实例化发生在编译期。如果你在代码中使用了 Stack<int>,编译器就会生成一份完整的 Stack<int> 类定义。如果用了 Stack<float>,又会生成另一份。这就是所谓的“模板代码膨胀”——每个实例化版本都会产生独立的二进制代码。
我遇到过一种情况:项目中用了十几个不同的类型实例化同一个模板,结果可执行文件体积暴涨。后来我们用了一些技巧(比如类型擦除、模板显式实例化)来控制膨胀。这些后面会讲到。
显式实例化与隐式实例化
实例化有两种方式:
- 隐式实例化:编译器看到你用了
Stack<int>,自动生成代码。这是最常见的方式。 - 显式实例化:你主动告诉编译器:“嘿,帮我生成
Stack<double>的代码”。写法如下:
template class Stack<double>; // 显式实例化
显式实例化有什么用?我举个例子。假设你的模板定义在头文件里,但你想把某些常用类型的实例化放在一个单独的.cpp文件中,以减少编译时间。你可以这样做:
// stack_inst.cpp
#include "stack.h"
template class Stack<int>;
template class Stack<double>;
然后在其他文件中只声明这些实例化版本(不包含模板定义),链接时就能找到。这招在大型项目中特别管用。
知识体系图
下面这张图总结了类模板的核心概念和流程:
避坑指南
最后分享几个我踩过的坑:
- 模板代码放在.cpp里:我曾经把类模板的实现放在.cpp文件中,然后在其他文件里include头文件。链接时一堆“未定义引用”的错误。记住:模板的完整定义必须在每个使用它的翻译单元中可见。
- 忘记写template前缀:定义类外成员函数时,漏掉
template <typename T>是常见错误。编译器会报“非模板类的成员函数定义”之类的错误。 - 模板参数名冲突:类模板参数名和成员函数模板参数名不要重复。比如类模板用T,成员函数模板又用T,会引发歧义。
我的习惯:类模板参数用T,成员函数模板参数用U。这样一眼就能区分。当然,你也可以用更描述性的名字,比如ElementType、ValueType等。
好了,类模板的基础就讲到这里。记住三个核心:定义时加template前缀,成员函数定义要带模板参数,实例化时填具体类型。把这三点吃透,后面的进阶内容就好办了。
公众号:蓝海资料掘金营,微信deep3321