c++::模板你不知道的事~

一、相信大家会有疑问,为什么要使用模板?别着急,先来看一个例子:

首先如果我们要写一个通用加法的函数,会采用什么办法呢:

方法一:采用函数重载的方式,你需要什么类型我们就重载出来什么类型

int Add(const int &x, const int &y)
{
	return (x + y);
} 
float Add(const float &x, const float &y)
{
	return (x + y);
}
【缺点】:

1)、只要有新类型出现,就要重新添加对应函数。
2)、除类型外,所有函数的函数体都相同,代码的复用率不高
3)、如果函数只是返回值类型不同,函数重载不能解决
4)、一个方法有问题,所有的方法都有问题,不好维护。

方法二:使用公共基类,将通用的代码放到公共基类里

#include<iostream>
using namespace std;
//通用加法抽象类
class General{
public:
	//这个加法函数返回值不能随参数的变化而变化
	//因此,给它一个最大的long long
	virtual long long Add() = 0;
};
//适用于整型
class IntAdd:public General{
public:
	IntAdd(const int left = 0,const int right = 0)
		:_left(left)
		,_right(right){}
	//抽象类内Add函数的实现
	virtual long long Add(){
		return _left+_right;
	}
private:
	int _left;
	int _right;
};
//适用于char
class CharAdd:public General{
public:
	CharAdd(const char left,const char right)
		:_left(left)
		,_right(right){}
	//对抽象类中Add函数的实现
	virtual long long Add(){
		return _left+_right;
	}
private:
	char _left;
	char _right;
};
//...更多待你实现
int main(){
	//一个基类指针
	General *p = NULL;
	//实例化一个IntAdd对象
	p = new IntAdd(3,5);
	cout<<p->Add()<<endl;
	delete p;
	//实例化一个CharAdd对象
	p = new CharAdd('1','2');
	cout<<p->Add()<<endl;
	delete p;
	return 0;
}

【缺点】:

1)、借助公共基类来编写通用代码,将失去类型检查的优点;2)、对于以后实现的许多类,都必须继承自某个特定的基类,代码维护更加困难。

方法三:使用特殊的预处理

#define ADD(a, b) ((a) + (b))
【缺点】:因为宏不是函数,没有办法进行参数检测

!!既然这几种方法都有各自的缺陷,这时我们就引入了泛型编程,也就是模板,利用模板来解决。但其实细细的说,模板只是泛型编程的基础。那么接下来让我们走进模板的世界。吐舌头



二、函数模板

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

2、格式:


注意:1)、typename是用来定义模板参数关键字,也可以使用class。建议尽量使用typename。
    不能使用struct代替typename。

    2)、模板的形参名可以随便起

3、函数模板的实例化:模板是一个蓝图,它本身不是类或者函数,编译器用模板产生指定的类或者函数的特定类型版本,产生模板特定类型的过程称为函数模板实例化。

函数模板实例化的分类:显式实例化和隐式实例化


4、函数模板的使用规则:

1)模板函数可以被定义为inline函数

template<typename T>
inline T Add(const T x, const T y)
{
    return (x + y);
}
注意:inline关键字必须放在模板形参表之后,返回值之前,不能放在template之前


2)函数模板的不足之处:参数不能进行隐式转换(与平常普通函数的对比)

     解决的方案:

 a.显式实例化

#include<iostream>
using namespace std;
template<typename T>
T Add(T a, T b)
{
	return (a+b);
}
int main()
{
	cout<<Add<int>('1','2')<<endl;
	return 0;
}

 b.将其中的一个参数进行强制类型转化

#include<iostream>
using namespace std;
template<typename T>
T Add(T a, T b)
{
	return (a+b);
}
int main()
{
	cout<<Add(<int>'1',2)<<endl;
	return 0;
}

 c.再重新定义一个函数模板,即多带一个参数,而这个参数的类型是与你实例化的参数类型匹配的

#include<iostream>
using namespace std;
template<typename T1,typename T2>
T1 Add(T1 a,T2 b)
{
	return (a+b);
}
int main()
{
	cout<<Add('1',2)<<endl;
	return 0;
}
3)    template语句与函数模板定义语句之间不允许插入别的语句。
1.	template <typename T>  
2.	int i;//错误,在template语句与函数模板定义语句之间不允许插入别的语句  
3.	 T Add(T x,T y)  
4.	{  
5.	    return (x+y);  
6.	}  

注意:模板被编译了两次:
**实例化之前,检查模板代码本身,查看是否出现语法错误,如:遗漏分号
**在实例化期间,检查模板代码,查看是否所有的调用都有效,如:实例化类型不支持某些函数调

5、函数模板的实参推演:从函数实参确定模板形参类型和值的过程称为模板实参推断多个类型形参的实参必须完全匹配

1、类型形参转换

在用实参调用模板函数的时候,一般不会转换实参以匹配已有的实例化,而是会通过模板函数产生新的实例以适应该实参

例如:template <typename T>int compare(const T& v1,const T& v2)
           short s1,s2;
           int i1,i2;
           compare(i1,i2);   //产生实例compare(int ,int)
           compare(s1,s2);   //产生实例compare(short,short)

即便有了函数compare(int ,int),在用short类型的数据调用函数compare时,也不会将short类型转换成int类型而调用compare(int ,int),而是会用模板产生一个新的实例compare(short,short)。

对于模板类型的类型形参与实参的转换,编译器只会执行两种转换:

 1、const转换:接受const引用或const指针的函数分别用非const对象的引用或指针来调用,无需产生新的实例化。如果函数接受非引用类型,形参类型和实参都忽略const,即,无论传递const或非const对象给接受非引用类型的函数,都使用相同的实例化。

 2、数组或函数到指针的转换:如果模板形参不是引用类型,则对数组或函数类型的实参应用常规指针转换。数组实参将当作指向其第一个元素的指针,函数实参当作指向函数类型的指针。

例如:template <typename T>
         T  Fun(T,T);
          template <typename T>
         T  Fun(const T&,const T&);
           string s1("a value");
           const string s2("another value");
           Fun(s1,s2);   //调用实例Fun(string,string)。因为模板函数接受非引用类型,实参被复制,所以忽略实参的const
           Fun1(s1,s2);   //调用实例Fun1(const string&,const string&)。形参接受const引用类型,非const实参可转换为const引用
          int a[10],b[42];
          Fun(a,b);  //调用实例Fun(int *,int *)。数组不同不重要,两个数组都可以转换为指针。
          Fun1(a,b);   //编译错误。形参为引用,数组不能转换为指针,此时a和b的类型不匹配,调用出错。
!!!注意:

如果形参是数组的引用,编译器不会将数组实参转化为指针,而是传递数组的引用本身。数组大小成为形参和实参类型的一部分,编译器会检查实参大小和形参大小是否匹配.

6、模板参数

函数模板有两种类型的参数:模板参数和调用参数


模板类型的形参

1)模板形参的使用规则:

模板形参名字只能在模板形参之后到模板声明或定义的末尾之间使用,遵循名字屏蔽规则


2)模板形参的名字在同一模板形参列表中只能使用一次

#include<iostream>
using namespace std;
template<typename T,typename T>
void Funtest(T1 a,T2 b)
{}
int main()
{
	Funtest(1,'2');
        return 0;
}


3)所有模板形参的前面必须加上class或者typename关键字修饰

template<typename T,U>
void Funtest(T,U)
{}         


4)在函数模板的内部不能指定缺省的模板实参

template<class T, U, typename V>
void f1(T, U, V);
template<class T>
T f2(int &T);
template<class T>
T f3 (T, T);

非模板类型的形参:

非模板类型形参是模板内部定义的常量,在需要常量表达式的时候,可以使用非模板类型参数

例如:数组长度
#include<iostream>
using namespace std;
template<typename T,int N>
void Array(T (&arr)[N])             
{
	for(size_t i = 0; i<N; i++)
	{
		cout<<arr[i];
	}
}
int main()
{
	int arr[5] = {0,1,2,3,4};
	Array(arr);
	return 0;
}

!!!重要!重要!重要!

【再次总结】:

1)、模板形参表使用<>括起来

2)、和函数参数表一样,跟多个参数时必须用逗号隔开,类型可以相同也可以不相同

3)、模板形参表不能为空

4)、模板形参可以是类型形参,也可以是非类型新参,类型形参跟在class和typename后

5)、模板类型形参可作为类型说明符用在模板中的任何地方,与内置类型或自定义类型使用方法完全相同,可用于指定函数形参类型、返回值、局部变量和强制类型转换

6)、模板形参表中,class和typename具有相同的含义,可以互换,使用typename更加直观。但关键字typename是作为C++标准加入到C++中的,旧的编译器可能不支持

7、模板函数的重载

1)显式定义的同名的函数和模板函数构成重载

#include<iostream>
using namespace std;
int MAX(const int&a, const int& b)
{
	return a>b?a:b;
}
template<typename T>
T MAX(iconst T&a,const T&b)
{
	return a>b?a:b; 
}
int main()
{
	cout<<MAX(1,2)<<endl;
	return 0;
}

2)模板函数自己的重载,当然要符合重载的条件

举个例子;

template<typename T>
T Max(const T& left, const T& right)
{
  return left>right? left:right;
}
 template<typename T>
T Max(const T& a, const T& b, const T& c)
{
  return Max(Max(a, b), c);
};

注意:函数的所有重载版本的声明都应该位于该函数被调用位置之前。
【说明】
1)、一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例
化为这个非模板函数
2)、对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调动非模板
函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数,
那么将选择模板。
3)、显式指定一个空的模板实参列表,该语法告诉编译器只有模板才能来匹配这个调用,
而且所有的模板参数都应该根据实参演绎出来。
4)、模板函数不允许自动类型转换,但普通函数可以进行自动类型转换


!!现在有个问题:就我上面求最大数的模板来说,如果传进去的是字符串,还可以比较大小吗?眼见为实:

#include<iostream>
using namespace std;
template<typename T>
T Max(T a,T b)
{
	return a>b?a:b;
}
int main()
{
	char* p1 = "world";
	char* p2 = "hello";
	cout<<Max(p1,p2)<<endl;
	return 0;
}

运行结果:

有些同学可能会说,对着呢啊。其实这里藏着一个小细节,这个结果只是巧合,我们不妨将p1,p2的值换一下,将char* p1 = "hello",char* p2 = "world",这个时候见证奇迹的时刻:

运行结果:

这是什么鬼?这个时候我们发现,也可以通过调用监视窗口来看;


我们可以看到在比较字符串的时候并没有真正的比较里面的内容只是将两者的地址传了过去,如果我们要比较,我们该怎么做呢?这个时候就引入了我下面要讲的函数模板的特化。

8、模板函数的特化

先拿个例子来说吧:

#include<iostream>
#include<string>
using namespace std;
template<typename T>
int Compare(T a,T b)
{
	if(a>b)
		return 1;
	else if(a<b)
		return -1;
	else
		return 0;
}
int main()
{
	const char* str1 = "hello";
	const char* str2 = "world";
	cout<<Compare(1,2)<<endl;
	cout<<Compare(str1,str2)<<endl;
	return 0;
}

引入函数模板的特化,可以这样定义:

template<>
int Compare<const char*>(const char*  str1,const char* str2)
{
	return strcmp(str1,str2);
}

1)、格式:

模板函数特化形式如下:

(1)、关键字template后面接一对空的尖括号<>

(2)、再接模板名和一对尖括号,尖括号中指定这个特化定义的模板形参

(3)、函数形参表

(4)、函数体
template<>
返回值 函数名<Type>(参数列表)
{
// 函数体
}

2)注意:

(1)、特化的声明必须与特定的模板相匹配

(2)、假如少了模板形参表? 只是定义了一个普通函数,该函数含有返回类型和与模板实例化相匹配的形参表。
        intcompare(intv1, intv2)
{
return0;
}

 (3)、千万注意:在模板特化版本的调用中,实参类型必须与特化版本函数的形参类型完全匹配,
如果不匹配,编译器将为实参模板定义中实例化一个实例。

#include<iostream>
#include<string>
using namespace std;
template<typename T>
int Compare(T a,T b)
{
	if(a>b)
		return 1;
	else if(a<b)
		return -1;
	else
		return 0;
}
template<>
int Compare<const char*>(const char*  str1,const char* str2)
{
	return strcmp(str1,str2);
}
int main()
{
	const char* str1 = "hello";
	const char* str2 = "world";
	cout<<Compare(1,2)<<endl;
	cout<<Compare(str1,str2)<<endl;
	return 0;
}

以这个代码为例,如果在主函数里定义的变量和你特化的模板函数的形参不匹配,那么就不会调用你特化出的版本,编译器会调用原始的模板函数,并为你实例化出一个函数

注意:特化不能出现在模板实例的调用之后,应该在头文件中包含模板特化的声明,然
后使用该特化版本的每个源文件包含该头文件


三、类模板

1、定义:模板类也是模板,必须以关键字template开头,后接模板形参表。

2、格式:

template<typename 形参名1,typename 形参名2, ...typename 形参名n>
class 类名
{ ... };

3、模板类的实例化:(以顺序表做个例子)

只要有一种不同的类型,编译器就会实例化出一个对应的类。
SeqList<intsl1;
SeqList<doublesl2;
当定义上述两种类型的顺序表时,编译器会使用int和double分别代替模板形参,重新编写
SeqList类,最后创建名为SeqList<int>和SeqList<double>的类。

4、1)模板参数--实现容器适配器

template <typename T>
class SeqList
{
private :
	int _size ;
	int _capacity ;
	T* _data ;
};
// template <class T, class Container>
template <class T, class Container = SeqList<T> > // 缺省参数
class Stack
{
public :
	void Push (const T& x);
	void Pop ();
	const T& Top();
	bool Empty ();
private :
	Container _con ;
};

 

 
2)模板的模板参数--实现容器适配器 

template <typename T>
class SeqList
{
private :
  int _size ;
  int _capacity;
  T* _data ;
};
// template <class T, template<class> class Container>
template <class T, template<class> class Container = SeqList> // 缺省参数
class Stack
{
public :
  void Push(const T& x );
  void Pop();
  const T& Top();
  bool Empty();
private :
  Container<T > _con;
};
void Test()
{
    Stack<int> s1;
    Stack<int , SeqList> s2;
}



 

 
3)非模板类型的类模板参数 

// 静态顺序表
//template<typename T, size_t MAX_SIZE>
template <typename T, size_t MAX_SIZE = 10> //带缺省模板参数
class SeqList
{
public :
  SeqList();
private :
T _array [MAX_SIZE];
  int _size ;
};
template <typename T, size_t MAX_SIZE>
SeqList <T, MAX_SIZE>::SeqList()
: _size(0)
{}
void Test()
{
 SeqList<int> s1;
 SeqList<int , 20> s2;
}
注意:
浮点数和类对象是不允许作为非类型模板参数的
//template<class T, string name>
template <class T, double MaxSize>
class Test
{
private :
  double _value ;
};
5、类模板的特化

1)全特化:

template <typename T>
class SeqList
{
public :
	SeqList();
	~ SeqList();
private :
	int _size ;
	int _capacity ;
	T* _data ;
};
template<typename T>
SeqList <T>:: SeqList()
: _size(0)
, _capacity(10)
, _data(new T[ _capacity])
{
	cout<<"SeqList<T>" <<endl;
} 
template<typename T>
SeqList <T>::~ SeqList()
{
	delete[] _data ;
} 
template <>
class SeqList <int>
{
public :
	SeqList(int capacity);
	~ SeqList();
private :
	int _size ;
	int _capacity ;
	int* _data ;
};
// 特化后定义成员函数不再需要模板形参
SeqList <int>:: SeqList(int capacity)
: _size(0)
, _capacity(capacity )
, _data(new int[ _capacity])
{
	cout<<"SeqList<int>" <<endl;
} 
// 特化后定义成员函数不再需要模板形参
SeqList <int>::~ SeqList()
{
	delete[] _data ;
} 
void test1 ()
{
	SeqList<double > sl2;
	SeqList<int > sl1(2);
}


 

 
2)偏特化 

template <typename T1, typename T2>
class Data
{
public :
	Data();
private :
	T1 _d1 ;
	T2 _d2 ;
};
template <typename T1, typename T2>
Data<T1 , T2>::Data()
{
	cout<<"Data<T1, T2>" <<endl;
} 
// 局部特化第二个参数
template <typename T1>
class Data <T1, int>
{
public :
	Data();
private :
	T1 _d1 ;
	int _d2 ;
};
template <typename T1>
Data<T1 , int>::Data()
{
	cout<<"Data<T1, int>" <<endl;
}

PS:下面的例子可以看出,偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限
制所设计出来的一个特化版本。

// 局部特化两个参数为指针类型
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public :
	Data();
private :
	T1 _d1 ;
	T2 _d2 ;
	T1* _d3 ;
	T2* _d4 ;
};
template <typename T1, typename T2>
Data<T1 *, T2*>:: Data()
{
	cout<<"Data<T1*, T2*>" <<endl;
} 
// 局部特化两个参数为引用
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public :
	Data(const T1& d1, const T2& d2);
private :
	const T1 & _d1;
	const T2 & _d2;
	T1* _d3 ;
	T2* _d4 ;
};
template <typename T1, typename T2>
Data<T1 &, T2&>:: Data(const T1& d1, const T2& d2)
: _d1(d1 )
, _d2(d2 )
{
	cout<<"Data<T1&, T2&>" <<endl;
} 
void test2 ()
{
	Data<double , int> d1;
	Data<int , double> d2;
	Data<int *, int*> d3;
	Data<int&, int&> d4(1, 2);
}


注意:模板的全特化和偏特化都是在已定义的模板基础之上,不能单独存在。






  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值