<C++实践系列>C++中的虚函数(virtual function)

2016-01-29 12:22 5 1 收藏

<C++实践系列>C++中的虚函数(virtual function),&lt;C++实践系列&gt;C++中的虚函数(virtual function)

【 tulaoshi.com - C语言心得技巧 】

<C++实践系列C++中的虚函数(virtual function)
作者:张笑猛

提交者:eastvc 发布日期:2003-11-22 14:46:56
原文出处:http://objects.nease.net/


1.简介

    虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。假设我们有下面的类层次:

class A
{
public:
    virtual void foo() { cout << "A::foo() is called" << endl;}
};

class B: public A
{
public:
    virtual void foo() { cout << "B::foo() is called" << endl;}
};

那么,在使用的时候,我们可以:

A * a = new B();
a-foo();       // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!

    这个例子是虚函数的一个典型应用,通过这个例子,也许你就对虚函数有了一些概念。它虚就虚在所谓“推迟联编”或者“动态联编”上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函数。

(本文来源于图老师网站,更多请访问https://www.tulaoshi.com)

    虚函数只能借助于指针或者引用来达到多态的效果,如果是下面这样的代码,则虽然是虚函数,但它不是多态的:

class A
{
public:
    virtual void foo();
};

class B: public A
{
    virtual void foo();
};

void bar()
{
    A a;
    a.foo();   // A::foo()被调用
}

1.1 多态

    在了解了虚函数的意思之后,再考虑什么是多态就很容易了。仍然针对上面的类层次,但是使用的方法变的复杂了一些:

void bar(A * a)
{
    a-foo();  // 被调用的是A::foo() 还是B::foo()?
}

因为foo()是个虚函数,所以在bar这个函数中,只根据这段代码,无从确定这里被调用的是A::foo()还是B::foo(),但是可以肯定的说:如果a指向的是A类的实例,则A::foo()被调用,如果a指向的是B类的实例,则B::foo()被调用。

这种同一代码可以产生不同效果的特点,被称为“多态”。

1.2 多态有什么用?

    多态这么神奇,但是能用来做什么呢?这个命题我难以用一两句话概括,一般的C++教程(或者其它面向对象语言的教程)都用一个画图的例子来展示多态的用途,我就不再重复这个例子了,如果你不知道这个例子,随便找本书应该都有介绍。我试图从一个抽象的角度描述一下,回头再结合那个画图的例子,也许你就更容易理解。

(本文来源于图老师网站,更多请访问https://www.tulaoshi.com)

    在面向对象的编程中,首先会针对数据进行抽象(确定基类)和继承(确定派生类),构成类层次。这个类层次的使用者在使用它们的时候,如果仍然在需要基类的时候写针对基类的代码,在需要派生类的时候写针对派生类的代码,就等于类层次完全暴露在使用者面前。如果这个类层次有任何的改变(增加了新类),都需要使用者“知道”(针对新类写代码)。这样就增加了类层次与其使用者之间的耦合,有人把这种情况列为程序中的“bad smell”之一。

    多态可以使程序员脱离这种窘境。再回头看看1.1中的例子,bar()作为A-B这个类层次的使用者,它并不知道这个类层次中有多少个类,每个类都叫什么,但是一样可以很好的工作,当有一个C类从A类派生出来后,bar()也不需要“知道”(修改)。这完全归功于多态--编译器针对虚函数产生了可以在运行时刻确定被调用函数的代码。

1.3 如何“动态联编”

    编译器是如何针对虚函数产生可以再运行时刻确定被调用函数的代码呢?也就是说,虚函数实际上是如何被编译器处理的呢?Lippman在深度探索C++对象模型[1]中的不同章节讲到了几种方式,这里把“标准的”方式简单介绍一下。

    我所说的“标准”方式,也就是所谓的“VTABLE”机制。编译器发现一个类中有被声明为virtual的函数,就会为其搞一个虚函数表,也就是VTABLE。VTABLE实际上是一个函数指针的数组,每个虚函数占用这个数组的一个slot。一个类只有一个VTABLE,不管它有多少个实例。派生类有自己的VTABLE,但是派生类的VTABLE与基类的VTABLE有相同的函数排列顺序,同名的虚函数被放在两个数组的相同位置上。在创建类实例的时候,编译器还会在每个实例的内存布局中增加一个vptr字段,该字段指向本类的VTABLE。通过这些手段,编译器在看到一个虚函数调用的时候,就会将这个调用改写,针对1.1中的例子:

void bar(A * a)
{
    a-foo();
}

会被改写为:

void bar(A * a)
{
    (a-vptr[1])();
}

  &n

来源:https://www.tulaoshi.com/n/20160129/1485821.html

延伸阅读
函数重载是用来描述同名函数具有相同或者相似功能,但数据类型或者是参数不同的函数管理操作的称呼! 我们来举一个实际应用中的例子来说明问题:我们要进行两种不同数据类型的和操作为了实现它,在c语言中我们就要写两个不同名称的涵数来进行区分例如:int testa(int a,int b)和float testb(float a,floatb),这样字是没有问题,但是总有一点不好,这...
在将各种类型的数据构造成字符串时,sprintf 的强大功能很少会让你失望。由于sprintf 跟printf 在用法上几乎一样,只是打印的目的地不同而已,前者打印到字符串中,后者则直接在命令行上输出。这也导致sprintf 比printf 有用得多。 sprintf 是个变参函数,定义如下: int sprintf( char *buffer, const char *format [, argument] ... ); 除了前...
笔者在开发某软件过程中碰到这样一个问题,前级模块传给我二进制数据,输入参数为 char* buffer和 int length,buffer是数据的首地址,length表示这批数据的长度。数据的特点是:长度不定,类型不定,由第一个字节(buffer[0])标识该数据的类型,共有256(28 )种可能性。我的任务是必须对每一种可能出现的数据类型都要作处理,并且我的模块包...
   持久对象 (persistent objects)广泛应用于游戏、分布式数据库系统、多媒体以及图形应用程序中。目前C++并不直接支持持久性(persistence)(但有一些在C++未来版本中添加持久性和反射(reflection)的建议)。 !-- frame contents -- !-- /frame contents -- 持久对象可以在创建它的程序的作用域之外保持自身状态。...
函数参数的求值顺序 当一个函数带有多个参数时,C++语言没有规定在函数调用时实参的求值顺序。而编译器根据对代码进行优化的需要自行规定对实参的求值顺序。 !-- frame contents -- !-- /frame contents -- 有的编译器规定自左至右,有的编译器规定自右至左,这种对求值顺序的不同规定,对一般参数来讲没有影响。但是,假...

经验教程

265

收藏

83
微博分享 QQ分享 QQ空间 手机页面 收藏网站 回到头部