IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> 【C++】详谈模版 -> 正文阅读

[数据结构与算法]【C++】详谈模版


泛型编程与模板

实现一个简单的Swap交换函数

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

很明显上面的Swap函数只能交换int类型的数据,而实际中要交换的数据类型会有许多。不过C++支持函数重载,我们可以将所有的交换数据类型的Swap函数全重载出来:

// 交换char类型数据的Swap函数
void Swap (char& left, char& right)
{...}
// 交换double类型数据的Swap函数
void Swap (double& left, double& right)
{...}
// 交换...类型数据的Swap函数

如果要实现一个通用类型的Swap函数,采用函数重载就非常不可取了:

  • 重载的函数仅仅只是类型不同,代码的复用率低
  • 实际中除了要交换C++的内置类型数据,还可能交换用户自定义类型的数据,如果事先不知道这个类型是什么,如何写出该类型的交换函数?

我们常用的C++各种库函数,比如STL库(标准模板库),里面涉及的都是通用函数,这些通用函数的实现就是利用了泛型编程

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

泛型 是指具有在多种数据类型上皆可操作的含义,与模板有些相似

STL包含了许多常用的算法和数据结构,但其内部实现均将算法与数据结构完全分离,其中算法是泛型的,不与任何特定数据结构或对象类型系在一起

==模版 != 泛型编程,模版可以做到与类型无关,不一定能做到代码通用;

但模版是泛型编程的基础,是实现泛型编程非常重要的一个环境;
模板的本质:类型参数化

为什么说模版 != 泛型编程?
举例:若要实现一个通用的查找方法find()函数

// 我们自然想到了用模版来实现:
template<typename T>
int find(const T& array[], size_t size, const T& data)
{
	for (size_t i = 0; i < size; ++i)
	{
		if (array[i] == data)
		{
			return i;
		}
	}
	return -1;
}

但这个find函数模版真的是一个泛型方法吗?

如果我们要查找的数据不是在连续的空间(如数组),而是在链表、堆或二叉树中,那么这个方法根本无法实现功能;

因此这个find只是个函数模版,而不是泛型编程代码;C++的STL(标准模版库)大都是泛型编程,用到了许多特殊的方法来实现泛型编程(迭代器…),其中数据结构就是用模版来进行封装的;


模版有哪些

1. 函数模版

概念

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

定义

  • 定义模板参数列表

    定义模板关键字template
    定义模版参数关键字typname,也可用class(早期的编译器只能用class)
    (注意这里不能用struct替换class)

    template<typename T1, typename T2,......,typename Tn>
    返回值类型 函数名(参数列表){}
    (T就是一个通用类型,可替换名字)

  • 定义函数模板

    函数定义时将需要替换的类型直接用通用类型即可。

例子:

  1. 一个Swap函数模版的例子:(模版类型只有一个)
template<class T>  //函数模版的模版参数列表,告诉编辑器T是一个类型
void Swap (T& left, T& right)  //函数模版的实现
{...}

// 注意,上述代码只是一个函数模版(模具),并不是一个真正的函数。
  1. 一个多参数模版
template<class T1, class T2, typename T3>  // class与typename等价
void Fun(T1 a, T2 b, T3 c)  //这三个参数类型可以不一样
{
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
} 
  • 函数模版的注意事项:
    模版定义在主函数外;
    模版参数无特殊情况最好用引用类型(效率高);
    函数模版并不是真正的函数,只是编译器的一种编译规则

实例化——函数模版的使用

揭示函数模版的原理:

  • 函数模版并不是真正的函数,只是编译器的一种编译规则;
  • 函数模版未实例化前,编译器不会生成具体类型的函数;
  • 只有当该模版使用时被,才会实例化出对应类型的函数。
template<class T>  //模版类型只有一个
T Add(T a, T b)  //实现一个加法函数模版
{
	return a + b;
}
int main()
{
	// 函数模版的三种隐式实例化
	cout << Add(1, 2) << endl;
	cout << Add(1.1, 2.2) << endl;
	cout << Add('a', 'b') << endl;

	return 0;
}

通过反汇编代码查看模版的实例化情况:
在这里插入图片描述

函数模版实例化的分类:

  1. 隐式实例化——直接调用模版

    当用户直接调用,编译器会根据实例化的结果来推演参数的类型,根据推演的结果生成具体处理该参数类型的实例化函数。
    (上面的例子已经体现了函数模版隐式实例化的具体做法)

    但是隐式实例化无法处理某些特殊情况:
    在这里插入图片描述
    解决方法:用户强转类型 或 采用显示实例化模版
    在这里插入图片描述

  2. 显示实例化 ——函数名<类型>
    编译器不需要对实参类型进行推演,直接按照<>内类型与模版自动生成相应函数。
    在这里插入图片描述

几点规则

【思考】上面实现了一个Add()函数模版,可以实现任意类型的数据相加,但是当类型为char字符时,调用该模版返回的值并不是我想要的
在这里插入图片描述

  • 一个非模板函数可以和一个同名函数模板同时存在,且编译器优先调用已存在的非模版函数而不是模版,但该函数模板仍可以被显示实例化后调用
    在这里插入图片描述
    在这里插入图片描述

  • 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
    在这里插入图片描述

  • 模版与同名函数同时存在,但使用时如果函数需类型转换,编译器会对比模版与函数,优先选择最匹配的方式
    在这里插入图片描述

2. 类模版

概念

通过替换类中的类型为通用类型,让类变成模版类,这时类名就是模版类名。
类模板不同于函数模板的地方在于,编译器不能为类模板推断参数类型

定义

  • 定义模版参数列表
    同函数模版一样,使用关键字templatetypename(或class)

  • 定义类模版
    定义类,并将类中元素类型替换为通用类型;

    • 类模板的成员函数:
      对于类来说,其内部有成员函数成员变量,他们定义时涉及的类型均可使用通用类型,但需特别注意成员函数:

      成员函数在类中定义:直接替换类型为通用类型即可;
      对于类模版来说,一般成员函数最好直接定义在类中;

      成员函数在类外定义:需在函数定义上方添加新的模版参数列表,其实相当于定义了一个函数模版(类模版中的成员函数模版)

    • 类模板和友元:【?】

      如果一个类模板包含一个非模板友元,则该友元可以访问该模板实例的所有成员

      如果友元也是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例。

例:实现一个简单的自定义顺序表模版

// 动态顺序表类模版
template <typename T>
class SeqList
{
private:
	// 检测顺序表空间是否扩容
	void CheckCapacity()
	{
		if (_size < _capacity)
		{
			return;
		}
		// 开辟新空间(左移一位变两倍)
		T* temp = new T[_capacity << 1];
		// 拷贝原空间
		memcpy(temp, _arr, _size*sizeof(T));
		// 释放旧空间
		delete[] _arr;
		// 更新对象成员
		_arr = temp;
		_capacity *= 2;
	}
public:
	// 无参构造函数
	SeqList()
		:_arr(new T[3])
		, _size(0)
		, _capacity(3)
	{}
	// 带参构造函数
	SeqList(T* arr, size_t size)
		:_arr(new T[size])
		, _size(size)
		, _capacity(size)
	{
		for (size_t i = 0; i < size; ++i)
		{
			_arr[i] = arr[i];
		}
	}
	// 拷贝构造函数
	SeqList(const SeqList& s)
		: _arr(new T[s._size])
		, _size(s._size)
		, _capacity(s._size)
	{
		for (size_t i = 0; i < s._size; ++i)
		{
			_arr[i] = s._arr[i];
		}
	}
	// 打印顺序表
	void Print()
	{
		// 判断是否有元素
		if (_size == 0)
		{
			cout << "Print Error!" << endl;
			return;
		}
		for (int i = 0; i < _size; i++)
		{
			cout << _arr[i] << " ";
		}
		cout << endl;
	}
	// 尾插--类内仅作声明,类外定义成员函数
	void PushBack(const T& data);
	// 尾删
	void PopBack()
	{
		// 判断是否有元素
		if (_size == 0)
		{
			cout << "PopBack Error!" << endl;
			return;
		}
		// 更新对象成员
		_size--;
	}
	// 测试类外定义成员函数模版
	template<typename U>  //函数模版参数
	void Func(U a);  //类内函数声明


private:
	// 定义一个动态顺序表
	T* _arr;          // 动态数组
	size_t _size;     // 有效元素的个数
	size_t _capacity; // 表示空间总的大小
};

// 类模板成员函数类外定义,需要加上类的模板参数列表
template<typename T>
void SeqList<T>::PushBack(const T& data)
{
	// 检测是否需扩容
	CheckCapacity();
	// 尾部直接添加
	_array[_size] = data;
	// 更新对象成员
	_size++;
}

// 类外定义成员函数,就相当与定义了一个函数模版
// 成员函数的模版参数可以与类模版不同
// 测试:(该函数无实际意义)
template<typename T>   //类模版的模版参数列表
template<typename U>   //函数模版的模版参数列表
					   //注意这两个模版不能写到一起			
void SeqList<T>::Func(U a)
{
	cout << a << endl;
}

实例化

类模版只有显示实例化一种方法
例:实例上面的顺序表类模版

int main()
{
	// 定义一个类对象
	// SeqList<int>相当于一个类名
	SeqList<int> s1;
	// 后面方法与正常类使用无异
	s1.PushBack(1);
	s1.PushBack(2);
	s1.PopBack();

	// 使用成员函数模版
	s1.Func<double>(1.1);

	return 0;
}

在这里插入图片描述

注意事项

  • 类模板名是一个模版名,并不是类型,不能用来实例化对象。要实例化对象需要用类模板名<具体类型>(相当于一个类)来进行实例化

    元素可以为内置类型,比如:
    SeqList<int>是一个元素为int的顺序表类、
    SeqList<double>是一个元素为double的顺序表类、

    元素也可以是自定义类型SeqList<Data>是一个元素为Data对象的顺序表类;

  • 类外定义成员函数或成员函数模版,记得添加类模版和函数模版的参数列表

  • 类模板是一个类家族,模板类是通过类模板实例化的具体类


非类型模版参数

模版的实质就是类型参数化,对于函数模版类模版的定义,第一步都是定义模版参数列表

模板参数种类

上述的参数列表可以有两种模版参数

  1. 类型形参:跟在classtypename后的参数类型名称,可以是内置类型自定义类型
  2. 非类型形参(非类型模版参数):用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用
    例:一个函数模版采用非类型参数
    在这里插入图片描述

注意事项

  1. 类模版和函数模版均可采用非类型模版参数;

  2. 非类型模版参数可以设为缺省参数;

  3. 非类型模版参数本质是常量,函数内不可当成左值修改内容
    在这里插入图片描述

  4. 浮点数、类对象以及字符串是不允许作为非类型模板参数;
    在这里插入图片描述

  5. 非类型的模板参数必须在编译期就能确认结果;
    在这里插入图片描述
    在这里插入图片描述


模版的特化

为什么要特化?

这是一个很简单的Max()函数模版:

// 实现一个返回两值中的最大值
template <typename T>
T Max(T left, T right)
{
	return left > right ? left : right;
}

使用该模版的实例化:
在这里插入图片描述

通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的使用模版的实例化代码无法达到目的,出现结果错误。

为了解决这种问题,C++提供了模版特化这样的机制,函数模版和类模版都可以特化

模版特化种类

1. 函数模版特化(不推荐使用)

特化步骤:
在这里插入图片描述
例:上述Max()函数模版对于字符串类型的特化

// 实现一个返回两值中的最大值
template <typename T>
T Max(T left, T right)
{
	return left > right ? left : right;
}
// Max()模版的特化
template<>  
char* Max<char*>(char* left, char* right)  
//注意模版中所有的通用类型都必须用<>内的数据类型替换
{
	return strcmp(left, right) > 0 ? left : right;
}

在这里插入图片描述
为什么不推荐使用函数模版特化?

对于刚才的Max()函数模版,我们不希望函数内部修改参数,为了代码的安全与高效,将其修改为:

// 实现一个返回两值中的最大值
template <typename T>
const T& Max(const T& left, const T& right)  //&引用类型更高效、const更安全
{
	return left > right ? left : right;
}

修改了函数模版,其特化理所当然也要改变,但是并不容易正确使用
在这里插入图片描述
而前面我们在函数模版的匹配规则说过,C++支持与函数模版同名的函数存在,使用模版同名函数也能处理上述问题,而且简单不易出错。因此我们推荐使用模版同名函数,而不是为函数模版提供特化
在这里插入图片描述

实际例子展示:注意那个特殊的特化函数模版的例子

#include<iostream>
using namespace std;

// 本体函数模版
template<typename Type>
const Type Max(const Type &a, const Type &b)
{
	cout << "This is Max<Type>" << endl;
	return a > b ? a : b;
}

// 函数模版的特化
// 特化1
template<>
const int Max<int>(const int &a, const int &b)
{
	cout << "This is Max<int>" << endl;
	return a > b ? a : b;
}

// 特化2
template<>
const char Max<char>(const char &a, const char &b)
{
	cout << "This is Max<char>" << endl;
	return a > b ? a : b;
}

// 特化3
// 一个特殊易错的例子:中间必须是const char*&
template<>
const char*& Max<const char*&>(const char* &a, const char* &b)
{
	cout << "This is Max<char>" << endl;
	return a > b ? a : b;
}

// 同名函数
int Max(const int &a, const int &b)
{
	cout << "This is Max" << endl;
	return a > b ? a : b;
}

int main()

{
	Max(10, 20);        //优先调用同名函数
	Max(12.34, 23.45);  //调用本体模版
	Max('A', 'B');      //调用特化模版2
	Max<int>(20, 30);   //调用特化模版1  
	return 0;
}

2. 类模版特化

特化步骤:大体与函数特化步骤相同

类模版特化分为
在这里插入图片描述

全特化:将模板参数列表中所有的参数都确定化

// 类模版特化
// 测试类模版
template<class T1, class T2>
class Test
{
public:
	Data() 
	{ 
		cout << "调用Test类模版" << endl; 
	}
private:
	T1 _d1;
	T2 _d2;
};

// 全特化
template<>
class Test<int, char>
{
public:
	Test()
	{
		cout << "调用类模版特化" << endl;
	}
private:
	int _d1;
	char _d2;
};

在这里插入图片描述

偏特化(两种):

  • 部分特化:将模板参数类表中的一部分参数特化
// 偏特化
// 形式1:部分特化
template<class T1>
class Test<T1, char>
{
public:
	Test()
	{
		cout << "调用偏特化" << endl;
	}
private:
	int _d1;
	char _d2;
};

在这里插入图片描述

  • 对参数限制:偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本
// 形式2:参数限制
template<class T1, class T2>
class Test<T1*, T2*>
{
public:
	Test()
	{
		cout << "偏特化:参数限制" << endl;
	}
private:
	T1* _d1;
	T2* _d2;
};

在这里插入图片描述

了解一下

类模版特化用途之一:类型萃取(仅作了解)
类型萃取了解
C++之类型萃取


模版优缺点

优点缺点
模版复用代码,节省资源,方便代码迭代开发模版会导致代码膨胀,也会导致编译时间变长
增强了代码的灵活性出现模版编译错误时,错误信息提示不准确,不易定位错误

模版还有一个很重要的概念:分离编译这个概念对于自定义实现模版真的非常重要,详细内容可参考下一篇博客:【C++】模版的分离编译与多文件编程

  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2022-05-05 11:44:27  更:2022-05-05 11:45:35 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 5:24:44-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码