Crest的OO核心实现
至此,我们已经有了一个比较好用的宏封装的代码风格,一个基础的类对象结构的模拟代码,但是始终没有接触到OO的核心实现。这次,我们终于要进入实质的内容了---实现对象的继承和接口的实现。
单对象的内存布局
我们在《Crest的语法---宏的魔术汇演》实现了一个简单对象,含有成员变量,成员函数,如下图所示。其中buffer指向字符缓冲区,虚函数Format表现为一个函数指针。
这种结构把数据和成员函数一起放到了实例对象中,但是这个结构一旦遇到大规模的对象使用场景的时候,就会浪费很多空间,因为对于相同对象而言,其成员函数都是完全一样的,没有必要每一个对象都保留一份成员函数副本。所以我们可以把这样的结构修改一下:
这样就简单了,一个CString类在内存中可以有很多个实例,但是都共享同一个CStringType对象,CStringType对象负责存放虚函数和纯虚函数的指针。这个所谓的CStringType在C++里边叫做VTable,在delphi中叫做VMT,其实所有的OO语言都存在这个东西。
比较仔细的同学可能就有些疑问了,看前一篇文章中的代码,Format是虚函数,OnFormat是纯虚函数,DoFormat则是实函数,按照道理,实函数应该不要出现在CStringType中的。是的,因为我们用C语言的struct来模拟class,本来struct就不支持public / protected / private等可见性定义,模拟出来的都只能是public的,这是限制之一。如果我们还要严格区分虚实函数的话,也会因为C语言的限制,导致虚实函数调用语法的不一致,反而引来很多麻烦,更何况java也不区分虚实函数,它所有的成员函数都是虚函数,都是可以重载的,所以就算借鉴java的特性吧。
对象的继承结构
单个对象我们已经完成了内存布局的设计,接下来我们要实现类的继承。
要实现继承结构面临的第一个需求就是基类和派生类的相互转换。在编程实践中我们已经知道,基类和派生类之间,派生类可以当作基类一样来使用,就像 CObject obj = new CString() 这样,反过来也可以这样CString str = (CString) obj实现强制转换。大多数单继承结构的类都用简单的方法实现都是保证CString的前半部分和基类CObject的内存结构完全相同。
如上图所示,如果一个指针指向CString结构,因为内存结构相同,也就等同于指向了一个CObject结构。
也存在另外一种基类和派生类的转换方式,就是COM的QueryInterface的方式,所有的类型转换都通过调用QueryInterface来获得,这样的基类、派生类甚至接口,甚至都可以用不同的对象来实现,不过代价很大,暂时不用。
针对一个CString类,我们现在需要CString和CStringType配合使用才能完成一个整个类的功能,基类CObject同样也需要CObject和CObjectType配合。因此基类和派生类同构的原则同样要适用于CStringType和CObjectType之间。这样,从图中我们可以看到,一旦CString把自己的toString实现的函数指针填入CStringType的toString位置,就可以实现所谓的"重载(override)"。
这里要注意的是,自此,我们之前谈的this指针就隐含划分为两个部分,一个是CString,一个是CStringType。
但是我们如果要调用基类的函数呢?C++的语法是CObject::Format(xxx),java语法是super.Format(xxx),C#的语法和java类似。这里的关键是要不要在CStringType中设置一个指向CObjectType的指针。从易用性看,java/C#的方法更简单,C++的方法则有信息重复的嫌疑,万一修改基类名称了,C++需要修改不止一个地方,而java/C#则只需修改一个地方。但是我们的Crest实现的时候,如果用C++方式实现,则就可以不需要再CStringType中放置一个指针,否则就需要。这个在单片机编程时候可能更在意一些。决定放置一个指针,结构就变成了下图的样子:
接口实现
一个基本的单继承结构已经设计完毕,接下来要准备实现接口(interface)。和基类相比,接口就简单得多,因为接口不涉及到数据,所以所有的工作就集中在我们如何来调整CStringType上。
在具体设计接口之前,我们要谈一下理论。如果把一个类CString的所有成员函数看作是一个集合a的话,CString实现的任意接口b中包含的全部函数集合c都是a的子集。所以,接口的实现其实不过就是把宿主对象的成员函数复制出来,按照某个规定的顺序重新排列一下而已。接口和基类之间的另外一个区别就是接口不需要宿主对象与之同构,而且不同的类型实现的相同接口之间的唯一相同的地方就是函数的排列顺序。
具体实现起来,我们就用一个额外的表来存放它,我们称之为InterfaceChain。
这样的结构已经可以支持IFormat fmt = (IFormat)new CString()语句了,但是CString str=(CString)fmt呢?出错了!所以我们要给每一个InterfaceChain元素添加一个owner,让他们指向CStringType,这样我们可以用CString str = (CString)fmt.owner来讲interface实例指针转换回对象指针。
以上设计的是我们的核心结构,而且随着未来特性的增加,会越来越复杂。这样的结构当然不能展示给用户使用,相反隐藏得越深越好。这一切还是要拜托神奇的macro来完成了。具体如何来封装和引用这些结构,下一次再讲。
![老翅寒暑](https://images.cnblogs.com/cnblogs_com/BigTall/46728/o_qrcode-bigtall.png)
公众号:老翅寒暑