1.定义模板类
下面以第十章的Stack类为基础来建立模板。原来的类声明如下:
typedef unsigned long Item;
class Stack
{
private:
enum { MAX = 10 };
Item* pitems;
int size;
int top;
public:
Stack(int n = MAX);
Stack(const Stack& st);
~Stack();
bool isempty() const;
bool isfull() const;
bool push(const Item& item);
bool pop(Item& item);
Stack& operator=(const Stack& st);
friend ostream& operator<<(ostream& os, const Stack& st);
};
采用模板时,将使用模板定义替换Stack声明,使用模板成员函数替换Stack的成员函数。
template<class Type>
class Stack
{
private:
enum { MAX = 10 };
Type* pitems;
int size;
int top;
public:
Stack( );
bool isempty() const;
bool isfull() const;
bool push(const Type& item);
bool pop(Type& item);
};
template<class Type>
Stack<Type>::Stack()
{
top = 0; //top只有有数据输入才会增加
}
template<class Type>
bool Stack<Type>::isempty() const
{
return (top == 0);
}
template<class Type>
bool Stack<Type>::isfull() const
{
return (top == MAX);
}
template<class Type>
bool Stack<Type>::push(const Type& item)
{
if (top == MAX)
return false;
else
pitems[top++] = item;
return true;
}
template<class Type>
bool Stack<Type>::pop(Type& item)
{
if (top == 0)
return 0;
else
item = pitems[--top];
return true;
}
模板定义:
template<class Type>
class Stack
{
private:
enum { MAX = 10 };
Type* pitems;
int size;
int top;
public:
Stack( );
bool isempty() const;
bool isfull() const;
bool push(const Type& item);
bool pop(Type& item);
};
模板类以第一行template<class Type>开头,class可以替换为typename.也可以使用自己的泛型名代替Type,比如用T。当模板被调用时,Type将被具体的类型值取代。可以使用泛型名标识要存储再栈中的类型。
模板成员函数:
template<class Type>
Stack<Type>::Stack()
{
top = 0; //top只有有数据输入才会增加
}
template<class Type>
bool Stack<Type>::isempty() const
{
return (top == 0);
}
template<class Type>
bool Stack<Type>::isfull() const
{
return (top == MAX);
}
template<class Type>
bool Stack<Type>::push(const Type& item)
{
if (top == MAX)
return false;
else
pitems[top++] = item;
return true;
}
template<class Type>
bool Stack<Type>::pop(Type& item)
{
if (top == 0)
return 0;
else
item = pitems[--top];
return true;
}
每个函数头都将以相同的模板声明开头,同样应该使用泛型名代替typedef标识符。另外,还需将限定符从Stack::改为Stack<Type>:: 。
注意:模板不是类和成员函数定义,是C++编译指令,说明了如何生成类和成员函数的定义。不能将模板成员函数放在独立的实现文件中,因为模板不是函数,模板必须与特定的模板实例化请求一起使用。最简单的办法就是将所有模板信息放在一个头文件中,并在要使用这些模板的文件中包含该头文件。
2.使用模板类
#include <iostream>
#include <cctype>
#include<string>
#include "stack.h"
int main()
{
using namespace std;
Stack<string> st;
char ch;
string po;
cout << "Please enter A to add a purchase order,\n" << "P to process a PO, Q to quit.\n";
while (cin >> ch && toupper(ch) != 'Q')
{
while (cin.get() != '\n')
continue;
if (!isalpha(ch))
{
cout << '\a';
continue;
}
switch (ch)
{
case 'A':
case 'a': cout << "Enter a PO number to add: ";
cin >> po;
if (st.isfull())
cout << "stack already full.\n";
else
st.push(po);
break;
case 'p':
case 'P': if (st.isempty())
cout << "stack already empty.\n";
else
{
st.pop(po);
cout << "PO #" << po << " popped.\n";
}
break;
}
cout << "Please enter A to add a purchase order,\n" << "P to process a PO, or Q to quit.\n";
}
cout << "Bye\n";
return 0;
}
下面的代码生成两个栈:
Stack<string> st;
Stack<int> st2;
注意:泛型标识符成为类型参数,意味着它们类似于变量,但是赋给它们的不能是数字,只能是类型。必须显式地提供所需的类型,这与常规模板不同,常规模板可以根据函数参数类型来确定要生成哪种函数,类模板不能这样。
3.深入探讨模板类
对于上边的示例,我们可以将内置类型或对象用作类模板的类型,指针可以吗?下面解释上边的示例不适合用char *指针的原因,然后介绍一个适合使用栈指针的示例。
3.1不正确的使用指针栈
简要介绍3个试图对上边示例程序修改,使之使用指针栈的简单但有缺陷的示例。这三个示例都已完全正确的栈模板为基础:
1.string po 替换为 char * po;
这种方法很快失败了,因为仅仅创建指针,没有创建用于保存输入字符串的空间。
2.string po替换为 char po [40];
这在pop函数中也是失败的,因为引用变量pitems必须引用某种类型的左值,而不是数组名。其次,假设代码可以给pitems赋值,即使引用变量能够引用数组,也不能为数组名赋值。
template<class Type>
bool Stack<Type>::pop(Type& item)
{
if (top == 0)
return 0;
else
item = pitems[--top];
return true;
}
3.string po替换为 char *po = new char[40];
为字符串分配空间,并且po是变量,这里的最基本的问题是,只有一个po变量,该变量总是指向相同的内存单元。每次读取新字符时,老字符就会被覆盖,并且出栈操作每次弹出的也是相同的地址,没有任何用途。
3.2正确使用指针栈
使用指针栈的方法之一是:让调用程序提供一个指针数组,其中每个数组都指向不同的字符串。栈的任务是管理指针,而不是创建指针。
假设我们要模拟下面的情况。 某人将一车文件夹交给P,如果P的收取篮是空,他将取出最上层文件夹放进去,如果收取篮是满的,他将取出收取篮最上层文件夹,放入发出篮中。如果收取篮不满也不空,则上边两种行为都可能发生。
可以用一个指针数组来模拟这种情况,指针指向表示车中的文件夹,用栈表示收取篮,第二个指针数组表示发出篮。指针从输入数组压入栈表示将文件添加到收取篮,栈中弹出项目,将它添加到发出数组中表示处理文件。
因为用到了动态分配数组,我们需要有析构,复制和等号的重载。
//STACK.H
#include <iostream>
using std::ostream;
template<class Type>
class Stack
{
private:
enum { MAX = 10 };
Type *pitems;
int size;
int top;
public:
explicit Stack(int ss = size);
bool isempty() const;
bool isfull() const;
bool push(const Type& item);
bool pop(Type& item);
Stack(const Stack& st);
~Stack() { delete[] pitems; }
Stack& operator=(const Stack& st);
};
template<class Type>
Stack<Type>::Stack(int ss):size(ss),top(0)
{
pitems = new Type[size];
}
template<class Type>
bool Stack<Type>::isempty() const
{
return (top == 0);
}
template<class Type>
bool Stack<Type>::isfull() const
{
return (top == size);
}
template<class Type>
bool Stack<Type>::push(const Type& item)
{
if (top == size)
return false;
else
pitems[top++] = item;
return true;
}
template<class Type>
bool Stack<Type>::pop(Type& item)
{
if (top == 0)
return 0;
else
item = pitems[--top];
return true;
}
template<class Type>
Stack<Type>::Stack(const Stack& st)
{
size = st.size;
pitems = new Type[size];
top = st.top;
for (int i = 0; i < top; i++)
pitems[i] = st.pitems[i];
}
template<class Type>
Stack<Type>& Stack<Type>::operator=(const Stack& st)
{
if (this == &st)
return &this;
delete[] pitems;
size = st.size;
pitems = new Type[size];
top = st.top;
for (int i = 0; i < top; i++)
pitems[i] = st.pitems[i];
}
对于赋值运算符号的重载,原型声明是将返回类型声明为Stack&,而实际模板函数定义将返回类型定义为 Stack<Type>&,前者是后者的缩写,但只能在类中使用。但在类的外边,就必须用完整的Stack<Type>&。(函数定义用缩写报错,因为类已经出了)
#include <iostream>
#include <cstdlib>
#include<ctime>
#include "stack.h"
const int Num = 10;
int main()
{
using namespace std;
srand(time(0));
cout << "Please enter stack size:";
int size;
cin >> size;
Stack<const char* >st(size);
//In basket
const char* in[Num]
{
"1:HD DUASO","2:Kijfd du9",
"3:Qjfd dis","4:Pfdews fds",
"5:Adjwes dso","6:Bfedo fuedo",
"7:Ffjei fdso","8:Lhjfcs dhsi",
"9:Rjfd fdjs","10:Wfji fds0i"
};
//out basket
const char* out[Num];
int processed = 0;
int nextin = 0;
while (processed < Num) //全部输出
{
if (st.isempty())
st.push(in[nextin++]);
else if (st.isfull())
st.pop(out[processed++]);
else if (rand() & 2 && nextin < Num)
st.push(in[nextin++]);
else
st.pop(out[processed++]);
}
for (int i = 0; i < Num; i++)
cout << out[i] << endl;
cout << "Bye\n";
return 0;
}
字符串本身不会移动,把字符串压入栈本质上是创建一个新指向该字符串的指针,栈弹出字符串把地址值复制到out数组中。
4.数组模板示例和非类型参数
模板常用作容器类,因为类型参数的概念非常适合于将相同的存储方案用于不同的类型。现在来学习非类型参数(表达式参数)以及如何使用数组来处理继承族。
1.首先介绍一个允许指定数组大小的简单模板。有两种方式,第一种是再类中使用动态数组和构造函数参数来提供元素数目(Stack类就是这样的方式),另一种方法是使用模板参数来提供数组的大小(array就是这样做的)。
#ifndef ARRAYTP_H_
#define ARRAYTP_H_
#include <iostream>
#include<cstdlib>
template<class T, int n>
class ArrayTP
{
private:
T ar[n];
public:
ArrayTP();
explicit ArrayTP(const T& v);
virtual const T& operator[](int i) const;
virtual T& operator[](int i);
};
template<class T, int n>
ArrayTP<T, n>::ArrayTP()
{
}
template<class T, int n>
ArrayTP<T, n>::ArrayTP(const T& v)
{
for (int i = 0; i < n; i++)
ar[i] = v;
}
template<class T, int n>
const T& ArrayTP<T, n>::operator[](int i) const
{
if (i < 0 || i >= n)
{
std::cerr << "Error in array limits.";
std::exit(EXIT_FAILURE);
}
return ar[i];
}
template<class T, int n>
T& ArrayTP<T, n>::operator[](int i)
{
if (i < 0 || i >= n)
{
std::cerr << "Error in array limits.";
std::exit(EXIT_FAILURE);
}
return ar[i];
}
#endif
说明:
1.模板头:
template<class T, int n>
关键字class指出T为参数类型,int 指出 n的类型为int。这种参数(指定特殊的类型而不是用作泛型名)称为非类型或表达式参数。表达式参数有一些限制。表达式参数可以是整型、枚举、引用或指针。double m是非法的,double * m是合法的。另外,模板代码不能修改参数的值,也不能使用参数的地址。在模板中不能使用n++或&n这样的操作。另外,实例化模板时,用作表达式参数的值必须是常量表达式。
该方法的优点在于方法使用的是为自动变量维护的内存栈,而不是动态内存,执行速度更快。
缺点是:每种数组大小都将生成自己的模板,内存占用大。
另一个区别是,构造函数方法更通用,因为数组大小是作为类成员存储在定义中。这样可以将一种尺寸的数组赋给另一个尺寸的数组,也可以允许创建允许数组大小可变的类。
5.模板多功能性
可以将用于常规类的技术用于模板类。
模板类可用作基类,也可用作组件类,还可用作其他模板的类型参数。
1.递归使用模板
对于前边的数组模板定义,可以这样使用:
ArrayTP< ArrayTP<int, 5> , 10> twodee;
这使得twodee是一个包含10个数组,其中每个元素都是一个包含5个int元素的数组,常规数组声明如下:int twodee[10][5];维的顺序与等价二维数组相反。
#include<iostream>
#include"arraytp.h"
int main()
{
using std::cout;
using std::cin;
using std::endl;
ArrayTP<int, 10> sums;
ArrayTP<double, 10> aves;
ArrayTP< ArrayTP<int, 5> , 10> twodee;
int i, j;
for (i = 0; i < 10; i++)
{
sums[i] = 0;
for (j = 0; j < 5; j++)
{
twodee[i][j] = (i + 1) * (j + 1);
sums[i] += twodee[i][j];
}
aves[i] = (double)sums[i] / 10;
}
for (i = 0; i < 10; i++)
{
for (j = 0; j < 5; j++)
{
cout.width(2);
cout << twodee[i][j] << " ";
}
cout << ": sum = ";
cout.width(3);
cout << sums[i] << ",average = " << aves[i] << endl;
aves[i] = (double)sums[i] / 10;
}
cout << "Done!";
return 0;
}
2.使用多个类型参数
模板可以包含多个类型参数。例如,假设希望类可以保存两种值,则可以创建并使用Pair模板来保存两个不同的值。
#include<iostream>
#include<string>
template<class T1,class T2>
class pairs
{
private:
T1 a;
T2 b;
public:
pairs(){}
pairs(const T1& n, const T2& m):a(n),b(m){}
T1& first() { return a; };
T2& second() { return b; };
T1 first() const { return a; };
T2 second() const { return b; };
};
int main()
{
using namespace std;
pairs<string, int> rating[4] =
{
pairs<string,int>("The Purpled Duck",5),
pairs<string,int>("Jaquie's Frisco",4),
pairs<string,int>("Cafe Sou",5),
pairs<string,int>("Bertie's Eats",3),
};
int joints = sizeof(rating) / sizeof(pair<string, int>);
cout << "Rating:\tEatery\n";
for (int i = 0; i < joints; i++)
cout << rating[i].second() << ":\t"
<< rating[i].first() << endl;
cout << "Oops!Revised reating:\n";
rating[3].second() = 6;
cout << rating[3].second() << ":\t"
<< rating[3].first() << endl;
return 0;
}
注意:在main()中必须使用pairs<string,int>来调用构造函数,并将它作为sizeof()参数,这是因为类名是 pairs<string,int>,而不是pair。
3.默认类型模板参数
类模板的另一项新特性是,可以为类型参数提供默认值:
template<class T1, class T2 = int> class Topo{...};
这样,如果省略T2的值,编译器将使用int;但不能为函数模板参数提供默认值。可以为非类型参数提供默认值,这对于类模板和函数模板都是适用的。
6.模板的具体化
模板以泛型的方式描述类,而具体化是使用具体的类型生成类声明。具体化可以有隐式实例化,显式实例化和显式具体化。
1.隐式实例化
声明一个或多个对象,指出所需的类型,而编译器使用通过模板提供的处方生成具体的类定义。编译器在没有指定对象前,是不会生成类的隐式实例化的。
2.显式实例化
使用关键字template并指出所需类型来声明类时,编译器将生成类声明的显式实例化。生命必须位于模板定义所在的名称空间中。
3.显式具体化
显式具体化是特定类型(用于替换模板中的泛型)的定义。在需要为特殊类型实例化时,对模板进行修改,使其行为不同,在这种情况下,应该创建显式具体化。具体化格式如下:
template<> class Classname<specialized-type-name>{......};
4.部分具体化
C++还允许部分具体化,即部分限制模板的通用性。部分具体化可以给类型参数之一指定具体的类型:
template<class T1,class T2> class Pair{...};
template<class T1>,class Pair<T1,int>{...};
关键字template后边的<>声明的是没有被具体化的参数,如果指定所有类型,<>内将为空,这将导致显式具体化。
如果有多个模板可供选择,编译器将使用具体化程序最高的模板。
也可也i通过指针提供特殊版本 来部分具体化现有的模板:
template<class T>
class Feeb{};
template<class T*>
class Feeb{}; 提供修改后的代码
如果提供的类型不是指针,编译器将使用通用版本,如果提供的是指针,编译器将使用指针集体化版本,如果没有进行部分具体化版本,当传入指针参数,声明会使用通用模板,指针就是传入类型参数。
部分具体化特性使得能够设置各种限制。
template<class T1, class T2, class T3> class Trio{...};
template<class T1, class T2> class Trio<T1,T2,T2>{...};
template<class T1> class Trio<t1,t1*,t1*>{...};
给定上述声明,编译器将做出不同选择。
Trio<int,short,char *> t1;
Trio<int,> t2;
Trio<int> t3;
7.成员模板
模板可用作结构、类或模板类的成员。
#include<iostream>
using std::cout;
using std::cin;
using std::endl;
template<typename T>
class beta
{
private:
template<typename V>
class hold
{
private:
V val;
public:
hold(V v = 0):val(v){}
void show()const { cout << val << endl; }
V Value() const { return val; }
};
hold<T> q;
hold<int>n;
public:
beta(T t, int i):q(t),n(i){}
template<typename U>
U blab(U u, T t) { return (n.Value() + q.Value()) * u / t; }
void Show() const { q.show(); n.show(); }
};
int main()
{
beta<double> guy(3.5, 3);
cout << "T was set to double\n";
guy.Show();
cout << "V was ste to T, which is double ,then V was set to int:\n";
cout << guy.blab(10, 2.3) << endl;
cout << "U was set to int\n";
cout << guy.blab(10.0, 2.3) << endl;
cout << "U was set to double\n";
return 0;
}
就是可以嵌套。
如果在类外边的定义,在beta模板之外定义模板的方法的代码如下:
template<typename T>
template<typename V>
U beta<T>::blab(U u, T t)
{
return return (n.Value() + q.Value()) * u / t;
}
注意模板的嵌套和声明,是beta<T>的成员。
8.将模板用作参数
模板除了可以包含类型参数 和非类型参数,模板还可以包含本身就是模板的参数。
template <template<typename T> class Thing >
class Crab
{
private:
Thing<int> s1;
Thing<double> s2;
public:
Crab() {};
bool push(int a, double x) { return s1.push(a) && s2.push(x); }
bool pop(int a, double x) { return s1.pop(a) && s2.pop(x); }
};
int main()
{
using std::cout;
using std::cin;
using std::endl;
Crab<Stack> nebula;
}
说明:
1. template<typename T> class 是类型,Thing是参数。
2.Crab<Stack> nebula;为了使上述声明被接受,模板参数Stack必须是一个模板类; bebula声明将用Stack<int> 和 Stack<double>代替Thing<int>和Thing<double>.
除此之外,我们还可以混合使用模板参数和常规参数,例如,Crab类的声明可以像这样打头:
template <template<typename T> class Thing, typename U,typename V >
class Crab
{
private:
Thing<U> s1;
Thing<V> s2;
public:
Crab() {};
bool push(int a, double x) { return s1.push(a) && s2.push(x); }
bool pop(int a, double x) { return s1.pop(a) && s2.pop(x); }
};
现在成员s1和s2可存储的数据类型为泛型,而不是用硬编码指定的类型。模板参数T表示一种模板类型,类型参数U和V表示非模板类型(而不是非类型参数或表达式参数)。
9.模板类和友元
模板类声明也可以有友元。模板的友元分为3类:非模板友元、约束模板友元、非约束模板友元。
1.非模板友元函数
在模板类中将一个常规函数声明为友元,该友元函数称为模板所有实例化的友元。该友元函数本身不是模板函数,而只是使用一个模板作为参数,这意味着必须为要使用的友元定义显式具体化。
#include<iostream>
using std::cout;
using std::endl;
template<typename T>
class HasFriend
{
private:
T item;
static int ct;
public:
HasFriend(const T& i) :item(i) { ct++;}
~HasFriend() { ct--; }
friend void counts();
friend void reports(HasFriend<T>&);
};
template<typename T>
int HasFriend<T>::ct = 0;
void counts()
{
cout << "int count: " << HasFriend<int>::ct << " ;";
cout << "double count: " << HasFriend<double>::ct <<endl;
}
void reports(HasFriend<int>& hf)
{
cout << "HasFriend<int>: " << hf.item << endl;
}
void reports(HasFriend<double>& hf)
{
cout << "HasFriend<double>: " << hf.item << endl;
}
int main()
{
cout << "No objects declared:";
counts();
HasFriend<int> hfi1(10);
cout << "After declared:";
counts();
HasFriend<int> hfi2(20);
cout << "After declared:";
counts();
HasFriend<double> hfdb(10.5);
cout << "After declared:";
counts();
reports(hfi1);
reports(hfi2);
reports(hfdb);
return 0;
}
注意:这里的友元全都具体化了;counts()函数不是通过对象调用的,通过访问全局对象,或全局指针访问非全局对象,可以访问独立于对象的模板类的静态数据成员。
2.模板类的约束模板友元函数
约束类模板友元函数是使友元函数本身称为模板,使得类的每一个具体化都获得与友元匹配的具体化,是所有HasFriend类的友元,调用函数需要指明具体化。这分为3步:1.在类定义前声明每个模板函数;2.在函数中再次将模板声明为友元。这些语句根据类模板参数的类型声明具体化(如果没有参数,必须使用模板参数语法来指明具体化);3.为友元提供模板定义。
#include<iostream>
using std::cout;
using std::endl;
template<typename T> void counts();
template<typename T> void reports(T &);
template<typename T>
class HasFriend
{
private:
T item;
static int ct;
public:
HasFriend(const T& i) :item(i) { ct++; }
~HasFriend() { ct--; }
friend void counts<T>(); //没有参数的显式指明类型 <>表明是个模板
friend void reports<HasFriend<T>>(HasFriend<T>&); //<>编译器可以从参数类型推断出要使用的具体化。使用<>格式也能获得同样的效果
};
template<typename T>
int HasFriend<T>::ct = 0;
template<typename T>
void counts()
{
cout << "template size: " << sizeof(HasFriend<T>) << " ;";
cout << "count: " << HasFriend<T>::ct << endl;
}
template<typename T>
void reports(T& hf)
{
cout << "HasFriend<T>: " << hf.item << endl;
}
int main()
{
cout << "No objects declared:";
counts<int>();
HasFriend<int> hfi1(10);
cout << "After declared:";
counts<int>();
HasFriend<int> hfi2(20);
cout << "After declared:";
counts<int>;
HasFriend<double> hfdb(10.5);
cout << "After declared:";
counts<double>();
reports(hfi1);
reports(hfi2);
reports(hfdb);
return 0;
}
3.模板类的非约束模板友元函数
前一节中的约束模板友元函数是在类外边声明的模板的具体化。通过在类内部声明模板,可以创建非约束友元函数,即每个函数具体化都是每个类具体化的友元。对于非约束友元,友元模板类型参数与模板类类型参数使不同的:
#include<iostream>
using std::cout;
using std::endl;
template<typename T>
class HasFriend
{
private:
T item;
static int ct;
public:
HasFriend(const T& i) :item(i) { ct++; }
~HasFriend() { ct--; }
template<typename C, typename D> friend void show2(C&, D&);
};
template<typename T>
int HasFriend<T>::ct = 0;
template<typename C, typename D> void show2(C& c, D& d) //直接显式两个对象
{
cout << c.item << "," << d.item << endl;
}
int main()
{
cout << "No objects declared:";
HasFriend<int> hfi1(10);
cout << "After declared:";
HasFriend<int> hfi2(20);
cout << "After declared:";
show2(hfi1, hfi2);
HasFriend<double> hfdb(10.5);
cout << "After declared:";
show2(hfi1, hfdb);
return 0;
}
10.模板别名
如果能为类型指定别名,将很方便。
1.使用typedef为模板具体化指定别名:
typedef std::array<double,12> arrd;
arrd gallons;
2.使用模板提供一系列别名:
template<typename T>
using arrtype = std::array<T,12>;
arrtype<double> gallons;
3.C++y允许将语法using = 用于非模板。用于非模板,这种语法与常规typedef等价:
using std::array<double,12> = arrd;
arrd gallons;