C++面向对象特性实现机制的初步分析 Part2

类别:编程语言 点击:0 评论:0 推荐:

本人beta版的毕业论文,请各位指正!

 

Chapter 1 准备知识

 

C++是一种面向对象的高级语言,要了解它的一些内部机制,我们有必要先熟悉其二进制代码的编译过程,并且要了解运行这些二进制代码时内存中各个区域的变化情况。

 

1.程序对内存的使用方法

代码区

全局数据区

堆区

栈区

code area

data area

stack area

heap area

任何需要CPU执行的程序都必须以二进制代码的形式存储在内存中,因此,程序代码在内存中的存储方式和结构是我们首先需要了解的。当程序得到操作系统分配给它的内存区域之后,它将这个区域分为四个部分,见右图:

图 1-1

这四个区域存储含有不同逻辑意义的二进制信息,请看下面的代码:

 

/////////////////////////////////////////////

//Source Code 1.1

#include <iostream.h>

 

static int a=5;

static int b=1;

 

class Test

{

public:   

       Test(){cout<<"Create!"<<endl;}

       ~Test(){cout<<"Destroy!"<<endl;}

private:

       int count;

};

 

int foo(int x, int y)

{

       return x+y;

}

 

void main(void)

{

       int i=0;

       i=foo(a,b);

       cout<<"i= "<<i<<endl;

 

       Test* pTest;

       pTest= new Test;

}

 

 

 

 

//静态全局变量,存储在data area中

//静态全局变量,存储在data area中

 

//类 Test, 用来测试对heap区域的使用

 

 

//构造函数

//析构函数

 

//私有数据

 

 

//函数,编译器生成代码后,将其存储在code

//area中,函数运行时的中间变量,如形参x,y

//以及x+y产生的临时变量,都保存在stack

//area中,在函数返回时,系统自动清空函数

//压入stack area内的变量

 

 

// main()函数为程序的入口,程序开始运行时,// CPU中的指令寄存器被设置为code area中

// main()函数的位置

 

//声明指向Test类的指针

//new操作符在heap中构造Test类的实例

//并将指针pTest指向Test实例的入口地址

 

将上面的程序编译,系统将各个函数的代码存放到代码区,将静态变量,常量,全局变量保存在全局数据区。运行时,CPU首先执行main()函数中的代码,系统向栈区内压入main()函数的局部变量i,程序运行到对foo()函数的调用时,系统将CPU指令寄存器压栈,并将其内容设定为foo()的地址,控制转到foo()函数。foo()函数在栈区中创建其局部变量x,y, 完成x+y的运行后将结果以临时变量的方式返回给调用者。return之后,系统清空foo()在栈区中存放的变量,并将先前压栈的指令寄存器内容出栈,转而执行main()中接下来的代码。

 

声明Test* pTest后,main在栈区中创建一个指针变量,然后调用new在堆区中创建一个Test的实例,并将pTest指向它。运行该程序,我们可以发现,Test类的构造函数被执行,但析构函数没有执行。原因是这样的,栈区内的变量值在创建这个变量的函数以内有效,例如,函数foo 返回后,形参x, y从栈中清除,main返回后,指针变量pTest也被从栈区中清除;但指针指向的堆区中的Test实例,仍然存在,类的析构函数没有被调用。由此我们可以看出栈区和堆区得区别:

 

栈区(stack area):  存放程序的局部数据,即各个函数中的局部变量

堆区(heap area):  存放程序运行中的动态数据

 

对栈区的使用,在编译的时候就已经确定,而堆区的使用是在程序运行中动态进行的。堆区中的数据的创建和删除要由程序员来自己控制,系统不会对它像栈区那样进行自动的清除,使用堆区要防止产生例子程序中那样的错误,用new创建对象时候,使用完毕后一定要把它delete, 不然会出现内存泄漏(memory leak)。

 

栈区和堆区的区别,后文还会有进一步的分析。

 

以上是对程序执行过程中对各个内存区使用情况的一个简单介绍。实际情况可能比这里描述的要复杂一些,比如传递函数返回值的那个临时变量的处理,函数如何对栈进行清除(涉及到调用规范__cdecl, __fastcall 和 __stdcall)。本节主要是了解程序对内存的使用情况,对这样的细节就不做深入地分析了。

 

2. C++ Class内存格局

 

C++引入了Class这个概念,Class封装了一组相关的数据和对这些数据的操作,这是实现面向对象编程的基础。在下面一节中,我将针对Class的内存布局,做一些分析。

 

我们知道,Class中有成员函数和成员变量,其中,成员函数分为普通成员函数,静态成员函数和虚函数,成员变量分为普通成员变量和静态成员变量。我们从最简单的情况开始分析。请看下面的代码:

 

/////////////////////////////////////////////

//Source Code 1.2

#include<iostream.h>

 

class Test1

{

public:

       int foo(){return 0;}

private:

       int     member_1;

       float   member_2;

       double* p;

};

 

class Test2

{

public:

       int foo(){return 0;}

       virtual int v_foo(){return 0;}

private:

       int     member_1;

       float   member_2;

       double* p;

};

 

class Test3

{

public:

       int foo(){return 0;}

       int static s_member;

private:

       int     member_1;

       float   member_2;

       double* p;

};

 

class Test4

{

public:

       int foo(){return 0;}

       static int s_foo(){return 0;}

private:

       int     member_1;

       float   member_2;

       double* p;

};

 

void main(void)

{

int a=sizeof Test1;

int b=sizeof Test2;

int c=sizeof Test3;

int d=sizeof Test4;

 

cout<<"Size of class Test1 is:"<<a<<endl

       <<"Size of class Test2 is:"<<b<<endl

       <<"Size of class Test3 is:"<<c<<endl

       <<"Size of class Test4 is:"<<d<<endl;

}

 

 

 

 

//Test1类,其中封装了三个普通的成员变量

//一个普通成员函数

 

//返回int型值的普通成员函数

//在32位系统中,各个变量的尺寸如下

//sizeof (int)      -->4

//sizeof (float)     -->4

//sizeof (double*)  -->4

 

 

//Test2类在Test1的基础上增加了虚函数

 

 

 

//虚函数

 

 

 

 

 

 

//Test3类在Test1的基础上增加了静态成员//变量

 

 

//静态成员变量

 

 

 

 

 

 

//Class4类在Test1的基础上增加了静态成

//员函数

 

 

//返回int型值的静态成员函数

 

 

 

 

 

 

//在main中测试每一个类的尺寸

 

 

 

 

 

 

//输出:Size of class Test1 is: 12

//输出:Size of class Test2 is: 16

//输出:Size of class Test3 is: 12

//输出:Size of class Test4 is: 12

 

 

现在我们来分析一下程序输出的结果:

 

Test1类的尺寸为12,恰好为三个普通成员变量尺寸之和,那成员函数foo()哪里去了?留下这个疑问,继续往下看。Test2比Test1多了一个虚函数,结果尺寸比Test1多了4个字节,一个函数怎么会只有4个字节?留下第二个疑问,继续。Test3类和Test4类分别比Test1类多了一个静态成员变量和静态成员函数,但这个增加却没有在类的尺寸上反映出来,不解!

 

通过归纳,我们可以总结出这样的事实

a. 类中包括普通成员变量,但不包括普通成员函数

b. 虚函数以某种形式包含在类中,但函数体本身肯定不在类中

c. 类的静态成员变量和静态成员函数不包括在类中

 

进一步思考,我们可以想到:

 

为了提高性能,减少对内存的需要,我们没有必要把类的函数包含在类的实体中,创建10个Test1的实例,这10个实例中包含10份相同的的foo()代码是非常愚蠢的。类中包含类本身数据(普通成员变量),当要对数据进行操作时,通过一定的方式调用成员函数即可。类实际上是调用Test1::foo()来访问成员函数的(这其中的一些细节会在“封装”一章中进一步阐述)。

 

再看虚函数的问题,增加虚函数后,类的体积增加了4个字节,这恰好是一个指针的尺寸,我们有理由认为这4个字节是指向虚函数入口的函数指针,这样我们第二个问题也可以得到很好地解释。可是我们为什么不用Test2::v_foo()的方式来访问类的虚函数呢?这个问题会在“多态”一章中,给出完美的答案。(其实类中的这个指针指向的是虚拟函数表而非虚函数的实际入口地址,具体请参考“多态”一章)

 

现在再来看静态成员函数和静态成员变量,根据上面的思路,一切都已经很清楚了。静态成员为所有类的实例所共享,没有必要把它们都放到实例中去,可以像寻址成员函数那样对它们进行操作:Test3::s_member和 Test4::s_foo()。

 

通过下面这张图,可以更清楚地了解C++ Class的内存格局

 

 

Test::V_foo()

int  member_1

float member_2

double* p

指向虚函数的指针

Test 类

实际上此处为虚拟函数表

 

Test::foo()

Test::s_foo()

Test::s_member

普通成员函数

静态成员函数

静态成员变量

图1-2

 

3. 编译期和运行期的区别

 

编译期是源代码向二进制指令转化的过程,而运行期则是CPU执行这些指令的过程。

本文地址:http://com.8s8s.com/it/it29697.htm