有一定面向对象知识的朋友对继承与多态一定很熟悉,C++想实现继承的话就要使用虚函数,那么什么是虚函数,其原理是什么,下面尽量给大家分析一下C++中其运行机制:

首先,基础,什么是虚函数,什么是多态?

答:被virtual关键字修饰的成员函数,就是虚函数。虚函数用来实现多态性(Polymorphism),将接口与实现进行分离;简单来说就是实现共同的方法,但因个体差异而采用不同的策略。

举例来说,我有一个动物的基类,这个类里面包含“跑”(virtual  void  run())的一个成员方法,这个基类有两个派生类——猫和袋鼠,我想让猫和袋鼠都可以使用跑的方法,但是很明显他们两个跑的方式是不一样的。这样,由于个体的不同而实现同一方法的不同效果就是多态。

情况1:

class A{
public:
void Show(){ cout<<”I am A”<<endl;}
};
class B:public A{
public:
void Show (){ cout<<”I am B”<<endl;}
};
int main(){
A a;
B b;
a. Show ();
b. Show ();
}

这里为了演示方便就声明了A,B两个类,B继承了A。

这样打印出来的语句肯定是,i am A,I am B。不过这并不是多态,B在声明Show()方法的时候完全是自己的

Show(),他们调用的完全两个对象的方法,所以并不是多态(其实这是一种隐藏)。如何才是多态?一切用指向基类的指针或引用来操作对象是多态的基本特征,也就是说得有指针而且还是指向基类的指针。

那改一下吧~

情况2:

int main(){
A a;
B b;
A *p1=&a;
A *p2=&b;
p1-> Show ();
p2-> Show ();
}

那现在结果呢?I am A,I am A。还是不对,虽然p2已经指向b,但是还是调用的A的Show()。

所以,我们不妨按照虚函数的定义,试一下,把Show前面加一个virtual。

情况3:

class A{
public:
virtual void Show(){ cout<<”I am A”<<endl;}
};
class B:public A{
public:
virtual void Show (){ cout<<”I am B”<<endl;}
};

现在重新运行main的代码,这样输出的结果就是I am A,I
am B了。

简单总结,指向基类的指针在操作它的多态类对象时,会根据不同的类对象,调用其相应的函数,这个函数就是虚函数。

必须是基类指针指向派生类对象,派生类指针不能指基类。所以派生类只能操作父类有的属性及函数。对于那些父类没有的属性,必须将父类指针强制转化为子类指针后才可使用。

那么为什么会根据不同的类对象,调用不同版本的函数呢?

—————————————————————————————类的指针与对象分析———————————————————————————————————

写到这里,我想先放一放,非常建议大家和我一起研究一下类的指针和类的对象,这对之后的理解有很大的帮助。

A a;                          //类的对象

A *p1=&a;或者A*p1=newA();    //类的指针

A*p1=NULL;                   //类的指针,是可以先行定义的

B b;                         //类的对象

(这里要说明一下,用new创建的指针(必须赋予指针)需要结束之后调用delete,才能执行析构函数。而B
b不需要手动释放,可以自动调用构造函数与析构函数)

类的指针(用 ->操作符):他是一个内存地址值,他指向内存中存放的类对象(包括一些成员变量所赋的值),如果用new声明的话那么他使用的是内存堆,是个永久变量,除非你释放它(否则也是在栈中).
并且没有调用构造函数。

类的对象(用 .
操作符)
:他是利用类的构造函数在内存中分配一块内存(包括一些成员变量所赋的值,在运行时就分配了对应大小的内存),成员函数的地址是全局已知的,所以其内存无需保存在对象实例中(关于类占用内存的大小,请参考另一篇博客XXXX)。类的对象使用的是内存栈,是个局部的临时变量。

理解: 当类是有虚函数的基类,假如Show是它的一个虚函数,则调用Show时:   

       类的对象:调用的是它自己的Show;   

      类的指针:调用的是分配给它空间时那种类的Show(父类的指针可以指向子类的对象);

(我们使用基类的引用或指针调用函数的时候(p2-> Show ();)并不清楚该函数真正调用的对象是什么类型,如果是虚函数的话,就只能在运行时才决定(如果不是虚函数,我们认为编译时就可以定下来了)~不过这里大家肯定还会有疑问?既然它调用的是指针所指向的对象,那不加virtual的函数为什么就没有效果呢?继续往下看)

————————————————————————————————————————————————————————————————————————

好了,下面我们继续学习虚函数,对上面的例子再进行分析!

在上面的情况2下(没有虚函数的情况),我们给A类型指针传递了不同类的地址,按常理来说我们希望这个指针能够区分是基类还是派生类,然而结果却是都当做派生类来处理。所以我们自然想改变这种情况,虚函数也就诞生了~并且实现了多态的效果

虚函数这样的特点到底如何实现?答案就是虚函数表!   Virtual Table简称为V-table(虚表)

在虚函数存在的类中,编译器就会为他们创建一个vptr指针,这个指针指向的就是虚表(V-Table)。

这里我们着重看一下这张虚函数表。在C++的标准规格说明书中说到,编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)。这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

下面举代码例子:

class A{
public:
virtual void show(){ cout<<”Show A”<<endl;}
virtual void print(){cout<<”print A”<<endl;}
}; typedef void (*Fun)(void); //指向返回void类型并且无参数的函数的指针,类比typedef void (*) (void) Fun;
A a;
Fun pFun = NULL;
cout << "虚函数表地址:" << (int*)(&a) << endl; //取a的地址之后强转成int*,这样获取的就是虚函数表的地址(其实就是vptr)
cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&a) << endl; //对vptr解引用(等价于*vptr),强转成int*就是第一个虚函数的地址
// Invoke the first virtual function
pFun = (Fun)*((int*)*(int*)(&a));
pFun();

结果打印:

虚函数表地址:0012FED4

虚函数表 — 第一个函数地址:0044F148

A::Show

下面我们再声明两个类:

class B:public A{
public:
virtual void show(){ cout<<”Show B”<<endl;}
virtual void printB(){cout<<”print B”<<endl;} //注意这里没有覆盖A的方法
};
class C: public A {
public:
virtual void showC(){ cout<<”Show C”<<endl;}
virtual printC(){cout<<”print C”<<endl;}
};

这里C没有覆盖A的方法,而B覆盖了A的方法

我们看一下不同情况下的虚函数的表是什么样的

对于无覆盖的虚函数

A  a;

我们可以看到下面几点:

1)虚函数按照其声明顺序放于表中。

2)父类的虚函数在子类的虚函数前面。

对于有覆盖的虚函数表(没错,其实这就是我们常说的覆盖,而博客一开始的情况一就是隐藏的例子)

我们可以看到下面几点:

1)覆盖的show()函数被放到了虚表中原来父类虚函数的位置。

2)没有被覆盖的函数位置不变。

这样,我们就可以看到对于下面这样的程序,

A *a = new B();
a->show();

由a所指的内存中的虚函数表的show()的位置已经被B::
show ()函数地址所取代,于是在实际调用发生时,是B:: show ()被调用了。多态就这样实现了~

这里附一张多继承的效果图,B继承与A1,A2,A3、

补充:

是基类虚表中有的,在派生类中都有,并根据派生类自己的虚函数进行了扩展

虽然上面例子的a可以访问show(),但是却不能访问printB,即使a虚表里面拥有(父类不可以访问子类自己的虚函数)

如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,我们还是有办法访问到。

最后再说明几点:

1.      静态成员函数不能是虚函数,因为静态成员函数的特点是不受限制于某个对象。 

2.      内联(inline)函数不能是虚函数,因为内联函数不能在运行中动态确定位置。即使虚函数在类的内部定义,但是在编译的时候系统仍然将它看做是非内联的。 

3.      构造函数不能是虚函数,因为构造的时候,对象还是一片未定型的空间,只有构造完成后,对象才是具体类的实例。 

4.      析构函数可以是虚函数,而且通常声名为虚函数,继承关系下,派生类的实例中会为基类的成员变量申请相应的内存。析构的时候,我们需要析构基类以及派生类的所有占用空间,不用虚函数的话,就会只调用基类的析构函数

说了这么多,应该对虚函数与多态有一个全新的了解了吧~

参考了wswifth的博客(感谢!)

C++虚函数与虚表的更多相关文章

  1. c++复习基础要点02 虚函数与模板 与static inline是否共存

    1.      虚函数能否定义为模板函数 当一个类有虚函数时,它一定有一个虚表,用来纪录每个虚函数的实际地址.这也就是说这个虚表的大小是在编译期就确定了的.有多少个虚函数,虚表就纪录几个.       ...

  2. c++虚函数&重写

    虚函数是C++中实现多态的一种方法,父类A的一个函数声明为虚函数,在子类B中覆盖定义之后,当在调用的时候使用A*a=new B(),此时调用对应的那个虚函数的名字,则会执行B中的函数.当父类中没有定义 ...

  3. C++反汇编-虚函数

    学无止尽,积土成山,积水成渊-<C++反汇编与逆向分析技术揭秘> 读书笔记 在C++中,使用关键字virtual声明为虚函数. 虚函数地址表(虚表) 定义:当类中定义有虚函数时,编译器会把 ...

  4. 《C++反汇编与逆向分析技术揭秘》之11——虚函数

    虚函数的机制 当类中定义有虚函数时,编译器会将该类中所有虚函数的首地址保存在一张地址表中,这张表被称为虚函数地址表.编译器还会在类中添加一个虚表指针. 举例: CVirtual类的构造函数中没有进行任 ...

  5. 探索C++虚函数

    探索C++虚函数 1 测试环境 各个编译器对虚函数的实现有各自区别,但原理大致相同.本文基于VS2008探索虚函数 2 测试代码 #pragma once #include <iostream& ...

  6. C++类虚函数内存分布(这个 你必须懂)

    转自:http://www.cnblogs.com/jerry19880126/p/3616999.html C++类内存分布 书上类继承相关章节到这里就结束了,这里不妨说下C++内存分布结构,我们来 ...

  7. [022]c++虚函数、多态性与虚表

    原文出处:http://my.oschina.net/hnuweiwei/blog/280894 目录[-] 多态 虚函数 纯虚函数 虚表 一般继承(无虚函数覆盖) 一般继承(有虚函数覆盖) 多重继承 ...

  8. c++虚表的使用 通过虚表调用虚函数的演示代码

    //演示一下c++如何找到虚表地址vptr以及如何通过虚表调用虚函数 //zhangpeng@myhexin.com 20130811 #include <iostream> using ...

  9. 类虚函数表原理实现分析(当我们将虚表地址[n]中的函数替换,那么虚函数的实现就由我们来控制了)

    原理分析 当调用一个虚函数时, 编译器生成的代码会调用 虚表地址[0](param1, param2)这样的函数. 已经不是在调用函数名了. 当我们将虚表地址[n]中的函数实现改为另外的函数, 虚函数 ...

随机推荐

  1. Android--数据持久化存储概述

    Android数据持久化存储共有四种方式,分别是文件存储.SharedPreferences.Sqlite数据库和ContentProvider.在本篇幅中只介绍前面三种存储方式,因为ContentP ...

  2. muduo源代码分析--Reactor在模型muduo使用(两)

    一. TcpServer分类: 管理所有的TCP客户连接,TcpServer对于用户直接使用,直接控制由用户生活. 用户只需要设置相应的回调函数(消息处理messageCallback)然后TcpSe ...

  3. python 反转列表

    翻转一个链表 您在真实的面试中是否遇到过这个题? Yes 样例 给出一个链表1->2->3->null,这个翻转后的链表为3->2->1->null 步骤是这样的: ...

  4. 清晰明亮的白色lua协程(coroutine)

    协同程序线程类和多线程下似:它有它自己的堆栈.自己的局部变量.它有自己的指令指针,但是,其他协程共享全局变量和其他项目信息.主要不同在于:多处理器的情况下.概念上来说多线程是同一时候执行多个线程,而协 ...

  5. 设计模式(四)The Factory Pattern 出厂模式

    一.简单工厂 定义:定义一个创建对象的接口,可是由其子类决定要实例化的对象是哪一个,工厂方法让类的实例化推迟到子类. 通俗的来讲就是由工厂方法确定一个框架.详细的实现由其子类来完毕. 与简单工厂相比, ...

  6. 联合概率(joint probability)、分布函数(distribution function)

    0. PMF 与 PDF 的记号 PMF:PX(x) PDF:fX(x) 1. 联合概率 联合概率:是指两个事件同时发生的概率. P(A,B)=P(B|A)⋅P(A)⇒P(B|A)=P(A,B)P(A ...

  7. 在asp.net core中使用cookie认证

    以admin控制器为要认证的控制器举例 1.对控制器设置权限特性 //a 认证命名空间 using Microsoft.AspNetCore.Authorization; using Microsof ...

  8. c#开发移动APP-Xamarin入门扩展剖析

    原文:c#开发移动APP-Xamarin入门扩展剖析 上节将Phoneword应用程序扩展到包含第二个屏幕,该屏幕可以跟踪应用程序的拨打历史 Navigation Xamarin.Form提供了一个内 ...

  9. android4.0 USB Camera示例(四)CMOS

    上一页下一页说usb camera uvc标准 顺便说说CMOS一起做 操作基本一至, 前HAL在那里我已经提供了层CMOS相关接口 JNIEXPORT jint JNICALL Java_com_d ...

  10. 【剑指offer】直扑克

    个大王,2个小王(一副牌原本是54张^_^)...他随机从中抽出了5张牌,想測測自己的手气,看看能不能抽到顺子,假设抽到的话,他决定去买体育彩票,嘿嘿! ! "红心A,黑桃3,小王,大王,方 ...