瑞星卡卡安全论坛技术交流区系统软件 【转载】软件开发人员必备工具书 【代码大全】

«910111213141516   16  /  16  页   跳转

【转载】软件开发人员必备工具书 【代码大全】

继续

附件附件:

下载次数:210
文件类型:application/octet-stream
文件大小:
上传时间:2006-8-25 17:09:28
描述:



gototop
 

真实的数据可能是上述几种类型数据的组合,这时可以用上述几种操作的组合处理复杂的
数据结构。
10 .5  变量功能单一性
    可以通过几种“巧妙”的办法使变量具有一个以上的功能,但最好不要使用这种所谓的巧
妙办法。
    应使每一个变量只具有一个功能。有时人们会试图在两个不同的地方使用同一变量来进行
两个不同的活动。通常,变量名对其中的一个活动来说是不恰当的,或者在两种情况下都充当
了“临时变量”的角色(且是用无意义的X或Temp来命名的)。下面给出了一个用C语言写成的
一个临时变量具有两个功用的例子:
/* Compute roots of a quadratic equation.
第十章变量147
This code assumes that ( b * b - 4 * a * c ) is positive. */

Temp = sqrt(b * b - 4 * a * c);
root[0] = (-b + Temp)/(2 * a);
root[1] = (-b - Temp)/(2 * a);
    ...
    /* swap the roots */

Temp = root[0];
root[0] = root[1];
root[1] = Temp;
    这段程序的问题是:头几行代码中的Temp与后几行代码中的Temp是什么关系呢?事实上它
们之间没有任何关系,可是,这样使用它会使人们误以为它们之间存在着某种联系,从而产生
困惑,以下是对上面程序的改进:
     
/* Compute roots of a quadratic equation.
This code assumes that (b^2 - 4 * a * c) is positive. */

Discriminant = sqrt(b * b - 4 * a * c);
root[0] = (-b + Discriminant)/(2 * a);
root[1] = (-b - Discriminant)/(2 * a);
...
/* swap the roots */
Oldroot = root[0];
root[0] = root[1];
root[1] = Oldroot;
    避免使用具有隐含意义的变量。一个变量具有一个以上功用的另一种情况是同一变量取值
不同时,其代表的意义也不同。如变量PageCount代表是已经打印的页数,但当它等于-l时则表
示发生了错误;变量CustomerID代表的是顾客号码,但当它的值超过500,000时,你要把
CustomerId的值减掉500,00O以得到一个过期未付款的帐号;而BytesWritten一般表示的是某一输
出文件的字节数,但当其值为负时,则表示的是用于输出磁盘驱动器数目。
    应避免使用上述具有隐含意义的变量。技术上把这种滥用称之为“混合关联”。使用同一变
量来承担两项任务意味着对其中一项任务来说变量类型是错误的。比如上例中的PageCount,在
正常情况下代表页数时是整型的,而当它等于-1表示发生了错误时,则是把整型变量当作逻辑
型来用了。
    即使你清楚这种用法,别人也往往很难理解。如果用两个变量来分别承担两项工作的话,
其意义的清晰性将会使你感到满意,并且,也不会在存储上有额外的麻烦。
保证所有说明的变量。与用一个变量来承担两项工作相反的另一个极端是根本不用它,而
研究表明未被使用的变量往往是与出错率高相联系的。因此,要养成确实用到每一个说明变量
的习惯。某些编译程序会对说明但未用到的变量给出警告。
gototop
 

10.6    全局变量
    全局变量在程序的任何地方都可以进行存取。有时它也被非正式地用来指可存取范围大于
局部变量的变量,如在某个单一文件中可以随处存取的模块变量。但是,在单独一个文件中随
处可存取,本身并不能表示某一变量是全局的。
    绝大多数有经验的程序员都认定使用全局变量要比局部变量危险,同时他们也认为利用几
个子程序来存取数据是非常有益的。尽管对使用全局数据的危险性有许多附和的意见,但研究
发现全局变量的使用与错误率上升之间并无联系。
    即使使用全局变量是没有危险的,使用它也决非最好的编程方法。本书其余部分将讨论由
此而引入的问题。
10.6.1  伴随全局变量的常见问题 
    如果不加选择地使用全局变量,或者不使用它们就感到很不方便,那么你很可能还没有充
分意识到信息隐蔽和模块化的好处。模块化和信息隐蔽可能并不是最终真理,但它们对程序的
可读性和维护性的贡献是令其它技术望尘莫及的,一旦你懂得了这一点,你就会使用与全局变
量关系尽可能少的子程序和模块。
对全局数据的疏忽改变。你可以会在某处改变全局变量的值,而在别处又会错误地以为它
仍保持着原值,这就是所谓的副作用,例如在下面的Pascal程序段中,TheAnswer就是个全局变
量:
    TheAnswer := GetTheAnswer; ——TheAnswer是一个全局变量
    OtherAnswer := GetOldAnswer; ——GetOtherAnswer改变了TheAnswer
    AverageAnswer := (TheAnswer + OtherAnswer )/2; ——AverageAnswer是错误的
    你可能以为对GetOtherAnswer的调用并没有改变TheAnswer的值,如果真的是这样的话,那
么第三行中的平均就是错误的。事实上,对GetOtherAnswer的调用,的确改变了TheAnswer的值,
因此程序中有一个需要改正的错误。
伴随全局变量的奇怪的别名问题。“别名”指的是用两个或更多的名称来叫某一变量。当
全局变量被传入子程序,然后又被子程序既用作全局变量又用作参数时,就会产生这种问题。
以下是一个用到全局变量的Pascal子程序: 
Procedure WriteGlobal (VAR InputVar:Integer);
begin
Globa1Var := InputVar + 1;
Writeln( 'InputVariable:', InputVar);
writeln( 'GlobalVariable:', Globe1Var);
end;

    下面是一个把全局变量当作变元来调用上面子程序的程序:
WriteGlobel(GlobalVar);
由于WriteGlobal把InputVar加1后得到了GlobalVar,你会以为GlobalVar要比InputVar大l,但下面
gototop
 

却是足以令你大吃一惊的运行结果:
Input Variable: 2
G1obal Variable: 2
    这里令人难以置信的是:事实上GlobalVar和InputVar是同一个变量,由于GlobalVar是通过
调用子程序被传入WriteGlobal()的,因此它被用两个不同的名字提及了,或者说它是被“别名”
了,从而Writeln()的结果与你想要的便是大相径庭的了。虽然你用两个不同的名字来区分全局
变量,但它们还是将同一变量打印了两次。
    有全局数据的代码重入问题。通过不止一个控制进入代码,已经变得越来越普遍了。在
Microsoft Windows、Apple Macintosh和OS/2 Presentation Manager及递归子程序中都用到了这种
代码。代码重入使得全局变量不仅可能被子程序们分享,而且可能被同一程序的不同拷贝所分
享。在这种环境下,你必须保证即使在某一程序的多个拷贝都在运行的情况下,全局数据仍保
持着它原来的意义。这是一个很有意义的问题,你可以根据后面推荐的技术来避免这一问题。
    全局数据妨碍重新使用的代码。为了从另一个程序中借用某段代码,首先你要从这个程序
中把这段代码取出来,然后把它插入要借用它的程序中。理想的情况是你只需把要用的模块或
单个子程序拿出来放入另一个程序中就可以了。
    但全局变量的引入则使这一过程变得复杂化了。如果你要借用的模块或子程序使用了全局
变量,你就不能简单地把它拿出来再放入另一个新程序了。这时你必须对新程序或旧的代码进
行修改以使得它们是相容的。如果想走捷径的话,那最好对旧代码进行修改,使其不再使用全
局数据。这样作的好处是下次再要借用这段代码时就非常方便了。如果不这样作的话,那你就
需要改动新程序,在其中建立旧有的模块或子程序要用到的全局数据。这时全局变量就像病毒
一样,不仅感染了旧程序,而且随着被借用的旧程序中的模块或子程序传播到了新程序中。
    全局变量会损害模块性和可管理性。开发大于几百行规模软件的一个主要问题便是管理的
复杂性,使其成这可管理的唯一办法便是将程序成为几个部分,以便每次只考虑其中的一个部
分。模块化便是将程序分为几部分的最有力工具之一。
但是全局数据却降低了你进行模块化的能力。如果使用了全局数据,不能做到每次只集中
精力考虑一个子程序吗?当然不能,这时你将不得不同时考虑与它使用了相同全局数据的其余所
有子程序,尽管全局数据并没有摧毁程序的模块性,使它减弱了模块性,仅凭这一点就该避免
使用它。
10.6.2    使用全局数据的理由
    在某些情况下全局数据也是很有用的:
    保存全局数值,有时候需要在整个程序中都要用到某些数据。这些数据可能是反映程序状
态的——在交互式状态时还是批处理状态?是正常状态还是错误修正状态?它们也可能是在整
个程序中都要用到的信息,如程序中每一个子程序都要用到的一个数据表。
    代替命名常量。尽管C、Pascal等绝大多数现代语言都支持命名常量,但仍有一些语言不支
持,这时,可以用全局变量来代替命名常量。例如,你可以通过分别给TRUE和FALSE赋值”1”
和”0”采用它们代替常量型值1和0,或者通过LinesPerPage = 66这一语句把每页行数(66行)
赋给LinesPerPage,从而用LinesPerPage来取代66。通过使用这种方法,可以改善代码的可读性和
gototop
 

易改动性。
方便常用数据的使用。有时候需要非常频繁地使用某一个变量,以至于它几乎出现在每一
个子程序的参数表中,与其在每个子程序的参数表中都将这个变量写一次,倒不如使它成为全
局变量方便。在这种情况下,这一变量几乎是到处都被存取的,不过,很少有这种情况,更多
的情况是它是被一组有限的子程序存取的,这时你可以将这些子程序及数据装入一个模块,这
将在稍后详细讨论。
消除“穿梭”数据。有时候把某个数据传入一个子程序中仅仅是为了使它可以把这一数据
传入另一个子程序中,当这个传递链中的某个子程序并没有用到这个数据时,这个数据就被叫
做“穿梭数据”。使用全局变量可以消除这一现象。
10.6.3  怎样降低使用全局数据的危险
你可能会认为下面这些准则让人感到很不自由也很不舒服,或许的确是这样,但我想你一
定知道“良药苦口利于病”这句话吧?
先使所有变量都成为局部的,然后再根据需要把其中某一些改为全局变量。首先使所有变
量针对单个子程序来说都是局部的。如果发现还需要在别的地方使用它,那么在使它成为全局
变量之前应先使它成为模块变量。如果最后发现必须使它成为全局变量,你可以这样作,但必
须确认这是迫不得已的。如果开始就使某一变量成为全局的,那么你决不会再把它变成局部的,
而很可能如果开始把它用成局部的话,你永远也不会再把它变为全局的。
区分全局和模块变量。如果某些变量是在整个程序中存取的,那么它们就是真正的全局变
量。而某些变量只在一组子程序中存取,事实上是模块变量。在指定的那组子程序中,对模块
进行任何存取操作都是可以的,如果其它子程序要使用它的话,应通过存取子程序来进行。即
使是程序语言允许,也不要像对待全局变量那样对模块变量进行存取操作。要在自己的耳边不
停地说“模块化!模块化!模块化!”。
建立使你一眼即可识别出全局变量的命名约定。如果使用全局变量的操作是十分明显的,
可以避免许多错误。如果不只是出于一个目的使用全局变量(如既作为变量又用替换命名常量),
那一定要保证命名约定可以区分这些不同目的。
建立一个清楚标出所有全局变量的注释表。建立标识全局变量的命名约定,对表明变量的
功用是非常有帮助的,而一个标有所有全局变量的注释表则是读你程序的人最强有力的辅助具
之一(Glass 1982)。
如果你用的是Fortran 语言,那么仅使用标号COMMON 语句,不要使用空白COMMON。
空白COMMON 可以使任意一个子程序存取任意一个变量。使用命名的COMMON 来详细规定
可以存取特定COMMON数据的特定子程序,这是一种在Fortran 中模拟使用模块数据的方法。
研究表明这种方法是很有效的。
使用加锁技术来控制对全局变量的存取。与多用户数据库环境下,当前值的控制方式类似,
在全局变量被使用或更新之前锁定要求,这个变量必须被“登记借出”,在变量被使用过之后,
再被“登记归还”,在它不能被使用期间(已经被登记借出),如果程序其它部分企图要求将它
登记借出,那么加锁/开锁子程序将打印出错误信息。
加锁技术在开发阶段是有用的。当程序最终成为产品时,程序应该被改动来做比打印更有
意义的工作,使用存取子程序来存取全局变量的好处之一,就是使你可以方便地实现加锁/开
gototop
 

锁技术,而如果不加限制地存取全局变量的话,就很难实现这一技术。
不要通过把数据放人庞大的变量,同时又到处传递它来掩盖你使用了全局变量的事实。把
什么都放入一个巨大的结构,可能从字面上满足了避免使用全局变量这准则,但这只是表面文
章,事实上这样做,得不到任何真正模块化的好处,如果你使用了全局变量的话,那就分开使
用它,不要企图用臃肿的数据结构来掩盖它。
10.6.4  用存取子程序来代替全局数据
用全局数据能作的一切,都可以通过使用存取子程序来做得更好,存取子程序是建立在抽
象数据类型和信息隐蔽的基础上的。即使不愿使用纯粹的抽象数据类型,仍然可以通过使用存
取子程序来对数据进行集中控制并减少因改动对程序的影响。
存取子程序的优点
以下是使用存取子程序的优点:
· 可以对数据进行集中控制。如果你以后又找到了更合适的数据结构,那么不必在每一处涉
及到数据的地方都进行修改,而只修改存取子程序就可以了,修改的影响可以被限制在存
取子程序内部。
· 可以把所有对数据的引用分隔开来,从而防止因其错误造成的影响蔓延。使用类似
Stack.array[stack.top]new_element的语句来把元素压入堆栈时,很容易忘记检查堆栈是否溢
出从而造成错误。如果使用存取子程序,例如push stack( new_element ),就可以把检查堆
栈是否溢出的代码写入push_stack()子程序,这样每次调用子程序都可以对堆栈自动进行检
查,而你则可以不必再考虑堆栈溢出问题。
· 你可以自动获得信息隐蔽带来的好处。即使你不是为了信息隐蔽才使用存取子程序的,它
也是信息隐蔽的范例性技术之一。你可以改变存取子程序的内部而不必改动程序的其余部
分。打个比方,存取子程序使你可以改变房屋的内部陈设而不会变动房屋的外观,这样你
仍然可以很容易便找着你的家。
· 存取子程序很容易转换为抽象数据类型。存取子程序的一个优点是:它可以得到很高的抽
象级,而直接存取全局变量却难以做到。例如,你可以用存取子程序ifPageFull()来代替
语句if lineCount > Maxlines,虽然这是个很小的收益,但是大量的这类差别便积聚成了高
质量软件与东拼西凑到一起的软件之间的不同之处。
怎样使用存取子程序
以下是关于存取子程序理论与应用简短论述:将数据隐含在模块中,编写可以使你访问并
修改数据的子程序,数据所在模块之外的子程序要求存取数据时,应让它通过存取子程序而不
直接地存取模块内的数据。比如,假设有一个状态变量你可以通过两个存取子程序Getstatus()
和SetStatus()来对其进行存取操作。
以下是关于存取子程序使用的几条较为详细的准则:
要求所有子程序来对数据进行存取操作。通过存取子程序将数据结构隐含起来。通常需要
两个子程序,一个读取数据的值,而另一个用于赋给它新值。除去几个可以直接存取数据的服
务性子程序,其它子程序都应通过存取子程序接口来对数据进行存取。
gototop
 

不要把所有的全局数据都放入同一个模块中。如果你把所有的全局数据都归成一个大堆,
并编写对其存取的子程序,的确可以消除由全局数据带来的问题,但同时也抛掉了信息隐蔽和
抽象数据类型所带来的优点。编写存取子程序之前,应先考虑一下每一全局数据应属于哪一个
模块,然后把这个全局数据、相应的存取子程序和其关联的子程序放入那个模块中。
在存取子程序中建立某种程度的抽象。在数据所代表的意义层次上而不是计算机本身的实
现细节层次上建立存取子程序,可以使你更容易应付可能的变动。
请比较下列两组语句:
直接使用复杂数据通过存取子程序使用复杂数据
node=node.next  node=nearestNeighbor(node)
node=node.next node=nextEmployee(node)
node=node.next node=nextRatele(node)
Event=EventQueue[QueueFront] Event=HighestprioirtyEvent( )
Event=EventQueue[QueueFront] Event=LowesPriotityEvent( )
表中前三个语句对中,抽象的存取子程序告诉你的信息要比数据结构所告诉你的多得多。
如果直接使用数据结构,那么一次需要做工作就太多了。你必须在表示出数据结构本身正在做
什么(移到链表中的下一个链)的同时,表示出正在对数据结构所代表的实体做什么(调用一
个邻居、下一个雇员或税率),这对于简单数据结构来说是很重的负担。把信息隐蔽在存取子程
序后面,可以使代码自己指出这些,并且可以使得其它人从问题域而不是实现细节的层次上来
阅读程序。
把对数据的所有存取保持在同一抽象水平上。如果你用了某一存取子程序对某一数据进行
了一项操作,那么对这一数据的其它操作也应通过存取子程序来实现。比如若是通过存取子程
序来从数据结构中读取数值的,那么对该数据结构的写操作也应通过存取子程序来实现。又比
如假设你通过调用initstack()子程序将元素压入堆栈,但你接着又用value=array(strack.top)来
获得堆栈的一个入口,那么此时对数据的观察点便不连续了。这种不连续性使别人很难理解你
的程序。因此,要保持对数据所有存取操作抽象水平的一致性。
在上表中的后两个语句对中,两个事件排队的操作是平行进行的。在队列中插入一个事件
将比表中其它操作都更复杂,因为你不得不改变队列的前后顺序,调整现存事件以便为它腾出
空间,再写几行代码以便找到插入它的地方等等,从一个序列中移出一个事件几乎是同样麻烦
的。因此,在编码时如果把复杂操作放入子程序,而其余操作直接对数据进行,将产生对数据
结构拙劣的、非平等的使用。请比较下面的两组语句:
对复杂数据的非平行使用对复杂数据的平行使用
对复杂数据的非平行使用对复杂数据的平行使用
Event=EventQueue[QueueFront] Event=HightestPriorityEvent( )
Event=EventQueue[QueueBack] Event=LowestPriorityEvent( )
AddEvent(Event) AddEvent(Event)
EventCount=EventCount-1 RemoveEvent(Event)
应注意这些准则适用许多模块和子程序构成的大型程序。在小一些的程序中,存取子程序
gototop
 

的地位也会相应降低。但不管怎样,实践已经证明,存取子程序是增强程序灵活性并避免由全
局变量带来问题的有效手段之一。
10.6.5  检查表
使用数据时通常要考虑的一些问题
一般数据
· 是否已经使变量的作用域尽可能地小?
· 是否把对变量的使用集中到了一起?
· 控制结构与数据结构是相对应的吗?
· 每个变量是否有且仅有一个功能?
· 每个变量的含义都是明确的吗?是否保证了每个变量都没有隐含的意义?
· 每一个说明过的变量都被用到了吗?
全局变量
· 是否是在迫不得已的情况下,才使某些变量成为全局的?
· 命名约定是否对局部、模块和全局变量进行了区分?
· 是否说明了所有全局变量?
· 程序中是否不含有伪全局变量——传往各个子程序的庞大而臃肿的数据结构?
· 是否用存取子程序来代替了全局数据?
· 是把存取子程序和数据组织成模块而不是随意归成一堆的吗?
· 存取子程序的抽象层次是否超过了程序语言实现细节?
· 所有相互有联系的存取子程序,其抽象程度都是一致的吗?
10.7  小结
· 尽量减小变量的作用域。把对变量引用集中到一起,应尽量使变量成为局部或模块的,
避免使用全局变量。
· 使每个变量有且仅有一个功能。
· 并不是因为全局数据危险才避免使用它们,之所以避免用它是因为可以用更好的技术
来代替它。
· 如果全局数据确实不可避免的话,应通过存取子程序来对其进行存取操作。存取子程
序不仅具备全局变量和全部功能,而且可以提供更多的功能。
gototop
 
«910111213141516   16  /  16  页   跳转
页面顶部
Powered by Discuz!NT