﻿<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"><channel><title>C++博客-拂晓·明月·弯刀-文章分类-转载</title><link>http://www.cppblog.com/ietj/category/5889.html</link><description>观望，等待只能让出现的机会白白溜走</description><language>zh-cn</language><lastBuildDate>Sun, 29 May 2011 17:10:13 GMT</lastBuildDate><pubDate>Sun, 29 May 2011 17:10:13 GMT</pubDate><ttl>60</ttl><item><title>windows dll 和 Linux so 的异同</title><link>http://www.cppblog.com/ietj/articles/147032.html</link><dc:creator>一路风尘</dc:creator><author>一路风尘</author><pubDate>Tue, 24 May 2011 06:56:00 GMT</pubDate><guid>http://www.cppblog.com/ietj/articles/147032.html</guid><wfw:comment>http://www.cppblog.com/ietj/comments/147032.html</wfw:comment><comments>http://www.cppblog.com/ietj/articles/147032.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/ietj/comments/commentRss/147032.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/ietj/services/trackbacks/147032.html</trackback:ping><description><![CDATA[<p>摘要：动态链接库技术实现和设计程序常用的技术，在Windows和Linux系统中都有动态库的概念，采用动态库可以有效的减少程序大小，节省空间，提高效率，增加程序的可扩展性，便于模块化管理。 </p>
<p>但不同操作系统的动态库由于格式 不同，在需要不同操作系统调用时需要进行动态库程序移植。本文分析和比较了两种操作系统动态库技术，并给出了将Visual C++编制的动态库移植到Linux上的方法和经验。 </p>
<p>1、引言 </p>
<p>动态库（Dynamic Link Library abbr，DLL）技术是程序设计中经常采用的技术。其目的减少程序的大小，节省空间，提高效率，具有很高的灵活性。 </p>
<p>采用动态库技术对于升级软件版本更加容易。与静态库（Static Link Library）不同，动态库里面的函数不是执行程序本身的一部分，而是根据执行需要按需载入，其执行代码可以同时在多个程序中共享。 </p>
<p>在 Windows和Linux操作系统中，都可采用这种方式进行软件设计，但他们的调用方式以及程序编制方式不尽相同。本文首先分析了在这两种操作系统中通常采用的动态库调用方法以及程序编制方式，然后分析比较了这两种方式的不同之处，最后根据实际移植程序经验，介绍了将VC++编制的Windows动态库移植到Linux下的方法。 </p>
<p>2、动态库技术 </p>
<p>2.1 Windows动态库技术 </p>
<p>动态链接库是实现Windows应用程序共享资源、节省内存空间、提高使用效率的一个重要技术手段。常见的动态库包含外部函数和资源，也有一些动态库只包含资源，如Windows字体资源文件，称之为资源动态链接库。通常动态库以.dll，.drv、.fon等作为后缀。 </p>
<p>相应的windows静态库通常以.lib结尾，Windows自己就将一些主要的系统功能以动态库模块的形式实现。 </p>
<p>Windows动态库在运行时被系统加载到进程的虚拟空间中，使用从调用进程的虚拟地址空间分配的内存，成为调用进程的一部分。DLL也只能被该进程的线程所访问。DLL的句柄可以被调用进程使用；调用进程的句柄可以被DLL使用。 </p>
<p>DLL 模块中包含各种导出函数，用于向外界提供服务。DLL可以有自己的数据段，但没有自己的堆栈，使用与调用它的应用程序相同的堆栈模式；一个DLL在内存中只有一个实例；DLL实现了代码封装性；DLL的编制与具体的编程语言及编译器无关，可以通过DLL来实现混合语言编程。DLL函数中的代码所创建的任何对象（包括变量）都归调用它的线程或进程所有。 </p>
<p>根据调用方式的不同，对动态库的调用可分为静态调用方式和动态调用方式。 </p>
<p>(1) 静态调用，也称为隐式调用，由编译系统完成对DLL的加载和应用程序结束时DLL卸载的编码（Windows系统负责对DLL调用次数的计数），调用方式简单，能够满足通常的要求。通常采用的调用方式是把产生动态连接库时产生的.LIB文件加入到应用程序的工程中，想使用DLL中的函数时，只须在源文件中声明一下。 </p>
<p>LIB文件包含了每一个DLL导出函数的符号名和可选择的标识号以及DLL文件名，不含有实际的代码。Lib文件包含的信息进入到生成的应用程序中，被调用的DLL文件会在应用程序加载时同时加载在到内存中。 </p>
<p>(2)动态调用，即显式调用方式，是由编程者用API函数加载和卸载DLL来达到调用DLL的目的，比较复杂，但能更加有效地使用内存，是编制大型应用程序时的重要方式。在Windows系统中，与动态库调用有关的函数包括： </p>
<p>&#9312;LoadLibrary（或MFC 的AfxLoadLibrary），装载动态库。 </p>
<p>&#9313;GetProcAddress，获取要引入的函数，将符号名或标识号转换为DLL内部地址。 </p>
<p>&#9314;FreeLibrary（或MFC的AfxFreeLibrary），释放动态链接库。 </p>
<p>在 windows中创建动态库也非常方便和简单。在Visual C++中，可以创建不用MFC而直接用C语言写的DLL程序，也可以创建基于MFC类库的DLL程序。每一个DLL必须有一个入口点，在VC++ 中，DllMain是一个缺省的入口函数。DllMain负责初始化(Initialization)和结束(Termination)工作。 </p>
<p>动态库输出函数也有两种约定，分别是基于调用约定和名字修饰约定。DLL程序定义的函数分为内部函数和导出函数，动态库导出的函数供其它程序模块调用。通常可以有下面几种方法导出函数： </p>
<p>&#9312;采用模块定义文件的EXPORT部分指定要输入的函数或者变量。 </p>
<p>&#9313;使用MFC提供的修饰符号_declspec(dllexport)。 </p>
<p>&#9314;以命令行方式，采用/EXPORT命令行输出有关函数。 </p>
<p>在windows动态库中，有时需要编写模块定义文件(.DEF)，它是用于描述DLL属性的模块语句组成的文本文件。 </p>
<p>2.2 Linux共享对象技术 </p>
<p>在 Linux操作系统中，采用了很多共享对象技术（Shared Object），虽然它和Windows里的动态库相对应，但它并不称为动态库。相应的共享对象文件以.so作为后缀，为了方便，在本文中，对该概念不进行专门区分。Linux系统的/lib以及标准图形界面的/usr/X11R6/lib等目录里面，就有许多以so结尾的共享对象。 </p>
<p>同样，在Linux下，也有静态函数库这种调用方式，相应的后缀以.a结束。Linux采用该共享对象技术以方便程序间共享，节省程序占有空间，增加程序的可扩展性和灵活性。Linux还可以通过LD-PRELOAD变量让开发人员可以使用自己的程序库中的模块来替换系统模块。 </p>
<p>同 Windows系统一样，在Linux中创建和使用动态库是比较容易的事情，在编译函数库源程序时加上-shared选项即可，这样所生成的执行程序就是动态链接库。通常这样的程序以so为后缀，在Linux动态库程序设计过程中，通常流程是编写用户的接口文件，通常是.h文件，编写实际的函数文件，以.c或.cpp为后缀，再编写makefile文件。对于较小的动态库程序可以不用如此，但这样设计使程序更加合理。 </p>
<p>编译生成动态连接库后，进而可以在程序中进行调用。在Linux中，可以采用多种调用方式，同Windows的系统目录(..\system32等)一样，可以将动态库文件拷贝到/lib目录或者在/lib目录里面建立符号连接，以便所有用户使用。 </p>
<p>下面介绍Linux调用动态库经常使用的函数，但在使用动态库时，源程序必须包含dlfcn.h头文件，该文件定义调用动态链接库的函数的原型。 </p>
<p>(1)_打开动态链接库：dlopen，函数原型void *dlopen (const char *filename, int flag); dlopen用于打开指定名字(filename)的动态链接库，并返回操作句柄。 </p>
<p>(2)取函数执行地址：dlsym，函数原型为: void *dlsym(void *handle, char *symbol); dlsym根据动态链接库操作句柄(handle)与符号(symbol)，返回符号对应的函数的执行代码地址。 </p>
<p>(3)关闭动态链接库：dlclose，函数原型为: int dlclose (void *handle); dlclose用于关闭指定句柄的动态链接库，只有当此动态链接库的使用计数为0时,才会真正被系统卸载。 </p>
<p>(4)动态库错误函数：dlerror，函数原型为: const char *dlerror(void); 当动态链接库操作函数执行失败时，dlerror可以返回出错信息，返回值为NULL时表示操作函数执行成功。 </p>
<p>在取到函数执行地址后，就可以在动态库的使用程序里面根据动态库提供的函数接口声明调用动态库里面的函数。在编写调用动态库的程序的makefile文件时，需要加入编译选项-rdynamic和-ldl。 </p>
<p>除了采用这种方式编写和调用动态库之外，Linux操作系统也提供了一种更为方便的动态库调用方式，也方便了其它程序调用，这种方式与Windows系统的隐式链接类似。其动态库命名方式为&#8220;lib*.so.*&#8221;。在这个命名方式中，第一个*表示动态链接库的库名，第二个*通常表示该动态库的版本号，也可以没有版本号。 </p>
<p>在这种调用方式中，需要维护动态链接库的配置文件/etc /ld.so.conf来让动态链接库为系统所使用，通常将动态链接库所在目录名追加到动态链接库配置文件中。如具有X window窗口系统发行版该文件中都具有/usr/X11R6/lib，它指向X window窗口系统的动态链接库所在目录。 </p>
<p>为了使动态链接库能为系统所共享，还需运行动态链接库的管理命令./sbin/ldconfig。在编译所引用的动态库时，可以在gcc采用 &#8211;l或-L选项或直接引用所需的动态链接库方式进行编译。在Linux里面，可以采用ldd命令来检查程序依赖共享库。 </p>
<p>3、两种系统动态库比较分析 </p>
<p>Windows和Linux采用动态链接库技术目的是基本一致的，但由于操作系统的不同，他们在许多方面还是不尽相同，下面从以下几个方面进行阐述。 </p>
<p>(1) 动态库程序编写，在Windows系统下的执行文件格式是PE格式，动态库需要一个DllMain函数作为初始化的人口，通常在导出函数的声明时需要有 _declspec(dllexport)关键字。Linux下的gcc编译的执行文件默认是ELF格式，不需要初始化入口，亦不需要到函数做特别声明，编写比较方便。 </p>
<p>(2)动态库编译，在windows系统下面，有方便的调试编译环境，通常不用自己去编写makefile文件，但在linux下面，需要自己动手去编写makefile文件，因此，必须掌握一定的makefile编写技巧，另外，通常Linux编译规则相对严格。 </p>
<p>(3)动态库调用方面，Windows和Linux对其下编制的动态库都可以采用显式调用或隐式调用，但具体的调用方式也不尽相同。 </p>
<p>(4) 动态库输出函数查看，在Windows中，有许多工具和软件可以进行查看DLL中所输出的函数，例如命令行方式的dumpbin以及VC++工具中的 DEPENDS程序。在Linux系统中通常采用nm来查看输出函数，也可以使用ldd查看程序隐式链接的共享对象文件。 </p>
<p>(5)对操作系统的依赖，这两种动态库运行依赖于各自的操作系统，不能跨平台使用。因此，对于实现相同功能的动态库，必须为两种不同的操作系统提供不同的动态库版本。 </p>
<p>4、动态库移植方法 </p>
<p>如果要编制在两个系统中都能使用的动态链接库，通常会先选择在Windows的VC++提供的调试环境中完成初始的开发，毕竟VC++提供的图形化编辑和调试界面比vi和gcc方便许多。完成测试之后，再进行动态库的程序移植。 </p>
<p>通常gcc默认的编译规则比VC++默认的编译规则严格，即使在VC++下面没有任何警告错误的程序在gcc调试中也会出现许多警告错误，可以在gcc中采用-w选项关闭警告错误。 </p>
<p>下面给出程序移植需要遵循的规则以及经验。 </p>
<p>(1) 尽量不要改变原有动态库头文件的顺序。通常在C/C++语言中，头文件的顺序有相当的关系。另外虽然C/C++语言区分大小写，但在包含头文件时，Linux必须与头文件的大小写相同，因为ext2文件系统对文件名是大小写敏感，否则不能正确编译，而在Windows下面，头文件大小写可以正确编译。 </p>
<p>(2)不同系统独有的头文件。在Windows系统中，通常会包括 windows.h头文件，如果调用底层的通信函数，则会包含winsock..h头文件。因此在移植到Linux系统时，要注释掉这些Windows系统独有的头文件以及一些windows系统的常量定义说明，增加Linux都底层通信的支持的头文件等。 </p>
<p>(3) 数据类型。VC++具有许多独有的数据类型，如__int16，__int32，TRUE，SOCKET等，gcc编译器不支持它们。通常做法是需要将 windows.h和basetypes.h中对这些数据进行定义的语句复制到一个头文件中，再在Linux中包含这个头文件。例如将套接字的类型为 SOCKET改为int。 </p>
<p>(4)关键字。VC++中具有许多标准C中所没有采用的关键字，如BOOL，BYTE，DWORD，__asm等，通常在为了移植方便，尽量不使用它们，如果实在无法避免可以采用#ifdef 和#endif为LINUX和WINDOWS编写两个版本。 </p>
<p>(5) 函数原型的修改。通常如果采用标准的C/C++语言编写的动态库，基本上不用再重新编写函数，但对于系统调用函数，由于两种系统的区别，需要改变函数的调用方式等，如在Linux编制的网络通信动态库中，用close()函数代替windows操作系统下的closesocket()函数来关闭套接字。另外在Linux下没有文件句柄，要打开文件可用open和fopen函数，具体这两个函数的用法可参考文献[2]。 </p>
<p>(6)makefile 的编写。在windows下面通常由VC++编译器来负责调试，但gcc需要自己动手编写makefile文件，也可以参照VC++生成的 makefile文件。对于动态库移植，编译动态库时需要加入-shared选项。对于采用数学函数，如幂级数的程序，在调用动态库是，需要加入-lm。 </p>
<p>(7)其它一些需要注意的地方 </p>
<p>&#9312;程序设计结构分析，对于移植它人编写的动态库程序，程序结构分析是必不可少的步骤，通常在动态库程序中，不会包含界面等操作，所以相对容易一些。 </p>
<p>&#9313;在Linux中，对文件或目录的权限分为拥有者、群组、其它。所以在存取文件时，要注意对文件是读还是写操作，如果是对文件进行写操作，要注意修改文件或目录的权限，否则无法对文件进行写。 </p>
<p>&#9314; 指针的使用，定义一个指针只给它分配四个字节的内存，如果要对指针所指向的变量赋值，必须用malloc函数为它分配内存或不把它定义为指针而定义为变量即可，这点在linux下面比windows编译严格。同样结构不能在函数中传值，如果要在函数中进行结构传值，必须把函数中的结构定义为结构指针。 </p>
<p>&#9315;路径标识符，在Linux下是&#8220;/&#8221;，在Windows下是&#8220;\&#8221;，注意windows和Linux的对动态库搜索路径的不同。 </p>
<p>&#9316;编程和调试技巧方面。对不同的调试环境有不同的调试技巧，在这里不多叙述。 </p>
<p>5、结束语 </p>
<p>本文系统分析了windows和Linux动态库实现和使用方式，从程序编写、编译、调用以及对操作系统依赖等方面综合分析比较了这两种调用方式的不同之处，根据实际程序移植经验，给出了将VC++编制的Windows动态库移植到Linux下的方法以及需要注意的问题，同时并给出了程序示例片断，实际在程序移植过程中，由于系统的设计等方面，可能移植起来需要注意的方面远比上面复杂，本文通过总结归纳进而为不同操作系统程序移植提供了有意的经验和技巧。</p>
<p>(本文来源于internet，感谢原创作者)</p>
<p>--------------------------------------------------------------------------</p>
<p>unix具体编译的例子：</p>
<p>源文件为main.c, x.c, y.c, z.c,头文件为x.h,y.h,z.h</p>
<p># 声称动代连接库，假设名称为libtest.so <br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; gcc x.c y.c z.c -fPIC -shared -o libtest.so </p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; # 将main.c和动态连接库进行连接生成可执行文件 <br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; gcc main.c -L. -ltest -o main </p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; # 输出LD_LIBRARY_PATH环境变量，一边动态库装载器能够找到需要的动态库 <br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:. </p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; # 测试是否动态连接，如果列出libtest.so，那么应该是连接正常了 <br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ldd main</p>
<p>-fPIC：表示编译为位置独立的代码，不用此选项的话编译后的代码是位置相关的所以动态载入时是通&nbsp; 过&nbsp;&nbsp;&nbsp; 代码拷贝的方式来满足不同进程的需要，而不能达到真正代码段共享的目的。 </p>
<p>-L.：表示要连接的库在当前目录中 </p>
<p>-ltest：编译器查找动态连接库时有隐含的命名规则，即在给出的名字前面加上lib，后面加上.so来确定库的名称 </p>
<p>LD_LIBRARY_PATH：这个环境变量指示动态连接器可以装载动态库的路径。 <br />当然如果有root权限的话，可以修改/etc/ld.so.conf文件，然后调用 <br />/sbin/ldconfig来达到同样的目的，不过如果没有root权限，那么只能采用输出LD_LIBRARY_PATH的方法了。</p>
<p>&nbsp;</p>
<p>本文来自CSDN博客，转载请标明出处：<a href="http://blog.csdn.net/yang_lang/archive/2010/10/08/5926486.aspx">http://blog.csdn.net/yang_lang/archive/2010/10/08/5926486.aspx</a></p><img src ="http://www.cppblog.com/ietj/aggbug/147032.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/ietj/" target="_blank">一路风尘</a> 2011-05-24 14:56 <a href="http://www.cppblog.com/ietj/articles/147032.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>考虑错误情况</title><link>http://www.cppblog.com/ietj/articles/144610.html</link><dc:creator>一路风尘</dc:creator><author>一路风尘</author><pubDate>Tue, 19 Apr 2011 15:40:00 GMT</pubDate><guid>http://www.cppblog.com/ietj/articles/144610.html</guid><wfw:comment>http://www.cppblog.com/ietj/comments/144610.html</wfw:comment><comments>http://www.cppblog.com/ietj/articles/144610.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/ietj/comments/commentRss/144610.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/ietj/services/trackbacks/144610.html</trackback:ping><description><![CDATA[转自：<a href="http://msdn.microsoft.com/zh-cn/library/ms809695.aspx#mainSection"><u><font color=#0000ff>http://msdn.microsoft.com/zh-cn/library/ms809695.aspx#mainSection</font></u></a><br><br><span class=Apple-style-span style="WORD-SPACING: 0px; FONT: medium Simsun; TEXT-TRANSFORM: none; COLOR: rgb(0,0,0); TEXT-INDENT: 0px; WHITE-SPACE: normal; LETTER-SPACING: normal; BORDER-COLLAPSE: separate; orphans: 2; widows: 2; webkit-border-horizontal-spacing: 0px; webkit-border-vertical-spacing: 0px; webkit-text-decorations-in-effect: none; webkit-text-size-adjust: auto; webkit-text-stroke-width: 0px"><span class=Apple-style-span style="FONT-SIZE: 13px; FONT-FAMILY: 'Segoe UI', Verdana, Arial; TEXT-ALIGN: left">
<div>发布日期 : 9/9/2004<span><span class=Apple-converted-space>&nbsp;</span>|<span class=Apple-converted-space>&nbsp;</span></span>更新日期 : 9/9/2004</div>
<p>Jon Pincus<br>Microsoft Corporation</p>
<p><strong>摘要：</strong>讨论错误处理范例以及与多个范例相关联的陷阱，并提供了两个简单但至关重要的有关错误情况处理的原则。</p>
<h5>本页内容</h5>
<p><a style="COLOR: rgb(19,100,196); TEXT-DECORATION: none" href="http://msdn.microsoft.com/zh-cn/library/ms809695.aspx#EFAA"><img style="BORDER-TOP-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-RIGHT-WIDTH: 0px" alt=简介 src="http://i.msdn.microsoft.com/dynimg/IC79147.gif" border=0><span class=Apple-converted-space>&nbsp;</span></a><a style="COLOR: rgb(19,100,196); TEXT-DECORATION: none" href="http://msdn.microsoft.com/zh-cn/library/ms809695.aspx#EFAA">简介</a><span class=Apple-converted-space>&nbsp;</span><br><a style="COLOR: rgb(19,100,196); TEXT-DECORATION: none" href="http://msdn.microsoft.com/zh-cn/library/ms809695.aspx#EEAA"><img style="BORDER-TOP-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-RIGHT-WIDTH: 0px" alt=错误处理范例 src="http://i.msdn.microsoft.com/dynimg/IC79147.gif" border=0><span class=Apple-converted-space>&nbsp;</span></a><a style="COLOR: rgb(19,100,196); TEXT-DECORATION: none" href="http://msdn.microsoft.com/zh-cn/library/ms809695.aspx#EEAA">错误处理范例</a><span class=Apple-converted-space>&nbsp;</span><br><a style="COLOR: rgb(19,100,196); TEXT-DECORATION: none" href="http://msdn.microsoft.com/zh-cn/library/ms809695.aspx#EDAA"><img style="BORDER-TOP-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-RIGHT-WIDTH: 0px" alt="多个 API 约定的问题" src="http://i.msdn.microsoft.com/dynimg/IC79147.gif" border=0><span class=Apple-converted-space>&nbsp;</span></a><a style="COLOR: rgb(19,100,196); TEXT-DECORATION: none" href="http://msdn.microsoft.com/zh-cn/library/ms809695.aspx#EDAA">多个 API 约定的问题</a><span class=Apple-converted-space>&nbsp;</span><br><a style="COLOR: rgb(19,100,196); TEXT-DECORATION: none" href="http://msdn.microsoft.com/zh-cn/library/ms809695.aspx#ECAA"><img style="BORDER-TOP-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-RIGHT-WIDTH: 0px" alt=有关错误情况的两个简单原则 src="http://i.msdn.microsoft.com/dynimg/IC79147.gif" border=0><span class=Apple-converted-space>&nbsp;</span></a><a style="COLOR: rgb(19,100,196); TEXT-DECORATION: none" href="http://msdn.microsoft.com/zh-cn/library/ms809695.aspx#ECAA">有关错误情况的两个简单原则</a><span class=Apple-converted-space>&nbsp;</span><br><a style="COLOR: rgb(19,100,196); TEXT-DECORATION: none" href="http://msdn.microsoft.com/zh-cn/library/ms809695.aspx#EBAA"><img style="BORDER-TOP-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-RIGHT-WIDTH: 0px" alt=小结 src="http://i.msdn.microsoft.com/dynimg/IC79147.gif" border=0><span class=Apple-converted-space>&nbsp;</span></a><a style="COLOR: rgb(19,100,196); TEXT-DECORATION: none" href="http://msdn.microsoft.com/zh-cn/library/ms809695.aspx#EBAA">小结</a><span class=Apple-converted-space>&nbsp;</span><br></p>
<h3 id=EFAA style="FONT-WEIGHT: bold; FONT-SIZE: 1.07em; COLOR: rgb(63,82,156); FONT-FAMILY: 'Segoe UI', Verdana, Arial">简介</h3>
<p>有能力的程序员能够编写在未发生异常情况时正常运行的代码。使程序员出类拔萃的技能之一是能够编写在发生错误和出现&#8220;意外事件&#8221;时仍然能继续运行的代码。然而，术语&#8220;意外事件&#8221;会给人一种错误的印象。如果您的代码嵌入在一个广泛分布的成功产品中，那么您应该预料到代码可能发生的各种异常（且可怕）的情况。计算机将耗尽内存，文件未如您所愿地存在于应该存在的地方，从未失败的函数有可能在新版本的操作系统中失败，等等，不一而足。如果您希望代码能继续可靠地运行，那么就需要预见所有这些事件。</p>
<p>本文讨论的特定类别的异常事件是错误情况。错误情况并没有一个准确的定义。直观地讲，它就是指通常能够成功的事情并未成功。内存分配失败就是一个典型示例。通常，系统有大量内存可以满足需求，但是偶尔计算机也可能会由于某种原因而特别繁忙，例如，运行大型的电子表格计算，或者在 Internet 世界中有人正在对该计算机发动拒绝服务攻击。如果您要编写任意类型的重要组件、服务或应用程序，那么就需要预见到这种情况。</p>
<p>本文采用的编码示例基于 C/C++，但一般原则是独立于编程语言的。即使不使用 C/C++ 代码，使用多种语言的程序员也应该记住这一点。例如，<em>PC Week</em><span class=Apple-converted-space>&nbsp;</span>举办的黑客竞赛 (Hackpcweek.com) 的获胜者，就是利用某些 Perl CGI 代码中检查返回代码的失败，来部分地对 Apache 服务器发起攻击。有关进一步的信息，请参阅 http://www.zdnet.com/eweek/stories/general/0,11011,2350744,00.html 上的文章。</p>
<p>任何重要的软件部分都会与其他的层和组件进行交互。编写处理错误的代码的第一步，是了解当错误发生时系统的其余部分正在执行哪些操作。本文的其余部分将讨论错误处理范例，然后讨论与多个范例相关联的陷阱。本文还包括了两个简单但至关重要的有关错误情况处理的原则。</p>
<div><a style="COLOR: rgb(19,100,196); TEXT-DECORATION: none" href="http://msdn.microsoft.com/zh-cn/library/ms809695.aspx#mainSection"><img style="BORDER-TOP-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-RIGHT-WIDTH: 0px" alt="" src="http://i.msdn.microsoft.com/dynimg/IC170569.gif" border=0><span class=Apple-converted-space>&nbsp;</span></a><a style="COLOR: rgb(19,100,196); TEXT-DECORATION: none" href="http://msdn.microsoft.com/zh-cn/library/ms809695.aspx#mainSection">返回页首</a><span class=Apple-converted-space>&nbsp;</span><br></div>
<h3 id=EEAA style="FONT-WEIGHT: bold; FONT-SIZE: 1.07em; COLOR: rgb(63,82,156); FONT-FAMILY: 'Segoe UI', Verdana, Arial">错误处理范例</h3>
<p>无论何时发生错误情况，都需要考虑三个独立的问题：检测错误情况、报告错误情况以及对错误情况作出响应。通常，处理这些问题的职责分散在代码的不同组件或层中。例如，检测系统是否内存不足是操作系统的工作。内存分配函数的工作是将这种情况报告给它的调用方。例如，<strong>VirtualAlloc</strong><span class=Apple-converted-space>&nbsp;</span>会返回 NULL，Microsoft 基础类库 (MFC) 运算符new 会引发<strong>CMemoryException *</strong>，而<span class=Apple-converted-space>&nbsp;</span><strong>HeapAlloc</strong><span class=Apple-converted-space>&nbsp;</span>可能会返回 NULL 或引发结构化异常。调用方的工作就是对此作出响应，方法是清理自身的工作，捕捉异常，并且还可能将失败报告给它的调用方或用户。</p>
<p>因为不同的层和组件需要相互协作以处理这些错误，所以第一步是定义特殊词汇，即，所有组件共同遵守的约定。遗憾的是，在 C、C++ 或其他任何语言中都没有单一且定义完善的约定。相反，却存在许多约定，并且每种约定都有各自的优缺点。</p>
<p>下面列出了一些最常用的约定：</p>
<ul style="PADDING-LEFT: 0px; MARGIN-LEFT: 40px; LIST-STYLE-TYPE: disc">
    <li style="LIST-STYLE-IMAGE: url(http://i3.msdn.microsoft.com/Hash/fcc063e32e5c2395ba48efadd8423b60.gif)">
    <p><strong>返回一个</strong><span class=Apple-converted-space>&nbsp;</span><strong>BOOL<span class=Apple-converted-space>&nbsp;</span></strong><strong>值以指示成功或失败。</strong>Windows API 和 MFC 中的很多调用都返回 TRUE 以指示成功，返回 FALSE 以指示失败。这种方法既好又简单。但问题在于，该方法不对失败进行解释，也不区分不同类型的成功或失败。（当使用 Windows API 时，您可以使用<span class=Apple-converted-space>&nbsp;</span><strong>GetLastError</strong><span class=Apple-converted-space>&nbsp;</span>来获得特定代码。）</p>
    </li>
    <li style="LIST-STYLE-IMAGE: url(http://i3.msdn.microsoft.com/Hash/fcc063e32e5c2395ba48efadd8423b60.gif)">
    <p><strong>返回状态。</strong>遗憾的是，随着 Windows API 的变化，该约定出现了两种不同的样式。COM 函数返回 HRESULT：HRESULT &gt;= 0（例如，S_OK）表示成功，HRESULT &lt; 0（例如，E_FAIL）表示失败。其他函数（例如，<strong>Registry</strong><span class=Apple-converted-space>&nbsp;</span>函数）则返回 WINERROR.H 中定义的错误代码。在该约定中，ERROR_SUCCESS (0) 是唯一的成功值，所有其他值都表示各种形式的失败。这种不一致有可能造成各种各样的混乱。更糟糕的是，值 0（它在 Boolean 样式的返回值约定中表示失败）在此处表示成功。其后果将在后面进行讨论。在任何事件中，状态返回方法都具有优势 — 不同的值可以表示不同类型的失败（或者对于 HRESULT 而言，表示成功）。</p>
    </li>
    <li style="LIST-STYLE-IMAGE: url(http://i3.msdn.microsoft.com/Hash/fcc063e32e5c2395ba48efadd8423b60.gif)">
    <p><strong>返回一个</strong><span class=Apple-converted-space>&nbsp;</span><strong>NULL<span class=Apple-converted-space>&nbsp;</span></strong><strong>指针。</strong>C 样式内存分配函数（例如，<strong>HeapAlloc</strong>、<strong>VirtualAlloc</strong>、<strong>GlobalAlloc</strong><span class=Apple-converted-space>&nbsp;</span>和<span class=Apple-converted-space>&nbsp;</span><strong>malloc</strong>）通常返回一个 NULL 指针。另一个示例是 C 标准库的<span class=Apple-converted-space>&nbsp;</span><strong>fopen</strong><span class=Apple-converted-space>&nbsp;</span>函数。与 BOOL 返回值相同，还需要其他一些机制来区分不同种类的失败。</p>
    </li>
    <li style="LIST-STYLE-IMAGE: url(http://i3.msdn.microsoft.com/Hash/fcc063e32e5c2395ba48efadd8423b60.gif)">
    <p><strong>返回一个</strong><span class=Apple-converted-space>&nbsp;</span><strong>&#8220;</strong><span class=Apple-converted-space>&nbsp;</span><strong>不可能的值</strong><span class=Apple-converted-space>&nbsp;</span><strong>&#8221;</strong><span class=Apple-converted-space>&nbsp;</span><strong>。</strong>该值通常为 0（对于整数，如<span class=Apple-converted-space>&nbsp;</span><strong>GetWindowsDirectory</strong>）或 &#8211;1（对于指针或长度，如 C 标准库的<span class=Apple-converted-space>&nbsp;</span><strong>fgets</strong><span class=Apple-converted-space>&nbsp;</span>函数）。NULL 指针约定的泛化是它找到某个值 — 例程采用其他方式将无法返回该值，并且让该值表示错误。因为通常没有中心模式，所以在将该方法扩展到大型 API 时会在某种程度上出现问题。</p>
    </li>
    <li style="LIST-STYLE-IMAGE: url(http://i3.msdn.microsoft.com/Hash/fcc063e32e5c2395ba48efadd8423b60.gif)">
    <p><strong>引发</strong><span class=Apple-converted-space>&nbsp;</span><strong>C++<span class=Apple-converted-space>&nbsp;</span></strong><strong>异常。</strong>对于纯粹的 C++ 程序来说，该约定可让您使用语言功能来获益。Bobby Schmidt 最近撰写的有关<a style="COLOR: rgb(19,100,196); TEXT-DECORATION: none" href="http://msdn.microsoft.com/library/en-us/dndeepc/html/deep051099.asp">异常</a>的&#8220;Deep C++&#8221;系列文章详细讨论了这些问题。然而，将该方法与旧式的 C 代码或者与 COM 结合可能会带来问题。对于 COM 方法而言，引发 C++ 异常是非法的。此外，C++ 异常是开销相对较大的一种机制。如果操作本身的价值不高，那么过大的开销通常会抵消所获得的好处。</p>
    </li>
    <li style="LIST-STYLE-IMAGE: url(http://i3.msdn.microsoft.com/Hash/fcc063e32e5c2395ba48efadd8423b60.gif)">
    <p><strong>引发结构化异常。</strong>这里的注意事项恰与 C++ 异常相反。该方法对于 C 代码而言非常整洁，但与 C++ 的交互性不太好。同样，该方法不能与 COM 有效地结合。</p>
    </li>
</ul>
<p><strong>注</strong><span class=Apple-converted-space>&nbsp;</span>如果您要选用较旧的代码基，那么您有时会看到&#8220;原生的异常处理机制&#8221;。C++ 编译器只是在最近才开始比较好地处理异常。在过去，开发人员经常基于 Windows 结构化异常处理 (SEH) 或 C 库的<span class=Apple-converted-space>&nbsp;</span><strong>setjmp</strong>/<strong>longjmp</strong><span class=Apple-converted-space>&nbsp;</span>机制来构建他们自己的机制。如果您已经继承了这些代码基中的一个，则需要自担风险，重新编写它可能是最好的选择。否则，最好由经验非常丰富的程序员来处理。</p>
<p>错误处理是任何 API 定义的关键部分。无论您是要设计 API 还是使用他人设计的 API，都是如此。对于 API 定义而言，错误行为范例与类定义或命名方案同样重要。例如，MFC API 非常明确地规定了在资源分配失败时哪些函数引发哪些异常，以及哪些函数返回 BOOL 成功/失败调用。API 的设计者明确地将某些想法植入这一规定，用户需要理解其意图并按照已经构建的规则进行操作。</p>
<p>如果您要使用现有的 API，则必须处理所有现有约定。如果您要设计 API，则应该选用能够与您已经使用的 API 相适应的约定。</p>
<p>如果您要使用多个 API，则通常要使用多个约定。某些约定组合可以很好地工作，因为这些约定很难混淆。但是，某些约定组合却很容易出错。本文所讨论的许多特定陷阱就是由于存在这些不一致而造成的。</p>
<div><a style="COLOR: rgb(19,100,196); TEXT-DECORATION: none" href="http://msdn.microsoft.com/zh-cn/library/ms809695.aspx#mainSection"><img style="BORDER-TOP-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-RIGHT-WIDTH: 0px" alt="" src="http://i.msdn.microsoft.com/dynimg/IC170569.gif" border=0><span class=Apple-converted-space>&nbsp;</span></a><a style="COLOR: rgb(19,100,196); TEXT-DECORATION: none" href="http://msdn.microsoft.com/zh-cn/library/ms809695.aspx#mainSection">返回页首</a><span class=Apple-converted-space>&nbsp;</span><br></div>
<h3 id=EDAA style="FONT-WEIGHT: bold; FONT-SIZE: 1.07em; COLOR: rgb(63,82,156); FONT-FAMILY: 'Segoe UI', Verdana, Arial">多个 API 约定的问题</h3>
<p>混用和匹配不同的 API 约定通常是无法避免的，但这非常容易出错。一些实例是显而易见的。例如，如果您尝试在同一个可执行文件中混用 Windows SEH 和 C++ 异常，则很可能会失败。其他示例更为微妙。其中一个反复出现的特定示例就与 HRESULT 有关，并且是以下示例的某种变体：</p>
<pre>extern BOOL DoIt();
BOOL ok;
ok = DoIt(...);
if (FAILED(ok))     // WRONG!!!
return;
</pre>
<p>该示例为何是错误的？FAILED 是一个 HRESULT 样式的宏，因此它会检查其参数是否小于 0。以下是它的定义（摘自 winerror.h）：</p>
<pre>#define FAILED(Status) ((HRESULT)(Status)&lt;0)
</pre>
<p>因为 FALSE 被定义为 0，所以 FAILED(FALSE) == 0 是违反直觉的，这无须多言。而且，因为该定义嵌入了强制转换，所以即使您使用警告级别 4，也不会获得编译器警告。</p>
<p>当您处理 BOOL 时，不应该使用宏，但应该进行显式检查：</p>
<pre>BOOL ok;
ok = DoIt(...);
if (! ok)
return;
</pre>
<p>相反，当您处理 HRESULT 时，则应该始终使用 SUCCEEDED 和 FAILED 宏。</p>
<pre>HRESULT hr;
hr = ISomething-&gt;DoIt(...);
if (! hr)     // WRONG!!!
return;
</pre>
<p>这是一个恶性错误，因为它很容易被忽略。如果<span class=Apple-converted-space>&nbsp;</span><strong>CoDoIt<span class=Apple-converted-space>&nbsp;</span></strong>返回 S_OK，则测试将成功完成。但是，如果<span class=Apple-converted-space>&nbsp;</span><strong>CoDoIt<span class=Apple-converted-space>&nbsp;</span></strong>返回某个其他成功状态，会怎样呢？那样，hr &gt; 0，所以 !hr == 0；if 测试失败，代码将返回实际上并未发生的错误。</p>
<p>下面是另一个示例：</p>
<pre>HRESULT hr;
hr = ISomething-&gt;DoIt(...);
if (hr == S_OK)     // STILL WRONG!!!
return;
</pre>
<p>人们有时会插话说<span class=Apple-converted-space>&nbsp;</span><strong>ISomething::DoIt</strong><span class=Apple-converted-space>&nbsp;</span>在成功时总是返回 S_OK，因此最后两个代码片段肯定都没有问题。但是，这不是一个安全的假设。COM 接口的说明非常清楚。函数在成功时可以返回任何成功值，因此<span class=Apple-converted-space>&nbsp;</span><strong>ISomething::DoIt<span class=Apple-converted-space>&nbsp;</span></strong>的众多实现者中的任何一个都可能选择返回某个值，例如 S_FALSE。在这种情况下，您的代码将中止运行。</p>
<p>正确的解决方案是使用宏，这也就是宏存在的原因。</p>
<pre>HRESULT hr;
hr = ISomething-&gt;DoIt(...);
if (FAILED(hr))
return;
</pre>
<p>因为已经引出了 HRESULT 的主题，所以现在是提醒您 S_FALSE 特性的大好时机：</p>
<ul style="PADDING-LEFT: 0px; MARGIN-LEFT: 40px; LIST-STYLE-TYPE: disc">
    <li style="LIST-STYLE-IMAGE: url(http://i3.msdn.microsoft.com/Hash/fcc063e32e5c2395ba48efadd8423b60.gif)">
    <p>它是一个成功代码，而不是一个失败代码，因此 SUCCEEDED(S_FALSE) == 1。</p>
    </li>
    <li style="LIST-STYLE-IMAGE: url(http://i3.msdn.microsoft.com/Hash/fcc063e32e5c2395ba48efadd8423b60.gif)">
    <p>它被定义为 1，而不是 0，因此 S_FALSE == TRUE。</p>
    </li>
</ul>
<div><a style="COLOR: rgb(19,100,196); TEXT-DECORATION: none" href="http://msdn.microsoft.com/zh-cn/library/ms809695.aspx#mainSection"><img style="BORDER-TOP-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-RIGHT-WIDTH: 0px" alt="" src="http://i.msdn.microsoft.com/dynimg/IC170569.gif" border=0><span class=Apple-converted-space>&nbsp;</span></a><a style="COLOR: rgb(19,100,196); TEXT-DECORATION: none" href="http://msdn.microsoft.com/zh-cn/library/ms809695.aspx#mainSection">返回页首</a><span class=Apple-converted-space>&nbsp;</span><br></div>
<h3 id=ECAA style="FONT-WEIGHT: bold; FONT-SIZE: 1.07em; COLOR: rgb(63,82,156); FONT-FAMILY: 'Segoe UI', Verdana, Arial">有关错误情况的两个简单原则</h3>
<p>有许多简单的方法可以使代码更可靠地处理错误情况。相反，人们不愿意做的许多简单事情会使代码在发生错误情况时变得脆弱和不可靠。</p>
<h4 style="FONT-WEIGHT: bold; FONT-SIZE: 1em; COLOR: rgb(63,82,156); FONT-FAMILY: 'Segoe UI', Verdana, Arial">总是检查返回状态</h4>
<p>没有比这更简单的事情了。几乎所有函数都提供某种表明它们是成功还是失败的指示，但如果您不检查它们，则这一点没有任何用处。这能有多困难呢？可以将其视为一个卫生问题。您知道在吃东西之前应该洗手，但您可能并不总是这样做。这与检查返回值的道理相同。</p>
<p>下面是一个涉及到<span class=Apple-converted-space>&nbsp;</span><strong>GetWindowsDirectory</strong><span class=Apple-converted-space>&nbsp;</span>函数的简单而实用的示例。MSDN 文档清楚地说明了<span class=Apple-converted-space>&nbsp;</span><strong>GetWindowsDirectory</strong><span class=Apple-converted-space>&nbsp;</span>的错误行为：</p>
<pre>Return Values
</pre>
<p>如果该函数失败，则返回值为 0。要获得扩展的错误信息，请调用<span class=Apple-converted-space>&nbsp;</span><strong>GetLastError</strong><span class=Apple-converted-space>&nbsp;</span>函数。</p>
<p>实际上，文档中写得非常清楚。</p>
<p>下面是一个判断 Windows 目录驻留在哪个驱动器中的代码片段。</p>
<pre>TCHAR cDriveLetter;
TCHAR szWindowsDir[MAX_PATH];
GetWindowsDirectory(szWindowsDir, MAX_PATH);
cDriveLetter = szWindowsDir[0];   // WRONG!!!
...
</pre>
<p>如果<span class=Apple-converted-space>&nbsp;</span><strong>GetWindowsDirectory</strong><span class=Apple-converted-space>&nbsp;</span>失败，会发生什么情况呢？（如果您不相信<span class=Apple-converted-space>&nbsp;</span><strong>GetWindowsDirectory</strong><span class=Apple-converted-space>&nbsp;</span>会失败，这只是您暂时的观点。）好，该代码不检查返回值，因此分配给 cDriveLetter 的值未初始化。未初始化的内存可以具有任意值。实际上，该代码将随机选择驱动器。这样做几乎不可能是正确的。</p>
<p>正确的做法是检查错误状态。</p>
<pre>TCHAR cDriveLetter;
TCHAR szWindowsDir[MAX_PATH];
if (GetWindowsDirectory(szWindowsDir, MAX_PATH))
{
cDriveLetter = szWindowsDir[0];
...
}
</pre>
<p>这种情况还能发生吗？不检查返回值的最常见借口是&#8220;我知道那个函数绝对不会失败&#8221;。<strong>GetWindowsDirectory</strong><span class=Apple-converted-space>&nbsp;</span>就是一个很好的示例。一直到 Windows&#174; 98 和 Windows NT&#174; 4.0，它实际上确实没有失败过，因此许多人养成了一个不好的习惯，即，假设它永远不会失败。</p>
<p>现在 Windows 终端服务器出现了，要判断单个用户的 Windows 目录变得更为复杂。<strong>GetWindowsDirectory</strong><span class=Apple-converted-space>&nbsp;</span>必须完成更多的工作，可能包括分配内存。而且，因为开发这一函数的开发人员非常负责任，所以他完成了正确的工作并检查内存分配是否成功，如果不成功，则返回描述完整的错误状态。</p>
<p>这就导致了另外一些问题：如果<span class=Apple-converted-space>&nbsp;</span><strong>GetWindowsDirectory</strong><span class=Apple-converted-space>&nbsp;</span>在失败时已经将它的输出初始化为空字符串，是否会有所帮助？答案是否定的。结果不会是未初始化的，但它们仍将是粗心大意的应用程序所未曾料到的东西。假设您具有一个由 cDriveLetter &#8211; 'A' ; 索引的数组，那么现在该索引将突然变为负值。</p>
<p>即使终端服务器对于您不是问题，但同样的情况可能会发生在 Windows API 的任何未来实现中。您希望禁止正在开发的应用程序在将来版本的操作系统或替代实现（如 Embedded NT）中运行吗？一种良好的习惯是记住以下事实：代码经常在其预期的到期日之后继续生存。</p>
<p>有时，检查返回值是不够的。请考虑 Windows API<span class=Apple-converted-space>&nbsp;</span><strong>ReadFile</strong>。您经常会看到如下代码：</p>
<pre>LONG buffer[CHUNK_SIZE];
ReadFile(hFile, (LPVOID)buffer,
CHUNK_SIZE*sizeof(LONG), &amp;cbRead, NULL);
if (buffer[0] == 0)   // DOUBLY WRONG!!!
...
</pre>
<p>如果读取操作失败，说明缓冲区的内容是未初始化的。多数情况下它可能为零，但这一点并不确定。</p>
<p>读取文件失败的原因有许多。例如，该文件可能是远程文件，而网络可能发生故障。即使它是本地文件，磁盘也可能恰好不合时宜地损坏。如果是这种情况，则文件的格式可能完全不同于预期格式。他人可能无意中或别有用心地替换了您认为应该存在于某个位置的文件，或者该文件可能只有一个字节。更为奇怪的事情已经发生了。</p>
<p>要处理这一情况，您不但需要检查读取操作是否成功，还必须检查以确保您已经读取了正确数量的字节。</p>
<pre>LONG buffer[CHUNK_SIZE];
BOOL ok;
ok = ReadFile(hFile, (LPVOID)buffer,
CHUNK_SIZE*sizeof(LONG), &amp;cbRead, NULL);
if (ok &amp;&amp; cbRead &gt; sizeof(LONG)) {
if (buffer[0] == 0)
...
}
else
// handle the read failure; for example
...
</pre>
<p>无庸讳言，上述代码有点儿复杂。但是，编写可靠的代码要比编写并不总是能够正常工作的代码更为复杂。对上述代码产生性能方面的疑问是很正常的。虽然添加了几个测试，但在全局上下文（函数调用、磁盘操作，至少复制 CHUNK_SIZE * sizeof(LONG) 个字节）中，其影响是极小的。</p>
<p>通常，每当需要进行返回值检查时，总是涉及到一个函数调用，因此性能开销不太重要。在某些情况下，编译器可能会内联该函数，但是如果发生这种行为，并且由于返回常数而实际上不需要检查返回值时，编译器会将测试优化掉。</p>
<p>诚然，还是有一些特殊情况：您通过删除返回值检查而节省的少数 CPU 循环至关重要；编译器无法为您提供帮助；您控制了要调用的函数的行为。在上述情况下，省略一些返回值检查是有意义的。如果您认为自己处于类似的情形，则应该与其他开发人员进行讨论，重新审视真正的性能折衷，然后，如果您仍然确信这样做是正确的，则在代码中每个省略返回值检查的地方加上明确的注释，说明您的决定并证明它的正确性。</p>
<h4 style="FONT-WEIGHT: bold; FONT-SIZE: 1em; COLOR: rgb(63,82,156); FONT-FAMILY: 'Segoe UI', Verdana, Arial">总是检查内存分配</h4>
<p>无论是使用<span class=Apple-converted-space>&nbsp;</span><strong>HeapAlloc</strong>、<strong>VirtualAlloc</strong>、<strong>IMalloc::Alloc</strong>、<strong>SysAllocString</strong>、<strong>GlobalAlloc</strong>、<strong>malloc</strong><span class=Apple-converted-space>&nbsp;</span>还是任何 C++ 运算符 new，您都不能想当然地认为内存分配成功。同样的道理也适用于其他各种资源，包括 GDI 对象、文件、注册表项等等。</p>
<p>下面是一个很好的独立于平台的错误代码示例，这些代码是在 C 标准库的基础上编写的：</p>
<pre>char *str;
str = (char *)malloc(MAX_PATH);
str[0] = 0;         // WRONG!!!
...
</pre>
<p>在此例中，如果内存耗尽，则<span class=Apple-converted-space>&nbsp;</span><strong>malloc</strong><span class=Apple-converted-space>&nbsp;</span>将返回 NULL，而 str 的反引用将是 NULL 指针的反引用。这会造成访问冲突，从而导致程序崩溃。除非您是在内核模式下运行（例如，设备驱动程序），否则访问冲突将导致蓝屏或可利用的安全漏洞。</p>
<p>解决方案非常简单。检查返回值是否为 NULL，并执行正确的操作。</p>
<pre>char *str;
str = (char *)malloc(MAX_PATH);
if (str != NULL)
{
str[0] = 0;
...
}
</pre>
<p>与返回值检查一样，许多人相信实际上不会发生内存分配问题。诚然，该问题并不总是发生，但这并不意味着它永远不会发生。如果您让成千上万（或数以百万）的用户运行您的软件，即使该问题每月仅对每个用户发生一次，后果也是严重的。</p>
<p>许多人相信，在内存耗尽时做什么都是无所谓的。程序应该退出。但是，这在许多方面都不适用。首先，假设程序在内存耗尽时退出，那么将不会保存数据文件。其次，人们通常期望服务和应用程序能够长期运行，因此它们能够在内存暂时不足时继续正常运行是至关重要的。第三，对于在嵌入式环境中运行的软件而言，退出不是可行的选择。处理内存分配可能非常麻烦，但这件事情必须做。</p>
<p>有时，意外的 NULL 指针可能不会导致程序崩溃，但这仍然不是件好事情。</p>
<pre>HBITMAP hBitmap;
HBITMAP hOldBitmap;
hBitmap = CreateBitmap(. . .);
hOldBitmap = SelectObject(hDC, hBitmap);   // WRONG!!!
...
</pre>
<p><strong>SelectObject</strong><span class=Apple-converted-space>&nbsp;</span>的文档在对 NULL 位图执行哪些操作方面含糊不清。这可能不会导致崩溃，但它显然是不可靠的。代码很可能出于某种原因而创建位图，并希望用它来进行一些绘图工作。但是，因为它未能创建位图，所以绘图操作将不会发生。即使代码没有崩溃，这里也明显存在一个错误。同样，您需要进行检查。</p>
<pre>HBITMAP hBitmap;
HBITMAP hOldBitmap;
hBitmap = CreateBitmap(. . .);
if (hBitmap != NULL)
{
hOldBitmap = SelectObject(hDC, hBitmap);
...
}
else
...
</pre>
<p>当您使用 C++ 运算符 new 时，事情开始变得更加有趣。例如，如果您要使用 MFC，则全局运算符 new 将在内存耗尽时引发一个异常。这意味着您不能执行以下操作：</p>
<pre>int *ptr1;
int *ptr2;
ptr1 = new int[10];
ptr2 = new int[10];   // WRONG!!!!
</pre>
<p>如果第二次内存分配引发异常，则第一次分配的内存将泄漏。如果您的代码嵌入到将要长期运行的服务或应用程序中，则这些泄漏会累积起来。</p>
<p>只是捕捉异常是不够的，您的异常处理代码还必须是正确的。不要掉到下面这个诱人的陷阱中：</p>
<pre>int *ptr1;
int *ptr2;
try {
ptr1 = new int[10];
ptr2 = new int[10];
}
catch (CMemoryException *ex) {
delete [] ptr1;   // WRONG!!!
delete [] ptr2;    // WRONG!!!
}
</pre>
<p>如果第一次内存分配引发了异常，您将捕捉该异常，但要删除一个未初始化的指针。如果您足够幸运，这将导致即时访问冲突和崩溃。更有可能的是，它将导致堆损坏，从而造成数据损坏和/或在将来难以调试的崩溃。尽力初始化下列变量是值得的：</p>
<pre>int *ptr1 = 0;
int *ptr2 = 0;
try {
ptr1 = new int[10];
ptr2 = new int[10];
}
catch (CMemoryException *ex) {
delete [] ptr1;
delete [] ptr2;
}
</pre>
<p>应该指出的是，C++ 运算符 new 有许多微妙之处。您可以用多种不同的方式来修改全局运算符 new 的行为。不同的类可以具有它们自己的运算符 new，并且如果您不使用 MFC，则可能会看到不同的默认行为。例如，在内存分配失败时返回 NULL，而不是引发异常。有关该主题的详细信息，请参阅 Bobby Schmidt 的&#8220;Deep C++&#8221;系列文章中有关处理异常的<a style="COLOR: rgb(19,100,196); TEXT-DECORATION: none" href="http://msdn.microsoft.com/library/en-us/dndeepc/html/deep081999.asp">第 7 部分</a>。</p>
<div><a style="COLOR: rgb(19,100,196); TEXT-DECORATION: none" href="http://msdn.microsoft.com/zh-cn/library/ms809695.aspx#mainSection"><img style="BORDER-TOP-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-RIGHT-WIDTH: 0px" alt="" src="http://i.msdn.microsoft.com/dynimg/IC170569.gif" border=0><span class=Apple-converted-space>&nbsp;</span></a><a style="COLOR: rgb(19,100,196); TEXT-DECORATION: none" href="http://msdn.microsoft.com/zh-cn/library/ms809695.aspx#mainSection">返回页首</a><span class=Apple-converted-space>&nbsp;</span><br></div>
<h3 id=EBAA style="FONT-WEIGHT: bold; FONT-SIZE: 1.07em; COLOR: rgb(63,82,156); FONT-FAMILY: 'Segoe UI', Verdana, Arial">小结</h3>
<p>如果您要编写可靠的代码，则至关重要的一点是从一开始就考虑如何处理异常事件。您不能事后再考虑对异常事件的处理。在考虑此类事件时，错误处理是一个关键的方面。</p>
<p>错误处理很难正确执行。尽管本文只是粗浅地讨论了这一问题，但其中介绍的原则奠定了一个强大的基础。请记住以下要点：</p>
<ul style="PADDING-LEFT: 0px; MARGIN-LEFT: 40px; LIST-STYLE-TYPE: disc">
    <li style="LIST-STYLE-IMAGE: url(http://i3.msdn.microsoft.com/Hash/fcc063e32e5c2395ba48efadd8423b60.gif)">
    <p>在设计应用程序（或 API）时，应预先考虑您喜欢的错误处理范例。</p>
    </li>
    <li style="LIST-STYLE-IMAGE: url(http://i3.msdn.microsoft.com/Hash/fcc063e32e5c2395ba48efadd8423b60.gif)">
    <p>在使用 API 时，应了解它的错误处理范例。</p>
    </li>
    <li style="LIST-STYLE-IMAGE: url(http://i3.msdn.microsoft.com/Hash/fcc063e32e5c2395ba48efadd8423b60.gif)">
    <p>如果您处于存在多个错误处理范例的情况下，请警惕可能造成混乱的根源。</p>
    </li>
    <li style="LIST-STYLE-IMAGE: url(http://i3.msdn.microsoft.com/Hash/fcc063e32e5c2395ba48efadd8423b60.gif)">
    <p>总是检查返回状态。</p>
    </li>
    <li style="LIST-STYLE-IMAGE: url(http://i3.msdn.microsoft.com/Hash/fcc063e32e5c2395ba48efadd8423b60.gif)">
    <p>总是检查内存分配。</p>
    </li>
</ul>
<p>如果您执行了上述所有操作，就能够编写出可靠的应用程序。</span></span></p>
<img src ="http://www.cppblog.com/ietj/aggbug/144610.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/ietj/" target="_blank">一路风尘</a> 2011-04-19 23:40 <a href="http://www.cppblog.com/ietj/articles/144610.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>如何编写异常安全的C++代码</title><link>http://www.cppblog.com/ietj/articles/144566.html</link><dc:creator>一路风尘</dc:creator><author>一路风尘</author><pubDate>Tue, 19 Apr 2011 09:09:00 GMT</pubDate><guid>http://www.cppblog.com/ietj/articles/144566.html</guid><wfw:comment>http://www.cppblog.com/ietj/comments/144566.html</wfw:comment><comments>http://www.cppblog.com/ietj/articles/144566.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/ietj/comments/commentRss/144566.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/ietj/services/trackbacks/144566.html</trackback:ping><description><![CDATA[<span class=Apple-style-span style="WORD-SPACING: 0px; FONT: medium Simsun; TEXT-TRANSFORM: none; COLOR: rgb(0,0,0); TEXT-INDENT: 0px; WHITE-SPACE: normal; LETTER-SPACING: normal; BORDER-COLLAPSE: separate; orphans: 2; widows: 2; webkit-border-horizontal-spacing: 0px; webkit-border-vertical-spacing: 0px; webkit-text-decorations-in-effect: none; webkit-text-size-adjust: auto; webkit-text-stroke-width: 0px"><span class=Apple-style-span style="FONT-SIZE: 14px; LINE-HEIGHT: 21px; FONT-FAMILY: Arial">
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">关于<span lang=EN-US>C++</span>中异常的争论何其多也，但往往是一些不合事实的误解。异常曾经是一个难以用好的语言特性，幸运的是，随着<span lang=EN-US>C++</span>社区经验的积累，今天我们已经有足够的知识轻松编写异常安全的代码了，而且编写异常安全的代码一般也不会对性能造成影响。<span lang=EN-US><o:p></o:p></span></p>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　使用异常还是返回错误码？这是个争论不休的话题。大家一定听说过这样的说法：只有在真正异常的时候，才使用异常。那什么是<span lang=EN-US>&#8220;</span>真正异常的时候<span lang=EN-US>&#8221;</span>？在回答这个问题以前，让我们先看一看程序设计中的不变式原理。<span lang=EN-US><o:p></o:p></span></p>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　对象就是属性聚合加方法，如何判定一个对象的属性聚合是不是处于逻辑上正确的状态呢？这可以通过一系列的断言，最后下一个结论说：这个对象的属性聚合逻辑上是正确的或者是有问题的。这些断言就是衡量对象属性聚合对错的不变式。<span lang=EN-US><o:p></o:p></span></p>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　我们通常在函数调用中，实施不变式的检查。不变式分为三类：前条件，后条件和不变式。前条件是指在函数调用之前，必须满足的逻辑条件，后条件是函数调用后必须满足的逻辑条件，不变式则是整个函数执行中都必须满足的条件。在我们的讨论中，不变式既是前条件又是后条件。前条件是必须满足的，如果不满足，那就是程序逻辑错误，后条件则不一定。现在，我们可以用不变式来严格定义异常状况了：满足前条件，但是无法满足后条件，即为异常状况。当且仅当发生异常状况时，才抛出异常。<span lang=EN-US><o:p></o:p></span></p>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　关于何时抛出异常的回答中，并不排斥返回值报告错误，而且这两者是正交的。然而，从我们经验上来说，完全可以在这两者中加以选择，这又是为什么呢？事实上，当我们做出这种选择时，必然意味着接口语意的改变，在不改变接口的情况下，其实是无法选择的<span lang=EN-US>(</span>试试看，用返回值处理构造函数中的错误<span lang=EN-US>)</span>。通过不变式区别出正常和异常状况，还可以更好地提炼接口。<span lang=EN-US><o:p></o:p></span></p>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　对于异常安全的评定，可分为三个级别：基本保证、强保证和不会失败。<span lang=EN-US><o:p></o:p></span></p>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　基本保证：确保出现异常时程序（对象）处于未知但有效的状态。所谓有效，即对象的不变式检查全部通过。<span lang=EN-US><br></span>　　强保证：确保操作的事务性，要么成功，程序处于目标状态，要么不发生改变。<span lang=EN-US><br></span>　　不会失败：对于大多数函数来说，这是很难保证的。对于<span lang=EN-US>C++</span>程序，至少析构函数、释放函数和<span lang=EN-US>swap</span>函数要确保不会失败，这是编写异常安全代码的基础。<span lang=EN-US><o:p></o:p></span></p>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　首先从异常情况下资源管理的问题开始<span lang=EN-US>.</span>很多人可能都这么干过<span lang=EN-US>:<o:p></o:p></span></p>
<div style="BORDER-RIGHT: rgb(0,153,204) 1pt solid; PADDING-RIGHT: 4pt; BORDER-TOP: rgb(0,153,204) 1pt solid; PADDING-LEFT: 4pt; PADDING-BOTTOM: 4pt; MARGIN-LEFT: 3.75pt; BORDER-LEFT: rgb(0,153,204) 1pt solid; MARGIN-RIGHT: 3.75pt; PADDING-TOP: 4pt; BORDER-BOTTOM: rgb(0,153,204) 1pt solid; BACKGROUND-COLOR: rgb(221,237,251); background-origin: initial; background-clip: initial">
<p class=code style="MARGIN: 3.75pt 0cm; BACKGROUND-COLOR: rgb(221,237,251); TEXT-ALIGN: justify; background-origin: initial; background-clip: initial"><span lang=EN-US style="FONT-SIZE: 12pt; COLOR: windowtext; FONT-FAMILY: 宋体">Type* obj = new Type;<br>try{&nbsp; do_something...}<br>catch(...){ delete obj; throw;}<o:p></o:p></span></p>
</div>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　不要这么做<span lang=EN-US>!</span>这么做只会使你的代码看上去混乱<span lang=EN-US>,</span>而且会降低效率<span lang=EN-US>,</span>这也是一直以来异常名声不大好的原因之一<span lang=EN-US>.<span class=Apple-converted-space>&nbsp;</span></span>请借助于<span lang=EN-US>RAII</span>技术来完成这样的工作<span lang=EN-US>:<o:p></o:p></span></p>
<div style="BORDER-RIGHT: rgb(0,153,204) 1pt solid; PADDING-RIGHT: 4pt; BORDER-TOP: rgb(0,153,204) 1pt solid; PADDING-LEFT: 4pt; PADDING-BOTTOM: 4pt; MARGIN-LEFT: 3.75pt; BORDER-LEFT: rgb(0,153,204) 1pt solid; MARGIN-RIGHT: 3.75pt; PADDING-TOP: 4pt; BORDER-BOTTOM: rgb(0,153,204) 1pt solid; BACKGROUND-COLOR: rgb(221,237,251); background-origin: initial; background-clip: initial">
<p class=code style="MARGIN: 3.75pt 0cm; BACKGROUND-COLOR: rgb(221,237,251); TEXT-ALIGN: justify; background-origin: initial; background-clip: initial"><span lang=EN-US style="FONT-SIZE: 12pt; COLOR: windowtext; FONT-FAMILY: 宋体">auto_ptr&lt;Type&gt; obj_ptr(new Type);<br>do_something...<o:p></o:p></span></p>
</div>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　这样的代码简洁、安全而且无损于效率。当你不关心或是无法处理异常时<span lang=EN-US>,</span>请不要试图捕获它。并非使用<span lang=EN-US>try...catch</span>才能编写异常安全的代码<span lang=EN-US>,</span>大部分异常安全的代码都不需要<span lang=EN-US>try...catch</span>。我承认，现实世界并非总是如上述的例子那样简单，但是这个例子确实可以代表很多异常安全代码的做法。在这个例子中，<span lang=EN-US>boost::scoped_ptr</span>是<span lang=EN-US>auto_ptr</span>一个更适合的替代品。<span lang=EN-US><o:p></o:p></span></p>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　现在来考虑这样一个构造函数：<span lang=EN-US><o:p></o:p></span></p>
<div style="BORDER-RIGHT: rgb(0,153,204) 1pt solid; PADDING-RIGHT: 4pt; BORDER-TOP: rgb(0,153,204) 1pt solid; PADDING-LEFT: 4pt; PADDING-BOTTOM: 4pt; MARGIN-LEFT: 3.75pt; BORDER-LEFT: rgb(0,153,204) 1pt solid; MARGIN-RIGHT: 3.75pt; PADDING-TOP: 4pt; BORDER-BOTTOM: rgb(0,153,204) 1pt solid; BACKGROUND-COLOR: rgb(221,237,251); background-origin: initial; background-clip: initial">
<p class=code style="MARGIN: 3.75pt 0cm; BACKGROUND-COLOR: rgb(221,237,251); TEXT-ALIGN: justify; background-origin: initial; background-clip: initial"><span lang=EN-US style="FONT-SIZE: 12pt; COLOR: windowtext; FONT-FAMILY: 宋体">Type() : m_a(new TypeA), m_b(new TypeB){}<o:p></o:p></span></p>
</div>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　假设成员变量<span lang=EN-US>m_a</span>和<span lang=EN-US>m_b</span>是原始的指针类型，并且和<span lang=EN-US>Type</span>内的申明顺序一致。这样的代码是不安全的，它存在资源泄漏问题，构造函数的失败回滚机制无法应对这样的问题。如果<span lang=EN-US>new TypeB</span>抛出异常<span lang=EN-US>,new TypeA</span>返回的资源是得不到释放机会的<span lang=EN-US>.</span>曾经<span lang=EN-US>,</span>很多人用这样的方法避免异常<span lang=EN-US>:<o:p></o:p></span></p>
<div style="BORDER-RIGHT: rgb(0,153,204) 1pt solid; PADDING-RIGHT: 4pt; BORDER-TOP: rgb(0,153,204) 1pt solid; PADDING-LEFT: 4pt; PADDING-BOTTOM: 4pt; MARGIN-LEFT: 3.75pt; BORDER-LEFT: rgb(0,153,204) 1pt solid; MARGIN-RIGHT: 3.75pt; PADDING-TOP: 4pt; BORDER-BOTTOM: rgb(0,153,204) 1pt solid; BACKGROUND-COLOR: rgb(221,237,251); background-origin: initial; background-clip: initial">
<p class=code style="MARGIN: 3.75pt 0cm; BACKGROUND-COLOR: rgb(221,237,251); TEXT-ALIGN: justify; background-origin: initial; background-clip: initial"><span lang=EN-US style="FONT-SIZE: 12pt; COLOR: windowtext; FONT-FAMILY: 宋体">Type() : m_a(NULL), m_b(NULL){<br>&nbsp;&nbsp;&nbsp;&nbsp; auto_ptr&lt;TypeA&gt; tmp_a(new TypeA);<br>&nbsp;&nbsp;&nbsp;&nbsp; auto_ptr&lt;TypeB&gt; tmp_b(new TypeB);<br>&nbsp;&nbsp;&nbsp;&nbsp; m_a = tmp_a.release();<br>&nbsp;&nbsp;&nbsp;&nbsp; m_b = tmp_b.release();<br>}<o:p></o:p></span></p>
</div>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　当然<span lang=EN-US>,</span>这样的方法确实是能够实现异常安全的代码的<span lang=EN-US>,</span>而且其中实现思想将是非常重要的<span lang=EN-US>,</span>在如何实现强保证的异常安全代码中会采用这种思想<span lang=EN-US>.</span>然而这种做法不够彻底，至少析构函数还是要手动完成的。我们仍然可以借助<span lang=EN-US>RAII</span>技术，把这件事做得更为彻底：<span lang=EN-US>shared_ptr&lt;TypeA&gt; m_a; shared_ptr&lt;TypeB&gt; m_b;</span>这样<span lang=EN-US>,</span>我们就可以轻而易举地写出异常安全的代码<span lang=EN-US>:<o:p></o:p></span></p>
<div style="BORDER-RIGHT: rgb(0,153,204) 1pt solid; PADDING-RIGHT: 4pt; BORDER-TOP: rgb(0,153,204) 1pt solid; PADDING-LEFT: 4pt; PADDING-BOTTOM: 4pt; MARGIN-LEFT: 3.75pt; BORDER-LEFT: rgb(0,153,204) 1pt solid; MARGIN-RIGHT: 3.75pt; PADDING-TOP: 4pt; BORDER-BOTTOM: rgb(0,153,204) 1pt solid; BACKGROUND-COLOR: rgb(221,237,251); background-origin: initial; background-clip: initial">
<p class=code style="MARGIN: 3.75pt 0cm; BACKGROUND-COLOR: rgb(221,237,251); TEXT-ALIGN: justify; background-origin: initial; background-clip: initial"><span lang=EN-US style="FONT-SIZE: 12pt; COLOR: windowtext; FONT-FAMILY: 宋体">Type() : m_a(new TypeA), m_b(new TypeB){}<o:p></o:p></span></p>
</div>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　如果你觉得<span lang=EN-US>shared_ptr</span>的性能不能满足要求，可以编写一个接口类似<span lang=EN-US>scoped_ptr</span>的智能指针类<span lang=EN-US>,</span>在析构函数中释放资源即可。如果类设计成不可复制的，也可以直接用<span lang=EN-US>scoped_ptr</span>。强烈建议不要把<span lang=EN-US>auto_ptr</span>作为数据成员使用<span lang=EN-US>,scoped_ptr</span>虽然名字不大好，但是至少很安全而且不会导致混乱。<span lang=EN-US><o:p></o:p></span></p>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　<span lang=EN-US>RAII</span>技术并不仅仅用于上述例子中，所有必须成对出现的操作都可以通过这一技术完成而不必<span lang=EN-US>try...catch.</span>下面的代码也是常见的：<span lang=EN-US><o:p></o:p></span></p>
<div style="BORDER-RIGHT: rgb(0,153,204) 1pt solid; PADDING-RIGHT: 4pt; BORDER-TOP: rgb(0,153,204) 1pt solid; PADDING-LEFT: 4pt; PADDING-BOTTOM: 4pt; MARGIN-LEFT: 3.75pt; BORDER-LEFT: rgb(0,153,204) 1pt solid; MARGIN-RIGHT: 3.75pt; PADDING-TOP: 4pt; BORDER-BOTTOM: rgb(0,153,204) 1pt solid; BACKGROUND-COLOR: rgb(221,237,251); background-origin: initial; background-clip: initial">
<p class=code style="MARGIN: 3.75pt 0cm; BACKGROUND-COLOR: rgb(221,237,251); TEXT-ALIGN: justify; background-origin: initial; background-clip: initial"><span lang=EN-US style="FONT-SIZE: 12pt; COLOR: windowtext; FONT-FAMILY: 宋体">a_lock.lock();<span class=Apple-converted-space>&nbsp;</span><br>try{ ...} catch(...) {a_lock.unlock();throw;}<br>a_lock.unlock();<o:p></o:p></span></p>
</div>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　可以这样解决，先提供一个成对操作的辅助类：<span lang=EN-US><o:p></o:p></span></p>
<div style="BORDER-RIGHT: rgb(0,153,204) 1pt solid; PADDING-RIGHT: 4pt; BORDER-TOP: rgb(0,153,204) 1pt solid; PADDING-LEFT: 4pt; PADDING-BOTTOM: 4pt; MARGIN-LEFT: 3.75pt; BORDER-LEFT: rgb(0,153,204) 1pt solid; MARGIN-RIGHT: 3.75pt; PADDING-TOP: 4pt; BORDER-BOTTOM: rgb(0,153,204) 1pt solid; BACKGROUND-COLOR: rgb(221,237,251); background-origin: initial; background-clip: initial">
<p class=code style="MARGIN: 3.75pt 0cm; BACKGROUND-COLOR: rgb(221,237,251); TEXT-ALIGN: justify; background-origin: initial; background-clip: initial"><span lang=EN-US style="FONT-SIZE: 12pt; COLOR: windowtext; FONT-FAMILY: 宋体">struct scoped_lock{<br>&nbsp;&nbsp;&nbsp; explicit scoped_lock(Lock&amp; lock) : m_l(lock){m_l.lock();}<br>&nbsp;&nbsp;&nbsp; ~scoped_lock(){m_l.unlock();}<br>private:&nbsp;<span class=Apple-converted-space>&nbsp;</span><br>&nbsp;&nbsp;&nbsp; Lock&amp; m_l;<br>};<o:p></o:p></span></p>
</div>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　然后，代码只需这样写：<span lang=EN-US><o:p></o:p></span></p>
<div style="BORDER-RIGHT: rgb(0,153,204) 1pt solid; PADDING-RIGHT: 4pt; BORDER-TOP: rgb(0,153,204) 1pt solid; PADDING-LEFT: 4pt; PADDING-BOTTOM: 4pt; MARGIN-LEFT: 3.75pt; BORDER-LEFT: rgb(0,153,204) 1pt solid; MARGIN-RIGHT: 3.75pt; PADDING-TOP: 4pt; BORDER-BOTTOM: rgb(0,153,204) 1pt solid; BACKGROUND-COLOR: rgb(221,237,251); background-origin: initial; background-clip: initial">
<p class=code style="MARGIN: 3.75pt 0cm; BACKGROUND-COLOR: rgb(221,237,251); TEXT-ALIGN: justify; background-origin: initial; background-clip: initial"><span lang=EN-US style="FONT-SIZE: 12pt; COLOR: windowtext; FONT-FAMILY: 宋体">scoped_lock guard(a_lock);<br>do_something...<o:p></o:p></span></p>
</div>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　清晰而优雅！继续考察这个例子<span lang=EN-US>,</span>假设我们并不需要成对操作<span lang=EN-US>,<span class=Apple-converted-space>&nbsp;</span></span>显然，修改<span lang=EN-US>scoped_lock</span>构造函数即可解决问题。然而，往往方法名称和参数也不是那么固定的，怎么办？可以借助这样一个辅助类：<span lang=EN-US><o:p></o:p></span></p>
<div style="BORDER-RIGHT: rgb(0,153,204) 1pt solid; PADDING-RIGHT: 4pt; BORDER-TOP: rgb(0,153,204) 1pt solid; PADDING-LEFT: 4pt; PADDING-BOTTOM: 4pt; MARGIN-LEFT: 3.75pt; BORDER-LEFT: rgb(0,153,204) 1pt solid; MARGIN-RIGHT: 3.75pt; PADDING-TOP: 4pt; BORDER-BOTTOM: rgb(0,153,204) 1pt solid; BACKGROUND-COLOR: rgb(221,237,251); background-origin: initial; background-clip: initial">
<p class=code style="MARGIN: 3.75pt 0cm; BACKGROUND-COLOR: rgb(221,237,251); TEXT-ALIGN: justify; background-origin: initial; background-clip: initial"><span lang=EN-US style="FONT-SIZE: 12pt; COLOR: windowtext; FONT-FAMILY: 宋体">template&lt;typename FEnd, typename FBegin&gt;<br>struct pair_guard{<br>&nbsp;&nbsp;&nbsp; pair_guard(FEnd fe, FBegin fb) : m_fe(fe) {if (fb) fb();}<br>&nbsp;&nbsp;&nbsp;&nbsp; ~pair_guard(){m_fe();}<br>private:<br>&nbsp;&nbsp;&nbsp;&nbsp; FEnd m_fe;<br>&nbsp;&nbsp;&nbsp;&nbsp; ...//</span><span style="FONT-SIZE: 12pt; COLOR: windowtext; FONT-FAMILY: 宋体">禁止复制<span lang=EN-US><br>};<br>typedef pair_guard&lt;function&lt;void () &gt; , function&lt;void()&gt; &gt; simple_pair_guard;<o:p></o:p></span></span></p>
</div>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　好了，借助<span lang=EN-US>boost</span>库<span lang=EN-US>,</span>我们可以这样来编写代码了：<span lang=EN-US><o:p></o:p></span></p>
<div style="BORDER-RIGHT: rgb(0,153,204) 1pt solid; PADDING-RIGHT: 4pt; BORDER-TOP: rgb(0,153,204) 1pt solid; PADDING-LEFT: 4pt; PADDING-BOTTOM: 4pt; MARGIN-LEFT: 3.75pt; BORDER-LEFT: rgb(0,153,204) 1pt solid; MARGIN-RIGHT: 3.75pt; PADDING-TOP: 4pt; BORDER-BOTTOM: rgb(0,153,204) 1pt solid; BACKGROUND-COLOR: rgb(221,237,251); background-origin: initial; background-clip: initial">
<p class=code style="MARGIN: 3.75pt 0cm; BACKGROUND-COLOR: rgb(221,237,251); TEXT-ALIGN: justify; background-origin: initial; background-clip: initial"><span lang=EN-US style="FONT-SIZE: 12pt; COLOR: windowtext; FONT-FAMILY: 宋体">simple_pair_guard guard(bind(&amp;Lock::unlock, a_lock), bind(&amp;Lock::lock, a_lock) );<br>do_something...<o:p></o:p></span></p>
</div>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　我承认，这样的代码不如前面的简洁和容易理解，但是它更灵活<span lang=EN-US>,</span>无论函数名称是什么，都可以拿来结对。我们可以加强对<span lang=EN-US>bind</span>的运用，结合占位符和<span lang=EN-US>reference_wrapper,</span>就可以处理函数参数、动态绑定变量。所有我们在<span lang=EN-US>catch</span>内外的相同工作，交给<span lang=EN-US>pair_guard</span>去完成即可。<span lang=EN-US><o:p></o:p></span></p>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　考察前面的几个例子，也许你已经发现了，所谓异常安全的代码，竟然就是如何避免<span lang=EN-US>try...catch</span>的代码，这和直觉似乎是违背的。有些时候，事情就是如此违背直觉。异常是无处不在的，当你不需要关心异常或者无法处理异常的时候，就应该避免捕获异常。除非你打算捕获所有异常，否则，请务必把未处理的异常再次抛出。<span lang=EN-US>try...catch</span>的方式固然能够写出异常安全的代码，但是那样的代码无论是清晰性和效率都是难以忍受的，而这正是很多人抨击<span lang=EN-US>C++</span>异常的理由。在<span lang=EN-US>C++</span>的世界，就应该按照<span lang=EN-US>C++</span>的法则来行事。<span lang=EN-US><o:p></o:p></span></p>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　如果按照上述的原则行事，能够实现基本保证了吗？诚恳地说，基础设施有了，但技巧上还不够，让我们继续分析不够的部分。<span lang=EN-US><o:p></o:p></span></p>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　对于一个方法常规的执行过程，我们在方法内部可能需要多次修改对象状态，在方法执行的中途，对象是可能处于非法状态的（非法状态<span lang=EN-US><span class=Apple-converted-space>&nbsp;</span>!=<span class=Apple-converted-space>&nbsp;</span></span>未知状态），如果此时发生异常，对象将变得无效。利用前述的手段，在<span lang=EN-US>pair_guard</span>的析构中修复对象是可行的，但缺乏效率，代码将变得复杂。最好的办法是<span lang=EN-US>......</span>是避免这么作，这么说有点不厚道，但并非毫无道理。当对象处于非法状态时，意味着此时此刻对象不能安全重入、不能共享。现实一点的做法是：<span lang=EN-US><o:p></o:p></span></p>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　<span lang=EN-US>a.</span>每一次修改对象，都确保对象处于合法状态<span lang=EN-US><br></span>　　<span lang=EN-US>b.</span>或者当对象处于非法状态时，所有操作决不会失败。<span lang=EN-US><o:p></o:p></span></p>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　在接下来的强保证的讨论中细述如何做到这两点。<span lang=EN-US><o:p></o:p></span></p>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　强保证是事务性的，这个事务性和数据库的事务性有区别，也有共通性。实现强保证的原则做法是：在可能失败的过程中计算出对象的目标状态，但是不修改对象，在决不失败的过程中，把对象替换到目标状态。考察一个不安全的字符串赋值方法：<span lang=EN-US><o:p></o:p></span></p>
<div style="BORDER-RIGHT: rgb(0,153,204) 1pt solid; PADDING-RIGHT: 4pt; BORDER-TOP: rgb(0,153,204) 1pt solid; PADDING-LEFT: 4pt; PADDING-BOTTOM: 4pt; MARGIN-LEFT: 3.75pt; BORDER-LEFT: rgb(0,153,204) 1pt solid; MARGIN-RIGHT: 3.75pt; PADDING-TOP: 4pt; BORDER-BOTTOM: rgb(0,153,204) 1pt solid; BACKGROUND-COLOR: rgb(221,237,251); background-origin: initial; background-clip: initial">
<p class=code style="MARGIN: 3.75pt 0cm; BACKGROUND-COLOR: rgb(221,237,251); TEXT-ALIGN: justify; background-origin: initial; background-clip: initial"><span lang=EN-US style="FONT-SIZE: 12pt; COLOR: windowtext; FONT-FAMILY: 宋体">string&amp; operator=(const string&amp; rsh){<br>&nbsp;&nbsp;&nbsp; if (this != &amp;rsh){<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; myalloc locked_pool(m_data);<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; locked_pool.deallocate(m_data);<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (rsh.empty())<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m_data = NULL;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; else{<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m_data = locked_pool.allocate(rsh.size() + 1);<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; never_failed_copy(m_data, rsh.m_data, rsh.size() + 1);<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br>&nbsp;&nbsp;&nbsp; }<br>&nbsp;&nbsp;&nbsp; return *this;<br>}<o:p></o:p></span></p>
</div>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　<span lang=EN-US>locked_pool</span>是为了锁定内存页。为了讨论的简单起见，我们假设只有<span lang=EN-US>locked_pool</span>构造函数和<span lang=EN-US>allocate</span>是可能抛出异常的，那么这段代码连基本保证也没有做到。若<span lang=EN-US>allocate</span>失败，则<span lang=EN-US>m_data</span>取值将是非法的。参考上面的<span lang=EN-US>b</span>条目，我们可以这样修改代码：<span lang=EN-US><o:p></o:p></span></p>
<div style="BORDER-RIGHT: rgb(0,153,204) 1pt solid; PADDING-RIGHT: 4pt; BORDER-TOP: rgb(0,153,204) 1pt solid; PADDING-LEFT: 4pt; PADDING-BOTTOM: 4pt; MARGIN-LEFT: 3.75pt; BORDER-LEFT: rgb(0,153,204) 1pt solid; MARGIN-RIGHT: 3.75pt; PADDING-TOP: 4pt; BORDER-BOTTOM: rgb(0,153,204) 1pt solid; BACKGROUND-COLOR: rgb(221,237,251); background-origin: initial; background-clip: initial">
<p class=code style="MARGIN: 3.75pt 0cm; BACKGROUND-COLOR: rgb(221,237,251); TEXT-ALIGN: justify; background-origin: initial; background-clip: initial"><span lang=EN-US style="FONT-SIZE: 12pt; COLOR: windowtext; FONT-FAMILY: 宋体">myalloc locked_pool(m_data);<br>&nbsp;&nbsp;&nbsp; locked_pool.deallocate(m_data);&nbsp;&nbsp; //</span><span style="FONT-SIZE: 12pt; COLOR: windowtext; FONT-FAMILY: 宋体">进入非法状态<span lang=EN-US><br>&nbsp;&nbsp;&nbsp; m_data = NULL;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //</span>立刻再次回到合法状态<span lang=EN-US>,</span>且不会失败<span lang=EN-US><br>&nbsp;&nbsp;&nbsp; if(!rsh.empty()){<br>&nbsp;&nbsp;&nbsp; m_data = locked_pool.allocate(rsh.size() + 1);<br>&nbsp;&nbsp;&nbsp; never_failed_memcopy(m_data, rsh.m_data, rsh.size() + 1);<br>}<o:p></o:p></span></span></p>
</div>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　现在，如果<span lang=EN-US>locked_pool</span>失败，对象不发生改变。如果<span lang=EN-US>allocate</span>失败，对象是一个空字符串，这既不是初始状态，也不是我们预期的目标状态，但它是一个合法状态。我们阐明了实现基本保证所需要的技巧部分，结合前述的基础设施（<span lang=EN-US>RAII</span>的运用），完全可以实现基本保证了<span lang=EN-US>...</span>哦<span lang=EN-US>,</span>其实还是有一点疏漏，不过，那就留到最后吧。<span lang=EN-US><o:p></o:p></span></p>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　继续，让上面的代码实现强保证：<span lang=EN-US><o:p></o:p></span></p>
<div style="BORDER-RIGHT: rgb(0,153,204) 1pt solid; PADDING-RIGHT: 4pt; BORDER-TOP: rgb(0,153,204) 1pt solid; PADDING-LEFT: 4pt; PADDING-BOTTOM: 4pt; MARGIN-LEFT: 3.75pt; BORDER-LEFT: rgb(0,153,204) 1pt solid; MARGIN-RIGHT: 3.75pt; PADDING-TOP: 4pt; BORDER-BOTTOM: rgb(0,153,204) 1pt solid; BACKGROUND-COLOR: rgb(221,237,251); background-origin: initial; background-clip: initial">
<p class=code style="MARGIN: 3.75pt 0cm; BACKGROUND-COLOR: rgb(221,237,251); TEXT-ALIGN: justify; background-origin: initial; background-clip: initial"><span lang=EN-US style="FONT-SIZE: 12pt; COLOR: windowtext; FONT-FAMILY: 宋体">myalloc locked_pool(m_data);<br>&nbsp;&nbsp;&nbsp; char* tmp<span class=Apple-converted-space>&nbsp;</span></span><span style="FONT-SIZE: 12pt; COLOR: windowtext; FONT-FAMILY: 宋体">＝<span lang=EN-US><span class=Apple-converted-space>&nbsp;</span>NULL;<br>&nbsp;&nbsp;&nbsp; if(!rsh.empty()){<br>&nbsp;&nbsp;&nbsp; tmp = locked_pool.allocate(rsh.size() + 1);<span class=Apple-converted-space>&nbsp;</span><br>&nbsp;&nbsp;&nbsp; never_failed_memcopy(tmp, rsh.m_data, rsh.size() + 1); //</span>先生成目标状态<span lang=EN-US><br>&nbsp;&nbsp;&nbsp; }<br>&nbsp;&nbsp;&nbsp; swap(tmp, m_data);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //</span>对象安全进入目标状态<span lang=EN-US><br>&nbsp;&nbsp;&nbsp; m_alloc.deallocate(tmp);&nbsp;&nbsp;&nbsp; //</span>释放原有资源<span lang=EN-US><o:p></o:p></span></span></p>
</div>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　强保证的代码多使用了一个局部变量<span lang=EN-US>tmp</span>，先计算出目标状态放在<span lang=EN-US>tmp</span>中，然后在安全进入目标状态，这个过程我们并没有损失什么东西（代码清晰性，性能等等）。看上去，实现强保证并不比基本保证困难多少，一般而言，也确实如此。不过，别太自信，举一种典型的很难实现强保证的例子，对于区间操作的强保证：<span lang=EN-US><o:p></o:p></span></p>
<div style="BORDER-RIGHT: rgb(0,153,204) 1pt solid; PADDING-RIGHT: 4pt; BORDER-TOP: rgb(0,153,204) 1pt solid; PADDING-LEFT: 4pt; PADDING-BOTTOM: 4pt; MARGIN-LEFT: 3.75pt; BORDER-LEFT: rgb(0,153,204) 1pt solid; MARGIN-RIGHT: 3.75pt; PADDING-TOP: 4pt; BORDER-BOTTOM: rgb(0,153,204) 1pt solid; BACKGROUND-COLOR: rgb(221,237,251); background-origin: initial; background-clip: initial">
<p class=code style="MARGIN: 3.75pt 0cm; BACKGROUND-COLOR: rgb(221,237,251); TEXT-ALIGN: justify; background-origin: initial; background-clip: initial"><span lang=EN-US style="FONT-SIZE: 12pt; COLOR: windowtext; FONT-FAMILY: 宋体">for (itr = range.begin(); itr != range.end(); ++itr){<br>&nbsp;&nbsp;&nbsp; itr-&gt;do_something();<br>}<o:p></o:p></span></p>
</div>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　如果某个<span lang=EN-US>do_something</span>失败了，<span lang=EN-US>range</span>将处于什么状态？这段代码仍然做到了基本保证，但不是强保证的，根据实现强保证的基本原则，我们可以这么做：<span lang=EN-US><o:p></o:p></span></p>
<div style="BORDER-RIGHT: rgb(0,153,204) 1pt solid; PADDING-RIGHT: 4pt; BORDER-TOP: rgb(0,153,204) 1pt solid; PADDING-LEFT: 4pt; PADDING-BOTTOM: 4pt; MARGIN-LEFT: 3.75pt; BORDER-LEFT: rgb(0,153,204) 1pt solid; MARGIN-RIGHT: 3.75pt; PADDING-TOP: 4pt; BORDER-BOTTOM: rgb(0,153,204) 1pt solid; BACKGROUND-COLOR: rgb(221,237,251); background-origin: initial; background-clip: initial">
<p class=code style="MARGIN: 3.75pt 0cm; BACKGROUND-COLOR: rgb(221,237,251); TEXT-ALIGN: justify; background-origin: initial; background-clip: initial"><span lang=EN-US style="FONT-SIZE: 12pt; COLOR: windowtext; FONT-FAMILY: 宋体">tmp = range;<br>for (itr = tmp.begin(); itr != tmp.end(); ++itr){<br>&nbsp;&nbsp;&nbsp; itr-&gt;do_something();<br>}<br>swap(tmp, range);<o:p></o:p></span></p>
</div>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　似乎很简单啊！呵呵，这样的做法并非不可取，只是有时候行不通。因为我们额外付出了性能的代价，而且，这个代价可能很大。无论如何，我们阐述了实现强保证的方法，怎么取舍则由您决定了。<span lang=EN-US><o:p></o:p></span></p>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　接下来讨论最后一种异常安全保证：不会失败。<span lang=EN-US><o:p></o:p></span></p>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　通常，我们并不需要这么强的安全保证，但是我们至少必须保证三类过程不会失败：析构函数，释放类函数，<span lang=EN-US>swap</span>。析构和释放函数不会失败，这是<span lang=EN-US>RAII</span>技术有效的基石，<span lang=EN-US>swap</span>不会失败，是为了<span lang=EN-US>&#8220;</span>在决不失败的过程中，把对象替换到目标状态<span lang=EN-US>&#8221;</span>。我们前面的所有讨论都是建立在这三类过程不会失败的基础上的，在这里，弥补了上面的那个疏漏。<span lang=EN-US><o:p></o:p></span></p>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　一般而言，语言内部类型的赋值、取地址等运算是不会发生异常的，上述三类过程逻辑上也是不会发生异常的。内部运算中，除法运算可能抛出异常。但是地址访问错通常是一种错误，而不是异常，我们本应该在前条件检查中就发现的这一点的。所有不会发生异常操作的简单累加，仍然不会导致异常。<span lang=EN-US><o:p></o:p></span></p>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　好了，现在我们可以总结一下编写异常安全代码的几条准则了：<span lang=EN-US><o:p></o:p></span></p>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　<span lang=EN-US>1.</span>只在应该使用异常的地方抛出异常<span lang=EN-US><br></span>　　<span lang=EN-US>2.</span>如果不知道如何处理异常，请不要捕获<span lang=EN-US>(</span>截留<span lang=EN-US>)</span>异常。<span lang=EN-US><br></span>　　<span lang=EN-US>3.</span>充分使用<span lang=EN-US>RAII</span>，旁路异常。<span lang=EN-US><br></span>　　<span lang=EN-US>4.</span>努力实现强保证，至少实现基本保证。<span lang=EN-US><br></span>　　<span lang=EN-US>5.</span>确保析构函数、释放类函数和<span lang=EN-US>swap</span>不会失败。<span lang=EN-US><o:p></o:p></span></p>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　另外，还有一些语言细节问题，因为和这个主题有关也一并列出：<span lang=EN-US><o:p></o:p></span></p>
<p style="MARGIN: 0px 0px 14px; LINE-HEIGHT: 21px; TEXT-ALIGN: justify">　　<span lang=EN-US>1.</span>不要这样抛出异常：<span lang=EN-US>throw new exception;</span>这将导致内存泄漏。<span lang=EN-US><br></span>　　<span lang=EN-US>2.</span>自定义类型，应该捕获异常的引用类型：<span lang=EN-US>catch(exception&amp; e)</span>或<span lang=EN-US>catch(const exception&amp; e)</span>。<span lang=EN-US><br></span>　　<span lang=EN-US>3.</span>不要使用异常规范，即使是空异常规范。编译器并不保证只抛出异常规范允许的异常，更多内容请参考相关书籍。</p>
</span></span>
<img src ="http://www.cppblog.com/ietj/aggbug/144566.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/ietj/" target="_blank">一路风尘</a> 2011-04-19 17:09 <a href="http://www.cppblog.com/ietj/articles/144566.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>契约思想的一个反面案例</title><link>http://www.cppblog.com/ietj/articles/144565.html</link><dc:creator>一路风尘</dc:creator><author>一路风尘</author><pubDate>Tue, 19 Apr 2011 09:00:00 GMT</pubDate><guid>http://www.cppblog.com/ietj/articles/144565.html</guid><wfw:comment>http://www.cppblog.com/ietj/comments/144565.html</wfw:comment><comments>http://www.cppblog.com/ietj/articles/144565.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/ietj/comments/commentRss/144565.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/ietj/services/trackbacks/144565.html</trackback:ping><description><![CDATA[<span class=Apple-style-span style="WORD-SPACING: 0px; FONT: medium Simsun; TEXT-TRANSFORM: none; COLOR: rgb(0,0,0); TEXT-INDENT: 0px; WHITE-SPACE: normal; LETTER-SPACING: normal; BORDER-COLLAPSE: separate; orphans: 2; widows: 2; webkit-border-horizontal-spacing: 0px; webkit-border-vertical-spacing: 0px; webkit-text-decorations-in-effect: none; webkit-text-size-adjust: auto; webkit-text-stroke-width: 0px"><span class=Apple-style-span style="FONT-SIZE: 14px; LINE-HEIGHT: 21px; FONT-FAMILY: verdana, sans-serif; TEXT-ALIGN: left">
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">刚刚发表了《什么是契约》一文，突然发现自己通篇都在写理论，没有实例来证明。所以赶快补充一个反面案例——C++ IOStream。说是反面，不是因为IOStream库设计得不精彩（恰恰相反，你很难找到比IOStream设计更为精彩的C++库了），而是想展示一下，在没有契约概念的思想体系里，组件设计将为权责不清的错误处理付出多大的代价。</p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">大家知道，C++ IOStream库非常经典，最先起源于Bjarne Stroustrup的Stream库，之后经过Jerry Schwartz、Martin Carroll、Andy Koenig等人的改进，成为IOStream库，并被并入Bell实验室发行的USD C++库中，广为传播。后来USD库逐渐消亡了，而IOStream由于获得广泛应用，得以幸免，并以新的形式被置于标准库中。</p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">对于错误处理，当IOStream库诞生的时候（大约1985－1987），C++还没有异常机制。因此，Jerry Schwartz发明了这样一套错误处理机制：</p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">例1：经典的IOStream错误处理：</p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">&nbsp; ifstream ifs("filename.txt", ios::in);<br>&nbsp; if (!ifs) {&nbsp; // 这里实施了向void*转型的操作<br>&nbsp;&nbsp;&nbsp; // 文件打开失败，实施错误处理<br>&nbsp; }</p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">先测试文件是否打开，再实施具体操作，这是经典IOStream库的一个惯用法（idiom)。</p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">我们现在设想用户没有很好地执行这个idiom：</p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">&nbsp; int val;<br>&nbsp; ifstream ifs("filename.txt", ios::in);<br>&nbsp; ifs &gt;&gt; val;&nbsp;</p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">如果filename.txt打开失败，会发生什么情况？</p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">如果哪位还有当年的Borland C++ 3.1，可以试着测试一下。我估计是什么也不发生，或者说，程序处于极端危险的&#8220;undefined behavior&#8221;状态。</p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">这种情况对C++库开发者来说是不能接受的。因此，尽管问题的出现是由于用户的错误（他们没有正确地测试流状态），但是由于非契约思想体系下的权责不清，IOStream库的开发者开始追求足以应对用户错误的组件开发技术。由此，IOStream开始在一个方向上被拖入了复杂性的泥潭之中。</p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">我们看看标准库中的对付这种情况采用什么办法。标准IOStream有一个成员函数叫做exceptions()，专门用来帮助程序员切换异常模式。缺省情况下，异常触发并没有打开，所以情形跟经典IOStream库相同。如果你在操作IOStream之前如下调用：</p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">strm.exceptions(std::ios::eofbit | std::ios::failbit |<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; std::ios::badbit);<br>则当流不处于good状态时，执行类似 strm &gt;&gt; val；这样的操作时，将会抛出异常。</p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">这样做看起来不错，是吗？</p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">我觉得不是。请恕我C++标准的异议，这是我第一次正式对C++标准中的设计提出异议。</p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">这种设计带来的缺点，首先是复杂。在Nicolai Josuttis的The C++ Standard Library中，对这个机制整整用了5页纸来解释，还意犹未尽。复杂的设计必然带来复杂的使用规则，而面对复杂的使用规则，用户是可以投票的，那就是你做你的，我不用！读这篇帖子的人，谁在实际项目中使用过exceptions()？事实上，我个人是害怕exception甚于害怕undefined behavior。</p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">而对于用户来说，你可以不用，却不得不为对它付出运行性能和空间的代价。诸位有兴趣，不妨追踪一个IOStream功能的实现，看看为了支持这个异常，IOStream库的设计这耗费了多大的心力，而你的CPU又为此耗费了多少clock。</p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">缺点之二，是异常本身的问题——即使你抓到了异常，又当如何？程序可能已经完全离开了发生异常时的执行环境，也许你连异常为什么发生都搞不清楚，谈何处理？也无非就是通知用户一声：&#8220;我完了，因为一个异常发生在XXXXXXXX处，你要报告的话给我发Email吧。&#8221; 是啊，你还能做什么呢？</p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">我们试着用契约观点来分析这一状况，如果说&#8220;先测试，再使用&#8221;在传统上是一个idiom，那么在contract思想里上升为一个契约。对于C++来说，应该将&#8220;流处于good状态&#8221;作为一个契约，在每一个成员函数里进行检查。甚至你还可以设置一个调试期标志，专门用来核查用户是否检查过流状态。在必要的操作进行之前，你可以先用断言检查用户是否检查过流状态，满足了契约。这样一来，在契约之下，用户将被迫以正确的方式使用组件，从而大幅度简化组件开发的复杂度。</p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">再来考虑异常。如果真正发生了异常，在Eiffel中提供了retry机制。Bjarne Stroustrup说过，retry可以做到，但是往往没有意义。为什么没有意义呢？因为C++中没有契约的思想，异常的产生可能根本就是程序员的bug。在这种情况下，无论retry多少次，结果都是一样的糟。可是在Eiffel里情形不同。如果各方面对于契约都做到很好的遵循，那么真正发生异常的时候，我们大可以比较有把握的说，这可能是一个很偶然的事件导致的。比如说网络环境下，另一个用户在那一瞬间突然对文件实施了一个操作，或者硬件的一次偶然异常。对于这种情况，&#8220;再试一次&#8221;成了合情合理的选择。我们很可能将异常扼杀在摇篮之中，从而不给上层模块带来任何影响。</p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">谁说契约思想不伟大呢？</p>
</span></span>
<img src ="http://www.cppblog.com/ietj/aggbug/144565.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/ietj/" target="_blank">一路风尘</a> 2011-04-19 17:00 <a href="http://www.cppblog.com/ietj/articles/144565.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>从Eiffel谈契约式设计</title><link>http://www.cppblog.com/ietj/articles/144563.html</link><dc:creator>一路风尘</dc:creator><author>一路风尘</author><pubDate>Tue, 19 Apr 2011 08:45:00 GMT</pubDate><guid>http://www.cppblog.com/ietj/articles/144563.html</guid><wfw:comment>http://www.cppblog.com/ietj/comments/144563.html</wfw:comment><comments>http://www.cppblog.com/ietj/articles/144563.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/ietj/comments/commentRss/144563.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/ietj/services/trackbacks/144563.html</trackback:ping><description><![CDATA[<span class=Apple-style-span style="WORD-SPACING: 0px; FONT: medium Simsun; TEXT-TRANSFORM: none; COLOR: rgb(0,0,0); TEXT-INDENT: 0px; WHITE-SPACE: normal; LETTER-SPACING: normal; BORDER-COLLAPSE: separate; orphans: 2; widows: 2; webkit-border-horizontal-spacing: 0px; webkit-border-vertical-spacing: 0px; webkit-text-decorations-in-effect: none; webkit-text-size-adjust: auto; webkit-text-stroke-width: 0px"><span class=Apple-style-span style="FONT-SIZE: 13px; COLOR: rgb(68,68,68); LINE-HEIGHT: 18px; FONT-FAMILY: Arial, 'Trebuchet MS', sans-serif; TEXT-ALIGN: left">
<p style="MARGIN-BOTTOM: 18px">注：本文转自孟岩的博客(http://blog.csdn.net/myan)</p>
<p style="MARGIN-BOTTOM: 18px">假设你现在正在面试，主考不紧不慢地给出下一道题目：&#8220;请用C语言写一个类似strcpy的函数。要考虑可能发生的异常情况。&#8221; 你会怎么做呢？很明显，对方不是在考察你的编程能力，因为复制字符串实在太容易了。对方是在考察你的编程风格（习惯），或者说，要看看你编码的质量。</p>
<p style="MARGIN-BOTTOM: 18px">下面是多种可能的做法：</p>
<p style="MARGIN-BOTTOM: 18px">void<br>string_copy1(char* dest, const char* source)<br>{<br>assert(dest != NULL); /* 使用断言 */<br>assert(source != NULL);<br>while (*source != &#8216;\0&#8242;) {<br>*dest = *source;<br>++dest;<br>++source;<br>}</p>
<p style="MARGIN-BOTTOM: 18px">*dest = &#8216;\0&#8242;;<br>}</p>
<p style="MARGIN-BOTTOM: 18px"><span id=more-322></span>void<br>string_copy2(char* dest, const char* source)<br>{<br>if (dest != NULL &amp;&amp; source != NULL) {&nbsp; /* 对错误消极静默 */<br>while (*source != &#8216;\0&#8242;) {<br>*dest = *source;<br>++dest;<br>++source;<br>}</p>
<p style="MARGIN-BOTTOM: 18px">*dest = &#8216;\0&#8242;;<br>}<br>}</p>
<p style="MARGIN-BOTTOM: 18px">int<br>string_copy3(char* dest, const char* source)<br>{<br>if (dest != NULL &amp;&amp; source != NULL) {<br>while (*source != &#8216;\0&#8242;) {<br>*dest = *source;<br>++dest;<br>++source;<br>}</p>
<p style="MARGIN-BOTTOM: 18px">*dest = &#8216;\0&#8242;;<br>return SUCCESS;&nbsp; /* 返回表示正确的值 */<br>}<br>else {<br>errno = E_INVALIDARG;&nbsp; /* 设定错误号 */<br>return FAILED;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /*&nbsp; 返回表示错误的值 */<br>}<br>}<br>// C++<br>void<br>string_copy4(char* dest, const char* source)<br>{<br>if (dest == NULL || source == NULL)<br>throw Invalid_Argument_Error();&nbsp; /*&nbsp; 抛出异常 */</p>
<p style="MARGIN-BOTTOM: 18px">while (*source != &#8216;\0&#8242;) {<br>*dest = *source;<br>++dest;<br>++source;<br>}</p>
<p style="MARGIN-BOTTOM: 18px">*dest = &#8216;\0&#8242;;<br>}</p>
<p style="MARGIN-BOTTOM: 18px">如果你是主考，不知道面对这样四份答卷，你的评分如何？当然，你可以心里揣着一个&#8220;标准答案&#8221;，&#8220;顺我者昌，逆我者亡&#8221;。但是如果以认真的态度面对这四份答卷，我想很多人都会难以抉择。</p>
<p style="MARGIN-BOTTOM: 18px">因为这里涉及到了软件开发中的一个带有本质性的难题——错误处理。</p>
<p style="MARGIN-BOTTOM: 18px">历来错误处理一直是软件开发者所面临的最大困难之一。Bjarne Stroustrup在谈到其原因时说道，能够探察错误的一方不知道如何处理错误，知道如何处理错误的一方没有能力探察错误，而直接采用防御性代码来解 决，会使得程序的正常结构被打乱，从而带来更多的错误。这种困境是非常难以应对的——费心耗力而未必有回报。因此，更多的人采用鸵鸟战术，对可能发生的错 误视而不见，任其自然。</p>
<p style="MARGIN-BOTTOM: 18px">C++、Java和其他语言对错误处理问题的回答是异常机制。这种机制在正常的程序执行流之外开辟了专门的信道，专门用来在不同程序模块之间报告错误，解 决上述错误探察与处理策略分散的矛盾。然而，有了异常处理机制后，开发者开始有一种倾向，就是使用异常来处理所有的错误。我曾经就这个问题在 comp.lang.c++.moderated上展开讨论，结果是发现有相当多的人，包括Boost开发组里的很多专家，都认为异常是错误处理的通用解 决方案。</p>
<p style="MARGIN-BOTTOM: 18px">对此我不能赞同。并且我认为滥用异常比不用异常的危害更大。</p>
<p style="MARGIN-BOTTOM: 18px">The Pragmatic Programmer是一本在国外程序员中间颇为流行的书，其中在讲到错误处理时，有一句箴言：<br>&#8220;只在真正异常的状况下使用异常。&#8221;</p>
<p style="MARGIN-BOTTOM: 18px">书中举了一个例子，如果你需要当前目录下的一个名叫&#8220;app.dat&#8221;的文件，而这个文件不存在，那么这不叫异常状况，这是你应该预料到的、并且显式处理 的情况。而如果你要到Windows目录下寻找user.dat文件，却没找到，那才叫做异常状况——因为每一个正常运行的Windows系统都应该有这 个文件。</p>
<p style="MARGIN-BOTTOM: 18px">我非常赞成书中的那句忠告，可是究竟什么是&#8220;真正异常&#8221;的状况？书中的这个例子显然只是一个颇具感性的、寓言似的故事，具有所有寓言的共同特点——读起来觉得豁然开朗，收获很大，实际上帮不了你什么忙。这种例子对于我们的实际开发，仍然提供不了真正的帮助。</p>
<p style="MARGIN-BOTTOM: 18px">究竟应该如何看待错误？怎样才能最好地错误处理？</p>
<p style="MARGIN-BOTTOM: 18px">说实话，在这两个问题上，我们所见到的大部分语言都没有给出很好的回答。C秉承一贯风格，把所有的东西推给开发者考虑；Ada发明了异常，但是又为异常所 累（知道阿里亚纳5火箭的处女航为什么失败吗？）；C++企图将Ada的异常机制融合进自己的体系中，结果异常成了C++中最难以处理的东西；Java和 C#显然都没有耐心重新考虑错误处理这桩事，而只是简单的将C++的异常机制完善化了事。</p>
<p style="MARGIN-BOTTOM: 18px">与上述这些语言不同，Eiffel从一开始就把错误处理放在核心的位置上予以考虑，并以&#8220;契约&#8221;思想为核心，建立了整个的错误处理思想体系。在我了解的语 言里，Eiffel是对这个问题思考最为深刻一个，因此，Eiffel历来享有&#8220;高质量系统开发语言&#8221;的声誉。（事实上，Bertrand Meyer很不喜欢别人称Eiffel为&#8220;编程语言&#8221;，他反复强调，Eiffel是一个Software Development Framework。不过本文只涉及语言特性，所以姑且称Eiffel语言。）</p>
<p style="MARGIN-BOTTOM: 18px">Eiffel把软件错误产生的本质归结与&#8220;契约&#8221;的破坏。Eiffel认为，一个软件能够正常运作，正确完成任务，是需要一系列条件的。这些条件包括客观 运行环境良好，操作者操作正确，软件内部功能正确等等。因此，软件的正确运行，是环境、操作者与软件本身三方面合作的结果。相应的，系统的错误，也是由于 三者中有一方没有正确履行自己的职责而导致的。细化到软件内部，每个软件都是由若干不同的模块组成的，软件的错误，是由于某些模块没有正确履行自己的职 责。要彻底杜绝软件错误，只有分清各自模块的责任，并且建立机制，敦促各模块正确履行自己的责任，然后才有可能做到Bug-free。（鉴于系统中错综复 杂的关系，以及开发者认识能力的局限，我认为真正无错误的系统是不可能的。但是当前一般软件系统中的质量问题远远比应有的严重。）</p>
<p style="MARGIN-BOTTOM: 18px">如何保证各方恪守职责呢？Eiffel引入了契约（Contract）这个概念。这里的契约与我们通常所说的商业契约很相似，有以下几个特点：</p>
<p style="MARGIN-BOTTOM: 18px">1. 契约关系的双方是平等的，对整个bussiness的顺利进行负有共同责任，没有哪一方可以只享有权利而不承担义务。<br>2. 契约关系经常是相互的，权利和义务之间往往是互相捆绑在一起的；<br>3. 执行契约的义务在我，而核查契约的权力在人；<br>4. 我的义务保障的是你的利益，而你的义务保障的是我的利益；<br>将契约关系引入到软件开发领域，尤其是面向对象领域之后，在观念上给我们带来了几大冲击：</p>
<p style="MARGIN-BOTTOM: 18px">1. 一般的观点，在软件体系中，程序库和组件库被类比为server，而使用程序库、组件库的程序被视为client。根据这种C/S关系，我们往往对库程序 和组件的质量提出很严苛的要求，强迫它们承担本不应该由它们来承担的责任，而过分纵容client一方，甚至要求库程序去处理明显由于client错误造 成的困境。客观上导致程序库和组件库的设计和编写异常困难，而且质量隐患反而更多；同时client一方代码大多松散随意，质量低劣。这种情形，就好像在 一个权责不清的企业里，必然会养一批尸位素餐的混混，苦一批任劳任怨，不计得失的老黄牛。引入契约观念之后，这种C/S关系被打破，大家都是平等的，你需 要我正确提供服务，那么你必须满足我提出的条件，否则我没有义务&#8220;排除万难&#8221;地保证完成任务。</p>
<p style="MARGIN-BOTTOM: 18px">2. 一般认为在模块中检查错误状况并且上报，是模块本身的义务。而在契约体制下，对于契约的检查并非义务，实际上是在履行权利。一个义务，一个权利，差别极大。例如上面的代码：<br>if (dest == NULL) { &#8230; }<br>这就是义务，其要点在于，一旦条件不满足，我方（义务方）必须负责以合适手法处理这尴尬局面，或者返回错误值，或者抛出异常。而：<br>assert(dest != NULL);<br>这是检查契约，履行权利。如果条件不满足，那么错误在对方而不在我，我可以立刻&#8220;撕毁合同&#8221;，罢工了事，无需做任何多余动作。这无疑可以大大简化程序库和组件库的开发。</p>
<p style="MARGIN-BOTTOM: 18px">3. 契约所核查的，是&#8220;为保证正确性所必须满足的条件&#8221;，因此，当契约被破坏时，只表明一件事：软件系统中有bug。其意义是说，某些条件在到达我这里时，必 须已经确保为&#8220;真&#8221;。谁来确保？应该是系统中的其他模块在先期确保。如果在我这里发现契约没有被遵守，那么表明系统中其他模块没有正确履行自己的义务。就 拿上面提到的&#8220;打开文件&#8221;的例子来说，如果有一个模块需要一个FILE*，而在契约检查中发现该指针为NULL，则意味着有一个模块没有履行其义务，即 &#8220;检查文件是否存在，确保文件以正确模式打开，并且保证指针的正确性&#8221;。因此，当契约检查失败时，我们首先要知道这意味着程序员错误，而且要做的不是纠正 契约核查方，而是纠正契约提供方。换句话说，当你发现:<br>assert(dest != NULL);<br>报错时，你要做的不是去修改你的string_copy函数，而是要让任何代码在调用string_copy时确保dest指针不为空。</p>
<p style="MARGIN-BOTTOM: 18px">4. 我们以往对待&#8220;过程&#8221;或&#8220;函数&#8221;的理解是：完成某个计算任务的过程，这一看法只强调了其目标，没有强调其条件。在这种理解下，我们对于exception 的理解非常模糊和宽泛：只要是无法完成这个计算过程，均可被视为异常，也不管是我自己的原因，还是其他人的原因（典型的权责不清）。正是因为这种模糊和宽 泛，&#8220;究竟什么时候应该抛出异常&#8221;成为没有人能回答的问题。而引入契约之后，&#8220;过程&#8221;和&#8220;函数&#8221;被定义为：完成契约的过程。基于契约的相互性，如果这个契 约的失败是因为其他模块未能履行契约，本过程只需报告，无需以任何其他方式做出反应。而真正的异常状况是&#8220;对方完全满足了契约，而我依然未能如约完成任 务&#8221;的情形。这样以来，我们就给&#8220;异常&#8221;下了一个清晰、可行的定义。</p>
<p style="MARGIN-BOTTOM: 18px">5. 一般来说，在面向对象技术中，我们认为&#8220;接口&#8221;是唯一重要的东西，接口定义了组件，接口确定了系统，接口是面向对象中我们唯一需要关心的东西，接口不仅是 必要的，而且是充分的。然而，契约观念提醒我们，仅仅有接口还不充分，仅仅通过接口还不足以传达足够的信息，为了正确使用接口，必须考虑契约。只有考虑契 约，才可能实现面向对象的目标：可靠性、可扩展性和可复用性。反过来，&#8220;没有契约的复用根本就是瞎胡闹。（Bertrand Meyer语）&#8221;。</p>
<p style="MARGIN-BOTTOM: 18px">由上述观点可以看出，虽然Eiffel所倡导的Design By Contract在表象上不过是系统化的断言（assertion）机制，然而在背后，确实是完全的思想革新。正如Ivar Jacoboson访华时对《程序员》杂志所说：&#8220;我认为Bertrand Meyer的方向——Design by Contract——是正确的方向，我们都会沿着他的足迹前进。我相信，大型厂商（微软、IBM，当然还有Rational）都不会对Bertrand Meyer的成就坐视不理。所有这些厂商都会在这个方向上有所行动。&#8221;</span></span></p>
<img src ="http://www.cppblog.com/ietj/aggbug/144563.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/ietj/" target="_blank">一路风尘</a> 2011-04-19 16:45 <a href="http://www.cppblog.com/ietj/articles/144563.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>svn强制写日志和可以修改历史日志(svn钩子的应用)</title><link>http://www.cppblog.com/ietj/articles/142904.html</link><dc:creator>一路风尘</dc:creator><author>一路风尘</author><pubDate>Tue, 29 Mar 2011 01:15:00 GMT</pubDate><guid>http://www.cppblog.com/ietj/articles/142904.html</guid><wfw:comment>http://www.cppblog.com/ietj/comments/142904.html</wfw:comment><comments>http://www.cppblog.com/ietj/articles/142904.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/ietj/comments/commentRss/142904.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/ietj/services/trackbacks/142904.html</trackback:ping><description><![CDATA[<div><strong>1。强制写日志</strong></div>
<div></div>
<div>在每次提交的时候写明提交的目的是一个很好的习惯，Subversion默认没有提供，但是可以通过钩子实现：将下面的代码存为放到版本库的hooks目录下即可，当你不写日志提交的话就会报告错误。</div>
<div>文件名:<font face="Courier New">pre-commit-log.bat</font></div>
<div>
<p><font face="Courier New">@echo off<br>setlocal<br>set REPOS=%1<br>set TXN=%2<br>rem check that logmessage contains at least 5 characters<br>svnlook log "%REPOS%" -t "%TXN%" | findstr ".........." &gt; nul<br>if %errorlevel% gtr 0 goto err<br>exit 0<br>:err<br>echo Empty log message not allowed. Commit aborted! 1&gt;&amp;2<br>exit 1</font></p>
<font face="Courier New">
<div><font face="Courier New"><strong>2。可以修改历史日志</strong></font></div>
<div><font face="Courier New">历史上的日志写的不好，我们希望修改，默认对Subversion是不允许的，并且会提示需要创建pre-revprop-change钩子。最简单的方式是创建一个空白的pre-revprop-change.bat文件放到版本库的hooks目录下，但是这种方式不够严格，用户具备了修改所有属性的权利，例如修改作者的权利（可以提交垃圾然后栽赃嫁祸了）。所以要限制用户只能修改日志，所以有了下面的代码，将其存为pre-revprop-change.bat放到版本库的hooks目录下即可。</font></div>
<div><font face="Courier New">文件名:<font face="Courier New">pre-revprop-change.bat</font></font></div>
<p><font face="Courier New"><font face="Courier New">REM SVN pre-revprop-change hook allows edit of logmessages from TSVN</font></font></p>
<p><font face="Courier New">setlocal<br>set REPOS=%1<br>set REV=%2<br>set USER=%3<br>set PROPNAME=%4<br>set ACTION=%5</font></p>
<p><font face="Courier New">if not "%ACTION%"=="M" goto refuse<br>if not "%PROPNAME%"=="svn:log" goto refuse<br>goto OK</font></p>
<p><font face="Courier New">:refuse<br>echo Cann't set %PROPNAME%/%ACTION%, only svn:log is allowed 1&gt;&amp;2<br>endlocal<br>exit 1</font></p>
<p><font face="Courier New">:OK<br>endlocal<br>exit 0<br></font></p>
</font></div>
<img src ="http://www.cppblog.com/ietj/aggbug/142904.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/ietj/" target="_blank">一路风尘</a> 2011-03-29 09:15 <a href="http://www.cppblog.com/ietj/articles/142904.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>VC++6.0调试篇：远程调试</title><link>http://www.cppblog.com/ietj/articles/142817.html</link><dc:creator>一路风尘</dc:creator><author>一路风尘</author><pubDate>Mon, 28 Mar 2011 00:47:00 GMT</pubDate><guid>http://www.cppblog.com/ietj/articles/142817.html</guid><wfw:comment>http://www.cppblog.com/ietj/comments/142817.html</wfw:comment><comments>http://www.cppblog.com/ietj/articles/142817.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/ietj/comments/commentRss/142817.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/ietj/services/trackbacks/142817.html</trackback:ping><description><![CDATA[<span class=Apple-style-span style="WORD-SPACING: 0px; FONT: medium Simsun; TEXT-TRANSFORM: none; COLOR: rgb(0,0,0); TEXT-INDENT: 0px; WHITE-SPACE: normal; LETTER-SPACING: normal; BORDER-COLLAPSE: separate; orphans: 2; widows: 2; webkit-border-horizontal-spacing: 0px; webkit-border-vertical-spacing: 0px; webkit-text-decorations-in-effect: none; webkit-text-size-adjust: auto; webkit-text-stroke-width: 0px"><span class=Apple-style-span style="FONT-SIZE: 14px; LINE-HEIGHT: 21px; FONT-FAMILY: verdana, sans-serif; TEXT-ALIGN: left">
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">VC开发环境之所以提供远程调试的能力，是因为有些情况下单机调试会让你崩溃掉。。。比如，调试GUI程序的WM_PAINT消息，因为要单步调试，所以调试器会对界面的重绘产生副作用（Heisenberg不确定性原理）。当然还有些别的情况也适用，比如程序在测试环境运行的好好的，但是在客户那行为总是异常，这时候如果可以TCP远程连接上去维护的话，就能通过远程调试的特性在出现状况的系统环境中排错~</p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">&nbsp;</p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 下面来说一下具体的做法。先明确下概念，远程调试嘛，自然是两个机器之间调试。程序运行在目标机器上，调试器运行在本机。当然，目标机器上还是要有少许辅助程序才能跟本机的调试器connect上，以便通讯。一般来说，只需要copy四个文件到目标机器上就行了：MSVCMON.EXE、DM.DLL、TLN0T.DLL和MSDIS110.DLL。这四个文件都能在VC6目录的Common\MSDEV98\Bin目录下面找到。copy过去之后，运行msvcom.exe，看下图片~</p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><img style="BORDER-TOP-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-RIGHT-WIDTH: 0px" height=174 alt=目标机器 src="http://www.cppblog.com/ietj/admin/VC6远程调试_files/TargetMechine.JPG" width=261 align=center></p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">有个Settings的按钮，不用管。直接点Connect就行了~</p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">&nbsp;</p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">接着看看本机这边调试器的设置。首先设置好远程调试开关，在Build菜单下有个Debuger Remote Connecting的子菜单，点之。出现个窗口，默认是在Local项，我们要选的是Network(TCP/IP)，然后点设定。会弹出一个对话框，输入目标机器的ip或者机器名，最后点OK就行了。</p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><img style="BORDER-TOP-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-RIGHT-WIDTH: 0px" height=519 alt=本机调试设置 src="http://www.cppblog.com/ietj/admin/VC6远程调试_files/Host.JPG" width=631 align=center></p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">&nbsp;</p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">接下来把工程打开，设置最后一步。假设生成的可执行程序名为RemoteDebug.exe，在目标机器上的路径为d:\Prj\Remote.exe，那么，在本机的Project Settings里面，选择Debug页面的Remote executable path and file name下面的编辑框中输入目标机器中程序的路径：d:\Prj\RemoteDebug.exe。注意，这里写的是从目标机器的角度所看到的路径。</p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">&nbsp;</p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><img style="BORDER-TOP-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-RIGHT-WIDTH: 0px" height=383 alt=项目设置 src="http://www.cppblog.com/ietj/admin/VC6远程调试_files/PrjSetting.JPG" width=379 align=center></p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px">然后编译一下程序，把新编译出来的RemoteDebug.exe复制到目标机器的d:\Prj下面，就可以在本机像平常一样调试了。</p>
</span></span>
<img src ="http://www.cppblog.com/ietj/aggbug/142817.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/ietj/" target="_blank">一路风尘</a> 2011-03-28 08:47 <a href="http://www.cppblog.com/ietj/articles/142817.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>VC++ 6.0 中如何使用 CRT 调试功能来检测内存泄漏</title><link>http://www.cppblog.com/ietj/articles/142529.html</link><dc:creator>一路风尘</dc:creator><author>一路风尘</author><pubDate>Tue, 22 Mar 2011 16:00:00 GMT</pubDate><guid>http://www.cppblog.com/ietj/articles/142529.html</guid><wfw:comment>http://www.cppblog.com/ietj/comments/142529.html</wfw:comment><comments>http://www.cppblog.com/ietj/articles/142529.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/ietj/comments/commentRss/142529.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/ietj/services/trackbacks/142529.html</trackback:ping><description><![CDATA[<span class=Apple-style-span style="WORD-SPACING: 0px; FONT: medium Simsun; TEXT-TRANSFORM: none; COLOR: rgb(0,0,0); TEXT-INDENT: 0px; WHITE-SPACE: normal; LETTER-SPACING: normal; BORDER-COLLAPSE: separate; orphans: 2; widows: 2; webkit-border-horizontal-spacing: 0px; webkit-border-vertical-spacing: 0px; webkit-text-decorations-in-effect: none; webkit-text-size-adjust: auto; webkit-text-stroke-width: 0px"><span class=Apple-style-span style="FONT-SIZE: 14px; COLOR: rgb(85,85,85); LINE-HEIGHT: 28px; FONT-FAMILY: 宋体, 'Arial Narrow', arial, serif"><font size=3>最近看了周星星 Blog 中的一篇文章：&#8220;</font><a style="COLOR: rgb(51,102,153); TEXT-DECORATION: none" href="http://blog.vckbase.com/bruceteen/archive/2004/10/28/1130.aspx" target=_blank><font color=#000000 size=3>VC++6.0中内存泄漏检测</font></a><font size=3>&#8221;，受益匪浅，便运行其例子代码想看看 Output 窗口中的输出结果，可惜怎么弄其输出都不是预期的东西，郁闷了半天，便到水坛里找到周星星，请求他指点一、二，然而未果。没有办法，最后我一头栽进 MSDN 库狂搜了一把，功夫不负有心人，我搜出很多有关这方面的资料，没过多久我便基本上就找到了答案......<br>首先，检测内存泄漏的基本工具是调试器和 CRT 调试堆函数。为了使用调试堆函数，必须在要检测内存泄漏和调试的程序中添加下面的语句：</font>
<pre style="BACKGROUND-POSITION: 0px 0px; OVERFLOW-Y: auto; FONT-SIZE: 12px; OVERFLOW-X: auto; MARGIN: 0px 0px 1em; WIDTH: 712px; FONT-FAMILY: 'Courier New', monospace; BACKGROUND-COLOR: rgb(247,247,247); background-origin: initial; background-clip: initial"><font size=3>#define _CRTDBG_MAP_ALLOC  #include&lt;stdlib.h&gt;  #include&lt;crtdbg.h&gt;   #include "debug_new.h" </font></pre>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><font size=3>　　MSDN 如是说：&#8220;必须保证上面声明的顺序，如果改变了顺序，可能不能正常工作。&#8221;至于这是为什么，我们不得而知。MS 的老大们经常这样故弄玄虚。<br>针对非 MFC 程序，再加上周星星的头文件：debug_new.h，当然如果不加这一句，也能检测出内存泄漏，但是你无法确定在哪个源程序文件中发生泄漏。Output 输出只告诉你在 crtsdb.h 中的某个地方有内存泄漏。我测试时 REG_DEBUG_NEW 没有起作用。加不加这个宏都可以检测出发生内存分配泄漏的文件。<br>其次，一旦添加了上面的声明，你就可以通过在程序中加入下面的代码来报告内存泄漏信息了：<br></font></p>
<pre style="BACKGROUND-POSITION: 0px 0px; OVERFLOW-Y: auto; FONT-SIZE: 12px; OVERFLOW-X: auto; MARGIN: 0px 0px 1em; WIDTH: 712px; FONT-FAMILY: 'Courier New', monospace; BACKGROUND-COLOR: rgb(247,247,247); background-origin: initial; background-clip: initial"><font size=3>      _CrtDumpMemoryLeaks(); </font></pre>
<font size=3>　　这就这么简单。我在周星星的例子代码中加入这些机关后，在 VC++ 调试会话（按 F5 调试运行） Output 窗口的 Debug 页便看到了预期的内存泄漏 dump。该 dump 形式如下：</font>
<pre style="BACKGROUND-POSITION: 0px 0px; OVERFLOW-Y: auto; FONT-SIZE: 12px; OVERFLOW-X: auto; MARGIN: 0px 0px 1em; WIDTH: 712px; FONT-FAMILY: 'Courier New', monospace; BACKGROUND-COLOR: rgb(247,247,247); background-origin: initial; background-clip: initial"><font size=3>Detected memory leaks!  Dumping objects -&gt;  c:\Program Files\...\include\crtdbg.h(552) : {45} normal block at 0x00441BA0, 2 bytes long.  Data: &lt;AB&gt; 41 42  c:\Program Files\...\include\crtdbg.h(552) : {44} normal block at 0x00441BD0, 33 bytes long.  Data: &lt; C &gt; 00 43 00 CD CD CD CD CD CD CD CD CD CD CD CD CD  c:\Program Files\...\include\crtdbg.h(552) : {43} normal block at 0x00441C20, 40 bytes long.  Data: &lt; C &gt; E8 01 43 00 16 00 00 00 00 00 00 00 00 00 00 00  Object dump complete. </font></pre>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><font size=3>更具体的细节请参考本文附带的源代码文件。<br><br>下面是我看过 MSDN 资料后，针对&#8220;如何使用 CRT 调试功能来检测内存泄漏？&#8221;的问题进行了一番编译和整理，希望对大家有用。如果你的英文很棒，那就不用往下看了，建议直接去读 MSDN 库中的技术原文。<br>C/C++ 编程语言的最强大功能之一便是其动态分配和释放内存，但是中国有句古话：&#8220;最大的长处也可能成为最大的弱点&#8221;，那么 C/C++ 应用程序正好印证了这句话。在 C/C++ 应用程序开发过程中，动态分配的内存处理不当是最常见的问题。其中，最难捉摸也最难检测的错误之一就是内存泄漏，即未能正确释放以前分配的内存的错误。偶尔发生的少量内存泄漏可能不会引起我们的注意，但泄漏大量内存的程序或泄漏日益增多的程序可能会表现出各种 各样的征兆：从性能不良（并且逐渐降低）到内存完全耗尽。更糟的是，泄漏的程序可能会用掉太多内存，导致另外一个程序垮掉，而使用户无从查找问题的真正根源。此外，即使无害的内存泄漏也可能殃及池鱼。<br>幸运的是，Visual Studio 调试器和 C 运行时 (CRT) 库为我们提供了检测和识别内存泄漏的有效方法。下面请和我一起分享收获——如何使用 CRT 调试功能来检测内存泄漏？</font></p>
<ol style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 5px 0px 5px 35px; PADDING-TOP: 0px; LIST-STYLE-TYPE: none">
    <li style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 2px; MARGIN: 0px; PADDING-TOP: 2px; LIST-STYLE-TYPE: decimal"><a style="COLOR: rgb(51,102,153); TEXT-DECORATION: none" href="http://www.vckbase.com/document/viewdoc/?id=1349#如何启用内存泄漏检测机制"><font color=#000000 size=3>如何启用内存泄漏检测机制？</font></a>
    <ul style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 5px 0px 5px 35px; PADDING-TOP: 0px; LIST-STYLE-TYPE: none">
        <li style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; FONT-WEIGHT: bold; PADDING-BOTTOM: 2px; MARGIN: 0px; PADDING-TOP: 2px; LIST-STYLE-TYPE: decimal"><a style="COLOR: rgb(51,102,153); TEXT-DECORATION: none" href="http://www.vckbase.com/document/viewdoc/?id=1349#使用 _CrtSetDbgFlag"><font color=#000000 size=3>使用 _CrtSetDbgFlag</font></a></li>
        <li style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; FONT-WEIGHT: bold; PADDING-BOTTOM: 2px; MARGIN: 0px; PADDING-TOP: 2px; LIST-STYLE-TYPE: decimal"><a style="COLOR: rgb(51,102,153); TEXT-DECORATION: none" href="http://www.vckbase.com/document/viewdoc/?id=1349#设置 CRT 报告模式"><font color=#000000 size=3>设置 CRT 报告模式</font></a></li>
    </ul>
    </li>
    <li style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 2px; MARGIN: 0px; PADDING-TOP: 2px; LIST-STYLE-TYPE: decimal"><a style="COLOR: rgb(51,102,153); TEXT-DECORATION: none" href="http://www.vckbase.com/document/viewdoc/?id=1349#解释内存块类型"><font color=#000000 size=3>解释内存块类型</font></a></li>
    <li style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 2px; MARGIN: 0px; PADDING-TOP: 2px; LIST-STYLE-TYPE: decimal"><a style="COLOR: rgb(51,102,153); TEXT-DECORATION: none" href="http://www.vckbase.com/document/viewdoc/?id=1349#如何在内存分配序号处设置断点？"><font color=#000000 size=3>如何在内存分配序号处设置断点？</font></a></li>
    <li style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 2px; MARGIN: 0px; PADDING-TOP: 2px; LIST-STYLE-TYPE: decimal"><a style="COLOR: rgb(51,102,153); TEXT-DECORATION: none" href="http://www.vckbase.com/document/viewdoc/?id=1349#如何比较内存状态？"><font color=#000000 size=3>如何比较内存状态？</font></a></li>
    <li style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 2px; MARGIN: 0px; PADDING-TOP: 2px; LIST-STYLE-TYPE: decimal"><a style="COLOR: rgb(51,102,153); TEXT-DECORATION: none" href="http://www.vckbase.com/document/viewdoc/?id=1349#结论"><font color=#000000 size=3>结论</font></a></li>
</ol>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><strong><a style="COLOR: rgb(51,102,153); TEXT-DECORATION: none" name=如何启用内存泄漏检测机制><font size=3>如何启用内存泄漏检测机制</font></a><font size=3>？</font></strong><br><br><font size=3>　　VC++ IDE 的默认状态是没有启用内存泄漏检测机制的，也就是说即使某段代码有内存泄漏，调试会话的 Output 窗口的 Debug 页不会输出有关内存泄漏信息。你必须设定两个最基本的机关来启用内存泄漏检测机制。<br><br>一是使用调试堆函数：</font></p>
<pre style="BACKGROUND-POSITION: 0px 0px; OVERFLOW-Y: auto; FONT-SIZE: 12px; OVERFLOW-X: auto; MARGIN: 0px 0px 1em; WIDTH: 712px; FONT-FAMILY: 'Courier New', monospace; BACKGROUND-COLOR: rgb(247,247,247); background-origin: initial; background-clip: initial"><font size=3>#define _CRTDBG_MAP_ALLOC  #include&lt;stdlib.h&gt;  #include&lt;crtdbg.h&gt; </font></pre>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><font size=3>注意：#include 语句的顺序。如果更改此顺序，所使用的函数可能无法正确工作。<br><br>通过包含 crtdbg.h 头文件，可以将 malloc 和 free 函数映射到其&#8220;调试&#8221;版本 _malloc_dbg 和 _free_dbg，这些函数会跟踪内存分配和释放。此映射只在调试（Debug）版本（也就是要定义 _DEBUG）中有效。发行版本（Release）使用普通的 malloc 和 free 函数。<br>#define 语句将 CRT 堆函数的基础版本映射到对应的&#8220;调试&#8221;版本。该语句不是必须的，但如果没有该语句，那么有关内存泄漏的信息会不全。<br><br>二是在需要检测内存泄漏的地方添加下面这条语句来输出内存泄漏信息：</font></p>
<pre style="BACKGROUND-POSITION: 0px 0px; OVERFLOW-Y: auto; FONT-SIZE: 12px; OVERFLOW-X: auto; MARGIN: 0px 0px 1em; WIDTH: 712px; FONT-FAMILY: 'Courier New', monospace; BACKGROUND-COLOR: rgb(247,247,247); background-origin: initial; background-clip: initial"><font size=3>_CrtDumpMemoryLeaks();</font></pre>
<font size=3>　　当在调试器下运行程序时，_CrtDumpMemoryLeaks 将在 Output 窗口的 Debug 页中显示内存泄漏信息。比如：</font>
<pre style="BACKGROUND-POSITION: 0px 0px; OVERFLOW-Y: auto; FONT-SIZE: 12px; OVERFLOW-X: auto; MARGIN: 0px 0px 1em; WIDTH: 712px; FONT-FAMILY: 'Courier New', monospace; BACKGROUND-COLOR: rgb(247,247,247); background-origin: initial; background-clip: initial"><font size=3>Detected memory leaks! Dumping objects -&gt; C:\Temp\memleak\memleak.cpp(15) : {45} normal block at 0x00441BA0, 2 bytes long. Data: &lt;AB&gt; 41 42  c:\program files\microsoft visual studio\vc98\include\crtdbg.h(552) : {44} normal block at 0x00441BD0, 33 bytes long. Data: &lt; C &gt; 00 43 00 CD CD CD CD CD CD CD CD CD CD CD CD CD  c:\program files\microsoft visual studio\vc98\include\crtdbg.h(552) : {43} normal block at 0x00441C20, 40 bytes long. Data: &lt; C &gt; 08 02 43 00 16 00 00 00 00 00 00 00 00 00 00 00  Object dump complete.</font></pre>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><font size=3>如果不使用 #define _CRTDBG_MAP_ALLOC 语句，内存泄漏的输出是这样的：</font></p>
<pre style="BACKGROUND-POSITION: 0px 0px; OVERFLOW-Y: auto; FONT-SIZE: 12px; OVERFLOW-X: auto; MARGIN: 0px 0px 1em; WIDTH: 712px; FONT-FAMILY: 'Courier New', monospace; BACKGROUND-COLOR: rgb(247,247,247); background-origin: initial; background-clip: initial"><font size=3>Detected memory leaks! Dumping objects -&gt; {45} normal block at 0x00441BA0, 2 bytes long. Data: &lt;AB&gt; 41 42  {44} normal block at 0x00441BD0, 33 bytes long. Data: &lt; C &gt; 00 43 00 CD CD CD CD CD CD CD CD CD CD CD CD CD  {43} normal block at 0x00441C20, 40 bytes long. Data: &lt; C &gt; C0 01 43 00 16 00 00 00 00 00 00 00 00 00 00 00  Object dump complete.</font></pre>
<font size=3>　　根据这段输出信息，你无法知道在哪个源程序文件里发生了内存泄漏。下面我们来研究一下输出信息的格式。第一行和第二行没有什么可说的，从第三行开始：</font>
<pre style="BACKGROUND-POSITION: 0px 0px; OVERFLOW-Y: auto; FONT-SIZE: 12px; OVERFLOW-X: auto; MARGIN: 0px 0px 1em; WIDTH: 712px; FONT-FAMILY: 'Courier New', monospace; BACKGROUND-COLOR: rgb(247,247,247); background-origin: initial; background-clip: initial"><font size=3>xx}：花括弧内的数字是内存分配序号，本文例子中是 {45}，{44}，{43}； block：内存块的类型，常用的有三种：normal（普通）、client（客户端）或 CRT（运行时）；本文例子中是：normal block；  用十六进制格式表示的内存位置，如：at 0x00441BA0 等； 以字节为单位表示的内存块的大小，如：32 bytes long；  前 16 字节的内容（也是用十六进制格式表示），如：Data: &lt;AB&gt; 41 42 等；</font></pre>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><font size=3>　　仔细观察不难发现，如果定义了 _CRTDBG_MAP_ALLOC ，那么在内存分配序号前面还会显示在其中分配泄漏内存的文件名，以及文件名后括号中的数字表示发生泄漏的代码行号，比如：</font></p>
<pre style="BACKGROUND-POSITION: 0px 0px; OVERFLOW-Y: auto; FONT-SIZE: 12px; OVERFLOW-X: auto; MARGIN: 0px 0px 1em; WIDTH: 712px; FONT-FAMILY: 'Courier New', monospace; BACKGROUND-COLOR: rgb(247,247,247); background-origin: initial; background-clip: initial"><font size=3>C:\Temp\memleak\memleak.cpp(15) </font></pre>
<font size=3>　　双击 Output 窗口中此文件名所在的输出行，便可跳到源程序文件分配该内存的代码行（也可以选中该行，然后按 F4，效果一样） ，这样一来我们就很容易定位内存泄漏是在哪里发生的了，因此，_CRTDBG_MAP_ALLOC 的作用显而易见。<br><br><em style="FONT-STYLE: italic"><strong><a style="COLOR: rgb(51,102,153); TEXT-DECORATION: none" name="使用 _CrtSetDbgFlag">使用 _CrtSetDbgFlag</a></strong></em><br><br>如果程序只有一个出口，那么调用 _CrtDumpMemoryLeaks 的位置是很容易选择的。但是，如果程序可能会在多个地方退出该怎么办呢？在每一个可能的出口处调用 _CrtDumpMemoryLeaks 肯定是不可取的，那么这时可以在程序开始处包含下面的调用：</font>
<pre style="BACKGROUND-POSITION: 0px 0px; OVERFLOW-Y: auto; FONT-SIZE: 12px; OVERFLOW-X: auto; MARGIN: 0px 0px 1em; WIDTH: 712px; FONT-FAMILY: 'Courier New', monospace; BACKGROUND-COLOR: rgb(247,247,247); background-origin: initial; background-clip: initial"><font size=3>_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );</font></pre>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><font size=3>　　这条语句无论程序在什么地方退出都会自动调用 _CrtDumpMemoryLeaks。注意：这里必须同时设置两个位域标志：_CRTDBG_ALLOC_MEM_DF 和 _CRTDBG_LEAK_CHECK_DF。<br><br><em style="FONT-STYLE: italic"><strong><a style="COLOR: rgb(51,102,153); TEXT-DECORATION: none" name="设置 CRT 报告模式">设置 CRT 报告模式</a></strong></em><br><br>默认情况下，_CrtDumpMemoryLeaks 将内存泄漏信息 dump 到 Output 窗口的 Debug 页， 如果你想将这个输出定向到别的地方，可以使用 _CrtSetReportMode 进行重置。如果你使用某个库，它可能将输出定向到另一位置。此时，只要使用以下语句将输出位置设回 Output 窗口即可：</font></p>
<pre style="BACKGROUND-POSITION: 0px 0px; OVERFLOW-Y: auto; FONT-SIZE: 12px; OVERFLOW-X: auto; MARGIN: 0px 0px 1em; WIDTH: 712px; FONT-FAMILY: 'Courier New', monospace; BACKGROUND-COLOR: rgb(247,247,247); background-origin: initial; background-clip: initial"><font size=3>_CrtSetReportMode( _CRT_ERROR, _CRTDBG_MODE_DEBUG );</font></pre>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><font size=3>有关使用 _CrtSetReportMode 的详细信息，请参考 MSDN 库关于 _CrtSetReportMode 的描述。<br><br><strong><a style="COLOR: rgb(51,102,153); TEXT-DECORATION: none" name=解释内存块类型>解释内存块类型</a></strong><br><br>前面已经说过，内存泄漏报告中把每一块泄漏的内存分为 normal（普通块）、client（客户端块）和 CRT 块。事实上，需要留心和注意的也就是 normal 和 client，即普通块和客户端块。</font></p>
<ul style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 5px 0px 5px 35px; PADDING-TOP: 0px; LIST-STYLE-TYPE: none">
    <li style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; FONT-WEIGHT: bold; PADDING-BOTTOM: 2px; MARGIN: 0px; PADDING-TOP: 2px; LIST-STYLE-TYPE: disc"><font size=3>normal block（普通块）：这是由你的程序分配的内存。</font></li>
    <li style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; FONT-WEIGHT: bold; PADDING-BOTTOM: 2px; MARGIN: 0px; PADDING-TOP: 2px; LIST-STYLE-TYPE: disc"><font size=3>client block（客户块）：这是一种特殊类型的内存块，专门用于 MFC 程序中需要析构函数的对象。MFC new 操作符视具体情况既可以为所创建的对象建立普通块，也可以为之建立客户块。</font></li>
    <li style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; FONT-WEIGHT: bold; PADDING-BOTTOM: 2px; MARGIN: 0px; PADDING-TOP: 2px; LIST-STYLE-TYPE: disc"><font size=3>CRT block（CRT 块）：是由 C RunTime Library 供自己使用而分配的内存块。由 CRT 库自己来管理这些内存的分配与释放，我们一般不会在内存泄漏报告中发现 CRT 内存泄漏，除非程序发生了严重的错误（例如 CRT 库崩溃）。</font></li>
</ul>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><font size=3>除了上述的类型外，还有下面这两种类型的内存块，它们不会出现在内存泄漏报告中：</font></p>
<ul style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 5px 0px 5px 35px; PADDING-TOP: 0px; LIST-STYLE-TYPE: none">
    <li style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; FONT-WEIGHT: bold; PADDING-BOTTOM: 2px; MARGIN: 0px; PADDING-TOP: 2px; LIST-STYLE-TYPE: disc"><font size=3>free block（空闲块）：已经被释放(free)的内存块。</font></li>
    <li style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; FONT-WEIGHT: bold; PADDING-BOTTOM: 2px; MARGIN: 0px; PADDING-TOP: 2px; LIST-STYLE-TYPE: disc"><font size=3>Ignore block（忽略块）：这是程序员显式声明过不要在内存泄漏报告中出现的内存块。</font></li>
</ul>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><strong><a style="COLOR: rgb(51,102,153); TEXT-DECORATION: none" name=如何在内存分配序号处设置断点？><font size=3>如何在内存分配序号处设置断点？</font></a></strong><br><br><font size=3>　　在内存泄漏报告中，的文件名和行号可告诉分配泄漏的内存的代码位置，但仅仅依赖这些信息来了解完整的泄漏原因是不够的。因为一个程序在运行时，一段分配内存的代码可能会被调用很多次，只要有一次调用后没有释放内存就会导致内存泄漏。为了确定是哪些内存没有被释放，不仅要知道泄漏的内存是在哪里分配的，还要知道泄漏产生的条件。这时内存分配序号就显得特别有用——这个序号就是文件名和行号之后的花括弧里的那个数字。<br>例如，在本文例子代码的输出信息中，&#8220;45&#8221;是内存分配序号，意思是泄漏的内存是你程序中分配的第四十五个内存块：</font></p>
<pre style="BACKGROUND-POSITION: 0px 0px; OVERFLOW-Y: auto; FONT-SIZE: 12px; OVERFLOW-X: auto; MARGIN: 0px 0px 1em; WIDTH: 712px; FONT-FAMILY: 'Courier New', monospace; BACKGROUND-COLOR: rgb(247,247,247); background-origin: initial; background-clip: initial"><font size=3>Detected memory leaks! Dumping objects -&gt; C:\Temp\memleak\memleak.cpp(15) : {45} normal block at 0x00441BA0, 2 bytes long. Data: &lt;AB&gt; 41 42  ...... Object dump complete. </font></pre>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><font size=3>　　CRT 库对程序运行期间分配的所有内存块进行计数，包括由 CRT 库自己分配的内存和其它库（如 MFC）分配的内存。因此，分配序号为 N 的对象即为程序中分配的第 N 个对象，但不一定是代码分配的第 N 个对象。（大多数情况下并非如此。）<br>这样的话，你便可以利用分配序号在分配内存的位置设置一个断点。方法是在程序起始附近设置一个位置断点。当程序在该点中断时，可以从 QuickWatch（快速监视）对话框或 Watch（监视）窗口设置一个内存分配断点：<br><br>例如，在 Watch 窗口中，在 Name 栏键入下面的表达式：</font></p>
<pre style="BACKGROUND-POSITION: 0px 0px; OVERFLOW-Y: auto; FONT-SIZE: 12px; OVERFLOW-X: auto; MARGIN: 0px 0px 1em; WIDTH: 712px; FONT-FAMILY: 'Courier New', monospace; BACKGROUND-COLOR: rgb(247,247,247); background-origin: initial; background-clip: initial"><font size=3>_crtBreakAlloc</font></pre>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><font size=3>如果要使用 CRT 库的多线程 DLL 版本（/MD 选项），那么必须包含上下文操作符，像这样：</font></p>
<pre style="BACKGROUND-POSITION: 0px 0px; OVERFLOW-Y: auto; FONT-SIZE: 12px; OVERFLOW-X: auto; MARGIN: 0px 0px 1em; WIDTH: 712px; FONT-FAMILY: 'Courier New', monospace; BACKGROUND-COLOR: rgb(247,247,247); background-origin: initial; background-clip: initial"><font size=3>{,,msvcrtd.dll}_crtBreakAlloc</font></pre>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><font size=3>　　现在按下回车键，调试器将计算该值并把结果放入 Value 栏。如果没有在内存分配点设置任何断点，该值将为 &#8211;1。<br>用你想要在其位置中断的内存分配的分配序号替换 Value 栏中的值。例如输入 45。这样就会在分配序号为 45 的地方中断。&nbsp;<br>在所感兴趣的内存分配处设置断点后，可以继续调试。这时，运行程序时一定要小心，要保证内存块分配的顺序不会改变。当程序在指定的内存分配处中断时，可以查看 Call Stack（调用堆栈）窗口和其它调试器信息以确定分配内存时的情况。如果必要，可以从该点继续执行程序，以查看对象发生了什么情况，或许可以确定未正确释放对象的原因。<br>尽管通常在调试器中设置内存分配断点更方便，但如果愿意，也可在代码中设置这些断点。为了在代码中设置一个内存分配断点，可以增加这样一行（对于第四十五个内存分配）：</font></p>
<pre style="BACKGROUND-POSITION: 0px 0px; OVERFLOW-Y: auto; FONT-SIZE: 12px; OVERFLOW-X: auto; MARGIN: 0px 0px 1em; WIDTH: 712px; FONT-FAMILY: 'Courier New', monospace; BACKGROUND-COLOR: rgb(247,247,247); background-origin: initial; background-clip: initial"><font size=3>_crtBreakAlloc = 45;</font></pre>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><font size=3>你还可以使用有相同效果的 _CrtSetBreakAlloc 函数：</font></p>
<pre style="BACKGROUND-POSITION: 0px 0px; OVERFLOW-Y: auto; FONT-SIZE: 12px; OVERFLOW-X: auto; MARGIN: 0px 0px 1em; WIDTH: 712px; FONT-FAMILY: 'Courier New', monospace; BACKGROUND-COLOR: rgb(247,247,247); background-origin: initial; background-clip: initial"><font size=3>_CrtSetBreakAlloc(45);</font></pre>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><strong><a style="COLOR: rgb(51,102,153); TEXT-DECORATION: none" name=如何比较内存状态？><font size=3>如何比较内存状态？</font></a></strong><br><br><font size=3>　　定位内存泄漏的另一个方法就是在关键点获取应用程序内存状态的快照。CRT 库提供了一个结构类型 _CrtMemState。你可以用它来存储内存状态的快照：</font></p>
<pre style="BACKGROUND-POSITION: 0px 0px; OVERFLOW-Y: auto; FONT-SIZE: 12px; OVERFLOW-X: auto; MARGIN: 0px 0px 1em; WIDTH: 712px; FONT-FAMILY: 'Courier New', monospace; BACKGROUND-COLOR: rgb(247,247,247); background-origin: initial; background-clip: initial"><font size=3>_CrtMemState s1, s2, s3;</font></pre>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><font size=3>　　若要获取给定点的内存状态快照，可以向 _CrtMemCheckpoint 函数传递一个 _CrtMemState 结构。该函数用当前内存状态的快照填充此结构：</font></p>
<pre style="BACKGROUND-POSITION: 0px 0px; OVERFLOW-Y: auto; FONT-SIZE: 12px; OVERFLOW-X: auto; MARGIN: 0px 0px 1em; WIDTH: 712px; FONT-FAMILY: 'Courier New', monospace; BACKGROUND-COLOR: rgb(247,247,247); background-origin: initial; background-clip: initial"><font size=3>_CrtMemCheckpoint( &amp;s1 );</font></pre>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><font size=3>　　通过向 _CrtMemDumpStatistics 函数传递 _CrtMemState 结构，可以在任意地方 dump 该结构的内容：</font></p>
<pre style="BACKGROUND-POSITION: 0px 0px; OVERFLOW-Y: auto; FONT-SIZE: 12px; OVERFLOW-X: auto; MARGIN: 0px 0px 1em; WIDTH: 712px; FONT-FAMILY: 'Courier New', monospace; BACKGROUND-COLOR: rgb(247,247,247); background-origin: initial; background-clip: initial"><font size=3>_CrtMemDumpStatistics( &amp;s1 );</font></pre>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><font size=3>该函数输出如下格式的 dump 内存分配信息：</font></p>
<pre style="BACKGROUND-POSITION: 0px 0px; OVERFLOW-Y: auto; FONT-SIZE: 12px; OVERFLOW-X: auto; MARGIN: 0px 0px 1em; WIDTH: 712px; FONT-FAMILY: 'Courier New', monospace; BACKGROUND-COLOR: rgb(247,247,247); background-origin: initial; background-clip: initial"><font size=3>0 bytes in 0 Free Blocks. 75 bytes in 3 Normal Blocks. 5037 bytes in 41 CRT Blocks. 0 bytes in 0 Ignore Blocks. 0 bytes in 0 Client Blocks. Largest number used: 5308 bytes. Total allocations: 7559 bytes.</font></pre>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><font size=3>　　若要确定某段代码中是否发生了内存泄漏，可以通过获取该段代码之前和之后的内存状态快照，然后使用 _CrtMemDifference 比较这两个状态：</font></p>
<pre style="BACKGROUND-POSITION: 0px 0px; OVERFLOW-Y: auto; FONT-SIZE: 12px; OVERFLOW-X: auto; MARGIN: 0px 0px 1em; WIDTH: 712px; FONT-FAMILY: 'Courier New', monospace; BACKGROUND-COLOR: rgb(247,247,247); background-origin: initial; background-clip: initial"><font size=3>_CrtMemCheckpoint( &amp;s1 );// 获取第一个内存状态快照  // 在这里进行内存分配  _CrtMemCheckpoint( &amp;s2 );// 获取第二个内存状态快照  // 比较两个内存快照的差异 if ( _CrtMemDifference( &amp;s3, &amp;s1, &amp;s2) )      _CrtMemDumpStatistics( &amp;s3 );// dump 差异结果</font></pre>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><font size=3>　　顾名思义，_CrtMemDifference 比较两个内存状态（前两个参数），生成这两个状态之间差异的结果（第三个参数）。在程序的开始和结尾放置 _CrtMemCheckpoint 调用，并使用 _CrtMemDifference 比较结果，是检查内存泄漏的另一种方法。如果检测到泄漏，则可以使用 _CrtMemCheckpoint 调用通过二进制搜索技术来分割程序和定位泄漏。<br><br><strong><a style="COLOR: rgb(51,102,153); TEXT-DECORATION: none" name=结论>结论</a></strong><br><br>尽管 VC ++ 具有一套专门调试 MFC 应用程序的机制，但本文上述讨论的内存分配很简单，没有涉及到 MFC 对象，所以这些内容同样也适用于 MFC 程序。在 MSDN 库中可以找到很多有关 VC++ 调试方面的资料，如果你能善用 MSDN 库，相信用不了多少时间你就有可能成为调试高手。<br><br>本人水平不高，谬误在所难免，请大家拍砖，不要客气。顺祝大家圣诞快乐！<br><br>JerryZ 于 2004 年平安夜，<br></font></p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><font size=3></font>&nbsp;</p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px" align=center><font size=3><strong>调试方法和技巧<br></strong><br>作者：</font><a style="COLOR: rgb(51,102,153); TEXT-DECORATION: none" href="http://blog.vckbase.com/feifan/" target=_blank><font color=#000000 size=3>非凡</font></a></p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><strong><font size=3>便于调试的代码风格：</font></strong></p>
<ol style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 5px 0px 5px 35px; PADDING-TOP: 0px; LIST-STYLE-TYPE: none">
    <li style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 2px; MARGIN: 0px; PADDING-TOP: 2px; LIST-STYLE-TYPE: decimal"><font size=3>不用全局变量</font></li>
    <li style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 2px; MARGIN: 0px; PADDING-TOP: 2px; LIST-STYLE-TYPE: decimal"><font size=3>所有变量都要初始化，成员变量在构造函数中初始化</font></li>
    <li style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 2px; MARGIN: 0px; PADDING-TOP: 2px; LIST-STYLE-TYPE: decimal"><font size=3>尽量使用const</font></li>
    <li style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 2px; MARGIN: 0px; PADDING-TOP: 2px; LIST-STYLE-TYPE: decimal"><font size=3>详尽的注释</font></li>
</ol>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><strong><font size=3>VC++编译选项：</font></strong></p>
<ol style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 5px 0px 5px 35px; PADDING-TOP: 0px; LIST-STYLE-TYPE: none">
    <li style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 2px; MARGIN: 0px; PADDING-TOP: 2px; LIST-STYLE-TYPE: decimal"><font size=3>总是使用/W4警告级别</font></li>
    <li style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 2px; MARGIN: 0px; PADDING-TOP: 2px; LIST-STYLE-TYPE: decimal"><font size=3>在调试版本里总是使用/GZ编译选项，用来发现在Release版本中才有的错误</font></li>
    <li style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 2px; MARGIN: 0px; PADDING-TOP: 2px; LIST-STYLE-TYPE: decimal"><font size=3>没有警告的编译：保证在编译后没有任何警告，但是在消除警告前要进行仔细检查</font></li>
</ol>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><strong><font size=3>调试方法：</font></strong></p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><font size=3>1、使用 Assert（原则：尽量简单）<br>assert只在debug下生效，release下不会被编译。<br><br>例子：</font></p>
<pre style="BACKGROUND-POSITION: 0px 0px; OVERFLOW-Y: auto; FONT-SIZE: 12px; OVERFLOW-X: auto; MARGIN: 0px 0px 1em; WIDTH: 712px; FONT-FAMILY: 'Courier New', monospace; BACKGROUND-COLOR: rgb(247,247,247); background-origin: initial; background-clip: initial"><font size=3>char* strcpy(char* dest,char* source) { 	assert(source!=0); 	assert(dest!=0); 	char* returnstring = dest; 	 	while((*dest++ = *source++)!= &#8216;\0&#8217;) 	{ 		; 	} 	return returnstring; }      </font></pre>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><font size=3>2、防御性的编程<br><br>例子：</font></p>
<pre style="BACKGROUND-POSITION: 0px 0px; OVERFLOW-Y: auto; FONT-SIZE: 12px; OVERFLOW-X: auto; MARGIN: 0px 0px 1em; WIDTH: 712px; FONT-FAMILY: 'Courier New', monospace; BACKGROUND-COLOR: rgb(247,247,247); background-origin: initial; background-clip: initial"><font size=3>char* strcpy(char* dest,char* source) { 	if(source == 0) 	{ 		assert(false); 		reutrn 0; 	}  	if(dest == 0) 	{ 		assert(false); 		return 0; 	} 	char* returnstring = dest; 	while((*dest++ = *source++)!= &#8216;\0&#8217;) 	{ 		; 	} 	return returnstring; }      </font></pre>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><font size=3>3、使用Trace<br><br>以下的例子只能在debug中显示，<br><br>例子：<br><br>a)、TRACE</font></p>
<pre style="BACKGROUND-POSITION: 0px 0px; OVERFLOW-Y: auto; FONT-SIZE: 12px; OVERFLOW-X: auto; MARGIN: 0px 0px 1em; WIDTH: 712px; FONT-FAMILY: 'Courier New', monospace; BACKGROUND-COLOR: rgb(247,247,247); background-origin: initial; background-clip: initial"><font size=3>CString csTest ＝ &#8220;test&#8221;； TRACE（&#8220;CString is ％s\n&#8221;，csTest）；</font></pre>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><font size=3>b)、ATLTRACE<br><br>c)、afxDump</font></p>
<pre style="BACKGROUND-POSITION: 0px 0px; OVERFLOW-Y: auto; FONT-SIZE: 12px; OVERFLOW-X: auto; MARGIN: 0px 0px 1em; WIDTH: 712px; FONT-FAMILY: 'Courier New', monospace; BACKGROUND-COLOR: rgb(247,247,247); background-origin: initial; background-clip: initial"><font size=3>CTime time = CTime::GetCurrentTime(); #ifdef _DEBUG afxDump &lt;&lt; time &lt;&lt; &#8220;\n&#8221;; #endif</font></pre>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><font size=3>4、用GetLastError来检测返回值，通过得到错误代码来分析错误原因<br><br>5、把错误信息记录到文件中<br><br><strong>异常处理</strong><br><br>程序设计时一定要考虑到异常如何处理，当错误发生后，不应简单的报告错误并退出程序，应当尽可能的想办法恢复到出错前的状态或者让程序从头开始运行，并且对于某些错误，应该能够容错，即允许错误的存在，但是程序还是能够正常完成任务。<br><br><strong>调试技巧</strong><br><br>1、VC++中F5进行调试运行<br><br>a)、在output Debug窗口中可以看到用TRACE打印的信息<br>b)、 Call Stack窗口中能看到程序的调用堆栈<br><br>2、当Debug版本运行时发生崩溃，选择retry进行调试，通过看Call Stack分析出错的位置及原因<br>3、使用映射文件调试<br><br>a)、创建映射文件：Project settings中link项，选中Generate mapfile，输出程序代码地址：/MAPINFO: LINES，得到引出序号：/MAPINFO: EXPORTS。<br>b)、程序发布时，应该把所有模块的映射文件都存档。<br>c)、查看映射文件：见&#8221; 通过崩溃地址找出源代码的出错行&#8221;文件。<br><br>4、可以调试的Release版本<br><br>Project settings中C++项的Debug Info选择为Program Database，Link项的Debug中选择Debug Info和Microsoft format。<br><br>5、查看API的错误码，在watch窗口输入@err可以查看或者@err,hr，其中&#8221;,hr&#8221;表示错误码的说明。<br>6、Set Next Statement：该功能可以直接跳转到指定的代码行执行，一般用来测试异常处理的代码。<br>7、调试内存变量的变化：当内存发生变化时停下来。<br><br><strong>常见错误</strong><br><br>1、在函数返回的时候程序崩溃的原因<br><br>a)、写自动变量越界<br>b)、函数原型不匹配<br><br>2、MFC<br><br>a)、使用错误的函数原型处理用户定义消息<br><br>正确的函数原型为：</font></p>
<pre style="BACKGROUND-POSITION: 0px 0px; OVERFLOW-Y: auto; FONT-SIZE: 12px; OVERFLOW-X: auto; MARGIN: 0px 0px 1em; WIDTH: 712px; FONT-FAMILY: 'Courier New', monospace; BACKGROUND-COLOR: rgb(247,247,247); background-origin: initial; background-clip: initial"><font size=3>afx_msg LRESULT OnMyMessage(WPARAM wParam,LPARAM lParam);</font></pre>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><font size=3>3、谨慎使用TerminateThread：使用TerminateThread会造成资源泄漏，不到万不得已，不要使用。<br><br>4、使用_beginthreadex，不要使用Create Thread来常见线程。<br><br><strong>参考资料：</strong><br>《Windows程序调试》<br></font></p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px" align=center><font size=3><strong>功能强大的vc6调试器<br></strong><br><br>作者：</font><a style="COLOR: rgb(51,102,153); TEXT-DECORATION: none" href="&#109;&#97;&#105;&#108;&#116;&#111;&#58;&#121;&#121;&#50;&#98;&#101;&#116;&#116;&#101;&#114;&#64;&#49;&#50;&#54;&#46;&#99;&#111;&#109;"><font color=#000000 size=3>yy2better</font></a></p>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><br><font size=3>&nbsp;&nbsp;要成为一位优秀的软件工程师，调试能力必不可缺。本文将较详细介绍VC6调试器的主要用法。&nbsp;<br>&nbsp;&nbsp;windows平台的调试器主要分为两大类：&nbsp;<br>&nbsp;&nbsp;1 用户模式(user-mode)调试器：它们都基于win32 Debugging API，有使用方便的界面，主要用于调试用户模式下的应用程序。这类调试器包括Visual C++调试器、WinDBG、BoundChecker、Borland C++ Builder调试器、NTSD等。&nbsp;<br>&nbsp;&nbsp;2 内核模式(kernel-mode)调试器：内核调试器位于CPU和操作系统之间，一旦启动，操作系统也会中止运行，主要用于调试驱动程序或用户模式调试器不易调试的程序。这类调试器包括WDEB386、WinDBG和softice等。其中WinDBG和softice也可以调试用户模式代码。&nbsp;<br>&nbsp;&nbsp;国外一位调试高手曾说，他70％调试时间是在用VC++，其余时间是使用WinDBG和softice。毕竟，调试用户模式代码，VC6调试器的效率是非常高的。因此，我将首先在本篇介绍VC6调试器的主要用法，其他调试器的用法及一些调试技能在后续文章中阐述。&nbsp;<br><br><strong>一 位置断点（Location Breakpoint）</strong>&nbsp;<br>&nbsp;&nbsp;大家最常用的断点是普通的位置断点，在源程序的某一行按F9就设置了一个位置断点。但对于很多问题，这种朴素的断点作用有限。譬如下面这段代码：</font></p>
<pre style="BACKGROUND-POSITION: 0px 0px; OVERFLOW-Y: auto; FONT-SIZE: 12px; OVERFLOW-X: auto; MARGIN: 0px 0px 1em; WIDTH: 712px; FONT-FAMILY: 'Courier New', monospace; BACKGROUND-COLOR: rgb(247,247,247); background-origin: initial; background-clip: initial"><font size=3>void CForDebugDlg::OnOK() 	 { 	for (int i = 0; i &lt; 1000; i++)	//A 	{ 		int k = i * 10 - 2;	//B 		SendTo(k);		//C 		int tmp = DoSome(i);	//D 		int j = i / tmp;	//E 	} }             </font></pre>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><font size=3>&nbsp;&nbsp;执行此函数，程序崩溃于E行，发现此时tmp为0，假设tmp本不应该为0，怎么这个时候为0呢？所以最好能够跟踪此次循环时DoSome函数是如何运行的，但由于是在循环体内，如果在E行设置断点，可能需要按F5（GO）许多次。这样手要不停的按，很痛苦。使用VC6断点修饰条件就可以轻易解决此问题。步骤如下。&nbsp;<br>&nbsp;&nbsp;1 Ctrl+B打开断点设置框，如下图：&nbsp;<br><img style="BORDER-RIGHT: 0px; BORDER-TOP: 0px; VERTICAL-ALIGN: top; BORDER-LEFT: 0px; BORDER-BOTTOM: 0px" onclick='window.open("http://blog.51cto.com/viewpic.php?refimg=" + this.src)' alt="" src="http://www.cppblog.com/ietj/admin/VC++%206_0%20中如何使用%20CRT%20调试功能来检测内存泄漏%20-%201.files/debug1.jpg" onload="if(this.width>650) this.width=650;">&nbsp;<br><font color=#0000ff><strong>Figure 1</strong>设置高级位置断点</font>&nbsp;<br>&nbsp;&nbsp;2 然后选择D行所在的断点，然后点击condition按钮，在弹出对话框的最下面一个编辑框中输入一个很大数目，具体视应用而定，这里1000就够了。&nbsp;<br>&nbsp;&nbsp;3 按F5重新运行程序，程序中断。Ctrl+B打开断点框，发现此断点后跟随一串说明：...487 times remaining。意思是还剩下487次没有执行，那就是说执行到513（1000－487）次时候出错的。因此，我们按步骤2所讲，更改此断点的skip次数,将1000改为513。&nbsp;<br>&nbsp;&nbsp;4 再次重新运行程序，程序执行了513次循环，然后自动停在断点处。这时，我们就可以仔细查看DoSome是如何返回0的。这样，你就避免了手指的痛苦，节省了时间。&nbsp;<br>&nbsp;&nbsp;再看位置断点其他修饰条件。如<strong>Figure 1</strong>所示，在&#8220;Enter the expression to be evaluated:&#8221;下面，可以输入一些条件，当这些条件满足时，断点才启动。譬如，刚才的程序，我们需要i为100时程序停下来，我们就可以输入在编辑框中输入&#8220;i==100&#8221;。&nbsp;<br>&nbsp;&nbsp;另外，如果在此编辑框中如果只输入变量名称，则变量发生改变时，断点才会启动。这对检测一个变量何时被修改很方便，特别对一些大程序。&nbsp;<br>&nbsp;&nbsp;用好位置断点的修饰条件，可以大大方便解决某些问题。&nbsp;<br><br><strong>二 数据断点（Data Breakpoint）</strong>&nbsp;<br>&nbsp;&nbsp;软件调试过程中，有时会发现一些数据会莫名其妙的被修改掉（如一些数组的越界写导致覆盖了另外的变量），找出何处代码导致这块内存被更改是一件棘手的事情（如果没有调试器的帮助）。恰当运用数据断点可以快速帮你定位何时何处这个数据被修改。譬如下面一段程序：</font></p>
<pre style="BACKGROUND-POSITION: 0px 0px; OVERFLOW-Y: auto; FONT-SIZE: 12px; OVERFLOW-X: auto; MARGIN: 0px 0px 1em; WIDTH: 712px; FONT-FAMILY: 'Courier New', monospace; BACKGROUND-COLOR: rgb(247,247,247); background-origin: initial; background-clip: initial"><font size=3>#include "stdafx.h" #include <string.h></string.h>  int main(int argc, char* argv[]) { 	char szName1[10]; 	char szName2[4]; 	strcpy(szName1,"shenzhen");		 	printf("%s\n", szName1);		//A  	strcpy(szName2, "vckbase");		//B 	printf("%s\n", szName1); 	printf("%s\n", szName2);  	return 0; }       </font></pre>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><font size=3>&nbsp;&nbsp;这段程序的输出是</font></p>
<pre style="BACKGROUND-POSITION: 0px 0px; OVERFLOW-Y: auto; FONT-SIZE: 12px; OVERFLOW-X: auto; MARGIN: 0px 0px 1em; WIDTH: 712px; FONT-FAMILY: 'Courier New', monospace; BACKGROUND-COLOR: rgb(247,247,247); background-origin: initial; background-clip: initial"><font size=3>      	szName1: shenzhen 	szName1: ase 	szName2: vckbase      </font></pre>
<p style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1em 0px 0.5em; PADDING-TOP: 0px"><font size=3>szName1何时被修改呢？因为没有明显的修改szName1代码。我们可以首先在A行设置普通断点，F5运行程序，程序停在A行。然后我们再设置一个数据断点。如下图：&nbsp;<br><img style="BORDER-RIGHT: 0px; BORDER-TOP: 0px; VERTICAL-ALIGN: top; BORDER-LEFT: 0px; BORDER-BOTTOM: 0px" onclick='window.open("http://blog.51cto.com/viewpic.php?refimg=" + this.src)' alt="" src="http://www.cppblog.com/ietj/admin/VC++%206_0%20中如何使用%20CRT%20调试功能来检测内存泄漏%20-%201.files/debug2.jpg" width=650 onload="if(this.width>650) this.width=650;">&nbsp;<br><font color=#0000ff><strong>Figure 2</strong>&nbsp;数据断点</font>&nbsp;<br>&nbsp;&nbsp;F5继续运行，程序停在B行，说明B处代码修改了szName1。B处明明没有修改szName1呀？但调试器指明是这一行，一般不会错，所以还是静下心来看看程序，哦，你发现了：szName2只有4个字节，而strcpy了7个字节，所以覆写了szName1。&nbsp;<br>&nbsp;&nbsp;数据断点不只是对变量改变有效，还可以设置变量是否等于某个值。譬如，你可以将Figure 2中红圈处改为条件&#8221;szName2[0]==''''y''''&#8220;,那么当szName2第一个字符为y时断点就会启动。&nbsp;<br>&nbsp;&nbsp;可以看出，数据断点相对位置断点一个很大的区别是不用明确指明在哪一行代码设置断点。&nbsp;<br><br><strong>三 其他</strong>&nbsp;<br>&nbsp;&nbsp;1 在call stack窗口中设置断点，选择某个函数，按F9设置一个断点。这样可以从深层次的函数调用中迅速返回到需要的函数。&nbsp;<br>&nbsp;&nbsp;2 Set Next StateMent命令（debug过程中，右键菜单中的命令）&nbsp;<br>&nbsp;&nbsp;此命令的作用是将程序的指令指针（EIP）指向不同的代码行。譬如，你正在调试上面那段代码，运行在A行，但你不愿意运行B行和C行代码，这时，你就可以在D行，右键，然后&#8220;Set Next StateMent&#8221;。调试器就不会执行B、C行。只要在同一函数内，此指令就可以随意跳前或跳后执行。灵活使用此功能可以大量节省调试时间。&nbsp;<br>&nbsp;&nbsp;3 watch窗口&nbsp;<br>&nbsp;&nbsp;watch窗口支持丰富的数据格式化功能。如输入0x65,u，则在右栏显示101。&nbsp;<br>&nbsp;&nbsp;实时显示windows API调用的错误：在左栏输入@err,hr。&nbsp;<br>&nbsp;&nbsp;在watch窗口中调用函数。提醒一下，调用完函数后马上在watch窗口中清除它，否则，单步调试时每一步调试器都会调用此函数。&nbsp;<br>&nbsp;&nbsp;4 messages断点不怎么实用。基本上可以用前面讲述的断点代替。&nbsp;<br><strong>总结</strong>&nbsp;<br>&nbsp;&nbsp;调试最重要的还是你要思考，要猜测你的程序可能出错的地方，然后运用你的调试器来证实你的猜测。而熟练使用上面这些技巧无疑会加快这个过程。最后，大家如果有关于调试方面的问题，我乐意参与探讨。</font></p>
</span></span><br class=Apple-interchange-newline>
<img src ="http://www.cppblog.com/ietj/aggbug/142529.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/ietj/" target="_blank">一路风尘</a> 2011-03-23 00:00 <a href="http://www.cppblog.com/ietj/articles/142529.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>如何有效地报告 Bug</title><link>http://www.cppblog.com/ietj/articles/142528.html</link><dc:creator>一路风尘</dc:creator><author>一路风尘</author><pubDate>Tue, 22 Mar 2011 15:57:00 GMT</pubDate><guid>http://www.cppblog.com/ietj/articles/142528.html</guid><wfw:comment>http://www.cppblog.com/ietj/comments/142528.html</wfw:comment><comments>http://www.cppblog.com/ietj/articles/142528.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/ietj/comments/commentRss/142528.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/ietj/services/trackbacks/142528.html</trackback:ping><description><![CDATA[<span class=Apple-style-span style="WORD-SPACING: 0px; FONT: medium Simsun; TEXT-TRANSFORM: none; COLOR: rgb(0,0,0); TEXT-INDENT: 0px; WHITE-SPACE: normal; LETTER-SPACING: normal; BORDER-COLLAPSE: separate; orphans: 2; widows: 2; webkit-border-horizontal-spacing: 0px; webkit-border-vertical-spacing: 0px; webkit-text-decorations-in-effect: none; webkit-text-size-adjust: auto; webkit-text-stroke-width: 0px"><span class=Apple-style-span style="FONT-SIZE: 13px; FONT-FAMILY: Verdana, Geneva, Arial, Helvetica, sans-serif">
<h2 style="MARGIN-TOP: 10px; FONT-SIZE: 13px; MARGIN-BOTTOM: 3px">作者：<a style="COLOR: navy; TEXT-DECORATION: none" href="http://pobox.com/~anakin/">Simon Tatham</a><span class=Apple-converted-space>&nbsp;</span>专业的自由软件程序员</h2>
<p class=author style="MARGIN: 10px 0px">翻译：Dasn</p>
<p style="MARGIN: 10px 0px">[<span class=Apple-converted-space>&nbsp;</span><a style="COLOR: navy; TEXT-DECORATION: none" href="http://www.chiark.greenend.org.uk/~sgtatham/bugs.html">English</a><span class=Apple-converted-space>&nbsp;</span>|<span class=Apple-converted-space>&nbsp;</span><strong>简体中文</strong><span class=Apple-converted-space>&nbsp;</span>|<span class=Apple-converted-space>&nbsp;</span><a style="COLOR: navy; TEXT-DECORATION: none" href="http://www.chiark.greenend.org.uk/~sgtatham/bugs-cz.html">Česky</a><span class=Apple-converted-space>&nbsp;</span>|<span class=Apple-converted-space>&nbsp;</span><a style="COLOR: navy; TEXT-DECORATION: none" href="http://www.chiark.greenend.org.uk/~sgtatham/bugs-da.html">Dansk</a><span class=Apple-converted-space>&nbsp;</span>|<span class=Apple-converted-space>&nbsp;</span><a style="COLOR: navy; TEXT-DECORATION: none" href="http://www.chiark.greenend.org.uk/~sgtatham/bugs-de.html">Deutsch</a><span class=Apple-converted-space>&nbsp;</span>|<span class=Apple-converted-space>&nbsp;</span><a style="COLOR: navy; TEXT-DECORATION: none" href="http://www.chiark.greenend.org.uk/~sgtatham/bugs-fr.html">Fran&#231;ais</a><span class=Apple-converted-space>&nbsp;</span>|<span class=Apple-converted-space>&nbsp;</span><a style="COLOR: navy; TEXT-DECORATION: none" href="http://www.chiark.greenend.org.uk/~sgtatham/bugs-pl.html">Polski</a><span class=Apple-converted-space>&nbsp;</span>|<span class=Apple-converted-space>&nbsp;</span><a style="COLOR: navy; TEXT-DECORATION: none" href="http://www.chiark.greenend.org.uk/~sgtatham/bugs-ru.html">Русский</a><span class=Apple-converted-space>&nbsp;</span>|<span class=Apple-converted-space>&nbsp;</span><a style="COLOR: navy; TEXT-DECORATION: none" href="http://www.chiark.greenend.org.uk/~sgtatham/bugs-tw.html">繁體中文</a><span class=Apple-converted-space>&nbsp;</span>]</p>
<h3 style="FONT-SIZE: 12px"><a name=intro>引言</a></h3>
<p style="MARGIN: 10px 0px">为公众写过软件的人，大概都收到过很拙劣的bug（计算机程序代码中的错误或程序运行时的瑕疵——译者注）报告，例如：</p>
<p style="MARGIN: 10px 0px">在报告中说&#8220;不好用&#8221;；</p>
<p style="MARGIN: 10px 0px">所报告内容毫无意义；</p>
<p style="MARGIN: 10px 0px">在报告中用户没有提供足够的信息；</p>
<p style="MARGIN: 10px 0px">在报告中提供了<em>错误</em>信息；</p>
<p style="MARGIN: 10px 0px">所报告的问题是由于用户的过失而产生的；</p>
<p style="MARGIN: 10px 0px">所报告的问题是由于其他程序的错误而产生的；</p>
<p style="MARGIN: 10px 0px">所报告的问题是由于网络错误而产生的；</p>
<p style="MARGIN: 10px 0px">这便是为什么&#8220;技术支持&#8221;被认为是一件可怕的工作，因为有拙劣的bug报告需要处理。然而并不是所有的bug报告都令人生厌：我在业余时间维护自由软件，有时我会收到非常清晰、有帮助并且<em>&#8220;有内容&#8221;</em>的bug报告。</p>
<p style="MARGIN: 10px 0px">在这里我会尽力阐明如何写一个好的bug报告。我非常希望每一个人在报告bug之前都读一下这篇短文，当然我也希望用户在给<em>我</em>报告bug之前已经读过这篇文章。</p>
<p style="MARGIN: 10px 0px">简单地说，报告bug的目的是为了让程序员看到程序的错误。您可以亲自示范，也可以给出能导致程序出错的、详尽的操作步骤。如果程序出错了，程序员会收集额外的信息直到找到错误的原因；如果程序没有出错，那么他们会请您继续关注这个问题，收集相关的信息。</p>
<p style="MARGIN: 10px 0px">在bug报告里，要设法搞清什么是事实（例如：&#8220;我在电脑旁&#8221;和&#8220;XX出现了&#8221;）什么是推测（例如：&#8220;我<em>想</em>问题可能是出在&#8230;&#8230;&#8221;）。如果愿意的话，您可以省去推测，但是千万别省略事实。</p>
<p style="MARGIN: 10px 0px">当您报告bug的时候（既然您已经这么做了），一定是希望bug得到及时修正。所以此时针对程序员的任何过激或亵渎的言语（甚至谩骂）都是与事无补的——因为这可能是程序员的错误，也有可能是您的错误，也许您有权对他们发火，但是如果您能多提供一些有用的信息（而不是激愤之词）或许bug会被更快的修正。除此以外，请记住：如果是免费软件，作者提供给我们已经是出于好心，所以要是太多的人对他们无礼，他们可能就要&#8220;收起&#8221;这份好心了。</p>
<h3 style="FONT-SIZE: 12px"><a name=respect>&#8220;程序不好用&#8221;</a></h3>
<p style="MARGIN: 10px 0px">程序员不是弱智：如果程序一点都不好用，他们不可能不知道。他们不知道一定是因为程序在他们看来工作得很正常。所以，或者是您作过一些与他们不同的操作，或者是您的环境与他们不同。他们需要信息，报告bug也是为了提供信息。信息总是越多越好。</p>
<p style="MARGIN: 10px 0px">许多程序，特别是自由软件，会公布一个&#8220;已知bug列表&#8221;。如果您找到的bug在列表里已经有了，那就不必再报告了，但是如果您认为自己掌握的信息比列表中的丰富，那无论如何也要与程序员联系。您提供的信息可能会使他们更简单地修复bug。</p>
<p style="MARGIN: 10px 0px">本文中提到的都是一些指导方针，没有哪一条是必须恪守的准则。不同的程序员会喜欢不同形式的bug报告。如果程序附带了一套报告bug的准则，一定要读。如果它与本文中提到的规则相抵触，那么请以它为准。</p>
<p style="MARGIN: 10px 0px">如果您不是报告bug，而是寻求帮助，您应该说明您曾经到哪里找过答案，（例如：我看了第四章和第五章的第二节，但我找不到解决的办法。）这会使程序员了解用户喜欢到哪里去找答案，从而使程序员把帮助文档做得更容易使用。</p>
<h3 style="FONT-SIZE: 12px"><a name=showme>&#8220;演示给我看&#8221;</a></h3>
<p style="MARGIN: 10px 0px">报告bug的最好的方法之一是&#8220;演示&#8221;给程序员看。让程序员站在电脑前，运行他们的程序，指出程序的错误。让他们看着您启动电脑、运行程序、如何进行操作以及程序对您的输入有何反应。</p>
<p style="MARGIN: 10px 0px">他们对自己写的软件了如指掌，他们知道哪些地方不会出问题，而哪些地方最可能出问题。他们本能地知道应该注意什么。在程序真的出错之前，他们可能已经注意到某些地方不对劲，这些都会给他们一些线索。他们会观察程序测试中的每一个<em>细节</em>，并且选出他们认为有用的信息。</p>
<p style="MARGIN: 10px 0px">这些可能还不够。也许他们觉得还需要更多的信息，会请您重复刚才的操作。他们可能在这期间需要与您交流一下，以便在他们需要的时候让bug重新出现。他们可能会改变一些操作，看看这个错误的产生是个别问题还是相关的一类问题。如果您不走运，他们可能需要坐下来，拿出一堆开发工具，花上几个小时来<em>好好地</em>研究一下。但是最重要的是在程序出错的时候让程序员在电脑旁。一旦他们看到了问题，他们通常会找到原因并开始试着修改。</p>
<h3 style="FONT-SIZE: 12px"><a name=showmehow>&#8220;告诉我该怎么做&#8221;</a></h3>
<p style="MARGIN: 10px 0px">如今是网络时代，是信息交流的时代。我可以点一下鼠标把自己的程序送到俄罗斯的某个朋友那里，当然他也可以用同样简单的方法给我一些建议。但是如果我的程序出了什么问题，我<em>不可能</em>在他旁边。&#8220;演示&#8221;是很好的办法，但是常常做不到。</p>
<p style="MARGIN: 10px 0px">如果您必须报告bug，而此时程序员又不在您身边，那么您就要想办法让bug<em>重现</em>在他们面前。当他们亲眼看到错误时，就能够进行处理了。</p>
<p style="MARGIN: 10px 0px">确切地告诉程序员您做了些什么。如果是一个图形界面程序，告诉他们您按了哪个按钮，依照什么顺序按的。如果是一个命令行程序，精确的告诉他们您键入了什么命令。您应该尽可能详细地提供您所键入的命令和程序的反应。</p>
<p style="MARGIN: 10px 0px">把您能想到的所有的输入方式都告诉程序员，如果程序要读取一个文件，您可能需要发一个文件的拷贝给他们。如果程序需要通过网络与另一台电脑通讯，您或许不能把那台电脑复制过去，但至少可以说一下电脑的类型和安装了哪些软件（如果可以的话）。</p>
<h3 style="FONT-SIZE: 12px"><a name=discribe>&#8220;哪儿出错了？在我看来一切正常哦！&#8221;</a></h3>
<p style="MARGIN: 10px 0px">如果您给了程序员一长串输入和指令，他们执行以后没有出现错误，那是因为您没有给他们足够的信息，可能错误不是在每台计算机上都出现，您的系统可能和他们的在某些地方不一样。有时候程序的行为可能和您预想的不一样，这也许是误会，但是您会认为程序出错了，程序员却认为这是对的。</p>
<p style="MARGIN: 10px 0px">同样也要描述发生了什么。精确的描述您看到了什么。告诉他们为什么您觉得自己所看到的是错误的，最好再告诉他们，您认为自己应该看到什么。如果您只是说：&#8220;程序出错了&#8221;，那您很可能漏掉了非常重要的信息。</p>
<p style="MARGIN: 10px 0px">如果您看到了错误消息，一定要仔细、准确的告诉程序员，这<em>确实</em>很重要。在这种情况下，程序员只要修正错误，而不用去找错误。他们需要知道是什么出问题了，系统所报的错误消息正好帮助了他们。如果您没有更好的方法记住这些消息，就把它们写下来。只报告&#8220;程序出了一个错&#8221;是毫无意义的，除非您把错误消息一块报上来。</p>
<p style="MARGIN: 10px 0px">特殊情况下，如果有错误消息号，<em>一定</em>要把这些号码告诉程序员。不要以为您看不出任何意义，它就没有意义。错误消息号包含了能被程序员读懂的各种信息，并且很有可能包含重要的线索。给错误消息编号是因为用语言描述计算机错误常常令人费解。用这种方式告诉您错误的所在是一个最好的办法。</p>
<p style="MARGIN: 10px 0px">在这种情形下，程序员的排错工作会十分高效。他们不知道发生了什么，也不可能到现场去观察，所以他们一直在搜寻有价值的线索。错误消息、错误消息号以及一些莫名其妙的延迟，都是很重要的线索，就像办案时的指纹一样重要，保存好。</p>
<p style="MARGIN: 10px 0px">如果您使用UNIX系统，程序可能会产生一个内核输出（coredump）。内核输出是特别有用的线索来源，别扔了它们。另一方面，大多数程序员不喜欢收到含有大量内核输出文件的EMAIL，所以在发邮件之前最好先问一下。还有一点要注意：内核输出文件记录了完整的程序状态，也就是说任何秘密（可能当时程序正在处理一些私人信息或秘密数据）都可能包含在内核输出文件里。</p>
<h3 style="FONT-SIZE: 12px"><a name=antelope>&#8220;出了问题之后，我做了&#8230;&#8230;&#8221;</a></h3>
<p style="MARGIN: 10px 0px">当一个错误或bug发生的时候，您可能会做许多事情。但是大多数人会使事情变的更糟。我的一个朋友在学校里误删了她所有的Word文件，在找人帮忙之前她重装了Word，又运行了一遍碎片整理程序，这些操作对于恢复文件是毫无益处的，因为这些操作搞乱了磁盘的文件区块。恐怕在这个世界上没有一种反删除软件能恢复她的文件了。如果她不做任何操作，或许还有一线希望。</p>
<p style="MARGIN: 10px 0px">这种用户仿佛一只被逼到墙角的鼬（黄鼠狼、紫貂一类的动物——译者注）：背靠墙壁，面对死亡的降临奋起反扑，疯狂攻击。他们认为做点什么总比什么都不做强。然而这些在处理计算机软件问题时并不适用。</p>
<p style="MARGIN: 10px 0px">不要做鼬，做一只羚羊。当一只羚羊面对料想不到的情况或受到惊吓时，它会一动不动，是为了不吸引任何注意，与此同时也在思考解决问题的最好办法（如果羚羊有一条技术支持热线，此时占线。）。然后，一旦它找到了最安全的行动方案，它便去做。</p>
<p style="MARGIN: 10px 0px">当程序出毛病的时候，立刻停止正在做的<em>任何操作</em>。不要按任何健。仔细地看一下屏幕，注意那些不正常的地方，记住它或者写下来。然后慎重地点击&#8220;确定&#8221; 或&#8220;取消&#8221;，选择一个最安全的。学着养成一种条件反射——一旦电脑出了问题，先不要动。要想摆脱这个问题，关掉受影响的程序或者重新启动计算机都不好，一个解决问题的好办法是让问题再次产生。程序员们喜欢可以被重现的问题，快乐的程序员可以更快而且更有效率的修复bug。</p>
<h3 style="FONT-SIZE: 12px"><a name=symptoms>&#8220;我想粒子的跃迁与错误的极化有关&#8221;</a></h3>
<p style="MARGIN: 10px 0px">并不只是非专业的用户才会写出拙劣的bug报告，我见过一些非常差的bug报告出自程序员之手，有些还是非常优秀的程序员。</p>
<p style="MARGIN: 10px 0px">有一次我与另一个程序员一起工作，他一直在找代码中的bug，他常常遇到一个bug，但是不会解决，于是就叫我帮忙。&#8220;出什么毛病了？&#8221;我问。而他的回答却总是一些关于bug的意见。如果他的观点正确，那的确是一件好事。这意味着他已经完成了工作的一半，并且我们可以一起完成另一半工作。这是有效率并有用的。</p>
<p style="MARGIN: 10px 0px">但事实上他常常是错的。这就会使我们花上半个小时在原本正确的代码里来回寻找错误，而实际上问题出在别的地方。我敢肯定他不会对医生这么做。&#8220;大夫，我得了Hydroyoyodyne（真是怪病——译者），给我开个方子&#8221;，人们知道不该对一位医生说这些。您描述一下症状，哪个地方不舒服，哪里疼、起皮疹、发烧&#8230;&#8230;让医生诊断您得了什么病，应该怎样治疗。否则医生会把您当做疑心病或精神病患者打发了，这似乎没什么不对。</p>
<p style="MARGIN: 10px 0px">做程序员也是一样。即便您自己的&#8220;诊断&#8221;有时真的有帮助，也要只说&#8220;症状&#8221;。&#8220;诊断&#8221;是可说可不说的，但是&#8220;症状&#8221;一定要说。同样，在bug报告里面附上一份针对bug而做出修改的源代码是有用处的，但它并不能替代bug报告本身。</p>
<p style="MARGIN: 10px 0px">如果程序员向您询问额外的信息，千万别应付。曾经有一个人向我报告bug，我让他试一个命令，我知道这个命令不好用，但我是要看看程序会返回一个什么错误（这是很重要的线索）。但是这位老兄根本就没试，他在回复中说&#8220;那肯定不好用&#8221;，于是我又花了好些时间才说服他试了一下那个命令。</p>
<p style="MARGIN: 10px 0px">用户多动动脑筋对程序员的工作是有帮助的。即使您的推断是错误的，程序员也应该感谢您，至少您<em>想</em>去帮助他们，使他们的工作变的更简单。不过千万别忘了报告&#8220;症状&#8221;，否则只会使事情变得更糟。</p>
<h3 style="FONT-SIZE: 12px"><a name=intermittent>&#8220;真是奇怪，刚才还不好用，怎么现在又好了？&#8221;</a></h3>
<p style="MARGIN: 10px 0px">&#8220;间歇性错误&#8221;着实让程序员发愁。相比之下，进行一系列简单的操作便能导致错误发生的问题是简单的。程序员可以在一个便于观察的条件下重复那些操作，观察每一个细节。太多的问题在这种情况下不能解决，例如：程序每星期出一次错，或者偶然出一次错，或者在程序员面前从不出错（程序员一离开就出错。——译者）。当然还有就是程序的截止日期到了，那肯定要出错。</p>
<p style="MARGIN: 10px 0px">大多数&#8220;间歇性错误&#8221;并不是真正的&#8220;间歇&#8221;。其中的大多数错误与某些地方是有联系的。有一些错误可能是内存泄漏产生的，有一些可能是别的程序在不恰当的时候修改某个重要文件造成的，还有一些可能发生在每一个小时的前半个小时中（我确实遇到过这种事情）。</p>
<p style="MARGIN: 10px 0px">同样，如果您能使bug重现，而程序员不能，那很有可能是他们的计算机和您的计算机在某些地方是不同的，这种不同引起了问题。我曾写过一个程序，它的窗口可以<em>蜷缩</em>成一个小球呆在屏幕的左上角，它在别的计算机上只能在 800x600 的解析度工作，但是在我的机器上却可以在 1024x768 下工作。</p>
<p style="MARGIN: 10px 0px">程序员想要了解任何与您发现的问题相关的事情。有可能的话您到另一台机器上试试，多试几次，两次，三次，看看问题是不是经常发生。如果问题出现在您进行了一系列操作之后，不是您想让它出现它就会出现，这就有可能是长时间的运行或处理大文件所导致的错误。程序崩溃的时候，您要尽可能的记住您都做了些什么，并且如果您看到任何图形,也别忘了提一下。您提供的任何事情都是有帮助的。即使只是概括性的描述（例如：当后台有EMACS运行时，程序常常出错），这虽然不能提供导致问题的直接线索，但是可能帮助程序员重现问题。</p>
<p style="MARGIN: 10px 0px">最重要的是：程序员想要确定他们正在处理的是一个真正的&#8220;间歇性错误&#8221;呢，还是一个在另一类特定的计算机上才出现的错误。他们想知道有关您计算机的许多细节，以便了解您的机器与他们的有什么不同。有许多细节都依仗特定的程序，但是有一件东西您一定要提供——版本号。程序的版本、操作系统的版本以及与问题有关的程序的版本。</p>
<h3 style="FONT-SIZE: 12px"><a name=clarity>&#8220;我把磁盘装进了 Windows&#8230;&#8230;&#8221;</a></h3>
<p style="MARGIN: 10px 0px">表意清楚在一份bug报告里是最基本的要求。如果程序员不知道您说的是什么意思，那您就跟没说一样。我收到的bug报告来自世界各地，有许多是来自非英语国家，他们通常为自己的英文不好而表示歉意。总的来说，这些用户发来的bug报告通常是清晰而且有用的。几乎所有不清晰的bug报告都是来自母语是英语的人，他们总是以为只要自己随便说说，程序员就能明白。</p>
<ul>
    <li><em>精确</em>。如果做相同的事情有两种方法，请说明您用的是哪一种。例如：&#8220;我选择了&#8216;载入&#8217;&#8221;，可能意味着&#8220;我用鼠标点击&#8216;载入&#8217;&#8221;或&#8220;我按下了&#8216;ALT+L&#8217;&#8221;，说清楚您用了哪种方法，有时候这也有关系。</li>
    <li><em>详细</em>。信息宁多毋少！如果您说了很多，程序员可以略去一部分，可是如果您说的太少，他们就不得不回过头再去问您一些问题。有一次我收到了一份bug报告只有一句话，每一次我问他更多事情时，他每次的回复都是一句话，于是我花了几个星期的时间才得到了有用的信息。</li>
    <li><em>慎用代词</em>。诸如&#8220;它&#8221;，&#8220;窗体&#8221;这些词，当它们指代不清晰的时候不要用。来看看这句话：&#8220;我运行了FooApp，它弹出一个警告窗口，我试着关掉它，它就崩溃了。&#8221;这种表述并不清晰，用户究竟关掉了哪个窗口？是警告窗口还是整个FooApp程序？您可以这样说，&#8220;我运行FooApp程序时弹出一个警告窗口，我试着关闭警告窗口，FooApp崩溃了。&#8221;这样虽然罗嗦点，但是很清晰不容易产生误解。</li>
    <li><em>检查</em>。重新读一遍您写的bug报告，<em>您</em>觉得它是否清晰？如果您列出了一系列能导致程序出错的操作，那么照着做一遍，看看您是不是漏写了一步。</li>
</ul>
<h3 style="FONT-SIZE: 12px"><a name=summary>小结：</a></h3>
<ul>
    <li>bug报告的首要目的是让程序员亲眼看到错误。如果您不能亲自做给他们看，给他们能使程序出错的详细的操作步骤。</li>
    <li>如果首要目的不能达成，程序员<em>不能</em>看到程序出错。这就需要bug报告的第二个目的来描述程序的什么地方出毛病了。详细的描述每一件事情：您看到了什么，您想看到什么，把错误消息记下来，<em>尤其</em>是&#8220;错误消息号&#8221;。</li>
    <li>当您的计算机做了什么您料想不到的事，<em>不要动</em>！在您平静下来之前什么都别做。不要做您认为不安全的事。</li>
    <li>尽量试着自己&#8220;诊断&#8221;程序出错的原因（如果您认为自己可以的话）。即使做出了&#8220;诊断&#8221;，您仍然应该报告&#8220;症状&#8221;。</li>
    <li>如果程序员需要，请准备好额外的信息。如果他们不需要，就不会问您要。他们不会故意为难自己。您手头上一定要有程序的版本号，它很可能是必需品。</li>
    <li>表述清楚，确保您的意思不能被曲解。</li>
    <li>总的来说，最重要的是要做到<em>精确</em>。程序员喜欢精确。</li>
</ul>
</span></span>
<img src ="http://www.cppblog.com/ietj/aggbug/142528.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/ietj/" target="_blank">一路风尘</a> 2011-03-22 23:57 <a href="http://www.cppblog.com/ietj/articles/142528.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>makefile教程---unix/linux下</title><link>http://www.cppblog.com/ietj/articles/43299.html</link><dc:creator>一路风尘</dc:creator><author>一路风尘</author><pubDate>Tue, 26 Feb 2008 13:52:00 GMT</pubDate><guid>http://www.cppblog.com/ietj/articles/43299.html</guid><wfw:comment>http://www.cppblog.com/ietj/comments/43299.html</wfw:comment><comments>http://www.cppblog.com/ietj/articles/43299.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/ietj/comments/commentRss/43299.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/ietj/services/trackbacks/43299.html</trackback:ping><description><![CDATA[<p>
<div class=twikiToc>
<ul>
    <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#Makefile学习教程: 跟我一起写 Mak"><u><font color=#0066cc>Makefile学习教程: 跟我一起写 Makefile</font></u></a>
    <ul>
        <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#0 Makefile概述"><u><font color=#0066cc>0 Makefile概述</font></u></a>
        <ul>
            <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#0.1 关于程序的编译和链接"><u><font color=#0066cc>0.1 关于程序的编译和链接</font></u></a> </li>
        </ul>
        <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#1 Makefile 介绍"><u><font color=#0066cc>1 Makefile 介绍</font></u></a>
        <ul>
            <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#1.1 Makefile的规则"><u><font color=#0066cc>1.1 Makefile的规则</font></u></a>
            <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#1.2 一个示例"><u><font color=#0066cc>1.2 一个示例</font></u></a>
            <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#1.3 make是如何工作的"><u><font color=#0066cc>1.3 make是如何工作的</font></u></a>
            <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#1.4 makefile中使用变量"><u><font color=#0066cc>1.4 makefile中使用变量</font></u></a>
            <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#1.5 让make自动推导"><u><font color=#0066cc>1.5 让make自动推导</font></u></a>
            <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#1.6 另类风格的makefile"><u><font color=#0066cc>1.6 另类风格的makefile</font></u></a>
            <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#1.7 清空目标文件的规则"><u><font color=#0066cc>1.7 清空目标文件的规则</font></u></a> </li>
        </ul>
        <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#2 Makefile 总述"><u><font color=#0066cc>2 Makefile 总述</font></u></a>
        <ul>
            <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#2.1 Makefile里有什么？"><u><font color=#0066cc>2.1 Makefile里有什么？</font></u></a>
            <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#2.2Makefile的文件名"><u><font color=#0066cc>2.2Makefile的文件名</font></u></a>
            <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#2.3 引用其它的Makefile"><u><font color=#0066cc>2.3 引用其它的Makefile</font></u></a>
            <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#2.4 环境变量 MAKEFILES"><font color=#0066cc><u>2.4 环境变量 MAKEFILES </u></font></a>
            <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#2.5 make的工作方式"><u><font color=#0066cc>2.5 make的工作方式</font></u></a> </li>
        </ul>
        <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#3 Makefile书写规则"><u><font color=#0066cc>3 Makefile书写规则</font></u></a>
        <ul>
            <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#3.1 规则举例"><u><font color=#0066cc>3.1 规则举例</font></u></a>
            <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#3.2 规则的语法"><u><font color=#0066cc>3.2 规则的语法</font></u></a>
            <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#3.3 在规则中使用通配符"><u><font color=#0066cc>3.3 在规则中使用通配符</font></u></a>
            <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#3.4 文件搜寻"><u><font color=#0066cc>3.4 文件搜寻</font></u></a>
            <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#3.5 伪目标"><u><font color=#0066cc>3.5 伪目标</font></u></a>
            <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#3.6 多目标"><u><font color=#0066cc>3.6 多目标</font></u></a>
            <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#3.7 静态模式"><u><font color=#0066cc>3.7 静态模式</font></u></a>
            <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#3.8 自动生成依赖性"><u><font color=#0066cc>3.8 自动生成依赖性</font></u></a> </li>
        </ul>
        <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#4 Makefile 书写命令"><u><font color=#0066cc>4 Makefile 书写命令</font></u></a>
        <ul>
            <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#4.1 显示命令"><u><font color=#0066cc>4.1 显示命令</font></u></a>
            <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#4.2 命令执行"><u><font color=#0066cc>4.2 命令执行</font></u></a>
            <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#4.3 命令出错"><u><font color=#0066cc>4.3 命令出错</font></u></a>
            <li><a href="http://www.stlchina.org/twiki/bin/view.pl/ScriptProgram/LearnMakefile#4.4 嵌套执行make"><u><font color=#0066cc>4.4 嵌套执行make</font></u></a> </li>
        </ul>
        </li>
    </ul>
    </li>
</ul>
</div>
<h3><a name="0 Makefile概述"></a>0 Makefile概述 </h3>
<hr>
什么是makefile？或许很多Winodws的程序员都不知道这个东西，因为那些Windows的IDE都为你做了这个工作，但我觉得要作一个好的和professional的程序员，makefile还是要懂。这就好像现在有这么多的HTML的编辑器，但如果你想成为一个专业人士，你还是要了解HTML的标识的含义。特别在Unix下的软件编译，你就不能不自己写makefile了，会不会写makefile，从一个侧面说明了一个人是否具备完成大型工程的能力。
<p>因为，makefile关系到了整个工程的编译规则。一个工程中的源文件不计数，其按类型、功能、模块分别放在若干个目录中，makefile定义了一系列的规则来指定，哪些文件需要先编译，哪些文件需要后编译，哪些文件需要重新编译，甚至于进行更复杂的功能操作，因为makefile就像一个Shell脚本一样，其中也可以执行操作系统的命令。
<p>makefile带来的好处就是——&#8220;自动化编译&#8221;，一旦写好，只需要一个make命令，整个工程完全自动编译，极大的提高了软件开发的效率。make是一个命令工具，是一个解释makefile中指令的命令工具，一般来说，大多数的IDE都有这个命令，比如：Delphi的make，Visual C++的nmake，Linux下GNU的make。可见，makefile都成为了一种在工程方面的编译方法。
<p>现在讲述如何写makefile的文章比较少，这是我想写这篇文章的原因。当然，不同产商的make各不相同，也有不同的语法，但其本质都是在&#8220;文件依赖性&#8221;上做文章，这里，我仅对GNU的make进行讲述，我的环境是RedHat Linux 8.0，make的版本是3.80。必竟，这个make是应用最为广泛的，也是用得最多的。而且其还是最遵循于IEEE 1003.2-1992 标准的（POSIX.2）。
<p>在这篇文档中，将以C/C++的源码作为我们基础，所以必然涉及一些关于C/C++的编译的知识，相关于这方面的内容，还请各位查看相关的编译器的文档。这里所默认的编译器是UNIX下的GCC和CC。
<p>
<h4><a name="0.1 关于程序的编译和链接"></a>0.1 关于程序的编译和链接 </h4>
在此，我想多说关于程序编译的一些规范和方法，一般来说，无论是C、C++、还是pas，首先要把源文件编译成中间代码文件，在Windows下也就是 .obj 文件，UNIX下是 .o 文件，即 Object File，这个动作叫做编译（compile）。然后再把大量的Object File合成执行文件，这个动作叫作链接（link）。
<p>编译时，编译器需要的是语法的正确，函数与变量的声明的正确。对于后者，通常是你需要告诉编译器头文件的所在位置（头文件中应该只是声明，而定义应该放在C/C++文件中），只要所有的语法正确，编译器就可以编译出中间目标文件。一般来说，每个源文件都应该对应于一个中间目标文件（O文件或是OBJ文件）。
<p>链接时，主要是链接函数和全局变量，所以，我们可以使用这些中间目标文件（O文件或是OBJ文件）来链接我们的应用程序。链接器并不管函数所在的源文件，只管函数的中间目标文件（Object File），在大多数时候，由于源文件太多，编译生成的中间目标文件太多，而在链接时需要明显地指出中间目标文件名，这对于编译很不方便，所以，我们要给中间目标文件打个包，在Windows下这种包叫&#8220;库文件&#8221;（Library File)，也就是 .lib 文件，在UNIX下，是Archive File，也就是 .a 文件。
<p>总结一下，源文件首先会生成中间目标文件，再由中间目标文件生成执行文件。在编译时，编译器只检测程序语法，和函数、变量是否被声明。如果函数未被声明，编译器会给出一个警告，但可以生成Object File。而在链接程序时，链接器会在所有的Object File中找寻函数的实现，如果找不到，那到就会报链接错误码（Linker Error），在VC下，这种错误一般是：Link 2001错误，意思说是说，链接器未能找到函数的实现。你需要指定函数的Object File.
<p>好，言归正传，GNU的make有许多的内容，闲言少叙，还是让我们开始吧。
<p>
<h3><a name="1 Makefile 介绍"></a>1 Makefile 介绍 </h3>
<hr>
make命令执行时，需要一个 Makefile 文件，以告诉make命令需要怎么样的去编译和链接程序。
<p>首先，我们用一个示例来说明Makefile的书写规则。以便给大家一个感兴认识。这个示例来源于GNU的make使用手册，在这个示例中，我们的工程有8个C文件，和3个头文件，我们要写一个Makefile来告诉make命令如何编译和链接这几个文件。我们的规则是：
<ol>
    <li>如果这个工程没有编译过，那么我们的所有C文件都要编译并被链接。
    <li>如果这个工程的某几个C文件被修改，那么我们只编译被修改的C文件，并链接目标程序。
    <li>如果这个工程的头文件被改变了，那么我们需要编译引用了这几个头文件的C文件，并链接目标程序。 </li>
</ol>
<p>只要我们的Makefile写得够好，所有的这一切，我们只用一个make命令就可以完成，make命令会自动智能地根据当前的文件修改的情况来确定哪些文件需要重编译，从而自己编译所需要的文件和链接目标程序。
<h4><a name="1.1 Makefile的规则"></a>1.1 Makefile的规则 </h4>
在讲述这个Makefile之前，还是让我们先来粗略地看一看Makefile的规则。
<pre style="PADDING-BOTTOM: 0px">target ... : prerequisites ...
command
...
...
</pre>
target也就是一个目标文件，可以是Object File，也可以是执行文件。还可以是一个标签（Label），对于标签这种特性，在后续的&#8220;伪目标&#8221;章节中会有叙述。
<p>prerequisites就是，要生成那个target所需要的文件或是目标。
<p>command也就是make需要执行的命令。（任意的Shell命令）
<p>这是一个文件的依赖关系，也就是说，target这一个或多个的目标文件依赖于prerequisites中的文件，其生成规则定义在command中。说白一点就是说，prerequisites中如果有一个以上的文件比target文件要新的话，command所定义的命令就会被执行。这就是Makefile的规则。也就是Makefile中最核心的内容。
<p>说到底，Makefile的东西就是这样一点，好像我的这篇文档也该结束了。呵呵。还不尽然，这是Makefile的主线和核心，但要写好一个Makefile还不够，我会以后面一点一点地结合我的工作经验给你慢慢到来。内容还多着呢。：）
<h4><a name="1.2 一个示例"></a>1.2 一个示例 </h4>
正如前面所说的，如果一个工程有3个头文件，和8个C文件，我们为了完成前面所述的那三个规则，我们的Makefile应该是下面的这个样子的。
<pre style="PADDING-BOTTOM: 0px">    edit : main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
cc -o edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h buffer.h
cc -c display.c
insert.o : insert.c defs.h buffer.h
cc -c insert.c
search.o : search.c defs.h buffer.h
cc -c search.c
files.o : files.c defs.h buffer.h command.h
cc -c files.c
utils.o : utils.c defs.h
cc -c utils.c
clean :
rm edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
</pre>
反斜杠（\）是换行符的意思。这样比较便于Makefile的易读。我们可以把这个内容保存在文件为&#8220;Makefile&#8221;或&#8220;makefile&#8221;的文件中，然后在该目录下直接输入命令&#8220;make&#8221;就可以生成执行文件edit。如果要删除执行文件和所有的中间目标文件，那么，只要简单地执行一下&#8220;make clean&#8221;就可以了。
<p>在这个makefile中，目标文件（target）包含：执行文件edit和中间目标文件（*.o），依赖文件（prerequisites）就是冒号后面的那些 .c 文件和 .h文件。每一个 .o 文件都有一组依赖文件，而这些 .o 文件又是执行文件 edit 的依赖文件。依赖关系的实质上就是说明了目标文件是由哪些文件生成的，换言之，目标文件是哪些文件更新的。
<p>在定义好依赖关系后，后续的那一行定义了如何生成目标文件的操作系统命令，一定要以一个Tab键作为开头。记住，make并不管命令是怎么工作的，他只管执行所定义的命令。make会比较targets文件和prerequisites文件的修改日期，如果prerequisites文件的日期要比targets文件的日期要新，或者target不存在的话，那么，make就会执行后续定义的命令。
<p>这里要说明一点的是，clean不是一个文件，它只不过是一个动作名字，有点像C语言中的lable一样，其冒号后什么也没有，那么，make就不会自动去找文件的依赖性，也就不会自动执行其后所定义的命令。要执行其后的命令，就要在make命令后明显得指出这个lable的名字。这样的方法非常有用，我们可以在一个makefile中定义不用的编译或是和编译无关的命令，比如程序的打包，程序的备份，等等。
<p>
<h4><a name="1.3 make是如何工作的"></a>1.3 make是如何工作的 </h4>
在默认的方式下，也就是我们只输入make命令。那么，
<p>
<ol>
    <li>make会在当前目录下找名字叫&#8220;Makefile&#8221;或&#8220;makefile&#8221;的文件。
    <li>如果找到，它会找文件中的第一个目标文件（target），在上面的例子中，他会找到&#8220;edit&#8221;这个文件，并把这个文件作为最终的目标文件。
    <li>如果edit文件不存在，或是edit所依赖的后面的 .o 文件的文件修改时间要比edit这个文件新，那么，他就会执行后面所定义的命令来生成edit这个文件。
    <li>如果edit所依赖的.o文件也存在，那么make会在当前文件中找目标为.o文件的依赖性，如果找到则再根据那一个规则生成.o文件。（这有点像一个堆栈的过程）
    <li>当然，你的C文件和H文件是存在的啦，于是make会生成 .o 文件，然后再用 .o 文件生命make的终极任务，也就是执行文件edit了。 </li>
</ol>
<p>这就是整个make的依赖性，make会一层又一层地去找文件的依赖关系，直到最终编译出第一个目标文件。在找寻的过程中，如果出现错误，比如最后被依赖的文件找不到，那么make就会直接退出，并报错，而对于所定义的命令的错误，或是编译不成功，make根本不理。make只管文件的依赖性，即，如果在我找了依赖关系之后，冒号后面的文件还是不在，那么对不起，我就不工作啦。
<p>通过上述分析，我们知道，像clean这种，没有被第一个目标文件直接或间接关联，那么它后面所定义的命令将不会被自动执行，不过，我们可以显示要make执行。即命令——&#8220;make clean&#8221;，以此来清除所有的目标文件，以便重编译。
<p>于是在我们编程中，如果这个工程已被编译过了，当我们修改了其中一个源文件，比如file.c，那么根据我们的依赖性，我们的目标file.o会被重编译（也就是在这个依性关系后面所定义的命令），于是file.o的文件也是最新的啦，于是file.o的文件修改时间要比edit要新，所以edit也会被重新链接了（详见edit目标文件后定义的命令）。
<p>而如果我们改变了&#8220;command.h&#8221;，那么，kdb.o、command.o和files.o都会被重编译，并且，edit会被重链接。
<h4><a name="1.4 makefile中使用变量"></a>1.4 makefile中使用变量 </h4>
在上面的例子中，先让我们看看edit的规则：
<pre style="PADDING-BOTTOM: 0px">      edit : main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
cc -o edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
</pre>
我们可以看到[.o]文件的字符串被重复了两次，如果我们的工程需要加入一个新的[.o]文件，那么我们需要在两个地方加（应该是三个地方，还有一个地方在clean中）。当然，我们的makefile并不复杂，所以在两个地方加也不累，但如果makefile变得复杂，那么我们就有可能会忘掉一个需要加入的地方，而导致编译失败。所以，为了makefile的易维护，在makefile中我们可以使用变量。makefile的变量也就是一个字符串，理解成C语言中的宏可能会更好。
<p>比如，我们声明一个变量，叫objects, OBJECTS, objs, OBJS, obj, 或是 OBJ，反正不管什么啦，只要能够表示obj文件就行了。我们在makefile一开始就这样定义： </p>
<pre style="PADDING-BOTTOM: 0px">     objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
</pre>
于是，我们就可以很方便地在我们的makefile中以&#8220;$(objects)&#8221;的方式来使用这个变量了，于是我们的改良版makefile就变成下面这个样子：
<pre style="PADDING-BOTTOM: 0px">    objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
edit : $(objects)
cc -o edit $(objects)
main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h buffer.h
cc -c display.c
insert.o : insert.c defs.h buffer.h
cc -c insert.c
search.o : search.c defs.h buffer.h
cc -c search.c
files.o : files.c defs.h buffer.h command.h
cc -c files.c
utils.o : utils.c defs.h
cc -c utils.c
clean :
rm edit $(objects)
</pre>
<p>于是如果有新的 .o 文件加入，我们只需简单地修改一下 objects 变量就可以了。
<p>关于变量更多的话题，我会在后续给你一一道来。
<h4><a name="1.5 让make自动推导"></a>1.5 让make自动推导 </h4>
GNU的make很强大，它可以自动推导文件以及文件依赖关系后面的命令，于是我们就没必要去在每一个[.o]文件后都写上类似的命令，因为，我们的make会自动识别，并自己推导命令。
<p>只要make看到一个[.o]文件，它就会自动的把[.c]文件加在依赖关系中，如果make找到一个whatever.o，那么whatever.c，就会是whatever.o的依赖文件。并且 cc -c whatever.c 也会被推导出来，于是，我们的makefile再也不用写得这么复杂。我们的是新的makefile又出炉了。 </p>
<pre style="PADDING-BOTTOM: 0px">    objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
edit : $(objects)
cc -o edit $(objects)
main.o : defs.h
kbd.o : defs.h command.h
command.o : defs.h command.h
display.o : defs.h buffer.h
insert.o : defs.h buffer.h
search.o : defs.h buffer.h
files.o : defs.h buffer.h command.h
utils.o : defs.h
.PHONY : clean
clean :
rm edit $(objects)
</pre>
这种方法，也就是make的&#8220;隐晦规则&#8221;。上面文件内容中，&#8220;.PHONY&#8221;表示，clean是个伪目标文件。
<p>关于更为详细的&#8220;隐晦规则&#8221;和&#8220;伪目标文件&#8221;，我会在后续给你一一道来。
<h4><a name="1.6 另类风格的makefile"></a>1.6 另类风格的makefile </h4>
即然我们的make可以自动推导命令，那么我看到那堆[.o]和[.h]的依赖就有点不爽，那么多的重复的[.h]，能不能把其收拢起来，好吧，没有问题，这个对于make来说很容易，谁叫它提供了自动推导命令和文件的功能呢？来看看最新风格的makefile吧。
<pre style="PADDING-BOTTOM: 0px">    objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
edit : $(objects)
cc -o edit $(objects)
$(objects) : defs.h
kbd.o command.o files.o : command.h
display.o insert.o search.o files.o : buffer.h
.PHONY : clean
clean :
rm edit $(objects)
</pre>
这种风格，让我们的makefile变得很简单，但我们的文件依赖关系就显得有点凌乱了。鱼和熊掌不可兼得。还看你的喜好了。我是不喜欢这种风格的，一是文件的依赖关系看不清楚，二是如果文件一多，要加入几个新的.o文件，那就理不清楚了。
<h4><a name="1.7 清空目标文件的规则"></a>1.7 清空目标文件的规则 </h4>
每个Makefile中都应该写一个清空目标文件（.o和执行文件）的规则，这不仅便于重编译，也很利于保持文件的清洁。这是一个&#8220;修养&#8221;（呵呵，还记得我的《编程修养》吗）。一般的风格都是：
<pre style="PADDING-BOTTOM: 0px">        clean:
rm edit $(objects)
</pre>
更为稳健的做法是：
<pre style="PADDING-BOTTOM: 0px">        .PHONY : clean
clean :
-rm edit $(objects)
</pre>
前面说过，.PHONY意思表示clean是一个&#8220;伪目标&#8221;，。而在rm命令前面加了一个小减号的意思就是，也许某些文件出现问题，但不要管，继续做后面的事。当然，clean的规则不要放在文件的开头，不然，这就会变成make的默认目标，相信谁也不愿意这样。不成文的规矩是——&#8220;clean从来都是放在文件的最后&#8221;。
<p>
<p>上面就是一个makefile的概貌，也是makefile的基础，下面还有很多makefile的相关细节，准备好了吗？准备好了就来。
<hr>
<h3><a name="2 Makefile 总述"></a>2 Makefile 总述 </h3>
<h4><a name="2.1 Makefile里有什么？"></a>2.1 Makefile里有什么？ </h4>
Makefile里主要包含了五个东西：显式规则、隐晦规则、变量定义、文件指示和注释。
<p>
<ol>
    <li>显式规则。显式规则说明了，如何生成一个或多的的目标文件。这是由Makefile的书写者明显指出，要生成的文件，文件的依赖文件，生成的命令。
    <li>隐晦规则。由于我们的make有自动推导的功能，所以隐晦的规则可以让我们比较粗糙地简略地书写Makefile，这是由make所支持的。
    <li>变量的定义。在Makefile中我们要定义一系列的变量，变量一般都是字符串，这个有点你C语言中的宏，当Makefile被执行时，其中的变量都会被扩展到相应的引用位置上。
    <li>文件指示。其包括了三个部分，一个是在一个Makefile中引用另一个Makefile，就像C语言中的include一样；另一个是指根据某些情况指定Makefile中的有效部分，就像C语言中的预编译#if一样；还有就是定义一个多行的命令。有关这一部分的内容，我会在后续的部分中讲述。
    <li>注释。Makefile中只有行注释，和UNIX的Shell脚本一样，其注释是用&#8220;#&#8221;字符，这个就像C/C++中的&#8220;//&#8221;一样。如果你要在你的Makefile中使用&#8220;#&#8221;字符，可以用反斜框进行转义，如：&#8220;\#&#8221;。 </li>
</ol>
<p>最后，还值得一提的是，在Makefile中的命令，必须要以[Tab]键开始。
<h4><a name=2.2Makefile的文件名></a>2.2Makefile的文件名 </h4>
默认的情况下，make命令会在当前目录下按顺序找寻文件名为&#8220;GNUmakefile&#8221;、&#8220;makefile&#8221;、&#8220;Makefile&#8221;的文件，找到了解释这个文件。在这三个文件名中，最好使用&#8220;Makefile&#8221;这个文件名，因为，这个文件名第一个字符为大写，这样有一种显目的感觉。最好不要用&#8220;GNUmakefile&#8221;，这个文件是GNU的make识别的。有另外一些make只对全小写的&#8220;makefile&#8221;文件名敏感，但是基本上来说，大多数的make都支持&#8220;makefile&#8221;和&#8220;Makefile&#8221;这两种默认文件名。
<p>当然，你可以使用别的文件名来书写Makefile，比如：&#8220;Make.Linux&#8221;，&#8220;Make.Solaris&#8221;，&#8220;Make.AIX&#8221;等，如果要指定特定的Makefile，你可以使用make的&#8220;-f&#8221;和&#8220;--file&#8221;参数，如：make -f Make.Linux或make --file Make.AIX。
<h4><a name="2.3 引用其它的Makefile"></a>2.3 引用其它的Makefile </h4>
在Makefile使用include关键字可以把别的Makefile包含进来，这很像C语言的#include，被包含的文件会原模原样的放在当前文件的包含位置。include的语法是：
<div class=BeautifierPlugin>
<div class=fragment>
<pre style="PADDING-BOTTOM: 0px">
<pre style="PADDING-BOTTOM: 0px">include &lt;filename&gt;</pre>
</pre>
</div>
</div>
filename可以是当前操作系统Shell的文件模式（可以保含路径和通配符）
<p>在include前面可以有一些空字符，但是绝不能是[Tab]键开始。include和<filename>可以用一个或多个空格隔开。举个例子，你有这样几个Makefile：a.mk、b.mk、c.mk，还有一个文件叫foo.make，以及一个变量$(bar)，其包含了e.mk和f.mk，那么，下面的语句： </p>
<pre style="PADDING-BOTTOM: 0px">    include foo.make *.mk $(bar)
</pre>
等价于：
<pre style="PADDING-BOTTOM: 0px">    include foo.make a.mk b.mk c.mk e.mk f.mk
</pre>
make命令开始时，会把找寻include所指出的其它Makefile，并把其内容安置在当前的位置。就好像C/C++的#include指令一样。如果文件都没有指定绝对路径或是相对路径的话，make会在当前目录下首先寻找，如果当前目录下没有找到，那么，make还会在下面的几个目录下找：
<p>
<ol>
    <li>如果make执行时，有&#8220;-I&#8221;或&#8220;--include-dir&#8221;参数，那么make就会在这个参数所指定的目录下去寻找。
    <li>如果目录
    <prefix>/include（一般是：/usr/local/bin或/usr/include）存在的话，make也会去找。 </li>
</ol>
<p>如果有文件没有找到的话，make会生成一条警告信息，但不会马上出现致命错误。它会继续载入其它的文件，一旦完成makefile的读取，make会再重试这些没有找到，或是不能读取的文件，如果还是不行，make才会出现一条致命信息。如果你想让make不理那些无法读取的文件，而继续执行，你可以在include前加一个减号&#8220;-&#8221;。如：
<div class=BeautifierPlugin>
<div class=fragment>
<pre style="PADDING-BOTTOM: 0px">
<pre style="PADDING-BOTTOM: 0px">-include &lt;filename&gt;</pre>
</pre>
</div>
</div>
其表示，无论include过程中出现什么错误，都不要报错继续执行。和其它版本make兼容的相关命令是sinclude，其作用和这一个是一样的。
<h4><a name="2.4 环境变量 MAKEFILES"></a>2.4 环境变量 MAKEFILES </h4>
如果你的当前环境中定义了环境变量MAKEFILES，那么，make会把这个变量中的值做一个类似于include的动作。这个变量中的值是其它的Makefile，用空格分隔。只是，它和include不同的是，从这个环境变中引入的Makefile的&#8220;目标&#8221;不会起作用，如果环境变量中定义的文件发现错误，make也会不理。
<p>但是在这里我还是建议不要使用这个环境变量，因为只要这个变量一被定义，那么当你使用make时，所有的Makefile都会受到它的影响，这绝不是你想看到的。在这里提这个事，只是为了告诉大家，也许有时候你的Makefile出现了怪事，那么你可以看看当前环境中有没有定义这个变量。
<h4><a name="2.5 make的工作方式"></a>2.5 make的工作方式 </h4>
GNU的make工作时的执行步骤入下：（想来其它的make也是类似）
<p>
<ol>
    <li>读入所有的Makefile。
    <li>读入被include的其它Makefile。
    <li>初始化文件中的变量。
    <li>推导隐晦规则，并分析所有规则。
    <li>为所有的目标文件创建依赖关系链。
    <li>根据依赖关系，决定哪些目标要重新生成。
    <li>执行生成命令。 </li>
</ol>
<p>1-5步为第一个阶段，6-7为第二个阶段。第一个阶段中，如果定义的变量被使用了，那么，make会把其展开在使用的位置。但make并不会完全马上展开，make使用的是拖延战术，如果变量出现在依赖关系的规则中，那么仅当这条依赖被决定要使用了，变量才会在其内部展开。
<p>当然，这个工作方式你不一定要清楚，但是知道这个方式你也会对make更为熟悉。有了这个基础，后续部分也就容易看懂了。
<h3><a name="3 Makefile书写规则"></a>3 Makefile书写规则 </h3>
<hr>
规则包含两个部分，一个是依赖关系，一个是生成目标的方法。
<p>在Makefile中，规则的顺序是很重要的，因为，Makefile中只应该有一个最终目标，其它的目标都是被这个目标所连带出来的，所以一定要让make知道你的最终目标是什么。一般来说，定义在Makefile中的目标可能会有很多，但是第一条规则中的目标将被确立为最终的目标。如果第一条规则中的目标有很多个，那么，第一个目标会成为最终的目标。make所完成的也就是这个目标。
<p>好了，还是让我们来看一看如何书写规则。
<h4><a name="3.1 规则举例"></a>3.1 规则举例 </h4>
<pre style="PADDING-BOTTOM: 0px"> foo.o : foo.c defs.h       # foo模块
cc -c -g foo.c
</pre>
看到这个例子，各位应该不是很陌生了，前面也已说过，foo.o是我们的目标，foo.c和defs.h是目标所依赖的源文件，而只有一个命令&#8220;cc -c -g foo.c&#8221;（以Tab键开头）。这个规则告诉我们两件事：
<p>
<ol>
    <li>文件的依赖关系，foo.o依赖于foo.c和defs.h的文件，如果foo.c和defs.h的文件日期要比foo.o文件日期要新，或是foo.o不存在，那么依赖关系发生。
    <li>如果生成（或更新）foo.o文件。也就是那个cc命令，其说明了，如何生成foo.o这个文件。（当然foo.c文件include了defs.h文件） </li>
</ol>
<p>
<h4><a name="3.2 规则的语法"></a>3.2 规则的语法 </h4>
<pre style="PADDING-BOTTOM: 0px">      targets : prerequisites
command
...
</pre>
或是这样：
<pre style="PADDING-BOTTOM: 0px">      targets : prerequisites ; command
command
...
</pre>
targets是文件名，以空格分开，可以使用通配符。一般来说，我们的目标基本上是一个文件，但也有可能是多个文件。
<p>command是命令行，如果其不与&#8220;target:prerequisites&#8221;在一行，那么，必须以[Tab键]开头，如果和prerequisites在一行，那么可以用分号做为分隔。（见上）
<p>prerequisites也就是目标所依赖的文件（或依赖目标）。如果其中的某个文件要比目标文件要新，那么，目标就被认为是&#8220;过时的&#8221;，被认为是需要重生成的。这个在前面已经讲过了。
<p>如果命令太长，你可以使用反斜框（&#8216;\&#8217;）作为换行符。make对一行上有多少个字符没有限制。规则告诉make两件事，文件的依赖关系和如何成成目标文件。
<p>一般来说，make会以UNIX的标准Shell，也就是/bin/sh来执行命令。
<h4><a name="3.3 在规则中使用通配符"></a>3.3 在规则中使用通配符 </h4>
<p>如果我们想定义一系列比较类似的文件，我们很自然地就想起使用通配符。make支持三各通配符：&#8220;*&#8221;，&#8220;?&#8221;和&#8220;[...]&#8221;。这是和Unix的B-Shell是相同的。
<p>波浪号（&#8220;~&#8221;）字符在文件名中也有比较特殊的用途。如果是&#8220;~/test&#8221;，这就表示当前用户的$HOME目录下的test目录。而&#8220;~hchen/test&#8221;则表示用户hchen的宿主目录下的test目录。（这些都是Unix下的小知识了，make也支持）而在Windows或是MS-DOS下，用户没有宿主目录，那么波浪号所指的目录则根据环境变量&#8220;HOME&#8221;而定。
<p>通配符代替了你一系列的文件，如&#8220;*.c&#8221;表示所以后缀为c的文件。一个需要我们注意的是，如果我们的文件名中有通配符，如：&#8220;*&#8221;，那么可以用转义字符&#8220;\&#8221;，如&#8220;\*&#8221;来表示真实的&#8220;*&#8221;字符，而不是任意长度的字符串。
<p>好吧，还是先来看几个例子吧： </p>
<pre style="PADDING-BOTTOM: 0px">    clean:
rm -f *.o
</pre>
上面这个例子我不不多说了，这是操作系统Shell所支持的通配符。这是在命令中的通配符。
<pre style="PADDING-BOTTOM: 0px">    print: *.c
lpr -p $?
touch print
</pre>
上面这个例子说明了通配符也可以在我们的规则中，目标print依赖于所有的[.c]文件。其中的&#8220;$?&#8221;是一个自动化变量，我会在后面给你讲述。
<pre style="PADDING-BOTTOM: 0px">    objects = *.o
</pre>
上面这个例子，表示了，通符同样可以用在变量中。并不是说[*.o]会展开，不！objects的值就是&#8220;*.o&#8221;。Makefile中的变量其实就是C/C++中的宏。如果你要让通配符在变量中展开，也就是让objects的值是所有[.o]的文件名的集合，那么，你可以这样：
<pre style="PADDING-BOTTOM: 0px">    objects := $(wildcard *.o)
</pre>
这种用法由关键字&#8220;wildcard&#8221;指出，关于Makefile的关键字，我们将在后面讨论。
<h4><a name="3.4 文件搜寻"></a>3.4 文件搜寻 </h4>
<p>在一些大的工程中，有大量的源文件，我们通常的做法是把这许多的源文件分类，并存放在不同的目录中。所以，当make需要去找寻文件的依赖关系时，你可以在文件前加上路径，但最好的方法是把一个路径告诉make，让make在自动去找。
<p>Makefile文件中的特殊变量&#8220;VPATH&#8221;就是完成这个功能的，如果没有指明这个变量，make只会在当前的目录中去找寻依赖文件和目标文件。如果定义了这个变量，那么，make就会在当当前目录找不到的情况下，到所指定的目录中去找寻文件了。 </p>
<pre style="PADDING-BOTTOM: 0px">    VPATH = src:../headers
</pre>
上面的的定义指定两个目录，&#8220;src&#8221;和&#8220;../headers&#8221;，make会按照这个顺序进行搜索。目录由&#8220;冒号&#8221;分隔。（当然，当前目录永远是最高优先搜索的地方）
<p>另一个设置文件搜索路径的方法是使用make的&#8220;vpath&#8221;关键字（注意，它是全小写的），这不是变量，这是一个make的关键字，这和上面提到的那个VPATH变量很类似，但是它更为灵活。它可以指定不同的文件在不同的搜索目录中。这是一个很灵活的功能。它的使用方法有三种：
<ol>
    <li>vpath &lt; pattern&gt; &lt; directories&gt; <br>为符合模式&lt; pattern&gt;的文件指定搜索目录&lt; directories&gt;。
    <li>vpath &lt; pattern&gt;<br>清除符合模式&lt; pattern&gt;的文件的搜索目录。
    <li>vpath <br>清除所有已被设置好了的文件搜索目录。 </li>
</ol>
<p>vapth使用方法中的&lt; pattern&gt;需要包含&#8220;%&#8221;字符。&#8220;%&#8221;的意思是匹配零或若干字符，例如，&#8220;%.h&#8221;表示所有以&#8220;.h&#8221;结尾的文件。&lt; pattern&gt;指定了要搜索的文件集，而&lt; directories&gt;则指定了
<pattern>的文件集的搜索的目录。例如： </p>
<pre style="PADDING-BOTTOM: 0px">    vpath %.h ../headers
</pre>
该语句表示，要求make在&#8220;../headers&#8221;目录下搜索所有以&#8220;.h&#8221;结尾的文件。（如果某文件在当前目录没有找到的话）
<p>我们可以连续地使用vpath语句，以指定不同搜索策略。如果连续的vpath语句中出现了相同的&lt; pattern&gt;，或是被重复了的&lt; pattern&gt;，那么，make会按照vpath语句的先后顺序来执行搜索。如： </p>
<pre style="PADDING-BOTTOM: 0px">    vpath %.c foo
vpath %   blish
vpath %.c bar
</pre>
其表示&#8220;.c&#8221;结尾的文件，先在&#8220;foo&#8221;目录，然后是&#8220;blish&#8221;，最后是&#8220;bar&#8221;目录。
<pre style="PADDING-BOTTOM: 0px">    vpath %.c foo:bar
vpath %   blish
</pre>
而上面的语句则表示&#8220;.c&#8221;结尾的文件，先在&#8220;foo&#8221;目录，然后是&#8220;bar&#8221;目录，最后才是&#8220;blish&#8221;目录。
<h4><a name="3.5 伪目标"></a>3.5 伪目标 </h4>
<p>最早先的一个例子中，我们提到过一个&#8220;clean&#8221;的目标，这是一个&#8220;伪目标&#8221;， </p>
<pre style="PADDING-BOTTOM: 0px">    clean:
rm *.o temp
</pre>
正像我们前面例子中的&#8220;clean&#8221;一样，即然我们生成了许多文件编译文件，我们也应该提供一个清除它们的&#8220;目标&#8221;以备完整地重编译而用。 （以&#8220;make clean&#8221;来使用该目标）
<p>因为，我们并不生成&#8220;clean&#8221;这个文件。&#8220;伪目标&#8221;并不是一个文件，只是一个标签，由于&#8220;伪目标&#8221;不是文件，所以make无法生成它的依赖关系和决定它是否要执行。我们只有通过显示地指明这个&#8220;目标&#8221;才能让其生效。当然，&#8220;伪目标&#8221;的取名不能和文件名重名，不然其就失去了&#8220;伪目标&#8221;的意义了。
<p>当然，为了避免和文件重名的这种情况，我们可以使用一个特殊的标记&#8220;.PHONY&#8221;来显示地指明一个目标是&#8220;伪目标&#8221;，向make说明，不管是否有这个文件，这个目标就是&#8220;伪目标&#8221;。 </p>
<pre style="PADDING-BOTTOM: 0px">    .PHONY : clean
</pre>
只要有这个声明，不管是否有&#8220;clean&#8221;文件，要运行&#8220;clean&#8221;这个目标，只有&#8220;make clean&#8221;这样。于是整个过程可以这样写：
<pre style="PADDING-BOTTOM: 0px">     .PHONY: clean
clean:
rm *.o temp
</pre>
伪目标一般没有依赖的文件。但是，我们也可以为伪目标指定所依赖的文件。伪目标同样可以作为&#8220;默认目标&#8221;，只要将其放在第一个。一个示例就是，如果你的Makefile需要一口气生成若干个可执行文件，但你只想简单地敲一个make完事，并且，所有的目标文件都写在一个Makefile中，那么你可以使用&#8220;伪目标&#8221;这个特性：
<pre style="PADDING-BOTTOM: 0px">    all : prog1 prog2 prog3
.PHONY : all
prog1 : prog1.o utils.o
cc -o prog1 prog1.o utils.o
prog2 : prog2.o
cc -o prog2 prog2.o
prog3 : prog3.o sort.o utils.o
cc -o prog3 prog3.o sort.o utils.o
</pre>
我们知道，Makefile中的第一个目标会被作为其默认目标。我们声明了一个&#8220;all&#8221;的伪目标，其依赖于其它三个目标。由于伪目标的特性是，总是被执行的，所以其依赖的那三个目标就总是不如&#8220;all&#8221;这个目标新。所以，其它三个目标的规则总是会被决议。也就达到了我们一口气生成多个目标的目的。&#8220;.PHONY : all&#8221;声明了&#8220;all&#8221;这个目标为&#8220;伪目标&#8221;。
<p>随便提一句，从上面的例子我们可以看出，目标也可以成为依赖。所以，伪目标同样也可成为依赖。看下面的例子： </p>
<pre style="PADDING-BOTTOM: 0px">    .PHONY: cleanall cleanobj cleandiff
cleanall : cleanobj cleandiff
rm program
cleanobj :
rm *.o
cleandiff :
rm *.diff
</pre>
&#8220;make clean&#8221;将清除所有要被清除的文件。&#8220;cleanobj&#8221;和&#8220;cleandiff&#8221;这两个伪目标有点像&#8220;子程序&#8221;的意思。我们可以输入&#8220;make cleanall&#8221;和&#8220;make cleanobj&#8221;和&#8220;make cleandiff&#8221;命令来达到清除不同种类文件的目的
<p>
<h4><a name="3.6 多目标"></a>3.6 多目标 </h4>
<p>Makefile的规则中的目标可以不止一个，其支持多目标，有可能我们的多个目标同时依赖于一个文件，并且其生成的命令大体类似。于是我们就能把其合并起来。当然，多个目标的生成规则的执行命令是同一个，这可能会可我们带来麻烦，不过好在我们的可以使用一个自动化变量&#8220;$@&#8221;（关于自动化变量，将在后面讲述），这个变量表示着目前规则中所有的目标的集合，这样说可能很抽象，还是看一个例子吧。 </p>
<pre style="PADDING-BOTTOM: 0px">    bigoutput littleoutput : text.g
generate text.g -$(subst output,,$@) &gt; $@
<pre style="PADDING-BOTTOM: 0px">    上述规则等价于：
</pre>
bigoutput : text.g
generate text.g -big &gt; bigoutput
littleoutput : text.g
generate text.g -little &gt; littleoutput
</pre>
其中，-$(subst output,,$@)中的&#8220;$&#8221;表示执行一个Makefile的函数，函数名为subst，后面的为参数。关于函数，将在后面讲述。这里的这个函数是截取字符串的意思，&#8220;$@&#8221;表示目标的集合，就像一个数组，&#8220;$@&#8221;依次取出目标，并执于命令。
<p>
<p>
<h4><a name="3.7 静态模式"></a>3.7 静态模式 </h4>
<p>静态模式可以更加容易地定义多目标的规则，可以让我们的规则变得更加的有弹性和灵活。我们还是先来看一下语法：
<div class=BeautifierPlugin>
<div class=fragment>
<pre style="PADDING-BOTTOM: 0px">
<pre style="PADDING-BOTTOM: 0px">&lt;targets ...&gt;: &lt;target-pattern&gt;: &lt;prereq-patterns ...&gt;
　　　&lt;commands&gt;
...</pre>
</pre>
</div>
</div>
<p>targets定义了一系列的目标文件，可以有通配符。是目标的一个集合。
<p>target-parrtern是指明了targets的模式，也就是的目标集模式。
<p>prereq-parrterns是目标的依赖模式，它对target-parrtern形成的模式再进行一次依赖目标的定义。
<p>这样描述这三个东西，可能还是没有说清楚，还是举个例子来说明一下吧。如果我们的&lt;target-parrtern&gt;定义成&#8220;%.o&#8221;，意思是我们的<target>集合中都是以&#8220;.o&#8221;结尾的，而如果我们的&lt;prereq-parrterns&gt;定义成&#8220;%.c&#8221;，意思是对&lt;target-parrtern&gt;所形成的目标集进行二次定义，其计算方法是，取&lt;target-parrtern&gt;模式中的&#8220;%&#8221;（也就是去掉了[.o]这个结尾），并为其加上[.c]这个结尾，形成的新集合。
<p>所以，我们的&#8220;目标模式&#8221;或是&#8220;依赖模式&#8221;中都应该有&#8220;%&#8221;这个字符，如果你的文件名中有&#8220;%&#8221;那么你可以使用反斜杠&#8220;\&#8221;进行转义，来标明真实的&#8220;%&#8221;字符。
<p>看一个例子： </p>
<pre style="PADDING-BOTTOM: 0px">    objects = foo.o bar.o
all: $(objects)
$(objects): %.o: %.c
$(CC) -c $(CFLAGS) $&lt; -o $@
</pre>
上面的例子中，指明了我们的目标从$object中获取，&#8220;%.o&#8221;表明要所有以&#8220;.o&#8221;结尾的目标，也就是&#8220;foo.o bar.o&#8221;，也就是变量$object集合的模式，而依赖模式&#8220;%.c&#8221;则取模式&#8220;%.o&#8221;的&#8220;%&#8221;，也就是&#8220;foo bar&#8221;，并为其加下&#8220;.c&#8221;的后缀，于是，我们的依赖目标就是&#8220;foo.c bar.c&#8221;。而命令中的&#8220;$&lt;&#8221;和&#8220;$@&#8221;则是自动化变量，&#8220;$&lt;&#8221;表示所有的依赖目标集（也就是&#8220;foo.c bar.c&#8221;），&#8220;$@&#8221;表示目标集（也褪恰癴oo.o bar.o&#8221;）。于是，上面的规则展开后等价于下面的规则：
<pre style="PADDING-BOTTOM: 0px">    foo.o : foo.c
$(CC) -c $(CFLAGS) foo.c -o foo.o
bar.o : bar.c
$(CC) -c $(CFLAGS) bar.c -o bar.o
</pre>
试想，如果我们的&#8220;%.o&#8221;有几百个，那种我们只要用这种很简单的&#8220;静态模式规则&#8221;就可以写完一堆规则，实在是太有效率了。&#8220;静态模式规则&#8221;的用法很灵活，如果用得好，那会一个很强大的功能。再看一个例子：
<pre style="PADDING-BOTTOM: 0px">    files = foo.elc bar.o lose.o
$(filter %.o,$(files)): %.o: %.c
$(CC) -c $(CFLAGS) $&lt; -o $@
$(filter %.elc,$(files)): %.elc: %.el
emacs -f batch-byte-compile $&lt;
</pre>
<p>$(filter %.o,$(files))表示调用Makefile的filter函数，过滤&#8220;$filter&#8221;集，只要其中模式为&#8220;%.o&#8221;的内容。其的它内容，我就不用多说了吧。这个例字展示了Makefile中更大的弹性。
<h4><a name="3.8 自动生成依赖性"></a>3.8 自动生成依赖性 </h4>
<p>在Makefile中，我们的依赖关系可能会需要包含一系列的头文件，比如，如果我们的main.c中有一句&#8220;#include "defs.h"&#8221;，那么我们的依赖关系应该是： </p>
<pre style="PADDING-BOTTOM: 0px">    main.o : main.c defs.h
</pre>
但是，如果是一个比较大型的工程，你必需清楚哪些C文件包含了哪些头文件，并且，你在加入或删除头文件时，也需要小心地修改Makefile，这是一个很没有维护性的工作。为了避免这种繁重而又容易出错的事情，我们可以使用C/C++编译的一个功能。大多数的C/C++编译器都支持一个&#8220;-M&#8221;的选项，即自动找寻源文件中包含的头文件，并生成一个依赖关系。例如，如果我们执行下面的命令：
<pre style="PADDING-BOTTOM: 0px">    cc -M main.c
</pre>
其输出是：
<pre style="PADDING-BOTTOM: 0px">    main.o : main.c defs.h
</pre>
于是由编译器自动生成的依赖关系，这样一来，你就不必再手动书写若干文件的依赖关系，而由编译器自动生成了。需要提醒一句的是，如果你使用GNU的C/C++编译器，你得用&#8220;-MM&#8221;参数，不然，&#8220;-M&#8221;参数会把一些标准库的头文件也包含进来。
<p>gcc -M main.c的输出是： </p>
<pre style="PADDING-BOTTOM: 0px">    main.o: main.c defs.h /usr/include/stdio.h /usr/include/features.h \
/usr/include/sys/cdefs.h /usr/include/gnu/stubs.h \
/usr/lib/gcc-lib/i486-suse-linux/2.95.3/include/stddef.h \
/usr/include/bits/types.h /usr/include/bits/pthreadtypes.h \
/usr/include/bits/sched.h /usr/include/libio.h \
/usr/include/_G_config.h /usr/include/wchar.h \
/usr/include/bits/wchar.h /usr/include/gconv.h \
/usr/lib/gcc-lib/i486-suse-linux/2.95.3/include/stdarg.h \
/usr/include/bits/stdio_lim.h
</pre>
gcc -MM main.c的输出则是：
<pre style="PADDING-BOTTOM: 0px">    main.o: main.c defs.h
</pre>
那么，编译器的这个功能如何与我们的Makefile联系在一起呢。因为这样一来，我们的Makefile也要根据这些源文件重新生成，让Makefile自已依赖于源文件？这个功能并不现实，不过我们可以有其它手段来迂回地实现这一功能。GNU组织建议把编译器为每一个源文件的自动生成的依赖关系放到一个文件中，为每一个&#8220;name.c&#8221;的文件都生成一个&#8220;name.d&#8221;的Makefile文件，[.d]文件中就存放对应[.c]文件的依赖关系。
<p>于是，我们可以写出[.c]文件和[.d]文件的依赖关系，并让make自动更新或自成[.d]文件，并把其包含在我们的主Makefile中，这样，我们就可以自动化地生成每个文件的依赖关系了。
<p>这里，我们给出了一个模式规则来产生[.d]文件： </p>
<pre style="PADDING-BOTTOM: 0px">    %.d: %.c
@set -e; rm -f $@; \
$(CC) -M $(CPPFLAGS) $&lt; &gt; $@.$$$$; \
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' &lt; $@.$$$$ &gt; $@; \
rm -f $@.$$$$
</pre>
<p>这个规则的意思是，所有的[.d]文件依赖于[.c]文件，&#8220;rm -f $@&#8221;的意思是删除所有的目标，也就是[.d]文件，第二行的意思是，为每个依赖文件&#8220;$&lt;&#8221;，也就是[.c]文件生成依赖文件，&#8220;$@&#8221;表示模式&#8220;%.d&#8221;文件，如果有一个C文件是name.c，那么&#8220;%&#8221;就是&#8220;name&#8221;，&#8220;$$$$&#8221;意为一个随机编号，第二行生成的文件有可能是&#8220;name.d.12345&#8221;，第三行使用sed命令做了一个替换，关于sed命令的用法请参看相关的使用文档。第四行就是删除临时文件。
<p>总而言之，这个模式要做的事就是在编译器生成的依赖关系中加入[.d]文件的依赖，即把依赖关系： </p>
<pre style="PADDING-BOTTOM: 0px">    main.o : main.c defs.h
</pre>
转成：
<pre style="PADDING-BOTTOM: 0px">    main.o main.d : main.c defs.h
</pre>
于是，我们的[.d]文件也会自动更新了，并会自动生成了，当然，你还可以在这个[.d]文件中加入的不只是依赖关系，包括生成的命令也可一并加入，让每个[.d]文件都包含一个完赖的规则。一旦我们完成这个工作，接下来，我们就要把这些自动生成的规则放进我们的主Makefile中。我们可以使用Makefile的&#8220;include&#8221;命令，来引入别的Makefile文件（前面讲过），例如：
<pre style="PADDING-BOTTOM: 0px">    sources = foo.c bar.c
include $(sources:.c=.d)
</pre>
上述语句中的&#8220;$(sources:.c=.d)&#8221;中的&#8220;.c=.d&#8221;的意思是做一个替换，把变量$(sources)所有[.c]的字串都替换成[.d]，关于这个&#8220;替换&#8221;的内容，在后面我会有更为详细的讲述。当然，你得注意次序，因为include是按次来载入文件，最先载入的[<no>.d]文件中的目标会成为默认目标
<h3><a name="4 Makefile 书写命令"></a>4 Makefile 书写命令 </h3>
<hr>
<p>每条规则中的命令和操作系统Shell的命令行是一致的。make会一按顺序一条一条的执行命令，每条命令的开头必须以[Tab]键开头，除非，命令是紧跟在依赖规则后面的分号后的。在命令行之间中的空格或是空行会被忽略，但是如果该空格或空行是以Tab键开头的，那么make会认为其是一个空命令。
<p>我们在UNIX下可能会使用不同的Shell，但是make的命令默认是被&#8220;/bin/sh&#8221;——UNIX的标准Shell解释执行的。除非你特别指定一个其它的Shell。Makefile中，&#8220;#&#8221;是注释符，很像C/C++中的&#8220;//&#8221;，其后的本行字符都被注释。
<p>
<h4><a name="4.1 显示命令"></a>4.1 显示命令 </h4>
<p>通常，make会把其要执行的命令行在命令执行前输出到屏幕上。当我们用&#8220;@&#8221;字符在命令行前，那么，这个命令将不被make显示出来，最具代表性的例子是，我们用这个功能来像屏幕显示一些信息。如： </p>
<pre style="PADDING-BOTTOM: 0px">    @echo 正在编译XXX模块......
</pre>
当make执行时，会输出&#8220;正在编译XXX模块......&#8221;字串，但不会输出命令，如果没有&#8220;@&#8221;，那么，make将输出：
<pre style="PADDING-BOTTOM: 0px">    echo 正在编译XXX模块......
正在编译XXX模块......
</pre>
如果make执行时，带入make参数&#8220;-n&#8221;或&#8220;--just-print&#8221;，那么其只是显示命令，但不会执行命令，这个功能很有利于我们调试我们的Makefile，看看我们书写的命令是执行起来是什么样子的或是什么顺序的。
<p>而make参数&#8220;-s&#8221;或&#8220;--slient&#8221;则是全面禁止命令的显示。
<p>
<h4><a name="4.2 命令执行"></a>4.2 命令执行 </h4>
<p>当依赖目标新于目标时，也就是当规则的目标需要被更新时，make会一条一条的执行其后的命令。需要注意的是，如果你要让上一条命令的结果应用在下一条命令时，你应该使用分号分隔这两条命令。比如你的第一条命令是cd命令，你希望第二条命令得在cd之后的基础上运行，那么你就不能把这两条命令写在两行上，而应该把这两条命令写在一行上，用分号分隔。如： </p>
<pre style="PADDING-BOTTOM: 0px">    示例一：
exec:
cd /home/hchen
pwd
示例二：
exec:
cd /home/hchen; pwd
</pre>
<p>当我们执行&#8220;make exec&#8221;时，第一个例子中的cd没有作用，pwd会打印出当前的Makefile目录，而第二个例子中，cd就起作用了，pwd会打印出&#8220;/home/hchen&#8221;。
<p>make一般是使用环境变量SHELL中所定义的系统Shell来执行命令，默认情况下使用UNIX的标准Shell——/bin/sh来执行命令。但在MS-DOS下有点特殊，因为MS-DOS下没有SHELL环境变量，当然你也可以指定。如果你指定了UNIX风格的目录形式，首先，make会在SHELL所指定的路径中找寻命令解释器，如果找不到，其会在当前盘符中的当前目录中寻找，如果再找不到，其会在PATH环境变量中所定义的所有路径中寻找。MS-DOS中，如果你定义的命令解释器没有找到，其会给你的命令解释器加上诸如&#8220;.exe&#8221;、&#8220;.com&#8221;、&#8220;.bat&#8221;、&#8220;.sh&#8221;等后缀。
<p>
<h4><a name="4.3 命令出错"></a>4.3 命令出错 </h4>
<p>每当命令运行完后，make会检测每个命令的返回码，如果命令返回成功，那么make会执行下一条命令，当规则中所有的命令成功返回后，这个规则就算是成功完成了。如果一个规则中的某个命令出错了（命令退出码非零），那么make就会终止执行当前规则，这将有可能终止所有规则的执行。
<p>有些时候，命令的出错并不表示就是错误的。例如mkdir命令，我们一定需要建立一个目录，如果目录不存在，那么mkdir就成功执行，万事大吉，如果目录存在，那么就出错了。我们之所以使用mkdir的意思就是一定要有这样的一个目录，于是我们就不希望mkdir出错而终止规则的运行。
<p>为了做到这一点，忽略命令的出错，我们可以在Makefile的命令行前加一个减号&#8220;-&#8221;（在Tab键之后），标记为不管命令出不出错都认为是成功的。如： </p>
<pre style="PADDING-BOTTOM: 0px">   clean:
-rm -f *.o
</pre>
还有一个全局的办法是，给make加上&#8220;-i&#8221;或是&#8220;--ignore-errors&#8221;参数，那么，Makefile中所有命令都会忽略错误。而如果一个规则是以&#8220;.IGNORE&#8221;作为目标的，那么这个规则中的所有命令将会忽略错误。这些是不同级别的防止命令出错的方法，你可以根据你的不同喜欢设置。
<p>还有一个要提一下的make的参数的是&#8220;-k&#8221;或是&#8220;--keep-going&#8221;，这个参数的意思是，如果某规则中的命令出错了，那么就终目该规则的执行，但继续执行其它规则。
<h4><a name="4.4 嵌套执行make"></a>4.4 嵌套执行make </h4>
<p>在一些大的工程中，我们会把我们不同模块或是不同功能的源文件放在不同的目录中，我们可以在每个目录中都书写一个该目录的Makefile，这有利于让我们的Makefile变得更加地简洁，而不至于把所有的东西全部写在一个Makefile中，这样会很难维护我们的Makefile，这个技术对于我们模块编译和分段编译有着非常大的好处。
<p>例如，我们有一个子目录叫subdir，这个目录下有个Makefile文件，来指明了这个目录下文件的编译规则。那么我们总控的Makefile可以这样书写： </p>
<pre style="PADDING-BOTTOM: 0px">    subsystem:
cd subdir &amp;&amp; $(MAKE)
其等价于：
subsystem:
$(MAKE) -C subdir
<pre style="PADDING-BOTTOM: 16px">定义$(MAKE)宏变量的意思是，也许我们的make需要一些参数，所以定义成一个变量比较利于维护。这两个例子的意思都是先进入&#8220;subdir&#8221;目录，然后执行make命令。
我们把这个Makefile叫做&#8220;总控Makefile&#8221;，总控Makefile的变量可以传递到下级的Makefile中（如果你显示的声明），但是不会覆盖下层的Makefile中所定义的变量，除非指定了&#8220;-e&#8221;参数。
如果你要传递变量到下级Makefile中，那么你可以使用这样的声明：
<div class=BeautifierPlugin>
<div class=fragment>
<pre style="PADDING-BOTTOM: 0px">
<pre style="PADDING-BOTTOM: 0px">export &lt;variable ...&gt;</pre>
</pre>
</div>
</div>
如果你不想让某些变量传递到下级Makefile中，那么你可以这样声明：
<div class=BeautifierPlugin>
<div class=fragment>
<pre style="PADDING-BOTTOM: 0px">
<pre style="PADDING-BOTTOM: 0px">unexport &lt;variable ...&gt;</pre>
</pre>
</div>
</div>
如：
<pre style="PADDING-BOTTOM: 0px">示例一：
export variable = value
其等价于：
variable = value
export variable
其等价于：
export variable := value
其等价于：
variable := value
export variable
示例二：
export variable += value
其等价于：
variable += value
export variable
</pre>
如果你要传递所有的变量，那么，只要一个export就行了。后面什么也不用跟，表示传递所有的变量。
需要注意的是，有两个变量，一个是SHELL，一个是MAKEFLAGS，这两个变量不管你是否export，其总是要传递到下层Makefile中，特别是MAKEFILES变量，其中包含了make的参数信息，如果我们执行&#8220;总控Makefile&#8221;时有make参数或是在上层Makefile中定义了这个变量，那么MAKEFILES变量将会是这些参数，并会传递到下层Makefile中，这是一个系统级的环境变量。
但是make命令中的有几个参数并不往下传递，它们是&#8220;-C&#8221;,&#8220;-f&#8221;,&#8220;-h&#8221;&#8220;-o&#8221;和&#8220;-W&#8221;（有关Makefile参数的细节将在后面说明），如果你不想往下层传递参数，那么，你可以这样来：
<pre style="PADDING-BOTTOM: 0px">subsystem:
cd subdir &amp;&amp; $(MAKE) MAKEFLAGS=
</pre>
如果你定义了环境变量MAKEFLAGS，那么你得确信其中的选项是大家都会用到的，如果其中有&#8220;-t&#8221;,&#8220;-n&#8221;,和&#8220;-q&#8221;参数，那么将会有让你意想不到的结果，或许会让你异常地恐慌。
还有一个在&#8220;嵌套执行&#8221;中比较有用的参数，&#8220;-w&#8221;或是&#8220;--print-directory&#8221;会在make的过程中输出一些信息，让你看到目前的工作目录。比如，如果我们的下级make目录是&#8220;/home/hchen/gnu/make&#8221;，如果我们使用&#8220;make -w&#8221;来执行，那么当进入该目录时，我们会看到：
<pre style="PADDING-BOTTOM: 0px">make: Entering directory `/home/hchen/gnu/make'.
</pre>
而在完成下层make后离开目录时，我们会看到：
<pre style="PADDING-BOTTOM: 0px">make: Leaving directory `/home/hchen/gnu/make'
</pre>
当你使用&#8220;-C&#8221;参数来指定make下层Makefile时，&#8220;-w&#8221;会被自动打开的。如果参数中有&#8220;-s&#8221;（&#8220;--slient&#8221;）或是&#8220;--no-print-directory&#8221;，那么，&#8220;-w&#8221;总是失效的。
---++++4.5 定义命令包
如果Makefile中出现一些相同命令序列，那么我们可以为这些相同的命令序列定义一个变量。定义这种命令序列的语法以&#8220;define&#8221;开始，以&#8220;endef&#8221;结束，如：
<pre style="PADDING-BOTTOM: 0px">    define run-yacc
yacc $(firstword $^)
mv y.tab.c $@
endef
</pre>
这里，&#8220;run-yacc&#8221;是这个命令包的名字，其不要和Makefile中的变量重名。在&#8220;define&#8221;和&#8220;endef&#8221;中的两行就是命令序列。这个命令包中的第一个命令是运行Yacc程序，因为Yacc程序总是生成&#8220;y.tab.c&#8221;的文件，所以第二行的命令就是把这个文件改改名字。还是把这个命令包放到一个示例中来看看吧。
<pre style="PADDING-BOTTOM: 0px">    foo.c : foo.y
$(run-yacc)
</pre>
我们可以看见，要使用这个命令包，我们就好像使用变量一样。在这个命令包的使用中，命令包&#8220;run-yacc&#8221;中的&#8220;$^&#8221;就是&#8220;foo.y&#8221;，&#8220;$@&#8221;就是&#8220;foo.c&#8221;（有关这种以&#8220;$&#8221;开头的特殊变量，我们会在后面介绍），make在执行命令包时，命令包中的每个命令会被依次独立执行。</pre>
</pre>
<img src ="http://www.cppblog.com/ietj/aggbug/43299.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/ietj/" target="_blank">一路风尘</a> 2008-02-26 21:52 <a href="http://www.cppblog.com/ietj/articles/43299.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>makefile教程---nmake命令编译器的使用</title><link>http://www.cppblog.com/ietj/articles/43240.html</link><dc:creator>一路风尘</dc:creator><author>一路风尘</author><pubDate>Tue, 26 Feb 2008 01:00:00 GMT</pubDate><guid>http://www.cppblog.com/ietj/articles/43240.html</guid><wfw:comment>http://www.cppblog.com/ietj/comments/43240.html</wfw:comment><comments>http://www.cppblog.com/ietj/articles/43240.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/ietj/comments/commentRss/43240.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/ietj/services/trackbacks/43240.html</trackback:ping><description><![CDATA[<h4 class=dtH1>简介</h4>
<p>大家已经习惯于微软提供的功能强大的IDE，已经很少考虑手动编连项目了，所谓技多不压身，有空的时候还是随我一块了解一下命令行编译。</p>
<p>C/C++/VC++程序员或有Unix/Linux编程经验应该很熟悉，以前我曾写过一篇文章描述用csc/vbc来进行命令行编译，今天再介绍一下MS提供的更加快捷有效的编译工具NMake。 </p>
<p>MSDN的描述: Microsoft 程序维护实用工具 (NMAKE.EXE) 是一个 32 位，基于说明文件中包含的命令生成项目的工具。</p>
<p>NMake具有丰富的选项，可以完成复杂编译操作。它可以辨别源代码的改动，并选择性的编译，为你节省大量不必要的编译时间。</p>
<h4 class=dtH1>使用NMAKE</h4>
<p>语法：NMAKE [options] [macros] [targets] [@commandfile]</p>
<p>说明：其中，<a href="ms-help://MS.VSCC.2003/MS.MSDNQTR.2003FEB.2052/vccore/html/_asug_nmake_options.htm" target=_blank><font color=#4371a6>options</font></a>是NMAKE的选项，<a href="ms-help://MS.VSCC.2003/MS.MSDNQTR.2003FEB.2052/vccore/html/_asug_macros_and_nmake.htm" target=_blank><font color=#4371a6>macros</font></a>是在命令行中的宏定义，targets是NMAKE的目标文件列表，commandfile是包含命令行输入的文本文件（或响应文件）。 </p>
<p>NMAKE 使用指定 /F 选项的Makefile(生成文件,通常名字是makefile)；如果未指定 /F 选项，则使用当前目录下的Makefile。如果未指定Makefile，则 NMAKE 使用推理规则生成命令行 targets。 </p>
<p>NMake本身很简单，与NMAKE配合的是Makefile。Makefile的语法比较复杂，通常需要开发者自己手动编写Makefile，下一节我们详细讲解Makefile。 </p>
<p>上面的options和macros做了MSDN的链接，内容较多，请自己查询相关页，可以从<a href="ms-help://MS.VSCC.2003/MS.MSDNQTR.2003FEB.2052/vccore/html/_asug_overview.3a_.nmake_reference.htm" target=_blank><font color=#4371a6>这里</font></a>进入NMake的MSDN帮助页面，在线帮助点<a href="http://msdn.microsoft.com/library/chs/default.asp?url=/library/chs/vccore/html/_asug_overview.3a_.nmake_reference.asp" target=_blank><font color=#4371a6>这里</font></a>。 </p>
<h4 class=dtH1>编写MAKEFILE</h4>
<p>注：本节内容来自MSDN，熟悉此节的朋友可以直接跳过</p>
<p>Makefile的组成部分包括：成文件包含： </p>
<ul type=disc>
    <li><a href="http://truly.cnblogs.com/archive/2005/08/13/213810.html#_asug_description_blocks"><font color=#0066cc>描述块 (description block)</font></a>
    <li><a href="http://truly.cnblogs.com/archive/2005/08/13/213810.html#_asug_commands_in_a_makefile"><font color=#0066cc>命令</font></a>
    <li><a href="http://truly.cnblogs.com/archive/2005/08/13/213810.html#_asug_macros_and_nmake"><font color=#0066cc>宏</font></a>
    <li><a href="http://truly.cnblogs.com/archive/2005/08/13/213810.html#_asug_inference_rules"><font color=#0066cc>推理规则 (inference rules)</font></a>
    <li><a href="http://truly.cnblogs.com/archive/2005/08/13/213810.html#_asug_dot_directives"><font color=#0066cc>点指令</font></a>
    <li><a href="http://truly.cnblogs.com/archive/2005/08/13/213810.html#_asug_makefile_preprocessing"><font color=#0066cc>预处理指令</font></a> </li>
</ul>
<a name=_asug_description_blocks></a>
<p><strong>a.描述块</strong></p>
<p>描述块是后面可跟有命令块的依赖项行：</p>
<pre class=code>    targets... : dependents...
commands...</pre>
<p>依赖项行指定一或多个目标以及零或多个依赖项。目标必须位于行首。用冒号 (:) 将目标和依赖项分开；允许使用空格或制表符。若要拆分行，请在目标或依赖项后面使用反斜杠 (\ )。如果目标不存在、目标的时间戳比依赖项早或者目标是伪目标，则 NMAKE 执行命令。如果某依赖项是其他地方的目标，并且不存在或对于自己的依赖项已过期，则 NMAKE 在更新当前依赖项之前更新该依赖项。 </p>
<p><a name=_asug_commands_in_a_makefile></a><strong>b.命令</strong></p>
<p>如果依赖项已过期，则描述块或推理规则指定要运行的命令块。NMAKE 在运行命令之前显示每个命令，除非使用了 /S 选项、<strong>.SILENT</strong>、<strong>!CMDSWITCHES</strong> 或 @。如果描述块后面没有紧跟命令块，NMAKE 将查找匹配的推理规则。</p>
<p>命令块包含一个或多个命令，每个命令位于各自的命令行上。在依赖项（或规则）和命令块之间不能出现空行。但是可以出现只包含空格或制表符的行；该行被解释为空命令，并且不出现错误。命令行之间允许有空行。</p>
<p>命令行以一个或多个空格或制表符开始。后面紧跟着换行符的反斜杠 ( \ ) 在命令中被解释为空格；在行尾使用反斜杠继续下一行命令。如果反斜杠后紧跟有其他任何字符（包括空格或制表符），则 NMAKE 按原义解释反斜杠。</p>
<p>无论后面是否紧跟有命令块，前面带分号 (;) 的命令可以出现在依赖项行上或推理规则中：</p>
<pre class=code>project.obj : project.c project.h ; cl /c project.c</pre>
<a name=_asug_macros_and_nmake></a>
<p><strong>c.宏</strong></p>
<p>宏用另一个字符串替换生成文件中的特定字符串。使用宏可以：
<ul type=disc>
    <li>创建可生成不同项目的生成文件。
    <li>指定命令选项。
    <li>设置环境变量。 </li>
</ul>
<p>可以定义您<strong>自己的宏</strong>或使用 NMAKE 的<strong>预定义宏</strong>。</p>
<a name=_asug_inference_rules></a>
<p><strong>d.推理规则</strong></p>
<p>推理规则提供命令来更新目标并推理目标的依赖项。推理规则中的扩展名与具有相同基名称的单个目标和依赖项匹配。推理规则是用户定义的，或预定义的；预定义的规则可以重新定义。</p>
<p>如果过期的依赖项没有命令，并且如果 <strong>.SUFFIXES</strong> 包含依赖项的扩展名，则 NMAKE 使用其扩展名与当前或指定目录中的目标和现有文件匹配的规则。如果有多个规则与现有文件匹配，<strong>.SUFFIXES</strong> 列表将确定使用哪一个规则；列表优先级从左向右按降序排列。如果依赖文件不存在，并且未在另一个描述块中作为目标列出，则推理规则可以从具有相同基名称的另一个文件创建缺少的依赖项。如果描述块的目标没有依赖项或命令，推理规则可以更新目标。即使不存在描述块，推理规则也可以生成命令行目标。即使指定了显式依赖项，NMAKE 也可以调用推理依赖项的规则。</p>
<a name=_asug_dot_directives></a>
<p><strong>e.点指令</strong></p>
<p>在描述块之外的行首指定点指令。点指令以句点 ( . ) 开始，后面跟一个冒号 (:)。允许使用空格或制表符。点指令名区分大小写并且应为大写。</p>
<div class=tablediv>
<table class=dtTABLE id=Table1 cellSpacing=0>
    <tbody>
        <tr vAlign=top>
            <th width="31%">指令</th>
            <th width="69%">作用</th>
        </tr>
        <tr vAlign=top>
            <td width="31%"><strong>.IGNORE :</strong></td>
            <td width="69%">忽略从指定该指令的位置到生成文件末尾之间，由命令返回的非零退出代码。默认情况下，如果命令返回非零退出代码，NMAKE 将暂停。若要还原错误检查，请使用 <strong>!CMDSWITCHES</strong>。若要忽略单个命令的退出代码，请使用短划线 (-) 修饰符。若要忽略整个文件的退出代码，请使用 /I 选项。</td>
        </tr>
        <tr vAlign=top>
            <td width="31%"><strong>.PRECIOUS :</strong> <em>targets</em></td>
            <td width="69%">若更新 <em>targets</em> 的命令暂停，则将 <em>targets</em> 保留在磁盘上；若命令通过删除文件处理中断，则该指令无效。用一或多个空格或制表符分隔目标名称。默认情况下，如果通过使用 CTRL+C 或 CTRL+BREAK 组合键中断生成，NMAKE 将删除目标。<strong>.PRECIOUS</strong> 的每一次使用都应用于整个生成文件；多次指定是累计的。</td>
        </tr>
        <tr vAlign=top>
            <td width="31%"><strong>.SILENT :</strong></td>
            <td width="69%">取消从指定该指令的位置到生成文件末尾之间的已执行命令的显示。默认情况下，NMAKE 显示它调用的命令。若要还原回显，请使用 <strong>!CMDSWITCHES</strong>。若要取消单个命令的回显，请使用 <strong>@</strong> 修饰符。若要取消整个文件的回显，请使用 /S 选项。</td>
        </tr>
        <tr vAlign=top>
            <td width="31%"><strong>.SUFFIXES :</strong> <em>list</em></td>
            <td width="69%">列出推理规则匹配的扩展名；预定义为：.exe .obj .asm .c .cpp .cxx .bas .cbl .for .pas .res .rc。</td>
        </tr>
    </tbody>
</table>
</div>
<p>若要更改 <strong>.SUFFIXES</strong> 列表顺序或指定新列表，请清除此列表并指定新的设置。若要清除此列表，请不要在冒号后指定扩展名：</p>
<pre class=code>.SUFFIXES :</pre>
<p>若要将其他后缀添加到列表的末尾，请指定</p>
<pre class=code>.SUFFIXES : suffixlist</pre>
<p>其中 <em>suffixlist</em> 是附加后缀的列表，由一或多个空格或制表符分隔。若要查看 <strong>.SUFFIXES</strong> 的当前设置，请运行选项为 /P 的 NMAKE。</p>
<a name=_asug_makefile_preprocessing></a>
<p><strong>f.预处理指令</strong></p>
<p>可以通过使用预处理指令和表达式控制 NMAKE 会话。预处理指令可以放置在生成文件或 Tools.ini 文件中。使用指令可以有条件地处理生成文件，显示错误信息，包括其他生成文件，取消定义宏以及打开或关闭某些选项。</p>
<ul></ul>
    <h4 class=dtH1>Makefile示例</h4>
    <p>看了一堆理论，很累了吧？下面看一段简单的MakeFile</p>
    <p>
    <table id=Table3 cellSpacing=0 cellPadding=0 width="95%">
        <tbody>
            <tr>
                <td class=code>
                <pre class=code># 宏定义
                SOURCES=AssemblyInfo.cs \
                Form1.cs \
                Form2.cs \
                Form3.cs \
                HelloWorld.cs
                # 引用规则
                # 目标:
                CLRProfiler.exe : $(SOURCES) #&lt;-－依赖项
                # 标志
                # 下面是命令
                csc /t:winexe /out:HelloWorld.exe /r:System.Windows.Forms.dll $(SOURCES)
                clean:
                del HelloWorld.exe
                </pre>
                </td>
            </tr>
            <tr>
                <td class=space style="BORDER-LEFT: 0px">&nbsp;</td>
            </tr>
        </tbody>
    </table>
    </p>
    <p>将上述代码保存为Makefile(没有后缀)放在你的项目文件夹下, 然后打开VS2003.NET命令行窗口，进入项目夹所在路径，打入NMake回车, ok</p>
    <p><strong>示例2</strong> </p>
    <p>下面演示一下多个项目时的编译，每个单独的项目创建单独的makefile，解决方案下放一个总的makefile</p>
    <p>
    <table cellSpacing=0 cellPadding=0 width="95%">
        <tbody>
            <tr>
                <td class=code>
                <pre class=code>all:
                # 分别对项目进行编译
                cd project1
                nmake
                cd ..
                cd project2
                nmake
                cd ..
                cd project3
                nmake
                cd ..
                # 将编译结果汇总到当前路径
                copy project1\project1.dll
                copy project2\project2.dll
                copy project3\project3.exe
                clean:
                # 清除编译结果
                del project1.dll
                del project2.dll
                del project3.exe
                cd project1
                nmake clean
                cd ..
                cd project2
                nmake clean
                cd ..
                cd project3
                nmake clean
                cd ..
                </pre>
                </td>
            </tr>
            <tr>
                <td class=space style="BORDER-LEFT: 0px">&nbsp;</td>
            </tr>
        </tbody>
    </table>
    </p>
    <h4 class=dtH1>小节</h4>
    <p>本文简单介绍了NMAKE的用法，并对Makefile的语法做了介绍。篇幅所限，既不能面面俱到，又不能深入剖析，只希望能够让更多人了解此工具。笔者也是刚刚接触，经验不多，还请各位网友多多拍砖！</p>
    <h4 class=dtH1>附表（makefile中常用的几个符号）</h4>
    <p>
    <table class=dtTABLE id=Table2 cellSpacing=0>
        <tbody>
            <tr vAlign=top>
                <th width="25%">符合</th>
                <th width="75%">作用</th>
            </tr>
            <tr vAlign=top>
                <td width="25%"><strong>^</strong> (caret)</td>
                <td width="75%">用于关闭某些字符所具有的特殊意义，使其只表示字面上的意义。例如：^#abc表示#abc这个字符串，而#abc则用于在makefile中加入注释，#在这里为注释标志，就像C++中的//。另外，在一行的末尾加上^，可以使行尾的回车换行符成为字串的一部分。 </td>
            </tr>
            <tr vAlign=top>
                <td width="25%"><strong>#</strong> (number sign)</td>
                <td width="75%">注释标志，NMAKE会忽略所有从#开始到下一个换行符之间的所有文本。这里要注意的是：在command lines中不能存在注释。因为对于command lines，NMAKE是将其整行传递给OS的。通常对于command lines的注释都是放在行与行之间。 </td>
            </tr>
            <tr vAlign=top>
                <td width="25%"><strong>\</strong> (backslash)</td>
                <td width="75%">用于将两行合并为一行。将其放在行尾，NMAKE就会将行尾的回车换行符解释为空格（space）。</td>
            </tr>
            <tr vAlign=top>
                <td width="25%"><strong>%</strong> (percent symbol)</td>
                <td width="75%">表示其后的字符串为一文件名。</td>
            </tr>
        </tr>
        <tr vAlign=top>
            <td width="25%"><strong>(</strong> (left parentheses)</td>
            <td width="75%"></td>
        </tr>
        <tr vAlign=top>
            <td width="25%"><strong>)</strong> (right parentheses)</td>
            <td width="75%"></td>
        </tr>
        <tr vAlign=top>
            <td width="25%"><strong>{</strong> </td>
            <td width="75%"></td>
        </tr>
        <tr vAlign=top>
            <td width="25%"><strong>}</strong> </td>
            <td width="75%"></td>
        </tr>
        <tr vAlign=top>
            <td width="25%"><strong>!</strong> (exclamation symbol)</td>
            <td width="75%">命令修饰符</td>
        </tr>
        <tr vAlign=top>
            <td width="25%"><strong>@</strong> (at sign)</td>
            <td width="75%">命令修饰符</td>
        </tr>
        <tr vAlign=top>
            <td width="25%"><strong>-</strong> (hyphen)</td>
            <td width="75%"></td>
        </tr>
        <tr vAlign=top>
            <td width="25%"><strong>:</strong> (colon)</td>
            <td width="75%">用于dependent lines和inference rules中，用于分隔target和dependent。</td>
        </tr>
        <tr vAlign=top>
            <td width="25%"><strong>;</strong> (semicolon)</td>
            <td width="75%">如果对于一个dependent line只有一条命令，则可以将该命令放在dependent line的后面，二者之间用&#8220;；&#8221;分隔。</td>
        </tr>
        <tr vAlign=top>
            <td width="25%"><strong>$</strong> (dolor sign)</td>
            <td width="75%">用于调用宏</td>
        </tr>
    </tbody>
</table>
</p>
<img src ="http://www.cppblog.com/ietj/aggbug/43240.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/ietj/" target="_blank">一路风尘</a> 2008-02-26 09:00 <a href="http://www.cppblog.com/ietj/articles/43240.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>C++编译器----cl.exe的全部命令开关</title><link>http://www.cppblog.com/ietj/articles/43233.html</link><dc:creator>一路风尘</dc:creator><author>一路风尘</author><pubDate>Mon, 25 Feb 2008 13:36:00 GMT</pubDate><guid>http://www.cppblog.com/ietj/articles/43233.html</guid><wfw:comment>http://www.cppblog.com/ietj/comments/43233.html</wfw:comment><comments>http://www.cppblog.com/ietj/articles/43233.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/ietj/comments/commentRss/43233.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/ietj/services/trackbacks/43233.html</trackback:ping><description><![CDATA[<p>/C:在预处理输出中保留注释语句<br>/c:只编译，不连接，相当于在"Build"菜单下选择了"Compile"<br>/D:定义常量和宏，与源程序里的#define 有相同效果<br>/E:预处理C、C＋＋源文件，将源文件中所有的预编译指令及宏展开，将注释去掉，然后将预处理器的输出拷贝至标准输出设备输出，并且在每个文件的开头和末尾加入#line<br>/EH:指定编译器用何种异常处理模型<br>/EP:同/E,只是去掉了#line<br>/F:设置程序的堆栈大小<br>/FA:设置生成何种列表文件（汇编、汇编与机器码、汇编与源码、汇编与机器码以及源码）<br>/Fa:指定用/FA设置的列表文件的存放路径及（或）文件名<br>/FD:生成文件的相互依赖信息<br>/Fd:设置程序数据库文件（PDB）的存放路径及（或）文件名<br>/Fe:设置最终可执行文件的存放路径及（或）文件名<br>/FI:预处理指定的头文件，与源文件中的＃include有相同效果<br>/Fm:创建map文件<br>/Fo:设置编译后Obj文件的存放路径及（或）文件名<br>/Fp:设置预编译文件（pch）的存放路径及（或）文件名<br>/FR:生成浏览信息（sbr）文件<br>/Fr:同/FR,不同之处在于/Fr不包括局部变量信息<br>/G3:为80386处理器优化代码生成<br>/G4:为80486处理器优化代码生成<br>/G5:为Pentium处理器优化代码生成<br>/G6:为Pentium Pro处理器优化代码生成<br>/GA:为Windows应用程序作优化<br>/GB:为Pentium处理器优化代码生成，使用80386、80486、Pentium、Pentium Pro的混合指令集，是代码生成的默认选项（程序属性选项中Processor对应Blend）<br>/GD:为Windows动态库（dll）作优化，此开关在VC6中没有实现<br>/Gd:指定使用__cdecl的函数调用规则<br>/Ge:激活堆栈检测<br>/GF:消除程序中的重复的字符串，并将她放到只读的缓冲区中<br>/Gf:消除程序中的重复字符串<br>/Gh:在每个函数的开头调用钩子（hook）函数--penter<br>/Gi:允许渐进编译<br>/Gm:允许最小化rebuild<br>/GR:允许运行时类型信息(Run-Time Type Infomation)<br>/Gr:指定使用__fastcall的函数调用规则<br>/Gs:控制堆栈检测所用内存大小<br>/GT:支持用__declspec(thread)分配的数据的fier-safety<br>/GX:允许同步异常处理，与/EHsc开关等价<br>/Gy:允许编译器将每一个函数封装成COMDATs的形式，供连接器调用<br>/GZ:允许在Debug build 的时候捕捉Release build的错误<br>/Gz:指定使用__stdcall的函数调用规则<br>/H:限制外部名字的长度<br>/HELP:列出编译器的所有的命令开关<br>/I:指定头文件的搜索路径<br>/J:将char的缺省类型从signed char改成unsigned char<br>/LD:创建一个动态连接库<br>/LDd:创建一个Debug版本的动态链接库<br>/link:将指定的选项传给连接器<br>/MD:选择多线程、DLL版本的C Run－Time库<br>/MDd:选择多线程、DLL、Debug版本的C Run－Time库<br>/ML:选择单线程版本的C Run—Time库<br>/MLd:选择单线程、Debug版本的C Run—Time库<br>/MT:选择多线程版本的C Run-Time库<br>/MTd:选择多线程、Debug版本的C Run—Time库<br>/nologo:不显示程序的版权信息<br>/O1:优化使产生的可执行代码最小<br>/O2:优化使产生的可执行代码速度最快<br>/Oa:指示编译器程序里没有使用别名，可以提高程序的执行速度<br>/Ob:控制内联（inline）函数的展开<br>/Od:禁止代码优化<br>/Og:使用全局优化<br>/Oi:用内部函数去代替程序里的函数调用，可以使程序运行的更快，但程序的长度变长<br>/Op:提高浮点数比较运算的一致性<br>/Os:产生尽可能小的可执行代码<br>/Ot:产生尽可能块的可执行代码<br>/Ow:指示编译器在函数体内部没有使用别名<br>/Ox:组合了几个优化开关，达到尽可能多的优化<br>/Oy:阻止调用堆栈里创建帧指针<br>/Q1f:对核心级的设备驱动程序生成单独的调试信息<br>/QI0f:对Pentium 0x0f错误指令作修正<br>/Qifdiv:对Pentium FDIV错误指令作修正<br>/P:将预处理输出写到指定文件里，文件的后缀名为I<br>/TC:将命令行上的所有文件都当作C源程序编译，不管后缀名是否为.c<br>/Tc:将指定的文件当作C源程序编译，不管后缀名是否为.c<br>/TP:将命令行上的所有文件都当作C＋＋源程序编译，不管后缀名是否为.cpp<br>/Tp:将指定文件当作C＋＋源程序编译，不管后缀名是否为.cpp<br>/U:去掉一个指定的前面定义的符号或常量<br>/u:去掉所有前面定义的符号或常量<br>/V:在编译的obj文件里嵌入版本号<br>/vd:禁止/允许构造函数置换<br>/vmb:选择指针的表示方法，使用这个开关，在声明指向某个类的成员的指针之前，必须先定义这个类<br>/vmg:选择指针的表示方法，使用这个开关，在声明指向某个类的成员的指针之前，不必先定义这个类，但要首先指定这个类是使用何种继承方法<br>/vmm:设置指针的表示方法为Single Inheritance and Multiple Inheritance<br>/vms:设置指针的表示方法为Single Inheritance<br>/vmv:设置指针的表示方法为Any class<br>/W:设置警告等级<br>/w:禁止所有警告<br>/X:阻止编译器搜索标准的include 目录<br>/Yc:创建预编译头文件（pch）<br>/Yd:在所有的obj文件里写上完全的调试信息<br>/Yu:在build过程中使用指定的预编译头文件<br>/YX:指示编译器若预编译头文件存在，则使用它，若不存在，则创建一个<br>/Z7:生成MSC7.0兼容的调试信息<br>/Za:禁止语言扩展(Microsoft Extensions to C)<br>/Zd:调试信息只包含外部和全局的符号信息以及行号信息<br>/Ze:允许语言扩展(Microsoft Extensions to C)<br>/Zg:为源文件里面定义的每个函数生成函数原型<br>/ZI:生成程序库文件（Pdb）并支持Edit and Continue调试特性<br>/Zi:生成程序库文件（pdb），包含类型信息和符号调试信息<br>/ZL:从obj文件里去掉缺省的库文件名<br>/Zm:设置编译器的内存分配xianzhi<br>/Zn:禁止浏览信息文件里面的封装<br>/Zp:设置结构成员在内存里面的封装格式<br>/Zs:快速检查语法错误<br>－－－－－－－－－－－－－－－－－－－－－－－－－－<br>vc所支持的文件类型</p>
<p>DSW:全称是Developer Studio Workspace，最高级别的配置文件，记录了整个工作空间的配置信息，她是一个纯文本的文件，在vc创建新项目的时候自动生成<br>DSP:全称是Developer Studio Project，也是一个配置文件，不过她记录的是一个项目的所有配置信息，纯文本文件<br>OPT：与DSW、DSP配合使用的配置文件，她记录了与机器硬件有关的信息，同一个项目在不同的机器上的opt文件内容是不同的<br>CLW：记录了跟ClassWizard相关的信息，如果丢失了clw文件，那么在Class View面板里就没有类信息<br>PLG：实际上是一个超文本文件，可以用Internet Explorer打开，记录了Build的过程，是一个日志型文件<br>RC：资源描述文件，记录了所有的资源信息，在资源编辑器里作的修改，实际上都是对RC文件的修改<br>RC2：附加的资源描述文件，不能直接资源编辑器修改，只能手工添加，可以用来添加额外的资源<br>RES：经过资源编辑器编译之后的资源文件，以二进制方式存放<br>SBR：编译器生成的浏览信息文件，在代码导航的时候非常有用，她需要在编译时指定/FR或者/Fr开关<br>BSC：BSCMAKE.EXE将所有的SBR文件作为输入，经过处理之后输出一个BSC文件，在代码导航的时候实际用到的是BSC文件<br>ILK：当选定渐增型编译连接时，连接器自动生成ILK文件，记录连接信息<br>PDB：全称是Program DataBase，即程序数据库文件，用来记录调试信息，是一个相当重要的文件，没有他，程序无法正常调试<br>LIB：如果项目输出是Dll的话，一般会输出一个跟项目同名的Lib文件，记录输出的函数信息<br>EXP：同Lib，是跟Dll一起生成的输出文件<br>PCH：全称是PreCompiled Header，就是预先编译好的头文件，在编译时指定/Yu开关时编译器自动生成</p>
<br><img src ="http://www.cppblog.com/ietj/aggbug/43233.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/ietj/" target="_blank">一路风尘</a> 2008-02-25 21:36 <a href="http://www.cppblog.com/ietj/articles/43233.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item></channel></rss>