`

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

阅读更多

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

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


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;
}


先来看void test_fun_base(role_base *pBase)中队role_base三个值的读取方法
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] 


        其中不管mrole,role_base,monster类,产生的汇编代码都是一样的。那么能推出的端倪是,C++中的动态,其实都只是将指针指向一个共同的基类内存块,然后加以访问偏移规则。而编译器是在控制你的书写规则。就比如对于一个role_base,你无法直接访问派生类的值,你必须进行一个强行的转换,然后编译器才认为你做的是对的。

再来看派生类的访问方法
	
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] 


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

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



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
请按任意键继续. . .
*/


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

     
1
1
分享到:
评论
4 楼 lin_style 2009-12-28  
稻-草 写道
这种代码越多,越难维护

是啊,没办法,先过了再说吧。有时间在重构
3 楼 lin_style 2009-12-28  
chester60 写道
这个问题你可以看下面这个连接里的PDF

http://download.csdn.net/source/806435


呵呵,谢谢。硬盘里下着呢,一直没看
2 楼 稻-草 2009-12-28  
这种代码越多,越难维护
1 楼 chester60 2009-12-28  
这个问题你可以看下面这个连接里的PDF

http://download.csdn.net/source/806435

相关推荐

    非编程题3

    在多重继承的场景下,基类指针转换为派生类指针可能需要地址偏移,这是因为派生类可能包含多个基类实例,每个基类在内存中的位置可能不同。然而,如果基类是虚基类(virtual base class),如题目中提到的A类型是B...

    vc6.0读取和显示DXF文件

    我们可以创建一个CView的派生类,覆盖OnDraw函数,该函数会在窗口上绘制图形。在OnDraw中,根据读取的DXF数据,调用相应的绘图函数,如CDC::MoveTo和CDC::LineTo来绘制线条,CDC::Ellipse来绘制圆等。 此外,为了...

    第5次-2017310233-方雅晴-金融实验1

    在多重继承的情况下,基类指针转换为派生类指针需要注意地址偏移的问题,这是因为派生类在内存中不仅包含了基类的成员,还有自己的成员。而虚拟基类(Virtual Base Class)的引入解决了多重继承中的一些问题,特别是...

    MFC-file-read.rar_file read MFC_file.read mfc_mfc file_mfc文件

    在Microsoft Foundation Class (MFC)库中,文件操作是一个核心功能,它允许程序员与磁盘上的文件进行交互,包括读取、写入和追加数据。MFC为C++程序员提供了一种面向对象的方式来处理文件操作,使得这些任务更加简便...

    Delphi环境下Access数据库中OLE对象的读写操作(论文)

    Delphi中的VCL(Visual Component Library)提供了处理流操作的基类TStream及其派生类。 3. TStream类及其属性:TStream是所有流类的基类,它定义了两个重要属性,分别是Size和Position。Size表示流对象的大小,即...

    C++自我梳理.pdf

    C++允许创建派生类来继承一个基类,继承允许派生类获得基类的属性和方法。多态允许使用基类指针或引用指向派生类对象,并通过该指针或引用调用派生类的方法。多态的实现依赖于虚函数机制。 C++中的数组是一种数据...

    c#_文本IO流

    在使用`Stream`及其派生类时,需要注意的是,每个流对象都有一个当前位置,同步操作时所有操作都基于这个位置,而在异步操作中,可能涉及多个位置。流的超时机制是为了防止长时间无操作导致的资源浪费或阻塞。 创建...

    delphi、C++ builder流操作

    在Delphi中,TFileStream是TStream的一个重要派生类,专门用于文件操作。创建TFileStream对象时,需要指定文件名和打开模式,如只读、只写、读写以及文件共享模式。这使得我们能方便地读取和写入文件。 通过实例,...

    C++常用类和API函数

    #### CWinApp类:派生的程序对象的基类 - **简介**:`CWinApp` 类用于表示应用程序对象。 #### CWnd类:提供所有窗口类的基本函数 - **简介**:`CWnd` 类提供了所有窗口类的基础功能。 ### 总结 以上介绍了 C++ 中...

    华为java面试题

    - 类的继承关系中,构造方法的执行顺序遵循从基类到派生类的原则。 - 构造方法的调用必须出现在第一行,通常是通过`super()`调用基类的构造方法。 12. **内部类的实现方式** - 内部类分为成员内部类、局部内部类...

    Bjarne Stroustrup的FAQ:C++的风格与技巧

    如果类层次结构中的基类的析构函数不是虚的,那么在通过指向基类的指针删除派生类对象时,只会调用基类的析构函数,从而导致资源泄露。 ##### (7) 为什么不能有虚拟构造函数? 构造函数不能是虚函数,因为构造函数...

    c# 加密和解密相关代码

    //定义两个值类型变量 if (int.TryParse(txt_Num.Text, out P_int_Num) //判断输入是否是数值 && int.TryParse(txt_Key.Text, out P_int_Key)) { txt_Encrypt.Text = (P_int_Num ^ P_int_Key).ToString(); //加密...

    嵌入式软件面试题整理.pdf

    - `dynamic_cast`:用于派生类向基类的转换,并进行类型检查。 #### Linux多线程中可重入函数和不可重入函数 - **可重入函数**:可以被多个线程同时安全调用。 - **不可重入函数**:包含静态变量或全局变量,可能...

Global site tag (gtag.js) - Google Analytics