在类的定义中,前面带有关键字virtual的成员函数称为虚函数。如下形式:
class base{ virtual int get(); // 虚函数 } int base::get(){}对于虚函数,需要注意的几点是:
virtual关键字只用在类定义里的函数声明中,函数的具体实现时不使用该关键字;构造函数和静态成员函数(带有static关键字)不能是虚函数。以上是虚函数的简要内容,后面部分会介绍关于虚函数的详细内容。
在 C + + {\rm C++} C++中,多态存在于类与类之间存在继承关系时的情况。多态的实质是在调用成员函数时,根据调用函数的对象的不同类型来执行不同的函数。多态的表现形式之一(通过基类的指针实现)如下:
派生类的指针可以赋给基类指针;通过基类指针调用基类和派生类中的同名虚函数时:若该指针指向一个基类的对象,那么被调用的是基类的虚函数;若该指针指向一个派生类的对象,那么被调用的是派生类的虚函数。这就是上述提到的根据对象的类型来执行不同的函数。上面仅用文字介绍了多态的内容,下面以一个实例来说明多态机制:
class CBase { public: virtual void SomeVirtualFunction() {} }; class CDerived :public CBase { public: virtual void SomeVirtualFunction() {} }; int main() { CDerived ODerived; CBase* p = &ODerived; // 调用哪一个虚函数取决于p所指向的内容 // 具体到该程序,该虚函数来自于派生类 p->SomeVirtualFunction(); return 0; }上述指针p指向派生类CDerived的对象,所以p调用的是派生类CDerived的虚函数。上面介绍的是多态的其中一种形式,多态还存在其他的表现形式:
派生类的对象可以赋给基类的引用;通过基类引用调用基类和派生类中的同名虚函数时:如该引用引用的是一个基类的对象,那么被调用的是基类的虚函数;若该引用引用的是一个派生类的对象,那么被调用的是派生类的虚函数。 class CBase { public: virtual void SomeVirtualFunction() {} }; class CDerived :public CBase { public: virtual void SomeVirtualFunction() {} }; int main() { CDerived ODerived; CBase& r = ODerived; // 调用哪一个虚函数取决于r所引用的内容 // 具体到该程序,该虚函数来自于派生类 r.SomeVirtualFunction(); return 0; }上面是多态的两种典型形式,接下来来看一个综合的例子:
class A { public: virtual void Print() { cout << "A::Print" << endl; } }; class B :public A { public: virtual void Print() { cout << "B::Print" << endl; } }; class C :public A { public: virtual void Print() { cout << "C::Print" << endl; } }; class D :public B { public: virtual void Print() { cout << "D::Print" << endl; } }; int main() { // 为每一个类定义一个对象 A a; B b; C c; D d; // 为每一个对象定义一个指针 A* pa = &a; B* pb = &b; C* pc = &c; D* pd = &d; // 输出语句 pa->Print(); // A::Print pa = pb; pa->Print(); // B::Print pa = pc; pa->Print(); // C::Print pa = pd; pa->Print(); // D::Print return 0; }我们可以从以上程序总结出多态的作用:在面向对象的程序设计中使用多态,能够增强程序的可扩充性,即程序需要修改或增加功能的时候,需要改动和增加的代码较少。下一部分将用两个实际例子来说明多态的用法和作用。
本部分介绍基于多态实现的两个 C + + {\rm C++} C++案例,二者均来自参考部分的课程。
游戏介绍请参考百度百科。现在,游戏中有很多种怪物,每种怪物都是一个类与之对应,每个具体的怪物就是一个类的对象。如士兵类CSoldier、凤凰类CPhonex、狼类CWolf、龙类CDragon和天使类CAngel。并且,怪物间能够相互攻击,攻击敌人和被攻击时都有相应的动作,而这些动作都是通过类的成员函数实现。每个类的成员变量对应于怪物的属性,如生命值、攻击力等。在游戏版本升级时,要增加新的怪物——雷鸟。这就要考虑如何编写代码使得在新添加一个类CThunderBird时,程序总体的改动很小。
这里,为每个怪物编写三个表示动作的成员函数,Attack、FightBack和Hurted。Attack表示攻击动作,在调用Attack函数时对应于调用被攻击者的Hurted函数,被攻击者的生命值减少,同时也调用被攻击者的FightBack的函数,表示对攻击者的反击;FightBack表示被攻击时的反击动作;Hurted表示被攻击时的动作,同时减少生命值。这里,我们创建基类CCreature,所有怪物类都继承自该基类。
// 基类CCreature class CCreature { protected: int m_nLifeValue, m_nPower; // 怪物的生命值和攻击力 public: // 多态,将成员函数定义为虚函数 virtual void Attack(CCreature* pCreature) {} // 攻击动作 virtual void Hurted(int nPower) {} // 受伤动作 virtual void FightBack(CCreature* pCreature) {} // 反击动作 }; // 我们以其中一个类举例说明 class CSoldier :public CCreature { public: // 多态 virtual void Attack(CCreature* pCreature) {} virtual void Hurted(int nPower) {} virtual void FightBack(CCreature* pCreature) {} }; void CSoldier::Attack(CCreature* p) { // ... // 该处写表示攻击动作的代码 // ... p->Hurted(m_nPower); // p表示被攻击对象 p->FightBack(this); // 被攻击对象反击 } void CSoldier::Hurted(int nPower) { // ... // 该处写表示受伤动作的代码 // ... m_nLifeValue -= nPower; // 被攻击时,生命值减少 } void CSoldier::FightBack(CCreature* p) { // ... // 该处写表示反击动作的代码 // ... p->Hurted(m_nPower / 2); // 以攻击者的1/2攻击力作为被攻击者反击时的攻击力 }由上述代码可知,当我们需要增加一个CThunderBird类时,仅在类中改变基类的对应成员函数的实现方式即可,而不必修改原来的代码。上面代码的使用方式如下:
// 声明怪物对象 CSoldier Soldier; CPhonex Phonex; CWolf Wolf; // 士兵攻击凤凰和狼 Soldier.Attack(& Phonex); Soldier.Attack(& Wolf); void CSoldier::Attack(CCreature* p) { // ... // 该处写表示攻击动作的代码 // ... p->Hurted(m_nPower); // p表示被攻击对象 p->FightBack(this); // 被攻击对象反击 }观察上面士兵攻击凤凰和狼的代码部分,我们在前面提到,通过基类引用调用基类和派生类中的同名虚函数时:如该引用引用的是一个基类的对象,那么被调用的是基类的虚函数;若该引用引用的是一个派生类的对象,那么被调用的是派生类的虚函数。这里在语句Soldier.Attack(& Phonex),对应于函数中的语句p->Hurted(m_nPower),由于这里p表示对象Phonex,所以这里调用的Hurted和FightBack都是CPhonex类的成员函数,这就是多态的实现。
几何形体处理程序实现的功能如下,输入若干个几何形体的参数,要求按面积排序输出,并且在输出中指明具体的几何形体。输入的规则如下:输入的第一行表示几何形体的数目 n {\rm n} n,接着下面输入 n {\rm n} n行值,每一行以一个字母 c {\rm c} c开头。如果 c {\rm c} c是 ′ R ′ {\rm 'R'} ′R′,则表示一个矩形,本行后面跟两个整数分别表示举行的宽和高;若干 c {\rm c} c是 ′ C ′ {\rm 'C'} ′C′,则表示一个圆,本行后面跟一个整数表示圆的半径;如果 c {\rm c} c是 ′ T ′ {\rm 'T'} ′T′,则表示一个三角形,本行后面紧跟三个整数表示三角形三条边的长度。输出的规则如下:按面积从小到大依次输出每个几何形体的种类及面积。每行输出一个几何形体,输出的具体格式为 形体名称:面积。如:
// 输入 3 // 表示一共输入三个几何形体 R 3 5 // 矩形,其宽和高分别为3和5 C 9 // 圆,其半径为9 T 3 4 5 // 三角形,其三条边分别为3 4 5 // 经程序处理后,每个形体的面积以此为:矩形为6,圆为254.34,三角形为15. // 输出 Triangle: 6 Rectangle: 15 Circle: 254.34与上一个例子相似,我们首先定义一个基类CShape,所以几何形体类都继承自该基类。所有形体的共同点有:求面积和打印信息。而由于不同形体计算面积的方式不同,我们在各自的几何形体类中具体实现这两个函数,这就涉及到多态。
// 基类 class CShape { public: virtual double Area() = 0; // 纯虚函数 virtual void PrintInfo() = 0; }; // 矩形类 class CRectangle :public CShape { public: int w, h; virtual double Area(); virtual void PrintInfo();; }; // 圆类 class CCircle :public CShape { public: int r; virtual double Area(); virtual void PrintInfo();; }; // 三角形类 class CTriangle :public CShape { public: int a, b, c; virtual double Area(); virtual void PrintInfo();; }; // 矩形 double CRectangle::Area() { return w * h; } void CRectangle::PrintInfo() { cout << "Rectangle: " << Area() << endl; } // 圆 double CCircle::Area() { return 3.14 * r * r; } void CCircle::PrintInfo() { cout << "Circle: " << Area() << endl; } // 三角形 double CTriangle::Area() { double p = (a + b + c) / 2.0; return sqrt(p * (p - a) * (p - b) * (p - c)); } void CTriangle::PrintInfo() { cout << "Triangle: " << Area() << endl; }在上面程序中,由于在基类中不涉及计算面积和输出信息的具体实现,我们将对应成员函数定义为纯虚函数。同时,由于我们需要对输出结果按面积排序,我们先定义自己的比较函数以作为函数qsort的参数。qsort比较函数的某一位参数可以让我们自定义排序的类型,具体的写法可参考网上相关资料。
// 直接声明为n的最大值 CShape* pShapes[100]; // 由于输出结果需要对面积排序,这里我们定义比较函数 int MyCompare(const void* s1, const void* s2) { // 我们注意到MyCompare参数的类型是void*,所以其不能直接位于赋值 // 运算符的右边。我们再使用一个*来取对应的值,即**s。 double a1, a2; CShape** p1; CShape** p2; // 强制类型转换 p1 = (CShape**)s1; p2 = (CShape**)s2; a1 = (*p1)->Area(); a2 = (*p2)->Area(); // 根据面积的大小关系返回不同的值 if (a1 < a2) { return -1; } else if (a1 > a2) { return 1; } else { return 0; } }下面是主函数的实现:
int main() { int n; CRectangle* pr; CCircle* pc; CTriangle* pt; // 从键盘输入总的几何形体数 cin >> n; // 使用循环输入剩余内容 for (int i = 0; i < n; ++i) { char c; // 根据输入字符确定几何形体 cin >> c; switch (c) { case 'R': pr = new CRectangle(); // 输入宽和高 cin >> pr->w >> pr->h; pShapes[i] = pr; break; case 'C': pc = new CCircle(); // 圆的半径 cin >> pc->r; pShapes[i] = pc; break; case 'T': pt = new CTriangle(); // 输入三角形三条边的边长 cin >> pt->a >> pt->b >> pt->c; pShapes[i] = pt; break; default: break; } } // 对结果排序 qsort(pShapes, n, sizeof(CShape*), MyCompare); // 输出 for (int i = 0; i < n; ++i) { pShapes[i]->PrintInfo(); } return 0; }注意,派生类和基类中的同名同参数表的函数,不加virtual关键字也自动成为虚函数。最后给出在构造函数和析构函数中调用虚函数的性质:
在非构造函数和非析构函数的成员函数中调用虚函数,即是多态;在构造函数和析构函数中调用虚函数,不是多态。编译时即可确定,调用的函数是自己的类或基类中定义的函数,不会等到运行时才决定调用的函数是自己的还是派生类的。由前面的内容,我们可以总结出多态的相关内容:使用多态时,程序不必为每一个派生类都编写函数调用,派生类可以只关注本类中对应函数的实现功能,做到基类函数的同一接口的不同实现方式,大大提高了程序的可复用性;同时,基类也可以调用派生类的成员,即向后兼容,提高了程序的可扩充性和可维护性。后一点内容我们在以后的文章中会介绍。总之,多态是面向过程编程和面向对象编程的显著差异之一。灵活使用多态对于编写健壮、可扩充、可复用的程序至关重要。
