致谢


  本文是基于对<Inside the c++ object model>的阅读和gdb的使用而完成的.在此感谢Lippman对cfront中对象模型的解析,这些解析帮助读者拨开迷雾.此外,Linux下无比强大的gdb工具更是驱散"黑暗"的"明灯".  :)

No-Inheritance


 class Base {
public:
int a = ;
static int b;
int c = ; void showBase1();
static int showBase2();
}; void Base::showBase1() {
cout<<"Base"<<endl;
}
int Base::showBase2() {
cout<<"base2"<<endl;
}

解析:

  使用GDB查看内存空间,显示

    Base中a的地址为0x7fffffffdc50

    Base中c的地址为0x7fffffffdc54

    Base中b的地址为0x601068

    showBase1的地址为0x40085e(参数为Base * const -> this指针为常量指针)

    showBase2的地址为0x400888(参数为void)

  显然

    non-static data member是存储在class object中;

    static data member, member function因为是所有本class的对象所share的,所以放置在了一个公共区域;

Inheritance without Polymorphism


 class Base {
public:
int a = ;
static int b;
int c = ; void showBase1();
};
int Base::b = ;
void Base::showBase1() {
cout<<"Base"<<endl;
} class Inheri : public Base {
public:
int c = ;
static int d; static int showInheri1();
};
int Inheri::d = ;
int Inheri::showInheri1(){
cout<<"Inheri"<<endl;
}

解析:  

  使用GDB查看内存空间,显示

    Inheri中a的地址为0x7fffffffdc70

    Inheri中c的地址为0x7fffffffdc78

    Inheri中b的地址为0x601068

    Inheri中b的地址为0x60106c

    showBase1的地址为0x40085e(参数为Base * const -> this指针为常量指针)

    showInheri1的地址为0x400888(参数为void)

  显然

    derived class object中包含 基类和派生类的non-static data member;

No-Inheritance with Polymorphism


 class Base {
public:
int a = ;
static int b;
int c = ; virtual void showBase1();//virtual function
};
int Base::b = ;
void Base::showBase1() {
cout<<"Base"<<endl;
}

  使用gdb查看base class object( p ptr )

     " {_vptr.Base = 0x400af0 <vtable for Base+16>, a = 21, static b = 10, c = 22} "

  在类中使用虚机制(虚函数,虚基类,虚继承..)时,会为每个object添加vptr来指向所对应的vtbl.

  

  使用gdb查看vptr指向的虚函数(p /a *(void**)0x400af0

    " {0x40092e <Base::showBase1()>, 0x697265686e4936} " 

  

Single-Inheritance with Virtual Mechanism


class Base {
public:
int a = ;
static int b;
int c = ; virtual void showBase1();
};
int Base::b = ;
void Base::showBase1() {
cout<<"Base"<<endl;
} class Inheri : public Base {
public:
int c = ;
static int d; static int showInheri1();
};
int Inheri::d = ;
int Inheri::showInheri1(){
cout<<"Inheri"<<endl;
}

  查看Inheri class object

    {<Base> = {_vptr.Base = 0x400bc0 <vtable for Inheri+16>, a = 21, static b = 10, c = 22}, c = 23, static d = 11}

  查看Ineri class object 的vtbl

    0x400998 <Inheri::show()>

  可以看到derived class object直接使用了从base class subobject中继承而来的vptr.

  同样,查看Base class object

    {_vptr.Base = 0x400be0 <vtable for Base+16>, a = 21, static b = 10, c = 22}

  查看Base class object的vtbl

    0x40096e <Base::show()>         

  

  从这里可以看出来,在Single Inheritance中每个class object的vtbl中都只包含本class所对应的virtual function.

  

  我们再测试一下derived class赋值给base class pointer的情况.

    Base* bbptr = new Inheri;

  查看bbptr所指向的内存:

    {<Base> = {_vptr.Base = 0x400bc0 <vtable for Inheri+16>, a = 21, static b = 10, c = 22}, c = 23, static d = 11}

  查看vtbl中的内容:

    0x400998 <Inheri::show()>

  这里我们可以有两点发现:

    1) 虽然使用的是基类指针Base* 来接Inheri对象,但是其vptr所指向的vtbl仍然是Inheri class的;

    2) 每个class所对应的vtbl在内存中只有一份,在测试中bbptr和iptr指向的vtbl都是位于0x400bc0

Multiple-Inheritance with Virtual Mechanism


    

 class Base {
public:
int a = ;
static int b;
int c = ; virtual void show();//inline
};
int Base::b = ;
void Base::show() {
cout<<"Base"<<endl;
} class Inheri : public Base {
public:
int c = ;
static int d; virtual void show();
};
int Inheri::d = ;
void Inheri::show(){
cout<<"Inheri"<<endl;
} class OtherBase{
public:
int oa;
virtual void show();
};
void OtherBase::show(){
cout<<"OtherBase"<<endl;
} class Final : public OtherBase,public Inheri{
public:
virtual void show();
};
void Final::show() {
cout<<"Final"<<endl;
}

  查看Final对象

    "{<OtherBase> = {_vptr.OtherBase = 0x400cd0 <vtable for Final+16>, oa = 0}, <Inheri> = {<Base> = {_vptr.Base = 0x400ce8 <vtable for Final+40>, a = 21, static b = 10, c = 22}, c = 23, static d = 11}, <No data fields>}"

    可以观察到:

      1) 子对象从右向左的被构建

      2) 分别包含OtherBase和Base的vptr,这是为了在derived class object 赋予base class object时更容易处理.

继续查看OtherBase和Base中vptr的信息

    _vptr.OtherBase所指向的vtbl中信息为 : 0x400a5c <Final::show()>

     _vptr.Base所指项的vtbl中信息为 : 0x400a86 <_ZThn16_N5Final4showEv>

    可以看到这两个vtbl所保存的都是Final::show.因此, 通过Final来为各个基类指针赋值时,最后总是调用Final自身的虚函数.

Virtual Inheritance


  

 class _ios {
public:
int i;
virtual void show();
};
void _ios::show() {
cout<<"ios"<<endl;
} class _istream : public _ios {
public:
int is;
virtual void show();
};
void _istream::show() {
cout<<"istream"<<endl;
} class _ostream : public _ios {
public:
int os;
virtual void show();
};
void _ostream::show() {
cout<<"ostream"<<endl;
} class _iostream : public _istream, public _ostream {
public:
int ios;
virtual void show();
};
void _iostream::show() {
cout<<"iostream"<<endl;
}

  在未使用virtual inheritance时, 查看 _iostream 对象, 会看到两份 _ios 类的对象,分别属于_istream和_ostream :

    "{<_istream> = {<_ios> = {_vptr._ios = 0x400c90 <vtable for _iostream+16>, i = 0}, is = 0}, <_ostream> = {<_ios> = {

_vptr._ios = 0x400ca8 <vtable for _iostream+40>, i = 0}, os = 0}, ios = 0}"

    符合之前介绍的single inheritance with polymorphism, _istream和_ostream分别使用从_ios中而来的vptr.ios来指向自己的vtbl. 

  使用virtual inheritance时 ,可以看到只有一份 _ios对象:

    "{<_istream> = {<_ios> = {_vptr._ios = 0x400d18 <vtable for _iostream+88>, i = 0}, _vptr._istream = 0x400cd8 <vtable for _iostream+24>, is = 0}, <_ostream> = {_vptr._ostream = 0x400cf8 <vtable for _iostream+56>, os = 0}, ios = 0}"

    在虚继承中,没有和单一继承中那样继承基类的vptr, 而是拥有自己的vptr.

继续查看vtbl中内容,分别显示 :

    0x400a40 <_ZTv0_n24_N9_iostream4showEv>

    0x400a3a <_ZThn16_N9_iostream4showEv>

    可见, 虚函数表中的函数也都是_iostream class中的member function.因此, 无论_iostream对象赋值给那个base class subobject的指针,总能调用到_iostream class的virtual function.

Reference


  <Inside the C++ Object Model>

  <GDB Manul>

备注


  更多内容详见 https://github.com/CarlSama/Inside-The-CPP-Object-Model-Reading-Notes

C++ 虚函数机制学习的更多相关文章

  1. C++中对C的扩展学习新增内容———面向对象(继承)函数扩展性及虚函数机制

    1.c语言中的多态,动态绑定和静态绑定 void do_speak(void(*speak)()) { speak(); } void pig_speak() { cout << &quo ...

  2. 浅谈C++虚函数机制

    0.前言 在后端面试中语言特性的掌握直接决定面试成败,C++语言一直在增加很多新特性来提高使用者的便利性,但是每种特性都有复杂的背后实现,充分理解实现原理和设计原因,才能更好地掌握这种新特性. 只要出 ...

  3. 匹夫细说C#:从园友留言到动手实现C#虚函数机制

    前言 上一篇文章匹夫通过CIL代码简析了一下C#函数调用的话题.虽然点击进来的童鞋并不如匹夫预料的那么多,但也还是有一些挺有质量的来自园友的回复.这不,就有一个园友提出了这样一个代码,这段代码如果被编 ...

  4. [C/C++] 虚函数机制

    转自:c++ 虚函数的实现机制:笔记 1.c++实现多态的方法 其实很多人都知道,虚函数在c++中的实现机制就是用虚表和虚指针,但是具体是怎样的呢?从more effecive c++其中一篇文章里面 ...

  5. 【高级】C++中虚函数机制的实现原理

    多态是C++中的一个重要特性,而虚函数却是实现多态的基石.所谓多态,就是基类的引用或者指针可以根据其实际指向的子类类型而表现出不同的功能.这篇文章讨论这种功能的实现原理,注意这里并不以某个具体的编译器 ...

  6. C++ 多态、虚函数机制以及虚函数表

    1.非virtual函数,调用规则取决于对象的显式类型.例如 A* a  = new B(); a->display(); 调用的就是A类中定义的display().和对象本体是B无关系. 2. ...

  7. c++虚函数的学习

    1.虚函数 #include<iostream.h> class Base { public: void print() { cout<<"Base"< ...

  8. [置顶] 【C/C++学习】之十三、虚函数剖析

    所谓虚函数,虚就虚在“推迟联编”或者“动态联编”上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的.由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被称为“ ...

  9. 你好,C++(37)上车的人请买票!6.3.3 用虚函数实现多态

    6.3.3  用虚函数实现多态 在理解了面向对象的继承机制之后,我们知道了在大多数情况下派生类是基类的“一种”,就像“学生”是“人”类中的一种一样.既然“学生”是“人”的一种,那么在使用“人”这个概念 ...

随机推荐

  1. android开发 不注意的异常

    近期刚上班,做了一个应用,要上线.測试天天測试,天天有bug,个人总结了几个常常忽略的地方: 1:在 继承 BaseAdapter 的 @Override public int getCount() ...

  2. C#整理4——循环语句

    一.循环语句  定义:可以反复执行某段代码,直到不满足循环条件为止. 循环的四要素:初始条件.循环条件.状态改变.循环体. 1.初始条件:循环最开始的状态. 2.循环条件:在什么条件下进行循环,不满足 ...

  3. C# 后台调用script使用类

    在网站的开发的时候,总是会用到一些前台的提示的script的代码,从项目中整理了一份常用的方法. public class Jscript { public Jscript() { // // TOD ...

  4. JS函数——作用域

    一 : 作用域的相关概念 首先看下 变量作用域 的概念:一个变量的作用域是程序源代码中定义这个变量的区域.————————<javascript权威指南>第六版全局变量拥有全局作用域,函数 ...

  5. HDU 1021 - Fibonacci Again

    找规律,分析让 F[N] 每一项对 3 取余的余数: 1,2,0, 2,2,1,0, 1,1,2,0, 2,2,1,0, 1,1,2,0, 2,2,1,0 ......... 显然循环了 #inclu ...

  6. hdu3525

    题目大意:某个大学有个2个校区,此大学有n(1<=n<=10000)个运动员,这n个运动员在每个校区都挑选了m(1<=m<=10)个拉拉队.现在每个校区(A/B)中,这m*n个 ...

  7. 轻松解决ubuntu系统引导问题

    什么是ppa PPA,表示 Personal Package Archives,也就是个人软件包集. 有很多软件因为种种原因,不能进入官方的 Ubuntu 软件仓库. 为了方便 Ubuntu 用户使用 ...

  8. Android 大神博客汇集

    非常给力的CSDNBlog和个人Blog,这些Blog都有一个共同的特点,即内容详实,讲解透彻,也算是给后来的初学者指一条路吧!只要你下定决心跟随强者的脚步,成为人们眼中的大神,只不过是时间问题! 下 ...

  9. canvas总结:线段宽度与像素边界

    在canvas中,我们经常需要绘制线段,主要使用moveTo和lineTo两个方法,moveTo移动至线段起始点,lineTo将线段绘制至终点.同时,绘制线段时可以指定线段的宽度,使用lineWidt ...

  10. Linux查看网卡状态

    观看网卡传送.接收数据包的状态 $ netstat  -i Kernel Interface table Iface MTU Met RX-OK RX-ERR RX-DRP RX-OVR TX-OK ...