统计

  • 随笔 - 50
  • 文章 - 42
  • 评论 - 147
  • 引用 - 0

留言簿(6)

随笔分类

文章分类

Link

搜索

  •  

积分与排名

  • 积分 - 160082
  • 排名 - 161

最新评论

阅读排行榜

评论排行榜

MSVC++ 对象内存模型深入解析与具体应用 (二)

MSVC++ 对象内存模型深入解析与具体应用

前言:本文之所以强调MSVC, 旨在提醒读者在不同平台和解释器下内存布局和实现上存在差异,但编程思想通用,文中内容大多来自笔者实际工作经验和网上搜集,力求正确,但水平有限,如有不当之处,敬请指出

面向对象:本文面向有一定C/C++基础,并且可以读懂部分汇编的读者

版权:欢迎转载,但请注明出处http://www.cppblog.com/dawnbreak/ 保留对本文的一切权力

目录
1. C++基本类型与结构体内存布局
                Key words: class,  struct, memory alignment

2.虚表, 多态与动态绑定

                Key words: Virtual Table, polymiorphism

3.对象池

                Key words: object pool , reload, new ,delete

4.内存泄漏检测

                Key words: memory leak detect

5.智能指针

                Key words: smart pointer

6.   编译期类型约束
   
                Key words: compile-time ,type-constraint


 Appendix 1: C++堆栈祥解



第二章  虚表, 多态与动态绑定

Key words: Virtual Table, polymiorphism

1.虚表-Virtual Table 多态-polymiorphism

       虚函数表由虚函数的地址组成,表中函数地址的顺序和它们第一次出现的顺序(即在类定义的顺序)一致。若有重载的函数,则替换掉基类函数的地址,事实上你可以简单的将虚表定义理解如下:

        Int* virtualTable[size]//普通的指针数组而已

        
多数情形下,MSVC的类按如下格局分布:

Ÿ           指向虚函数表的指针(_vtable__vftable_),不过它只在类包括虚函数,以及不能从基类复用合适的函数表时才会被添加。

Ÿ           基类。

Ÿ             函数成员。

请看如下例子:


#include "stdafx.h"
#include 
"assert.h"
#include 
"iostream"
using namespace std;
class A
{
public:
  
int b1;
  
static int b3;
  
int b2;
public:

  
virtual void A_virt1()
  
{
      std::cout
<<"  call of first A's vf"<<std::endl;
  }


  
virtual void A_virt2()
  
{
      std::cout
<<"  call of second A's vf"<<std::endl;
  }


}
;
int A::b3=100;
//#pragma pack(1)
class B:public A
{
    
int a1;
    
char b1;
    
float c1;
  
virtual void A_virt2()
  
{
      std::cout
<<"  call of second B's vf"<<std::endl;
  }
    
  
virtual void B_virt1()
  
{
        std::cout
<<"  call of second B's vf1"<<std::endl;
  }

    
virtual void B_virt2()
  
{
        std::cout
<<"  call of second B's vf2"<<std::endl;
  }

    
void getsome();
}
;
void B::getsome()
{
    
int a=1;
}


class D:public A
{
}
;

class C:public B,public D
{
  
virtual void B_virt1()
  
{
        std::cout
<<"  call of first C's vf"<<std::endl;
  }

}
;
int _tmain(int argc, _TCHAR* argv[])
{
    typedef 
void(*pfunc)();
    cout
<<"test the class memory layout-virtual table"<<endl;
    C cc;
    (pfunc(((
int**)(&cc))[0][0]))();
    (pfunc(((
int**)(&cc))[0][1]))();
    (pfunc(((
int**)(&cc))[0][2]))();
    (pfunc(((
int**)(&cc))[0][3]))();

    system(
"pause");
    
return 0;
}

程序输出结果:


以下是各个类在内存中的布局图

class A size(12):
 
+---
 
0 | {vfptr}
 
4 | b1
 
8 | b2
 
+---
A::$vftable@:
 
| &A_meta
 
|  0
 
0 | &A::A_virt1
 
1 | &A::A_virt2
A::A_virt1 
this adjustor: 0
A::A_virt2 
this adjustor: 0
class B size(24):
 
+---
 
| +--- (base class A)
 
0 | | {vfptr}
 
4 | | b1
 
8 | | b2
 
| +---
12 | a1
16 | b1
   
| <alignment member> (size=3)
20 | c1
 
+---
B::$vftable@:
 
| &B_meta
 
|  0
 
0 | &A::A_virt1
 
1 | &B::A_virt2
 
2 | &B::B_virt1
 
3 | &B::B_virt2
B::A_virt2 
this adjustor: 0
B::B_virt1 
this adjustor: 0
B::B_virt2 
this adjustor: 0
class D size(12):
 
+---
 
| +--- (base class A)
 
0 | | {vfptr}
 
4 | | b1
 
8 | | b2
 
| +---
 
+---
D::$vftable@:
 
| &D_meta
 
|  0
 
0 | &A::A_virt1
 
1 | &A::A_virt2
class C size(36):
 
+---
 
| +--- (base class B)
 
| | +--- (base class A)
 
0 | | | {vfptr}
 
4 | | | b1
 
8 | | | b2
 
| | +---
12 | | a1
16 | | b1
   
| | <alignment member> (size=3)
20 | | c1
 
| +---
 
| +--- (base class D)
 
| | +--- (base class A)
24 | | | {vfptr}
28 | | | b1
32 | | | b2
 
| | +---
 
| +---
 
+---
C::$vftable@B@:
 
| &C_meta
 
|  0
 
0 | &A::A_virt1
 
1 | &B::A_virt2
 
2 | &C::B_virt1
 
3 | &B::B_virt2
C::$vftable@D@:
 
| -24
 
0 | &A::A_virt1
 
1 | &A::A_virt2
C::B_virt1 
this adjustor: 0

为了调用虚函数,编译器首先需要从_vftable_取得函数地址,然后就像调用简单方法一样(例如,传入_this_指针作为隐含参数)。例如:

          cc.A_virt2()

          ;esi = ptr [cc]

          mov eax, [esi]  ;fetch virtual table pointer

          mov ecx, esi

          call [eax+4]  ;call second virtual method

          ;cc->B_virt1()

          ;edi = pC

          lea edi, [esi+8] ;adjust this pointer

          mov eax, [edi]   ;fetch virtual table pointer

          mov ecx, edi

call [eax]       ;call first virtual method


注意到上面class A的内存布局图,首先是VT指针,然后是成员变量b1,b2, 而对于静态成员b3并没有体现,事实上b是存储在程序的全局静态数据区,供该类的所有实例共享,这里请注意在classA中虚表中虚函数出现的顺序和位置,这一点很重要,接着再看classB中虚函数出现的顺序和位置,注意到A_virt1,A_virt2在classA和classB中出现的顺序和位置一致,而所不同的是在classB的虚表中A_virt2已经被替换,这就是多态的关键所在,每一个虚函数本身其实不过是一个固定的偏移量,而真正实现多态的其实是在编译器的虚函数表的替换动作.

而对于多继承情况要复杂一些,例如在ClassC中每一个继承路径中都存在一个虚表,如果在没函数里再加入如下调用:
 (pfunc(((int**)(&cc))[6][0]))();
 (pfunc(((int**)(&cc))[6][1]))();
会输出:
call of first A's vf
call of second A' vf
这样一个类中同时存在两个一抹一样的函数,那么当你用
C cc;
cc.A_virt2()时会怎么样呢?
你会得到以下错误:
error C2385: ambiguous access of 'A_virt2'

解决办法有两种:
1. 调用时加入域操作符,例如:
cc.A::A_virt2();
cc.B::A_virt2( );
这种办法最稳妥也最清晰
2. 使用虚基类
代码改动如下:

。。。。。。。
class B:virtual public A
。。。。。。。
class D:virtual public A
。。。。。。。
int _tmain(int argc, _TCHAR* argv[])
{
。。。。。。。
 (pfunc(((int**)(&cc))[0][0]))();
 (pfunc(((int**)(&cc))[0][1]))();
 //(pfunc(((int**)(&cc))[0][2]))();
 //(pfunc(((int**)(&cc))[0][3]))();
 //(pfunc(((int**)(&cc))[6][0]))();
 //(pfunc(((int**)(&cc))[6][1]))();
 cc.A::A_virt2();
 cc.A_virt2();
。。。。。。。。
}

内存布局变为:

class C size(36):
 
+---
 
| +--- (base class B)
 
0 | | {vfptr}
 
4 | | {vbptr}
 
8 | | a1
12 | | b1
   
| | <alignment member> (size=3)
16 | | c1
 
| +---
 
| +--- (base class D)
20 | | {vbptr}
 
| +---
 
+---
 
+--- (virtual base A)
24 | {vfptr}
28 | b1
32 | b2
 
+---
C::$vftable@B@:
 
| &C_meta
 
|  0
 
0 | &C::B_virt1
 
1 | &B::B_virt2
C::$vbtable@B@:
 
0 | -4
 
1 | 20 (Cd(B+4)A)
C::$vbtable@D@:
 
0 | 0
 
1 | 4 (Cd(D+0)A)
C::$vftable@A@:
 
| -24
 
0 | &A::A_virt1
 
1 | &thunk: this-=4goto B::A_virt2

 

多了一个vbtable存储偏移量,第一个元素存储vbtable与该类的偏移量,第二个元素存储vbtable与公共基类的偏移量,而且注意到,vftable@A 的第二个虚函数被定向到B:A_virt2
这样问题解决了,但是你会得到一个警告:
Warning 1 warning C4250: 'C' : inherits 'B::B::A_virt2' via dominance
显示继承了 'B::B::A_virt2‘ ,也就是说你在调用
cc.A_virt2时,默认直接去调用B::A_virt2,这可能并不是你所期望的,所以使用时需要慎重


2 . 动态邦定与静态邦定
邦定是指一个计算机程序自身彼此关联的过程。按照邦定所进行的阶段不同,可分为两种不同的邦定方法:静态邦定和动态邦定。
静态邦定
静态邦定是指邦定工作出现在编译连接阶段,这种邦定又称早期邦定,因为这种邦定过程是在程序开始运行之前完成的。
在编译时所进行的这种邦定又称静态束定。在编译时就解决了程序中的操作调用与执行该操作代码间的关系,确定这种关系又称为束定,在编译时束定又称静态束定。
 1class AA
 2{
 3public:
 4   void test()
 5   {
 6      cout<<"I am class AA!"<<endl;
 7   }

 8}
;
 9class BB
10{
11public:
12   void test()
13   {
14      cout<<"I am class BB!"<<endl;
15   }

16}
;
17int _tmain(int argc, _TCHAR* argv[])
18{
19    AA *A=(AA *)(new BB);
20    A->test();
21}

读者可以想一下以上例子的结果,如果说是I am class BB!

C++没有你想得那么职能,C++调用函数不过是指针偏移,而一般成员函数代码是在数据存储区的共享代码段,声明了AA类的指针 A 就已经指定了偏移的起点是类型AA的代码段起点,这一步就是所谓的动态邦定,而调用->test();只能得到I am class AA!

也许你要说我并没有实例化AA怎么会有那一段代码呢,请注意代码生成和实例化是完全不同的两个阶段,编译在编译时发现你调用了AA::test(); 那么就会载入相应的symbol,程序启动时就会载入相应代码段。

也许你还要说没有用继承的关系,那么你可以自己试验一下使BB继承自AA 结果还是一样的

想要实现想要的结果唯一的方法就是使用虚函数来实现动态邦定;

 

posted on 2009-03-11 14:25 pear_li 阅读(1938) 评论(4)  编辑 收藏 引用 所属分类: C++

评论

# re: MSVC++ 对象内存模型深入解析与具体应用 (二) 2009-03-11 15:07 电脑学习步步高

  回复  更多评论    

# re: MSVC++ 对象内存模型深入解析与具体应用 (二) 2009-03-12 12:49 梦在天涯

你可以研究下对象与对象的类型信息怎么联系起来的,就是当调用dynamic_cast的时候为什么能够正确的识别对象的类型!

  回复  更多评论    

# re: MSVC++ 对象内存模型深入解析与具体应用 (二) 2009-03-12 12:53 梦在天涯

加油!
  回复  更多评论    

# re: MSVC++ 对象内存模型深入解析与具体应用 (二) 2012-09-07 11:02 刘伟

如果把内存布局画成图就更好了,像《Inside the C++ Object Model》那样。感谢楼主的文章。学习了
  回复  更多评论    

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理