C++ 模板初阶:让类型成为参数
一、从函数重载到泛型编程
想象一下,当你需要一个交换两个变量的函数,它可以处理 int、double、string……如果用函数重载,你要购买的int版、double版、string版各写一份,代码完全相同只是类型不同。这既繁琐又容易出错。
C++ 的模板机制就是让你只写一份代码,然后告诉编译器:“这里的类型先用 T 替代,你根据实际调用时的参数类型自己生成对应的代码”。
这种编程思想称为泛型编程(Generic Programming),即编写与类型无关的通用代码,实现代码复用。模板是泛型编程的基础。
二、函数模板
函数模板代表了一个函数家族,它本身不是函数,而是编译器用于生成具体函数的模具。
2.1 格式与原理
示例 1:函数模板定义与使用
#include <iostream>
#include <string>
template<typename T>
T Max(T a, T b) {
return a > b ? a : b;
}
int main() {
// 隐式实例化:编译器根据实参自动推测 T
std::cout << Max(3, 7) << "\n"; // int
std::cout << Max(3.14, 2.71) << "\n"; // double
std::cout << Max(std::string("Cat"),
std::string("Dog")) << "\n"; // string
return 0;
}
在编译阶段,编译器看到 Max(3, 7) 时,会根据实参 3 和 7 的类型将 T 推测为 int,然后生成一份专门处理 int 的 Max 函数。这个过程完全由编译器自动完成,对用户透明。
2.2 隐式实例化与显式实例化
模板的实例化是指编译器根据实际类型生成具体代码的过程,分为两种方式:
- 隐式实例化:编译器根据实参自动推测类型,最简洁
- 显式实例化:在函数名后用 <> 指定类型,编译器会尝试进行隐式类型转换
示例 2:隐式与显式实例化对比
#include <iostream>
template<typename T>
T Add(T a, T b) {
return a + b;
}
int main() {
int x = 10, y = 20;
double p = 3.14, q = 2.71;
// 隐式实例化:类型自动推测
std::cout << Add(x, y) << "\n"; // T=int
std::cout << Add(p, q) << "\n"; // T=double
// Add(x, p); // 编译错误:无法确定 T 是 int 还是 double
// 显式实例化:指定类型,编译器会尝试隐式转换
std::cout << Add<double>(x, p) << "\n"; // T=double, int被转为double
return 0;
}
注意:模板中编译器一般不会进行类型转换操作。当 Add(x, p) 时,一个是 int 一个是 double,编译器无法确定 T 到底应该是哪个类型,因此会报错。解决办法是用显式实例化或者自己强制转换。
2.3 模板参数的匹配原则
当同时存在普通函数和函数模板时,编译器遵循以下规则:
- 如果普通函数与实参完全匹配,优先调用普通函数(不会从模板实例化)
- 如果模板可以生成更好匹配的版本,则优先使用模板
- 普通函数支持自动类型转换,模板函数不支持
示例 3:模板匹配规则
#include <iostream>
// 普通函数
int Max(int a, int b) {
std::cout << "Normal Max: ";
return a > b ? a : b;
}
// 函数模板
template<typename T>
T Max(T a, T b) {
std::cout << "Template Max: ";
return a > b ? a : b;
}
int main() {
std::cout << Max(3, 7) << "\n"; // 调用普通函数
std::cout << Max(3.14, 2.71) << "\n"; // 调用模板(普通函数不匹配)
std::cout << Max<int>(3, 7) << "\n"; // 显式指定,调用模板
return 0;
}
三、类模板
与函数模板类似,类模板让类的成员变量和成员函数也能处理任意类型。这在学习类与对象时的 Vector、Stack 等数据结构中非常常见。
3.1 定义格式
示例 4:类模板——简单的 Stack
#include <iostream>
template<typename T>
class Stack {
public:
Stack(int cap = 4)
: _data(new T[cap]), _size(0), _capacity(cap) {}
~Stack() { delete[] _data; }
void push(const T& val) {
if (_size == _capacity) {
_capacity *= 2;
T* tmp = new T[_capacity];
for (int i = 0; i < _size; ++i) tmp[i] = _data[i];
delete[] _data;
_data = tmp;
}
_data[_size++] = val;
}
T pop() { return _data[--_size]; }
bool empty() const { return _size == 0; }
private:
T* _data;
int _size;
int _capacity;
};
int main() {
Stack<int> st;
st.push(10);
st.push(20);
st.push(30);
while (!st.empty())
std::cout << st.pop() << " ";
std::cout << "\n";
Stack<double> st2;
st2.push(1.5);
st2.push(2.5);
std::cout << st2.pop() << "\n";
return 0;
}
注意:Stack 是类模板名,Stack<int> 和 Stack<double> 才是真正的类。类模板的实例化必须显式指定类型,不能像函数模板那样隐式推测。
3.2 成员函数的类外定义
如果将类模板的成员函数定义在类外,需要重复模板参数列表,并用类型 Stack<T> 指明所属类。
示例 5:类模板成员函数类外定义
#include <iostream>
#include <string>
template<typename T>
class Wrapper {
public:
Wrapper(const T& val);
void show() const;
private:
T _val;
};
// 类外定义:需要重复 template<typename T>
template<typename T>
Wrapper<T>::Wrapper(const T& val) : _val(val) {}
template<typename T>
void Wrapper<T>::show() const {
std::cout << "Value: " << _val << "\n";
}
int main() {
Wrapper<int> w1(42);
Wrapper<std::string> w2("Hello Templates!");
w1.show();
w2.show();
return 0;
}
四、总结与展望
本篇介绍了模板的基础知识:
- 泛型编程的思想——让类型成为参数,代码复用
- 函数模板的定义、隐式与显式实例化、匹配原则
- 类模板的定义、实例化、成员函数类外定义
模板是 C++ 极其强大的特性,它让数据结构和算法与类型解耦,为后续学习 STL(标准模板库)奠定了基础。在后续的内容中,我们还将探讨模板特化、变参模板、模板元编程等更深入的话题。
更多推荐

所有评论(0)