C++: 模板初阶

一. 泛型编程

如何实现一个通用的交换函数呢? 参数是不同类型的数据.

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

使用函数重载虽然可以实现, 但是仍然有不足:

  1. 重载的函数仅仅是类型不同, 代码复用率比较低, 只要有新类型出现时, 就需要用户自己增加对应的函数
  2. 代码的可维护性比较低, 一个出错可能所有的重载均出错.

如果需要能在用户提供的类型上使用此函数, 这种策略就失效了.


那么, 能否告诉编译器一个模子, 让编译器根据不同的类型利用该模子来生成代码呢?

就像活字印刷术一样, 只要提供了字模, 就可以用不同的颜色来印出同样的字

在这里插入图片描述

C++提供了模板的语法, 给这个"模具"中填充不同的类型, 就可以生成具体类型的代码

泛型编程: 编写与类型无关的通用代码, 是代码复用的一种手段. 模板是泛型编程的基础.

在这里插入图片描述

二. 函数模板

针对上面 Swap 函数有多种类型参数的情况, 可以定义一个通用的函数模板(function template), 而不是为每个类型都定义一个新函数.

一个函数模板就是一个公式, 可用来生成针对特定类型的函数版本. Swap 的模板版本可能像下面这样:

template <typename T>     // 模板定义格式:  template<typename T1, typename T2, ... , typename Tn>
void Swap(T &left, T &right)
{
  T temp = left;
  left = right;
  right = temp;
}

模板定义以关键字 template 开始, 后面跟一个模板参数列表, 这是一个用逗号分割的一个或多个模板参数的列表, 用小于号 < 和 大于号 > 包围起来.

在模板定义中, 模板参数列表不能为空.

模板参数列表的作用很像函数参数列表.
函数参数列表定义了形参对象, 模板参数列表定义了类型.

模板参数表示类或函数定义中用到的类型或值. 当使用模板的时候, 显示或隐式地制定模板实参, 将其绑定到模板参数上.

比如上述的 Swap 函数声明了两个名为 T 的类型参数, T 表示一个类型, T 的实际类型则在编译时根据传入的参数确定.

对于不同的参数类型, 最终调用的函数参数类型也不同

在这里插入图片描述


函数模板的原理

函数模板是一个蓝图, 它本身并不是函数, 是编译器用使用方式产生特定具体类型函数的模具. 所以其实模板就是将本来应该我们做得重复的事情交给了编译器.

在这里插入图片描述

在编译器编译阶段, 对于函数模板的使用, 编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用.

例如: 当用 double 类型使用函数模板时, 编译器通过对实参类型的推演, 将 T 确定为 double 类型, 然后产生一份专门处理 double 类型的代码

注意 函数模板的模板参数类型可以写 typename, 也可以写 class (不可以写 struct).

函数模板的实例化

用不同类型的参数使用函数模板时, 称为函数模板的实例化.

在这里插入图片描述

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

template <typename T>
T Add(const T &left, const T &right)
{
  return left + right;
}

int main()
{
  int a1 = 10, b1 = 20;
  Add(a1, b1);    // 推演出模板参数类型为 int

  double a2 = 1.1, b2 = 2.2;
  Add(a2, b2);    // 推演出模板参数类型为 double

  Add(a1, b2);    // 推演失败

  return 0;
}

前两个可以通过编译, 函数模板类型相同, 编译器可以推演出函数模板的类型.

第三个传入了一个 int 类型和一个 double 类型, 编译器推演模板参数类型失败.

在这里插入图片描述

因为定义模板函数的时候, 规定了模板函数参数只有一个类型 T, 传入两个类型, 编译器不能确定应该是将 T 演绎成哪个类型.

在模板中, 编译器不会进行类型转换操作, 一旦转换出问题, 编译器就会背黑锅, 所以不会隐式转换.

有两种处理方式:

  1. 用户自己进行强化类型转换
  2. 使用显示实例化
Add(a1, (int)b2);   // 用户自己强制类型转换

显示实例化: 在函数名后的<>中制定模板参数的世纪类型

int main()
{
  int a1 = 10;
  double b = 20.0;

  Add<int> (a1, b2);  // 显示实例化

  return 0;
}

如果类型不匹配, 编译器会进行隐式类型转换, 如果无法转换成功, 编译器会报错.


如果函数参数没有模板参数, 那么就无法使用隐式实例化, 只能进行显示实例化.

// 申请一个T类型十个元素的数组并返回
template <typename T>
T *f()
{
  T *p = new T[10];
  return p;
}

int main()
{
  int *p1 = f<int>();
  double *p2 = f<double>();

  return 0;
}

模板参数的匹配原则

  1. 一个非模板函数和一个同名的函数模板同时存在, 而且该函数模板可以被实例化为这个非模板函数
// 专门处理 int 类型的加法函数
int Add(const int &left, const int &right)
{
  cout << "(int) Add" << endl;
  return left + right;
}

// 通用加法函数
template <typename T>
T Add(const T &left, const T &right)
{
  cout << "(template) Add" << endl;
  return left + right;
}

int main()
{
  Add(1, 2);          // 有现成的优先用现成的
  Add<int>(1, 2);     // 如果指定显示实例化, 用函数模板
}

在这里插入图片描述


  1. 对于非模板函数和同名函数, 如果其他条件都相同, 在调用时会优先调用非模板函数而不会从该模板产生一个实例. 如果模板可以产生一个具有更好匹配的函数, 那么将选择模板.
// 专门处理 int 类型的加法函数
int Add(const int &left, const int &right)
{
  cout << "(int) Add" << endl;
  return left + right;
}

// 通用加法函数
template <typename T1, typename T2>
T1 Add(const T1 &left, const T2 &right)
{
  cout << "(template) Add" << endl;
  return left + right;
}

int main()
{
  Add(1, 2);          // 与非函数模板完全匹配, 不需要函数模板实例化
  Add(1, 2.0);        // 有更合适的不会进行隐式类型转换调用非函数模板, 而会直接调用函数模板实例化的实例
}

在这里插入图片描述


  1. 函数模板不允许自动类型转换, 而普通函数可以进行自动类型转换
// 专门处理 int 类型的加法函数
int Add(const int &left, const int &right)
{
  cout << "(int) Add" << endl;
  return left + right;
}

// 通用加法函数
template <typename T>
T Add(const T &left, const T &right)
{
  cout << "(template) Add" << endl;
  return left + right;
}

int main()
{
  Add(1, 2.0); // 函数模板不能进行自动类型转换, 只能调用普通函数, 传参的时候进行自动类型转化
}

在这里插入图片描述


总结

  1. 优先调用现有的普通函数
  2. 没有函数模板, 普通函数参数可以自动类型转换的, 使用普通函数.
  3. 可以通过函数模板实例化更合适的函数, 哪怕普通函数可以自动类型转换, 也用函数模板.

三. 类模板

以前写数据结构的时候, 通常会用 typedef 重命名数据结构内数据的类型.
例如 Stack 中用 typedef int STDataType, 制定 Stack 中的数据类型是 int 类型的.

但是, 这样只能保证一次只能用 int, 如果需要用到 double 的, 需要再重新写一份.

有了类模板就能很好的解决这个问题.

类模板的定义格式

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

例如: Stack

template <class T>
class Stack
{
public:
  Stack(size_t capacity = 10)
      : _array(new T[capacity])
      , _capacity(capacity)
      , _top(0)
  {}

  ~Stack();
  
  void push(const T &data);
  //...
private:
  T *_array;
  int _capacity;
  int _top;
};

// 类外定义成员函数必须要加模板参数列表
template <class T>
Stack<T>::~Stack()
{
  delete[](_array);
  _capacity = _top = 0;
}

类外定义成员函数, 必须要加上模板参数列表声明, 并且指定类域时必须加上模板参数列表.

同时, 类模板成员函数的声明与定义必须放在同一个文件中, 否则编译器会链接失败.

类模板的实例化

类模板实例化与函数模板实例化不同, 类模板都是显示实例化, 需要在类模板名字后面跟<>, 然后将实例化的类型放在<>中即可, 类模板名字不是真正的类, 而实例化的结果才是真正的类

Stack<int> s1;
Stack<double> s2;

普通类的类名即是类型, 而类模板的类名不是类型, 类名<数据类型> 才是整个类的类型.

Stack<int>Stack<double> 不是同一个类, 他们只是同一类模板显示实例化生成的不同类.

本章完.

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值