12_模板

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;
}

所谓的模板无非就是编译器帮我们多写一份代码

当然模板可能会有一些高大上的语法, 等到以后遇到的话, 可以看下反汇编来看下它的本质是什么