即日起在codingBlog上分享您的技术经验即可获得积分,积分可兑换现金哦。

不同类型对象的内存结构比较

编程语言 u012179109 13℃ 0评论

在前面总结C语言的语法规则时,我们总是以内存为根本进行说明。这样也使得面向过程的问题处理变得更加容易理解。而在C++中,由于语言设计的本身就是以现实对象的观点来看待要处理的问题,如果仍然以内存为核心来进行说明,反而会导致偏离面向对象的本意。但是仍然要说明的一点就是面向对象的语言设计的程序和面向过程设计的程序,在计算机真正执行的时候是没有任何区别的,所以我们在理解一些面向对象的语法时,仍然可以从它的内存使用方式以及语法的功能实现上加以说明。

以下的程序和反汇编代码都是基于VS2013,如有差异也没关系^^

以下分为无继承简单类,有继承简单类, 含有静态成员的类三个方面说明




1、一个简单的类所生成的对象,简单的类是指:只有基本类型的成员以及一些普通的函数。

classA

{

public:

A()

{

a= 3;

}

voidfun()

{

intb = a + 3;

return;

}

private:

inta;

};

//执行以下语句

A*pa = new A();

00F3968D push        4 //
首先 A对象只占4个字节的空间,也可以sizeofA)看到

00F3968F call        operator new(0F31415h) 
//调用new
运算符(一种特殊函数),参数就是之前压栈的数据

00F39694  add        esp,4 

00F39697 mov         dword ptr[ebp-0E0h],eax 
// 把申请到的内存空间的首地址临时保存在栈中

00F3969D  mov        dword ptr [ebp-4],0 

00F396A4 cmp         dword ptr[ebp-0E0h],0 
// 对申请到的内存空间进行检验,如果是NULL的化就不调用构造函数

00F396AB  je         wmain+70h (0F396C0h) 

00F396AD  mov        ecx,dword ptr [ebp-0E0h] 

00F396B3 call        A::A (0F314F6h) 
//正常情况下调用构造函数进行初始化。

00F396B8 mov         dword ptr[ebp-0F4h],eax 
// 赋值给pa

00F396BE  jmp        wmain+7Ah (0F396CAh) 

00F396C0  mov        dword ptr [ebp-0F4h],0 

00F396CA  mov        eax,dword ptr [ebp-0F4h] 

00F396D0  mov        dword ptr [ebp-0ECh],eax 

00F396D6  mov        dword ptr [ebp-4],0FFFFFFFFh 

00F396DD  mov        ecx,dword ptr [ebp-0ECh] 

00F396E3  mov        dword ptr [pa],ecx 

pa->fun();

00F396E6  mov        ecx,dword ptr [pa] 

00F396E9 call        A::fun (0F31519h) 
// 跳转到fun
函数所在的空间。

 

我们先看一下0F31519h处的数据,

_wmain:

00F314F1  jmp        wmain (0F39650h) 

A::A:

00F314F6  jmp        A::A (0F36870h) 

std::operator<<>:

00F314FB  jmp        std::operator<< >+4B0h(0F32CF0h) 

std::operator<<>:

00F31500  jmp        std::operator<< >+560h(0F32DA0h) 

std::operator<<>:

00F31505  jmp        std::operator<< >+500h(0F32D40h) 

_GetModuleHandleW@4+18:

00F3150A  jmp        _GetModuleHandleW@4+12h (0F394F0h) 

A::A:

00F3150F  jmp        A::A (0F36870h) 

_GetModuleHandleW@4+114:

00F31514  jmp        _GetModuleHandleW@4+72h (0F39550h) 

A::fun:

00F31519  jmp        A::fun (0F368D0h) 

可以看到在这个函数入口表中,保存了很多函数的入口地址,并且直接是一条跳转指令,执行这条指令之后,跳转到函数中执行。

—h:\workspace\c++\testcplus\testcplus\testcplus.cpp ————————-

voidfun()

{

00F368D0  push       ebp 

00F368D1  mov        ebp,esp 

00F368D3  sub        esp,0D8h 

00F368D9  push       ebx 

00F368DA  push       esi 

00F368DB  push       edi 

00F368DC  push       ecx 

00F368DD  lea        edi,[ebp-0D8h] 

00F368E3  mov        ecx,36h 

00F368E8  mov        eax,0CCCCCCCCh 

00F368ED  rep stos   dword ptr es:[edi] 

00F368EF  pop        ecx 

00F368F0  mov        dword ptr [this],ecx 

intb = a + 3;

00F368F3  mov        eax,dword ptr [this] 

00F368F6  mov        ecx,dword ptr [eax] 

00F368F8  add        ecx,3 

00F368FB  mov        dword ptr [b],ecx 

return;

}

这里面主要有三点内容:

1、申请对象空间时,其实只是保存成员所占的空间,在内存对齐时,与结构体分配空间的方式一致。

2、函数入口表,观察函数入口表的地址可以看到,在地址较低的地方保存了所有函数的入口地址。除了我们定义的函数,还有许多运行时函数。

3、类中真正的函数区:里面存储着函数的操作代码。需要说明的是,类代码的载入顺序是在编译时已经确定的。

2、简单类的继承情况

classA

{

public:

A()

{

a= 3;

}

voidfun()

{

intb = a + 3;

return;

}

private:

inta;

};

 

classB : public A

{

public:

voidg()

{

inta = 2 + 3;

}

private:

intb;

 

};

//main
函数中代码

B*pb = new B();

pb->g();

pb->fun();

VS 2013中反汇编代码如下:

pb->g();

002A2D86  mov        ecx,dword ptr [pb] 

002A2D89  call       B::g (02A151Eh) 

pb->fun();

002A2D8E  mov        ecx,dword ptr [pb] 

002A2D91  call       A::fun (02A1519h)

我们可以看一下函数入口表的内容:

A::fun:

002A14F1 jmp         A::fun (02A9650h) 

A::A:

002A14F6  jmp        A::A (02A6870h) 

_wmain:

002A14FB  jmp        wmain (02A2CF0h) 

_wmain+176:

002A1500  jmp        wmain+0B0h (02A2DA0h) 

_wmain+80:

002A1505  jmp        wmain+50h (02A2D40h) 

B::g:

002A150A  jmp        B::g (02A94F0h) 

A::A:

002A150F  jmp        A::A (02A6870h) 

B::g:

002A1514  jmp        B::g+60h (02A9550h) 

A::fun:

002A1519 jmp         A::fun (02A9650h) 

B::g:

002A151E  jmp        B::g (02A94F0h) 

B::B:

002A1523  jmp        B::B (02A68D0h)

仔细观察的话,可以发现一些有意思的事情,首先,这里面对于fun()函数只有A::fun(),而没有B::fun()

其次是,A::fun()函数入口有两个,并且他们是一样的。

由此,我们就可以推测出,在载入A类的时候把它的函数入口保存在入口表中,由于B继承AB中就有A中的所有方法。所以在载入B的函数入口时,就把B自身的函数和从A继承来的函数又重新载入一遍。这里我们可以会觉得有点重复,浪费了内存空间。实际上这样设置函数入口表还有很多其他的妙用。

 

另外,可能你会想到如果用A的对象调用fun()函数,就会通过上面的入口进入了(002A14F1h),但实际上,编译器并没有进行区分是A的对象调用fun,还是B的对象调用fun(),都用最后一个入口(002A1519)。

 

前面主要讨论了方法的执行问题,现在我们需要说明一下,一个对象的存储问题,在上面的例子中,基类A4个字节,子类B8个字节,其中有4个字节是因为继承了A的属性。查看内存可以发现它们在内存中的存放顺序是从基类继承的成员在前,子类特有的在后。

 

ps:在我试验的过程中,发现每次载入程序时,函数入口地址都有一些规律,

比如:fun的地址,第一次是02A
9650h,而第二次是
016
9650h,可以发现,他们的地址后4位都是一样的(二进制就是16位),如果你看过王爽老师的汇编的化,就会发现,现在的程序依然是通过内存段的方式来使用内存。这样对于程序的装载很方便。段式的内存使用和页式的内存管理是有区别的,一个是程序方面的,一个是系统方面的,这一点,我在操作系统的笔记中会详细说明。

 

3、关于静态方法的讨论

首先需要明确几点基本语法:


静态方法中不能使用非静态的成员变量和成员方法。


静态成员变量是在类载入时分配空间,与对象的定义无关。

在之前观察的基础上,我们直接以含有静态成员的类的继承为例,同时为了说明静态成员变量的一些性能,我们定义了全局变量。

classA

{

public:

A()

{

a= 3;

}

void fun() //
普通方法

{

intb = a + 3;

return;

}

static void func()//静态方法

{

intc = b ^ 3;

}

private:

inta;

static int b; //
静态成员

};

 

classB : public A

{

public:

voidg()

{

inta = 2 + 3;

}

};

 

int A::b = 0; //
静态成员初始化

int c = 5; //
全局变量

 

int_tmain(int argc, _TCHAR* argv[])

{

A* pa = new A();

pa->fun();

 

pa->func();

A::func();

 

B*pb = new B();

pb->g();

pb->fun();

 

pb->func();

B::func();

 

return0;

}

查看反汇编代码如下:

A* pa = new A();

01076A3D push        4 
// 申请空间的大小,可见静态成员所使用的空间跟普通成员不在一起。

01076A3F call        operator new(0107141Fh) 
//申请空间

01076A44  add        esp,4 

01076A47  mov        dword ptr [ebp-104h],eax 

01076A4D  mov        dword ptr [ebp-4],0 

01076A54  cmp        dword ptr [ebp-104h],0 

01076A5B  je         wmain+70h (01076A70h) 

01076A5D  mov        ecx,dword ptr [ebp-104h] 

01076A63 call        A::A (01071249h) 
//调用构造函数

01076A68 mov         dword ptr[ebp-118h],eax 
//赋值给pa

01076A6E  jmp        wmain+7Ah (01076A7Ah) 

01076A70  mov        dword ptr [ebp-118h],0 

01076A7A  mov        eax,dword ptr [ebp-118h] 

01076A80  mov        dword ptr [ebp-110h],eax 

01076A86  mov        dword ptr [ebp-4],0FFFFFFFFh 

01076A8D  mov        ecx,dword ptr [ebp-110h] 

01076A93  mov        dword ptr [pa],ecx 

 申请空间的大小,可见静态成员所使用的空间跟普通成员不在一起,在查看变量的地址后这一点会更加清晰。其他的前面都已说明

pa->fun();

01076A96  mov        ecx,dword ptr [pa] 

01076A99  call       A::fun (01071483h) 

pa->func();

01076A9E  call       A::func (0107100Ah) 

A::func();

01076AA3  call       A::func (0107100Ah)

关于这几行,首先是静态方法的调用方式,可以通过对象调用,也可以直接用类名调用,编译器把他们当做同样的内容进行处理。其次,静态方法的入口表和普通方法的入口表是在一起的,说明他们是一起载入的。也就是说,在载入一个类的时候,它的所有方法都是一起载入的,他们的区别也就是编译器所能识别的方式,普通方法只能通过对象调用。

A::fun:

01071483  jmp        A::fun (01076930h) 

A::func:

0107100A  jmp        A::func (01076980h) 

从这里可以看出来,函数内容载入时,也是一起顺序载入的。

B*pb = new B();

pb->fun();

01076B09  mov        ecx,dword ptr [pb] 

01076B0C  call       A::fun (01071483h) 

pb->func();

01076B11  call       A::func (0107100Ah) 

B::func();

01076B16  call       A::func (0107100Ah) 

由于B继承A
所以我们同样可以用B的对象来调用从A继承来的静态方法。可以认为B中有这些方法。

另外,我们看一下A::b的地址和全局变量c的地址

&A::b =
0x01080334h

&c  =
0x0108000Ch

可以看出他们的保存位置还是很接近的。

 

根据程序中保存的数据的功能,我们可以把内存分为以下几部分

栈区:用来存储
局部变量和参数变量

堆区域:用来存放用new
关键字分配的对象

代码区域:存放方法中的操作代码

方法入口表:存放方法的入口

全局(静态)变量区:存放静态变量和全局变量

 

转载请注明:CodingBlog » 不同类型对象的内存结构比较

喜欢 (0)or分享 (0)
发表我的评论
取消评论

*

表情