昨天用cocos2d-x的时候遇到了一个疑问,当然运行是没问题的,由于以前没有过多的使用多继承,所以也没往深处想,可是函数调用的时候却怎么也想不通。
当时的情况是这样的,写了一个自己的精灵,继承与CCSprite和CCTouchDelegate,然后该精灵会注册触摸,简单说就是一个CCTouchDelegate指针指向了我new的自己的精灵对象,然后供cocos2d-x调用CCTouchDelegate里面重写的事件。
一切都那么的顺理成章,的确理论上是这样的,实际上也是这样的,可是仔细一想,由于我自己的精灵是一个多继承对象,CCTouchDelegate的指针实际上是指向了我自己精灵的CCTouchDelegate部分,即CCTouchDelegate指针的值不等于我自己精灵指针的值,这也是复合理论的,可在cocos2d-x里处理事件时,它会自动调用我这个CCTouchDelegate的触摸虚函数,这时候问题就来了,按MS编译器的调用方式,即会压入CCTouchDelegate指针入ecx,然后寻找虚函数表内重写的触摸事件的函数,该函数已被我重写,即调用了我自己重写的函数,然后我们关注一下这个重写的函数,该函数会调用CCSprite的函数与数据,而前面压入ecx的值是CCTouchDelegate的指针,这不是不对了么!怎么用CCTouchDelegate的指针来访问CCSprite呢?
上面是问题的引发。
为了解决这个问题,做了些小测试。
class BaseOne
{
public:
BaseOne()
{
m_nCount = 1;
}
virtual ~BaseOne(){}
void Add(int _nNumber)
{
m_nCount += _nNumber;
}
protected:
int m_nCount;
};
class BaseTwo
{
public:
BaseTwo()
{
m_nSecCount = 2;
}
virtual ~BaseTwo(){}
virtual void DoSecond()
{
void* ptr = this;
++m_nSecCount;
}
protected:
int m_nSecCount;
};
class Deriver : public BaseOne, public BaseTwo
{
public:
Deriver()
{
m_nThird = 0;
}
virtual void DoSecond()
{
void* ptr = this;
--m_nSecCount;
--m_nThird;
--m_nCount;
}
protected:
int m_nThird;
};
int _tmain(int argc, _TCHAR* argv[])
{
Deriver* pDer = new Deriver;
BaseOne* pOne = pDer;
BaseTwo* pTwo = pDer;
printf("Der %08x\nOne %08x\nTwo %08x",
pDer, pOne, pTwo);
pTwo->DoSecond();
return 0;
}
在main函数内,派生类的地址,基类1的地址,基类2的地址都是符合多继承的设计的,即我继承了两个基类,则有2个虚函数表。当我调用DoSecond函数时候,我看了下ecx,该值居然是基类2的地址,而不是派生类的地址! 可是我在派生类重写的虚函数内,还访问了基类1的数据!这是怎么回事呢?
看下虚函数的汇编码:
void* ptr = this;
00412213 mov eax,dword ptr [ebp-8]
00412216 sub eax,8
00412219 mov dword ptr [ebp-14h],eax
--m_nSecCount;
0041221C mov eax,dword ptr [ebp-8]
0041221F mov ecx,dword ptr [eax+4]
00412222 sub ecx,1
00412225 mov edx,dword ptr [ebp-8]
00412228 mov dword ptr [edx+4],ecx
--m_nThird;
0041222B mov eax,dword ptr [ebp-8]
0041222E mov ecx,dword ptr [eax+8]
00412231 sub ecx,1
00412234 mov edx,dword ptr [ebp-8]
00412237 mov dword ptr [edx+8],ecx
--m_nCount;
0041223A mov eax,dword ptr [ebp-8]
0041223D mov ecx,dword ptr [eax-4]
00412240 sub ecx,1
00412243 mov edx,dword ptr [ebp-8]
00412246 mov dword ptr [edx-4],ecx
原来如此!按着this指针来逆向偏移访问数据了!
从派生类指针来调用基类2的虚函数,和基类2指针调用虚函数,它们要实现的效果应该是一样的,即传入函数的this指针地址得是派生类的地址。则有两种方案,第一种 派生类虚函数接受的this指针直接为派生类指针,第二种 派生类接收基类2的基类指针。由于需要由基类2直接调用派生类改写的虚函数,故方法1不能够使用,则只能使用方法2,故无论基类2调用虚函数,还是派生类调用虚函数,该虚函数的ecx都为基类2的指针,改写的虚函数由于已知是多继承模式,可以由基类2指针逆向获得派生类的指针。
故总结虚函数多态的MS编译器的实现方式:
非多继承,压入对象指针,查虚函数表调用函数
多继承,判断调用哪一个基类的虚函数,假如是基类1,则压入对象指针,如基类2,则进行偏移,压入对象指针,调用虚函数。