本章将阐述一些具体的 STL 模板思想,并简单介绍操作符重载与模板的联系。
模板是 C++ 语言中重要的概念。它提供了一种通用的方法来开发重用的代码,即以创建参数化的 C++ 类型。模板分为两种类型:函数模板和类模板。函数模板的用法同 C++ 预处理器的用法有一定的类似之处,它们都提供编译代码过程中的文本替换功能,但函数模板还能对类型进行一定的保护。使用类模板可以编写通用的、类型安全的类。
1.编写一个数组元素求和的函数模板。
#include <iostream>
using namespace std;
/*
int sum(int data[],int nsize)
{
int sum=0;
for(int i=0;i<nsize;i++)
{
sum+=data[i];
}
return sum;
}
*/
template <class T>
T sum(T data[],int nsize)
{
T sum=0;
for(int i=0;i<nsize;i++)
{
sum+=data[i];
}
return sum;
}
int main()
{
int data1[]={1,2,3,4,5};
float data2[]={1.1,2.2,3.3,4.4,5.5};
double data3[]={1.1,2.2,3.3,4.4,5.5};
cout<<sum(data1,5)<<endl<<sum(data2,5)<<endl<<sum(data3,5);
return 0;
}
注释中的代码为int型数组相加的函数,将其改为下方模板函数时,只需要将数据类型抽象出来,使用 template<class name> 代替原先的变量,即可实现模板功能,函数中的类型相应地也要改为 name 。
该函数已经能够实现对不同的基本数据类型进行求和,若进一步思考,希望用该函数能够实现对链表、集合等元素的求和,这即是 STL 的思维方式。
另外,用于模板时,class 关键词可以用 typename 代替,即 template<typename name> 。
2.编写动态数组的模板类,体会 STL vector 的编写思想。
#include <iostream>
using namespace std;
template <class T>
//一个简单的模板类,只有构造函数、析构函数、添加元素函数,但已经能够说明模板类的特征了。
class MyArray
{
private:
int m_nTotalSize; //数组总长度
int m_nValidSize; //数组有效长度,即当前元素数
T * m_pData; //数据指针
public:
//构造函数
MyArray(int nSize=2) //假设默认数组长度为2
{
m_pData=new T[nSize];
m_nTotalSize=nSize;
m_nValidSize=0;
}
//获取数组有效长度
int GetValidSize()
{
return m_nValidSize;
}
//获取数组总长度
int GetTotalSize()
{
return m_nTotalSize;
}
//返回某一位置元素
T Get(int pos)
{
return m_pData[pos];
}
//添加一个元素
void Add(T value)
{
if(m_nValidSize<m_nTotalSize) //若数组未满
{
m_pData[m_nValidSize]=value;
m_nValidSize++;
}
else //数组满时动态增加数组大小
{
T * pOldData=m_pData; //保存当前数据指针
m_pData=new T[m_nTotalSize*2]; //原先数组空间大小扩大两倍
for(int i=0;i<m_nTotalSize;i++) //拷贝原先数据
{
m_pData[i]=pOldData[i];
}
m_nTotalSize*=2; //当前数组总长度更新
delete pOldData; //释放旧数组占用的内存
m_pData[m_nValidSize]=value; //添加新元素
m_nValidSize++; //更新数组有效程度
}
}
//析构函数
virtual ~MyArray()
{
if(m_pData!=NULL)
{
delete []m_pData;
m_pData=NULL;
}
}
};
int main()
{
MyArray<int> array1;
cout<<"数组总长度:"<<array1.GetTotalSize()<<" 数组有效长度:"<<array1.GetValidSize()<<endl;
array1.Add(1);
array1.Add(2);
array1.Add(3);
cout<<"数组总长度:"<<array1.GetTotalSize()<<" 数组有效长度:"<<array1.GetValidSize()<<endl;
cout<<array1.Get(0)<<" "<<array1.Get(1)<<" "<<array1.Get(2)<<endl;
MyArray<double> array2;
cout<<"数组总长度:"<<array2.GetTotalSize()<<" 数组有效长度:"<<array2.GetValidSize()<<endl;
array2.Add(1.1);
array2.Add(2.2);
array2.Add(3.3);
cout<<"数组总长度:"<<array2.GetTotalSize()<<" 数组有效长度:"<<array2.GetValidSize()<<endl;
cout<<array2.Get(0)<<" "<<array2.Get(1)<<" "<<array2.Get(2)<<endl;
return 0;
}
该模板类模拟了一个简化的类似 vector 的动态数组,并实现了添加元素功能。可以看到该数组类默认数组大小是2,当插入3个元素之后,数组大小动态增加为原来的两倍,这也是 vector 中的实现方法。虽然实现了数组动态大小,但是需要将数组的原先数据做一份拷贝,因此此时会有效率上的损失。
程序中演示了对于 int 型和 double 型类的使用,说明了模板函数抽象的意义所在,即能实现代码的重用。
本类能体现出 STL 容器关于内存"内存分配、销毁、再分配"的思想,也就是把内存管理的部分进一步抽象,编程系统代码,应用方不必明了过程中的内存变化,用专家级编写的代码,而不是自己编写的代码来管理内存。STL 是编写普通模板类发展的必然结果,不是一种新技术。
3.traits技术
STL标准模板库非常强调软件的复用,traits 技术是重要的手段。traits 的中文意思就是特性,traits 就像特性萃取机,提取不同类的特性,以便能够统一处理,traits 依靠显式模板特殊化来把代码中因类型不同而发生变化的片段拖出来,用统一的接口来包装。这个接口可以包含 C++ 类所能包含的任何东西,如内嵌类型、成员函数、成员变量。作为客户的模板代码,可以通过 traits 模板类所公开的接口来间接访问。下面通过一个简单的例子加以理解。已知整形数组类 IntArray 与浮点型数组 FloatArray ,求数组和与一个数的乘积。
按普通方式编写的代码如下:(红色为两个数组类的区别)
#include <iostream>
using namespace std;
class IntArray
{
private:
int a[10];
public:
IntArray()
{
for(int i=0;i<10;i++)
{
a[i]=i+1;
}
}
//该函数输出所有元素的和与times的乘积
int Calculate(int times)
{
int sum=0;
for(int i=0;i<10;i++)
{
sum+=a[i];
}
return sum * times;
}
};
class FloatArray
{
private:
float a[10];
public:
FloatArray()
{
for(int i=0;i<10;i++)
{
a[i]=i+1;
}
}
//该函数输出所有元素的和与times的乘积
float Calculate(float times)
{
int sum=0;
for(int i=0;i<10;i++)
{
sum+=a[i];
}
return sum * times;
}
};
int main()
{
IntArray a;
FloatArray f;
cout<<"整形数组和的 2 倍是:"<<a.Calculate(2)<<endl;
cout<<"浮点型数组和的 2.2倍是: "<<f.Calculate(2.1)<<endl;
}
可以发现,两个数组类的 Calculate 函数出了参数类型、返回值类型不同外,其余都一样,那么能否通过一个类的接口函数来完成上述功能呢?可以,当然要用到模板。这里要增加一个类 Array 。
template<class T>
class Array
{
public:
float Calculate(T &t,float times)
{
return t.Calculate(times);
}
};
并且主函数需要改变:
int main()
{
IntArray a;
FloatArray f;
Array<IntArray> A1;
Array<FloatArray> A2;
cout<<"整形数组和的 2 倍是:"<<A1.Calculate(a,2)<<endl;
cout<<"浮点型数组和的 2.2倍是: "<<A2.Calculate(f,2.1)<<endl;
}
这里使用 Array 接口函数实现对整形数组和浮点型数组类的操作。但是仔细分析一下就能发现,细节上还是有问题的。比如在 IntArray 中的 Calculate() 函数的参数和返回值都是 int 类型,FloatArray 中的 Calculate() 函数的参数和返回值都是 float 类型,而模板类 Array 中将 Calculate() 函数的参数和返回值都固定为了 float 类型,虽然从程序的结果来看似乎是正确的,但是并不够严密,当程序复杂点时极有可能出错,那么如何解决输出、输出参数类型的不同呢?traits 技术就是很好的解决方法,步骤与如下所示。
步骤一:定义基本模板类
template<class T>class NumTraits
{
};
NumTraits 类可以什么都不写,只是说明它是一个模板类。
步骤二:模板特化。
template<>class NumTraits<IntArray>
{
public:
typedef int inputType;
typedef int resultType;
};
template<>
class NumTraits<FloatArray>
{
public:
typedef int inputType;
typedef int resultType;
};
可以看出相应模板特化类中只是用了 typedef 重定义函数,将 IntArray 和 FloatArray 两个数组类中 Calculate() 函数的参数类型、返回值类型重新定义成 inputType 和 resultType,为编写模板类共同的调用接口做准备。
步骤三:统一模板调用类编写
template<class T>class Array
{
public:
typename NumTraits<T>::resultType Calculate(T &t,typename NumTraits<T>::inputType times)
{
return t.Calculate(times);
}
};
这里 typename 关键字的作用是告诉编译器 NumTraits<T>::resultType 和 NumTraits<T>::inputType 是一个类型而不是变量。
乍一看 Array 类的 Calculate() 函数有些难懂。当模板参数代表 IntArray 时,该定义变为如下代码:
typename NumTraits<IntArray>::resultType Calculate(T &t,typename NumTraits<IntArray>::inputType times)
根据(2)中模板特化定义,NumTraits<IntArray>::resultType 代表 int,NumTraits<IntArray>::inputType 也代表 int ,于是上述定义就变为 int Calculate(T &t,int times)。当模板参数代表 FloatType 时情况类似这里就不做赘述了。
因此 Array 类中的 Calculate() 函数的参数类型和返回值类型是可变的,随着模板参数的不同而不同。因此在模板特化类中给输入、输出参数进行 typedef 重定义非常重要,而且起的对应名称还要相同。
最后,Array 类中的 Calculate() 函数定义看起来很繁琐,这里再次采用 typedef 定义使其清晰:
template<class T>
class Array
{
public:
typedef typename NumTraits<T>::resultType result;
typedef typename NumTraits<T>::inputType input;
result Calculate(T &t,input times)
{
return t.Calculate(times);
}
};
最后给上完整代码:
#include <iostream>
using namespace std;
class IntArray
{
private:
int a[10];
public:
IntArray()
{
for(int i=0;i<10;i++)
{
a[i]=i+1;
}
}
//该函数输出所有元素的和与times的乘积
int Calculate(int times)
{
int sum=0;
for(int i=0;i<10;i++)
{
sum+=a[i];
}
return sum * times;
}
};
class FloatArray
{
private:
float a[10];
public:
FloatArray()
{
for(int i=0;i<10;i++)
{
a[i]=i+1;
}
}
//该函数输出所有元素的和与times的乘积
float Calculate(float times)
{
int sum=0;
for(int i=0;i<10;i++)
{
sum+=a[i];
}
return sum * times;
}
};
//基本模板类
template<class T>
class NumTraits
{
};
//模板特化
template<>
class NumTraits<IntArray>
{
public:
typedef int inputType;
typedef int resultType;
};
//模板特化
template<>
class NumTraits<FloatArray>
{
public:
typedef float inputType;
typedef float resultType;
};
//统一模板调用类编写
template<class T>
class Array
{
public:
typedef typename NumTraits<T>::resultType result;
typedef typename NumTraits<T>::inputType input;
result Calculate(T &t,input times)
{
return t.Calculate(times);
}
};
int main()
{
IntArray a;
FloatArray f;
Array<IntArray> A1;
Array<FloatArray> A2;
cout<<"整形数组和的 2 倍是:"<<A1.Calculate(a,2)<<endl;
cout<<"浮点型数组和的 2.2倍是: "<<A2.Calculate(f,2.1)<<endl;
return 0;
}
4.模板与操作符重载
例如有关于大小比较的模板函数时,有下列代码:
#include <iostream>
using namespace std;
template <class U,class V>
bool Grater(U const &u,V const &v)
{
return u>v;
}
int main()
{
cout<<Grater(2,1)<<endl;
cout<<Grater(2.2,1.1)<<endl;
cout<<Grater('a',10)<<endl;
return 0;
}
该模板函数在比较基本类型时完全正确,但是当 U 或者 V 中有一个表示类时,无法比较,此时就需要重载操作符。
假设有一个学生类:
class Student
{
private:
char name[20];
int grade;
public:
Student(char name[],int grade)
{
strcpy(this->name,name);
this->grade=grade;
}
bool operator>(const int &value)const
{
return this->grade>value;
}
};
需要比较他的成绩是否大于99分,使用 Grater(s,99) 时,需要重载 Student 类的 ">" 操作符,代码如下所示:
bool operator>(const int &value)const
{
return this->grade>value;
}
现在已经可以将 Student 类与 int 型进行比较了。
完整的模板与重载操作符函数如下:
#include <iostream>
#include <string.h>
using namespace std;
class Student
{
private:
char name[20];
int grade;
public:
Student(char name[],int grade)
{
strcpy(this->name,name);
this->grade=grade;
}
bool operator>(const int &value)const
{
return this->grade>value;
}
};
template <class U,class V>
bool Grater(U const &u,V const &v)
{
return u>v;
}
int main()
{
Student s("Raito",100);
cout<<Grater(s,99)<<endl;
return 0;
}
由于 STL 中有大量的模板函数,因此很多时候都要重载与之对应的操作符。模板函数相当于已经编写好的应用框架,操作符重载相当于调用的接口。