类模板
定义与作用
类模板允许定义一个类的蓝图,其成员的类型由模板参数决定:
template <typename T>
class Stack {
std::vector<T> data_;
public:
void push(const T& value);
T pop();
};
核心价值:集合类、容器类、包装类的泛型化——一套代码适配所有类型。
核心原理
类模板实例化
完整示例
示例一:泛型栈的实现
场景说明:从零实现一个功能完整的泛型栈,对比不同模板实参实例化出的独立类型。
#include <iostream>
#include <vector>
#include <stdexcept>
#include <initializer_list>
#include <utility>
template <typename T>
class Stack {
public:
Stack() = default;
// 列表初始化构造
Stack(std::initializer_list<T> init) : data_(init) {}
// 拷贝构造
Stack(const Stack& other) = default;
// 移动构造
Stack(Stack&& other) noexcept = default;
// 压栈(常量引用版)
void push(const T& value) {
std::cout << " 压栈(拷贝): " << value << std::endl;
data_.push_back(value);
}
// 压栈(右值引用版——移动语义)
void push(T&& value) {
std::cout << " 压栈(移动): " << value << std::endl;
data_.push_back(std::move(value));
}
// 出栈
T pop() {
if (empty()) throw std::underflow_error("栈空:不能 pop");
T top = std::move(data_.back());
data_.pop_back();
return top;
}
// 查看栈顶
T& top() {
if (empty()) throw std::underflow_error("栈空");
return data_.back();
}
const T& top() const {
if (empty()) throw std::underflow_error("栈空");
return data_.back();
}
bool empty() const { return data_.empty(); }
size_t size() const { return data_.size(); }
// 清空
void clear() { data_.clear(); }
private:
std::vector<T> data_;
};
int main() {
std::cout << "===== Stack<int> =====" << std::endl;
Stack<int> int_stack;
int_stack.push(10);
int_stack.push(20);
int_stack.push(30);
std::cout << "栈大小: " << int_stack.size() << std::endl;
while (!int_stack.empty()) {
std::cout << "弹出: " << int_stack.pop() << std::endl;
}
std::cout << "\n===== Stack<std::string> =====" << std::endl;
Stack<std::string> str_stack = {"Alice", "Bob", "Charlie"};
std::cout << "栈顶: " << str_stack.top() << std::endl;
str_stack.push(std::string("Dave")); // 右值——触发移动版本
std::cout << "全部出栈:" << std::endl;
while (!str_stack.empty()) {
std::cout << " " << str_stack.pop() << std::endl;
}
// Stack<int> 和 Stack<double> 是两个不同的类型
std::cout << "\n===== 类型独立性 =====" << std::endl;
Stack<int> si;
si.push(42);
Stack<double> sd;
sd.push(3.14);
// si = sd; // 编译错误!Stack<int> 和 Stack<double> 不兼容
std::cout << "Stack<int> 大小: " << si.size() << std::endl;
std::cout << "Stack<double> 大小: " << sd.size() << std::endl;
return 0;
}
预期输出:
===== Stack<int> =====
压栈(拷贝): 10
压栈(拷贝): 20
压栈(拷贝): 30
栈大小: 3
弹出: 30
弹出: 20
弹出: 10
===== Stack<std::string> =====
栈顶: Charlie
压栈(移动): Dave
全部出栈:
Dave
Charlie
Bob
Alice
===== 类型独立性 =====
压栈(拷贝): 42
压栈(拷贝): 3.14
Stack<int> 大小: 1
Stack<double> 大小: 1
示例二:Pair 的实现与 CTAD(C++17)
#include <iostream>
#include <string>
#include <utility>
// 手写 Pair 类模板
template <typename T1, typename T2>
class Pair {
public:
Pair() : first_(), second_() {}
Pair(const T1& first, const T2& second)
: first_(first), second_(second) {}
// 通用构造(支持移动)
template <typename U1, typename U2>
Pair(U1&& first, U2&& second)
: first_(std::forward<U1>(first))
, second_(std::forward<U2>(second)) {}
T1 first_;
T2 second_;
};
// C++17 CTAD 推导指引(手工版标准 Pair 不需要,编译器自带)
// 此处演示手工 Pair 的 CTAD
template <typename T1, typename T2>
Pair(T1, T2) -> Pair<T1, T2>;
// 打印函数模板
template <typename T1, typename T2>
std::ostream& operator<<(std::ostream& os, const Pair<T1, T2>& p) {
return os << "{" << p.first_ << ", " << p.second_ << "}";
}
int main() {
std::cout << "===== 手写 Pair =====" << std::endl;
// 显式指定模板参数
Pair<std::string, int> p1("张三", 95);
std::cout << p1 << std::endl;
// C++17 CTAD:编译器自动推导
Pair p2("李四", 88.5); // 推导为 Pair<const char*, double>
std::cout << p2 << std::endl;
Pair p3(std::string("王五"), 92); // 推导为 Pair<std::string, int>
std::cout << p3 << std::endl;
// 嵌套
Pair<Pair<std::string, int>, double> nested(Pair<std::string, int>("赵六", 85), 3.7);
std::cout << "嵌套: " << nested << std::endl;
return 0;
}
预期输出:
===== 手写 Pair =====
{张三, 95}
{李四, 88.5}
{王五, 92}
嵌套: {{赵六, 85}, 3.7}
逐段分析:
- 类模板定义在类名后紧接着
<typename T>——每个成员函数定义也都需要这个前缀 Stack<int>和Stack<double>是完全不同的类型——各自独立实例化模板成员函数- CTAD(C++17)允许省略模板参数,编译器从构造函数实参推导——
Pair("李四", 88.5)不需要写<const char*, double> - 推导指引
Pair(T1, T2) -> Pair<T1, T2>;告诉 CTAD 如何从构造函数参数推导模板参数
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
| 类模板的成员函数定义在 .cpp | 链接时找不到定义 | 定义和声明都在头文件,或用显式实例化 |
| 混淆不同模板实参的类型 | 尝试赋值不兼容的类型 | 使用同一个模板实参,或写转换函数 |
忘记成员函数定义前的 template<typename T> | 编译错误 | 每个类外定义都要重复模板声明 |
类模板中的 using 别名 | 别名本身不需要 typename | 注意依赖名与非依赖名的区别 |
| CTAD 推导意外 | 推导为不希望的类型 | 显式指定模板参数或使用推导指引修正 |
常见面试问题
类模板和函数模板的区别?——函数模板通过实参推导类型参数;类模板必须显式指定模板实参(C++17 前),各实例是独立类型。类模板可以偏特化,函数模板不可。
为什么类模板的完整定义必须在头文件?——编译器需要看到完整的类定义才能为每个实例化生成代码。每个翻译单元独立实例化,链接时去重。
C++17 CTAD 是什么?——Class Template Argument Deduction,类模板参数推导。允许
std::pair p(1, 2.0)省略模板参数,编译器从构造函数推导。类模板的静态成员在不同实例间共享吗?——不共享。
Stack<int>::static_var和Stack<double>::static_var是不同的变量,各自独立。模板类的友元如何处理?——友元函数模板需前置声明,类模板内的友元声明可能涉及模板参数。
小结
- 类模板:用
<typename T>参数化整个类的类型——每个实例是独立类型 - 定义必须在头文件——除非显式实例化
- 不同模板实参生成完全不同的类型——
Stack<int>≠Stack<double> - CTAD(C++17):从构造函数参数推导类模板参数,省略显式指定
- 成员函数定义需在类外时加
template<typename T>前缀