一篇文章带你掌握C++模板编程

 

模板初阶

泛型编程

在计算机程序设计领域,为了避免因数据类型的不同,而被迫重复编写大量相同业务逻辑的代码,人们发展的泛型及泛型编程技术。什么是泛型呢?实质上就是不使用具体数据类型(例如 int、double、float 等),而是使用一种通用类型来进行程序设计的方法,该方法可以大规模的减少程序代码的编写量,让程序员可以集中精力用于业务逻辑的实现。泛型也是一种数据类型,只不过它是一种用来代替所有类型的“通用类型”

我们通常如何实现一个通用的交换函数呢?

void Swap(int& left, int& right)
{
  int temp = left;
  left = right;
  right = temp;
}
void Swap(double& left, double& right)
{
  double temp = left;
  left = right;
  right = temp;
}
void Swap(char& left, char& right)
{
  char temp = left;
  left = right;
  right = temp;
}
......

Swap函数能实现各种类型的变量交换,但是只要类型不同就需要重新写一个

使用函数重载虽然可以实现,但是有一下几个不好的地方:

  • 重载的函数仅仅只是类型不同,代码的复用率比较低,只要有新类型出现时,就需要增加对应的函数
  • 代码的可维护性比较低,一个出错可能所有的重载均出错,那能否告诉编译器一个模版,让编译器根据不同的类型利用该模版来生成代码呢?

可以的,C++语法中有了模板:

函数模板

函数模板概念

所谓函数模板,实际上是建立一个通用函数,它所用到的数据的类型(包括返回值类型、形参类型、局部变量类型)可以不具体指定,而是用一个虚拟的类型来代替(实际上是用一个标识符来占位),等发生函数调用时再根据传入的实参来逆推出真正的类型。 这个通用函数就称为 函数模板(Function Template) 。函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。

函数模板格式

template<typename T1, typename T2,…,typename Tn>
返回值类型 函数名(参数列表){}

template<typename T>
//或者 template<class T>
void Swap(T& x1, T& x2)
{
  T temp = left;
  left = right;
  right = temp;
}

T1,T2等等是什么类型现在也不确定,一会用的时候才能确定

注意:

typename是用来定义模板参数关键字,也可以使用class

函数模板的原理

函数模板本身并不是函数,是编译器根据调用的参数类型产生特定具体类型函数的模具,所以其实模板就是将本来应该我们做的重复的事情交给了编译器,我们看下面的例子:

template<class T>
void Swap(T& x, T& y)
{
	T temp = x;
	x = y;
	y = temp;
}
int main()
{
	int a = 1;
	int b = 2;
	Swap(a, b);
	char A = 'a';
	char B = 'b';
	Swap(A,B);
	return 0;
}

image-20211024163521112

在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用int类型使用函数模板时,编译器通过对实参类型的推演,将T确定为int类型,然
后产生一份专门处理int类型的代码,对于字符类型也是如此。

然而当我们在写了函数时,不会进入模板函数里,没有写具体的函数时,就会进入模板函数里,我们看下面的例子:

void Swap(int& x, int& y)
{
	int temp = x;
	x = y;
	y = temp;
}
template<class T>
void Swap(T& x, T& y)
{
	T temp = x;
	x = y;
	y = temp;
}
int main()
{
	int a = 1;
	int b = 2;
	Swap(a, b);
	char A = 'a';
	char B = 'b';
	Swap(A,B);
	return 0;
}

我们进行调式:

模板

我们可以看到int类型的交换函数我们写了,调用时调用的是我们写的,而char类型的我们没写,就用了模板。

那么这里调用的是模板函数吗?

不是的,实际上这里会有两个过程

1、模板推演,推演T的具体类型是什么

2、推演出T的具体类型后实例化生成具体的函数

上面的代码实例化生成了下面的函数:

void Swap(char& x, char& y)
{
	char temp = x;
	x = y;
	y = temp;
}

真正调用的还是两个函数,但是其中的一个函数不是我们自己写的,而是我们给了编译器一个模板,然后编译器进行推演在编译之前实例化生成三个对应的函数,模板是给编译器用的,编译器充当了写函数的工具:

image-20211024161726234

可以看到这里是调用了Swap<char>函数

在C++当中,其实内置类型也可以像自定义类型那样这样初始化:

int a(1);
int(2);//匿名

image-20211024162224522

void Swap(T& x1, T& x2)
{
  T temp(x1);
  x1 = x2;
  x2 = x1;
}

所以模板还可以这样写,可以使内置类型和自定义类型兼容:

void Swap(T& x1, T& x2)
{
  T temp(x1);
  x1 = x2;
  x2 = x1;
}

我们来具体看一看函数模板的实例化:

函数模板的实例化

用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。

隐式实例化:让编译器根据实参推演模板参数的实际类型

template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}
int main()
{
  int a1 = 10, a2 = 20;
  double d1 = 10.0, d2 = 20.0;
  Add(a1, a2);
  Add(d1, d2);
// 此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化
  Add(a1, d2);
  return 0;
}

该语句是不能够通过编译的,因为在编译期间,当编译器看到该实例化时,用a1去推T是int,而用d2去推是double,但是模板参数列表里只有一个T,编译器不能明确该T是int还是double,T是不明确的,所以编译器会报错

那么怎么处理呢?

解决方式:

1、调用者自己强制转换

//实参去推演形参的类型
Add(a1, (int)d2);
Add((double)a1,d2);

这里可以将d2先强制类型转换,然后再进行推演;或者将a1先强制类型转换再进行推演

2、使用显式实例化

//实参不需要去推演形参的类型,显式实例化指定T的类型
Add<int>(a1, d2);
Add<double>(a1,d2);

这种方式是显式实例化指定T的类型

显式实例化在哪种场景可用呢?看下面的这种场景:

class A
{
  A(int a=0):_a(a)
  {}
private:
  int _a;
};
template<class T>
T func(int x)
{
  T a(x);
  return a;
}
int main()
{
  func<A>(1);
  func<int>(2);
  return 0;
}

有些函数模板里面参数中没用模板参数,函数体内才有用到模板参数,此时就无法通过参数去推演T的类型,这时只能显示实例化

上面我们提了一点模板参数的匹配原则,下面我们具体看看模板参数的匹配原则:

模板参数的匹配原则

一个非模板函数可以和一个同名的函数模板同时存在,此时如果调用地方参数与非模板函数完全匹配,则会调用非模板函数

int Add(int left, int right)
{
	return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right)
{
	return left + right;
}
int main()
{
  Add(1,2);//调用自己的函数
  return 0;
}

Add(1,2)参数是int类型,而我们有现成的int参数的Add函数,所以有现成的就用现成的,编译器也会偷懒

那么如果我们想让这里调用必须用模板呢?显式实例化:

Add<int>(1,2);

这样编译器就强制会用模板去实例化函数

一个非模板函数可以和一个同名的函数模板同时存在,此时如果调用地方参数与非模板函数不完全匹配,则会优先使用模板实例化函数

int Add(int left, int right)
{
	return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right)
{
	return left + right;
}
int main()
{
  Add(1.1,2.2);//使用模板实例化函数
  return 0;
}

模板匹配原则总结:

有现成完全匹配的,那就直接调用,没有现成调用的,实例化模板生成,如果有需要转换类型才能匹配的函数(也就是不完全匹配),那么它会优先选择去实例化模板生成。

优先级:

完全匹配>模板>转换类型匹配

 

类模板

类模板的定义格式

template<class T1, class T2, ..., class Tn>
class 类模板名
{
	//类内成员定义
};

我们来看一个类模板的使用场景:

typedef int STDateType;
class Stack
{
private:
  STDateType* _a;
  int _top;
  int _capacity;
};
int main()
{
  Stack st1;
  Stack st2;
  return 0;
}

这是我们定义的栈数据结构,我们创建了两个栈对象,但是现在st1和st2的存储数据的类型都是int,要是想转换数据类型呢?

typedef double STDateType;

我们这样就转换了,但是我们要是想st1为int,st2为double呢:

Stack st1;//int
Stack st2;//double

此时需要写多个类,名字还得不一样,如下:

typedef int STDateType1;
typedef double STDateType2;
class IntStack
{
private:
  STDateType1* _a;
  int _top;
  int _capacity;
};
class DoubleStack
{
private:
  STDateType2* _a;
  int _top;
  int _capacity;
};

这样太麻烦了,那么什么办法可以解决呢?类模板可以解决:

//类模板
template<class T>
class Stack
{
private:
  T* _a;
  int _top;
  int _capaticy;
};
int main()
{
  //类模板的使用都是显式实例化
  Stack<double> st1;
  Stack<int> st2;
  return 0;
}

注意:Stack不是具体的类,是编译器根据被实例化的类型生成具体类的模具

类模板的实例化

//类模板
template<class T>
class Stack
{
public:
  Stack(int capacity = 4)
      :_a(new T(capacity))
       ,_top(0)
       ,_capacity(capacity)
      {}
  ~Stack()
  {
      delete[] _a;
      _a = nullptr;
      _top = _capacity = 0;
  }
  void Push(const T& x)
  {
      //...
  }
private:
  T* _a;
  int _top;
  int _capaticy;
};
int main()
{
  //类模板的使用都是显式实例化
  Stack<double> st1;
  Stack<int> st2;
  return 0;
}

注意:类模板的使用都是显式实例化

假设我们想类里面声明和类外面定义成员函数呢?

//类模板
template<class T>
class Stack
{
public:
  Stack(int capacity = 4)
      :_a(new T(capacity))
       ,_top(0)
       ,_capacity(capacity)
      {}
  ~Stack()
  {
      delete[] _a;
      _a = nullptr;
      _top = _capacity = 0;
  }
  //假设我们想类里面声明和定义分离呢?
  void Push(const T& x);
private:
  T* _a;
  int _top;
  int _capaticy;
};
//在类外面定义
template<class T>
void Stack<T>::Push(const T& x);
{
  //...
}
int main()
{
  //类模板的使用都是显式实例化
  Stack<TreeNode*> st1;
  Stack<int> st2;
  return 0;
}
//在类外面定义
template<class T>
void Stack<T>::Push(const T& x);
{
  //...
}

在类外面定义我们必须要加模板的关键字,以及需要在实现的函数前面表明域Stack<T>。普通类,类名就是类型,对于类模板,类名不是类型,类型是Stack<T>,需要写指定

注意:

模板不支持把声明写到.h,定义写到.cpp,这种声明和定义分开实现的方式,会出现链接错误

 

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注编程宝库的更多内容!

#ifndef Rational_hpp#define Rational_hpp#include <stdio.h>#include <string>using namespace st ...