文章目录

  • 1. 虚函数
  • 2. 多态
  • 3. 基于多态的实例
    • 3.1 魔法门之英雄无敌
    • 3.2 几何形体处理程序
  • 4. 总结
  • 参考

1. 虚函数

在类的定义中,前面带有关键字virtual的成员函数称为虚函数。如下形式:

class base{virtual int get();    // 虚函数
}
int base::get(){}

对于虚函数,需要注意的几点是:

  • virtual关键字只用在类定义里的函数声明中,函数的具体实现时不使用该关键字;
  • 构造函数和静态成员函数(带有static关键字)不能是虚函数。

以上是虚函数的简要内容,后面部分会介绍关于虚函数的详细内容。


2. 多态

在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::Printpa = pb;pa->Print(); // B::Printpa = pc;pa->Print(); // C::Printpa = pd;pa->Print(); // D::Printreturn 0;
}

我们可以从以上程序总结出多态的作用:在面向对象的程序设计中使用多态,能够增强程序的可扩充性,即程序需要修改或增加功能的时候,需要改动和增加的代码较少。下一部分将用两个实际例子来说明多态的用法和作用。


3. 基于多态的实例

本部分介绍基于多态实现的两个C++{\rm C++}C++案例,二者均来自参考部分的课程。

3.1 魔法门之英雄无敌

游戏介绍请参考百度百科。现在,游戏中有很多种怪物,每种怪物都是一个类与之对应,每个具体的怪物就是一个类的对象。如士兵类CSoldier、凤凰类CPhonex、狼类CWolf、龙类CDragon和天使类CAngel。并且,怪物间能够相互攻击,攻击敌人和被攻击时都有相应的动作,而这些动作都是通过类的成员函数实现。每个类的成员变量对应于怪物的属性,如生命值、攻击力等。在游戏版本升级时,要增加新的怪物——雷鸟。这就要考虑如何编写代码使得在新添加一个类CThunderBird时,程序总体的改动很小。

这里,为每个怪物编写三个表示动作的成员函数,AttackFightBackHurtedAttack表示攻击动作,在调用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,所以这里调用的HurtedFightBack都是CPhonex类的成员函数,这就是多态的实现。

3.2 几何形体处理程序

几何形体处理程序实现的功能如下,输入若干个几何形体的参数,要求按面积排序输出,并且在输出中指明具体的几何形体。输入的规则如下:输入的第一行表示几何形体的数目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关键字也自动成为虚函数。最后给出在构造函数和析构函数中调用虚函数的性质:

  • 在非构造函数和非析构函数的成员函数中调用虚函数,即是多态;
  • 在构造函数和析构函数中调用虚函数,不是多态。编译时即可确定,调用的函数是自己的类或基类中定义的函数,不会等到运行时才决定调用的函数是自己的还是派生类的。

4. 总结

由前面的内容,我们可以总结出多态的相关内容:使用多态时,程序不必为每一个派生类都编写函数调用,派生类可以只关注本类中对应函数的实现功能,做到基类函数的同一接口的不同实现方式,大大提高了程序的可复用性;同时,基类也可以调用派生类的成员,即向后兼容,提高了程序的可扩充性和可维护性。后一点内容我们在以后的文章中会介绍。总之,多态是面向过程编程和面向对象编程的显著差异之一。灵活使用多态对于编写健壮、可扩充、可复用的程序至关重要。


参考

  1. 北京大学公开课:程序设计与算法(三)C++面向对象程序设计.


(八)C++学习 | 虚函数和多态的基本概念相关推荐

  1. 子类重写父类虚函数_C/C++编程笔记:关于C++的虚函数和多态,你真的了解吗?...

    前言 本章节主要针对于C++中的虚函数和多态做一个详细介绍. 虚函数 虚函数的长相其实很简单,在C++类型用virtual修饰的函数就是虚函数,如下代码: 虚函数对于本类的影响:存在虚函数类的内存会多 ...

  2. C++ 面向对象(二)多态 : 虚函数、多态原理、抽象类、虚函数表、继承与虚函数表

    目录 多态 多态的概念 多态的构成条件 虚函数 虚函数的重写 协变(返回值不同) 析构函数的重写(函数名不同) final和override final override 重载, 重写, 重定义对比 ...

  3. 初入c++(六)虚函数实现多态,虚析构函数,虚函数表和多态实现机制,纯虚函数。

    1.c++多态的概念以及用途. 1.1虚函数实现多态 通过基类指针只能够访问派生类的成员变量,不能够访问派生类的成员函数. 解决问题的办法:使用虚函数(virtual function),只需要在函数 ...

  4. (转) 用虚函数实现多态

    http://www.cnblogs.com/nihaoCPP/p/4422682.html 用虚函数实现多态 在理解了面向对象的继承机制之后,我们知道了在大多数情况下派生类是基类的"一种& ...

  5. C++中虚函数与多态实现

    多态,什么是多态?在计算机语言中,多态就是指一个接口或者方法,有多种展现形态.在C++中,通过父类指针调用子类方法,可以让父类指针有多种形态. C++中实现多态的方式有:虚函数,重载,模板,绑定等.此 ...

  6. C语言面向对象编程(三):虚函数与多态

    在< C++ 编程思想>一书中对虚函数的实现机制有详细的描述,一般的编译器通过虚函数表,在编译时插入一段隐藏的代码,保存类型信息和虚函数地址,而在调用时,这段隐藏的代码可以找到和实际对象一 ...

  7. C++虚函数实现多态的机制

    该博文为原创文章,未经博主同意不得转载,如同意转载请注明博文出处 本文章博客地址:https://cplusplus.blog.csdn.net/article/details/105043638 虚 ...

  8. C++编程思想:继承与虚函数以及多态

    文章目录 简介 实现虚函数多态的技术原理 对象切边 析构函数和构造函数中的虚函数 使用继承的类的析构函数应该使用虚函数修饰符 简介 继承与虚函数与多态是浑然一体的三个概念,父类中虚函数可以表现出多态特 ...

  9. C++之关于虚函数和多态的分析和补充

    前文:<Essential C++>笔记之漫游:面向对象编程思维 同时推荐一篇总结得非常清晰的博文:C++ 子类继承父类纯虚函数.虚函数和普通函数的区别   不说废话,直接上程序: 例一( ...

最新文章

  1. 在为时已晚前 阻止物联网安全威胁和攻击
  2. php云开发要去的地方
  3. 云海技术u盘怎么恢复成普通盘_BITLOCKER加密中断数据无法读取恢复一例
  4. 使用Bootstrap后,关于IE与Chrome显示字体的问题
  5. python编译反编译,你不知道的心机与陷阱
  6. 朋友圈加粗字体数字_Excel中Ctrl键与26个字母、10个数字及特殊符号的搭配使用快捷键...
  7. 一些好看的渐变色(配色)网站推荐
  8. 公司天天开会,是不是浪费生命呢?
  9. UWB PDOA brief introduction
  10. 开发案例---微信定时推送:生日祝福
  11. SkyWalking Agent数据采集和上报原理浅析
  12. QT自定义控件--时钟
  13. uefi +gpt 系统安装 和 传统legacy + mbr 的区别
  14. python获取pdf页数_Python读取pdf页面的一部分
  15. 使用python自动生成病例数据并导出excel(附代码)
  16. 鸿蒙系统什么时候投入市场,华为鸿蒙系统市场占有率要达多少才可能存活
  17. 使用飞信API 发送 短信
  18. 如何系统地学习 C++ 语言?
  19. JS验证身份证号地区码及最后一位校验码
  20. C 语言实例 - 输入n个整数,使其从大到小输出

热门文章

  1. 解决Win系统缺少msvcp100.dll无法启动问题
  2. OpenGL之——多纹理,混合纹理设置渲染
  3. 计算机毕业设计Java云端存储的待办清单的设计(源码+系统+mysql数据库+Lw文档)
  4. iphone 导入出错_iPhone-一切可能出错的地方
  5. linux 查看环境变量
  6. 变色龙常用参数的意义说明
  7. Android中如何把网络资源图片转化成bitmap
  8. POI 获取Word大纲级别,操作Word(2018-06-03更新)
  9. 如何拥有(建)一个自己的网站-服务器建站
  10. (最完美)红米手机4的USB调试模式在哪里开启的经验