2005-4-28 1 C++大学基础教程 第11章 多态性 北京邮电大学电信工程学院 计算机技术中心 多态性(Polymorphism)是面向 对象程序设计的主要特征之一。 多态性对于软件功能的扩展和软 件重用都有重要的作用。是学习 面向对象程序设计必须要掌握的 主要内容之一。 第十一章 多态性 11.1 多态性的概念 11.2 继承中的静态联编 11.3 虚函数和运行时的多态 11.4 纯虚函数和抽象类 11.5 继承和派生的应用 11.6 模板 11.1 多态性的概念 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -5- 11.1.1面向对象程序设计中多态的表现 总的来说,不同对象对于相同的消息有不同的 响应,就是面向对象程序设计中的多态性。 具体在程序中,多态性有两种表现的方式: ? 同一个对象调用名字相同、但是参数不同的函数, 表现出不同的行为。在同一个类中定义的重载函数 的调用,属于这种情况。 ? 不同的对象调用名字和参数都相同的函数,表现出 不同的行为。在派生类的应用中,经常会看到这样 的调用。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -6- 11.1.1面向对象程序设计中多态的表现 面向对象程序设计中多态性表现为以下几种形 式: ? 重载多态:通过调用相同名字的函数,表现出不同 的行为。运算符重载也是一种重载多态。 ? 运行多态:通过基类的指针,调用不同派生类的同 名函数,表现出不同的行为。许多面向对象程序设 计的书籍中所说的多态性,就是这种多态。 ? 模板多态,也称为参数多态:通过一个模板,得到 不同的函数或不同的类。这些函数或者类具有不同 的特性和不同的行为。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -7- 11.1.2 多态的实现:联编 一个具有多态性的程序语句,在执行的 时候,必须确定究竟是调用哪一个函 数。也就是说,在执行的时候调用哪个 函数是唯一地确定的。确定具有多态性 的语句究竟调用哪个函数的过程称为联 编(Binding),有的资料也翻译成 “绑 定 ”。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -8- 11.1.2 多态的实现:联编 联编有两种方式:静态联编和动态联 编。 在源程序编译的时候就能确定具有多态 性的语句调用哪个函数,称为静态联 编。 对于重载函数的调用就是在编译的时候 确定具体调用哪个函数,所以是属于静 态联编。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -9- 11.1.2 多态的实现:联编 动态联编则是必须在程序运行时,才能够确定 具有多态性的语句究竟调用哪个函数。用动态 联编实现的多态,也称为运行时的多态。以上 所述的几种多态形式中,只有运行多态是属于 动态联编。以后我们会看到,在一个循环中的 同一个语句,第一次循环时调用的是一个函 数,第二次循环时调用的是另一个函数。这种 结果,程序不运行是看不到的。所以称为动态 联编。 11.2 继承中的静态联编 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -11- 11.2.1派生类对象调用同名函数 在派生类中可以定义和基类中同名的成员函 数。这是对基类进行改造,为派生类增加新的 行为的一种常用的方法。 通过不同的派生类的对象,调用这些同名的成 员函数,实现不同的操作,也是多态性的一种 表现。 在程序编译的时候,就可以确定派生类对象具 体调用哪个同名的成员函数。这是通过静态联 编实现的多态。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -12- 例 11.1 定义 Circle类和 Rectangle类为 Shape类的派生类,通 过 Circle类和 Rectangle类的对象调用重载函数 getArea()显示 对象的面积。 // 例11.1: shape.h #ifndef SHAPE_H #define SHAPE_H class Shape { public: double getArea() const; void print() const; }; // Shape类定义结束 基类Shape的定义 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -13- class Circle : public Shape { public: Circle( int = 0, int = 0, double = 0.0 ); double getArea() const; // 返回面积 void print() const; // 输出Circle 类对象t private: int x,y; // 圆心座标 double radius; // 圆半径 }; // 派生类Circle定义 结束 class Rectangle : public Shape { public: Rectangle( int = 0, int = 0); // 构造函数 double getArea() const; // 返回面积 void print() const; // 输 出 Rectangle 类对象 private: int a,b; // 矩形的长和宽 }; // 派生类Rectangle定义结束 派生类Circle的定义 派生类Rectangle的定义 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -14- // 例11.1: shape.cpp #include <iostream> using namespace std; #include "shape.h" double Shape::getArea() const { cout<<"基类的getArea函数,面积是 "; return 0.0; } // Shape类getArea函 数的定义 void Shape::print() const { cout<<"Base class Object"<<endl; } //Shape类print函数定义 包含头文件 基类成员函数的定义 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -15- Circle::Circle( int xValue, int yValue, double radiusValue ) { x=xValue; y=yValue; radius= radiusValue ; } // Circle类构造函数 double Circle::getArea() const { cout<<"Circle类的getArea函数,面积是 "; return 3.14159 * radius * radius; } // Circle类getArea函数定义 void Circle::print() const { cout << "center is "; cout<<"x="<<x<<" y="<<y; cout << "; radius is " << radius<<endl; } // Circle类print函数定义 Circle类成员函数的定义 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -16- Rectangle::Rectangle( int aValue, int bValue ) { a=aValue; b=bValue; } // Rectangle类构造函数 double Rectangle::getArea() const { cout<<"Rectangle类的getArea函数,面积是 "; return a * b; } // Rectangle类getArea函数定 义 void Rectangle::print() const { cout << "hight is "<<a; cout<<"width is"<<b<<endl; } // Rectangle类print函数定 义 Rectangle类成 员函数的定义 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -17- // 例11.1: 11_1.cpp #include <iostream> using std::cout; using std::endl; #include "shape.h" // 包含头文件 void main() { Circle circle( 22, 8, 3.5 ); // 创建Circle类对象 Rectangle rectangle( 10, 10 ); // 创建Rectangle类 对象 cout << "调用的是 "; cout<<circle.getArea() << endl; // 静态联编 cout << "调用的是"; cout<<rectangle.getArea() << endl; // 静态联编 } // 结束main函数 例11.1的主函数 调用的是 Circle类的 getarea函 数,面积是 38.4845 调用的是 Ractangle类的 getarea函数,面积是 100 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -18- 11.2.1派生类对象调用同名函数 对于派生类对象调用成员函数,可以有以下的 结论: 派生类对象可以直接调用本类中与基类成员函 数同名的函数,不存在二义性; 在编译时就能确定对象将调用哪个函数,属于 静态联编,不属于运行时的多态。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -19- 11.2.2通过基类指针调用同名函数 从继承的角度来看,派生类对象是基类对象的 一个具体的特例。或者说,派生类对象是某一 种特定类型的基类对象。 ? 例如,Circle类是Shape类的公有继承, “圆 ”是 “图 形 ”的一种特例。或者说,圆是一种特定的图形, 具有图形的基本特征。 但是,这种关系不是可逆的。不可以说基类的 对象具有派生类对象的特征,基类对象也不是 派生类对象的一个特例。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -20- 11.2.2通过基类指针调用同名函数 在关于基类对象和派生类对象的操作上,可以 允许以下的操作: ? 派生类对象可以赋值给基类对象; ? 派生类对象的地址可以赋值给基类对象的指针。或 者说,可以用派生类对象的地址初始化基类对象的 指针; ? 可以将基类对象的引用,定义为派生类对象的别 名,或者说,用派生类对象初始化基类的引用。 ? 通过派生类对象的地址初始化的基类对象的指针, 可以访问基类的公有成员,也可以访问和基类成员 函数同名的函数。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -21- 11.2.2通过基类指针调用同名函数 以下这些操作是不可以进行的: ? 不可以将基类对象赋值给派生类对象; ? 不可以用基类对象的地址初始化派生类对象的 指针; ? 不可以将派生类对象的引用定义为基类对象的 别名; ? 不可以通过用派生类对象初始化的基类对象的 指针,访问派生类新增加的和基类公有成员不 重名的公有成员。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -22- 例 11.2 在例 11.1所定义的类的基础上,观察通过派生类对象地址初 始化的基类对象的指针访问 getArea函数的结果。 #include <iostream> using namespace std; #include "shape.h" // 包含头文件 void main() { Shape *shape_ptr; // 指向基类对象的指针 Circle circle( 22, 8, 3.5 ); // 创 建 Circle类对象 Rectangle rectangle( 10, 10 ); // 创建Rectangle类对象 shape_ptr = &circle; // Circle类对象地址初 始化基类指针 cout << "circle 对 象初始化s hape_ptr指针访问的getArea函数是 "<<endl; cout<<shape_ptr->getArea() << endl; // 静态联编 shape_ptr = &rectangle; // Rectangle类对象地址初始化基类 指针 cout << "rectangle 对象初始化shape_ptr指针访问的getArea函数 是 "<<endl; cout<<shape_ptr->getArea() << endl; // 静态联编 } // 结束 main函数 circle 对 象初始化sha pe_ptr指 针访问的getArea函数是 基类的getArea函数,面积是 0 rectangle 对 象初始化 shape_ptr 指针访问的getArea函数是 基类的getArea函数,面积是 0 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -23- 11.2.2通过基类指针调用同名函数 程序运行结果表明: ? 确实可以用派生类对象的地址初始化基类对象的指 针; ? 通过用派生类对象地址初始化的基类对象指针,只 能调用基类的公有成员函数。在以上例子中,就是 调用基类的getArea函数,而不是派生类的getArea 函数。 ? 这种调用关系的确定,也是在编译的过程中完成 的,属于静态联编,而不属于运行时的多态。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -24- 11.2.2通过基类指针调用同名函数 程序运行结果表明: ? 确实可以用派生类对象的地址初始化基类对象的指 针; ? 通过用派生类对象地址初始化的基类对象指针,只 能调用基类的公有成员函数。在以上例子中,就是 调用基类的getArea函数,而不是派生类的getArea 函数。 ? 这种调用关系的确定,也是在编译的过程中完成 的,属于静态联编,而不属于运行时的多态。 11.3 虚函数和运行时的多态 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -26- 11.3 虚函数和运行时的多态 通过指向基类的指针访问基类和派生类 的同名函数,是实现运行时的多态的必 要条件,但不是全部条件。 除此以外,还必须将基类中的同名函数 定义为 虚函数 。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -27- 11.3.1 虚函数 虚函数可以在类的定义中声明函数原型的时候 来说明,格式如下: virtual <返回值类型> 函数名(参数表); ? 在函数原型中声明函数是虚函数后,具体定义这个 函数时就不需要再说明它是虚函数了。 如果在基类中直接定义同名函数,定义虚函数 的格式是: virtual <返回值类型> 函数名(参数表) {<函数体>} 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -28- 11.3.1 虚函数 基类中的同名函数声明或定义为虚函数后,派 生类的同名函数无论是不是用virtual来说 明,都将自动地成为虚函数。从程序可读性考 虑,一般都会在这些函数的声明或定义时,用 virtual来加以说明。 只要对例11.2中的头文件稍加修改,也就是将 基类和派生类中的getArea函数都声明为虚函 数,在重新编译和运行程序,就可以得到运行 时的多态的效果。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -29- 例11.3 将例11.2进行修改,使得程序具有运行时的多态的效 果。 // 例11.3: shape1.h #ifndef SHAPE_H #define SHAPE_H class Shape { public: virtual double getArea() const; void print() const; }; // Shape类定义结束 基类Shape的定义 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -30- class Circle : public Shape { public: Circle( int = 0, int = 0, double = 0.0 ); virtual double getArea() const; // 返回面积 void print() const; // 输出Circle 类对象t private: int x,y; // 圆心座标 double radius; // 圆半径 }; // 派生类Circle定义 结束 class Rectangle : public Shape { public: Rectangle( int = 0, int = 0); // 构造函数 virtual double getArea() const; // 返回面积 void print() const; // 输 出 Rectangle 类对象 private: int a,b; // 矩形的长和宽 }; // 派生类Rectangle定义结束 派生类Circle的定义 派生类Rectangle的定义 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -31- // 例11.3: shape1.cpp #include <iostream> using namespace std; #include "shape1.h" double Shape::getArea() const { cout<<"基类的getArea函数,面积是 "; return 0.0; } // Shape类getArea函 数的定义 void Shape::print() const { cout<<"Base class Object"<<endl; } //Shape类print函数定义 包含头文件 基类成员函数的定义 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -32- Circle::Circle( int xValue, int yValue, double radiusValue ) { x=xValue; y=yValue; radius= radiusValue ; } // Circle类构造函数 double Circle::getArea() const { cout<<"Circle类的getArea函数,面积是 "; return 3.14159 * radius * radius; } // Circle类getArea函数定义 void Circle::print() const { cout << "center is "; cout<<"x="<<x<<" y="<<y; cout << "; radius is " << radius<<endl; } // Circle类print函数定义 Circle类成员函数的定义 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -33- Rectangle::Rectangle( int aValue, int bValue ) { a=aValue; b=bValue; } // Rectangle类构造函数 double Rectangle::getArea() const { cout<<"Rectangle类的getArea函数,面积是 "; return a * b; } // Rectangle类getArea函数定 义 void Rectangle::print() const { cout << "hight is "<<a; cout<<"width is"<<b<<endl; } // Rectangle类print函数定 义 Rectangle类成 员函数的定义 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -34- // 例11.3: 11_3.cpp #include <iostream> using std::cout; using std::endl; #include "shape1.h" // 包含头文件 void main() { Circle circle( 22, 8, 3.5 ); // 创建Circle类对象 Rectangle rectangle( 10, 10 ); // 创建Rectangle类 对象 cout << "调用的是 "; cout<<circle.getArea() << endl; // 静态联编 cout << "调用的是"; cout<<rectangle.getArea() << endl; // 静态联编 } // 结束main函数 例11.1的主函数 circle 对象 初始化shape_ptr指针访问的 getArea函数是 Circle类的getArea函数,面积是 38.4845 rectangle 对象初始化shape_ptr指针访问 的getArea函数是 Rectangle类的getArea函数,面积是 100 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -35- 11.3.1 虚函数 这个结果和例11.2的结果大不相同。同样的 shape_ptr->getArea()函数调用,当shape_ptr指 针中是Circle类对象地址时,访问的是Circle类 的getArea函数。而shape_ptr指针中是Rectangle 类对象的地址时,访问的是Rectangle类的 getArea函数。 这种方式的函数调用,在编译的时候是不能确定 具体调用哪个函数的。只有程序运行后,才能知 道指针shape_ptr中存放的是什么对象的地址,然 后再决定调用哪个派生类的函数。是 一种运行时 决定的多态性 。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -36- 11.3.1 虚函数 要实现运行时的多态,需要以下条件: ? 必须通过指向基类对象的指针访问和基类成员 函数同名的派生类成员函数; ? 或者用派生类对象初始化的基类对象的引用访 问和基类成员函数同名的派生类成员函数; ? 派生类的继承方式必须是公有继承; ? 基类中的同名成员函数必须定义为虚函数。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -37- 11.3.2 虚函数的使用 虚函数必须正确的定义和使用。否则,即使在 函数原型前加了virtual的说明,也可能得不 到运行时多态的特性。 ? 必须首先在基类中声明虚函数。在多级继承的情况 下,也可以不在最高层的基类中声明虚函数。例如 在第二层定义的虚函数,可以和第三层的虚函数形 成动态联编。但是,一般都是在最高层的基类中首 先声明虚函数。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -38- 11.3.2 虚函数的使用 ? 基类和派生类的同名函数,必须函数名、返回 值、参数表全部相同,才能作为虚函数来使 用。否则,即使函数用virtual来说明,也不具 有虚函数的行为。 ? 静态成员函数不可以声明为虚函数。构造函数 也不可以声明为虚函数。 ? 析构函数可以声明为虚函数,即可以定义虚析 构函数。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -39- 例 11.4 虚函数的正确使用。分析以下程序,编译时哪个语句会出现 错误?为什么?将有错误的语句 屏蔽掉以后,程序运行结果如何? 其中哪些调用是静态联编,哪些是动态联编? #include <iostream.h> class BB {public: virtual void vf1(){cout<<"BB::vf1被调用\n";} virtual void vf2(){cout<<"BB::vf2被调用\n";} void f(){cout<<"BB::f被调用\n";} }; class DD:public BB {public: virtual void vf1(){cout<<"DD::vf1被调用\n";} void vf2(int i){cout<<i<<endl;} void f(){cout<<"DD::f\n被调用";} }; 有虚函数的基类 派生类 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -40- void main() { DD d; BB *bp=&d; bp->vf1(); bp->vf2(); bp->vf2(10); bp->f(); } 将这个语句注释掉后,运 行结果将显示: DD::vf1被调用 BB::vf2被调用 BB::f被调用 函数调用 bp->vf2(10);是 错误的。因为派生类的 vf2函数和基类的 vf2函数 的参数不同,派生类的 vf2就不是虚函数。 其中 bp->vf1()调用是动态联 编。 bp->vf2()是静态联编。 bp->f()也是静态联编。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -41- 11.3.3 虚析构函数 如果用 动态创建 的派生类对象的地址初始化基 类的指针,创建的过程不会有问题:仍然是先 调用基类构造函数,再执行派生类构造函数。 但是,在用delete运算符删除这个指针的时 候,由于指针是指向基类的,通过静态联编, 只会调用基类的析构函数,释放基类成员所占 用的空间。而 派生类成员所占用的空间将不会 被释放 。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -42- #include <iostream> using namespace std; class Shape { public: Shape(){cout<<"Shape类构造函数被调用\n";} ~Shape(){cout<<"Shape类析构函数被调用\n";}; }; // Shape类定义结束 class Circle : public Shape { //派生类Circle的定义 public: Circle( int xx= 0, int yy= 0, double rr= 0.0 ) {x = xx; y = yy; radius =rr; cout<<"Circle类构造函数被调用\n"; } ~Circle() {cout<<"Circle类析构函数被调用\n"; } private: int x,y; // 圆心座标 double radius; // 圆半径 }; // 派生类Circle定义 结束 void main() {Shape *shape_ptr; shape_ptr = new(Circle)(3,4,5); delete shape_ptr; } 例 11.5 定义简 单的 Shape类和 Circle类,观察 基类指针的创建 和释放时如何调 用构造函数和析 构函数。 基类Shape的定义 主函数 程序运行后在屏 幕上显示: Shape类构造函 数被调用 Circle类构造函 数被调用 Shape类析构函 数被调用 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -43- 11.3.3 虚析构函数 为了解决派生类对象释放不彻底的问题,必须 将基类的析构函数定义为虚析构函数。格式是 在析构函数的名字前添加virtual关键字。函 数原型如下: virtual ~Shape(); 此时,无论派生类析构函数是不是用virtual 来说明,也都是虚析构函数。 再用delete shape_ptr来释放基类指针时,就 会通过动态联编调用派生类的析构函数。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -44- 11.3.3 虚析构函数 将例11.5程序中的~Shape析构函数作以上修改 后,运行的结果将是: Shape类构造函数被调用 Circle类构造函数被调用 Circle类析构函数被调用 Shape类析构函数被调用 11.4 纯虚函数和抽象类 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -46- 11.4 纯虚函数和抽象类 在前面的几个例子中,基类Shape本身并 不是一个具体的 “形状 ”的抽象,而是各 种实际的 “形状 ”的抽象。 在C++中,对于那些在基类中不需要定义 具体的行为的函数,可以定义为 纯虚函 数。 对于那些只是反映一类事物公共特性的 类,在C++中可以定义为 “抽象类 ”。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -47- 11.4 纯虚函数和抽象类 纯虚函数声明的格式是: virtual <返回值类型> 函数名(参数表) = 0; 纯虚函数的声明和使用有以下的特点: ? 纯虚函数一定是在基类中声明的。 ? 在多级继承的情况下,纯虚函数除了在最高层基类 中声明外,也可以在较低层的基类中声明。 ? 纯虚函数是没有函数体的。函数体是用 “= 0”来代 替了。 ? 纯虚函数是不可以被调用的。凡是需要被调用的函 数都不可以声明为纯虚函数。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -48- 11.4 纯虚函数和抽象类 抽象类的定义是基于纯虚函数的。 凡是带有一个或几个纯虚函数的类,就是抽象 类。 抽象类定义的一般形式是: class 类名 {public: virtual <返回值类型> 函数名(参数表) = 0; 其他函数的声明; ……. }; 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -49- 11.4 纯虚函数和抽象类 抽象类的定义和使用具有以下的特点: ? 抽象类是不可以实例化的,也就是不可以定义抽象 类的对象。 ? 但是,可以定义抽象类的指针和抽象类的引用。目 的是通过这些指针或引用访问派生类的虚函数,实 现运行时的多态。 ? 如果抽象类的派生类中没有具体实现纯虚函数的功 能,这样的派生类仍然是抽象类。 ? 抽象类中除了纯虚函数外,还可以定义其他的非纯 虚函数。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -50- 11.4 纯虚函数和抽象类 虚函数、纯虚函数、多态性在面向对象程序设 计中有很大的作用。可以增强程序的通用性、 可扩展性和灵活性。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -51- 例 11.6 编写一个程序,可以动态的创建 Circle 类或者 Rectangle类的对象,并且显示所创建对 象的面积。在编程中注意使用多态性。 动态创建 Circlae类或 者 Rectangle类对象 动态创建 Circlae类或 者 Rectangle类对象 动态创建 Circlae类或 者 Rectangle类对象 希望用 3个函数实现 题目的功能。如果 不使用运行时的多 态,很难实现 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -52- // 例11.6: shape2.h #ifndef SHAPE_H #define SHAPE_H class Shape { //基类Shape的定义 public: virtual double getArea() const=0; //纯虚函数 void print() const; virtual ~Shape(){} //虚析构函 数 }; // Shape类定义结 束 基类Shape的定义 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -53- class Circle : public Shape { //派生类Circle的定 义 public: Circle( int = 0, int = 0, double = 0.0 ); double getArea() const; // 返回面积 void print() const; // 输出Circle 类对象t private: int x,y; // 圆心座标 double radius; // 圆半径 }; // 派生类Circle定义 结束 class Rectangle : public Shape {// 派生类Rectangle的定义 public: Rectangle( int = 0, int = 0); // 构造函数 double getArea() const; // 返回面积 void print() const; // 输出Rectangle类对象 private: int a,b; // 矩形的长和宽 }; // 派生类Rectangle 定义结束 派生类Circle的定义 派生类Rectangle的定义 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -54- // 例11.6: shape2.cpp #include <iostream> using namespace std; #include "shape2.h" // 包含头 文件 void Shape::print() const { cout<<"Base class Object"<<endl; } //Shape类print函数定义 包含头文件 基类成员函数的定义 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -55- Circle::Circle( int xValue, int yValue, double radiusValue ) { x=xValue; y=yValue; radius= radiusValue ; } // Circle类构造函数 double Circle::getArea() const { cout<<"Circle类的getArea函数,面积是 "; return 3.14159 * radius * radius; } // Circle类getArea函数定义 void Circle::print() const { cout << "center is "; cout<<"x="<<x<<" y="<<y; cout << "; radius is " << radius<<endl; } // Circle类print函数定义 Circle类成员函数的定义 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -56- Rectangle::Rectangle( int aValue, int bValue ) { a=aValue; b=bValue; } // Rectangle类构造函数 double Rectangle::getArea() const { cout<<"Rectangle类的getArea函数,面积是 "; return a * b; } // Rectangle类getArea函数定义 void Rectangle::print() const { cout << "hight is "<<a; cout<<"width is"<<b<<endl; } // Rectangle类print函数定义 Rectangle类成 员函数的定义 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -57- // 例11.6: 11_6.cpp #include <iostream> using namespace std; #include "shape2.h" // 包含头文件 void creat_object(Shape **ptr); void display_area(Shape *ptr); void delete_object(Shape *ptr); void main() { Shape *shape_ptr; creat_object(&shape_ptr); display_area(shape_ptr); delete_object(shape_ptr); } // 结束main函数 例11.6的主函数 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -58- void creat_object(Shape **ptr) { char type; *ptr = NULL; do{ cout<<"创建对象。c:C ircle类 对象;r:Rectangle类对象 "<<endl; cin>>type; switch (type) {case 'c': {int xx,yy; double rr; cout<<"请输入圆心的座标和圆的半径:"; cin>>xx>>yy>>rr; *ptr = new Circle(xx,yy,rr); break; } case 'r': {int aa,bb; cout<<"请输入矩形的长和宽:"; cin>>aa>>bb; *ptr = new Rectangle(aa,bb); break;} default:cout<<"类型错误,请重新选择\n"; } }while(*ptr==NULL); Creat_object函数 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -59- void display_area(Shape *ptr) { cout<<"显示所创建对象的面积,调用的是"<<endl; cout<<ptr->getArea() << endl; } void delete_object(Shape *ptr) { delete(ptr); } 创建对象。c:Circle类对象;r:Rectangle类对 象 c 请输入圆心的座标和圆的半径:1 1 5 显示所创建对象的面积,调用的是Circle类的 getArea函数,面积是78.54 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -60- 11.4 纯虚函数和抽象类 这个程序中,使用了本章所介绍的虚函数、纯 虚函数、虚析构函数、基类指针访问派生类对 象等技术,实现了运行时的多态。 程序具有很好的可扩展性。如果需要增加新的 派生类,当然要增加和派生类定义有关的代 码,和创建对象所需要的语句。但是,对于函 数display_area,display_object等函数 都 不用修改。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -61- 11.4 纯虚函数和抽象类 这个例子还显示了:抽象类中可以为各派生类 定义一些通用的接口。这些通用的接口就是抽 象类中的纯虚函数。新增加的派生类的对象, 都可以使用这样的通用接口,表现派生类对象 的行为特性。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -62- 11.4 纯虚函数和抽象类 例11.7 在例11.6的基础上,现在需要增加一 个Rectangle的派生类:Cube,也就是立方体 类。任意创建Circle、Rectangle、Cube类的 对象,并显示对象的面积。 现在需要做的只有两件事: ? 定义新的Cube类; ? 在create_object函数中增加创建Cube类对象的内 容。 ? 其他函数都不需要修改。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -63- // 例11.7: cube.h #ifndef CUBE_H #define CUBE_H class Cube : public Rectangle { //派生类Cube的定义 public: Cube(int, int, int); double getArea() const; void print() const; private: int c; }; // Cube类定 义结束 #endif //cube.h文件结束 例11.7新增加的 类定义和头文件 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -64- // 例11.7: 11_7.cpp #include <iostream> using std::cout; using std::cin; using std::endl; #include "shape2.h" // 包含头文件 #include "cube.h" void creat_object(Shape **ptr); void display_area(Shape *ptr); void delete_object(Shape *ptr); void main() { Shape *shape_ptr; creat_object(&shape_ptr); display_area(shape_ptr); delete_object(shape_ptr); } // 结束main函数 Main函数只是多包 含了一个头文件 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -65- void creat_object(Shape **ptr) { char type; *ptr = NULL; //空指针 do{ cout<<"创建对象。请选择:"; cout<<"c:Circle类对象;r:Rectangle类对象;u:Cube类对 象"<<endl; cin>>type; switch (type) {case 'c': //创建Ciecle类对象 {int xx,yy; double rr; cout<<"请输入圆心的座标和圆的半径:"; cin>>xx>>yy>>rr; *ptr = new Circle(xx,yy,rr); break; } 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -66- case 'r': //创建Rectangle类对象 {int aa,bb; cout<<"请输入矩形的长和宽:"; cin>>aa>>bb; *ptr = new Rectangle(aa,bb); break; } case 'u': //创建Cube类对象 {int aa,bb,cc; cout<<"请输入立方体的长、宽、高:"; cin>>aa>>bb>>cc; *ptr = new Cube(aa,bb,cc); break; } default:cout<<"类型错误,请重新选择\n"; } }while(*ptr==NULL); } Creat_object函数 多增加了一个分支 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -67- 11.4 纯虚函数和抽象类 程序执行过程: 创建对象。请选择:c:Circle类对象; r:Rectangle类对象;u:Cube类对象 u 请输入立方体的长、宽、高:4 5 6 显示所创建对象的面积,调用的是 Cube类的getArea函数,还要调用 Rectangle类 的getArea函数,面积是 120 ……… 11.5 继承和派生的应用 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -69- 11.5 继承和派生的应用 例11.8 设计并实现一个学生学分管理系统。 主要功能包括: ? 管理的对象:大学本科生和研究生; ? 可以增加学生和学生的基本信息,包括姓名,学 号,研究生还要包括导师姓名; ? 可以相加所选的课程、学分、课程类别,本科生的 课程类别是必修和选修,研究生的课程类别是学位 课还是任选课; ? 显示学生的基本信息和课程/学分信息; ? 系统要有良好的可扩展性。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -70- 11.5 继承和派生的应用 面向对象程序设计要考虑: ? 对象的抽象,即类的定义; ? 对于本例来说,可以有一个基类Student,反映学 生的基本信息和基本行为;有两个派生类: UnderGraduate(本科生)和Graduate(研究 生)。 ? 数据结构。也就是用什么方式存储对象。也包括用 什么方式存储对象的数据成员。 ? 由于学生不止一个,所以要用数组或者链表来存放 学生对象。为简单起见,使用两个对象数组, UnderStu[]存放UnderGraduate对象, GraduateStu[]存放Graduate对象。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -71- 11.5 继承和派生的应用 为了解决创建对象存储到数组中去,类的数据 成员中需要定义一个静态数据成员StuNum,用 来跟踪创建对象的数目,同时也是决定存放到 数组的数组元素的序号。 采用类似于例11.6的方法,设计3个函数来完 成程序的功能: ? 创建对象:creat_object(Stu_ptr); ? 输入学分:InputCredit(Stu_ptr); ? 显示信息:DisplayStu(Stu_ptr); ? 其中Stu_ptr是指向对象的指针。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -72- 11.5 继承和派生的应用 但是,不可以用所创建的对象数组的成员作为函数的 实参数。 可以另外创建一个对象指针数组,每个成员都是一个 指针,指向对象数组的对象成员。 由于有两个对象数组,所以要用一个二维指针数组 通 过两个 for循环语句,将两个对象数组元素的地址写入 这个指针数组: for(int i=0;i<10;i++) Stu_ptr[0][i]=&UnderStu[i]; for(i=0;i<10;i++) Stu_ptr[1][i]=&GraduateStu[i]; 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -73- 11.5 继承和派生的应用 另外定义一个函数SelectStuType,函数的返回值 是指针数组的行地址:需要访问UnderStu对象数 组时,返回指针数组第0行的地址,需要访问 GraduateStu对象数组时,返回指针数组第1行的 地址。函数的原型是: Student **SelectStuType(Student *Array_ptr[][10]); ? 注意返回值的类型是Student **,后一个 “*”表示返回 的是指针,而 “Student *”表示指针所指向的是Student 类对象的地址。 ? 只要通过简单的运算,就可以访问某一行的某个元素。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -74- // 例11.8 简单信息管理系统 // 例11.8: student.h #ifndef SHAPE_H #define SHAPE_H struct credit //课程_学分结构体定义 {char course[20]; //课程名 int hour; //学分 char type[5]; //课程类型 }; 用结构体表示 学生的学分构 成 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -75- class Student { protected: char *name; //姓名 int student_ID; //个人编号 credit credit_hour[10]; //选修 的课程和学 分 int num; //修课门数 public: Student(); //构造函数 virtual ~Student(){}; //虚析构函 数 char * getname(); //显示姓名函数 virtual void InputInfo(); //输入 学生基本信 息 virtual void input_credit()=0; //输入课程和 学分 virtual void displayInfo()=0; //显示学生信息 () //学生人数 基类Student的 定义 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -76- class UnderGraduate : public Student { public: virtual void InputInfo(); //输入 学生基本信 息 virtual void input_credit(); //输入课程 和学分 virtual void displayInfo(); //显示学生 信息 int GetStuNum(); private: static int StuNum; //本科学生人数 }; // 派生类UnderGraduate定义结束 派生类 UnderGraduate 的定义 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -77- class Graduate : public Student { public: virtual void InputInfo(); //输入 学生基本信 息 virtual void input_credit(); //输入课程 和学分 virtual void displayInfo(); //显示学生 信息 int GetStuNum(); private: static int StuNum; //研究生人 数 char *tutor; //导师姓名 }; // 派生类Graduate定义结束 #endif 派生类Graduate 的定义 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -78- // 例11.8: student.cpp #include <iostream> #include <iomanip> using namespace::std; #include "student.h" // 包含头文件 int UnderGraduate::StuNum=0; //静态数据成员初始 化 int Graduate::StuNum=0; //静态数据成员初始 化 Student::Student() //Student类构造函数 { num = 0; //选课数初始化为0 } 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -79- void Student::InputInfo() //Student 类 InputInfo函数 { char namestr[20]; //输入学生姓名临时存放 在namestr中 cout<<"请输入下一个学生的姓名:"; cin>>namestr; name=new char[strlen(namestr)+1]; //为指针name申请地址 strcpy(name, namestr); //将临时存放的姓名复制 到 name cout<<"输入学生的学号:"; cin>>student_ID; } char * Student::getname() //Student类显示姓名函 数 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -80- void UnderGraduate::InputInfo() //输入本科生信息的函 数 {Student::InputInfo(); StuNum++; //本科生总数加1 } void UnderGraduate::input_credit() //输入本科生选修的课程和学分 {char answer; do{ cout<<"输入课程名称、学分、课程类别:必修/选修\n"; cin>>credit_hour[num].course>>credit_hour[num].h our >>credit_hour[num].type; num++; cout<<"继续输入课程?\n"; cin>>answer; }while(answer == 'y'); } 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -81- int UnderGraduate::GetStuNum() //取得本科生人数 {return StuNum; } void UnderGraduate::displayInfo() //显示本科生信息 { cout << "本科学生姓名:"<<name; cout<<" 学号:"<<student_ID<<endl; cout<<"课程名称 学分 课程类别 \n"; for(int i=0;i<num;i++) cout<<left<<setw(10)<<credit_hour[i].course <<setw(10)<<credit_hour[i].hour <<setw(10)<<credit_hour[i].type<<endl; } 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -82- void Graduate::InputInfo() { char namestr[20]; //输入导师姓名临时存放在 namestr中 Student::InputInfo(); StuNum++; cout<<"请输入导师姓名:"; cin>>namestr; tutor=new char[strlen(namestr)+1]; //为指针tutor申 请地址 strcpy(tutor, namestr); //将临时存放的姓名 复制到 tutor } 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -83- void Graduate::input_credit() //输入研究生选修课程和学分 {char answer; do{ cout<<"输入课程名称、学分、课程类别:学位/任选\n"; cin>>credit_hour[num].course>>credit_hour[num].h our >>credit_hour[num].type; num++; cout<<"还有课程要输入? "; cin>>answer; }while(answer == 'y'); } 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -84- int Graduate::GetStuNum() {return StuNum; } void Graduate::displayInfo() //显示研究生信息 { cout << "研究生姓名:"<<name; cout<<" 学号:"<<student_ID<<endl; cout<<" 导师姓名:"<<tutor<<endl; cout<<"课程名称 学分 课程类别 \n"; for(int i=0;i<num;i++) cout<<left<<setw(10)<<credit_hour[i].course <<setw(10)<<credit_hour[i].hour <<setw(10)<<credit_hour[i].type<<endl; } 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -85- // 例11.8: student_app.cpp #include <iostream> using namespace::std; #include "student.h" // 包含头文 件 Student **SelectStuType(Student *Array_ptr[][10]); void creat_object(Student *Array_ptr[][10]); void DisplayStu(Student *Array_ptr[][10]); void InputCredit(Student *Array_ptr[][10]); 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -86- void main() { UnderGraduate UnderStu[10]; Graduate GraduateStu[10]; Student *Stu_ptr[2][10]; //指针数组 for(int i=0;i<10;i++) Stu_ptr[0][i]=&UnderStu[i]; //第0行存放UnderStu各元素地址 for(i=0;i<10;i++) Stu_ptr[1][i]=&GraduateStu[i]; //第1行存放GraduateStu各元素地址 creat_object(Stu_ptr); InputCredit(Stu_ptr); DisplayStu(Stu_ptr); } // 结束main函数 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -87- Student **SelectStuType(Student *Array_ptr[][10]) { char type; Student **ptr = NULL; do{ cout<<"请选择学生类型:"; cout<<"U:本科生;G:研究生 : "; cin>>type; switch (type) {case 'U': ptr=Array_ptr[0]; //指针数组第0行地址 break; case 'G': ptr=Array_ptr[1]; //指针数组第1行地址 break; default:cout<<"类型错误,请重新选择\n"; } }while(ptr==NULL); return ptr; //返回指针数组的行地址 } 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -88- void creat_object(Student *Array_ptr[][10]) { char answer; Student **row_ptr; //存放指针数组行地址 Student *ptr = NULL; //存放指针数组的元素值 cout<<"输入学生基本信息\n"; do{ row_ptr=SelectStuType(Array_ptr); ptr=*(row_ptr + (row_ptr[0])->GetStuNum()); ptr->InputInfo(); //调用虚函数InputInfo cout<<"继续输入学生基本信息(y/n)? "; cin>>answer; }while(answer=='y'); } 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -89- void DisplayStu(Student *Array_ptr[][10]) { char answer; Student **row_ptr; Student *ptr = NULL; int StuNum; cout<<"显示学生基本信息\n"; do{ row_ptr=SelectStuType(Array_ptr); ptr=*row_ptr; StuNum = ptr->GetStuNum(); //取得本科生或研究生人数 for(int i=0;i<StuNum;i++) row_ptr[i]->displayInfo(); //调用虚函数displayInfo cout<<"继续显示学生信息(y/n)? "; cin>>answer; }while(answer=='y'); } 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -90- void InputCredit(Student *Array_ptr[][10]) { char answer; Student **row_ptr; Student *ptr = NULL; int StuNum; cout<<"输入已选课程、学分、课程类型\n"; do{ row_ptr=SelectStuType(Array_ptr); ptr=*row_ptr; StuNum = ptr->GetStuNum(); for(int i=0;i<StuNum;i++) {cout<<"输入"<<row_ptr[ i]->getname()<<"的课程 和学分:\n"; row_ptr[i]->input_credit(); } cout<<"继续输入其他类别学生的课程和学分(y/n)? "; cin>>answer; }while(answer=='y'); } 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -91- void InputCredit(Student *Array_ptr[][10]) { char answer; Student **row_ptr; Student *ptr = NULL; int StuNum; cout<<"输入已选课程、学分、课程类型\n"; do{ row_ptr=SelectStuType(Array_ptr); ptr=*row_ptr; StuNum = ptr->GetStuNum(); for(int i=0;i<StuNum;i++) {cout<<"输入"<<row_ptr[ i]->getname()<<"的课程 和学分:\n"; row_ptr[i]->input_credit(); } cout<<"继续输入其他类别学生的课程和学分(y/n)? "; cin>>answer; }while(answer=='y'); } 11.6 模板 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -93- 11.6 模板 模板是C++中的通用程序模块。在这些程序 模块中有一些数据类型是不具体的,或者说 是抽象的。当这些抽象的数据类型更换为不 同的具体数据类型以后,就会产生一系列具 体的程序模块。 C++中的模块包括函数模板和类模板。 函数模板实例化后产生的函数,称为模板函 数。类模板实例化后产生的类,称为模板 类。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -94- 11.6.1 函数模板 函数模板定义的基本格式 : template <typename 参数化类型名> 函数返回类型 函数名(形式参数列表) {函数体 } ? “template”是定义模板的关键字。 ? 在一对尖括号<>内,关键字 “typename”后面声 明所使用的 “参数化类型名 ”。 ? 关键字 “typename”可以用 “class”取代 。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -95- 11.6.1 函数模板 模板的其余部分和一般的函数定义的格式完 全相同。只是在函数定义时可以使用参数化 类型来代表各种具体的数据类型。 参数化类型可以用于: ? 函数返回值类型; ? 函数参数表内形式参数的类型; ? 函数体内,自动变量的类型。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -96- 例11.9 函数模板的定义和使用:定义和使用确定3个数 据的最大值函数模板。 #include<iostream> using namespace::std; template<typename T> T max_value(T x,T y,T z) //函数模板的定义:求x、y、z的最大值 { T temp; temp = x>y?x:y; return temp>z?temp:z; } void main() { cout<<max_value(12,32,21)<<endl; //用整数作实参调用函数模板 cout<<max_value('a','A','9')<<endl; //用字符作实参调用函数模板 } 程序执行后,输出: 32 a 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -97- 11.6.1 函数模板 函数模板定义的一般格式 template <typename 参数化类型名1, ………typename 参数化类型名n> 函数返回类型 函数名(形式参数列表) {函数体 } 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -98- 例11.10 编写一个函数模板,可以按指定的操作数类型 进行乘法。 // 例11.10 带有两个形式参数的函数模板 #include<iostream.h> //p10-.cpp template<typename P1,typename P2> P1 cal(P1 x, P2 y) //函数模板有两个参数化类型名:P1和P2 { return (x * (P1)y); } //按x类型进行乘 法 void main() { unsigned short w=230; short z=150; cout<<cal(w,z)<<endl; //按无符号数 相乘 cout<<cal(z,w)<<endl; //按有符号数 相乘 } 程序运行的结果是: 34500 -31036 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -99- 11.6.1 函数模板 带有确定类型的参数的函数模板 函数模板的形式参数表中除了使用参数化类 型名以外,还可以使用确定类型的参数。也 就是说,函数模板的参数表中,一定要包含 参数化类型名,但不一定都使用参数化类型 名。还可以根据需要,使用确定类型的参 数。如整型的参数、实型的参数,等等。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -100- 例11.11 设计和编写一个通用的输入数组元素的函数模板。可以用 它来输入各种不同数据类型的数组。 #include<iostream.h> //p11-11.cpp #include <typeinfo.h> template <class Q1> //函数模板 void ArrayInput(Q1 array, int num) {cout << "输入 "<<num<<"个"<<ty peid(Q1).name()<<'\b'<<"型数据 "<<endl; for (unsigned j= 0; j < num; j++) cin >> array[j]; //输入数组元素 } void main() { int number; float floatArray[4]; int intArray[3]; number=sizeof(floatArray)/sizeof(float); ArrayInput(floatArray, number); //输入整型数组元素 number=sizeof(intArray)/sizeof(int); ArrayInput(intArray, number ); //输入浮点型数组元 素 } 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -101- 11.6.1 函数模板 程序中使用了C++所定义的typeid运算符。它 可以在程序运行时,显示指定的数据的类 型。使用的格式是: ? typeid(表达式).name() ? 或者 typeid(类型标识符).name() ? 为了符合一般的阅读习惯,在程序中通过输出一 个 “退格 ”符,将 “*”覆盖了。最后看到的就是 “输 入X个XX型数据 ”。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -102- 11.6.2 函数模板使用中的问题 用户定义的类取代参数化类型 : ? 在这种情况下,函数模板实例化后,在函数的表达 式中参与运算的就是类的对象。对象可以直接使用 的运算符只有赋值运算符 “=”。如果表达式中需要 对象进行其他的运算,就必须在相应的类的定义 中,对于要使用的运算符进行重载。 ? 例如,在例11.9中,函数模板是求3个实参的最大 值,需要使用运算符 “>”。如果要用对象来作实 参,相应的类中要对 “>”运算符进行重载。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -103- 例11.12 用例11.9的函数模板,求3个Circle类对象的最大值。 // 例11.12 对象作为函数模板的实参 #include<iostream.h> //p11_12.cpp //using namespace::std; template<typename T> T max_value(T x,T y,T z) //函数模板的定义:求x、y、z的最大值 { T temp; if(x>y) temp = x; else temp = y; if(z>temp) temp =z; return temp; } 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -104- class Circle //Circle类的定义 {public: friend ostream &operator<<( ostream &, Circle & ); //重载 “<<”运算符 Circle( int a = 0, int b = 0, double c = 0.0 ) {x = a; y = b; radius = c; } int operator>(Circle m2) //重载 “>”运算符 {if(radius>m2.radius) return 1; else return 0; } private: int x,y; // 圆心座标 double radius; // 圆半径 }; // 类Circle定义结束 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -105- ostream &operator<<( ostream &out, Circle &C1 ) {out<<"x="<<C1.x<<" y="<<C1.y; out<<" radius="<<C1.radius; return out; } void main() { Circle C1(2,3,5),C2(3,5,8),C3(3,2,6); //定义3个 Circle类对象 cout<<max_value(12,32,21)<<endl; //用整数作实参调用函数模板 cout<<max_value('a','A','9')<<endl; //用字符作实参调用函数模板 cout<<max_value(C1,C2,C3)<<endl; //用对象作参数调用函数模板 } 程序运行结果是: 32 a x=3 y=5 radius=8 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -106- 11.6.2 函数模板使用中的问题 函数模板不支持参数自动转换 对于例11.9的函数模板: template <typename T> T max_value(T x,T y,T z); ? 如果用以下的方式来调用: cout<<max_value(12,3.2,21)<<endl; 在编译时会出现编译错误: “template parameter 'T' is ambiguous” 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -107- 11.6.2 函数模板使用中的问题 在定义指向一维数组的指针时,还必须指出一 维数组的大小。 声明指向一维数组的指针的格式如下: <类型名> (*指针变量名)[一维数组大小]; 例如,和图11.3中两个二维数组所对应的指向 一维数组的指针定义如下: ? char (*ptchb)[4], (*ptchc)[2]; ? ptchb=B; ptchc=C; 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -108- 11.6.2 函数模板使用中的问题 要解决这样的问题,就需要函数模板和一般的 函数联合使用。即需要函数模板和一般的函数 的重载。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -109- 11.6.3 重载函数模板 重载函数模板可以是函数模板和另一个参数数 目不同的函数模板的重载,也可以是函数模板 和非模板函数的重载。一般所说的函数模板重 载是指后一种情况。 ? 函数模板和非模板函数重载,可以解决函数模板不 支持参数自动转换的问题。 ? 当所使用的参数类型(不是类类型)不支持函数模 板中某些操作时,就需要为这样的参数专门编写相 应的非模板函数。和已经定义的函数模板形成重载 的关系。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -110- 11.6.3 重载函数模板 例如,需要用例11.9的函数模板来比较3个结 构体变量,返回其中的最大值。由于结构体变 量不支持直接的比较操作 “>”。要实现这样的 功能,就要再写一个函数。 假定结构体的定义是: struct course { char *name; float score; }; 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -111- 11.6.3 重载函数模板 3个这样的结构体变量的比较函数如下: course max_value(course s1,course s2,course s3) {course s4; if(s1.score>s2.score) s4=s1; else s4=s2; if(s3.score>s4.score) s4=s3; return s4; } 这样就形成了函数模板和非模板函数的重载: template<typename T> T max_value(T x,T y,T z); course max_value(course s1,course s2,course s3); 11.6.4 类模板 类模板的定义 template <class 参数化类型名1, ……, class 参数化类型名n> class 类名 {数据成员定义; 成员函数原型; …… }; 11.6.4 类模板 在模板类的外部定义类模板的成员函数时的格 式: ? 首先要使用类模板的头部,以表明类模板定义了几个 参数化类型名; ? 作用域运算符 “::”前面的类名用类模板名代替,而且 也要在尖括号中注明所有类模板的参数化类型名; ? 函数的参数表或者自动变量的定义,则是可以使用参 数化类型名,也可以不使用。 11.6.4 类模板 在类外部定义成员函数的格式如下: template < class T1, ……., class Tn> 返回值类型 类模板名< T1, ……, Tn >::成员 函数名( 参数表 ) {函数体} 11.6.4 类模板 类模板的实例化 函数模板的实例化和函数调用是一起完成的。类 模板的实例化则是对象的实例化一起完成的: ? 在类模板后面的尖括号中,表明取代参数化类型的实 际类型名; ? 再写对象名和构造对象所需要的实参数。 一般化的格式如下: ? 类模板名<类型1, ……, 类型n> 对象名(实参数); 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -116- 例11.13 类模板的定义和实例化的示例。 // 例11.13 类模板 #include <iostream> using namespace std; template <class T1, class T2> class MyClass { T1 x; T2 y; public: MyClass( T1 a, T2 b ); void display( ); }; template < class T1, class T2> MyClass< T1,T2 >::MyClass( T1 a, T2 b ) { x = a; y = b; } 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -117- template <class T1, class T2> void MyClass< T1, T2 >::display( ) { cout<<x<<endl; cout<<y<<endl; } void main() { MyClass<int,float> ss(6,6.6); ss.display(); MyClass<char,char *> ww('x',"A string"); ww.display(); } 11.6 类模板 示例是带有两个参 数化类型的类模板。在类外 部定义成员函数时 ,先要声明使用了两个参数 化类型的名字,然 后,在类模板名后面注明这 个模板要使用这些 名字。再具体的定义成员函 数。 类模板进行两次实 例化,分别输出不同类型的 数据:第一次输出 整型和实型,第二次输出字 符和字符串。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -119- 11.6 类模板 带有确定类型参数的类模板 类模板声明时在尖 括号内除了声明要使用的参 数化类型名外,还可以包括确定类型的类型 名。如: template <class T, int i> class MyStack 在类模板实例化和声明对象时,这个参数i要 用具体的整型值来代替,如: MyStack<int,5> ss; 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -120- 11.6 类模板 例11.14 用类模板来定义栈类。栈是一种先进 后出的数据区。各 种数据都可以定义相应的栈 类。使用类模板后 ,就不需要定义各种不同的 栈类了。 用数组作为栈的存 储体。在创建栈对象时,可 以指定栈的大小。安排一个栈指针top指向栈 顶。定义两个栈的基本操作:进栈(p ush)和 出栈(pop)。定义相应的类模板,并测试其 功能。 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -121- //例11.14 栈类模板 //例11.14:11_14.cpp #include <iostream> using namespace std; template <class T, int i> //类模板定 义 class MyStack { T StackBuffer[i]; int size; int top; public: MyStack( void ) : size( i ) {top = i;}; void push( const T item ); T pop( void ); }; 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -122- template <class T, int i> //push成员函数定 义 void MyStack< T, i >::push( const T item ) { if( top >0 ) StackBuffer[--top] = item; else cout<<"堆栈溢出错误."<<endl; return; } 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -123- template <class T, int i> //pop成员函数定 义 T MyStack< T, i >::pop( void ) { if( top < i ) return StackBuffer[top++]; else cout<< "堆栈无数据可以出栈."<<endl; exit(0); } 2005-4-28 北京邮电大学电信工程学院计算机技术中心 -124- void main() //类模板测试程序 {MyStack<int,5> ss; ss.push(4); cout<<ss.pop()<<endl; MyStack<char*,5> ww; ww.push("China"); cout<<ww.pop()<<endl; cout<<ww.pop()<<endl; } 程序运行结果是: 4 China 堆栈无数据可以出栈. 总结 多态性是面向对象程序设计最重要的特 点之一。本章介绍了多态性中最重要的 两个表现:运行多态和参数多态。参数 多态就是模板的使用。 运行多态的表现就是: ? 一种形态的语句:通过基类指针访问基类和 派生类的同名函数; ? 多种条件的执行:用不同派生类对象的地址 初始化这个基类指针; 总结 模板分为函数模板和类模板,定义的方 式基本是相同的。在使用上稍有差别: 函数模板通过函数参数的虚实结合就能 得到具体的模板函数。 而使用类模板时,要在类模板名后面具 体说明模板类的实际数据类型。