前两篇中,我们分别介绍了面向对象的封装和继承两个特性,今天我们来说最后一个特性:多态。
什么是多态
用一句话来概括多态:允许父类的指针指向子类对象。
为什么要用父类的指针去指向子类对象呢?我们想象一个最简单的场景,如果我们需要一个函数的参数是可变数据类型,那如何实现呢?C++是不允许模糊数据类型存在的,这个需求听起来几乎不可能实现。不过有了多态,我们可以把参数类型设置为父类的指针类型,这样在参数传递的时候我们就可以传递这个父类的任意一个子类的对象了。听起来比较乱,我们用一个例子来解释。
先看看下面这段代码:
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void Run() = 0;
virtual void Cry() = 0;
};
class Dog : public Animal
{
void Run()
{
cout << "Dog is running ..." << endl;
}
void Cry()
{
cout << "Dog is crying ..." << endl;
}
};
class Cat : public Animal
{
void Run()
{
cout << "Cat is running ..." << endl;
}
void Cry()
{
cout << "Cat is crying ..." << endl;
}
};
void Functions(Animal* pAnimal)
{
pAnimal->Run();
pAnimal->Cry();
}
int main()
{
Dog dog;
Cat cat;
Functions(&dog);
Functions(&cat);
}
运行结果:
先看Functions()函数,它的参数是一个Animal类指针,Animal是一个抽象类,派生了两个子类:Dog和Cat。在main函数中,我们可以很方便地把这两个子类的对象传给Animal指针。这就是多态。
多态的背后隐藏着伟大的设计模式思想,希望大家慢慢体会。
抽象类
Animal是个抽象类。它的特点是:
- 包含虚函数
- 不能实例化
为什么要用抽象类呢?它的意义仅仅是定义成员函数的外观。在这个例子中,我们定义了两个虚函数:Run()和Cry()。有了这样一个抽象类,我们才能保证Dog和Cat两个子类都拥有一模一样的两个成员函数。否则,Functions()函数就没法正常工作了。
星空问题的新需求
回到星空那段代码,假如我们这个项目需要A,B,C,三个人同时开发。A开发点状星星,B开发矩形的星星,C开发X形的星星。我们要确保三个人能同时开发,完成后能很容易的把三份代码加入项目中,该怎么办呢?
先分析一下代码:
class Star
{
public:
Star(){}
~Star(){}
void Init();
void Move();
protected:
void Draw();
void NewPos();
void Remove();
double m_x = 0;
int m_y;
double m_step;
int m_color;
};
Star类中,影响星星形状的成员函数是Draw()和Remove(),我们可以利用多态把这两个函数抽象出来成为一个抽象类,之后让A,B,C三个人分别实现一个子类,这样就OK了。
来看看代码吧:
#include <graphics.h>
#include <time.h>
#include <conio.h>
#define SCREEN_WIDTH 1024
#define SCREEN_HEIGHT 768
#define MAXSTAR 400
class StarType
{
public:
virtual void Draw(int x, int y, int color) = 0;
virtual void Remove(int x, int y) = 0;
};
class PointStar : public StarType
{
void Draw(int x, int y, int color)
{
putpixel((int)x, y, color);
setcolor(color);
circle(x, y, 1);
}
void Remove(int x, int y)
{
putpixel((int)x, y, 0);
setcolor(0);
circle(x, y, 1);
}
};
class RectStar : public StarType
{
void Draw(int x, int y, int color)
{
putpixel((int)x, y, color);
setcolor(color);
rectangle(x - 1, y - 1, x + 1, y + 1);
}
void Remove(int x, int y)
{
putpixel((int)x, y, 0);
setcolor(0);
rectangle(x - 1, y - 1, x + 1, y + 1);
}
};
class XStar : public StarType
{
void Draw(int x, int y, int color)
{
settextcolor(color);
outtextxy(x, y, _T("x"));
}
void Remove(int x, int y)
{
settextcolor(0);
outtextxy(x, y, _T("x"));
}
};
class Star
{
public:
Star(){}
~Star(){}
void Init();
void Init(StarType* pStarType);
void Move();
protected:
void NewPos();
double m_x = 0;
int m_y;
double m_step;
int m_color;
StarType* m_pStarType;
};
void Star::Init()
{
if (m_x == 0)
{
m_x = rand() % SCREEN_WIDTH;
}
else
{
m_x = 0;
}
m_y = rand() % SCREEN_HEIGHT;
m_step = (rand() % 5000) / 1000.0 + 1;
m_color = (int)(m_step * 255 / 6.0 + 0.5); // 速度越快,颜色越亮
m_color = RGB(m_color, m_color, m_color);
}
void Star::Init(StarType* pStarType)
{
this->Init();
m_pStarType = pStarType;
}
void Star::Move()
{
m_pStarType->Remove(m_x, m_y);
NewPos();
m_pStarType->Draw(m_x, m_y, m_color);
}
void Star::NewPos()
{
m_x += m_step;
if (m_x > SCREEN_WIDTH)
this->Init();
}
void main()
{
srand((unsigned)time(NULL));
initgraph(SCREEN_WIDTH, SCREEN_HEIGHT);
Star star[MAXSTAR];
PointStar pointStar;
RectStar rectStar;
XStar xStar;
for (int i = 0; i < MAXSTAR; i++)
{
switch (i % 3)
{
case 0:
star[i].Init(&pointStar);
break;
case 1:
star[i].Init(&rectStar);
break;
case 2:
star[i].Init(&xStar);
break;
default:
break;
}
}
while (!kbhit())
{
for (int i = 0; i < MAXSTAR; i++)
{
star[i].Move();
}
Sleep(50);
}
closegraph();
}
效果如下:
程序中,我们定义了一个抽象类StarType,之后三个类分别派生与这个抽象类。
我们重载了Star类的Init()方法,当我们要实现不同样子的星星时,就把相应的对象地址通过Init()方法传给Star类保存在m_pStarType属性中。当执行Move函数时,就会通过这个属性调用不同的方法来完成。是不是很神奇呢?
这其实是一种设计模式,假如我们有一天需要再多加几种星星,只需要再多实现几个StarType的派生类就行了,其他代码几乎不用改变。
这部分重在理解编程的思想,希望大家多多思考,有问题欢迎讨论。
到此,我们这个项目就告一段落了,对于一个稍大的项目而言,文件划分非常重要。我会将划分好的代码放在GitHub上,欢迎大家下载源码学习。
我是天花板,让我们一起在软件开发中自我迭代。
如有任何问题,欢迎与我联系。
上一篇:C++代码训练营 | 另一片星空