不会飞的鸟

2010年12月10日 ... 不鸟他们!!! 我要用自己开发的分布式文件系统、分布式调度系统、分布式检索系统, 做自己的搜索引擎!!!大鱼有大志!!! ---杨书童

#

游戏引擎基础(七)(网络和连线游戏环境)

7部份: 网络和连线游戏环境


网络游戏
  我记得一些年前坐在GDC(游戏开发者大会)听负责开发X-Wing Vs TIE Fighter的家伙们题为淹没在Internet” 的演讲,全是关于让网络游戏实时地在Internet上工作的东西。他们选择那个题目是多么的正确啊。当它开始处理数据包的丢失,乱序,潜伏(一个数据包发送到它的目的地所花的时间)等等时,它确实淹没了。然而它是可能的。对于Internet需要一些聪明和经验,但它是肯定可能的。看看今天大量的连线游戏,从Quake IIIUnreal TournamentCounter Strike一直到EverQuestUltima Online

  如今大多数真正有长久生命力的游戏都至少有一些连线成分。最纯粹的单人游戏容易玩一次,也许两次,或者甚至三次如果它是非常好的游戏,但一旦游戏结束,就被束之高阁了。如果你想要有任何长久生命力,那么多人连线游戏就是形势的核心所在,并且那意味着和Internet打交道,为编码者打开了那个潘多拉的盒子。

  那么跟Internet打交道包括些什么呢?首先是要理解Internet是怎么工作的,和点对点与客户机/服务器体系结构的快速讨论。点对点就是你在两台机器上运行游戏,并简单地在它们之间共享输入。每个单独的游戏假定它是正确的,并仅仅在它一幀接一幀的刷新中合并来自另外一台机器的输入。客户机/服务器是一台机器有效地运行游戏,别的机器仅仅是一个终端,接受来自玩家的输入,并渲染服务器让它渲染的任何东西。

  客户机/服务器的优点是每台机器都将会展现相同的游戏,因为所有的处理都在一个地方完成,没有跨越多台机器,你可以不用考虑每台机器相互之间的同步问题。不足之处是,服务器本身需要有一些重要的CPU可用时间来处理每一个连接的客户机,和一个合适的网络连接来确保每一个客户机及时地接收到它的更新。


了解IP
  我们都已经听说过TCP/IP(传输控制协议/网间协议)和UDP(用户数据包协议), Web网络上有大量关于这些协议的深奥的技术资讯。实际上,在Cisco网站上有一些极好的TCP/IP指导。我们将在较高层面上介绍一些TCP/IP的基本知识,目的是让你更好地了解使用这些标准协议的网络游戏设计者面临的挑战。

  TCP/IPUDP/IP是两层的通信协议系统。IP层负责网际数据包的传输。UDP或者TCP层将大的数据包传给IPIP将数据包分割为小的子数据包,为每个数据包加上一个信封,计算出目的地的IP地址,应该如何到达那里,然后将数据包发送到你的ISP,或者不管怎样你连接到网络。 这实在象是在一张明信片上写下你要发送的,贴上邮票,写上地址,塞进一个邮箱,它就送走了。

  UDPTCP是从你编码者或者游戏接收数据包的高层协议,并决定该如何处理这些数据包。UDPTCP的区别在于TCP保证数据包的传送和有序,而UDP不保证。UDP是一条直接和IP对话的小路,而TCP是在你和IP之间的一个接口。它像是在你和你的邮件之间有一个管理员助手。使用UDP你会自己为你的信打字,把它们放进一个信封等等。使用TCP你会仅仅向你的管理员口授信稿,管理员会做全部的工作并追踪确认信件送到了。

  然而,所有这些令人惊奇的为你完成的工作伴随着代价。为了确定数据包通过Internet完好无损地送到了目的方,TCP期待从目的方为它发送的每个数据包发回一个应答包(网络用语是ACK)。如果它在一定时间内没有收到ACK,它就停止发送任何新的数据包,重新发送丢失的数据包,并且将继续这样做直到收到目的方的回应。当你访问一个网页时,我们都已经看到了这种情形,在半途中下载停止了一会然后又重新开始了。可能是一个数据包在什么地方丢失了(假定不时ISP的问题),在任何更多的数据包被发送以前TCP要求重新发送它。

  这一切的问题是,在认识到出了差错的发送者和实际上正在送达的数据包之间出现了延迟。有时这能花上数秒钟,如果你仅仅只是下载一个文件或一个网页,这不是什么大碍,但如果这是一个游戏数据包而且每秒至少有十次,那么你真的是遇到麻烦了,尤其是因为它停止了其他一切事情。实际上就是这个问题所以几乎没有游戏选择使用TCP作为它们主要的Internet协议,除非它不是一个实时动作游戏。大多数游戏使用 UDP--他们不能保证有序或可靠送达,但它确实很快或者结果是至少通常比TCP/IP更快。现在我们了解这些了,接下来呢?


客户端预测
  因为 UDP 明显的是快速响应游戏的方式,我们将必须自己处理数据包的丢失和乱序。边而且这是技巧所在。不用说出太多的代码秘密,我就能说有方法。作为开始,有客户端预言,一个被谈论得相当多的词语。当你作为一个客户端连接到一个大的服务器,但是不能连贯地看见来自服务器的更新,客户端预言开始起作用了。正在你的电脑上运行的游戏部分看着你正给它的输入,并在缺乏来自服务器的任何弃绝信息的情况下,对它认为将继续进行的事情作出最好的猜测。它将会显示被猜测的数据,然后当它得到来自服务器的世界的最新状态时,改正它自己,如果需要。你可能会对这个方法的效力感到惊讶。大体而言,大部分时间数据包不容易丢失大多数时候是一秒的几十分之一,这种情况下游戏没有太多的时间偏离服务器实际上认为正在发生的事情。偏离确实会随着时间变的比较大,大多数游戏里面有一个超时功能,当出现很长时间没有来自服务器的联络时就停止游戏。

  你正在创造的游戏类型在这里有关系 -- 第一人称射击游戏不需要这样有效的客户端预言,因为它多数情况下仅仅处理我在哪儿,我是否要射击?。在第三人称游戏中,你必须更加精确,因此你能够正确地预测你的角色正在播放的动画,并且动作流畅。在这种情形中流畅的动画是完全必要的。Heretic II在这方面有很大的问题,并且是当它开始网络编码时Raven一直不得不处理的最困难的事情之一。

  当然如果你有一个很不错的网络连接,比如宽带连接,那么这个问题就远没有那么重要。对比较大的数据包有一个更宽的管道,对你的网络连通时间更快速。事实上,宽带对于游戏的主要优点不比较胖的管道多,但大大减少了延迟,特别是你到ISP的第一跳上。对于56K 调制解调器,第一跳典型的延迟是100ms,这已经严重地增加了你到网络上任意一台游戏服务器的潜在连通时间。对于宽带连接比如像DSL,第一跳的延迟时间多半是20ms。使用Windows中一个叫做TraceRouteTRACERT.EXE)的命令行程序并指定一个目标IP地址或者域名,你能够找出你的第一跳的连通时间。仔细观察第一跳,因为这几乎总是你到你的ISP的网络连通时间。并且观察你在你的ISP的网络内部用了多少跳直到你看见在一个给定跳上列出的一个不同的域名。

  请注意,宽带并不总是能解决延迟问题。你仍然受最慢的路由器/服务器和数据包从服务器穿越网络到达你的跳数(反之亦然)的支配。有一个宽带连接确实容易缓和这些,但不可能它们最后就消失了。当然,如果你打算要运行某种服务器,你将会需要一个具有足够快速的向上游的数据速率的带宽,因为仅仅一个调制解调器不能够处理一个服务器产生的负荷。

  值得一提的是,如果你想要在PS2或者Xbox上面玩网络游戏,你将需要一个宽带连接,因为它们两者都不支持调制解调器。


包大小,智能数据传输,和反作弊
  别的必须被处理的事情是数据包的大小。如果你在一个游戏里面64个人都在跑来跑去相互攻击,从一台机器发送到另外一台机器的数据包能变得相当大,达到了一些调制解调器没有带宽处理这些数据的程度。这正在变得特别和那些有着很大的地表系统的游戏有关。这里增加的问题是,因为你有这个很好的地表系统,你能够看得很远,因此能够看见许多其他游戏玩家,使得你为了精确渲染所需要的来自服务器的数据数量以很快的速率增长。我们能做什么呢?

  好吧,首先必要的是只发送绝对必须的东西给任何给定的客户端,因此他仅仅得到从他的角度观察游戏所需要的东西。发送在他视野以外的人们的数据没有一点意义他将看不见这些。同时,你最好确保只发送那些每幀之间实际上发生改变的数据。如果一个家伙仍然在播放相同的动画,重新发送数据没有意义。当然,如果数据包丢失时这确实带来一些问题,但这就是为什么好的网络程序员被支付很多金钱,来处理类似这样的东西。

  还有一些其他的事情也要处理。最近已经有大量的令人苦恼的连线作弊正在发生。这是某些人修改游戏以给他们不正当利益的地方。尽管严格意义上这不是网络的一部分,但它确实发生了。有时人们会创作一些模块,允许他们立即瞄准进入视野的任何人,或者简单地允许他们看穿墙壁,或者让其他游戏玩家看不见他们自己。大部份时间这些事情可以在网络层内部或者在服务器上被处理。任何有100%命中率的人被简单地踢出游戏,因为在人力所及的范围内那是不可能的。

  游戏开发者必须尽一切可能制止作弊行为,但很不幸,人做的东西可以被人突破。所有你能做的就是让作弊变得困难,当确实发生时去尝试发现它。

  好吧,现在就到这里了。在第8部分中,我们将会看看游戏脚本系统的趣味世界,根据游戏过程中出现的事件来渲染或使能预先定义的场景和行为,协助故事叙述。



梦在天涯 2007-12-04 13:24 发表评论

posted @ 2009-04-10 10:44 不会飞的鸟 阅读(103) | 评论 (0)编辑 收藏

游戏引擎基础(八)(脚本系统)

     摘要: 第8部份: 脚本系统脚本系统  我们从第七部分的游戏网络问题来到了脚本系统,因为其呈现的故事叙述机会,最近已经形成一种很大的游戏元素。在一个需要以受控制的方式解释的情景,预先编制的电影脚本是解决问题的方法。在电影中,这通常用来处理或者由主角向一个伙伴解释情形,或者敌人对英雄解释。当然,有其它的方法来做这件事情 -- 叙事者,倒叙,等等 – 但通常是使用实时情景的人们和事件来完成。当然,...  阅读全文

梦在天涯 2007-12-04 13:27 发表评论

posted @ 2009-04-10 10:44 不会飞的鸟 阅读(131) | 评论 (0)编辑 收藏

游戏引擎基础(九)(现成产品与定做的游戏引擎设计工具,游戏特定主题)

     摘要: 第9部分: 现成产品与定做的游戏引擎设计工具,游戏特定主题现成产品与定做的设计工具  我们从第8部份的脚本引擎来到这一章节中的许多主题,我们认为那些铁杆游戏玩家和有志成为游戏开发者的那些人将会发现它们相当有趣。我们将开始讨论现成产品与定制的设计工具。   你的工具的选择是你引擎设计的一个非常重要的部份,因为这是你将用来给你的游戏产生内容的东西,是最耗时的部份。在这个过程中有助于节省时间和资源的任何...  阅读全文

梦在天涯 2007-12-04 13:28 发表评论

posted @ 2009-04-10 10:44 不会飞的鸟 阅读(84) | 评论 (0)编辑 收藏

游戏引擎基础(十)(人工智能和导航)

10部分: 人工智能和导航(路径发现)


人工智能(AI
  我们上面已经用了其他九个章节介绍了游戏引擎,现在让我们深入到非常有趣和重要的人工智能主题。人工智能如今正在变成被谈论得最多的仅次于游戏引擎渲染能力的游戏开发领域之一,确实如此。直到大约两年半以前,游戏似乎主要是在考虑你能够渲染多少个多边形,眼睛是多么的漂亮,和劳拉的胸部是多么的有弹性...既然我们现在已经能够渲染出非常真实的乳房,中心就开始转移到我们实际上用那些多边形做什么了(即玩游戏)。因为它给你提供实际玩游戏的刺激作用和参与游戏世界中正在进行的事情,所以人工智能在这个领域非常关键。

  人工智能包括了全部的东西,从在Tetris中决定哪一块新砖头掉落(这很大程度上知识一个随即数产生器), 一直到创造基于小组的策略游戏,这些游戏和你交互,并且实际上在你玩的时候向你学习。人工智能包含了许多规则,如果你(作为一个游戏开发者)没有花费足够多的时间让它正确地工作,它会反过来在你屁股上咬一口。所以让我们谈论一些哪些规则?这样你能更好地理解人工智能系统会确实是多么的复杂。为了避免法律上的纠纷,我们将使用一个假设的游戏而不是一个真实的游戏作为例子。

  假设我们的游戏中有坏份子生活在3D世界中,干着他们的事情,而且如果你打搅了他们的正常次序他们就会反抗你(玩家)。你必须决定的第一件事情就是他们正在从事的到底是什么事情呢?他们正在守卫什么东西吗?在巡查?在计划一个聚会?在购买食品杂货?在整理床铺?建立行为的基线是游戏开发者的工作之一。一旦有了这个,你就总有NPC(非玩家角色)或计算机控制的能够恢复去做的事情,玩家与他们的交互就应当能被完成。

  一旦我们知道一个NPC角色需要做什么比如它在守卫一扇门,并且在这个区域小巡逻,NPC也必须有世界意识。游戏设计者需要决定NPC的人工智能将如何看见世界,和它的知识范围。你将会仅仅说计算机知道正在进行的每件事情吗?这通常被认为是一件糟糕的事情,因为非常明显计算机能够看见和听见你不能看见和听见的事情,这被当成是在作弊。不是一种有趣的经历。或者你将模拟他的视野,这样他只能够对他能看见的事物作出反应吗?当有墙壁出现时这里就有问题了,因为你开始进入那些我在第九部分提到的追踪例程,看看NPC是否试图对被墙壁挡住的人作出反应。这是一个很明显的人工智能问题,但是当涉及到门和窗户时,这个甚至变得更加复杂了。

  当你开始为AI刺激例程增加听觉意识时,这依然变得更加复杂了。但是,这个意识是那些关键的小事情之一,这些使得假想的游戏世界似乎更加真实,或者能够去除怀疑的悬念。如果你碰到过这样的事情,请举手:你在枪战中跟一个NPC交战,免除了一个NPC,你绕着角落行走并遇到了另外一个NPC依然保持他的缺省行为模式,没有意识到刚刚发生的事情。现在,枪是嘈杂的事物,枪战可能已经明显地提醒了一个倾听NPC有些事情正在进行。避免这种事情的技巧在于找到一个有效的方式来决定声源(即你武器的发射)的距离是否足够接近到NPC能够听见。

  接下来就是决策例程。当我们的巡逻NPC角色能够听到但不能看见某物时,你试图实现什么样的行为呢?他去寻找它吗?不理睬它?你如何决定什么是重要的声音他应该去或者不去调查?如同你看见的一样,这会很快变得非常的复杂。有很多方法来建造处理这些事情的代码,但通常这样是一个好主意,建立一个不是对特定的NPC而是对所有的NPC都起作用的系统,该系统基于你能够在游戏引擎以外的文本文件中建立的属性。这样就不需要程序员为一个给定的角色而改变AI,并且如果你对游戏代码做了改动,它将立即自动地应用到所有的角色,这在大多数情况下是一件好事情。

  其他的世界意识问题会冒出来,比如这样的情形,两个守卫彼此紧挨着站立,你用狙击武器干掉了一个,而另外一个站在哪儿完全不知已经发生的事情。再者,遵守真实世界行为的细节是一款好游戏和一款伟大游戏的之间的区别。

  让我们说你已经把所有的刺激-响应部分准备好了你已经扫描了世界,决定NPC应当对正在进行的一些事情作出反应他听到了玩家角色发出了声响并且你(游戏开发者)决定了他应当对这个做些什么他将去调查。现在更加复杂的事情来了。他如何离开现在的位置,到达他认为发出声音的地方,而不会想通常的数字傻瓜一样跑到墙壁里面,碰到家具呢?继续往下看


有关正确的路径 --- 世界导航
  快速,准确的世界导航( 也叫做路径-发现) 近来已经成为游戏开发者的圣杯。 让它看起来非常信服是一件非常困难的事情。你需要有局部世界的地理知识墙壁的位置,台阶,悬崖和建筑物等的边缘。你也需要世界中的对象的知识比如家具,汽车,尤其是其他人的位置。真正最后的因素是问题所在,一会儿我们将回到这一点上。

  世界导航通常被分为两个领域,世界导航和局部导航。二者实际上只是范围上的区别,但大多数的程序员分别对待它们,因为这样处理起来容易一些。世界导航例程处理理解房间,门和一般的地理学,并计算出让玩家或者角色从世界中的A点到达B点的一条路径。它将让你从A点到达B,这是一句很容易说的话,不是吗?说起来容易,但做起来很困难。理解世界是一个非常复杂问题,我已经看到过许多尝试过的解决办法。QuakeIII的机器人遵照建造的预先处理过的地图,一般的说法,使用原来地图的地面。预处理器检测地面元素,由地图建造者作上标记,并自己建造一个只使用地面的世界简化地图。机器人并不关心墙壁,因为他们从不接近它们,就像他们遵照地面的地图一样,设计上已经把避免墙壁构造在里面了。

  其他方法在地图本身里面建造一些小的结点,AI可以追随它们。这些结点通常被建造在彼此的视线里面,有从一个结点到其他所有结点的连接,角色AI能够直接看见,所以你就确保了从一个结点移动到另外一个结点时AI不会试图穿越墙壁。如果有门或者降落物,你能够事先用这些结点对路径的信息编码,于是NPC能够采用适当的行为等候电梯,打开一扇门,或者从一点跳到另外一点。这实际上是HereticII使用的系统,也是Raven在他们其他的大多数游戏中使用的系统。

  关于这个主题,3D RealmsJess Crable,现在为Duke Nukem Forever工作,如是说:

  "导航在许多方面是个巨大的挑战,主要是当游戏中有大量正在发生的事情和一些非计划性的东西,比如障碍。为了避免和(或)真实地对非计划性的障碍物导航(例如像另外的AI),AI需要很好地知道正在它周围发生的事情。比较而言另外一个巨大的挑战就是真实感。如果AI正在表现玩家在实际生活中看到的一些东西,比如说一个人,或者一条狗, 那么让它看上去真实可信就更加困难。"

  然后就是局部导航。我们可能有一条路径让我们的 NPC 从他在世界中的位置,移动到他认为听到声音的地方,但你不能盲目地按照这个执行并期望得到看起来不错的结果。这种性质的路径倾向于非常特定于一个给定的目的。当你沿着走廊从一个房间跑到另外一个房间时,它很好,但如果你试图指导他穿越一个巨大的房间时,路径结点方法容易最终得到一些看起来很奇怪的发现路径。这些路径也不是动态的。因为他们被预先建造,他们不容易考虑到世界的任何动态变化。桌子可能有被移动过了,椅子被破坏了,墙壁被摧残,当然,人们会移动。这就是局部导航不同于世界导航的地方。它必须考虑局部世界并导航NPC在里面穿越。它必须知道周围的环境,存在哪些可以选择的路径,并决定选择哪一条。

  在局部导航中最大的问题是其他的NPC。给定一个发现路径的具体例程,如果你在相同的一般区域中有不止一个NPC,他们都试图到达世界的同一地点,结果是他们都非常容易有相同的路径。然后他们试图沿着这个路径行进,结果彼此遇到一起,然后花费他们所有的时间试图将彼此分开,并且一旦成功地分开了,他们再次试图到达目标,然后我们又再次看到同样的事情发生。这一切看起来都是非常的愚蠢,这不是大多数人想要的效果。所以需要一些路径发现中的变化来避免这种情形,需要一些妥善处理避免的代码。有大量能够帮助解决这种情形的算法。


人工智能和角色动画问题
  当然,当角色自己在世界中行走时你必须完全地决定你想要角色播放什么动画。听起来无足轻重?不是的。关于这个主题,Raven Chris Reed—Soldier of FortuneII使用名为LICHAI系统的现在的负责人如是说:

  "此刻我能告诉你,我们在平滑移动上正有着最大的困难。在一个多丘陵的长满草的丛林中试图让五个角色在彼此附近行走是一个非常困难的问题。让底层系统完美是重要的,因为除非角色在较低层次上(避免墙壁,适当的动画)看起来真实,他们不能够有效地表达任何较高层次决定的智能。由于这个单独的原因,动画和底层的移动是最重要的和最难实现的。它确实需要完美。"

  因此我们已经让我们的角色从A点到达了B点,他自己穿越世界,在途中避免障碍物,正确播放动画,现在到达了这里。他看见了你。接下来做什么呢?很明显更多的是作出决策。他将向你射击。太棒了。你回应射击。现在干什么?当他试着逃走的时候,现在你再次经历全部同样的事情。

  为了让这些情形看起来令人信服,你看见了这里必须要处理的大量问题。如果你建立你的AI使用没有动画的行为让NPC执行,这能被混合。一些Soldier of Fortune中的AI就是这样的例子。他们受到了指责,因为坏家伙没有以适当的方式对刺激作出反应。当他们明显应该这样做的时候,敌方NPC不扫射,或者不逃跑。部分问题是他们没有扫射敌人NPC的动画,或者让他们往回跑,因为空间的问题。因此世界上所有最伟大的AI代码都不能够解决这个问题。这是所有要考虑的重要事情。

  想知道隐藏的难点吗?看看我前面所有的描述,然后试着将它应用到一组NPC上,这些NPC彼此必须说话,设定目标,彼此沟通,但不妨碍彼此的方式。一旦你这么做了,试试那些代码,作为玩家的队友做上面所描述的这些,然而不要在枪战中妨碍他。现在这是复杂的。然后这成为乐趣。这是最困难的部分。Raven Chris Reed关于AI‘感觉的一些评论:

  "我认为反馈是AI的一个极大的问题。如果角色对于他周围环境的变化不产生反应,游戏的真实感就被完全打破了。这有许多明显的例子(听见枪炮声,看见同伴被击中...),以及一些更加微妙的事情(当两个人通过门厅时看着彼此并点头致意)。玩家是乐意接受一些生硬和可预测性的,但是这些事物容易把游戏带到现实生活。"

  并且Jess Crable 赞同:

  "平衡是非常重要的对玩家将会有多大的乐趣至关重要,但还有其他的问题要平衡。游戏玩家时常说他们想在游戏中看见更加真实的人工智能。然而,太多的真实感开始把乐趣带走。在这两者之间必须要有一个好的平衡。变化和随机同样也很重要行为的变化,和保持在可信范围内的一定程度的不可预测性。"


游戏规则与自然发生的游戏
  在我们关于AI的所有描述中,我们采用的是FPS的方式。有不止一种的AI。我们已经描述的是处理3D世界一组规则。AI远远不止这些。时常最好的AI实际上非常的简单。它就是一组规则,玩家必须响应和处理的响应(或开始)动作的规则。

  这里应当处理一个被称为自然发生的游戏的专业术语。 自然发生的游戏本质上创造游戏将遵守的规则,那将会造成游戏程序员不能预见的情形。

  举例来说,象棋能被认为是自然发生的游戏。有一组规则,但游戏能够陷入各种程序员不能够以个别方式处理的情形。你不能为每一种可能的棋局情形编码规则。很清楚,游戏玩家每次不会总是面临相同的游戏情景。一定程度上,进行中的游戏情形会根据他的行动而发生变化。Black and White是这种情形的一个完美的例子,和The Sims一样游戏有它自己的规则,但你如何运用和调和他们是你自己的事情。实际上,你在玩游戏的过程中创造着游戏,而不是照着游戏设计者/程序员已经为你定义的路线进行。

  有可能把基于规则的,自然发生的游戏方式和FPS环境混合在一起。Half Life中的一些海军陆战队士兵的行为就是这样做的压制火力和侧翼攻击从设定的规则中动态完成。它看起来是动态的,而且一定程度上它是这样。然而,在FPS世界中仅仅有一组规则时常是不够的。几何和其他AI时常能够打败简单的规则,这让保持正确并依然有趣变得更加困难。所以对那些可怜的AI程序员有一些同情心吧。他们的工作不容易。


  好吧,下面还有一个章节,仅仅还剩下一个章节了。在最后的章节里,我们将讨论头顶显示,菜单系统,游戏定制和配置,游戏引擎版权与建造,最后是游戏“mods”



梦在天涯 2007-12-04 13:29 发表评论

posted @ 2009-04-10 10:44 不会飞的鸟 阅读(191) | 评论 (0)编辑 收藏

游戏引擎基础(十一)(最后的章节)

     摘要: 第11部份: 最后的章节前端  你已经看到了菜单系统,你可能理解游戏内的头顶显示(HUDs)时常是游戏经历中被忽视和诽谤的部分。最近,这个领域开始被给人印象非常深刻的Black and White所关注,这款游戏实际上没有HUD。在Peter Molyneux经历了Dungeon Keeper以后,它在屏幕上大量的图标,他决定游戏的大部分被这些图标占用了,主要的屏幕没有被足够利用。因此他决定废除所...  阅读全文

梦在天涯 2007-12-04 13:31 发表评论

posted @ 2009-04-10 10:44 不会飞的鸟 阅读(63) | 评论 (0)编辑 收藏

[转载]堆栈,堆栈,堆和栈的区别

堆和栈的区别
一、预备知识—程序的内存分配
一个由c/C++编译的程序占用的内存分为以下几个部分
1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。
3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放 
4、文字常量区—常量字符串就是放在这里的。 程序结束后由系统释放
5、程序代码区—存放函数体的二进制代码。
二、例子程序 
这是一个前辈写的,非常详细 
//main.cpp 
int a = 0; 全局初始化区 
char *p1; 全局未初始化区 
main() 

int b; 栈 
char s[] = "abc"; 栈 
char *p2; 栈 
char *p3 = "123456"; 123456\0在常量区,p3在栈上。 
static int c =0; 全局(静态)初始化区 
p1 = (char *)malloc(10); 
p2 = (char *)malloc(20); 
分配得来得10和20字节的区域就在堆区。 
strcpy(p1, "123456"); 123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。 

 


二、堆和栈的理论知识 
2.1申请方式 
stack: 
由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间 
heap: 
需要程序员自己申请,并指明大小,在c中malloc函数 
如p1 = (char *)malloc(10); 
在C++中用new运算符 
如p2 = (char *)malloc(10); 
但是注意p1、p2本身是在栈中的。 


2.2 
申请后系统的响应 
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。 
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时, 
会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。 

2.3申请大小的限制 
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。 
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。 


2.4申请效率的比较: 
栈由系统自动分配,速度较快。但程序员是无法控制的。 
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便. 
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。 

2.5堆和栈中的存储内容 
栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。 
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。 
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。 

2.6存取效率的比较 

char s1[] = "aaaaaaaaaaaaaaa"; 
char *s2 = "bbbbbbbbbbbbbbbbb"; 
aaaaaaaaaaa是在运行时刻赋值的; 
而bbbbbbbbbbb是在编译时就确定的; 
但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。 
比如: 
#include 
void main() 

char a = 1; 
char c[] = "1234567890"; 
char *p ="1234567890"; 
a = c[1]; 
a = p[1]; 
return; 

对应的汇编代码 
10: a = c[1]; 
00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh] 
0040106A 88 4D FC mov byte ptr [ebp-4],cl 
11: a = p[1]; 
0040106D 8B 55 EC mov edx,dword ptr [ebp-14h] 
00401070 8A 42 01 mov al,byte ptr [edx+1] 
00401073 88 45 FC mov byte ptr [ebp-4],al 
第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到edx中,在根据edx读取字符,显然慢了。 


2.7小结: 
堆和栈的区别可以用如下的比喻来看出: 
使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。 
使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。 



windows进程中的内存结构


在阅读本文之前,如果你连堆栈是什么多不知道的话,请先阅读文章后面的基础知识。 

接触过编程的人都知道,高级语言都能通过变量名来访问内存中的数据。那么这些变量在内存中是如何存放的呢?程序又是如何使用这些变量的呢?下面就会对此进行深入的讨论。下文中的C语言代码如没有特别声明,默认都使用VC编译的release版。 

首先,来了解一下 C 语言的变量是如何在内存分部的。C 语言有全局变量(Global)、本地变量(Local),静态变量(Static)、寄存器变量(Regeister)。每种变量都有不同的分配方式。先来看下面这段代码: 

#include <stdio.h> 

int g1=0, g2=0, g3=0; 

int main() 

static int s1=0, s2=0, s3=0; 
int v1=0, v2=0, v3=0; 

//打印出各个变量的内存地址 

printf("0x%08x\n",&v1); //打印各本地变量的内存地址 
printf("0x%08x\n",&v2); 
printf("0x%08x\n\n",&v3); 
printf("0x%08x\n",&g1); //打印各全局变量的内存地址 
printf("0x%08x\n",&g2); 
printf("0x%08x\n\n",&g3); 
printf("0x%08x\n",&s1); //打印各静态变量的内存地址 
printf("0x%08x\n",&s2); 
printf("0x%08x\n\n",&s3); 
return 0; 

编译后的执行结果是: 

0x0012ff78 
0x0012ff7c 
0x0012ff80 

0x004068d0 
0x004068d4 
0x004068d8 

0x004068dc 
0x004068e0 
0x004068e4 

输出的结果就是变量的内存地址。其中v1,v2,v3是本地变量,g1,g2,g3是全局变量,s1,s2,s3是静态变量。你可以看到这些变量在内存是连续分布的,但是本地变量和全局变量分配的内存地址差了十万八千里,而全局变量和静态变量分配的内存是连续的。这是因为本地变量和全局/静态变量是分配在不同类型的内存区域中的结果。对于一个进程的内存空间而言,可以在逻辑上分成3个部份:代码区,静态数据区和动态数据区。动态数据区一般就是“堆栈”。“栈(stack)”和“堆(heap)”是两种不同的动态数据区,栈是一种线性结构,堆是一种链式结构。进程的每个线程都有私有的“栈”,所以每个线程虽然代码一样,但本地变量的数据都是互不干扰。一个堆栈可以通过“基地址”和“栈顶”地址来描述。全局变量和静态变量分配在静态数据区,本地变量分配在动态数据区,即堆栈中。程序通过堆栈的基地址和偏移量来访问本地变量。 


├———————┤低端内存区域 
│ …… │ 
├———————┤ 
│ 动态数据区 │ 
├———————┤ 
│ …… │ 
├———————┤ 
│ 代码区 │ 
├———————┤ 
│ 静态数据区 │ 
├———————┤ 
│ …… │ 
├———————┤高端内存区域 


堆栈是一个先进后出的数据结构,栈顶地址总是小于等于栈的基地址。我们可以先了解一下函数调用的过程,以便对堆栈在程序中的作用有更深入的了解。不同的语言有不同的函数调用规定,这些因素有参数的压入规则和堆栈的平衡。windows API的调用规则和ANSI C的函数调用规则是不一样的,前者由被调函数调整堆栈,后者由调用者调整堆栈。两者通过“__stdcall”和“__cdecl”前缀区分。先看下面这段代码: 

#include <stdio.h> 

void __stdcall func(int param1,int param2,int param3) 

int var1=param1; 
int var2=param2; 
int var3=param3; 
printf("0x%08x\n",¶m1); //打印出各个变量的内存地址 
printf("0x%08x\n",¶m2); 
printf("0x%08x\n\n",¶m3); 
printf("0x%08x\n",&var1); 
printf("0x%08x\n",&var2); 
printf("0x%08x\n\n",&var3); 
return; 

int main() 

func(1,2,3); 
return 0; 

编译后的执行结果是: 

0x0012ff78 
0x0012ff7c 
0x0012ff80 

0x0012ff68 
0x0012ff6c 
0x0012ff70 


├———————┤<—函数执行时的栈顶(ESP)、低端内存区域 
│ …… │ 
├———————┤ 
│ var 1 │ 
├———————┤ 
│ var 2 │ 
├———————┤ 
│ var 3 │ 
├———————┤ 
│ RET │ 
├———————┤<—“__cdecl”函数返回后的栈顶(ESP) 
│ parameter 1 │ 
├———————┤ 
│ parameter 2 │ 
├———————┤ 
│ parameter 3 │ 
├———————┤<—“__stdcall”函数返回后的栈顶(ESP) 
│ …… │ 
├———————┤<—栈底(基地址 EBP)、高端内存区域 


上图就是函数调用过程中堆栈的样子了。首先,三个参数以从又到左的次序压入堆栈,先压“param3”,再压“param2”,最后压入“param1”;然后压入函数的返回地址(RET),接着跳转到函数地址接着执行(这里要补充一点,介绍UNIX下的缓冲溢出原理的文章中都提到在压入RET后,继续压入当前EBP,然后用当前ESP代替EBP。然而,有一篇介绍windows下函数调用的文章中说,在windows下的函数调用也有这一步骤,但根据我的实际调试,并未发现这一步,这还可以从param3和var1之间只有4字节的间隙这点看出来);第三步,将栈顶(ESP)减去一个数,为本地变量分配内存空间,上例中是减去12字节(ESP=ESP-3*4,每个int变量占用4个字节);接着就初始化本地变量的内存空间。由于“__stdcall”调用由被调函数调整堆栈,所以在函数返回前要恢复堆栈,先回收本地变量占用的内存(ESP=ESP+3*4),然后取出返回地址,填入EIP寄存器,回收先前压入参数占用的内存(ESP=ESP+3*4),继续执行调用者的代码。参见下列汇编代码: 

;--------------func 函数的汇编代码------------------- 

:00401000 83EC0C sub esp, 0000000C //创建本地变量的内存空间 
:00401003 8B442410 mov eax, dword ptr [esp+10] 
:00401007 8B4C2414 mov ecx, dword ptr [esp+14] 
:0040100B 8B542418 mov edx, dword ptr [esp+18] 
:0040100F 89442400 mov dword ptr [esp], eax 
:00401013 8D442410 lea eax, dword ptr [esp+10] 
:00401017 894C2404 mov dword ptr [esp+04], ecx 

……………………(省略若干代码) 

:00401075 83C43C add esp, 0000003C ;恢复堆栈,回收本地变量的内存空间 
:00401078 C3 ret 000C ;函数返回,恢复参数占用的内存空间 
;如果是“__cdecl”的话,这里是“ret”,堆栈将由调用者恢复 

;-------------------函数结束------------------------- 


;--------------主程序调用func函数的代码-------------- 

:00401080 6A03 push 00000003 //压入参数param3 
:00401082 6A02 push 00000002 //压入参数param2 
:00401084 6A01 push 00000001 //压入参数param1 
:00401086 E875FFFFFF call 00401000 //调用func函数 
;如果是“__cdecl”的话,将在这里恢复堆栈,“add esp, 0000000C” 

聪明的读者看到这里,差不多就明白缓冲溢出的原理了。先来看下面的代码: 

#include <stdio.h> 
#include <string.h> 

void __stdcall func() 

char lpBuff[8]="\0"; 
strcat(lpBuff,"AAAAAAAAAAA"); 
return; 

int main() 

func(); 
return 0; 

编译后执行一下回怎么样?哈,“"0x00414141"指令引用的"0x00000000"内存。该内存不能为"read"。”,“非法操作”喽!"41"就是"A"的16进制的ASCII码了,那明显就是strcat这句出的问题了。"lpBuff"的大小只有8字节,算进结尾的\0,那strcat最多只能写入7个"A",但程序实际写入了11个"A"外加1个\0。再来看看上面那幅图,多出来的4个字节正好覆盖了RET的所在的内存空间,导致函数返回到一个错误的内存地址,执行了错误的指令。如果能精心构造这个字符串,使它分成三部分,前一部份仅仅是填充的无意义数据以达到溢出的目的,接着是一个覆盖RET的数据,紧接着是一段shellcode,那只要着个RET地址能指向这段shellcode的第一个指令,那函数返回时就能执行shellcode了。但是软件的不同版本和不同的运行环境都可能影响这段shellcode在内存中的位置,那么要构造这个RET是十分困难的。一般都在RET和shellcode之间填充大量的NOP指令,使得exploit有更强的通用性。 


├———————┤<—低端内存区域 
│ …… │ 
├———————┤<—由exploit填入数据的开始 
│ │ 
│ buffer │<—填入无用的数据 
│ │ 
├———————┤ 
│ RET │<—指向shellcode,或NOP指令的范围 
├———————┤ 
│ NOP │ 
│ …… │<—填入的NOP指令,是RET可指向的范围 
│ NOP │ 
├———————┤ 
│ │ 
│ shellcode │ 
│ │ 
├———————┤<—由exploit填入数据的结束 
│ …… │ 
├———————┤<—高端内存区域 


windows下的动态数据除了可存放在栈中,还可以存放在堆中。了解C++的朋友都知道,C++可以使用new关键字来动态分配内存。来看下面的C++代码: 

#include <stdio.h> 
#include <iostream.h> 
#include <windows.h> 

void func() 

char *buffer=new char[128]; 
char bufflocal[128]; 
static char buffstatic[128]; 
printf("0x%08x\n",buffer); //打印堆中变量的内存地址 
printf("0x%08x\n",bufflocal); //打印本地变量的内存地址 
printf("0x%08x\n",buffstatic); //打印静态变量的内存地址 

void main() 

func(); 
return; 

程序执行结果为: 

0x004107d0 
0x0012ff04 
0x004068c0 

可以发现用new关键字分配的内存即不在栈中,也不在静态数据区。VC编译器是通过windows下的“堆(heap)”来实现new关键字的内存动态分配。在讲“堆”之前,先来了解一下和“堆”有关的几个API函数: 

HeapAlloc 在堆中申请内存空间 
HeapCreate 创建一个新的堆对象 
HeapDestroy 销毁一个堆对象 
HeapFree 释放申请的内存 
HeapWalk 枚举堆对象的所有内存块 
GetProcessHeap 取得进程的默认堆对象 
GetProcessHeaps 取得进程所有的堆对象 
LocalAlloc 
GlobalAlloc 

当进程初始化时,系统会自动为进程创建一个默认堆,这个堆默认所占内存的大小为1M。堆对象由系统进行管理,它在内存中以链式结构存在。通过下面的代码可以通过堆动态申请内存空间: 

HANDLE hHeap=GetProcessHeap(); 
char *buff=HeapAlloc(hHeap,0,8); 

其中hHeap是堆对象的句柄,buff是指向申请的内存空间的地址。那这个hHeap究竟是什么呢?它的值有什么意义吗?看看下面这段代码吧: 

#pragma comment(linker,"/entry:main") //定义程序的入口 
#include <windows.h> 

_CRTIMP int (__cdecl *printf)(const char *, ...); //定义STL函数printf 
/*--------------------------------------------------------------------------- 
写到这里,我们顺便来复习一下前面所讲的知识: 
(*注)printf函数是C语言的标准函数库中函数,VC的标准函数库由msvcrt.dll模块实现。 
由函数定义可见,printf的参数个数是可变的,函数内部无法预先知道调用者压入的参数个数,函数只能通过分析第一个参数字符串的格式来获得压入参数的信息,由于这里参数的个数是动态的,所以必须由调用者来平衡堆栈,这里便使用了__cdecl调用规则。BTW,Windows系统的API函数基本上是__stdcall调用形式,只有一个API例外,那就是wsprintf,它使用__cdecl调用规则,同printf函数一样,这是由于它的参数个数是可变的缘故。 
---------------------------------------------------------------------------*/ 
void main() 

HANDLE hHeap=GetProcessHeap(); 
char *buff=HeapAlloc(hHeap,0,0x10); 
char *buff2=HeapAlloc(hHeap,0,0x10); 
HMODULE hMsvcrt=LoadLibrary("msvcrt.dll"); 
printf=(void *)GetProcAddress(hMsvcrt,"printf"); 
printf("0x%08x\n",hHeap); 
printf("0x%08x\n",buff); 
printf("0x%08x\n\n",buff2); 

执行结果为: 

0x00130000 
0x00133100 
0x00133118 

hHeap的值怎么和那个buff的值那么接近呢?其实hHeap这个句柄就是指向HEAP首部的地址。在进程的用户区存着一个叫PEB(进程环境块)的结构,这个结构中存放着一些有关进程的重要信息,其中在PEB首地址偏移0x18处存放的ProcessHeap就是进程默认堆的地址,而偏移0x90处存放了指向进程所有堆的地址列表的指针。windows有很多API都使用进程的默认堆来存放动态数据,如windows 2000下的所有ANSI版本的函数都是在默认堆中申请内存来转换ANSI字符串到Unicode字符串的。对一个堆的访问是顺序进行的,同一时刻只能有一个线程访问堆中的数据,当多个线程同时有访问要求时,只能排队等待,这样便造成程序执行效率下降。 

最后来说说内存中的数据对齐。所位数据对齐,是指数据所在的内存地址必须是该数据长度的整数倍,DWORD数据的内存起始地址能被4除尽,WORD数据的内存起始地址能被2除尽,x86 CPU能直接访问对齐的数据,当他试图访问一个未对齐的数据时,会在内部进行一系列的调整,这些调整对于程序来说是透明的,但是会降低运行速度,所以编译器在编译程序时会尽量保证数据对齐。同样一段代码,我们来看看用VC、Dev-C++和lcc三个不同编译器编译出来的程序的执行结果: 

#include <stdio.h> 

int main() 

int a; 
char b; 
int c; 
printf("0x%08x\n",&a); 
printf("0x%08x\n",&b); 
printf("0x%08x\n",&c); 
return 0; 

这是用VC编译后的执行结果: 
0x0012ff7c 
0x0012ff7b 
0x0012ff80 
变量在内存中的顺序:b(1字节)-a(4字节)-c(4字节)。 

这是用Dev-C++编译后的执行结果: 
0x0022ff7c 
0x0022ff7b 
0x0022ff74 
变量在内存中的顺序:c(4字节)-中间相隔3字节-b(占1字节)-a(4字节)。 

这是用lcc编译后的执行结果: 
0x0012ff6c 
0x0012ff6b 
0x0012ff64 
变量在内存中的顺序:同上。 

三个编译器都做到了数据对齐,但是后两个编译器显然没VC“聪明”,让一个char占了4字节,浪费内存哦。 


基础知识: 
堆栈是一种简单的数据结构,是一种只允许在其一端进行插入或删除的线性表。允许插入或删除操作的一端称为栈顶,另一端称为栈底,对堆栈的插入和删除操作被称为入栈和出栈。有一组CPU指令可以实现对进程的内存实现堆栈访问。其中,POP指令实现出栈操作,PUSH指令实现入栈操作。CPU的ESP寄存器存放当前线程的栈顶指针,EBP寄存器中保存当前线程的栈底指针。CPU的EIP寄存器存放下一个CPU指令存放的内存地址,当CPU执行完当前的指令后,从EIP寄存器中读取下一条指令的内存地址,然后继续执行。 


参考:《Windows下的HEAP溢出及其利用》by: isno 
《windows核心编程》by: Jeffrey Richter 



摘要: 讨论常见的堆性能问题以及如何防范它们。(共 9 页)

前言
您是否是动态分配的 C/C++ 对象忠实且幸运的用户?您是否在模块间的往返通信中频繁地使用了“自动化”?您的程序是否因堆分配而运行起来很慢?不仅仅您遇到这样的问题。几乎所有项目迟早都会遇到堆问题。大家都想说,“我的代码真正好,只是堆太慢”。那只是部分正确。更深入理解堆及其用法、以及会发生什么问题,是很有用的。

什么是堆?
(如果您已经知道什么是堆,可以跳到“什么是常见的堆性能问题?”部分)

在程序中,使用堆来动态分配和释放对象。在下列情况下,调用堆操作: 

事先不知道程序所需对象的数量和大小。


对象太大而不适合堆栈分配程序。
堆使用了在运行时分配给代码和堆栈的内存之外的部分内存。下图给出了堆分配程序的不同层。


GlobalAlloc/GlobalFree:Microsoft Win32 堆调用,这些调用直接与每个进程的默认堆进行对话。

LocalAlloc/LocalFree:Win32 堆调用(为了与 Microsoft Windows NT 兼容),这些调用直接与每个进程的默认堆进行对话。

COM 的 IMalloc 分配程序(或 CoTaskMemAlloc / CoTaskMemFree):函数使用每个进程的默认堆。自动化程序使用“组件对象模型 (COM)”的分配程序,而申请的程序使用每个进程堆。

C/C++ 运行时 (CRT) 分配程序:提供了 malloc() 和 free() 以及 new 和 delete 操作符。如 Microsoft Visual Basic 和 Java 等语言也提供了新的操作符并使用垃圾收集来代替堆。CRT 创建自己的私有堆,驻留在 Win32 堆的顶部。

Windows NT 中,Win32 堆是 Windows NT 运行时分配程序周围的薄层。所有 API 转发它们的请求给 NTDLL。

Windows NT 运行时分配程序提供 Windows NT 内的核心堆分配程序。它由具有 128 个大小从 8 到 1,024 字节的空闲列表的前端分配程序组成。后端分配程序使用虚拟内存来保留和提交页。

在图表的底部是“虚拟内存分配程序”,操作系统使用它来保留和提交页。所有分配程序使用虚拟内存进行数据的存取。

分配和释放块不就那么简单吗?为何花费这么长时间?

堆实现的注意事项
传统上,操作系统和运行时库是与堆的实现共存的。在一个进程的开始,操作系统创建一个默认堆,叫做“进程堆”。如果没有其他堆可使用,则块的分配使用“进程堆”。语言运行时也能在进程内创建单独的堆。(例如,C 运行时创建它自己的堆。)除这些专用的堆外,应用程序或许多已载入的动态链接库 (DLL) 之一可以创建和使用单独的堆。Win32 提供一整套 API 来创建和使用私有堆。有关堆函数(英文)的详尽指导,请参见 MSDN。

当应用程序或 DLL 创建私有堆时,这些堆存在于进程空间,并且在进程内是可访问的。从给定堆分配的数据将在同一个堆上释放。(不能从一个堆分配而在另一个堆释放。)

在所有虚拟内存系统中,堆驻留在操作系统的“虚拟内存管理器”的顶部。语言运行时堆也驻留在虚拟内存顶部。某些情况下,这些堆是操作系统堆中的层,而语言运行时堆则通过大块的分配来执行自己的内存管理。不使用操作系统堆,而使用虚拟内存函数更利于堆的分配和块的使用。

典型的堆实现由前、后端分配程序组成。前端分配程序维持固定大小块的空闲列表。对于一次分配调用,堆尝试从前端列表找到一个自由块。如果失败,堆被迫从后端(保留和提交虚拟内存)分配一个大块来满足请求。通用的实现有每块分配的开销,这将耗费执行周期,也减少了可使用的存储空间。

Knowledge Base 文章 Q10758,“用 calloc() 和 malloc() 管理内存” (搜索文章编号), 包含了有关这些主题的更多背景知识。另外,有关堆实现和设计的详细讨论也可在下列著作中找到:“Dynamic Storage Allocation: A Survey and Critical Review”,作者 Paul R. Wilson、Mark S. Johnstone、Michael Neely 和 David Boles;“International Workshop on Memory Management”, 作者 Kinross, Scotland, UK, 1995 年 9 月(http://www.cs.utexas.edu/users/oops/papers.html)(英文)。

Windows NT 的实现(Windows NT 版本 4.0 和更新版本) 使用了 127 个大小从 8 到 1,024 字节的 8 字节对齐块空闲列表和一个“大块”列表。“大块”列表(空闲列表[0]) 保存大于 1,024 字节的块。空闲列表容纳了用双向链表链接在一起的对象。默认情况下,“进程堆”执行收集操作。(收集是将相邻空闲块合并成一个大块的操作。)收集耗费了额外的周期,但减少了堆块的内部碎片。

单一全局锁保护堆,防止多线程式的使用。(请参见“Server Performance and Scalability Killers”中的第一个注意事项, George Reilly 所著,在 “MSDN Online Web Workshop”上(站点:http://msdn.microsoft.com/workshop/server/iis/tencom.asp(英文)。)单一全局锁本质上是用来保护堆数据结构,防止跨多线程的随机存取。若堆操作太频繁,单一全局锁会对性能有不利的影响。

什么是常见的堆性能问题?
以下是您使用堆时会遇到的最常见问题: 

分配操作造成的速度减慢。光分配就耗费很长时间。最可能导致运行速度减慢原因是空闲列表没有块,所以运行时分配程序代码会耗费周期寻找较大的空闲块,或从后端分配程序分配新块。


释放操作造成的速度减慢。释放操作耗费较多周期,主要是启用了收集操作。收集期间,每个释放操作“查找”它的相邻块,取出它们并构造成较大块,然后再把此较大块插入空闲列表。在查找期间,内存可能会随机碰到,从而导致高速缓存不能命中,性能降低。


堆竞争造成的速度减慢。当两个或多个线程同时访问数据,而且一个线程继续进行之前必须等待另一个线程完成时就发生竞争。竞争总是导致麻烦;这也是目前多处理器系统遇到的最大问题。当大量使用内存块的应用程序或 DLL 以多线程方式运行(或运行于多处理器系统上)时将导致速度减慢。单一锁定的使用—常用的解决方案—意味着使用堆的所有操作是序列化的。当等待锁定时序列化会引起线程切换上下文。可以想象交叉路口闪烁的红灯处走走停停导致的速度减慢。 
竞争通常会导致线程和进程的上下文切换。上下文切换的开销是很大的,但开销更大的是数据从处理器高速缓存中丢失,以及后来线程复活时的数据重建。

堆破坏造成的速度减慢。造成堆破坏的原因是应用程序对堆块的不正确使用。通常情形包括释放已释放的堆块或使用已释放的堆块,以及块的越界重写等明显问题。(破坏不在本文讨论范围之内。有关内存重写和泄漏等其他细节,请参见 Microsoft Visual C++(R) 调试文档 。)


频繁的分配和重分配造成的速度减慢。这是使用脚本语言时非常普遍的现象。如字符串被反复分配,随重分配增长和释放。不要这样做,如果可能,尽量分配大字符串和使用缓冲区。另一种方法就是尽量少用连接操作。
竞争是在分配和释放操作中导致速度减慢的问题。理想情况下,希望使用没有竞争和快速分配/释放的堆。可惜,现在还没有这样的通用堆,也许将来会有。

在所有的服务器系统中(如 IIS、MSProxy、DatabaseStacks、网络服务器、 Exchange 和其他), 堆锁定实在是个大瓶颈。处理器数越多,竞争就越会恶化。

尽量减少堆的使用
现在您明白使用堆时存在的问题了,难道您不想拥有能解决这些问题的超级魔棒吗?我可希望有。但没有魔法能使堆运行加快—因此不要期望在产品出货之前的最后一星期能够大为改观。如果提前规划堆策略,情况将会大大好转。调整使用堆的方法,减少对堆的操作是提高性能的良方。

如何减少使用堆操作?通过利用数据结构内的位置可减少堆操作的次数。请考虑下列实例:

struct ObjectA {
   // objectA 的数据 
}

struct ObjectB {
   // objectB 的数据 
}

// 同时使用 objectA 和 objectB

//
// 使用指针 
//
struct ObjectB {
   struct ObjectA * pObjA;
   // objectB 的数据 
}

//
// 使用嵌入
//
struct ObjectB {
   struct ObjectA pObjA;
   // objectB 的数据 
}

//
// 集合 – 在另一对象内使用 objectA 和 objectB
//

struct ObjectX {
   struct ObjectA  objA;
   struct ObjectB  objB;
}

避免使用指针关联两个数据结构。如果使用指针关联两个数据结构,前面实例中的对象 A 和 B 将被分别分配和释放。这会增加额外开销—我们要避免这种做法。


把带指针的子对象嵌入父对象。当对象中有指针时,则意味着对象中有动态元素(百分之八十)和没有引用的新位置。嵌入增加了位置从而减少了进一步分配/释放的需求。这将提高应用程序的性能。


合并小对象形成大对象(聚合)。聚合减少分配和释放的块的数量。如果有几个开发者,各自开发设计的不同部分,则最终会有许多小对象需要合并。集成的挑战就是要找到正确的聚合边界。


内联缓冲区能够满足百分之八十的需要(aka 80-20 规则)。个别情况下,需要内存缓冲区来保存字符串/二进制数据,但事先不知道总字节数。估计并内联一个大小能满足百分之八十需要的缓冲区。对剩余的百分之二十,可以分配一个新的缓冲区和指向这个缓冲区的指针。这样,就减少分配和释放调用并增加数据的位置空间,从根本上提高代码的性能。


在块中分配对象(块化)。块化是以组的方式一次分配多个对象的方法。如果对列表的项连续跟踪,例如对一个 {名称,值} 对的列表,有两种选择:选择一是为每一个“名称-值”对分配一个节点;选择二是分配一个能容纳(如五个)“名称-值”对的结构。例如,一般情况下,如果存储四对,就可减少节点的数量,如果需要额外的空间数量,则使用附加的链表指针。 
块化是友好的处理器高速缓存,特别是对于 L1-高速缓存,因为它提供了增加的位置 —不用说对于块分配,很多数据块会在同一个虚拟页中。

正确使用 _amblksiz。C 运行时 (CRT) 有它的自定义前端分配程序,该分配程序从后端(Win32 堆)分配大小为 _amblksiz 的块。将 _amblksiz 设置为较高的值能潜在地减少对后端的调用次数。这只对广泛使用 CRT 的程序适用。
使用上述技术将获得的好处会因对象类型、大小及工作量而有所不同。但总能在性能和可升缩性方面有所收获。另一方面,代码会有点特殊,但如果经过深思熟虑,代码还是很容易管理的。

其他提高性能的技术
下面是一些提高速度的技术: 

使用 Windows NT5 堆 
由于几个同事的努力和辛勤工作,1998 年初 Microsoft Windows(R) 2000 中有了几个重大改进:

改进了堆代码内的锁定。堆代码对每堆一个锁。全局锁保护堆数据结构,防止多线程式的使用。但不幸的是,在高通信量的情况下,堆仍受困于全局锁,导致高竞争和低性能。Windows 2000 中,锁内代码的临界区将竞争的可能性减到最小,从而提高了可伸缩性。


使用 “Lookaside”列表。堆数据结构对块的所有空闲项使用了大小在 8 到 1,024 字节(以 8-字节递增)的快速高速缓存。快速高速缓存最初保护在全局锁内。现在,使用 lookaside 列表来访问这些快速高速缓存空闲列表。这些列表不要求锁定,而是使用 64 位的互锁操作,因此提高了性能。


内部数据结构算法也得到改进。
这些改进避免了对分配高速缓存的需求,但不排除其他的优化。使用 Windows NT5 堆评估您的代码;它对小于 1,024 字节 (1 KB) 的块(来自前端分配程序的块)是最佳的。GlobalAlloc() 和 LocalAlloc() 建立在同一堆上,是存取每个进程堆的通用机制。如果希望获得高的局部性能,则使用 Heap(R) API 来存取每个进程堆,或为分配操作创建自己的堆。如果需要对大块操作,也可以直接使用 VirtualAlloc() / VirtualFree() 操作。

上述改进已在 Windows 2000 beta 2 和 Windows NT 4.0 SP4 中使用。改进后,堆锁的竞争率显著降低。这使所有 Win32 堆的直接用户受益。CRT 堆建立于 Win32 堆的顶部,但它使用自己的小块堆,因而不能从 Windows NT 改进中受益。(Visual C++ 版本 6.0 也有改进的堆分配程序。)

使用分配高速缓存 
分配高速缓存允许高速缓存分配的块,以便将来重用。这能够减少对进程堆(或全局堆)的分配/释放调用的次数,也允许最大限度的重用曾经分配的块。另外,分配高速缓存允许收集统计信息,以便较好地理解对象在较高层次上的使用。

典型地,自定义堆分配程序在进程堆的顶部实现。自定义堆分配程序与系统堆的行为很相似。主要的差别是它在进程堆的顶部为分配的对象提供高速缓存。高速缓存设计成一套固定大小(如 32 字节、64 字节、128 字节等)。这一个很好的策略,但这种自定义堆分配程序丢失与分配和释放的对象相关的“语义信息”。 

与自定义堆分配程序相反,“分配高速缓存”作为每类分配高速缓存来实现。除能够提供自定义堆分配程序的所有好处之外,它们还能够保留大量语义信息。每个分配高速缓存处理程序与一个目标二进制对象关联。它能够使用一套参数进行初始化,这些参数表示并发级别、对象大小和保持在空闲列表中的元素的数量等。分配高速缓存处理程序对象维持自己的私有空闲实体池(不超过指定的阀值)并使用私有保护锁。合在一起,分配高速缓存和私有锁减少了与主系统堆的通信量,因而提供了增加的并发、最大限度的重用和较高的可伸缩性。

需要使用清理程序来定期检查所有分配高速缓存处理程序的活动情况并回收未用的资源。如果发现没有活动,将释放分配对象的池,从而提高性能。

可以审核每个分配/释放活动。第一级信息包括对象、分配和释放调用的总数。通过查看它们的统计信息可以得出各个对象之间的语义关系。利用以上介绍的许多技术之一,这种关系可以用来减少内存分配。

分配高速缓存也起到了调试助手的作用,帮助您跟踪没有完全清除的对象数量。通过查看动态堆栈返回踪迹和除没有清除的对象之外的签名,甚至能够找到确切的失败的调用者。

MP 堆 
MP 堆是对多处理器友好的分布式分配的程序包,在 Win32 SDK(Windows NT 4.0 和更新版本)中可以得到。最初由 JVert 实现,此处堆抽象建立在 Win32 堆程序包的顶部。MP 堆创建多个 Win32 堆,并试图将分配调用分布到不同堆,以减少在所有单一锁上的竞争。

本程序包是好的步骤 —一种改进的 MP-友好的自定义堆分配程序。但是,它不提供语义信息和缺乏统计功能。通常将 MP 堆作为 SDK 库来使用。如果使用这个 SDK 创建可重用组件,您将大大受益。但是,如果在每个 DLL 中建立这个 SDK 库,将增加工作设置。

重新思考算法和数据结构 
要在多处理器机器上伸缩,则算法、实现、数据结构和硬件必须动态伸缩。请看最经常分配和释放的数据结构。试问,“我能用不同的数据结构完成此工作吗?”例如,如果在应用程序初始化时加载了只读项的列表,这个列表不必是线性链接的列表。如果是动态分配的数组就非常好。动态分配的数组将减少内存中的堆块和碎片,从而增强性能。

减少需要的小对象的数量减少堆分配程序的负载。例如,我们在服务器的关键处理路径上使用五个不同的对象,每个对象单独分配和释放。一起高速缓存这些对象,把堆调用从五个减少到一个,显著减少了堆的负载,特别当每秒钟处理 1,000 个以上的请求时。

如果大量使用“Automation”结构,请考虑从主线代码中删除“Automation BSTR”,或至少避免重复的 BSTR 操作。(BSTR 连接导致过多的重分配和分配/释放操作。)

摘要
对所有平台往往都存在堆实现,因此有巨大的开销。每个单独代码都有特定的要求,但设计能采用本文讨论的基本理论来减少堆之间的相互作用。 

评价您的代码中堆的使用。


改进您的代码,以使用较少的堆调用:分析关键路径和固定数据结构。


在实现自定义的包装程序之前使用量化堆调用成本的方法。


如果对性能不满意,请要求 OS 组改进堆。更多这类请求意味着对改进堆的更多关注。


要求 C 运行时组针对 OS 所提供的堆制作小巧的分配包装程序。随着 OS 堆的改进,C 运行时堆调用的成本将减小。


操作系统(Windows NT 家族)正在不断改进堆。请随时关注和利用这些改进。
Murali Krishnan 是 Internet Information Server (IIS) 组的首席软件设计工程师。从 1.0 版本开始他就设计 IIS,并成功发行了 1.0 版本到 4.0 版本。Murali 组织并领导 IIS 性能组三年 (1995-1998), 从一开始就影响 IIS 性能。他拥有威斯康星州 Madison 大学的 M.S.和印度 Anna 大学的 B.S.。工作之外,他喜欢阅读、打排球和家庭烹饪。



http://community.csdn.net/Expert/FAQ/FAQ_Index.asp?id=172835
我在学习对象的生存方式的时候见到一种是在堆栈(stack)之中,如下  
CObject  object;  
还有一种是在堆(heap)中  如下  
CObject*  pobject=new  CObject();  
 
请问  
(1)这两种方式有什么区别?  
(2)堆栈与堆有什么区别??  
 
 
---------------------------------------------------------------  
 
1)  about  stack,  system  will  allocate  memory  to  the  instance  of  object  automatically,  and  to  the
 heap,  you  must  allocate  memory  to  the  instance  of  object  with  new  or  malloc  manually.  
2)  when  function  ends,  system  will  automatically  free  the  memory  area  of  stack,  but  to  the 
heap,  you  must  free  the  memory  area  manually  with  free  or  delete,  else  it  will  result  in  memory
leak.  
3)栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。  
4)堆上分配的内存可以有我们自己决定,使用非常灵活。  
---------------------------------------------------------------  

posted @ 2009-04-10 09:11 不会飞的鸟 阅读(121) | 评论 (0)编辑 收藏

[交互设计工具/原型创作工具]Axure RP Pro 5.0新特性 破解版 注册码

先感谢木的{moond},要不是一次提供了那么多画原型图的工具,我现在还在Word和Visio里面打转呢。

这些软件能试用的我差不多都用了一下,其中觉得Axure的RP Pro 4挺好的,功能包括站点地图、流程设计、页面框架设计、简单交互设计等,可以生成HTML、Word等格式。

RP操作比用Dreamweaver简单多了,图片、文字、输入框等等所有组件全是可视化操作,可以很方便的实现网站交互内容的原型设计,可将以前死板的页面版式全部替换为有点击、链接效果的网页,nice~

同时可以为网站设计提供AJAX式的交互处理,RP提供一种动态层的组件,在同一页设定不同状态的效果,然后用链接、按钮等触发即可产生动态效果,这样网站设计就变得更加生动,意图也就更加直观。

可惜,这么好的软件只能试用30天,哪位兄台要是有解决方法一定要通知我,这里先谢了~

以下对RP5的新特性做了下简单翻译:

Axure RP Pro 5.0增加了众多新特性,并且着重增强了设计师间的协作、交互功能和更多的定制功能,生成也变的更快。

新功能包括:

1. 共享工程
1.1 可以在网络驱动器上创建共享工程,而不必非安装在服务器上
1.2 管理和便捷页面只需要通过简单的 check in / check out 即可
1.3 维护和浏览历史版本
1.4 更多介绍

2. 交互功能的增强
2.1 OnFocus 和 OnLostFocus 事件 (更多)
2.2 使用和禁止Widgets行为
2.3 模仿在图片上的锚点/热区行为 (更多)
2.4 移动动态层(Dynamic Panels)行为 (更多)
2.5 在其他行为运行之前暂停当前行为
2.6 页面间多达25个变量的储存

3. 规范的增强
3.1 可以更快的把演示生成为Microsoft Word 2007格式(使用Microsoft Office兼容包可与Microsoft Word 2000+兼容)
3.2 有1栏和2栏布局的选项
3.3 可以更简单的定制Word模板
3.4 有更多新选项去配置规范内容

4. 更多增强
4.1 优化Tab交互
4.2 可以选择与主窗体分离(比如Sitemap和Widgets)
4.3 有更好的保存机制来处理关闭和重开一个文件
4.4 带有梯度和透明设置的颜色工具
4.5 打印设置是被保存的

查看原文

同时提示一下:如果之前装过RP4,并且有license的话,RP5可以直接使用,
是作为免费升级的,当然,你也可以只当下来看看,有30天试用

=======================================================================

互联网行业产品经理的一项重要工作,就是进行产品原型设计(Prototype Design)。而产品原型设计最基础的工作,就是结合批注、大量的说明以及流程图画框架图wireframe,将自己的产品原型完整而准确的表述给UI、UE、程序工程师,市场人员,并通过沟通会议,反复修改prototype 直至最终确认,开始投入执行。

进行产品原型设计的软件工具也有很多种,我写的这个教程所介绍的Axure RP,是taobao、dangdang等国内大型网络公司的团队在推广使用的原型设计软件。同时,此软件也在产品经理圈子中广为流传。之所以Axure RP得到了大家的认同和推广,正是因为其简便的操作和使用,符合了产品经理、交互设计师们的需求。

在正式谈Axure RP之前,我们先来看看做产品原型设计,现在大致有哪些工具可以使用,而他们的利弊何在。
纸笔:简单易得,上手难度为零。有力于瞬间创意的产生与记录,有力于对文档即时的讨论与修改。但是保真度不高,难以表述页面流程,更难以表述交互信息与程序需求细节。
Word:上手难度普通。可以画wireframe,能够画页面流程,能够使用批注与文字说明。但是对交互表达不好,也不利于演示。
PPT:上手难度普通。易于画框架图,易于做批注,也可以表达交互流程,也擅长演示。但是不利于大篇幅的文档表达。
Visio:功能相对比较复杂。善于画流程图,框架图。不利于批注与大篇幅的文字说明。同样不利于交互的表达与演示。
Photshop/fireworks:操作难度相对较大,易于画框架图、流程图。不利于表达交互设计,不擅长文字说明与批注。
Dreamweave:操作难度大,需要基础的html知识。易于画框架图、流程图、表达交互设计。不擅长文字说明与批注。

以上这些工具,都是产品经理经常会使用到的,但是从根本上来说,这些工具都不是做prototype design的专门利器,需要根据产品开发不同的目的,不同的开发阶段,选择不同的工具搭配使用,才能达到表达、沟通的目的。

比如使用纸笔,更适合在产品创意阶段使用,可以快速记录闪电般的思路和灵感;也可以在即时讨论沟通时使用,通过图形快速表达自己的产品思路,及时的画出来,是再好不过的方法。而word则适合在用文字详细表达产品,对产品进行细节说明时使用,图片结合文字的排版,是word最擅长的工作。而ppt自然是演示时更好。visio则可以适用于各种流程图、关系图的表达,更可通过画use case 获取用户需求。PS/FW是图片处理的工具,DW则是所见即所得的网页开发软件,这些是设计师的看家本领,对于普通的产品经理来说,需要耗费太多的精力去掌握。

其实每件工具,每个软件,在创造它的初期,软件设计师们都给它赋予了性格、气质。因为每个工具的产生,都是为了满足人类的某一方面需求。比如锄头是锄土的,起子是起螺丝的,电熨斗是烫衣服的。但是不同的工具都有自己的工作领域,在其他领域它并不擅长。而以上的软件在创造的初期,并非为了帮助产品经理、ue完成产品原型设计,因此他们都不能在prototype design 这件工作上得心应手。而Axure RP正是在互联网产品大张其道的前提下,为满足prototype design创建的需求,应运而生。

Axure RP 能帮助网站需求设计者,快捷而简便的创建 基于目录组织的原型文档、功能说明、交互界面以及带注释的wireframe网页,并可自动生成用于演示的网页文件和word文档,以提供演示与开发。

没错!Axure RP 的特点是:

  • 快速创建带注释的wireframe文件,并可根据所设置的时间周期,软件自动保存文档,确保文件安全。
  • 在不写任何一条html与javascript语句的情况下,通过创建的文档以及相关条件和注释,一键生成html prototype演示。
  • 根据设计稿,一键生成一致而专业的word版本的原型设计文档。

说到这里相信很多人已经激起了兴趣,但是在开始学习之前,我认为我们还是有必要先了解一下软件短短的历史,创造这一软件的公司-Axure Software Solutions, Inc.该公司创建于2002年五月,Axure RP是这一软件公司的旗舰产品,2003年一月Axure RP第一版本上线发表,至今已经正式发行到了第四个版本,而我提笔写到这里的时候,Axure 5.0版本beta版本已经正式提供下载试用,虽然我已经下载使用,但是我想,写教程还是应该先从稳定的4.6版说起,至于5.0版,我们可以伴随着软件一起成长。

接下来我会结合图片,分几个步骤分享我对Axure的认识,
一、 界面与功能
二、 工具栏
三、 站点地图
四、 组件与使用方法
五、 复用模板与使用
六、 交互功能与注释
七、 实例

当然,在书写的过程中我会根据具体的情况再进行调整,尽量做到图文并茂,易于理解。写这个教程的目的,一方面为自己熟悉与更加理解Axure,另一方面也鞭策自己完善自己的blog网站www.2tan.net,同时也希望以自己的绵薄之力,为希望学习Axure的朋友分享一点经验。由于这是我第一次尝试写教程,难免会出现偏颇,也希望朋友们能够不吝赐教,共同进步。

另,为e文好的朋友附上自学Axure RP的几个途径:
1、 打开软件,按F1调取帮助文档,对照文档学习。
2、 登录http://www.axure.com/au-home.aspx 查看flash视频学习。
3、 登录 Axure 博客 http://axure.com/cs/blogs/Default.aspx,了解软件最新信息。
4、 登录讨论组http://axure.com/cs/forums/Default.aspx,参与讨论。

并提供Axure RP pro 4.6版本的下载
http://www.2tan.net/LoadMod.asp?plugins=downloadForPJBlog
由于放存软件的网络硬盘在下载数量不多的情况下可能会删除文件,如遇到死链接,可留言给我,我将重新上传:)
(软件仅供学习使用,反对商业用途 -_-!!!)


Name:3ddown
Serial:FiCGEezgdGoYILo8U/2MFyCWj0jZoJc/sziRRj2/ENvtEq+7w1RH97k5MWctqVHA

posted @ 2009-03-20 13:23 不会飞的鸟 阅读(724) | 评论 (1)编辑 收藏

PowerDesigner下建索引、自增列、检查设计模型

From: http://www.qqread.com/java/2006/07/u996183002.html

这段时间,使用PD做数据库模型,感觉很不错,将自已的经验总给一下.还有许多功能我没时间总结,以后有时间,继续补吧
  
. 如何在PowerDesigner下建索引
  
 1. 双击表设计图,出来Table Properties,在Tab 页中选择 Indexes
  
   数据库建模工具PowerDesigner总结(组图)



 2. 单击新建索引的属性,出现Indexex Properties
  
 数据库建模工具PowerDesigner总结(组图)



 3. 增加一个索引包含的字段
  
 数据库建模工具PowerDesigner总结(组图)



点击"下图中的增加列图标出现图如下"



选中相关字段后,点OK
最后返回属性页,点preview出现图如下



好了,索引建立成功

======================================

.如何在PowerDesigner 下建自增列
  
 1. 使用SqlServer 数据库中的下列语句来完成
  
  建表语句中,在要做为自增列的字段中,加上如下
  IDENTITY(1,1)
  
  还有可以使用下面语句,重置自增种子
  dbcc checkident(ConfigSys,reseed,0);
  
.如何在PowerDesigner 下检查设计模型
  
  1. 在菜单栏中选择 Tools -? Check Model, 如下图
  
 数据库建模工具PowerDesigner总结(组图)



  2. 选择要检查的每项设置
  
 数据库建模工具PowerDesigner总结(组图)



  3. 确定后,将出来检查结果汇总信息
  
 数据库建模工具PowerDesigner总结(组图)

posted @ 2009-03-18 09:03 不会飞的鸟 阅读(1993) | 评论 (0)编辑 收藏

c语言中rand()函数怎么用?

rand(产生随机数)
相关函数
srand

表头文件
#include<stdlib.h>

定义函数
int rand(void)

函数说明
rand()会返回一随机数值,范围在0至RAND_MAX 间。在调用此函数产生随机数前,必须先利用srand()设好随机数种子,如果未设随机数种子,rand()在调用时会自动设随机数种子为1。关于随机数种子请参考srand()。

返回值
返回0至RAND_MAX之间的随机数值,RAND_MAX定义在stdlib.h,其值为2147483647。

范例
/* 产生介于1 到10 间的随机数值,此范例未设随机数种子,完整的随机数产生请参考
srand()*/
#include<stdlib.h>
main()
{
int i,j;
for(i=0;i<10;i++)
{
j=1+(int)(10.0*rand()/(RAND_MAX+1.0));
printf("%d ",j);
}
}

执行
9 4 8 8 10 2 4 8 3 6
9 4 8 8 10 2 4 8 3 6





srand(设置随机数种子)
相关函数
rand

表头文件
#include<stdlib.h>

定义函数
void srand (unsigned int seed);

函数说明
srand()用来设置rand()产生随机数时的随机数种子。参数seed必须是个整数,通常可以利用geypid()或time(0)的返回值来当做seed。如果每次seed都设相同值,rand()所产生的随机数值每次就会一样。

返回值

范例
/* 产生介于1 到10 间的随机数值,此范例与执行结果可与rand()参照*/
#include<time.h>
#include<stdlib.h>
main()
{
int i,j;
srand((int)time(0));
for(i=0;i<10;i++)
{
j=1+(int)(10.0*rand()/(RAND_MAX+1.0));
printf(" %d ",j);
}
}

执行
5 8 8 8 10 2 10 8 9 9
2 9 7 4 10 3 2 10 8 7

posted @ 2009-03-11 11:04 不会飞的鸟 阅读(6750) | 评论 (0)编辑 收藏

如何使用 类进行文件的 I/O 处理

原文出处:How to Use <fstream> Classes for File I/O
摘要:传统的文件 I/O 库如 Unix 的 <io.h> 和 <stdio.h> ,由于其程序接口的原因,在很大程度上强制程序员进行某些处理,缺乏类型安全和国际化支持。C++ 的 <fstream> 库则在文件的 I/O 方面提供了一个增强的、面向对象的、具有国际化意识的库。本文将介绍如何使用这个库进行文件的 I/O 处理并利用它来编写易于跨平台的代码。


  大多数 C++ 程序员都熟悉不止一个文件 I/O 库。首先是传统的 Unix 风格的库,它由一些低级函数如 read() 和 open()组成。其次是 ANSI C 的 <stdio.h> 库,它包含 fopen() 和 fread()等函数。其它的还有一些具备所有权的库或框架,比如 MFC,它有很多自己的文件处理类。
  这些库一般都很难跨平台使用。更糟的是,上述提到的 C 库由于其程序接口的原因,在很大程度上强制程序员进行某些处理,而且缺乏类型安全支持。
  标准 C++ 提供提供了一个增强的、面向对象的、具有国际化意识的  <fstream> 库。这个库包含一系列派生于标准 ios_base 和 ios 类的类模板。因此, <fstream> 提供了高级的自动控制机制和健壮性。本文下面将示范如何使用  <fstream> 类实现文件的输入/输出处理:

第一步:创建文件流
  输入文件流(ifstream)支持重载的 >> 操作符,同样,输出文件流(ofstream)支持重载的 << 操作符。结合了输入和输出的文件流被称为 fstream。下面的程序创建了一个 ifstream 对象:dict,并将该对象中的每一个单字显示到屏幕上:

#include <iostream>
#include <string>
#include <fstream>
#include <cstdlib>
using namespace std;
int main()
{
string s;
cout<<"enter dictionary file: ";
cin>>s;
ifstream dict (s.c_str());
if (!dictionary) // were there any errors on opening?
exit(-1);
while (dictionary >> s) cout << s <<''\n'';
}      
  我们必须调用 string::c_str() 成员函数,因为 fstream 对象只接受常量字符串作为文件名。当你将文件名作为参数传递时,构造函数试图打开指定的文件。接着,我们用重载的 !操作符来检查文件的状态。如果出错,该操作符估值为 true。最后一行是个循环,每次反复都从文件读取一个单字,将它拷贝到 s,然后显示出来。注意我们不必显式地检查 EOF,因为重载操作符 >> 会自动处理。此外,我们不用显式地关闭此文件,因为析构函数会为我们做这件事情。
  过时和荒废的 <fstream.h> 库支持 ios::nocreate 和 ios::noreplace 标志。但新的 <fstream> 库已经取代了 <fstream.h> 并不再支持这两个标志。
 
文件的打开模式
  如果你不显式指定打开模式,fstream 类将使用默认值。例如,ifstream 默认以读方式打开某个文件并将文件指针置为文件的开始处。为了向某个文件写入数据,你需要创建一个 ofstream 对象。<fstream> 定义了下列打开模式和文件属性:
ios::app // 从后面添加
ios::ate // 打开并找到文件尾
ios::binary // 二进制模式 I/O (与文本模式相对)
ios::in // 只读打开
ios::out // 写打开
ios::trunc // 将文件截为 0 长度

你可以用位域操作符 OR 组合这些标志:

ofstream logfile("login.dat", ios::binary | ios::app);

fstream 类型对象同时支持读和写操作:

fstream logfile("database.dat", ios::in | ios::out);

第二步:设置文件的位置
  文件具备一个逻辑指针,它指向该文件中的某个偏移位置。你可以通过调用seekp()成员函数,以字节为单位将这个指针定位到文件的任意位置。为了获取从文件开始处到当前偏移的字节数,调用seekp()即可。在下面的例子中,程序将文件位置前移10个字节,然后调用 tellp()报告新位置:

ofstream fout("parts.txt");
fout.seekp(10); // 从0偏移开始前进 10 个字节
cout<<"new position: "<<fout.tellp(); // 显示 10

你可以用下面的常量重新定位文ian指针:

ios::beg // 文件开始位置
ios::cur // 当前位置,例如: ios::cur+5
ios::end // 文件尾

第三步:读写数据
  fstream 类为所有内建数据类型以及 std::string 和 std::complex 类型重载 << 和 >> 操作符。下面的例子示范了这些操作符的使用方法:

fstream logfile("log.dat");
logfile<<time(0)<<"danny"<<''\n''; // 写一条新记录
logfile.seekp(ios::beg); // 位置重置
logfile>>login>>user; // 读取以前写入的值

posted @ 2009-03-05 11:52 不会飞的鸟 阅读(162) | 评论 (0)编辑 收藏

仅列出标题
共9页: 1 2 3 4 5 6 7 8 9