http://blog.csdn.net/hackbuteer1/article/details/7883531

转载请标明出处,原文地址:http://blog.csdn.net/hackbuteer1/article/details/7883531
一、虚函数的工作原理

     
虚函数的实现要求对象携带额外的信息,这些信息用于在运行时确定该对象应该调用哪一个虚函数。典型情况下,这一信息具有一种被称为 vptr(virtual table
pointer,虚函数表指针)的指针的形式。vptr 指向一个被称为 vtbl(virtual
table,虚函数表)的函数指针数组,每一个包含虚函数的类都关联到 vtbl。当一个对象调用了虚函数,实际的被调用函数通过下面的步骤确定:找到对象的 vptr
指向的 vtbl,然后在 vtbl 中寻找合适的函数指针。
     
虚拟函数的地址翻译取决于对象的内存地址,而不取决于数据类型(编译器对函数调用的合法性检查取决于数据类型)。如果类定义了虚函数,该类及其派生类就要生成一张虚拟函数表,即vtable。而在类的对象地址空间中存储一个该虚表的入口,占4个字节,这个入口地址是在构造对象时由编译器写入的。所以,由于对象的内存空间包含了虚表入口,编译器能够由这个入口找到恰当的虚函数,这个函数的地址不再由数据类型决定了。故对于一个父类的对象指针,调用虚拟函数,如果给他赋父类对象的指针,那么他就调用父类中的函数,如果给他赋子类对象的指针,他就调用子类中的函数(取决于对象的内存地址)。
      虚函数需要注意的大概就是这些个地方了,之前在More
effective C++上好像也有见过,不过这次在Visual
C++权威剖析这本书中有了更直白的认识,这本书名字很牛逼,看看内容也就那么回事,感觉名不副实,不过说起来也是有其独到之处的,否则也没必要出这种书了。
     
每当创建一个包含有虚函数的类或从包含有虚函数的类派生一个类时,编译器就会为这个类创建一个虚函数表(VTABLE)保存该类所有虚函数的地址,其实这个VTABLE的作用就是保存自己类中所有虚函数的地址,可以把VTABLE形象地看成一个函数指针数组,这个数组的每个元素存放的就是虚函数的地址。在每个带有虚函数的类
中,编译器秘密地置入一指针,称为v p o i n t e r(缩写为V P T R),指向这个对象的V TA B L E。
当构造该派生类对象时,其成员VPTR被初始化指向该派生类的VTABLE。所以可以认为VTABLE是该类的所有对象共有的,在定义该类时被初始化;而VPTR则是每个类对象都有独立一份的,且在该类对象被构造时被初始化。
      通过基类指针做虚函数调
用时(也就是做多态调用时),编译器静态地插入取得这个V P T R,并在V TA B L
E表中查找函数地址的代码,这样就能调用正确的函数使晚捆绑发生。为每个类设置V TA B L E、初始化V P T
R、为虚函数调用插入代码,所有这些都是自动发生的,所以我们不必担心这些。

  1. #include<iostream>
  2. using namespace std;
  3. class A
  4. {
  5. public:
  6. virtual void fun1()
  7. {
  8. cout << "A::fun1()" << endl;
  9. }
  10. virtual void fun2()
  11. {
  12. cout << "A::fun2()" << endl;
  13. }
  14. };
  15. class B : public A
  16. {
  17. public:
  18. void fun1()
  19. {
  20. cout << "B::fun1()" << endl;
  21. }
  22. void fun2()
  23. {
  24. cout << "B::fun2()" << endl;
  25. }
  26. };
  27. int main()
  28. {
  29. A *pa = new B;
  30. pa->fun1();
  31. delete pa;
  32. system("pause");
  33. return 0;
  34. }
#include<iostream>
using namespace std; class A
{
public:
virtual void fun1()
{
cout << "A::fun1()" << endl;
}
virtual void fun2()
{
cout << "A::fun2()" << endl;
}
}; class B : public A
{
public:
void fun1()
{
cout << "B::fun1()" << endl;
}
void fun2()
{
cout << "B::fun2()" << endl;
}
}; int main()
{
A *pa = new B;
pa->fun1();
delete pa; system("pause");
return 0;
}

毫无疑问,调用了B::fun1(),但是B::fun1()不是像普通函数那样直接找到函数地址而执行的。真正的执行方式是:首先取出pa指针所指向的对象的vptr的值,这个值就是vtbl的地址,由于调用的函数B::fun1()是第一个虚函数,所以取出vtbl第一个表项里的值,这个值就是B::fun1()的地址了,最后调用这个函数。因此只要vptr不同,指向的vtbl就不同,而不同的vtbl里装着对应类的虚函数地址,所以这样虚函数就可以完成它的任务,多态就是这样实现的。
     
而对于class A和class B来说,他们的vptr指针存放在何处?其实这个指针就放在他们各自的实例对象里。由于class A和class
B都没有数据成员,所以他们的实例对象里就只有一个vptr指针。
      虚拟函数使用的缺点
  优点讲了一大堆,现在谈一下缺点,虚函数最主要的缺点是执行效率较低,看一看虚拟函数引发的多态性的实现过程,你就能体会到其中的原因,另外就是由于要携带额外的信息(VPTR),所以导致类多占的内存空间也会比较大,对象也是一样的。

    
含有虚函数的对象在内存中的结构如下:

  1. class A
  2. {
  3. private:
  4. int a;
  5. int b;
  6. public:
  7. virtual void fun0()
  8. {
  9. cout<<"A::fun0"<<endl;
  10. }
  11. };
class A
{
private:
int a;
int b;
public:
virtual void fun0()
{
cout<<"A::fun0"<<endl;
}
};


1、直接继承
那我们来看看编译器是怎么建立VPTR指向的这个虚函数表的,先看下面两个类:

  1. class base
  2. {
  3. private:
  4. int a;
  5. public:
  6. void bfun()
  7. {
  8. }
  9. virtual void vfun1()
  10. {
  11. }
  12. virtual void vfun2()
  13. {
  14. }
  15. };
  16. class derived : public base
  17. {
  18. private:
  19. int b;
  20. public:
  21. void dfun()
  22. {
  23. }
  24. virtual void vfun1()
  25. {
  26. }
  27. virtual void vfun3()
  28. {
  29. }
  30. };
class base
{
private:
int a;
public:
void bfun()
{
}
virtual void vfun1()
{
}
virtual void vfun2()
{
}
}; class derived : public base
{
private:
int b;
public:
void dfun()
{
}
virtual void vfun1()
{
}
virtual void vfun3()
{
}
};

两个类的VPTR指向的虚函数表(VTABLE)分别如下:
base类
                    
——————
VPTR——>    |&base::vfun1 |
                     
——————
                    |&base::vfun2 |
                   
——————
      
derived类
                      ———————
VPTR——>   
|&derived::vfun1 |
                     ———————
                  
|&base::vfun2     |
                    ———————
                  
|&derived::vfun3 |
                    ———————
      每当创建一个包含有虚函数的类或从包含有虚函数的类派生一个类时,编译器就为这个类创建一个VTABLE,如上图所示。在这个表中,编译器放置了在这个类中或在它的基类中所有已声明为virtual的函数的地址。如果在这个派生类中没有对在基类中声明为virtual的函数进行重新定义,编译器就使用基类
的这个虚函数地址。(在derived的VTABLE中,vfun2的入口就是这种情况。)然后编译器在这个类中放置VPTR。当使用简单继承时,对于每个对象只有一个VPTR。VPTR必须被初始化为指向相应的VTABLE,这在构造函数中发生。
      
一旦VPTR被初始化为指向相应的VTABLE,对象就"知道"它自己是什么类型。但只有当虚函数被调用时这种自我认知才有用。

      
没有虚函数类对象的大小正好是数据成员的大小,包含有一个或者多个虚函数的类对象编译器向里面插入了一个VPTR指针(void
*),指向一个存放函数地址的表就是我们上面说的VTABLE,这些都是编译器为我们做的我们完全可以不关心这些。所以有虚函数的类对象的大小是数据成员的大小加上一个VPTR指针(void
*)的大小。

总结一下VPTR 和
VTABLE 和类对象的关系:
      
每一个具有虚函数的类都有一个虚函数表VTABLE,里面按在类中声明的虚函数的顺序存放着虚函数的地址,这个虚函数表VTABLE是这个类的所有对象所共有的,也就是说无论用户声明了多少个类对象,但是这个VTABLE虚函数表只有一个。
      
在每个具有虚函数的类的对象里面都有一个VPTR虚函数指针,这个指针指向VTABLE的首地址,每个类的对象都有这么一种指针。
2、虚继承
    
这个是比较不好理解的,对于虚继承,若派生类有自己的虚函数,则它本身需要有一个虚指针,指向自己的虚表。另外,派生类虚继承父类时,首先要通过加入一个虚指针来指向父类,因此有可能会有两个虚指针。
二、(虚)继承类的内存占用大小
    
首先,平时所声明的类只是一种类型定义,它本身是没有大小可言的。 因此,如果用sizeof运算符对一个类型

C++虚函数、虚继承的更多相关文章

  1. 【整理】C++虚函数及其继承、虚继承类大小

    参考文章: http://blog.chinaunix.net/uid-25132162-id-1564955.html http://blog.csdn.net/haoel/article/deta ...

  2. C++ - 类的虚函数\虚继承所占的空间

    类的虚函数\虚继承所占的空间 本文地址: http://blog.csdn.net/caroline_wendy/article/details/24236469 char占用一个字节, 但不满足4的 ...

  3. C++学习 - 虚表,虚函数,虚函数表指针学习笔记

    http://blog.csdn.net/alps1992/article/details/45052403 虚函数 虚函数就是用virtual来修饰的函数.虚函数是实现C++多态的基础. 虚表 每个 ...

  4. C++解析(25):关于动态内存分配、虚函数和继承中强制类型转换的疑问

    0.目录 1.动态内存分配 1.1 new和malloc的区别 1.2 delete和free的区别 2.虚函数 2.1 构造函数与析构函数是否可以成为虚函数? 2.2 构造函数与析构函数是否可以发生 ...

  5. 虚函数&&虚继承

    如果说没有虚函数的虚继承只是一个噩梦的话,那么这里就是真正的炼狱.这个C++中最复杂的继承层次在VS上的实现其实我没有完全理解,摸爬滚打了一番也算得出了微软的实现方法吧,至于一些刁钻的实现方式我也想不 ...

  6. C++基础 (6) 第六天 继承 虚函数 虚继承 多态 虚函数

    继承是一种耦合度很强的关系 和父类代码很多都重复的 2 继承的概念 3 继承的概念和推演 语法: class 派生类:访问修饰符 基类 代码: … … 4 继承方式与访问控制权限 相对的说法: 爹派生 ...

  7. C#中的虚函数及继承关系

    转载:http://blog.csdn.net/suncherrydream/article/details/8423991 若一个实例方法声明前带有virtual关键字,那么这个方法就是虚方法. 虚 ...

  8. C++继承-重载-多态-虚函数

    C++ 继承 基类 & 派生类 一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数.定义一个派生类,我们使用一个类派生列表来指定基类.类派生列表以一个或多个基类命名,形式如下: ...

  9. virtual之虚函数,虚继承

    当类中包含虚函数时,则该类每个对象中在内存分配中除去数据外还包含了一个虚函数表指针(vfptr),指向虚函数表(vftable),虚函数表中存放了该类包含的虚函数的地址. 当子类通过虚继承的方式从父类 ...

  10. 虚函数&纯虚函数&抽象类&虚继承

    C++ 虚函数&纯虚函数&抽象类&接口&虚基类   1. 多态 在面向对象语言中,接口的多种不同实现方式即为多态.多态是指,用父类的指针指向子类的实例(对象),然后通过 ...

随机推荐

  1. 135.Candy---贪心

    题目链接 题目大意:分糖果,每个小朋友都有一个ratings值,且每个小朋友至少都要有一个糖果,而且每个小朋友的ratings值如果比左右邻舍的小朋友的ratings值高,则其糖果数量也比邻舍的小朋友 ...

  2. js获取系统时间

    //------------------------------------获取系统日期时间 var oDate=new Date(); //alert(oDate.getFullYear());// ...

  3. python_异常处理

    常用异常种类 AttributeError 试图访问一个对象没有的树形,比如foo.x,但是foo没有属性x IOError 输入/输出异常:基本上是无法打开文件 ImportError 无法引入模块 ...

  4. LightOJ 1319 Monkey Tradition(中国剩余定理)

    题目链接:https://vjudge.net/contest/28079#problem/U 题目大意:给你n(n<12)行,每行有pi,ri,求一个数ans满足ans%pi=ri(i从1~n ...

  5. ECMA6

    let关键字 用来替代var 的关键字,不能重复定义一个变量 举例: for(var i=0; i<5; i++){      setTimeout(function(){           ...

  6. fastdfs5.11+centos7.2 按照部署(三)【转载】

    1.测试 前面两篇博文已对FastDFS的安装和配置,做了比较详细的讲解.FastDFS的基础模块都搭好了,现在开始测试下载. 1.1 配置客户端 同样的,需要修改客户端的配置文件: vim /etc ...

  7. LoadRunner的Capture Level说明

    LoadRunner的Capture Level说明 Capture Level的设置说明: 1.Socket level data. Capture data using trapping on t ...

  8. 六 Python基础 字符串和编码

    字符编码 我们已经讲过了,字符串也是一种数据类型,但是,字符串比较特殊的是还有一个编码问题. 因为计算机只能处理数字,如果要处理文本,就必须先把文本转换为数字才能处理.最早的计算机在设计时采用8个比特 ...

  9. javascript 进制转换(2进制、8进制、10进制、16进制之间的转换)

    //十进制转其他 var x=110; alert(x); alert(x.toString(8)); alert(x.toString(32)); alert(x.toString(16)); // ...

  10. thinkjs项目中使用mongoose需要注意的地方

    原文链接thinkjs项目中使用mongoose需要注意的地方 由于thinkjs不支持mongodb的关联模型查询,所以我不得不使用mongoose这个第三方odm. 我有两个选择,一是像我在exp ...