`

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

 
阅读更多
虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。假设我们有下面的类层次: class A { public: virtual void foo() { cout 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()被调用 } 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++教程(或者其它面向对象语言的教程)都用一个画图的例子来展示多态的用途,我就不再重复这个例子了,如果你不知道这个例子,随便找本书应该都有介绍。我试图从一个抽象的角度描述一下,回头再结合那个画图的例子,也许你就更容易理解。   在面向对象的编程中,首先会针对数据进行抽象(确定基类)和继承(确定派生类),构成类层次。这个类层次的使用者在使用它们的时候,如果仍然在需要基类的时候写针对基类的代码,在需要派生类的时候写针对派生类的代码,就等于类层次完全暴露在使用者面前。如果这个类层次有任何的改变(增加了新类),都需要使用者“知道”(针对新类写代码)。这样就增加了类层次与其使用者之间的耦合,有人把这种情况列为程序中的“bad smell”之一。   多态可以使程序员脱离这种窘境。再回头看看1.1中的例子,bar()作为A-B这个类层次的使用者,它并不知道这个类层次中有多少个类,每个类都叫什么,但是一样可以很好的工作,当有一个C类从A类派生出来后,bar()也不需要“知道”(修改)。这完全归功于多态--编译器针对虚函数产生了可以在运行时刻确定被调用函数的代码 2006-1-3 23:35:20 loadstone 等级:管理员 文章:169 积分:1482 门派:无门无派 注册:2005年10月11日 第 2 楼 QQ 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])(); }   因为派生类和基类的foo()函数具有相同的VTABLE索引,而他们的vptr又指向不同的VTABLE,因此通过这样的方法可以在运行时刻决定调用哪个foo()函数。   虽然实际情况远非这么简单,但是基本原理大致如此。 1.4 overload和override   虚函数总是在派生类中被改写,这种改写被称为“override”。我经常混淆“overload”和“override”这两个单词。但是随着各类C++的书越来越多,后来的程序员也许不会再犯我犯过的错误了。但是我打算澄清一下:   override是指派生类重写基类的虚函数,就象我们前面B类中重写了A类中的foo()函数。重写的函数必须有一致的参数表和返回值(C++标准允许返回值不同的情况,这个我会在“语法”部分简单介绍,但是很少编译器支持这个feature)。这个单词好象一直没有什么合适的中文词汇来对应,有人译为“覆盖”,还贴切一些。 overload约定成俗的被翻译为“重载”。是指编写一个与已有函数同名但是参数表不同的函数。例如一个函数即可以接受整型数作为参数,也可以接受浮点数作为参数。 二. 虚函数的语法   虚函数的标志是“virtual”关键字。 2.1 使用virtual关键字   考虑下面的类层次: class A { public: virtual void foo(); }; class B: public A { public: void foo(); // 没有virtual关键字! }; class C: public B // 从B继承,不是从A继承! { public: void foo(); // 也没有virtual关键字! };   这种情况下,B::foo()是虚函数,C::foo()也同样是虚函数。因此,可以说,基类声明的虚函数,在派生类中也是虚函数,即使不再使用virtual关键字。 2.2 纯虚函数   如下声明表示一个函数为纯虚函数: class A { public: virtual void foo()=0; // =0标志一个虚函数为纯虚函数 };   一个函数声明为纯虚后,纯虚函数的意思是:我是一个抽象类!不要把我实例化!纯虚函数用来规范派生类的行为,实际上就是所谓的“接口”。它告诉使用者,我的派生类都会有这个函数。 2.3 虚析构函数   析构函数也可以是虚的,甚至是纯虚的。例如: class A { public: virtual ~A()=0; // 纯虚析构函数 }; 2006-1-3 23:35:40 loadstone 等级:管理员 文章:169 积分:1482 门派:无门无派 注册:2005年10月11日 第 3 楼 QQ 当一个类打算被用作其它类的基类时,它的析构函数必须是虚的。考虑下面的例子: 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的。   纯虚的析构函数并没有什么作用,是虚的就够了。通常只有在希望将一个类变成抽象类(不能实例化的类),而这个类又没有合适的函数可以被纯虚化的时候,可以使用纯虚的析构函数来达到目的。 2.4 虚构造函数?   构造函数不能是虚的。 三. 虚函数使用技巧 3.1 private的虚函数   考虑下面的例子: class A { public: void foo() { bar();} private: virtual void bar() { ...} }; class B: public A { private: virtual void bar() { ...} };   在这个例子中,虽然bar()在A类中是private的,但是仍然可以出现在派生类中,并仍然可以与public或者protected的虚函数一样产生多态的效果。并不会因为它是private的,就发生A::foo()不能访问B::bar()的情况,也不会发生B::bar()对A::bar()的override不起作用的情况。   这种写法的语意是:A告诉B,你最好override我的bar()函数,但是你不要管它如何使用,也不要自己调用这个函数。 3.2 构造函数和析构函数中的虚函数调用   一个类的虚函数在它自己的构造函数和析构函数中被调用的时候,它们就变成普通函数了,不“虚”了。也就是说不能在构造函数和析构函数中让自己“多态”。例如: 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()。 3.3 多继承中的虚函数 3.4 什么时候使用虚函数   在你设计一个基类的时候,如果发现一个函数需要在派生类里有不同的表现,那么它就应该是虚的。从设计的角度讲,出现在基类中的虚函数是接口,出现在派生类中的虚函数是接口的具体实现。通过这样的方法,就可以将对象的行为抽象化。   以设计模式[2]中Factory Method模式为例,Creator的factoryMethod()就是虚函数,派生类override这个函数后,产生不同的Product类,被产生的Product类被基类的AnOperation()函数使用。基类的AnOperation()函数针对Product类进行操作,当然Product类一定也有多态(虚函数)。 2006-1-3 23:36:00 loadstone 等级:管理员 文章:169 积分:1482 门派:无门无派 注册:2005年10月11日 第 4 楼 QQ 另外一个例子就是集合操作,假设你有一个以A类为基类的类层次,又用了一个std::vector来保存这个类层次中不同类的实例指针,那么你一定希望在对这个集合中的类进行操作的时候,不要把每个指针再cast回到它原来的类型(派生类),而是希望对他们进行同样的操作。那么就应该将这个“一样的操作”声明为virtual。   现实中,远不只我举的这两个例子,但是大的原则都是我前面说到的“如果发现一个函数需要在派生类里有不同的表现,那么它就应该是虚的”。这句话也可以反过来说:“如果你发现基类提供了虚函数,那么你最好override它”。 附:C++中的虚函数和纯虚函数用法   1.虚函数和纯虚函数可以定义在同一个类(class)中,含有纯虚函数的类被称为抽象类(abstract class),而只含有虚函数的类(class)不能被称为抽象类(abstract class)。   2.虚函数可以被直接使用,也可以被子类(sub class)重载以后以多态的形式调用,而纯虚函数必须在子类(sub class)中实现该函数才可以使用,因为纯虚函数在基类(base class) 只有声明而没有定义。   3.虚函数和纯虚函数都可以在子类(sub class)中被重载,以多态的形式被调用。   4.虚函数和纯虚函数通常存在于抽象基类(abstract base class -ABC)之中,被继承的子类重载,目的是提供一个统一的接口。   5.虚函数的定义形式:virtual {method body} ;纯虚函数的定义形式:virtual { } = 0; 在虚函数和纯虚函数的定义中不能有static标识符,原因很简单,被static修饰的函数在编译时候要求前期bind,然而虚函数却是动态绑定(run-time bind),而且被两者修饰的函数生命周期(life recycle)也不一样。   6.如果一个类中含有纯虚函数,那么任何试图对该类进行实例化的语句都将导致错误的产生,因为抽象基类(ABC)是不能被直接调用的。必须被子类继承重载以后,根据要求调用其子类的方法。   以下为一个简单的虚函数和纯虚寒数的使用演示,目的是抛砖引玉! #include //father class class Virtualbase { public: virtual void Demon()= 0; //prue virtual function virtual void Base() {coutDemon(); inst->Base(); // inst = new Virtualbase(); // inst->Base() return ; }
分享到:
评论

相关推荐

    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