1.模板概念
什么是模板?
模板可以看作是函数或类的蓝图。与其指定具体的数据类型(如 int
或 double
),你可以使用一个占位符(通常称为“模板参数”),这个占位符在使用模板时被替换为具体的类型。
*(概念,可跳过)在这里提一个小点:泛型编程是一种编程范式,它允许编写代码时使用类型参数,从而使得代码能够处理多种数据类型而不需要重复编写相似的代码。通过泛型编程,开发者可以编写更加通用、可复用的代码,提高代码的灵活性和可维护性。
在C++中,泛型编程主要通过模板来实现。模板是一种编译器机制,它允许在编写函数或类时使用类型参数。类型参数可以在实例化模板时由具体的类型来替代。
先看下面用c语言写的一段代码:
void Swap(int* x,int* y)
{
int temp = *x;
*x = *y;
*y = temp;
}
void Swap(double* x,double* y)
{
double temp = *x;
*x = *y;
*y = temp;
}
int main()
{
int a = 0, b = 1;
double c = 1.1, d = 2.2;
Swap(&a, &b);//当int类型的a和b会传地址调用第一个Swap
Swap(&c, &d);//当double类型的c和d会传地址调用第二个Swap
return 0;
}
明明是只是更换类型,代码却要重复编写,如果还需要使用更多类型的Swap会让代码更冗余,重复的部分过多所以C++做出改进:提出模板。
2.模板的分类
在C++语言种,模板分为:函数模板和类模板。
3.模板的简单使用(函数模板)
使用模板来实现上面C语言代码中需要的功能:(模板的写法)
关键字:template,typename
// 函数模板实例化生成具体函数
// 函数模板根据调用,自己推导模板参数的类型,实例化出对应的函数
template<typename T>
void Swap(T* x,T* y)
{
T* temp=x;
*x=*y;
*y=*temp;
}
int main()
{
int a = 0, b = 1;
double c = 1.1, d = 2.2;
Swap(&a, &b); // 使用模板函数交换 int 类型
Swap(&c, &d); // 使用模板函数交换 double 类型
return 0;
}
观察上面代码,template<typename T>就是模板的写法,可以把T看作是可以变化的类型,实际上T通过传来的参数 推演出T该用的类型。
当使用模板时,编译器会在编译期间根据你提供的具体类型生成相应的代码。。如上面例子,当你调用 Swap(a, b)
时,编译器会生成一个针对 int
类型的 Swap
函数版本;当你调用 Swap(c, d)
时,编译器会生成一个针对 double
类型的 Swap
函数版本。
注意:在C++并不是没有生成 Swap(double* x,double* y)和Swap(int* x,int* y) 函数,只是不需要我们自己定义,而是交给编译器去做。
下面看函数模板更多的用法:
// 函数模板实例化生成具体函数
// 函数模板根据调用,自己推导模板参数的类型,实例化出对应的函数
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
template<class T>
T* Alloc(int n)
{
return new T[n];
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
// 实参传递的类型,推演T的类型
cout << Add(a1, a2) << endl;
cout << Add(d1, d2) << endl;
cout << Add(a1, (int)d1) << endl;
cout << Add((double)a1, d1) << endl;
// 显示实例化,用指定类型实例化
cout << Add<int>(a1, d1) << endl;
cout << Add<double>(a1, d1) << endl;
// 有些函数无法自动推,只能显示实例化
double* p1 = Alloc<double>(10);
return 0;
}
上面代码出现了模板 新的用法那么Add<int>(a1,d1)是什么意思呢?
这个用法叫做:显示实例化,意思就是当调用Add函数时,直接告诉编译器T的类型就是int,不用推演了! 通用下面Add<double>也是一样的道理。
所以可以看出图1、2行,和3、4行代码效果是一样的。
最后看看Alloc函数传参的类型和模板类型T没关系,所以,编译器无法自动推演出此class T,究竟是什么类型,所以只能显示实例化,否则报错!
4.类模板
- 类模板:用于定义通用的类,可以操作不同类型的数据
类模板和函数模板的使用非常类似:
其实就是在整个类中,储存和处理数据,数据类型都用模板template<typename T>中的T
template<typename T>
class Stack {
private: //类成员声明
T* data;
int top;
int capacity;
public:
Stack(int size) : capacity(size), top(-1) {
data = new T[capacity];
}
~Stack() {
delete[] data;
}
void push(const T& value) {
if (top < capacity - 1) {
data[++top] = value;
}
}
void pop() {
if (top >= 0) {
--top;
}
}
T peek() const {
if (top >= 0) {
return data[top];
}
throw std::out_of_range("Stack is empty");
}
bool isEmpty() const {
return top == -1;
}
};
代码解析:这是自己实现的一个栈Stack,栈中存放的数据类型我不确定(可能是int,可能是float),所以当在类中函数形参 返回值等,需要声明栈内数据的数据类型都用T,(类前面使用类模板template<typename T>)。
详解:
template<typename T>
声明了一个模板。T
是一个类型参数,表示栈中元素的类型。这个类型在你创建栈对象时确定。模板允许代码泛型化,使得同一段代码可以处理不同的数据类型,而不需要重复编写代码。- 类成员声明
T* data
: 一个指向T
类型数组的指针,用于存储栈中的元素。int top
: 表示栈顶元素的索引。初始值为-1
,表示栈为空。int capacity
: 栈的容量,即最多可以存储多少个元素。
- 构造函数
Stack(int size)
: 构造函数,用于创建一个容量为size
的栈。: capacity(size), top(-1)
是初始化列表,初始化栈的容量为size
,并将top
设为-1
。data = new T[capacity];
动态分配一个容量为capacity
的T
类型数组,用于存储栈的元素。
- 析构函数
~Stack()
是析构函数,当栈对象的生命周期结束时调用,用于释放栈中动态分配的内存 (data
)。delete[] data;
释放用new
动态分配的数组内存,防止内存泄漏。
push
void push(const T& value)
: 向栈中添加一个元素。元素类型根据模板走if (top < capacity - 1)
: 检查栈是否已满。如果栈未满,执行以下操作:data[++top] = value;
: 将top
递增 1 后,把value
存储在数组data
的栈顶位置。
pop()
从栈中移除栈顶元素。
完成类声明后,使用这个栈储存int类型数据 例子如下:
#include <iostream>
#include <stdexcept> // 为了处理 std::out_of_range 异常
using namespace std;
int main() {
// 创建一个可以存储 int 类型元素的栈,容量为5
Stack<int> intStack(5);
// 向栈中压入元素
intStack.push(10);
intStack.push(20);
intStack.push(30);
// 取出栈顶元素并打印
cout << "Top element: " << intStack.peek() << endl; // 此时应输出:30
// 弹出栈顶元素
intStack.pop();
// 再次取出栈顶元素并打印
cout << "Top element after pop: " << intStack.peek() << endl; // 此时应输出:20
// 检查栈是否为空
if (!intStack.isEmpty())
{
cout << "Stack is not empty." << endl;
}
else {
cout << "Stack is empty." << endl;
}
// 捕获尝试从空栈中取元素的异常
try {
Stack<int> emptyStack(5);
emptyStack.peek(); // 这将抛出 std::out_of_range 异常
} catch (const std::out_of_range& e) {
cout << "Exception caught: " << e.what() << endl; // 输出:Exception caught: Stack is empty
}
return 0;
}
也可以让这个栈储存double类型数据:
#include <iostream>
#include <stdexcept> // 为了处理 std::out_of_range 异常
using namespace std;
int main() {
// 创建一个可以存储 double 类型元素的栈,容量为5
Stack<double> doubleStack(5);
// 向栈中压入元素
doubleStack.push(3.14);
doubleStack.push(2.718);
doubleStack.push(1.618);
// 取出栈顶元素并打印
cout << "Top element: " << doubleStack.peek() << endl; // 输出:1.618
// 弹出栈顶元素
doubleStack.pop();
// 再次取出栈顶元素并打印
cout << "Top element after pop: " << doubleStack.peek() << endl; // 输出:2.718
// 检查栈是否为空
if (!doubleStack.isEmpty()) {
cout << "Stack is not empty." << endl;
} else {
cout << "Stack is empty." << endl;
}
// 捕获尝试从空栈中取元素的异常
try {
Stack<double> emptyStack(5);
emptyStack.peek(); // 这将抛出 std::out_of_range 异常
} catch (const std::out_of_range& e) {
cout << "Exception caught: " << e.what() << endl; // 输出:Exception caught: Stack is empty
}
return 0;
}
当然,还可以实例化成char型、long型等,在这里我就不一一演示了。