存档:软件工程/编程技巧/设计模式

算法世界的哲学思考

九月 16, 2009 | 数学, 数据结构算法, 软件工程/编程技巧/设计模式 | RSS 2.0

每每读到一些好的算法都会让人有叹为观止的感觉,巧妙的设计让我们回味起数学,物理等,物理的测万有引力,远古时代没有任何工具的情况下的测量出地球的半径,人就是这么一个奇妙的动物。想体味下巧妙,你可以看下《啊哈,灵机一动》google黑板报的《数学之美》系列,微软的《编程之美》等,大道至简,简单的道理抽象到了一定的高度就适应于任何事物,在我看来数学和哲学都是抽象到一定程度的事物,你可以体味下1+1=2,事物的都是有联系的普遍性。俗话说大道至简。
读计算机以来,自然也学到不少算法,虽然现在搞些web的开发,当初也是软件开发的,总是忘不了学过的那些经典的算法,经常及其老师讲解时候的费力和樯橹灰飞烟灭。自己体味好多计算机里面的算法,定理,思想,更大自然世界很协调。
先举几个简单的小例子:
1 将两只小猫放到足球场的对面,相距100码,他们以每分钟10码的速度相向行走。同时这两只小猫的母亲在足球场的一端,它可以以每分钟100码的速度跑步,猫妈妈从一只小猫跑到另一只小猫,来回轮流跑而速度不减,一直跑到两只小猫在中场相遇,问猫妈妈跑了多远?
2 渔民在池塘放养了大量鱼苗。养到一定程度后,我们要估计现在池塘中有多少鱼?
3 有一根27厘米的细长杆,在第3厘米,7厘米,11厘米,17厘米,23厘米在五个位置各放有一只蚂蚁,木杆很细,不能同时通过两次蚂蚁,开始时,蚂蚁头朝右还是朝左是任意的,他们只会超前或者调头,当不会后退。当任意两只蚂蚁碰头时,两只蚂蚁会同时掉头朝反方向走,假设蚂蚁每秒钟可以走一厘米的距离,所有蚂蚁离开木杆的最短时间和最长时间。
4 棋盘覆盖问题。在一个2k x 2k ( 即:2^k x 2^k )个方格组成的棋盘中,恰有一个方格与其他方格不同,称该方格为一特殊方格,且称该棋盘为一特殊棋盘。在棋盘覆盖问题中,要用图示的4种不同形态的L型骨牌覆盖给定的特殊棋盘上除特殊方格以外的所有方格,且任何2个L型骨牌不得重叠覆盖。

1005341

5 某一天,一个新研究员向托马斯爱迪生报到。爱迪生要求他计算出一个空灯泡壳的体积。在使用测径仪和微积分进行数小时饿计算后,这个新员工给出了答案150立方厘米,而爱迪生在几秒之内计算就给出了更接近155,他是如何实现的呢?

—————————————-给出部分提示————————–

1 不要用蛮力,首先通过两只小猫可以计算出猫妈妈的跑步的时间,两只小猫相距100米,每分钟20码在接近,故5分钟完成,所以猫妈妈是500米

2 这个牵涉到数学中的概率等的知识,概率统计还是挺实用的一门学科,可以考虑用极大似然估算算法,即捞出一定数量的鱼,不能过少,坐上标记,过一定时间取出看看标记的比例

3 和第一个的思想有点像,碰头后蚂蚁会继续潜行,看做是个体运动就好考虑了

4 第四题图片方格少,还好,如果多了,你就不一定能搞定了,还需保证是4的k次幂,其实可以采用分治的思想,即图上可以上下左右四部分,带黑色宽那个就可以填好了,挨着黑色块的即中心的可以填好了,然后就把其他的搞定,如果更多如何?就行切分,分治,分而治理。

5 想想阿基米德王冠的故事你就明白了

现在可以看到了

一题三给我们一个解决问题的思路用时间求解较容易,摊还分析就是用这样一个思路,一如一个附加变量,位势,

二题,五题也是给我们一种思路转换问题的思路。4题给我们一个解决问题分而治之。

可以看到什么样的哲学思想呢?首先事物是联系的,解决了这个事物,和其关系的别的也就可以相应的解决了,整体和局部的思想,局部解决了,整体也就好了。

看看别的算法

1 递归于分治策略,递归?现实生活中哪里有递归的影子?没想到?那就照照镜子吧,看到了,这个就是递归,事物本身的属性,局部整体的哲学思想

2 动态规划,类似分治递归,当他们的若干个问题不是独立的是联系的,第n个问题和n-1个问题是有关系的,

3 贪心算法很明晰矛盾分主要矛盾和次要矛盾,抓住了主要矛盾就抓住了问题的核心

4 概率算法:事物发展的不确定性,螺旋式发展。舍伍德算法,拉斯维加斯算法,我们经典的抛针算π的算法,蒙特卡洛算法,现在基本都忘记了。

5 线性规划,回溯法,分枝限界法 这些都是通过把问题抽象,合理的布局,解决之,算是事物之间的联系吧。别的暂时没行到。

6np完全性理论和近似算法:图灵机等,应该是事物的发展变化的各个状态,著名的图灵机问题,问题的各个态势的变化,这个算法我不熟。

暂时先想到这里,也算是对自己学过的算法课程的一个回忆。

没有评论 »

For , While 那个更快?

九月 5, 2009 | c/c++, 数据结构算法, 软件工程/编程技巧/设计模式 | RSS 2.0

到底是for快还是while快?有人做了一下测试:

for (;;) {
}

while (1) {
}

生成的汇编分别是:

for的是:

.text
.globl _main
_main:
pushl    %ebp
movl    %esp, %ebp
subl    $8, %esp
L2:
jmp    L2
.subsections_via_symbols

while的是:

.text
.globl _main
_main:
pushl    %ebp
movl    %esp, %ebp
subl    $8, %esp
L2:
jmp    L2
.subsections_via_symbols

现在你知道了吧

—————————————————http://blog.marcelotoledo.org/2008/10/22/for-or-while/


				

没有评论 »

高效产生不重复的随机数

三月 24, 2009 | c/c++, 数学, 数据结构算法, 软件工程/编程技巧/设计模式 | RSS 2.0

编程珠玑里面的

for(i = 0; i < n; i++)
{
x[i] = i;
}
for(i = 0; i < k; i++)
{
t = rand(i,n-1);
swap(x[i], x[t]);
out(x[i]);
}

没有评论 »

高内聚,松耦合

二月 16, 2009 | 网站架构, 软件工程/编程技巧/设计模式 | RSS 2.0

耦合关系直接决定着软件面对变化时候
    1 模块与模块之间的紧耦合使得软件面对变化时,相关的模块都要随之
更改。    
    2模块与模块之间的松耦合使得软件变化时,一些模块更容易替换或者更改
但其它模块保持不变

大自然是个天生松耦合,高内聚的
高内聚,松耦合 - 玉树临风 - 玉树临风真情无限
顺便学习下高内聚,松耦合
内聚:一个模块内各个元素彼此结合的紧密程度

耦合:一个软件结构内不同模块之间互连程度的度量

一个完整的系统,模块与模块之间,尽可能的使其独立存在。

也就是说,让每个模块,尽可能的独立完成某个特定的子功能。

模块与模块之间的接口,尽量的少而简单。

如果某两个模块间的关系比较复杂的话,最好首先考虑进一步的模块划分。

这样有利于修改和组合。

在一个模块内,让每个元素之间都尽可能的紧密相连。

也就是充分利用每一个元素的功能,各施所能,以最终实现某个功能。

如果某个元素与该模块的关系比较疏松的话,可能该模块的结构还不够完善,或者是该元素是多余的。

内聚和耦合,包含了横向和纵向的关系。功能内聚和数据耦合,是我们需要达成的目标。横向的内聚和耦合,通常体现在系统的各个模块、类之间的关系,而纵向的耦合,体现在系统的各个层次之间的关系。

对于我在编码中的困惑,我是这样想的,用面向对象的思想去考虑一个类的封装。
一个方法,如何封装,拿到现实生活中来看,看这种能力(方法)是否是属于这类事物(类)的本能。
如果是,就封装在这个类里。
如果不是,则考虑封装在其它类里。
如果这种能力,很多事物都具有,则一定要封装在这类事物的总类里。
如果这种能力,很多事物都会经常用到,则可以封装成一个总类的静态方法

一个优秀软件开发人员的必修课:高内聚

高内聚 Java 软件工程 软件模式    

一个重要的模式:高内聚。

2.  高内聚(High Cohesion

高内聚是另一个普遍用来评判软件设计质量的标准。内聚,更为专业的说法叫功能内聚,是对软件系统中元素职责相关性和集中度的度量。如果元素具有高度相关的职责,除了这些职责内的任务,没有其它过多的工作,那么该元素就具有高内聚性,反之则为低内聚性。高内聚要求软件系统中的各个元素具有较高的协作性,因为在我们在完成软件需求中的一个功能,可能需要做各种事情,但是具有高内聚性的一个元素,只完成它职责内的事情,而把那些不在它职责内的事情拿去请求别人来完成。这就好像,如果我是一个项目经理,我的职责是监控和协调我的项目各个阶段的工作。当我的项目进入需求分析阶段,我会请求需求分析员来完成;当我的项目进入开发阶段,我会请求软件开发人员来完成;当我的项目需要测试的时候,我会请求测试人员。。。。。。如果我参与了开发,我就不是一个高内聚的元素,因为开发不是我的职责。我们的项目为什么要高内聚呢?我觉得可以从可读性、复用性、可维护性和易变更性四个方面来理解。

1.可读性

一个人写文章、讲事情,条理清晰才能易于理解,这同样发生在读写软件代码上。如果一堆代码写得一团乱麻,东一个跳转西一个调用,读它的人会感觉非常头疼。这种事情也许一直在写程序的你我都曾经有过经历。如果一段程序条理非常清晰,每个类通过名称或说明都能清楚明白它的意义,类的每个属性、函数也都是易于理解的它所应当完成的任务和行为,这段程序的可读性必然提高。在软件产业越来越密集,软件产业中开发人员协作越来越紧密、分工越来越细的今天,软件可读性的要求相信也越来越为人们所重视。

2.复用性

在软件开发中,最低等级的复用是代码拷贝,然后是函数的复用、对象的复用、组件的复用。软件开发中最懒的人是最聪明的人,他们总是想到复用。在代码编写的时候突然发现某个功能是曾经实现过的功能,直接把它拷贝过来就ok了。如果这段代码在同一个对象中,那么就提出来写一个函数到处调用就行了。如果不是在同一个对象中呢,就将其抽象成一个对象到处调用吧。如果不在一个项目中呢,那就做成组件给各个项目引用吧。代码复用也使我们的代码在复用的过程中不断精化、不断健壮、提高代码质量。代码的复用的确给我们的开发带来了不少便利,但是一段代码能否在各个需要的地方都能复用呢?这给我们的软件开发质量提出了新的要求:好的代码可以复用,不好的则不行。软件中的一个对象如果能保证能完成自己职能范围内的各项任务,同时又不去理会与自己职能无关的其它任务,那么它就能够保证功能的相对独立性,也就可以脱离自己所处的环境而复用到其它环境中,这是一个具有内聚性的对象。

3.可维护性和易变更性

在前面《如何在struts+spring+hibernate的框架下构建低耦合高内聚的软件》中我提到,我们现在的软件是在不断变更的,这种变更不仅来自于我们的客户,更来自于我们的市场。如果我们的软件通过变更能及时适应我们的市场需求,我们就可以在市场竞争中获胜。如何能及时变更以适应我们的市场呢,就是通过调整软件的结构,使我们每次的变更付出的代价最小,耗费的人力最小,这种变更才最快最经济。高内聚的软件,每个系统、模块、类的任务都高度相关,就使每一次的变更涉及的范围缩小到最小。比如评审表发生了变更,只会与评审表对象有关,我们不会去更改其它的对象。如果我们能做到这一点,我们的系统当然是可维护性好、易变更性好的系统。

那么,我们如何做到高内聚呢?就拿前面我提到的评审项目举例。我现在要为“评审表”对象编写一段填写并保存评审表的代码。评审表对象的职责是更新和查询评审表的数据,但是在显示一个要填写的评审表的时候,我需要显示该评审计划的名称、该评审计划有哪些评审对象需要评审。现在我如何编写显示一个要填写的评审表的代码?我在评审表对象的这个相应的函数中编写一段查询评审计划和评审对象的代码吗?假如你这样做了,你的代码就不是高内聚的,因为查询评审计划和评审对象的数据不是它的职责。正确的方法应当去请求“评审计划”对象和“评审对象”对象来完成这些工作,而“评审表”对象只是获取其结果。

另外,如果一个对象要完成一个虽然在自己职责范围内,但过程非常复杂的任务时,也应当将该任务分解成数个功能相对独立的子函数来完成。我曾经看见一个朋友写的数百行的一个函数,让人读起来非常费劲。同时这样的函数中一些相对独立的代码,本可以复用到其它代码中,也变成了不可能。所以我给大家的建议是,不要写太长的函数,超过一百行就可以考虑将一些功能分解出去。

与“低耦合”一样,高内聚也不是一个绝对,而是一个相对的指标,应当适当而不能过度。正如我们在现实生活中,如果在一个十来人的小公司,每个人的分工可能会粗一些,所分配的职责会广一些杂一些,因为其总体的任务少;而如果在一个一两百人的大公司,每个人的分工会细一些,所分配的任务会更加专一些,因为总体任务多,更需要专业化的分工来提高效率。软件开发也是一样,如果“评审计划”对象完成的业务功能少,并且不复杂,它完全可以代理它的子表“评审对象”和“评审者”的管理。但是“评审计划”对象需要完成的“对评审计划表的管理”这个基本职责包含的业务功能繁多或者复杂,它就应当将“对评审对象表的管理”交给“评审对象”对象,将“对评审者表的管理”交给“评审者”对象。同样,高内聚的可维护性好、易变更性好只能是一个相对的指标。如果一个变更的确是大范围的变更,你永远不可能通过内聚就不进行大范围的变更了。同时内聚也是要付出代价的,所以你也不必要去为了一个不太可能的变更去进行过度设计,应当掌握一个度。过度的内聚必将增加系统中元素之间的依赖,提高耦合度。所以“高内聚”与“低耦合”是矛盾的,必须权衡利弊,综合地去处理。在李洋等人翻译的《UML和模式应用》中,将内聚和耦合翻译为软件工程中的阴与阳,是中国人对内聚和耦合的最佳解释。

综上所述,“高内聚”给软件项目带来的优点是:可读性强、易维护和变更、支持低耦合、移植和重用性强。

 

一个优秀软件开发人员的必修课:GRASP2)低耦合

关键字: 设计模式       

我偶然在googleyahoo这样的搜索引擎搜索GRASP发现,除了国外的网站,国内网站多介绍和讨论GoF而很少介绍GRASP,即使这少量的文章也讲解非常粗略。个人认为作为优秀的开发人员,理解GRASPGoF更重要,故写此文章。前面我在《 (原创)一个优秀软件开发人员的必修课:GRASP软件开发模式浅析》中介绍了使用GRASP的目的,今天允许我调换一下顺序,先从低耦合讲起,因为诸如创建者模式、信息专家模式的根本目的就是降低耦合。

1.    低耦合(Low Coupling

“低耦合”这个词相信大家已经耳熟能详,我们在看spring的书籍、MVC的数据、设计模式的书籍,无处不提到“低耦合、高内聚”,它已经成为软件设计质量的标准之一。那么什么是低耦合?耦合就是对某元素与其它元素之间的连接、感知和依赖的量度。这里所说的元素,即可以是功能、对象(类),也可以指系统、子系统、模块。假如一个元素A去连接元素B,或者通过自己的方法可以感知B,或者当B不存在的时候就不能正常工作,那么就说元素A与元素B耦合。耦合带来的问题是,当元素B发生变更或不存在时,都将影响元素A的正常工作,影响系统的可维护性和易变更性。同时元素A只能工作于元素B存在的环境中,这也降低了元素A的可复用性。正因为耦合的种种弊端,我们在软件设计的时候努力追求“低耦合”。低耦合就是要求在我们的软件系统中,某元素不要过度依赖于其它元素。请注意这里的“过度”二字。系统中低耦合不能过度,比如说我们设计一个类可以不与JDK耦合,这可能吗?除非你不是设计的Java程序。再比如我设计了一个类,它不与我的系统中的任何类发生耦合。如果有这样一个类,那么它必然是低内聚(关于内聚的问题我随后讨论)。耦合与内聚常常是一个矛盾的两个方面。最佳的方案就是寻找一个合适的中间点。

哪些是耦合呢?

1.元素B是元素A的属性,或者元素A引用了元素B的实例(这包括元素A调用的某个方法,其参数中包含元素B)。

2.元素A调用了元素B的方法。

3.元素A 没有评论 »

设计模式的一点笔记

二月 11, 2009 | 软件工程/编程技巧/设计模式 | RSS 2.0

概念层:对象是某种拥有责任的抽象
规格层面:对象是一系列可以对其它对象使用的公共接口
语言层面:对象封装了代码和数据,状态和行为
怎样设计好的
遵循一定的面向对象原则
熟悉一些典型的马面想对象的设计模式
原则:
从设计原则到设计模式:
1 针对接口编程,而不是针对实现编程
2优先使用对象组合,而不是类继承,破坏封装性。子类父类耦合度搞。
只有符合isa的关系才使用继承
类继承:白箱复用,对象组合:黑箱复用,耦合度低
3 封装变化点
使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一侧长城不良的
影响,从而实现层次见的松耦合
设计原则
1 单一职责原则(srp)
一个类应该仅有一个引起它变化的原因
2 开放封闭原则(ocp)
类模块应该是可扩展的,但是不可修改的(对外扩展开发,对更改封闭)
3 liskov替换原则(lsp)
子类必须能够替换他们的基类 is-a 的关系
4 依赖倒置原则(dip)
高层模块不应该底层模块,二者都应该依赖于抽象
抽象不应该依赖于实现细节,实现细节应该依赖与抽象
5 接口隔离原则(isp)
不应该强迫客户程序依赖他们不用的方法

那里有变化,就封装那里,在变化和不变化之间形成一个隔离。这样就封装了变化,隔离了变化

没有评论 »

Design pattern (computer science) and linux study site

一月 7, 2009 | 软件工程/编程技巧/设计模式 | RSS 2.0

发现wikipedia的设计模式讲的还不错,而且还带各种代码,学习学习
http://en.wikipedia.org/wiki/Design_pattern_(computer_science)
20  http://ubuntu:ubuntuftp@ftp.ubuntu.org.cn/   
21.         http://mirrors.huihoo.org/       
23.         http://bingle.pku.edu.cn          、
24.         http://so.hustonline.net/
25.         ftp://ftp:ftp@www.chinalinux.org.cn/
26     http://www.fdigg.net/
 

没有评论 »

PHP Iterator Standard PHP Library (SPL) Functions

十二月 31, 2008 | php, 软件工程/编程技巧/设计模式 | RSS 2.0

PHP 迭代器 (转)

感谢,bitbird对我的指导 


PHP 迭代器

作者: Dejan Bosanac
译者: detrox

PHP arrays are generally a very powerful objectcontainer. But still, we can easily add a little more fuel to them.Imagine an iterator object as a kind of wrapper around our arrays. Whatwe will try to accomplish here is to create unique interface fortraversing arrays and to add a little more control over how our objectsare created and finally, to support lazy loading.

PHP数组一般地说是一个十分强大的对象容器。但我们仍然可以给他来点涡轮增压。把迭代器对象设想为一种数组的包装。我们要在这里试着完成的是为遍历数组和在如何创建对象上加入一点额外的控制去创建一个唯一的接口,最后去支持傻瓜式的读取。

Interface
接口

Iterator has a very simple and many times seen interface.

迭代器有一个简单的和经常被看到的接口

<?php
function Iterator($array) // Constructor. Takes array to be traversed as a parameter. 构造函数。使需要遍历的数组作为一个参数
function reset() // Sets internal pointer to the first element 设置内部指针指向第一个元素
function end() // Sets internal pointer to the last element 设置内部指针指向最后一个元素
function seek($position) // Sets internal pointer to desired position 设置内部指针指向一个指定的元素
function next() // Returns next element in the iteration 返回后一个元素
function previous() // Returns previous element in the iteration 返回前一个元素
?>

With the interface like this you can easily perfoRM allyour daily tasks (such as traversing arrays) in any way you want andfrom any position you want. One advantage of using this approachagainst native PHP array functions is that you have one interface forall of your array tasks. You will not use foreach() construct in onecase, list-each combination in other and yet next() and prev()functions in third any more. One more advantage is that now you caneasily position on any particular element and start traversing fromthere (in any way you want). Here are few code examples:

有了这样的接口,我们就可以在任何时间,任何地点,用我们喜欢的任何方法轻轻松松执行我们的日常事务(像数组遍历)。使用这个东西相对于PHP本身的数组函数的一个优势就是你有了一个为你的所有数组任务[工作]的接口。你就用不着在一段代码里用foreach()构造,另外一段用list和each的组合,然后还有别的地方一会儿用到next(),一会儿用到perv()。另一个优势是现在你可以简单的定位于一个确定的元素并且可以从这里开始遍历(以你想要得任何方式). 这里有一些例子:

<?php
// $array = ?. // initialize the array 初始化数组
$iterator = new Iterator($array);
// traverse the array 遍历数组
while ($elem = $iterator->next()) {
echo $elem;
}

// traverse array in reverse order 反序便利
$iterator->end();
while ($elem = $iterator->previous()) {
echo $elem;
}

// traverse array from fifth element on 从第五元素开始遍历
$iterator->seek(5);

while ($elem = $iterator->next()) {
echo $elem;
}
?>

OK, you say, this is all nice but there is nothing I can’t do with combination of native PHP functions. Besidesthe fact that you are accessing all your arrays through uniqueinterface, another (and most important) advantage is that Iterator’sobject structure allows you to easily expand its functionality.

OK, 你说,这是很不错,但是没有什么我不能使用原始的PHP函数组合来完成的。除了这个事实,你能通过唯一的接口存取你的所有数组,另外(也是最重要的)优势是迭代器的对象结构容许你轻松的扩展它的功能

ObjectIterator interface
对像迭代器器接口

Often I have ended up in situations where my object methods had toreturn an array of some other object as a result. Usually that objectis loaded from the database, but could be some other situation such asobtaining objects through some RPC protocol (XML-RPC, SOAP,…) or endless other situation combinations that you experience everyday. In this article we will focus on the first problem and brieflyexplain how to empower Iterator for the purpose you’d need.

我经常中断于我的对象方法要返回一些其他对象的数组作为结果[的情况]。通常这钟对象是从数据库读取得,但是也可能是其他状况比如获得一个通过一些RPC 协议 (XML-RPC, SOAP,…) 得到的对象或是你每天要经历的无尽的其他情况的组合。在这篇文章中,我们将聚焦在第一个问题并且简要的解释如何去为你的目的实现迭代器。

Suppose that you are developing an address book for some large webapplication. Your address book will work with companies and persons. Inaddition, companies could have an endless number of employees (that arealso kinds of persons). So far we have recognized two objects in ourapplication: Company and Person. Also, it is clear that the companywill have method getEmployees() that returns an array of Personobjects. There are a number of possible implementations of this method.Here are some usual implementations:

假设你在为一个大型web应用程序开发一个通讯录。你的通讯录将工作在公司和人上。另外,公司可能有无穷多的雇员(这也是一类人)。现在我们认清了应用程序中的两个对象:公司和个人。同时也清楚了Company将有一个方法getEmployees()返回一个人对象的数组。这个方法的实现有很多。这里是一些常规的实现方法:

First, you could write a query to collect all the ids of all thecompany employees. Then you could make an array that contains all theobjects and returns this array. This would look something like this(supposing you have a database

首先,你可以写一个查询去收集所有公司员工的id。然后,你能制造一个数组包含所有的对象再返回这个数组。这看上去像这样(假设你有一个数据库)

<?php
function getEmployees() {
$query = “select id FROM persons WHERE companyid = $this->companyid”;
$stmt = execute_query($query); // creates statement object 创建语句对象
$this->employees = array();

while ($row = fetch_object($stmt) {
$this->employess[$row->id] = new Person($row->id); // object creation 对象创建
}

return $this->employees;
}
?>

and the usage would be:

使用起来像是这样:

<?php
$company = new Company(”Datagate”);
$employees = $company->getEmployees();
foreach ($employees as $id =>$employee)
$employee->addVacationDays(3); // object usage 对象的使用
?>

OK, these look like fairly obvious solutions. But, it has few bigflaws. All objects are created but we don’t know if we’re going to usethem all. There are two performance problems here. First accessing arelational database (for creating these objects) can be very timeexpensive. And if the company has 500 employees and you need to accessdata for only 50, that is lot of time wasted. Imagine now, that we areloading these objects through RPC which is even slower. This couldseriously affect application performance. Now, even if all objects areneeded we don’t need them at the same time, we need objects one by oneas we are traversing the array. The solution above is a huge waste of resources (memory and time).

OK, 这些看上去像是清晰明确的解决方案。但是,它存在一些大的瑕疵。所有的对象都被创建但我们却不知道他们是否都要被使用。这里有两个性能的问题。首先存取一个关系数据库(为了创建这些对象)时间开销很大。其次,如果公司有500个员工并且你只需要为其中50个存取数据,这将是极大的时间上的浪费。现在想象一下,我们将这些对象通过更缓慢的RPC读取。这将严重的影响应用程序的性能。现在,即使所有的对象都是被需要的我们也不是在同一时间需要他们,我们需要一个接着一个的[处理]对象就像我们在遍历一个数组。上面的解方案是一个巨大的资源(内存和时间)浪费。

The solution to these performance problems looks so obvious. Let’sreturn just an array of employee ids. The code would look somethinglike this:

对于这个性能问题的解决方案看起来是那么显而易见。让我们来仅仅返回一个包含员工id的数组。代码看上去像是这样的:

<?php
function getEmployees() {
$query = “SELECT id FROM persons WHERE companyid = $this->companyid”;
$stmt = execute_query($query); // creates statement object
$this->employees = array();
while ($row = fetch_object($stmt) {
$this->employess[$row->id] = $row->id;
}
return $this->employees;
}
?>

and the usage would be:

使用就是这样:

<?php
$company = new Company(”Datagate”);
$employees = $company->getEmployees();
foreach ($employees as $id) {
$employee = new Employee($id); // object creation
$employee->addVacationDays(3); // object usage
}
?>

This looks fine at the first sight. We have saved time and memory ,but another problem has arisen. Suppose that the code for creatingEmployee object changes, for example you need to add extra argument tothe constructor or some extra initialization (these are things that arehappening on real projects). In that case you’ll need to modify yourcode in many places (wherever you have used getEmployees() method), Andthat is a problem.

第一眼看上去挺不错。我们节省了时间和内存,但是另一个问题出现了。假设为创建员工对象的代码发生了变化,例如你需要给构造函数加入额外的参数或者一些额外的初始化(这是真实的项目中将会发生的)。在这个问题上,你将要在很多地方修改你的代码(任何你使用getEmployees()方法的地方),这是个问题。

The third solution is to use an ObjectIterator class that isextended from Iterator. In this example we will see how easy it is toextend Iterator class to serve your purposes. When you are usingObjectIterator your getEmployee() function will stay the same as insecond solution. So, we have saved our resources. No unnecessaryobjects are created and everything looks just fine. Now let’s look atthe usage code:

第三个解决方案是使用一个对象迭代器类,它迭代器的扩展。在这个例子里我们将看到扩展迭代器为你的目的服务是何等容易。当你在使用对象迭代器时你的getEmployee()[应该是getEmplyees()吧]函数将保持和第二个解决方案一样。因此,我们节省了我们的资源。没有不需要的对象被创建而且每件事看上去都很好。现在,让我们看看使用代码:

<?php
$company = new Company(”Datagate”);
$iterator = new Iterator($company->getEmployees(), “Employee”);
while ($employee = $iterator->next()) {
$employee->addVacationDays(3);
}
?>

We see now that the object creation code is hidden in the ObjectIterator class, so it is now easy to support changes.

我们现在看到了对象创建的代码被隐藏在对象迭代器类里,因此现在很容易去支持改变。

ObjectIterator implementation
对象迭代器的实现

The code for ObjectIterator is quite simple.
对象迭代器的代码十分简单

<?php
/**
* Implements iterator for traversing collection of objects
* for the given array od object identifiers
*
* @version 1.0
* @author <a href=Mailto:chubrilo@yahoo.com>Dejan Bosanac</a>
*/
class ObjectIterator extends Iterator { var $_objectName;
/**
* Constructor
* Calls initialization method (@see Iterator::_initialize())
* and do some specific configuration
* @param array $array array of object ids to be traversed
* @param string $objectName class of objects to be created
* @access public
*
* 构造函数
* 调用初始化方法(参考Iterator::_initialize())
* 做一些特殊的配置
* @参数数组 $array 要遍历的对象id的数组
* @参数字符串 $objectName 要被创建的对象的类
*/
function ObjectIterator($array, $objectName) {
$this->_initialize($array);
$this->_objectName = $objectName;
}
/**
* Returns object with given id
* @param Object $id identifier of the object
* @return Object next object in the collection
* @access private
*
* 用给出的id返回对象
* @参数对象 $id 标示一个对象
* @返回对象 集合里的下一个对象
* @存取 私有
*/
function _fetchObject($id) {
return new $this->_objectName($id);
}
}
?>

It has $_objectName class member that represent class of the objectthat has to be returned by next() and previous() methods. Theconstructor sets this internal variable and calls an initializationfunction (defined in Iterator class). The most important thing is the_fetchObject() function. It encapsulates code for object creation. Itis called from next() and previous() methods and takes object id as aparameter. So all your object creation code is localized here, thusmaking it easy to change and expand.

类成员$_objectName代表对象所属的类它必须被next()和pervious()方法返回。构造函数设置内部变量并且调用初始化函数(已经在迭代器类定义了)。最重要的事情是_fetchObject()函数。他封装了对象创建的代码。它被next()和pervious()方法调用并且用对象的id作为参数)。这样一来你所有的对象创建代码都在这里了,因此他被制作的容易改变和扩展

So, here are instructions for creating our new type of iterators.First, make your constructor (which has to have an array as aparameter) which calls _initialize() function from Iterator class.Second, override _fetchObject method to do whatever you have to do tomake your objects. And, that would be all.

这里是我们创建新的一类迭代器的方法。第一,制作你的构造函数(它有一个数组作为参数),它调用从迭代器类_initialize()函数。第二,重载 _fetchObject方法去做任何你必须对你的对象做的。这就是全部。

In conclusion
结论

Iterator will not slow down your application in a way that it willneed new hardware to run it. It has some overhead, but for that priceyou get clean and easy readable code that is flexible enough for futuresoftware enhancements.

迭代器不会以任何方式减慢你的应用程序除非它需要一套新的硬件去运行。它有一些负荷,但是给了你清晰易读的代码,这将使今后软件的扩展十分方便

.NET.yu/~chuki/DOWNLOAD/iterator.tar.gz”>你可以在这里下载源程序

没有评论 »

【转】unicode、utf-8、ansi的故事

十一月 28, 2008 | 数据结构算法, 软件工程/编程技巧/设计模式 | RSS 2.0

快下班时,爱问问题的小朋友Nico又问了一个问题:”sqlserver里面有char和nchar,那个n据说是指unicode的数据,这个是什么意思。”并不是所有简单的问题都很容易回答,就像这个问题一样。于是我答应专门写一篇BLOG来从头讲讲编码的故事。那么就让我们找个草堆坐下,先抽口烟,看看夜晚天空上的银河,然后想一想要从哪里开始讲起。嗯,也许这样开始比较好……

很久很久以前,有一群人,他们决定用8个可以开合的晶体管来组合成不同的状态,以表示世界上的万物。他们看到8个开关状态是好的,于是他们把这称为”字节”。再后来,他们又做了一些可以处理这些字节的机器,机器开动了,可以用字节来组合出很多状态,状态开始变来变去。他们看到这样是好的,于是它们就这机器称为”计算机”。

开始计算机只在美国用。八位的字节一共可以组合出256(2的8次方)种不同的状态。他们把其中的编号从0开始的32种状态分别规定了特殊的用途,一但终端、打印机遇上约定好的这些字节被传过来时,就要做一些约定的动作。遇上00×10,终端就换行,遇上0×07, 终端就向人们嘟嘟叫,例好遇上0×1b,打印机就打印反白的字,或者终端就用彩色显示字母。他们看到这样很好,于是就把这些0×20以下的字节状态称为”控制码”。

他们又把所有的空格、标点符号、数字、大小写字母分别用连续的字节状态表示,一直编到了第127号,这样计算机就可以用不同字节来存储英语的文字了。大家看到这样,都感觉很好,于是大家都把这个方案叫做 ANSI 的”Ascii”编码(American Standard Code forInformation Interchange,美国信息互换标准代码)。当时世界上所有的计算机都用同样的ASCII方案来保存英文文字。

后来,就像建造巴比伦塔一样,世界各地的都开始使用计算机,但是很多国家用的不是英文,他们的字母里有许多是ASCII里没有的,为了可以在计算机保存他们的文字,他们决定采用127号之后的空位来表示这些新的字母、符号,还加入了很多画表格时需要用下到的横线、竖线、交叉等形状,一直把序号编到了最后一个状态255。从128到255这一页的字符集被称”扩展字符集”。从此之后,贪婪的人类再没有新的状态可以用了,美帝国主义可能没有想到还有第三世界国家的人们也希望可以用到计算机吧!

等中国人们得到计算机时,已经没有可以利用的字节状态来表示汉字,况且有6000多个常用汉字需要保存呢。但是这难不倒智慧的中国人民,我们不客气地把那些127号之后的奇异符号们直接取消掉,规定:一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(他称之为高字节)从0xA1用到0xF7,后面一个字节(低字节)从0xA1到0xFE,这样我们就可以组合出大约7000多个简体汉字了。在这些编码里,我们还把数学符号、罗马希腊的字母、日文的假名们都编进去了,连在 ASCII里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的”全角”字符,而原来在127号以下的那些就叫”半角”字符了。

中国人民看到这样很不错,于是就把这种汉字方案叫做 “GB2312″。GB2312 是对 ASCII的中文扩展。但是中国的汉字太多了,我们很快就就发现有许多人的人名没有办法在这里打出来,特别是某些很会麻烦别人的国家领导人。于是我们不得不继续把GB2312 没有用到的码位找出来老实不客气地用上。

后来还是不够用,于是干脆不再要求低字节一定是127号之后的内码,只要第一个字节是大于127就固定表示这是一个汉字的开始,不管后面跟的是不是扩展字符集里的内容。结果扩展之后的编码方案被称为 GBK 标准,GBK 包括了 GB2312的所有内容,同时又增加了近20000个新的汉字(包括繁体字)和符号。后来少数民族也要用电脑了,于是我们再扩展,又加了几千个新的少数民族的字,GBK 扩成了 GB18030。从此之后,中华民族的文化就可以在计算机时代中传承了。

中国的程序员们看到这一系列汉字编码的标准是好的,于是通称他们叫做 “DBCS”(Double Byte Charecter Set双字节字符集),也叫做ANSI字符集。在ANSI/DBCS系列标准里,最大的特点是两字节长的汉字字符和一字节长的英文字符并存于同一套编码方案里,因此他们写的程序为了支持中文处理,必须要注意字串里的每一个字节的值,如果这个值是大于127的,那么就认为一个双字节字符集里的字符出现了。那时候凡是受过加持,会编程的计算机僧侣们都要每天念下面这个咒语数百遍:”一个汉字算两个英文字符!一个汉字算两个英文字符……”

因为当时各个国家都像中国这样搞出一套自己的编码标准,结果互相之间谁也不懂谁的编码,谁也不支持别人的编码,连大陆和台湾这样只相隔了150海里,使用着同一种语言的兄弟地区,也分别采用了不同的 ANSI/DBCS编码方案——当时的中国人想让电脑显示汉字,就必须装上一个”汉字系统”,专门用来处理汉字的显示、输入的问题,但是那个台湾的愚昧封建人士写的算命程序就必须加装另一套支持 BIG5编码的什么”倚天汉字系统”才可以用,装错了字符系统,显示就会乱了套!这怎么办?而且世界民族之林中还有那些一时用不上电脑的穷苦人民,他们的文字又怎么办?真是计算机的巴比伦塔命题啊!

正在这时,大天使加百列及时出现了——一个叫 ISO(国际标谁化组织)的国际组织决定着手解决这个问题。他们采用的方法很简单:废了所有的地区性编码方案,重新搞一个包括了地球上所有文化、所有字母和符号的编码!他们打算叫它”Universal Multiple-Octet Coded Character Set”,简称 UCS,俗称“UNICODE”。

UNICODE 开始制订时,计算机的存储器容量极大地发展了,空间再也不成为问题了。于是 ISO就直接规定必须用两个字节,也就是16位来统一表示所有的字符,对于ascii里的那些“半角”字符,UNICODE包持其原编码不变,只是将其长度由原来的8位扩展为16位,而其他文化和语言的字符则全部重新统一编码。由于”半角”英文符号只需要用到低8位,所以其高8位永远是0,因此这种大气的方案在保存英文文本时会多浪费一倍的空间。

这时候,从旧社会里走过来的程序员开始发现一个奇怪的现象:他们的strlen函数靠不住了,一个汉字不再是相当于两个字符了,而是一个!是的,从UNICODE开始,无论是半角的英文字母,还是全角的汉字,它们都是统一的”一个字符”!同时,也都是统一的”两个字节”,请注意”字符”和”字节”两个术语的不同,“字节”是一个8位的物理存贮单元,而“字符”则是一个文化相关的符号。在UNICODE中,一个字符就是两个字节。一个汉字算两个英文字符的时代已经快过去了。

从前多种字符集存在时,那些做多语言软件的公司遇上过很大麻烦,他们为了在不同的国家销售同一套软件,就不得不在区域化软件时也加持那个双字节字符集咒语,不仅要处处小心不要搞错,还要把软件中的文字在不同的字符集中转来转去。UNICODE 对于他们来说是一个很好的一揽子解决方案,于是从Windows NT 开始,MS 趁机把它们的操作系统改了一遍,把所有的核心代码都改成了用 UNICODE方式工作的版本,从这时开始,WINDOWS 系统终于无需要加装各种本土语言系统,就可以显示全世界上所有文化的字符了。

但是,UNICODE 在制订时没有考虑与任何一种现有的编码方案保持兼容,这使得 GBK 与 UNICODE 在汉字的内码编排上完全是不一样的,没有一种简单的算术方法可以把文本内容从 UNICODE 编码和另一种编码进行转换,这种转换必须通过查表来进行。

如前所述,UNICODE是用两个字节来表示为一个字符,他总共可以组合出65535不同的字符,这大概已经可以覆盖世界上所有文化的符号。如果还不够也没有关系,ISO已经准备了UCS-4方案,说简单了就是四个字节来表示一个字符,这样我们就可以组合出21亿个不同的字符出来(最高位有其他用途),这大概可以用到银河联邦成立那一天吧!

UNICODE 来到时,一起到来的还有计算机网络的兴起,UNICODE 如何在网络上传输也是一个必须考虑的问题,于是面向传输的众多UTF(UCS Transfer Format)标准出现了,顾名思义,UTF8就是每次8个位传输数据,而UTF16就是每次16个位,只不过为了传输时的可靠性,从UNICODE到UTF时并不是直接的对应,而是要过一些算法和规则来转换。

受到过网络编程加持的计算机僧侣们都知道,在网络里传递信息时有一个很重要的问题,就是对于数据高低位的解读方式,一些计算机是采用低位先发送的方法,例如我们PC机采用的 INTEL架构,而另一些是采用高位先发送的方式,在网络中交换数据时,为了核对双方对于高低位的认识是否是一致的,采用了一种很简便的方法,就是在文本流的开始时向对方发送一个标志符——如果之后的文本是高位在先,那就发送”FEFF”,反之,则发送”FFFE”。不信你可以用二进制方式打开一个UTF-X格式的文件,看看开头两个字节是不是这两个字节?

讲到这里,我们再顺便说说一个很著名的奇怪现象:当你在 windows的记事本里新建一个文件,输入”联通”两个字之后,保存,关闭,然后再次打开,你会发现这两个字已经消失了,代之的是几个乱码!呵呵,有人说这就是联通之所以拼不过移动的原因。其实这是因为GB2312编码与UTF-8编码产生了编码冲撞的原因。

从网上引来一段从UNICODE到UTF-8的转换规则:
Unicode              UTF-8
0000 – 007F        0xxxxxxx
0080 – 07FF        110xxxxx 10xxxxxx
0800 – FFFF        1110xxxx 10xxxxxx 10xxxxxx

例如”汉”字的Unicode编码是6C49。6C49在0800-FFFF之间,所以要用3字节模板:1110xxxx 10xxxxxx10xxxxxx。将6C49写成二进制是:0110 1100 0100 1001,将这个比特流按三字节模板的分段方法分为0110 110001001001,依次代替模板中的x,得到:1110-0110 10-110001 10-001001,即E6 B1 89,这就是其UTF8的编码。

而当你新建一个文本文件时,记事本的编码默认是ANSI, 如果你在ANSI的编码输入汉字,那么他实际就是GB系列的编码方式,在这种编码下,”联通”的内码是:
c1 1100 0001
aa 1010 1010
cd 1100 1101
a8 1010 1000

注意到了吗?第一二个字节、第三四个字节的起始部分的都是”110″和”10″,正好与UTF8规则里的两字节模板是一致的,于是再次打开记事本时,记事本就误认为这是一个UTF8编码的文件,让我们把第一个字节的110和第二个字节的10去掉,我们就得到了”00001101010″,再把各位对齐,补上前导的0,就得到了”0000 0000 01101010″,不好意思,这是UNICODE的006A,也就是小写的字母”j”,而之后的两字节用UTF8解码之后是0368,这个字符什么也不是。这就是只有”联通”两个字的文件没有办法在记事本里正常显示的原因。

而如果你在”联通”之后多输入几个字,其他的字的编码不见得又恰好是110和10开始的字节,这样再次打开时,记事本就不会坚持这是一个utf8编码的文件,而会用ANSI的方式解读之,这时乱码又不出现了。

没有评论 »

c编程修养

十月 26, 2008 | c/c++, 软件工程/编程技巧/设计模式 | RSS 2.0

  什么是好的程序员?是不是懂得很多技术细节?还是懂底层编程?还是编程速度比较快?

  我觉得都不是。对于一些技术细节来说和底层的技术,只要看帮助,查资料就能找到,对

  于速度快,只要编得多也就熟能生巧了。

  我认为好的程序员应该有以下几方面的素质:

  1、有专研精神,勤学善问、举一反三。

  2、积极向上的态度,有创造性思维。

  3、与人积极交流沟通的能力,有团队精神。

  4、谦虚谨慎,戒骄戒燥。

  5、写出的代码质量高。包括:代码的稳定、易读、规范、易维护、专业。

  这些都是程序员的修养,这里我想谈谈“编程修养”,也就是上述中的第5点。我觉得,如

  果我要了解一个作者,我会看他所写的小说,如果我要了解一个画家,我会看他所画的图

  画,如果我要了解一个工人,我会看他所做出来的产品,同样,如果我要了解一个程序员

  ,我想首先我最想看的就是他的程序代码,程序代码可以看出一个程序员的素质和修养,

  程序就像一个作品,有素质有修养的程序员的作品必然是一图精美的图画,一首美妙的歌

  曲,一本赏心悦目的小说。

  我看过许多程序,没有注释,没有缩进,胡乱命名的变量名,等等,等等,我把这种人统

  称为没有修养的程序,这种程序员,是在做创造性的工作吗?不,完全就是在搞破坏,他

  们与其说是在编程,还不如说是在对源程序进行“加密”,这种程序员,见一个就应该开

  除一个,因为他编的程序所创造的价值,远远小于需要在上面进行维护的价值。

  程序员应该有程序员的修养,那怕再累,再没时间,也要对自己的程序负责。我宁可要那

  种动作慢,技术一般,但有良好的写程序风格的程序员,也不要那种技术强、动作快的“

  搞破坏”的程序员。有句话叫“字如其人”,我想从程序上也能看出一个程序员的优劣。

  因为,程序是程序员的作品,作品的好坏直截关系到程序员的声誉和素质。而“修养”好

  的程序员一定能做出好的程序和软件。

  有个成语叫“独具匠心”,意思是做什么都要做得很专业,很用心,如果你要做一个“匠

  ”,也就是造诣高深的人,那么,从一件很简单的作品上就能看出你有没有“匠”的特性

  ,我觉得做一个程序员不难,但要做一个“程序匠”就不简单了。编程序很简单,但编出

  有质量的程序就难了。

  我在这里不讨论过深的技术,我只想在一些容易让人忽略的东西上说一说,虽然这些东西

  可能很细微,但如果你不注意这些细微之处的话,那么他将会极大的影响你的整个软件质

  量,以及整个软件程的实施,所谓“千里之堤,毁于蚁穴”。

  “细微之处见真功”,真正能体现一个程序的功底恰恰在这些细微之处。

  这就是程序员的——编程修养。我总结了在用C/C++语言(主要是C语言)进行程序写作上

  的三十二个“修养”,通过这些,你可以写出质量高的程序,同时也会让看你程序的人渍

  渍称道,那些看过你程序的人一定会说:“这个人的编程修养不错”。

  ————————————————————————

  01、版权和版本

  02、缩进、空格、换行、空行、对齐

  03、程序注释

  04、函数的[in][out]参数

  05、对系统调用的返回进行判断

  06、if 语句对出错的处理

  07、头文件中的#ifndef

  08、在堆上分配内存

  09、变量的初始化

  10、h和c文件的使用

  11、出错信息的处理

  12、常用函数和循环语句中的被计算量

  13、函数名和变量名的命名

  14、函数的传值和传指针

  15、修改别人程序的修养

  16、把相同或近乎相同的代码形成函数和宏

  17、表达式中的括号

  18、函数参数中的const

  19、函数的参数个数

  20、函数的返回类型,不要省略

  21、goto语句的使用

  22、宏的使用

  23、static的使用

  24、函数中的代码尺寸

  25、typedef的使用

  26、为常量声明宏

  27、不要为宏定义加分号

  28、||和&&的语句执行顺序

  29、尽量用for而不是while做循环

  30、请sizeof类型而不是变量

  31、不要忽略Warning

  32、书写Debug版和Release版的程序

  21、goto语究 使劲

  22、宏的使用

  23、static的使用

  24、函数中的代码尺寸

  25、typedef的使用

  26、为常量声明宏

  27、不要为宏定义加分号

  28、||和&&的语句执行顺序

  29、尽量用for而不是while做循环

  30、请sizeof类型而不是变量

  31、不要忽略Warning

  32、书写Debug版和Release版的程序

  ————————————————————————

  1、版权和版本

  ———————

  好的程序员会给自己的每个函数,每个文件,都注上版权和版本。

  对于C/C++的文件,文件头应该有类似这样的注释:

  /************************************************************************

  *

  * 文件名:network.c

  *

  * 文件描述:网络通讯函数集

  *

  * 创建人: Hao Chen, 2003年2月3日

  *

  * 版本号:1.0

  *

  * 修改记录:

  *

  *

  ************************************************************************/

  而对于函数来说,应该也有类似于这样的注释:

  /*================================================================

  *

  * 函 数 名:XXX

  *

  * 参 数:

  *

  * type name [IN] : descripts

  *

  * 功能描述:

  *

  * …………..

  *

  * 返 回 值:成功TRUE,失败FALSE

  *

  * 抛出异常:

  *

  * 作 者:ChenHao 2003/4/2

  *

  *

  ================================================================*/

  这样的描述可以让人对一个函数,一个文件有一个总体的认识,对代码的易读性和易维护

  性有很大的好处。这是好的作品产生的开始。

  2、缩进、空格、换行、空行、对齐

  ————————————————

  i) 缩进应该是每个程序都会做的,只要学程序过程序就应该知道这个,但是我仍然看过不

  缩进的程序,或是乱缩进的程序,如果你的公司还有写程序不缩进的程序员,请毫不犹豫

  的开除他吧,并以破坏源码罪起诉他,还要他赔偿读过他程序的人的精神损失费。缩进,

  这是不成文规矩,我再重提一下吧,一个缩进一般是一个TAB键或是4个空格。(最好用TAB

  键)

  ii) 空格。空格能给程序代来什么损失吗?没有,有效的利用空格可以让你的程序读进来

  更加赏心悦目。而不一堆表达式挤在一起。看看下面的代码:

  ha=(ha*128+*key++)%tabPtr->size;

  ha = ( ha * 128 + *key++ ) % tabPtr->size;

  有空格和没有空格的感觉不一样吧。一般来说,语句中要在各个操作符间加空格,函

  数调用时,要以各个参数间加空格。如下面这种加空格的和不加的:

  if ((hProc=OpenProcess(PROCESS_ALL_ACCESS,FALSE,pid))==NULL){

  }

  if ( ( hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid) ) == NULL ){

  }

  iii) 换行。不要把语句都写在一行上,这样很不好。如:

  for(i=0;i<len;i++) if((a[i]<’0′||a[i]>’9′)&&(a[i]<’a'||a[i]>’z')) break;

  我拷,这种即无空格,又无换行的程序在写什么啊?加上空格和换行吧。

  for ( i=0; i<len; i++) {

  if ( ( a[i] < ‘0′ || a[i] > ‘9′ ) &&

  ( a[i] < ‘a’ || a[i] > ‘z’ ) ) {

  break;

  }

  }

  好多了吧?有时候,函数参数多的时候,最好也换行,如:

  CreateProcess(

  NULL,

  cmdbuf,

  NULL,

  NULL,

  bInhH,

  dwCrtFlags,

  envbuf,

  NULL,

  &siStartInfo,

  &prInfo

  );

  条件语句也应该在必要时换行:

  if ( ch >= ‘0′ || ch <= ‘9′ ||

  ch >= ‘a’ || ch <= ‘z’ ||

  ch >= ‘A’ || ch <= ‘Z’ )

  iv) 空行。不要不加空行,空行可以区分不同的程序块,程序块间,最好加上空行。如:

  HANDLE hProcess;

  PROCESS_T procInfo;

  /* open the process handle */

  if((hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid)) == NULL)

  {

  return LSE_MISC_SYS;

  }

  memset(&procInfo, 0, sizeof(procInfo));

  procInfo.idProc = pid;

  procInfo.hdProc = hProcess;

  procInfo.misc |= MSCAVA_PROC;

  return(0);

  v) 对齐。用TAB键对齐你的一些变量的声明或注释,一样会让你的程序好看一些。如:

  typedef struct _pt_man_t_ {

  int numProc; /* Number of processes */

  int maxProc; /* Max Number of processes */

  int maxProc; /* Max Number of processes */

  int numEvnt; /* Number of events */

  int maxEvnt; /* Max Number of events */

  HANDLE* pHndEvnt; /* Array of events */

  DWORD timeout; /* Time out interval */

  HANDLE hPipe; /* Namedpipe */

  TCHAR usr[MAXUSR];/* User name of the process */

  int numMsg; /* Number of Message */

  int Msg[MAXMSG];/* Space for intro process communicate */

  } PT_MAN_T;

  怎么样?感觉不错吧。

  这里主要讲述了如果写出让人赏心悦目的代码,好看的代码会让人的心情愉快,读起代码

  也就不累,工整、整洁的程序代码,通常更让人欢迎,也更让人称道。现在的硬盘空间这

  么大,不要让你的代码挤在一起,这样它们会抱怨你虐待它们的。好了,用“缩进、空格

  、换行、空行、对齐”装饰你的代码吧,让他们从没有秩序的土匪中变成一排排整齐有秩

  序的正规部队吧。

  3、程序注释

  3、程序注释

  ——————

  养成写程序注释的习惯,这是每个程序员所必须要做的工作。我看过那种几千行,却居然

  没有一行注释的程序。这就如同在公路上驾车却没有路标一样。用不了多久,连自己都不

  知道自己的意图了,还要花上几倍的时间才看明白,这种浪费别人和自己的时间的人,是

  最为可耻的人。

  是的,你也许会说,你会写注释,真的吗?注释的书写也能看出一个程序员的功底。一般

  来说你需要至少写这些地方的注释:文件的注释、函数的注释、变量的注释、算法的注释

  、功能块的程序注释。主要就是记录你这段程序是干什么的?你的意图是什么?你这个变

  量是用来做什么的?等等。

  不要以为注释好写,有一些算法是很难说或写出来的,只能意会,我承认有这种情况的时

  候,但你也要写出来,正好可以训练一下自己的表达能力。而表达能力正是那种闷头搞技

  术的技术人员最缺的,你有再高的技术,如果你表达能力不行,你的技术将不能得到充分

  的发挥。因为,这是一个团队的时代。

  好了,说几个注释的技术细节:

  i) 对于行注释(“//”)比块注释(“/* */”)要好的说法,我并不是很同意。因为一

  些老版本的C编译器并不支持行注释,所以为了你的程序的移植性,请你还是尽量使用块注

  释。

  ii) 你也许会为块注释的不能嵌套而不爽,那么你可以用预编译来完成这个功能。使用“#

  if 0”和“#endif”括起来的代码,将不被编译,而且还可以嵌套。

  4、函数的[in][out]参数

  ———————————

  我经常看到这样的程序:

  FuncName(char* str)

  {

  int len = strlen(str);

  …..

  }

  char*

  GetUserName(struct user* pUser)

  {

  return pUser->name;

  }

  不!请不要这样做。

  你应该先判断一下传进来的那个指针是不是为空。如果传进来的指针为空的话,那么,你

  的一个大的系统就会因为这一个小的函数而崩溃。一种更好的技术是使用断言(assert)

  ,这里我就不多说这些技术细节了。当然,如果是在C++中,引用要比指针好得多,但你也

  需要对各个参数进行检查。

  写有参数的函数时,首要工作,就是要对传进来的所有参数进行合法性检查。而对于传出

  的参数也应该进行检查,这个动作当然应该在函数的外部,也就是说,调用完一个函数后

  ,应该对其传出的值进行检查。

  当然,检查会浪费一点时间,但为了整个系统不至于出现“非法操作”或是“Core Dump”

  的系统级的错误,多花这点时间还是很值得的。

  5、对系统调用的返回进行判断

  ——————————————

  继续上一条,对于一些系统调用,比如打开文件,我经常看到,许多程序员对fopen返回的

  指针不做任何判断,就直接使用了。然后发现文件的内容怎么也读出不,或是怎么也写不

  进去。还是判断一下吧:

  fp = fopen(”log.txt”, “a”);

  if ( fp == NULL ){

  printf(”Error: open file error”);

  return FALSE;

  }

  其它还有许多啦,比如:socket返回的socket号,malloc返回的内存。请对这些系统调用

  返回的东西进行判断。

  6、if 语句对出错的处理

  ———————————

  我看见你说了,这有什么好说的。还是先看一段程序代码吧。

  if ( ch >= ‘0′ && ch <= ‘9′ ){

  /* 正常处理代码 */

  }else{

  /* 输出错误信息 */

  printf(”error ……”);

  return ( FALSE );

  }

  这种结构很不好,特别是如果“正常处理代码”很长时,对于这种情况,最好不要用else

  。先判断错误,如:

  if ( ch < ‘0′ || ch > ‘9′ ){

  /* 输出错误信息 */

  printf(”error ……”);

  return ( FALSE );

  }

  /* 正常处理代码 */

  ……

  这样的结构,不是很清楚吗?突出了错误的条件,让别人在使用你的函数的时候,第一眼

  就能看到不合法的条件,于是就会更下意识的避免。

  7、头文件中的#ifndef

  ——————————

  千万不要忽略了头件的中的#ifndef,这是一个很关键的东西。比如你有两个C文件,这两

  个C文件都include了同一个头文件。而编译时,这两个C文件要一同编译成一个可运行文件

  ,于是问题来了,大量的声明冲突。

  还是把头文件的内容都放在#ifndef和#endif中吧。不管你的头文件会不会被多个文件引用

  管你的头文件会不会被多个文件引用

  ,你都要加上这个。一般格式是这样的:

  #ifndef <标识>

  #define <标识>

  ……

  ……

  #endif

  <标识>在理论上来说可以是自由命名的,但每个头文件的这个“标识”都应该是唯一的。

  标识的命名规则一般是头文件名全大写,前后加下划线,并把文件名中的“.”也变成下划

  线,如:stdio.h

  #ifndef _STDIO_H_

  #define _STDIO_H_

  ……

  #endif

  (BTW:预编译有多很有用的功能。你会用预编译吗?)

  (BTW:预编译有多很有用的功能。你会用预编译吗?)

  8、在堆上分配内存

  —————————

  可能许多人对内存分配上的“栈 stack”和“堆 heap”还不是很明白。包括一些科班出身

  的人也不明白这两个概念。我不想过多的说这两个东西。简单的来讲,stack上分配的内存

  系统自动释放,heap上分配的内存,系统不释放,哪怕程序退出,那一块内存还是在那里

  。stack一般是静态分配内存,heap上一般是动态分配内存。

  由malloc系统函数分配的内存就是从堆上分配内存。从堆上分配的内存一定要自己释放。

  用free释放,不然就是术语——“内存泄露”(或是“内存漏洞”)—— Memory Leak。

  于是,系统的可分配内存会随malloc越来越少,直到系统崩溃。还是来看看“栈内存”和

  “堆内存”的差别吧。

  栈内存分配

  —————

  char*

  AllocStrFromStack()

  {

  char pstr[100];

  return pstr;

  }

  堆内存分配

  —————

  char*

  AllocStrFromHeap(int len)

  {

  char *pstr;

  if ( len <= 0 ) return NULL;

  return ( char* ) malloc( len );

  }

  对于第一个函数,那块pstr的内存在函数返回时就被系统释放了。于是所返回的char*什么

  也没有。而对于第二个函数,是从堆上分配内存,所以哪怕是程序退出时,也不释放,所

  以第二个函数的返回的内存没有问题,可以被使用。但一定要调用free释放,不然就是Mem

  ory Leak!

  在堆上分配内存很容易造成内存泄漏,这是C/C++的最大的“克星”,如果你的程序要稳定

  ,那么就不要出现Memory Leak。所以,我还是要在这里千叮咛万嘱付,在使用malloc系统

  蛑龈叮谑褂胢alloc系统

  函数(包括calloc,realloc)时千万要小心。

  记得有一个UNIX上的服务应用程序,大约有几百的C文件编译而成,运行测试良好,等使用

  时,每隔三个月系统就是down一次,搞得许多人焦头烂额,查不出问题所在。只好,每隔

  两个月人工手动重启系统一次。出现这种问题就是Memery Leak在做怪了,在C/C++中这种

  问题总是会发生,所以你一定要小心。一个Rational的检测工作——Purify,可以帮你测

  试你的程序有没有内存泄漏。

  我保证,做过许多C/C++的工程的程序员,都会对malloc或是new有些感冒。当你什么时候

  在使用malloc和new时,有一种轻度的紧张和惶恐的感觉时,你就具备了这方面的修养了。

  对于malloc和free的操作有以下规则:

  1) 配对使用,有一个malloc,就应该有一个free。(C++中对应为new和delete)

  2) 尽量在同一层上使用,不要像上面那种,malloc在函数中,而free在函数外。最好在同

  一调用层上使用这两个函数。

  3) malloc分配的内存一定要初始化。free后的指针一定要设置为NULL。

  注:虽然现在的操作系统(如:UNIX和Win2k/NT)都有进程内存跟踪机制,也就是如果你

  有没有释放的内存,操作系统会帮你释放。但操作系统依然不会释放你程序中所有产生了M

  emory Leak的内存,所以,最好还是你自己来做这个工作。(有的时候不知不觉就出现Mem

  ory Leak了,而且在几百万行的代码中找无异于海底捞针,Rational有一个工具叫Purify

  蛐械拇胫姓椅抟煊诤5桌陶耄琑ational有一个工具叫Purify

  ,可能很好的帮你检查程序中的Memory Leak)

  9、变量的初始化

  ————————

  接上一条,变量一定要被初始化再使用。C/C++编译器在这个方面不会像JAVA一样帮你初始

  化,这一切都需要你自己来,如果你使用了没有初始化的变量,结果未知。好的程序员从

  来都会在使用变量前初始化变量的。如:

  1) 对malloc分配的内存进行memset清零操作。(可以使用calloc分配一块全零的内存

  )

  2) 对一些栈上分配的struct或数组进行初始化。(最好也是清零)

  不过话又说回来了,初始化也会造成系统运行时间有一定的开销,所以,也不要对所有的

  变量做初始化,这个也没有意义。好的程序员知道哪些变量需要初始化,哪些则不需要。

  如:以下这种情况,则不需要。

  char *pstr; /* 一个字符串 */

  pstr = ( char* ) malloc( 50 );

  if ( pstr == NULL ) exit(0);

  strcpy( pstr, “Hello Wrold” );

  strcpy( pstr, “Hello Wrold” );

  但如果是下面一种情况,最好进行内存初始化。(指针是一个危险的东西,一定要初始化

  )

  char **pstr; /* 一个字符串数组 */

  pstr = ( char** ) malloc( 50 );

  if ( pstr == NULL ) exit(0);

  /* 让数组中的指针都指向NULL */

  memset( pstr, 0, 50*sizeof(char*) );

  而对于全局变量,和静态变量,一定要声明时就初始化。因为你不知道它第一次会在哪里

  被使用。所以使用前初始这些变量是比较不现实的,一定要在声明时就初始化它们。如:

  Links *plnk = NULL; /* 对于全局变量plnk初始化为NULL */

  10、h和c文件的使用

  —————————

  —————————

  H文件和C文件怎么用呢?一般来说,H文件中是declare(声明),C文件中是define(定义

  )。因为C文件要编译成库文件(Windows下是.obj/.lib,UNIX下是.o/.a),如果别人要

  使用你的函数,那么就要引用你的H文件,所以,H文件中一般是变量、宏定义、枚举、结

  构和函数接口的声明,就像一个接口说明文件一样。而C文件则是实现细节。

  H文件和C文件最大的用处就是声明和实现分开。这个特性应该是公认的了,但我仍然看到

  有些人喜欢把函数写在H文件中,这种习惯很不好。(如果是C++话,对于其模板函数,在V

  C中只有把实现和声明都写在一个文件中,因为VC不支持export关键字)。而且,如果在H

  文件中写上函数的实现,你还得在makefile中把头文件的依赖关系也加上去,这个就会让

  你的makefile很不规范。

  最后,有一个最需要注意的地方就是:带初始化的全局变量不要放在H文件中!

  例如有一个处理错误信息的结构:

  char* errmsg[] = {

  /* 0 */ “No error”,

  /* 1 */ “Open file error”,

  /* 2 */ “Failed in sending/receiving a message”,

  /* 3 */ …

没有评论 »

C 语言最大难点揭秘(转)

十月 21, 2008 | c/c++, 软件工程/编程技巧/设计模式 | RSS 2.0

本文将带您了解一些良好的和内存相关的编码实践,以将内存错误保持在控制范围内。内存错误是 C 和 C++编程的祸根:它们很普遍,认识其严重性已有二十多年,但始终没有彻底解决,它们可能严重影响应用程序,并且很少有开发团队对其制定明确的管理计划。但好消息是,它们并不怎么神秘。

    C 和 C++程序中的内存错误非常有害:它们很常见,并且可能导致严重的后果。来自计算机应急响应小组(请参见参考资料)和供应商的许多最严重的安全公告都是由简单的内存错误造成的。自从 70 年代末期以来,C 程序员就一直讨论此类错误,但其影响在 2007年仍然很大。更糟的是,如果按我的思路考虑,当今的许多 C 和 C++程序员可能都会认为内存错误是不可控制而又神秘的顽症,它们只能纠正,无法预防。

但事实并非如此。本文将让您在短时间内理解与良好内存相关的编码的所有本质:

正确的内存管理的重要性
内存错误的类别
内存编程的策略

正确的内存管理的重要性

存在内存错误的 C 和 C++程序会导致各种问题。如果它们泄漏内存,则运行速度会逐渐变慢,并最终停止运行;如果覆盖内存,则会变得非常脆弱,很容易受到恶意用户的攻击。从1988 年著名的莫里斯蠕虫 攻击到有关 Flash Player和其他关键的零售级程序的最新安全警报都与缓冲区溢出有关:“大多数计算机安全漏洞都是缓冲区溢出”,Rodney Bates 在 2004 年写道。

在可以使用 C 或 C++ 的地方,也广泛支持使用其他许多通用语言(如 Java?、Ruby、Haskell、C#、Perl、Smalltalk等),每种语言都有众多的爱好者和各自的优点。但是,从计算角度来看,每种编程语言优于 C 或 C++的主要优点都与便于内存管理密切相关。与内存相关的编程是如此重要,而在实践中正确应用又是如此困难,以致于它支配着面向对象编程语言、功能性编程语言、高级编程语言、声明性编程语言和另外一些编程语言的所有其他变量或理论。

与少数其他类型的常见错误一样,内存错误还是一种隐性危害:它们很难再现,症状通常不能在相应的源代码中找到。例如,无论何时何地发生内存泄漏,都可能表现为应用程序完全无法接受,同时内存泄漏不是显而易见。

因此,出于所有这些原因,需要特别关注 C 和 C++ 编程的内存问题。让我们看一看如何解决这些问题,先不谈是哪种语言。

内存错误的类别

首先,不要失去信心。有很多办法可以对付内存问题。我们先列出所有可能存在的实际问题:

内存泄漏
错误分配,包括大量增加 free() 释放的内存和未初始化的引用
悬空指针
数组边界违规
这是所有类型。即使迁移到 C++ 面向对象的语言,这些类型也不会有明显变化;无论数据是简单类型还是 C 语言的 struct 或 C++的类,C 和 C++ 中内存管理和引用的模型在原理上都是相同的。以下内容绝大部分是“纯 C”语言,对于扩展到 C++ 主要留作练习使用。

内存泄漏

在分配资源时会发生内存泄漏,但是它从不回收。下面是一个可能出错的模型(请参见清单 1):

清单 1. 简单的潜在堆内存丢失和缓冲区覆盖

复制内容到剪贴板
代码:

              
        void f1(char *explanation)
        {
            char p1;

            p1 = malloc(100);
            (void) sprintf(p1,
                           "The f1 error occurred because of '%s'.",
                           explanation);
            local_log(p1);
        }
  

您看到问题了吗?除非 local_log() 对 free() 释放的内存具有不寻常的响应能力,否则每次对 f1 的调用都会泄漏 100 字节。在记忆棒增量分发数兆字节内存时,一次泄漏是微不足道的,但是连续操作数小时后,即使如此小的泄漏也会削弱应用程序。

在实际的 C 和 C++ 编程中,这不足以影响您对 malloc() 或 new 的使用,本部分开头的句子提到了“资源”不是仅指“内存”,因为还有类似以下内容的示例(请参见清单 2)。FILE 句柄可能与内存块不同,但是必须对它们给予同等关注:

清单 2. 来自资源错误管理的潜在堆内存丢失

复制内容到剪贴板
代码:

               
        int getkey(char *filename)
        {
            FILE *fp;
            int key;

            fp = fopen(filename, "r");
            fscanf(fp, "%d", &key);
            return key;
        }

fopen 的语义需要补充性的 fclose。在没有 fclose() 的情况下,C 标准不能指定发生的情况时,很可能是内存泄漏。其他资源(如信号量、网络句柄、数据库连接等)同样值得考虑。

内存错误分配

错误分配的管理不是很困难。下面是一个示例(请参见清单 3):

复制内容到剪贴板
代码:

清单 3. 未初始化的指针
               
        void f2(int datum)
        {
            int *p2;

                /* Uh-oh!  No one has initialized p2. */
            *p2 = datum;
               ...
        }

关于此类错误的好消息是,它们一般具有显著结果。在 AIX? 下,对未初始化指针的分配通常会立即导致 segmentation fault错误。它的好处是任何此类错误都会被快速地检测到;与花费数月时间才能确定且难以再现的错误相比,检测此类错误的代价要小得多。

在此错误类型中存在多个变种。free() 释放的内存比 malloc() 更频繁(请参见清单 4):

清单 4. 两个错误的内存释放

复制内容到剪贴板
代码:

               
        /* Allocate once, free twice. */
        void f3()
        {
            char *p;

            p = malloc(10);
             ...
            free(p);
             ...
            free(p);
        }

        /* Allocate zero times, free once. */
        void f4()
        {
            char *p;

                /* Note that p remains uninitialized here. */
            free(p);
        }

这些错误通常也不太严重。尽管 C 标准在这些情形中没有定义具体行为,但典型的实现将忽略错误,或者快速而明确地对它们进行标记;总之,这些都是安全情形。

悬空指针

悬空指针比较棘手。当程序员在内存资源释放后使用资源时会发生悬空指针(请参见清单 5):

清单 5. 悬空指针

复制内容到剪贴板
代码:

               
       void f8()
       {
           struct x *xp;

           xp = (struct x *) malloc(sizeof (struct x));
           xp.q = 13;
           ...
           free(xp);
           ...
               /* Problem!  There's no guarantee that
                  the memory block to which xp points
                  hasn't been overwritten. */
           return xp.q;
       }

传统的“调试”难以隔离悬空指针。由于下面两个明显原因,它们很难再现:

即使影响提前释放内存范围的代码已本地化,内存的使用仍然可能取决于应用程序甚至(在极端情况下)不同进程中的其他执行位置。
悬空指针可能发生在以微妙方式使用内存的代码中。结果是,即使内存在释放后立即被覆盖,并且新指向的值不同于预期值,也很难识别出新值是错误值。
悬空指针不断威胁着 C 或 C++ 程序的运行状态。

数组边界违规

数组边界违规十分危险,它是内存错误管理的最后一个主要类别。回头看一下清单 1;如果 explanation 的长度超过80,则会发生什么情况?回答:难以预料,但是它可能与良好情形相差甚远。特别是,C 复制一个字符串,该字符串不适于为它分配的 100个字符。在任何常规实现中,“超过的”字符会覆盖内存中的其他数据。内存中数据分配的布局非常复杂并且难以再现,所以任何症状都不可能追溯到源代码级别的具体错误。这些错误通常会导致数百万美元的损失。

内存编程的策略

勤奋和自律可以让这些错误造成的影响降至最低限度。下面我们介绍一下您可以采用的几个特定步骤;我在各种组织中处理它们的经验是,至少可以按一定的数量级持续减少内存错误。

编码风格

编码风格是最重要的,我还从没有看到过其他任何作者对此加以强调。影响资源(特别是内存)的函数和方法需要显式地解释本身。下面是有关标头、注释或名称的一些示例(请参见清单 6)。

清单 6. 识别资源的源代码示例

复制内容到剪贴板
代码:

     
        /********
         * ...
         *
         * Note that any function invoking protected_file_read()
         * assumes responsibility eventually to fclose() its
         * return value, UNLESS that value is NULL.
         *
         ********/
        FILE *protected_file_read(char *filename)
        {
            FILE *fp;

            fp = fopen(filename, "r");
            if (fp) {
                ...
            } else {
                ...
            }
            return fp;
        }

        /*******
         * ...
         *
         * Note that the return value of get_message points to a
         * fixed memory location.  Do NOT free() it; remember to
         * make a copy if it must be retained ...
         *
         ********/
        char *get_message()
        {
            static char this_buffer[400];

            ...
            (void) sprintf(this_buffer, ...);
            return this_buffer;
        }

        /********
         * ...
         * While this function uses heap memory, and so
         * temporarily might expand the over-all memory
         * footprint, it properly cleans up after itself.
         *
         ********/
        int f6(char *item1)
        {
            my_class c1;
            int result;
            ...
            c1 = new my_class(item1);
            ...
            result = c1.x;
            delete c1;
            return result;
        }
        /********
         * ...
         * Note that f8() is documented to return a value
         * which needs to be returned to heap; as f7 thinly
         * wraps f8, any code which invokes f7() must be
         * careful to free() the return value.
         *
         ********/
        int *f7()
        {
            int *p;

            p = f8(...);
            ...
            return p;
        }
     

使这些格式元素成为您日常工作的一部分。可以使用各种方法解决内存问题:

专用库
语言
软件工具
硬件检查器
在这整个领域中,我始终认为最有用并且投资回报率最大的是考虑改进源代码的风格。它不需要昂贵的代价或严格的形式;可以始终取消与内存无关的段的注释,但影响内存的定义当然需要显式注释。添加几个简单的单词可使内存结果更清楚,并且内存编程会得到改进。

我没有做受控实验来验证此风格的效果。如果您的经历与我一样,您将发现没有说明资源影响的策略简直无法忍受。这样做很简单,但带来的好处太多了。

检测

检测是编码标准的补充。二者各有裨益,但结合使用效果特别好。机灵的 C 或 C++专业人员甚至可以浏览不熟悉的源代码,并以极低的成本检测内存问题。通过少量的实践和适当的文本搜索,您能够快速验证平衡的 *alloc() 和free() 或者 new 和 delete 的源主体。人工查看此类内容通常会出现像清单 7 中一样的问题。

清单 7. 棘手的内存泄漏

复制内容到剪贴板
代码:

           
        static char *important_pointer = NULL;
        void f9()
        {
            if (!important_pointer)
                important_pointer = malloc(IMPORTANT_SIZE);
            ...
            if (condition)
                    /* Ooops!  We just lost the reference
                       important_pointer already held. */
                important_pointer = malloc(DIFFERENT_SIZE);
            ...
        }
   

如果 condition为真,简单使用自动运行时工具不能检测发生的内存泄漏。仔细进行源分析可以从此类条件推理出证实正确的结论。我重复一下我写的关于风格的内容:尽管大量发布的内存问题描述都强调工具和语言,对于我来说,最大的收获来自“软的”以开发人员为中心的流程变更。您在风格和检测上所做的任何改进都可以帮助您理解由自动化工具产生的诊断。

静态的自动语法分析

当然,并不是只有人类才能读取源代码。您还应使静态语法分析 成为开发流程的一部分。静态语法分析是 lint、严格编译 和几种商业产品执行的内容:扫描编译器接受的源文本和目标项,但这可能是错误的症状。

希望让您的代码无 lint。尽管 lint已过时,并有一定的局限性,但是,没有使用它(或其较高级的后代)的许多程序员犯了很大的错误。通常情况下,您能够编写忽略 lint的优秀的专业质量代码,但努力这样做的结果通常会发生重大错误。其中一些错误影响内存的正确性。与让客户首先发现内存错误的代价相比,即使对这种类别的产品支付最昂贵的许可费也失去了意义。清除源代码。现在,即使 lint 标记的编码可能向您提供所需的功能,但很可能存在更简单的方法,该方法可满足lint,并且比较强键又可移植。

内存库

补救方法的最后两个类别与前三个明显不同。前者是轻量级的;一个人可以容易地理解并实现它们。另一方面,内存库和工具通常具有较高的许可费用,对部分开发人员来说,它们需要进一步完善和调整。有效地使用库和工具的程序员是理解轻量级的静态方法的人员。可用的库和工具给人的印象很深:其作为组的质量很高。但是,即使最优秀的编程人员也可能会被忽略内存管理基本原则的非常任性的编程人员搅乱。据我观察,普通的编程人员在尝试利用内存库和工具进行隔离工作时也只能感到灰心。

由于这些原因,我们催促 C 和 C++ 程序员为解决内存问题先了解一下自己的源。在这完成之后,才去考虑库。

使用几个库能够编写常规的 C 或 C++ 代码,并保证改进内存管理。Jonathan Bartlett 在 developerWorks 的2004评论专栏中介绍了主要的候选项,可以在下面的参考资料部分获得。库可以解决多种不同的内存问题,以致于直接对它们进行比较是非常困难的;这方面的常见主题包括垃圾收集、智能指针 和 智能容器。大体上说,库可以自动进行较多的内存管理,这样程序员可以犯更少的错误。

我对内存库有各种感受。他们在努力工作,但我看到他们在项目中获得的成功比预期要小,尤其在 C 方面。我尚未对这些令人失望的结果进行仔细分析。例如,业绩应该与相应的手动内存管理一样好,但是这是一个灰色区域——尤其在垃圾收集库处理速度缓慢的情况下。通过这方面的实践得出的最明确的结论是,与 C关注的代码组相比,C++ 似乎可以较好地接受智能指针。

内存工具

开发真正基于 C 的应用程序的开发团队需要运行时内存工具作为其开发策略的一部分。已介绍的技术很有价值,而且不可或缺。在您亲自尝试使用内存工具之前,其质量和功能您可能还不了解。

本文主要讨论了基于软件的内存工具。还有硬件内存调试器;在非常特殊的情况下(主要是在使用不支持其他工具的专用主机时)才考虑它们。

市场上的软件内存工具包括专有工具(如 IBM Rational? Purify 和 Electric Fence)和其他开放源代码工具。其中有许多可以很好地与 AIX 和其他操作系统一起使用。

所有内存工具的功能基本相同:构建可执行文件的特定版本(很像在编译时通过使用 -g 标记生成的调试版本)、练习相关应用程序和研究由工具自动生成的报告。请考虑如清单 8 所示的程序。

清单 8. 示例错误

复制内容到剪贴板
代码:

              
        int main()
        {
            char p[5];
            strcpy(p, "Hello, world.");
            puts(p);
   &nbsp ...

没有评论 »