在C++中,模板类允许我们把数据类型作为参数传递给类,从而让同一套逻辑适配多种类型。这种机制被称为泛型编程,核心关键字是template和class或typename。通过模板类,可以避免为int、double、string分别重写几乎相同的容器或算法代码。

一、类模板的基本声明与定义
类模板的声明以template开头,后面跟尖括号括起来的模板参数列表。最常见的形式是类型参数,用class或typename引导。两者在声明类型参数时完全等价,但typename还能用于声明依赖类型,后面会提到。
下面是一个最简单的类模板,实现一个固定大小的数组包装:
#include <iostream>
using namespace std;
template <class T>
class Box {
public:
Box(T v) : value(v) {}
T get() const { return value; }
void set(T v) { value = v; }
private:
T value;
};
int main() {
Box<int> iBox(10);
Box<string> sBox(string("hello"));
cout << iBox.get() << endl;
cout << sBox.get() << endl;
return 0;
}
在上面的代码中,Box不是一个具体的类,而是一个类模板。只有当编译器看到Box<int>时,才会实例化出真正的类。这种延迟实例化机制让模板既灵活又高效。
如果把成员函数写在类外,必须带上模板参数列表,并且类名后要加<T>。很多编译错误都源于忘记写这一层修饰。
template <class T>
class Box {
public:
Box(T v);
T get() const;
private:
T value;
};
template <class T>
Box<T>::Box(T v) : value(v) {}
template <class T>
T Box<T>::get() const {
return value;
}
二、typename与class的区别及非类型参数
在模板参数中,typename和class绝大多数情况下可以互换。但当我们想声明一个依赖于模板参数的类型时,必须用typename告诉编译器后面的是类型而不是静态成员。例如遍历容器时的迭代器类型。
除了类型参数,模板还支持非类型参数,比如整数、指针、引用。这可以用来控制数组大小或传递编译期常量。
template <typename T, int Size>
class StaticArray {
public:
T& at(int i) { return data[i]; }
int length() const { return Size; }
private:
T data[Size];
};
int main() {
StaticArray<double, 5> arr;
arr.at(0) = 3.14;
return 0;
}
非类型参数必须是编译期常量,这保证了数组大小在栈上就能确定。若传入变量会导致编译失败,这是模板与运行时多态的重要区别。
使用非类型模板参数时,注意不同实例化会产生不同类。StaticArray<int, 5>和StaticArray<int, 6>是两个完全无关的类型,不能互相赋值。
三、模板类的特化:全特化与偏特化
当通用模板对某个特定类型效率低下或语义不对时,可以提供特化版本。全特化是针对所有模板参数都给出具体类型;偏特化则只固定部分参数或加指针、引用约束。
以下示例展示了对bool类型的全特化,用位压缩代替真实字节存储:
template <class T>
class Storage {
public:
Storage(T v) : val(v) {}
T val;
};
template <>
class Storage<bool> {
public:
Storage(bool v) : bit(v ? 1 : 0) {}
bool get() const { return bit == 1; }
private:
unsigned char bit;
};
偏特化常用于指针类型,统一处理所有T*。这样能避免为int*、double*重复写逻辑。
template <class T>
class PointerWrap {
public:
PointerWrap(T* p) : ptr(p) {}
T* ptr;
};
template <class T>
class PointerWrap<T*> {
public:
PointerWrap(T* p) : ptr(p) {}
T* ptr;
void reset(T* p) { ptr = p; }
};
特化版本必须和主模板在同一命名空间,且参数匹配优先级更高。编译器会优先选择最具体的特化,这在写跨平台或调试用类型时非常实用。
四、完整示例:模板栈与简单智能指针
下面给出一个支持任意类型的栈模板,包含入栈、出栈、判空。它演示了如何组合前面讲到的类内定义与模板参数。
#include <iostream>
#include <vector>
using namespace std;
template <typename T>
class Stack {
public:
void push(const T& item) { data.push_back(item); }
void pop() { if (!data.empty()) data.pop_back(); }
T top() const { return data.back(); }
bool empty() const { return data.empty(); }
private:
vector<T> data;
};
int main() {
Stack<int> s;
s.push(1);
s.push(2);
cout << s.top() << endl;
s.pop();
cout << s.top() << endl;
return 0;
}
再来看一个极简的智能指针模板,用来理解RAII与模板结合的价值。它在析构时自动释放资源,避免泄漏。
template <typename T>
class SimplePtr {
public:
explicit SimplePtr(T* p = nullptr) : ptr(p) {}
~SimplePtr() { delete ptr; }
T& operator*() const { return *ptr; }
private:
T* ptr;
};
int main() {
SimplePtr<int> p(new int(42));
cout << *p << endl;
return 0;
}
这两个例子说明模板类并不神秘,本质是把类型推迟到使用时绑定。只要把握住实例化规则和特化优先级,就能写出可复用的基础库组件。
五、常见错误与编写建议
第一,模板代码通常要放在头文件中,因为编译器需要看到完整定义才能实例化。若分离到cpp,链接时会报找不到符号。
第二,注意模板参数名冲突。如果嵌套模板中用到依赖类型,务必加typename,否则GCC会报需要typename的错误。
template <typename T>
void print(const T& c) {
typename T::iterator it; // 必须写typename
}
第三,避免过度模板化。不是所有代码都适合模板,若类型极少且逻辑差异大,普通重载或继承可能更清晰。模板带来的编译时间增长和报错可读性下降也是实际项目要考虑的成本。
掌握以上写法后,你可以逐步把项目中重复的类结构重构为模板类,并结合标准库的type_traits做编译期分支,写出既安全又高效的C++泛型代码。