答案 0 :(得分:51)
如果要覆盖派生类的某个行为(读取方法)而不是为基类实现的行为(并且希望在运行时通过指向基类的指针),则使用虚函数。
经典示例是当您有一个名为Shape
的基类和从中派生出来的具体形状(类)时。每个具体类都覆盖(实现一个名为Draw()
的虚方法)。
类层次结构如下:
以下代码段显示了该示例的用法;它创建了一个Shape
类指针数组,其中每个指针都指向一个不同的派生类对象。在运行时,调用Draw()
方法会导致调用该派生类重写的方法,并绘制(或呈现)特定的Shape
。
Shape *basep[] = { &line_obj, &tri_obj,
&rect_obj, &cir_obj};
for (i = 0; i < NO_PICTURES; i++)
basep[i] -> Draw ();
上述程序只使用指向基类的指针来存储派生类对象的地址。这提供了松耦合,因为如果随时添加新的具体派生类shape
,程序不必急剧改变。原因是实际使用(依赖)具体Shape
类型的代码段最少。
以上是着名Open Closed Principle设计原则的SOLID的一个很好的例子。
答案 1 :(得分:22)
当您需要以相同方式处理不同对象时,可以使用虚函数。它被称为多态性。让我们假设您有一些基类 - 类似于经典的形状:
class Shape
{
public:
virtual void draw() = 0;
virtual ~Shape() {}
};
class Rectange: public Shape
{
public:
void draw() { // draw rectangle here }
};
class Circle: public Shape
{
public:
void draw() { // draw circle here }
};
现在你可以拥有不同形状的矢量:
vector<Shape*> shapes;
shapes.push_back(new Rectangle());
shapes.push_back(new Circle());
你可以画出这样的形状:
for(vector<Shape*>::iterator i = shapes.begin(); i != shapes.end(); i++)
{
(*i)->draw();
}
通过这种方式,您可以使用一个虚拟方法绘制不同的形状 - draw()。根据有关指针后面对象类型的运行时信息选择正确的方法版本。
<强>通知强> 当您使用虚函数时,您可以将它们声明为纯虚拟(就像在类Shape中一样,只需在方法proto之后放置“= 0”)。在这种情况下,您将无法使用纯虚函数创建对象实例,它将被称为Abstract类。
在析构函数之前还要注意“虚拟”。如果您计划通过指向其基类的指针来处理对象,则应该声明析构函数为virtual,因此当您为基类指针调用“delete”时,将调用所有析构函数链并且不会发生内存泄漏。
答案 2 :(得分:20)
想想动物类,从中衍生出来的是猫,狗和牛。动物类有一个
virtual void SaySomething()
{
cout << "Something";
}
功能
Animal *a;
a = new Dog();
a->SaySomething();
不应该打印“Something”,狗应该说“Bark”,猫应该说“Meow”。在这个例子中,你看到a是一只狗,但有时候你有一个动物指针并且不知道它是哪种动物。你不想知道它是哪种动物,你只想让动物说些什么。所以你只需要调用虚函数,猫会说“喵”,狗就会说“吠”。
当然,SaySomething函数应该是纯虚拟的,以避免可能的错误。
答案 3 :(得分:1)
您将使用虚函数来实现“多态”,特别是在您有对象的情况下,不知道实际的底层类型是什么,但知道您要对其执行什么操作,以及此实现(它是如何做的)取决于你实际拥有的类型。
基本上通常所谓的“利斯科夫替代原则”是以芭芭拉·莱斯科夫(Barbara Liskov)的名字命名的,他在1983年左右谈到这一点。
在需要使用动态运行时决策的地方,在调用调用函数的代码时,您不知道现在或将来可以通过它传递什么类型,这是一个很好的模型。
这不是唯一的方法。有各种各样的“回调”可以采用“blob”数据,并且你可能有回调表依赖于进来的数据中的标题块,例如:消息处理器。为此,不需要使用虚函数,实际上您可能会使用的是如何仅使用一个条目(例如,只有一个虚函数的类)实现v表的方式。