【C++初阶】模板初阶——为何C++能和C语言拉开差距?


◆ 博主名称:Severus-Yuan
◆ 来自泥潭,充满野心,强大冷静,从不后悔
◆ 欢迎光临我的思考天地,期待我们在这刹那的同频共振


Severus-Yuan的专栏 链接🔗
C语言 C语言
初阶数据结构与算法 初阶数据结构与算法
数据结构算法题 数据结构算法题
C++ C++

文章目录


(一)引入

在前面的学习中,我们知道C++支持函数重载,可就算是支持函数重载,在写一些很相似的函数时,同样不方便。

譬如实现不同类型参数类型和返回值类型的Add函数:

cpp 复制代码
//整型   整型  整型
int Add(int x,int y)
{
  return x+y;
}

//浮点型   整型    浮点型
double Add(int x, double y)
{
  return x+y;
}

//浮点型   浮点型   浮点型
double Add(double x,double y)
{
  return x+y;
}

从上述重载函数就能窥见:

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

我们不禁想:能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成我们想要的代码呢?

能。这就是我们今天谈论的主题:模板。

(二)模板

泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。

模板是泛型编程的基础。

模板分为类模板和函数模板,下面我们逐一讲解。

(1)函数模板

1.概念:

函数模板代表了一个函数家族,该函数模板与类型无关。在使用时被参数化,根据实参类型产生函数的特定类型版本。函数模板不是函数,调用也不是调用函数模板,而是调用根据函数模板生成的具体函数。

2.函数模板的格式

cpp 复制代码
//T1.T2....Tn都是会在函数内部用到的类型(内置/自定义都行)
template<typename/class T1, typename/class T2,......,typename/class Tn>
返回值类型 函数名(参数列表)
{
   函数体
}

这里的T可以只有一个也可以有多个,可以做函数的参数也可以不做函数的参数。当然模板参数的名称也可以不是T。

typename是用来定义模板参数关键字,也可以使用class。(切记:不能使用struct代替

class)

①单一类型的T
cpp 复制代码
#include<iostream>
using namespace std;

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

int main()
{
  int a = 10;
  int b = 20;
  double c = 10.2;
  double d = 20.4;
  //正常函数怎么调用,这儿就怎么调用
  cout<<Add(a,b)<<"/"<<Add(c,d)<<endl;
  return 0;
}

注意:这里的Add(a,b)、Add(c,d)虽然出自同一套模板,但是由于参数类型不同,这完全是两个不同的函数。

在单模板参数的情况下,如果传了不同类型的实参(编译器不知道T的类型到底是啥),就会报错。但并非没有解决办法,要么进行进行强制类型转换,要么进行显示实例化(后文函数模板实例化有讲)。

强制类型转换:在调用时的实参前写强制类型转换符直接强转就行。

cpp 复制代码
#include<iostream>
using namespace std;

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

int main()
{
  int a = 10;
  int b = 20;
  double c = 10.2;
  double d = 20.4;
  cout<<Add(a,(int)c)<<"/"<<Add((double)b,d)<<endl;
  return 0;
}
②多类型的T
cpp 复制代码
#include<iostream>
using namespace std;

template<class T1,class T2>
void Add(const T1& x, const T2& y)
{
	cout << x + y << endl;
}

int main()
{
	int a = 10;
	double b = 10.5;
	Add(a, b);
	return 0;
}

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

3.函数模板的实例化

通过函数模板生成对应函数的过程就是函数模板的实例化。

把函数模板中的类型参数替代成具体类型的过程叫做模板参数的实例化。模板参数实例化分为隐式/推导实例化和显示实例化。

①隐式/推导实例化

上面举的单类型和多类型中的例子都是隐式/推导实例化,即不明确指明模板参数的类型,而是让编译器根据实参的类型推演出模板参数的实际类型。

cpp 复制代码
#include<iostream>
using namespace std;

template<class T1,class T2>
void Add(const T1& x, const T2& y)
{
	cout << x + y << endl;
}

int main()
{
	int a = 10;
	double b = 10.5;
	Add(a, b);
	 //实参:a是整型;b是浮点型------>推出T1类型为int,T2类型为double
	return 0;
}

隐式/推导实例化调用函数的全过程:传递实参------>编译器根据实参类型推出模板参数的类型------>生成具体的函数------>调用。

②显式实例化

显式实例化:即在调用时,在函数名之后加一个<>,并在<>中指定模板参数的实际类型,这样编译器就不用根据实参的类型推导模板参数的类型了。

显式实例化可支持单类型模板参数,也支持多类型模板参数。多参数情况下,如果<>中写一个就只指定了第一个模板参数的类型,后面的参数类型还是得根据实参类型进行推导。<>中的参数类型和模板参数是一一对应的,不是和函数的参数列表一一对应的。

注意:

显式实例化目的是为了指定函数模板的参数类型,指定T的类型后,可能出现<>中指定的类型和实际实参的类型不匹配的情况。这时候编译器会试着让实参隐式类型转换成指定类型。转换成功就可以编译通过,转换失败就要编译报错了。显式实例化不是专门为了让实参进行类型转换的。

特别注意:

<>中的类型直接指定template后的T1......Tn的类型,所以写<>中的类型应该关注template后的T。虽然显式实例化的<>和函数调用放在一块,但不能看着实参写<>,不然很容易思维混乱。

关于这点,我们写个样例来强化一下记忆。(我一开始就想错了,不写个代码,以后回顾博客可能都看不懂过去的自己写了啥(晕))

cpp 复制代码
#include<iostream>
using namespace std;

template<class T>
void func()
{
	int a = 5;
	double b = 5.2;
	T sum = a + b;
	cout << sum << endl;
}

int main()
{
	func<int>();
	return 0;
}
cpp 复制代码
#include<iostream>
using namespace std;

template<class T1,class T2>
void func(int a,double b)
{
	T1 sum1 = a + b;
	T2 sum2 = a+b;
	cout << sum1 << endl;
	cout << sum2 << endl;
}

int main()
{
	func<double,int>(5.5,8);
	return 0;
}

提到隐式类型转换,正好谈谈我在学习这里时出的编译报错。

代码如下:

cpp 复制代码
#include<iostream>
using namespace std;

template<class T>
void Swap(T& x, T& y)
{
	T tmp = x;
	//注意:这里一定不能写成T& tmp = x;
	x = y;
	y = tmp;
}

int main()
{
	int a = 10;
	int b = 20;
	double c = 10.5;
	double d = 20.5;
	//推导、隐式初始化
	Swap(a, b);
	Swap<int>(c,a);
	Swap(c, (double)a);
	return 0;
}

这两处标红的代码有相似之处,比如都进行了类型转换,但是类型转换有什么特别的呢?

显式/隐式类型转换都有一个特点:发生类型转换时会先生成临时变量,再将临时变量拷贝给新变量。

而临时变量也有一个特点:临时变量具有常性。

那为什么具有常性的临时变量放在上述代码中会报错呢?

如果我们细心看,就能发现:函数模板的形参的类型是引用。对象的访问权限只能不变或者缩小,可这里,面对具有常性不能更改的临时变量却拿普通的T类型接收,当然会报错。简单来说就是临时对象要用常引用接收。

那是不是我们把形参类型改成const T&就可以了呢?非也。

Swap函数的功能是交换两个变量,那么实参的值会改变,那么就不能设置成const类型。所以在这,调用Swap函数时,不要传参时进行类型转换。

好的,我们继续回归对显示实例化的讨论。

可能你会觉得显式实例化有些没必要,但其实并不然,在有些情况下显示实例化是必不可少的,比如下面这种情况:编译器推导模板参数的类型是通过调用时的实参类型得来的,那如果模板参数并没有用在函数的形参上呢?这样的情况,我们想知道函数模板的类型就必须显式实例化了。

cpp 复制代码
#include<iostream>
using namespace std;

template<class T >
void func(int a,int b)
{
	int a = 10;
	double b = 10.5;
	T sum = a + b;
	cout << sum << endl;
}

int main()
{
	func<int>(5,5.5);
	//指定T的类型是int,显式实例化
	return 0;
}

(4)模板参数的匹配原则

  1. 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数;
cpp 复制代码
// 专门处理int的加法函数
 int Add(int left, int right)
{
   return left + right;
}

 // 通用加法函数
 template<class T>
 T Add(T left, T right)
{
   return left + right;
}

 void Test()
{
   Add(1, 2);    
   Add<int>(1, 2);  
}
  1. 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例(有现成外卖为啥还要再买一份一模一样的呢?)。但如果函数模板可以产生一个具有更好匹配的函数,那么在调用时,将选择模板实例化出的函数。
cpp 复制代码
// 专门处理int的加法函数
int Add(int left, int right)
{
   return left + right;
}

// 通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
   return left + right;
}

void Test()
{
   Add(1, 2);   
// 与非函数模板类型(int,int)完全匹配,不需要函数模板实例化
   Add(1, 2.0);  
//模板函数可以生成更加匹配的版本(int,double),编译器根据实参生成更加匹配的Add函数
}

(2)类模板

1.类模板的定义格式

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

2.类模板的实例化

类模板实例化与函数模板实例化不同,类模板实例化就像是类实例化对象那样,不过需要在类模板名字后跟<>,然后将实例化的类型放在<>中。

类模板只有显式实例化,不能推导。

cpp 复制代码
//类模板
//栈
template<class T>
class Stack
{
public:
	//构造函数
	Stack(size_t capacity = 4)
		:_arr(new T[capacity])
		, _capacity(capacity)
		, _size(0)
	{

	}

	//push函数
	void STPush(int x)
	{
		//先检查空间还够不够,需不需要扩容
		if (_capacity == _size)
		{
			T* tmp = new T[2 * _capacity];
			memset(tmp,_arr,sizeof(T)*_size);
			delete[] _arr;
			_arr = tmp;
			_capacity *= 2;
		}

		//Push
		_arr[_size++] = x;
	}

	//析构函数
	~Stack()
	{
		delete[] _arr;
		_size = _capacity = 0;
	}

private:
	T* _arr;
	size_t _capacity;
	size_t _size;
};

class A
{
private:
	int _a;
};

int main()
{
  //类模板实例化
	Stack<int> st1;
	Stack<double> st2;
	Stack<A> st3;
	//以上三个类是同一个类型吗?

    Stack<int>* st4 = new Stack<int>;
    delete st4;
	return 0;
}
答疑解惑:

①这里栈中存的类型不定时明明可以用typedef,我们为啥要用类模板呢?

函数模板可以同时实现不同函数模板参数类型的实例化(int、double、A),但是typedef每次只能实现一种类型,除非写更多不同名的类类型实现栈。

②实例化的三个类对象(栈)是同一个类型吗?

虽然st1、st2和st3都是由同一个类模板得来的,但它们不是同一个类型。模板参数的类型不同,得到的类型就不同。就像函数模板一样,虽然不同函数模板参数实例化出的函数十分相似,但这些函数终究不是同一个函数。

类模板名<实例化类型> 这个整体才是类型。

cpp 复制代码
// Stack是类名,Stack<int>才是类型
Stack<int> st1;   
Stack<double> st2; 

③Stack * st4 = new Stack;delete st4;的整个过程的内存空间是怎样的?

声明和定义分离:

模板支持在同一文件内的声明和定义在分离,但不建议将声明和定义分离到两个文件,.h 和.cpp会出现链接错误,具体原因后面会讲。

cpp 复制代码
#include<iostream>
using namespace std;

 //类模版
template<typename T>
class Stack
{
public:
  Stack(size_t capacity = 4)
  {
    _array = new T[capacity];
    _capacity = capacity;
    _size = 0;
  }

  //声明
  void Push(const T& data);
  
private:
  T* _array;
  size_t _capacity;
  size_t _size;
};

//模版不建议声明和定义分离到两个文件.h 和.cpp会出现链接错误,具体原因后面会讲
//需要单独再声明一次模板参数,不然编译器不知道T是从哪里来的
template<class T>
void Stack<T>::Push(const T& data)
{
    //扩容
    _array[_size] = data;
    ++_size;
}

//定义、声明时参数名不同无所谓,因为在真正调用Push函数时,T/X已经替换成了实际的类型
//template<class X>
//void Stack<X>::Push(const X& data)
//{
//	//扩容
//	_array[_size] = data;
//	++_size;
//}

int main()
{
   Stack<int> st1;   // int
   Stack<double> st2; // double
   return 0;
}

初学C++,如有错误,欢迎指出!

------end------

相关推荐
艾莉丝努力练剑3 小时前
【AI大模型接入SDK】LLM会话管理模块设计
网络·c++·人工智能·学习·大模型
小此方3 小时前
「C++AI大模型接入SDK」(一) API接入与本地两种方式对比、API Key获取、API报文详解与简单API的构建
开发语言·c++·人工智能
GG-_-Bond11 小时前
9.1 kv存储resp协议模拟面试
c++
米糕闯编程12 小时前
鱼香ros2(五)用C++编写节点
c++·ros2·cmakelists·鱼香
xiangyun6113 小时前
【408数据结构 08】队列:循环队列判空判满,408年年考,一次讲清
c语言·开发语言·数据结构·c++·算法
xiangyun6113 小时前
【408数据结构 06】双链表、循环链表、静态链表
c语言·开发语言·数据结构·c++·算法
fpcc15 小时前
计算机原理—构建过程中的分段分析
c++
xxwxx__15 小时前
C++ AVL 树深度剖析:平衡二叉搜索树原理、源码与面试考点
数据结构·c++
Escalating_xu15 小时前
【C语言常见概念】从第一行代码到字符串结束标志:一次打通编译、main、ASCII、转义字符与注释
c语言·c++