1 / 123

第七章 类属和模板

第七章 类属和模板. 在程序设计中,我们总会发现程序的某些组成模块 所实现的 逻辑功能 是 相同 的,而 不同的 只是被 处理 对 象(数据)的 类型 ,例如下列函数模块: 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; }

ally
Download Presentation

第七章 类属和模板

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. 第七章 类属和模板

  2. 在程序设计中,我们总会发现程序的某些组成模块在程序设计中,我们总会发现程序的某些组成模块 所实现的逻辑功能是相同的,而不同的只是被处理对 象(数据)的类型,例如下列函数模块: 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; } 若能将处理对象(数据)的类型作为参数传递给提 供同一逻辑功能的模块,便可以实现用同一模块处理 不同类型对象的目的,从而大幅度地提高代码重用度 和可维护性。这种将程序模块编写成参数化模板的方 法就是类属编程。

  3. 在 C++ 中,类属编程有两种实现方式: ⑴ 传统的采用创建类属数据结构的编程方式; ⑵ 采用 C++ 提供的类属工具 —— 参数化模板进行编 程的方式。 本章的重点是模板编程,但对传统类属编程的了解 将有助于对模板的理解。

  4. 本章要点 1 类属编程 类属编程的必要性,类属表的编制和应用实例。 2 模板编程 模板编程的概念,函数模板与模板函数,类模板与模板类,类模板的派生。 3 利用模板工具实现类属数据容器的实例 栈,队列,数组。

  5. 7.1 类属 7.1.1 为什麽要引入类属编程 为什麽要引入类属编程呢?可以通过一个实例来说 明。若有一个整数链表,可以将它定义成一个类,此 类具有将整数插入链表、在链表中查找指定整数、从 链表中删除指定整数等操作,类定义和使用如下: #include <iostream.h> struct node { // 链表结点的数据结构 int val; // 结点值 node* next; // 结点链值 };

  6. class intlist// 整数链表类 { node * head; // 链表头指针 int size; // 链表中的结点个数 public: intlist() // 构造函数 { head = 0; size = 0; } ~intlist(); // 析构函数 bool insert(int); // 向链表中插入一个结点值 bool deletes(int); // 从链表中删除一个结点值 bool contains(int); // 判断链表中是否包含指定结点值 void print(); // 显示输出链表中所有结点值 };

  7. intlist::~intlist() { node* temp; // 定义一个结点型指针用于指向被删结点 for (node* p = head;p;) // 循环删除链表中的所有结点 { temp = p; // 另时指针指向当前结点 p = p->next; // 修改链表中的当前结点指针 delete temp; // 删除当前结点 } }

  8. 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; // 返回失败标志 }

  9. bool intlist::deletes(int x) { node* temp; // 定义临时指针用于指向被删结点 if (head->val == x) // 判别链头结点值是否等于指定值 { temp = head->next; // 临时指针指向下一个结点 delete head; // 删除链头结点 size--; // 链表中结点个数减1 head = temp; // 修改头指针指向下一个结点 return true; // 返回成功标志 }

  10. 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; // 返回失败标志 }

  11. 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"; // 显示行结束符 }

  12. 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; }

  13. 假设又需要设计一个相似的链表,所不同的是新链假设又需要设计一个相似的链表,所不同的是新链 表要保存的数据是double类型的。显然,新链表除了 与整数链表中每一个结点的数据类型不同外,其余均 完全相同。但即使如此,实现新链表的程序代码仍需 要重写一遍,即在上述程序中所有出现 int的地方,均 改为 double,并将类名改为 dublist。修改如下: struct node { double val; node* next; };

  14. class dublist { node * head; int size; public: dublist() { head = 0; size = 0; } ~dublist(); bool insert(double); bool deletes(double); bool contains(double); void print(); };

  15. 在相应的函数定义中,也需要将所有 int参变量改为 double类型(具体修改不再重复)。 假如现在又需要定义一个 float类型链表,则要另建 立一套适应 float类型的新程序代码。按照这样的解决 方法,需要为每一种使用链表结构保存的预定义或用 户自定义类型对象定义适应相应类型的相同逻辑功能 的不同程序代码。显然,这样的方法不但使程序代码 的重用性差,而且维护起来非常困难。类属编程是解 决这类问题的好方法。

  16. 类属的概念来源于 ALGOL68。在 Ada语言中,类属 是它的一种最重要的核心编程概念,类属也就是软件 处理数据的类型参数化,它表现了一个软件元素能处 理多种类型数据的能力。类属的概念后来有了进一步 的扩展。 类属可分为无约束类属机制和约束类属机制,其中 无约束类属机制是指对类属参数没有施加任何特殊的 限制,而约束类属机制则意味着类属参数需要一定的 辅助条件。

  17. 7.1.2 类属表 与其它抽象数据类型一样,类属表是由一个值的集 合和此集合上的可执行操作关联在一起的数据类型。 不同之处在于该值集合的类型不确定,因此不能被预 先定义,只有用确定类型的数据表导出类属表时,值 集合才被建立。 在 C++中,要定义对一个数据表的操作时,必须预 先确定表的值集合,即必须确定值集合的类型。如何 来处理这一矛盾,实现一个类属表定义呢?

  18. 定义一个类型不预先确定值集合的方法是将值集合 中的每个数据元素定义为字符类型指针,即可以用 动态创建的字节串表示值集合中将要存储的任意类 型数据。 ⑵ 由导出类属表的具体数据值集合中的数据类型确定 动态创建字节串的长度。例如,一个整型数占两个 字节,则应该用长度为 2的字节串来表示集合元素 值;又如一个浮点型数占四个字节,则可以长度为 4的字节串来表示集合元素值。

  19. 确定的类型数据与动态创建的字节串的关系是通过 共享相同的内存单元,而使用不同的类型表示和操 作实现的。而字节串长度是通过类属表值集合对象 被创建时所执行的类属表构造函数来传递的。 下面给出一个类属表的具体定义,分析程序的构成 和执行逻辑,可以加深理解类属表的实现机制和意 义。程序代码如下: #include <iostream.h> struct node { // 链表中结点的数据结构 node* next; // 结点的链域 char*contents; // 结点的值域(字符串型指针) };

  20. 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(); } // 析构函数 };

  21. 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; // 修改头指针,使之指向新结点 } }

  22. 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;

  23. while(current != 0) { // 查找链尾(循环结束时,previous指向表的最后结点) previous = current; current = current->next; } previous->next=newnode; // 尾结点的链域指向新结点 } else { // 为空 head = newnode; // 加入新结点 } }

  24. 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; // 返回所获取的数据 } }

  25. void list::clear() { node* p = head; // 定义临时指针 p 指向链表头结点 while (p != 0) // 判别链表是否为空 { // 顺序删除链表中的所有结点 node* pp = p; // 定义临时指针 pp 指向被删除的结点 p = p->next; // 使 p 指向被删除结点的下一个结点 delete []pp->contents; // 释放被删除结点的值域内存 delete pp; // 释放被删除结点的内存 } }

  26. 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);

  27. 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; }

  28. 在本程序中应注意下面几个问题: ⑴ 结点的存储分配: 成员函数 insert和 append 需要为结点 newnode和结 点的数据值域 newnode->contents动态分配内存。结 点数据值的大小被保存在私有数据成员 size中。 ⑵ 结点的数据内容的赋值: 根据 size中的数据长度,逐字节传送。 ⑶main函数中类属对象的创建: 创建 list类属表对象 my_list时,须使用 sizeof(type) 正确传递结点的数据的字节数,例如:sizeof(float)。

  29. ⑷ 插入结点时的实参传递: 调用成员函数 insert和 append时,需要传递 char* 类型实参,这就是说,将要插入链表的数据强制转 换成 char类型,并将数据内存地址作为实参值传递 给被调用的函数,例如: my_list.insert((char*)&r); ⑸输出时的类型转换: 将存储在链表结点值域中的 char 类型数据强制转换 成所需要的数据类型后输出,例如: r = (float)*(float*)my_list.get();

  30. 7.1.3 从类属表中导出栈和队列 类属表设计好以后,我们就可以利用继承机制由它 派生出一些其他类,例如,可以用它派生整数栈类和 整数队列类。在派生中需注意以下几个问题: 1 栈的特点是先进后出。 ⑴ 将进和出的操作放在链表的表头进行; ⑵ 栈中的 push操作可借用类属表中的 insert函数; ⑶ 栈中的 pop操作可借用类属表中的 get函数。

  31. 2 队列的特点是先进先出。 ⑴ 将链表设计成一端插入,另一端取出。例如,链 表尾部插入,链表头部取出; ⑵ 队列的 put操作可借用类属表中的 append函数; ⑶ 队列的 get操作可借用类属表中的 get函数。 3 类型的转换。 因为整数栈和队列实际上均是属性链表,为了使用 方便,在数据插入和提取时,其数据都已经确定是 整数,而不应是类属链表中的字符串;所以在调用 类属链表 list中的成员函数时,不要忘记进行强制 类型转换。实现代码示意如下:

  32. struct node { node* next; char* contents; }; class list { node* head; int size; public:

  33. list(int s){ head = 0; size = s; } void insert(char* a); void append(char* a); char* get(); void clear(); ~list(){ clear(); } };

  34. class int_stack : list { public: int_stack() : list(sizeof(int)) { } void push(int a) { list::insert((char*)&a); } intpop() { return *((int*)list::get()); } };

  35. class int_queue : list { public: int_queue() : list(sizeof(int)) { } void put(int a) { list::append((char*)&a); } intget() { return *((int*)list::get()); } }; 返回

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

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

  38. 7.2.2 函数模板与模板函数 1 什麽是函数模板与模板函数 函数模板的声明格式如下: template <classtype> 类型名函数模板名(参数表列) {函数模板定义体} ⑴ type表示模板形参名的一般形式,与说明一般函 数形参名相同,可以是用户命名任何合法标识, 只不过说明的是数据类型而不是数据值。 ⑵ 函数模板的类型名和参数表列中参数类型名可以 是确定的预定义或自定义类型名,也可以是模板 形参名。

  39. 例如,将求两个数的最大值函数定义成函数模板:例如,将求两个数的最大值函数定义成函数模板: template <classT> T max(T x, T y) { return (x > y)? x : y; } 如此定义的函数模板 max代表的是一类函数。若要 使用函数模板 max 进行求最大值运算,必须首先将 模板形参 T实例化为确定数据类型(如 int、float、 double等)。 从这个意义上说:函数模板不是一个完全的函数, 将 T实例化的对象类型称之为模板实参,实例化后 的函数模板称为模板函数。 一个使用函数模板的完整程序如下所示:

  40. #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; }

  41. 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)

  42. #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};

  43. 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; }

  44. 几点说明: ⑴ 在函数模板中允许使用多个类型参数。但应注意 每个模板形参前必须有关键字 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); }

  45. 程序执行结果: 10 hao 0.123 10 ⑵template语句与函数模板定义语句之间不允许有 任何其他语句,例如: template <classT> int i; // 错误,不允许有别的语句 T Max(T x, T y) { return (x > y) ? x : y; }

  46. ⑶模板函数与重载函数比较: 函数重载需要通过多个函数重载版本来实现,每 个函数版本可以执行不同的操作。 函数模板是通过模板形参实例化为不同类型数据 提供操作的摸板函数版本,但所有模板函数都必 须执行相同的操作逻辑。因此,下面的重载函数 就不能用模板函数代替。 void outdata(int i) { cout << i; } void outdata(double d) { cout << "d = " << d << endl; }

  47. 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); // 错误 }

  48. 分析出现错误的原因是:函数模板被调用时,编译分析出现错误的原因是:函数模板被调用时,编译 器按最先遇到的实参类型隐含生成一个模板函数, 然后用该模板函数对以后出现的所有模板实参进行 一致性检查。例如,对语句 Max(i,c),编译器先按 照实参 i将模板形参 T实例化为 int类型,隐含生成 模板函数 Max(int,int),然后用该模板函数检查此后 出现的模板实参,由于第二个模板实参 c的类型与 模板函数的第二个参数类型 int不符,便发生错误。 解决这个问题有两种方法:

  49. ⑴采用强制类型转换, 例如,将调用语句 Max(i, c); 改写为: Max(i, int(c)); ⑵重载函数扩充函数模板,这种重载有两种表达式: ① 只声明一个函数的原型,而不给出定义体,它的 定义体是借用函数模板的定义体。当执行此重载 版本时,会自动调用函数模板的定义体。例如, 可将上面的程序改写如下:

  50. 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); }

More Related