下面的C++代码定义了一个类Point:
view plaincopy to clipboardprint?
class Point
{
public:
Point(float xval);
virtual ~Point();
float x() const;
static int PointCount();
protected:
virtual ostream& print( ostream &) const;
float _x;
static int _point_count;
};
class Point
{
public:
Point(float xval);
virtual ~Point();
float x() const;
static int PointCount();
protected:
virtual ostream& print( ostream &) const;
float _x;
static int _point_count;
};
现在定义Point的一个对象pt:Point pt; 这个笔记讨论的问题就是:如果让你设计这个对象的内存布局,你会怎么设计它?(内存布局在这里指的是对象各个成员在内存的排放)
下面给出Lippman在《深度探索C++对象模型》中提出的三种可能的内存布局。
一、简单的方式(原文是:simple object model)
Point pt;如下图所示:
看着上面的图,疑问就来了:“简单在哪里啊,简单在哪里?”,简单在编译器设计者的眼睛里…从编译器设计者的角度来说,这种设计是比较简单的,符合他们偷懒的习惯,因为每个对象一出来,有几个成员,就分配几个指针就可以了,比如pt,有7个成员(下面再讨论有继承的情况),所以就有7个指针,如果是数据成员,那么就指向实际的数据;如果是成员函数,就指向该函数的入口地址。而且,这种做法还有第二个好处,那就是避免了不同大小的成员有不同的存储空间这么一个东西。什么意思呢?比如有一个类有两个数据成员:char,int的,char占一个字节,而int占4字节,但是我用的是32位机,所以默认情况是4字节对齐的(关于结构体的对齐方式,请google相关的资料),所以编译器为对象分配空间大小的时候,还得考虑这些情况,多累啊…现在的这个模型,很好,反正每个指针都是4字节,就没有什么对齐不对齐的说法了。那对于编译器的设计者们就简单多了。
下面来看看这种模型怎么用于继承和多态的例子(是我的讨论,书上写的不是很详细)
比如下面的代码:
+ expand sourceview plaincopy to clipboardprint?
class Point2D : public Point
{
public:
Point2D(float xval, float yval);
virtual ~Point2D();
virtual ostream& print(ostream&);
protected:
float _y;
};
class Point2D : public Point
{
public:
Point2D(float xval, float yval);
virtual ~Point2D();
virtual ostream& print(ostream&);
protected:
float _y;
};
那么现在定义一个Point2D的对象pt2d,这个对象的内存布局又是怎么通过上面的所谓简单的内存布局体现出来的呢?
有以下两种方法:
1、在pt2d中,再分配多一个指针,指向基类的布局。
下面我们来看看这样的布局怎么实现多态?
以上面的虚函数print为例子,如以上的代码。我的办法就是:调用虚函数的时候,先查找派生类的部分,有没有指向print的指针(在本例中,有);如果有,就调用派生类的,如果没有,再到基类那边去找。
是不是感到有些眼熟啊?比如在MFC消息映射的实现代码中…可以参考在下写的“MFC消息映射原理”
2、对于派生类的第二种布局就是把基类的成员直接嵌入派生类里面去。
关于多态的实现也是通过类似于上面的第一种派生布局的方法来实现的,即先找派生类的,再找基类的。
其实,这种布局还有第三个优点,那就是减少了编译依赖,比如突然有一天,你要在类里面改一个成员的名字,或者改一个成员函数的实现,这种布局,只需要重新编译类的实际定义的地方就可以了,关于类的对象及其使用的地方,都可以不用编译(不过对于增加或者减少成员的时候就不行)
优点说完了,好像也挺好的;不过根据中国的伟大的阴阳学说,下面该说缺点了:
不知看到了上面的三个优点,你发现了什么?不知道有没有看见,漏了一个重要的东西,那就是---效率;效率是c++的最重要的指标之一(c++的两条最重要的指标:与c兼容,追上c的效率);所以,缺点就是没有效率。
具体分析一下,每个对象都放着指向本类的成员函数的指针,这样会造成大量的重复。多态的时候,还得先找派生类,再找基类,会浪费了大量的时间;又浪费时间又浪费空间的做法,C++怎么可以接受呢?所以这种内存布局不出现在实际的C++编译当中。但是,这种做法启发了Lippman他们,发明了:指向对象成员的指针
二、表格驱动(Table-Driven)的内存布局
还是上面的Point类的对象pt,看一下这种模型是什么东东?
表格驱动的内存模型,比起上面的第一种布局,还多了一个间接层,那样做的好处就是:在扩展成员的时候,pt本身的布局没有变,还是两个表格,变的只是定义的东西,编译器就可能会根据这些情况来优化布局,使得减少编译依赖。
缺点嘛,不用说,上面的那个简单的内存布局都已经是低效而不为c++所接收的了,更何况这个,马上就淘汰出局了。不过这个模型也给Lippman一些启发:发明了虚函数表。
关于这个模型在继承的情况下究竟是如何的?下面画个图看看:
关于表格驱动的简介:
以上的布局,由于对象的内存里面只是两个指针指向两个相应的表格,所以叫做表格驱动;表格驱动在很多地方都用的到,比如:有限状态机(编译原理和IP协议)等等,是用的比较多的一种方法,缺点是效率,优点是灵活。这里就不多说了,如果有兴趣,可以看看相关文章。
顺便说一句,其实MFC里面的消息映射的做法,与其说是参考第一种布局,还不如说参考第二种布局,用的就是表格驱动(类似的还有QT的信号与槽,VCL的事件机制等等)
<!--[if !supportLists]-->三、<!--[endif]-->C++中真实的对象的内存布局
千呼万唤始出来,终于到正题了。说道这里,想起一句经典:“美女这东西,就像鲜花一样,需要绿叶的陪衬才显示出她的娇美”~
绿叶已经介绍完毕,下面看鲜花,呵呵。
下面的重点在于解释一下这个内存布局的特点:
1、非静态(nonstatic)数据成员,每个对象的内存空间里面有一份。
2、静态数据成员,静态与非静态的成员函数,在整个内存里面只有一份实体。
3、定义了虚函数的每一个类,都有一份虚函数表;表项里面放的是这个类的相关的函数调用(等一下画个关于继承的看看就知道了)
4、每个定义了虚函数的类的对象里面都有一个指针(像上面的_vptr_Point),指向本类的虚函数表。
注意:
1)、如果有定义了两个对象Point pt1; Point pt2; 那么pt1,和pt2的_vptr_point指向的都是同一个表格。*(pt1._vptr_point) == *(pt2._vptr_point),因为每个类的虚函数表只有一份!
2)、_vptr_point只是为了说明问题而模拟出来的,也就是说c++程序是不能访问这个东西的。(为了显示我的博学,写个诗句在这里做验证:不识庐山真面目,只缘身在此山中),所以你不能通过c++本身来理解c++底层的机制,只能通过更为底层的语言(汇编,或者机器码,甚至可以用0-1电流来理解都是可以滴)
缺点就是上面二种类型的优点,这个优点就是上面两种模型的缺点,就这么简单。
下面以Point2D的内存布局图结束这篇笔记吧。(_vptr_Point2D所指向的这个表格就是Point2D的虚函数表,留意一下和上面的Point pt的布局有什么不同)
关于这个模型对c++的影响到底有多大,请看下文~
PS. 本文所作笔记的所有题材来源于《深度探索c++对象模型》的第一章第一节。
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/jiangnanyouzi/archive/2009/01/14/3775637.aspx
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/jiangnanyouzi/archive/2009/01/14/3775637.aspx