`
jiagou
  • 浏览: 2532807 次
文章分类
社区版块
存档分类
最新评论

C++中的虚函数(virtual function)

 
阅读更多

简 介

缺省情况下,类的成员函数是非虚拟的。当一个成员函数为非虚拟的时候,通过一个类对象(指针或引用)而被调用的该成员函数,是该类对象的静态类型中定义的成员函数。

当成员函数是虚拟的时候,通过指针或引用而被调用的该成员函数,是在该类对象的动态类型中被定义的成员函数。但是,正如所发生的,类对象的静态和动态类型是相同的,所以虚拟函数机制只在使用指针和引用时才会如预期般地起作用。

第一次引入虚拟函数的基类时,必须在类声明中指定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).doc

    C++中的虚函数(virtual function).doc virtual function

    C++中的虚函数(virtual function).rar_C++虚函数_虚函数

    C++中的虚函数(virtual function)

    C++ 虚函数表解析

    一篇关于C++ 虚函数表解析详解的文章,大家一块学习。 博主还有好多值得阅读的文章,网址http://blog.csdn.net/haoel

    深入剖析C++虚函数表

    对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。 在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应...

    虚函数表工作原理

    对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。 在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应...

    深入解析C++中的虚函数与多态

    对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)和一个指向虚函数表的指针(vptr)来实现的。虚函数表,简称为vtbl,虚函数表表对实现多态起着至关重要

    C++中的Virtual Function (虚函数)

    1.C++ Virtual 用法  这里只讲语法,因为讲原理比较难。还没有涉及到构造函数。那么直接上代码了: // VitualFunction.cpp : Defines the entry point for the console application. // #include "stdafx.h" #...

    c++ 虚函数与纯虚函数的区别(深入分析)

    在面向对象的C++语言中,虚函数(virtual function)是一个非常重要的概念。因为它充分体现 了面向对象思想中的继承和多态性这两大特性,在C++语言里应用极广。比如在微软的MFC类库中,你会发现很多函数都有virtual...

    (转)多重继承下的虚函数表

    对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为VFTable。 在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应...

    虚函数分析论文

    virtual function analysis 构建方法 讲述虚函数分析C++语言

    探讨C++中不能声明为虚函数的有哪些函数

    1.为什么C++不支持普通函数为虚函数? 普通函数(非成员函数)只能被overload,不能被override,声明为虚函数也没有什么意思,因此编译器会在编译时邦定函数。 多态的运行期行为体现在虚函数上,虚函数通过继承方式...

    C++函数中那些不可以被声明为虚函数的函数

     1、为什么C++不支持普通函数为虚函数?  普通函数(非成员函数)只能被overload,不能被override,声明为虚函数也没有什么意思,因此编译器会在编译时邦定函数。  2、为什么C++不支持构造函数为虚函数?  ...

    C++ 类中有虚函数(虚函数表)时 内存分布详解

    对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应...

    The implementation mechanism for C++ virtual function

    C++这门语言是一门比较复杂的语言,光从这门语言本身去学习是不够的。 要想更好的使用C++这门编程语言,我们需要了解C++后面的一些东西。 功能点很多,泛泛而谈恐怕效果不是太...焦点也集中在虚函数和多态性这一点。

    C++类的虚函数虚继承所占的空间

     GCC中, 无论是虚函数还是虚继承, 都需要将指针存储在虚函数表(virtual function table), 占用4个字节.  继承会继承基类的数据, 和虚函数表, 即继承基类的空间.  代码: /* * test.cpp * * Created ...

    Explanation about “pure virtual function call” on Win32 platform.pdf

    主要通过一个经典例子讲解win32平台上出现“pure virtual function call”的前前后后。希望对广大学习C++的朋友有帮助。

    VirtualFunction.zip

    oday安全:软件漏洞分析技术第6章第3小节攻击C++虚函数的实验,没有使用溢出,修改读入数据中的几处关键值,运行自己shellcode。有兴趣的小伙伴尝试一下。

    c++学习文档

    虚函数(virtual function)、运算符重载(Operator Overloading)、多重继承(Multiple Inheritance)、模板(Template)、异常(Exception)、RTTI、命名空间(Name Space)逐渐被加入标准。 C++ 1998年国际标准...

Global site tag (gtag.js) - Google Analytics