疱基类/派生类,巧用读取偏移解决一个不同派生类读取同个值的问题

本文探讨了C++中通过基类接口访问不同派生类成员的技巧,利用内存布局和偏移实现特定功能,适用于特定场景下的高效编程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

[size=medium]
有奇怪需求的代码,都是历史的问题遗留。在这个时候,就需要借助语言的一些特性。就比如这次遇到的:在一个基类接口函数里,调用一个取得GetMaxLife的函数。因为扩充的需求,人物里加入了一些Life的数值,而怪物类没有。不幸的是它们都继承自这个基类。那么问题就转变成,如何在这个基类接口函数里,识别到不同的派生类做不同的事。想到了这么几个方法
1:GetMaxLife是个虚函数。如果是的话一切都解决了。噩梦的是,可惜非也。连修改的权限都没有,因为一个更底的基类(GetMaxLife是属于它的)的初始化是根据sizeof的判定。多次尝试,还是放弃
2:手工的判断是人物类和怪物类,也就是加个if的语句,而且源代码中也提供了这种判定。采用为备选方案
3:无论接口如何,在计算机看来都是一个内存的读取规则。是否能依据这个规则,给怪物类加上一个同样的变量并初始为0,这样的话即使加上也不影响原来的游戏逻辑。

虽然2更简单,但趁着有时间,研究了一下3。demo代码如下(后文涉及到简单的汇编分析,所以一并带上汇编代码)
平台:vs08 + win2003 x86
[/size]

class role_base
{
//variable
public:
int base_1;
int base_2;
int base_3;
private:

//function
public:
role_base()
{
base_1=10;
base_2=20;
base_3=30;
}

private:

};
class monster:public role_base
{
public:
int monster_1;
int monster_2;
int monster_3;

private:

public:
monster()
{
monster_1=100;
monster_2=200;
monster_3=300;
}

private:

};
class mrole:public role_base
{
public:
int mrole_1;
int mrole_2;
int mrole_3;
int mrole_add;
private:

public:
mrole()
{
mrole_1=1000;
mrole_2=2000;
mrole_3=3000;
mrole_add=4000;
}
private:
};

void test_fun_base(role_base *pBase)
{
/*
这个就是被mrole,monster调用的公共函数接口。mrole中的mrole_add是额外加的,希望这样访问。并且能够控制monster的读取值。
cout<<( (mrole*)pBase)->mrole_add<<endl;
*/
cout<<pBase->base_1<<endl;
00401919 mov ecx,dword ptr [pBase]
0040191C mov edx,dword ptr [ecx]
0040191E push edx
cout<<pBase->base_2<<endl;
00401939 mov ecx,dword ptr [pBase]
0040193C mov edx,dword ptr [ecx+4]
0040193F push edx
cout<<pBase->base_3<<endl;
0040195A mov ecx,dword ptr [pBase]
0040195D mov edx,dword ptr [ecx+8]
00401960 push edx
mrole_add
}

void test_fun_monster(monster *pMonster)
{
cout<<pMonster->monster_1<<endl;
004018A9 mov ecx,dword ptr [pMonster]
004018AC mov edx,dword ptr [ecx+0Ch]
004018AF push edx
cout<<pMonster->monster_2<<endl;
004018CA mov ecx,dword ptr [pMonster]
004018CD mov edx,dword ptr [ecx+10h]
004018D0 push edx
cout<<pMonster->monster_3<<endl;
004018EB mov ecx,dword ptr [pMonster]
004018EE mov edx,dword ptr [ecx+14h]
004018F1 push edx
}
int _tmain(int argc, _TCHAR* argv[])
{
role_base Role;
monster Monster;
mrole MRole;

/*
现实中的执行情况,2个派生类同时执行
*/
test_fun_base(&Monster);
test_fun_base(&MRole);
/*
2个测试访问规则的方法
*/
test_fun_base(&Role);
test_fun_monster(&Monster);

return 0;
}


[size=medium]先来看void test_fun_base(role_base *pBase)中队role_base三个值的读取方法[/size]

base_1
0040191C mov edx,dword ptr [ecx]
base_2
0040191C mov edx,dword ptr [ecx+4]
base_3
0040191C mov edx,dword ptr [ecx+8]

[size=medium]
其中不管mrole,role_base,monster类,产生的汇编代码都是一样的。那么能推出的端倪是,C++中的动态,其实都只是将指针指向一个共同的基类内存块,然后加以访问偏移规则。而编译器是在控制你的书写规则。就比如对于一个role_base,你无法直接访问派生类的值,你必须进行一个强行的转换,然后编译器才认为你做的是对的。[/size]
再来看派生类的访问方法
	
monster_1
004018A9 mov ecx,dword ptr [pMonster]
004018AC mov edx,dword ptr [ecx+0Ch]
monster_2
004018CA mov ecx,dword ptr [pMonster]
004018CD mov edx,dword ptr [ecx+10h]
monster_3
004018EB mov ecx,dword ptr [pMonster]
004018EE mov edx,dword ptr [ecx+14h]

[size=medium]
这里注意的是访问的偏移规则,在我用的平台里,假如一个role_base类,按照变量申明的顺序,最先申明的值反而是在栈的低地址(栈是反向增长)。当申明一个派生类的时候,可以把基类看成一个先申明的变量,当访问派生类变量的时候,先跨过了0ch(role_base大小)

在熟悉了这么访问规则后,是否可以构造出这样的访问规则,控制怪物类读取变量偏移值,当然这个偏移值和人物类访问的一样。而在这里,我们能控制的偏移只是role_base大小,如果新加入的值是紧挨着的role_base,(需要考虑到额外的内存对齐规则,如果末尾是char)那就太完美了。再加入到刚才分析的,先申请的变量在栈的地地址上,完整代码如下:
[/size]


class role_base
{
//variable
public:
int base_1;
int base_2;
int base_3;
private:

//function
public:
role_base()
{
base_1=10;
base_2=20;
base_3=30;
}

private:

};

class monster:public role_base
{
public:
int monster_add; //这个值必须最前
int monster_1;
int monster_2;
int monster_3;

private:

public:
monster()
{
monster_1=100;
monster_2=200;
monster_3=300;
monster_add=400;
}

private:

};

class mrole:public role_base
{
public:
int mrole_add; //这个值必须最前
int mrole_1;
int mrole_2;
int mrole_3;
private:

public:
mrole()
{
mrole_1=1000;
mrole_2=2000;
mrole_3=3000;
mrole_add = 4000;
}
private:
};

void test_fun_base(role_base *pBase)
{
cout<<pBase->base_1<<endl;
cout<<pBase->base_2<<endl;
cout<<pBase->base_3<<endl;
cout<< *(int*)( (int)pBase+sizeof(role_base) )<<endl;
}

void test_fun_monster(monster *pMonster)
{
cout<<pMonster->monster_1<<endl;
cout<<pMonster->monster_2<<endl;
cout<<pMonster->monster_3<<endl;
}


int _tmain(int argc, _TCHAR* argv[])
{
role_base Role;
monster Monster;
mrole MRole;

/*
现实中的执行情况,2个派生类同时执行
*/
test_fun_base(&Monster);
test_fun_base(&MRole);

return 0;
}
/*
输出:
10
20
30
400
10
20
30
4000
请按任意键继续. . .
*/


[size=medium] 不好的地方就是,非常依赖栈的申明顺序。不过也无所谓,反正现在就一个平台^_^
最后,再说明一个非常优雅的办法。在另一个类中申明一个同名变量就可以了,前提是集成关系必须一样O(∩_∩)O!以上纯属爱好研究。。
[/size]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值