C++模板---泛型编程

第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;
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值