魅族M8 | 苹果iPhone | |
网络制式 | GSM/TD-SCDMA | GSM 850/900/1800/1900,EDGE |
CPU | ARM11+video Codec(MPEG4/H.263/H.264) | Intel XScale处理器 |
操作系统 | WinCE 6.0 | Mac OS X |
外型尺寸 | 105×57×11.5mm | 115×61×11.6mm |
显示屏 | VGA+720×480像素3.3英寸触摸屏 | 320×480像素3.5英寸触摸屏 |
摄像头 | 300万+30万像素双摄像头 | 200万像素 |
接口 | 蓝牙,30PIN扩展接口(TV OUT,LINE IN/OUT) | 蓝牙2.0,802.11b/g Wi-Fi等 |
最近,想买手机,听说国外的还不错,于是差了一下他们的cpu,尽管出现过高频低能的笑话,但我自己觉得频率高还是好的,呵呵,为此,我就对M8和iphone做一个对比:
Cpu:
Xscanle处理器频率在我估计:CPU处理速度在高频的情况下可以达到650 MHz,功耗较低水平,但XScale源于ARM内核。有人说iPhone处理器频率高达806MHz!至少使用3颗ARM芯片,这种说法好像不太靠谱喔,误导观众,纯属炒作(ARM控股公司CEO Warren East又抛出了新的说法,他认为iPhone的触摸屏幕下面至少有三颗ARM处理器在工作),苹果iPhone主处理器将是ARM架构下Marvell公司的PXA320。该处理器由Intel开发,(但有人说苹果用的就是三星的S3C6400处理器. )其真实型号是S5L8900B01,但我认为还是S3C6400,(基于ARM1176JZF-S内核)懂了吧,否则3D效果不会那么好,在功耗上才有如此低的表现水平。
ARM11:
M8将采用ARM11,533Mhz的CPU。大家一致认为这款CPU应该是三星的S3C6400,根据目前公布的配置,M8将采用ARM11,533Mhz的CPU。大家一致认为这款CPU应该是三星的S3C6400,。
语出惊人:
根据目前公布的配置,M8将采用ARM11,533Mhz的CPU。大家一致认为这款CPU应该是三星的S3C6400。
得出结论:M8的cpu和苹果一样。。。。。:-)
国人抄袭本领:
够牛,连cpu都給抄袭了,统一用三星,苹果说我是用的Intel Xscale处理器,国人说我用的就是ARM11,你苹果还是基于我ARM1176内核构造的呢,难道就比你苹果差很多了吗?佩服。。。。佩服。
但不得不佩服魅族的M8研发人员还是很牛B的。。
此文章代表个人观点。。。。谢谢,有漏笔之出还请尽量!
用优化精灵测试到内存是172MB。
今天偶然探到WIN CE的底层硬件测试了下 的确内存是170M多。远远达不到256M
CPU的确是三星的6410的
首先要对坚决立志做处女到结婚的人表示钦佩,为有处女情结的男人深表同情并悲哀。为这个社会还有这种女同胞倍感可贵,但我却认为一般情况下一个女人做处女身到结婚,存在了巨大的风险。
幸福和是处女与否无关
这个现象分析起来不好分析,但可以反过来看这个问题。那就是婚姻幸福的人当中,这个做妻子的以前是否热烈地恋爱过没有关系,与和这个女人的观念和开放程度没关系。从某种角度来看,很多开放和在社会里混得走,活动能力十分强劲的人似乎下半辈子都过得很幸福,甚至不少嫁给了声名显赫的男人过着耀眼的生活。
相反地,很多传统的女人结婚后,过着传统软弱的生活,并不能保证幸福的婚姻和未来。因为男人的变化是不可确定的,离婚的人当中,传统的人也不少,甚至随着社会发展那种爹妈负责相亲的人离婚的会越来越多,这些情况下处女与否已经不再重要。
那么,单纯以为是处女了就幸福的女人是痴人做梦;而不是处女的女同胞来说,这个也不是决定因素,因为一个人是要看综合的很多方面。但为那些只为了自己的信仰,而不是为了男人看法保持处女身的女性表示敬仰。
做处女要付出很多代价
我有一个朋友,读书时一个女生宿舍的同学多次笑取她太传统了,在大学的环境里这种取笑有点讽刺的意思,简单说就是这个女人太落后了。这种优点在特定环境里反而成了缺点,一直感受到各方压力。如今的社会里能进行着柏拉图式精神恋爱的是凤毛麟角,非常之少。我这个朋友的经历一直让人觉得付出的代价太多,开始谈了好几个男朋友,结果都和我说,她没有哪个男朋友保持到三个月以上。很简单的道理,女人呢觉得你爱我,你就不能伤害我;男人呢觉得你爱我,你就影响相信我,愿意和做任何事情。而很多情况下,性的确有足于让大家更默契和更有感情,也能化解不少矛盾。男人单独和女朋友一起相处到一定程度都是情不自禁希望发生性关系,并非不对。
这样的结果就是,自尊心强的男人经常提出要求后被拒绝,自尊心受到很多伤害,自然对这个女孩子有很多抵触。到后来的失望、绝望。那么坚持做处女的人就会在这个过程里因为这个观念放弃一个又一个男人,男人也一个又一个离去。也许有一天,不排除这样的情况,当这个一直做处女的女孩子结婚后觉得很不幸福,或者觉得男人很不如别人,很怀念当初和其谈过恋爱的人,可惜已经错过。
除非是遇到其他一些特殊情况——要么这个女人是否优秀,在某些领域十分出众,最好是明星;要么遇到一个男人十分欣赏这个女人的很多方面,包括追求、信仰甚至地位等等,并在很多方面有一致的观点。否则怎么坚持下去,是个巨大的挑战。
一直当处女的人大多缺乏魅力
大多数情况下,很多一直做处女的女孩子并没有真正体味过爱情的滋味,没有经历过热烈的爱情的感受。导致其实这样的女孩子其实很多不懂爱情,甚至不懂得怎么去处理爱情上的事情。这样的女人在男人眼中缺乏魅力,经常很传统羞涩十分,没有浪漫的特征。在性技巧和营造一个征服男人的性氛围方面,就更不用提了。一方面男人不喜欢自己的女人过去和其他男人过多的关系,但另一个方面却是男人更希望自己的女人拥有十分优秀的性技巧。
如果没有一个经营爱情的能力和享受爱情的能力,那么这个女人即使到了婚嫁年龄准备找个男人嫁了,但却因为能力不足不断错过自己心仪的对象。这个时候女人在爱情上屡尝挫折一旦失去了信心,这种情况就会恶性循环。到后来就是保住了身子嫁了一个不中意的男人。
如果最后结婚的人都不是自己最中意的,那么为了这个男人一直做处女十分有这个必要,有待大家讨论。奉劝要做处女的人一定要多学习点东西,对爱情掌控自如。
男人大多没有处女情结,不值得刻意
这里想给大家说个故事,这个故事就是“坐怀不乱”这个典故。现代有社会学家分析,发生这个事情可能有三种情况,一是这个男人身体有问题;二是这个男人性取向有问题;三就是这个女人实在太丑。甚至有后来者分析说,晚上单独和一个女人相处一室,对这个女人做了事情就是犯罪,但要是连想做的念头都没,那就是更大的犯罪。说到底,七情六欲人之常情,大家都不要看得过重。
大多数男人是没有处女情结的。男人喜欢处女只是为了显示自己的占有欲,如果单纯从快乐的角度来看,男人并不喜欢和处女发生性,会觉得很没趣。现代社会里大多数男人都不是很在意自己的老婆是否是处女,因为自己大多也不是处男,除非这个老婆没有其他任何优点而言。男人只是不喜欢知道自己的女人属于过别人。
但要给女同胞们一个忠告:千万别让自己老公过多知道自己过去和前男友的亲密关系,更不要让其联想到过去和其他男人发生性的细节,否则男人都会很难受。
最后表达一个观点:鄙视自己不是处男却要找处女结婚的男人,但强烈鄙视修补处女膜去结婚的女人,支持这种女人一旦发现男人们将其立即开除。
幸福和是处女与否无关
这个现象分析起来不好分析,但可以反过来看这个问题。那就是婚姻幸福的人当中,这个做妻子的以前是否热烈地恋爱过没有关系,与和这个女人的观念和开放程度没关系。从某种角度来看,很多开放和在社会里混得走,活动能力十分强劲的人似乎下半辈子都过得很幸福,甚至不少嫁给了声名显赫的男人过着耀眼的生活。
相反地,很多传统的女人结婚后,过着传统软弱的生活,并不能保证幸福的婚姻和未来。因为男人的变化是不可确定的,离婚的人当中,传统的人也不少,甚至随着社会发展那种爹妈负责相亲的人离婚的会越来越多,这些情况下处女与否已经不再重要。
那么,单纯以为是处女了就幸福的女人是痴人做梦;而不是处女的女同胞来说,这个也不是决定因素,因为一个人是要看综合的很多方面。但为那些只为了自己的信仰,而不是为了男人看法保持处女身的女性表示敬仰。
做处女要付出很多代价
我有一个朋友,读书时一个女生宿舍的同学多次笑取她太传统了,在大学的环境里这种取笑有点讽刺的意思,简单说就是这个女人太落后了。这种优点在特定环境里反而成了缺点,一直感受到各方压力。如今的社会里能进行着柏拉图式精神恋爱的是凤毛麟角,非常之少。我这个朋友的经历一直让人觉得付出的代价太多,开始谈了好几个男朋友,结果都和我说,她没有哪个男朋友保持到三个月以上。很简单的道理,女人呢觉得你爱我,你就不能伤害我;男人呢觉得你爱我,你就影响相信我,愿意和做任何事情。而很多情况下,性的确有足于让大家更默契和更有感情,也能化解不少矛盾。男人单独和女朋友一起相处到一定程度都是情不自禁希望发生性关系,并非不对。
这样的结果就是,自尊心强的男人经常提出要求后被拒绝,自尊心受到很多伤害,自然对这个女孩子有很多抵触。到后来的失望、绝望。那么坚持做处女的人就会在这个过程里因为这个观念放弃一个又一个男人,男人也一个又一个离去。也许有一天,不排除这样的情况,当这个一直做处女的女孩子结婚后觉得很不幸福,或者觉得男人很不如别人,很怀念当初和其谈过恋爱的人,可惜已经错过。
除非是遇到其他一些特殊情况——要么这个女人是否优秀,在某些领域十分出众,最好是明星;要么遇到一个男人十分欣赏这个女人的很多方面,包括追求、信仰甚至地位等等,并在很多方面有一致的观点。否则怎么坚持下去,是个巨大的挑战。
一直当处女的人大多缺乏魅力
大多数情况下,很多一直做处女的女孩子并没有真正体味过爱情的滋味,没有经历过热烈的爱情的感受。导致其实这样的女孩子其实很多不懂爱情,甚至不懂得怎么去处理爱情上的事情。这样的女人在男人眼中缺乏魅力,经常很传统羞涩十分,没有浪漫的特征。在性技巧和营造一个征服男人的性氛围方面,就更不用提了。一方面男人不喜欢自己的女人过去和其他男人过多的关系,但另一个方面却是男人更希望自己的女人拥有十分优秀的性技巧。
如果没有一个经营爱情的能力和享受爱情的能力,那么这个女人即使到了婚嫁年龄准备找个男人嫁了,但却因为能力不足不断错过自己心仪的对象。这个时候女人在爱情上屡尝挫折一旦失去了信心,这种情况就会恶性循环。到后来就是保住了身子嫁了一个不中意的男人。
如果最后结婚的人都不是自己最中意的,那么为了这个男人一直做处女十分有这个必要,有待大家讨论。奉劝要做处女的人一定要多学习点东西,对爱情掌控自如。
男人大多没有处女情结,不值得刻意
这里想给大家说个故事,这个故事就是“坐怀不乱”这个典故。现代有社会学家分析,发生这个事情可能有三种情况,一是这个男人身体有问题;二是这个男人性取向有问题;三就是这个女人实在太丑。甚至有后来者分析说,晚上单独和一个女人相处一室,对这个女人做了事情就是犯罪,但要是连想做的念头都没,那就是更大的犯罪。说到底,七情六欲人之常情,大家都不要看得过重。
大多数男人是没有处女情结的。男人喜欢处女只是为了显示自己的占有欲,如果单纯从快乐的角度来看,男人并不喜欢和处女发生性,会觉得很没趣。现代社会里大多数男人都不是很在意自己的老婆是否是处女,因为自己大多也不是处男,除非这个老婆没有其他任何优点而言。男人只是不喜欢知道自己的女人属于过别人。
但要给女同胞们一个忠告:千万别让自己老公过多知道自己过去和前男友的亲密关系,更不要让其联想到过去和其他男人发生性的细节,否则男人都会很难受。
最后表达一个观点:鄙视自己不是处男却要找处女结婚的男人,但强烈鄙视修补处女膜去结婚的女人,支持这种女人一旦发现男人们将其立即开除。
早起者并不一定收获最多,但成功者一定是方向对,善管理风险的人。不管是寿国梁、曹军这样的无奈者,还是张树新、王峻涛这样的迷失者,其先发优势都已消失殆净。从“先驱”到“先烈”的转变,折射出的是市场的易
变性和持续成功的“无规律性”。这种“无规律性”既表现在你所需要坚持的东西上,也表现在日常的经营管理中。
8848和王峻涛的“失败”,曾经令绝大部分的专业人士对B2B产生了动摇;张树新、张征宇也并不是不知道迅速扩张的风险;邓国顺当然更是不会想到,专利保护竟然没有能够使自己快速成长。这些遗憾其实何止发生在这七个遗憾者的身上,绝大部分的行业跟随者甚至那些成功者,也多多少少存在着的问题。但对比少数成功者的成功原因,可以得出这样一个结论:那些遗憾者真正遗憾的是,看到了机会,也抢先跑出去了,却没找对方向。
如果再去细究“没找对方向”的原因,那么基本上可以认为,与“智造中国”中的那10个代表人物相比,“遗憾人物”的最大缺陷就是资源利用和行业整合能力的欠缺,以及企业管理能力的平庸。
NO.1 张树新:不是我不明白Internet变化快
第一个做Internet
第一个从Internet中赚到了“大钱”
是明星也是流星
如果要有“中国硅谷”之称的中关村的那些生意人去回忆,1996年是什么给他们留下的印象最深?那么答案肯定不是“联想在那一年拿下中国pc市场第一”这件事,而是号称中国互联网代名词的瀛海威公司,以及它的那句广告词--“中国人离信息高速公路还有多远?向北1500米。”
当时,42岁的柳传志已经带领联想的创业者辛辛苦苦走过了的12年,才开始进入辉煌期。可这一过程对于张树新来说,却只需要2年的时间。
没有人怀疑作为中国科技大学历史上第一位女学生会主席、当时已经33岁的张树新的能力。她在一年前(即1995年9月)将一台486电脑联上网时,所有的中国人都把她视为“中国互联网的传教士”。在当时网友的嘴里,“瀛海威=网络=Internet!”而在这些网友中间,还有丁磊、王峻涛、王志东的影子。
但在出现了1个月最高净收益900万元的可喜业绩之后,瀛海威这个曾经令中国无数网民无比振奋的名字,却早早地在1997年就出现了衰落:公开消息显示,在1996年至1997年间,张树新带领的瀛海威进行了大量的网络接入站点投资,但因中国电信的介入及接入价格的走低,瀛海威公司出现了大幅的亏损
转到第二年(即1998年),干了3年的张树新正式辞职,离开了瀛海威。从此,张树新和这家曾是互联网代名词的公司一样,淡出了人们的视线。
NO.2 王峻涛:B2B没有选择痴情的我
第一个站出来说B2B,做B2B
当时很受VC的青睐
竞争对手做成了世界NO.1
1997年,当33岁的杭州人马云从自己做的“中国黄页”网站上脱身出来,参与到原外经贸部做的网上商品交易市场中去时,比他年长2岁、号称“中国电子商务第一人”的福州人王峻涛正在用他的后来很出名的网名“老榕”,发帖子呢,其中包括那个也许是他一生中最出名的帖子--《大连金州不相信眼泪》。
两年过去,马云创立了名字带点外国神话色彩的阿里巴巴网,而王峻涛则使用了一个在中国妇孺皆知的名字--珠穆朗玛(8848),这又好记又吉利。后来的发展也印证了这个名字的优势:很快,所有的人包括VC都记住了8848这个中国B2B的标杆;但吉利的名字没有让8848享受到幸运,就连已经大大出名的“老榕”王峻涛,也在经历了8848的股东与管理层之变后,辗转到MY8848,igo5,最后还是落在B2B网站6688上。
这些名字看起来还是脱不了B2B和8848的影子,但就像B2B被提及的数次越来越少一样,“老榕”王峻涛这个名字也越来越少有人提及;反之,曾经比他名气小得多的马云和阿里巴巴却在2003年像横空出世一样震惊了世界,因为马云称阿里巴巴已经做到了世界B2B网站的NO.1。到此时人们彻底对王峻涛和6688失去了兴趣。
NO.3 曹军:努力到了,只欠东风
拥有WAPI标准下的部分核心 产品的技术
我国最终采用了WI-Fi联盟的标准
尽管两家公司前途迷茫的原因都跟WAPI标准强制执行政策的失效有关,但是跟具有18年海外工作经历的寿国梁不同,曹军一直在国内做生意,而且本身并不是做技术出身。但也许就是因为并非技术出身,曹军显然比寿国梁更善于从市场中去找机会。显然,曹军一开始就已经敏锐地意识到,通过政府的力量来推进他所看重的西安电子科技大学的无线局域网研究成果,也许是一个比较有利的途径。
事实也证明了曹军卓越的政府公关能力:从2000年底开始成立西电捷通公司,到2001年6月信息产业部下达WAPI标准起草任务,同年11月西电捷通发起中国宽带无线IP标准工作组并完成标准草案,再到2003年5月WAPI正式颁布,2003年11月26日出台WAPI标准强制执行政策,这中间每一步都不超过半年的时间。
经过这一系列的推进,此时的西电捷通已经开发出基于WAPI标准下的关键设备--WLAN网卡、无线接入点等成型产品。可惜的是,这一怎么看都是利好的生意,当然也包括曹军的西电捷通公司,最终因人所共知的原因还是没有发展起来。
NO.4 寿国梁:没成功,不是自己的错
拥有WAPI核心芯片组技术
我国最终采用了Wi-Fi联盟的标准
如果没有2004年初的那场“中外WAPI标准之争”的风波,也许到现在为止还都没几个IT从业者听说过寿国梁和六合万通的名字。跟其他回国创业者--如中星微邓中翰类似,在日本一呆就是18年的绍兴人寿国梁在日本已经有了一定的事业基础。1999年底回到国内时,他同样也选择了在有中国IT硅谷之称的中关村创业。而且,比邓大6岁的寿国梁也选择了自己已经在国外开展过的业务--芯片设计。然而,与邓中翰稍有不同的是,寿国梁设计的“万通芯”的竞争对手是INTEL,而邓的“星光”芯则不是。这一点在当初可能并没有引起寿国梁足够的重视。
在2004年4月22日之前,包括寿国梁在内的所有国内业界人士都愿意相信,2004年6月1日是英特尔用于WI-Fi设备的迅驰移动芯片在中国市场的死期,因为我国政府有关部门在2003年11月26日已经出台相关政策,准备在这天强制执行与INTEL的WI-Fi联盟标准不同的中国标准--WAPI。而六合万通的“万通1号”是WAPI标准下的核心芯片组。如果这个“事实”成真,没有人会怀疑“万通1号”和六合万通的前景。
但后来的事实是,以全球芯片巨头INTEL为代表的WI-Fi联盟在这场并不太长的标准之争中最终获胜。
NO.5 邓国顺:拿了专利丢了市场
开发出世界第一款U盘
曾占据一半的中国U盘市场
很快被竞争对手超越
1993年,一个清华大学毕业的江苏人在中关村创立了自己的公司--华旗资讯;同年,一个从中山大学毕业后在联想干了一段时间的湖南人,于当年去新加坡进了Philips公司工作。这两个在当时看来前途各异、毫不相干的人,却在后来成了针锋相对的竞争对手。后者虽然占有天时之优势,却最终没有战胜前者。这两个人,前者就是排在本刊“智造中国十大人物”中的冯军,后者就是拥有最多闪存(也称U盘)专利的深圳朗科公司总裁邓国顺。
1999年,32岁的邓国顺从新加坡回国后,和成晓华研发出了替代传统软盘的革命性产品--世界第一款闪存盘。而小邓国顺两岁的冯军正续“小太阳键盘”他做了6年的这个电脑外设生意。与邓国顺不同,尽管冯军已经在事业上有了积累,也有了自己的品牌“爱国者”。但就革命性的技术来说,冯军一个也没有;而邓国顺不但有,并且仅U盘申请的专利就已经超过150件。
专利的优势也阶段性地体现到了朗科的业绩上。据朗科提供的数据,2003年该公司占据了国内U盘市场50%,销量达150多万个。但到了2004年,据CCID的数据,华旗的U盘市场份额达31.2%,朗科只占18.1%。在这一变化的背后,是发生在2004年中的热闹一时的朗科诉华旗、索尼等多家公司侵犯专利权案。
尽管朗科在今年4月初停产1.1标准U盘全面升级到2.0标准的做法,目的仍是希望通过专利权保护来获得市场的认可,但这时的U盘市场早已不是3年前的样子,朗科的声音也显得越来越弱。
NO.6 张征宇:我一直没有转过弯来
商务通“成了PDA代名词
做两年就做到10亿元年营业额
智能手机领域的弱者
呼机,手机,商务通,一个都不能少。”这句广告词成就了一家公司--恒基伟业,和一个40岁才创业的博士--杭州人张征宇。即使用“一夜暴福”来形容张征宇也不为过:刚成立两年的公司2000年的现金收入就达10个亿。即使现在的中国网络公司都没有这样的业绩。
但这家公司跟这句广告词的命运有惊人的相似处:作为PDA代名词的“商务通”,跟呼机一样似乎又在一夜之间就消失了;而手机却越来越热门,甚至中国货一度抢过了洋手机的风头。当张征宇看中智能手机的机会时,小他3岁的多普达公司总裁杨兴平还在美国做研究呢。
遗憾的是,在2000年“商务通”达到巅峰之后,价格战和消费者兴趣的转移,使“商务通”很快成为一种过季商品。但张征宇显然不甘于PDA的没落:他毅然决然地在广西北海兴建恒基伟业工业园,并计划5年内投资18亿。但销售额的直线下滑已经让张征宇无力四顾,对智能手机这一新业务的投入也就成为了空中楼阁。
而从2002年开始做智能手机的杨兴平,在经历了3年的艰苦开拓之后,终于让多普达在智能手机领域站稳了脚跟。眼下,既没人怀念PDA和“商务通”,也没有人知道恒基伟业的智能手机。
NO.7 边晓春:聪明总被聪明误
创立中国第一个游戏公司前导软件有限公司
第一个接受风险投资的游戏公司
创造中国游戏的多个第一,包括第一个倒下当今天人们在谈论年轻的陈天桥及其天文数字一样的财富时,没有多少人能想起边晓春和中国第一个游戏公司--前导。
1996年,尽管国内第一部策略游戏《官渡》的销量不尽如人意,但却从海外收回了7万美元的版税。同年底,前导引进《命令与征服》并创下一周内出货1万套且全部结款的记录。1997年,《赤壁》与联想电脑捆绑销售,率先突破了国产游戏软件销量10万套的大关。
但过度发展不但没有创造神话,反而要了前导软件公司的命。《赤壁》投入97万元,《水浒》投入145万元,《齐天大圣》成本则升至170万元。成本如此迅猛增长使得游戏开发风险加大,1998年下半年公司就已经负债1000万元。
此外,前导软件曾被视为成功吸引风险投资的典范。1998年初,边晓春在谈到他对风险投资的理解时打了个比方:“如果你手里有50元钱,通常的做法是投入市场,得到10元利润,再用60元继续经营。我的做法是,只拿出30元投入一个主要市场,而将另外两个10元分别开始两个新项目,然后把这3个项目捆在一起去寻找投资者,用得到的200元投资去发展这3个项目。”
然而仅仅过了半年,前导公司就宣布退出游戏市场。一年后,边晓春对自己的“50元理论”作了反思:“我确实错了,因为这种理念并未使前导公司摆脱危机。那么到底错在哪里呢?我目前的认识是:如果你把那30元都赔光了,也就不会有人给你的三个项目投资了,尽管这三个项目在未来确实可能是好项目。也就是说,作为风险投资企业,虽然必须也只能从资本市场得到快速发展的动力;然而,只有产品市场,才是企业的立身之本。”
变性和持续成功的“无规律性”。这种“无规律性”既表现在你所需要坚持的东西上,也表现在日常的经营管理中。
8848和王峻涛的“失败”,曾经令绝大部分的专业人士对B2B产生了动摇;张树新、张征宇也并不是不知道迅速扩张的风险;邓国顺当然更是不会想到,专利保护竟然没有能够使自己快速成长。这些遗憾其实何止发生在这七个遗憾者的身上,绝大部分的行业跟随者甚至那些成功者,也多多少少存在着的问题。但对比少数成功者的成功原因,可以得出这样一个结论:那些遗憾者真正遗憾的是,看到了机会,也抢先跑出去了,却没找对方向。
如果再去细究“没找对方向”的原因,那么基本上可以认为,与“智造中国”中的那10个代表人物相比,“遗憾人物”的最大缺陷就是资源利用和行业整合能力的欠缺,以及企业管理能力的平庸。
NO.1 张树新:不是我不明白Internet变化快
第一个做Internet
第一个从Internet中赚到了“大钱”
是明星也是流星
如果要有“中国硅谷”之称的中关村的那些生意人去回忆,1996年是什么给他们留下的印象最深?那么答案肯定不是“联想在那一年拿下中国pc市场第一”这件事,而是号称中国互联网代名词的瀛海威公司,以及它的那句广告词--“中国人离信息高速公路还有多远?向北1500米。”
当时,42岁的柳传志已经带领联想的创业者辛辛苦苦走过了的12年,才开始进入辉煌期。可这一过程对于张树新来说,却只需要2年的时间。
没有人怀疑作为中国科技大学历史上第一位女学生会主席、当时已经33岁的张树新的能力。她在一年前(即1995年9月)将一台486电脑联上网时,所有的中国人都把她视为“中国互联网的传教士”。在当时网友的嘴里,“瀛海威=网络=Internet!”而在这些网友中间,还有丁磊、王峻涛、王志东的影子。
但在出现了1个月最高净收益900万元的可喜业绩之后,瀛海威这个曾经令中国无数网民无比振奋的名字,却早早地在1997年就出现了衰落:公开消息显示,在1996年至1997年间,张树新带领的瀛海威进行了大量的网络接入站点投资,但因中国电信的介入及接入价格的走低,瀛海威公司出现了大幅的亏损
转到第二年(即1998年),干了3年的张树新正式辞职,离开了瀛海威。从此,张树新和这家曾是互联网代名词的公司一样,淡出了人们的视线。
NO.2 王峻涛:B2B没有选择痴情的我
第一个站出来说B2B,做B2B
当时很受VC的青睐
竞争对手做成了世界NO.1
1997年,当33岁的杭州人马云从自己做的“中国黄页”网站上脱身出来,参与到原外经贸部做的网上商品交易市场中去时,比他年长2岁、号称“中国电子商务第一人”的福州人王峻涛正在用他的后来很出名的网名“老榕”,发帖子呢,其中包括那个也许是他一生中最出名的帖子--《大连金州不相信眼泪》。
两年过去,马云创立了名字带点外国神话色彩的阿里巴巴网,而王峻涛则使用了一个在中国妇孺皆知的名字--珠穆朗玛(8848),这又好记又吉利。后来的发展也印证了这个名字的优势:很快,所有的人包括VC都记住了8848这个中国B2B的标杆;但吉利的名字没有让8848享受到幸运,就连已经大大出名的“老榕”王峻涛,也在经历了8848的股东与管理层之变后,辗转到MY8848,igo5,最后还是落在B2B网站6688上。
这些名字看起来还是脱不了B2B和8848的影子,但就像B2B被提及的数次越来越少一样,“老榕”王峻涛这个名字也越来越少有人提及;反之,曾经比他名气小得多的马云和阿里巴巴却在2003年像横空出世一样震惊了世界,因为马云称阿里巴巴已经做到了世界B2B网站的NO.1。到此时人们彻底对王峻涛和6688失去了兴趣。
NO.3 曹军:努力到了,只欠东风
拥有WAPI标准下的部分核心 产品的技术
我国最终采用了WI-Fi联盟的标准
尽管两家公司前途迷茫的原因都跟WAPI标准强制执行政策的失效有关,但是跟具有18年海外工作经历的寿国梁不同,曹军一直在国内做生意,而且本身并不是做技术出身。但也许就是因为并非技术出身,曹军显然比寿国梁更善于从市场中去找机会。显然,曹军一开始就已经敏锐地意识到,通过政府的力量来推进他所看重的西安电子科技大学的无线局域网研究成果,也许是一个比较有利的途径。
事实也证明了曹军卓越的政府公关能力:从2000年底开始成立西电捷通公司,到2001年6月信息产业部下达WAPI标准起草任务,同年11月西电捷通发起中国宽带无线IP标准工作组并完成标准草案,再到2003年5月WAPI正式颁布,2003年11月26日出台WAPI标准强制执行政策,这中间每一步都不超过半年的时间。
经过这一系列的推进,此时的西电捷通已经开发出基于WAPI标准下的关键设备--WLAN网卡、无线接入点等成型产品。可惜的是,这一怎么看都是利好的生意,当然也包括曹军的西电捷通公司,最终因人所共知的原因还是没有发展起来。
NO.4 寿国梁:没成功,不是自己的错
拥有WAPI核心芯片组技术
我国最终采用了Wi-Fi联盟的标准
如果没有2004年初的那场“中外WAPI标准之争”的风波,也许到现在为止还都没几个IT从业者听说过寿国梁和六合万通的名字。跟其他回国创业者--如中星微邓中翰类似,在日本一呆就是18年的绍兴人寿国梁在日本已经有了一定的事业基础。1999年底回到国内时,他同样也选择了在有中国IT硅谷之称的中关村创业。而且,比邓大6岁的寿国梁也选择了自己已经在国外开展过的业务--芯片设计。然而,与邓中翰稍有不同的是,寿国梁设计的“万通芯”的竞争对手是INTEL,而邓的“星光”芯则不是。这一点在当初可能并没有引起寿国梁足够的重视。
在2004年4月22日之前,包括寿国梁在内的所有国内业界人士都愿意相信,2004年6月1日是英特尔用于WI-Fi设备的迅驰移动芯片在中国市场的死期,因为我国政府有关部门在2003年11月26日已经出台相关政策,准备在这天强制执行与INTEL的WI-Fi联盟标准不同的中国标准--WAPI。而六合万通的“万通1号”是WAPI标准下的核心芯片组。如果这个“事实”成真,没有人会怀疑“万通1号”和六合万通的前景。
但后来的事实是,以全球芯片巨头INTEL为代表的WI-Fi联盟在这场并不太长的标准之争中最终获胜。
NO.5 邓国顺:拿了专利丢了市场
开发出世界第一款U盘
曾占据一半的中国U盘市场
很快被竞争对手超越
1993年,一个清华大学毕业的江苏人在中关村创立了自己的公司--华旗资讯;同年,一个从中山大学毕业后在联想干了一段时间的湖南人,于当年去新加坡进了Philips公司工作。这两个在当时看来前途各异、毫不相干的人,却在后来成了针锋相对的竞争对手。后者虽然占有天时之优势,却最终没有战胜前者。这两个人,前者就是排在本刊“智造中国十大人物”中的冯军,后者就是拥有最多闪存(也称U盘)专利的深圳朗科公司总裁邓国顺。
1999年,32岁的邓国顺从新加坡回国后,和成晓华研发出了替代传统软盘的革命性产品--世界第一款闪存盘。而小邓国顺两岁的冯军正续“小太阳键盘”他做了6年的这个电脑外设生意。与邓国顺不同,尽管冯军已经在事业上有了积累,也有了自己的品牌“爱国者”。但就革命性的技术来说,冯军一个也没有;而邓国顺不但有,并且仅U盘申请的专利就已经超过150件。
专利的优势也阶段性地体现到了朗科的业绩上。据朗科提供的数据,2003年该公司占据了国内U盘市场50%,销量达150多万个。但到了2004年,据CCID的数据,华旗的U盘市场份额达31.2%,朗科只占18.1%。在这一变化的背后,是发生在2004年中的热闹一时的朗科诉华旗、索尼等多家公司侵犯专利权案。
尽管朗科在今年4月初停产1.1标准U盘全面升级到2.0标准的做法,目的仍是希望通过专利权保护来获得市场的认可,但这时的U盘市场早已不是3年前的样子,朗科的声音也显得越来越弱。
NO.6 张征宇:我一直没有转过弯来
商务通“成了PDA代名词
做两年就做到10亿元年营业额
智能手机领域的弱者
呼机,手机,商务通,一个都不能少。”这句广告词成就了一家公司--恒基伟业,和一个40岁才创业的博士--杭州人张征宇。即使用“一夜暴福”来形容张征宇也不为过:刚成立两年的公司2000年的现金收入就达10个亿。即使现在的中国网络公司都没有这样的业绩。
但这家公司跟这句广告词的命运有惊人的相似处:作为PDA代名词的“商务通”,跟呼机一样似乎又在一夜之间就消失了;而手机却越来越热门,甚至中国货一度抢过了洋手机的风头。当张征宇看中智能手机的机会时,小他3岁的多普达公司总裁杨兴平还在美国做研究呢。
遗憾的是,在2000年“商务通”达到巅峰之后,价格战和消费者兴趣的转移,使“商务通”很快成为一种过季商品。但张征宇显然不甘于PDA的没落:他毅然决然地在广西北海兴建恒基伟业工业园,并计划5年内投资18亿。但销售额的直线下滑已经让张征宇无力四顾,对智能手机这一新业务的投入也就成为了空中楼阁。
而从2002年开始做智能手机的杨兴平,在经历了3年的艰苦开拓之后,终于让多普达在智能手机领域站稳了脚跟。眼下,既没人怀念PDA和“商务通”,也没有人知道恒基伟业的智能手机。
NO.7 边晓春:聪明总被聪明误
创立中国第一个游戏公司前导软件有限公司
第一个接受风险投资的游戏公司
创造中国游戏的多个第一,包括第一个倒下当今天人们在谈论年轻的陈天桥及其天文数字一样的财富时,没有多少人能想起边晓春和中国第一个游戏公司--前导。
1996年,尽管国内第一部策略游戏《官渡》的销量不尽如人意,但却从海外收回了7万美元的版税。同年底,前导引进《命令与征服》并创下一周内出货1万套且全部结款的记录。1997年,《赤壁》与联想电脑捆绑销售,率先突破了国产游戏软件销量10万套的大关。
但过度发展不但没有创造神话,反而要了前导软件公司的命。《赤壁》投入97万元,《水浒》投入145万元,《齐天大圣》成本则升至170万元。成本如此迅猛增长使得游戏开发风险加大,1998年下半年公司就已经负债1000万元。
此外,前导软件曾被视为成功吸引风险投资的典范。1998年初,边晓春在谈到他对风险投资的理解时打了个比方:“如果你手里有50元钱,通常的做法是投入市场,得到10元利润,再用60元继续经营。我的做法是,只拿出30元投入一个主要市场,而将另外两个10元分别开始两个新项目,然后把这3个项目捆在一起去寻找投资者,用得到的200元投资去发展这3个项目。”
然而仅仅过了半年,前导公司就宣布退出游戏市场。一年后,边晓春对自己的“50元理论”作了反思:“我确实错了,因为这种理念并未使前导公司摆脱危机。那么到底错在哪里呢?我目前的认识是:如果你把那30元都赔光了,也就不会有人给你的三个项目投资了,尽管这三个项目在未来确实可能是好项目。也就是说,作为风险投资企业,虽然必须也只能从资本市场得到快速发展的动力;然而,只有产品市场,才是企业的立身之本。”
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#define NEWFILE (O_WRONLY|O_CREAT|O_TRUNC)
#define SIZE 80
int write_buffer(int fd,char *buf,int count);
void case8(int i,char *j);
void default8();
int main(int argc, char * argv[])
{
int outfile,i;
char filename[]={"test.dat"};
char buffer[SIZE];
char *buf;
char *argv2;
printf("%d\n\n\n",argc);
if(argc<=2)
{
default8();
exit(0);
}
switch(atoi(argv[1]))
{
case 0:
{
case8(atoi(argv[1]),argv[2]);
break;
}
case 1:
{
argv2=argv[2];
case8(atoi(argv[1]),argv[2]);
break;
}
case 2:
{
argv2=argv[2];
case8(atoi(argv[1]),argv[2]);
break;
}
case 3:
{
argv2=argv[2];
case8(atoi(argv[1]),argv[2]);
break;
}
default:
{
default8();
}
}
}
/*
for(i=0;i<argc;i++)
{
printf("the argv's value is %d\n",argc);
printf("argv[%d]=%s\n",i,argv[i]);
}
*/
/*
buf = argv[1];
if((outfile=open(filename,NEWFILE,0640))==-1)
{
printf("ERRO,OPEN FILE FAILED!\n");
exit(255);
}
//printf("%s",buf);
fgets(buffer,SIZE-1,stdin);
printf("%s\n",buffer);
if(write_buffer(outfile,buffer,SIZE)==-1)
{
printf("ERROR,WRITE FAILED:\n",strerror(errno));
exit(255);
}
close(outfile);
return 0;
int write_buffer(int fd,char *buf,int count)
{
int n;
n=write(fd,buf,strlen(buf));
return 1;
}
*/
void case8(int i,char *j){
printf("you enter is ./a.o %d %s\n",i,j);
}
void default8(){
printf("which you put a not suitable for our need:");
printf("填加测试企业:../bin/example 0\n");
printf("按照企业id删除企业:../bin/example 1 100071\n");
printf("按照企业名称删除企业:../bin/example 2 \"my enterprise name\"\n");
printf("按照企业名称取得g级邮箱标识:../bin/example 3 \"my enterprise name\"\n");
printf("按照企业id取得g级邮箱标识:../bin/example 4 \"my enterprise ID\"\n");
printf("other:待补充\n");
}
#include <sys/stat.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#define NEWFILE (O_WRONLY|O_CREAT|O_TRUNC)
#define SIZE 80
int write_buffer(int fd,char *buf,int count);
void case8(int i,char *j);
void default8();
int main(int argc, char * argv[])
{
int outfile,i;
char filename[]={"test.dat"};
char buffer[SIZE];
char *buf;
char *argv2;
printf("%d\n\n\n",argc);
if(argc<=2)
{
default8();
exit(0);
}
switch(atoi(argv[1]))
{
case 0:
{
case8(atoi(argv[1]),argv[2]);
break;
}
case 1:
{
argv2=argv[2];
case8(atoi(argv[1]),argv[2]);
break;
}
case 2:
{
argv2=argv[2];
case8(atoi(argv[1]),argv[2]);
break;
}
case 3:
{
argv2=argv[2];
case8(atoi(argv[1]),argv[2]);
break;
}
default:
{
default8();
}
}
}
/*
for(i=0;i<argc;i++)
{
printf("the argv's value is %d\n",argc);
printf("argv[%d]=%s\n",i,argv[i]);
}
*/
/*
buf = argv[1];
if((outfile=open(filename,NEWFILE,0640))==-1)
{
printf("ERRO,OPEN FILE FAILED!\n");
exit(255);
}
//printf("%s",buf);
fgets(buffer,SIZE-1,stdin);
printf("%s\n",buffer);
if(write_buffer(outfile,buffer,SIZE)==-1)
{
printf("ERROR,WRITE FAILED:\n",strerror(errno));
exit(255);
}
close(outfile);
return 0;
int write_buffer(int fd,char *buf,int count)
{
int n;
n=write(fd,buf,strlen(buf));
return 1;
}
*/
void case8(int i,char *j){
printf("you enter is ./a.o %d %s\n",i,j);
}
void default8(){
printf("which you put a not suitable for our need:");
printf("填加测试企业:../bin/example 0\n");
printf("按照企业id删除企业:../bin/example 1 100071\n");
printf("按照企业名称删除企业:../bin/example 2 \"my enterprise name\"\n");
printf("按照企业名称取得g级邮箱标识:../bin/example 3 \"my enterprise name\"\n");
printf("按照企业id取得g级邮箱标识:../bin/example 4 \"my enterprise ID\"\n");
printf("other:待补充\n");
}
若映射成功则返回映射区的内存起始地址,否则返回MAP_FAILED(-1),错误原因存于errno 中。
错误代码
EBADF 参数fd 不是有效的文件描述词
EACCES 存取权限有误。如果是MAP_PRIVATE 情况下文件必须可读,使用MAP_SHARED则要有PROT_WRITE以及该文件要能写入。
EINVAL 参数start、length 或offset有一个不合法。
EAGAIN 文件被锁住,或是有太多内存被锁住。
ENOMEM 内存不足。
范例
/* 利用mmap()来读取/etc/passwd 文件内容*/
#include
#include
#include
#include
#include
main()
{
int fd;
void *start;
struct stat sb;
fd=open(“/etc/passwd”,O_RDONLY); /*打开/etc/passwd*/
fstat(fd,&sb); /*取得文件大小*/
start=mmap(NULL,sb.st_size,PROT_READ,MAP_PRIVATE,fd,0);
if(start= = MAP_FAILED) /*判断是否映射成功*/
return;
printf(“%s”,start);
munma(start,sb.st_size); /*解除映射*/ freebsd下这一行报错。。
closed(fd);
}
错误代码
EBADF 参数fd 不是有效的文件描述词
EACCES 存取权限有误。如果是MAP_PRIVATE 情况下文件必须可读,使用MAP_SHARED则要有PROT_WRITE以及该文件要能写入。
EINVAL 参数start、length 或offset有一个不合法。
EAGAIN 文件被锁住,或是有太多内存被锁住。
ENOMEM 内存不足。
范例
/* 利用mmap()来读取/etc/passwd 文件内容*/
#include
#include
#include
#include
#include
main()
{
int fd;
void *start;
struct stat sb;
fd=open(“/etc/passwd”,O_RDONLY); /*打开/etc/passwd*/
fstat(fd,&sb); /*取得文件大小*/
start=mmap(NULL,sb.st_size,PROT_READ,MAP_PRIVATE,fd,0);
if(start= = MAP_FAILED) /*判断是否映射成功*/
return;
printf(“%s”,start);
munma(start,sb.st_size); /*解除映射*/ freebsd下这一行报错。。
closed(fd);
}
fgets问题
fgets(由文件中读取一字符串)
相关函数
open,fread,fscanf,getc
表头文件
include
定义函数
har * fgets(char * s,int size,FILE * stream);
函数说明
fgets()用来从参数stream所指的文件内读入字符并存到参数s所指的内存空间,直到出现换行字符、读到文件尾或是已读了size-1个字符为止,最后会加上NULL作为字符串结束。
返回值
gets()若成功则返回s指针,返回NULL则表示有错误发生。
范例
#include
main()
{
char s[80];
fputs(fgets(s,80,stdin),stdout);
}
执行
this is a test /*输入*/
this is a test /*输出*/
这里是函数的说明
代码:
--------------------------------------------------------------------------------
typedef char (*ARRAYPTR)[10];
typedef char ARRAY[10];
main(void)
{
ARRAYPTR ap;
ARRAY a;
printf("Input a string : ");
fgets(a,100,stdin);
ap=&a;
printf("\nECHO : %s\n",ap);
}
100就是size,你上面已经写了。stdin是“标准输入”,一般指终端输入(键盘输入)。这一句的意思是从键盘输入一个字符串,这个字符串加上换行符不能超过100个字节,因此实际最多可以输入99个字符。
fgets(由文件中读取一字符串)
相关函数
open,fread,fscanf,getc
表头文件
include
定义函数
har * fgets(char * s,int size,FILE * stream);
函数说明
fgets()用来从参数stream所指的文件内读入字符并存到参数s所指的内存空间,直到出现换行字符、读到文件尾或是已读了size-1个字符为止,最后会加上NULL作为字符串结束。
返回值
gets()若成功则返回s指针,返回NULL则表示有错误发生。
范例
#include
main()
{
char s[80];
fputs(fgets(s,80,stdin),stdout);
}
执行
this is a test /*输入*/
this is a test /*输出*/
这里是函数的说明
代码:
--------------------------------------------------------------------------------
typedef char (*ARRAYPTR)[10];
typedef char ARRAY[10];
main(void)
{
ARRAYPTR ap;
ARRAY a;
printf("Input a string : ");
fgets(a,100,stdin);
ap=&a;
printf("\nECHO : %s\n",ap);
}
100就是size,你上面已经写了。stdin是“标准输入”,一般指终端输入(键盘输入)。这一句的意思是从键盘输入一个字符串,这个字符串加上换行符不能超过100个字节,因此实际最多可以输入99个字符。
1、人之所以痛苦,在于追求错误的东西。
2、与其说是别人让你痛苦,不如说自己的修养不够。
3、如果你不给自己烦恼,别人也永远不可能给你烦恼。因为你自己的 内心,你放不下。
4、好好的管教你自己,不要管别人。
5、不宽恕众生,不原谅众生,是苦了你自己。
6、别说别人可怜,自己更可怜,自己修行又如何?自己又懂得人生多少?
7、福报不够的人,就会常常听到是非;福报够的人,从来就没听到过是非。
8、你永远要感谢给你逆境的众生。
9、你永远要宽恕众生,不论他有多坏,甚至他伤害过你,你一定要放下,才能得到真正的快乐。
10、这个世界本来就是痛苦的,没有例外的。
11、当你快乐时,你要想,这快乐不是永恒的。当你痛苦时你要想这痛苦也不是永恒的。
12、认识自己,降伏自己,改变自己,才能改变别人。
13、不要浪费你的生命,在你一定会后悔的地方上。
14、你什么时候放下,什么时候就没有烦恼。
15、每一种创伤,都是一种成熟。
16、当你知道迷惑时,并不可怜, 当你不知道迷惑时,才是最可怜的。
17、狂妄的人有救,自卑的人没有救。
18、你不要一直不满人家,你应该一直检讨自己才对。不满人家,是苦了你自己。
19、你要包容那些意见跟你不同的人,这样子日子比较好过。你要是一直想改变他,那样子你会很痛苦。要学学怎样忍受他才是。你要学学怎样包容他才是。
20、承认自己的伟大,就是认同自己的愚疑。
21、一个人如果不能从内心去原谅别人,那他就永远不会心安理得。
22、心中装满着自己的看法与想法的人,永远听不见别人的心声。
23、毁灭人只要一句话,培植一个人却要千句话,请你多口下留情。
24、当你劝告别人时,若不顾及别人的自尊心,那么再好的言语都没有用的。
25、不要在你的智慧中夹杂着傲慢。不要使你的谦虚心缺乏智慧。
26、根本不必回头去看咒骂你的人是谁?如果有一条疯狗咬你一口,难道你也要趴下去反咬他一口吗?
27、忌妒别人,不会给自己增加任何的好处。忌妒别人,也不可能减少别人的成就。
28、永远不要浪费你的一分一秒,去想任何你不喜欢的人。
29、多少人要离开这个世间时,都会说出同一句话,这世界真是无奈与凄凉啊!
30、恋爱不是慈善事业,不能随便施舍的。感情是没有公式,没有原则,没有道理可循的。可是人们至死都还在执著与追求。
31、请你用慈悲心和温和的态度,把你的不满与委屈说出来,别人就容易接受。
32、创造机会的人是勇者。等待机会的人是愚者。
33、能说不能做,不是真智慧。
34、多用心去倾听别人怎么说,不要急着表达你自己的看法。
35、同样的瓶子,你为什么要装毒药呢?同样的心理,你为什么要充满着烦恼呢?
36、得不到的东西,我们会一直以为他是美好的,那是因为你对他了解太少,没有时间与他相处在一起。当有一天,你深入了解后,你会发现原不是你想像中的那么美好。
37、这个世间只有圆滑,没有圆满的。
38、活着一天,就是有福气,就该珍惜。当我哭泣我没有鞋子穿的时候,我发现有人却没有脚。
39、多一分心力去注意别人,就少一分心力反省自己,你懂吗?
40、欲知世上刀兵劫,但听屠门夜半声。不要光埋怨自己多病,灾祸横生,多看看横死在你刀下的众生又有多少?
41、憎恨别人对自己是一种很大的损失。
42、每一个人都拥有生命,但并非每个人都懂得生命,乃至于珍惜生命。不了解生命的人,生命对他来说,是一种惩罚。
43、自以为拥有财富的人,其实是被财富所拥有。
44、情执是苦恼的原因,放下情执,你才能得到自在。
45、随缘不是得过且过,因循苟且,而是尽人事听天命。
46、不要太肯定自己的看法,这样子比较少后悔。
47、当你对自己诚实的时候,世界上没有人能够欺骗得了你。
48、用伤害别人的手段来掩饰自己缺点的人,是可耻的。
49、内心充满忌妒,心中不坦白,言语不正的人,不能算是一位五官端正的人。
50、多讲点笑话,以幽默的态度处事,这样子日子会好过一点。
51、活在别人的掌声中,是禁不起考验的人。
52、不要刻意去猜测他人的想法,如果你没有智慧与经验的正确判断,通常都会有错误的。
53、要了解一个人,只需要看他的出发点与目的地是否相同,就可以知道他是否真心的。
54、人生的真理,只是藏在平淡无味之中。
55、不洗澡的人,硬擦香水是不会香的。名声与尊贵,是来自于真才实学的。有德自然香。
56、与其你去排斥它已成的事实,你不如去接受它。
57、逆境是成长必经的过程,能勇于接受逆境的人,生命就会日渐的茁壮。
58、你要感谢告诉你缺点的人。
59、能为别人设想的人,永远不寂寞。
60、如果你能像看别人缺点一样,如此准确般的发现自己的缺点,那么你的生命将会不平凡。
61、原谅别人,就是给自己心中留下空间,以便回旋。
62、时间总会过去的,让时间流走你的烦恼吧!
63、你硬要把单纯的事情看得很严重,那样子你会很痛苦。
64、永远扭曲别人善意的人,无药可救。
65、人不是坏的,只是习气罢了,每个人都有习气,只是深浅不同罢了。只要他有向善的心,能原谅的就原谅他,不要把他看做是坏人。
66、说一句谎话,要编造十句谎话来弥补,何苦呢?
67、其实爱美的人,只是与自己谈恋爱罢了。
68、世界上没有一个永远不被毁谤的人,也没有一个永远被赞叹的人。当你话多的时候,别人要批评你,当你话少的时候,别人要批评你,当你沈默的时候,别人还是要批评你。在这个世界上,没有一个人不被批评的。
69、夸奖我们,赞叹我们的,这都不是名师。会讲我们,指示我们的,这才是良师,有了他们我们才会进步。
70、你目前所拥有的都将随着你的死亡而成为他人的,那为何不现在就乐施给真正需要的人呢?
71、白白的过一天,无所事事,就像犯了窃盗罪一样。
72、沈默是毁谤最好的答覆。
73、对人恭敬,就是在庄严你自己。
74、拥有一颗无私的爱心,便拥有了一切。
75、仇恨永远不能化解仇恨,只有宽容才能化解仇恨,这是永恒的至理。
76、你接受比抱怨还要好,对于不可改变的事实,你除了接受以外,没有更好的办法了。
77、不要因为众生的愚疑,而带来了自己的烦恼。不要因为众生的无知,而痛苦了你自己。
78、别人讲我们不好,不用生气、难过。说我们好也不用高兴,这不好中有好,好中有坏,就看你会不会用?
79、当你的错误显露时,可不要发脾气,别以为任性或吵闹,可以隐藏或克服你的缺点。
80、不要常常觉得自己很不幸,世界上比我们痛苦的人还要多。
81、愚痴的人,一直想要别人了解他。有智慧的人,却努力的了解自己。
82、来是偶然的,走是必然的。所以你必须,随缘不变,不变随缘。 83、只要面对现实,你才能超越现实。
84、良心是每一个人最公正的审判官,你骗得了别人,却永远骗不了你自己的良心。
85、不懂得自爱的人,是没有能力去爱别人的。
86、做事就是在学做人而已。
87、有时候我们要冷静问问自已,我们在追求什么?我们活着为了什么?
88、不要因为小小的争执,远离了你至亲的好友,也不要因为小小的怨恨,忘记了别人的大恩。
89、勇于接受别人的批评,正好可以调整自己的缺点。
90、感谢上天我所拥有的,感谢上天我所没有的。
91、说话不要有攻击性,不要有杀伤力,不夸已能,不扬人恶,自然能化敌为友。
92、一个常常看别人缺点的人,自己本身就不够好,因为他没有时间检讨他自己。
93、是非天天有,不听自然无,是非天天有,不听还是有,是非天天有,看你怎么办?
94、如果你真的爱他,那么你必须容忍他部份的缺点。
95、要克服对死亡的恐惧,你必须要接受世上所有的人,都会死去的观念。
96、虽然你讨厌一个人,但却又能发觉他的优点好处,像这样子有修养的人,天下真是太少了。
97、诚实的面对你内心的矛盾和污点,不要欺骗你自己。
98、因果不曾亏欠过我们什么,所以请不要抱怨。
99、我们确实有如是的优点,但也要隐藏几分,这个叫做涵养。
100、大多数的人一辈子只做了三件事;自欺、欺人、被人欺。
101、太过于欣赏自己的人,不会去欣赏别人的优点。
102、心是最大的骗子,别人能骗你一时,而它却会骗你一辈子。
103、当你手中抓住一件东西不放时,你只能拥有这件东西,如果你肯放手,你就有机会选择别的。人的心若死执自己的观念,不肯放下,那么他的智慧也只能达到某种程度而已。
104、人家怕你,并不是一种福,人家欺你,并不是一种辱。
105、不是某人使我烦恼,而是我拿某人的言行来烦恼自己。
106、不要刻意去曲解别人的善意,你应当往好的地方想。
107、世上的事,不如己意者,那是当然的。
108、我的财富并不是因为我拥有很多,而是我要求的很少。
109、吃了就一定要拉,人一定要学会随缘放下,否则就会?便秘。
110、常以为别人在注意你,或希望别人注意你的人,会生活的比较烦恼。
111、我能为你煮东西,但我不能为你吃东西。各人吃饭是各人饱,各人生死是个人了。
112、看轻别人很容易,要摆平自己却很困难。
113、你只管活你自己的,不必去介意别人的扭曲与是非。
114、如果你准备结婚的话,告诉你一句非常重要的哲学名言,你一定要忍耐包容对方的缺点,世界上没有绝对幸福圆满的婚姻,幸福只是来自于无限的容忍与互相尊重。
115、如果你能够平平安安的渡过一天,那就是一种福气了。多少人在今天已经见不到明天的太阳,多少人在今天已经成了残废,多少人在今天已经失去了自由,多少人在今天已经家破人亡。
116、是非和得失,要到最后的结果,才能评定。
117、你不必和因果争吵,因果从来就不会误人。你也不必和命运争吵,命运它是最公平的审判官。
118、你有你的生命观,我有我的生命观,我不干涉你。只要我能,我就感化你。如果不能,那我就认命。
119、你希望掌握永恒,那你必须控制现在。
120、恶口永远不要出自于我们的口中,不管他有多坏,有多恶。你愈骂他,你的心就被污染了,你要想,他就是你的善知识。
121、当你明天开始生活的时候,有人跟你争执,你就让他赢,这个赢跟输,都只是文字的观念罢了。当你让对方赢,你并没有损失什么。所谓的赢,他有赢到什么?得到什么?所谓的输,你又输到什么?失去什么?
122、我们大部份的生命都浪费在文字语言的捉摸上。
123、你不要常常觉得自己很委曲,你应该要想,他对我这样已经很好了,这就是修行的功夫。
124、别人可以违背因果,别人可以害我们,打我们,毁谤我们。可是我们不能因此而憎恨别人,为什么?我们一定要保有一颗完整的 本性和一颗清净的心。
125、与任何人接触时,要常常问自己,我有什么对他有用?使他得益。如果我不能以个人的道德、学问和修持的力量,来使人受益,就等于欠了一份债。
126、如果一个人没有苦难的感受,就不容易对他人给予同情。你要学救苦救难的精神,就得先受苦受难。
127、一般人在遇到对方的权势大,财富大,气力大,在无可奈何的情形之下而忍,这算什么忍耐呢?真正的忍是,就算他欺负了你,对不住你,但他什么都不及你,你有足够的力量对付他,而你却能容忍他,认为他的本性和我一样,只是一时糊涂,或在恶劣的环境中受到熏染罢了,你不必与他计较,能在这样的情况及心境之下容忍那才是真正的忍耐。
128、如果我们放眼从累生历劫去看,那么一切的众生,谁不曾做过我的父母、兄弟姊妹、亲戚眷属?谁不曾做过我的仇敌冤家?如果说有恩,个个与我有恩;如果说有冤,个个与我有冤。这样子我们还有什么恩怨亲疏之别呢?再就智慧愚笨来说,人人有聪明的时候,也有愚痴的时候,聪明的人可能变愚痴,愚痴的人也可能变聪明。最坏的人,也曾做过许多好事,而且不会永远坏;好人也曾做过许多坏事,将来也不一定会好。如此我们反覆思索,所谓的冤亲、贤愚,这许多差别的概念,自然就会渐渐淡了。这绝对不是混沌,也不是不知好坏,而是要将我们无始以来的偏私差别之见,以一视同仁的平等观念罢了!
129、世界原本就不是属于你,因此你用不着抛弃,要抛弃的是一切的执著。万物皆为我所用,但非我所属。
130、宁可自己去原谅别人,莫让别人来原谅你。
131、当你用烦恼心来面对事物时,你会觉得一切都是业障,世界也会变得丑陋可恨。
132、欲为诸佛龙象,先做众生马牛。
133、虽然我们不能改变周遭的世界,我们就只好改变自己,用爱心和智慧来面对这一切.
2、与其说是别人让你痛苦,不如说自己的修养不够。
3、如果你不给自己烦恼,别人也永远不可能给你烦恼。因为你自己的 内心,你放不下。
4、好好的管教你自己,不要管别人。
5、不宽恕众生,不原谅众生,是苦了你自己。
6、别说别人可怜,自己更可怜,自己修行又如何?自己又懂得人生多少?
7、福报不够的人,就会常常听到是非;福报够的人,从来就没听到过是非。
8、你永远要感谢给你逆境的众生。
9、你永远要宽恕众生,不论他有多坏,甚至他伤害过你,你一定要放下,才能得到真正的快乐。
10、这个世界本来就是痛苦的,没有例外的。
11、当你快乐时,你要想,这快乐不是永恒的。当你痛苦时你要想这痛苦也不是永恒的。
12、认识自己,降伏自己,改变自己,才能改变别人。
13、不要浪费你的生命,在你一定会后悔的地方上。
14、你什么时候放下,什么时候就没有烦恼。
15、每一种创伤,都是一种成熟。
16、当你知道迷惑时,并不可怜, 当你不知道迷惑时,才是最可怜的。
17、狂妄的人有救,自卑的人没有救。
18、你不要一直不满人家,你应该一直检讨自己才对。不满人家,是苦了你自己。
19、你要包容那些意见跟你不同的人,这样子日子比较好过。你要是一直想改变他,那样子你会很痛苦。要学学怎样忍受他才是。你要学学怎样包容他才是。
20、承认自己的伟大,就是认同自己的愚疑。
21、一个人如果不能从内心去原谅别人,那他就永远不会心安理得。
22、心中装满着自己的看法与想法的人,永远听不见别人的心声。
23、毁灭人只要一句话,培植一个人却要千句话,请你多口下留情。
24、当你劝告别人时,若不顾及别人的自尊心,那么再好的言语都没有用的。
25、不要在你的智慧中夹杂着傲慢。不要使你的谦虚心缺乏智慧。
26、根本不必回头去看咒骂你的人是谁?如果有一条疯狗咬你一口,难道你也要趴下去反咬他一口吗?
27、忌妒别人,不会给自己增加任何的好处。忌妒别人,也不可能减少别人的成就。
28、永远不要浪费你的一分一秒,去想任何你不喜欢的人。
29、多少人要离开这个世间时,都会说出同一句话,这世界真是无奈与凄凉啊!
30、恋爱不是慈善事业,不能随便施舍的。感情是没有公式,没有原则,没有道理可循的。可是人们至死都还在执著与追求。
31、请你用慈悲心和温和的态度,把你的不满与委屈说出来,别人就容易接受。
32、创造机会的人是勇者。等待机会的人是愚者。
33、能说不能做,不是真智慧。
34、多用心去倾听别人怎么说,不要急着表达你自己的看法。
35、同样的瓶子,你为什么要装毒药呢?同样的心理,你为什么要充满着烦恼呢?
36、得不到的东西,我们会一直以为他是美好的,那是因为你对他了解太少,没有时间与他相处在一起。当有一天,你深入了解后,你会发现原不是你想像中的那么美好。
37、这个世间只有圆滑,没有圆满的。
38、活着一天,就是有福气,就该珍惜。当我哭泣我没有鞋子穿的时候,我发现有人却没有脚。
39、多一分心力去注意别人,就少一分心力反省自己,你懂吗?
40、欲知世上刀兵劫,但听屠门夜半声。不要光埋怨自己多病,灾祸横生,多看看横死在你刀下的众生又有多少?
41、憎恨别人对自己是一种很大的损失。
42、每一个人都拥有生命,但并非每个人都懂得生命,乃至于珍惜生命。不了解生命的人,生命对他来说,是一种惩罚。
43、自以为拥有财富的人,其实是被财富所拥有。
44、情执是苦恼的原因,放下情执,你才能得到自在。
45、随缘不是得过且过,因循苟且,而是尽人事听天命。
46、不要太肯定自己的看法,这样子比较少后悔。
47、当你对自己诚实的时候,世界上没有人能够欺骗得了你。
48、用伤害别人的手段来掩饰自己缺点的人,是可耻的。
49、内心充满忌妒,心中不坦白,言语不正的人,不能算是一位五官端正的人。
50、多讲点笑话,以幽默的态度处事,这样子日子会好过一点。
51、活在别人的掌声中,是禁不起考验的人。
52、不要刻意去猜测他人的想法,如果你没有智慧与经验的正确判断,通常都会有错误的。
53、要了解一个人,只需要看他的出发点与目的地是否相同,就可以知道他是否真心的。
54、人生的真理,只是藏在平淡无味之中。
55、不洗澡的人,硬擦香水是不会香的。名声与尊贵,是来自于真才实学的。有德自然香。
56、与其你去排斥它已成的事实,你不如去接受它。
57、逆境是成长必经的过程,能勇于接受逆境的人,生命就会日渐的茁壮。
58、你要感谢告诉你缺点的人。
59、能为别人设想的人,永远不寂寞。
60、如果你能像看别人缺点一样,如此准确般的发现自己的缺点,那么你的生命将会不平凡。
61、原谅别人,就是给自己心中留下空间,以便回旋。
62、时间总会过去的,让时间流走你的烦恼吧!
63、你硬要把单纯的事情看得很严重,那样子你会很痛苦。
64、永远扭曲别人善意的人,无药可救。
65、人不是坏的,只是习气罢了,每个人都有习气,只是深浅不同罢了。只要他有向善的心,能原谅的就原谅他,不要把他看做是坏人。
66、说一句谎话,要编造十句谎话来弥补,何苦呢?
67、其实爱美的人,只是与自己谈恋爱罢了。
68、世界上没有一个永远不被毁谤的人,也没有一个永远被赞叹的人。当你话多的时候,别人要批评你,当你话少的时候,别人要批评你,当你沈默的时候,别人还是要批评你。在这个世界上,没有一个人不被批评的。
69、夸奖我们,赞叹我们的,这都不是名师。会讲我们,指示我们的,这才是良师,有了他们我们才会进步。
70、你目前所拥有的都将随着你的死亡而成为他人的,那为何不现在就乐施给真正需要的人呢?
71、白白的过一天,无所事事,就像犯了窃盗罪一样。
72、沈默是毁谤最好的答覆。
73、对人恭敬,就是在庄严你自己。
74、拥有一颗无私的爱心,便拥有了一切。
75、仇恨永远不能化解仇恨,只有宽容才能化解仇恨,这是永恒的至理。
76、你接受比抱怨还要好,对于不可改变的事实,你除了接受以外,没有更好的办法了。
77、不要因为众生的愚疑,而带来了自己的烦恼。不要因为众生的无知,而痛苦了你自己。
78、别人讲我们不好,不用生气、难过。说我们好也不用高兴,这不好中有好,好中有坏,就看你会不会用?
79、当你的错误显露时,可不要发脾气,别以为任性或吵闹,可以隐藏或克服你的缺点。
80、不要常常觉得自己很不幸,世界上比我们痛苦的人还要多。
81、愚痴的人,一直想要别人了解他。有智慧的人,却努力的了解自己。
82、来是偶然的,走是必然的。所以你必须,随缘不变,不变随缘。 83、只要面对现实,你才能超越现实。
84、良心是每一个人最公正的审判官,你骗得了别人,却永远骗不了你自己的良心。
85、不懂得自爱的人,是没有能力去爱别人的。
86、做事就是在学做人而已。
87、有时候我们要冷静问问自已,我们在追求什么?我们活着为了什么?
88、不要因为小小的争执,远离了你至亲的好友,也不要因为小小的怨恨,忘记了别人的大恩。
89、勇于接受别人的批评,正好可以调整自己的缺点。
90、感谢上天我所拥有的,感谢上天我所没有的。
91、说话不要有攻击性,不要有杀伤力,不夸已能,不扬人恶,自然能化敌为友。
92、一个常常看别人缺点的人,自己本身就不够好,因为他没有时间检讨他自己。
93、是非天天有,不听自然无,是非天天有,不听还是有,是非天天有,看你怎么办?
94、如果你真的爱他,那么你必须容忍他部份的缺点。
95、要克服对死亡的恐惧,你必须要接受世上所有的人,都会死去的观念。
96、虽然你讨厌一个人,但却又能发觉他的优点好处,像这样子有修养的人,天下真是太少了。
97、诚实的面对你内心的矛盾和污点,不要欺骗你自己。
98、因果不曾亏欠过我们什么,所以请不要抱怨。
99、我们确实有如是的优点,但也要隐藏几分,这个叫做涵养。
100、大多数的人一辈子只做了三件事;自欺、欺人、被人欺。
101、太过于欣赏自己的人,不会去欣赏别人的优点。
102、心是最大的骗子,别人能骗你一时,而它却会骗你一辈子。
103、当你手中抓住一件东西不放时,你只能拥有这件东西,如果你肯放手,你就有机会选择别的。人的心若死执自己的观念,不肯放下,那么他的智慧也只能达到某种程度而已。
104、人家怕你,并不是一种福,人家欺你,并不是一种辱。
105、不是某人使我烦恼,而是我拿某人的言行来烦恼自己。
106、不要刻意去曲解别人的善意,你应当往好的地方想。
107、世上的事,不如己意者,那是当然的。
108、我的财富并不是因为我拥有很多,而是我要求的很少。
109、吃了就一定要拉,人一定要学会随缘放下,否则就会?便秘。
110、常以为别人在注意你,或希望别人注意你的人,会生活的比较烦恼。
111、我能为你煮东西,但我不能为你吃东西。各人吃饭是各人饱,各人生死是个人了。
112、看轻别人很容易,要摆平自己却很困难。
113、你只管活你自己的,不必去介意别人的扭曲与是非。
114、如果你准备结婚的话,告诉你一句非常重要的哲学名言,你一定要忍耐包容对方的缺点,世界上没有绝对幸福圆满的婚姻,幸福只是来自于无限的容忍与互相尊重。
115、如果你能够平平安安的渡过一天,那就是一种福气了。多少人在今天已经见不到明天的太阳,多少人在今天已经成了残废,多少人在今天已经失去了自由,多少人在今天已经家破人亡。
116、是非和得失,要到最后的结果,才能评定。
117、你不必和因果争吵,因果从来就不会误人。你也不必和命运争吵,命运它是最公平的审判官。
118、你有你的生命观,我有我的生命观,我不干涉你。只要我能,我就感化你。如果不能,那我就认命。
119、你希望掌握永恒,那你必须控制现在。
120、恶口永远不要出自于我们的口中,不管他有多坏,有多恶。你愈骂他,你的心就被污染了,你要想,他就是你的善知识。
121、当你明天开始生活的时候,有人跟你争执,你就让他赢,这个赢跟输,都只是文字的观念罢了。当你让对方赢,你并没有损失什么。所谓的赢,他有赢到什么?得到什么?所谓的输,你又输到什么?失去什么?
122、我们大部份的生命都浪费在文字语言的捉摸上。
123、你不要常常觉得自己很委曲,你应该要想,他对我这样已经很好了,这就是修行的功夫。
124、别人可以违背因果,别人可以害我们,打我们,毁谤我们。可是我们不能因此而憎恨别人,为什么?我们一定要保有一颗完整的 本性和一颗清净的心。
125、与任何人接触时,要常常问自己,我有什么对他有用?使他得益。如果我不能以个人的道德、学问和修持的力量,来使人受益,就等于欠了一份债。
126、如果一个人没有苦难的感受,就不容易对他人给予同情。你要学救苦救难的精神,就得先受苦受难。
127、一般人在遇到对方的权势大,财富大,气力大,在无可奈何的情形之下而忍,这算什么忍耐呢?真正的忍是,就算他欺负了你,对不住你,但他什么都不及你,你有足够的力量对付他,而你却能容忍他,认为他的本性和我一样,只是一时糊涂,或在恶劣的环境中受到熏染罢了,你不必与他计较,能在这样的情况及心境之下容忍那才是真正的忍耐。
128、如果我们放眼从累生历劫去看,那么一切的众生,谁不曾做过我的父母、兄弟姊妹、亲戚眷属?谁不曾做过我的仇敌冤家?如果说有恩,个个与我有恩;如果说有冤,个个与我有冤。这样子我们还有什么恩怨亲疏之别呢?再就智慧愚笨来说,人人有聪明的时候,也有愚痴的时候,聪明的人可能变愚痴,愚痴的人也可能变聪明。最坏的人,也曾做过许多好事,而且不会永远坏;好人也曾做过许多坏事,将来也不一定会好。如此我们反覆思索,所谓的冤亲、贤愚,这许多差别的概念,自然就会渐渐淡了。这绝对不是混沌,也不是不知好坏,而是要将我们无始以来的偏私差别之见,以一视同仁的平等观念罢了!
129、世界原本就不是属于你,因此你用不着抛弃,要抛弃的是一切的执著。万物皆为我所用,但非我所属。
130、宁可自己去原谅别人,莫让别人来原谅你。
131、当你用烦恼心来面对事物时,你会觉得一切都是业障,世界也会变得丑陋可恨。
132、欲为诸佛龙象,先做众生马牛。
133、虽然我们不能改变周遭的世界,我们就只好改变自己,用爱心和智慧来面对这一切.
[实践OK]PHP7的session设置问题只在php.ini里设置不生效,得在php-fpm.conf里包含文件里设置,php.ini配置,php session默认存放位置,php.ini 里面的 session.save_path。在PHP7里修改php.ini不行的配置方法。
Php/Js/Shell/Go jack 2007-11-15 18:10
PHP7的session设置问题只在php.ini里设置不生效,得在php-fpm.conf里包含文件里设置:
cat etc/php-fpm.d/www.conf
php_value[session.save_handler] = files
php_value[session.save_path] = /data/tmp/session
来自:https://typecodes.com/web/php7configure.html
===========================================================
php session默认存放位置,php.ini 里面的 session.save_path
采用的是php的缓存。这部分可以自己写,例如把session放到某个cache服务中(redis,memcached)
参考:http://www.php.net/manual/zh/book.session.php
請問各位 php 前輩,有無可能用 include 或 require 整個目錄下的檔案
例:
require(models/survey/SurveyItem.inc');
改成
require(models/survey/');
另外請問一下,同一 php 檔案下是否不能出現 2次以上
相同的 function?
例:
在一php檔裡有 session_start ,而 require的檔案裡 也有 session_start
, 在我local 端的server裡,跑起來會出現
Notice: A session had already been started - ignoring session_start() in C:\xxxxxxxxx.php on line 43
但在虛擬主機上又很正常,為何會如此呢?
感謝~~
在XP系统下,我配置了session
如下:
session.save_path = "C:/temp"
session.auto_start = 1
将安全等级调低,在php.ini中
作者: freelin 发布日期: 2004-5-26
之前我从来没设过安全等级,请问是哪一选项?谢
作者: suse 发布日期: 2004-5-26
php.ini中error_reporting = E_ALL & ~E_NOTICE
或在程序开头加上:
error_reporting(E_ALL & ~E_NOTICE);
还不行还是你配置有问题。。。
cat etc/php-fpm.d/www.conf
php_value[session.save_handler] = files
php_value[session.save_path] = /data/tmp/session
来自:https://typecodes.com/web/php7configure.html
===========================================================
php session默认存放位置,php.ini 里面的 session.save_path
采用的是php的缓存。这部分可以自己写,例如把session放到某个cache服务中(redis,memcached)
参考:http://www.php.net/manual/zh/book.session.php
請問各位 php 前輩,有無可能用 include 或 require 整個目錄下的檔案
例:
require(models/survey/SurveyItem.inc');
改成
require(models/survey/');
另外請問一下,同一 php 檔案下是否不能出現 2次以上
相同的 function?
例:
在一php檔裡有 session_start ,而 require的檔案裡 也有 session_start
, 在我local 端的server裡,跑起來會出現
Notice: A session had already been started - ignoring session_start() in C:\xxxxxxxxx.php on line 43
但在虛擬主機上又很正常,為何會如此呢?
感謝~~
在XP系统下,我配置了session
如下:
session.save_path = "C:/temp"
session.auto_start = 1
将安全等级调低,在php.ini中
作者: freelin 发布日期: 2004-5-26
之前我从来没设过安全等级,请问是哪一选项?谢
作者: suse 发布日期: 2004-5-26
php.ini中error_reporting = E_ALL & ~E_NOTICE
或在程序开头加上:
error_reporting(E_ALL & ~E_NOTICE);
还不行还是你配置有问题。。。
短信息:
总记录数 874
免费短信记录数 300
按主被叫分类
主叫通话 15次 费用合计 4.32元
被叫通话 50次 费用合计 53.25元
按通话地点分类
本地通话 50次 费用合计 32.70元
漫游通话 15次 费用合计 24.87元
结论:被叫电话太多,要免费接听的。。。。
短息条数太多,估计以后会少一些。。。。。下个月继续跟踪!
1.不成熟的男人总是在意女人的姿色;成熟的男人则很会看老婆的脸色。
2.不成熟的男人总是在太太面前轻许承诺;成熟的男人则会在太太面前兜售甜言蜜语。
3.不成熟的男人和太太一起上街时,总是捂紧自己的腰包,那能捂得住吗?而成熟的男人却是闭紧自己的眼睛,正所谓眼不见心不烦。
4.发了奖金以后,不成熟的男人会将钱悄悄塞进衣兜里,一旦被老婆发现就会死得很惨;成熟的男人则 用老婆的名字存在银行里,不被发现当然是好事,万一不小心被发现也不是什么坏事,老婆一定会爱死你。
5.不成熟的男人会满脸笑容地陪老婆逛商场;成熟的男人只有和老婆一起逛菜市的时候才会精神百倍。
6.不成熟的男人一和美女有了眼神的碰撞就会害羞得满脸通红;成熟的男人总会把美女逗得满脸通红。
7.不成熟的男人给太太写完检讨就会把它们全都扔进垃圾桶;成熟的男人则会把老婆检验合格的检讨书寄到杂志社。
8.不成熟的男人会在任何人面前酗酒,包括老婆,一不小心就酒后吐真言,其结局悲惨不堪;成熟的男人无论如何也不会在太太面前喝太多的酒,这样可以守口如瓶,心中的秘密永远也不会见光。
9.不成熟的男人最爱唱情歌;成熟的男人不仅会唱情歌,而且更会唱赞歌。
10.不成熟的男人希望自己的老婆擅长往脸上抹粉(化妆)——看上去养眼,舒服;成熟的男人做梦都想自己的老婆爱揉面粉(做家务)。
2.不成熟的男人总是在太太面前轻许承诺;成熟的男人则会在太太面前兜售甜言蜜语。
3.不成熟的男人和太太一起上街时,总是捂紧自己的腰包,那能捂得住吗?而成熟的男人却是闭紧自己的眼睛,正所谓眼不见心不烦。
4.发了奖金以后,不成熟的男人会将钱悄悄塞进衣兜里,一旦被老婆发现就会死得很惨;成熟的男人则 用老婆的名字存在银行里,不被发现当然是好事,万一不小心被发现也不是什么坏事,老婆一定会爱死你。
5.不成熟的男人会满脸笑容地陪老婆逛商场;成熟的男人只有和老婆一起逛菜市的时候才会精神百倍。
6.不成熟的男人一和美女有了眼神的碰撞就会害羞得满脸通红;成熟的男人总会把美女逗得满脸通红。
7.不成熟的男人给太太写完检讨就会把它们全都扔进垃圾桶;成熟的男人则会把老婆检验合格的检讨书寄到杂志社。
8.不成熟的男人会在任何人面前酗酒,包括老婆,一不小心就酒后吐真言,其结局悲惨不堪;成熟的男人无论如何也不会在太太面前喝太多的酒,这样可以守口如瓶,心中的秘密永远也不会见光。
9.不成熟的男人最爱唱情歌;成熟的男人不仅会唱情歌,而且更会唱赞歌。
10.不成熟的男人希望自己的老婆擅长往脸上抹粉(化妆)——看上去养眼,舒服;成熟的男人做梦都想自己的老婆爱揉面粉(做家务)。
分手后不能做朋友
分手后不能做朋友。我选择做陌生人的理由是:既然分手了,就不要再有暧昧的关系,那样只会让自己伤心,难受,进也不是,退也不是,既然分手了,就彻底的退出,快乐的生活,开心是现在唯一的追求。我只能对她说句对不起,不管是什么原因,分手了就分手了,既然我们不能再相互爱下去,就让我们做陌生人吧
分手后不能做朋友.如果你曾经深深的爱过这个人,她曾经是你生命中不可分割的一部分,那么怎么去转换角色,才能若无其事的把她看成是一个朋友?这对大多数人来说,恐怕都是难以做到的。
分手后不能做朋友。如果说分手后,还会保持联系,这只能说明有一方还没真正放下对方,她还不愿从对方的生活中彻底消失,所以会找各种看似很冠冕堂皇的理由去接近对方。可是,扪心自问,如果你们是真正的爱过一场的话,如果他真的对不起你,真的能做成朋友?NO,因为现实是残酷的,并没有我们想象的那么多姿多彩。
分手后不能做朋友。假设双方分手之后都另结新欢,你却仍以旧爱做朋友,新情人心理怎么想。谁不认为你与旧爱是藕断丝连呢?新欢必然心中不满,更怕在拉拉扯扯中搞出一个对角关系来。虽然也有一些相安无事的例子,但这些故事总不免另人听出一丝尴尬来。当你看到旧情人,你又怎能忘记曾与他同甘共苦一起走过的日子?除非未曾刻骨铭心。
分手后不能做朋友。看着自己以前的恋人,与别人亲亲我我、欢天喜地,你心里真的就那么平衡吗?人都是有尊严的,何必自找苦吃呢?还是早点脱离苦海,把它封在心里,怀念,或者扔掉它,重新过自己的生活这才是明智之举!
分手后不能做朋友。分手了,不做朋友是为了不再给对方希望,不给任何机会,这个人没爱过你或者已经不再爱你,再抱着希望简直是一种酷刑,放手,反而是大家解脱的最好办法,而做为任何一方,既然没有了爱,也就没有必要苦恋不舍了。所以,既然没有缘分一路同行的走下去,那就彻底分开吧,默默的祝福你,但是我的生活中,已经没有你。
分手后不能做朋友。分手之后仍要做一对朋友,岂不是把过去的一切又带回自己的生活中?何苦呢?歌里就这样唱到:“日复一日,年复一年,淡忘了原初的伤疤,只是一回想起来,还是有一丝隐隐的酸楚在脑海里残留着,久久不能散去。曾经挚爱的人转眼间已成陌路,那份爱也已经随风而逝,说是可以忘记,可生活就象表演,不经意间的相遇,看着她远去的背影往昔一样在脑海间漂动,那种伤是难以语表的。慢慢发现,激情与浪漫已渐渐远去,难以再爱。 ”
是啊,有时候感情就像一场游戏,但也并不是谁都愿意这么做。人这一辈子记得最多的应该是最快乐的那段日子,即使有伤害和痛苦,也不会为此后悔过,人不能后悔,后悔就代表否定自己,否定自己那就不可能快乐。也许你的收获仅仅是一些支离破碎的记忆,并不如你想象的那么美好,有一个美满快乐的结局,但起码自己曾经那么的珍惜和努力过,足矣!
分手后不能做朋友。我选择做陌生人的理由是:既然分手了,就不要再有暧昧的关系,那样只会让自己伤心,难受,进也不是,退也不是,既然分手了,就彻底的退出,快乐的生活,开心是现在唯一的追求。我只能对她说句对不起,不管是什么原因,分手了就分手了,既然我们不能再相互爱下去,就让我们做陌生人吧
分手后不能做朋友.如果你曾经深深的爱过这个人,她曾经是你生命中不可分割的一部分,那么怎么去转换角色,才能若无其事的把她看成是一个朋友?这对大多数人来说,恐怕都是难以做到的。
分手后不能做朋友。如果说分手后,还会保持联系,这只能说明有一方还没真正放下对方,她还不愿从对方的生活中彻底消失,所以会找各种看似很冠冕堂皇的理由去接近对方。可是,扪心自问,如果你们是真正的爱过一场的话,如果他真的对不起你,真的能做成朋友?NO,因为现实是残酷的,并没有我们想象的那么多姿多彩。
分手后不能做朋友。假设双方分手之后都另结新欢,你却仍以旧爱做朋友,新情人心理怎么想。谁不认为你与旧爱是藕断丝连呢?新欢必然心中不满,更怕在拉拉扯扯中搞出一个对角关系来。虽然也有一些相安无事的例子,但这些故事总不免另人听出一丝尴尬来。当你看到旧情人,你又怎能忘记曾与他同甘共苦一起走过的日子?除非未曾刻骨铭心。
分手后不能做朋友。看着自己以前的恋人,与别人亲亲我我、欢天喜地,你心里真的就那么平衡吗?人都是有尊严的,何必自找苦吃呢?还是早点脱离苦海,把它封在心里,怀念,或者扔掉它,重新过自己的生活这才是明智之举!
分手后不能做朋友。分手了,不做朋友是为了不再给对方希望,不给任何机会,这个人没爱过你或者已经不再爱你,再抱着希望简直是一种酷刑,放手,反而是大家解脱的最好办法,而做为任何一方,既然没有了爱,也就没有必要苦恋不舍了。所以,既然没有缘分一路同行的走下去,那就彻底分开吧,默默的祝福你,但是我的生活中,已经没有你。
分手后不能做朋友。分手之后仍要做一对朋友,岂不是把过去的一切又带回自己的生活中?何苦呢?歌里就这样唱到:“日复一日,年复一年,淡忘了原初的伤疤,只是一回想起来,还是有一丝隐隐的酸楚在脑海里残留着,久久不能散去。曾经挚爱的人转眼间已成陌路,那份爱也已经随风而逝,说是可以忘记,可生活就象表演,不经意间的相遇,看着她远去的背影往昔一样在脑海间漂动,那种伤是难以语表的。慢慢发现,激情与浪漫已渐渐远去,难以再爱。 ”
是啊,有时候感情就像一场游戏,但也并不是谁都愿意这么做。人这一辈子记得最多的应该是最快乐的那段日子,即使有伤害和痛苦,也不会为此后悔过,人不能后悔,后悔就代表否定自己,否定自己那就不可能快乐。也许你的收获仅仅是一些支离破碎的记忆,并不如你想象的那么美好,有一个美满快乐的结局,但起码自己曾经那么的珍惜和努力过,足矣!
su :切换并取代该用户的身份
执行范例:
1>切换到root
[lammy198@localhost lammy198]$ su - //可以写成su - root
Password: //输入密码
[root@localhost root]#
2>切换到lammy198
由root切换,则不需要输入密码
[root@localhost root]# su - lammy198
[lammy198@localhost lammy198]$
不是由root切换,则需要输入密码
[lammy@localhost lammy]$ su - lammy198 //不可以写成su -
Password: //输入密码
[lammy198@localhost lammy198]$
adduser :新建系统上的用户
[root@localhost root]# adduser -D //显示建立帐号时的默认值
GROUP=100 //所属主组的ID
HOME=/home //用户的根目录
INACTIVE=-1
EXPIRE=
SHELL=/bin/bash //所使用的SHELL
SKEL=/etc/skel //所应用的设置文件
创建lammyt的帐号
[root@localhost root]# adduser -m lammyt
[root@localhost root]# id lammyt //为什么groups=502而不是100?
uid=502(lammyt) gid=502(lammyt) groups=502(lammyt)
userdel :删除帐号
[root@localhost root]# userdel lammyt
[root@localhost root]# id lammyt
id: lammyt: No such user
useradd :新建帐号
[root@localhost root]# useradd lammyt
[root@localhost root]# id lammyt
uid=504(lammyt) gid=504(lammyt) groups=504(lammyt)
who :显示登陆当前登陆用户的信息
[root@localhost root]# who
root :0 Sep 16 18:05
root pts/2 Sep 21 12:16 (:0.0)
id :显示拥护组的ID
[root@localhost root]# id -a //显示用户的帐户信息
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel)
ps :显示当前系统中由该用户运行的进程列表
[root@localhost root]# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 14:38 ? 00:00:06 init
root 2 1 0 14:38 ? 00:00:00 [keventd]
root 3 1 0 14:38 ? 00:00:00 [kapmd]
root 4 1 0 14:38 ? 00:00:00 [ksoftirqd_CPU0]
root 9 1 0 14:38 ? 00:00:00 [bdflush]
root 5 1 0 14:38 ? 00:00:00 [kswapd]
root 6 1 0 14:38 ? 00:00:00 [kscand/DMA]
free :查看当前系统内存的使用情况
[root@localhost root]# free
total used free shared buffers cached
Mem: 412856 402316 10540 0 35908 218360
-/+ buffers/cache: 148048 264808
Swap: 522104 1612 520492
df :查看文件系统的磁盘空间占用情况
[root@localhost root]# df
文件系统 1K-块 已用 可用 已用% 挂载点
/dev/sda3 9653708 2690872 6472452 30% /
/dev/sda1 147766 9376 130761 7% /boot
none 206428 0 206428 0% /dev/shm
du :统计目录或文件所占磁盘空间的大小
fdisk :查看磁盘分区情况及对硬盘进行分区管理
[root@localhost root]# fdisk -l
Disk /dev/sda: 10.7 GB, 10737418240 bytes
255 heads, 63 sectors/track, 1305 cylinders
Units = cylinders of 16065 * 512 = 8225280 bytes
Device Boot Start End Blocks Id System
/dev/sda1 * 1 19 152586 83 Linux
/dev/sda2 20 84 522112+ 82 Linux swap
/dev/sda3 85 1305 9807682+ 83 Linux
使用fdisk必须拥有root的权限
[lammy@localhost lammy]$ fdisk -l
-bash: fdisk: command not found
mount :磁盘挂载
[root@localhost root]# mount -l //列出以挂载的设备,文件系统名称和挂载点
/dev/sda3 on / type ext3 (rw) [/]
none on /proc type proc (rw)
usbdevfs on /proc/bus/usb type usbdevfs (rw)
/dev/sda1 on /boot type ext3 (rw) [/boot]
none on /dev/pts type devpts (rw,gid=5,mode=620)
none on /dev/shm type tmpfs (rw)
none on /proc/fs/vmblock/mountPoint type vmblock (rw)
cd :更改工作路径
cd -可以回到前次工作目录
./代表当前目录, ../代表上级目录
ls :列出目录内容
[root@localhost /]# ls //列出目录
bin dev home lib misc opt root soft tmp var
boot etc initrd lost+found mnt proc sbin themes usr
[root@localhost /]# ls -l //列出目录的详细内容
总用量 201
drwxr-xr-x 2 root root 4096 9月 5 23:19 bin
drwxr-xr-x 4 root root 1024 9月 5 23:07 boot
drwxr-xr-x 20 root root 118784 9月 16 18:05 dev
drwxr-xr-x 56 root root 4096 9月 26 21:41 etc
drwxr-xr-x 8 root root 4096 9月 26 21:40 home
drwxr-xr-x 2 root root 4096 2003-01-25 initrd 后面的没有贴出来
mkdir :创建目录
[root@localhost root]# mkdir -p ./test/test //-p设置路径
[root@localhost root]# ls
gcc_programe minicom.log other test vmware-tools-distrib
[root@localhost root]# cd test
[root@localhost test]# ls
test
cat :连接并显示指定的一个和多个文件的有关信息
-n 由第一行开始对所有输出的行数编号
-b 和-n相似,只不过对于空白行不编号
cp :将给出的文件或目录复制到另一个文件或目录中
[root@localhost root]# mkdir -p ./test/lammy
[root@localhost root]# cp -a ./test/lammy ./
[root@localhost root]# ls
gcc_programe lammy minicom.log other test vmware-tools-distrib
[root@localhost root]# ls ./test
lammy test
mv :为文件或目录改名或将文件由一个目录移入到另一个目录中
[root@localhost root]# mkdir -p ./test/lammyt
[root@localhost root]# ls ./test
lammy lammyt test
[root@localhost root]# mv -i ./test/lammyt ./
[root@localhost root]# ls
gcc_programe lammy lammyt minicom.log other test vmware-tools-distrib
[root@localhost root]# ls ./test
lammy test
该实例把./test下的lammyt移到./下
[root@localhost root]# mkdir ./lammyt/lammyt
[root@localhost root]# ls ./lammyt
lammyt
[root@localhost root]# mv ./lammyt/lammyt ./lammyt/lammy198
[root@localhost root]# ls ./lammyt
lammy198
重命名实验
rm :删除一个目录中的一个或多个文件
[root@localhost root]# rm -i lammy
rm:是否删除目录‘lammy’? y
rm: 无法删除目录‘lammy’: 是一个目录
[root@localhost root]# rm -r lammy //删除单个目录
rm:是否删除目录‘lammy’? y
[root@localhost root]# ls
gcc_programe lammyt minicom.log other test vmware-tools-distrib
该实例说明如果煤油使用-r,则rm不会删除目录;
[root@localhost root]# rm -r test //删除多个目录
rm:是否进入目录‘test’? y
rm:是否删除目录‘test/test’? y
rm:是否删除目录‘test/lammy’? y
rm:是否删除目录‘test’? y
执行范例:
1>切换到root
[lammy198@localhost lammy198]$ su - //可以写成su - root
Password: //输入密码
[root@localhost root]#
2>切换到lammy198
由root切换,则不需要输入密码
[root@localhost root]# su - lammy198
[lammy198@localhost lammy198]$
不是由root切换,则需要输入密码
[lammy@localhost lammy]$ su - lammy198 //不可以写成su -
Password: //输入密码
[lammy198@localhost lammy198]$
adduser :新建系统上的用户
[root@localhost root]# adduser -D //显示建立帐号时的默认值
GROUP=100 //所属主组的ID
HOME=/home //用户的根目录
INACTIVE=-1
EXPIRE=
SHELL=/bin/bash //所使用的SHELL
SKEL=/etc/skel //所应用的设置文件
创建lammyt的帐号
[root@localhost root]# adduser -m lammyt
[root@localhost root]# id lammyt //为什么groups=502而不是100?
uid=502(lammyt) gid=502(lammyt) groups=502(lammyt)
userdel :删除帐号
[root@localhost root]# userdel lammyt
[root@localhost root]# id lammyt
id: lammyt: No such user
useradd :新建帐号
[root@localhost root]# useradd lammyt
[root@localhost root]# id lammyt
uid=504(lammyt) gid=504(lammyt) groups=504(lammyt)
who :显示登陆当前登陆用户的信息
[root@localhost root]# who
root :0 Sep 16 18:05
root pts/2 Sep 21 12:16 (:0.0)
id :显示拥护组的ID
[root@localhost root]# id -a //显示用户的帐户信息
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel)
ps :显示当前系统中由该用户运行的进程列表
[root@localhost root]# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 14:38 ? 00:00:06 init
root 2 1 0 14:38 ? 00:00:00 [keventd]
root 3 1 0 14:38 ? 00:00:00 [kapmd]
root 4 1 0 14:38 ? 00:00:00 [ksoftirqd_CPU0]
root 9 1 0 14:38 ? 00:00:00 [bdflush]
root 5 1 0 14:38 ? 00:00:00 [kswapd]
root 6 1 0 14:38 ? 00:00:00 [kscand/DMA]
free :查看当前系统内存的使用情况
[root@localhost root]# free
total used free shared buffers cached
Mem: 412856 402316 10540 0 35908 218360
-/+ buffers/cache: 148048 264808
Swap: 522104 1612 520492
df :查看文件系统的磁盘空间占用情况
[root@localhost root]# df
文件系统 1K-块 已用 可用 已用% 挂载点
/dev/sda3 9653708 2690872 6472452 30% /
/dev/sda1 147766 9376 130761 7% /boot
none 206428 0 206428 0% /dev/shm
du :统计目录或文件所占磁盘空间的大小
fdisk :查看磁盘分区情况及对硬盘进行分区管理
[root@localhost root]# fdisk -l
Disk /dev/sda: 10.7 GB, 10737418240 bytes
255 heads, 63 sectors/track, 1305 cylinders
Units = cylinders of 16065 * 512 = 8225280 bytes
Device Boot Start End Blocks Id System
/dev/sda1 * 1 19 152586 83 Linux
/dev/sda2 20 84 522112+ 82 Linux swap
/dev/sda3 85 1305 9807682+ 83 Linux
使用fdisk必须拥有root的权限
[lammy@localhost lammy]$ fdisk -l
-bash: fdisk: command not found
mount :磁盘挂载
[root@localhost root]# mount -l //列出以挂载的设备,文件系统名称和挂载点
/dev/sda3 on / type ext3 (rw) [/]
none on /proc type proc (rw)
usbdevfs on /proc/bus/usb type usbdevfs (rw)
/dev/sda1 on /boot type ext3 (rw) [/boot]
none on /dev/pts type devpts (rw,gid=5,mode=620)
none on /dev/shm type tmpfs (rw)
none on /proc/fs/vmblock/mountPoint type vmblock (rw)
cd :更改工作路径
cd -可以回到前次工作目录
./代表当前目录, ../代表上级目录
ls :列出目录内容
[root@localhost /]# ls //列出目录
bin dev home lib misc opt root soft tmp var
boot etc initrd lost+found mnt proc sbin themes usr
[root@localhost /]# ls -l //列出目录的详细内容
总用量 201
drwxr-xr-x 2 root root 4096 9月 5 23:19 bin
drwxr-xr-x 4 root root 1024 9月 5 23:07 boot
drwxr-xr-x 20 root root 118784 9月 16 18:05 dev
drwxr-xr-x 56 root root 4096 9月 26 21:41 etc
drwxr-xr-x 8 root root 4096 9月 26 21:40 home
drwxr-xr-x 2 root root 4096 2003-01-25 initrd 后面的没有贴出来
mkdir :创建目录
[root@localhost root]# mkdir -p ./test/test //-p设置路径
[root@localhost root]# ls
gcc_programe minicom.log other test vmware-tools-distrib
[root@localhost root]# cd test
[root@localhost test]# ls
test
cat :连接并显示指定的一个和多个文件的有关信息
-n 由第一行开始对所有输出的行数编号
-b 和-n相似,只不过对于空白行不编号
cp :将给出的文件或目录复制到另一个文件或目录中
[root@localhost root]# mkdir -p ./test/lammy
[root@localhost root]# cp -a ./test/lammy ./
[root@localhost root]# ls
gcc_programe lammy minicom.log other test vmware-tools-distrib
[root@localhost root]# ls ./test
lammy test
mv :为文件或目录改名或将文件由一个目录移入到另一个目录中
[root@localhost root]# mkdir -p ./test/lammyt
[root@localhost root]# ls ./test
lammy lammyt test
[root@localhost root]# mv -i ./test/lammyt ./
[root@localhost root]# ls
gcc_programe lammy lammyt minicom.log other test vmware-tools-distrib
[root@localhost root]# ls ./test
lammy test
该实例把./test下的lammyt移到./下
[root@localhost root]# mkdir ./lammyt/lammyt
[root@localhost root]# ls ./lammyt
lammyt
[root@localhost root]# mv ./lammyt/lammyt ./lammyt/lammy198
[root@localhost root]# ls ./lammyt
lammy198
重命名实验
rm :删除一个目录中的一个或多个文件
[root@localhost root]# rm -i lammy
rm:是否删除目录‘lammy’? y
rm: 无法删除目录‘lammy’: 是一个目录
[root@localhost root]# rm -r lammy //删除单个目录
rm:是否删除目录‘lammy’? y
[root@localhost root]# ls
gcc_programe lammyt minicom.log other test vmware-tools-distrib
该实例说明如果煤油使用-r,则rm不会删除目录;
[root@localhost root]# rm -r test //删除多个目录
rm:是否进入目录‘test’? y
rm:是否删除目录‘test/test’? y
rm:是否删除目录‘test/lammy’? y
rm:是否删除目录‘test’? y
简单示例:
gdb server
b server.c 10 //在server.c的第10行断点
n
p
bt //堆栈查看
p cfg->type //查看结构体某个字段的值
得到的函数列表:
info functions
启动 GDB 调试工具
$ gdb --quiet
info 命令列出程序信息
(gdb) info proc
list 命令
(gdb) list main
disassemble 命令
(gdb) disass main
========================================
刚刚学了下gdb调试器的简单使用,感觉还不错,趁热打铁,把过程讲述下,自己也增强下,呵呵,废话少说,Begin!!!
[root@localhost hello]# vim test.c //新建一个test.c的源文件
在test.c中键入如下代码,很简单的程序:
/*test.c*/
#include
int sum(int m);
int main(void)
{
int i,m =0;
sum(50);
for(i=50; i!=0; i--)m+=i;
printf("The sum of 1~50 is %d \n",m);
}
int sum(int m)
{
int i,n =0;
// sum(50);
for(i=m; i!=0; i--)n+=i;
printf("The sum of 1~m is %d \n",n);
}
完了后保存它,返回终端
[root@localhost hello]# gcc -g test.c -o test //记得一定要加 -g,这样编译出的可执行代码才包含调试的信息,否则gdb是无法载入的
[root@localhost hello]# gdb test //test为带有调试信息的目标文件
GNU gdb Red Hat Linux (5.3post-0.20021129.18rh)
Copyright 2003 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux-gnu"...
(gdb) //以上的信息就是gdb的启动画面,指定了gdb的版本号等信息
//然后键入 l 查看加载的文件,l表示list,b表示breakpoint,p表示print
(gdb) l //一次加载10行总[个人总结],
1 /*test.c*/
2 #include
3 int sum(int m);
4
5 int main(void)
6 {
7 int i,m =0;
8 sum(50);
9 for(i=50; i!=0; i--)m+=i;
10 printf("The sum of 1~50 is %d \n",m);
(gdb)
(gdb) l //继续通过参数 l 加载,加载接下来的10行
11 }
12
13 int sum(int m)
14 {
15 int i,n =0;
16 // sum(50);
17 for(i=m; i!=0; i--)n+=i;
18 printf("The sum of 1~m is %d \n",n);
19 }
(gdb) l //加载完毕,再加载显然是不会在加了哈
Line number 20 out of range; test.c has 19 lines.
//首先来看怎么设断点
(gdb) b 8 //b表示设断点,后面表示行号,就是加载时显示的行号
Breakpoint 1 at 0x804833f: file test.c, line 8.
(gdb) b 9
Breakpoint 2 at 0x804834c: file test.c, line 9.
(gdb) info b //我设了两个断点,通过info 来查看断点信息
Num Type Disp Enb Address What
1 breakpoint keep y 0x0804833f in main at test.c:8
2 breakpoint keep y 0x0804834c in main at test.c:9
(gdb)
(gdb) r //r表示run,运行至断点1,如果没设断点,则运行至结尾
Starting program: /root/hello/test
Breakpoint 1, main () at test.c:8
8 sum(50);
(gdb) c //c表示continue,继续运行
Continuing.
The sum of 1~m is 1275
Breakpoint 2, main () at test.c:9
9 for(i=50; i!=0; i--)m+=i;
(gdb) //呵呵,上面几个操作看明白了吧
//怎么来看变量值呢
(gdb) l //先用l来看看源码
4
5 int main(void)
6 {
7 int i,m =0;
8 sum(50);
9 for(i=50; i!=0; i--)m+=i;
10 printf("The sum of 1~50 is %d \n",m);
11 }
12
13 int sum(int m)
(gdb) p m //p表示print,打印m的值
$1 = 0
(gdb) p i //打印i的值,i为什么这么大,应该不用我介绍了吧
$2 = 1073828704
//变量知道怎么看了,那么单步运行呢
(gdb) n //n表示单步运行,这表示一次运行一行,所以它不会进入调用的函数
10 printf("The sum of 1~50 is %d \n",m);
(gdb) n //n的运行机制通过这俩个n应该要看出个门道来喽,我用颜色强调了下,明白了没
The sum of 1~50 is 1275
11 }
//那么如果碰到调用函数怎么进入呢,不急,有办法
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/hello/test
Breakpoint 1, main () at test.c:8
8 sum(50); //前面设的断点,表示下一步将运行这断点所在的行
(gdb) s //用s来进入函数,也就说s也是单步运行的意思,但是它碰到函数时会进入函数运行; 而n不会,直接跳过
sum (m=50) at test.c:15 //仔细看看,是不是进入了sum函数哈
15 int i,n =0;
(gdb) s
17 for(i=m; i!=0; i--)n+=i;
(gdb) s
18 printf("The sum of 1~m is %d \n",n);
(gdb)
//这样在windows下的编译器的调试方法在gdb中都有相应的命令对应,并且更加灵活,哦忘了说怎么取消断点拉,呵呵简单
(gdb) info b //首先查看断点信息
Num Type Disp Enb Address What
1 breakpoint keep y 0x0804833f in main at test.c:8
breakpoint already hit 1 time
2 breakpoint keep y 0x0804834c in main at test.c:9
(gdb) delet 1 //用delet取消断点,后面的数字应该明白吧
//还有其它命令可以通过输入help来查看,或者help command[需要查看的命令]来查看
//退出gdb,返回终端是通过q来实现的
(gdb) q
The program is running. Exit anyway? (y or n) y
[root@localhost hello]#
还有其它很多命令,我就不一一介绍拉。
gdb server
b server.c 10 //在server.c的第10行断点
n
p
bt //堆栈查看
p cfg->type //查看结构体某个字段的值
得到的函数列表:
info functions
启动 GDB 调试工具
$ gdb --quiet
info 命令列出程序信息
(gdb) info proc
list 命令
(gdb) list main
disassemble 命令
(gdb) disass main
========================================
刚刚学了下gdb调试器的简单使用,感觉还不错,趁热打铁,把过程讲述下,自己也增强下,呵呵,废话少说,Begin!!!
[root@localhost hello]# vim test.c //新建一个test.c的源文件
在test.c中键入如下代码,很简单的程序:
/*test.c*/
#include
int sum(int m);
int main(void)
{
int i,m =0;
sum(50);
for(i=50; i!=0; i--)m+=i;
printf("The sum of 1~50 is %d \n",m);
}
int sum(int m)
{
int i,n =0;
// sum(50);
for(i=m; i!=0; i--)n+=i;
printf("The sum of 1~m is %d \n",n);
}
完了后保存它,返回终端
[root@localhost hello]# gcc -g test.c -o test //记得一定要加 -g,这样编译出的可执行代码才包含调试的信息,否则gdb是无法载入的
[root@localhost hello]# gdb test //test为带有调试信息的目标文件
GNU gdb Red Hat Linux (5.3post-0.20021129.18rh)
Copyright 2003 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux-gnu"...
(gdb) //以上的信息就是gdb的启动画面,指定了gdb的版本号等信息
//然后键入 l 查看加载的文件,l表示list,b表示breakpoint,p表示print
(gdb) l //一次加载10行总[个人总结],
1 /*test.c*/
2 #include
3 int sum(int m);
4
5 int main(void)
6 {
7 int i,m =0;
8 sum(50);
9 for(i=50; i!=0; i--)m+=i;
10 printf("The sum of 1~50 is %d \n",m);
(gdb)
(gdb) l //继续通过参数 l 加载,加载接下来的10行
11 }
12
13 int sum(int m)
14 {
15 int i,n =0;
16 // sum(50);
17 for(i=m; i!=0; i--)n+=i;
18 printf("The sum of 1~m is %d \n",n);
19 }
(gdb) l //加载完毕,再加载显然是不会在加了哈
Line number 20 out of range; test.c has 19 lines.
//首先来看怎么设断点
(gdb) b 8 //b表示设断点,后面表示行号,就是加载时显示的行号
Breakpoint 1 at 0x804833f: file test.c, line 8.
(gdb) b 9
Breakpoint 2 at 0x804834c: file test.c, line 9.
(gdb) info b //我设了两个断点,通过info 来查看断点信息
Num Type Disp Enb Address What
1 breakpoint keep y 0x0804833f in main at test.c:8
2 breakpoint keep y 0x0804834c in main at test.c:9
(gdb)
(gdb) r //r表示run,运行至断点1,如果没设断点,则运行至结尾
Starting program: /root/hello/test
Breakpoint 1, main () at test.c:8
8 sum(50);
(gdb) c //c表示continue,继续运行
Continuing.
The sum of 1~m is 1275
Breakpoint 2, main () at test.c:9
9 for(i=50; i!=0; i--)m+=i;
(gdb) //呵呵,上面几个操作看明白了吧
//怎么来看变量值呢
(gdb) l //先用l来看看源码
4
5 int main(void)
6 {
7 int i,m =0;
8 sum(50);
9 for(i=50; i!=0; i--)m+=i;
10 printf("The sum of 1~50 is %d \n",m);
11 }
12
13 int sum(int m)
(gdb) p m //p表示print,打印m的值
$1 = 0
(gdb) p i //打印i的值,i为什么这么大,应该不用我介绍了吧
$2 = 1073828704
//变量知道怎么看了,那么单步运行呢
(gdb) n //n表示单步运行,这表示一次运行一行,所以它不会进入调用的函数
10 printf("The sum of 1~50 is %d \n",m);
(gdb) n //n的运行机制通过这俩个n应该要看出个门道来喽,我用颜色强调了下,明白了没
The sum of 1~50 is 1275
11 }
//那么如果碰到调用函数怎么进入呢,不急,有办法
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/hello/test
Breakpoint 1, main () at test.c:8
8 sum(50); //前面设的断点,表示下一步将运行这断点所在的行
(gdb) s //用s来进入函数,也就说s也是单步运行的意思,但是它碰到函数时会进入函数运行; 而n不会,直接跳过
sum (m=50) at test.c:15 //仔细看看,是不是进入了sum函数哈
15 int i,n =0;
(gdb) s
17 for(i=m; i!=0; i--)n+=i;
(gdb) s
18 printf("The sum of 1~m is %d \n",n);
(gdb)
//这样在windows下的编译器的调试方法在gdb中都有相应的命令对应,并且更加灵活,哦忘了说怎么取消断点拉,呵呵简单
(gdb) info b //首先查看断点信息
Num Type Disp Enb Address What
1 breakpoint keep y 0x0804833f in main at test.c:8
breakpoint already hit 1 time
2 breakpoint keep y 0x0804834c in main at test.c:9
(gdb) delet 1 //用delet取消断点,后面的数字应该明白吧
//还有其它命令可以通过输入help来查看,或者help command[需要查看的命令]来查看
//退出gdb,返回终端是通过q来实现的
(gdb) q
The program is running. Exit anyway? (y or n) y
[root@localhost hello]#
还有其它很多命令,我就不一一介绍拉。
1.语言中变量的实质
要理解C指针,我认为一定要理解C中“变量”的存储实质,所以我就从“变量”这个东西开始讲起吧!
先来理解理解内存空间吧!请看下图:
内存地址→ 6 7 8 9 10 11 12 13
-----------------------------------------------------------------
··· | | | | | | | |··
-----------------------------------------------------------------
如图所示,内存只不过是一个存放数据的空间,就好像我的看电影时的电影院中的座位一样。每个座位都要编号,我们的内存要存放各种各样的数据,当然我们要知道我们的这些数据存放在什么位置吧!所以内存也要象座位一样进行编号了,这就是我们所说的内存编址。座位可以是按一个座位一个号码的从一号开始编号,内存则是按一个字节一个字节进行编址,如上图所示。每个字节都有个编号,我们称之为内存地址。好了,我说了这么多,现在你能理解内存空间这个概念吗?
我们继续看看以下的C、C++语言变量申明:
int I;
char a;
每次我们要使用某变量时都要事先这样申明它,它其实是内存中申请了一个名为i的整型变量宽度的空间(DOS下的16位编程中其宽度为二个字节),和一个名为a的字符型变量宽度的空间(占一个字节)。
我们又如何来理解变量是如何存在的呢。当我们如下申明变量时:
int I;
char a;
内存中的映象可能如下图:
内存地址→ 6 7 8 9 10 11 12 13
------------------------------------------------------------------
···| | | | | | | |··
------------------------------------------------------------------
变量名|→i ←|→a ←|
图中可看出,i在内存起始地址为6上申请了两个字节的空间(我这里假设了int的宽度为16位,不同系统中int的宽度是可能不一样的),并命名为i。 a在内存地址为8上申请了一字节的空间,并命名为a。这样我们就有两个不同类型的变量了。
2.赋值给变量
再看下面赋值:
i=30
a=’t’
你当然知道个两个语句是将30存入i变量的内存空间中,将’t’字符存入a变量的内存空间中。我们可以这样的形象理解啦:
内存地址→ 6 7 8 9 10 11 12 13
-----------------------------------------------------------------------
··· | 30 | ‘t’ | | | | |··
-----------------------------------------------------------------------
|→i ←|→a ←|
3.变量在哪里?(即我想知道变量的地址)
好了,接下来我们来看看&i是什么意思?
是取i变量所在的地址编号嘛!我们可以这样读它:返回i变量的地址编号。你记住了吗?
我要在屏幕上显示变量的地址值的话,可以写如下代码:
printf(“%d”,&i);
以上图的内存映象所例,屏幕上显示的不是i值30,而是显示i的内存地址编号6了。当然实际你操作的时,i变量的地址值不会是这个数了。
这就是我认为作为初学者们所应想象的变量存储实质了。请这样理解吧!
最后总结代码如下:
int main()
{
int i=39;
printf(“%d\n”,i); //①
printf(“%d\n”, &i); //②
}
现在你可知道①、②两个printf分别在屏幕上输出的是i的什么东西啊?
好啦!下面我们就开始真正进入指针的学习了。Come on !(待续…)
二.指针是什么东西<想说弄懂你不容易啊!我们许多初学指针的人都要这样的感慨。我常常在思索它,为什么呢?其实生活中处处都有指针。我们也处处在使用它。有了它我们的生活才更加方便了。没有指针,那生活才不方便。不信?你看下面的例子。
这是一个生活中的例子:比如说你要我借给你一本书,我到了你宿舍,但是你人不在宿舍,于是我把书放在你的2层3号的书架上,并写了一张纸条放在你的桌上。纸条上写着:你要的书在第2层3号的书架上。当你回来时,看到这张纸条。你就知道了我借与你的书放在哪了。你想想看,这张纸条的作用,纸条本身不是书,它上面也没有放着书。那么你又如何知道书的位置呢?因为纸条上写着书的位置嘛!其实这张纸条就是一个指针了。它上面的内容不是书本身,而是书的地址,你通过纸条这个指针找到了我借给你的本书。
那么我们C,C++中的指针又是什么呢?请继续跟我来吧,看下面看一个申明一整型指针变量的语句如下:
int * pi;
pi是一个指针,当然我们知道啦,但是这样说,你就以为pi一定是个多么特别的东西了。其实,它也只过是一个变量而已。与上一篇中说的变量并没有实质的区别。不信你看下面图。
内存地址→6 7 8 9 10 11 12 13 14
--------------------------------------------------------------
···| 30 | ‘t’ | | | | | | |...
--------------------------------------------------------------
变量 |→i ←|→a ←| |→ pi ←|
(说明:这里我假设了指针只占2个字节宽度,实际上在32位系统中,指针的宽度是4个字节宽的,即32位。)由图示中可以看出,我们使用int *Pi申明指针变量; 其实是在内存的某处申明一个一定宽度的内存空间,并把它命名为Pi。你能在图中看出pi与前面的i,a 变量有什么本质区别吗,没有,当然没有!pi也只不过是一个变量而已嘛!那么它又为什么会被称为指针?关键是我们要让这个变量所存储的内容是什么。现在我要让pi成为真正有意义上的指针。请接着看下面语句:
pi=&i;
你应该知道 &i是什么意思吧!再次提醒你啦:这是返回i变量的地址编号。整句的意思就是把i地址的编号赋值给pi,也就是你在pi上写上i的地址编号。结果如下图所示:
内存地址→6 7 8 9 10 11 12 13 14
------------------------------------------------------------------
···| 30 | ‘t’ | | | 6 | | |...
------------------------------------------------------------------
变量 |→i ←|→a ←| |→ pi ←|
你看,执行完pi=&i;后,在图示中的系统中,pi的值是6。这个6就是i变量的地址编号,这样pi就指向了变量i了。你看,pi与那张纸条有什么区别?pi不就是那张纸条嘛!上面写着i的地址,而i就是那个本书。你现在看懂了吗?因此,我们就把pi称为指针。所以你要记住,指针变量所存的内容就是内存的地址编号!好了,现在我们就可以通过这个指针pi来访问到i这个变量了,不是吗?。看下面语句:
printf(“%d”,*pi);
那么*pi什么意思呢?你只要这样读它:pi内容所指的地址的内容(嘻嘻,看上去好像在绕口令了),就pi这张“纸条”上所写的位置上的那本 “书”---i 。你看,Pi内容是6,也就是说pi指向内存编号为6的地址。*pi嘛!就是它所指地址的内容,即地址编号6上的内容了。当然就是30的值了。所以这条语句会在屏幕上显示30。也就是说printf(“%d”,*pi);语句等价于printf( “%d”, i ) ,请结合上图好好体会吧!各位还有什么疑问,可以发Email:yyf977@163.com。
到此为止,你掌握了类似&i , *pi写法的含义和相关操作吗。总的一句话,我们的纸条就是我们的指针,同样我们的pi也就是我们的纸条!剩下的就是我们如何应用这张纸条了。最后我给你一道题:程序如下。
char a,*pa
a=10
pa=&a
*pa=20
printf( “%d”, a)
你能直接看出输出的结果是什么吗?如果你能,我想本篇的目的就达到了。好了,就说到这了。Happy to Study!在下篇中我将谈谈“指针的指针”即对int * * ppa;中ppa 的理解。
《彻底搞定C指针》第3篇--指针与数组名
build:2007-1-30 17:58:50 author:白云小飞 editor:beyond update:2006-11-26 browse:76
1.数组元素
看下面代码
int i,a[]={3,4,5,6,7,3,7,4,4,6};
for (i=0;i<=9;i++)
{
printf ( “%d”, a[i] );
}
很显然,它是显示a 数组的各元素值。
我们还可以这样访问元素,如下
int i,a[]={3,4,5,6,7,3,7,4,4,6};
for (i=0;i<=9;i++)
{
printf ( “%d”, *(a+i) );
}
它的结果和作用完全一样
2. 通过指针访问数组元素
int i,*pa,a[]={3,4,5,6,7,3,7,4,4,6};
pa =a ;//请注意数组名a直接赋值给指针pa
for (i=0;i<=9;i++)
{
printf ( “%d”, pa[i] );
}
很显然,它也是显示a 数组的各元素值。
另外与数组名一样也可如下:
int i,*pa,a[]={3,4,5,6,7,3,7,4,4,6};
pa =a;
for (i=0;i<=9;i++)
{
printf ( “%d”, *(pa+i) );
}
看pa=a即数组名赋值给指针,以及通过数组名、指针对元素的访问形式看,它们并没有什么区别,从这里可以看出数组名其实也就是指针。难道它们没有任何区别?有,请继续。
3. 数组名与指针变量的区别
请看下面的代码:
int i,*pa,a[]={3,4,5,6,7,3,7,4,4,6};
pa =a;
for (i=0;i<=9;i++)
{
printf ( “%d”, *pa );
pa++ ; //注意这里,指针值被修改
}
可以看出,这段代码也是将数组各元素值输出。不过,你把{}中的pa改成a试试。你会发现程序编译出错,不能成功。看来指针和数组名还是不同的。其实上面的指针是指针变量,而数组名只是一个指针常量。这个代码与上面的代码不同的是,指针pa在整个循环中,其值是不断递增的,即指针值被修改了。数组名是指针常量,其值是不能修改的,因此不能类似这样操作:a++。前面4,5节中pa[i],*(pa+i)处,指针pa的值是使终没有改变。所以变量指针pa与数组名a可以互换。
4. 申明指针常量
再请看下面的代码:
int i, a[]={3,4,5,6,7,3,7,4,4,6};
int * const pa=a;//注意const的位置:不是const int * pa,
for (i=0;i<=9;i++)
{
printf ( “%d”, *pa );
pa++ ; //注意这里,指针值被修改
}
这时候的代码能成功编译吗?不能。因为pa指针被定义为常量指针了。这时与数组名a已经没有不同。这更说明了数组名就是常量指针。但是…
int * const a={3,4,5,6,7,3,7,4,4,6};//不行
int a[]={3,4,5,6,7,3,7,4,4,6};//可以,所以初始化数组时必定要这样。
以上都是在VC6.0上实验。
《彻底搞定C指针》第4篇const int * pi/int * const pi的区别
build:2007-1-30 17:58:50 author:白云小飞 editor:beyond update:2006-11-26 browse:69
1 int i 说起
你知道我们申明一个变量时象这样int i ;这个i是可能在它处重新变赋值的。如下:
int i=0;
//…
i=20;//这里重新赋值了
不过有一天我的程序可能需要这样一个变量(暂且称它变量),在申明时就赋一个初始值。之后我的程序在其它任何处都不会再去重新对它赋值。那我又应该怎么办呢?用const 。
//**************
const int ic =20;
//…
ic=40;//这样是不可以的,编译时是无法通过,因为我们不能对const 修饰的ic重新赋值的。
//这样我们的程序就会更早更容易发现问题了。
//**************
有了const修饰的ic 我们不称它为变量,而称符号常量,代表着20这个数。这就是const 的作用。ic是不能在它处重新赋新值了。
认识了const 作用之后,另外,我们还要知道格式的写法。有两种:const int ic=20;与int const ic=20;。它们是完全相同的。这一点我们是要清楚。总之,你务必要记住const 与int哪个写前都不影响语义。有了这个概念后,我们来看这两个家伙:const int * pi与int const * pi ,按你的逻辑看,它们的语义有不同吗?呵呵,你只要记住一点,int 与const 哪个放前哪个放后都是一样的,就好比const int ic;与int const ic;一样。也就是说,它们是相同的。
好了,我们现在已经搞定一个“双包胎”的问题。那么int * const pi与前两个式子又有什么不同呢?我下面就来具体分析它们的格式与语义吧!
2 const int * pi的语义
我先来说说const int * pi是什么作用 (当然int const * pi也是一样的,前面我们说过,它们实际是一样的)。看下面的例子:
//*************代码开始***************
int i1=30;
int i2=40;
const int * pi=&i1;
pi=&i2; //4.注意这里,pi可以在任意时候重新赋值一个新内存地址
i2=80; //5.想想看:这里能用*pi=80;来代替吗?当然不能
printf( “%d”, *pi ) ; //6.输出是80
//*************代码结束***************
语义分析:
看出来了没有啊,pi的值是可以被修改的。即它可以重新指向另一个地址的,但是,不能通过*pi来修改i2的值。这个规则符合我们前面所讲的逻辑吗?当然符合了!
首先const 修饰的是整个*pi(注意,我写的是*pi而不是pi)。所以*pi是常量,是不能被赋值的(虽然pi所指的i2是变量,不是常量)。
其次,pi前并没有用const 修饰,所以pi是指针变量,能被赋值重新指向另一内存地址的。你可能会疑问:那我又如何用const 来修饰pi呢?其实,你注意到int * const pi中const 的位置就大概可以明白了。请记住,通过格式看语义。哈哈,你可能已经看出了规律吧?那下面的一节也就没必要看下去了。不过我还得继续我的战斗!
3 再看int * const pi
确实,int * const pi与前面的int const * pi会很容易给混淆的。注意:前面一句的const 是写在pi前和*号后的,而不是写在*pi前的。很显然,它是修饰限定pi的。我先让你看例子:
//*************代码开始***************
int i1=30;
int i2=40;
int * const pi=&i1;
//pi=&i2; 4.注意这里,pi不能再这样重新赋值了,即不能再指向另一个新地址。
//所以我已经注释了它。
i1=80; //5.想想看:这里能用*pi=80;来代替吗?可以,这里可以通过*pi修改i1的值。
//请自行与前面一个例子比较。
printf( “%d”, *pi ) ; //6.输出是80
//***************代码结束*********************
语义分析:
看了这段代码,你明白了什么?有没有发现pi值是不能重新赋值修改了。它只能永远指向初始化时的内存地址了。相反,这次你可以通过*pi来修改i1的值了。与前一个例子对照一下吧!看以下的两点分析
1). pi因为有了const 的修饰,所以只是一个指针常量:也就是说pi值是不可修改的(即pi不可以重新指向i2这个变量了)(看第4行)。
2). 整个*pi的前面没有const 的修饰。也就是说,*pi是变量而不是常量,所以我们可以通过*pi来修改它所指内存i1的值(看5行的注释)
总之一句话,这次的pi是一个指向int变量类型数据的指针常量。
我最后总结两句:
1).如果const 修饰在*pi前则不能改的是*pi(即不能类似这样:*pi=50;赋值)而不是指pi。
2).如果const 是直接写在pi前则pi不能改(即不能类似这样:pi=&i;赋值)。
请你务必先记住这两点,相信你一定不会再被它们给搞糊了。现在再看这两个申明语句int const *pi和int * const pi时,呵呵,你会头昏脑胀还是很轻松惬意?它们各自申明的pi分别能修改什么,不能修改什么?再问问自己,把你的理解告诉我吧,可以发帖也可以发到我的邮箱(我的邮箱yyf977@163.com)!我一定会答复的。
3.补充三种情况。
这里,我再补充以下三种情况。其实只要上面的语义搞清楚了,这三种情况也就已经被包含了。不过作为三种具体的形式,我还是简单提一下吧!
情况一:int * pi指针指向const int i常量的情况
//**********begin*****************
const int i1=40;
int *pi;
pi=&i1; //这样可以吗?不行,VC下是编译错。
//const int 类型的i1的地址是不能赋值给指向int 类型地址的指针pi的。否则pi岂不是能修改i1的值了吗!
pi=(int* ) &i1; // 这样可以吗?强制类型转换可是C所支持的。
//VC下编译通过,但是仍不能通过*pi=80来修改i1的值。去试试吧!看看具体的怎样。
//***********end***************
情况二:const int * pi指针指向const int i1的情况
//*********begin****************
const int i1=40;
const int * pi;
pi=&i1;//两个类型相同,可以这样赋值。很显然,i1的值无论是通过pi还是i1都不能修改的。
//*********end*****************
情况三:用const int * const pi申明的指针
//***********begin****************
int i
const int * const pi=&i;//你能想象pi能够作什么操作吗?pi值不能改,也不能通过pi修改i的值。因为不管是*pi还是pi都是const的。
//************end****************
下篇预告:函数参数的指针传递,值传递,引用传递 迷惑(以为a,b已经代替了x,y,对x,y的操作就是对a,b的操作了,这是一个错误的观点啊!)。
彻底搞定C指针——第5篇:函数参数的传递
build:2007-1-30 17:58:50 author:白云小飞 editor:beyond update:2006-11-26 browse:99
一. 三道考题 开讲之前,我先请你做三道题目。(嘿嘿,得先把你的头脑搞昏才行……唉呀,谁扔我鸡蛋?)
1. 考题一:程序代码如下:
void Exchg1(int x, int y)
{
int tmp;
tmp=x;
x=y;
y=tmp;
printf(“x=%d,y=%d\n”,x,y)
}
void main()
{
int a=4,b=6;
Exchg1 (a,b) ;
printf(“a=%d,b=%d\n”,a,b)
}
输出的结果:
x=____, y=____
a=____, b=____
问下划线的部分应是什么,请完成。
2. 考题二:代码如下。
Exchg2(int *px, int *py)
{
int tmp=*px;
*px=*py;
*py=tmp;
print(“*px=%d,*py=%d\n”,*px,*py);
}
main()
{
int a=4;
int b=6;
Exchg2( &a,&b);
Print(“a=%d,b=%d\n”, a, b);
}
输出的结果为:
*px=____, *py=____
a=____, b=____
问下划线的部分应是什么,请完成。
3. 考题三:
Exchg2(int &x, int &y)
{
int tmp=x;
x=y;
y=tmp;
print(“x=%d,y=%d\n”,x,y);
}
main()
{
int a=4;
int b=6;
Exchg2(a,b);
Print(“a=%d,b=%d\n”, a, b);
}
输出的结果:
x=____, y=____
a=____, b=____
问下划线的部分输出的应是什么,请完成。
你不在机子上试,能作出来吗?你对你写出的答案有多大的把握?
正确的答案,想知道吗?(呵呵,让我慢慢地告诉你吧!)
好,废话少说,继续我们的探索之旅了。
我们都知道:C语言中函数参数的传递有:值传递,地址传递,引用传递这三种形式。题一为值传递,题二为地址传递,题三为引用传递。不过,正是这几种参数传递的形式,曾把我给搞得晕头转向。我相信也有很多人与我有同感吧?
下面请让我逐个地谈谈这三种传递形式。
二. 函数参数传递方式之一:值传递
1. 值传递的一个错误认识
先看题一中Exchg1函数的定义:
void Exchg1(int x, int y) //定义中的x,y变量被称为Exchg1函数的形式参数
{
int tmp;
tmp=x;
x=y;
y=tmp;
printf(“x=%d,y=%d\n”,x,y)
}
问:你认为这个函数是在做什么呀?
答:好像是对参数x,y的值对调吧?
请往下看,我想利用这个函数来完成对a,b两个变量值的对调,程序如下:
void main()
{
int a=4,b=6;
Exchg1 (a,b) //a,b变量为Exchg1函数的实际参数。
/ printf(“a=%d,b=%d\n”,a,b)
}
我问:Exchg1 ()里头的 printf(“x=%d,y=%d\n”,x,y)语句会输出什么啊?
我再问:Exchg1 ()后的 printf(“a=%d,b=%d\n”,a,b)语句输出的是什么?
程序输出的结果是:
x=6 , y=4
a=4 , b=6 //为什么不是a=6,b=4呢?
奇怪,明明我把a,b分别代入了x,y中,并在函数里完成了两个变量值的交换,为什么a,b变量值还是没有交换(仍然是a==4,b==6,而不是a==6,b==4)?如果你也会有这个疑问,那是因为你跟本就不知实参a,b与形参x,y的关系了。
2. 一个预备的常识
为了说明这个问题,我先给出一个代码:
int a=4;
int x;
x=a;
x=x+3;
看好了没,现在我问你:最终a值是多少,x值是多少?
(怎么搞的,给我这个小儿科的问题。还不简单,不就是a==4 x==7嘛!)
在这个代码中,你要明白一个东西:虽然a值赋给了x,但是a变量并不是x变量哦。我们对x任何的修改,都不会改变a变量。呵呵!虽然简单,并且一看就理所当然,不过可是一个很重要的认识喔。
3. 理解值传递的形式
看调用Exch1函数的代码:
main()
{
int a=4,b=6;
Exchg1(a,b) //这里调用了Exchg1函数
printf(“a=%d,b=%d”,a,b)
}
Exchg1(a,b)时所完成的操作代码如下所示。
int x=a;//←
int y=b;//←注意这里,头两行是调用函数时的隐含操作
int tmp;
tmp=x;
x=y;
y=tmp;
请注意在调用执行Exchg1函数的操作中我人为地加上了头两句:
int x=a;
int y=b;
这是调用函数时的两个隐含动作。它确实存在,现在我只不过把它显式地写了出来而已。问题一下就清晰起来啦。(看到这里,现在你认为函数里面交换操作的是a,b变量或者只是x,y变量呢?)
原来 ,其实函数在调用时是隐含地把实参a,b 的值分别赋值给了x,y,之后在你写的Exchg1函数体内再也没有对a,b进行任何的操作了。交换的只是x,y变量。并不是a,b。当然a,b的值没有改变啦!函数只是把a,b的值通过赋值传递给了x,y,函数里头操作的只是x,y的值并不是a,b的值。这就是所谓的参数的值传递了。
哈哈,终于明白了,正是因为它隐含了那两个的赋值操作,才让我们产生了前述的迷惑(以为a,b已经代替了x,y,对x,y的操作就是对a,b的操作了,这是一个错误的观点啊!)。
第6篇 指向另一指针的指针
build:2007-1-30 17:58:50 author:白云小飞 editor:beyond update:2006-11-26 browse:119
一.针概念:
早在本系列第二篇中我就对指针的实质进行了阐述。今天我们又要学习一个叫做指向另一指针地址的指针。让我们先回顾一下指针的概念吧!
当我们程序如下申明变量:
short int i;
char a;
short int * pi;
程序会在内存某地址空间上为各变量开辟空间,如下图所示。
内存地址→6 7 8 9 10 11 12 13 14 15
-------------------------------------------------------------------------------------
… | | | | | | | | | |
-------------------------------------------------------------------------------------
|short int i |char a| |short int * pi|
图中所示中可看出:
i 变量在内存地址5的位置,占两个字节。
a变量在内存地址7的位置,占一个字节。
pi变量在内存地址9的位置,占两个字节。(注:pi 是指针,我这里指针的宽度只有两个字节,32位系统是四个字节)
接下来如下赋值:
i=50;
pi=&i;
经过上在两句的赋值,变量的内存映象如下:
内存地址→6 7 8 9 10 11 12 13 14 15
--------------------------------------------------------------------------------------
… | 50 | | | 6 | | | |
--------------------------------------------------------------------------------------
|short int i |char a| |short int * pi|
看到没有:短整型指针变量pi的值为6,它就是I变量的内存起始地址。所以,这时当我们对*pi进行读写操作时,其实就是对i变量的读写操作。如:
*pi=5; //就是等价于I=5;
你可以回看本系列的第二篇,那里有更加详细的解说。
二. 指针的地址与指向另一指针地址的指针
在上一节中,我们看到,指针变量本身与其它变量一样也是在某个内存地址中的,如pi的内存起始地址是10。同样的,我们也可能让某个指针指向这个地址。
看下面代码:
short int * * ppi; //这是一个指向指针的指针,注意有两个*号
ppi=π
第一句:short int * * ppi;——申明了一个指针变量ppi,这个ppi是用来存储(或称指向)一个short int * 类型指针变量的地址。
第二句:&pi那就是取pi的地址,ppi=π就是把pi的地址赋给了ppi。即将地址值10赋值给ppi。如下图:
内存地址→6 7 8 9 10 11 12 13 14 15
------------------------------------------------------------------------------------
… | 50 | | | 6 | 10 | |
------------------------------------------------------------------------------------
|short int i|char a| |short int * pi|short int ** ppi|
从图中看出,指针变量ppi的内容就是指针变量pi的起始地址。于是……
ppi的值是多少呢?——10。
*ppi的值是多少呢?——6,即pi的值。
**ppi的值是多少呢?——50,即I的值,也是*pi的值。
呵呵!不用我说太多了,我相信你应明白这种指针了吧!
三. 一个应用实例
1. 设计一个函数:void find1(char array[], char search, char * pi)
要求:这个函数参数中的数组array是以0值为结束的字符串,要求在字符串array中查找字符是参数search里的字符。如果找到,函数通过第三个参数(pa)返回值为array字符串中第一个找到的字符的地址。如果没找到,则为pa为0。
设计:依题意,实现代码如下。
void find1(char [] array, char search, char * pa)
{
int i;
for (i=0;*(array+i)!=0;i++)
{
if (*(array+i)==search)
{
pa=array+i
break;
}
else if (*(array+i)==0)
{
pa=0;
break;
}
}
}
你觉得这个函数能实现所要求的功能吗?
调试:
我下面调用这个函数试试。
void main()
{
char str[]={“afsdfsdfdf\0”}; //待查找的字符串
char a=’d’; //设置要查找的字符
char * p=0; //如果查找到后指针p将指向字符串中查找到的第一个字符的地址。
find1(str,a,p); //调用函数以实现所要操作。
if (0==p )
{
printf (“没找到!\n”);//1.如果没找到则输出此句
}
else
{
printf(“找到了,p=%d”,p); //如果找到则输出此句
}
}
分析:
上面代码,你认为会是输出什么呢?
运行试试。
唉!怎么输出的是:没有找到!
而不是:找到了,……。
明明a值为’d’,而str字符串的第四个字符是’d’,应该找得到呀!
再看函数定义处:void find1(char [] array, char search, char * pa)
看调用处:find1(str,a,p);
依我在第五篇的分析方法,函数调用时会对每一个参数进行一个隐含的赋值操作。
整个调用如下:
array=str;
search=a;
pa=p; //请注意:以上三句是调用时隐含的动作。
int i;
for (i=0;*(array+i)!=0;i++)
{
if (*(array+i)==search)
{
pa=array+i
break;
}
else if (*(array+i)==0)
{
pa=0;
break;
}
}
哦!参数pa与参数search的传递并没有什么不同,都是值传递嘛(小语:地址传递其实就是地址值传递嘛)!所以对形参变量pa值(当然值是一个地址值)的修改并不会改变实参变量p值,因此p的值并没有改变(即p的指向并没有被改变)。
(如果还有疑问,再看一看《第五篇:函数参数的传递》了。)
修正:
void find2(char [] array, char search, char ** ppa)
{
int i;
for (i=0;*(array+i)!=0;i++)
{
if (*(array+i)==search)
{
*ppa=array+i
break;
}
else if (*(array+i)==0)
{
*ppa=0;
break;
}
}
}
主函数的调用处改如下:
find2(str,a,&p); //调用函数以实现所要操作。
再分析:
这样调用函数时的整个操作变成如下:
array=str;
search=a;
ppa=&p; //请注意:以上三句是调用时隐含的动作。
int i;
for (i=0;*(array+i)!=0;i++)
{
if (*(array+i)==search)
{
*ppa=array+i
break;
}
else if (*(array+i)==0)
{
*ppa=0;
break;
}
}
看明白了吗?
ppa指向指针p的地址。
对*ppa的修改就是对p值的修改。
你自行去调试。
经过修改后的程序就可以完成所要的功能了。
看懂了这个例子,也就达到了本篇所要求的目的。
第7篇 函数名与函数指针
build:2007-1-30 17:58:50 author:白云小飞 editor:beyond update:2006-11-26 browse:94
一 数调用
一个通常的函数调用的例子:
//自行包含头文件
void MyFun(int x); //此处的申明也可写成:void MyFun( int );
int main(int argc, char* argv[])
{
MyFun(10); //这里是调用MyFun(10);函数
return 0;
}
void MyFun(int x) //这里定义一个MyFun函数
{
printf(“%d\n”,x);
}
这个MyFun函数是一个无返回值的函数,它并不完成什么事情。这种调用函数的格式你应该是很熟悉的吧!看主函数中调用MyFun函数的书写格式:
MyFun(10);
我们一开始只是从功能上或者说从数学意义上理解MyFun这个函数,知道MyFun函数名代表的是一个功能(或是说一段代码)。
直到——
学习到函数指针概念时。我才不得不在思考:函数名到底又是什么东西呢?
(不要以为这是没有什么意义的事噢!呵呵,继续往下看你就知道了。)
二 函数指针变量的申明
就象某一数据变量的内存地址可以存储在相应的指针变量中一样,函数的首地址也以存储在某个函数指针变量里的。这样,我就可以通过这个函数指针变量来调用所指向的函数了。
在C系列语言中,任何一个变量,总是要先申明,之后才能使用的。那么,函数指针变量也应该要先申明吧?那又是如何来申明呢?以上面的例子为例,我来申明一个可以指向MyFun函数的函数指针变量FunP。下面就是申明FunP变量的方法:
void (*FunP)(int) ; //也可写成void (*FunP)(int x);
你看,整个函数指针变量的申明格式如同函数MyFun的申明处一样,只不过——我们把MyFun改成(*FunP)而已,这样就有了一个能指向MyFun函数的指针FunP了。(当然,这个FunP指针变量也可以指向所有其它具有相同参数及返回值的函数了。)
三 通过函数指针变量调用函数
有了FunP指针变量后,我们就可以对它赋值指向MyFun,然后通过FunP来调用MyFun函数了。看我如何通过FunP指针变量来调用MyFun函数的:
//自行包含头文件
void MyFun(int x); //这个申明也可写成:void MyFun( int );
void (*FunP)(int ); //也可申明成void(*FunP)(int x),但习惯上一般不这样。
int main(int argc, char* argv[])
{
MyFun(10); //这是直接调用MyFun函数
FunP=&MyFun; //将MyFun函数的地址赋给FunP变量
(*FunP)(20); //这是通过函数指针变量FunP来调用MyFun函数的。
}
void MyFun(int x) //这里定义一个MyFun函数
{
printf(“%d\n”,x);
}
请看黑体字部分的代码及注释。
运行看看。嗯,不错,程序运行得很好。
哦,我的感觉是:MyFun与FunP的类型关系类似于int 与int *的关系。函数MyFun好像是一个如int的变量(或常量),而FunP则像一个如int *一样的指针变量。
int i,*pi;
pi=&i; //与FunP=&MyFun比较。
(你的感觉呢?)
呵呵,其实不然——
四 调用函数的其它书写格式
函数指针也可如下使用,来完成同样的事情:
//自行包含头文件
void MyFun(int x);
void (*FunP)(int ); //申明一个用以指向同样参数,返回值函数的指针变量。
int main(int argc, char* argv[])
{
MyFun(10); //这里是调用MyFun(10);函数
FunP=MyFun; //将MyFun函数的地址赋给FunP变量
FunP(20); //这是通过函数指针变量来调用MyFun函数的。
return 0;
}
void MyFun(int x) //这里定义一个MyFun函数
{
printf(“%d\n”,x);
}
我改了黑体字部分(请自行与之前的代码比较一下)。
运行试试,啊!一样地成功。
咦?
FunP=MyFun;
可以这样将MyFun值同赋值给FunP,难道MyFun与FunP是同一数据类型(即如同的int 与int的关系),而不是如同int 与int*的关系了?(有没有一点点的糊涂了?)
看来与之前的代码有点矛盾了,是吧!所以我说嘛!
请容许我暂不给你解释,继续看以下几种情况(这些可都是可以正确运行的代码哟!):
代码之三:
int main(int argc, char* argv[])
{
MyFun(10); //这里是调用MyFun(10);函数
FunP=&MyFun; //将MyFun函数的地址赋给FunP变量
FunP(20); //这是通过函数指针变量来调用MyFun函数的。
return 0;
}
代码之四:
int main(int argc, char* argv[])
{
MyFun(10); //这里是调用MyFun(10);函数
FunP=MyFun; //将MyFun函数的地址赋给FunP变量
(*FunP)(20); //这是通过函数指针变量来调用MyFun函数的。
return 0;
}
真的是可以这样的噢!
(哇!真是要晕倒了!)
还有呐!看——
int main(int argc, char* argv[])
{
(*MyFun)(10); //看,函数名MyFun也可以有这样的调用格式
return 0;
}
你也许第一次见到吧:函数名调用也可以是这样写的啊!(只不过我们平常没有这样书写罢了。)
那么,这些又说明了什么呢?
呵呵!依据以往的知识和经验来推理本篇的“新发现”,我想就连“福尔摩斯”也必定会由此分析并推断出以下的结论:
1. 其实,MyFun的函数名与FunP函数指针都是一样的,即都是函数指针。MyFun函数名是一个函数指针常量,而FunP是一个函数数指针变量,这是它们的关系。
2. 但函数名调用如果都得如(*MyFun)(10);这样,那书写与读起来都是不方便和不习惯的。所以C语言的设计者们才会设计成又可允许MyFun(10);这种形式地调用(这样方便多了并与数学中的函数形式一样,不是吗?)。
3. 为统一起见,FunP函数指针变量也可以FunP(10)的形式来调用。
4. 赋值时,即可FunP=&MyFun形式,也可FunP=MyFun。
上述代码的写法,随便你爱怎么着!
请这样理解吧!这可是有助于你对函数指针的应用喽!
最后——
补充说明一点:在函数的申明处:
void MyFun(int ); //不能写成void (*MyFun)(int )。
void (*FunP)(int ); //不能写成void FunP(int )。
(请看注释)这一点是要注意的。
五 定义某一函数的指针类型:
就像自定义数据类型一样,我们也可以先定义一个函数指针类型,然后再用这个类型来申明函数指针变量。
我先给你一个自定义数据类型的例子。
typedef int* PINT; //为int* 类型定义了一个PINT的别名
int main()
{
int x;
PINT px=&x; //与int * px=&x;是等价的。PINT类型其实就是int * 类型
*px=10; //px就是int*类型的变量
return 0;
}
根据注释,应该不难看懂吧!(虽然你可能很少这样定义使用,但以后学习Win32编程时会经常见到的。)
下面我们来看一下函数指针类型的定义及使用:(请与上对照!)
//自行包含头文件
void MyFun(int x); //此处的申明也可写成:void MyFun( int );
typedef void (*FunType)(int ); //这样只是定义一个函数指针类型
FunType FunP; //然后用FunType类型来申明全局FunP变量
int main(int argc, char* argv[])
{
//FunType FunP; //函数指针变量当然也是可以是局部的 ,那就请在这里申明了。
MyFun(10);
FunP=&MyFun;
(*FunP)(20);
return 0;
}
void MyFun(int x)
{
printf(“%d\n”,x);
}
看黑体部分:
首先,在void (*FunType)(int ); 前加了一个typedef 。这样只是定义一个名为FunType函数指针类型,而不是一个FunType变量。
然后,FunType FunP; 这句就如PINT px;一样地申明一个FunP变量。
其它相同。整个程序完成了相同的事。
这样做法的好处是:
有了FunType类型后,我们就可以同样地、很方便地用FunType类型来申明多个同类型的函数指针变量了。如下:
FunType FunP2;
FunType FunP3;
//……
六 函数指针作为某个函数的参数
既然函数指针变量是一个变量,当然也可以作为某个函数的参数来使用的。所以,你还应知道函数指针是如何作为某个函数的参数来传递使用的。
给你一个实例:
要求:我要设计一个CallMyFun函数,这个函数可以通过参数中的函数指针值不同来分别调用MyFun1、MyFun2、MyFun3这三个函数(注:这三个函数的定义格式应相同)。
实现:代码如下:
//自行包含头文件
void MyFun1(int x);
void MyFun2(int x);
void MyFun3(int x);
typedef void (*FunType)(int ); //②. 定义一个函数指针类型FunType,与①函数类型一至
void CallMyFun(FunType fp,int x);
int main(int argc, char* argv[])
{
CallMyFun(MyFun1,10); //⑤. 通过CallMyFun函数分别调用三个不同的函数
CallMyFun(MyFun2,20);
CallMyFun(MyFun3,30);
}
void CallMyFun(FunType fp,int x) //③. 参数fp的类型是FunType。
{
fp(x);//④. 通过fp的指针执行传递进来的函数,注意fp所指的函数是有一个参数的
}
void MyFun1(int x) // ①. 这是个有一个参数的函数,以下两个函数也相同
{
printf(“函数MyFun1中输出:%d\n”,x);
}
void MyFun2(int x)
{
printf(“函数MyFun2中输出:%d\n”,x);
}
void MyFun3(int x)
{
printf(“函数MyFun3中输出:%d\n”,x);
}
输出结果:略
分析:(看我写的注释。你可按我注释的①②③④⑤顺序自行分析。)
要理解C指针,我认为一定要理解C中“变量”的存储实质,所以我就从“变量”这个东西开始讲起吧!
先来理解理解内存空间吧!请看下图:
内存地址→ 6 7 8 9 10 11 12 13
-----------------------------------------------------------------
··· | | | | | | | |··
-----------------------------------------------------------------
如图所示,内存只不过是一个存放数据的空间,就好像我的看电影时的电影院中的座位一样。每个座位都要编号,我们的内存要存放各种各样的数据,当然我们要知道我们的这些数据存放在什么位置吧!所以内存也要象座位一样进行编号了,这就是我们所说的内存编址。座位可以是按一个座位一个号码的从一号开始编号,内存则是按一个字节一个字节进行编址,如上图所示。每个字节都有个编号,我们称之为内存地址。好了,我说了这么多,现在你能理解内存空间这个概念吗?
我们继续看看以下的C、C++语言变量申明:
int I;
char a;
每次我们要使用某变量时都要事先这样申明它,它其实是内存中申请了一个名为i的整型变量宽度的空间(DOS下的16位编程中其宽度为二个字节),和一个名为a的字符型变量宽度的空间(占一个字节)。
我们又如何来理解变量是如何存在的呢。当我们如下申明变量时:
int I;
char a;
内存中的映象可能如下图:
内存地址→ 6 7 8 9 10 11 12 13
------------------------------------------------------------------
···| | | | | | | |··
------------------------------------------------------------------
变量名|→i ←|→a ←|
图中可看出,i在内存起始地址为6上申请了两个字节的空间(我这里假设了int的宽度为16位,不同系统中int的宽度是可能不一样的),并命名为i。 a在内存地址为8上申请了一字节的空间,并命名为a。这样我们就有两个不同类型的变量了。
2.赋值给变量
再看下面赋值:
i=30
a=’t’
你当然知道个两个语句是将30存入i变量的内存空间中,将’t’字符存入a变量的内存空间中。我们可以这样的形象理解啦:
内存地址→ 6 7 8 9 10 11 12 13
-----------------------------------------------------------------------
··· | 30 | ‘t’ | | | | |··
-----------------------------------------------------------------------
|→i ←|→a ←|
3.变量在哪里?(即我想知道变量的地址)
好了,接下来我们来看看&i是什么意思?
是取i变量所在的地址编号嘛!我们可以这样读它:返回i变量的地址编号。你记住了吗?
我要在屏幕上显示变量的地址值的话,可以写如下代码:
printf(“%d”,&i);
以上图的内存映象所例,屏幕上显示的不是i值30,而是显示i的内存地址编号6了。当然实际你操作的时,i变量的地址值不会是这个数了。
这就是我认为作为初学者们所应想象的变量存储实质了。请这样理解吧!
最后总结代码如下:
int main()
{
int i=39;
printf(“%d\n”,i); //①
printf(“%d\n”, &i); //②
}
现在你可知道①、②两个printf分别在屏幕上输出的是i的什么东西啊?
好啦!下面我们就开始真正进入指针的学习了。Come on !(待续…)
二.指针是什么东西<想说弄懂你不容易啊!我们许多初学指针的人都要这样的感慨。我常常在思索它,为什么呢?其实生活中处处都有指针。我们也处处在使用它。有了它我们的生活才更加方便了。没有指针,那生活才不方便。不信?你看下面的例子。
这是一个生活中的例子:比如说你要我借给你一本书,我到了你宿舍,但是你人不在宿舍,于是我把书放在你的2层3号的书架上,并写了一张纸条放在你的桌上。纸条上写着:你要的书在第2层3号的书架上。当你回来时,看到这张纸条。你就知道了我借与你的书放在哪了。你想想看,这张纸条的作用,纸条本身不是书,它上面也没有放着书。那么你又如何知道书的位置呢?因为纸条上写着书的位置嘛!其实这张纸条就是一个指针了。它上面的内容不是书本身,而是书的地址,你通过纸条这个指针找到了我借给你的本书。
那么我们C,C++中的指针又是什么呢?请继续跟我来吧,看下面看一个申明一整型指针变量的语句如下:
int * pi;
pi是一个指针,当然我们知道啦,但是这样说,你就以为pi一定是个多么特别的东西了。其实,它也只过是一个变量而已。与上一篇中说的变量并没有实质的区别。不信你看下面图。
内存地址→6 7 8 9 10 11 12 13 14
--------------------------------------------------------------
···| 30 | ‘t’ | | | | | | |...
--------------------------------------------------------------
变量 |→i ←|→a ←| |→ pi ←|
(说明:这里我假设了指针只占2个字节宽度,实际上在32位系统中,指针的宽度是4个字节宽的,即32位。)由图示中可以看出,我们使用int *Pi申明指针变量; 其实是在内存的某处申明一个一定宽度的内存空间,并把它命名为Pi。你能在图中看出pi与前面的i,a 变量有什么本质区别吗,没有,当然没有!pi也只不过是一个变量而已嘛!那么它又为什么会被称为指针?关键是我们要让这个变量所存储的内容是什么。现在我要让pi成为真正有意义上的指针。请接着看下面语句:
pi=&i;
你应该知道 &i是什么意思吧!再次提醒你啦:这是返回i变量的地址编号。整句的意思就是把i地址的编号赋值给pi,也就是你在pi上写上i的地址编号。结果如下图所示:
内存地址→6 7 8 9 10 11 12 13 14
------------------------------------------------------------------
···| 30 | ‘t’ | | | 6 | | |...
------------------------------------------------------------------
变量 |→i ←|→a ←| |→ pi ←|
你看,执行完pi=&i;后,在图示中的系统中,pi的值是6。这个6就是i变量的地址编号,这样pi就指向了变量i了。你看,pi与那张纸条有什么区别?pi不就是那张纸条嘛!上面写着i的地址,而i就是那个本书。你现在看懂了吗?因此,我们就把pi称为指针。所以你要记住,指针变量所存的内容就是内存的地址编号!好了,现在我们就可以通过这个指针pi来访问到i这个变量了,不是吗?。看下面语句:
printf(“%d”,*pi);
那么*pi什么意思呢?你只要这样读它:pi内容所指的地址的内容(嘻嘻,看上去好像在绕口令了),就pi这张“纸条”上所写的位置上的那本 “书”---i 。你看,Pi内容是6,也就是说pi指向内存编号为6的地址。*pi嘛!就是它所指地址的内容,即地址编号6上的内容了。当然就是30的值了。所以这条语句会在屏幕上显示30。也就是说printf(“%d”,*pi);语句等价于printf( “%d”, i ) ,请结合上图好好体会吧!各位还有什么疑问,可以发Email:yyf977@163.com。
到此为止,你掌握了类似&i , *pi写法的含义和相关操作吗。总的一句话,我们的纸条就是我们的指针,同样我们的pi也就是我们的纸条!剩下的就是我们如何应用这张纸条了。最后我给你一道题:程序如下。
char a,*pa
a=10
pa=&a
*pa=20
printf( “%d”, a)
你能直接看出输出的结果是什么吗?如果你能,我想本篇的目的就达到了。好了,就说到这了。Happy to Study!在下篇中我将谈谈“指针的指针”即对int * * ppa;中ppa 的理解。
《彻底搞定C指针》第3篇--指针与数组名
build:2007-1-30 17:58:50 author:白云小飞 editor:beyond update:2006-11-26 browse:76
1.数组元素
看下面代码
int i,a[]={3,4,5,6,7,3,7,4,4,6};
for (i=0;i<=9;i++)
{
printf ( “%d”, a[i] );
}
很显然,它是显示a 数组的各元素值。
我们还可以这样访问元素,如下
int i,a[]={3,4,5,6,7,3,7,4,4,6};
for (i=0;i<=9;i++)
{
printf ( “%d”, *(a+i) );
}
它的结果和作用完全一样
2. 通过指针访问数组元素
int i,*pa,a[]={3,4,5,6,7,3,7,4,4,6};
pa =a ;//请注意数组名a直接赋值给指针pa
for (i=0;i<=9;i++)
{
printf ( “%d”, pa[i] );
}
很显然,它也是显示a 数组的各元素值。
另外与数组名一样也可如下:
int i,*pa,a[]={3,4,5,6,7,3,7,4,4,6};
pa =a;
for (i=0;i<=9;i++)
{
printf ( “%d”, *(pa+i) );
}
看pa=a即数组名赋值给指针,以及通过数组名、指针对元素的访问形式看,它们并没有什么区别,从这里可以看出数组名其实也就是指针。难道它们没有任何区别?有,请继续。
3. 数组名与指针变量的区别
请看下面的代码:
int i,*pa,a[]={3,4,5,6,7,3,7,4,4,6};
pa =a;
for (i=0;i<=9;i++)
{
printf ( “%d”, *pa );
pa++ ; //注意这里,指针值被修改
}
可以看出,这段代码也是将数组各元素值输出。不过,你把{}中的pa改成a试试。你会发现程序编译出错,不能成功。看来指针和数组名还是不同的。其实上面的指针是指针变量,而数组名只是一个指针常量。这个代码与上面的代码不同的是,指针pa在整个循环中,其值是不断递增的,即指针值被修改了。数组名是指针常量,其值是不能修改的,因此不能类似这样操作:a++。前面4,5节中pa[i],*(pa+i)处,指针pa的值是使终没有改变。所以变量指针pa与数组名a可以互换。
4. 申明指针常量
再请看下面的代码:
int i, a[]={3,4,5,6,7,3,7,4,4,6};
int * const pa=a;//注意const的位置:不是const int * pa,
for (i=0;i<=9;i++)
{
printf ( “%d”, *pa );
pa++ ; //注意这里,指针值被修改
}
这时候的代码能成功编译吗?不能。因为pa指针被定义为常量指针了。这时与数组名a已经没有不同。这更说明了数组名就是常量指针。但是…
int * const a={3,4,5,6,7,3,7,4,4,6};//不行
int a[]={3,4,5,6,7,3,7,4,4,6};//可以,所以初始化数组时必定要这样。
以上都是在VC6.0上实验。
《彻底搞定C指针》第4篇const int * pi/int * const pi的区别
build:2007-1-30 17:58:50 author:白云小飞 editor:beyond update:2006-11-26 browse:69
1 int i 说起
你知道我们申明一个变量时象这样int i ;这个i是可能在它处重新变赋值的。如下:
int i=0;
//…
i=20;//这里重新赋值了
不过有一天我的程序可能需要这样一个变量(暂且称它变量),在申明时就赋一个初始值。之后我的程序在其它任何处都不会再去重新对它赋值。那我又应该怎么办呢?用const 。
//**************
const int ic =20;
//…
ic=40;//这样是不可以的,编译时是无法通过,因为我们不能对const 修饰的ic重新赋值的。
//这样我们的程序就会更早更容易发现问题了。
//**************
有了const修饰的ic 我们不称它为变量,而称符号常量,代表着20这个数。这就是const 的作用。ic是不能在它处重新赋新值了。
认识了const 作用之后,另外,我们还要知道格式的写法。有两种:const int ic=20;与int const ic=20;。它们是完全相同的。这一点我们是要清楚。总之,你务必要记住const 与int哪个写前都不影响语义。有了这个概念后,我们来看这两个家伙:const int * pi与int const * pi ,按你的逻辑看,它们的语义有不同吗?呵呵,你只要记住一点,int 与const 哪个放前哪个放后都是一样的,就好比const int ic;与int const ic;一样。也就是说,它们是相同的。
好了,我们现在已经搞定一个“双包胎”的问题。那么int * const pi与前两个式子又有什么不同呢?我下面就来具体分析它们的格式与语义吧!
2 const int * pi的语义
我先来说说const int * pi是什么作用 (当然int const * pi也是一样的,前面我们说过,它们实际是一样的)。看下面的例子:
//*************代码开始***************
int i1=30;
int i2=40;
const int * pi=&i1;
pi=&i2; //4.注意这里,pi可以在任意时候重新赋值一个新内存地址
i2=80; //5.想想看:这里能用*pi=80;来代替吗?当然不能
printf( “%d”, *pi ) ; //6.输出是80
//*************代码结束***************
语义分析:
看出来了没有啊,pi的值是可以被修改的。即它可以重新指向另一个地址的,但是,不能通过*pi来修改i2的值。这个规则符合我们前面所讲的逻辑吗?当然符合了!
首先const 修饰的是整个*pi(注意,我写的是*pi而不是pi)。所以*pi是常量,是不能被赋值的(虽然pi所指的i2是变量,不是常量)。
其次,pi前并没有用const 修饰,所以pi是指针变量,能被赋值重新指向另一内存地址的。你可能会疑问:那我又如何用const 来修饰pi呢?其实,你注意到int * const pi中const 的位置就大概可以明白了。请记住,通过格式看语义。哈哈,你可能已经看出了规律吧?那下面的一节也就没必要看下去了。不过我还得继续我的战斗!
3 再看int * const pi
确实,int * const pi与前面的int const * pi会很容易给混淆的。注意:前面一句的const 是写在pi前和*号后的,而不是写在*pi前的。很显然,它是修饰限定pi的。我先让你看例子:
//*************代码开始***************
int i1=30;
int i2=40;
int * const pi=&i1;
//pi=&i2; 4.注意这里,pi不能再这样重新赋值了,即不能再指向另一个新地址。
//所以我已经注释了它。
i1=80; //5.想想看:这里能用*pi=80;来代替吗?可以,这里可以通过*pi修改i1的值。
//请自行与前面一个例子比较。
printf( “%d”, *pi ) ; //6.输出是80
//***************代码结束*********************
语义分析:
看了这段代码,你明白了什么?有没有发现pi值是不能重新赋值修改了。它只能永远指向初始化时的内存地址了。相反,这次你可以通过*pi来修改i1的值了。与前一个例子对照一下吧!看以下的两点分析
1). pi因为有了const 的修饰,所以只是一个指针常量:也就是说pi值是不可修改的(即pi不可以重新指向i2这个变量了)(看第4行)。
2). 整个*pi的前面没有const 的修饰。也就是说,*pi是变量而不是常量,所以我们可以通过*pi来修改它所指内存i1的值(看5行的注释)
总之一句话,这次的pi是一个指向int变量类型数据的指针常量。
我最后总结两句:
1).如果const 修饰在*pi前则不能改的是*pi(即不能类似这样:*pi=50;赋值)而不是指pi。
2).如果const 是直接写在pi前则pi不能改(即不能类似这样:pi=&i;赋值)。
请你务必先记住这两点,相信你一定不会再被它们给搞糊了。现在再看这两个申明语句int const *pi和int * const pi时,呵呵,你会头昏脑胀还是很轻松惬意?它们各自申明的pi分别能修改什么,不能修改什么?再问问自己,把你的理解告诉我吧,可以发帖也可以发到我的邮箱(我的邮箱yyf977@163.com)!我一定会答复的。
3.补充三种情况。
这里,我再补充以下三种情况。其实只要上面的语义搞清楚了,这三种情况也就已经被包含了。不过作为三种具体的形式,我还是简单提一下吧!
情况一:int * pi指针指向const int i常量的情况
//**********begin*****************
const int i1=40;
int *pi;
pi=&i1; //这样可以吗?不行,VC下是编译错。
//const int 类型的i1的地址是不能赋值给指向int 类型地址的指针pi的。否则pi岂不是能修改i1的值了吗!
pi=(int* ) &i1; // 这样可以吗?强制类型转换可是C所支持的。
//VC下编译通过,但是仍不能通过*pi=80来修改i1的值。去试试吧!看看具体的怎样。
//***********end***************
情况二:const int * pi指针指向const int i1的情况
//*********begin****************
const int i1=40;
const int * pi;
pi=&i1;//两个类型相同,可以这样赋值。很显然,i1的值无论是通过pi还是i1都不能修改的。
//*********end*****************
情况三:用const int * const pi申明的指针
//***********begin****************
int i
const int * const pi=&i;//你能想象pi能够作什么操作吗?pi值不能改,也不能通过pi修改i的值。因为不管是*pi还是pi都是const的。
//************end****************
下篇预告:函数参数的指针传递,值传递,引用传递 迷惑(以为a,b已经代替了x,y,对x,y的操作就是对a,b的操作了,这是一个错误的观点啊!)。
彻底搞定C指针——第5篇:函数参数的传递
build:2007-1-30 17:58:50 author:白云小飞 editor:beyond update:2006-11-26 browse:99
一. 三道考题 开讲之前,我先请你做三道题目。(嘿嘿,得先把你的头脑搞昏才行……唉呀,谁扔我鸡蛋?)
1. 考题一:程序代码如下:
void Exchg1(int x, int y)
{
int tmp;
tmp=x;
x=y;
y=tmp;
printf(“x=%d,y=%d\n”,x,y)
}
void main()
{
int a=4,b=6;
Exchg1 (a,b) ;
printf(“a=%d,b=%d\n”,a,b)
}
输出的结果:
x=____, y=____
a=____, b=____
问下划线的部分应是什么,请完成。
2. 考题二:代码如下。
Exchg2(int *px, int *py)
{
int tmp=*px;
*px=*py;
*py=tmp;
print(“*px=%d,*py=%d\n”,*px,*py);
}
main()
{
int a=4;
int b=6;
Exchg2( &a,&b);
Print(“a=%d,b=%d\n”, a, b);
}
输出的结果为:
*px=____, *py=____
a=____, b=____
问下划线的部分应是什么,请完成。
3. 考题三:
Exchg2(int &x, int &y)
{
int tmp=x;
x=y;
y=tmp;
print(“x=%d,y=%d\n”,x,y);
}
main()
{
int a=4;
int b=6;
Exchg2(a,b);
Print(“a=%d,b=%d\n”, a, b);
}
输出的结果:
x=____, y=____
a=____, b=____
问下划线的部分输出的应是什么,请完成。
你不在机子上试,能作出来吗?你对你写出的答案有多大的把握?
正确的答案,想知道吗?(呵呵,让我慢慢地告诉你吧!)
好,废话少说,继续我们的探索之旅了。
我们都知道:C语言中函数参数的传递有:值传递,地址传递,引用传递这三种形式。题一为值传递,题二为地址传递,题三为引用传递。不过,正是这几种参数传递的形式,曾把我给搞得晕头转向。我相信也有很多人与我有同感吧?
下面请让我逐个地谈谈这三种传递形式。
二. 函数参数传递方式之一:值传递
1. 值传递的一个错误认识
先看题一中Exchg1函数的定义:
void Exchg1(int x, int y) //定义中的x,y变量被称为Exchg1函数的形式参数
{
int tmp;
tmp=x;
x=y;
y=tmp;
printf(“x=%d,y=%d\n”,x,y)
}
问:你认为这个函数是在做什么呀?
答:好像是对参数x,y的值对调吧?
请往下看,我想利用这个函数来完成对a,b两个变量值的对调,程序如下:
void main()
{
int a=4,b=6;
Exchg1 (a,b) //a,b变量为Exchg1函数的实际参数。
/ printf(“a=%d,b=%d\n”,a,b)
}
我问:Exchg1 ()里头的 printf(“x=%d,y=%d\n”,x,y)语句会输出什么啊?
我再问:Exchg1 ()后的 printf(“a=%d,b=%d\n”,a,b)语句输出的是什么?
程序输出的结果是:
x=6 , y=4
a=4 , b=6 //为什么不是a=6,b=4呢?
奇怪,明明我把a,b分别代入了x,y中,并在函数里完成了两个变量值的交换,为什么a,b变量值还是没有交换(仍然是a==4,b==6,而不是a==6,b==4)?如果你也会有这个疑问,那是因为你跟本就不知实参a,b与形参x,y的关系了。
2. 一个预备的常识
为了说明这个问题,我先给出一个代码:
int a=4;
int x;
x=a;
x=x+3;
看好了没,现在我问你:最终a值是多少,x值是多少?
(怎么搞的,给我这个小儿科的问题。还不简单,不就是a==4 x==7嘛!)
在这个代码中,你要明白一个东西:虽然a值赋给了x,但是a变量并不是x变量哦。我们对x任何的修改,都不会改变a变量。呵呵!虽然简单,并且一看就理所当然,不过可是一个很重要的认识喔。
3. 理解值传递的形式
看调用Exch1函数的代码:
main()
{
int a=4,b=6;
Exchg1(a,b) //这里调用了Exchg1函数
printf(“a=%d,b=%d”,a,b)
}
Exchg1(a,b)时所完成的操作代码如下所示。
int x=a;//←
int y=b;//←注意这里,头两行是调用函数时的隐含操作
int tmp;
tmp=x;
x=y;
y=tmp;
请注意在调用执行Exchg1函数的操作中我人为地加上了头两句:
int x=a;
int y=b;
这是调用函数时的两个隐含动作。它确实存在,现在我只不过把它显式地写了出来而已。问题一下就清晰起来啦。(看到这里,现在你认为函数里面交换操作的是a,b变量或者只是x,y变量呢?)
原来 ,其实函数在调用时是隐含地把实参a,b 的值分别赋值给了x,y,之后在你写的Exchg1函数体内再也没有对a,b进行任何的操作了。交换的只是x,y变量。并不是a,b。当然a,b的值没有改变啦!函数只是把a,b的值通过赋值传递给了x,y,函数里头操作的只是x,y的值并不是a,b的值。这就是所谓的参数的值传递了。
哈哈,终于明白了,正是因为它隐含了那两个的赋值操作,才让我们产生了前述的迷惑(以为a,b已经代替了x,y,对x,y的操作就是对a,b的操作了,这是一个错误的观点啊!)。
第6篇 指向另一指针的指针
build:2007-1-30 17:58:50 author:白云小飞 editor:beyond update:2006-11-26 browse:119
一.针概念:
早在本系列第二篇中我就对指针的实质进行了阐述。今天我们又要学习一个叫做指向另一指针地址的指针。让我们先回顾一下指针的概念吧!
当我们程序如下申明变量:
short int i;
char a;
short int * pi;
程序会在内存某地址空间上为各变量开辟空间,如下图所示。
内存地址→6 7 8 9 10 11 12 13 14 15
-------------------------------------------------------------------------------------
… | | | | | | | | | |
-------------------------------------------------------------------------------------
|short int i |char a| |short int * pi|
图中所示中可看出:
i 变量在内存地址5的位置,占两个字节。
a变量在内存地址7的位置,占一个字节。
pi变量在内存地址9的位置,占两个字节。(注:pi 是指针,我这里指针的宽度只有两个字节,32位系统是四个字节)
接下来如下赋值:
i=50;
pi=&i;
经过上在两句的赋值,变量的内存映象如下:
内存地址→6 7 8 9 10 11 12 13 14 15
--------------------------------------------------------------------------------------
… | 50 | | | 6 | | | |
--------------------------------------------------------------------------------------
|short int i |char a| |short int * pi|
看到没有:短整型指针变量pi的值为6,它就是I变量的内存起始地址。所以,这时当我们对*pi进行读写操作时,其实就是对i变量的读写操作。如:
*pi=5; //就是等价于I=5;
你可以回看本系列的第二篇,那里有更加详细的解说。
二. 指针的地址与指向另一指针地址的指针
在上一节中,我们看到,指针变量本身与其它变量一样也是在某个内存地址中的,如pi的内存起始地址是10。同样的,我们也可能让某个指针指向这个地址。
看下面代码:
short int * * ppi; //这是一个指向指针的指针,注意有两个*号
ppi=π
第一句:short int * * ppi;——申明了一个指针变量ppi,这个ppi是用来存储(或称指向)一个short int * 类型指针变量的地址。
第二句:&pi那就是取pi的地址,ppi=π就是把pi的地址赋给了ppi。即将地址值10赋值给ppi。如下图:
内存地址→6 7 8 9 10 11 12 13 14 15
------------------------------------------------------------------------------------
… | 50 | | | 6 | 10 | |
------------------------------------------------------------------------------------
|short int i|char a| |short int * pi|short int ** ppi|
从图中看出,指针变量ppi的内容就是指针变量pi的起始地址。于是……
ppi的值是多少呢?——10。
*ppi的值是多少呢?——6,即pi的值。
**ppi的值是多少呢?——50,即I的值,也是*pi的值。
呵呵!不用我说太多了,我相信你应明白这种指针了吧!
三. 一个应用实例
1. 设计一个函数:void find1(char array[], char search, char * pi)
要求:这个函数参数中的数组array是以0值为结束的字符串,要求在字符串array中查找字符是参数search里的字符。如果找到,函数通过第三个参数(pa)返回值为array字符串中第一个找到的字符的地址。如果没找到,则为pa为0。
设计:依题意,实现代码如下。
void find1(char [] array, char search, char * pa)
{
int i;
for (i=0;*(array+i)!=0;i++)
{
if (*(array+i)==search)
{
pa=array+i
break;
}
else if (*(array+i)==0)
{
pa=0;
break;
}
}
}
你觉得这个函数能实现所要求的功能吗?
调试:
我下面调用这个函数试试。
void main()
{
char str[]={“afsdfsdfdf\0”}; //待查找的字符串
char a=’d’; //设置要查找的字符
char * p=0; //如果查找到后指针p将指向字符串中查找到的第一个字符的地址。
find1(str,a,p); //调用函数以实现所要操作。
if (0==p )
{
printf (“没找到!\n”);//1.如果没找到则输出此句
}
else
{
printf(“找到了,p=%d”,p); //如果找到则输出此句
}
}
分析:
上面代码,你认为会是输出什么呢?
运行试试。
唉!怎么输出的是:没有找到!
而不是:找到了,……。
明明a值为’d’,而str字符串的第四个字符是’d’,应该找得到呀!
再看函数定义处:void find1(char [] array, char search, char * pa)
看调用处:find1(str,a,p);
依我在第五篇的分析方法,函数调用时会对每一个参数进行一个隐含的赋值操作。
整个调用如下:
array=str;
search=a;
pa=p; //请注意:以上三句是调用时隐含的动作。
int i;
for (i=0;*(array+i)!=0;i++)
{
if (*(array+i)==search)
{
pa=array+i
break;
}
else if (*(array+i)==0)
{
pa=0;
break;
}
}
哦!参数pa与参数search的传递并没有什么不同,都是值传递嘛(小语:地址传递其实就是地址值传递嘛)!所以对形参变量pa值(当然值是一个地址值)的修改并不会改变实参变量p值,因此p的值并没有改变(即p的指向并没有被改变)。
(如果还有疑问,再看一看《第五篇:函数参数的传递》了。)
修正:
void find2(char [] array, char search, char ** ppa)
{
int i;
for (i=0;*(array+i)!=0;i++)
{
if (*(array+i)==search)
{
*ppa=array+i
break;
}
else if (*(array+i)==0)
{
*ppa=0;
break;
}
}
}
主函数的调用处改如下:
find2(str,a,&p); //调用函数以实现所要操作。
再分析:
这样调用函数时的整个操作变成如下:
array=str;
search=a;
ppa=&p; //请注意:以上三句是调用时隐含的动作。
int i;
for (i=0;*(array+i)!=0;i++)
{
if (*(array+i)==search)
{
*ppa=array+i
break;
}
else if (*(array+i)==0)
{
*ppa=0;
break;
}
}
看明白了吗?
ppa指向指针p的地址。
对*ppa的修改就是对p值的修改。
你自行去调试。
经过修改后的程序就可以完成所要的功能了。
看懂了这个例子,也就达到了本篇所要求的目的。
第7篇 函数名与函数指针
build:2007-1-30 17:58:50 author:白云小飞 editor:beyond update:2006-11-26 browse:94
一 数调用
一个通常的函数调用的例子:
//自行包含头文件
void MyFun(int x); //此处的申明也可写成:void MyFun( int );
int main(int argc, char* argv[])
{
MyFun(10); //这里是调用MyFun(10);函数
return 0;
}
void MyFun(int x) //这里定义一个MyFun函数
{
printf(“%d\n”,x);
}
这个MyFun函数是一个无返回值的函数,它并不完成什么事情。这种调用函数的格式你应该是很熟悉的吧!看主函数中调用MyFun函数的书写格式:
MyFun(10);
我们一开始只是从功能上或者说从数学意义上理解MyFun这个函数,知道MyFun函数名代表的是一个功能(或是说一段代码)。
直到——
学习到函数指针概念时。我才不得不在思考:函数名到底又是什么东西呢?
(不要以为这是没有什么意义的事噢!呵呵,继续往下看你就知道了。)
二 函数指针变量的申明
就象某一数据变量的内存地址可以存储在相应的指针变量中一样,函数的首地址也以存储在某个函数指针变量里的。这样,我就可以通过这个函数指针变量来调用所指向的函数了。
在C系列语言中,任何一个变量,总是要先申明,之后才能使用的。那么,函数指针变量也应该要先申明吧?那又是如何来申明呢?以上面的例子为例,我来申明一个可以指向MyFun函数的函数指针变量FunP。下面就是申明FunP变量的方法:
void (*FunP)(int) ; //也可写成void (*FunP)(int x);
你看,整个函数指针变量的申明格式如同函数MyFun的申明处一样,只不过——我们把MyFun改成(*FunP)而已,这样就有了一个能指向MyFun函数的指针FunP了。(当然,这个FunP指针变量也可以指向所有其它具有相同参数及返回值的函数了。)
三 通过函数指针变量调用函数
有了FunP指针变量后,我们就可以对它赋值指向MyFun,然后通过FunP来调用MyFun函数了。看我如何通过FunP指针变量来调用MyFun函数的:
//自行包含头文件
void MyFun(int x); //这个申明也可写成:void MyFun( int );
void (*FunP)(int ); //也可申明成void(*FunP)(int x),但习惯上一般不这样。
int main(int argc, char* argv[])
{
MyFun(10); //这是直接调用MyFun函数
FunP=&MyFun; //将MyFun函数的地址赋给FunP变量
(*FunP)(20); //这是通过函数指针变量FunP来调用MyFun函数的。
}
void MyFun(int x) //这里定义一个MyFun函数
{
printf(“%d\n”,x);
}
请看黑体字部分的代码及注释。
运行看看。嗯,不错,程序运行得很好。
哦,我的感觉是:MyFun与FunP的类型关系类似于int 与int *的关系。函数MyFun好像是一个如int的变量(或常量),而FunP则像一个如int *一样的指针变量。
int i,*pi;
pi=&i; //与FunP=&MyFun比较。
(你的感觉呢?)
呵呵,其实不然——
四 调用函数的其它书写格式
函数指针也可如下使用,来完成同样的事情:
//自行包含头文件
void MyFun(int x);
void (*FunP)(int ); //申明一个用以指向同样参数,返回值函数的指针变量。
int main(int argc, char* argv[])
{
MyFun(10); //这里是调用MyFun(10);函数
FunP=MyFun; //将MyFun函数的地址赋给FunP变量
FunP(20); //这是通过函数指针变量来调用MyFun函数的。
return 0;
}
void MyFun(int x) //这里定义一个MyFun函数
{
printf(“%d\n”,x);
}
我改了黑体字部分(请自行与之前的代码比较一下)。
运行试试,啊!一样地成功。
咦?
FunP=MyFun;
可以这样将MyFun值同赋值给FunP,难道MyFun与FunP是同一数据类型(即如同的int 与int的关系),而不是如同int 与int*的关系了?(有没有一点点的糊涂了?)
看来与之前的代码有点矛盾了,是吧!所以我说嘛!
请容许我暂不给你解释,继续看以下几种情况(这些可都是可以正确运行的代码哟!):
代码之三:
int main(int argc, char* argv[])
{
MyFun(10); //这里是调用MyFun(10);函数
FunP=&MyFun; //将MyFun函数的地址赋给FunP变量
FunP(20); //这是通过函数指针变量来调用MyFun函数的。
return 0;
}
代码之四:
int main(int argc, char* argv[])
{
MyFun(10); //这里是调用MyFun(10);函数
FunP=MyFun; //将MyFun函数的地址赋给FunP变量
(*FunP)(20); //这是通过函数指针变量来调用MyFun函数的。
return 0;
}
真的是可以这样的噢!
(哇!真是要晕倒了!)
还有呐!看——
int main(int argc, char* argv[])
{
(*MyFun)(10); //看,函数名MyFun也可以有这样的调用格式
return 0;
}
你也许第一次见到吧:函数名调用也可以是这样写的啊!(只不过我们平常没有这样书写罢了。)
那么,这些又说明了什么呢?
呵呵!依据以往的知识和经验来推理本篇的“新发现”,我想就连“福尔摩斯”也必定会由此分析并推断出以下的结论:
1. 其实,MyFun的函数名与FunP函数指针都是一样的,即都是函数指针。MyFun函数名是一个函数指针常量,而FunP是一个函数数指针变量,这是它们的关系。
2. 但函数名调用如果都得如(*MyFun)(10);这样,那书写与读起来都是不方便和不习惯的。所以C语言的设计者们才会设计成又可允许MyFun(10);这种形式地调用(这样方便多了并与数学中的函数形式一样,不是吗?)。
3. 为统一起见,FunP函数指针变量也可以FunP(10)的形式来调用。
4. 赋值时,即可FunP=&MyFun形式,也可FunP=MyFun。
上述代码的写法,随便你爱怎么着!
请这样理解吧!这可是有助于你对函数指针的应用喽!
最后——
补充说明一点:在函数的申明处:
void MyFun(int ); //不能写成void (*MyFun)(int )。
void (*FunP)(int ); //不能写成void FunP(int )。
(请看注释)这一点是要注意的。
五 定义某一函数的指针类型:
就像自定义数据类型一样,我们也可以先定义一个函数指针类型,然后再用这个类型来申明函数指针变量。
我先给你一个自定义数据类型的例子。
typedef int* PINT; //为int* 类型定义了一个PINT的别名
int main()
{
int x;
PINT px=&x; //与int * px=&x;是等价的。PINT类型其实就是int * 类型
*px=10; //px就是int*类型的变量
return 0;
}
根据注释,应该不难看懂吧!(虽然你可能很少这样定义使用,但以后学习Win32编程时会经常见到的。)
下面我们来看一下函数指针类型的定义及使用:(请与上对照!)
//自行包含头文件
void MyFun(int x); //此处的申明也可写成:void MyFun( int );
typedef void (*FunType)(int ); //这样只是定义一个函数指针类型
FunType FunP; //然后用FunType类型来申明全局FunP变量
int main(int argc, char* argv[])
{
//FunType FunP; //函数指针变量当然也是可以是局部的 ,那就请在这里申明了。
MyFun(10);
FunP=&MyFun;
(*FunP)(20);
return 0;
}
void MyFun(int x)
{
printf(“%d\n”,x);
}
看黑体部分:
首先,在void (*FunType)(int ); 前加了一个typedef 。这样只是定义一个名为FunType函数指针类型,而不是一个FunType变量。
然后,FunType FunP; 这句就如PINT px;一样地申明一个FunP变量。
其它相同。整个程序完成了相同的事。
这样做法的好处是:
有了FunType类型后,我们就可以同样地、很方便地用FunType类型来申明多个同类型的函数指针变量了。如下:
FunType FunP2;
FunType FunP3;
//……
六 函数指针作为某个函数的参数
既然函数指针变量是一个变量,当然也可以作为某个函数的参数来使用的。所以,你还应知道函数指针是如何作为某个函数的参数来传递使用的。
给你一个实例:
要求:我要设计一个CallMyFun函数,这个函数可以通过参数中的函数指针值不同来分别调用MyFun1、MyFun2、MyFun3这三个函数(注:这三个函数的定义格式应相同)。
实现:代码如下:
//自行包含头文件
void MyFun1(int x);
void MyFun2(int x);
void MyFun3(int x);
typedef void (*FunType)(int ); //②. 定义一个函数指针类型FunType,与①函数类型一至
void CallMyFun(FunType fp,int x);
int main(int argc, char* argv[])
{
CallMyFun(MyFun1,10); //⑤. 通过CallMyFun函数分别调用三个不同的函数
CallMyFun(MyFun2,20);
CallMyFun(MyFun3,30);
}
void CallMyFun(FunType fp,int x) //③. 参数fp的类型是FunType。
{
fp(x);//④. 通过fp的指针执行传递进来的函数,注意fp所指的函数是有一个参数的
}
void MyFun1(int x) // ①. 这是个有一个参数的函数,以下两个函数也相同
{
printf(“函数MyFun1中输出:%d\n”,x);
}
void MyFun2(int x)
{
printf(“函数MyFun2中输出:%d\n”,x);
}
void MyFun3(int x)
{
printf(“函数MyFun3中输出:%d\n”,x);
}
输出结果:略
分析:(看我写的注释。你可按我注释的①②③④⑤顺序自行分析。)
在 Unix 上写过程序的人一般都遇到过 Makefile,尤其是用 C 来开发程序的人。用 make 来开发和编译程序的确很方便,可是要写出一个MakeFile就不那么简单了。偏偏介绍 Makefile 的文件不多,GNU Make 那份印出来要几百页的文件,光看完 Overview 自己就快要先Over了,难怪许多人闻 Unix色变。本文将介绍如何利用 GNU Autoconf 及 Automake 这两套软件来帮助『自动』产生 Makefile 文件,并且让开发出来的的软件可以象 Apache, MySQL 和常见的 GNU 软件一样,只要会 ``./configure'', ``make'', ``make install'' 就可以把程序安装到系统中。如果您有心开发 Open Source 的软件,或只是想在 Unix 系统下写写程序。希望这份介绍文件能帮助您轻松的进入 Unix Programming 的殿堂。
1. 简介
Makefile 基本上就是『目标』(target), 『关联』(dependencies) 和『动作』三者所组成的一系列规则。而 make 就会根据 Makefile 的规则来决定如何编译 (compile) 和连接 (link) 程式。实际上,make 可做的不只是编译和连接程序,例如 FreeBSD 的 port collection 中,Makefile还可以做到自动下载远程程序,解压缩 (extract) , 打补丁 (patch),设定,然后编译,安装到系统中。
Makefile 基本结构虽然很简单,但是妥善运用这些规则就可以变换出许多不同的花样。却也因为这样,许多刚刚开始学习写Makefile 时会觉得没有规范可以遵循,每个人写出来的Makefile都不大一样,不知道从哪里下手,而且常常会受到自己的开发环境的限制,只要环境参数不同或者路径更改,可能 Makefile 就得跟着修改修改。虽然有 GNU Makefile Conventions (GNU Makefile惯例例)订出一些使用 GNU 程式设计时撰写 Makefile 的一些标准和规范,但是内容很长而且很复杂,并且经常作一些调整,为了减轻程序开发人员维护Makefile 的负担,因此出现了Automake。
程序设计者只需要写一些预先定义好的宏 (macro),提交给Automake处理后会产生一个可以供 Autoconf 使用的 Makefile.in文件。再配合利用 Autoconf产生的自动培植设置文件 configure 即可产生一份符合符合 GNU Makefile 惯例的 Makeifle 了。
2. 上路之前
在开始使用 Automake 之前,首先确认你的系统安装有如下软件:
1.
GNU Automake
2.
GNU Autoconf
3.
GNU m4
4.
perl
5.
GNU Libtool (如果你需要产生 shared library)
建议最好也使用 GNU C/C++ 编译器 、GNU Make 以及其它 GNU 的工具程序来作为开发的环境,这些工具都是属于 Open Source Software 不但免费而且功能强大。如果你是使用 Red Hat Linux 可以找到所有上述软件的 rpm 文件,FreeBSD 也有现成的 package 可以直接安装,或也可以自行下载这些软件的源代码回来安装。下面的示例是在Red Hat Linux 5.2 + CLE2 的环境下所完成的。
3. 一个简单的例子
Automake 所产生的 Makefile 除了可以做到程式的编译和连接,也已经把如何产生程序文件 (如 manual page, info 文件及 dvi 文件) 的动作,还有把源码文件包装起来以供发布都考虑进去了,所以程序源代码所存放的目录结构最好符合GNU 的标准惯例,接下来就用一个hello.c 来做为例子。
在工作目录下建立一个新的子目录"devel"',再在 devel 下建立一个"hello"' 的子目录,这个目录将作为存放 hello这个程序及其相关文件的地方:
% mkdir devel
% cd devel
% mkdir hello
% cd hello
用编辑器写一个hello.c文件,
#include
int main(int argc, char** argv) {
printf(``Hello, GNU!\n'');
return 0;
}
接下来就要用 Autoconf 及 Automake 来产生 Makefile 文件了,
1.
用 autoscan 产生一个 configure.in 的原型,执行autoscan 后会产生一个configure.scan 的文件,可以用它作为 configure.in文件的蓝本。
% autoscan
% ls
configure.scan hello.c
2.
编辑 configure.scan文件,如下所示,并且改名为configure.in
dnl Process this file with autoconf to produce a configure script. AC_INIT(hello.c) AM_INIT_AUTOMAKE(hello, 1.0)
dnl Checks for programs.
AC_PROG_CC
dnl Checks for libraries.
dnl Checks for header files.
dnl Checks for typedefs, structures, and compiler characteristics.
dnl Checks for library functions.
AC_OUTPUT(Makefile)
3.
执行 aclocal 和 autoconf ,分别会产生 aclocal.m4 及 configure 两个文件
% aclocal
% autoconf
% ls
aclocal.m4 configure configure.in hello.c
4.
编辑 Makefile.am 文件,内容如下
AUTOMAKE_OPTIONS= foreign
bin_PROGRAMS= hello
hello_SOURCES= hello.c
5.
执行 automake --add-missing ,Automake 会根据Makefile.am 文件产生一些文件,包含最重要的 Makefile.in
% automake --add-missing automake: configure.in: installing `./install-sh' automake: configure.in: installing `./mkinstalldirs' automake: configure.in: installing `./missing'
6.
最后执行 ./configure ,
% ./configure creating cache ./config.cache checking for a BSD compatible install... /usr/bin/install -c checking whether build environment is sane... yes checking whether make sets $... yes checking for working aclocal... found checking for working autoconf... found checking for working automake... found checking for working autoheader... found checking for working makeinfo... found checking for gcc... gcc checking whether the C compiler (gcc ) works... yes checking whether the C compiler (gcc ) is a cross-compiler... no checking whether we are using GNU C... yes checking whether gcc accepts -g... yes updating cache ./config.cache creating ./config.status creating Makefile
现在你的目录下已经产生了一个 Makefile 档,下个 ``make'' 指令就可以开始编译 hello.c 成执行档,执行 ./hello 和 GNU 打声招呼吧!
% make gcc -DPACKAGE=\"hello\" -DVERSION=\"1.0\" -I. -I. -g -O2 -c hello.c gcc -g -O2 -o hello hello.o % ./hello Hello! GNU!
你还可以试试 ``make clean'',''make install'',''make dist'' 看看会有什麼结果。你也可以把产生出来的 Makefile 秀给你的老板,让他从此对你刮目相看 :-)
4. 追根问底
上述产生Makefile 的过程和以往自行编写的方式非常不一样,舍弃传统自定义make 的规则,使用 Automake 只需用到一些已经定义好的宏就可以了。我们把宏及目标 (target)写在Makefile.am 文件内,Automake 读入 Makefile.am 文件后会把这一串已经定义好的宏展开并产生相对应的 Makefile.in 文件,然后再由 configure这个 shell script 根据 Makefile.in 产生合适的Makefile。
利用 autoconf 及 automake产生Makefile 的流程
上图表示在上一范例中要使用的文件档案及产生出来的文件,有星号 (*) 者代表可执行文件。在此示例中可由 Autoconf 及 Automake 工具所产生的额外文件有 configure.scan、aclocal.m4、configure、Makefile.in,需要自行加入设置的有configure.in 及 Makefile.am。
4.1 编辑 configure.in 文件
Autoconf 是用来产生 'configure'文件的工具。'configure' 是一个 shell script,它可以自动设定原始程序以符合各种不同平台上Unix 系统的特性,并且根据系统参数及环境产生合适的Makefile文件或C 的头文件(header file),让原始程式可以很方便地在不同的平台上进行编译。Autoconf会读取 configure.in 文件然后产生'configure' 这个 shell script。
configure.in 文件内容是一系列GNU m4 的宏,这些宏经autoconf处理后会变成检查系统特性的shell scripts。 configure.in 内宏的顺序并没有特别的规定,但是每一个configure.in 文件必须在所有宏前加入 AC_INIT 宏,然后在所有宏的最后加上 AC_OUTPUT宏。可先用 autoscan 扫描原始文件以产生一个 configure.scan 文件,再对 configure.scan 做些修改成 configure.in 文件。在范例中所用到的宏如下:
dnl
这个宏后面的字不会被处理,可以视为注释
AC_INIT(FILE)
该宏用来检查源代码所在路径,autoscan 会自动产生,一般无须修改它。
AM_INIT_AUTOMAKE(PACKAGE,VERSION)
这个是使用 Automake 所必备的宏,PACKAGE 是所要产生软件套件的名称,VERSION 是版本编号。
AC_PROG_CC
检查系统可用的C编译器,若源代码是用C写的就需要这个宏。
AC_OUTPUT(FILE)
设置 configure 所要产生的文件,若是Makefile ,configure 便会把它检查出来的结果带入 Makefile.in 文件后产生合适的 Makefile。
实际上,这里使用 Automake 时,还需要一些其他的宏,这些额外的宏我们用 aclocal来帮助产生。执行 aclocal会产生aclocal.m4 文件,如果无特别的用途,可以不需要修改它,用 aclocal 所产生的宏会告诉 Automake如何动作。
有了 configure.in 及 aclocal.m4两个文件以后,便可以执行 autoconf来产生 configure 文件了。
4.2 编辑Makefile.am 文件
接下来要编辑Makefile.am 文件,Automake 会根据 configure.in 中的宏把Makefile.am 转成 Makefile.in 文件。 Makefile.am 文件定义所要产生的目标:
AUTOMAKE_OPTIONS
设置 automake 的选项。Automake 主要是帮助开发 GNU 软件的人员来维护软件,所以在执行 automake 时,会检查目录下是否存在标准 GNU 软件中应具备的文件,例如 'NEWS'、'AUTHOR'、'ChangeLog' 等文件。设置 foreign 时,automake 会改用一般软件的标准来检查。
bin_PROGRAMS
定义要产生的执行文件名。如果要产生多个执行文件,每个文件名用空白符隔开。
hello_SOURCES
定义 'hello' 这个执行程序所需要的原始文件。如果 'hello'这个程序是由多个原始文件所产生,必须把它所用到的所有原始文件都列出来,以空白符隔开。假设 'hello' 还需要 'hello.c'、'main.c'、'hello.h' 三个文件的话,则定义
hello_SOURCES= hello.c main.c hello.h
如果定义多个执行文件,则对每个执行程序都要定义相对的filename_SOURCES。
编辑好 Makefile.am 文件,就可以用 automake --add-missing来产生 Makefile.in。加上 --add-missing 选项来告诉 automake顺便假如包装一个软件所必须的文件。Automake产生生出来的 Makefile.in 文件是完全符合 GNU Makefile 的惯例,只要执行 configure这个shell script 便可以产生合适的 Makefile 文件了。
4.3 使用 Makefile
利用 configure 所产生的 Makefile文件有几个预先设定的目标可供使用,这里只用几个简述如下:
make all
产生设定的目标,既次范例中的执行文件。只敲入make 也可以,此时会开始编译源代码,然后连接并产生执行文件。
make clean
清除之前所编译的执行文件及目标文件(object file, *.o)。
make distclean
除了清除执行文件和目的文件以外,也把 configure 所产生的 Makefile 清除掉。
make install
将程序安装到系统中,若源码编译成功,且执行结果正确,便可以把程序安装到系统预先设定的执行文件存放路径中,若用 bin_PROGRAMS 宏的话,程序会被安装到 /usr/local/bin下。
make dist
将程序和相关的文档包装为一个压缩文档以供发布 (distribution) 。执行完在目录下会产生一个以PACKAGE-VERSION.tar.gz 为名称的文件。PACKAGE 和 VERSION 这两个参数是根据 configure.in 文件中 AM_INIT_AUTOMAKE(PACKAGE, VERSION) 的定义。在此范例中会产生 'hello-1.0.tar.gz' 的文件。
make distcheck
和 make dist 类似,但是加入检查包装以后的压缩文件是否正常,这个目标除了把程序和相关文档包装成 tar.gz 文件外,还会自动把这个压缩文件解开,执行 configure,并执行 make all ,确认编译无错误以后,户显示这个 tar.gz 文件已经准备好可以发布了。这个检查非常有用,检查过关的套件,基本上可以给任何具备 GNU 开发环境的人去重新编译成功。就 hello-1.tar.gz 这个范例而言,除了在Red Hat Linux 上,在 FreeBSD 2.2.x 也可以正确编译。
要注意的是,利用 Autoconf 及 Automake 所产生出来的软件套件是可以在没有安装 Autoconf 及 Automake 的环境使用的,因为 configure 是一个 shell script,它己被设计为可以在一般 Unix 的 sh 这个 shell 下执行。但是如果要修改 configure.in 及 Makefile.am 文件再产生新的 configure 及 Makefile.in 文件时就一定要有 Autoconf 及 Automake 了。
5. 相关资料
Autoconf 和 Automake 功能十分强大,可以从它们附带的 info 稳当4中找到详细的使用方法说明。你也可以从许多现有的GNU 软件或 Open Source 软件中找到相关的 configure.in 或 Makefile.am 文件,他们是学习 Autoconf 及 Automake 更多技巧的最佳范例。
这个简介只用到了 Autoconf 及 Automake 的皮毛罢了,如果你有心加入 Open Source 软件开发的行列,希望这篇文章可以帮助你对产生 Makefile 有个简单的了解。其它有关开发 GNU 程式或 C 程序设计及 Makefile 的详细运用及技巧,建议从 GNU Coding Standards (GNU 编码规定) 读起,里面包含了 GNU Makefile 惯例,及开发 GNU 软件的标准程序和惯例。这些 GNU 软件的在线说明文件可以在 http://www.gnu.org/ 上找到。
6. 结束语
利用 Autoconf 及 Automake,产生一个 Makefile 似乎不再象以前那么困难了,而使用 Autoconf 也使得我们在不同平台上或各家 Unix 之间发布及便宜程序变的简单,这对于在Unix 系统上程序开发员来说减轻了许多负担。妥善运用这些 GNU 的工具软件,可以帮助我们更容易的去开发程序,而且更容易维护源代码。
1. 简介
Makefile 基本上就是『目标』(target), 『关联』(dependencies) 和『动作』三者所组成的一系列规则。而 make 就会根据 Makefile 的规则来决定如何编译 (compile) 和连接 (link) 程式。实际上,make 可做的不只是编译和连接程序,例如 FreeBSD 的 port collection 中,Makefile还可以做到自动下载远程程序,解压缩 (extract) , 打补丁 (patch),设定,然后编译,安装到系统中。
Makefile 基本结构虽然很简单,但是妥善运用这些规则就可以变换出许多不同的花样。却也因为这样,许多刚刚开始学习写Makefile 时会觉得没有规范可以遵循,每个人写出来的Makefile都不大一样,不知道从哪里下手,而且常常会受到自己的开发环境的限制,只要环境参数不同或者路径更改,可能 Makefile 就得跟着修改修改。虽然有 GNU Makefile Conventions (GNU Makefile惯例例)订出一些使用 GNU 程式设计时撰写 Makefile 的一些标准和规范,但是内容很长而且很复杂,并且经常作一些调整,为了减轻程序开发人员维护Makefile 的负担,因此出现了Automake。
程序设计者只需要写一些预先定义好的宏 (macro),提交给Automake处理后会产生一个可以供 Autoconf 使用的 Makefile.in文件。再配合利用 Autoconf产生的自动培植设置文件 configure 即可产生一份符合符合 GNU Makefile 惯例的 Makeifle 了。
2. 上路之前
在开始使用 Automake 之前,首先确认你的系统安装有如下软件:
1.
GNU Automake
2.
GNU Autoconf
3.
GNU m4
4.
perl
5.
GNU Libtool (如果你需要产生 shared library)
建议最好也使用 GNU C/C++ 编译器 、GNU Make 以及其它 GNU 的工具程序来作为开发的环境,这些工具都是属于 Open Source Software 不但免费而且功能强大。如果你是使用 Red Hat Linux 可以找到所有上述软件的 rpm 文件,FreeBSD 也有现成的 package 可以直接安装,或也可以自行下载这些软件的源代码回来安装。下面的示例是在Red Hat Linux 5.2 + CLE2 的环境下所完成的。
3. 一个简单的例子
Automake 所产生的 Makefile 除了可以做到程式的编译和连接,也已经把如何产生程序文件 (如 manual page, info 文件及 dvi 文件) 的动作,还有把源码文件包装起来以供发布都考虑进去了,所以程序源代码所存放的目录结构最好符合GNU 的标准惯例,接下来就用一个hello.c 来做为例子。
在工作目录下建立一个新的子目录"devel"',再在 devel 下建立一个"hello"' 的子目录,这个目录将作为存放 hello这个程序及其相关文件的地方:
% mkdir devel
% cd devel
% mkdir hello
% cd hello
用编辑器写一个hello.c文件,
#include
int main(int argc, char** argv) {
printf(``Hello, GNU!\n'');
return 0;
}
接下来就要用 Autoconf 及 Automake 来产生 Makefile 文件了,
1.
用 autoscan 产生一个 configure.in 的原型,执行autoscan 后会产生一个configure.scan 的文件,可以用它作为 configure.in文件的蓝本。
% autoscan
% ls
configure.scan hello.c
2.
编辑 configure.scan文件,如下所示,并且改名为configure.in
dnl Process this file with autoconf to produce a configure script. AC_INIT(hello.c) AM_INIT_AUTOMAKE(hello, 1.0)
dnl Checks for programs.
AC_PROG_CC
dnl Checks for libraries.
dnl Checks for header files.
dnl Checks for typedefs, structures, and compiler characteristics.
dnl Checks for library functions.
AC_OUTPUT(Makefile)
3.
执行 aclocal 和 autoconf ,分别会产生 aclocal.m4 及 configure 两个文件
% aclocal
% autoconf
% ls
aclocal.m4 configure configure.in hello.c
4.
编辑 Makefile.am 文件,内容如下
AUTOMAKE_OPTIONS= foreign
bin_PROGRAMS= hello
hello_SOURCES= hello.c
5.
执行 automake --add-missing ,Automake 会根据Makefile.am 文件产生一些文件,包含最重要的 Makefile.in
% automake --add-missing automake: configure.in: installing `./install-sh' automake: configure.in: installing `./mkinstalldirs' automake: configure.in: installing `./missing'
6.
最后执行 ./configure ,
% ./configure creating cache ./config.cache checking for a BSD compatible install... /usr/bin/install -c checking whether build environment is sane... yes checking whether make sets $... yes checking for working aclocal... found checking for working autoconf... found checking for working automake... found checking for working autoheader... found checking for working makeinfo... found checking for gcc... gcc checking whether the C compiler (gcc ) works... yes checking whether the C compiler (gcc ) is a cross-compiler... no checking whether we are using GNU C... yes checking whether gcc accepts -g... yes updating cache ./config.cache creating ./config.status creating Makefile
现在你的目录下已经产生了一个 Makefile 档,下个 ``make'' 指令就可以开始编译 hello.c 成执行档,执行 ./hello 和 GNU 打声招呼吧!
% make gcc -DPACKAGE=\"hello\" -DVERSION=\"1.0\" -I. -I. -g -O2 -c hello.c gcc -g -O2 -o hello hello.o % ./hello Hello! GNU!
你还可以试试 ``make clean'',''make install'',''make dist'' 看看会有什麼结果。你也可以把产生出来的 Makefile 秀给你的老板,让他从此对你刮目相看 :-)
4. 追根问底
上述产生Makefile 的过程和以往自行编写的方式非常不一样,舍弃传统自定义make 的规则,使用 Automake 只需用到一些已经定义好的宏就可以了。我们把宏及目标 (target)写在Makefile.am 文件内,Automake 读入 Makefile.am 文件后会把这一串已经定义好的宏展开并产生相对应的 Makefile.in 文件,然后再由 configure这个 shell script 根据 Makefile.in 产生合适的Makefile。
利用 autoconf 及 automake产生Makefile 的流程
上图表示在上一范例中要使用的文件档案及产生出来的文件,有星号 (*) 者代表可执行文件。在此示例中可由 Autoconf 及 Automake 工具所产生的额外文件有 configure.scan、aclocal.m4、configure、Makefile.in,需要自行加入设置的有configure.in 及 Makefile.am。
4.1 编辑 configure.in 文件
Autoconf 是用来产生 'configure'文件的工具。'configure' 是一个 shell script,它可以自动设定原始程序以符合各种不同平台上Unix 系统的特性,并且根据系统参数及环境产生合适的Makefile文件或C 的头文件(header file),让原始程式可以很方便地在不同的平台上进行编译。Autoconf会读取 configure.in 文件然后产生'configure' 这个 shell script。
configure.in 文件内容是一系列GNU m4 的宏,这些宏经autoconf处理后会变成检查系统特性的shell scripts。 configure.in 内宏的顺序并没有特别的规定,但是每一个configure.in 文件必须在所有宏前加入 AC_INIT 宏,然后在所有宏的最后加上 AC_OUTPUT宏。可先用 autoscan 扫描原始文件以产生一个 configure.scan 文件,再对 configure.scan 做些修改成 configure.in 文件。在范例中所用到的宏如下:
dnl
这个宏后面的字不会被处理,可以视为注释
AC_INIT(FILE)
该宏用来检查源代码所在路径,autoscan 会自动产生,一般无须修改它。
AM_INIT_AUTOMAKE(PACKAGE,VERSION)
这个是使用 Automake 所必备的宏,PACKAGE 是所要产生软件套件的名称,VERSION 是版本编号。
AC_PROG_CC
检查系统可用的C编译器,若源代码是用C写的就需要这个宏。
AC_OUTPUT(FILE)
设置 configure 所要产生的文件,若是Makefile ,configure 便会把它检查出来的结果带入 Makefile.in 文件后产生合适的 Makefile。
实际上,这里使用 Automake 时,还需要一些其他的宏,这些额外的宏我们用 aclocal来帮助产生。执行 aclocal会产生aclocal.m4 文件,如果无特别的用途,可以不需要修改它,用 aclocal 所产生的宏会告诉 Automake如何动作。
有了 configure.in 及 aclocal.m4两个文件以后,便可以执行 autoconf来产生 configure 文件了。
4.2 编辑Makefile.am 文件
接下来要编辑Makefile.am 文件,Automake 会根据 configure.in 中的宏把Makefile.am 转成 Makefile.in 文件。 Makefile.am 文件定义所要产生的目标:
AUTOMAKE_OPTIONS
设置 automake 的选项。Automake 主要是帮助开发 GNU 软件的人员来维护软件,所以在执行 automake 时,会检查目录下是否存在标准 GNU 软件中应具备的文件,例如 'NEWS'、'AUTHOR'、'ChangeLog' 等文件。设置 foreign 时,automake 会改用一般软件的标准来检查。
bin_PROGRAMS
定义要产生的执行文件名。如果要产生多个执行文件,每个文件名用空白符隔开。
hello_SOURCES
定义 'hello' 这个执行程序所需要的原始文件。如果 'hello'这个程序是由多个原始文件所产生,必须把它所用到的所有原始文件都列出来,以空白符隔开。假设 'hello' 还需要 'hello.c'、'main.c'、'hello.h' 三个文件的话,则定义
hello_SOURCES= hello.c main.c hello.h
如果定义多个执行文件,则对每个执行程序都要定义相对的filename_SOURCES。
编辑好 Makefile.am 文件,就可以用 automake --add-missing来产生 Makefile.in。加上 --add-missing 选项来告诉 automake顺便假如包装一个软件所必须的文件。Automake产生生出来的 Makefile.in 文件是完全符合 GNU Makefile 的惯例,只要执行 configure这个shell script 便可以产生合适的 Makefile 文件了。
4.3 使用 Makefile
利用 configure 所产生的 Makefile文件有几个预先设定的目标可供使用,这里只用几个简述如下:
make all
产生设定的目标,既次范例中的执行文件。只敲入make 也可以,此时会开始编译源代码,然后连接并产生执行文件。
make clean
清除之前所编译的执行文件及目标文件(object file, *.o)。
make distclean
除了清除执行文件和目的文件以外,也把 configure 所产生的 Makefile 清除掉。
make install
将程序安装到系统中,若源码编译成功,且执行结果正确,便可以把程序安装到系统预先设定的执行文件存放路径中,若用 bin_PROGRAMS 宏的话,程序会被安装到 /usr/local/bin下。
make dist
将程序和相关的文档包装为一个压缩文档以供发布 (distribution) 。执行完在目录下会产生一个以PACKAGE-VERSION.tar.gz 为名称的文件。PACKAGE 和 VERSION 这两个参数是根据 configure.in 文件中 AM_INIT_AUTOMAKE(PACKAGE, VERSION) 的定义。在此范例中会产生 'hello-1.0.tar.gz' 的文件。
make distcheck
和 make dist 类似,但是加入检查包装以后的压缩文件是否正常,这个目标除了把程序和相关文档包装成 tar.gz 文件外,还会自动把这个压缩文件解开,执行 configure,并执行 make all ,确认编译无错误以后,户显示这个 tar.gz 文件已经准备好可以发布了。这个检查非常有用,检查过关的套件,基本上可以给任何具备 GNU 开发环境的人去重新编译成功。就 hello-1.tar.gz 这个范例而言,除了在Red Hat Linux 上,在 FreeBSD 2.2.x 也可以正确编译。
要注意的是,利用 Autoconf 及 Automake 所产生出来的软件套件是可以在没有安装 Autoconf 及 Automake 的环境使用的,因为 configure 是一个 shell script,它己被设计为可以在一般 Unix 的 sh 这个 shell 下执行。但是如果要修改 configure.in 及 Makefile.am 文件再产生新的 configure 及 Makefile.in 文件时就一定要有 Autoconf 及 Automake 了。
5. 相关资料
Autoconf 和 Automake 功能十分强大,可以从它们附带的 info 稳当4中找到详细的使用方法说明。你也可以从许多现有的GNU 软件或 Open Source 软件中找到相关的 configure.in 或 Makefile.am 文件,他们是学习 Autoconf 及 Automake 更多技巧的最佳范例。
这个简介只用到了 Autoconf 及 Automake 的皮毛罢了,如果你有心加入 Open Source 软件开发的行列,希望这篇文章可以帮助你对产生 Makefile 有个简单的了解。其它有关开发 GNU 程式或 C 程序设计及 Makefile 的详细运用及技巧,建议从 GNU Coding Standards (GNU 编码规定) 读起,里面包含了 GNU Makefile 惯例,及开发 GNU 软件的标准程序和惯例。这些 GNU 软件的在线说明文件可以在 http://www.gnu.org/ 上找到。
6. 结束语
利用 Autoconf 及 Automake,产生一个 Makefile 似乎不再象以前那么困难了,而使用 Autoconf 也使得我们在不同平台上或各家 Unix 之间发布及便宜程序变的简单,这对于在Unix 系统上程序开发员来说减轻了许多负担。妥善运用这些 GNU 的工具软件,可以帮助我们更容易的去开发程序,而且更容易维护源代码。






