简 介
缺省情况下,类的成员函数是非虚拟的。当一个成员函数为非虚拟的时候,通过一个类对象(指针或引用)而被调用的该成员函数,是该类对象的静态类型中定义的成员函数。
当成员函数是虚拟的时候,通过指针或引用而被调用的该成员函数,是在该类对象的动态类型中被定义的成员函数。但是,正如所发生的,类对象的静态和动态类型是相同的,所以虚拟函数机制只在使用指针和引用时才会如预期般地起作用。
第一次引入虚拟函数的基类时,必须在类声明中指定virtual关键字。如果虚函数的定义放在类的外面,则不能再次指定关键字virtual。假设有下面的类层次:
class A
{
public:
virtual void foo() { cout << "A::foo() is called" << endl;}
};
class B: public A
{
public:
//备注:只要在基类中已声明为virtual,这里即使不使用virtual关键字,默认也是虚函数
//同样,如果还有从B派生的子类,对应的成员函数也是虚函数
virtual void foo() { cout << "B::foo() is called" << endl;}
};
那么,在使用的时候,我们可以:
A * a = new B();
a->foo();// 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!
这个例子是虚函数的一个典型应用。所谓虚函数,虚就虚在“推迟联编”或者“动态联编”上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被称为“虚”函数。
虚函数只能借助于指针或者引用来达到多态的效果,如果是下面这样的代码,则虽然是虚函数,但它不是多态的:
class A
{
public:
virtual void foo();
};
class B: public A
{
virtual void foo();
};
void bar()
{
A a;
a.foo(); // A::foo()被调用
}
多 态
多态(polymorphism)一词最初来源于希腊语polumorphos,含义是具有多种形式或形态的情形。在程序设计领域,一个广泛认可的定义是“一种将不同的特殊行为和单个泛化记号相关联的能力”。和纯粹的面向对象程序设计语言不同,C++中的多态有着更广泛的含义。除了常见的通过类继承和虚函数机制生效于运行期的动态多态(dynamic polymorphism)外,模板也允许将不同的特殊行为和单个泛化记号相关联,由于这种关联处理于编译期而非运行期,因此被称为静态多态(static polymorphism)【荣耀】。
在了解了虚函数的意思之后,再理解多态的相关概念就比较容易了。仍然针对上面的类层次,但是使用的方法变的复杂了一些:
void bar(A * a)
{
a->foo(); // 被调用的是A::foo() 还是B::foo()?
}
因为foo()是个虚函数,所以在bar这个函数中,只根据这段代码,无从确定这里被调用的是A::foo()还是B::foo(),但是可以肯定的说:如果a指向的是A类的实例,则A::foo()被调用,如果a指向的是B类的实例,则B::foo()被调用。
多态这么神奇,但是能用来做什么呢?这个命题我难以用一两句话概括,一般的C++教程(或者其它面向对象语言的教程)都用一个画图的例子来展示多态的用途,这里就不再重复了。下面将从抽象的角度描述一下多态的相关思想:
在面向对象的编程中,首先会针对数据进行抽象(确定基类)和继承(确定派生类),构成类层次。这个类层次的使用者在使用它们的时候,如果仍然在需要基类的时候写针对基类的代码,在需要派生类的时候写针对派生类的代码,就等于类层次完全暴露在使用者面前。如果这个类层次有任何的改变(增加了新类),都需要使用者“知道”(针对新类写代码)。这样就增加了类层次与其使用者之间的耦合。
多态可以使程序员脱离这种窘境。再回头看看上面的例子,bar()作为A-B这个类层次的使用者,它并不知道这个类层次中有多少个类,每个类都叫什么,但是一样可以很好的工作,当有一个C类从A类派生出来后,bar()也不需要“知道”(修改)。这完全归功于多态--编译器针对虚函数产生了可以在运行时刻确定被调用函数的代码。
如何实现“动态联编”
编译器是如何针对虚函数产生可以在运行时刻确定被调用函数的代码呢?也就是说,虚函数实际上是如何被编译器处理的呢?Lippman在《深度探索C++对象模型》中的不同章节讲到了几种方式,这里把“标准的”方式简单介绍一下。
这里所说的“标准”方式,也就是所谓的“VTABLE”机制。编译器发现一个类中有被声明为virtual的函数,就会为其搞一个虚函数表,也就是VTABLE。VTABLE实际上是一个函数指针的数组,每个虚函数占用这个数组的一个slot。一个类只有一个VTABLE,不管它有多少个实例。派生类有自己的VTABLE,但是派生类的VTABLE与基类的VTABLE有相同的函数排列顺序,同名的虚函数被放在两个数组的相同位置上。在创建类实例的时候,编译器还会在每个实例的内存布局中增加一个vptr字段,该字段指向本类的VTABLE。通过这些手段,编译器在看到一个虚函数调用的时候,就会将这个调用改写,针对上面的例子:
void bar(A * a)
{
a->foo();
}
会被改写为:
void bar(A * a)
{
(a->vptr[1])();
}
因为派生类和基类的foo()函数具有相同的VTABLE索引,而他们的vptr又指向不同的VTABLE,因此通过这样的方法可以在运行时刻决定调用哪个foo()函数。虽然实际情况远非这么简单,但是基本原理大致如此。
纯虚函数
如下声明表示一个函数为纯虚函数(纯虚函数也可以有定义)(如果一个类里面有一个或多个纯虚函数,这个类就是抽象类):
class A
{
public:
virtual void foo()=0; // =0标志一个虚函数为纯虚函数
};
纯虚函数用来规范派生类的行为,实际上就是所谓的“接口”。它告诉使用者,我的派生类都会有这个函数。试图创建一个抽象基类的独立类对象会导致编译时刻错误。那么,一般在什么情况下使用纯虚函数呢?
l 当想在基类中抽象出一个方法,且该基类只做能被继承,而不能被实例化;
l 这个方法必须在派生类(derived class)中被实现;
如果满足以上两点,可以考虑将该方法申明为pure virtual function。下面举例说明相关的概念,首先定义一个形状的类(Cshape),但凡是形状我们都要求其能显示自身,所以我们定义了如下的一个类:
class CShape
{
virtual void Show(){};
};
现实中没有Cshape这样的形状,因此我们不想让CShape这个类被实例化,我们首先想到的是将Show函数的定义(实现)部分删除如下:
class CShape
{
virtual void Show();//删除实现部分
};
当我们使用下面的语句实例化一个CShape时:
CShape cs; //这是我们不允许的,但仅用上面的代码是可以通过编译(但link时失败)。
怎么样避免一个CShape被实例化,且在编译时就被发现?答案是使用pure virtual funcion。我们再次修改CShape类如下:
class CShape
{
public:
virtual void Show()=0;
};
这时在实例化CShape时就会有类似于下面这样的报错信息:
error C2259: 'CShape' : cannot instantiate abstract class due to following members:
warning C4259: 'void __thiscall CShape::Show(void)' : pure virtual function was not defined
下面再来看看被继承的情况:首先定义一个CPoint2D的类,它继承自Cshape,必须实现基类(CShape)中定义的Show()方法。
我们最初的本意是让每一个派生自CShape的类,都要实现Show()方法,但是偶尔会忘记在某一个派生类中实现Show()方法,为了避免这种情况,pure virtual funcion发挥作用了,我们看以下代码:
class CPoint2D : public CShape
{
public:
CPoint2D()
{
printf("CPoint2D ctor is invoked/n");
};
void Msg()
{
printf("CPoint2D.Msg() is invoked/n");
};
/*---I'm sorry to forget implement the Show()---
void Show()
{
printf("Show() from CPoint2D/n");
};
----------------------------------------------*/
};
void main()
{
CPoint2D p2d; //如果派生类(CPoint2D)没有实现Show(),则编译不通过
p2d.Msg();
CShape *pShape = &p2d;
pShape->Show();
//不能实例化基类
//CShape cs;
}
如果我们忘记在派生类Cpoint2D中实现Show()方法,在实例化CPoint2D时,编译时会出现类似于下面这样的报错:
error C2259: 'CShape' : cannot instantiate abstract class due to following members:
warning C4259: 'void __thiscall CShape::Show(void)' : pure virtual function was not defined
虚析构函数
析构函数也可以是虚的,甚至是纯虚的。例如:
class A
{
public:
virtual ~A()=0; // 纯虚析构函数
};
当一个类打算被用作其它类的基类时,它的析构函数必须是虚的。考虑下面的例子:
class A
{
public:
A() { ptra_ = new char[10];}
~A() { delete[] ptra_;} // 非虚析构函数
private:
char * ptra_;
};
class B: public A
{
public:
B() { ptrb_ = new char[20];}
~B() { delete[] ptrb_;}
private:
char * ptrb_;
};
void foo()
{
A * a = new B;
delete a;
}
在这个例子中,程序也许不会象你想象的那样运行,在执行delete a的时候,实际上只有A::~A()被调用了,而B类的析构函数并没有被调用!这是否有点儿可怕?如果将上面A::~A()改为virtual,就可以保证B::~B()也在delete a的时候被调用了。因此基类的析构函数都必须是virtual的。
构造函数和析构函数中的虚函数调用
一个类的虚函数在它自己的构造函数和析构函数中被调用的时候,它们就变成普通函数了,不“虚”了。也就是说不能在构造函数和析构函数中让自己“多态”。
派生类对象中构造函数的调用顺序是,先调用基类的构造函数,然后是派生类的构造函数。例如,当我们定义一个Manger 类对象时,如:
Manger tim( "tim" );
构造函数的调用顺序是先Employee, 然后Manger。
当执行Employee基类的构造函数时,tim 的Manger部分还没有被初始化。实际上,tim还不是一个完整的Manger,只有它的Employee子对象被构造了。
如果在基类的构造函数中调用了一个虚拟函数,而基类和派生类都定义了该函数的实例,将会怎么样?应该调用哪一个函数实例?如果可以调用虚拟函数的派生类实例,并且它访问任意的派生类成员,那么调用的结果在逻辑上是未定义的。而程序可能会崩溃。
为了防止这样的事情发生,在基类构造函数中调用的虚拟实例总是在基类中活动的虚拟实例。实际上,在基类构造函数中,派生类对象只不过是一个基类类型的对象而已。
对于派生类对象,在基类析构函数中也是如此:派生类部分也是未定义的,但是,这一次不是因为它还没有被构造,而是因为它已经被销毁。
class A
{
public:
A() { foo();} // 在这里,无论如何都是A::foo()被调用!
~A() { foo();} // 同上
virtual void foo();
};
class B: public A
{
public:
virtual void foo();
};
void bar()
{
A * a = new B;
delete a;
}
如果你希望delete a的时候,会导致B::foo()被调用,那么你就错了。同样,在new B的时候,A的构造函数被调用,但是在A的构造函数中,被调用的是A::foo()而不是B::foo()。
参考资料:
《C++ primer》
《深度探索C++对象模型》
http://www.royaloo.com/articles/articles_2003/PolymorphismInCpp.htm
http://blog.chinaunix.net/u/11680/showart_351509.html
http://www.vckbase.com/document/viewdoc/?id=939
http://www.vckbase.com/document/viewdoc/?id=950
http://baike.baidu.com/view/161302.htm
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/wuliming_sc/archive/2009/01/31/3855903.aspx
分享到:
相关推荐
### C++中的虚函数(virtual function) #### 一、简介 虚函数是C++中用于实现多态(polymorphism)的核心机制。它允许通过基类的指针或引用调用派生类中重写的成员函数,从而实现了动态绑定。下面通过具体的示例来...
### C++虚函数实现原理 #### 一、引言 在C++中,虚函数机制是面向对象编程中实现多态的重要方式之一。通过虚函数,可以在基类中定义一个接口,并由派生类来具体实现该接口的功能,进而允许在运行时根据对象的实际...
在 C++ 中,虚函数的应用非常广泛,例如在 GUI 编程中,用虚函数可以实现按钮的点击事件处理,用虚函数可以实现多态机制,提高代码的复用性和灵活性。 虚函数是 C++ 中的一种强大的机制,允许我们实现多态机制,...
在C++编程语言中,虚函数(virtual function)是一个核心特性,它允许程序员实现多态性,这是面向对象编程中的重要概念。多态性意味着一个基类指针或引用可以调用不同派生类中重定义的同名成员函数。通过使用虚函数...
虚函数(Virtual Function)是基类中声明的一种特殊函数,可以在派生类中被重定义。当基类指针或引用指向派生类对象时,通过指针或引用调用虚函数会根据实际的对象类型执行相应的函数实现,这就是动态绑定或后期绑定...
### C++虚函数与虚函数表的理解 #### 一、虚函数的概念 在C++中,虚函数(Virtual Function)是一种特殊类型的成员函数,它允许基类指针或引用指向派生类对象,并通过该基类指针或引用调用派生类中重写的同名函数。...
在C++中,虚函数是一种重要的特性,它使得程序能够实现多态性。多态性允许程序员使用基类类型的指针来调用派生类的成员函数。本文将深入探讨虚函数背后的实现机制——虚函数表(Virtual Table),并解释其工作原理。...
在 C++ 语言中,虚函数(Virtual Function)是一种非常重要的机制,它允许在继承关系中实现多态性(Polymorphism)。虚函数的存在使得我们可以在不同的类中实现相同的接口,但是具有不同的实现方式。 首先,我们...
在C++编程语言中,虚函数与动态绑定是面向对象编程中的重要概念,它们使得程序在运行时能够根据对象的实际类型来调用相应的成员函数,从而实现多态性。下面将详细解析C++中虚函数的动态绑定机制,以及如何通过示例...
在C++编程语言中,虚函数(Virtual Functions)和多态(Polymorphism)是面向对象编程的重要特性,它们使得程序具有高度的灵活性和可扩展性。本文将深入探讨这两个概念,结合示例代码进行详细解释。 首先,我们来...
在C++编程语言中,虚函数表(Virtual Function Table,简称VFT或VTbl)是实现多态性的重要机制,特别是在面向对象编程中。多态性允许子类重写父类的方法,使得相同的消息可以有不同的行为。虚函数表是这种功能的底层...
本文将深入探讨C++虚函数表的相关知识点,并通过标题中提到的测试源码进行详细讲解。 首先,理解虚函数的定义:在C++中,当一个基类的指针或引用指向派生类的对象时,如果该基类有一个虚函数,那么通过指针或引用...
在C++编程语言中,虚函数(Virtual Function)是一个关键特性,它允许程序员实现多态性,这是面向对象编程中的核心概念。虚函数使得子类能够重写父类的方法,从而在运行时根据对象的实际类型调用正确的方法,而不是...
在C++编程语言中,虚函数表(Virtual Function Table,简称vtable)是实现多态性的一个关键机制。...通过阅读“C++虚函数表解析.docx”文件,你可以更深入地了解这个主题,包括其实际应用和潜在的优化策略。
在C++编程语言中,虚函数(Virtual Function)是一个核心特性,它允许子类重写父类的方法,实现多态性。多态性是面向对象编程的三大特性之一,另外两个是封装和继承。理解并熟练运用虚函数是C++程序员必备的技能。 ...
在C++中,虚函数(Virtual Function)是实现多态性的一种手段。多态性允许我们使用一个接口来表示不同的类实例,从而使得程序更加灵活、可扩展。 - **多态**:在面向对象编程中,多态是指不同类的对象可以通过同一...