作者:HW-HM
接【码手防御-7】程序界面(1)
第二次执行时,只输出了a,意外吧?因为strtok会在在输入串里面写进空字节,以表示一个标记的结束。同时我们注意到while循环中的strtok调用传入的指针为空,这意味着它在不同调用之间保留了一些秘密数据,这个情况很可能成为程序错误的原因,也排除了并行使用这个函数的可能性。
要使用户不容易忽视错误情况。有异常机制的函数,可以通过抛出异常来使调用者必须处理对应的错误。而对于C语言这样无异常机制的函数,不要在正常地返回值中隐藏错误代码。以C语言的getchar说明:
char c;
c = getchar();
if( c == EOF )
……
如上的代码在不进行符号扩展的机器上,c总是正数因为它是char类型而EOF却是负数,结果上面的测试条件总会失败。为了避免这一点,必须用int而不用char来保存getchar返回值的变量,这与我们的直觉getchar应该返回char很不一致,要求我们倍加小心。如果修改成如下的界面,就不容易出现这样的使用错误了。
bool fGetChar(char* pch);
通过这一界面我们可以很自然地写出:
chat ch;
if( fGetChar( &ch ) )
ch中是下一个字符
else
碰到了EOF,ch中是无用信息
当设计函数的界面时,要选择使程序员第一次就能够写出正确代码的设计。不要使用引起混淆的双重意义的返回值,每个输出应该只代表一种数据类型,要在设计中显式地体现出这些要点,使用户很难忽视这些重要的细节。
不要模棱两可,要明确地定义函数的参数,消除未定义的行为。以一个复制子串的函数说明,它从strFrom开始取size个字符,并把它们存储到从strTo开始的字符串中:
char* CopySubStr( char* strTo, char* strFrom, size_t size )
{
char* strStart = strTo;
while(size-- > 0)
strTo++ = strFrom++;
*strTo=‘\0’;
return(strStart);
}
假设调用“CopySubStr(strTo, 'I am handsome', 55)”,想截取am后的字符串,输入5时错误的输入了55,这时程序就越界写了。你可以说调用者没满足契约,该函数在这种情况下的行为是未定义的,但这无助于排除这样的错误,好的实现是明确要求size小于源串的长度,使用assert进行确认。
char* CopySubStr(char strTo, strFrom, size_t size)
{
char* strStart = strTo;
assert( strTo != NULL && strFrom != NULL );
assert( size <= strlen(strFrom) );
while( size-- > 0 )
strTo++ = strFrom++;
*strTo=‘\0’;
return (strStart );
}
编写函数使其在给定有效的输入情况下不会失败。如果函数返回错误代码,那么该函数的每个调用者都必须对该错误进行处理。如果发现自己在设计函数时要返回一个错误代码,那么要先停下来问自己:是否还有其它的设计方法可以不用返回该错误情况;如果发现无法消除错误的情况,那么可以考虑干脆不允许这些有问题的情况出现,即用断言对函数的输入进行验证。前一篇博客已经有对应的例子了。
一个界面在使用时不应该强求另外的东西,应该使界面成为自给自足的。不能为了设计者或实现者的某些方便,让调用者依赖不相关的东西。以头文件包含说明,我们在这方面做的很不好。调用者要使用一个函数或类,只需要包含对应的头文件就可以了,不能因为包含了这个头文件而导致递归包含进无关的头文件。
在各处都用同样方式做同样的事。一致性和规范性是非常重要的,相关的事物应该具有相关的意义。譬如流操作相关函数,都把流作为第一个参数;而字符串操作函数,目的字符串始终为第一个参数,如果有源字符串,则为第二个参数。
释放资源与分配资源应该在同一个层次进行。资源的分配状态在跨过界面时不应该改变。如果一个函数进入时,流是打开的,那么退出时,流仍然是打开的。
在低层检查错误,在高层处理它们。错误应该在尽可能低的层次上检测和发现,但应该在某个高一些的层次上处理。这里的高层是指有足够上下文信息进行处理的层。
使程序在调用点明了易懂。要避免布尔参数,事实上,布尔参数常常表明相应的函数可以做两种不同的事情,用布尔参数来选择想要做的事情;也可以很灵活地不只限于两种不同的功能,但程序员使用布尔值来指明唯一感兴趣的两种情况。如下的函数将整数转换未字符串,如果isDecimal为真,则按照10进制转换,如果为假,则按照16进制转换。
void UnsignedToStr(unsigned int val, char* str, bool isDecimal);
如此调用“UnsignedToStr(a, str, true)”,你能看出是要做什么吗?
我们可以通过定义宏来解决。
在UnsignedToStr头文件中
#define DECIMAL true
#define HEXADECIMAL false
UnsignedToStr(a, str, DECIMAL);
当然我们也可以修改函数的签名,改成支持各种进制的转换。
#define DECIMAL 10
#define HEXADECIMAL 16
void UnsignedToStr(unsigned u, char* str, unsigned base);
编写注解突出可能的异常情况。如果getchar的函数头有这样的注释,就可以减少误用的可能性。
/* getchar ─── 等价于getc(stdin)
* getchar从stdin返回了一个字符,当发生了
* 错误时,它返回“int”EOF。该函数的一种
* 典型用法是:
* int ch; // 为了容纳EOF,ch必须是int 类型
* if( (ch=getchar()) != EOF )
* 成功 ─── ch是下一个字符
* else
* 失败 ─── ferror(stdin)将给出错误的类型
*/
进行界面设计时要遵循两个设计原则:
接口隔离原则:使用多个专门的接口比使用单一的总接口要好。也就是说,一个类对另外一个类的依赖性应当是建立在最小的接口上,不应该强迫客户依赖于他们不使用的方法。接口越简单越清晰越好。
最少知识原则:一个软件实体应当尽可能少的与其他实体发生相互作用。让调用者知道得越少越好。
该结束了。在设计程序界面时,问问自己:
1、 接口里的这些东西都是调用者要关心的吗?
2、 调用者会如何误用你的接口,应该如何避免被误用?
3、 接口中需要调用者关注的东西,是否足够引起关注?
4、 函数是否清清楚楚地做了一件事并且只做了一件?
再回味下开篇那段话:
在造墙之前,我必须设法弄清,
该把什么放在墙里,什么放在墙外,
最需要防御的又是什么。
确实有些东西不喜欢墙,
总希望它倒下来。






































苏公安网备 32011402010933号