第七章 类属和模板

Post on 27-Jan-2016

133 Views

Category:

Documents

6 Downloads

Preview:

Click to see full reader

DESCRIPTION

第七章 类属和模板. 在程序设计中,我们总会发现程序的某些组成模块 所实现的 逻辑功能 是 相同 的,而 不同的 只是被 处理 对 象(数据)的 类型 ,例如下列函数模块: int max( int x, int y) { return (x > y)? x : y; } float max( float x, float y){ return (x > y)? x : y; } double max( double x, double y) { return (x > y)? x : y; } - PowerPoint PPT Presentation

TRANSCRIPT

第七章 类属和模板

在程序设计中,我们总会发现程序的某些组成模块所实现的逻辑功能是相同的,而不同的只是被处理对象(数据)的类型,例如下列函数模块: int max(int x, int y) { return (x > y)? x : y; }

float max(float x, float y) { return (x > y)? x : y; }

double max(double x, double y) { return (x > y)? x : y; }

若能将处理对象(数据)的类型作为参数传递给提供同一逻辑功能的模块,便可以实现用同一模块处理不同类型对象的目的,从而大幅度地提高代码重用度和可维护性。这种将程序模块编写成参数化模板的方法就是类属编程。

在 C++ 中,类属编程有两种实现方式:

⑴ 传统的采用创建类属数据结构的编程方式;

⑵ 采用 C++ 提供的类属工具 —— 参数化模板进行编

程的方式。

本章的重点是模板编程,但对传统类属编程的了解

将有助于对模板的理解。

本章要点1 类属编程

类属编程的必要性,类属表的编制和应用实例。

2 模板编程

模板编程的概念,函数模板与模板函数,类模板与模板类,类模板的派生。

3 利用模板工具实现类属数据容器的实例

栈,队列,数组。

7.1 类属7.1.1 为什麽要引入类属编程 为什麽要引入类属编程呢?可以通过一个实例来说

明。若有一个整数链表,可以将它定义成一个类,此类具有将整数插入链表、在链表中查找指定整数、从链表中删除指定整数等操作,类定义和使用如下:#include <iostream.h>

struct node { // 链表结点的数据结构int val; // 结点值node* next; // 结点链值

};

class intlist // 整数链表类{

node * head; // 链表头指针int size; // 链表中的结点个数

public:

intlist() // 构造函数{ head = 0; size = 0; }

~intlist(); // 析构函数bool insert(int); // 向链表中插入一个结点值bool deletes(int); // 从链表中删除一个结点值bool contains(int); // 判断链表中是否包含指定结点值void print(); // 显示输出链表中所有结点值

};

intlist::~intlist()

{

node* temp; // 定义一个结点型指针用于指向被删结点

for (node* p = head;p;) // 循环删除链表中的所有结点

{

temp = p; // 另时指针指向当前结点

p = p->next; // 修改链表中的当前结点指针

delete temp; // 删除当前结点

}

}

bool intlist::insert(int x)

{

node* nodes = new node; // 创建一个新结点 if (nodes) // 判别新结点是否创建成功 {

nodes->val = x; // 将指定值赋予新结点的值域 nodes->next = head; // 将链表的头指针赋予新结点链域 head = nodes; // 修改链表头指针使之指向新结点 size++; // 链表中的结点个数增 1

return true; // 返回成功标志 }

return false; // 返回失败标志}

bool intlist::deletes(int x)

{

node* temp; // 定义临时指针用于指向被删结点

if (head->val == x) // 判别链头结点值是否等于指定值

{

temp = head->next; // 临时指针指向下一个结点

delete head; // 删除链头结点

size--; // 链表中结点个数减 1

head = temp; // 修改头指针指向下一个结点

return true; // 返回成功标志

}

for (node* p = temp = head; p; temp = p, p = p->next)

{ // 循环查找被删结点位置 if ((p->val == x) && (p != head))

{

temp->next = p->next; // 临时指针指向下一结点delete p; // 删除被查找到的结点size--; // 链表中结点个数减 1

return true; // 返回成功标志 }

}

return false; // 返回失败标志}

bool intlist::contains(int x)

{

for (node* p = head; p; p = p->next) // 循环查找指定结点if (p->val == x) // 判断当前结点值是否等于指定值 return true; // 返回找到标志

return false; // 返回未找到标志}

void intlist::print()

{

for (node* p = head; p; p = p->next) // 顺序链表中的结点cout << p->val << " "; // 显示当前结点值

cout << "\n"; // 显示行结束符}

main()

{

intlist list1; // 创建链表对象 list1

list1.insert(20); // 向链表中顺序插入 20 、 45 、 23 、36

list1.insert(45);

list1.insert(23);

list1.insert(36);

list1.print(); // 显示输出链表内容 list1.deletes(23); // 从链表中顺序删除 23 、 44

list1.deletes(44);

list1.print(); // 显示输出链表内容 return 1;

}

假设又需要设计一个相似的链表,所不同的是新链表要保存的数据是 double 类型的。显然,新链表除了

与整数链表中每一个结点的数据类型不同外,其余均完全相同。但即使如此,实现新链表的程序代码仍需要重写一遍,即在上述程序中所有出现 int 的地方,均

改为 double ,并将类名改为 dublist 。修改如下:struct node

{

double val;

node* next;

};

class dublist

{

node * head;

int size;

public:

dublist() { head = 0; size = 0; }

~dublist();

bool insert(double);

bool deletes(double);

bool contains(double);

void print();

};

在相应的函数定义中,也需要将所有 int 参变量改为

double 类型(具体修改不再重复)。

假如现在又需要定义一个 float 类型链表,则要另建

立一套适应 float 类型的新程序代码。按照这样的解决

方法,需要为每一种使用链表结构保存的预定义或用

户自定义类型对象定义适应相应类型的相同逻辑功能

的不同程序代码。显然,这样的方法不但使程序代码

的重用性差,而且维护起来非常困难。类属编程是解

决这类问题的好方法。

类属的概念来源于 ALGOL68 。在 Ada 语言中,类属

是它的一种最重要的核心编程概念,类属也就是软件

处理数据的类型参数化,它表现了一个软件元素能处

理多种类型数据的能力。类属的概念后来有了进一步

的扩展。

类属可分为无约束类属机制和约束类属机制,其中

无约束类属机制是指对类属参数没有施加任何特殊的

限制,而约束类属机制则意味着类属参数需要一定的

辅助条件。

7.1.2 类属表

与其它抽象数据类型一样,类属表是由一个值的集

合和此集合上的可执行操作关联在一起的数据类型。

不同之处在于该值集合的类型不确定,因此不能被预

先定义,只有用确定类型的数据表导出类属表时,值

集合才被建立。

在 C++ 中,要定义对一个数据表的操作时,必须预

先确定表的值集合,即必须确定值集合的类型。如何

来处理这一矛盾,实现一个类属表定义呢?

⑴ 定义一个类型不预先确定值集合的方法是将值集合

中的每个数据元素定义为字符类型指针,即可以用

动态创建的字节串表示值集合中将要存储的任意类

型数据。

⑵ 由导出类属表的具体数据值集合中的数据类型确定

动态创建字节串的长度。例如,一个整型数占两个

字节,则应该用长度为 2 的字节串来表示集合元素

值;又如一个浮点型数占四个字节,则可以长度为

4 的字节串来表示集合元素值。

⑶ 确定的类型数据与动态创建的字节串的关系是通过 共享相同的内存单元,而使用不同的类型表示和操 作实现的。而字节串长度是通过类属表值集合对象 被创建时所执行的类属表构造函数来传递的。 下面给出一个类属表的具体定义,分析程序的构成

和执行逻辑,可以加深理解类属表的实现机制和意义。程序代码如下:#include <iostream.h>

struct node { // 链表中结点的数据结构 node* next; // 结点的链域 char *contents; // 结点的值域(字符串型指针)};

class list // 类属链表类{

node* head; // 头指针 int size; // 结点值域的字节数public:

list(int s) { head = 0; size = s; } // 构造函数 void insert(char* a); // 将指定值加入到链表头 void append(char* a); // 将指定值加入到链表尾 char* get(); // 从链表头获取结点值 void clear(); // 删除链表中的全部结点 ~list(){ clear(); } // 析构函数};

void list::insert(char* a)

{

node* temp; // 定义临时指针指向插入的新结点 temp = new node; // 为新结点分配内存空间 temp->contents = new char[size]; // 为新结点值域分配内

存if(temp != 0 && temp->contents != 0) // 判定结点是否有效

{

for (int i = 0; i < size; i++) // 以字节方式为新结点值域赋值 temp->contents[i] = a[i];

temp->next = head; // 新结点的链域指向链表头结点 head = temp; // 修改头指针,使之指向新结点

}

}

void list::append(char* a)

{

node *previous, *current, *newnode; // 定义临时指针 newnode = new node; // 为加入的的新结点分配内存 newnode->contents = new char[size];// 新结点值域分配内

存 if(newnode==0 || newnode->contents==0) // 判定结点有效?

return;

newnode->next = 0; // 新结点链域为空(表示尾结点) for (int i = 0; i < size; i++) // 以字节方式为新结点值域赋值

newnode->contents[i] = a[i];

if (head) // 判别链表是否为空 { // 不为空

previous = head;

current = head->next;

while(current != 0)

{ // 查找链尾 ( 循环结束时, previous 指向表的最后结点 )

previous = current;

current = current->next;

}

previous->next=newnode; // 尾结点的链域指向新结点

}

else

{ // 为空 head = newnode; // 加入新结点

}

}

char* list::get()

{

if (head == 0) { cout << "This is a empty list"; return 0; }

else

{ // 不为空char* r; // 定义字符型 指针指向将要返回的数据

值r = new char[size];

// 根据数据类型为返回数据值域分配内存空间node* f = head; // 定义临时指针指向链表头结点for (int i = 0; i < size; i++) // 传递返回值

r[i] = f->contents[i];

head = head->next; // 头指针指向链表头的下一个结点 return f; // 返回所获取的数据

}

}

void list::clear()

{

node* p = head; // 定义临时指针 p 指向链表头结点

while (p != 0) // 判别链表是否为空

{ // 顺序删除链表中的所有结点

node* pp = p; // 定义临时指针 pp 指向被删除的结点

p = p->next; // 使 p 指向被删除结点的下一个结点

delete []pp->contents; // 释放被删除结点的值域内存

delete pp; // 释放被删除结点的内存

}

}

main()

{

list my_list(sizeof(float)); // 创建一个 float 数据类型的链表

// 链表头中顺序加入数据 1.5 、 2.5 、 3.5 和 6.0

float r;

r = 1.5;

my_list.insert((char*)&r);

r = 2.5;

my_list.insert((char*)&r);

r = 3.5;

my_list.insert((char*)&r);

r = 6.0;

my_list.insert((char*)&r);

for (int i = 0; i < 4; i++) // 顺序显示输出链表各个结点的值

{

r = (float)*(float*)my_list.get(); // 类型转换,获得浮点数值

cout << r << '\n';

}

return 1;

}

在本程序中应注意下面几个问题:⑴ 结点的存储分配: 成员函数 insert 和 append 需要为结点 newnode

和结 点的数据值域 newnode->contents 动态分配内存。结 点数据值的大小被保存在私有数据成员 size 中。

⑵ 结点的数据内容的赋值: 根据 size 中的数据长度,逐字节传送。 ⑶ main 函数中类属对象的创建: 创建 list 类属表对象 my_list 时,须使用 sizeof(t

ype)

正确传递结点的数据的字节数,例如: sizeof(float) 。

⑷ 插入结点时的实参传递: 调用成员函数 insert 和 append 时,需要传递 c

har*

类型实参,这就是说,将要插入链表的数据强制转 换成 char 类型,并将数据内存地址作为实参值传递 给被调用的函数,例如: my_list.insert((char*)&r);

⑸ 输出时的类型转换: 将存储在链表结点值域中的 char 类型数据强制转换 成所需要的数据类型后输出,例如: r = (float)*(float*)my_list.get();

7.1.3 从类属表中导出栈和队列

类属表设计好以后,我们就可以利用继承机制由它

派生出一些其他类,例如,可以用它派生整数栈类和

整数队列类。在派生中需注意以下几个问题:

1 栈的特点是先进后出。

⑴ 将进和出的操作放在链表的表头进行;

⑵ 栈中的 push 操作可借用类属表中的 insert 函数;

⑶ 栈中的 pop 操作可借用类属表中的 get 函数。

2 队列的特点是先进先出。 ⑴ 将链表设计成一端插入,另一端取出。例如,链

表尾部插入,链表头部取出; ⑵ 队列的 put 操作可借用类属表中的 append

函数; ⑶ 队列的 get 操作可借用类属表中的 get 函数。3 类型的转换。 因为整数栈和队列实际上均是属性链表,为了使用 方便,在数据插入和提取时,其数据都已经确定是 整数,而不应是类属链表中的字符串;所以在调用 类属链表 list 中的成员函数时,不要忘记进行强制 类型转换。实现代码示意如下:

struct node

{

node* next;

char* contents;

};

class list

{

node* head;

int size;

public:

list(int s){ head = 0; size = s; }

void insert(char* a);

void append(char* a);

char* get();

void clear();

~list(){ clear(); }

};

class int_stack : list

{

public:

int_stack() : list(sizeof(int)) { }

void push(int a)

{

list::insert((char*)&a);

}

int pop()

{

return *((int*)list::get());

}

};

class int_queue : list

{

public:

int_queue() : list(sizeof(int)) { }

void put(int a)

{

list::append((char*)&a);

}

int get()

{

return *((int*)list::get());

}

}; 返回

7.2 模板编程7.2.1 模板的概念 模板是实现类属机制的一种工具,模板的功能非常强,既可以实现无约束类属机制,也可以实现约束类属机制。模板允许用户构造模板函数(类属函数),还允许用户构造模板类(类属类)。下图所示意的是类模板或函数模板、类、对象、函数之间的关系:

函数模板或类模板

模板函数

实例化

模板类

实例化

对象实例化

模板定义的一般形式:template < 模板形参表列 >

模板定义体其中:

① template :定义模板形参说明行的关键字,表示定义 一个模板的开始。② 模板形参表列:由若干模板形参组成的。每个模板 形参均是由关键字 class 和类型形参名组成。③ 模板定义体:函数模板的定义,或类模板的定义。注意,模板形参说明行与模板定义体之间不允许有任何其他语句。

7.2.2 函数模板与模板函数1 什麽是函数模板与模板函数 函数模板的声明格式如下: template <class type>

类型名 函数模板名 ( 参数表列 )

{ 函数模板定义体 }

⑴ type 表示模板形参名的一般形式,与说明一般函数形参名相同,可以是用户命名任何合法标识,只不过说明的是数据类型而不是数据值。

⑵ 函数模板的类型名和参数表列中参数类型名可以

是确定的预定义或自定义类型名,也可以是模板

形参名。

例如,将求两个数的最大值函数定义成函数模板: template <class T>

T max(T x, T y) { return (x > y)? x : y; }

如此定义的函数模板 max 代表的是一类函数。若要 使用函数模板 max 进行求最大值运算,必须首先将 模板形参 T 实例化为确定数据类型(如 int 、 flo

at 、 double 等)。 从这个意义上说:函数模板不是一个完全的函数, 将 T 实例化的对象类型称之为模板实参,实例化后 的函数模板称为模板函数。 一个使用函数模板的完整程序如下所示:

#include <iostream.h>

template <class AT>

AT Max(AT x, AT y) { return (x > y)? x : y; }

void main()

{

int i1 = 10, i2 = 56;

float f1 = 12.5, f2 = 24.5;

double d1 = 50.344, d2 = 4656.346;

char c1 = 'k', c2 = 'n';

cout << "The max of i1,i2 is: " << Max(i1, i2) << endl;

cout << "The max of f1,f2 is: " << Max(f1, f2) << endl;

cout << "The max of d1,d2 is: " << Max(d1, d2) << endl;

cout << "The max of c1,c2 is: " << Max(c1, c2) << endl;

}

主函数中对函数模板 Max 的四次调用: Max(i1,i

2) 、 Max(f1,f2) 、 Max(d1,d2) 、 Max(c1,c2) 分别将模板参数 AT 实例化为 int 、 float 、 double 和 char 。下图给出了 函数模板 Max 和四个模板函数的关系。

函数模板实现了函数参数类型的通用性,作为一种 代码重用机制,可以大幅度地提高程序设计效率和 可维护性。又例如:

函数模板Max(x, y)

模板函数Max(c1, c2)

char实例化

模板函数Max(d1, d2)

double实例化

模板函数Max(f1, f2)

float实例化

模板函数Max(i1, i2)

int实例化

#include <iostream.h>

template <class T>

T sum(T *array, int size = 0)

{

T total = 0;

for (int i = 0; i < size; i++) total += array[i];

return total;

}

int int_array[] = {1,2,3,4,5,6,7,8,9,10};

double double_array[] =

{1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8,9.9,10.10};

main()

{

int itotal = sum(int_array, 10);

double dtotal = sum(double_array, 10);

cout << "The summary of integer array are: "

<< itotal << endl;

cout << "The summary of double array are: "

<< dtotal << endl;

return 1;

}

几点说明: ⑴ 在函数模板中允许使用多个类型参数。但应注意

每个模板形参前必须有关键字 class 。例如: #include <iostream.h>

template <class type1, class type2>

void myfunc(type1 x, type2 y)

{ cout << x << ' ' << y << endl; }

void main()

{

myfunc(10, "hao");

myfunc(0.123, 10L);

}

程序执行结果: 10 hao

0.123 10

⑵ template 语句与函数模板定义语句之间不允许有

任何其他语句,例如:

template <class T>

int i; // 错误,不允许有别的语句

T Max(T x, T y) { return (x > y) ? x : y; }

⑶ 模板函数与重载函数比较:

函数重载需要通过多个函数重载版本来实现,每

个函数版本可以执行不同的操作。

函数模板是通过模板形参实例化为不同类型数据

提供操作的摸板函数版本,但所有模板函数都必

须执行相同的操作逻辑。因此,下面的重载函数

就不能用模板函数代替。

void outdata(int i) { cout << i; }

void outdata(double d) { cout << "d = " << d << endl;

}

2 重载模板函数 虽然函数模板中的模板形参 T 可以实例化为各种类 型,但每次被实例化的各模板实参必须保持完全一 致的类型,否则会发生错误。例如: template <class T>

T Max(T x, T y) { return (x > y) ? x : y; }

void fun(int i, char c)

{

Max(i, i); // 正确Max(c, c); // 正确Max(i, c); // 错误Max(c, i); // 错误

}

分析出现错误的原因是:函数模板被调用时,编译

器按最先遇到的实参类型隐含生成一个模板函数,

然后用该模板函数对以后出现的所有模板实参进行

一致性检查。例如,对语句 Max(i, c) ,编译器先按

照实参 i 将模板形参 T 实例化为 int 类型,隐含生成

模板函数 Max(int, int) ,然后用该模板函数检查此后

出现的模板实参,由于第二个模板实参 c 的类型与

模板函数的第二个参数类型 int 不符,便发生错误。

解决这个问题有两种方法:

⑴ 采用强制类型转换, 例如,将调用语句 Max(i, c);

改写为: Max(i, int(c));

⑵ 重载函数扩充函数模板,这种重载有两种表达式: ① 只声明一个函数的原型,而不给出定义体,它的

定义体是借用函数模板的定义体。当执行此重载

版本时,会自动调用函数模板的定义体。例如,可将上面的程序改写如下:

template <class T>

T Max(T x, T y) { return (x > y) ? x : y; }

int Max(int, int); // 重载函数声明

void fun(int i, char c)

{

Max(i, i);

Max(c, c);

Max(i, c);

Max(c, i);

}

虽然该重载函数借用了函数模板的定义体,但它

支持数据类型之间的隐式转换。例如: Max(i,

c)

和 Max(c, i) 就是通过重载函数 Max(int, int) 完成了

类型的隐式转换,避免了错误发生。注意,多数

C++ 编译器不支持这种方法,例如 Visual C++ 。

② 定义一个完整的重载函数,函数所带的参数类型

可以随意。例如:char* Max(char* x, char* y)

{ return (strcmp(x,y) > 0)? x : y; }

该函数实际上是为上述函数模板所能实例化的模

板函数增加了一个新的重载函数版本,当出现调

用语句 Max(“abcd”, “efgh”); 时,执行的是这个重载

函数。

在 C++ 中,函数模板与同名的重载函数的调用顺序

遵循下述约定:

⑴ 查询一个参数完全匹配的函数,如果找到了,则查 询结束并调用查询到的函数,否则继续查询。

⑵ 查询一个函数模板,将其实例化,产生一个匹配的 模板函数,如果找到了,则查询结束并调用实例化 后的函数模板,否则继续查询。⑶ 若 ⑴ 和 ⑵ 都失败,再尝试对函数的其他的匹配, 例如,通过类型转换实现参数匹配等,如果成功, 则调用匹配成功的函数。如果 ⑴ ⑵ ⑶ 均未找到匹 配的函数,则是一个失配性调用错误。如果在 ⑴ 中匹配多于一个,则导致二义性调用错误。

7.2.3 类模板与模板类1 类模板与模板类的概念 ⑴ 什麽是类模板: 如果有一族类的结构和行为逻辑都是相同的,不

同之处仅在类的全部或部分数据成员的类型不同。则可以定义一个有确定的结构和行为逻辑,而某些数据成员的类型不确定,某些成员函数

的参数、返回 类型不确定的 “类”,即类模板。

与函数模板相同,类模板定义中未确定的数据类

型是由模板形参说明行声明的。只有当模板形参

被确定的类型实参替代,类模板才被实例化一个

完全确定的类。

⑵ 类模板的定义:

定义一个类模板,一般包括两方面的内容:

① 首先要定义类模板,其一般形式为:

template <class T>

class 类模板名

{ 类数据成员和成员函数定义代码 };

在类模板定义中凡采用模板形参类型声明的

数据成员,成员函数参数和返回类型均可以

使用 T , T* 或 T& 等进行类型声明,例如:

template <class T>

class vector

{

T* data;

int size;

public:

vector(int);

T& operator[] (int);

};

② 在类定义体外定义成员函数的实现代码时, 若所定义的成员函数的形参数据、返回

数据 或局部变量数据中包含了模板参数类型,

则 必须在函数体之前进行模板声明,并且

在函 数名所属的类模板名后加缀模板形参名。 例如: template <class T>

vector<T>::vector(int i)

{ … };

template <class T>

T& vector<T>::operator[] (int i)

{ … }

⑶ 类模板的使用:

类模板的使用实际上是将类模板实例化成一个具

体的类或类对象,调用格式如下:

类模板名 < 实参类型 > 实例化对象名 ;

typedef 类模板名 < 实参类型 > 实例化类名 ;

例如:

template <class T>

class vector { … };

main()

{

vector<int> x(5); // 创建一个有 5 个元素的整数向量

for (int i = 0; i < 5; i++)

x[i] = i; // 调用下标运算符为向量元素赋值 for (i = 0; i < 5; i++)

cout << x[i] << " " ; // 显示向量元素值 cout << "\n"

}

在实际应用中,我们可以将向量类模板 vector 实

例化为任何类型的向量对象,例如:vector<complex> v2(30);

vector<myclass> v3(4);

这些实例化的类模板:

vector<int> , vector<complex> 和 vector<myclas

s> 被

称为模板类。模板类是类模板实例化的产物。类

模板和模板类之间的关系如下图所示。类模板

vector<T>

模板类vector<myclass>

myclass实例化

模板类vector<complex>

complex实例化

模板类vector<int>

int实例化

例例 7-17-1 给出一个使用类模板 stack 的完整实例,在

此例中建立了字符型堆栈 cs1 、 cs2 和整型

堆栈 is1 、 is2 。

堆栈类模板 stack 与堆栈 cs1 、 cs2 、 is1 和 is2 之

间的关系如下图所示:

stack-stck[1..*]:Type-tos:int

+init()+pop():Type+push(in ob:Type)

Type

cs2:

<bind>char

is1:

<bind>int

cs1:

<bind>char

is2:

<bind>int

使用类模板的几点说明:

⑴ 在每个类模板定义之前,都需要在前面加上模板形

参说明行, 例如:

template <class Type>

使用类模板时,必须在类模板名字后加缀模板形参

名用于表示类模板,或者加缀模板实参名用于表示

模板类。例如:

stack<Type> 表示一个堆栈类模板

stack<int> 表示一个整型堆栈类

⑵ 模板类可以有多个参数,例如: #include <iostream.h>

template <class T1, class T2>

class myclass

{

T1 i;

T2 j;

public:

myclass(T1 a, T2 b) { i = a; j = b; }

void show() {cout << "i = " << i << " j = " << j << endl;

}

};

main()

{

myclass <int, double> ob1(12, 0.15);

myclass <char, char*> ob2('*', "This is a test");

ob1.show();

ob2.show();

return 1;

}

运行显示如下结果: i = 12 j = 0.15

i = * j = This is a test

⑶ 在使用类模板的源程序中必须包含类模板的完全定 义,即不仅要包含类模板的定义代码,还要包含类 模板的实现代码。因此,如果类模板的完全定义分 写在头文件 .h 和实现文件 .cpp 两个文件中,则在使 用类模板的源程序中就必须包含能体现类模板完全 定义的实现文件 .cpp ,而不能只包含仅体现类模板 定义的 .h 。例如:程序中有一个类模板 vector ,其 定义代码在 vector.h 中,实现代码在 vector.cpp

中, 则在使用类模板 vector 的程序源文件中使用的包含 应该是: #include “vector.cpp” 而不应是 #include "vector.h" 。

2 类模板使用举例例例 7-27-2 定义一个简单队列类模板,并将该模板实例化一

个对自定义职员类信息进行存储和操作的队列。问题分析: 简单队列是最有代表性的一种队列是一种具有 “先 进先出” 操作机制的数据容器。队列中存放的数 据,可以是预定义或自定义的任何类型。 本例中采用了链表队列模板。首先定义一个结构模 板 quenode 作为队列的结点,它与定义类模板方法 类似,再定义一个队列类模板 queue ,结构模板和 类模板取同样的模板形参名 T 。

另外定义了一个职员类 staff ,并将队列模板 que

ue

实例化为 staff 类型的队列 staffque 。

结构模板 quenode 、队列类模板 queue 、职员类 staff

和实例化后的职员信息队列对象 staffque 之间的关

系如下图所示:

quenode+nodedata:T+next:quenode*

Tqueue

#head:quenode<T>#tail:quenode<T>#quesize:int#allocateerror:bool

#copy(in q:queue<T>&):queue<T>&+pop(in x:T):bool+push(in x:T)+isempty()+operator=(in q:queue<T>&):queue<T>&+getallocateerror():bool

T

head

1

tail

1

staff+name:string+sex:string+age:int+salary:float

+assign(in name:string, in age:int, in salary:float, in sex:string)+print()

staffque:

<bind>staff

quedata

1..*

3 类模板的派生 ⑴ 从类模板派生类模板

与类派生相同,类模板也可以派生出新的类模板。注意,基类模板名必须是未实例化的模板名。类模板派生的一般形式:template < 模板形参表列 >

class 派生类模板名: 继承方式 基类模板名 < 模板形参表列

>

{ 派生类模板新增成员定义代码 }; 例如:template <T>

class derived : public base<T>

{ … };

例例 7-47-4 设计一个链表类模板 list ,并从 list 派生一

个集合类模板 set 。⑴ 由于集合和链表的差别在于集合中的元素

不 允许重复。因此插入操作时要判别集合中

是否 已有要插入的元素。因此,链表类模板 l

ist 的 插入操作 insert 应声明为虚函数,并在

集合类 模板 set 中重新定义;

⑵ list 中的其余成员函数均不需要再重新定义。list 和 set 之间的关系如下:

listT

setT

⑵ 从类模板派生模板类 从模板类派生模板类,必须使用类模板的实例作

为派生模板类的基类。由于模板形参已经被确定

的类型实参替代,因此在模板类派生定义之前不

需要模板形参声明。模板类派生的一般形式:class 派生类名

: 派生方式 基类模板名 < 模板实参表列>

{ 派生类新增成员定义代码 }; 例如:class derived : public base<int>

{ … };

例例 7-57-5设计一个链表类模板 list ,并从 list 派生一

个整数集合类 intset 。

返回

listT

intset

<bind>int

7.3 利用模板工具实现类属数据容器的实例7.3.1 栈 栈是一种线性表数据结构,它的特点是:① 操作在线性表的一端按 "先进后出 " ( FILO )的原则 进行。② 栈中的元素可以是任何类型(预定义类型和自定义 类型)。③ 实现线性表的存储结构可以是数组(矩阵),也可 以是链表。

显然,一个通用栈的实现应该在归纳共性的基础上:

① 定义抽象通用栈类模板 genabstractstack 以便实现不

同存储结构的栈的统一操作接口。

② 从抽象栈类模板派生定义针对某种确定存储结构的

通用栈类模板 genstack 以便适应不同类型栈元素的

操作需要。

③ 用通用栈类模板 genstack 定义确定类型的栈类或实

例。例如,栈元素为学生信息类 student 对象的堆栈

实例 studentstack 。

类层次结构图:

genabstractstackT

genstackT

intstack:

<bind>int

studentstack:

<bind>student

student

1..* nodedata

genstackrecT

top

1

1 建立抽象通用栈类模板 genabstractstack

原则:抽象出栈操作的统一接口(方法)规则,其 中与存储结构有关的方法声明纯虚方法。

模板类图描述如下:genabstractstack

#height:unsigned

+isempty():bool+push(in x:T&){abstract,}+pop(out x:T&):bool{abstract}+rollup(){abstract}+rolldown(){abstract}+clear(){abstract}+deque(in x:T&):bool

T

⑴ 属性:① 栈高 height , unsigned 类型变量,该数

据成员 应该被隐藏(不允许类外访问),但应能

在派 生类中被派生类新增成员函数访问,所以

将该 数据成员的访问属性设定为 protected 。

⑵ 操作:① 判空操作 isempty() ,该操作与存储结构无

关, 因此,可以在模板定义中实现具体定义。

返回值: bool 类型, true 表示栈空, false 表示

栈非空。 算法:

isempty()

BEGIN

if ( 栈高为 0 ) then return true

else return false

endif

END

② 压栈操作 push(T&) ,该操作与存储结构有关,

因此在模板定义中不能确定操作实现,故声明

为纯虚函数。 参数:模板形参的引用。

③ 弹栈操作 pop(T&) ,该操作与存储结构有关,

因此在模板定义中不能确定操作实现,故声明

为纯虚函数。

参数:模板形参的引用。

返回值: bool 类型, true 表示成功, false 表示

失败。

④ 上滚操作 rollup() ,该操作与存储结构有关,

因此在模板定义中不能确定操作实现,故声明

为纯虚函数。无参数且无返回。

⑤ 下滚操作 rolldown() ,该操作与存储结构有

关,因此在模板定义中不能确定操作实现,故

声明为纯虚函数。无参数且无返回。

⑥ 清除操作 clear() ,该操作与存储结构有关,因

此在模板定义中不能确定操作实现,故声明

为纯虚函数。无参数且无返回。

⑦ 取栈底元素操作 deque(T&) ,该操作虽然也与

存储结构有关,但可以通过调用成员函数

rolldown 和 pop 完成,可以定义操作实现。

参数:模板形参的引用。

返回值: bool 类型, true 表示成功, false 表示

失败。

算法:

deque([OUT] T& x)

BEGIN

rolldown();

pop(x);

END

2 建立通用栈类模板 genstack

首先确定保存栈元素的存储结构模板 genstackrec , 然后从 genabstractstack 派生 genstack 。 以链表存储结构为例, genstack 除了继承基类中已 经定义的属性和操作外,必须进行以下定义: · 对基类定义的纯虚函数进行实现定义。 · 新增属性 top 用于指示栈顶、 allocateerror 用于表 示存储分配状态。 · 新增成员函数 copy 完成堆栈复制、 getallocateerro

r

获取存储分配状态和运算符 operator = 。 类模板图描述如下:

genstack

#top:genstackrec<T>#allocateerror:bool

-copy(in g:genstack<T>&):genstack<T>&+push(in x:T&){virtual}+pop(out x:T&):bool{virtual}+rollup(){virtual}+rolldown(){virtual}+clear(){virtual}+operator=(in g:genstack<T>&):genstack<T>&+getallocateerror():bool

T

⑴ 属性:① 栈顶指针 top ,存储结构模板 genstackrec

类 型,该数据成员应该被隐藏(不允许类外

访 问),但应能在派生类中被派生类新增成

员函 数访问,所以将该数据成员的访问属性设

定为 protected 。② 结点存储分配标志 allocateerror , bool 类

型, 表示结点的存储空间分配是否成功。该数

据成 员应该被隐藏(不允许类外访问),但应

能在 派生类中被派生类新增成员函数访问,所

以将 该数据成员的访问属性设定为 protected 。

⑵ 操作:

① 栈复制操作 copy(genstack&) ,该操作只用于内

部操作,因此访问属性可以设定为 protected

或 private 。

参数:栈类模板实例的引用。

返回值:栈类模板实例的引用。

算法:

copy([IN] genstack& g)

BEGIN

if ( 目标栈不为空) then clear() endif

初始化目标栈并从源栈复制栈高 if ( 源栈为空) then return 空目标栈 endif

分配新结点作为目标栈栈顶结点 从源栈复制栈顶结点 while ( 未到源栈底)

分配新结点作为目标栈中下一个结点 ;

从源栈复制中下一个结点 ;

endwhile

return 目标栈 ;

END

② 栈类构造函数 genstack() 。

③ 栈类拷贝构造函数 genstack(genstack&) 。

参数:栈类模板实例引用。

算法:

genstack([IN] genstack& g)

BEGIN

目标栈栈顶指针置空 ;

copy(g);

END

④ 获取结点存储分配标志 getallocateerror() 。

返回值: bool 类型, true 分配成功, false 分配

失败。

算法:直接返回存储分配标志 allocateerr

or 。

⑤ 重新定义基类中的纯虚函数 clear() 。

算法:顺序弹出栈中的所有结点。

⑥ 重新定义基类中纯虚函数 push(T& x) 。

参数:模板形参类型引用。

算法:

push ([IN]T& x)

BEGIN

if ( 栈不为空 ) then if (分配新结点失败)

then 置存储分配错误标志并结束操作 else 为新结点赋值并压入堆栈

修改栈顶指针 endif

else if (分配栈顶结点失败) then 置存储分配错误标志并结束操作 else 新结点赋值并使栈顶指针指向新结

点 endif

endif

END

⑦ 定义基类中纯虚函数 pop(T& x) 。 参数:模板形参类型引用。 返回值: bool 类型, true 操作成

功, false 操作失败。

算法: pop([OUT] T& x)

BEGIN

if ( 栈中有元素) then x = 栈顶结点的值

删除栈顶结点并修改相应属性 return true

else return false

endif

END

⑧ 定义基类中纯虚函数 rollup() 。 算法: rollup()

BEGIN

if ( 栈为空或者栈中元素个数 < 2 )then 结束操作else 将栈顶结点从链表中分离

修改栈顶指针指向次栈顶结点 循环找到栈底结点 将分离的栈顶结点连接在栈底之后

endif

END

⑨ 定义基类中纯虚函数 rolldown() 。 算法: rolldown()

BEGIN

if ( 栈为空或者栈中元素个数 < 2 ) then 结束操作 else 循环找到栈底结点

将栈底结点从链表中分离 将分离的栈底结点连接在栈顶之前 修改栈顶指针指向原栈底结点

endif

END

⑩ 重载赋值运算符成员函数 operator= 。

参数:栈类模板实例引用。

返回值:栈类模板实例引用。

算法:调用栈复制成员函数 copy 。 operator=([IN] gestack& g)

BEGIN

copy(g)

return *this

END

3 定义模板实参类 student :

student

+name:string+sex:string+age:int+mark_average:float

+assign(in name:string, in age:int, in mark_average:float, in sex:string)

4 使用栈模板 使用预定义类型或自定义类型将栈类模板中模板参 数变为实参,方法有两种: ⑴ 实例化为模板类,例如,

typedef genstack<int> intstack;

⑵ 实例化为类对象,例如,genstack<int> intstack;

在本例中的使用: ⑴ 用 student 类将栈类模板实例化为栈类对象。 ⑵ 对 student 类栈对象进行各种操作; ⑶ 在一个全程函数 viewstack 中引用 student 类栈对

象,用于顺序显示栈中所有的 student 信息。

7.3.2 队列 队列也是一种线性表,它的特点:① 操作在线性表的两端按 "先进先出 " ( FIFO )的原则 进行;② 根据操作的差别队列可以分为: 简单队列:数据从队尾插入,队头弹出。 双端队列:数据从队头和队尾都可以插入和弹出。 优先队列:数据根据其优先级可以插入到队列的不

同位置。③ 队列中的元素可以是预定义和自定义的任何类型。④ 实现线性表的存储结构可以是数组(矩阵),也可 以是链表。

显然通用队列的实现应该在归纳共性的基础上:① 定义抽象通用队列类模板 abque 以便实现能适用于 不同存储结构的队列的统一操作接口。② 从抽象通用队列类模板派生定义用于某种确定存储 结构的各种通用队列类模板: 简单队列通用类模板 —— queue1 , 双端队列通用类模板 —— queue2 , 优先队列通用类模板 —— queue3 , 以适应不同类型队列种类和队列元素的需要;③ 使用各种队列通用类模板定义确定类型的队列类或 实例。例如,学生信息类 student 对象的队列实例 studentqueue 。

类层次结构图

abqueT

queue1T

queue2T

queue3T

intqueue1:

<bind>int

intqueue2:

<bind>int

intqueue3:

<bind>int

studentqueue1:

<bind>student

studentqueue2:

<bind>student

studentqueue3:

<bind>student

student1..* nodedata

1..*nodedata

1..*nodedata

quenodeT

head1

1 tail

quenodeT

head1

1 tail

quenodeT

head1

1 tail

1 建立抽象通用队列类模板 abque

原则:抽象出队列操作的统一接口(方法)规则, 其中与存储结构有关,并且适用于所有类型队列的 方法声明纯虚方法。模板类图描述如下:

abque

#qsize:int

+isempty():bool+pushfr(in x:T&){virtual}+pushta(in x:T&){abstract}+popfr(out x:T&):bool{abstract}+popta(out x:T&):bool{virtual}+clear(){abstract}

T

2 定义简单队列通用类模板 queue1

首先确定链表结点存储结构模板 quenode ,然后从 abque 派生 queue1 。 以链表存储结构为例,简单队列通用类模板除了继 承基类中已经定义的属性和操作外,必须进行如下 的定义工作: · 对基类定义的纯虚函数进行实现定义。 · 新增属性 head 指示队列头、 tail 指示队列尾和 allocateerror 表示队列元素结点的内存分配状态。 · 新增操作 copy 实现队列复制、 getallocateerror 获 取队列元素结点的内存分配状态、 put 队列插入 操作、 get 队列提取操作和运算符 operator = 。 类模板图描述如下:

queue1

#head:quenode<T>*#tail:quenode<T>*#allocateerror:bool

-copy(in q:queue1<T>&):queue1<T>&+pushta(in x:T&){virtual}+popfr(out x:T&):bool{virtual}+put(in x:T&)+get(out x:T&):bool+clear(){virtual}+operator=(in q:queue1<T>&): queue1<T>&+getallocateerror():bool

T

3 定义双端队列通用类模板 queue2

从抽象通用队列类模板 abque 派生双端队列通用类 模板 queue2 。 以链表存储结构为例,双端队列通用类模板除了继 承基类中已经定义的属性和操作外,必须进行如下 的定义工作: · 对基类中的全部虚函数进行实现和重载定义。 · 新增属性 head 指示队列头、 tail 指示队列尾和 allocateerror 表示队列元素结点的内存分配状态。 · 新增操作 copy 实现队列复制、 getallocateerror 获 取队列元素结点的内存分配状态、 put 队列插入 操作、 get 队列提取操作和运算符 operator = 。 类模板图描述如下:

queue2

#head:quenode<T>*#tail:quenode<T>*#allocateerror:bool

-copy(in q:queue2<T>&):queue2<T>&+pushfr(in x:T&){virtual}+pushta(in x:T&){virtual}+popfr(out x:T&):bool{virtual}+popta(out x:T&):bool{virtual}+put(in x:T&)+get(out x:T&):bool+clear(){virtual}+operator=(in q:queue2<T>&): queue2<T>&+getallocateerror():bool

T

4 定义优先队列通用类模板 queue3

从抽象通用队列类模板 abque 派生优先队列通用类 模板 queue3 。 以链表存储结构为例,优先队列通用类模板除了继 承基类中已经定义的属性和操作外,必须进行如下 的定义工作: · 对基类中的纯虚函数进行实现定义。 · 新增属性 head 指示队列头、 tail 指示队列尾和 allocateerror 表示队列元素结点的内存分配状态。 · 新增操作 copy 实现队列复制、 getallocateerror 获 取队列元素结点的内存分配状态、 put 队列插入 操作、 get 队列提取操作和运算符 operator = 。 类模板图描述如下:

queue3

#head:quenode<T>*#tail:quenode<T>*#allocateerror:bool

-copy(in q:queue3<T>&):queue3<T>&+pushta(in x:T&){virtual}+popfr(out x:T&):bool{virtual}+put(in x:T&)+put(in x:T&, in a:int)+get(out x:T&):bool+clear(){virtual}+operator=(in q:queue1<T>&): queue1<T>&+getallocateerror():bool

T

5 定义模板实参类 student :

各个类模板成员函数的算法分析作为课后练习,模仿例 7-6 中的分析和描述方法,写出本例每个类模板中完成各种队列主要操作的成员函数的算法。

student+name:string+sex:string+age:int+mark_average:float

+assign(in name:char*, in age:int, in mark_average:float, in sex:char*)

7.3.3 数组 数组是一个分布在连续内存空间上线性表,因此它

最大的优点是可以高效地索引存取表元素,而与表的

大小无关。但使用预定义数组机制定义数组的类型和

大小必须预先确定,并且不能改变。

这里讨论的数组类模板,在定义时并不确定它所包

含元素的类型,因此它的内存大小也只能在数组元素

类型被实例化之后才能被分配,但必须保持数组的区

域连续性特点。

对于一个数组类模板还应具备下列特定操作:

① 按照指定大小进行动态空间的再分配;

② 交换两个指定下标数组元素;

③ 数组中的元素排序;

④ 数组中的元素反序;

⑤ 向数组插入一个指定元素;

⑥ 从数组取出一个指定元素;

⑦ 删除一个指定下标数组元素

⑧ 在数组中查找一个指定元素。

通用数组类模板的实现应该在归纳共性的基础上:

① 定义抽象通用数组类模板 abarray 以便实现适应不同

存储结构的动态数组的统一操作接口;

② 从抽象通用数组类模板 abarray 派生定义通用数组类

模板 array 以适应某种确定的存储结构,而元素类型

不确定的数组的需要;

③ 使用通用数组类模板 array 定义确定元素类型的数组

类或实例。例如,学生信息类 student 对象的数组实

例 studentarray 。

类层次结构图:

abarrayT

arrayT

intarray:

<bind>int

studentarray:

<bind>student

student

1..* *dataptr

1 建立抽象通用数组类模板 abarray

原则:按照需求抽象出数组操作的统一操作接口

(方法)规则,其中与存储结构有关的方法声明纯

虚方法。

模板类图描述如下:

abarray

#size:unsigned#used:unsigned#inorder:order{enum}#errormessage:char[]

+expand(in newsize:unsigned):bool{abstract}+getsize():unsigned+getused():unsigned+geterrormessage():char*+getorder():order+resetorder()+store(in x:T&, in index:unsigned){abstract}+recall(out x:T&, in index:unsigned){abstract}+swapelem(in i:unsigned, in j:unsigned){abstract}+shellsort()+re_order()+linsearch(in key:T&):unsigned+binsearch(in key:T&):unsigned

T

2 定义通用数组类模板 array

从抽象通用数组类模板 abarray 派生通用数组类模 板 array 。通用数组类模板除了继承基类中已经定义 的属性和操作外,必须进行以下定义工作: · 对基类定义的纯虚函数进行实现定义。 · 新增属性 dataptr 指示动态数组空间。 · 新增操作 copy 实现数组复制、 insert 向数组插入 指定数据、 deletes 删除数组元素和重载运算符 operator = 、 operator[] 、 operator<< 。 类模板图描述如下:

array

#dataptr:T*

-copy(in arr:array<T>&):array<T>&+expand(in newsize:unsigned):bool{virtual}

+store(in x:T&, in index:unsigned){virtual}

+recall(in x:T&, in index:unsigned){virtual}

+swapelem(in i:unsigned, in j:unsigned){virtual}

+insert(in x:T&)

+deletes(in x:T&)

+deletes(in index:unsigned)+operator=(in arr:array<T>&): array<T>&+operator[](in i:unsigned):T&+operator<<(out stream:ostream&, in arr:array<T>&):ostream&

T

3 定义类模板实参类 student :

student+name:string+sex:string+age:int+mark_average:float

+assign(in name:char*, in age:int, in mark_average:float, in sex:char*)+operator<(in stu:student&):bool+operator<=(in stu:student&):bool+operator>(in stu:student&):bool+operator>=(in stu:student&):bool+operator==(in stu:student&):bool+operator!=(in stu:student&):bool+operator<<(in output:ostream&, in stu:student&):ostream&

通用数组类模板成员函数的算法分析作为课后练

习,模仿例 7-6 中的分析和描述方法,写出类模板中完

成各种主要操作的成员函数的算法。

在 Java 中如果使用普通的 javac 编译器是无法编译带有参数化类型(例如, List<String> )的代码。但我们

可以使用 gjcr 编译命令来编译带有参数化类型代码的程序(详细内容参阅有关的 Java 书籍和资料)。 使用 Java 的参数化类型编程与 C++ 类似,并且更

为简单。例如:// LinkedListGeneric.java

interface Collection<T>

{

public void add(T x);

public Iterator<T> iterator();

}

interface Iterator<T>

{

public T next();

public boolean hasNext();

}

class NoSuchElementException extends RuntimeException { }

class LinkedList<T> implements Collection<T>

{

protected class Node {

T item;

Node next = null;

Node(T item) { this.item = item; }

}

protected Node head = null, tail = null;

public LinkedList() {}

public void add(T item) // 实现 Collection.add()

{

if(head==null) {head = new Node(item); tail = hea

d;}

else { tail.next = new Node(item); tail = tail.next; }

}

public Iterator<T> iterator() // 实现 Collection.iterator

()

{

return new Iterator<T>()

{

protected Node ptr = head;

public boolean hasNext() // 实现 Iterator.hasNext()

{ return ptr != null; }

public T next() // 实现 Iterator.next()

{

if(ptr != null)

{

T item = ptr.item; ptr = ptr.next;

return item;

}

else thow new NoSuchElementException();

}

}

}

}

class Test

{

public static void main(String[] args)

{

String str = "";

// 实例化一个整数列表 LinkedList<Integer> intList =

new LinkedList<Integer>();

intList.add(new Integer(0));

intList.add(new Integer(1));

intList.add(new Integer(2));

Iterator<Integer> int_it = intList.iterator();

while(int_it.hasNext())

str += int_it.next().intValue() + " ";

System.out.println(str); // 显示输出 0 1 2

// 实例化一个字符串列表 LinkedList<String> stringList =

new LinkedList<String>();

stringList.add("zero");

stringList.add ("one");

stringList.add ("two");

str = "";

Iterator<String> string_it = stringList.iterator();

while(string_it.hasNext())

str += string_it.next() + " ";

System.out.println(str); // 显示输出 zero one two

}

}

top related