防御性编程【转】

http://www.cnblogs.com/bakari/archive/2012/08/27/2658215.html

本篇是我根据网上的一些陈述经过整理和总结而得。其中详细的内容我会标注出处。看不懂的可以查看原文.

一、什么事防御性编程

详细请见:http://www.uml.org.cn/codeNorms/201007165.asp

防御性编程是一种细致、谨慎的编程方法。为了开发可靠的软件,我们要设计系统中的每个组件,以使其尽可能地“保护”自己。我们通过明确地在代码中对设想进 行检查,击碎了未记录下来的设想。这是一种努力,防止(或至少是观察)我们的代码以将会展现错误行为的方式被调用。

防御性编程是一种编程习惯,是指预见在什么地方可能会出现问题,然后创建一个环境来测试错误,当预见的问题出现的时候通知你,并执行一个你指定的损害控 制动作,如停止程序执行,将用户重指向到一个备份的服务器,或者开启一个你可以用来诊断问题的调试信息。这些防御性编程环境通常的构造方法有:添加声明到代码中,执行按契约进行设计,开发软件防御防火墙,或者简单添加用来验证用户输入的代码。

应用防御性编程技术,你可以侦测到可能被忽略的错误,防止可能会导致灾难性后果的“小毛病”的出现,在时间的运行过程中为你节约大量的调试时间。 防御 性编程使我们可以尽早发现较小的问题,而不是等到它们发展成大的灾难的时候才发现。你常常可以看到“职业”的开发人员不假思索飞快地编写着代码。

他们开发软件的过程可能是这样的:

他们不断地受到那些从未有时间验证的错误的打击。这很难说是现代软件工程的进步,但它却不断地发生着。防御性编程帮助我们从一开始就编写正确的软件,而不再需要经历“编写-尝试-编写-尝试……”的循环过程。

防御性编程的软件开发过程变成:

当然,防御性编程并不能排除所有的程序错误。但是问题所带来的麻烦将会减少,并易于修改。防御性程序员只是抓住飘落的雪花,而不是被埋葬在错误的雪崩中。

防御性编程是一种防卫方式,而不是一种补救形式。我们可以将其与在错误发生之后再来改正错误的调试比较一下。调试就是如何来找到补救的办法。

对防御性编程的误解

关于防御性编程,有一些常见的误解。防御性编程并不是:

检查错误

如果代码中存在可能出现错误的情况,无论如何你都应该检查这些错误。这并不是防御性编码。它只是一种好的做法,是编写正确代码的一部分。

测试

测试你的代码并不是防御,而只是开发工作的另一个典型部分。测试工作不是防御性的,这项工作可以验证代码现在是正确的,但不能保证代码在经历将来的修改之后不会出错。即便是拥有了世界上最好的测试工具,也还是会有人对代码进行更改,并使代码进入过去未测试的状态。

调试

在调试期间,你可以添加一些防御性代码,不过调试是在程序出错之后进行的。防御性编程首先是“防止”程序出错的措施(或在错误以不可理解的方式出现之前发现它们,不然就需要整夜的调试)。

防御性编程真的值得我们来讨论吗?下面是一些支持和反对的意见:

反对意见

防御性编程消耗了程序员和计算机的资源。

— 它降低了代码的效率;即使是很少的额外代码也需要一些额外的执行时间。对于一个函数或一个类,这也许还不要紧,但是如果一个系统由10万个函数组成,问题就变得严重了。

— 每种防御性的做法都需要一些额外的工作。为什么要做这些工作呢?你需要做的已经够多的了,不是吗?只要确保人们正确地使用你的代码就可以了。如果他们使用的方式不正确,那么任何问题也都是他们自己造成的。

支持意见

反驳很有说服力。

— 防御性编程可以节省大量的调试时间,使你可以去做更有意义的事情。还记得墨菲吗:凡是可能会被错误地使用的代码,一定会被错误地使用。

— 编写可以正确运行、只是速度有些慢的代码,要远远好过大多数时间都正常运行、但是有时候会崩溃的代码(显示器闪烁高亮彩色火花)。

— 我们可以设计一些在版本构建中物理移除的防御性代码,以解决性能问题。总之,我们这里所考虑的大部分防御性措施,并不具有任何明显的开销。

— 防御性编程避免了大量的安全性问题,这在现代软件开发中是一个重大的问题。避免这些问题可以带来很多好处。

由于市场要求软件的开发更加快速和廉价,我们就需要致力于实现这一目标的技术。不要跳过眼前的这些额外工作,它们可以防止将来的痛苦和项目延迟。

防御性编程有助于程序的安全性,可以防范诸如此类恶意的滥用。黑客和病毒制造者常常会利用那些不严谨的代码,以控制某个应用程序,然后实施他们蓄意的破坏计划。这对软件开发的现代世界而言,无疑是个严重的威胁;这个问题涉及到诸如生产效率、金钱和个人隐私等方方面面。

软件滥用者形形色色,从利用程序小缺陷的不守规则的用户,到想尽办法非法进入他人系统的职业黑客。有太多的程序员在不经意间为这些人留下了可随意通过的后门。随着网络化计算机的兴起,粗心大意所带来的后果变得愈来愈显著了。

许多大型软件开发公司终于意识到了这种威胁,开始认真思考这个问题,将时间和资源投入到严谨的防御性编码工作中。事实上,在受到恶意进攻之后才亡羊补牢是很困难的。

二、防御性编程的技巧

详见:http://blog.csdn.net/everpenny/article/details/6316698

1、通过采用良好的编程风格,来防范大多数编码错误

如选用有意义的变量名,或者审慎地使用括号,都可以使编码变得更加清晰了,并减少缺陷出现的可能性。在        投入到编码工作之前,先考虑大体的设计方案,这也很关键。

2、不要仓促地写代码

在写每一行时都要三思而后行。可能会出现什么样的错误?你是否已经考虑了所有可能出现的逻辑分支?放慢  速度,有条不紊的编程虽然看上去很平凡,但这的确是减少缺陷的好办法。

3、不要相信任何人

不要相信任何人毫无疑问,任何人(包括你自己)都可能把缺陷引到你的逻辑程序当中,用怀疑的眼光审视所有的输入和所有的结果,直到你能证明它们是正确的为止。

下面这些情况可能是给你带来麻烦的原因:

真正的用户意外地提供了假的输入,或者错误地操作了程序;恶意的用户,故意造成不好的程序行为;客户端代码使用的参数调用了你的函数,或者提供了不一致的输入;运行环境没有为程序提供足够的服务;外部程序库运行失误,不遵从你所依赖的接口协议;

4、编码的目标是清晰,不只是简洁

简单就是一种美,不要让你的代码过于复杂。

5、不要让任何人做让他们不该做的修补工作

将所有变量保持在尽可能小的范围内。不到万不得已,不要声明全局变量。如果变量可以声明为函数内的局部变量,就不要再文件范围上声明。如果变量可以声明为循环体内的局部变量,就不要再函数范围上声明。

6、检查所有的返回值

如果一个函数返回一个值,它这样做肯定是由理由的。大多数难以察觉的错误都是因为程序员没有检查返回值而出现的。无论如何,都要在适当的级别上捕获和处理相应的异常。

7、审慎地处理内存(和其他宝贵的资源)

8 、使用安全的数据结构
     如果你做不到,那么就安全地使用危险的数据结构。
     最常见的安全隐患大概是由缓冲溢出引起的。缓冲溢出是由于不正确地使用固定大小的数据结构而造成的。如   果你的代码在没有检查一个缓冲的大小之前就写入这个缓冲,那么写入的内容总是有可能会超过缓冲的末尾的。
     这种情况很容易出现,如下面这一小段C语言代码所示:

1 char *unsafe_copy(const char *source)
2
3 {
4       char *buffer = new char[10];
5
6       strcpy(buffer, source);
7
8       return buffer;
9 }

如果source中数据的长度超过10个字符,它的副本就会超出buffer所保留内存的末尾。随后,任何事都可能会发生。数据出错是最好情况下的 结果——一些其他数据结构的内容会被覆盖。而在最坏的情况下,恶意用户会利用这个简单的错误,把可执行代码加入到程序堆栈中,并使用它来任意运行他自己的 程序,从而劫持了计算机。这类缺陷常常被系统黑客所利用,后果极其严重。
避免由于这些隐患而受到攻击其实很简单:不要编写这样的糟糕代码!使用更安全的、不允许破坏程序的数据结构——使用类似C++的string类的托管缓冲。或者
对不安全的数据类型系统地使用安全的操作。通过把strcpy更换为有大小限制的字符串复制操作strncpy,就可以使上面的C代码段得到保护。

1 char *safer_copy(const char *source)
2
3 {
4       char *buffer = new char[10];
5
6       strncpy(buffer, source, 10);
7
8       return buffer;
9 }

9、在声明的位置上初始化所有的变量

10、尽可能推迟一些声明变量

使变量的声明位置与使用它的位置尽量接近,从而防止它干扰代码的其他部分。不要再多个地方重用同一个临时变量,变量重用会使以后对代码重新完善的工作变得异常复杂。

11、审慎地进行强制转换

如果你真的想使用强制转换,就必须对之深思熟虑。你所告诉编译器的是:“忘记类型检查吧,我知道这个变量是什么,而你不知道。”你在类型系统中撕开了一个大洞,并直接穿越过去。这样做很不可靠。

12、其他

提供默认的行为

遵从语言的习惯

检查数值的上下限

正确设置常量

三、assert断言

转自:http://wenku.baidu.com/view/3daa77c689eb172ded63b787.html

assert断言是防御性编程经常用到的也是有必要用的手段之一。

assert() 是个定义在 <assert.h> 中的宏, 用来测试断言。一个断言本质上是写下程序员的假设, 如果假设被违反, 那表明有个严重的程序错误。例如, 一个假设只接受非空指针的函数, 可以写:
assert(p != NULL);
一个失败的断言会中断程序。断言不应该用来捕捉意料中的错误, 例如 malloc() 或 fopen() 的失败。

不是用来检查错误的

当程序员刚开始使用断言时,有时会错误地利用断言去检查真正地错误,而不去检查非法的情况。看看在下面的函数strdup中的两个断言:

 1  char* strdup(char* str)
 2  {
 3        char* strNew;
 4        assert(str != NULL);    //ERROR!
 5
 6        strNew = (char*)malloc(strlen(str)+1);
 7        assert(strNew != NULL);
 8
 9        strcpy(strNew, str);
10        return(strNew);
11  }

第一个断言的用法是正确的,因为它被用来检查在该程序正常工作时绝不应该发生的非法情况。第二个断言的用法相当不同,它所测试的是错误情况,是在其最终产品中肯定会出现并且必须对其进行处理的错误情况。

程序一般分为Debug版本和Release版本,Debug版本用于内部调试,Release版本发行给用户使用。
断言assert是仅在Debug版本起作用的宏,它用于检查“不应该”发生的情况。以下是一个内存复制程序,在运行过程中,如果assert的参数为假,那么程序就会中止(一般地还会出现提示对话,说明在什么地方引发了assert)。

 1 //复制不重叠的内存块
 2 void memcpy(void *pvTo, void *pvFrom, size_t size)
 3 {
 4       void *pbTo = (byte *) pvTo;
 5       void *pbFrom = (byte *) pvFrom;
 6
 7       assert( pvTo != NULL && pvFrom != NULL );
 8
 9       while(size - - > 0 )
10       *pbTo + + = *pbFrom + + ;
11
12       return (pvTo);
13 } 

assert不是一个仓促拼凑起来的宏,为了不在程序的Debug版本和Release版本引起差别,assert不应该产生任何副作用。所以assert不是函数,而是宏。程序员可以把assert看成一个在任何系统状态下都可以安全使用的无害测试手段。

以下是使用断言的几个原则:
1)使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。
2)使用断言对函数的参数进行确认。
3)在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?”一旦确定了的假定,就要使用断言对假定进行检查。
4)一般教科书都鼓励程序员们进行防错性的程序设计,但要记住这种编程风格会隐瞒错误。当进行防错性编程时,如果“不可能发生”的事情的确发生了,则要使用断言进行报警。

C++ ASSERT() 断言机制 (转载)

只有在生成DEBUG码时ASSERT()才起作用.
在生成Release码时编译器会跳过ASSERT().

ASSERT()用来检查上面运行结果有无错,例如送返的指针对不对,表达式返回值是不是"假",有错则打出有关信息并退出程序.

ASSERT()是一个调试程序时经常使用的宏,在程序运行时它计算括号内的表达式,如果表达式为FALSE (0),
程序将报告错误,并终止执行。如果表达式不为0,则继续执行后面的语句。这个宏通常原来判断程序中是否出现了明显非法的数据,如果出现了终止程序以免导致
严重后果,同时也便于查找错误。

 1 //原型定义:
 2 #include <assert.h>
 3 void assert( int expression_r_r_r );
 4 /*assert的作用是现计算表达式 expression_r_r_r ,如果其值为假(即为0),那么它先向stderr打印一条出错信息,然后通过调用 abort 来终止程序运行。请看下面的程序清单badptr.c:
 5 */
 6
 7 #include <stdio.h>
 8 #include <assert.h>
 9 #include <stdlib.h>
10 int main( void )
11 {
12       FILE *fp;
13       fp = fopen( "test.txt", "w" );//以可写的方式打开一个文件,如果不存在就创建一个同名文件
14       assert( fp );     //OK
15       fclose( fp );
16
17       fp = fopen( "noexitfile.txt", "r" );//以只读的方式打开一个文件,如果不存在就打开失败
18       assert( fp ); //ERROR!
19       fclose( fp ); //!!!!程序永远都执行不到这里来
20       return 0;
21 }
22 /*[[email protected] error_process]# gcc badptr.c
23  *[[email protected] error_process]# ./a.out
24  *a.out: badptr.c:14: main: Assertion `fp‘‘ failed.
25  *已放弃
26  */

使用assert的缺点是,频繁的调用会极大的影响程序的性能,增加额外的开销。
在调试结束后,可以通过在包含#include <assert.h>的语句之前插入 #define NDEBUG 来禁用assert调用,示例代码如下:

#include <stdio.h>
#define NDEBUG
#include <assert.h>用法总结与注意事项:

1)在函数开始处检验传入参数的合法性
如:

int resetBufferSize(int nNewSize)
{
/*功能:改变缓冲区大小,
 *参数:nNewSize 缓冲区新长度
 *返回值:缓冲区当前长度
 *说明:保持原信息内容不变 nNewSize<=0表示清除缓冲区
 */

     assert(nNewSize >= 0);
     assert(nNewSize <= MAX_BUFFER_SIZE);
     ...
}

时间: 2024-10-12 22:03:17

防御性编程【转】的相关文章

iOS防御性编程

防御性编程是一种细致.谨慎的编程方法.为了开发可靠的软件,我们要设计系统中的每个组件,以使其尽可能地“保护”自己.我们通过明确地在代码中对设想进行检查,击碎了未记录下来的设想.这是一种努力,防止(或至少是观察)我们的代码以将会展现错误行为的方式被调用. 防御性编程是一种编程习惯,是指预见在什么地方可能会出现问题,然后创建一个环境来测试错误,当预见的问题出现的时候通知你,并执行一个你指定的损害控制动作,如停止程序执行,将用户重指向到一个备份的服务器,或者开启一个你可以用来诊断问题的调试信息.这些防

追求代码质量: 用 AOP 进行防御性编程

原文出处: IBM中国 开发人员测试的主要缺点是:绝大部分测试都是在理想的场景中进行的.在这些情况下并不会出现缺陷 —— 能导致出现问题的往往是那些边界情况. 什么是边界情况呢?比方说,把 null 值传入一个并未编写如何处理 null 值的方法中,这就是一种边界情况.大多数开发人员通常都不能成功测试这样的场景,因为这没多大意义.但不管有没有意义,发生了这样的情况,就会抛出一个 NullPointerException,然后整个程序就会崩溃. 本月,我将为您推荐一种多层面的方法,来处理代码中那些

防御性编程习惯:求出链表中倒数第 m 个结点的值及其思想的总结

防御性编程习惯 程序员在编写代码的时候,预料有可能出现问题的地方或者点,然后为这些隐患提前制定预防方案或者措施,比如数据库发生异常之后的回滚,打开某些资源之前,判断图片是否存在,网络断开之后的重连次数或者是否连接备用网络,除法运算中的除数问题,函数或者类在接受数据的时候的过滤情况,比如如果输入一个指针参数,是否需要判断是不是空指针?输入一个字符串参数,是否需要判断字符串空否……总的来说就是防止出现不可预见的事情,设计出鲁棒性的代码. 看下面的例子 输入一个链表,输出链表中倒数第 m 个结点额内容

EAFP和LBYL 两种防御性编程风格

EAFP:Easier to ask for forgiveness than permission 获得事后原理总是比事先得到许可要容易的多. 这个EAFP在python中表现的比较多.EAFP,This common Python coding style assumes the existence of valid keys or attributes and catches exceptions if the assumption proves false. This clean and

防御性编程(转载)

本篇是我根据网上的一些陈述经过整理和总结而得.其中详细的内容我会标注出处.看不懂的可以查看原文. 一.什么事防御性编程 详细请见:http://www.uml.org.cn/codeNorms/201007165.asp 防御性编程是一种细致.谨慎的编程方法.为了开发可靠的软件,我们要设计系统中的每个组件,以使其尽可能地“保护”自己.我们通过明确地在代码中对设想进行检查,击碎了未记录下来的设想.这是一种努力,防止(或至少是观察)我们的代码以将会展现错误行为的方式被调用. 防御性编程是一种编程习惯

[转] LBYL与EAFP两种防御性编程风格

检查数据可以让程序更健壮,用术语来说就是防御性编程.检查数据的时候,有这样的两种不同的风格.LBYL:Look Before You Leap  EAFP:Easier to Ask Forgiveness than Permission LBYL即事先检查.EAFP是不检查,出了问题由异常处理来处理. d = {} words = ['a','d','a','c','b','z','d'] #LBYL for w in words: if w not in d: d[w] = 0 d[w] +

【ThinkingInC++】70、防御性编程

第二章 防御性编程 猜字游戏 /** * 书本:[ThinkingInC++] * 功能:实现一个猜谜的游戏 * 时间:2014年10月8日21:54:44 * 作者:cutter_point */ #include <cstdlib> #include <iostream> #include <string> using namespace std; int main() { cout<<"自己想出来一个数字,在1到100之间的,我来猜猜你的心思

【软件构造】第七章第三节 断言和防御性编程

第七章第三节 断言和防御性编程 本节:第2种技术--断言.防御式编程 Outline 断言 什么是断言 断言的应用场景 防御式编程(不是考点,不加叙述) Notes: ## 断言 [什么是断言] 作用:允许程序在运行时检查自己,测试有关程序逻辑的假设,如前置条件.后置条件.内部不变量.表示不变量.控制流不变量等 目的: 为了在开发阶段调试程序.尽快避免错误 使用阶段: 断言主要用于开发阶段,避免引入和帮助发现bug 实际运行阶段, 不再使用断言 软件发布阶段,禁用断言避免影响性能. [应用场景]

软件构造 第七章第三节 断言和防御性编程

第七章第三节 断言和防御性编程 断言:在开发阶段的代码中嵌入,检验某些"假设"是否成立.若成立,表明程序运行正常,否则表明存在错误. 可用于检查: 内部不变量: 表示不变量: 控制流不变量 方法的前置条件 方法的后置条件 断言主要用于开发阶段,避免引入和帮助发现bug 实际运行阶段, 不再使用断言 软件发布阶段,禁用断言避免影响性能. 断言?Correctness 错误/异常处理?Robustness Defensive Programming 对来自外部的数据源要仔细检查,例如:文件