【C++】泛型编程之模板初阶


在这里插入图片描述


1. 泛型编程

泛型编程让你编写完全一般化并可重复使用的算法,其效率与针对某特定数据类型而设计的算法相同。

比如:让你实现一个通用的交换函数,在没有泛型编程的时候,我们大概率只能实现下面的代码

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++中,也能够存在这样一个模具,通过给这个模具中填充不同头像(类型),来获得不同结果的照片
(即生成具体类型的代码)**,那将会节省许多头发。巧的是前人早已将树栽好,我们只需在此乘凉。
**泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础


2. 模板

2.1 模板的概念

模板就是建立通用的模具,大大提高复用性

例如生活中的模板

PPT模板

模板的特点:

  • 模板不可以直接使用,它只是一个框架
  • 模板的通用并不是万能的

C++提供两种模板机制:函数模板类模板

image-20221106105518343


3. 函数模板

3.1 函数模板概念

函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定
类型版本

3.2 函数模板语法

函数模板作用:

建立一个通用函数,其函数返回值类型和形参类型可以不具体制定,用一个虚拟的类型来代表。

语法:

template<typename T>
函数声明或定义

解释:

template — 声明创建模板

typename — 表面其后面的符号是一种数据类型,可以用class代替

T — 通用的数据类型,名称可以替换,通常为大写字母

例如用模板写交换函数:

template<typename T>
void Swap(T& left, T& right)
{
    
    
	T temp = left;
	left = right;
	right = temp;
}

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


3.3 函数模板的原理

大家都知道,瓦特改良蒸汽机,人类开始了工业革命,解放了生产力。机器生产淘汰掉了很多手工产品。

本质是什么,重复的工作交给了机器去完成。有人给出了论调:懒人创造世界。

image-20221106110520529

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

image-20221106111258645

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

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


3.4 函数模板的实例化

前面说了这么多模板的概念和理论,那在C++中如何使用模板编程呢?

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

  1. 隐式实例化:让编译器根据实参推演模板参数的实际类型
  2. 显式实例化:在函数名后的<>中指定模板参数的实际类型

示例

//利用模板提供通用的交换函数
template<typename T>
void Swap(T& left, T& right)
{
    
    
	T temp = left;
	left = right;
	right = temp;
}
int main() 
{
    
    
	int a = 10;
	int b = 20;

	//Swap(a, b);
	//利用模板实现交换
    
	//1、自动类型推导
	Swap(a, b);

	//2、显示指定类型
	Swap<int>(a, b);

	cout << "a = " << a << endl;
	cout << "b = " << b << endl;

	return 0;
}

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

总结:

  • 函数模板利用关键字 template
  • 使用函数模板有两种方式:自动类型推导、显示指定类型
  • 模板的目的是为了提高复用性,将类型参数化

注意事项:

  • 自动类型推导,必须推导出一致的数据类型T,才可以使用

  • 模板必须要确定出T的数据类型,才可以使用

示例

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

使用模板时必须确定出通用数据类型T,并且能够推导出一致的类型


3.5 模板参数的匹配原则

  1. 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函

  2. 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模
    板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板

  3. 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换

示例1

// 专门处理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); // 调用编译器特化的Add版本
}

示例2

// 专门处理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); // 与非函数模板类型完全匹配,不需要函数模板实例化
	Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
}

3.6 普通函数与函数模板的区别

普通函数与函数模板区别:

  • 普通函数调用时可以发生自动类型转换(隐式类型转换)
  • 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
  • 如果利用显示指定类型的方式,可以发生隐式类型转换

4. 类模板

4.1 类模板语法

类模板作用:

  • 建立一个通用类,类中的成员 数据类型可以不具体制定,用一个虚拟的类型来代表。

语法:

template<typename T>
class 类模板名
{
    
    
// 类内成员定义
};

解释:

template — 声明创建模板

typename — 表面其后面的符号是一种数据类型,可以用class代替

T — 通用的数据类型,名称可以替换,通常为大写字母

示例

// 动态顺序表
// 注意:Vector不是具体的类,是编译器根据被实例化的类型生成具体类的模具
template<class T>
class Vector
{
    
    
public:
	Vector(size_t capacity = 10)
		: _pData(new T[capacity])
		, _size(0)
		, _capacity(capacity)
	{
    
    }
	// 使用析构函数演示:在类中声明,在类外定义。
	~Vector();
	void PushBack(const T& data)void PopBack()// ...
private:
	T* _pData;
	size_t _size;
	size_t _capacity;
};

总结:类模板和函数模板语法相似,在声明模板template后面加类,此类称为类模板

4.2 类模板的实例化

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

// Vector类名,Vector<int>才是类型
Vector<int> s1;
Vector<double> s2;

4.3 类模板与函数模板区别

类模板与函数模板区别主要有两点:

  1. 类模板没有自动类型推导的使用方式
  2. 类模板在模板参数列表中可以有默认参数

4.4 类模板分文件编写

学习目标:

  • 掌握类模板成员函数分文件编写产生的问题以及解决方式

问题:

  • 类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到

解决:

  • 解决方式1:直接包含.cpp源文件
  • 解决方式2:将声明和实现写到同一个文件中,并更改后缀名为.hpp,hpp是约定的名称,并不是强制

示例:

类模板分文件编写.cpp中代码

#include<iostream>
using namespace std;

//#include "person.h"
#include "person.cpp" //解决方式1,包含cpp源文件

//解决方式2,将声明和实现写到一起,文件后缀名改为.hpp
#include "person.hpp"
void test01()
{
    
    
	Person<string, int> p("Tom", 10);
	p.showPerson();
}

int main() {
    
    

	test01();

	system("pause");

	return 0;
}

person.hpp中代码:

#pragma once
#include <iostream>
using namespace std;
#include <string>

template<class T1, class T2>
class Person {
public:
	Person(T1 name, T2 age);
	void showPerson();
public:
	T1 m_Name;
	T2 m_Age;
};

//构造函数 类外实现
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age) {
	this->m_Name = name;
	this->m_Age = age;
}

//成员函数 类外实现
template<class T1, class T2>
void Person<T1, T2>::showPerson() {
	cout << "姓名: " << this->m_Name << " 年龄:" << this->m_Age << endl;
}

总结:主流的解决方式是第二种,将类模板成员函数写到一起,并将后缀名改为.hpp

猜你喜欢

转载自blog.csdn.net/dongming8886/article/details/127714630