跳至主要内容

【转】strcpy如何实现

什么是契约――Eiffel的观点

    假设你现在正在面试,主考不紧不慢地给出下一道题目:"请用C语言写一个类似strcpy的函数。要考虑可能发生的异常情况。" 你会怎么做呢?很明显,对方不是在考察你的编程能力,因为复制字符串实在太容易了。对方是在考察你的编程风格(习惯),或者说,要看看你编码的质量。

    下面是多种可能的做法:

    void
    string_copy1(char* dest, const char* source)
    {
      assert(dest != NULL); /* 使用断言 */
      assert(source != NULL);
     
      while (*source != '\0') {
        *dest = *source;
        ++dest;
        ++source;
      }

      *dest = '\0';
    }

    void
    string_copy2(char* dest, const char* source)
    {
      if (dest != NULL && source != NULL) {  /* 对错误消极静默 */
        while (*source != '\0') {
          *dest = *source;
          ++dest;
          ++source;
        }

        *dest = '\0';
      }
    }

    int
    string_copy3(char* dest, const char* source)
    {
      if (dest != NULL && source != NULL) {
        while (*source != '\0') {
          *dest = *source;
          ++dest;
          ++source;
        }

        *dest = '\0';
        return SUCCESS;  /* 返回表示正确的值 */
      }                         
      else {
       errno = E_INVALIDARG;  /* 设定错误号 */
       return FAILED;         /*  返回表示错误的值 */
      }
    }
   
    // C++
    void
    string_copy4(char* dest, const char* source)
    {
       if (dest == NULL || source == NULL)
         throw Invalid_Argument_Error();  /*  抛出异常 */

       while (*source != '\0') {
         *dest = *source;
         ++dest;
         ++source;
       }

       *dest = '\0';
    }

    如果你是主考,不知道面对这样四份答卷,你的评分如何?当然,你可以心里揣着一个"标准答案","顺我者昌,逆我者亡"。但是如果以认真的态度面对这四份答卷,我想很多人都会难以抉择。

    因为这里涉及到了软件开发中的一个带有本质性的难题――错误处理。

    历来错误处理一直是软件开发者所面临的最大困难之一。Bjarne Stroustrup在谈到其原因时说道,能够探察错误的一方不知道如何处理错误,知道如何处理错误的一方没有能力探察错误,而直接采用防御性代码来解决,会使得程序的正常结构被打乱,从而带来更多的错误。这种困境是非常难以应对的――费心耗力而未必有回报。因此,更多的人采用鸵鸟战术,对可能发生的错误视而不见,任其自然。

    C++、Java和其他语言对错误处理问题的回答是异常机制。这种机制在正常的程序执行流之外开辟了专门的信道,专门用来在不同程序模块之间报告错误,解决上述错误探察与处理策略分散的矛盾。然而,有了异常处理机制后,开发者开始有一种倾向,就是使用异常来处理所有的错误。我曾经就这个问题在comp.lang.c++.moderated上展开讨论,结果是发现有相当多的人,包括Boost开发组里的很多专家,都认为异常是错误处理的通用解决方案。

    对此我不能赞同。并且我认为滥用异常比不用异常的危害更大。

    The Pragmatic Programmer是一本在国外程序员中间颇为流行的书,其中在讲到错误处理时,有一句箴言:
   
    "只在真正异常的状况下使用异常。"

    书中举了一个例子,如果你需要当前目录下的一个名叫"app.dat"的文件,而这个文件不存在,那么这不叫异常状况,这是你应该预料到的、并且显式处理的情况。而如果你要到Windows目录下寻找user.dat文件,却没找到,那才叫做异常状况――因为每一个正常运行的Windows系统都应该有这个文件。

    我非常赞成书中的那句忠告,可是究竟什么是"真正异常"的状况?书中的这个例子显然只是一个颇具感性的、寓言似的故事,具有所有寓言的共同特点――读起来觉得豁然开朗,收获很大,实际上帮不了你什么忙。这种例子对于我们的实际开发,仍然提供不了真正的帮助。

    究竟应该如何看待错误?怎样才能最好地错误处理?

    说实话,在这两个问题上,我们所见到的大部分语言都没有给出很好的回答。C秉承一贯风格,把所有的东西推给开发者考虑;Ada发明了异常,但是又为异常所累(知道阿里亚纳5火箭的处女航为什么失败吗?);C++企图将Ada的异常机制融合进自己的体系中,结果异常成了C++中最难以处理的东西;Java和C#显然都没有耐心重新考虑错误处理这桩事,而只是简单的将C++的异常机制完善化了事。

    与上述这些语言不同,Eiffel从一开始就把错误处理放在核心的位置上予以考虑,并以"契约"思想为核心,建立了整个的错误处理思想体系。在我了解的语言里,Eiffel是对这个问题思考最为深刻一个,因此,Eiffel历来享有"高质量系统开发语言"的声誉。(事实上,Bertrand Meyer很不喜欢别人称Eiffel为"编程语言",他反复强调,Eiffel是一个Software Development Framework。不过本文只涉及语言特性,所以姑且称Eiffel语言。)

    Eiffel把软件错误产生的本质归结与"契约"的破坏。Eiffel认为,一个软件能够正常运作,正确完成任务,是需要一系列条件的。这些条件包括客观运行环境良好,操作者操作正确,软件内部功能正确等等。因此,软件的正确运行,是环境、操作者与软件本身三方面合作的结果。相应的,系统的错误,也是由于三者中有一方没有正确履行自己的职责而导致的。细化到软件内部,每个软件都是由若干不同的模块组成的,软件的错误,是由于某些模块没有正确履行自己的职责。要彻底杜绝软件错误,只有分清各自模块的责任,并且建立机制,敦促各模块正确履行自己的责任,然后才有可能做到Bug-free。(鉴于系统中错综复杂的关系,以及开发者认识能力的局限,我认为真正无错误的系统是不可能的。但是当前一般软件系统中的质量问题远远比应有的严重。)

    如何保证各方恪守职责呢?Eiffel引入了契约(Contract)这个概念。这里的契约与我们通常所说的商业契约很相似,有以下几个特点:

1. 契约关系的双方是平等的,对整个bussiness的顺利进行负有共同责任,没有哪一方可以只享有权利而不承担义务。
2. 契约关系经常是相互的,权利和义务之间往往是互相捆绑在一起的;
3. 执行契约的义务在我,而核查契约的权力在人;
4. 我的义务保障的是你的利益,而你的义务保障的是我的利益;
 
    将契约关系引入到软件开发领域,尤其是面向对象领域之后,在观念上给我们带来了几大冲击:

1. 一般的观点,在软件体系中,程序库和组件库被类比为server,而使用程序库、组件库的程序被视为client。根据这种C/S关系,我们往往对库程序和组件的质量提出很严苛的要求,强迫它们承担本不应该由它们来承担的责任,而过分纵容client一方,甚至要求库程序去处理明显由于client错误造成的困境。客观上导致程序库和组件库的设计和编写异常困难,而且质量隐患反而更多;同时client一方代码大多松散随意,质量低劣。这种情形,就好像在一个权责不清的企业里,必然会养一批尸位素餐的混混,苦一批任劳任怨,不计得失的老黄牛。引入契约观念之后,这种C/S关系被打破,大家都是平等的,你需要我正确提供服务,那么你必须满足我提出的条件,否则我没有义务"排除万难"地保证完成任务。

2. 一般认为在模块中检查错误状况并且上报,是模块本身的义务。而在契约体制下,对于契约的检查并非义务,实际上是在履行权利。一个义务,一个权利,差别极大。例如上面的代码:
    if (dest == NULL) { ... }
这就是义务,其要点在于,一旦条件不满足,我方(义务方)必须负责以合适手法处理这尴尬局面,或者返回错误值,或者抛出异常。而:
    assert(dest != NULL);
这是检查契约,履行权利。如果条件不满足,那么错误在对方而不在我,我可以立刻"撕毁合同",罢工了事,无需做任何多余动作。这无疑可以大大简化程序库和组件库的开发。

3. 契约所核查的,是"为保证正确性所必须满足的条件",因此,当契约被破坏时,只表明一件事:软件系统中有bug。其意义是说,某些条件在到达我这里时,必须已经确保为"真"。谁来确保?应该是系统中的其他模块在先期确保。如果在我这里发现契约没有被遵守,那么表明系统中其他模块没有正确履行自己的义务。就拿上面提到的"打开文件"的例子来说,如果有一个模块需要一个FILE*,而在契约检查中发现该指针为NULL,则意味着有一个模块没有履行其义务,即"检查文件是否存在,确保文件以正确模式打开,并且保证指针的正确性"。因此,当契约检查失败时,我们首先要知道这意味着程序员错误,而且要做的不是纠正契约核查方,而是纠正契约提供方。换句话说,当你发现:
     assert(dest != NULL);
报错时,你要做的不是去修改你的string_copy函数,而是要让任何代码在调用string_copy时确保dest指针不为空。


4. 我们以往对待"过程"或"函数"的理解是:完成某个计算任务的过程,这一看法只强调了其目标,没有强调其条件。在这种理解下,我们对于exception的理解非常模糊和宽泛:只要是无法完成这个计算过程,均可被视为异常,也不管是我自己的原因,还是其他人的原因(典型的权责不清)。正是因为这种模糊和宽泛,"究竟什么时候应该抛出异常"成为没有人能回答的问题。而引入契约之后,"过程"和"函数"被定义为:完成契约的过程。基于契约的相互性,如果这个契约的失败是因为其他模块未能履行契约,本过程只需报告,无需以任何其他方式做出反应。而真正的异常状况是"对方完全满足了契约,而我依然未能如约完成任务"的情形。这样以来,我们就给"异常"下了一个清晰、可行的定义。

5. 一般来说,在面向对象技术中,我们认为"接口"是唯一重要的东西,接口定义了组件,接口确定了系统,接口是面向对象中我们唯一需要关心的东西,接口不仅是必要的,而且是充分的。然而,契约观念提醒我们,仅仅有接口还不充分,仅仅通过接口还不足以传达足够的信息,为了正确使用接口,必须考虑契约。只有考虑契约,才可能实现面向对象的目标:可靠性、可扩展性和可复用性。反过来,"没有契约的复用根本就是瞎胡闹。(Bertrand Meyer语)"。

    由上述观点可以看出,虽然Eiffel所倡导的Design By Contract在表象上不过是系统化的断言(assertion)机制,然而在背后,确实是完全的思想革新。正如Ivar Jacoboson访华时对《程序员》杂志所说:"我认为Bertrand Meyer的方向――Design by Contract――是正确的方向,我们都会沿着他的足迹前进。我相信,大型厂商(微软、IBM,当然还有Rational)都不会对Bertrand Meyer的成就坐视不理。所有这些厂商都会在这个方向上有所行动。"(参见《程序员》2002年第11期,P22)。

 

续篇-契约思想的一个反面案例   myan(原作)   

刚刚发表了《什么是契约》一文,突然发现自己通篇都在写理论,没有实例来证明。所以赶快补充一个反面案例――C++ IOStream。说是反面,不是因为IOStream库设计得不精彩(恰恰相反,你很难找到比IOStream设计更为精彩的C++库了),而是想展示一下,在没有契约概念的思想体系里,组件设计将为权责不清的错误处理付出多大的代价。

 

作者Blog:http://blog.csdn.net/myan/

评论

此博客中的热门博文

【转】VxWorks中的地址映射

在运用嵌入式系统VxWorks和MPC860进行通信系统设计开发时,会遇到一个映射地址不能访问的问题。 缺省情况下,VxWorks系统已经进行了如下地址的映射:   memory地址、bcsr(Board Control and Status)地址、PC_BASE_ADRS(PCMCIA)地址、Internal Memory地址、rom(Flach memory)地址等,但是当你的硬件开发中要加上别的外设时,如(falsh、dsp、FPGA等),对这些外设的访问也是通过地址形式进行读写,如果你没有加相应的地址映射,那么是无法访问这些外设的。   和VxWorks缺省地址映射类似,你也可以进行相应的地址映射。   如下是地址映射原理及实现:   1、 地址映射结构 在Tornado\target\h\vmLib.h文件中 typedef struct phys_mem_desc { void *virtualAddr; void *physicalAddr; UINT len; UINT initialStateMask; /* mask parameter to vmStateSet */ UINT initialState; /* state parameter to vmStateSet */ } PHYS_MEM_DESC; virtualAddr:你要映射的虚拟地址 physicalAddr:硬件设计时定义的实际物理地址 len;要进行映射的地址长度 initialStateMask:可以初始化的地址状态: 有如下状态: #define VM_STATE_MASK_VALID 0x03 #define VM_STATE_MASK_WRITABLE 0x0c #define VM_STATE_MASK_CACHEABLE 0x30 #define VM_STATE_MASK_MEM_COHERENCY 0x40 #define VM_STATE_MASK_GUARDED 0x80 不同的CPU芯片类型还有其特殊状态 initialState:实际初始化的地址状态: 有如下状态: #define VM_STATE_VALID 0x01 #define VM_STATE_VALID_NOT 0x00 #define VM_STATE_WRITA

【转】多迷人Gtkmm啊

前边已经说过用glade设计界面然后动态装载,接下来再来看看怎么改变程序的皮肤(主题)     首先从 http://art.gnome.org/themes/gtk2 下载喜欢的主题,从压缩包里提取gtk-2.0文件夹让它和我们下边代码生成的可执行文件放在同一个目录下,这里我下载的的 http://art.gnome.org/download/themes/gtk2/1317/GTK2-CillopMidnite.tar.gz     然后用glade设计界面,命名为main.glade,一会让它和我们下边代码生成的可执行程序放在同一个目录下边     然后开始写代码如下: //main.cc #include <gtkmm.h> #include <libglademm/xml.h> int main(int argc, char *argv[]) {     Gtk::Main kit(argc,argv);         Gtk::Window *pWnd;        gtk_rc_parse("E:\\theme-viewer\\themes\\gtk-2.0\\gtkrc");       Glib::RefPtr<Gnome::Glade::Xml> refXml;     try     {         refXml = Gnome::Glade::Xml::create("main.glade");     }     catch(const Gnome::Glade::XmlError& ex)     {         Gtk::MessageDialog dialog("Load glade file failed!", false,       \                                   Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK);         dialog.run();               return 1;     }         refXml->get_widget("main", pWnd);     if(pW

【转】https客户端的实现(libcurl)

一、              概念 1.         为什么要使用libcurl 1)        作为http的客户端,可以直接用socket连接服务器,然后对到的数据进行http解析,但要分析协议头,实现代理…这样太麻烦了。 2)        libcurl是一个开源的客户端url传输库,支持FTP,FTPS,TFTP,HTTP,HTTPS,GOPHER,TELNET,DICT,FILE和LDAP,支持Windows,Unix,Linux等平台,简单易用,且库文件占用空间不到200K 2.         get和post方式 客户端在http连接时向服务提交数据的方式分为get和post两种 1)        Get方式将所要传输的数据附在网址后面,然后一起送达服务器,它的优点是效率比较高;缺点是安全性差、数据不超过1024个字符、必须是7位的ASCII编码;查询时经常用此方法。 2)        Post通过Http post处理发送数据,它的优点是安全性较强、支持数据量大、支持字符多;缺点是效率相对低;编辑修改时多使用此方法。 3.         cookie与session 1)        cookie cookie是发送到客户浏览器的文本串句柄,并保存在客户机硬盘上,可以用来在某个Web站点会话之间持久地保持数据。cookie在客户端。 2)        session session是访问者从到达某个特定主页到离开为止的那段时间。每一访问者都会单独获得一个session,实现站点多个用户之间在所有页面中共享信息。session在服务器上。 3)        libcurl中使用cookie 保存cookie, 使之后的链接与此链接使用相同的cookie a)         在关闭链接的时候把cookie写入指定的文件 curl_easy_setopt(curl, CURLOPT_COOKIEJAR, "/tmp/cookie.txt"); b)        取用现在有的cookie,而不重新得到cookie curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "/tmp/cookie.txt"); b)        ht