【C++要笑着学】泛型编程 | 函数模板 | 函数模板实例化 | 类模板(二)

简介: 本章将正式开始介绍C++中的模板,为了能让大家更好地体会到用模板多是件美事!我们将会举例说明,大家可以试着把自己带入到文章中,跟着思路去阅读和思考,真的会很有意思!如果你对网络流行梗有了解,读起来将会更有意思!

Ⅲ.  函数模板实例化


0x00  引入:这些不同类型的Swap函数是怎么来的

int a = 0, b = 1;
Swap(a, b);

编译器在调用 Swap(a, b) 的时候,发现 a b 是整型的,编译器就开始找,


虽然没有找到整型对应的 Swap,但是这里有一份模板 ——


template<typename T>   // 大家好我是模板,飘过~
void Swap(T& rx, T& ry) {
  T tmp = rx;
  rx = ry;
  ry = tmp;
}

这里要的是整型,编译器就通过这个模板,推出一个 T 是 int 类型的函数。


这时编译器就把这个模板里的 T 都替换成 int,生成出一份 T 是 int 的函数。

69cd6a55c2c8425c3751513124484144_10936a3e3c3f48eebb29c96752d7ba2a.png



char e = 'e', f = 'f';
Swap(e, f);

一样的,如果要调用 Swap(e, f) ,e f 是字符型,编译器就会去实例化出一个 char 的。

b7bf26d6fdcbfa6b49488f6632580453_cc143c3704484e56aa5f8a884695baca.png


你调的函数还是那些函数,只是你写一份模板出来,让编译器去用模板生成那些函数。


前面注意事项那里我们说过,函数模板本身不是函数。


它是是编译器使用方式产生特定具体类型函数的模具,在编译器编译阶段,


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


比如:当用 double 类型使用函数模板时,编译器通过对实参类型的推演,


将 T 确定为 double 类型,然后产生一份专门处理 double 类型的代码,对于字符类型也是如此。


0x01  转到反汇编观察

🐞 我们刚才调试的时候在监视窗口已经看到了,它们的值成功交换了。


现在我们再调试一次,这次转到反汇编,去验证一下编译器通过模板生成函数这件事:

e02901d1d8f14ddb6ec88d16916941cf_9f8c339c33434553a07cce1dc6680d27.png10e7aad6b62d3695acc1de1eefb532b0_1148dcd283214fc29d5a5e1432661fde.png


0x01  模板实例化的定义

模板将我们本来应该要重复做的活,交给了编译器去做。


编译器不是人,它不会累,让编译器拿着模板实例化就完事了。


用手搓衣服舒服,还是用洗衣机洗舒服?


自己手写舒服,还是编译器自己去生成舒服?


📚 用不同类型的参数使用模板参数时,成为函数模板的实例化。


模板参数实例化分为:隐式实例化 和 显式实例化 ,下面我们来分别讲解一下这两种实例化。


0x02  模板的隐式实例化

📚 定义:让编译器根据实参,推演模板函数的实际类型。


我们刚才讲的 Swap 其实都是隐式实例化,就是让编译器自己去推。


💬 现在我们再举一个 Add 函数模板做参考:


#include <iostream>
using namespace std;
template<class T>
T Add(const T& x, const T& y) {
  return x + y;
}
int main(void)
{
  int a1 = 10, a2 = 20;
  double d1 = 10.1, d2 = 20.2;
  cout << Add(a1, a2) << endl;
  cout << Add(d1, d2) << endl;
  return 0;
}

19e1bd29ba5989379099926d49b68c64_925d825fa28e4b61b8ce1a930f6bfe4d.png


❓ 现在思考一个问题,如果出现 a1 + d2 这种情况呢?实例化能成功吗?


Add(a1, d2);

9ff3d4dab074d03cadb9704a31947b4f_b36508a9b48f497596261ebffd176251.png


这必然是失败的, 因为会出现冲突。一个要把它实例化成 int ,一个要把它实例成 double,

f534379e08ff8a981c56d2876655c1bb_131769f358ae4743981bcbe8cf8b2c2d.png

💡  解决方式


① 传参之前先进行强制类型转换,非常霸道的解决方式:


template<class T>
T Add(const T& x, const T& y) {
  return x + y;
}
int main(void)
{
  int a1 = 10, a2 = 20;
  double d1 = 10.1, d2 = 20.2;
  cout << Add(a1, a2) << endl;
  cout << Add(d1, d2) << endl;
  cout << Add((double)a1, d2) << endl;
  return 0;
}

cd81a2c041a5cd5332c8676ce8675ad3_02779ae4601441998dd9c9b50c65156c.png


② 写两个参数,那么返回的参数类型就会起决定性作用:


#include <iostream>
using namespace std;
template<class T1, class T2>
T1 Add(const T1& x, const T2& y) {   // 那么T1就是int,T2就是double
  return x + y;      // 范围小的会像范围大的提升,int会像double "妥协"
} // 最后表达式会是一个double,但是最后返回值又是T1,是int,又会转
int main(void)
{
  int a1 = 10, a2 = 20;
  double d1 = 10.1, d2 = 20.2;
  cout << Add(a1, d2) << endl;   // int,double  👆
  return 0;
}

b68505b1359151747c7e5c8fbcc080c3_a21766803b254721aed70a97ee39dc7d.png


当然,这种问题严格意义上来说是不会用多个参数来解决的,


这里只是想从语法上演示一下,我们还有更好地解决方式,我们继续往下看。


③ 我们还可以使用 "显式实例化" 来解决:


Add<int>(a1, d2);     // 指定实例化成int
Add<double>(a1, d2)   // 指定实例化成double

我们下面先来详细介绍一下显式实例化,然后再回来看看它是如何解决的。


0x03  模板的显式实例化

📚 定义:在函数名后的 < > 里指定模板参数的实际类型。


简单来说,显式实例化就是在中间加一个尖括号 < >  去指定你要实例化的类型。


(在函数名和参数列表中间加尖括号)

函数名 <类型> (参数列表);

💬 代码:解决刚才的问题

template<class T>
T Add(const T& x, const T& y) {
  return x + y;
}
int main(void)
{
  int a1 = 10, a2 = 20;
  double d1 = 10.1, d2 = 20.2;
  cout << Add(a1, a2) << endl;
  cout << Add(d1, d2) << endl;
  cout << Add<int>(a1, d2) << endl;     // 指定T用int类型
  cout << Add<double>(a1, d2) << endl;  // 指定T用double类型
  return 0;
}

🚩 运行结果:

6ff5924377d3b692d23d0e1ec823f989_d3661361003c492eb85cf92f754f7d15.png


🔑  解读:


像第一个 Add(a1, a2)  ,a2 是 double,它就要转换成 int 。


第二个 Add(a1, a2),a1 是 int,它就要转换成 double。


这种地方就是类型不匹配的情况,编译器会尝试进行隐式类型转换。


像 double 和 int 这种相近的类型,是完全可以通过隐式类型转换的。


如果无法成功转换,编译器将会报错。


🔺 总结:


函数模板你可以让它自己去推,但是推的时候不能自相矛盾。


你也可以选择去显式实例化,去指定具体的类型。


0x04  模板参数的匹配原则

我们还是用刚才的 Add 函数模板来举例,现在我需要对整型的 a1 和 a2 进行加法操作:


template<class T>
T Add(const T& x, const T& y) {
  return x + y;
}
int main(void)
{
  int a1 = 10, a2 = 20; 
  cout << Add(a1, a2) << endl;
  return 0;
}

我们是通过这个 Add 函数模板,生成 int 类型的加法函数的。


💬 如果我们有一个现成的、专门用来处理 int 类型加法的函数:


// 专门处理int的加法函数
int Add(int x, int y) {
  return x + y;
}
// 通用加法函数
template<class T>
T Add(const T& x, const T& y) {
  return x + y;
}
int main(void)
{
  int a1 = 10, a2 = 20; 
  cout << Add(a1, a2) << endl;
  return 0;
}


❓  思考:如果你是编译器,当 Add(a1, a2) 时你会选择用哪一个?


是用函数模板印一个 int 类型的 Add 函数,还是用这现成的 Add 函数呢?


我们继续往下看……


📚 匹配原则:


① 一个非模板函数可以和一个同名的模板函数同时存在,


而且该函数模板还可以被实例化为这个非模板函数:


// 专门处理int的加法函数
int Add(int x, int y) {
  cout << "我是专门处理int的Add函数: ";
  return x + y;
}
// 通用加法函数
template<class T>
T Add(const T& x, const T& y) {
  cout << "我是模板参数生成的: ";
  return x + y;
}
int main(void)
{
  int a1 = 10, a2 = 20; 
  cout << Add(a1, a2) << endl;       // 默认用现成的,专门处理int的Add函数
  cout << Add<int>(a1, a2) << endl;  // 指定让编译器用模板,印一个int类型的Add函数
  return 0;
}

890a914dfa866f65b85d925e8a2a8321_8d1aa8d4b6b94377bf956fc8c0de4a86.png


② 对于非模板函数和同名函数模板,如果其他条件都相同,


在调用时会优先调用非模板函数,而不会从该模板生成一个实例。


如果模板可以产生一个具有更好匹配的函数,那么将选择模板。


// 专门处理int的加法函数

int Add(int x, int y) {

cout << "我是专门处理int的Add函数: ";

return x + y;

}

// 通用加法函数

template

T1 Add(const T1& x, const T2& y) {

cout << "我是模板参数生成的: ";

return x + y;

}

int main(void)

{

cout << Add(1, 2) << endl;     // 用现成的

//(与非函数模板类型完全匹配,不需要函数模板实例化)

cout << Add(1, 2.0) << endl;   // 可以,但不是很合适,自己印更好

//(模板参数可以生成更加匹配的版本,编译器根据实参生产更加匹配的Add函数)

return 0;

}

// 专门处理int的加法函数
int Add(int x, int y) {
  cout << "我是专门处理int的Add函数: ";
  return x + y;
}
// 通用加法函数
template<class T1, class T2>
T1 Add(const T1& x, const T2& y) {
  cout << "我是模板参数生成的: ";
  return x + y;
}
int main(void)
{
  cout << Add(1, 2) << endl;     // 用现成的
  //(与非函数模板类型完全匹配,不需要函数模板实例化)
  cout << Add(1, 2.0) << endl;   // 可以,但不是很合适,自己印更好
  //(模板参数可以生成更加匹配的版本,编译器根据实参生产更加匹配的Add函数)
  return 0;
}

28bd6a7992c328adc0b0e4fbd16438f5_5b5823a1ba244009bb5cecd813c54f5a.png


Ⅳ.  类模板


0x00  引入:和本篇开头本质上是一样的问题

💬 就比如 Stack,如果我们定它是 int,那么它就是存整型的栈:


class Stack {
public:
  Stack(int capacity = 4) 
  : _top(0) 
  , _capacity(capacity) {
  _arr = new int[capacity];
  }
  ~Stack() {
  delete[] _arr;
  _arr = nullptr;
  _capacity = _top = 0;
  }
private:
  int* _arr;
  int _top;
  int _capacity;
};

❓ 如果我想改成存 double 类型的栈呢?


当时我们在讲解数据结构的时候,是用 typedef 来解决的。


typedef int STDataType;
class Stack {
public:
  Stack(STDataType capacity = 4) 
  : _top(0) 
  , _capacity(capacity) {
  _arr = new int[capacity];
  }
  ~Stack() {
  delete[] _arr;
  _arr = nullptr;
  _capacity = _top = 0;
  }
private:
  STDataType* _arr;
  int _top;
  int _capacity;
};


如果需要改变栈的数据类型,直接改 typedef 那里就可以了。


这依然是治标不治本,虽然看起来就像是支持泛型一样,


它最大的问题是不能同时存储两个类型,你就算是改也没法解决:


int main(void)
{
  Stack st1;   // 存int数据
  Stack st2;   // 存double数据
  return 0;
}

你只能做两个栈,如果需要更多的数据类型……


那就麻烦了,你需要不停地CV做出各种数据类型版本的栈:


class StackInt {...};
class StackDouble {...};
……

这和文章开头提到的问题(Swap)本质上是一个问题,就是不支持泛型。


它们类里面的代码几乎是完全一样的,只是类型的不同。


函数我们可以使用模板,类也是可以的,我们下面就来讲解一下类模板。


0x01  类模板的定义格式

📚 定义:和函数模板的定义方式是一样的,template 后面跟的是尖括号 < > :


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

💬 代码:解决刚才的问题


template<class T>
class Stack {
public:
  Stack(T capacity = 4) 
  : _top(0) 
  , _capacity(capacity) {
  _arr = new T[capacity];
  }
  ~Stack() {
  delete[] _arr;
  _arr = nullptr;
  _capacity = _top = 0;
  }
private:
  T* _arr;
  int _top;
  int _capacity;
};
int main(void)
{
  Stack st1;   // 存储int
  Stack st2;   // 存储double
  return 0;
}

 但是我们发现,类模板他好像不支持自动推出类型,


它不像函数模板,不指定它也可以根据传入的实参去推出对应的类型的函数以供调用。

381f399c3bbfaedbf983a977072a8377_20fd0ea3bb5c4f2da9f6e871ac5c1afd.png


函数模板之所以能推,是因为有实参传形参这么一个 "契机" ,让编译器能帮你推。


你定义一个类,它能推吗?没这个能力你知道吧!

7aef081e7bc624b3aa755affb80159d1_d43aab19a6ee41c9a7ad4dc4b75f99d1.png


所以这里只支持显示实例化,我们继续往下看。


0x02  类模板实例化

基于上面的原因,我们想要对类模板实例化,我们可以使用显示实例化。


类模板实例化在类模板名字后跟 < >,然后将实例化的类型放在 < > 中即可。


类名 <类型> 变量名;

💬 代码演示:解决刚才的问题


template<class T>
class Stack {
public:
  Stack(T capacity = 4) 
  : _top(0) 
  , _capacity(capacity) {
  _arr = new T[capacity];
  }
  ~Stack() {
  delete[] _arr;
  _arr = nullptr;
  _capacity = _top = 0;
  }
private:
  T* _arr;
  int _top;
  int _capacity;
};
int main(void)
{
  Stack<int> st1;      // 指定存储int
  Stack<double> st2;   // 指定存储double
  return 0;
}


📌 注意事项:


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


template<class T>
class Stack {...};

类模板名字不是真正的类,而实例化的结果才是真正的类。


② Stack 是类名,Stack 才是类型:


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

0x03  类外定义类模板参数

❓ 思考问题:下面的 Push 为什么会报错?


template<class T>
class Stack {
public:
  Stack(T capacity = 4) 
  : _top(0) 
  , _capacity(capacity) {
  _arr = new T[capacity];
  }
  // 这里我们让析构函数放在类外定义
  void Push(const T& x);
  ~Stack();
private:
  T* _arr;
  int _top;
  int _capacity;
};
/* 类外 */
void Stack::Push(const T& x) {   ❌
    ...
}


🔑 解答:


① Stack 是类名,Stack 才是类型。这里要拿 Stack 去指定类域才对。


② 类模板中的函数在类外定义,没加 "模板参数列表" ,编译器不认识这个 T 。类模板中函数放在类外进行定义时,需要加模板参数列表。


这段代码第一个问题是没有拿 Stack 去指定类域,


最大问题其实是编译器压根就不认识这个T!


即使你用拿类型 Stack 指定类域,编译器也一样认不出来:


我们拿析构函数 ~Stack 来演示一下:


 
         

0599cc44f60d08da727f19049936b673_88b7766c04874baeb5469d0813350bb2.png


💬 代码演示:我们现在来看一下如何添加模板参数列表!


template<class T>
class Stack {
public:
  Stack(T capacity = 4) 
  : _top(0) 
  , _capacity(capacity) {
  _arr = new T[capacity];
  }
  // 这里我们让析构函数放在类外定义
  ~Stack();
private:
  T* _arr;
  int _top;
  int _capacity;
};
// 类模板中函数放在类外进行定义时,需要加模板参数列表
template <class T>
Stack<T>::~Stack() {   // Stack是类名,不是类型! Stack<T> 才是类型,
  delete[] _arr;
  _arr = nullptr;
  _capacity = _top = 0;
}

这样编译器就能认识了。

本章完!

相关文章
|
2月前
|
存储 算法 C++
C++ STL 初探:打开标准模板库的大门
C++ STL 初探:打开标准模板库的大门
112 10
|
25天前
|
安全 编译器 C++
【C++11】可变模板参数详解
本文详细介绍了C++11引入的可变模板参数,这是一种允许模板接受任意数量和类型参数的强大工具。文章从基本概念入手,讲解了可变模板参数的语法、参数包的展开方法,以及如何结合递归调用、折叠表达式等技术实现高效编程。通过具体示例,如打印任意数量参数、类型安全的`printf`替代方案等,展示了其在实际开发中的应用。最后,文章讨论了性能优化策略和常见问题,帮助读者更好地理解和使用这一高级C++特性。
41 4
|
25天前
|
算法 编译器 C++
【C++】模板详细讲解(含反向迭代器)
C++模板是泛型编程的核心,允许编写与类型无关的代码,提高代码复用性和灵活性。模板分为函数模板和类模板,支持隐式和显式实例化,以及特化(全特化和偏特化)。C++标准库广泛使用模板,如容器、迭代器、算法和函数对象等,以支持高效、灵活的编程。反向迭代器通过对正向迭代器的封装,实现了逆序遍历的功能。
34 3
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
86 4
|
28天前
|
编译器 C++
【c++】模板详解(1)
本文介绍了C++中的模板概念,包括函数模板和类模板,强调了模板作为泛型编程基础的重要性。函数模板允许创建类型无关的函数,类模板则能根据不同的类型生成不同的类。文章通过具体示例详细解释了模板的定义、实例化及匹配原则,帮助读者理解模板机制,为学习STL打下基础。
31 0
|
2月前
|
编译器 程序员 C++
【C++打怪之路Lv7】-- 模板初阶
【C++打怪之路Lv7】-- 模板初阶
18 1
|
2月前
|
编译器 C语言 C++
C++入门6——模板(泛型编程、函数模板、类模板)
C++入门6——模板(泛型编程、函数模板、类模板)
59 0
C++入门6——模板(泛型编程、函数模板、类模板)
|
2月前
|
算法 编译器 C++
【C++篇】领略模板编程的进阶之美:参数巧思与编译的智慧
【C++篇】领略模板编程的进阶之美:参数巧思与编译的智慧
87 2
|
23天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
36 2
|
29天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
82 5