C++ 数据对齐 问题

上一篇文章提到了数据对齐问题,那我们就来看看C++数据对齐的规则,举例分析一下,最后看一个由数据对齐引起的BUG,并思考其带给我们的意义。

首先为什么要数据对齐?现代的64位计算机,总线位宽为64个比特(bit),即8个字节(byte),CPU的寄存器也是8个字节大小,所以访问内存也总是以8个字节为单位。如果一份不足8字节大小的数据因为布局问题跨越了两个8字节单位,这样要读这份数据的时候,就要通过两次总线传输,传递16字节的数据,CPU也要分级缓存这16字节的数据,这实在是太糟糕了。同理,对于32位的系统,小于4字节的数据就不应该夸两个4字节空间,还有其他的平台可能数据访问一次只有2字节甚至1字节。综合这些,数据对齐就是要求数据应该排放在其大小的自然边界上,简单说就是大小为N(1,2,4,8,16,32)的数据不应该放在两个N格子之间,而应该放在一个单独的N格子之内。

上面解释了为什么要数据对齐,要对齐的情况好像还挺多的,也挺复杂的。我们把编译器关于数据对齐的策略总结为三条规则。

规则一:成员变量根据自己的大小来对齐。

规则一的意思就是,关于结构体内该成员是否要对齐,对齐到什么位置,不是看上一个变量,也不是看下一个变量,而是看自己。定义一个int,就要对齐到sizeof(int)整数倍的位置,定义一个double,就要对齐到sizeof(double)整数倍的位置。

规则二:结构体大小必须是最大成员变量大小的整数倍。

规则二定义的是结构体数组最后补齐的情况,如果一个结构体中定义了一个int与一个double,那么最大成员变量是double,大小为8。这时结构体虽然只有4(int) + 8(double)= 12字节大小,但会补齐为8的整数倍 16。这样做是为了处理结构体数组时,每一个结构体内的每一个成员变量都处于边界上。

结合规则一与规则二,我们就可以处理简单结构体的数据对齐了,先来看个例子:

Continue reading

利用C++特性 从内部成员地址索引外部对象

标题很纠结,来看看实际需求。我们经常说的class A 包含 class B有两种形式,一种是继承,一种是组合。对于这两种情况,假设我们现在得到class B 的地址,怎么通过这个地址反向找到class A 的地址呢?

首先来看看组合的情况:

wp1

对于GetExternalData()函数而言,它只拿到了内部成员MyClassB的地址,但想通过它访问外部的MyClassA的数据要怎么做到呢?首先我们要对于MyClassA的内存布局有一个概念:

Continue reading

漫谈设计模式 —— 职责的封装

前两篇文章介绍了创建封装的模式结构封装的模式,这次我们接着来看剩下的模式。分别有:策略模式、迭代器模式、命令模式、模板方法模式、观察者模式、责任链模式、状态模式、解析器模式、备忘录模式、访问者模式、中介者模式。这些模式的共同点是职责的封装。他们都是把一部分职责从原有的对象中抽离出来,封装成一个单独的对象,达到不同的目的。

最简单的就是策略(Strategy)模式

Strategy

策略模式就是把某一部分职责,例如角色的攻击抽离出来,单独封装成一个类,然后派生不同的实例,实现不同的攻击方式。这样原有的角色类就不再需要为攻击的多样化而操心了,利用接口的形式解耦后角色可以动态配上不同的攻击行为。

另一个常见的迭代器(Iterator)模式也是同样的思路,把遍历管理器内元素的职责从管理器中抽离出来,单独封装成一个迭代器类,这样可以使得管理器接口更加简单,并且只专注于管理元素。不同的是,根据管理器使用的容器结构,迭代器会有对应的一个实现,所以管理器与迭代器是绑定的,不会像上面策略模式那样动态替换。

以上两个模式都致力于对象与行为解耦,再推进一步,操作可变,并且操作的对象也可变,这样的需求该怎么组织呢?例如游戏正常状态下,按下键盘w键是控制角色往前移动,而在剧情模式下,按下键盘w键是控制摄像机围绕剧情所在场景转动。对于这种需求,命令(Command)模式可以派上用场。

Continue reading

漫谈设计模式 —— 结构的封装

上一篇文章谈了一些对创建行为封装的模式,还有一些模式他们是在结构层面来封装的,分别有外观模式、适配器模式、代理模式、装饰者模式、组合模式、桥梁模式与蝇量模式。什么是在结构层面封装呢?我们可以先简单的理解为在一个原有的类上面再包了一个类。那为什么要这样做呢?这样做可以达到很多灵活的变化,我们首先来看最直观也是最简单的,就是改变接口。

用一个类包裹一个原有的类,从而改变它的接口,这就是适配器(Adapter)模式。举个简单的例子,假设我们游戏中特效管理器已经有一个在场景内指定坐标播放特效的接口SFXMgr::PlaySceneSFX(int nX, int nY, int nZ)。现在我们有一个新需求是通过D3DVECTOR传递坐标,那我们第一个想法就是重载PlaySceneSFX,提供SFXMgr::PlaySceneSFX(const D3DVECTOR& vPosition)接口。如果再来一个需求,是在场景内一个物件脚下播放特效,这样我们又得重载一个SFXMgr::PlaySceneSFX(const SceneObject* pObject)接口。这样做不但把特效管理器改乱了,而且特效管理器还要看见D3DVECTOR和SceneObject。为了解决这样的问题我们可以写一个SFXMgrAdapter包裹着SFXMgr,然后由SFXMgrAdapter提供各种各样的接口,最后完成转换后调用到SFXMgr::PlaySceneSFX接口完成真正的功能。

适配器模式就是通过包裹一个原有的类,提供客户期待的接口,这样不需要改动到原来的类,也不用客户完成调用的转换细节。适配器有两种实现方式:类适配器和对象适配器。其实就是我们所说的“包裹”是以何种方法实现的。类适配器通过继承来包裹原有的类,提供新接口。对象适配器则通过组合,拥有原有类的一个实例。两种适配器都是为了改变接口,并且最终功能都是在原有类中完成,属于改变接口但不改变行为。

Continue reading

漫谈设计模式 —— 创建行为的封装

在经典的24个设计模式中,有一部分是关于对创建行为的封装的,他们分别是原型模式、单例模式、生成器模式、简单工厂模式、工厂方法模式、抽象工厂模式。他们都是致力于把实例化对象的职责解耦出来,通过不同的形式封装达到不同的效果。我们从最简单的例子开始入手看看这几种模式的相似与不同。

通常,我们实例化一个对象最简单的就是通过new操作符直接创建,但如果该对象比较复杂,在创建的同时要进行许多初始化工作,例如游戏中创建一个NPC对象,在new一个NPC后,要把他添加到相应的场景中,还有要对他设置许多属性,还要根据他身上的装备来绑定各种部件。通常我们会写一个NPCFactory类,提供一个NPCFactory::CreateNPC()接口把这一连串创建NPC相关的流程封装起来。在设计模式中把这样的做法叫做简单工厂(Simple Factory)模式简单工厂严格来说不算是一个完整的设计模式,但很多人却误以为这就是所谓的工厂模式。

还有一个与上面说的很相似的是生成器(Builder)模式,不同点在于生成器模式把创建过程分开几个步骤封装到类中。就像这样:

NPCFactory.Init(szResourcePath);
NPCFactory.AddToScene(nSceneID);
NPCFactory.SetProperty();
NPCFactory.Equit();
NPC* pNpcA = NPCFactory.GetObject();

生成器模式的最大特点是分步骤,他把创建的过程封装成好几个步骤并交由使用者控制,使用者可以更灵活的控制创建,但相对应他的问题也是使用者必须熟悉创建流程与规则,要知道先Init、再设置、最后调GetObject才真正得到对象。

在介绍完简单工厂与生成器模式后,我们接着来看看另外两个很相似的工厂模式。工厂方法(Factory Method)模式抽象工厂(Abstract Factory)模式

Continue reading