关于c++的模板

目录

模板是什么?为什么要有?

函数模板

模版原理

模板参数的匹配规则

类模板

非类型模版参数

模板的特化

模板的分离编译

模板为什么不支持分离编译?


模板是什么?为什么要有?

        引子:

        在古代,人们在发明印刷术之前是有需求的!什么需求呢?首先因为我们要有书,而在还没有印刷术时,书只能通过人们来誊抄,这样子的效率是极其低的,以至于后来发明了雕版印刷术,而人们只要刻画出一本书的雕版再把这个雕版盖上墨水就能实现印刷了(如图1-1),但后来,人们渐渐发现如果我要印刷的都是同一本书,那么雕版印刷术确实很不错,但如果我想印刷其他书呢?不是就要另外弄一个新的雕版重新刻字吗?这也太浪费时间了吧!于是人们后来又发明了活字印刷术,活字印刷术就是把每个字都进行了单个雕刻,而如果我们需要印刷一本新的书,只需要找到对应的字即可印刷了!而c++设计出模板跟我们印刷术的发明是很像的!

     图1-1.(雕版印刷)   

 模板就是把一个类型进行泛型化,这样我们在写一个函数或是一个类的时候就不需要再写一个新的类型的函数重载。比如:如果我实现了一个swap函数,但只写了int类型的,那这个时候我们又想写一个double类型的交换函数,怎么办呢?再写一个函数重载?但是int和double类型的交换函数的逻辑是相同的,如果再写一个的话这样是不是太冗余了?有人或许又会说,我们可以使用typedef啊,如果我们需要int类型时就把int类型typedef,如果我们需要double类型时就把double类型typedef。这样或许可以解决一些问题,但是这却跟雕版印刷一样治标不治本!例如,如果我现在同时就要交换两个int类型和double类型呢?这时typedef就会失效了!这时就要用到我们的模板了

函数模板

首先,先介绍一下函数模板的格式

模板格式:template<class 参数名 , class 参数名2 , ....class 参数名n>(这里的参数名指的是模板参数名,而这里....表示模板参数可以有很多,并且这里的class是可以替换为typename的,只是我平时不常用typename所以就默认class就好了)

注意:我们的模板格式需要写在需要模板的函数/类上面

如下:我们写一个swap的模板,结合例子再理解一下

#include<iostream>
using namespace std;

template<class T>
void swap(T& a , T& b)
{
    T tmp = a;
    a = b;
    b = tmp;
}

int main()
{
    int a1 = 1 , b1 = 2;
    double a2 = 1.1 , b2 = 2.2;
    swap(a1,b1)//编译器会自动识别传过去的参数,所以这里会调用void swap(int& a, int& b);
    swap(a2,b2)//这里就是调用void swap(double& a,double& b);
    return 0;
}

模版原理

我们可以看到,在以上案例中,模版的功能好像非常的强大啊,它居然能自动识别类型,然后调用不同的函数,那模版到底是怎么做到的呢?

首先,我们仔细思考一下,如果我们没有模版的时候要写一个这样子的swap函数怎么实现的呢?没错,我们会利用函数重载直接把类型换了重写一份,而本质上模版的原理也是如此,只是他把需要我们重写的部分直接编译器实现了,而这好像也就给了我们模版会自动识别的错觉!

那么,让我们看看具体替换的过程吧,如图2

                                                        图2

 我们把按照模板替换类型后生成的叫做模板的实例化

那么,为什么叫做模板的实例化呢?因为在我们的模板是不会直接保存在地址空间里的,只有替换了具体类型后的会保存在地址空间里,这也是为什么要叫做实例化的原因!

模板参数的匹配规则

在我们使用模板时,是可以显示写函数名相同且拥有具体类型的函数,如下

#include<iostream>
using namespace std;

void Swap(int a , int b)
{
    int tmp = a;
    a = b;
    b = tmp;
}


template<class T>
void Swap(T& a , T& b)
{
    T tmp = a;
    a = b;
    b = tmp;
}

int main()
{
    int a = 1 , b = 2;
    Swap(a,b);//这里不会调用模板
    Swap<int>(a,b);//这里会通过模板再实例化一个
}

当显示写了以后,默认情况下如果我们要调用显示写的那个类型的交换函数,例如这里的int,那么编译器就不会再通过模板实例化一个函数,而是直接走显示写的那个函数

但我们可以强制他走模板实例化,可以在调用的地方函数名之后加上<类型>

在我们使用模板时,模板是不支持隐式类型的转化,也就是一个模板参数只支持一个类型,如下

template<class T>
T Add(T& a, T& b)
{
	return a + b;
}

int main()
{
	double a = 1.1;
	int b = 2;
	cout << Add(a, b) << endl;//此处会报错
	return 0;
}

 总结:模板参数默认匹配原则就是如果显示定义的参数更符合调用那么优先调用显示定义的,而当显示写的不符合类型时才会去调用模板

类模板

类模板和函数模板的语法格式及意义是很相似的

语法格式:

template<class 参数名1,class 参数名2, .... class 参数名n>

class 类模板名{

        

}

使用案例

#include<iostream>
using namespace std;

template<class T>
class Vector
{
    Vector()
    :_num(nullptr)
    ,_size(0)
    ,_capacity(0)
    {}
    //.....
private:
    T* _num;
    int _size;
    int _capacity;
};

可以看到,类模板可以参考函数模板的使用,但问题又来了,类模板我们应该怎么实例化出来呢?

类模板的实例化是通过创建对象的,当对象创建出来的时候也就实例化出来了,不过需要注意的是在我们实例化对象时要带上传过去的模板参数,如下

#include<iostream>
using namespace std;

template<class T>
class Vector
{
    Vector()
    :_num(nullptr)
    ,_size(0)
    ,_capacity(0)
    {}
    //.....
private:
    T* _num;
    int _size;
    int _capacity;
};

int main()
{
    Vector<int> v;//Vector在之后要加上<类型>,表示你传过去的T是一个int类型的参数
}

非类型模版参数

我们从上面的函数模版和类模版中了解到,在我们一般情况下都是类/函数模版,但在我们实际使用当中还会遇到一个场景,那就是定义一个静态的数组,有人可能会说,这还不简单,定义一个宏直接修改宏的大小就行了。那如果我们有两个不同的变量一个要求开100个空间,一个要求1000个空间呢?再写一个?那如果我们有n个变量要初始化成不同的长度呢?

通过以上的场景,我们就要引入非类型模版参数了

非类型模版参数就是把一个常量设置到模版参数中,当我们通过模版传大小过去时就可以形成自己需要的长度,如下

#include<iostream>
using namespace std;


template<class T , size_t size = 10>
class Array{
    Array()
    {
        cout << "size = " << size <<endl;
    }
private:
    T _array[size];
};

int main()
{   
    Array<int> a0;//打印size = 10;
    Array<int,100> a1;//打印size = 100;
    Array<int,1000> a2;//打印size = 1000;
}

模板的特化

在我们实际场景中还有一个特殊的场景,我们知道,模板可以把类型传过去然后通过把模板类型替换成传过去的类型然后实例化生成不同类型的实例化函数/类,但我们有时候需要传一些指针/引用来实现这个功能,那么我们就需要对这个指针/引用进行特殊化的处理,这时就要用到我们的模板特化了,如下我们用less函数来做这个案例

#include<iostream>
using namespace std;

template<class T>
bool Less(T a, T b)
{
    return a < b;
}

class Date {
public:
    Date(int year=1, int month=1,int day=1)
        :_year(year)
        ,_month(month)
        ,_day(day)
    {}
    bool operator<(const Date d)
    {
        return _year < d._year 
            || (_year == d._year && _month < d._month) 
            || (_year == d._year && _month == d._month && _day < d._day);
    }
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    //传两个Date类型,比较日期大小
    Date d1(2021, 9, 8);
    Date d2(2021, 9, 7);
    cout << Less(d2,d1) << endl;//此处运行正确,输出0
    //传两个Date*类型,比较日期大小
    Date* pd1 = &d1;
    Date* pd2 = &d2;
    cout << Less(pd2, pd1) << endl;//此处运行错误,输出1

错误原因:这里错误的原因是因为当我们把pd1及pd2传过去时Less函数会对指针指向的类型比较大小,而不是调用Date的operator<,当我们不想要这样的调用方式时就要对这个模板进行特殊处理

模板特化的格式如下示例

//普通模板
template<class T>
bool Less(T a, T b)
{
    return a < b;
}
//特化Date* 类型
template<>//这个<>里面不用写
bool Less<Date*>(Date* p1, Date* p2)
{
    //对Date*类型进行解引用处理
    return *p1 < *p2;
}

此时,如果我们再进行两个Date*类型的比较时就会自动调用这个特化后的模板

而我们把普通模板所有参数都显示实例化成自己需要的类型我们叫做全特化,如上代码就是实现了一个全特化

而在我们平时使用中,特化主要应用场景也不是函数,因为函数我们可以直接显示写一个,在我们平时使用中特化更多的应用场景还是类,如下为类的全特化

#include<iostream>
using namespace std;
//普通模板
template<class T1,class T2>
class Data
{
public:
    Data()
    {
        cout<<"Data"<<endl;
    }
private:
    T1 _d1;
    T2 _d2;
};
//全特化模板:所有模板参数都确定化
template<>
class Data<int*,double>
{
public:
    Data()
    {
        cout<<"Data<int*,double>"<<endl;
    }
private:
    int* _d1;
    double _d2;
};

类的全特化和函数的全特化是非常像的,而类的偏特化有两种方式,第一种就是只对一部的模板参数进行限制,但不会全部限制,也就是会有一部分模板一部分特化,如下

#include<iostream>
using namespace std;
//普通模板
template<class T1,class T2>
class Data
{
public:
    Data()
    {
        cout<<"Data"<<endl;
    }
private:
    T1 _d1;
    T2 _d2;
};
//偏特化模板一:对部分参数进行限制
template<class T>
class Data<int*,T>
{
public:
    Data()
    {
        cout<<"Data<int*,T>"<<endl;
    }
private:
    int* _d1;
    T _d2;
};

第二种偏特化不会对参数进行类型限制,而是在原模板的基础上加上指针/引用等特殊版本,如下

#include<iostream>
using namespace std;
//普通模板
template<class T1,class T2>
class Data
{
public:
    Data()
    {
        cout<<"Data"<<endl;
    }
private:
    T1 _d1;
    T2 _d2;
};
//偏特化模板二:针对原模版的特殊类型进行特殊处理
template<class T1,classT2>
class Data<T1*,T2*>
{
public:
    Data()
    {
        cout<<"Data<T1*,T2*>"<<endl;
    }
private:
    int* _d1;
    T _d2;
};
int main()
{
    Data<int,char> d1;//打印"Data"
    Data<int*,char*> d2'//打印cout<<"Data<T1*,T2*>"<<endl;
    return 0;
}

模板的分离编译

结论:

1、模板在同一个工程并且同一个文件下可以进行分离编译,不过要在定义的地方也加上模板参数,如下

----------------同一个.cpp下-------------
#include<iostream>
using namespace std;

template<class T>
class Data
{
public:
    void Print()
    {
        cout<<"Hello,I am Data<T>::Print()"<<endl;
    }
private:
    int _a;
    int _b;
};


template<class T>
Data<T>::Data()
{
    cout << "template<class T>" << endl;
}
----------------test.cpp文件下--------------

int main()
{
    Data<int> d;
    d.Print();//打印输出:Hello,I am Data<T>::Print()
    return 0;
}

2、模板不能定义和声明同时分离在不同的文件下,如下

----------------.h文件下-------------
#include<iostream>
using namespace std;

template<class T>
class Data
{
public:
    void Print()
    {
        cout<<"Hello,I am Data<T>::Print()"<<endl;
    }
private:
    int _a;
    int _b;
};

-------------.cpp文件下--------------

template<class T>
Data<T>::Data()
{
    cout << "template<class T>" << endl;
}

-------------test.cpp文件下-------------

int main()
{
    Data<int> d;
    d.Print();//程序报错
    return 0;
}

模板为什么不支持分离编译?

首先,在我们对象创建的时候我们会把.h文件下的类给实例化出来,此时Data类的Print函数只有一个声明,由函数重载那里的编译原理我们知道,如果函数调用我们只有他的声明那么在编译阶段就会暂时不填上他的地址,等到链接阶段以后我们进行合并符号表,这时声明也就找到了对应的定义最后再把定义的地址给填上,一个普通函数的分离编译就实现了,但这里的问题是我们虽然类已经实例化出来了,但是类里面只有函数的声明没有定义,而当我们要合并符号表时由于那个定义的地方也要用模板参数,而用了模板,函数就不会进行实例化,而没有实例化那么这个函数的定义就不会进入符号表也就导致了我们无法链接上!

图解

 

 

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值