C++ 模板是泛型编程的核心,允许你编写与数据类型无关的代码。这意味着你可以定义一套逻辑,让它同时适用于整数、浮点数甚至自定义对象,而无需重复编写多份相似的代码。本文将带你掌握函数模板与类模板的核心用法。
一、 函数模板:自动适应不同类型的函数
函数模板用于定义一个通用的函数,该函数可以接受多种类型的数据。
1. 定义函数模板
定义一个函数模板,使用 template 关键字,后跟尖括号 <> 包裹的类型参数。
编写以下语法结构:
template <typename T>
T getMax(T a, T b) {
return (a > b) ? a : b;
}
在这个例子中,T 是一个占位符,代表任何数据类型。typename 关键字也可以用 class 替代,两者在模板定义中含义相同。
2. 隐式实例化:让编译器自动推断类型
当你调用这个模板函数时,编译器会根据传入的实参自动推导 T 的类型。
执行以下调用代码:
int intA = 5, intB = 10;
double doubleA = 5.5, doubleB = 10.5;
// 推导 T 为 int
int result1 = getMax(intA, intB);
// 推导 T 为 double
double result2 = getMax(doubleA, doubleB);
编译器看到 getMax(intA, intB) 时,会自动生成一个处理 int 类型的函数版本;看到 getMax(doubleA, doubleB) 时,则生成处理 double 类型的版本。
3. 显式实例化:手动指定类型
当参数类型复杂或编译器无法自动推断时,你需要指定类型。
使用尖括号在函数名后显式声明类型:
// 强制指定 T 为 double,即便传入的是 int
double result3 = getMax<double>(intA, doubleB);
这种做法会导致 intA 被隐式转换为 double 再进行比较。
二、 类模板:通用类型的容器
类模板允许你定义一个可以存储任何类型数据的类结构,类似于标准库中的 vector 或 list。
1. 定义类模板
创建一个简单的存储类 Box,它可以存储任意类型的数据。
输入以下代码:
template <class T>
class Box {
private:
T item;
public:
// 构造函数
Box(T t) : item(t) {}
// 设置值
void setItem(T t) {
item = t;
}
// 获取值
T getItem() {
return item;
}
};
这里使用了 class T,效果与 typename T 完全一致。
2. 实例化类模板
与函数模板不同,类模板在使用时必须显式指定类型。编译器无法通过构造函数的参数完全推导出类模板的类型参数。
声明对象时指定类型:
// 实例化一个存储 int 的 Box
Box<int> intBox(100);
// 实例化一个存储 std::string 的 Box
Box<std::string> stringBox("Hello Templates");
3. 类模板成员函数的类外定义
如果在类外部定义成员函数,语法会比普通类稍微复杂一些。你需要重复模板声明,并指定作用域。
实现类外的成员函数:
// 必须重新声明 template <class T>
template <class T>
void Box::setItem(T t) {
item = t;
}
template <class T>
T Box::getItem() {
return item;
}
注意 Box 后面必须紧跟 <T>,告诉编译器这是类模板 Box 的成员函数,而不是普通类的成员函数。
三、 模板的类型推导与匹配逻辑
理解编译器如何选择模板函数有助于编写更健壮的代码。以下流程描述了当你调用一个重载函数(包含模板和非模板)时,编译器的决策过程。
如果存在多个匹配的模板,编译器会根据“特化程度”选择最具体的一个。如果优先级相同,编译器将报错“二义性”。
四、 函数模板与类模板的对比
为了加深理解,我们将这两种模板的核心区别进行对比。
| 特性 | 函数模板 | 类模板 |
|---|---|---|
| 类型推导 | 编译器通常能自动推导参数类型 | 编译器无法自动推导,必须显式指定 <Type> |
| 实例化关键词 | 调用时隐式生成或显式指定 func<int>() |
声明对象时必须指定 ClassName<int> obj |
| 主要用途 | 适用于算法逻辑相同,参数类型不同的场景 | 适用于数据结构相同,存储元素类型不同的场景 |
| 成员定义 | 通常在头文件中直接定义 | 类内定义可直接写,类外定义需带 template <T> |
五、 实战案例:通用的栈
结合上述知识,我们构建一个简单的栈类。它支持压入和弹出任意类型的数据。
编写完整代码:
#include <iostream>
#include <vector>
#include <stdexcept>
template <typename T>
class Stack {
private:
std::vector elements;
public:
void push(const T& element) {
elements.push_back(element);
}
T pop() {
if (elements.empty()) {
throw std::out_of_range("Stack<>::pop(): empty stack");
}
T elem = elements.back();
elements.pop_back();
return elem;
}
bool isEmpty() const {
return elements.empty();
}
};
int main() {
try {
// 实例化 int 栈
Stack<int> intStack;
intStack.push(10);
std::cout << "Int Stack Pop: " << intStack.pop() << std::endl;
// 实例化 string 栈
Stack<std::string> stringStack;
stringStack.push("Hello");
stringStack.push("World");
std::cout << "String Stack Pop: " << stringStack.pop() << std::endl;
} catch (const std::exception& ex) {
std::cerr << "Exception: " << ex.what() << std::endl;
}
return 0;
}
编译并运行这段代码。你会看到同一个类结构 Stack 成功处理了 int 和 std::string 两种完全不同的数据类型。如果在未来需要处理 double 类型,只需修改实例化代码,无需改动类定义。
确保在使用模板时,将声明和定义都放在头文件(.h 或 .hpp)中,或者确保编译器在实例化模板时能看到完整的定义,否则可能会遇到“未定义引用”的链接错误。

暂无评论,快来抢沙发吧!