本文是中国大学MOOC的郭炜老师网课的笔记,纯本人手打,如果觉得还行,不妨点个赞叭!
在类的定义中,前面有virtual关键字的成员函数就是虚函数:
// 类的定义
class base{
virtual int get();
};
// 函数的实现
int base::get()
{}
virtual 关键字只用在类定义里的函数声明中,写函数体时不用。
多态的表现形式有两种:
派生类的指针可以赋给基类指针。
通过基类指针调用基类和派生类中的同名虚函数时:
这种机制就叫做“多态”。
例如:
class CBase{
public:
virtual void f(){}
};
class CDerived:public CBase{
public:
virtual void f(){}
};
int main(){
CDerived ODerived;
CBase* p = & ODerived;
p->f(); // 调用哪个虚函数取决于p指向哪种类型的对象
return 0;
}
这种机制就叫做“多态”。
例如:
class CBase{
public:
virtual void f(){}
};
class CDerived:public CBase{
public:
virtual void f(){}
};
int main(){
CDerived ODerived;
CBase& r = ODerived;
r.f(); // 调用哪个虚函数取决于r引用哪种类型的对象
return 0;
}
在面向对象的程序设计中使用多态,能够增强程序的可扩充性,即程序需要修改或增加功能的时候,需要改动和增加的代码较少。
总而言之,使用虚函数是实现了一个接口多种方法。
游戏《魔法门之英雄无敌》
游戏中有若干种怪物,每种怪物都有一个类与之对应,每个怪物就是一个对象,例如有CSoldier、CDragon、CPhonex等怪物;
怪物可以互相攻击,攻击敌人和被攻击时都会有相应的动作,动作是通过对象的成员函数实现的;
现在游戏升级,要增加新的怪物–CThunderBird。
那么问题来了,如何编程能使升级时代码的改动和增加量较小?
为每个怪物类编写Attack、FightBack和 Hurted成员函数:
Attact函数表现攻击动作,攻击某个怪物,并调用被攻击怪物的Hurted函数,以减少被攻击怪物的生命值,同时也调用被攻击怪物的FightBack成员函数,遭受被攻击怪物反击。
Hurted函数减少自身生命值,并表现受伤动作。
FightBack成员函数表现反击动作,并调用被反击对象的Hurted成员函数,使被反击对象受伤。
然后设置基类CCreature,并且使CDragon、CWolf等其它类都从该类派生而来;
这样的话,有n种怪物,CDragon类中就有n个Attack成员函数,以及n个FightBack成员函数,对于其它类也是如此。
如果新增其它类,那么所有的类都需要增加两个成员函数
void Attack( CThunderBird * pThunderBird);
void FightBack( CThunderBird * pThunderBird) ;
在怪物种类较多时,工作量很大。
思路:
基类只有一个Attack成员函数,也只有一个FightBack成员函数,所有CCreature的派生类也是这样。
假如现在CDragon要攻击CPhonex,那么将调用CDragon::Attack(&CPhonex);
函数
上述函数的实现形式是这样的:
CDragon::Attack(CCreature* p){
// 攻击的动作
p->Hurted(m_nPower); // 多态
p->FightBack(this); // 多态
}
基类的指针p指向CPhonex这一派生类,根据多态,p->Hurted()调用的是CPhonex的Hurted()函数
CPhonex的受伤函数如下:
CPhonex::Hurted(int nPower){
// 受伤的动作
m_nLifeValue -= nPower;
}
接着,p->FightBack(this)调用的是CPhonex的FightBack()函数
成员函数通过一个名为this的隐式额外参数来访问调用它的对象。this参数是一个常量指针,被初始化为调用该函数的对象地址。在函数体内可以显式使用this指针。
默认情况下,this的类型是指向类类型非常量版本的常量指针。
CPhonex的反击函数的实现形式如下:
CPhonex::FightBack(CCreature* p){
// 反击的动作
p->Hurted(m_nPower/2); // 多态
}
由前面可以知道,this传入的是调用CDragon::Attack的对象地址,那么此时基类的指针p指向CDragon这一派生类,根据多态,p->Hurted()调用的是CDragon的Hurted()函数。
// CCreature.h
#include
class CCreature{
protected:
int nPower;
int nLifeValue;
std::string m_Name;
public:
virtual std::string My_Name() = 0;
virtual void Attack(CCreature* pCreature){}
virtual void Hurted(int nPower){}
virtual void FightBack(CCreature* pCreature){}
};
class CDragon : public CCreature{ // 龙
protected:
int m_nPower = 50;
int m_nLifeValue = 180;
std::string m_Name;
public:
CDragon(std::string name);
std::string My_Name();
void print_name();
void Attack(CCreature* pCreature);
void Hurted(int nPower);
void FightBack(CCreature* pCreature);
};
class CPhonex : public CCreature{ // 凤凰
protected:
int m_nPower = 60;
int m_nLifeValue = 160;
std::string m_Name;
public:
CPhonex(std::string str) {m_Name = str;}
std::string My_Name();
void Attack(CCreature* pCreature);
void Hurted(int nPower);
void FightBack(CCreature* pCreature);
};
class CThunderBird : public CCreature{ // 雷鸟
protected:
int m_nPower = 30;
int m_nLifeValue = 120;
std::string m_Name;
public:
CThunderBird(std::string str) {m_Name = str;}
std::string My_Name();
void Attack(CCreature* pCreature);
void Hurted(int nPower);
void FightBack(CCreature* pCreature);
};
// CCreature.cpp
#include "CCreature.h"
using namespace std;
// 构造函数实现
CDragon::CDragon(std::string name){
cout << "CDragon has been created: " << name << endl;
m_Name = name;
cout << "m_Name = " << m_Name << endl;
}
// Dragon虚函数的实现
void CDragon::Attack(CCreature* p){
printf("Now, %s Attacking %s.\n", m_Name.c_str(), p->My_Name().c_str());
p->Hurted(m_nPower);
p->FightBack(this);
}
void CDragon::Hurted(int nPower){
printf("Now, %s has been hurted.\n", m_Name.c_str());
m_nLifeValue -= nPower;
printf("Now, %s's LifeValue is %d.\n", m_Name.c_str(), m_nLifeValue);
}
void CDragon::FightBack(CCreature* p){
printf("Now, %s fightback.\n", m_Name.c_str());
p->Hurted(m_nPower/2);
}
void CDragon::print_name(){
printf("my name is %s.\n", m_Name.c_str());
}
// Phonex虚函数的实现
void CPhonex::Attack(CCreature* p){
printf("Now, %s Attacking.\n", m_Name.c_str());
p->Hurted(m_nPower);
p->FightBack(this);
}
void CPhonex::Hurted(int nPower){
printf("Now, %s has been hurted.\n", m_Name.c_str());
m_nLifeValue -= nPower;
printf("Now, %s's LifeValue is %d.\n", m_Name.c_str(), m_nLifeValue);
}
void CPhonex::FightBack(CCreature* p){
printf("Now, %s fightback.\n", m_Name.c_str());
p->Hurted(m_nPower/2);
}
// ThunderBird虚函数的实现
void CThunderBird::Attack(CCreature* p){
printf("Now, %s Attacking.\n", m_Name.c_str());
p->Hurted(m_nPower);
p->FightBack(this);
}
void CThunderBird::Hurted(int nPower){
printf("Now, %s has been hurted.\n", m_Name.c_str());
m_nLifeValue -= nPower;
printf("Now, %s's LifeValue is %d.\n", m_Name.c_str(), m_nLifeValue);
}
void CThunderBird::FightBack(CCreature* p){
printf("Now, %s fightback.\n", m_Name.c_str());
p->Hurted(m_nPower/2);
}
std::string CDragon::My_Name(){
return m_Name;
}
std::string CPhonex::My_Name(){
return m_Name;
}
std::string CThunderBird::My_Name(){
return m_Name;
}
// main.cpp
#include "CCreature.h"
using namespace std;
// 基类只有一个Attack成员函数,也只有一个FightBack成员函数
// 所有CCreature的派生类也是这样
int main(){
CDragon D1("dragon_1"), D2("dragon_2");
D1.print_name();
D2.print_name();
CPhonex P1("phonex_1");
CThunderBird T1("thunderbrid_1");
D1.Attack(&P1);
D1.Attack(&T1);
D1.Attack(&D2);
return 0;
}
问题
几何形体处理程序:输入若干个几何形体的参数,要求按面积排序输出。输出时要指明形状。
Input:
第一行是几何形体数目n(不超过100).下面有n行,每行以一个字母c开头.
若c是‘R’,则代表一个矩形,本行后面跟着两个整数,分别是矩形的宽和高;
若c是’C’,则代表一个圆,本行后面跟着一个整数代表其半径
若c是‘T’,则代表一个三角形,本行后面跟着三个整数,代表三条边的长度
Output:
按面积从小到大依次输出每个几何形体的种类及面积。每行一个几何形体,输出格式为:
形体名称:面积
例如:
Sample Input:
3
R 3 5
C 9
T 3 4 5
Sample Output:
Triangle: 6
Rectangle: 15
Circle: 254.34
思路
用基类指针数组存放指向各种派生类对象的指针,然后遍历该数组,就能按面积从小到大依次输出每个几何体的种类及其面积。
用sort,建立cmp()函数,利用基类指针指向派生类对象的Area()来比较。
代码
#include
#include
#include
#include
using namespace std;
class CShape{
public:
virtual double Area() = 0; // 纯虚函数
virtual void PrintInfo() = 0;
};
class CTriangle : public CShape{
public:
CTriangle(double x, double y, double z) {a = x, b = y, c = z;}
double Area(){
double p = (a + b + c) / 2.0;
return sqrt(p * (p-a) * (p-b) * (p-c));
}
void PrintInfo(){
cout << "Triangle's Area is " << Area() << endl;
}
private:
double a, b, c;
};
class CCricle : public CShape{
public:
CCricle(double a) {r = a;}
double Area(){
return 3.14 * r * r;
}
void PrintInfo(){
cout << "Cricle's Area is " << Area() << endl;
}
private:
double r;
};
class CRectangle : public CShape{
public:
CRectangle(double a, double b) {width = a, hight = b;}
double Area(){
return width * hight;
}
void PrintInfo(){
cout << "rectangle's Area is " << Area() << endl;
}
private:
double width, hight;
};
// CShape* pShapes[100]; // 用基类指针数组存放指向各种派生类对象的指针,然后遍历该数组,就能对各个派生类对象做各种操作,是很常见的做法
vector<CShape*> vec;
bool cmp(CShape* s1, CShape* s2){
return s1->Area() < s2->Area(); // 此句为多态,s1的类型是CShape*,是基类指针
}
int main(){
int n;
cin >> n;
CRectangle* pr;
CCricle* pc;
CTriangle* pt;
for (int i = 0; i < n; i++){
char c;
cin >> c;
switch(c){
case 'R':
double width, hight;
cin >> width >> hight;
pr = new CRectangle(width, hight);
// pShapes[i] = pr;
vec.push_back(pr);
break;
case 'T':
double a, b, c;
cin >> a >> b >> c;
pt = new CTriangle(a, b, c);
// pShapes[i] = pt;
vec.push_back(pt);
break;
case 'C':
double r;
cin >> r;
pc = new CCricle(r);
// pShapes[i] = pc;
vec.push_back(pc);
break;
}
}
// sort(pShapes, pShapes + n, cmp);
// for (int i = 0; i < n; i++){
// vec.push_back(pShapes[i]);
// pShapes[i]->PrintInfo();
// }
sort(vec.begin(), vec.end(), cmp);
for (int i = 0; i < n; i++){
vec[i]->PrintInfo();
}
return 0;
}
PS:
在构造函数和析构函数中调用虚函数,不是多态。编译时即可确定,调用的函数是自己的类或基类中定义的函数,不会等到运行时才决定调用自己的还是派生的函数。
指向基类的指针可以指向派生类对象,当基类指针指向派生类对象时,这种指针只能访问派生对象从基类继承而来的那些成员,不能访问子类特有的元素;
例如:有基类B和从B派生的子类D,则B *p; D dd; p = ⅆ
此时指针p只能访问从基类派生而来的成员,不能访问派生类D特有的成员,因为基类不知道派生类中的这些成员。
多态的关键在于通过基类指针或引用调用一个虚函数时,编译时不确定到底调用的是基类还是派生类的函数,运行时才确定。
—这一过程叫“动态联编”
动态联编如何实现的呢?
虚函数表
每一个有虚函数的类(或有虚函数的类的派生类)都有一个虚函数表,该类的任何对象中都放着虚函数表的指针。
虚函数表中列出了该类的虚函数表地址,多出来的4个字节就是用来放虚函数表的地址的。
通过基类的指针删除派生类对象时,通常情况下只调用基类的析构函数,但是,删除一个派生类的对象时,应该先调用派生类的析构函数,然后调用基类的析构函数。
解决方法:把基类的析构函数声明为virtual
- 派生类的析构函数可以virtual不进行声明
- 通过基类的指针删除派生类对象时,首先调用派生类的析构函数,然后调用基类的析构函数
一般来说,一个类如果定义了虚函数,则应该将析构函数也定义成虚函数。或者,一个类打算作为基类使用,也应该将析构函数定义为虚函数。
PS:不允许以虚函数作为构造函数
例子
#include
using namespace std;
class Base{
public:
int i;
virtual void print(){
cout << "Base";
}
virtual ~Base(){cout << "Bye from Base!" << endl;};
// 如果这里不加virtual关键字,那么通过基类的指针删除派生类对象时,只调用基类的析构函数
};
class Derived : public Base{
public:
int n;
virtual void print(){
cout << "Derived";
}
virtual ~Derived(){cout << "Bye from Derived!" << endl;};
};
int main(){
Base* pb;
pb = new Derived();
delete pb;
return 0;
}
结果:
Bye from Derived!
Bye from Base!
虚析构函数解决了通过基类指针去delete派生类对象的问题
纯虚函数:没有函数体的虚函数
抽象类:包含纯虚函数的类
抽象类只能作为基类来派生新类使用,不能创建抽象类的对象
抽象类的指针和引用可以指向由抽象类派生出来的类的对象
假设A是抽象类:
A a; // 错,A是抽象类,不可以创建对象
A *pa; // ok,可以定义抽象类的指针和引用
pa = new A; // 错误,A是抽象类,不能创建对象
如果一个类从抽象类派生而来,那么当且仅当它实现了基类的所有纯虚函数,它才能成为非抽象类。