1.23k likes | 1.42k Views
第七章 类属和模板. 在程序设计中,我们总会发现程序的某些组成模块 所实现的 逻辑功能 是 相同 的,而 不同的 只是被 处理 对 象(数据)的 类型 ,例如下列函数模块: 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; }
E N D
在程序设计中,我们总会发现程序的某些组成模块在程序设计中,我们总会发现程序的某些组成模块 所实现的逻辑功能是相同的,而不同的只是被处理对 象(数据)的类型,例如下列函数模块: intmax(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(type) 正确传递结点的数据的字节数,例如:sizeof(float)。
⑷ 插入结点时的实参传递: 调用成员函数 insert和 append时,需要传递 char* 类型实参,这就是说,将要插入链表的数据强制转 换成 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); } intpop() { return *((int*)list::get()); } };
class int_queue : list { public: int_queue() : list(sizeof(int)) { } void put(int a) { list::append((char*)&a); } intget() { return *((int*)list::get()); } }; 返回
实例化 实例化 模板函数 模板类 实例化 对象 7.2 模板编程 7.2.1 模板的概念 模板是实现类属机制的一种工具,模板的功能非常 强,既可以实现无约束类属机制,也可以实现约束类 属机制。模板允许用户构造模板函数(类属函数), 还允许用户构造模板类(类属类)。下图所示意的是 类模板或函数模板、类、对象、函数之间的关系: 函数模板或类模板
模板定义的一般形式: template <模板形参表列> 模板定义体 其中: ① template:定义模板形参说明行的关键字,表示定义 一个模板的开始。 ② 模板形参表列:由若干模板形参组成的。每个模板 形参均是由关键字 class和类型形参名组成。 ③模板定义体:函数模板的定义,或类模板的定义。 注意,模板形参说明行与模板定义体之间不允许有任 何其他语句。
7.2.2 函数模板与模板函数 1 什麽是函数模板与模板函数 函数模板的声明格式如下: template <classtype> 类型名函数模板名(参数表列) {函数模板定义体} ⑴ type表示模板形参名的一般形式,与说明一般函 数形参名相同,可以是用户命名任何合法标识, 只不过说明的是数据类型而不是数据值。 ⑵ 函数模板的类型名和参数表列中参数类型名可以 是确定的预定义或自定义类型名,也可以是模板 形参名。
例如,将求两个数的最大值函数定义成函数模板:例如,将求两个数的最大值函数定义成函数模板: template <classT> T max(T x, T y) { return (x > y)? x : y; } 如此定义的函数模板 max代表的是一类函数。若要 使用函数模板 max 进行求最大值运算,必须首先将 模板形参 T实例化为确定数据类型(如 int、float、 double等)。 从这个意义上说:函数模板不是一个完全的函数, 将 T实例化的对象类型称之为模板实参,实例化后 的函数模板称为模板函数。 一个使用函数模板的完整程序如下所示:
#include <iostream.h> template <class AT> ATMax(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; }
int 实例化 char 实例化 float 实例化 double 实例化 模板函数 Max(i1, i2) 模板函数 Max(f1, f2) 模板函数 Max(d1, d2) 模板函数 Max(c1, c2) 主函数中对函数模板Max的四次调用: Max(i1,i2)、 Max(f1,f2)、Max(d1,d2)、Max(c1,c2)分别将模板参数 AT实例化为 int、float、double和 char。下图给出了 函数模板Max 和四个模板函数的关系。 函数模板实现了函数参数类型的通用性,作为一种 代码重用机制,可以大幅度地提高程序设计效率和 可维护性。又例如: 函数模板 Max(x, y)
#include <iostream.h> template <classT> Tsum(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 <classtype1, classtype2> 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 <classT> 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 <classT> TMax(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 <classT> TMax(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); }