本章节主要记录针对C++类模板相关的编程技术知识,以方便随时回顾预览。如有涉及到其它问题,请随时联系进行处理。
一、概述
1.1 前因说明
继承(公有、私有或保护)和包含并不总是能够满足重用代码的需要。
举例,前面几节提到的Stack类设计只用于存储unsigned long值。也可以定义专门用于存储double值或string对象的Stack类,但是除了存储的对象类型不同之外,这两种Stack类的代码是相同的。
针对重用代码的方式,Stack类示例代码中使用typedef处理这种需求,然而这种方法有两个缺点:
- 1、 首先,每次修改类型时都需要编辑头文件
- 2、 其次,在每个程序中只能使用这种技术生成一种栈,即不能让typedef同时代表两种不同的类型,比方不能在同一个程序中同时定义int栈和string栈。
1.2 解决方式
针对上述代码相同,但存储类型不同的情况,与其编写新的类声明,不如编写一个 泛型(即独立于类型的)栈,然后将 具体的类型作为参数传递给这个类。这样就可以使用通用的代码生成存储不同类型值的栈。
1.3 类模板简述
C++的类模板为生成通用的类声明提供了一种更好的方法。
模板提供 参数化(parameterized)类型,即能够将类型名作为参数传递给接收方来建立类或函数。例如,将类型名int传递给Queue模板,可以让编译器构造一个对int进行排队的Queue类。
二、定义类模板
2.1 Stack类非模板类示例代码
以下示例代码是Stack类原来的示例代码,后面的将会在Stack类的基础上来建立模板。如下所示:
typedef unsigned long Item;
class Stack
{
private:
/* constant specific to class */
// 类特定常量
enum { MAX = 10 };
/* holds stack items*/
Item items[MAX];
/* index for top stack item */
int top;
public:
Stack();
bool isempty() const;
bool isfull() const;
// push() returns false if stack already is full, true otherwise
// add item to stack
bool push(const Item& item);
// pop() returns false if stack already is empty, true otherwise
// pop top into item
bool pop(Item& item);
};
2.2 类模板声明规则
同模板函数一样,模板类采用下面的代码开头,模板开头方式说明:
// 旧方式
// 1、这里使用class并不意味着Type必须是一个类
// 2、而只是表明Type是一个通用的类型说明符,在使用模板时,将使用实际的类型替换它
template <class Type>
// 新方式
// 3、较新的C++实现允许使用不太容易混淆的关键字typename代替class
template <typename Type>
- 1、 关键字template告诉编译器,将要定义一个模板。
- 2、 尖括号中的内容相当于函数的参数列表.
- 3、 可以把关键字class或关键字typename看作是变量的类型名,可以把Type看做是变量的名称,Type变量接受类型作为其值。
- 4、 可以使用个人定义的泛型名代替Type,其命名规则与其它标识符相同。当前流行的选项包括T和Type。
2.3 类模板声明说明
(1) 当模板被调用时,Type将被具体的类型值(如int或string)取代。
(2) 在模板定义中,可以使用泛型名来标识要存储在栈中的类型。对于Stack类来说,这意味着应将声明中所有的typedef标识符Item替换为Type。如下示例代码:
Item items[MAX];
// 更改为
Type items[MAX];
(3) 在模板类定义中,同样可以使用模板成员函数替换原有类的类方法。每个模板成员函数头都将以相同的模板声明打头,也将类限定符从Stack::改为Stack::,如下所示:
template <typename Type>
bool Stack<Type>::push(const Type& item)
{
}
(4) 如果在类声明中定义了方法(内联定义),则可以省略模板前缀和类限定符
2.4 类模板之Stack类
下面是Stack模板类,具体如下所示:
template <typename T>
class Stack
{
private:
enum {
MAX = 10,
};
T items[MAX];
int top;
public:
Stack();
bool isempty();
bool isfull();
bool push(const T& item);
bool pop(T& item);
};
template <typename T>
Stack<T>::Stack()
{
top = 0;
}
template <typename T>
bool Stack<T>::isempty()
{
return top == 0;
}
template <typename T>
bool Stack<T>::isfull()
{
return top == MAX;
}
template <typename T>
bool Stack<T>::push(const T& item)
{
if (top < MAX) {
items[top++] = item;
return true;
} else {
return false;
}
}
template <typename T>
bool Stack<T>::pop(T &item)
{
if (top > 0) {
item = items[--top];
return true;
} else {
return false;
}
}
2.4.1 Stack模板类说明
(1) Stack类中列出了类模板和函数模板,明白这些模板不是类和成员函数定义至关重要。它们是C++编译器指令,说明了如何生成类和成员函数定义。
(2) 模板的具体实现:如用来处理string对象的栈类,被称为 实例化(instantiation) 或 具体化(specialization)
(3) 不能将模板成员函数放在独立的实现文件(源文件)中(注:C++标准中最开始支持export关键字,可以使得模板成员函数实现放在独立的实现文件中,但现在C++11之后不再使用这样的关键字,而是将export关键字用于其它用途)
(4) 由于模板不是函数,不能单独被编译,模板必须与特定的模板实例化请求一起使用。
(5) 简单的方法是:将所有模板信息放在一个头文件中,并在要使用这些模板的文件中包含该头文件。
2.4.2 模板类使用
(1) 仅在程序包含模板并不能生成模板类,而必须请求实例化。为此,需要声明一个类型为模板类的对象,方法是使用所需的具体类型替换泛型名。如下示例所示:
// 用于存储int类型对象
// 类声明Stack<Type>将使用int替换模板中所有的Type
Stack<int> kernels;
// 用于存储string类型对象
// 类声明Stack<Type>将使用string替换模板中所有的Type
Stack<string> colonels;
(2) 看到(1)小节的实例声明后,编译器将按照Stack模板来生成两个独立的类声明和两组独立的类方法。
(3) 类声明Stack将使用int或string替换模板中所有的Type,使用的算法必须与类型一致。比如,Stack类假设可以将一个对象赋值给另一个对象,这种假设对于基本类型、结构体和类来说是成立的(除非将赋值运算符设置成私有的),这种假设对于数组则不成立。
(4) 泛型标识符: 例如这里的T,称为类型参数(type parameter),这意味着它们类似于变量,但是赋值给它们的不能是数值,而只能是类型。如Stack Kernels中,类型参数T的值为int。
注意:
必须显式的提供所需的类型,这与常规的函数模板是不同的,因为编译器可以根据函数的参数类型来确定要生成哪种函数。
以下示例代码是对Stack类模板的使用:
#incluce <iostream>
#include <string>
#include <cctype>
int main()
{
Stack<std::string> st;
char ch;
std::string po;
std::cout << "Please enter A to add a purchase order,\n"
<< "p to process a PO, or Q to quit.\n";
// std::toupper(int c):
// 描述:把小写字母转换为大写字母
// 返回:如果参数有相对应的大写字母,则该函数返回参数的大写字母,否则保持不变。返回值是一个可被隐式转换为 char 类型的 int 值。
while (std::cin >> ch && std::toupper(ch) != 'Q') {
while (std::cin.get() != '\n')
continue;
// std::isalpha(int c):
// 描述:检查字符是否是字母
// 返回:如果参数是字符,它返回一个不等于零的值;否则,返回0.
if (!std::isalpha(ch)) {
std::cout << '\a';
continue;
}
switch (ch) {
case 'A':
case 'a': {
std::cout << "Enter a PO number to add: ";
std::cin >> po;
if (st.isfull()) {
std::cout << "stack already full\n";
} else {
st.push(po);
}
}
break;
case 'P':
case 'p': {
if (st.isempty()) {
std::cout << "stack already empty\n";
} else {
st.pop(po);
std::cout << "PO #" << po << " popped\n";
}
}
break;
}
std::cout << "Please enter A to add a purchase order,\n"
<< "p to process a PO, or Q to quit.\n";
}
return 0;
}
三、深入探究类模板
3.1 指针栈
问题: 可以将内置类型或自定义类型用作类模板Stack类型。那指针可以吗?
答案: 是可以创建指针栈,但如果不对前面声明的Stack类模板做重大修改,则将无法正常工作。
3.1.1 不正确地使用指针栈
简述前面声明的Stack类模板不适用指针栈的原因,将通过3个修改后可以使用指针栈的简单示例(是有缺陷的)说明:
示例1:
将Stack类模板使用示例中的std::string po替换为char* po
Stack<std::string> st;
char ch;
std::string po;
std::cout << "Please enter A to add a purchase order,\n"
<< "p to process a PO, or Q to quit.\n";
// std::string po替换为char* po
char* po;
不正确处说明:
<1>: 此处仅仅声明了指针,但是没有申请用于保存输入字符串的内存空间
<2>: 程序将通过编译,但是程序运行时在cin试图将输入保存在某些不合适的内存单元中时崩溃。
示例2:
Stack<std::string> st;
char ch;
std::string po;
std::cout << "Please enter A to add a purchase order,\n"
<< "p to process a PO, or Q to quit.\n";
// std::string po替换为char* po
char po[50];
不正确处说明:
<1>: 此处为输入的字符创分配了空间。po的类型为char*,也可以被放在栈中。但是数组完全与Stack类模板的pop()函数设计冲突。
template <typename T>
bool Stack<T>::pop(T &item)
{
if (top > 0) {
item = items[--top];
return true;
} else {
return false;
}
}
<2>: 在pop函数实现中,引用变量item必须引用某种类型的左值,而不是数组名。
<3>: 原设计代码假设可以给item赋值。但是替换成char数组后,该假设赋值方式就错误了,因为不能为数组名赋值。
示例3:
Stack<std::string> st;
char ch;
std::string po;
std::cout << "Please enter A to add a purchase order,\n"
<< "p to process a PO, or Q to quit.\n";
// std::string po替换为char* po
char* po = new char[50];
不正确处说明:
<1>: 此方式虽然为po申请了内存空间,但是只有一个po变量,该变量总是指向相同的内存单元。
<2>: 当读取新字符串时,内存的内容都将发生变化,但是每次执行压入操作时,加入到栈中的地址都相同。
<3>: 当对栈执行弹出操作时,得到的地址总是相同的,它总是指向读入的最后一个字符创。
3.1.2正确使用指针栈
使用指针栈的方法之一: 让程序提供一个指针数组,其中每个指针都指向不同的字符串。
需要注意之处:
<1>: 创建不同指针是调用程序的职责,而不是栈的职责。栈的任务是管理指针,而不是创建指针。
// 说明1:
// 1、本StackDynamic类模板设计栈大小可变,在内部使用动态数组。设计方式是构造函数中接受一个设定数组大小的默认参数
template<typename T>
class StackDynamic {
private:
enum {
SIZE = 10,
};
int stacksize;
T *items;
int top;
public:
explicit StackDynamic(int ss = SIZE);
StackDynamic(const StackDynamic &sd);
~StackDynamic() {
delete[] items;
}
bool isempty() {
return top == 0;
}
bool isfull() {
return top == stacksize;
}
bool push(const T &item);
bool pop(T &item);
// 说明2:
// 1、将赋值运算符的返回类型声明为StackDynamic,而实际的模板函数定义中将返回类型声明为StackDynamic<T>
// 2、StackDynamic是StackDynamic<T>的缩写,但只能在类中使用。
// 3、可以在模板声明或模板函数定义内使用StackDynamic缩写。但是在类外,指定返回类型或使用作用域解析运算符时,必须使用完整的StackDynamic<T>
StackDynamic &operator=(const StackDynamic &sd);
};
template<typename T>
StackDynamic<T>::StackDynamic(int ss)
: stacksize(ss), top(0) {
items = new T[stacksize];
}
template<typename T>
StackDynamic<T>::StackDynamic(const StackDynamic &sd) {
stacksize = sd.stacksize;
top = sd.top;
items = new T[stacksize];
for (int i = 0; i < stacksize; ++i) {
items[i] = sd.items[i];
}
}
template<typename T>
bool StackDynamic<T>::push(const T &item) {
if (top < stacksize) {
items[top++] = item;
return true;
} else {
return false;
}
}
template<typename T>
bool StackDynamic<T>::pop(T &item) {
if (top > 0) {
item = items[--top];
return true;
} else {
return false;
}
}
template<typename T>
StackDynamic<T> &StackDynamic<T>::operator=(const StackDynamic<T> &sd) {
if (this == sd)
return this;
delete[] items;
stacksize = sd.stacksize;
top = sd.top;
items = new T[stacksize];
for (int i = 0; i < stacksize; ++i) {
items[i] = sd.items[i];
}
return *this;
}
指针栈的使用示例:
#include <iostream>
#include <cstdlib>
#include <ctime>
const int NUM = 10;
int main
{
std::srand(std::time(0));
std::cout << "Please enter stack size: ";
int stacksize;
std::cin >> stacksize;
// 根据输入的stacksize大小创建一个空的StackDynamic对象
StackDynamic<const char*> sd(stacksize);
// 输入
// 指针数组被初始化为一组字符串常量
const char* in[NUM] = {
" 1: Hank Gilgamesh", " 2: Kiki Ishtar",
" 3: Betty Rocker", " 4: Ian Flagranti",
" 5: Wolfgang Kibble", " 6: Portia Koop",
" 7: Joy Almondo", " 8: Xaverie Paprika",
" 9: Juan Moore", " 10: Misha Mache"
};
// 输出
const char* out[NUM];
int processed = 0;
int nextin = 0;
while (processed < NUM) {
if (sd.isempty()) {
sd.push(in[nextin++]);
} else if (sd.isfull()) {
sd.pop(out[processed++]);
} else if (std::rand() % 2 && nextin < NUM) {\
// 使用rand()、srand()、time()来生成随机数,此处是随机生成0或1
sd.push(in[nextin++]);
} else {
sd.pop(out[processed++]);
}
}
for (int i = 0; i < NUM; ++i) {
std::cout << out[i] << std::endl;
}
}
3.2 数组模板示例和非类型参数
模板常用作容器类,这是因为类型参数的概念非常适用于将相同的存储方案用于不同的类型。下面将深入探究非类型(或表达式)参数,以及如何使用数组来处理继承族。
允许指定数组大小的简单数组模板的几种方法:
<1>: 在类中使用动态数组和构造函数参数来提供元素数目,3.1.2章节的StackDynamic类模板就是这种设计。
<2>: 使用模板参数来提供常规数组的大小,C++11新增的模板array就是这种方式。
以下是使用模板参数来设定数组大小的示例:
// 说明
<typename T, int n>
class ArrayTP {
private:
T ar[n];
public:
ArrayTP() {}
explicit ArrayTP(const T &v);
virtual T &operator[](int i);
virtual T operator[](int i) const;
};
template<typename T, int n>
ArrayTP<T, n>::ArrayTP(const T &v) {
for (int i = 0; i < n; ++i) {
ar[i] = v;
}
}
template<typename T, int n>
T &ArrayTP<T, n>::operator[](int i) {
if (i < 0 || i >= n) {
std::cerr << "Error in array limits: "
<< i
<< " is out of range.\n";
std::exit(EXIT_FAILURE);
}
return ar[i];
}
template<typename T, int n>
T ArrayTP<T, n>::operator[](int i) const {
if (i < 0 || i >= n) {
std::cerr << "Error in array limits: "
<< i
<< " is out of range.\n";
std::exit(EXIT_FAILURE);
}
return ar[i];
}
程序说明:
<1>: 类模板开头的<typename T, int n>,typename指出T为类型参数;int指出n为int类型参数,这种参数(指定特殊的类型而不是用作泛型名)称为非类型(non-type)或表达式(expression)参数。
<2>: 根据使用示例说明,如下所示,并对这行代码进行说明:
ArrayTP<double, 12> eggweights;
- 编译器定义名为ArrayTP<double, 12>的类
- 并创建一个类型为ArrayTP<double, 12>的eggweights对象
- 定义类时,编译器将使用double替换T,使用12替换n
表达式参数限制:
<1>: 表达式参数可以是整型、枚举、引用或指针。因此,double m不合法,而double* pm是合法的。
<2>: 模板代码不能修改参数的值,也不能使用参数的地址。因此,不能在ArrayTP中使用n++或&n等表达式。
<3>: 实例化模板时,用作表达式参数的值必须是常量表达式。
使用模板参数来提供常规数组的大小的优缺点:
优点:
<1>: 构造函数方法使用的是通过new和delete管理的堆内存,而表达式参数方法使用的是为自动变量维护的内存栈
<2>: 因此,表达式参数方法执行速度将更快,尤其是在使用了很多小型数组时。
缺点:
<1>: 每种数组大小都将生成自己的模板,如下示例:
// 将生成两个独立的类声明
ArrayTP<double, 12> eggweights;
ArrayTP<double, 13> donuts;
// 只生成一个类声明,并将数组大小信息传递给类的构造函数
StackDynamic<int> eggs(12);
StackDynamic<int> dunkers(13);
<2>: 构造函数方法更通用,这是因为数组大小是作为类成员(而不是硬编码)存储在定义中的。这样可以将一种尺寸的数组赋给另一种尺寸的数组,也可以创建允许数组大小可变的类。
3.3 模板多功能性
可以将用于常规类的技术用于模板类:
(1) 模板类可用作基类
(2) 模板类可用作组件类
(3) 模板类可用作其它模板的类型参数
如下示例所示:
template <typename T>
class Array
{
private:
T entry;
};
template <typename T>
class GrowArray : public Array<T>
{
// do something
...
};
template <typename T>
class Stack
{
Array<T> ar;
};
// 1、C++98要求使用至少一个空白字符将两个>符号分开
// 2、c++11不要求这样
Array< Stack<int> > asi;
3.3.1 递归使用模板
另一个模板多功能性的例子是:可以递归使用模板。如对于前面章节的数组模板定义,可如下示例应用:
// twodee是一个包含10个元素的数组,其中每个元素都是一个包含5个int元素的数组
ArrayTP< ArrayTP<int, 5>, 10 > twodee;
// 与此声明等价的常规数组声明
int twodee[10][5];
说明:
<1>: 在模板语法中,二维的顺序与等价的二维数组相反。下述示例代码详细展示
#include <iostream>
int main()
{
// 创建sums一维数组,保存这10个组的总数
ArrayTP<int, 10> sums;
// 创建sums一维数组,保存这10个组的平均值
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) {
// 以两个字符的宽度显示下一个条目,假设整个数字的宽度不超过两个字符
std::cout.width(2);
std::cout << twodee[i][j] << ' ';
}
std::cout << ": sum = ";
std::cout.width(3);
std::cout << sums[i] << ", average = " << aves[i] << std::endl;
}
return 0;
}
3.3.2 使用多个类型参数
模板可以包含多个类型参数。例如想保存两种值,则可以创建并保存多个值的模板,比如标准模板库中提供的pair类模板。根据下面示例代码详细说明:
template<typename T1, typename T2>
class Pair {
private:
T1 a;
T2 b;
public:
T1 &first();
T2 &second();
T1 first() const { return a; }
T2 second() const { return b; }
Pair(const T1 &aval, const T2 &bval) : a(aval), b(bval) {}
Pair() {}
};
// 返回类型为引用,可通过赋值重新设置存储的值
template<typename T1, typename T2>
T1 &Pair<T1, T2>::first() {
return a;
}
// 返回类型为引用,可通过赋值重新设置存储的值
template<typename T1, typename T2>
T2 &Pair<T1, T2>::second() {
return b;
}
int main()
{
Pair<std::string, int> ratings[4] =
{
Pair<std::string, int>("The Purpled Duck", 5),
Pair<std::string, int>("Jaquie's Frisco A1 Fresco", 4),
Pair<std::string, int>("Cafe Souffle", 5),
Pair<std::string, int>("Bertie's Eats", 3)
};
int joints = sizeof(ratings) / sizeof(Pair<std::string, int >);
std::cout << "Rating:\t Eatery\n";
for (int i = 0; i < joints; ++i) {
std::cout << ratings[i].second() << ":\t" << ratings[i].first() << std::endl;
}
std::cout << "Oops! Revised rating:\n";
ratings[3].first() = "Bertie's Fab Eats";
ratings[3].second() = 6;
std::cout << ratings[3].second() << ":\t" << ratings[3].first() << std::endl;
}
示例代码说明:
<1>: main函数中,必须使用Pair<std::string, int>来调用构造函数,并将它作为sizeof的参数。这是因为类名是Pair<std::string, int>,而不是pair。
<2>: 另外说明下,Pair<char*, double>是另一个完全不同的类的名称。
3.3.3 默认类型模板参数
类模板另一项新特性是:可以为类型参数提供默认值,该默认类型设置为类。如下示例:
template <typename T1, typename T2 = int>
class Topo{
// do something
...
};
示例代码说明:
<1>: 如果省略T2的值,编译器将使用int。如下示例所示:
// T1是double类型,T2是double类型
Topo<double, double> m1;
// T1是double类型,T2是int类型
Topo<double> m2;
<2>: 虽然可以为类模板类型参数提供默认值,但 不能为函数模板参数提供默认值。
<3>: 可以为非类型参数提供默认值,这对于类模板和函数模板都是适用的。