C++中的函数模板,类模板

看一个我们经常用到的一个交换函数:
void Swap(int * a,int * b)                                         
{                                                                  
    int tmp=*a;                                                    
    *a=*b;                                                         
    *b=tmp;                                                                                                                         
}       

很明显上面的代码只能实现整型的交换,如果想要交换char类型,或者int *类型的变量的话,我们就要进行函数重载。 都是同样的逻辑,这样的话代码量是很大的。
那么C++中提供了一种 泛型编程所谓泛型编程,就是编写与类型无关的逻辑代码,是一种复用的方式,模板就是泛型编程的基础,模板分为模板函数和模板类。

一、函数模板

函数模板格式:

tamplate< class 形参名一,class 形参名二,class 形参名n>
函数结构

模板形参的定义既可以用class ,也可以用 typename
看下面的一个交换 函数的例子
 
 
#include <stdio.h>
#include <string>
#include <Windows.h>
#include <iostream>
using namespace std;

template<class T>
void Swap(T * a, T *b)
{
	T tmp = *a;
	*a = *b;
	*b = tmp;
}


void test_swap()
{
	int a = 2, b = 5;
	char ch1 = 'x', ch2 = 'w';
	printf("交换之前 a:%d  b:%d\n", a, b); 
	printf("交换之前 ch1:%c  ch2:%c\n", ch1, ch2);
	Swap(&a, &b);
	Swap(&ch1, &ch2);
	printf("交换之后 a:%d  b:%d\n", a, b);
	printf("交换之后 ch1:%c  ch2:%c\n", ch1, ch2);
}



int main()
{
	test_swap();
	system("pause");
	return 0;
}

这样我们就实现了当你给函数模板传什么类型的参数,他就会自己进行推演出T的类型,再将函数模板进行实例化,生成一份代码,这样其实我们的两个交换调用的并不是同一个函数
从底层汇编来看这里call的函数不是一个地址

还有一点应该清楚,因为函数模板其实是在实例化之后才会进行创建函数栈帧,实例化之后才会知道各个参数的类型和大小,

若是没有将函数模板进行实例化,即使函数模板中有很明显的语法错误,编译器是不会进行检查的。
当我们写下这样的代码


template<class T>
bool IsEqual(T a, T b)
{
	return a == b;
}

void test_IsEqual()
{
	int ret = IsEqual(1, 2);//两个相同类型
	cout << "结果:"<<ret << endl;
	ret = IsEqual(1, 1.0);//不同类型
	cout << "结果:" << ret << endl;

}


int main()
{
	//test_swap();
	test_IsEqual();
	system("pause");
	return 0;
}

当两个参数不相同时,这时就编不过了,因为这里的泛型T只能实例化出一种类型,所以如果两个参数类型不相同时,我们就采用另外一种方法
显示实例化
void test_IsEqual()
{
	int ret = IsEqual<int>(1, 2);
	cout << "结果:"<<ret << endl;
	ret = IsEqual<int>(1, 1.0);
	cout << "结果:" << ret << endl;

}

这样相当于显示的将这里的参数类型T实例化为int类型。


那么如果当有了函数模板的时候,我们还有一个自己的函数的时候,这个时候到底会调用哪一个呢,设想如果你是编译器的设计者,那应该是在可以实现相同发功能下尽可能的提高效率,很明显直接调用普通函数不用进行推演,效率也高。
可以自己尝试调试下面代码来验证是否是调用了这里的
 
 
template<class T>
bool IsEqual(const T &a,const  T  &b)
{
	return a == b;
}

bool IsEqual(const double &a,const  double  &b)
{
	return a == b;
}

bool IsEqual(const int &a, const  int  &b)
{
	return a == b;
}

int main()
{
	int a = 2, b = 5;
	double c = 2.3, d = 5.6;
	cout << IsEqual<int>(a, b) << endl;
	cout << IsEqual<double>(a, c) << endl;
	//如果有重载的函数,就不会去进行推演
	cout << IsEqual(a, b) << endl;
	cout << IsEqual(c, d) << endl;
	system("pause");
	return 0;
}


二、类模板

类模板结构:
template <class 形参名1, class 形参名2, class 形参名n>  

 class 类名 { ... };


按照我们之前写的顺序表和链表 我们都是将data类型重定义为datatype类型,现在有了模板的概念,我们就可以很方便的实现了。
这里贴上顺序表的代码
这里我们采用的是三个指针的方式,_start为开始_finish为顺序表结束的的位置,_endOfstorage为顺序表的最大容量的位置
当进行插入数据时,只需要移动_finish即可,进行扩容时就是重新找一块大的空间,将值进行复制后,将_endOfstorage指向最后位置。

//我也 很可怜的,代码贴上去就成这样了,修改之后还是这样了
//代码可以戳这里: 点击打开链接

 
 
#pragma once#include <stdio.h>#include <string>#include <string.h>#include <stdlib.h>#include <assert.h>#include <iostream>using namespace std;template <class T>class Vector{public: Vector():_start(NULL),_finish(NULL),_endOfstorage(NULL) {} ~Vector() { delete []_start; _start=_finish=_endOfstorage=NULL; } size_t Size() const { return _finish-_start; } size_t Capacity() const { return _endOfstorage-_start; } Vector(const Vector<T> &v); Vector<T> operator=(const Vector<T> &v); void PushBack(const T & value); void PushFront(const T & value); void PopFront(); T & Back(); T & Front(); bool Empty(); void PopBack(); void Insert(size_t pos,const T & value); void Erase(size_t pos); void clear(); void Display();protected: T *_start; T *_finish; T *_endOfstorage; void Expend(size_t n)//扩容可以不作为接口 { if(Capacity()==0) {//最开始 n=3; } if(n>Capacity()) { size_t size=Size();//这里需要先将size保存下来 T * tmp=new T[n]; T * start=tmp; T *finish=_start; while(finish!=_finish)//特别要注意这里只要设计到拷贝的时候,当是我们是内置类型的时候是可以直接用memcpy()的,但是因为T有可能是string类型或者甚至是链表等,这个时候就只能一个一个赋值了,下面会有讲到 { *start=*finish; ++start; ++finish; }//将旧的数据先复制过去 clear();//清理掉旧的 _start=tmp; _finish=tmp+size; _endOfstorage=_start+n; } }};template <class T>Vector<T>::Vector(const Vector<T> &v):_start(NULL),_finish(NULL),_endOfstorage(NULL){ _start=new T[v.Size()]; T *start=_start; T *finish=v._start; while(finish!=v._finish) { *start=*finish; ++start; ++finish; } //将v中的数据都拷贝进去 _finish=_start+v.Size(); _endOfstorage=_finish;}template<class T>Vector<T> Vector<T>:: operator=(const Vector<T> &v){
      //采用传统写法 //if(this!=&v) //{ //T *tmp=new T[v.Size()]; //T *start=tmp; //T *finish=v._start; //while(finish!=v._finish) //{ // *start=*finish; // start++; // finish++; //} //clear(); //_start=tmp; //_finish=_start+v.Size(); //_endOfstorage=_finish; //}
//采用现代写法,函数结束后tmp自己调用析构函数 if(this!=&v) { Vector<T> tmp(v); swap(tmp._start,_start); swap(tmp._finish,_finish); swap(tmp._endOfstorage,_endOfstorage); } return *this;}template<class T>void Vector<T>::Insert(size_t pos,const T & value){ if(pos>Size()) {//Size()位置也是可以进行插入的,为了进行尾插和头插 return; } if(Size()>=Capacity()) {//需要进行扩容 Expend(Capacity()*2); } //然后将pos位置后面的数据向后移动 T *cur=_finish;//这里采用指针的方法是为了避免pos的size_t类型的比较 while(cur!=_start+pos) { *cur=*(cur-1); --cur; } _start[pos]=value; _finish++;//完成一次插入后需要将_finish后移}template<class T>void Vector<T>:: PushBack(const T & value){//这里传参要传引用,因为value若是一个链表或者比较大的数据,传值会进行拷贝构造,代价比较大 Insert(Size(),value);}template<class T>void Vector<T>::PopBack(){ Erase(Size()-1);}template<class T>void Vector<T>::Erase(size_t pos){ if(pos>=Size()) { return; } if(Size()==0) { return; } //将pos位置开始的元素向前移动 T *cur=_start+pos+1; while(cur!=_finish) { *(cur-1)=*(cur); cur++; } --_finish;//删除一个元素之后需要将_finish向前移动}template<class T>void Vector<T>::PushFront(const T & value){ Insert(0,value);}template<class T>void Vector<T>::PopFront(){ Erase(0);}template<class T>bool Vector<T>::Empty(){ return _finish==_start;}template<class T>void Vector<T>::clear(){ delete[] _start; _start=_finish=_endOfstorage=NULL;}template<class T>void Vector<T>::Display(){ if(Size()==0) { return; } T *cur=_start; while(cur!=_finish) { cout<<*cur<<" "; ++cur; } cout<<endl;}

解释上面进行拷贝的时候要一个一个的赋值,
例如当T为string类型时

那么我们采用一个一个进行赋值的话,就会自己进行调用string类型的赋值运算符重载,自己的赋值重载会进行处理(VS下为深拷贝,Linux下为引用计数)。

这里应当注意,当采用模板类时,例如上面的Vector

这里的类名为Vector,一般只是用到构造函数和析构函数名
类型为Vector<T>,例如上面的类外实现函数时的类限定符

三、非类型模板参数

模板参数不一定一定是类型,也可以是常量
看下面我们定义的一个模板类
const int N = 100;
template<class T>
class Array
{
protected:
	T _arr[N];
	size_t _size;
};
int main()
{
	Array<int> arr1;//大小为100
	Array<int> arr2;//大小也为100

	return 0;
}

上面这种情况数组的大小是确定的,很明显这里是不合适的
这里可以将常量N当作模板参数传递过去,但是注意浮点类型和类和自定义类型不允许做模板参数
const int N = 100;
template<class T,size_t N=100>
class Array
{
protected:
	T _arr[N];
	size_t _size;
};
int main()
{
	Array<int,100> arr1;//大小为100
	Array<int,1000> arr2;//大小为1000

	return 0;
}

2、多学一点
我们知道C++库里有一个交换函数,swap();

也是用模板来实现的,当我们要进行自定义类型交换的时候,尽量不要用swap(),这里 会调用拷贝构造,来进行交换,开销太大

猜你喜欢

转载自blog.csdn.net/misszhoudandan/article/details/80290082