文档开发者中心
我的

【综合】【码手防御-7】程序界面(2)

作者: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、 函数是否清清楚楚地做了一件事并且只做了一件?

再回味下开篇那段话:

在造墙之前,我必须设法弄清,

该把什么放在墙里,什么放在墙外,

最需要防御的又是什么。

确实有些东西不喜欢墙,

总希望它倒下来。


点赞
1
收藏
1
回复
19
分享
举报
浏览1826 发布于2019-11-26 05:49未知归属地
全部评论
最多点赞
最新发布
最早发布

这是要火的节奏啊!!!!

19楼回复于2020-04-14 03:05 未知归属地

很不错的帖子

18楼回复于2019-11-27 08:18 未知归属地

给大神点赞

17楼回复于2019-11-26 12:11 未知归属地

不错不错,膜拜大神

16楼回复于2019-11-26 12:10 未知归属地

很不错的文章

15楼回复于2019-11-26 12:07 未知归属地

真心不错

14楼回复于2019-11-26 12:05 未知归属地

转走了转走了

13楼回复于2019-11-26 12:03 未知归属地

这一系列帖子都很棒呢

12楼回复于2019-11-26 12:03 未知归属地

大家多多分享呀

11楼回复于2019-11-26 08:47 未知归属地

很好的帖子呦

10楼回复于2019-11-26 08:47 未知归属地
共2页
写回答
新增插入模板功能
一键使用模板,快速填写内容,轻松发帖~
知道了
  • 为了保障您的信息安全,请勿上传您的敏感个人信息(如您的密码等信息)和您的敏感资产信息(如关键源代码、签名私钥、调试安装包、业务日志等信息),且您需自行承担由此产生的信息泄露等安全风险。
  • 如您发布的内容为转载内容,请注明内容来源。

我要发帖子

了解社区公约,与您携手共创和谐专业的开发者社区。