第2章 函数模板
函数模板提供了一种函数行为,该函数行为可以用多种不同的类型进行调用;也就是说,函数模板代表一个函数族、它表示看起来和普通的函数很相似,唯一的区别就是有些函数元素是未确定的:这些元素将在使用时被参数化
定义模板:
template <typename T>
inline T const& max(T const&a, T const& b)
{
return a < b ? b:a;
}
实参推导:
当我们为某些实参调用一个诸如max()的模板时,模板参数可以由我们所传递的实参来决定。如果我们传递了两个int给参数类型T const&,那么C++编译器能够得出结论:T必须是int。这里不允许进行自动类型转换,每个T都必须正确地匹配。
template<typename T>
inline T const& max(T const& a,T const& b);
max(4,7); //OK:两个实参的类型都是int
max(4,4,2); //ERROR:第一个T是int,而第2个T是double
有3种方法可以用来处理上面这个错误:
(1)对实参进行强制类型转换,使它们可以相互匹配:
? max( static_cast(4), 4.2); //OK
(2)显示指定(或限定)T的类型
? max(4,4,2); //OK
(3)指定两个参数可以具有不同的类型
第3章 类模板
类模板Stack的实现:
#include <vector>
#include <stdexcept>
template <typename T>
class Stack{
private:
std::vector<T> elems; //存储元素的容器
public:
void push(T const&);
void pop();
T top() const;
bool empty() const{
return elems.empty();
}
};
template <typename T>
void Stack<T>::push(T const& elem)
{
elems.push_back(elem);
}
template <typename T>
void Stack<T>::pop()
{
if(elems.empty()){
throw std::out_of_range("Stack<>::pop():empty stack");
}
elems.pop_back();
}
template <typename T>
T Stack<T>::top() const
{
if(elems.empty()){
throw std::out_of_range("Stack<>::pop():empty stack");
}
elems.back();
}
类模板的特化:
为了特化一个类模板,必须在起始处声明一个template<>,接下来声明用来特化类模板的类型。这个类型被用作模板实参,且必须在类名的后面直接指定:
template<>
class Stack<std::string>{
...
}
进行类模板的特化时,每个成员函数都必须重新定义为普通函数,原来模板函数中的每个T也相应地被进行特化的类型取代:
void Stack<std::string>::push(std::string const& elem)
{
elems.push_back(elem);
}
局部特化:
类模板可以被局部特化。可以在特定的环境下指定类模板的特定实现,并且要求某些模板参数仍然必须由用户来定义。如类模板:
template <typename T1,typename T2>
class Myclass{
...
};
可以有下面几种局部特化:
//局部特化:两个模板参数具有相同的类型
template <typename T>
class MyClass<T,T>{
...
};
//局部特化:第2个模板参数的类型是int
template<typename T>
class MyClass<T,int>{
...
};
//局部特化:两个模板参数都是指针类型
template<typename T1,typename T2>
class MyClass<T*,T*>{
...
};
如果有多个局部特化同等程度地匹配某个声明,那么就称该声明具有二义性:
MyClass<int,int> m; //ERROR:同等程度地匹配MyClass<T,T>和MyClass<T,int>
MyClass<int*,int*> m; //ERROR:同等程度地匹配MyClass<T,T>和MyClass<T1*,T2*>
为了解决第2中二义性,可以另外提供一个指向相同类型指针的特化:
template<typename T>
class MyClass<T*,T*>{
...
};
缺省模板实参:
如在类Stack<>中,可以把用于管理元素的容器定义为第2个模板参数,并且使用std::vector<>作为它的缺省值
template <typename T,typename CONT = std::vector<T> >
class Stack{
private:
CONT elems; //包含元素的容器
public:
void push(T const&);
void pop();
T top() const;
bool empty() const{
return elems.empty();
}
};
template<typename T,typename CONT>
void Stack<T,CONT>::push(T const& elem)
{
elems.push_back(elem);
}
如果你只传递第一个类型实参给这个类模板,那么将会利用vector来管理stack的元素
- 为了使用类模板,可以传入某个具体类型作为模板实参;然后编译器将会基于该类型来实例化类模板
- 可以用某种特定类型特化类模板
- 可以用某种特定类型局部特化类模板
- 可以为类模板的参数定义缺省值,这些值还可以引用之前的模板参数
第4章 非类型模板参数
对于函数模板和类模板,模板参数并不局限于类型,普通纸也可以作为模板参数。在基于类型参数的模板中,你定义了一些具体未加确定的代码,直到代码被调用时这些细节才被真正确定。然而,这里我们面对的这些细节是值value,而不是类型。当要使用基于值的模板时,你必须显式地指定这些值,才能够对模板进行实例化,并获得最终代码。
可以使用元素数目固定的数组来实现stack,优点是:无论是由你来亲自管理内存,还是由标准容器来管理内存,都可以避免内存管理开销。然而,决定一个栈的最佳容量是很困难的。一个好的解决办法就是:让栈的用户亲自制定数组的大小,并把它作为所需要的栈元素的最大个数。
#include <stdexcept>
#define MAXSIZE 1024
template<typename T,int MAXSIZE>
class Stack{
private:
T elmes[MAXSIZE]; //包含元素的数组
int numElems; //元素的当前总个数
public:
Stack();
void push(T const&);
void pop();
T top() const;
};
template<typename T,int MAXSIZE>
Stack<T,MAXSIZE>::Stack():numElems(0)
{
//do nothing
}
MAXSIZE是新加入的第2个模板参数,类型为int;它指定了数组最多可包含的栈元素的个数。
为了使用这个模板,你需要同时指定元素的类型和个数(即栈的最大容量):
Stack<int,20> int20Stack;
同样,我们可以为模板参数指定缺省值:
template<typename T = int, int MAXSIZE = 100>
class Stack{
...
};
第5章 技巧性基础知识
1.关键字typename
通常而言,当某个依赖模板参数的名称是一个类型时,就应该使用typename
考虑一个typename的典型应用,即在模板代码中 访问STL容器的迭代器
//打印STL容器的元素
template <typename T>
void printColl(T const& coll)
{
typename T::const_iterator pos; //用于迭代coll的迭代器
typename T::const_iterator end(coll.end()); //结束位置
for(pos==coll.begin();pos!=end;++pos){
std::cout<<*pos<<" ";
}
std::cout<<std::endl;
}
为了访问模板类型为T的const_iterator类型,需要在声明开始处使用关键字typename来加以限定,如:
typename T::const_iterator pos;
2.成员模板
对于元素类型不同的栈,你不能对它们进行相互赋值,即使这两种(元素的)类型之间存在隐式类型转换。
缺省赋值运算符要求两边具有相同的类型,当元素类型不同时,两个栈的类型显然不同,不能符合缺省赋值运算符的要求。
然而,通过定义一个身为模板的赋值运算符,针对元素类型可以转换的两个栈就可以进行相互赋值。
template<typename T>
class Stack{
private:
std::deque<T> elems; //存储元素的容器
public:
void push(T const&);
void pop();
//使用元素类型为T2的栈进行赋值
template<typename T2>
Stack<T>& operator= (Stack<T2> const&);
};
3.使用字符串作为函数模板的实参
有时,把字符串传递给函数模板的引用参数会导致出人意料的运行结果
#include <string>
//注意:引用参数
template <typename T>
inline T const& max1(T const& a,T const& b)
{
return a < b ? b:a;
}
int main()
{
std::string s;
max1("apple","peach"); //OK:相同类型的实参
max1("apple","tomato"); //ERROR:不同类型的实参
max1("apple",s); //ERROR:不同类型的实参
return 0;
}
问题在于:由于长度的区别,这些字符串属于不同的数组类型。也就是说,‘apple’和‘peach’具有相同的类型char const[6];然而‘tomato’的类型则是:char const[7].
模板实例化是一个通过使用具体值替换模板实参,从模板产生出普通类、函数或者成员函数的过程。这个过程最后获得的实体(比如类、函数或者成员函数)就是我们通常所说的特化(specialization)。对于仍然具有模板参数的特化,我们称之为局部特化(partial specialization)
template <typename T>
class MyClass<T,T>{
...
};
template <typename T> //局部特化
class MyClass<bool,T>{
...
};
泛型编程
1.函数模板
通过template关键字来声明使用模板,typename关键字来定义模板类型
template <typename T> //声明使用模板,并定义T是一个模板类型
void swap(T& a,T& b)
{
T c = a;
a = b;
b = c;
}
当我们使用int类型参数来调用swap,则T就会自动转换为int类型
函数模板的使用:分为自动调用和显示调用
int a = 0;
int b = 1;
swap(a,b); //自动调用,编译器根据a和b的类型来推导
float c = 0;
float d = 1;
swap<float>(c,d); //显示调用,告诉编译器,调用的参数是float类型
初探函数模板:
写两个函数模板,一个用来排序数组,一个用来打印数组
#include <iostream>
#include <string>
using namespace std;
template <typename T>
void Sort(T a[],int len)
{
for(int i = 1;i<len;++i)
for(int j = 0;j<i;j++)
if(a[i] < a[j])
{
T t = a[i];
a[i] = a[j];
a[j] = t;
}
}
template <typename T>
void Println(T a[],int len)
{
for(int i = 0;i<len;i++)
{
cout<<a[i]<<",";
}
cout<<endl;
}
int main()
{
int a[5] = {5,3,2,4,1};
Sort(a,5); //自动编译,编译器根据a和5的类型来推导
Println<int>(a,5); // 显示调用,告诉编译器,调用的参数是int类型
string s[5] = {"Java","C++","Pascal","Ruby","Basic"};
Sort(s,5);
Println(s,5);
return 0;
}
深入理解函数模板:
为什么函数模板能够执行不同的类型参数?
- 其实编译器对函数模板进行了两次编译
- 第一次编译时,首先检查函数模板本身有没有语法错误
- 第二次编译时,会去找调用函数模板的代码,然后通过代码的真正参数,来生成真正的函数。
- 所以函数模板,其实只是一个模具,当我们调用它时编译器就会给我们生成真正的函数。
试验函数模板是否生成真正的函数:通过两个不同类型的函数指针指向函数模板,然后打印指针地址是否一致
#include <iostream>
using namespace std;
template <typename T>
void Swap(T& a,T& b)
{
T c = a;
a = b;
b = c;
}
int main()
{
void (*FPii)(int&,int&);
FPii = Swap; //函数指针FPii指向Swap函数
void (*FPff)(float&,float&);
FPff = Swap; //函数指针FPff指向Swap函数
cout<<reinterpret_cast<void *>(FPii)<<endl;
cout<<reinterpret_cast<void *>(FPff)<<endl;
//编译该行会出错,因为Swap()只是个模板,并不是一个真正函数
//cout<<reinterpret_cast<void *>(Swap)<<endl;
return 0;
}
输出结果:
0x402d60
0x402d10
可以发现两个不同类型的函数指针,指向同一个函数模板,但打印的地址却不一样,显然编译器默默帮我们生成了两个不同的真正函数
多参数函数模板
函数模板可以定义任意多个不同的类型参数
template <typename T1,typename T2,typename T3>
T1 Add(T2 a,T3 b)
{
return static_cast<T1>(a+b);
}
注意:
//T1 = int,T2 = double,T3 = double
int r1 = Add<int>(0.5,0.8);
//T1 = int,T2 = float,T3 = double
int r2 =Add<int,float>(0.5,0.8);
//T1 = int,T2 = float,T3 = double
int r3 =Add<int,float,float>(0.5,0.8);
开始试验多参数函数模板:
#include <iostream>
using namespace std;
template<typename T1,typename T2,typename T3>
T1 Add(T2 a,T3 b)
{
return static_cast<T1>(a+b);
}
int main()
{
int a = Add(1,1.5); //该行编译出错,没有指定返回值类型
int a = Add<int>(1,1.5);
cout<<a<<endl; //2
float b = Add<float,int,float>(1,1.5);
cout<<b<<endl; //2.5
return 0;
}
运行结果:
2
2.5
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8xT5Cr7D-1647411362275)(F:\C++\泛型编程\image-20220316103523833.png)]
#include <iostream>
using namespace std;
template <typename T>
T Max(T a,T b)
{
cout<<"T Max(T a,T b)"<<endl;
return a > b ? a : b;
}
template <typename T>
T Max(T* a,T* b) //重载函数模板
{
cout<<"T Max(T* a,T* b)"<<endl;
return *a > *b ? *a : *b;
}
int Max(int a,int b) //重载普通函数
{
cout<<"int Max(int a,int b)"<<endl;
return a > b ? a : b;
}
int main()
{
int a=0;
int b=1;
cout<<"a:b="<<Max(a,b) <<endl ; //调用普通函数 Max(int,int)
cout<<"a:b="<<Max<>(a,b)<<endl; //通过模板参数表 调用 函数模板 Max(int,int)
cout<<"1.5:2.0="<<Max(1.5,2.0)<<endl;
//由于两个参数默认都是double,所以无法隐式转换,则调用函数模板 Max(double,double)
int *p1 = new int(1);
int *p2 = new int(2);
cout<<"*p1:*p2="<<Max(p1,p2)<<endl; // 调用重载函数模板 Max(int* ,int* )
cout<<"'a',100="<< Max('a',100)<<endl;
//将char类型进行隐式转换,从而调用普通函数 Max(int,int)
delete p1;
delete p2;
return 0;
}
2.类模板
和函数模板一样,将泛型思想应用于类。编译器对类模板处理方式和函数模板相同,都进行两次编译。
类模板通常应用于数据结构方面,使得类的实现不再关注数据元素的具体类型,而只关注需要实现的功能。
使用方法:通过template关键字来声明,然后通过typename关键字来定义模板类型
template <typename T>
class operator
{
public:
T op(T a,T b);
};
类模板的使用:
- 定义对象时,必须制定类模板类型,因为编译器无法推导类型
- 使用具体类型 来定义对象
operator<int> op1;
operator<string> op2;
int i = op1.op(1,2);
string s = op2.op("D.T.","Software");
初探类模板:
//实现不同类型的加减乘除
#include <iostream>
#include <string>
using namespace std;
template <typename T>
class Operator
{
public:
T add(T a,T b)
{
return a+b;
}
T minus(T a, T b)
{
return a - b;
}
T multiply(T a, T b)
{
return a * b;
}
T divide(T a, T b)
{
return a / b;
}
};
string operator-(string& l,string& r)
{
return "Minus";
}
int main()
{
Operator<int> op1; //定义对象时,需要指定类模板类型
cout<<op1.add(1,3)<<endl;
Operator<string> op2;
cout <<op2.minus("D.T.","software")<<endl;
return 0;
}
类模板的工程应用
- 类模板必须在.h头文件中定义
- 类模板的成员函数不能分开在不同的文件中实现
- 类模板外部定义的成员函数和模板函数一样,还需要加上模板template声明,以及结构体声明
#ifndef _OPERATOR_H
#define _OPERATOR_H
template < typename T >
class Operator
{
public:
T add(T a, T b);
T minus(T a, T b);
T multiply(T a, T b);
T divide(T a, T b);
};
template < typename T > //外部定义的成员函数,都需要加上模板声明
T Operator<T> :: add(T a, T b) //同时加上结构体<T>声明
{
return a+b;
}
template < typename T >
T Operator<T> :: minus(T a, T b)
{
return a-b;
}
template < typename T >
T Operator<T> :: multiply(T a, T b)
{
return a*b;
}
template < typename T >
T Operator<T> :: divide(T a, T b)
{
return a/b;
}
#endif
多参数类模板:
类模板可以定义任意多个不同的类型参数,同时还要必须指定每个模板参数
template <typename T1,typename T2>
class Operator
{
public:
void add(T1 a,T2 b);
};
template<typename T1,typename T2>
void Operator<T1,T2>::add(T1 a,T2 b)
{
cout<<(a+b)<<endl;
}
int main()
{
Operator<int,float> op1; //定义op1对象时,必须指定类模板类型
op1.add(2,2.1);
return 0;
}
类模板也可以像函数重载一样,类模板通过特化的方式实现特殊情况。
类模板特化:
- 表示可以存在多个相同的类名,但是模板类型都不一致(和函数重载的参数类似)
- 特化类型有完全特化和部分特化两种类型
- 完全特化表示显示指定类型参数,模板声明只需写成template<>,并在类名右侧指定参数
template < typename T1,typename T2 > //声明的模板参数个数为2个
class Operator //正常的类模板
{
public:
void add(T1 a, T2 b)
{
cout<<a+b<<endl;
}
};
template <> //不需要指定模板类型,因为是完全特化的类模板
class Operator< int , int> //指定类型参数,必须为2个参数,和正常类模板参数个数一致
{
public:
void add(int a, int b)
{
cout<<a+b<<endl;
}
};
int main()
{
Operator<int,int> Op1; //匹配完全特化类模板:class Operator< int,int>
Operator<int,float> Op2; //匹配正常的类模板
return 0;
}
- 部分特化表示通过特定规则约束类型参数,和模板声明类似,并在类名右侧指定参数,比如:
template < typename T1,typename T2 > //声明的模板参数个数为2个
class Operator //正常的类模板
{
public:
void add(T1 a, T2 b)
{
cout<<a+b<<endl;
}
};
template < typename T > //有指定模板类型以及指定参数,所以是部分特化的类模板
class Operator< T* ,T*> //指定类型参数,必须为2个参数,和正常类模板参数个数一致
{
public:
void add(T* a, T* b)
{
cout<<*a+*b<<endl;
}
};
int main()
{
Operator<int*,int*> Op1; //匹配部分特化: class Operator< T* ,T*>
Operator<int,float> Op2; //匹配正常的类模板: class Operator
return 0;
}
编译时,会根据对象定义的类模板类型,首先去匹配完全特化,再来匹配部分特化,最后匹配正常的类模板。
初探类模板特化:
#include <iostream>
using namespace std;
template < typename T1,typename T2 >
class Operator //正常的类模板
{
public:
void add(T1 a, T2 b)
{
cout<<"add(T1 a, T2 b)"<<endl;
cout<<a+b<<endl;
}
};
template < typename T >
class Operator<T,T> //部分特化的类模板,当两个参数都一样,调用这个
{
public:
void add(T a, T b)
{
cout<<"add(T a, T b)"<<endl;
cout<<a+b<<endl;
}
};
template < typename T1,typename T2 >
class Operator<T1*,T2*> //部分特化的类模板,当两个参数都是指针,调用这个
{
public:
void add(T1* a, T2* b)
{
cout<<"add(T1* a, T2* b)"<<endl;
cout<<*a+*b<<endl;
}
};
template < >
class Operator<void*,void*> //完全特化的类模板,当两个参数都是void*,调用这个
{
public:
void add(void* a, void* b)
{
cout<<"add(void* a, void* b)"<<endl;
cout<<"add void* Error"<<endl; //void*无法进行加法
}
};
int main()
{
int *p1 = new int(1);
float *p2 = new float(1.25);
Operator<int,float> Op1; //匹配正常的类模板:class Operator
Op1.add(1,1.5);
Operator<int,int> Op2; //匹配部分特化的类模板:class Operator<T,T>
Op2.add(1,4);
Operator<int*,float*> Op3; //匹配部分特化的类模板:class Operator<T1*,T2*>
Op3.add(p1,p2);
Operator<void*,void*> Op4; //匹配完全特化的类模板:class Operator<void*,void*>
Op4.add(NULL,NULL);
delete p1;
delete p2;
return 0;
}
数值型模板参数:
之前,我们学习的模板参数都是带泛型的(表示不同类型),其实模板参数也可以是数值型参数。
template
<typename T,int N>
void func()
{
T a[N]; //使用模板参数定义局部数组
}
func<double,10>();
数值型模板参数必须在编译时被唯一确定
变量在运行期间是可变的,所以不能作为模板参数。类对象(可变)
通过数值参数的类模板来求 1+2+3+…+N的值:
#include <iostream>
using namespace std;
template <int N>
class Sum
{
public:
static const int VALUE = Sum<N-1>::VALUE + N;
};
template < >
class Sum < 1 >
{
public:
static const int VALUE = 1;
};
int main()
{
cout<<"1+2+3+...+10= "<<Sum<10>::VALUE<<endl;
cout<<"1+2+3+...+100= "<<Sum<100>::VALUE<<endl;
return 0;
}
,T2> Op3.add(p1,p2); Operator<void*,void*> Op4; //匹配完全特化的类模板:class Operator<void*,void*> Op4.add(NULL,NULL); delete p1; delete p2; return 0; }
数值型模板参数:
之前,我们学习的模板参数都是带泛型的(表示不同类型),其实模板参数也可以是**数值型参数**。
```c++
template
<typename T,int N>
void func()
{
T a[N]; //使用模板参数定义局部数组
}
func<double,10>();
数值型模板参数必须在编译时被唯一确定
变量在运行期间是可变的,所以不能作为模板参数。类对象(可变)
通过数值参数的类模板来求 1+2+3+…+N的值:
#include <iostream>
using namespace std;
template <int N>
class Sum
{
public:
static const int VALUE = Sum<N-1>::VALUE + N;
};
template < >
class Sum < 1 >
{
public:
static const int VALUE = 1;
};
int main()
{
cout<<"1+2+3+...+10= "<<Sum<10>::VALUE<<endl;
cout<<"1+2+3+...+100= "<<Sum<100>::VALUE<<endl;
return 0;
}
|