2009年5月5日星期二

Linux终端输出颜色控制

使用方法:
printf("\033[字背景颜色;字体颜色m字符串\033[0m" );
例子:
printf("\033[47;31mhello world\033[5m");
说明:
47是字背景颜色, 31是字体的颜色, hello world是字符串. 后面的\033[5m是控制码

格式: echo "\033[字背景颜色;字体颜色m字符串\033[0m"
例如: echo "\033[41;36m something here \033[0m"
其中41的位置代表底色, 36的位置是代表字的颜色
那些ascii code 是对颜色调用的始末.
\033[ ; m …… \033[0m
字背景颜色范围:40----49
40:黑
41:深红
42:绿
43:黄色
44:蓝色
45:紫色
46:深绿
47:白色
字颜色:30-----------39
30:黑
31:红
32:绿
33:黄
34:蓝色
35:紫色
36:深绿
37:白色

===========ANSI控制码的说明=============
\33[0m 关闭所有属性
\33[1m 设置高亮度
\33[4m 下划线
\33[5m 闪烁
\33[7m 反显
\33[8m 消隐
\33[30m -- \33[37m 设置前景色
\33[40m -- \33[47m 设置背景色
\33[nA 光标上移n行
\33[nB 光标下移n行
\33[nC 光标右移n行
\33[nD 光标左移n行
\33[y;xH设置光标位置
\33[2J 清屏
\33[K 清除从光标到行尾的内容
\33[s 保存光标位置
\33[u 恢复光标位置
\33[?25l 隐藏光标
\33[?25h 显示光标

我写的一个例程:
#include "stdio.h"

int main(void)
{
int color;
for (color = 0; color < 20; color++)
{
printf("\x1b[1;4;%dmThe color of this line is %d,
%s!\x1b[0m\n", 30+color, 30+color, "ha ha");
}
}
运行结果:

周立功25年嵌入式生涯

 《程序员》杂志的编辑约我写一篇命题作文,想了几天都无从下手不知道写什么才好。在这篇文章里,我不打算将创业的艰辛与喜悦重新回忆一遍,我确实不想去误导大家,因为我所处的年代是一个物质缺乏的年代,成功相对来说要容易得多。每个人的成长经历都有其个性化的东西,每个人的成功创业经验虽然有一些可以遵循的普遍规律,但一般来说都很难复制,成功需要不断地创新。
  对于今天很多的年轻人来说,一味注重技术至上的观念刻骨铭心,其实很多时候努力并非一定有回报。事实上,有不少出类拔萃的人才往往做出来的产品就是卖不掉,因为设计者压根儿就不了解用户的需求和心理以及产品功能的恰当定义,而总想在同事及其老板面前卖弄自己的技术和产品功能。其实有很多思维性的东西恰恰是很多人所忽视的,因为从一开始的出发点就错了,怎么可能取得辉煌的成绩呢?所以有时拥有卓越的技术并不一定代表人生的成功,很多企业就是死在一些自命不凡的“卓越人才”手中。

它山之石可以攻玉,减少“阶段0”的开发
有所“创造”确实是人人期待的,令人瞩目的发明虽然激动人心却谈何容易,人们时常将盖茨没有任何爆炸性的发明作为茶余饭后的笑料,我们不妨从另外一个角度去探索微软的成功奥妙从而为我们所用。确实不假,盖茨的DOS源代码是从帕特森手上“买”来的。这些年来,我收集了世界各地中英文版有关微软的专著来比较研究,大家仅仅注意到盖茨为建立整个行业架构标准的远见,并叹服盖茨深得市场运作经验的精髓,我认为这些研究成果都是后来者研究微软成功经验,是“牵强附会”不可复制的理论总结。
  当初微软公司还仅仅是一家很稚嫩的公司,可以说生存下去是盖茨作为老板唯一的使命。事实上,正当盖茨决定动手来写IBM所要的OS时,原计划在一年左右完成,但IBM公司只给了他几个月的时间。尽管帕特森的QDOS并不完善错误百出,但为了履行对IBM的承诺,盖茨购买了QDOS改贴标签后卖给了 IBM公司。盖茨对此心知肚明,因为他知道如果用一年的时间来做OS的开发,他将失去与巨人IBM的合作机会,那是一种浪费。付一小部分的技术费用没有关系,只要能获得Know-how,获得更高的利润就好了。通过支付权利金引入技术,然后以模仿的方式学习他人的技术,再改造成符合IBM需要即可。“买”――只要有现成的就不需要自己开发,这就是盖茨的过人之处,而且恰恰是很多人忽视的地方,值得我们所有人学习和仿效。盖茨就是由于没有“阶段0”,从而大幅度地降低开发风险。
当年,我是一个人单枪匹马借了2.15万元出来创业的,可以说是负资产,后来才开始有伙伴注资6万元。没有经验怎么办?我有一个非常好习惯,读书从不拘泥于他人的观点,也满足于人云亦云,否则那就是听别人讲故事,那不是您的东西,有入宝山空手而归。我认为,要想成功就必须“研究”成功者的轨迹,向成功者学习。
  下面我会把我“第一桶金”的故事告诉你,这也是尽量减少“阶段0”开发一个非常典型的例子。当年,《羊城晚报》几乎每天都有半版广告刊登信息台(听歌、悄悄话等)广告,一打听广告费每天几十万。当时相信不少人在利用公费电话在拨打这些信息台。后来我送货到客户那里发现电话机都外加了一个铁壳并上了一把锁,从看到那一幕开始,我决定做电话加密码锁,锁长途电话0字头,手机、BB机、信息台的9字头。通过朋友介绍,福州某公司有这个技术,于是我打了一个试探性的电话,使用5000元购买方案和源代码,对方非常爽快地同意了。我生怕有变立即坐飞机到福州去,很快就见到了郑新建工程师,他原来在福州某公司工作,我见到他的时候已经离开了福州某公司,但仍然自己写一些产品解决方案,通过福州某公司销售。买到方案与源代码之后,我立即带样机到各地去做测试,发现这台样机兼容性太差,而且市场已经开始在卖的产品也存在同样的问题,我想只要解决好兼容性的问题,如果在任何地区交换机局域网都能够使用,那就是最好的产品不愁没有市场。
我当时既没有资金也没有生产和销售经验,到底怎么将技术变成市场上所需要的产品呢?那是因为我找到了生产电话分线器的广州市白云区百新电器厂的老板陈国亮,由他生产和销售,每销售一台产品给我提成。以这个项目为起点,我们使用PIC单片机的数量很快就达到了每月100K以上,一年以后Microchip 香港公司经理Andi主动找上门来将当时的广州强力电子研究所发展成为了授权分销商。毫不否认盖茨的远见和判断力是微软至关重要的核心竞争力,但盖茨也绝非天才,否则盖茨就不会在浏览器上输给网景了,尽管微软使用的很多技术都不是微软所发明的,但毫不妨碍微软独步天下。

注重核心技术,其余的外包
通过创业以来第一个项目的成功,我深深地体会到商业模式比技术本身更重要。
通过十多年的努力,我们设计的“铁将军(Steel Mate)”品牌汽车防盗器、汽车倒车雷达不仅成为了全世界范围的“隐性冠军”,而且为GE等世界著名汽车厂商售前市场提供配套产品,与此同时我们为智能卡酒店门锁厂商设计与制造的各种智能卡门锁控制模块在国内也一直处于第一名的地位。他们为何能够取得与众不同的成功呢?其最大的长处就是产品创意与制造、简约时尚风格的工业设计、模具制造技术以及精心打造的销售渠道,这些厂商深刻明白“注重核心技术做自己擅长的,其余的外包分段取利”的基本道理,而对于我来说就是“智慧出租”,通过与强者合作获得双赢。通过这些成功经验的积累,我们开始全力以赴注重发展核心技术,将自己不擅长的技术全部外包。比如,我们长期投资清华大学计算机系,并建立以陈渝博士后为主Linux内核开发团队,为公司底层的技术提供良好支撑,而我们自己则将精力集中于Linux驱动开发。这种分工合作、协同开发的模式赢得市场上宝贵的时间,并快速取得了应用成果。
  也许有程序员会关心JTAG开发工具问题,那也是一个“外包”的项目。当时,我们在网上发现了一个由计算机爱好者业余时间设计的H-JTAG,这是一个比较稳定的调试器软件,正好适合我们使用。于是我就同设计者联系,并决定由周立功单片机资助他继续开发,同时,其开发成果还免费提供给网上的用户下载,保证它作为一个自由软件。其实,无论是Linux还是H-JTAG都是开源软件,如果担心其它的同行因此受益而超越自己的话,难免需要在公司负担很多开发人员,面面俱到地做许多事实上是重复的开发工作。其实,我们的合作伙伴都是所在领域的专家,如果自己从头到尾去学习和开发,不仅抢了合作伙伴的饭碗,而且结果未必理想。这种情况下不但会延误宝贵的商机,而且还会影响与合作伙伴之间的关系。其实只要引入我们企业长期积累和制定的嵌入式系统工程管理思想和规范,即可得到自己想要的结果:“做你最擅长的,其余的外包”,类似这样的案例在周立功单片机将会越来越多。

专注与差异化生存
1999年1月23日我离开了原公司,先后分别创立了广州周立功单片机发展有限公司与广州致远电子有限公司,分别从事贸易业务与产品制造。作为代理商通过为客户提供解决方案从而达到大批量销售芯片是代理行业惯常使用的手段,而实际上通过设计能够真正达到增值获得丰厚回报的代理商却少之又少。要知道过去公司的人才数量和资源是非常宝贵的,如果没有正确的战略,机会可能稍纵即逝。事实上创业开始的前几年,我们一共为用户设计了超过100个品类的产品,产品型号超过500种,可以说几乎涉及到了各行各业,但能够带来稳定而丰厚回报的案例几乎没有。我经过一段时间的思考和报表分析发现,目前公司的业务做得太多、太杂,没有将任何一个行业做精。于是我下定决心减少用户数量集中精力在1-2个行业做强、做大,直至成为顶尖的行业专家,坚持十年如一日重点做好汽车配套产品与智能卡门锁控制模块的开发。通过多年的精耕细作,我们通过为直接用户设计产品增值年销售单片机的数量已经超过了1000万片,无论是对用户还是对半导体公司来说,我们无疑是一家非常有价值的代理商。1999年5月,PHILIPS半导体公司邀请我去上海洽谈是否有意向代理LPC700单片机,当我拿到用户手册时离去上海的时间只有5天。当时公司不像现在有250位大学生,总共只有8个人,怎么办?我一边写商业计划书,一边与大家翻译用户手册、打字和贴图,面对机会就像抓住救命稻草一样加班加点,每天只休息3-5个小时。第5天我赶到上海东亚富豪酒店时,我将整整齐齐的材料放在了来自美国 PHILIPS半导体公司2位经理的面前,包括吃饭在内仅仅只洽谈了2个小时,代理权就正式谈下来了。事后他们告诉我,就是因为我与众不同,专注且非常有个性。
  风雨变换诱惑莫测,人怕出名猪怕壮,这是常理!随后找上门来要求我们代理芯片的著名半导体公司不下10家。这时候,对于很多人来说不做什么就是一个艰难的选择。此时,我认为不管对方开出的条件多好,有冲突的就是不做,性能不突出的也不做,如果能够形成价值链互补,就一定要通过自己的努力和业绩表现主动去“求”半导体公司授权给我们,我就是这样取得了Keil、Catalyst、Sipex的代理权和信用额度。2004年SHARP半导体公司北京、日本、美国一共五位经理来到我的办公室要求我代理SHARP的ARM,帮助SHARP在中国推广以及制造用于全球销售的开发套件,我根本没有思考就一口回绝了,可以说SHARP给我的条件非常之好,特别是美国人觉得是非常不可思议的事情,作为商人这么好的赚钱机会都不要。为什么?我们之所以暂时“强大”,就是因为我们长期以来专注于发展 PHILIPS半导体,集中精力对准焦点成为了小河里的大鱼,进而才引起了众人的关注。如果我们分散了精力,最后的结果一定会什么都不是!当决定专注的目标之后,接下来的就是想办法如何做得与众不同,这是一件不容易的事情。最重要的就是穷举用户最大的需求和竞争者最容易忽略之处,然后将其做到最好。
  为了推广好PHILIPS的单片机和ARM,我们定位于帮助初学者快速入门,至今一共编写出版了20多本专著,并将大部分版权全部捐献给了出版社用于降低成本,同时我也还会继续坚持将这项工作做下去、争取做得更好。邓小平有一句名言:“计算机要从娃娃抓起!”我认为,任何一个有远见的厂商,要想成功地推广嵌入式系统,一定要从在校大学生抓起,这必定是一件一举多得的好事。既能够提高企业的美誉度,又能获得市场占有率,还能为学生提供实践的理论基础。为何很少有厂商或者代理商心甘情愿去做?这不仅可帮助企业建立差异化,而且还能够给企业带来长远的利益,何乐而不为?比如说,至今在国内任何一家代理商的网站上几乎都找不到完整的芯片中文数据手册、用户手册与应用手册,不是我们的同行不知道其中的重要性。首先大家普遍认为这是半导体公司的份内之事,其次大家或许感到自己做好之后放到网站任由用户免费下载,会给其它代理商占了便宜。但很多人却始终不明白,这是满足用户需求制造公司服务差异化的最佳机会。于是我们决心从网站入手坚持长期投资,做中国最好的嵌入式系统专业技术网站,建立专业的技术支持与开发团队。特别是对于优质用户的服务,网站还专门开通了“快速绿色通道”,如果这样的用户遇到问题,都是由我、分公司经理及其相关服务部门的经理牵头负责实施“保姆式”的服务,我们实施的服务战略就是:“用有限的资源为有效的用户提供高质量的服务”。
  “你若亲近神,神就亲近你!” 2001年8月一篇来自PHILIPS内部刊物的报道《风险意味着机会》,我是被推上了“本地英雄”封底人物栏目的第一位亚洲代理商;2004年 PHILIPS半导体公司又给我授予了“优秀卓越贡献奖”,来自全国各地的用户连续多年通过《亚洲资源》媒体将我们公司评选为“本地十大最受客户欢迎的分销商”。没有用户崇高的忠诚度和长期的大力支持,我们不可能从1999年以来连续多年销售单片机取得PHILIPS亚太区第一名的业绩。一个“人弃我取” 的机会成就了我,尽管个人与团队的努力至关重要,但可以毫不掩饰地说,如果没有PHILIPS半导体经理的慧眼相马,一定是巧妇难为无米之炊,业界不会再有我的一席之地。在嵌入式系统应用技术方面与我们不相伯仲或者说比我们聪明能干的人才何止万千,为何只有少数人能够脱颖而出呢?就是因为想法太多、不专注,不能坚持在一个行业里面十年如一日地下苦功夫。但面对众多的竞争者和后来者,我每天都如履薄冰,不敢有丝毫的懈怠,每天坚持学习到深夜,因为唯有不断地学习和更新思维才有可能具备远见并及时规避可能出现的失误。

关注用户的需求
其实维系企业生命的关键在于产品是否能够获得市场的青睐,我们要做的并非是成为技术的领先者,而是要将技术成果转化为迎合消费者需要的商品。八十年代是一个物质缺乏的年代,而今天的人们更加注重消费享受和情感的分享,这是任何人都无法否认的现实。我们公司刚开始做产品时,由于我个人偏爱蓝色,所以我凭着老板的权威将所有产品的外观全部都做成蓝色,其实当时也有不少的人向我提出改进意见,但我仍然还是无动于衷,为什么?就是因为我是做技术出身的,而且从创业初期开始一直都是依靠技术一路拼杀过来的,可以说过于迷信技术的力量,因此在很长的一段时间内,公司完全走的是研究的路子。而我却恰恰忽视了用户读产品的心理需求,以至于在产品推向市场之后一直都无法打开局面。当时,EasyPRO系列编程器面市的时候,其销售数量一年下来都不到500套,同时有用户表示对于产品的设计很不满意,这可以说是一次深刻的教训。于是我下定决心组建工业设计事业部,首先从EasyPRO系列通用编程器开始,亲自管理、研究和参与产品的外观设计,将原来的模具全部废掉重新设计。尽管是第一次设计,在产品的外观上没有做出非常杰出的效果,但无论如何,新产品外观比原有的产品出众得多。2005年10个月的时间里,EasyPRO系列通用编程器的销售数量达到了6000套。2006年我们又推出了SmartPRO系列智能型通用编程器,预计本年度销售编程器的总数量将超过10000套,而这些产品将带着我和我们公司跻身于国内编程器市场主流品牌之列。从那次深刻的教训以后,我明白好的外观设计可以凸现产品的魅力,让产品与环境融为一体,刺激消费者产生强烈的购买欲望。即使是普通产品,经过精心设计,也会成为一件时尚产品,甚至让人们觉得它就是一件艺术品,从这一点来说,时尚、新潮、艺术化的外观设计是塑造产品品牌形象的第一要素。从那以后,我开始改变产品简介、广告以及展示会的形象设计,为了配合新的形象,我们展开了大规模、全方位的宣传:一方面尽量多地为用户提供产品简介手册,同时通过大量的媒体广告,让用户认识并初步了解公司,从而迅速有效地提高了品牌知名度;另一方面组织各种全国巡回演讲、免费ARM技术培训活动和产品展示会,而且我们每次参加展示会都是投入4个摊位并进行特装展位设计,目的就是为了获得与用户积极沟通的机会。2003年暑假期间,周立功单片机投资80万元邀请了行业3位著名的嵌入式系统专家组成讲师团在全国10个城市主办了免费的ARM巡回演讲,提供了免费的午餐,可以说规模空前。这次巡讲中,共有8000多人参加了这次活动,随后公司趁热打铁推出了售价400元的EasyARM2104开发学习套件,第一个月生产的1000套在15天之内抢购一空,直到销售了15000套之后才因新产品的推出而停产。这次活动不仅收回了80万元的宣传费用,而且还直接或间接地影响了超过50万嵌入式相关人员,强有力地达到了品牌宣传的目的。在赢得丰厚利润回报的同时,也打造了嵌入式行业一次非常成功的“事件营销”经典案例,我深深地体会到“俘虏”用户的心是提升品牌的唯一途径。

全球化市场意识
过去,一个公司可能需要等到10年之后才开始考虑是否全球化,而今天由于网络技术的发展,无论我们身处何地,世界各地的新信息仿佛就在眼前,谁也不会被边缘化了,所以今天成立公司明天就要准备全球销售。这几年ARM已经到了热得不得了的程度,很多人不以为然,其实这是一种必然趋势。事实上,我在美国 PHILIPS半导体公司看到,美国1997年出版的《嵌入式系统》杂志就已经大量刊登OS与32位嵌入式系统开发工具等文章和广告。在美国硅谷主办的嵌入式系统展示会上,来自欧美的上千展位展现在我眼前的是各种各样嵌入式系统产品,中国厂商普遍展示的是蜂鸣器、继电器等低档元器件。我第一次身临其境地感受到了中美之间的差距究竟有多大。中国太需要懂得国际市场需求和游戏规则的人才和企业家了。同样是做USB分析仪,全球只有5家,我们自己的USB分析仪升级版在中国目前定价为3800元,这款产品原来的售价只有1800元,用户还认为太贵了。其实这样的产品在国内一年的销售数量也就200台,但是同样的情况在美国,一个小公司一年的销售额就能够到2000万美元,他们的单台最低售价不低于10000美元,以至于美国同行直接给我来电话,希望我在售价的基础上至少加一个0。中国企业不做全球化销售能行吗?不行!而且中国企业必须向价值链顶端攀升,否则我们只能做欧美消费品市场的廉价加工基地。

个性化的企业文化,帮助他人走向成功
通过与用户合作所经历的共同成功过程中,用户将我教育成为了一个善良、上进、诚实、优秀的人才。我不仅懂得如何做自己最擅长事情的方式方法,而且也懂得了如何与用户、与员工分享成功的道理。一旦芯片销售数量增大成本下降就主动给用户降价,赢得了用户更长远的支持;公司效益提高了,年终就主动给员工涨工资、加奖金并对优秀员工提供购房津贴。由于公司的规模不大,我们一直以招聘二类本科应届生为主,在内部采取导师制的培养模式,为他们提供至少一年的严格训练,帮助他们规划职业生涯,想尽办法提供机会将他们培养成优秀的人才。这些年来不少公司打电话向我们公司挖人,关键人才从未出现过流失,至今跳槽的员工不超过12人,我深深地感到这就是我人生最大的快乐和财富,离开了支持我的用户和追随我的人才,我将变得一钱不值。为了保证企业的可持续发展所带来的人才困境,我与江西理工大学合作以五年为期开办了“3+1”嵌入式系统应用开发特训班,与此同时还设立了每人3000元共10人的“周立功奖学金”资助那些动手能力极强而成绩中等的学生,开展校企无缝联接,培养嵌入式系统创新性开发人才的有益尝试,即就是从7个班中选拔30位爱好者用3年的时间修完所有课程,我们为每一位学生提供一台计算机、全套开发工具及其设计中所发生的一切费用,由我们组织著名专家开设专题讲座和课程,学校组织1位专职辅导员和2位指导教师,并由我们提供必要的津贴,然后用一年的时间专门做毕业设计,为此我们制定了严谨、细致的培训计划。这些年我们还不断赞助各省电子大赛、毕业设计大赛、创新设计大赛,捐赠了50所大学单片机创新实验室,以求扩大企业的知名度和广泛寻求人才的来源。

最后的话
很多开发工程师以为这些都是老板或者经理们应该考虑的问题,与己无关,其实是大错而特错。作为一位优秀而卓越的开发人员,如果希望获得人生的完全成功和快乐,我们时刻需要明白,任何一个企业的可持续发展一定是群策群力的结果。1981年我经历了高考失败之后上了技校,1999年我作为劳动模范被保送上了大学,创业至今十多年来,往事依然历历在目,用户给予我的回报很多,很难在此一一完全道来,谨以此文与同行、用户分享和交流。这些年来,我陆续捐资资助教育、设立奖学金以及帮助家乡修建乡村水泥公路、祠堂等公益事业回馈社会。1990年的中秋节,我只身一人南下广东打工,不曾想到会有今天这样的成绩,我一直非常珍惜这来之不易的机会。作为一个嵌入式技术爱好者,我的愿望就是:“生命不息、奋斗不止!”力争为发展中国的嵌入式系统应用技术贡献自己的一份力量。 

有关用C实现汉字的显示3:图形模式下的中英文显示

有了以前的理论,用C实现汉字的显示对于我们来说已经变得不那么难了
那么,现在面临的一个问题是,当一个字符串里有中文字符又有英文字符时,由于一个中文字符占两个字节而一个英文字符只占一个字节,而

且他们的内码不同,如果我们还用前面所描述的方法,打印出来的肯定是乱码一堆,细心的读者会发现在

理论1里的字符串全都是中文字符,那是为了避免这一问题的出现。

  那么,怎么来解决中英文混排显示的问题呢? 其实不难,细心的读者通过阅读理论1的前几段就会发现“...于是我们的DOS前辈想了一个办法,就是将ASCII表的高128个很少用到的数值以两个为一组来表示汉字,即汉字的内码。而剩下的低128位则留给英文字符使用,即英文的内码...”,这些话里隐含了区分中文字符与英文字符的方法,即:
从字符串中读入一个字节数据,当最高二进制位为1 说明当前字符和下一字符一起构成一个中文字符 下次从该字节的下下字节开始读数据
从字符串中读入一个字节数据,当最高二进制位为0 说明当前字符为一英文字符 下次从该字节的下一字节读数据

  理论很难懂的话就让事实来说明一切吧

/*
*图形模式下的中英文显示
*LO几又VE 16:26 2005-5-25
*字体库下载
*将字体库文件放置于编译目录下[或者更改路径参数]
*hzk16文件下载地址:
*下载字体文件
*/
#include
#include

#define MAXX 640 /*屏幕宽度*/
#define MAXY 480 /*屏幕高度*/
#define WIDTH 20 /*每一汉字宽度*/
#define HIGH 20 /*每一汉字高度*/
#define CHSIZ 2 /*英文字体大小*/

int priChi(unsigned char *); /*中文打印函数,传入参数:中文数组指针 返回值 -1 异常 0 正常*/

int X=0; /*全局变量X Y控制中文打印格式*/
int Y=0;

int main()
{
  char chinese[][60]={/*TC编译器不支持长行 所以将中文字符串以二维形式存放*/
  "我放弃清华计?????!@!a专业的保送资格而选择参加高考的消息震撼",
  "从校长到班主任到各科目任课老师都找我谈过话我有点惊异他们的变",
  "化在我获得全国信息this is a program for type Whinese words ",
  "and english words.djfkjaiejfjas>ifojaewkfjaweifjaokjdsfjiaew",
  "说起这个特别奖还真的挺有意思虽然叫全国决赛但也无非是出几个笔",
  "试题再弄个上机程序编一下限时总共是三小时规定语言是西语言或者",
  "派司卡在我花了半个小时分别用两种语言把该程序完成后我觉得坐在",
  "那实在是浪费我的大好青春于是我决定用汇编语言把它再写一遍本来",
  "我准备用微操作的十六进制码写的但考虑到时间问题只好放弃有人说",
  "是金子总会发光此话诚不欺我啊正当我热火朝天全心投入编程的时候",
  "却不知道自己已经被某人注意了很久了三小时之后我走出考场之后某",
  "人赶紧抓住我很兴奋的叫了一声小朋友这声小朋友直接导致我在数年",
  "后还经常从噩梦中惊醒同时也是我拒绝清华邀请的直接导火索我们把",
  "话题回到事发现场我在吓了很大一跳后把头转了回来一个瘦干老头正",
  "抓着我的手两眼放光的样子还似乎是略带深情的看着我我全身鸡皮疙",
  "瘩顿时争先恐后的向外钻我浑身一个机灵赶紧抖手老头似乎感觉到了",
  "异样送开我的手略带尴尬的道小朋友我自我介绍一下我是清华大学计",
  "算机系的主任受邀到比赛的现场观看顺便看看有没有什么可挖掘的人",
  "才那个你明白我的意思吧这句话很是影响清华学生理解力在我心目中",
  "的地位你这意思不就是认为我是可挖掘的人才吗我点了点头老头看我",
  "能理解显的很兴奋然后开始滔滔不决的向我介绍清华计算机专业的实",
  "力如何如何的雄厚在国内甚至国际是如何如何的有影响力最后满怀深",
  "情的看了我眼严肃的告诉我他将代表清华计算机专业欢迎我去他们那",
  "念书我苦苦忍耐着他的飞溅唾沫委婉的表示这件事非同小可我必须回",
  "家和父母商量一下然后逃也似的离开当然第一个去的地方是卫生间我",
  "的脸啊谁知道老头的唾沫会不会让我的脸起老年斑回到学校的一个礼",
  "拜后我就接到了清华正式邀请这个消息经我班主任的乌鸦嘴迅速在全",
  "校蔓延而后又经过各种渠道迅速汇总到我父母耳朵。"};

  if(-1 == priChi(chinese) )
  {/*打印异常*/
  printf("Press any key to exit...");
  fflush(stdin);
  getch();
  return 1;
}
return 0;
}

int priChi(unsigned char *chi)
{
  unsigned char charc[2];
  unsigned char mat[16][2];
  int i=VGA,j=VGAHI,k;
  int sec,pot;
  FILE *HZK;
  if((HZK=fopen("hzk16","rb"))==NULL) /*打开字体库文件*/
  {/*打开字体库失败*/
  printf("Open style file (hzk16) failed!\n");
  return -1;
  }
  initgraph(&i,&j,""); /*图形模式初始化*/
  settextstyle(0,0,CHSIZ);/*英文字符初始化*/
  while(*chi)
  {/*每循环一次在图形模式下打印一个中文或者英文字符*/
  if(*chi & 0x80) /*最高位为1 -- 中文字符*/
  {
  sec = *chi-0xa0; /*获得中文字符的区码*/
  pot = *(chi+1)-0xa0; /*获得中文字符的位码*/
  fseek(HZK,(94*(sec-1)+(pot-1))*32l,SEEK_SET);
  fread(mat,32,1,HZK);
  for(j=0;j<16;j++)
  for(i=0;i<2;i++)
  for(k=0;k<8;k++)
  if(mat[j][i] & (0x80 >> k)) /*测试为1的位则显示*/
  putpixel(X+i*8+k,Y+j,WHITE);
  chi += 2; /*指针指向下一中文字符*/
  }
  else /*最高位为0 -- 英文字符*/
  {
  charc[0] = *chi; /*为输出英文字符作准备 */
  charc[1] = '\0';
  outtextxy(X,Y,charc);/*在指定位置(图形模式)发送一个字符串*/
  chi ++; /*指针指向下一英文字符*/
  }

  X += WIDTH; /*下一字符的横坐标 */
  if(X >= MAXX) /*满一行 */
  {
  X = 0; /*归零*/
  Y += HIGH; /*下一字符的纵坐标 */
  if(Y >= MAXY) /*满一页 */
  {
  printf("press any key to print other words");
  getch();
  system("cls"); /*清屏*/
  Y = 0; /*归零*/
  }
  }
  }
  getch();
  closegraph(); /*关闭图形系统*/
  fclose(HZK); /*关闭字体文件*/
  return 0;
}

  通过这张帖子,你应该对字符的显示有了更深一层次的认识吧。

点阵汉字显示

由于Turbo C应用于DOS操作系统下,在使用Turbo C进行程序设计时,一般情况下只好使用英文进行人机交互。要是想直接用中文界面,就需要另想他法了。

  如果使用中文DOS系统(如UCDOS),则可以解决在字符界面下的汉字显示问题。也就是说,可以用printf或其他字符串函数来输出汉字。

  但是,这样仍然有一些不方便。必须先启动中文DOS系统,再执行Turbo C或编译好的程序。并且在中文版DOS下运行Tubor C时,还可能出现一些问题。而对于图形界面来说,这种办法也行不通了。

  那么在图形界面下显示汉字的问题就迫切需要解决了。既然是图形界面只要把汉字当成一幅画,画在显示屏上不就可以了。关键在于如何取得汉字的图形,也就是汉字的点阵字模呢。其实那些中文版的DOS显示汉字的方式也就是在图形界面下画出汉字的,它们已经提供了现成的点阵字库文件。例如常用的16×16点阵HZK16文件,12×12点阵HZK12文件等等,这些文件包括了GB 2312字符集中的所有汉字。现在只要弄清汉字点阵在字库文件中的格式,就可以按照自己的意愿去显示汉字了。

  下面以HZK16文件为例,分析取得汉字字模的方法。

  HZK16文件是按照GB 2312-80标准,也就是通常所说的国标码或区位码的标准排列的。国标码分为 94 个区(Section),每个区 94 个位(Position),所以也称为区位码。其中01~09 区为符号、数字区,16~87 区为汉字区。而 10~15 区、88~94 区是空白区域。

  如何取得汉字的区位码呢?在计算机处理汉字和ASCII字符时,使每个ASCII字符占用1个字节,而一个汉字占用两个字节,其值称为汉字的内码。其中第一个字节的值为区号加上32(20H),第二个字节的值为位号加上32(20H)。为了与ASCII字符区别开,表示汉字的两个字节的最高位都是1,也就是两个字节的值都又加上了128(80H)。这样,通过汉字的内码,就可以计算出汉字的区位码。

  具体算式如下:
  qh=c1-32-128=c1-160 wh=c2-32-128=c2-160
  或   
  qh=c1-0xa0 wh=c2-0xa0
  qh,wh为汉字的区号和位号,c1,c2为汉字的第一字节和第二字节。
  根据区号和位号可以得到汉字字模在文件中的位置:
  location=(94*(qh-1)+(wh-1))*一个点阵字模的字节数。
  那么一个点阵字模究竟占用多少字节数呢?我们来分析一下汉字字模的具体排列方式。

  例如下图中显示的“汉”字,使用16×16点阵。字模中每一点使用一个二进制位(Bit)表示,如果是1,则说明此处有点,若是0,则说明没有。这样,一个16×16点阵的汉字总共需要16*16/8=32个字节表示。字模的表示顺序为:先从左到右,再从上到下,也就是先画左上方的8个点,再是右上方的8个点,然后是第二行左边8个点,右边8个点,依此类推,画满16×16个点。  

  对于其它点阵字库文件,则也是使用类似的方法进行显示。例如HZK12,但是HZK12文件的格式有些特别,如果你将它的字模当作12*12位计算的话,根本无法正常显示汉字。因为字库设计者为了使用的方便,字模每行的位数均补齐为8的整数倍,于是实际该字库的位长度是16*12,每个字模大小为24字节,虽然每行都多出了4位,但这4位都是0(不显示),并不影响显示效果。 还有UCDOS下的HZK24S(宋体)、HZK24K(楷体)或HZK24H(黑体)这些打印字库文件,每个字模占用24*24/8=72字节,不过这类大字模汉字库为了打印的方便,将字模都放倒了,所以在显示时要注意把横纵方向颠倒过来就可以了。

  这样我们就完全清楚了如何得到汉字的点阵字模,这样就可以在程序中随意的显示汉字了。

  如果在程序中使用的汉字数目不多,也可以不必总是在程序里带上几百K的字库文件,也许你的程序才只有几十K。这样可以事先将所需要显示的汉字字模提取出来,放在另一个文件里,按照自己的顺序读取文件就可以了。

  下面的程序说明了具体显示汉字的方法,以16×16汉字为例,使用HZK16文件。 

#include
#include

/* x,y为显示坐标,s为显示字符串,colour为颜色 */
void hanzi16(int x,int y,char *s,int colour)
{
 FILE *fp;
 char buffer[32]; /* 32字节的字模缓冲区 */
 register i,j,k;
 unsigned char qh,wh;
 unsigned long location;
 if((fp=fopen("hzk16","rb"))==NULL)
  {
  printf("Cant open hzk16!");
  getch();
  exit(0);
  }
 while(*s)
  {
  qh=*s-0xa0;
  wh=*(s+1)-0xa0;
  location=(94*(qh-1)+(wh-1))*32L; /* 计算汉字字模在文件中的位置 */
  fseek(fp,location,SEEK_SET);
  fread(buffer,32,1,fp);
  for(i=0;i<16;i++) j="0;j<2;j++)" k="0;k<8;k++)">>(7-k))&0x1)!=NULL)
  putpixel(x+8*j+k,y+i,colour);
  s+=2;
  x+=16; /* 汉字间距 */
  }
 fclose(fp);
}

main()
{
 int gd=DETECT,gm;
 initgraph(&gd,&gm,"");

 hanzi16(246,200,"疯狂甲虫乐园!",BROWN);

 getch();
 closegraph();
}

汉字编码问题

由于常常要和汉字处理打交道,因此,我常常受到汉字编码问题的困扰。在不断的打击与坚持中,也积累了一点汉字编码方面的经验,想和大家一起分享。
一、汉字编码的种类
汉字编码中现在主要用到的有三类,包括GBK,GB2312和Big5。
1、GB2312又称国标码,由国家标准总局发布,1981年5月1日实施,通行于大陆。新加坡等地也使用此编码。它是一个简化字的编码规范,当然也包括其他的符号、字母、日文假名等,共7445个图形字符,其中汉字占6763个。我们平时说6768个汉字,实际上里边有5个编码为空白,所以总共有6763个汉字。
GB2312规定"对任意一个图形字符都采用两个字节表示,每个字节均采用七位编码表示",习惯上称第一个字节为"高字节",第二个字节为"低字节"。GB2312中汉字的编码范围为,第一字节0xB0-0xF7(对应十进制为176-247),第二个字节0xA0-0xFE(对应十进制为160-254)。
GB2312将代码表分为94个区,对应第一字节(0xa1-0xfe);每个区94个位(0xa1-0xfe),对应第二字节,两个字节的值分别为区号值和位号值加32(2OH),因此也称为区位码。01-09区为符号、数字区,16-87区为汉字区(0xb0-0xf7),10-15区、88-94区是有待进一步标准化的空白区。

2、Big5又称大五码,主要为香港与台湾使用,即是一个繁体字编码。每个汉字由两个字节构成,第一个字节的范围从0X81-0XFE(即129-255),共126种。第二个字节的范围不连续,分别为0X40-0X7E(即64-126),0XA1-0XFE(即161-254),共157种。

3、GBK是GB2312的扩展,是向上兼容的,因此GB2312中的汉字的编码与GBK中汉字的相同。另外,GBK中还包含繁体字的编码,它与Big5编码之间的关系我还没有弄明白,好像是不一致的。GBK中每个汉字仍然包含两个字节,第一个字节的范围是0x81-0xFE(即129-254),第二个字节的范围是0x40-0xFE(即64-254)。GBK中有码位23940个,包含汉字21003个。

表1 汉字编码范围
名称            第一字节                        第二字节
GB2312     0xB0-0xF7(176-247)     0xA0-0xFE(160-254)
GBK          0x81-0xFE(129-254) 0x40-0xFE(64-254)
Big5          0x81-0xFE(129-255) 0x40-0x7E(64-126)
                                                       0xA1-0xFE(161-254)


二、对汉字进行hash
为了处理汉字的方便,在查找汉字的时候,我们通常会用到hash的方法,那怎么来确定一个汉字位置呢?这就和每种编码的排列有关了,这里主要给出一种hash函数的策略。
对于GB2312编码,设输入的汉字为GBword,我们可以采用公式(C1-176)*94 + (C2-161)确定GBindex。其中,C1表示第一字节,C2表示第二字节。具体如下:
GBindex = ((unsigned char)GBword.at(0)-176)*94 + (unsigned char)GBword.at(1) - 161;

之所以用unsigned char类型,是因为char是一个字节,如果用unsigend int,因为int是4个字节的,所以会造成扩展,导致错误。
对于GBK编码,设输入的汉字为GBKword,则可以采用公式
index=(ch1-0x81)*190+(ch2-0x40)-(ch2/128),其中ch1是第一字节,ch2是第二字节。
具体的,GBKindex = ((unsigned char)GBKword[0]-129)*190 + ((unsigned char)GBKword[1]-64) - (unsigned char)GBKword[1]/128;

三、怎样判断一个汉字的是什么编码
直接根据汉字的编码范围判断,对于GB2312和GBK可用下面两个程序实现。
1、判断是否是GB2312
bool isGBCode(const string& strIn)
{
unsigned char ch1;
unsigned char ch2;

if (strIn.size() >= 2)
{
ch1 = (unsigned char)strIn.at(0);
ch2 = (unsigned char)strIn.at(1);
if (ch1>=176 && ch1<=247 && ch2>=160 && ch2<=254)
return true;
else return false;
}
else return false;
}
2、判断是否是GBK编码
bool isGBKCode(const string& strIn)
{
unsigned char ch1;
unsigned char ch2;

if (strIn.size() >= 2)
{
ch1 = (unsigned char)strIn.at(0);
ch2 = (unsigned char)strIn.at(1);
if (ch1>=129 && ch1<=254 && ch2>=64 && ch2<=254)
return true;
else return false;
}
else return false;
}

3、对于Big5
它的范围为:高字节从0xA0到0xFE,低字节从0x40到0x7E,和0xA1到0xFE两部分。判断一个汉字是否是BIG5编码,可以如上对字符的编码范围判断即可。如何定位呢?那么也想象所有编码排列为一个二维坐标,纵坐标是高字节,横坐标是低字节。这样一行上的汉字个数:(0x7E-0x40+1)+(0xFE-0xA1+1)=157。那么定位算法分两块,为:
  if 0x40<=ch2<=0x7E: #is big5 char 
  index=((ch1-0xA1)*157+(ch2-0x40))*2 
  elif 0xA1<=ch2<=0xFE: #is big5 char 
  index=((ch1-0xA1)*157+(ch2-0xA1+63))*2

对于第二块,计算偏移量时因为有两块数值,所以在计算后面一段值时,不要忘了前面还有一段值。0x7E-0x40+1=63。

四、如果判断一个字符是西文字符还是中文字符
大家知道西文字符主要是指ASCII码,它用一个字节表示。且这个字符转换成数字之后,该数字是大于0的,而汉字是两个字节的,第一个字节的转化为数字之后应该是小于0的,因此可以根据每个字节转化为数字之后是否小于0,判断它是否是汉字。
例如,设输入字为strin,则,
  If (strin.at(0) < 0)
  cout << ”是汉字” << endl;
  else cout << ”不是汉字” << endl;
 
五、编码表下载
GBK编码表,下载
GB2312编码表,下载

汉字的动态编码与显示方案

摘要:综合几种常用单片机汉字显示方案,提出一种基于PC机预处理的汉字动态编码和动态字库的显示方法,较好地解决了存储空间、显示速度、软件开发维护几方面的相互矛盾;具有平台化的优点,同时,给出针对MCS51优化的汇编显示例程。

关键词:机内码 动态编码 字库

因为汉字本身的特点,显示汉字始终是计算机在我国应用普及的一个障碍。最初,为了能在PC机上显示、处理汉字,国人发明了一种硬件设备"汉卡",后来各种各样的采用纯软件技术的中文DOS逐渐成熟,其中、西文软件的运行速度和性能还是有明显的差距。最终在软件进入支持UNICODE、真正实现国际化的WIN95以后,硬件跨入"奔腾"时代,才实现了汉字与西文的统一显示,但是这一切是以硬件资源的飞速发展为前提的。以国际GB2312为例,一、二级汉字库共收录了6000多个汉字,每个字按16×16点阵计算,字模需要占用32字节的存储空间,整个字库的规模在200k字节以上,高点阵(24点阵以上)和矢量字库以及Windows用的TrueType字体的字库规模都是几兆字节大小,这在早期的386时代是难以想象的。单片机因为使用灵活、结构简单、体积小、成本低而在工业和生活中得到广泛应用,也正是因此,它的硬件资源很有
限,寻址和计算机能力都远低于PC机,显示汉字更受限制。人们不满足单片机系统采用LED数码管的简单显示,根据单片机的特点,开发出了很多种汉字显示方法。

1 几种常用单片机显示汉字方法

(1)采用标准字[1]

这种方法仿器中文DOS的办法,将一个标准的汉字库装入ROM存储器,再根据汉字的机内码在字库中寻址,找到对应的字模,提取后送到显示器显示。因为采用了和PC机相同的编码(机内码),软件的开发和维护非常简单,基本上与写PC机软件差不多。而对单片机系统自身的要求则相对高多了,16×16点阵的字库需要256K字节,但是一般8位单片机的寻址能力只有64K字节,要进行存储器扩充,除增加很大一部分硬件成本外,还因为要进行存储器分页管理、地址切换,显示速度明显受影响,而且只能显示一种点阵字体。

(2)直接固化显示字模[2]

将要显示的语句中全部汉字的字模数据依次提取出来,顺序存放在存储器中,当显示时,直接取出字模数据送至显示器即可。这种方法占用空间少,程序实现简单,显示速度快;但是字模数据的提取和存储安排是一件委有繁琐的事件,要想大量显示汉字或进行程序修改几乎是不可能的,软件的可维护性很差。

(3)建立带索引的小字库[3]

将全部要显示的汉字统一建成一个小字库,字库分为2部分:索引素和字模表。索引表由若干定长记录组成,记录的内容为:汉字机内码、地址码、识别码。其中地址码是该汉字字模在字模表中的位置,识别码标志该汉字的点阵形式或字体等。字模表中按素引存放汉字字模。显示汉字时先根据待显汉字的机内码在索引表中寻找,找到对应索引记录后,读出地址码和识别码,再根据此从字模表中读出字模,送显即可。这种方法可根据实际使用对字库进行裁剪,硬件开销较小,但是要进行复杂的查询运算,字多了平均寻找时间就会变长,效率降低。

2 汉字动态编码

综上所述,我们发现:在方法1中,程序员工作量最少,但单片要机的软、硬件开销最大;方法2中,单片机的开销较少,但是编写和维护软件极为困难;方法3,介于二者之间。显然,存储空间、显示速度、软件开发维护件间存在着矛盾。受各种PC机模拟软件的启发,我们提出一种基于PC机预处理的汉字显示方法--汉字动态编码,在实际应用中较好地解决了这一问题。其基本原理如下:建立一种新的编码机制,这个汉字编码是动态的;一个编码不与某个汉字具体相联系,而仅代表某个汉字在字库中的位置(这个位置也是动态的);用该码代替程序里字符串(C语言)或数据段(汇编语言)内汉字的机内码,单处机显示程序可根据这个新的编码直接在专门建立的动态小字库中找到字模,不用进行复杂的寻址、查找等运算,如图1所示。

实现汉字动态编码的过程就是先进行汉字识别,然后建立编码字典、提取字模、建立动态字库、改写机内码。首先扫描一遍程序文件,识别其中的汉字,将它们按出现先后顺序或机内码的大小排序,重复出现的剔除,建立了一个编码字典;根据汉字在编码字典的位置(序号),可以对汉字按区码、位码进行编码,也可以采用其它的方法编码,总之序号与它的动态编码存在一一对应关系;根据字典中每个汉字的机内码依次从PC机的汉字点阵字库中提取字模,顺序存储,建立一个小规模的动态字库,这样每个汉字的字模在字库中的位置就与其在编码字典中的序号、动态编码一一对应了。最后,再扫描一遍程序文件,按照编码字典将每个汉字的机内码改写为对应的动态编码。因为程序文件中的汉字随时会增减,编码随之而变,字库的大小也随时在变。所以称之为动态编码和动态字库。

考虑一般应用场合,1000个左右的汉字即可满足要求,按照汉字动态编码方法所需的字库仅为32K字节大小,只需要1片27256即可,几乎不用增加什么硬件。这样,字库的大小可由汉字的多少控制,程序的编写和维护可以沿用中文系统下的习惯,仅需要编写好的单片机程序用PC机进行一次预处理,程序员从繁杂的汉字处理工作中解放出来,有效地降低了软件和硬件开发成本。

3 汉字动态编码的具体实现

实现汉字动态编码的关键是建立编码字典和改写机内码。下面以是显示1行汉字"天上有个太阳,水中有个月亮"为例,说明动态编码的实现过程。

(1)汉字识别

汉字在PC机内的存储和处理是用机内码来实现的。每个汉字的机内码是唯一的,由2个字节组成,分区码和位码,为了和西文的ASCII码有区别,汉字机内码的区码和位码的取值都大于0A0H。我们要处理的源程序文件都是文本文件,存储的都是西文字符、控制符的ASCII码和中文字符的机内码,当扫描到文件中大于0A0H的字节内容时,即可判断该字节是汉字机内码的1个字节,而且肯定是成对出现,第1个字节是区别,第2个字节是位码,都大于0A0H,否则出错。

在C和汇编程序中表示字符的方式有所不同,但最终字符在文件中的存储格式是一样的。显示上面那行汉字,用C语言可以表示为:

char OneSent[]="天上有个太阳,水中有个月亮";

printfhz(OneSent);/*printfhz()显示函数*/

用十六进制编辑器(我们用的是UEdit32)察看文件中C语言字符串定义语句为:

63 68 61 72 20 20 4F 6E 65 53 65 6E 74 5B 5D 20 3D 20 22 CC EC C9 CF D3 D0
B8 F6 CC AB D1 F4 A3 AC CB AE D6 D0 D3 D0 B8 F6 D4 C2 C1 C1 22 20 3B 0D 0A

用汇编语言可以表示为:

ONESENT:DB '天上有个太阳,水中有个月亮',00H

MOV DPTR,ONESENT

LCALL DISPLAY;DISPLAY是显示子程序

用十六进制编辑器察看上面用汇编语言定义字符串的那一条语句为:

4F 4E 45 53 45 4E 54 3A 44 42 20 27 CC EC C9 CF D3 D0 B8 F6 CC AB D1 F4 A3
AC CB AE D6 D0 D3 D0 B8 F6 D4 C2 C1 C1 27 2C 30 30 48 0D 0A

由此可以观察到情况确如前所述。

(2)建立编码字典

编码字典是在扫描的同时逐步建立起来的,每扫描到一个汉字(包括全角符号),即与字典中已有的字符进行比较,如没有重复,是新的字符就顺序存入字典,否则继续扫描,直至文件结属。由于每个字符都是从尾部添加的,它们的序号也是依次递增的,根据序号就可以进行动态编码了。由于显示的汉字一般都得在256个以上,即使进行动态编码,也需要用2字节编码来实现。以MCS51系列单片机和16×16点阵汉字做一优化编码示例:8051的地址指针DPTR是16位指针,由高、低2字节指针DPH、DPL组合而成,如果将存储器按0FFH(256)字节分布,修改DPH即可直接寻址到任一页,修改DPL可寻址该页的任一字节。一个16×16点阵汉字的字模是32字节大小,每页存储器正好能容纳8个汉字字模。可以优化设计动态编码的高字节指向字模的页地址(DPH),低字节指向字模在该页的首地址(DPL)。考虑地址空间的有效分配,将字库的地址放在0A000H以后(程序或数据存储器均可),动态编码的高字节要加上地址有效分配,将字库的地址放在0A000H以后(程序或数据存储器均可),动态编码的高字节要加上地址的页偏移量(大于等于0A0H);考虑汉字与西文字符的区别,动态编码的低字节也需要加上一个大于或等于0A0H的偏移量。设某汉字在编码字典中的序号为Num,则该汉字的动态编码为:

动态编码高字节=页偏移量+Num/8

动态编码低字节=偏移量+(Num%8)×32 (1)

偏移量一般可设为0A0H。当单片机显示某个汉字时,只需将其动态编码的高字节送DPH,低字节减0A0H后送DPL,即可得到对应字模的地址指针。

(3)提取字模、建立动态字库

汉字机内码与点阵字库的详细关系可参考有关资料,它们存在如下联系:

字模首地址=((机内码高字节-1)×94+(机内码低字节-1))×N (2)

注:N为一个汉字点阵字模的字节数。

按照编码字典内容,根据字模首地址,依次取出汉字字模,顺序写入一个二进制文件,即建成动态字库(其它方法略),用烧录器写入EPROM,就可以使用了。

(4)编码改写

机内码是PC机识别处理汉字用的,单片机只能处理我们建立起来的动态编码,还得把程序中汉字的仅机码根据编码字典改成对应的动态编码才行。由于在编写源程序的文本编辑器中看到的是经过系统处理过的字节,看不到汉字的机内码,也无法对其进行改写。根据"汉字识别"一节所述,不经过文本编辑器,直接将动态编码(十六进制数)定改磁盘文件对应位置即可,但是处理过后的汉字在文本编辑器里会显示出乱码。

(5)汉字显示

在明白了动态编码与动态字库中字模的关系后,可以完成按照PC机下汉字显示原理进行单片机下的程序设计,编写前面的函数printhz()或子程序的DISPLAY,可参考相关资料[4]。

4 MCS51汉字显示例程

根据上述汉字动态编码方法,我们利用Borland
C++编写了PC机预处理程序,将ASM51或C51源程序用PC机预处理后,建立了动态字库和改写了机内码,并且用ASM51写了一个针对MCS51进行优化的子程序DIS_CHAR。它显示一个西文或中文字符,实现过程如图2所示。

西文字符码的显示与流字显示基本相同,将西文字库(仅数字和字符部分)装入程序存储器中,根据ASCII码的值计算出字模首地址,将字符字模依次读出,再送显示即可。

此方案不但可用于单片机系统中,还可应用于任何无中文系统支持的嵌入式系统中。根据这个思路还可设计出不同字体、点阵混合的字库,支持包含2万多个字符的新国标编码,甚至矢量字体在单片机系统中的应用也成为可能。由于技术水平有限,此方案还存在一些不足之处,如改写编码后源程序中汉字显示为乱码,不知道改码处理是否正确,操作比较繁琐。如果能采用插件技术实现此方案,编辑器中能正常显示汉字,而输出已经是改码后的程序文件,则能很好地解决上述不足。在这里,我们抛码引玉,希望有兴趣的朋友一起合作,实现单片机中文显示的广义开发平台。

动态编码预处理的C语言源程序(在BC++3.1下调试通过)见网站补充版(http://www.dpj.com.cn

网卡的组成工作原理

1.认识网卡,我们上网必备组件之一。
网卡工作在osi的最后两层,物理层和数据链路层,物理层定义了数据传送与接收所需要的电与光信号、线路状态、时钟基准、数据编码和电路等,并向数据链路层设备提供标准接口。物理层的芯片称之为PHY。数据链路层则提供寻址机构、数据帧的构建、数据差错检查、传送控制、向网络层提供标准的数据接口等功能。以太网卡中数据链路层的芯片称之为MAC控制器。很多网卡的这两个部分是做到一起的。他们之间的关系是pci总线接mac总线,mac接phy,phy接网线(当然也不是直接接上的,还有一个变压装置)。

下面继续让我们来关心一下PHY和MAC之间是如何传送数据和相互沟通的。通过IEEE定义的标准的MII/GigaMII(Media
Independed
Interfade,介质独立界面)界面连接MAC和PHY。这个界面是IEEE定义的。MII界面传递了网络的所有数据和数据的控制。
而MAC对PHY的工作状态的确定和对PHY的控制则是使用SMI(Serial Management
Interface)界面通过读写PHY的寄存器来完成的。PHY里面的部分寄存器也是IEEE定义的,这样PHY把自己的目前的状态反映到寄存器里面,MAC通过SMI总线不断的读取PHY的状态寄存器以得知目前PHY的状态,例如连接速度,双工的能力等。当然也可以通过SMI设置PHY的寄存器达到控制的目的,例如流控的打开关闭,自协商模式还是强制模式等。

我们看到了,不论是物理连接的MII界面和SMI总线还是PHY的状态寄存器和控制寄存器都是有IEEE的规范的,因此不同公司的MAC和PHY一样可以协调工作。当然为了配合不同公司的PHY的自己特有的一些功能,驱动需要做相应的修改。

一片网卡主要功能的实现就基本上是上面这些器件了。其他的,还有一颗EEPROM芯片,通常是一颗93C46。里面记录了网卡芯片的供应商ID、子系统供应商ID、网卡的MAC地址、网卡的一些配置,如SMI总线上PHY的地址,BOOTROM的容量,是否启用BOOTROM引导系统等东西。

很多网卡上还有BOOTROM这个东西。它是用于无盘工作站引导操作系统的。既然无盘,一些引导用必需用到的程序和协议栈就放到里面了,例如RPL、PXE等。实际上它就是一个标准的PCI
ROM。所以才会有一些硬盘写保护卡可以通过烧写网卡的BootRom来实现。其实PCI设备的ROM是可以放到主板BIOS里面的。启动电脑的时候一样可以检测到这个ROM并且正确识别它是什么设备的。AGP在配置上和PCI很多地方一样,所以很多显卡的BIOS也可以放到主板BIOS里面。这就是为什么板载的网卡我们从来没有看到过BOOTROM的原因。

2.工作过程

PHY在发送数据的时候,收到MAC过来的数据(对PHY来说,没有帧的概念,对它来说,都是数据而不管什么地址,数据还是CRC),每4bit就增加1bit的检错码,然后把并行数据转化为串行流数据,再按照物理层的编码规则(10Based-T的NRZ编码或100based-T的曼彻斯特编码)把数据编码,再变为模拟信号把数据送出去。收数据时的流程反之。现在来了解PHY的输出后面部分。一颗CMOS制程的芯片工作的时候产生的信号电平总是大于0V的(这取决于芯片的制程和设计需求),但是这样的信号送到100米甚至更长的地方会有很大的直流分量的损失。而且如果外部网现直接和芯片相连的话,电磁感应(打雷)和静电,很容易造成芯片的损坏。

再就是设备接地方法不同,电网环境不同会导致双方的0V电平不一致,这样信号从A传到B,由于A设备的0V电平和B点的0V电平不一样,这样会导致很大的电流从电势高的设备流向电势低的设备。我们如何解决这个问题呢?
这时就出现了Transformer(隔离变压器)这个器件。它把PHY送出来的差分信号用差模耦合的线圈耦合滤波以增强信号,并且通过电磁场的转换耦合到连接网线的另外一端。这样不但使网线和PHY之间没有物理上的连接而换传递了信号,隔断了信号中的直流分量,还可以在不同0V电平的设备中传送数据。

隔离变压器本身就是设计为耐2KV~3KV的电压的。也起到了防雷感应(我个人认为这里用防雷击不合适)保护的作用。有些朋友的网络设备在雷雨天气时容易被烧坏,大都是PCB设计不合理造成的,而且大都烧毁了设备的接口,很少有芯片被烧毁的,就是隔离变压器起到了保护作用。

发送数据时,网卡首先侦听介质上是否有载波(载波由电压指示),如果有,则认为其他站点正在传送信息,继续侦听介质。一旦通信介质在一定时间段内(称为帧间缝隙IFG=9.6微秒)是安静的,即没有被其他站点占用,则开始进行帧数据发送,同时继续侦听通信介质,以检测冲突。在发送数据期间,如果检测到冲突,则立即停止该次发送,并向介质发送一个"阻塞"信号,告知其他站点已经发生冲突,从而丢弃那些可能一直在接收的受到损坏的帧数据,并等待一段随机时间(CSMA/CD确定等待时间的算法是二进制指数退避算法)。在等待一段随机时间后,再进行新的发送。如果重传多次后(大于16次)仍发生冲突,就放弃发送。
接收时,网卡浏览介质上传输的每个帧,如果其长度小于64字节,则认为是冲突碎片。如果接收到的帧不是冲突碎片且目的地址是本地地址,则对帧进行完整性校验,如果帧长度大于1518字节(称为超长帧,可能由错误的LAN驱动程序或干扰造成)或未能通过CRC校验,则认为该帧发生了畸变。通过校验的帧被认为是有效的,网卡将它接收下来进行本地处理


网卡的原理及测试技术
网卡充当计算机和网络缆线之间的物理接口或连线将计算机中的数字信号转换成电或光信号,称为nic(
network interface card
)。数据在计算机总线中传输是并行方式即数据是肩并肩传输的,而在网络的物理缆线中说数据以串行的比特流方式传输的,网卡承担串行数据和并行数据间的转换。网卡在发送数据前要同接收网卡进行对话以确定最大可发送数据的大小、发送的数据量的大小、两次发送数据间的间隔、等待确认的时间、每个网卡在溢出前所能承受的最大数据量、数据传输的速度。
一、网卡的基本构造
网卡包括硬件和固件程序(只读存储器中的软件例程),该固件程序实现逻辑链路控制和媒体访问控制的功能网卡包括硬件和固件程序(只读存储器中的软件例程),该固件程序实现逻辑链路控制和媒体访问控制的功能,还记录唯一的硬件地址即mac地址,网卡上一般有缓存。网卡须分配中断irq及基本i/o端口地址,同时还须设置基本内存地址(base
memory address)和收发器(transceiver)
网卡的控制芯片
是网卡中最重要元件,是网卡的控制中心,有如电脑的cpu,控制着整个网卡的工作,负责数据的的传送和连接时的信号侦测。早期的10/100m的双速网卡会采用两个控制芯片(单元)分别用来控制两个不同速率环境下的运算,而目前较先进的产品通常只有一个芯片控制两种速度。
晶体震荡器
负责产生网卡所有芯片的运算时钟,其原理就象主板上的晶体震荡器一样,通常网卡是使用20或25hz的晶体震荡器。
boot rom插槽
如无特殊要求网卡中的这个插槽处在空置状态。一般是和boot
rom芯片搭配使用,其主要作用是引导电脑通过服务器引导进入win9x。
boot rom
就是启动芯片,让电脑可以在不具备硬盘、软驱和光驱的情况下,直接通过服务器开机,成为一个无硬盘无软驱的工作站。没有软驱就无法将资料输出,这样也可以达到资料保密的功能。同时,还可以节省下购买这些电脑部件的费用。在使用boot
rom时要注意自己使用何种网络操作系统,通常有boot rom for nt,boot rom for
unix,boot rom for netware等,boot rom启动芯片要自行购买。
eprom
从前的老式网卡都要靠设置跳线或是dip开关来设定irq、dma和i/o
port等值,而现在的网卡则都使用软件设定,几乎看不见跳线的存在。各种网卡的状态和网卡的信息等数据都存在这颗小小的eeprom里,通过它来自动设置。
内接式转换器
只要有bnc接头的网卡都会有这个芯片,并紧邻在bnc接头旁,它的功能是在网卡和bnc接头之间进行数据转换,让网卡能通过它从bnc接头送出或接收资料。
rj-45和bnc接头
rj-45是采用双绞线作为传输媒介的一种网卡接口,在100mbps网中最常应用。bnc是采用细同轴电缆作为传输媒介
信号指示灯
在网卡后方会有二到三个不等的信号灯,其作用是显示目前网络的连线状态,通常具有tx和rx两个信息。tx代表正在送出资料,rx代表正在接收资料,若看到两个灯同时亮则代表目前是处于全双工的运作状态,也可由此来辨别全双工的网卡是否处于全双工的网络环境中(见上图两个接口的中间部分)。也有部分低速网卡只用一个灯来表示信号,通过不同的灯光变换来表示网络是否导通。
二、网卡的分类
以频宽区分网卡种类
目前的以太网卡分为10mbps、100mbps和1000
mbps三种频宽,目前常见的三种架构有10baset、100basetx与base2,前两者是以rj-45双绞线为传输媒介,频宽分别有10mbps和100mbps。而双绞线又分为category
1至category
5五种规格,分别有不同的用途以及频宽,category通常简称cat,只要使用cat5规格的双绞线皆可用于10/100mbps频宽的网卡上。而10base2架构则是使用细同轴电缆作为传输媒介,频宽只有10mbps。这里提到的频宽10或100mbps是指网卡上的最大传送频宽,而频宽并不等于网络上实际的传送速度,实际速度要考虑到传送的距离,线路的品质,和网络上是否拥挤等因素,这里所谈的bps指的是每秒传送的bit(1个byte=8个bit)。而100mbps则称为高速以太网卡(fast
ethernet),多为pci接口。因为其速度快,目前新建的局域网络绝已大多数已采用100mbps的传输频宽,已有渐渐取代10mbps网卡的趋势。当前市面上的pci网卡多具有10/100mbps自动切换的功能,会根据所在的网络连线环境来自动调节网络速度。1000
mbps以太网卡多用于交换机或交换机与服务器之间的高速链路或backbone。
以接口类型区分网卡种类
以接口类型来分,网卡目前使用较普遍的是isa接口、pci接口、usb接口和笔记本电脑专用的pcmcia接口。现在的isa接口的网卡均采用16bit的总线宽度,其特性是采用programmed
i/o的模式传送资料,传送数据时必须通过cpu在i/o上开出一个小窗口,作为网卡与pc之间的沟通管道,需要占用较高的cpu使用率,在传送大量数据时效率较差。pci接口的网卡则采用32bit的总线频宽,采用bus
master的数据传送方式,传送数据是由网卡上的控制芯片来控制,不必通过i/o端口和cpu,可大幅降低cpu的占用率,目前产品多为10/100mbps双速自动侦测切换网卡。
以全双工/半双工来区分网卡种类
网络有半双工(half duplex)与全双工(full
duplex)之分,半双工网卡无法同一时间内完成接收与传送数据的动作,如10base2使用细同轴电缆的网络架构就是半双工网络,同一时间内只能进行传送或接收数据的工作,效率较低。要使用全双工的网络就必须要使用双绞线作为传输线才能达到,并且也要搭配使用全双工的集线器,要使用10base或100basetx的网络架构,网卡当然也要是全双工的产品
以网络物理缆线接头区分网卡
目前网卡常用的网线接头有rj-45与bnc两种,有的网卡同时具有两种接头,可适用于两种网络线,但无法两个接头同时使用。另外还有光纤接口的网卡,通常带宽在1000
mbps。
其他功能wol
有些网卡会有wol的功能,wol网络开机的功能(wake on
lan)。它可由另外一台电脑,使用软件制作特殊格式的信息包发送至一台装有具wol功能网卡的电脑,而该网卡接收到这些特殊格式的信息包后,就会命令电脑打开电源,目前已有越来越多的网卡支持网络开机的功能。
其它网卡
从网络传输的物理媒介上还有无线网卡,利用2.4ghz的无线电波来传输数据。目前ieee有两种规范802.11和802.11b,最高传输速率分别为2m和11m,接口有pci、usb和pcmcia几种。
三、网卡测试技术
基于操作系统的测试
网卡一个重要的性能是看其是否支持多种网络操作系统,比较流行的网络操作系统有windowsnt、unix(linux、freebsd、sco、solaris、hp厎)、novell、dec等。同时网卡应能够支持多种的网络协议,如tcp/ip、ipx/spx、apple、netbeui等。
基于主机的兼容性测试
硬件上的兼容性也是非常重要的一个方面,尤其在笔记本电脑上兼容性问题比较突出,根据本人的实际经验,甚至某些名牌的网卡在一些笔记本电脑上也存在较为严重的兼容性问题。在服务器或台式电脑方面这些问题不常出现。
网卡传输速率测试(数据吞吐量)
测试网卡的传输速率一般有硬件和软件两种方法,硬件是利用一些专用的仪器如网络分析仪、smartbits
smartcards等其他一些设备,利用icmp
echo请求和udp数据包来检测数据流量。通常测试的项目有以下几方面:
autonegotiation test
测试网卡速率、全双工/半双工和流控协商。协商决定着是否通过"暂停桢pause
frame"来允许流量控制。
arp test
测试网卡是否能对arp请求做出正确回应及是否在规定时间内应答。这个时间由测试者进行设置。
error test
测试网卡处理错误frame的能力,通常在较低的传输速率下进行此项测试(0.5%传输速率),有以下几个方面的测试:
网卡接收正确的frame,作出处理。
网卡接收到存在crc校验错的frame,网卡将其丢弃。
网卡接收到传输顺序错误的frame,网卡将其丢弃。
网卡接收到含有少量错误bits的frame,网卡应全部接收并处理。
网卡接收到超小frame,网卡应将其丢弃。
网卡接收到超长frame,网卡应将其丢弃。
packets loss test
rfc规定测试网卡在各种传输带宽利用率下的处理frame的能力,从初始化数据传输到传输速率的不断变化一直到传输结束,检查frame的丢失情况。
throughput test
数据吞吐量的测试也是rfc规定的一项测试内容,测试的结果反映出传输的最大带宽的利用率,每秒处理的frame和每秒处理的bits数量。
back-to-back test
同样此项测试也为rfc-2544的规定,测试在一个设定的最大传输速率下网卡可处理的并发frame的数量。最终反映出在不丢失数据包的情况下可并发传输的最大frame数量。
利用软件测试通常是利用zd的netbench来测试,一般只利用其测试网卡的最大传输速率。测试时要组成一个网络结构,一台windowsnt
server服务器,若干个windows9x或windowsnt
station客户端,传输大容量的文件如100mbps,测试的结果将反映出网卡的最大传输速率。另一个测试项目是测试网卡对较小的数据包请求的回应能力,这里有必要讨论一下tcp/ip的ping命令的机制。ping是利用发送和接收icmp
echo报文,来检测链路状态和协议设置。数据链路层封装的是frame,大小在64k~1518k之间,当发送frame时,网卡接受到frame时首先要读取桢头和桢尾的mac地址,当mac地址相匹配时再接封装读取ip地址。当网卡连续接收到frame时,要对每一个frame做出处理,当网卡或是系统无法处理这些数据包时,这些数据包将被丢弃。这种情况多发生在连续发送非常小的frame时。ping的机制是发送一个icmp报文,接收到一个icmp
echo后再发送下一个icmp报文。所以较小的连续的frame会对网卡和系统造成较大的压力。在netbench中,有一项测试就是测试网卡或系统对连续的小数据包的处理能力。
稳定性测试
一块好的网卡应该具有良好的稳定性,具体讲就是在不同的工作环境下和不同的工况下应具有稳定的表现。通常测试主要是高温和传输大文件测试。
高温测试一般是在30~35摄氏度下连续运行网卡的测试程序达一定的时间比如2小时以上,检测网卡高温下的稳定性。pcmcia接口的网卡一般有两种32位的和16位的,前者又称为cardbus网卡,数据带宽由16位增加到32位,使得pcmcia的网卡发热量成为一个显著的问题。
另一个测试是传输大的文件,某些品质较差的网卡在传输大容量的文件比如2gbps以上的文件时容易出错。
综上所述,在测试一块网卡时要进行全面的软、硬件及兼容性测试,可根据具体的应用和不同的要求,有机的选择测试项目,正确反映网卡的性能指标。

认识网卡——网络设备设计专家解密网络传输

作者简介:Ase,VIA网络研发工程师,VNT via networking technologies

AE/FAE。参与过100m/1000m网卡,usb2.0卡,1394卡,pcmcia的usb和1394卡,主板,带管理交换机,显卡等的设计工作。
一台冰冷的电脑,软件给了她血肉,网络就给了她灵魂!
一、什么是网卡?
网卡现在已经上成为了目前电脑里的标准配置之一。小小的网卡,究竟蕴涵着多少秘密呢?让我们一起来看。
我们最常用的网络设备当属网卡了。网卡本身是LAN(局域网)的设备,通过网关、路由器等设备就可以把这个局域网挂接到Internet上。而Internet本身就是无数个这样的局域网组成的。
网卡有许多种,按照数据链路层控制来分有以太网卡,令牌环网卡,ATM网卡等;按照物理层来分类有无线网卡,RJ-45网卡,同轴电缆网卡,光线网卡等等。它们的数据链路控制、寻址、帧结构等不同;物理上的连接方式不同、数据的编码、信号传输的介质、电平等不同。以下主要介绍我们最常用到的以太网网卡。
以太网采用的CSMA/CD(载波侦听多路访问/冲突检测)的控制技术。他主要定义了物理层和数据链路层的工作方式。数据链路层和物理层各自实现自己的功能,相互之间不关心对方如何操作。二者之间有标准的接口(例如MII,GMII等)来传递数据和控制。
以太网卡的物理层可以包含很多种技术,常见的有RJ45,光线,无线等,它们的区别在于传送信号的物理介质和媒质不同。这些都在IEEE的802协议族中有详细的定义。
这次我们主要讨论的RJ45的网卡属于IEEE802.3定义的范围。
二、网卡的组成
1.网卡的基本结构
一块以太网网卡包括OSI(开方系统互联)模型的两个层。物理层和数据链路层。物理层定义了数据传送与接收所需要的电与光信号、线路状态、时钟基准、数据编码和电路等,并向数据链路层设备提供标准接口。数据链路层则提供寻址机构、数据帧的构建、数据差错检查、传送控制、向网络层提供标准的数据接口等功能。
以太网卡中数据链路层的芯片一般简称之为MAC控制器,物理层的芯片我们简称之为PHY。许多网卡的芯片把MAC和PHY的功能做到了一颗芯片中,比如Intel
82559网卡的和3COM
3C905网卡。但是MAC和PHY的机制还是单独存在的,只是外观的表现形式是一颗单芯片。当然也有很多网卡的MAC和PHY是分开做的,比如D-LINK的DFE-530TX等。
  

  图一:MAC和PHY分开的以太网卡
  
  图二:MAC和PHY集成在一颗芯片的以太网卡
①RJ-45接口 ②Transformer(隔离变压器) ③PHY芯片
④MAC芯片 ⑤EEPROM ⑥BOOTROM插槽
⑦WOL接头 ⑧晶振 ⑨电压转换芯片
⑩LED指示灯
2.什么是MAC?
首先我们来说说以太网卡的MAC芯片的功能。以太网数据链路层其实包含MAC(介质访问控制)子层和LLC(逻辑链路控制)子层。一块以太网卡MAC芯片的作用不但要实现MAC子层和LLC子层的功能,还要提供符合规范的PCI界面以实现和主机的数据交换。
MAC从PCI总线收到IP数据包(或者其他网络层协议的数据包)后,将之拆分并重新打包成最大1518Byte,最小64Byte的帧。这个帧里面包括了目标MAC地址、自己的源MAC地址和数据包里面的协议类型(比如IP数据包的类型用80表示)。最后还有一个DWORD(4Byte)的CRC码。
可是目标的MAC地址是哪里来的呢?这牵扯到一个ARP协议(介乎于网络层和数据链路层的一个协议)。第一次传送某个目的IP地址的数据的时候,先会发出一个ARP包,其MAC的目标地址是广播地址,里面说到:"谁是xxx.xxx.xxx.xxx这个IP地址的主人?"因为是广播包,所有这个局域网的主机都收到了这个ARP请求。收到请求的主机将这个IP地址和自己的相比较,如果不相同就不予理会,如果相同就发出ARP响应包。这个IP地址的主机收到这个ARP请求包后回复的ARP响应里说到:"我是这个IP地址的主人"。这个包里面就包括了他的MAC地址。以后的给这个IP地址的帧的目标MAC地址就被确定了。(其它的协议如IPX/SPX也有相应的协议完成这些操作。)
IP地址和MAC地址之间的关联关系保存在主机系统里面,叫做ARP表,由驱动程序和操作系统完成。在Microsoft的系统里面可以用arp -a的命令查看ARP表。收到数据帧的时候也是一样,做完CRC以后,如果没有CRC效验错误,就把帧头去掉,把数据包拿出来通过标准的借口传递给驱动和上层的协议客栈,最终正确的达到我们的应用程序。
还有一些控制帧,例如流控帧也需要MAC直接识别并执行相应的行为。
以太网MAC芯片的一端接计算机PCI总线,另外一端就接到PHY芯片上。以太网的物理层又包括MII/GMII(介质独立接口)子层、PCS(物理编码子层)、PMA(物理介质附加)子层、PMD(物理介质相关)子层、MDI子层。而PHY芯片是实现物理层的重要功能器件之一,实现了前面物理层的所有的子层的功能。
3.网络传输的流程
PHY在发送数据的时候,收到MAC过来的数据(对PHY来说,没有帧的概念,对它来说,都是数据而不管什么地址,数据还是CRC),每4bit就增加1bit的检错码,然后把并行数据转化为串行流数据,再按照物理层的编码规则(10Based-T的NRZ编码或100based-T的曼彻斯特编码)把数据编码,再变为模拟信号把数据送出去。(注:关于网线上数据是数字的还是模拟的比较不容易理解清楚。最后我再说)
收数据时的流程反之。
PHY还有个重要的功能就是实现CSMA/CD的部分功能。它可以检测到网络上是否有数据在传送,如果有数据在传送中就等待,一旦检测到网络空闲,再等待一个随机时间后将送数据出去。如果两块网卡碰巧同时送出了数据,那样必将造成冲突,这时候,冲突检测机构可以检测到冲突,然后各等待一个随机的时间重新发送数据。
这个随机时间很有讲究的,并不是一个常数,在不同的时刻计算出来的随机时间都是不同的,而且有多重算法来应付出现概率很低的同两台主机之间的第二次冲突。
许多网友在接入Internt宽带时,喜欢使用"抢线"强的网卡,就是因为不同的PHY碰撞后计算随机时间的方法设计上不同,使得有些网卡比较"占便宜"。不过,抢线只对广播域的网络而言的,对于交换网络和ADSL这样点到点连接到局端设备的接入方式没什么意义。而且"抢线"也只是相对而言的,不会有质的变化。
4.关于网络间的冲突
现在交换机的普及使得交换网络的普及,使得冲突域网络少了很多,极大地提高了网络的带宽。但是如果用HUB,或者共享带宽接入Internet的时候还是属于冲突域网络,有冲突碰撞的。交换机和HUB最大的区别就是:一个是构建点到点网络的局域网交换设备,一个是构建冲突域网络的局域网互连设备。
我们的PHY还提供了和对端设备连接的重要功能并通过LED灯显示出自己目前的连接的状态和工作状态让我们知道。当我们给网卡接入网线的时候,PHY不断发出的脉冲信号检测到对端有设备,它们通过标准的"语言"交流,互相协商并却定连接速度、双工模式、是否采用流控等。
通常情况下,协商的结果是两个设备中能同时支持的最大速度和最好的双工模式。这个技术被称为Auto
Negotiation或者NWAY,它们是一个意思--自动协商。
5.PHY的输出部分
现在来了解PHY的输出后面部分。一颗CMOS制程的芯片工作的时候产生的信号电平总是大于0V的(这取决于芯片的制程和设计需求),但是这样的信号送到100米甚至更长的地方会有很大的直流分量的损失。而且如果外部网现直接和芯片相连的话,电磁感应(打雷)和静电,很容易造成芯片的损坏。
再就是设备接地方法不同,电网环境不同会导致双方的0V电平不一致,这样信号从A传到B,由于A设备的0V电平和B点的0V电平不一样,这样会导致很大的电流从电势高的设备流向电势低的设备。我们如何解决这个问题呢?
这时就出现了Transformer(隔离变压器)这个器件。它把PHY送出来的差分信号用差模耦合的线圈耦合滤波以增强信号,并且通过电磁场的转换耦合到连接网线的另外一端。这样不但使网线和PHY之间没有物理上的连接而换传递了信号,隔断了信号中的直流分量,还可以在不同0V电平的设备中传送数据。
隔离变压器本身就是设计为耐2KV~3KV的电压的。也起到了防雷感应(我个人认为这里用防雷击不合适)保护的作用。有些朋友的网络设备在雷雨天气时容易被烧坏,大都是PCB设计不合理造成的,而且大都烧毁了设备的接口,很少有芯片被烧毁的,就是隔离变压器起到了保护作用。
6.关于传输介质
隔离变压器本身是个被动元件,只是把PHY的信号耦合了到网线上,并没有起到功率放大的作用。那么一张网卡信号的传输的最长距离是谁决定的呢?
一张网卡的传输最大距离和与对端设备连接的兼容性主要是PHY决定的。但是可以将信号送的超过100米的PHY其输出的功率也比较大,更容易产生EMI的问题。这时候就需要合适的Transformer与之配合。作PHY的老大公司Marvell的PHY,常常可以传送180~200米的距离,远远超过IEEE的100米的标准。
RJ-45的接头实现了网卡和网线的连接。它里面有8个铜片可以和网线中的4对双绞(8根)线对应连接。其中100M的网络中1、2是传送数据的,3、6是接收数据的。1、2之间是一对差分信号,也就是说它们的波形一样,但是相位相差180度,同一时刻的电压幅度互为正负。这样的信号可以传递的更远,抗干扰能力强。同样的,3、6也一样是差分信号。
网线中的8根线,每两根扭在一起成为一对。我们制作网线的时候,一定要注意要让1、2在其中的一对,3、6在一对。否则长距离情况下使用这根网线的时候会导致无法连接或连接很不稳定。
现在新的PHY支持AUTO
MDI-X功能(也需要Transformer支持)。它可以实现RJ-45接口的1、2上的传送信号线和3、6上的接收信号线的功能自动互相交换。有的PHY甚至支持一对线中的正信号和负信号的功能自动交换。这样我们就不必为了到底连接某个设备需要使用直通网线还是交叉网线而费心了。这项技术已经被广泛的应用在交换机和SOHO路由器上。
在1000Basd-T网络中,其中最普遍的一种传输方式是使用网线中所有的4对双绞线,其中增加了4、5和7、8来共同传送接收数据。由于1000Based-T网络的规范包含了AUTO
MDI-X功能,因此不能严格确定它们的传出或接收的关系,要看双方的具体的协商结果。
7.PHY和MAC之间如何进行沟通
下面继续让我们来关心一下PHY和MAC之间是如何传送数据和相互沟通的。通过IEEE定义的标准的MII/GigaMII(Media
Independed
Interfade,介质独立界面)界面连接MAC和PHY。这个界面是IEEE定义的。MII界面传递了网络的所有数据和数据的控制。
而MAC对PHY的工作状态的确定和对PHY的控制则是使用SMI(Serial Management
Interface)界面通过读写PHY的寄存器来完成的。PHY里面的部分寄存器也是IEEE定义的,这样PHY把自己的目前的状态反映到寄存器里面,MAC通过SMI总线不断的读取PHY的状态寄存器以得知目前PHY的状态,例如连接速度,双工的能力等。当然也可以通过SMI设置PHY的寄存器达到控制的目的,例如流控的打开关闭,自协商模式还是强制模式等。
我们看到了,不论是物理连接的MII界面和SMI总线还是PHY的状态寄存器和控制寄存器都是有IEEE的规范的,因此不同公司的MAC和PHY一样可以协调工作。当然为了配合不同公司的PHY的自己特有的一些功能,驱动需要做相应的修改。
一片网卡主要功能的实现就基本上是上面这些器件了。
其他的,还有一颗EEPROM芯片,通常是一颗93C46。里面记录了网卡芯片的供应商ID、子系统供应商ID、网卡的MAC地址、网卡的一些配置,如SMI总线上PHY的地址,BOOTROM的容量,是否启用BOOTROM引导系统等东西。
很多网卡上还有BOOTROM这个东西。它是用于无盘工作站引导操作系统的。既然无盘,一些引导用必需用到的程序和协议栈就放到里面了,例如RPL、PXE等。实际上它就是一个标准的PCI
ROM。所以才会有一些硬盘写保护卡可以通过烧写网卡的BootRom来实现。其实PCI设备的ROM是可以放到主板BIOS里面的。启动电脑的时候一样可以检测到这个ROM并且正确识别它是什么设备的。AGP在配置上和PCI很多地方一样,所以很多显卡的BIOS也可以放到主板BIOS里面。这就是为什么板载的网卡我们从来没有看到过BOOTROM的原因。
8.网卡的供电
最后就是电源部分了。大多数网卡现在都使用3.3V或更低的电压。有的是双电压的。因此需要电源转换电路。
而且网卡为了实现Wake on
line功能,必须保证全部的PHY和MAC的极少一部分始终处于有电的状态,这需要把主板上的5V
Standby电压转换为PHY工作电压的电路。在主机开机后,PHY的工作电压应该被从5V转出来的电压替代以节省5V
Standby的消耗。(许多劣质网卡没有这么做)。
有Wake on
line功能的网卡一般还有一个WOL的接口。那是因为PCI2.1以前没有PCI设备唤醒主机的功能,所以需要着一根线通过主板上的WOL的接口连到南桥里面以实现WOL的功能。
新的主板合网卡一般支持PCI2.2/2.3,扩展了PME#信号功能,不需要那个接口而通过PCI总线就可以实现唤醒功能。
结语
一块以太网卡就是这些部分组成。它们紧密地配合并且相互协调,供给我们一个稳定而告诉的网络接入。网络的普及不但极大地增加了工作效率,而且使我们可以自由的驰骋在Internet的海洋中!
注解:
网线上的到底是模拟信号还是数字信号呢?
答案是模拟信号。因为它传出和接收是采用的模拟的技术。虽然它传送的信息是数字的,并不是传送的信息是数字的,信号就可以叫做数字信号。
简单的例子:我们知道电话是模拟信号,但是当我们拨号上网的时候,电话线里传送的是数字信息,但信号本身依旧是模拟的。然而ADSL同样是通过电话线传送的,却是数字信号。这取决于它传出和接受采用的技术。

如果判断一个字符是西文字符还是中文字符

一、如果判断一个字符是西文字符还是中文字符
大家知道西文字符主要是指ASCII码,它用一个字节表示。且这个字符转换成数字之后,该数字是大于0的,而汉字是两个字节的,第一个字节的转化为数字之后应该是小于0的,因此可以根据每个字节转化为数字之后是否小于0,判断它是否是汉字。
例如,设输入字为strin,则,
If (strin.at(0) < 0)
cout << "是汉字" << endl;
else cout << "不是汉字" << endl;

二、C++ 中判断字符中文编码
直接根据汉字的编码范围判断,对于GB2312和GBK可用下面两个程序实现。
1、判断是否是GB2312
bool isGBCode(const string& strIn)
{
unsigned char ch1;
unsigned char ch2;

if (strIn.size() >= 2)
{
ch1 = (unsigned char)strIn.at(0);
ch2 = (unsigned char)strIn.at(1);
if (ch1>=176 && ch1<=247 && ch2>=160 && ch2<=254)
return true;
else return false;
}
else return false;
}
2、判断是否是GBK编码
bool isGBKCode(const string& strIn)
{
unsigned char ch1;
unsigned char ch2;
if (strIn.size() >= 2)
{
ch1 = (unsigned char)strIn.at(0);
ch2 = (unsigned char)strIn.at(1);
if (ch1>=129 && ch1<=254 && ch2>=64 && ch2<=254)
return true;
else return false;
}
else return false;
}
3、判断是否为Big5
它的范围为:高字节从0xA0到0xFE,低字节从0x40到0x7E,和0xA1到0xFE两部分。判断一个汉字是否是BIG5编码,可以如上对字符的编码范围判断即可。

三、字符定位
1. big5
如何定位呢?那么也想象所有编码排列为一个二维坐标,纵坐标是高字节,横坐标是低字节。这样一行上的汉字个数:(0x7E-0x40+
1)+(0xFE-0xA1+1)=157。那么定位算法分两块,为:
if 0x40<=ch2<=0x7E: #is big5 char
index=((ch1-0xA1)*157+(ch2-0x40))*2
elif 0xA1<=ch2<=0xFE: #is big5 char
index=((ch1-0xA1)*157+(ch2-0xA1+63))*2
对于第二块,计算偏移量时因为有两块数值,所以在计算后面一段值时,不要忘了前面还有一段值。0x7E-0x40+1=63。
2.其他应该类似.
可以如下:对汉字进行hash
为了处理汉字的方便,在查找汉字的时候,我们通常会用到hash的方法,那怎么来确定一个汉字位置呢?这就和每种编码的排列有关了,这里主要给出一种hash函数的策略。
(1) GB2312编码
对于GB2312编码,设输入的汉字为GBword
(std::string),我们可以采用公式(C1-176)*94 +
(C2-161)确定GBindex。其中,C1表示第一字节,C2表示第二字节。具体如下:
GBindex = ((unsigned char)GBword.at(0)-176)*94 + (unsigned
char)GBword.at(1) - 161;
之所以用unsigned char类型,是因为char是一个字节,如果用unsigend
int,因为int是4个字节的,所以会造成扩展,导致错误。
(2) GBK编码
对于GBK编码,设输入的汉字为GBKword,则可以采用公式
index=(ch1-0x81)*190+(ch2-0x40)-(ch2/128),其中ch1是第一字节,ch2是第二字节。
具体的,
GBKindex = ((unsigned char)GBKword[0]-129)*190 +
((unsigned char)GBKword[1]-64) - (unsigned
char)GBKword[1]/128;
[参考] 字符集编码详解
http://www.cppblog.com/humanchao/archive/2007/09/27/32989.html
[参考]判断字符编码以及转码的一个工具类
http://hi.baidu.com/pazhu/blog/item/efcce7a2034ae9a8caefd05b.html

32位DSP两级cache的结构设计

随着半导体技术的发展,DSP性能不断提高,被广泛应用在控制,通信,家电等领域中。
  DSP内部核心部件ALU具有极高的处理速度,而外部存储器的速度相对较低,存储系统已成为制约DSP发展的一个瓶颈。本文参照计算机存储结构,利用虚拟存储技术,对存储系统的结构进行了改进。在DSP中引入二级Cache存储器结构,在较小的硬件开销下提高了DSP的工作速度。结合高性能低功耗DSP cache设计这个项目,对两级cache的结构和算法做了探讨。
  2 cache总体设计

  传统的存储器主要由Dram组成,它的工作速度较慢,cache存储器主要由SRAM组成。在DSP中,存储系统可分层设计,将之分为两部分:容量较小的cache存储器和容量较大的主存储器,cache中存放着和主存中一致的较常用的指令与数据。DSP执行操作时可先向速度较快

  图1 cache的结构及互连简图

  的cache取指令或数据,如果不命中则再从主存取指令或数据。通过提高cache的命中率可以大大加快DSP的整体运行速度,从而缓解由存储系统引起的瓶颈问题。
  基于上述原理,我们设计了DSP的cache总体结构,如图1所示。图中设计采用了两级cache设计,第一级cache采用分立结构,将指令cache和数据cache分开设计,这样CPU可以对数据和指令进行平行操作,结合DSP取址,译码,读数,执行的四级流水线结构,充分提高系统效率。二级cache采用统一结构,数据和指令共用一个cache,此时可以根据程序执行的具体情况,二级cache自动平衡指令和数据间的负载,从而提高命中率。DSP若在一级cache中未找到需要的指令和数据,则可在二级cache中寻找。此结构下,一级cache找不到的数据和指令多数可在二级cache中找到,提高了整个cache系统的命中率。
  增加一级cache的容量可提高命中率,但随着cache容量增大,电路结构将变得复杂,所用的芯片面积、功耗也会加大,而且cache的访问时间也会变长,从而影响到ALU的速度。综合考虑速度,面积,功耗等因素,我们把一级指令cache和数据cache的容量均定为4KB。
  二级cache处于一级cache和主存储器之间,访问时间是3到4个ALU时钟周期,其容量一般是为一级cache的4到8倍。设计中我们将二级cache的容量为定位32KB。
  3 cache的映射方式与地址结构
  cache采用的映射方式通常有直接映射、关联映射、组关联映射三种,直接映射命中率低,容易发生抖动,关联映射虽然命中率较高,但电路复杂,权衡电路复杂性和命中率,我们主要采用组关联映射方法。在组关联映射中,可将主存空间分成块,cache空间分为组,一组包含多行,行的大小与块的大小相等。主存中的特定块只能映射到cache中的特定组,但可以映射到组内的不同行。若用j表示主存的块号,i表示cache中的组号,m表示cache的总行数,当cache分为v个组,每组k个行时,存在以下关系(见公式1、2),
  此种映射方式通常称为k路组关联映射。利用公式(2),我们可以根据块的物理地址计算它能映射到的组号,块j 能被映射到相应组中k行的任何一行中。

  设计中二级cache采用4路组相联的结构,分为共256组,每组4行,每行8个32位单元,总容量位32KB。cache的控制逻辑将存储器地址简单的分为三个域:标记域,组号和字。为了降低系统的功耗,采用了标记(tag)和数据体相分离的方案。为了加快访问速度,把cache中行号相同的块放在一个数据体中实现。这样cache就可分为4个标记存储器,4个数据存储器。每个标记存储器可放256个标记,每个数据存储体有256行数据。地址的划分如图2,tag的结构见图3。

  图2 二级 cache的地址划分

  图3 L2 cache tag 的组成

  一级指令cache和数据cache采用组关联的结构,均分为32个组,每组4行,每行含有8个32位的单元,每个容量位4KB。一级cache的组和行与二级cache的组和行大小对应,在二级cache到指令cache和数据cache间,组之间我们采用直接映射的方式,组内用全关联方式。这样我们结合了组关联的灵活与全关联的命中率高的优点。

  和二级cache相似,也把每组块号相同的数据放在同一个数据体中,共分为4个标记存储器,四个数据体存储器。每个标记存储器可放32个标记,每个数据存储体有32行数据。对主存地址的划分如图4。

  图4 一级cache的地址划分

  tag的结构见图5。

  图5 一级cache tag结构

  其中,P位是数据存在位, M位是数据修改的标记位,用于写策略的实现。
  4 写策略及cache替换算法
  写策略通常采用写回或写直达,采用写回法时,仅当cache中的某行数据被替换时,才更新存储器中相应数据。采用写直达法时,则每次写操作都要同时更新cache和主存储器中的数据。
  所针对的DSP处于单处理器工作模式下,考虑到整个系统的数据处理效率,设计时我们采用写回法更新数据。写回法中,如果一级cache中的数据发生改变而未立即写回L2 cache和主存储器,或者L2 cache中的数据发生改变,未立即写回主存储器,那么就会造成数据不一致而导致错误。为保证数据的一致性,在驻留于cache中的某一块被替换之前,必须考虑它是否在cache中被修改。如果没有修改,则cache中原来的块就可以直接被替换掉,而不需回写;如果修改过,则意味着对cache这一行至少执行过一次写操作,那么在替换之前主存储器中的数据也必须随之做相应修改。为此我们在cache的tag中设置了修改位M,在执行回写操作前我们均对修改位进行判断,其值为1时表示数据被修改过,需回写,为0则表示未修改,不进行回写。
  Cache的替换算法有很多种 ,为了提高命中率,在设计时采用了优化的LRU算法:栈链法[6]。栈链法的管理规则如下:
  1) 把本次访问的块号与栈中保存的所有块号进行比较。如果发现有相等的,则cache命中,本次访问的块号从栈顶压入,栈内各单元的块号依次往下移,直至与本次访问的块号相等的那个单元为止,再往下的单元直至栈底都不改变。
  2)如果相联比较没有发现相等的,则cache失效。栈底单元中的块号就是要被替换的块号。
  实现时采用四个存储单元,每个单元两位,用来保存当前cache组的四个块号。首先是相联比较,以组号为地址,从四个标记寄存器中读取数据,和地址进行比较,然后就可以产生命中与否的信号,以及命中时相应的块号。

  5 如何根据地址在cache中找到所需要的数据

  图6 I cache查找数据的过程

  能够映射到cache中某一行的数据很多,那么是怎样在cache中找到所需要的数据呢?主要是借助于标记。以 I cache 为例,当CPU发出读信号时,则首先以组号PA[7:3]为地址,从I cache的四组标记寄存器中读取标记,送往对应的比较器,和地址信号PA[31:8]进行比较,如果比较相等,且存在位有效,则表示命中。HIT1表示第1组命中,依次类推。HIT1 ,HIT2,HIT3,HIT4经过或门以后,就是总体命中与否的输出信号。如果HIT1有效,以PA[7:0]对cache的数据体1进行寻址,读取相应的数据。其它情况类似。在这个过程中,可以看出,地址和数据之间的一一对应关系。
  6 数据块传输
  数据块传输是对存储器的一种重要操作,根据译码电路的层次性,知道如果只是地址的低位发生改变,译码电路很快就可以达到稳定状态,选择对应的单元,进行读写。因此对数据进行整组传输,有利于提高传输的效率。在该cache中,对存储器的访问都是定长的,如果产生不命中的信号,则立即产生8拍定长的读写信号。具体实现时,设计了一个控制块传输信号的模块。每当产生不命中的信号,则把块传输的初始地址读入到该模块的初始地址寄存器,设置相应的传输单元数为8,以及对应的cache单元的读写信号。在每个时钟的上升沿,地址寄存器增1,传输单元个数寄存器减1,当传输单元个数寄存器的数据为0时,就结束传输。
  由于L2 cache是个单端口的存储器,一级cache采用哈佛结构,对数据和指令同时进行操作,当D cache和I cache失效时,都会访问L2 cache,这样就有可能产生冲突。为了解决这个问题,在块传输控制的模块中,设置了一位busy位,用来标志总线忙状态。当某个请求得到响应,其余的请求只有进入等待状态。在设计时,制定了访问L2 cache的优先级协议:读指令不命中的优先级最高,写数据不命中的优先级次之,读数据不命中的优先级最低。当I cache和D cache同时产生不命中的信号时,根据优先级协议来访问L2 cache。
  7 结束语
  在命中率方面,采用两级cache结构及组关联映射方法提高了cache系统的命中率。在数据处理效率方面,由于一级cache采用哈佛结构,指令和数据可并行操作,显着提高了系统的数据处理能力。在功耗方面,采用了数据体和标记相分离的措施,这使得只有在cache命中的情况下,才会访问数据体,可降低系统的功耗。
  整个设计采用自顶向下的设计流程,用Verilog语言描述整个系统,在synopsys工具下进行仿真和综合。在综合的结果中,指令cache的延迟最长,为4.3ns.整个cache系统的等效门数约24万个门。

嵌入式开发—C语言面试题

1. 用预处理指令#define 声明一个常数,用以表明1年中有多少秒(忽略闰年问题)
#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL
我在这想看到几件事情:
1). #define 语法的基本知识(例如:不能以分号结束,括号的使用,等等)
2).
懂得预处理器将为你计算常数表达式的值,因此,直接写出你是如何计算一年中有多少秒而不是计算出实际的值,是更清晰而没有代价的。
3).
意识到这个表达式将使一个16位机的整型数溢出-因此要用到长整型符号L,告诉编译器这个常数是的长整型数。
4).
如果你在你的表达式中用到UL(表示无符号长整型),那么你有了一个好的起点。记住,第一印象很重要。

2. 写一个"标准"宏MIN,这个宏输入两个参数并返回较小的一个。
#define MIN(A,B) ((A) <= (B) (A) : ))

这个测试是为下面的目的而设的:

1).
标识#define在宏中应用的基本知识。这是很重要的,因为直到嵌入(inline)操作符变为标准C的一部分,宏是方便产生嵌入代码的唯一方法,对于嵌入式系统来说,为了能达到要求的性能,嵌入代码经常是必须的方法。
2).
三重条件操作符的知识。这个操作符存在C语言中的原因是它使得编译器能产生比if-then-else更优化的代码,了解这个用法是很重要的。
3). 懂得在宏中小心地把参数用括号括起来
4).
我也用这个问题开始讨论宏的副作用,例如:当你写下面的代码时会发生什么事?
least = MIN(*p++, b);

3. 预处理器标识#error的目的是什么?

如果你不知道答案,请看参考文献1。这问题对区分一个正常的伙计和一个书呆子是很有用的。只有书呆子才会读C语言课本的附录去找出象这种
问题的答案。当然如果你不是在找一个书呆子,那么应试者最好希望自己不要知道答案。

死循环(Infinite loops)

4. 嵌入式系统中经常要用到无限循环,你怎么样用C编写死循环呢?

这个问题用几个解决方案。我首选的方案是:
while(1) { }
一些程序员更喜欢如下方案:
for(;;) { }
这个实现方式让我为难,因为这个语法没有确切表达到底怎么回事。如果一个应试者给出这个作为方案,我将用这个作为一个机会去探究他们这样做的
基本原理。如果他们的基本答案是:"我被教着这样做,但从没有想到过为什么。"这会给我留下一个坏印象。
第三个方案是用 goto
Loop:
...
goto Loop;
应试者如给出上面的方案,这说明或者他是一个汇编语言程序员(这也许是好事)或者他是一个想进入新领域的BASIC/FORTRAN程序员。

数据声明(Data declarations)

5. 用变量a给出下面的定义
a) 一个整型数(An integer)
b) 一个指向整型数的指针(A pointer to an integer)
c) 一个指向指针的的指针,它指向的指针是指向一个整型数(A pointer to a
pointer to an integer)
d) 一个有10个整型数的数组(An array of 10 integers)
e) 一个有10个指针的数组,该指针是指向一个整型数的(An array of 10 pointers
to integers)
f) 一个指向有10个整型数数组的指针(A pointer to an array of 10 integers)
g) 一个指向函数的指针,该函数有一个整型参数并返回一个整型数(A pointer to
a function that takes an integer as an argument and returns an integer)
h)
一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数(
An array of ten pointers to functions that take an integer argument and
return an integer )

答案是:
a) int a; // An integer
b) int *a; // A pointer to an integer
c) int **a; // A pointer to a pointer to an integer
d) int a[10]; // An array of 10 integers
e) int *a[10]; // An array of 10 pointers to integers
f) int (*a)[10]; // A pointer to an array of 10 integers
g) int (*a)(int); // A pointer to a function a that takes an integer
argument and returns an integer
h) int (*a[10])(int); // An array of 10 pointers to functions that take an
integer argument and return an integer

人们经常声称这里有几个问题是那种要翻一下书才能回答的问题,我同意这种说法。当我写这篇文章时,为了确定语法的正确性,我的确查了一下书。
但是当我被面试的时候,我期望被问到这个问题(或者相近的问题)。因为在被面试的这段时间里,我确定我知道这个问题的答案。应试者如果不知道
所有的答案(或至少大部分答案),那么也就没有为这次面试做准备,如果该面试者没有为这次面试做准备,那么他又能为什么出准备呢?

Static

6. 关键字static的作用是什么?

这个简单的问题很少有人能回答完全。在C语言中,关键字static有三个明显的作用:
1). 在函数体,一个被声明为静态的变量在这一函数被调用过程中维持其值不变。
2).
在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变量。
3).
在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用。
大多数应试者能正确回答第一部分,一部分能正确回答第二部分,同是很少的人能懂得第三部分。这是一个应试者的严重的缺点,因为他显然不懂得本地化数据和代码范围的好处和重要性。

Const

7.关键字const是什么含意?
我只要一听到被面试者说:"const意味着常数",我就知道我正在和一个业余者打交道。去年Dan
Saks已经在他的文章里完全概括了const的所有用法,因此ESP(译者:Embedded
Systems Programming)的每一位读者应该非常熟悉const能做什么和不能做什么.
如果你从没有读到那篇文章,只要能说出const意味着
"只读"就可以了。尽管这个答案不是完全的答案,但我接受它作为一个正确的答案。(如果你想知道更详细的答案,仔细读一下Saks的文章吧。)如果应试者能正确回答这个问题,我将问他一个附加的问题:下面的声明都是什么意思?

const int a;
int const a;
const int *a;
int * const a;
int const * a const;

前两个的作用是一样,a是一个常整型数。第三个意味着a是一个指向常整型数的指针(也就是,整型数是不可修改的,但指针可以)。第四个意思a是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。最后一个意味着a是一个指向常整型数的常指针(也就是说,指针指向的整型数是不可修改的,同时指针也是不可修改的)。如果应试者能正确回答这些问题,那么他就给我留下了一个好印象。顺带提一句,也许你可能会问,即使不用关键字
const,也还是能很容易写出功能正确的程序,那么我为什么还要如此看重关键字const呢?我也如下的几下理由:
1).
关键字const的作用是为给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉了用户这个参数的应用目的。如果你曾花很多时间清理其它人留下的垃圾,你就会很快学会感谢这点多余的信息。(当然,懂得用const的程序员很少会留下的垃圾让别人来清理的。)
2). 通过给优化器一些附加的信息,使用关键字const也许能产生更紧凑的代码。
3).
合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少bug的出现。

Volatile

8. 关键字volatile有什么含意 并给出三个不同的例子。

一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:
1). 并行设备的硬件寄存器(如:状态寄存器)
2). 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
3). 多线程应用中被几个任务共享的变量
回答不出这个问题的人是不会被雇佣的。我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。嵌入式系统程序员经常同硬件、中断、RTOS等等打交道,所用这些都要求volatile变量。不懂得volatile内容将会带来灾难。
假设被面试者正确地回答了这是问题(嗯,怀疑这否会是这样),我将稍微深究一下,看一下这家伙是不是直正懂得volatile完全的重要性。
1). 一个参数既可以是const还可以是volatile吗?解释为什么。
2). 一个指针可以是volatile 吗?解释为什么。
3). 下面的函数有什么错误:
int square(volatile int *ptr)
{ return *ptr * *ptr;
} 下面是答案:
1).
是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
2).
是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。
3).
这段代码的有个恶作剧。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:
int square(volatile int *ptr)
{ int a,b;
a = *ptr;
b = *ptr;
return a * b;
}
由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:
long square(volatile int *ptr)
{ int a;
a = *ptr;
return a * a;
}

位操作(Bit manipulation)

9.
嵌入式系统总是要用户对变量或寄存器进行位操作。给定一个整型变量a,写两段代码,第一个设置a的bit
3,第二个清除a 的bit 3。在以上两个操作中,要保持其它位不变。

对这个问题有三种基本的反应
1). 不知道如何下手。该被面者从没做过任何嵌入式系统的工作。
2). 用bit fields。Bit
fields是被扔到C语言死角的东西,它保证你的代码在不同编译器之间是不可移植的,同时也保证了的你的代码是不可重用的。我最近不幸看到
Infineon为其较复杂的通信芯片写的驱动程序,它用到了bit
fields因此完全对我无用,因为我的编译器用其它的方式来实现bit
fields的。从道德讲:永远不要让一个非嵌入式的家伙粘实际硬件的边。
3). 用 #defines 和 bit masks
操作。这是一个有极高可移植性的方法,是应该被用到的方法。最佳的解决方案如下:
#define BIT3 (0x1<<3)
static int a;
void set_bit3(void)
{ a |= BIT3;
} void clear_bit3(void)
{ a &= ~BIT3;
}
一些人喜欢为设置和清除值而定义一个掩码同时定义一些说明常数,这也是可以接受的。我希望看到几个要点:说明常数、|=和&=~操作。

10.
嵌入式系统经常具有要求程序员去访问某特定的内存位置的特点。在某工程中,要求设置一绝对地址为0x67a9的整型变量的值为0xaa66。编译器是一个纯粹的ANSI编译器。写代码去完成这一任务。

这一问题测试你是否知道为了访问一绝对地址把一个整型数强制转换(typecast)为一指针是合法的。这一问题的实现方式随着个人风格不同而不同。典型的类似代码如下:
int *ptr;
ptr = (int *)0x67a9;
*ptr = 0xaa55;

一个较晦涩的方法是:
*(int * const)(0x67a9) = 0xaa55;

即使你的品味更接近第二种方案,但我建议你在面试时使用第一种方案。

中断(Interrupts)

11.
中断是嵌入式系统中重要的组成部分,这导致了很多编译开发商提供一种扩展—让标准C支持中断。具代表事实是,产生了一个新的关键字
__interrupt。下面的代码就使用了__interrupt关键字去定义了一个中断服务子程序(ISR),请评论一下这段代码的。

__interrupt double compute_area (double radius)
{ double area = PI * radius * radius;
printf(" Area = %f", area);
return area;
}

这个函数有太多的错误了,以至让人不知从何说起了:
1). ISR 不能返回一个值。如果你不懂这个,那么你不会被雇用的。
2). ISR 不能传递参数。如果你没有看到这一点,你被雇用的机会等同第一项。
3).
在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让额处的寄存器入栈,有些处理器/编译器就是不允许在ISR中做浮点运算。此外,ISR应该是短而有效率的,在ISR中做浮点运算是不明智的。
4).
与第三点一脉相承,printf()经常有重入和性能上的问题。如果你丢掉了第三和第四点,我不会太为难你的。不用说,如果你能得到后两点,那么你的被雇用前景越来越光明了。

代码例子(Code examples)
12 . 下面的代码输出是什么,为什么?

void foo(void)
{ unsigned int a = 6;
int b = -20;
(a+b > 6) puts("> 6") : puts("<= 6");
}

这个问题测试你是否懂得C语言中的整数自动转换原则,我发现有些开发者懂得极少这些东西。不管如何,这无符号整型问题的答案是输出是">
6"。原因是当表达式中存在有符号类型和无符号类型时所有的操作数都自动转换为无符号类型。因此-20变成了一个非常大的正整数,所以该表达式计算出的结果大于6。这一点对于应当频繁用到无符号数据类型的嵌入式系统来说是丰常重要的。如果你答错了这个问题,你也就到了得不到这份工作的边缘。

13. 评价下面的代码片断:

unsigned int zero = 0;
unsigned int compzero = 0xFFFF;
/*1's complement of zero */

对于一个int型不是16位的处理器为说,上面的代码是不正确的。应编写如下:

unsigned int compzero = ~0;

这一问题真正能揭露出应试者是否懂得处理器字长的重要性。在我的经验里,好的嵌入式程序员非常准确地明白硬件的细节和它的局限,然而PC机程序往往把硬件作为一个无法避免的烦恼。
到了这个阶段,应试者或者完全垂头丧气了或者信心满满志在必得。如果显然应试者不是很好,那么这个测试就在这里结束了。但如果显然应试者做得不错,那么我就扔出下面的追加问题,这些问题是比较难的,我想仅仅非常优秀的应试者能做得不错。提出这些问题,我希望更多看到应试者应付问题的方法,而不是答案。不管如何,你就当是这个娱乐吧…

动态内存分配(Dynamic memory allocation)

14.
尽管不像非嵌入式计算机那么常见,嵌入式系统还是有从堆(heap)中动态分配内存的过程的。那么嵌入式系统中,动态分配内存可能发生的问题是什么?

这里,我期望应试者能提到内存碎片,碎片收集的问题,变量的持行时间等等。这个主题已经在ESP杂志中被广泛地讨论过了(主要是
P.J. Plauger,
他的解释远远超过我这里能提到的任何解释),所有回过头看一下这些杂志吧!让应试者进入一种虚假的安全感觉后,我拿出这么一个小节目:下面的代码片段的输出是什么,为什么?

char *ptr;
if ((ptr = (char *)malloc(0)) == NULL)
puts("Got a null pointer");
else
puts("Got a valid pointer");

这是一个有趣的问题。最近在我的一个同事不经意把0值传给了函数malloc,得到了一个合法的指针之后,我才想到这个问题。这就是上面的代码,该代码的输出是"Got
a valid
pointer"。我用这个来开始讨论这样的一问题,看看被面试者是否想到库例程这样做是正确。得到正确的答案固然重要,但解决问题的方法和你做决定的基本原理更重要些。

Typedef

15. Typedef
在C语言中频繁用以声明一个已经存在的数据类型的同义字。也可以用预处理器做类似的事。例如,思考一下下面的例子:
#define dPS struct s *
typedef struct s * tPS;

以上两种情况的意图都是要定义dPS 和 tPS
作为一个指向结构s指针。哪种方法更好呢?(如果有的话)为什么?
这是一个非常微妙的问题,任何人答对这个问题(正当的原因)是应当被恭喜的。答案是:typedef更好。思考下面的例子:
dPS p1,p2;
tPS p3,p4;

第一个扩展为
struct s * p1, p2;

上面的代码定义p1为一个指向结构的指,p2为一个实际的结构,这也许不是你想要的。第二个例子正确地定义了p3
和p4 两个指针。

晦涩的语法

16. C语言同意一些令人震惊的结构,下面的结构是合法的吗,如果是它做些什么?
int a = 5, b = 7, c;
c = a+++b;

这个问题将做为这个测验的一个愉快的结尾。不管你相不相信,上面的例子是完全合乎语法的。问题是编译器如何处理它?水平不高的编译作者实际上会争论这个问题,根据最处理原则,编译器应当能处理尽可能所有合法的用法。因此,上面的代码被处理成:
c = a++ + b;
因此, 这段代码持行后a = 6, b = 7, c = 12。
如果你知道答案,或猜出正确答案,做得好。如果你不知道答案,我也不把这个当作问题。我发现这个问题的最大好处是:这是一个关于代码编写风格,代码的可读性,代码的可修改性的好的话题

What will print out?

main()
{ char *p1="name";
char *p2;
p2=(char*)malloc(20);
memset (p2, 0, 20);
while(*p2++ = *p1++);
printf("%sn",p2);

}

Answer:empty string.

What will be printed as the result of the operation below:

main()
{ int x=20,y=35;
x=y++ + x++;
y= ++y + ++x;
printf("%d%dn",x,y);
}

Answer : 5794

What will be printed as the result of the operation below:

main()
{ int x=5;
printf("%d,%d,%dn",x,x< <2,x>>2);
}

Answer: 5,20,1

What will be printed as the result of the operation below:

#define swap(a,b) a=a+b;b=a-b;a=a-b;
void main()
{ int x=5, y=10;
swap (x,y);
printf("%d %dn",x,y);
swap2(x,y);
printf("%d %dn",x,y);
}

int swap2(int a, int b)
{ int temp;
temp=a;
b=a;
a=temp;
return 0;

}

Answer: 10, 5
10, 5

What will be printed as the result of the operation below:

main()
{ char *ptr = " Cisco Systems";
*ptr++; printf("%sn",ptr);
ptr++;
printf("%sn",ptr);
}

Answer:Cisco Systems
isco systems

What will be printed as the result of the operation below:

main()
{ char s1[]="Cisco";
char s2[]= "systems";
printf("%s",s1);
} Answer: Cisco

What will be printed as the result of the operation below:

main()
{ char *p1;
char *p2;
p1=(char *)malloc(25);
p2=(char *)malloc(25);

strcpy(p1,"Cisco");
strcpy(p2,"systems");
strcat(p1,p2);

printf("%s",p1);

}

Answer: Ciscosystems

The following variable is available in file1.c, who can access it?:

static int average;

Answer: all the functions in the file1.c can access the variable.

WHat will be the result of the following code?

#define TRUE 0 // some code
while(TRUE)
{

// some code

}

Answer: This will not go into the loop as TRUE is defined as 0.

What will be printed as the result of the operation below:

int x;
int modifyvalue()
{ return(x+=10);
} int changevalue(int x)
{ return(x+=1);
}

void main()
{ int x=10;
x++;
changevalue(x);
x++;
modifyvalue();
printf("First output:%dn",x);

x++;
changevalue(x);
printf("Second output:%dn",x);
modifyvalue();
printf("Third output:%dn",x);

}

Answer: 12 , 13 , 13

What will be printed as the result of the operation below:

main()
{ int x=10, y=15;
x = x++;
y = ++y;
printf("%d %dn",x,y);
}

Answer: 11, 16

What will be printed as the result of the operation below:

main()
{ int a=0;
if(a==0)
printf("Cisco Systemsn");
printf("Cisco Systemsn");
}

Answer: Two lines with "Cisco Systems" will be printed.

再次更新C++相关题集

1. 以下三条输出语句分别输出什么?[C易]
char str1[] = "abc";
char str2[] = "abc";
const char str3[] = "abc";
const char str4[] = "abc";
const char* str5 = "abc";
const char* str6 = "abc";
cout << boolalpha << ( str1==str2 ) << endl; // 输出什么?
cout << boolalpha << ( str3==str4 ) << endl; // 输出什么?
cout << boolalpha << ( str5==str6 ) << endl; // 输出什么?

13. 非C++内建型别 A 和 B,在哪几种情况下B能隐式转化为A?[C++中等]
答:
a. class B : public A { ……} // B公有继承自A,可以是间接继承的
b. class B { operator A( ); } // B实现了隐式转化为A的转化
c. class A { A( const B& ); } //
A实现了non-explicit的参数为B(可以有其他带默认值的参数)构造函数
d. A& operator= ( const A& ); //
赋值操作,虽不是正宗的隐式类型转换,但也可以勉强算一个

12. 以下代码中的两个sizeof用法有问题吗?[C易]
void UpperCase( char str[] ) // 将 str 中的小写字母转换成大写字母
{ for( size_t i=0; i<sizeof(str)/sizeof(str[0]); ++i )
if( 'a'<=str[i] && str[i]<='z' )
str[i] -= ('a'-'A' );
} char str[] = "aBcDe";
cout << "str字符长度为: " << sizeof(str)/sizeof(str[0]) << endl;
UpperCase( str );
cout << str << endl;

7. 以下代码有什么问题?[C难]
void char2Hex( char c ) // 将字符以16进制表示
{ char ch = c/0x10 + '0'; if( ch > '9' ) ch += ('A'-'9'-1);
char cl = c%0x10 + '0'; if( cl > '9' ) cl += ('A'-'9'-1);
cout << ch << cl << ' ';
} char str[] = "I love 中国";
for( size_t i=0; i<strlen(str); ++i )
char2Hex( str[i] );
cout << endl;

4. 以下代码有什么问题?[C++易]
struct Test
{ Test( int ) {}
Test() {}
void fun() {}
};
void main( void )
{ Test a(1);
a.fun();
Test b();
b.fun();
}

5. 以下代码有什么问题?[C++易]
cout << (true?1:"1") << endl;

8. 以下代码能够编译通过吗,为什么?[C++易]
unsigned int const size1 = 2;
char str1[ size1 ];
unsigned int temp = 0;
cin >> temp;
unsigned int const size2 = temp;
char str2[ size2 ];

9. 以下代码中的输出语句输出0吗,为什么?[C++易]
struct CLS
{ int m_i;
CLS( int i ) : m_i(i) {}
CLS()
{ CLS(0);
} };
CLS obj;
cout << obj.m_i << endl;

10. C++中的空类,默认产生哪些类成员函数?[C++易]
答:
class Empty
{ public:
Empty(); // 缺省构造函数
Empty( const Empty& ); // 拷贝构造函数
~Empty(); // 析构函数
Empty& operator=( const Empty& ); // 赋值运算符
Empty* operator&(); // 取址运算符
const Empty* operator&() const; // 取址运算符 const
};

3. 以下两条输出语句分别输出什么?[C++难]
float a = 1.0f;
cout << (int)a << endl;
cout << (int&)a << endl;
cout << boolalpha << ( (int)a == (int&)a ) << endl; // 输出什么?
float b = 0.0f;
cout << (int)b << endl;
cout << (int&)b << endl;
cout << boolalpha << ( (int)b == (int&)b ) << endl; // 输出什么?

2. 以下反向遍历array数组的方法有什么错误?[STL易]
vector array;
array.push_back( 1 );
array.push_back( 2 );
array.push_back( 3 );

for( vector::size_type i=array.size()-1; i>=0; --i ) // 反向遍历array数组
{ cout << array[i] << endl;
}

6. 以下代码有什么问题?[STL易]
typedef vector IntArray;
IntArray array;
array.push_back( 1 );
array.push_back( 2 );
array.push_back( 2 );
array.push_back( 3 );
// 删除array数组中所有的2
for( IntArray::iterator itor=array.begin(); itor!=array.end(); ++itor )
{ if( 2 == *itor ) array.erase( itor );
}

11. 写一个函数,完成内存之间的拷贝。[考虑问题是否全面]
答:

void* mymemcpy( void *dest, const void *src, size_t count )
{
char* pdest = static_cast<char*>( dest );
const char* psrc = static_cast<const char*>( src );
if( pdest>psrc && pdest<psrc+cout ) 能考虑到这种情况就行了
{
for( size_t i=count-1; i!=-1; --i )
pdest[i] = psrc[i];
}
else
{
for( size_t i=0; i<count; ++i )
pdest[i] = psrc[i];
}
return dest;
}
int main( void )
{
char str[] = "0123456789";
mymemcpy( str+1, str+0, 9 );
cout << str << endl;

system( "Pause" );
return 0;
}

C++基本功:全面掌握const、volatile和mutable关键字

C++ 程式设计过程中 ,const 的使用可以频度是非常高的 .
它在保证程式安全方面起到了不可估量的作用 .
用一句话来表达最确切不过了:"小兵立大功" .
有了 const, 那么 mutable 当然缺不了 .
然作为 const 的同胞兄弟 ,volatile 却在很多人的视野中消失 . 其实
volatile 担负的责任有何尝小呢 ?
自然 , 它们的用法多样而灵巧 , 以至新手迷惑久久 ,
下面就来系统的探讨总结一下吧:
一 . 一般应用
1.const 修饰各种变量的用法 .
a. 取代 define
#define D_INT 100
#define D_LONG 100.29
………
const int D_INT = 100;
const D_INT = 100; // 如果定义的 int 类型 , 可省略 int.
const long D_LONG = 100.29;
………
const int& a = 100;
const 替代 define 虽然增加分配空间 , 可它却保证了类型安全 .
在 C 标准中 ,const 定义的数据相当于全局的 , 而 C++ 中视声明的位置而定
.
b. 修饰指针相关的变量
以三组简单的定义示意:
Group1:
int a = 0;
const int* b = &a;------------ [1]
int const *b = &a;------------ [2]
const int* const b = &a;---- [4]

Group2:
const char *p = "const";--------------[1]
char const *p = "const";--------------[2]
char* const p = "const";--------------[3]
const char * const p = "const";----[4]

Group3:
int a=0;
const int &b = a;---------------[1]
int const &b = a;---------------[2]
int & const b = a;--------------[3] //---> 修饰引用时 ,const 被忽略
const int & const b = a;-----[4]
总结:
1. 如果 const 位于星号左侧 , 则 const 用来修饰指针所指向的变量 ,
即指针指向的为不可变的 .
2. 如果 const 位于星号右侧 ,const 就是修饰指针本身 , 即指针本身是
不可变的 .
因此 ,[1] 和 [2] 的情况相同 , 指针所指向内容不可变 (const 放在变量
声明符的位置无关 ),
这种情况下不允许对内容进行更改 , 如不能 *a = 3 ;
3.[3] 中指针本身是不可变的,而指针所指向的内容是可变的 , 这种情况
下不能对指针本身
进行更改操作 , 如 a++ 是错误的
4.[4] 中指针本身和指向的内容均为常量 .( 引用特殊:引用在使用增加
遇义时 , 增加它代表的变量 . 所以 qualifiers on reference are
ignoredv.
延伸点 :
注意示例 :
1.const int& reference = 1000;
2.char* p = "const"
char*& q ;

2.const 在函数环境下的各种应用
常用法示例如下:
const A& _Fun(const A& _in); // 修饰引用型传入参数
// A _Fun(const A& _in);
//A& _Fun(const A& _in);
// 上面的两种 , 在函数内部有特殊的步骤 , 这里不详提了… ..

const A* _Fun( const A* _in); // 修饰指针型传入参数
void _Fun( ) const; // 修饰 class 成员函数
const A& _Fun(A& _in ); // 修饰返回值
const A & operator(const A& _in); // 同时修饰传入参数和返回值
a. 修饰参数
如 void _Fun(const A* _in) 或 void _Fun(const A& _in);
它们被修饰后 , 在函数执行期间行为特性同于上面的讲解 ,
注意:这不会改变原来数据的是否是 const 的属性 .
b. 修饰函数返回值
const A& _Fun( )
const A* _Fun( );
注意:由于生命期不同步的问题 , 不可将局部的变量的指针或引用返回
(static 除外 ).
另外 , 传出来的视情况 , 代表不同的意思…
对于 A& 返回类型 , 你若将之赋与其它变量 ,
那么它实际执行的是将返回的变量
( 或引用 ) 代表的数据赋出 .. 而你若将其它值赋予之 ,
那么被赋予的是变量或引
用代表的数据 . 而 const A& 一般是防止之做为左值被赋值 .
这个地方还有很多的细节问题 ( 譬如在连续赋值、返回的临时对象的处理、
重载的 const 和非 cosnt 运算符等等 ), 读者自己在实践中需要多多总结 .
二、难点
3. 修饰类成员函数的 const.
形如 :void _Fun() const { };
你需要知道的几点规则:
a.const 对象只能访问 const 成员函数 , 而非 const 对象可以访问任意
的成员函数 , 包括 const 成员函数 .
b.const 对象的成员是不可修改的 , 然而 const 对象通过指针维护的对象却
是可以修改的 .
c.const 成员函数不可以修改对象的数据 , 不管对象是否具有 const 性质 .
它在
编译时 , 以是否修改成员数据为依据 , 进行检查 .
e. 然而加上 mutable 修饰符的数据成员 , 对于任何情况下通过任何手段
都可修改 , 自然此时的 const 成员函数是可以修改它的…
4. 谈谈 volatile 和"完全 const 对象"
一个有 volatile 修饰的类只允许访问其接口的一个子集,这个子集由类的
实现者来控制 . 用户只有用 const_cast 才可以访问这个类型的全部接口 . 而且
,
象 const 一样,类的 volatile 属性会传递给它的成员 . 想象 const 修饰的对
象 , 它的成员变量是不可修改的 , 而它通过指针维护的对象或原生变量是可
修改 . 那么我们想 : 如果对象维护一个 char* , 则它相当于 char*
const chrptr ; 而不是 const char* cosnt chrptr; 对于类中的指针你需要
这样修饰以防止它或它维护的资源: cosnt x* xptr; 而不是 x*const xptr;
因为 cosnt 修饰的对象它默认 的行为是延续变量: x* cosnt xptr;
更重要的 ,volatile 修饰的数据 ,
编译器不可对其进行执行期寄存于寄存器的优化 .
这种特性 , 是为了多线程同步的需要 . 有兴趣者看参看 Andrei 的 GP 系列文章
.
5. 谈谈 const_cast 转换运算符
这个关键字最基础的用法是:去掉数据的 const 性质 .
值得注意的是:它只对指针、引用和其它的具有指向性质的类型 .
参考:
1. 《 Effective C++ 》关于 const 两种语义的论述
2.Andrei Alexandrescu 《 volatile ——编写多线程程序的好帮手》

用两个堆栈实现一个队列

栈的特点是后进先出,队列的特点是先进先出。所以,用两个栈s1和s2模拟一个队列时,s1作输入栈,逐个元素压栈,以此模拟队列元素的入队。当需要出队时,将栈s1退栈并逐个压入栈s2中,s1中最先入栈的元素,在s2中处于栈顶。s2退栈,相当于队列的出队,实现了先进先出。显然,只有栈s2为空且s1也为空,才算是队列空。
[算法讨论]算法中假定栈s1和栈s2容量相同。出队从栈s2出,当s2为空时,若s1不空,则将s1倒入s2再出栈。入队在s1,当s1满后,若s2空,则将s1倒入s2,之后再入队。因此队列的容量为两栈容量之和。元素从栈s1倒入s2,必须在s2空的情况下才能进行,即在要求出队操作时,若s2空,则不论s1元素多少(只要不空),就要全部倒入s2中。
下面两个方法的思路是一致的,只是一个是基于进栈与队列相同一个基于出栈与队列相同。
法(
1
int enqueue(stack s1,elemtp x)
//s1是容量为n的栈,栈中元素类型是elemtp。本算法将x入栈,若入栈成功返回1,否则返回0。
{if(top1==n && !Sempty(s2)) //top1是栈s1的栈顶指针,是全局变量。
{printf(“栈满”);return(0);} //s1满s2非空,这时s1不能再入栈。
if(top1==n && Sempty(s2)) //若s2为空,先将s1退栈,元素再压栈到s2。
{while(!Sempty(s1)) {POP(s1,x);PUSH(s2,x);}
PUSH(s1,x);
return(1); //x入栈,实现了队列元素的入队。
}
void dequeue(stack s2,s1)
//s2是输出栈,本算法将s2栈顶元素退栈,实现队列元素的出队。
{if(!Sempty(s2)) //栈s2不空,则直接出队。
{POP(s2,x); printf(“出队元素为”,x); }
else //处理s2空栈。
if(Sempty(s1)) {printf(“队列空”);exit(0);}//若输入栈也为空,则判定队空。
else //先将栈s1倒入s2中,再作出队操作。
{while(!Sempty(s1)) {POP(s1,x);PUSH(s2,x);}
POP(s2,x);
//s2退栈相当队列出队。
printf(“出队元素”,x);
}
}
//结束算法dequue。
int queue_empty()
//本算法判用栈s1和s2模拟的队列是否为空。
{if(Sempty(s1)&&Sempty(s2)) return(1);//队列空。
else return(0); //队列不空。
}
法(
2
ElementType DeQueue(S1)
{
if(Empty(S1)
{
printf(
"Error!");
exit(
0);
}
else
return Pop(S1);
}
void EnQueue(S1,ElementType x)
{
ElementType t;
while(!Empty(S1))
{
t
=Pop(S1);
Push(S2,t;
}
Push(S1,x);
while(!Empty(S2))
{
t
=Pop(S2);

Push(S1,t);

}

C语言中判断大小端的方法

有时候,用C语言写程序时需要知道是大端模式还是小端模式。
所谓的大端模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
所谓的小端模式,是指数据的低位保存在内存的低地址中,而数据的高位保存在内存的高地址中。
为什么会有大小端模式之分呢?
这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为 8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器),另外,对于位数大于 8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如果将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。例如一个16bit的short型x,在内存中的地址为0x0010,x的值为0x1122,那么0x11为高字节,0x22为低字节。对于大端模式,就将0x11放在低地址中,即0x0010中,0x22放在高地址中,即0x0011中。小端模式,刚好相反。我们常用的X86结构是小端模式,而KEIL C51则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。

下面这段代码可以用来测试一下你的编译器是大端模式还是小端模式:
 
short int x;
char x0,x1;
x=0x1122;
x0=((char*)&x)[0]; //低地址单元
x1=((char*)&x)[1]; //高地址单元

若x0=0x11,则是大端; 若x0=0x22,则是小端......

C语言嵌入式系统编程修炼(屏幕操作) 汉字处理

  现在要解决的问题是,嵌入式系统中经常要使用的并非是完整的汉字库,往往只是需要提供数量有限的汉字供必要的显示功能。例如,一个微波炉的LCD上没有必要提供显示"电子邮件"的功能;一个提供汉字显示功能的空调的LCD上不需要显示一条"短消息",诸如此类。但是一部手机、小灵通则通常需要包括较完整的汉字库。
  如果包括的汉字库较完整,那么,由内码计算出汉字字模在库中的偏移是十分简单的:汉字库是按照区位的顺序排列的,前一个字节为该汉字的区号,后一个字节为该字的位号。每一个区记录94个汉字,位号则为该字在该区中的位置。因此,汉字在汉字库中的具体位置计算公式为:94*(区号-1)+位号-1。减1是因为数组是以0为开始而区号位号是以1为开始的。只需乘上一个汉字字模占用的字节数即可,即:(94*(区号-1)+位号-1)*一个汉字字模占用字节数,以16*16点阵字库为例,计算公式则为:(94*(区号-1)+(位号- 1))*32。汉字库中从该位置起的32字节信息记录了该字的字模信息。
  对于包含较完整汉字库的系统而言,我们可以以上述规则计算字模的位置。但是如果仅仅是提供少量汉字呢?譬如几十至几百个?最好的做法是:
  定义宏:
# define EX_FONT_CHAR(value)
# define EX_FONT_UNICODE_VAL(value) (value),
# define EX_FONT_ANSI_VAL(value) (value),
  定义结构体:
typedef struct _wide_unicode_font16x16
{
 WORD value; /* 内码 */
 BYTE data[32]; /* 字模点阵 */
}Unicode;
#define CHINESE_CHAR_NUM … /* 汉字数量 */
  字模的存储用数组:
Unicode chinese[CHINESE_CHAR_NUM] =
{
{
EX_FONT_CHAR("业")
EX_FONT_UNICODE_VAL(0x4e1a)
{0x04, 0x40, 0x04, 0x40, 0x04, 0x40, 0x04, 0x44, 0x44, 0x46, 0x24, 0x4c, 0x24, 0x48, 0x14, 0x50, 0x1c, 0x50, 0x14, 0x60, 0x04, 0x40, 0x04, 0x40, 0x04, 0x44, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00}
},
{
EX_FONT_CHAR("中")
EX_FONT_UNICODE_VAL(0x4e2d)
{0x01, 0x00, 0x01, 0x00, 0x21, 0x08, 0x3f, 0xfc, 0x21, 0x08, 0x21, 0x08, 0x21, 0x08, 0x21, 0x08, 0x21, 0x08,
0x3f, 0xf8, 0x21, 0x08, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00}
},
{
EX_FONT_CHAR("云")
EX_FONT_UNICODE_VAL(0x4e91)
{0x00, 0x00, 0x00, 0x30, 0x3f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0xff, 0xfe, 0x03, 0x00, 0x07, 0x00,
0x06, 0x40, 0x0c, 0x20, 0x18, 0x10, 0x31, 0xf8, 0x7f, 0x0c, 0x20, 0x08, 0x00, 0x00}
},
{
EX_FONT_CHAR("件")
EX_FONT_UNICODE_VAL(0x4ef6)
{0x10, 0x40, 0x1a, 0x40, 0x13, 0x40, 0x32, 0x40, 0x23, 0xfc, 0x64, 0x40, 0xa4, 0x40, 0x28, 0x40, 0x2f, 0xfe,
0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40}
}
}
  要显示特定汉字的时候,只需要从数组中查找内码与要求汉字内码相同的即可获得字模。如果前面的汉字在数组中以内码大小顺序排列,那么可以以二分查找法更高效的查找到汉字的字模。
  这是一种很有效的组织小汉字库的方法,它可以保证程序有很好的结构。
  系统时间显示
  从NVRAM中可以读取系统的时间,系统一般借助NVRAM产生的秒中断每秒读取一次当前时间并在LCD上显示。关于时间的显示,有一个效率问题。因为时间有其特殊性,那就是60秒才有一次分钟的变化,60分钟才有一次小时变化,如果我们每次都将读取的时间在屏幕上完全重新刷新一次,则浪费了大量的系统时间。
  一个较好的办法是我们在时间显示函数中以静态变量分别存储小时、分钟、秒,只有在其内容发生变化的时候才更新其显示。
extern void DisplayTime(…)
{
 static BYTE byHour,byMinute,bySecond;
 BYTE byNewHour, byNewMinute, byNewSecond;
 byNewHour = GetSysHour();
 byNewMinute = GetSysMinute();
 byNewSecond = GetSysSecond();
 
 if(byNewHour!= byHour)
 {
  … /* 显示小时 */
  byHour = byNewHour;
 }
 if(byNewMinute!= byMinute)
 {
  … /* 显示分钟 */
  byMinute = byNewMinute;
 }
 if(byNewSecond!= bySecond)
 {
  … /* 显示秒钟 */
  bySecond = byNewSecond;
 }
}
  这个例子也可以顺便作为C语言中static关键字强大威力的证明。当然,在C++语言里,static具有了更加强大的威力,它使得某些数据和函数脱离"对象"而成为"类"的一部分,正是它的这一特点,成就了软件的无数优秀设计。
  动画显示
  动画是无所谓有,无所谓无的,静止的画面走的路多了,也就成了动画。随着时间的变更,在屏幕上显示不同的静止画面,即是动画之本质。所以,在一个嵌入式系统的LCD上欲显示动画,必须借助定时器。没有硬件或软件定时器的世界是无法想像的:
  (1) 没有定时器,一个操作系统将无法进行时间片的轮转,于是无法进行多任务的调度,于是便不再成其为一个多任务操作系统;
  (2) 没有定时器,一个多媒体播放软件将无法运作,因为它不知道何时应该切换到下一帧画面;
  (3) 没有定时器,一个网络协议将无法运转,因为其无法获知何时包传输超时并重传之,无法在特定的时间完成特定的任务。
  因此,没有定时器将意味着没有操作系统、没有网络、没有多媒体,这将是怎样的黑暗?所以,合理并灵活地使用各种定时器,是对一个软件人的最基本需求!
  在80186为主芯片的嵌入式系统中,我们需要借助硬件定时器的中断来作为软件定时器,在中断发生后变更画面的显示内容。在时间显示"xx:xx"中让冒号交替有无,每次秒中断发生后,需调用ShowDot:
void ShowDot()
{
 static BOOL bShowDot = TRUE; /* 再一次领略static关键字的威力 */
 if(bShowDot)
 {
  showChar(’:’,xPos,yPos);
 }
 else
 {
  showChar(’ ’,xPos,yPos);
 }
 bShowDot = ! bShowDot;
}
  菜单操作
  无数人为之绞尽脑汁的问题终于出现了,在这一节里,我们将看到,在C语言中哪怕用到一丁点的面向对象思想,软件结构将会有何等的改观!
  笔者曾经是个笨蛋,被菜单搞晕了,给出这样的一个系统:
 
图1 菜单范例
  要求以键盘上的"← →"键切换菜单焦点,当用户在焦点处于某菜单时,若敲击键盘上的OK、CANCEL键则调用该焦点菜单对应之处理函数。我曾经傻傻地这样做着:
/* 按下OK键 */
void onOkKey()
{
 /* 判断在什么焦点菜单上按下Ok键,调用相应处理函数 */
 Switch(currentFocus)
 {
  case MENU1:
   menu1OnOk();
   break;
  case MENU2:
   menu2OnOk();
   break;
  …
 }
}
/* 按下Cancel键 */
void onCancelKey()
{
 /* 判断在什么焦点菜单上按下Cancel键,调用相应处理函数 */
 Switch(currentFocus)
 {
  case MENU1:
   menu1OnCancel();
   break;
  case MENU2:
   menu2OnCancel();
   break;
  …
 }
}
  终于有一天,我这样做了:
/* 将菜单的属性和操作"封装"在一起 */
typedef struct tagSysMenu
{
 char *text; /* 菜单的文本 */
 BYTE xPos; /* 菜单在LCD上的x坐标 */
 BYTE yPos; /* 菜单在LCD上的y坐标 */
 void (*onOkFun)(); /* 在该菜单上按下ok键的处理函数指针 */
 void (*onCancelFun)(); /* 在该菜单上按下cancel键的处理函数指针 */
}SysMenu, *LPSysMenu;
  当我定义菜单时,只需要这样:
static SysMenu menu[MENU_NUM] =
{
 {
  "menu1", 0, 48, menu1OnOk, menu1OnCancel
 }
 ,
 {
  " menu2", 7, 48, menu2OnOk, menu2OnCancel
 }
 ,
 {
  " menu3", 7, 48, menu3OnOk, menu3OnCancel
 }
 ,
 {
  " menu4", 7, 48, menu4OnOk, menu4OnCancel
 }
 …
};
  OK键和CANCEL键的处理变成:
/* 按下OK键 */
void onOkKey()
{
 menu[currentFocusMenu].onOkFun();
}
/* 按下Cancel键 */
void onCancelKey()
{
 menu[currentFocusMenu].onCancelFun();
}
  程序被大大简化了,也开始具有很好的可扩展性!我们仅仅利用了面向对象中的封装思想,就让程序结构清晰,其结果是几乎可以在无需修改程序的情况下在系统中添加更多的菜单,而系统的按键处理函数保持不变。
  面向对象,真神了! 
  模拟MessageBox函数
  MessageBox函数,这个Windows编程中的超级猛料,不知道是多少入门者第一次用到的函数。还记得我们第一次在Windows中利用MessageBox输出 "Hello,World!"对话框时新奇的感觉吗?无法统计,这个世界上究竟有多少程序员学习Windows编程是从MessageBox ("Hello,World!",…)开始的。在我本科的学校,广泛流传着一个词汇,叫做"’Hello,World’级程序员",意指入门级程序员,但似乎"’Hello,World’级"这个说法更搞笑而形象。
   
图2 经典的Hello,World!
  图2给出了两种永恒经典的Hello,World对话框,一种只具有"确定",一种则包含"确定"、"取消"。是的,MessageBox的确有,而且也应该有两类!这完全是由特定的应用需求决定的。
  嵌入式系统中没有给我们提供MessageBox,但是鉴于其功能强大,我们需要模拟之,一个模拟的MessageBox函数为:
/******************************************
/* 函数名称: MessageBox
/* 功能说明: 弹出式对话框,显示提醒用户的信息
/* 参数说明: lpStr --- 提醒用户的字符串输出信息
/* TYPE --- 输出格式(ID_OK = 0, ID_OKCANCEL = 1)
/* 返回值: 返回对话框接收的键值,只有两种 KEY_OK, KEY_CANCEL
/******************************************
typedef enum TYPE { ID_OK,ID_OKCANCEL }MSG_TYPE;
extern BYTE MessageBox(LPBYTE lpStr, BYTE TYPE)
{
 BYTE keyValue = -1;
 ClearScreen(); /* 清除屏幕 */
 DisplayString(xPos,yPos,lpStr,TRUE); /* 显示字符串 */
 /* 根据对话框类型决定是否显示确定、取消 */
 switch (TYPE)
 {
  case ID_OK:
   DisplayString(13,yPos+High+1, " 确定 ", 0);
   break;
  case ID_OKCANCEL:
   DisplayString(8, yPos+High+1, " 确定 ", 0);
   DisplayString(17,yPos+High+1, " 取消 ", 0);
   break;
  default:
   break;
 }
 DrawRect(0, 0, 239, yPos+High+16+4); /* 绘制外框 */
 /* MessageBox是模式对话框,阻塞运行,等待按键 */
 while( (keyValue != KEY_OK) || (keyValue != KEY_CANCEL) )
 {
  keyValue = getSysKey();
 }
 /* 返回按键类型 */
 if(keyValue== KEY_OK)
 {
  return ID_OK;
 }
 else
 {
  return ID_CANCEL;
 }
}
  上述函数与我们平素在VC++等中使用的MessageBox是何等的神似啊?实现这个函数,你会看到它在嵌入式系统中的妙用是无穷的。
  总结
  本篇是本系列文章中技巧性最深的一篇,它提供了嵌入式系统屏幕显示方面一些很巧妙的处理方法,灵活使用它们,我们将不再被LCD上凌乱不堪的显示内容所困扰。
  屏幕乃嵌入式系统生存之重要辅助,面目可憎之显示将令用户逃之夭夭。屏幕编程若处理不好,将是软件中最不系统、最混乱的部分,笔者曾深受其害。

堆与栈的区别

一、预备知识—程序的内存分配
一个由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在常量区,p3在栈上。
static int c =0; 全局(静态)初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);
分配得来得10和20字节的区域就在堆区。
strcpy(p1, "123456"); 123456放在常量区,编译器可能会将它与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小结:
堆和栈的区别可以用如下的比喻来看出:
使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。
 
堆和栈的区别主要分:
操作系统方面的堆和栈,如上面说的那些,不多说了。
还有就是数据结构方面的堆和栈,这些都是不同的概念。这里的堆实际上指的就是(满足堆性质的)优先队列的一种数据结构,第1个元素有最高的优先权;栈实际上就是满足先进后出的性质的数学或数据结构。
虽然堆栈,堆栈的说法是连起来叫,但是他们还是有很大区别的,连着叫只是由于历史的原因。
 
对和栈的主要的区别由以下几点:  
  1、管理方式不同;  
  2、空间大小不同;  
  3、能否产生碎片不同;  
  4、生长方向不同;  
  5、分配方式不同;  
  6、分配效率不同;  
  管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。
   
  空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M(好像是,记不清楚了)。当然,我们可以修改:  
打开工程,依次操作菜单如下:Project->Setting->Link,在Category 中选中Output,然后在Reserve中设定堆栈的最大值和commit。  
注意:reserve最小值为4Byte;commit是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的值,可能增加内存的开销和启动时间。 
  碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以参考数据结构,这里我们就不再一一讨论了。  
  生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。  
  分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。  
  分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。  
  从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。  
  栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。  
  无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果,就算是在你的程序运行过程中,没有发生上面的问题,你还是要小心,说不定什么时候就崩掉,那时候debug可是相当困难的:)  
  对了,还有一件事,如果有人把堆栈合起来说,那它的意思是栈,可不是堆,呵呵,清楚了? 
全局变量和静态全局变量的区别
全局变量本身就是静态存储方式, 静态全局变量当然也是静态存储方式。 这两者在存储方式上并无不同。这两者的区别虽在于非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。 而静态全局变量则限制了其作用域, 即只在定义该变量的源文件内有效, 在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其它源文件中引起错误。
从以上分析可以看出, 把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量后是改变了它的作用域,限制了它的使用范围。因此static 这个说明符在不同的地方所起的作用是不同的。应予以注意。
Static
1. 静态变量,分配在静态存储区,在数据段中。函数退出之后,变量值不变。
2. 作用域,全局的静态变量、静态函数只能在本文件中使用。(不同于一般全局变量)
局部的静态变量同函数的局部变量
2,结构体和联合体的区别,
结构体是根据所有元素所占的内存空间的总和来为其分配存储空间
联合体是根据占内存空间最大的那个元素来为其分配存储空间 

不使用逻辑运算求得两数的最大值

下面将介绍两个不使用逻辑运算求两数最大值的算法:
 
算法一
 

int max(const int *p, const int *q)

{

    int array[] = {*p, *q};

    return array[(unsigned)(*- *q) >> (sizeof(int) * 8 - 1)];

}

算法二

int max(const int *p, const int *q)

{

    return (((*+ *q) + abs(*- *q)) / 2);

}

分析:

算法一利用计算机系统中数的存储方式为其补码这一特性。补码的最高位为符号位,如果为正,则最高位为0,反之则为1。通过右移运算得到其最高位的值。之所以转换为无符号数是因为无符号数右移时左边高位移入0,而对于有符号数,当原来符号位为0(该数为正)时左边也是移入0,但如果符号位为1,时左边移入0还是1则不确定,取决于所用的计算机系统。

当*p大于*q时,右移后得0,则函数返回数组中下标为0的元素,即*p;反之则*q。

缺点:如果*p与*q之差大于等于2^31,则算法出错。

算法二则利用<math.h>中的函数abs()。

如果a大于b,则

abs(a - b) = a - b

(a + b + abs(a - b)) / 2 = (a + b + a - b) / 2 = a

如果a小于b,则

abs(a - b) = b - a

(a + b + (abs(a - b)) /2 = (a + b + b - a) / 2 = b

缺点:要调用库函数。(a + b + abs(a - b))使a, b中的最大值的绝对值要小于2^30,否则可能会溢出(考虑有符号数的表示范围为-2^31~2^31-1)。

注:C99符加的标准ANSI C库中abs()包含在中。