目录
C++基础入门-CSDN博客https://blog.csdn.net/lh11223326/article/details/136971273?spm=1001.2014.3001.5501
一.泛型编程
在学习模版之前先来看如下几个交换函数。
void Swap(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
//交换 float 变量的值
void Swap(float* a, float* b) {
float temp = *a;
*a = *b;
*b = temp;
}
//交换 char 变量的值
void Swap(char* a, char* b) {
char temp = *a;
*a = *b;
*b = temp;
}
//交换 bool 变量的值
void Swap(bool* a, bool* b) {
char temp = *a;
*a = *b;
*b = temp;
}
使用函数重载虽然可以实现,但是有以下几个不好的地方:
- 重载的函数仅仅是类型不同而已,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数。
- 代码的可维护性比较低,一个出错可能所有的重载均出错。
如果在C++中,也能存在这样一个模具它能够填充不同材料(类型),来获得不同材料的铸件(即生成具体类型的代码),那将会节省许多头发。如下正好有一物可以解决:
泛型编程:编写与类型无关,是代码复用的一种手段,模版是泛型编程的基础。函数模版,类模版。
二.函数模版
1.概念:
我们知道,数据的值可以通过函数参数传递,在函数定义时数据的值是未知的,只有等到函数调用时接收到了实参才能确定其值,这就是值的参数化。在C++中,数据的类型也可以通过参数传递来传递,在函数定义时可以不指明具体的数据类型,当发生函数调用时,编译器可以根据传入的实参自动推断数据类型,这就是类型的参数化。
值(Value)和类型(Type)是数据的两个主要特征,他们在C++中都可以被参数化。
所谓函数模版,实际上是建立在一个通用函数,她所用到的数据的类型(包括返回值类型,形参类型,局部变量类型)可以不具体指定,而是用一个虚拟的类型代替(实际上使用一个标识符来占位),等发生函数调用时再根据传入的实参来逆推出真正的类型,这个通用函数就称为函数模版(Function Template).
在函数模版中,数据的值类型都被参数化了,发生函数调用时编译器会传入的实参来推演参数和类型,函数模版除了支持值的参数化,还支持类型的参数化。
一旦一定函数模版,就可以将类型参数用于函数定义和函数声明了,原来使用int,float,char等内置类型的地方,都可以用类型参数来代替。
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定 类型版本。
2.函数模版格式:
声明函数模版:
template<typename T>T max(T a,T b,T c);
template<typename T1,typename T2,.....typename Tn>
返回值类型 函数名(参数列表){}
template<typename T>
void Swap( T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
注意:typename是用来定义模版参数关键字,也可以使用class,但是不能使用struct代替class。
3.函数模版的原理
函数模版是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具,所以其实模版就是将本来应该我们做的重复的事情交给了编译器。
如下调用下面代码:
int main(){
double d1=2.0;
double d2=5.0;
Swap(d1,d2);
int i1=10;
int i2=20;
Swap(i1,i2);
char a='0';
char b='9';
Swap(a,b);
return 0;
}
经过:
template<typename T>
void Swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
然后变为:
void Swap(double &a, double &b) {
double temp = a;
a = b;
b = temp;
}
void Swap(int &a, int &b) {
int temp = a;
a = b;
b = temp;
}
void Swap(char &a, char &b) {
char temp = a;
a = b;
b = temp;
}
图片如下:
在编译器编译阶段,对于模版函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用,比如:当用double类型使用函数模版时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。
4.函数模版的实例化:
不同类型的参数使用函数模版时,称为函数模版的实例化,模版参数实例化分为:隐式实例化和显示实例化。
- 隐式实例化:让编译器根据实参推演模版参数的实际类型
- 显示实例化:在函数名后面的<>中指定模版参数的实际类型
如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
5.模版参数的匹配原则:
- 一个非模版函数可以和一个同名的函数模版同时存在,而且在该函数模版还可以被实例化为这个非模版函数。
- 对于非模版函数和同名函数模版,如果其他条件都相同,在调用时会优先调用非模版函数而不会从该模版产生出一个实例,如果模版可以产生一个具有更好匹配的函数,那么将选择模版。
- 模版函数不允许自动类型转换,但普通函数可以进行自动类型转换。
三.类模版
1.类模版的定义格式:
C++除了支持函数模版,还支持类模版(Class Tamplate).函数模版中定义的类型参数可以用在函数声明和函数定义中,类模版中定义的类型参数还可以用在函数定义中,类模版中定义的类型参数可以用在声明和类实现中,类模版的目的同样是将数据的类型参数化。
声明类模版的语法为:
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};类模版和函数模版都是以template开头(也可以使用class,目前来讲他们没有任何区别),后跟类型参数;类型参数不能为空,多个类型参数用逗号隔开。
一但声明了类模版,就可以将类参数用于类的成员函数和成员变量了,换句话说,原来使用int,float,char等内置类型的地方,都可以用类型参数来代替。
// 动态顺序表
// 注意: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();
// ...
size_t Size() { return _size; }
T& operator[](size_t pos)
{
assert(pos < _size);
return _pData[pos];
}
private:
T* _pData;
size_t _size;
size_t _capacity;
};
// 注意:类模板中函数放在类外进行定义时,需要加模板参数列表
template <class T>
Vector<T>::~Vector()
{
if (_pData)
delete[] _pData;
_size = _capacity = 0;
}
2.类模版的实例化:
类模版实例化与函数模版实例化不同,类模版实例化需要再类模版名字后跟<>,然后将实例化的类型放在<>中即可,类模版名字不是真正的类,而实例化的结果才是真正的类。
// Vector类名,Vector<int>才是类型
Vector<int> s1;
Vector<double> s2;
3.类模版实现变长数组:
#include <iostream>
#include <cstring>
using namespace std;
template <class T>
class CArray
{
int size; //数组元素的个数
T* ptr; //指向动态分配的数组
public:
CArray(int s = 0); //s代表数组元素的个数
CArray(CArray& a);
~CArray();
void push_back(const T& v); //用于在数组尾部添加一个元素v
CArray& operator=(const CArray& a); //用于数组对象间的赋值
T length() { return size; }
T& operator[](int i)
{//用以支持根据下标访问数组元素,如a[i] = 4;和n = a[i]这样的语句
return ptr[i];
}
};
template<class T>
CArray<T>::CArray(int s) :size(s)
{
if (s == 0)
ptr = NULL;
else
ptr = new T[s];
}
template<class T>
CArray<T>::CArray(CArray& a)
{
if (!a.ptr) {
ptr = NULL;
size = 0;
return;
}
ptr = new T[a.size];
memcpy(ptr, a.ptr, sizeof(T) * a.size);
size = a.size;
}
template <class T>
CArray<T>::~CArray()
{
if (ptr) delete[] ptr;
}
template <class T>
CArray<T>& CArray<T>::operator=(const CArray& a)
{ //赋值号的作用是使"="左边对象里存放的数组,大小和内容都和右边的对象一样
if (this == &a) //防止a=a这样的赋值导致出错
return *this;
if (a.ptr == NULL) { //如果a里面的数组是空的
if (ptr)
delete[] ptr;
ptr = NULL;
size = 0;
return *this;
}
if (size < a.size) { //如果原有空间够大,就不用分配新的空间
if (ptr)
delete[] ptr;
ptr = new T[a.size];
}
memcpy(ptr, a.ptr, sizeof(T) * a.size);
size = a.size;
return *this;
}
template <class T>
void CArray<T>::push_back(const T& v)
{ //在数组尾部添加一个元素
if (ptr) {
T* tmpPtr = new T[size + 1]; //重新分配空间
memcpy(tmpPtr, ptr, sizeof(T) * size); //拷贝原数组内容
delete[]ptr;
ptr = tmpPtr;
}
else //数组本来是空的
ptr = new T[1];
ptr[size++] = v; //加入新的数组元素
}
int main()
{
CArray<int> a;
for (int i = 0; i < 5; ++i)
a.push_back(i);
for (int i = 0; i < a.length(); ++i)
cout << a[i] << " ";
return 0;
}