第 5 章 继承、多态和虚函数 陈哲 副教授 南京航空航天大学 计算机科学与技术学院
5.1 继承 继承是OOP程序设计中很重要的一个方面。继承易于扩充现有类以满足新的应用。将已有的类称之为父类,也称基类,将新产生的类称为子类,也称为导出类或派生类。 导出类不做任何改变地继承了基类中的所有变量和函数(构造函数和析构函数除外),并且还可以增加新的数据成员和函数,从而使导出类比基类更为特殊化。 例5-1: Test类继承Grade类。
class Grade { char letter; float score; void calcGrade( ); public: void setScore(float s) { score = s; calcGrade( ); } float getScore( ) { return score; } char getLetter( ) { return letter; } };
// Definition of member function Grade::calcGrade void Grade::calcGrade( ) { if (score > 89) letter = 'A'; else if (score > 79) letter = 'B'; else if (score > 69) letter = 'C'; else if (score > 59) letter = 'D'; else letter = 'F'; }
class Test : public Grade { int numQuestions; float pointsEach; int numMissed; public: Test( int, int ); };
Test::Test(int q, int m) { float numericGrade; numQuestions = q; //参数q 代表问题的个数,m代表答错的题数 . Test::Test(int q, int m) { float numericGrade; numQuestions = q; numMissed = m; pointsEach = 100.0f / numQuestions; numericGrade = 100.0f - numMissed * pointsEach ; setScore(numericGrade); }
{ int questions, missed; cout << "How many questions ? "; void main( ) { int questions, missed; cout << "How many questions ? "; cin >> questions; cout << "How many questions missed? "; cin >> missed; Test exam(questions, missed); cout.precision(2); cout << "\n The score is " << exam.getScore( ); cout << "\n The grade is " << exam.getLetter( ); } 5-1.cpp
BadBase( ) { x = getVal( ); } // Error }; 上例中,父类中的公有成员在子类中仍是公有的,它们可以和子类中的公有成员一样被访问。但反过来是错误的,基类对象或基类中的某个函数不能调用子类中的函数。 class BadBase { int x; public: BadBase( ) { x = getVal( ); } // Error }; class Derived : public BadBase { int y; Derived( int z ) { y = z; } int getVal( ) { return y; }
5.2 保护成员和类的访问 基类中的保护成员和私有成员比较类似,唯一的区 别是:子类不可访问基类中的私有成员,但可访问 基类中的保护成员。 5.2 保护成员和类的访问 基类中的保护成员和私有成员比较类似,唯一的区 别是:子类不可访问基类中的私有成员,但可访问 基类中的保护成员。 在公有继承或保护继承的情况下,子类能访问基类 的protected成员。 Example: 例 5-2
class Grade { protected: char letter; float score; void calcGrade( ); public: void setScore(float s) { score = s; calcGrade( ); } float getScore( ) { return score; } char getLetter( ) { return letter; } };
class Test : public Grade { int numQuestions; float pointsEach; int numMissed; public: Test( int, int ); void adjustScore( ); // 新增加的函数 };
who //构造函数略 void Test::adjustScore( ) { if ((score - int(score)) >= 0.5f ) score += 0.5 ; calcGrade( ); } who
继承方式 基类成员在子类中的表现 private 1.基类的私有成员在子类中不可访问; 2.基类的保护成员变成了子类中的私有成员; 3.基类的公有成员变成了子类中的私有成员。 protected 2.基类的保护成员变成了子类中的保护成员; 3.基类的公有成员变成了子类中的保护成员。 public 3.基类的公有成员变成了子类中的公有成员。
Example: private base class protected base class public base class x is inaccessible private: y private: z protected: y protected: z public: z private: x protected: y public: z private base class protected base class public base class
注意 如果省略了继承修饰符,那么就是私有继承: class Test : Grade 不要将继承修饰符与成员的访问修饰符相混淆:成员访问修饰符是规定类外语句能否访问类中的成员,而继承修饰符是为了限定基类成员在子类中的表现。
5.3 构造函数和析构函数 当基类和子类都有构造函数时,如果定义一个子类对象,那么首先要调用基类的构造函数,然后再调用子类的构造函数; 析构函数的调用次序与此相反,即先调用子类的析构函数,然后再调用基类的析构函数。 Example: 例 5-3.
class BaseDemo { public: BaseDemo( ) { cout << "In BaseDemo constructor.\n"; } ~BaseDemo( ) { cout << "In BaseDemo destructor.\n"; } }; class DerivedDemo : public BaseDemo { DerivedDemo( ) { cout << "In DerivedDemo constructor.\n"; } ~ DerivedDemo( ) { cout << "In DerivedDemo destructor.\n"; }
void main( ) { } cout << "下面定义一个 DerivedDemo 类对象 \n" ; DerivedDemo object ; cout << "下面将要结束程序 \n" ; } 5-3.cpp
5.3.2 向基类的构造函数传参数 如果基类和子类都有缺省的构造函数,它们的调用是自动完成的,这是一种隐式调用。 5.3.2 向基类的构造函数传参数 如果基类和子类都有缺省的构造函数,它们的调用是自动完成的,这是一种隐式调用。 如果基类的构造函数带有参数,那么必须让子类的构造函数显式调用基类的构造函数,并且向基类构造函数传递适当的参数。 例 5-4
class Rectangle { protected: float width , length, area; public: Rectangle( ) { width = length = area = 0.0f ; } Rectangle ( float w, float l ) { width = w; length = l; area = width * length; } float getArea( ) { return area; } float getLen( ) { return length; } float getWidth( ){ return width; } };
class Cube : public Rectangle { protected: float height, volume; Cube(float, float, float); float getHeight ( ) { return height; } float getVol( ) { return volume; } }; Cube::Cube(float w, float l, float h) : Rectangle(w, l) height = h ; volume = area * height ; } 5-4.cpp
Note 如果基类没有缺省的构造函数,那么子类必须至少具有一个带参的构造函数,以便向基类构造函数传递参数。
5.3.3 初始化列表的作用 1. 如果类之间具有继承关系,子类必须在其初始化列表中调用基类的构造函数。例: class Base { Base( int x ); }; class Derived : public Base Derived(int x, int y): Base(x) { /* … */ }
5.3.3 初始化列表的作用 2. 类中的const常量只能在初始化列表中进行初始化,而不能在函数体内用赋值的方式来初始化。 class Base { const int SIZE ; Base(int size) : SIZE(size) { /* … */ } }; Base one(100);
5.3.3 初始化列表的作用 3. 对象类型的成员的初始化放在初始化列表中,则效率较高,反之较低。基本类型变量的初始化可以在初始化列表中,也可在构造函数中,效率上没区别。 class Base{ Base( ); Base(const Base &other); }; class Derived{ Base B_Member; public: Derived(const Base &a); }; 构造函数的实现: Derived::Derived(const Base & b ) : B_Member(b) { /* … */ } 也可这样实现,但效率较低。 Derived::Derived(const Base &b) { B_Member = b; }
5.4 覆盖基类的函数成员 重载的特点: (1) 重载表现为有多个函数,它们的名字相同,但参数 不全相同; 5.4 覆盖基类的函数成员 重载的特点: (1) 重载表现为有多个函数,它们的名字相同,但参数 不全相同; (2) 重载可以出现在同一个类中,也可出现在具有继承关系的父类与子类中; (3) 重载也可表现为外部函数的形式。
5.4 覆盖基类的函数成员 覆盖的特点: (1) 覆盖一定出现在具有继承关系的基类和子类之间; 5.4 覆盖基类的函数成员 覆盖的特点: (1) 覆盖一定出现在具有继承关系的基类和子类之间; (2) 覆盖除了要求函数名完全相同,还要求相应的参数个数和类型也完全相同 ; (3) 当进行函数调用时,子类对象所调用的是子类中定义的函数; (4) 覆盖是C++多态性的部分体现。
class MileDist { protected: float miles; public: void setDist( float d ) { miles = d; } float getDist( ) { return miles; } }; class FeetDist : public MileDist protected: float feet; void setDist(float); float getDist( ) { return feet; } float getMiles( ) { return miles; }
void FeetDist::setDist(float ft) { feet = ft; MileDist::setDist(feet/5280); //Call base class function } void main( ) { FeetDist feet; float ft; cout << "A distance in feet and convert it to miles:"; cin >> ft; feet.setDist(ft); cout << feet.getDist( ) << " feet equals "; cout << feet.getMiles( ) << " miles.\n"; 哪一个? 5-5.cpp
5.5 虚函数 函数覆盖体现了一定的多态性。但是,简单的函数覆盖并不能称为真正的多态性。 Example: 例 5-7
who class MileDist { protected: float miles; public: void setDist(float d){ miles = d; } float getDist( ) { return miles; } float square( ) { return getDist( ) * getDist( ); } }; who
class FeetDist : public MileDist { protected: float feet; public: void setDist( float ); float getDist( ) { return feet; } float getMiles( ) { return miles; } }; void FeetDist::setDist(float ft ) feet = ft; MileDist::setDist(feet / 5280); }
cout << "请输入以英尺为单位的距离:" ; cin >> ft ; feet.setDist( ft ) ; void main( ) { FeetDist feet; float ft; cout << "请输入以英尺为单位的距离:" ; cin >> ft ; feet.setDist( ft ) ; cout << feet.getDist( ) << " 英尺等于 " ; cout << feet.getMiles( ) << " 英里\n" ; cout << feet.getDist( ) << "英尺的平方等于 " ; cout << feet.square( ) << "平方英尺\n" ; } 5-7.cpp
错误的原因:C++编译器在缺省情况下,对函数成员的调用实施的是静态连编(也称静态绑定)。 解决方法:将getDist设置为虚函数。对于虚函数,编译器完成的是动态连编(也称动态绑定),即对函数的调用是在运行时确定的。 OOP: 覆盖和重载不能体现真正的多态性,只有虚函数才是多态性的表现。不支持多态性的语言,就不能称为OOP。
5.6 纯虚函数和抽象类 纯虚函数是在基类中声明的虚函数,没有函数体,要求继承基类的子类必须覆盖它。 5.6 纯虚函数和抽象类 纯虚函数是在基类中声明的虚函数,没有函数体,要求继承基类的子类必须覆盖它。 带有纯虚函数的类称为抽象类,不能定义抽象类的对象。 派生类可以根据自己的需要,分别覆盖它,从而实现真正意义上的多态性。 格式: virtual void showInfo( ) = 0;
class Student // 例 5-9 { protected: char name[51]; int hours; public: Student( ){ name[0] = hours = 0; } void setName(char *n) { strcpy(name, n); } // Pure virtual function virtual void setHours( ) = 0; virtual void showInfo( ) = 0; };
class CsStudent : public Student { int mathHours , csHours; public: void setMathHours(int mh){ mathHours = mh; } void setCsHours(int csh){ csHours = csh; } void setHours( ) { hours = mathHours + csHours; } void showInfo( ); };
void CsStudent::showInfo( ) { cout << " Name: " << name << endl; cout << "\t Math: " << mathHours << endl; cout << "\t CS : " << csHours; cout << "\n\t Total Hours: " << hours; } void main( ) { CsStudent student1; char chInput[51]; int intInput;
cout << "Enter the following information:\n"; cout << "Name: "; cin.getline(chInput, 51); student1.setName(chInput); cout << "Number of math hours completed: "; cin >> intInput; student1.setMathHours( intInput ); cout << "Number of CS hours completed: "; student1.setCsHours(intInput); student1.setHours( ); cout << "\nSTUDENT INFORMATION\n"; student1.showInfo( ); } 5-9.cpp
关于抽象类和纯虚函数小节 如果一个类包含有纯虚函数,那么它就是抽象类,必须让其它类继承; 基类中的纯虚函数没有代码; 不能定义抽象类的对象,即抽象基类不能实例化; 必须在子类中覆盖基类中的纯虚函数。
5.6.3 指向基类的指针 指向基类对象的指针可以指向其子类的对象; 5.6.3 指向基类的指针 指向基类对象的指针可以指向其子类的对象; 如果子类覆盖了基类中的成员,但通过基类指针所访问的成员仍是基类的成员,而不是子类成员。 Example: 例 5-10
void show( ){ cout << "In Base class.\n"; } }; class Base { public: void show( ){ cout << "In Base class.\n"; } }; class Derived : public Base { void show( ){ cout << "In Derived class.\n"; } void main( ) { Base *bptr; Derived dobject; bptr = &dobject; bptr->show( ); } 1. 调用谁? 2. 什么原因? 3. 如何解决? 4. 引用调用呢? 5-10.cpp
5.7 多重继承 class A class B class C 5.7 多重继承 class A Class C inherits all of Class B's members, including the ones Class B inherited from Class A. class B class C
5.8 多继承 class A class B class C 如果一个子类具有两个或多个直接父类,那么就称为多继承。 5.8 多继承 如果一个子类具有两个或多个直接父类,那么就称为多继承。 class A class B class C Constructors are called in the order they are listed in the first line of the class declaration.
class Date // 例 5-12 { protected: int day , month , year; public: Date(int d, int m, int y) day = d; month = m; year = y; } int getDay( ) { return day; } int getMonth( ) { return month; } int getYear( ) { return year; } };
class Time { protected: int hour , min , sec; public: Time(int h, int m, int s) hour = h; min = m; sec = s; } int getHour( ){ return hour; } int getMin( ) { return min; } int getSec( ) { return sec; } };
class DateTime : public Date, public Time { protected: char dTString[20]; public: DateTime( int, int, int, int, int, int); void getDateTime( char *str) strcpy(str, dTString); } }; 这个顺序很重要
DateTime::DateTime(int yr,int mon,int dy, int hr, int mt, int sc) : Time(hr, mt, sc), Date(dy, mon, yr) { char temp[10]; // In the form YY/MM/DD strcpy(dTString, itoa(getYear( ), temp, 10)); strcat(dTString, "/"); strcat(dTString, itoa(getMonth( ), temp, 10)); strcat(dTString, itoa(getDay( ), temp, 10)); strcat(dTString, " "); strcat(dTString, itoa(getHour( ), temp, 10)); strcat(dTString, ":"); strcat(dTString, itoa(getMin( ), temp, 10)); strcat(dTString, itoa(getSec( ), temp, 10)); } #include <stdlib.h> // itoa函数的用法 void main( ) { int i=12; char s[100]; itoa(i,s,16); cout<<s<<endl; }
pastDay.getDateTime( formatted ); void main( ) { char formatted[20]; DateTime pastDay(28, 7, 2010, 11, 32, 27) ; pastDay.getDateTime( formatted ); cout << formatted << endl; } 5-12.cpp
#include <ctime> #include <iostream> using namespace std; void main( ) { char tmpbuf[128]; // Display operating system-style date and time _strtime( tmpbuf ); cout<< "time:\t"<<tmpbuf<<endl ; _strdate( tmpbuf ); cout<< "date:\t"<<tmpbuf<<endl ; } 获取系统时间 5-a.cpp