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++(十五):模板 -> 正文阅读

[C++知识库]C++(十五):模板

模板

C++的另一种编程思想是泛型编程,其主要利用的技术就是模板。模板树妖分为两类: 函数模板类模板

1.函数模板

所谓函数模板,就是一个返回值和形参类型不用具体指定的通用函数,是一个虚拟的类型。

其目的是提高代码的复用性,将类型参数化。

1.1 语法

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

其中,

  1. template——声明创建模板
  2. typename——表明其后面的符号是一种数据类型,也可以用class代替
  3. T——通用数据类型,任意规范的名称均可,一般为大写字母

1.2 范例

这里写一个简单的实例:

#include <iostream>
#include <string>

using namespace std;

template<typename T>//模板声明,表明后面紧跟着的T是一个通用数据类型,编译器不应该报错
void myswap(T &a, T &b)
{
	T temp = a;
	a = b;
	b = temp;

	cout << a << "\t" << b << endl;
}

int main()
{
	int a = 1, b = 2;
	myswap(a, b);

	double c = 1.3, d = 3.2;
	myswap(c, d);

	return 0;

}

1.3 自动类型推导与显式指定类型

在上面的例子中,我们定义完 a 和 b 后直接将其输入myswap() 的形参中去,事实上是编译器自动判断T的类型的,因此这种方式称为自动类型推导。

与自动类型推导相对的,是我们显式的去告诉编译器我们输入的实参的类型,其语法是:

//自动类型推导
myswap(a,b)
//显式指定类型
myswap<int>(a,b)

1.4 注意事项

  1. 自动类型推导,其数据类型必须一致;
  2. 模板必须确定T的数据类型才可以使用。

对于第一点,其实不完全对,或者说,仅在上面那种写法下是正确的。

template<typename T>
void myswap(T &a, T &b)
{
	/*******/
}

int main()
{
	int a = 10;
	char b = 'b';
	myswap(a,b);//这个时候编译器会报错,因为无法判断形参的类型
}

但是在下面的情况下是可以不一致的。

template<class T,class V>
void aaa(T &a, V &b)
{
	cout << a - b << endl;
}
int main()
{
	int a = 10;
	char b = 'a';
	aaa(a, b);//这样编译器就可以判断T是int类型,V是char类型了
	aaa<int,char>(a,b);//这样的写法也是被编译器认可的
}

对于第二点,它的含义是,在模板函数的实现中必须出现通用类型T。

template<class T>
void aaa()
{
	cout << "aaa()实现" << endl;
}
int main()
{
	aaa();//这个调用是会报错的,应为aaa的定义中没有出现T
	//这个所谓的出现不一定是在函数体内,只出现在形参列表也是可以的,反正得出现一次至少
}

1.5 与普通函数的区别

  1. 普通函数可以发生自动类型转换;
  2. 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
  3. 但是通过显式指定类型的方式,则可以发生隐式类型转换

另外说一句,在引用传递的时候不会发生隐式类型转换。

1.6 函数模板与普通函数的调用规则

  1. 在都能调用的情况下,优先普通函数
  2. 通过空模板参数列表强制调用函数模板
  3. 函数模板可以重载
  4. 如果函数模板可以产生更好的匹配,使用函数模板

在都能调用的情况下,优先普通函数

void myprint(int a)
{
	cout << "普通函数" << endl;
}

template<typename T>
void myprint(T a)
{
	cout << "模板函数" << endl;
}

/******主函数*******/
int a =10;
myprint(a);

在这种情况下两个都能调用,编译器选择优先调用普通函数。

通过空模板参数列表强制调用函数模板

/******主函数*******/
int a =10;
myprint<>(a);

这里利用空模板参数列表强制调用了模板函数。

函数模板可以重载

template<typename T>
void myprint(T a)
{
	cout << "模板函数" << endl;
}

template<typename T>
void myprint(T a,T b)
{
	cout << "重载的模板函数" << endl;
}

/******主函数*******/
int a =10;
myprint(a);
myprint(1,2);

在这里,第一个函数调用的是普通函数;第二个调用的是重载的模板函数。

如果函数模板可以产生更好的匹配,使用函数模板

/******主函数*******/
char a ='a';
myprint(a);

在这里,事实上依然是普通函数与模板函数都能调用,但是由于调用普通函数需要发生隐式类型转换,而模板函数不需要,只要自动推导T的类型即可,因此编译器选择调用函数模板。

1.7 局限性

虽然模板具有很高的泛用性,但是对于自定义数据类型而言可能略有不足。

class PPP
{
public:
	PPP(string name,string id)
	{
		this->m_id = id;
		this->m_name = name;
	}
	string m_name;
	string m_id;
};

template<typename T>
bool com(T &a, T &b)
{
	return (a == b);
}

int main()
{
	PPP p1("tom","4732");
	PPP p2("tom", "732");
	if (com(p1, p2))
	{
		cout << "p1 == p2" << endl;
	}
	else
	{
		cout << "p1 != p2" << endl;
	}
	return 0;
}

对于上面的程序而言,com函数并不能判断PPP类的大小关系,因而这段代码无法实现。为了解决这个问题,有两种办法。其一就是之前学习的运算符重载,在这里我们可以重载==运算符。

	bool PPP::operator==(const PPP &p)
	{
		return (this->m_id == p.m_id && this->m_name == p.m_name);
	}

除了以上重载的办法,我们还可以利用模板特殊化这个com函数。

template<>bool com(PPP &a, PPP &p)
{
	return (a.m_id == p.m_id && a.m_name == p.m_name);
}

这个看上去就像一个全局函数,而且与重载相比好像没什么简单的地方。但是也许以后会有用处,先放在这,如果以后有用到这个方法的话就进行补充。

2. 类模板

2.1 语法

在template关键字后跟上类的定义或声明即可。

template<class T>
//类定义或声明

2.2 范例

template<class TypeName, class Typeage>
class PPP
{
public:
	PPP(TypeName name, Typeage age)
	{
		this->m_age = age;
		this->m_name = name;
	}
public:
	TypeName m_name;
	Typeage m_age;
};

2.3 与函数模板的区别

  1. 类模板没有自动类型推导,只能显式指定类型;
  2. 类模板的模板参数列表可以有默认参数的。

类模板没有自动类型推导
在上面类模板的基础下,

PPP<string,int> p1("ton",31);//这是正确的
PPP p1("ton",31);//这是错误的,编译器不能自动推导类模板的通用参数类型

类模板的模板参数列表可以有默认参数

template<class TypeName, class Typeage = int>//可以指定默认的数据类型
class PPP
{
public:
	PPP(TypeName name, Typeage age)
	{
		this->m_age = age;
		this->m_name = name;
	}
public:
	TypeName m_name;
	Typeage m_age;
};

2.4 类模板中成员函数的创建

如果没有用类模板去实例化一个模板对象的话,那么类模板中的方法并不会被创建,换言之,只要对象没有实例化,即使类模板中有看似互相矛盾的两个方法,编译器也不会报错。

class ppp
{
public:
	void show1()
	{
		cout << "ppp show()" << endl;
	}
};

class qqq
{
public:
	void show2()
	{
		cout << "qqq show()" << endl;
	}
};

template<class T>
class myclass
{
public:
	T obj;
	void func1()
	{
		obj.show1();
	}
	void func2()
	{
		obj.show2();
	}
};

int main()
{
	myclass<ppp> myc;
	myc.func1();
	//myc.func2();
	return 0;
}

func1 与 func2 在普通类中不可能同时出现,因为这个时候不可能同时调用两个类方法。模板参数确定后,编译器才会开始创建方法,在上面的代码中,我们指定模板参数为 ppp ,故而 func2 是无法调用的,会被编译器报错,但是 func1 仍然是可以正常工作的。也就是说,即使是模板参数被指定了, 看上去非法的 func2 也不会报错,只是无法调用罢了。

2.5 类模板对象做函数参数

主要有三种办法:

  1. 指定模板参数列表的对象
  2. 模板参数列表的参数化
  3. 模板对象的参数化

指定模板参数列表的对象

template<class T,class V>
class ppp
{
public:
	T m_name;
	V m_age;
	ppp(T name, V age)
	{
		m_age = age;
		m_name = name;
	}
	void pppshow()
	{
		cout << m_age << "   " << m_name << endl;
	}
};
//在这里,show的形参指定了模板参数列表
void show(ppp<string, int> &p)
{
	p.pppshow();
}

int main()
{
	ppp<string, int> p1("zs",213);
	show(p1);
}

模板参数列表的参数化 以及 模板对象的参数化

template<class T, class V>
void show2(ppp<T, V> &p)
{
	p.pppshow();
}

template<class T>
void show3(T &p)
{
	p.pppshow();
}

上面两个分别把函数形参的模板参数列表和类模板本身参数化了,事实上是一个类模板搭配函数模板的写法。

在实际使用中,指定参数列表的方式,也就是第一种方式是最常用的,后面两种方式都太过麻烦了。

2.6 类模板的继承

类模板做父类,子类必须指定父类的参数列表,无论子类是直接继承,还是通过类模板的方式继承。

template<class T,class V>
class base
{
public:
	T m_name;
	V m_age;
};
//在这里是直接继承,base的参数列表必须明确指定
class son : public base<string, int>
{
public:

	son();
};

//倘若想要灵活指定父类的数据类型,可以把子类写成类模板
//总而言之,我们必须把父类的参数写清楚了,才能知道要给类分配多少内存
template<class T1, class T2>
class son2 :public base<T1, T2>
{};

2.7 类模板方法的类外实现与分文件编写

首先是方法的类外实现。

template<class T>
class ppp
{
public:
	ppp();
	void showppp();
	T m_member;
};

template<class T>
ppp<T>::ppp()
{}

template<class T>
void ppp<T>::showppp()
{}

与普通类不同的是,在作用域的地方必须申明这是一个类模板,因此需要加上模板参数列表;又由于通用类型参数编译器不认识,所以前面要加上template关键词。

有了类外实现之后就可以尝试去分文件编写类模板了。但是由于之前提到过,类模板的方法并不是在一开始就创建好的,而是在调用的时候才创建。因此用常规的 .h 和 .cpp 这样的形式进行份文件的话,编译器无法将其链接到一起,会报错。

为了解决这个问题,主流的办法是把类模板的申明和方法的实现写在同一个文件内,后缀改为 .hpp,使用的时候就是:

#include "****.hpp"

2.8 类模板的友元

考虑全局函数作为友元,它有两种实现方式。其一是直接在类内实现全局友元函数。

template<class T>
class ppp
{
	friend void aaa(ppp<T> p)
	{
		cout << p.m_member << endl;
	}
		ppp(T name)
	{
		m_member = name;
	}
private:
	T m_member;
};

类内实现是比较简单的,无需考虑太多东西。但是类外实现就完全不一样了。

错误示范1

template<class T>
class ppp
{
	friend void aaa(ppp<T> p)public:
		ppp(T name)
	{
		m_member = name;
	}
private:
	T m_member;
};
//ppp<T>编译器不认识,因为不在模板内
void aaa(ppp<T> p)
{
	cout << p.m_member << endl;
}

错误示范2

//即使补上template关键字也不对
//在这种情况下aaa相当于一个函数模板了,并非普通的全局函数
//性质发生了变化,导致编译器无法正确的将这个函数模板与友元链接上
template<class T>
void aaa(ppp<T> p)
{
	cout << p.m_member << endl;
}

之前在函数模板与普通函数的调用规则那提到过,优先调用普通函数,在这里由于aaa是函数模板而不能和类模板处的普通友元函数成功链接。为了解决这个问题,我们可以强制友元函数调用函数模板。

//在友元处用空模板参数列表强制调用即可
friend void aaa<>(ppp<T> p)

但是这样仍然不可以解决我们的问题。

错误示范3

template<class T>
void aaa(ppp<T> p)
{
	cout << p.m_member << endl;
}

template<class T>
class ppp
{
	friend void aaa<>(ppp<T> p);
public:
	T m_member;
	ppp(T name)
	{
		m_member = name;
	}
};

由于aaa变成了函数模板,所以它必须在友元之前就被声明或者实现。但是由于形参为ppp类,编译器自上而下无法检索到ppp类的存在,依然会报错。

正确写法

template<class T>
class ppp;
template<class T>
void aaa(ppp<T> p)
{
	cout << p.m_member << endl;
}

template<class T>
class ppp
{
	friend void aaa<>(ppp<T> p);
public:
	T m_member;
	ppp(T name)
	{
		m_member = name;
	}
};

3. 类模板案例

自定义一个类模板,具有以下功能:

  1. 可以对内置数据类型以及自定义数据类型进行存储;
  2. 数组中的数据存储到堆区;
  3. 构造函数中传入数组的数量;
  4. 修改对应的拷贝构造函数以及重载=运算符;
  5. 提供尾插法和尾删法对数据增删;
  6. 通过下标的方式访问数组元素;
  7. 可以获取当前数组元素个数和容量。

现在分析:

  1. 对于要求二,自然是要new一个数据在堆区,这个数据应该用一个指针接收。
  2. 由于存储的是一组数据,自然需要new出一个数组。
  3. 那么这个类模板的成员包括T类型的指针,int类型的元素个和容量。

以下是类模板的结构:

template<class T>
class myvector
{
public:
	//构造函数
	myvector(unsigned int capacity);

	//拷贝构造函数
	myvector(const myvector<T> &myvec);

	//尾插
	void mypush_back(const T &t);

	//尾删
	void mypull_back();

	//begin函数
	T *begin();

	//end函数
	T *end();

	//可以通过下标访问数组元素
	T &operator[](unsigned int j);

	//存在默认重载的赋值运算符,需要修改
	myvector<T> &operator=(const myvector<T> &myvec);

	//返回数组的容量
	unsigned int getcapacity();

	//返回数组的元素数量
	unsigned int getsize();

	//析构函数
	~myvector();

private:
	//当前元素个数
	unsigned int m_size;

	//模板的容量
	unsigned int m_capacity;

	//明确这个类的数据类型  应该使用指针进行管理
	T *m_type;
};

3.1 构造函数

//有参构造  此处定义容器的容量
template<class T>
myvector<T>::myvector(unsigned int capacity)
{
	this->m_capacity = capacity;
	this->m_size = 0;
	this->m_type = new T[capacity];
}

3.2 拷贝构造函数

这里要使用 深拷贝 防止浅拷贝带来的析构重复删除指针的情况。

//拷贝构造函数 深拷贝
template<class T>
myvector<T>::myvector(const myvector<T> &myvec)
{
	this->m_capacity = myvec.m_capacity;
	this->m_size = myvec.m_size;
	m_type = new T[m_capacity];
	for (int i = 0; i != this->m_size; ++i)
	{
		this->m_type[i] = myvec.m_type[i];
	}
}

3.3 operator+函数重载

由于类自动提供一套重载的operator+函数,而且使用的是浅拷贝,因此我们有必要重写一下。

//存在默认重载的赋值运算符,需要修改
template<class T>
myvector<T> &myvector<T>::operator=(const myvector<T> &myvec)
{
	if (!this->m_type == NULL)
	{
		delete[] this->m_type;
		this->m_type = NULL;
	}
	this->m_capacity = myvec.m_capacity;
	this->m_size = myvec.m_size;
	this->m_type = new T[this->m_capacity];
	for (int i = 0; i != this->m_size; ++i)
	{
		this->m_type[i] = myvec.m_type[i];
	}
	return *this;
}

3.4 析构函数

这里清空指针。

//析构函数
template<class T>
myvector<T>::~myvector()
{
	if (!this->m_type == NULL)
	{
		delete[] this->m_type;
		this->m_type = NULL;
	}
}

3.5 通过下标访问数组元素

为了能够利用[]像数组一样访问数据,有必要重写一下operator[]函数。

//可以通过下标访问数组元素
template<class T>
T &myvector<T>::operator[](unsigned int j)
{
		return this->m_type[j];//非法的下标暂不考虑
}

3.6 尾插和尾删

尾插:当指针非空且元素数量小于当前容量的情况下我们在容器的后面插入一个对象。否则就直接退出程序。

//尾插
template<class T>
void myvector<T>::mypush_back(const T &t)
{
	if (this->m_type != NULL && this->m_size < this->m_capacity)
	{
		this->m_type[this->m_address] = t;
		++this->m_size;
	}
	else
	{
		return;
	}
}

尾删:做了一个逻辑删除,让用户访问不到这个对象。

//尾删
template<class T>
void myvector<T>::mypull_back()
{
	if (this->m_address == 0) return;
	--this->m_address;
}

3.7 begin和end函数

突然想用范围for循环遍历容器的元素,就简单写了这两个函数。

//begin函数
template<class T>
T *myvector<T>::begin()
{
	return this->m_type;
}

//end函数
template<class T>
T *myvector<T>::end()
{
	return (this->m_type + this->m_size);
}

4. 出现的一些小问题

  1. 不能将“this”指针从“const myvector”转换为“myvector &”
    这个问题是由于在一开始写拷贝构造函数的时候,调用了getcapacity()函数,而且拷贝构造函数的形参写的是const myvector &myvec,而getcapacity()的类型是个普通的成员函数。这就导致了 常量对象调用了非常量的成员函数 。要想解决这个问题,要么 不在拷贝构造函数中使用getcapacity()函数 ,要么 把拷贝构造函数的形参的const 前缀去除 ,要么 给getcapacity()函数价格后缀const
//返回数组的容量
	unsigned int getcapacity() const;
//这样的函数是常量成员函数,可以被常量对象调用
  1. “Person”: 没有合适的默认构造函数可用
    我自定义了一个Person类希望用myvector存储。
class Person
{
public:
	string m_name;
	string m_id;
	Person(string name, string id)
	{
		this->m_name = name;
		this->m_id = id;
	}
};

myvector<Person> arr(100);

但是这样的语句会报错,这是由于Person类中有有参构造函数,于是编译器不会给他设置一个默认构造函数了。但是myvector语句是要调用Person的无参构造函数的,因此而报错。
解决办法就是写一个空实现的无参构造函数即可。

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章           查看所有文章
加:2021-11-20 18:12:42  更:2021-11-20 18:15:20 
 
开发: 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/24 6:49:38-

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