【C++初阶】八、初识模板(泛型编程、函数模板、类模板)

=========================================================================

相关代码gitee自取

C语言学习日记: 加油努力 (gitee.com)

 =========================================================================

接上期

【C++初阶】七、内存管理
(C/C++内存分布、C++内存管理方式、operator new / delete 函数、定位new表达式)
-CSDN博客

 =========================================================================

                     

目录

             

一 . 泛型编程


二 . 函数模板

函数模板的概念

函数模板的格式

函数模板的原理

函数模板的实例化

隐式实例化:

显式实例化:

模板参数的匹配原则


三 . 类模板

类模板的定义格式

类模板的实例化

图示 --  以栈类为例:


本篇博客相关代码

Test.cpp文件 -- C++文件

         

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

             

一 . 泛型编程

                 

  • 我们以前写的函数一般都是针对某种类型实现两值交换Swap函数

    如果交换的两值int类型那就要将Swap函数参数设置为int类型

    如果交换的两值double类型那就要将Swap函数参数设置为double类型……
    通过函数重载实现
                      

  • 对于函数虽然函数重载可以实现函数参数多类型的问题
    但也有一些不好的地方
    1、重载的函数仅仅是类型不同而已具体实现实现逻辑都是很类似
    当接收的函数参数类型不同就需要用户自己增加对应的重载函数
    2、代码可维护性比较其中一个重载函数出错可能所有的重载函数都会出错
                           

  • 那能不能实现一个通用的Swap函数实现泛型编程
    泛型编程 -- 编写与类型无关的通用代码代码复用的一种手段
    C++中为解决这个问题有了模板的概念模板泛型编程的基础
                      
  • 有了模板,相当于告诉编译器一个模子
    编译器能够根据不同的类型利用该模子生成对应类型的代码

    模板分为函数模板类模板
图示:

         

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

             

二 . 函数模板

函数模板的概念

                   

函数模板代表了一个函数家族该函数模板与类型无关

在使用时被参数化根据实参类型产生函数的特定类型版本

                     

                     


                    

函数模板的格式

              

  • 注意
    typename是用来定义模板参数的关键字上面的T1T2Tn就是模板参数),
    除了可以使用typename来定义还可以使用class来定义

    //函数模板格式:
    template<typename T1, typename T2, ……, typename Tn>
    函数返回值类型 函数名(参数列表)
    {
        // 函数体
    }
图示:

                     

                     


                    

函数模板的原理

            

  • 函数模板是一个蓝图它本身并不是函数
    编译器使用后能产生特定具体类型函数摸具
    所以模板就是将本来应该由我们完成的重复的事情交给了编译器完成
                   
  • 编译器编译阶段对于函数模板的使用
    编译器需要根据传入的实参类型推演生成对应类型的函数以供调用
    比如
    当使用double类型调用函数模板编译器通过对实参类型推演
    模板参数T确定为double类型然后产生一份专门处理double类型的代码
图示:

                     

                     


                    

函数模板的实例化

                   

不同类型的参数调用模板称为函数模板的实例化
模板参数实例化分为隐式示例化显式实例化
               

                   

隐式实例化:

                       

  • 编译器根据实参推演模板参数的实际类型
    上面的图示中的模板参数实例化都是隐式实例化
图示:

                   

  • 隐式实例化
    如果只设置了一个模板参数实参中却有多种类型这时将不能通过编译
    此时有两种处理方式
    1、用户自己来强制转化2、使用显式实例化
图示:

                     

                       

---------------------------------------------------------------------------------------------

                  

显式实例化:

               

  • 不通过模板参数推演识别出实参的类型而是自己显式设置模板参数的类型
    函数名后的<>指定模板参数的实际类型即可
                   
  • 显式实例化如果类型不匹配编译器会尝试进行隐式类型转换
    如果无法转换成功编译器将会报错
                       
  • 显式实例化真正用法
    设置了一个模板函数参数中并没有设置模板参数
    函数体中却使用了模板参数类型或者返回值模板参数类型
    这种情况就需要显式实例化确定实参类型
图示:

                     

                     


                    

模板参数的匹配原则

                     

  • 一个非模板函数可以和一个同名的函数模板同时存在
    而且该函数模板还可以被实例化为这个非模板函数
图示:

                

  • 对于非模板函数同名函数模板如果其它条件都相同
    在调动时会优先调用非模板函数不会从该模板产生出一个示例
    如果模板可以产生一个具有更好匹配的函数那么将选择模板
图示:

                   

  • 模板函数不允许自动类型转换普通函数可以进行自动类型转换

         

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

             

三 . 类模板

类模板的定义格式

                 

  • 类模板定义函数模板定义类似定义时将函数的位置换成类即可
    模板参数类型类中定义成员类型时进行使用

    //类模板定义格式:
    template<class T1, class T2, ……, class Tn>
    class 类模板名
    {    
        // 类内成员定义    
    }

                     


                    

类模板的实例化

                

  • 类模板实例化函数模板实例化不同类模板实例化需要在类模板名字后跟<>
    然后将实例化的类型放在<>即可
                        
  • 类模板名字不是真正的类名显式实例化后的结果才是真正的类名
              
  • 同一个类模板显式实例化出的不同类这些类的类型不一样
    栈类模板为例
    Stack<int> st1 Stack<double> st2
    st1 的类型是 Stack<int> ,是用于存储int类型数据
    st2 的类型是 Stack<double> ,是用于存储double类型数据
    st1 st2 类型不一样
图示 --  以栈类为例:

            

  • 注意
    类模板成员函数的声明和实现分离不能分离到两个文件中
    分离时通常都写在一个.h文件
    而且分离后的成员函数实现部分需要设置对应的函数模板
    分离后不指定成员函数的类域而是指定其类模板类型
图示:

         

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

             

本篇博客相关代码

Test.cpp文件 -- C++文件:

#define _CRT_SECURE_NO_WARNINGS 1

//包含IO流:
#include <iostream>;
//完全展开std命名空间:
using namespace std;


//Swap函数 -- 交换两个int类型数据:
void Swap(int& left, int& right)
{
	int temp = left;
	left = right;
	right = temp;
}

//Swap函数 -- 交换两个double类型数据:
void Swap(double& left, double& right)
{
	double temp = left;
	left = right;
	right = temp;
}

//Swap函数 -- 交换两个char类型数据:
void Swap(char& left, char& right)
{
	char temp = left;
	left = right;
	right = temp;
}

/*
* 这里实现了三个Swap函数,
* 分别交换了三种不同的类型,
* 但实现的逻辑都是相同的,就只有交换类型不同,
* 所以就造成了某种程度的“冗余”
* 
* 上面的函数都需要针对具体的类型,
* 那能不能让一个代码能够针对广泛的类型呢,
* C++中就有了泛型编程:
*/

//函数模板:
template<typename T>
//tyename 也可以写成 class
//template<class T> 
//Swap函数 -- 交换两类型数据(泛型编程):
void Swap(T& left, T& right)
{
	char temp = left;
	left = right;
	right = temp;
}
/*
* 使用函数模板即可实现泛型编程,
* 让函数能够针对广泛的类型,
* 而不只能针对一种类型,
* 
* 通过关键字template即可定义一个模板,
* Swap函数的参数设置为模板参数
*/


//主函数:
int main()
{
	int a = 0; //int类型变量
	int b = 1; //int类型变量

	double c = 1.1; //double类型变量
	double d = 2.2; //double类型变量

	//调用设置了模板参数的Swap函数:
	Swap(a, b); //int类型 -- 模板参数T
	Swap(c, d); //double类型 -- 模板参数T

	/*
	* 这里调用的两个Swap函数实际不是同一个,
	* 两个Swap函数的函数地址不同,
	* 不同类型调用的Swap函数不同是由模板参数导致的
	* 
	*			模板的原理:
	* 模板参数接受参数如果是int类型,
	* 需要调用到int类型的函数,
	* T 模板参数就会推演成 int类型,(模板参数推演)
	* 然后就会实例化出具体的函数:
	* T 是int类型的对应函数。(模板实例化)
	* 
	* 如果接收的是double类型数据,
	* T 模板参数就会推演成 double类型,(模板参数推演)
	* 然后就会示例化出具体的函数:
	* T 是double类型的对应函数。(模板实例化)
	*/

	return 0;
}


//如果一个函数需要接收不同类型的参数:
template<class T1, class T2>
/*
* 如果需要接收不同类型的参数,
* 直接在模板中设置多个模板参数即可,
*(模板参数名可以随便取,但一般会取为T -- type)
* 
* 模板参数 和 函数参数 类似,
* 但是 函数参数 定义的是 形参对象,
* 而 模板参数 定义的则是 类型
*/
void func(const T1& t1, const T2& t2)
//模板参数T1接收一种类型,T2接收另一种类型
{
	cout << t1 << endl;
	cout << t2 << endl;
	/*
	* 设置了模板参数的函数,
	* 如果要进行输入或输出,
	* 就必须使用 cin/cout 进行 输入/输出 了,
	* 因为设置了模板参数,
	* 不知道实际传进来的数据是什么数据,
	* 因为使用 scanf/printf 必须要指定数据类型,
	* 所以这里使用 scanf/printf 来 输入/输出
	*/
}


//通用(泛型)加法函数:
template<class T>
T Add(T left, T right)
//接收 T 模板参数类型
{
	return left + right;
	//返回值也是 T 模板类型
}


template<class T>
T* f()
{
	//开辟T类型的动态空间:
	T* p = new T[10];
	//没设置模板参数T,却使用了T

	//返回T类型指针:
	return p;

	/*
	* 该函数没有设置模板参数T,
	* (设置的参数不是模板参数)
	* 但返回值却返回模板指针类型(T*),
	* 
	* 没设置模板参数就无法进行类型推演
	*/
}


//主函数:
int main()
{
	/*
	*			推演实例化:
	* 函数参数传递,推演出模板参数的类型,
	* 再生成(实例化)对应的函数
	*/

	//隐式实例化:
	
	//T1推演为int,T2推演为int:
	func(1, 2);

	//T1推演为double,T2推演为double:
	func(1.1, 2.2);

	//T1推演为double,T2推演为int:
	func(1.1, 2);


	//调用通用(泛型)加法函数:
	cout << Add(1, 2.2) << endl;
	/*
	* 该语句不能通过编译,因为在编译期间,
	* 当编译器看到该实例化时,需要推演其实参类型
	* 通过实参a1将T推演为int,通过实参d1将T推演为double类型,
	* 但模板参数列表中只有一个T,
	* 编译器无法确定此处到底该将T确定为int 或者 double类型而报错
	* 
	* 注意:在模板中,编译器一般不会进行类型转换操作,
	* 因为一旦转化出问题,编译器就需要背黑锅
	* 
	* 此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化
	*/	
	cout << Add(1, (int)2.2) << endl;



	//显式实例化:
	
	// 2.2 隐式转换为int类型:
	cout << Add<int>(1, 2.2) << endl; 

	// 1 隐式转换为double类型:
	cout << Add<double>(1, 2.2) << endl;

	/*
	* 直接显式实例化,指定将参数实例化为某种类型,
	* 而不通过模板参数的类型推演
	*/

	//函数没有设置模板参数:
	double* p = f<double>();
	/*
	* 函数参数没设置模板参数,
	* 但却使用了模板参数,
	* 编译器没法进行类型推演,
	* 所以此时就需要显式实例化来确定类型
	* (显式实例化的真正用法)
	*/

	return 0;
}



//使用栈解决多类型问题:
typedef int STDataType;
/*
* 想让栈存储int类型数据,
* 就在这里设置类型为int,
* 想让栈存储double类型数据,
* 就在这里设置类型为double,
* ……
*/

类模板:
//template<class T>
//
类模板 -- 栈类:
//class Stack
//{
//public: //公有成员函数:
//
//	//构造函数:
//	Stack(int capacity = 4)
//	{
//		//调用了构造函数则打印:
//		cout << "Stack(int capacity = 4)" << endl;
//
//		//使用new开辟栈容量大小的空间:
//		
//		// typedef 设置多类型:
//		//_a = new STDataType[capacity];
//
//		// 类模板 设置多类型:
//		_a = new T[capacity]; //使用模板T类型
//
//		_top = 0; //栈顶值默认为0
//		_capacity = capacity; //设置栈容量
//	}
//
//	//析构函数:
//	~Stack()
//	{
//		//调用了析构函数则打印:
//		cout << "~Stack()" << endl;
//
//		//使用delete释放new开辟的空间:
//		delete[] _a;
//
//		_a = nullptr; //置为空指针
//		_top = 0; //栈顶值置为0
//		_capacity = 0; //栈容量置为0
//	}
//
//private: //私有成员变量:
//
//	T* _a; //栈指针 -- 使用模板T类型
//	int _top; //栈顶值
//	int _capacity; //栈容量
//
//};


//类模板:
template<class T>

//类模板 -- 栈类:
class Stack
{
public: //公有成员函数:

	//构造函数 -- 类模板成员函数声明和定义分离:
	Stack(int capacity = 4);


	//析构函数:
	~Stack()
	{
		//调用了析构函数则打印:
		cout << "~Stack()" << endl;

		//使用delete释放new开辟的空间:
		delete[] _a;

		_a = nullptr; //置为空指针
		_top = 0; //栈顶值置为0
		_capacity = 0; //栈容量置为0
	}

private: //私有成员变量:

	T* _a; //栈指针 -- 使用模板T类型
	int _top; //栈顶值
	int _capacity; //栈容量 

};

//类模板成员函数的声明和实现分离:
template<class T>
Stack<T>::Stack(int capacity)
/*
* 实现时指定的不是Stack成员函数的类名(类域),
* 而是Stack成员函数的类型,
* (不是Stack::,而是Stack<T>)
* 需要把模板参数写出来
* 
*				注:
* 类模板不允许声明和定义分离到两个文件,
* 分离时都写在一个.h文件中
*/
{
	//调用了构造函数则打印:
	cout << "Stack(int capacity = 4)" << endl;

	//使用new开辟栈容量大小的空间:

	// typedef 设置多类型:
	//_a = new STDataType[capacity];

	// 类模板 设置多类型:
	_a = new T[capacity]; //使用模板T类型

	_top = 0; //栈顶值默认为0
	_capacity = capacity; //设置栈容量
}


//主函数:
int main()
{
	/*
	* typedef 可以解决多类型问题,
	* 那 typedef 可以代替 模板 吗?
	* 
	* 答案是不能,typedef设置一个类的类型后,
	* 该类的类型就只能是typedef设置的那一种了,
	* 
	* 如果用类模板的话设置一个类的话,
	* 该类的一个对象就可以是int类型,
	* 而该类的另一个对象还可以是double类型
	*/

	//显式实例化:
	//让这个栈类型对象存储int类型:
	Stack<int> st1; //int

	//让这个栈类型对象存储double类型:
	Stack<double> st2; //double

	/*
	* 函数模板可以显式实例化,也可以让它自己推演,
	* 函数模板大多数情况让它自己推演出类型,
	* 不进行显式实例化
	* 
	* 类模板能让一个类的对象是不同类型的,
	* (如:都是栈类对象,但一个栈对象存储int类型数据,
	* 另一个栈对象存储double类型数据)
	* 只需要在创建对象时显式实例化需要的类型即可。
	* 这时我们实现的数据结构,
	* 就跟具体的存储类型是无关的,想要哪种类型,
	* 在创建对象时就显式实例化哪种类型
	*/

	return 0;
}



// 专门处理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版本:
	Add<int>(1, 2); 
	//函数模板Add被实例化为非模板函数Add
}


//专门处理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(int, int):
	Add(1, 2);
	/*
	* 与非函数模板类型完全匹配,
	* 不需要函数模板实例化
	*/

	//Add(int, double):
	Add(1, 2.0);
	/*
	* 模板函数可以生成更加匹配的版本,
	* 编译器根据实参生成更加匹配的Add函数
	*/
}

猜你喜欢

转载自blog.csdn.net/weixin_63176266/article/details/134895933