目录 Table of Contents
12 模板
下面是一个针对 int 数组的冒泡排序
void Sort(int* arr, int nLength)
{
int i;
int k;
for(i = 0; k < nLength - 1; k++)
{
if(arr[k] > arr[k+1])
{
int temp = arr[k];
arr[k] = arr[k+1];
arr[k+1] = temp;
}
}
}
int main(int argc[], char* argv[])
{
int arr[] = {1, 4, 6, 2, 9, 8, 7};
Sort(arr, 7);
}
如果我们这个要排序的数组不是 int 类型, 那么这个函数还能使用吗 ?
我们编译一下, 发现报错
原因很简单, 这个函数只能针对 int 类型数组进行排序, 我现在是 char 类型
现在我们要对 char 类型排序, 只需要对上面函数代码进行一些修改即可 :
void SortByChar(char* arr, char nLength)
{
int i;
int k;
for(i = 0; k < nLength - 1; k++)
{
if(arr[k] > arr[k+1])
{
char temp = arr[k];
arr[k] = arr[k+1];
arr[k+1] = temp;
}
}
}
现在问题来了, 基本上相同的两段代码, 不同的地方非常有限, 仅仅是类型这个地方不一样, 需要处理一下, 其它所有地方的逻辑完全一样
如果有一百种类型需要排序, 那我们难道要写一百个这样的函数吗 ?
C++ 有解决这个问题的办法, 就是模板, 就是一样的代码写一份就 OK 了, 其它的不用管了
在函数中使用模板
函数模板的格式 :
template <class 形参名, class 形参名,......> 返回类型 函数名 (参数列表)
{
函数体
}
我们把刚才的冒泡排序用模板实现一下
template<class T> // 这里的 T 可以换成别的, 想叫什么就叫什么, 我这里面只有一个类型需要模板来替换, 如果有两个可以再加一个 class
void Sort(T* arr, int nLength)
{
int i;
int k;
for(i = 0; k < nLength - 1; k++)
{
if(arr[k] > arr[k+1])
{
T temp = arr[k];
arr[k] = arr[k+1];
arr[k+1] = temp;
}
}
}
比如说我上面 int 这里需要改, 那我就把 int 改成 T
如果这里面传进来的是 int 类型数组, 那么这个 T
就会被替换成 int; 如果传进来的是 char, 那么这个 T
就会被替换成 char
现在我们来看看模板的本质是什么
int main(int argc[], char* argv[])
{
char arr_1[] = {1, 4, 6, 2, 9, 8, 7};
int arr_2[] = {1, 4, 6, 2, 9, 8, 7};
Sort(arr_1, 7);
Sort(arr_2, 7);
}
我们连续调用这个函数两次, 第一次传进去 char 数组, 第二次传进去 int 数组
我们来看反汇编代码
;Sort(arr_1,7);
push 7
lea eax, [ebp-8]
push eax
call @ILT+0(Sort) (00401005)
add esp, 8
当我调用第一个函数的时候, 这个函数代码真正的地址在 00401090
我们来看看第二个函数
;Sort(arr_2,7);
push 7
lea ecx, [ebp-24h]
push ecx
call @ILT+10(Sort) (0040100F)
add esp, 8
我们跟进去, 这个函数真正的地址在 0040D5F0
函数还是这个函数, 但根本就不是一个地址
所以模板的本质就是, 编译器只要见到这种类型了, 它就会给你生成几份不同的函数. 当你传 char 类型的时候, 它给你生成一个函数, 地址在这; 当你传 int 类型的时候, 它又给你生成了一个函数, 地址在这.
我们现在传一个结构体类型, 给它排序
class Base
{
private:
int x;
int y;
};
排序, 需要的是对两个数的大小进行比较, 所以这里我们就要对这个大于号进行运算符重载
class Base
{
private:
int x;
int y;
public:
Base(int x, int y) // 写一个构造函数, 方便我们创建对象
{
this->x = x;
this->y = y;
}
bool operator>(Base& base) // 运算符重载, 传了对象的引用作为参数, 用的时候直接传对象即可
{
return this->x > base.x && this->y > base.y;
}
};
我们来测试一下
int main(int argc, char* argv[])
{
Base b1(1, 1), b2(3, 3), b3(2, 2), b4(5, 5), b5(4, 4);
Base arr3[] = {b1, b2, b3, b4, b5};
Sort(arr3, 5);
}
排序成功
在结构体/类中使用模板
类模板的格式为 :
template<class 形参名, class 形参名, ...> class 类名
{
...
}
我们看例子
struct Base
{
int x;
int y;
char a;
char b;
int Max()
{
if(x > y)
{
return x;
}
else
{
return y;
}
}
char Min()
{
if(a < b)
{
return a;
}
else
{
return b;
}
}
}
假设我希望任何类型都可以用这俩函数来比较大小, 我们来使用模板
template<class T, class M> // 放在上面可读性好一些
struct Base
{
T x;
T y;
M a;
M b;
T Max()
{
if(x > y)
{
return x;
}
else
{
return y;
}
}
M Min()
{
if(a < b)
{
return a;
}
else
{
return b;
}
}
}
现在我们定义这个 Base 可以使用任何类型, T M 可以替换成任何类型
但是有些小细节需要注意, 当你真正用这个 Base 的时候, 你这个类型是一个自己定义的其它的类型, 那你必须要重载这个大于号 > 和小于号 < 否则是无法编译通过的
我们现在来看看使用这个代码的时候需要注意什么
int main(int argc, char* argv[])
{
Base base;
return 0;
}
以往我们是这么创建对象的 Base base;
, 但是发现现在编译不通过, 原因很简单, 你没有明确告诉编译器这模板里的 T 和 M 具体是什么.
刚才用函数模板的时候, 你一传参数, 编译器就已经知道了你是什么类型的. 但是现在你创建对象的时候, 你不告诉它, 它就不知道这个 T M 是什么.
那么怎么告诉编译器呢, 语法是在后面跟上<>
, 在这尖括号里面, 看一下模板是写着两个类型, 那么你只要按着顺序写两个类型就可以了
int main(int argc, char* argv[])
{
Base<int, char> base; // 创建对象的时候, 第一个 int 对应模板里面的 T, 编译器就会把所有的 T 替换成 int; 第二个 char 对应模板里面的 M, 编译器就会把所有的 M 替换成 char.
return 0;
}
所谓的模板无非就是编译器帮我们多写一份代码
当然模板可能会有一些高大上的语法, 等到以后遇到的话, 可以看下反汇编来看下它的本质是什么