文章中有以下内容

  • 模板
    • 函数模板
      • 实例化
        • 隐式实例化
        • 显式实例化
      • 显式具体化
      • 函数调用的优先级
      • 函数模板返回值的类型
      • 可变参数的函数模板
    • 类模板
      • 类模板的实例化
        • 隐式实例化
        • 显式实例化
      • 类模板的显式具体化
        • 全部具体化
        • 部分具体化
      • 类模板的模板参数
        • 三种形式:
          • 用typename声明的类型参数
          • 非类型参数
          • 类模板参数
      • 类模板的继承与派生
    • 小结
  • Over
  • GL&HF

模板

在前面我们学过了类的继承,多态的思想,这些思想的目的都是让我们的程序设计变得简单和清晰,试想一下当我们在写一个函数重载的时候是不是就非常复杂。例如一个swap函数,用来交换两个相同类型的量。如果我们要重载的话,int类型要重载一个,string类型要重载一个,自定义类型Student也需要重载一个,这样就大大增加了代码的复杂度,为了解决这种问题,即实现代码重用,C++引入了模板

函数模板

如:

template<typename T>
void Swap(T& x,T& y){
	T z = x;
	x = y;
	y = z;
}

当我们使用Swap()函数的时候:
这里我们定义了int类型的a,b。当我们调用函数的时候,他会识别出a,b的类型,并且实例化出新的函数Swap();如果我们重新定义了string类型的变量,那么在编译的时候也会实例化出string类型的Swap函数。

int main(){
	int a,b;
	Swap(a,b);

	return 0;
}


实例化

隐式实例化

在上面的例子中,编译器在编译到Swap()时,自动的转换类型为int或者是string,我们并没有人为的去指明函数需要向哪个类型去实例化,对于这种实例化的方式,称之为隐式实例化。

显式实例化

对于隐式实例化有了一些体会之后,我们应该能想象出显式实例化是个什么东西,那么如何使用呢?
看下面的例子:

#include <iostream>
using namespace std;

template <typename T>//   👈   这里的类型参数可以有多个不同的。
T Add(T x, T y){
	return x+y;
}

int main(){
	int a=0;
	double b=0.1;
	cout<<Add<int>(a,b)<<endl;
	cout<<Add<double>(a,b)<<endl;
	return 0;
}

我们可以看到输出的结果一个为int 类型一个为double类型,这样就完成了显式实例化

注意:引用不能类型转换(不能通过显式实例化转换类型)。



显式具体化

如果我们想实现两个数组元素的相加,我们有如下的模板:

template <typename T>
T Add(T x,T y){
	return x+y;
}

我们知道,当我们进行内置的数据类型相加的时候,是合法的,如int,double类型的相加,但是,就像上面说的,我们如果想实现两个数组的相加,需要传递指针进去,那么两个指针的相加就没有任何的意义,先来看一下两个数组的声明:

int a[5] = {1,2,3,4,5};
int b[5] = {5,3,2,1,4};

这个时候,函数模板无法实现指针类型的处理,我们可以使用模板的显式具体化来解决函数模板处理特定数据类型的问题。

template <typename T>
T Add(T x, T y)
{
    return x + y;
}
template<>//   👈   因为已经具体化,所以括号里面没有参数
int* Add(int* x, int* y)//   👈   参数为int类型的指针
{
    int* z = new int[5];
    for(int i = 0; i < 5; i++)
    {
        z[i] = x[i] + y[i];//   👈   实现两个数组对应元素相加
    }
    return z;//   👈   返回新的数组.
}

这里的template<>及以下的几行由于已经具体化所以不是模板。



函数调用的优先级

  • 非模板函数优先于模板函数;
  • 函数模板中的显式具体化优先于隐式实例化
  • 转换少并且更具体的函数模板优先于其他函数模板


函数模板返回值的类型

当有不同类型的数据进行混合运算,最终的返回结果无法确定是什么类型。此时,我们可以使用auto和decltype关键字来确定函数模板的返回值类型。

是C++11标准中新增的特性。

template <typename T, typename U>
auto Add(T x, U y)->decltype(x+y) 
//   👆   ->decltype()是 后置返回类型
//   👆   auto和decltype是成对使用
//   👆   返回的就是decltype括号里面的类型。
{
    decltype(x+y) sum; //   👈   sum的数据类型与x+y的一致
    sum = x + y;
    return sum;
}

注意:

  • 函数模板中用typename声明到的类型参数在函数参数表中都要用到。
  • 函数模板的形参可以是确定的数据类型
  • 函数模板的模板参数可以使用内置数据类型,这种参数也成为“非类型模板参数”在函数体中可以直接调用。
  • 同一个函数模板中的类型参数不能重名,不同模板中可以重名
  • 函数模板声明中的类型参数名和定义时使用的类型参数名可以不同
  • 函数模板的参数可以有默认值


可变参数的函数模板

函数参数是可变的,数量是任意多个,类型是任意类型。
格式如下:

#include <iostream>
using namespace std;

template <typename T>
void showThis(T last){
	//   👆   递归调用的终止函数,作用是处理最后一个元素
    cout<<last<<endl;
    //   👆   这里参数名字可以任意,last只是为了方便理解;
}

template <typename T,typename ...Args>
void showThis(T first, Args... tails){ 
	//   👆   注意元运算符和Args之间的顺序!!!!
    //   👆   这里的Args,和first也只是为了方便理解,参数名字可以任意。
    cout<<first<<" ";
    showThis(tails...);//   👈   这里用到了递归的思想
}


int main(){
    showThis(1,2,5,8,"sdfsdf",'A');
    showThis("===","^^^^^^","******");
    return 0;
}

输出如下:

可以看到不论我们的实参的类型是什么,实参的数量有多少,都可以正确输出,也实现了我们所需的可变参数的函数模板的功能;
模板中的类型参数和形参名都可以为任意名称,这里只是为了方便理解。

  • "…"称为元运算符
  • Args是模板参数包
  • tails是参数展开包

类模板

先给一个例子

#include <iostream>
using namespace std;

template <typename T>
class Complex {
public:
    Complex(T real, T imag):tReal(real), tImag(imag) {};
    Complex operator+(Complex&); //   👈   重载加法运算符
    void ShowInfo();  //   👈   以(实部, 虑部)的形式输出复数
private:
    T tReal; //   👈   实部
    T tImag; //   👈   虚部
};
template <typename T>
Complex<T> Complex<T>::operator+(Complex<T>& c) {
	//   👆   这里的类名变为Complex<T>,以后要用到类名的地方都改为这个了
    //   👆   上面 最前面的Complex<T> 是函数的返回值类型,紧跟着的Complex<T>是类名
    Complex<T> temp = *this;//   👈   先把当前对象的值给temp
    temp.tReal = this->tReal + c.tReal;//   👈   然后分别更改实部和虚部,实现两个对象的相加
    temp.tImag = this->tImag + c.tImag;
    return temp;
}
template <typename T>
void Complex<T>::ShowInfo() {
    cout << "(" << tReal << ", " << tImag << ")" << endl;
}
int main() {
    Complex<int> c1(1, 2), c2(-3, 4);
    cout << "c1:";
    c1.ShowInfo();
    cout << "c2:";
    c2.ShowInfo();
    c1 = c1 + c2;
    cout << "c1+c2:";
    c1.ShowInfo();
    return 0;
}

当我们去生成一个类对象的时候,或者是调用其中的函数的时候,这个类及其成员函数都会实例化

类模板的实例化

如果类模板声明了多个类型参数,创建对象时需要为类模板的每一个类型参数指定一个具体的数据类型。数据类型之间用逗号分隔。

隐式实例化

如下:
注意:当我们实例化的时候,必须显式地为类模板提供所需的类型,编译器需要根据提供的类型实例化出一个模板类,不能缺省类型;

template<typename T1,typename T2>
class A{
private:
	T1 num;
	T2 name;
};
A<int,string> a;//   👈   创建对象a

也可以在类模板中为类型参数指定默认值:

template<typename T = int>
class Complex{
	...
};

Complex<>c;//   👈   省略类型,编译器使用int类型实例化类模板

Complex<int>c;//   👈   隐式实例化

只有声明对象的时候才会隐式实例化:

Complex<int>* pt;
//   👆   声明指针,不实例化对象,不需要实例化类模板
pt = new Complex<int>;
//   👆   实例化对象,需要实例化类模板

显式实例化

用关键字template为类模板显式地指定数据类型,编译器将生成类模板的显式实例化。例如:

template class Complex<int>;

类模板的显式具体化

(类比函数模板的显式具体化)
这里用Tuple(元组)来举个例子,元组就像(x,y)坐标一样。

template <typename T1, typename T2>
class Tuple
{
public:
    Tuple(T1 first, T2 second):tFirst(first), tSecond(second){}
    //   👆   初始化
    
    bool IsEqual(Tuple<T1, T2>&);
    //   👆   上面这一行为什么要加上<>呢?
    //   👆   类内可以省略这个,但是类外一定要加上这个<T1,T2>
    
private:
    T1 tFirst;//   👈   这里可以理解为T1类型的x
    T2 tSecond;//   👈   同样的,这里可以理解为T2类型的y
};
template <typename T1, typename T2>
bool Tuple<T1, T2>::IsEqual(Tuple<T1, T2>& s)
{
    cout << "调用模板函数IsEqual……" << endl;
    return this->tFirst == s.tFirst && this->tSecond == s.tSecond;
}

当我们来比较两个int类型的元素时,用上面这种是可以实现的,但如果我们想比较两个指针所指向的东西是否相等呢?
这时候我们引入一个全部具体化的概念。


全部具体化

template <>  //   👈   类模板Tuple的显式具体化-全部具体化,因为下面的两个类型全部指明,所以这里<>里面为空,这种方式也称作全部具体化。
class Tuple<char*, char*>  //   👈   指明要实例化的数据类型
{
public:
    Tuple(char* f, char* s):tFirst(f), tSecond(s){}
    bool IsEqual(Tuple&);//   👈   这里不再是模板,而是一个具体函数,因为已经指明的具体的类型
private:
    char* tFirst; //   👈   第一个元素表示学生姓名
    char* tSecond; //   👈   第二个元素表示家庭住址
};
bool Tuple<char*, char*>::IsEqual(Tuple<char*, char*>& s) //   👈   这个函数不需要加模板头
{
    cout << "调用全部具体化类函数IsEqual……" << endl;
    return strcmp(this->tSecond, s.tSecond)==0; //   👈   只比较家庭住址

	//   👆   strcmp()函数,来自于c.string头文件,比较两个字符串。
	//   👆   返回值为0,则相等
}

部分具体化

template <typename T>  //   👈   部分具体化,带上这个模板声明的   头
class Tuple<char*, T>  //   👈   指明要实例化的部分数据类型,未具体化的仍然用抽象数据类型T
{
public:
    Tuple(char* f, T s):tFirst(f), tSecond(s){}
    bool IsEqual(Tuple&);
private:
    char* tFirst; //   👈   表示学生的姓名
    T tSecond;    //   👈   表示学生的年龄、成绩等
};
template <typename T>  //   👈   带上这个模板声明的头
bool Tuple<char*, T>::IsEqual(Tuple<char*, T>& s)
{
    cout << "调用部分具体化类函数IsEqual……" << endl;
    return this->tSecond == s.tSecond;
}

我们来看一下函数的调用:

int main()
{
    Tuple<int, int> point1(1, 3), point2(2, -2);//平面坐标点
    //  👆   调用函数模板  👆   
    if(point1.IsEqual(point2)) cout << "这两个点的坐标相等" << endl;
    //   👆  使用函数模板的 IsEqual()  👆   
    else cout << "这两个点的坐标不相等" << endl;
    Tuple<char*, char*> student1("Kevin", "30 Beijing Road 30"), student2("Jason", "30 Beijing Road 30");
    //  👆   使用全部具体化的类  👆   
    if(student1.IsEqual(student2)) cout << "这两个学生的家庭住址相同" << endl;
    //  👆   使用全部具体化的   👆   
    else cout << "这两个学生的家庭住址不同" << endl;
    int age1 = 19, age2 = 20;
    Tuple<char*, int> student3("Kevin", 19), student4("Jason", 20);
    //  👆   使用部分具体化的类  👆   
    if(student3.IsEqual(student4)) cout << "这两个学生的年龄相同" << endl;
    else cout << "这两个学生的年龄不同" << endl;
    return 0;
}



类模板的模板参数

三种形式:

用typename声明的类型参数
template<typename T1,typename T2>
非类型参数
template<int SIZE>

相当于为类模板预定义的一些常量,在生成模板类的时候要求以常量作为实参传递给非类型参数。
非类型参数只能是 整型,枚举,指针和引用类型(如: double类型不能作为非类型参数,但double*和double&是可以作为非类型参数的

类模板参数

把一个类模板作为另一个类模板的参数

template<...,template<模板参数> class A,...>
class B{
	...
};


类模板的继承与派生

类模板可以作为基类派生,也可以通过其他的基类派生类模板,实现代码重用。

  • 类模板作为基类,派生普通类
  • 类模板作为基类,派生新的类模板
  • 普通类作为基类,派生类模板


小结

函数模板和类模板都是实现代码重用的方式,都要通过实例化去生成具体的模板函数或模板类,如果模板不适用于特殊的类型,这时候我们可以显式具体化此模板。



Over

GL&HF

更多推荐

C++(函数模板、类模板详解)