深入理解C++面向对象机制(零)单继承
零.声明
1.《深入理解C++面向对象机制》系列的博文是博主阅读《深度探索C++对象模型》之后的自我总结性质的文章。当然也希望这些文章能够帮助那些想深入了解C++的网友。
2.文章中会有一些被称为“编译器生成的代码”,这些代码并不是编译器真正的生成代码,只是为了方便讨论而写的模拟代码。
3.如果觉得文章对你有帮助而需要转载,也请阁下能够注明出处。
4.如果觉得博文对问题的讨论有误,也可以给博主留言。
一.引入
我们先从C++的多态机制开始。那什么是多态?而C++如何实现多态的呢?以及我们能够如何使用多态这个特性以及多态的意义是什么?前两个问题将在本文中依序解答,第三个问题会在设计模式系列中讨论。
首先什么是多态?接口的多种不同的实现方式即为多态。
多态性是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。
这是一个非常有用的性质,之后会讨论这其中的意义。接下来我们先来了解其实现方式。
直白的说,就是让程序在执行期去决定要干什么。而C++是通过虚函数(virtual function)来实现这一特性的。
二.虚函数
1.虚函数的实现机制
如果类中有虚函数,或者父类有虚函数。那么编译器会在类中安放一下指针(vptr)。Vptr指向了一张虚拟表(virtual table)。这个virtual table中有许多槽(slot),这些槽放置了类的动态类别和虚函数的地址。
接下用一个例子来说明虚函数的运行机制。
class CBase
{
public:
CBase();
virtual ~CBase();
protected:
int m_x;
public:
virtual void Fun1();
virtual void Fun2();
void Fun_();
};
class CDerived : public CBase
{
public:
CDerived();
~CDerived();
protected:
int m_y;
public:
virtual void Fun1();
virtual void Fun3();
void Fun_2();
};
下图便是这个例子的virtual table图。
图1.1
图1.1中class CBase在构造函数中,编译器会安插代码,对vptr赋值,让其指向CBase的virtualtable。其中第一个槽放了一个type_info,用来指明CBase对象的动态类型。CDerived也会做同样的事,只是它的vptr指向的是CDerived的virtual table。
我们来看一下CDerived的virtual table和CBase的区别。
第二个槽放置的是CDerived的析构函数,替换了CBase::~CBase,子类重新定义了析构函数;
第三个槽CDerived::Fun1替换了CBase::Fun1,子类重新定义了Fun1;
第四个槽还是CBase::Fun2,子类没有对Fun2的重新定义;
第五个槽CDerived::Fun3,父类中没有,而子类中定义的虚函数。
这样这个virtual table存放了子类实际需要调用虚函数的实际地址。接下来看一下实际使用时候的代码。
CBase * p = new CDerived();
p->Fun1();
p->Fun2();
p->Fun3();
delete p;
我们看一下编译器生成的代码:
CBase * p = __new(sizeof(CDerived));
p = CDerived::CDerived(p);
(*p->vptr[2])(p);
(*p->vptr[3])(p);
(*p->vptr[4])(p);
(*p->vptr[1])(p);
__delete(p);
看了上面代码,会发现编译器将我们代码改的“面目全非”了。在开始解释这些代码之前,我们先来了解一下类成员函数的一些基础知识。
2.类成员函数
看还是使用上面的例子来说明。不过这里我只做简单的介绍。类成员函数分为三种:1.虚函数;2.静态函数;3.普通函数。
这里还有一个概念,namemangling。这是为了防止函数重名的办法。比如CBase中的Fun_。编译器可能会将它变成这样:CBase_Fun__。当然有时候新名字中还会混杂着参数。比如Fun_(int i,string str);这个时候可能就会变成这样:CBase_Fun__int_string_。还有一个要点,成员函数如何去操作成员变量呢。
比如Fun_的实现是这样的
void CBase::Fun_()
{
m_x = 13;
}
那么我么如何访问m_x对它赋值。为了解决这个问题,编译器将this指针作为函数的一个参数传入。所以编译器生成的代码。
void CBase::Fun_(CBase * this)
{
this->m_x = 3;
}
这一点对于虚函数也是一样,当然静态函数不一样,但是这里不是我们讨论的范围。
3.解释
我们再看一下上面提到的那段代码。
CBase * p = __new(sizeof(CDerived));
p = CDerived::CDerived(p);
(*p->vptr[2])(p);
(*p->vptr[3])(p);
(*p->vptr[4])(p);
(*p->vptr[1])(p);
__delete(p);
跳开new和delete部分,之后会有博文介绍这两个,我们把注意移到中间部分。
(*p->vptr[2])(p);这个是Fun1,而vptr便是p的virtual table指针。通过它p找到了属于CDerived(p指向对象的实际类型)的virtual table。
然后Fun1的在virtual table的序号是2,所以我们通过p->vptr[2]找到了CDerived::Fun1的函数指针,然后按照“类成员函数”中讨论的那样,把对象的指针(这里也就是p)作为参数传入Fun1中。
所以变成了(*p->vptr[2])(p);其他Fun2,Fun3,析构函数都是一样。
三.结尾
现在我们已经了解了虚函数的运行机制,但是本文的讨论都是在单继承下的,在接下来的博文中会对多继承和虚拟继承做详细的说明。