模板【C++】

扳手大家应该都认识吧,就是一个用来拧螺丝的工具,由于存在各式各样的螺丝型号,所以工具包里的扳手也对应的有很多不同大小的型号,如下:

70d8709fe05c41fb8b6f8977d5fadcbc.png但一个聪明的师傅是不会傻傻地带这么多扳手去的,一来是笨重,二来是用起来很麻烦,要拧的动螺丝,扳手的规格必须和螺丝的大小匹配,所以每拧一个不同大小的螺丝就要拿出一堆扳手出来,然后一个一个地试。

那有什么更好的解决方案吗?我们可以用活动扳手来代替,活动扳手长这样:

b8a35ea6bc4c4fe093152c6c889c0b8e.png

通过调节活动扳手头部的旋轴,就可以调节活动扳手的规格,以此达到一把扳手顶一堆扳手的效果,而且对比之下轻便多了。

“喂!咱不是学模板的使用吗?怎么扯螺丝那去了?”OK,返回正题,如果说函数是一个扳手,那么模板函数就是一个活动扳手,模板就是一个让逻辑复用性增强的语法,把函数模板化,就像在普通股扳手上加上一个转轴一样,模板不仅能用在函数中,也有模板类的用法,后者模板类在STL中便便发挥了重要的作用。

模板的使用

在使用vector,string等STL容器时,在初始化时都要在容器名后面加上<>来定义元素的类型,使用这个尖括号就是在进行模板的实例化,类模板就是这样使用的;对于函数模板,在使用上模板函数和普通函数没有区别,而在定义和声明就有细微的差异(这个差异后文再提)

要定义一个模板类或则模板函数,格式如下:

// 类模板
template<class T1, classT2...> // 这里是用来声明模板参数的地方template是关键字,<>内放模板参数
class myVector
{
    T mem; // 可以用T来创建成员
    ...  // 类成员等
};

// 函数模板
template<class T1, classT2...>
函数返回类型 函数名(参数列表) // 参数的类型需要有一个T类型的参数(不用T那定义为普通函数即可)
{
    // 函数体
}


模板参数的类型名可以是class或者是typename,参数名可以不用是T1,T2,自定义即可,class和typename二者在使用中基本没有差别,笔者所知道的一个差异就是:在一个模板中想通过域访问符和模板参数来使用其他模板中的东西时,就得用typename来声明告诉编译器先实例化再去里面找东西,简单举例如下:

template <class T>
void func(T args)
{
    // 假设在上面已经定义好了一个模板类叫test,里面嵌套定义了一个模板类叫inside
    // 我们现在需要在这个函数模板中创建一个inside<T>的对象
    test<T>::inside<T> obj;  // 这样会失败,因为在编译器角度,test<T>是一个没有实例化的东西,自然就没法进入里面进行访问了,这时将模板参数类型改成typename,然后在需要实例化地方的前面加上typename即可
// 补充说明一下就是如果是test<int>这种指定了模板参数的就可以不用使用typename也能正常使用模板
}

// 正确写法
template <typename T>
void func(T args)
{
    typename test<T>::inside<T> obj;  // 这个typename是在告诉编译器,test<T>是一个类型
}

关于模板参数,对于模板类,需要在类名后在<>中指明模板参数类型,而函数模板就不需要,编译器会根据调用函数的参数列表来匹配对应的函数。

定义一个函数模板

假设现在给你一个任务,给我实现一个swap函数,且需要适用于多种类型的swap,“?这不是很简单吗?你要交换几个类型的我写几个给你就行。”肯定有人会这么想,但是这显然是一个不够明智的解决方案,内置类型是不多,但是自定义类型的个数是不确定的,这时还是来一个写一个的话,维护起来太费神气了,如果定义一个函数模板,就可以达到一劳永逸的效果:

template<class T>
void swap(T& x1, T& x2)
{
    T tmp = x1;
    x1 = x2;
    x2 = tmp;
}  // 轻轻松松七行搞定,你来多少个,都给你匹配上

对于一些特殊自定义类型,一般都会在类内自己定义好swap函数的,这里只是举一个一般性的例子。

定义一个类模板

假设现在再给你一个任务,给我模拟实现一下STL中的vector容器,“???”开个玩笑,开个玩笑,类模板的定义和函数模板逻辑上都是大差不差的,逻辑上都是确定好模板参数列表,然后在类中使用模板参数来做泛化地操作就是了,简单举例如下:

template<class T>
class A
{
public:
    A() {cout << "class A" << endl; }
    ...
private:
    T member;
};

 如果需要你实现一个getMember的函数,函数的返回类型会随着模板参数的变化而变化,此时我们用模板参数来作为函数getMember的返回类型即可:

template<class T>
class A
{
public:
    A() {cout << "class A" << endl; }
    T getMember() { return member; }
    ...

private:
    T member;
};

模板参数的玩法还有很多,比如在一个容器中封装一个反向迭代器等,大家可以肆意发挥自己的想象力。

模板的匹配

类模板的匹配规则很简单,因为类不支持重载,模板参数定下来就有唯一的实例化对象,所以在通过类名:即类型加<>来实例化时不会出现一对多的情况。

而对于函数模板,光看参数列表是不能确定所调用的函数的,在调用函数时,编译器会优先在普通函数中寻找匹配的函数,在找不到对应匹配的函数时,才会尝试通过函数模板来实例化出一个函数,这个尝试失败则会报错;其实普通函数就像是锅里面煮好的东西,而模板函数则是冰箱里待煮的一些速食食品和蔬菜生肉,在一个人饿的时候找东西吃时,假设要求要尽快吃好且这个人对食物的味道有一点要求,但不多的那种,那么这时候肯定优先考虑煮好的行不行,然后再考虑速食食品,最后才会想着自己动手做一顿咯,简单举例如下:

#include <iostream>
using namespace std;

// 锅里煮好的
void func(int x, int y)
{
    cout << "void func(int x, int y)" << endl;
}

// 速食食品
template<class T>
void func(T x, int y)
{
    cout << "void func(T x, int y)" << endl;

}

// 蔬菜生肉
template<class T>
void func(T x, T y)
{
    cout << "void func(T x, T y)" << endl;
}

int main()
{
    func(1, 1); // 1
    func(1.1, 1);  // 2
    func(1.1, 1.1);  // 3

    return 0;
}

试着猜猜main函数中三个func函数调用的分别是哪个函数? 

可以复制代码到本地编译器调试看看。

整型模板参数的使用

对于类模板,我们可以在模板参数列表中定义一个整型参数(char、short、int、size_t...)那么在后续要实例化这个对象时需要用一个整型来实例化改类对象:

template<class T, size_t N>
class array
{
public:
    ...

    size_t len(){ return N; }
    ...

private:
    T member[N];
}

如果直接将这个类暴露出来是会报错的,不是说模板定义有问题,而是原本的标准库中有array这个静态数组类模板,解决办法也很简单,把这个类模板放进自定义的命名空间中去就行,这里只是拿来讲整型模板参数的使用。

整型模板参数可以看成是类模板中定义的宏,之所以不把它当成变量,是因为一旦实例化后就不能修改,整型模板参数具有常量的常属性,所以在类模板中就把它当成一个宏来使用即可(需要注意的是这类模板参数只能是整型)

模板的特化

尽管一个模板已经可以解决绝大部分的问题了,但有些情况是需要特殊处理一下的,比如在一个比大小的函数中,实参不是对象本身,而是对象的指针时,靠比较指针值的大小来判断对象之间的大小显然是不行的,这个时候可以用上模板的特化功能了:

template<> // 全特化时,<>中不写内容
bool Less<int*>(int* num1, int*num2) // 在函数名或者类名后用<>来确定要特化类型
{
    return (*num1) < (*num2);
}

以上是一个全特化的例子,不完全特化的例子其实在定义一个函数模板的时候提到一下:

// 速食食品
template<class T>
void func(T x, int y)
{
    cout << "void func(T x, int y)" << endl;

}

是否完全特化会影响调用的优先级,准确的来说是特化的程度越高,优先级就越高,因为不完全特化在有三个以上模板参数时会有多种情况。

其实如果需要全特化的话,写成普通函数的效率是会高一些的,所以函数模板一般都不用特化,将要特化的情况单独写成一个普通函数即可,模板的特化在类模板中使用比较有发挥空间。

模板函数的显式调用

由于优先级的存在,模板函数总是替补登场,那如果就是想越过优先级较高的普通函数,来调用模板函数该怎么办呢?这个时候就可以像实例化类模板一样,在函数名后面用<>来显式指定模板参数即可:

template<typename T>
void func()
{
    cout << "void func<T>()" << endl;
}

void func()
{
    cout << "void func()" << endl;
}

int main()
{
    func<int>();
    func();

    return 0;
}

总结

模板实质上就是把程序员要做的事交给编译器来处理,用编译的花销来减少人工维护的成本,本文只是笔者的学习笔记,个人认为不够深入,还想学高阶的玩法的话就需要去阅读到大佬们的文章。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值