??xml version="1.0" encoding="utf-8" standalone="yes"?>
但不同操作系l的动态库׃格式 不同Q在需要不同操作系l调用时需要进行动态库E序UL。本文分析和比较了两U操作系l动态库技术,q给ZVisual C++~制的动态库UL到Linux上的Ҏ(gu)和经验?
1、引a
动态库QDynamic Link Library abbrQDLLQ技术是E序设计中经帔R用的技术。其目的减少E序的大,节省I间Q提高效率,h很高的灵zL?
采用动态库技术对于升UY件版本更加容易。与静态库QStatic Link LibraryQ不同,动态库里面的函C是执行程序本w的一部分Q而是Ҏ(gu)执行需要按需载入Q其执行代码可以同时在多个程序中׃n?
?Windows和Linux操作pȝ中,都可采用q种方式q行软g设计Q但他们的调用方式以?qing)程序编制方式不相同。本文首先分析了在这两种操作pȝ中通常采用的动态库调用Ҏ(gu)以及(qing)E序~制方式Q然后分析比较了q两U方式的不同之处Q最后根据实际移植程序经验,介绍了将VC++~制的Windows动态库UL到Linux下的Ҏ(gu)?
2、动态库技?
2.1 Windows动态库技?
动态链接库是实现Windows应用E序׃n资源、节省内存空间、提高用效率的一个重要技术手Dc(din)常见的动态库包含外部函数和资源,也有一些动态库只包含资源,如Windows字体资源文gQ称之ؓ(f)资源动态链接库。通常动态库?dllQ?drv?fon{作为后~?
相应的windows静态库通常?libl尾QW(xu)indows自己将一些主要的pȝ功能以动态库模块的Ş式实现?
Windows动态库在运行时被系l加载到q程的虚拟空间中Q用从调用q程的虚拟地址I间分配的内存,成ؓ(f)调用q程的一部分。DLL也只能被该进E的U程所讉K。DLL的句柄可以被调用q程使用Q调用进E的句柄可以被DLL使用?
DLL 模块中包含各U导出函敎ͼ用于向外界提供服务。DLL可以有自q数据D,但没有自q堆栈Q用与调用它的应用E序相同的堆栈模式;一个DLL在内存中只有一个实例;DLL实现了代码封装性;DLL的编制与具体的编E语a?qing)编译器无关Q可以通过DLL来实现合语a~程。DLL函数中的代码所创徏的Q何对象(包括变量Q都归调用它的线E或q程所有?
Ҏ(gu)调用方式的不同,对动态库的调用可分ؓ(f)静态调用方式和动态调用方式?
(1) 静态调用,也称为隐式调用,q译系l完成对DLL的加载和应用E序l束时DLL卸蝲的编码(Windowspȝ负责对DLL调用ơ数的计敎ͼQ调用方式简单,能够满通常的要求。通常采用的调用方式是把生动态连接库时生的.LIB文g加入到应用程序的工程中,想用DLL中的函数Ӟ只须在源文g中声明一下?
LIB文g包含了每一个DLL导出函数的符号名和可选择的标识号以及(qing)DLL文g名,不含有实际的代码。Lib文g包含的信息进入到生成的应用程序中Q被调用的DLL文g?x)在应用E序加蝲时同时加载在到内存中?
(2)动态调用,x式调用方式,是由~程者用API函数加蝲和卸载DLL来达到调用DLL的目的,比较复杂Q但能更加有效地使用内存Q是~制大型应用E序时的重要方式。在Windowspȝ中,与动态库调用有关的函数包括:(x)
①LoadLibraryQ或MFC 的AfxLoadLibraryQ,装蝲动态库?
②GetProcAddressQ获取要引入的函敎ͼ符号名或标识号转换为DLL内部地址?
③FreeLibraryQ或MFC的AfxFreeLibraryQ,释放动态链接库?
?windows中创建动态库也非常方便和单。在Visual C++中,可以创徏不用MFC而直接用C语言写的DLLE序Q也可以创徏ZMFCcd的DLLE序。每一个DLL必须有一个入口点Q在VC++ 中,DllMain是一个缺省的入口函数。DllMain负责初始?Initialization)和结?Termination)工作?
动态库输出函数也有两种U定Q分别是Z调用U定和名字修饰约定。DLLE序定义的函数分为内部函数和导出函数Q动态库导出的函C其它E序模块调用。通常可以有下面几U方法导出函敎ͼ(x)
①采用模块定义文g的EXPORT部分指定要输入的函数或者变量?
②使用MFC提供的修饰符号_declspec(dllexport)?
③以命令行方式Q采?EXPORT命o(h)行输出有兛_数?
在windows动态库中,有时需要编写模块定义文?.DEF)Q它是用于描qDLL属性的模块语句l成的文本文件?
2.2 Linux׃n对象技?
?Linux操作pȝ中,采用了很多共享对象技术(Shared ObjectQ,虽然它和W(xu)indows里的动态库相对应,但它q不UCؓ(f)动态库。相应的׃n对象文g?so作ؓ(f)后缀Qؓ(f)了方便,在本文中Q对该概念不q行专门区分。Linuxpȝ?lib以及(qing)标准囑Ş界面?usr/X11R6/lib{目录里面,有许多以sol尾的共享对象?
同样Q在Linux下,也有静态函数库q种调用方式Q相应的后缀?al束。Linux采用该共享对象技术以方便E序间共享,节省E序占有I间Q增加程序的可扩展性和灉|性。Linuxq可以通过LD-PRELOAD变量让开发h员可以用自qE序库中的模块来替换pȝ模块?
?Windowspȝ一P在Linux中创建和使用动态库是比较容易的事情Q在~译函数库源E序时加?shared选项卛_Q这h生成的执行程序就是动态链接库。通常q样的程序以so为后~Q在Linux动态库E序设计q程中,通常程是编写用L(fng)接口文gQ通常?h文gQ编写实际的函数文gQ以.c?cpp为后~Q再~写makefile文g。对于较?yu)的动态库E序可以不用如此Q但q样设计使程序更加合理?
~译生成动态连接库后,q而可以在E序中进行调用。在Linux中,可以采用多种调用方式Q同W(xu)indows的系l目?..\system32{?一P可以动态库文g拯?lib目录或者在/lib目录里面建立W号q接Q以便所有用户用?
下面介绍Linux调用动态库l常使用的函敎ͼ但在使用动态库Ӟ源程序必d含dlfcn.h头文Ӟ该文件定义调用动态链接库的函数的原型?
(1)_打开动态链接库QdlopenQ函数原型void *dlopen (const char *filename, int flag); dlopen用于打开指定名字(filename)的动态链接库Qƈq回操作句柄?
(2)取函数执行地址QdlsymQ函数原型ؓ(f): void *dlsym(void *handle, char *symbol); dlsymҎ(gu)动态链接库操作句柄(handle)与符?symbol)Q返回符号对应的函数的执行代码地址?
(3)关闭动态链接库QdlcloseQ函数原型ؓ(f): int dlclose (void *handle); dlclose用于关闭指定句柄的动态链接库Q只有当此动态链接库的用计Cؓ(f)0?才会(x)真正被系l卸载?
(4)动态库错误函数QdlerrorQ函数原型ؓ(f): const char *dlerror(void); 当动态链接库操作函数执行p|Ӟdlerror可以q回出错信息Q返回gؓ(f)NULL时表C操作函数执行成功?
在取到函数执行地址后,可以在动态库的用程序里面根据动态库提供的函数接口声明调用动态库里面的函数。在~写调用动态库的程序的makefile文gӞ需要加入编译选项-rdynamic?ldl?
除了采用q种方式~写和调用动态库之外QLinux操作pȝ也提供了一U更为方便的动态库调用方式Q也方便了其它程序调用,q种方式与Windowspȝ的隐式链接类伹{其动态库命名方式?#8220;lib*.so.*”。在q个命名方式中,W一?表示动态链接库的库名,W二?通常表示该动态库的版本号Q也可以没有版本受?
在这U调用方式中Q需要维护动态链接库的配|文?etc /ld.so.conf来让动态链接库为系l所使用Q通常动态链接库所在目录名q加到动态链接库配置文g中。如hX windowH口pȝ发行版该文g中都h/usr/X11R6/libQ它指向X windowH口pȝ的动态链接库所在目录?
Z使动态链接库能ؓ(f)pȝ所׃nQ还需q行动态链接库的管理命?/sbin/ldconfig。在~译所引用的动态库Ӟ可以在gcc采用 –l?L选项或直接引用所需的动态链接库方式q行~译。在Linux里面Q可以采用ldd命o(h)来检查程序依赖共享库?
3、两U系l动态库比较分析
Windows和Linux采用动态链接库技术目的是基本一致的Q但׃操作pȝ的不同,他们在许多方面还是不相同,下面从以下几个方面进行阐q?
(1) 动态库E序~写Q在Windowspȝ下的执行文g格式是PE格式Q动态库需要一个DllMain函数作ؓ(f)初始化的人口Q通常在导出函数的声明旉要有 _declspec(dllexport)关键字。Linux下的gcc~译的执行文仉认是ELF格式Q不需要初始化入口Q亦不需要到函数做特别声明,~写比较方便?
(2)动态库~译Q在windowspȝ下面Q有方便的调试编译环境,通常不用自己ȝ写makefile文gQ但在linux下面Q需要自己动手去~写makefile文gQ因此,必须掌握一定的makefile~写技巧,另外Q通常Linux~译规则相对严格?
(3)动态库调用斚wQW(xu)indows和Linux对其下编制的动态库都可以采用显式调用或隐式调用Q但具体的调用方式也不尽相同?
(4) 动态库输出函数查看Q在Windows中,有许多工具和软g可以q行查看DLL中所输出的函敎ͼ例如命o(h)行方式的dumpbin以及(qing)VC++工具中的 DEPENDSE序。在Linuxpȝ中通常采用nm来查看输出函敎ͼ也可以用ldd查看E序隐式链接的共享对象文件?
(5)Ҏ(gu)作系l的依赖Q这两种动态库q行依赖于各自的操作pȝQ不能跨q_使用。因此,对于实现相同功能的动态库Q必Mؓ(f)两种不同的操作系l提供不同的动态库版本?
4、动态库ULҎ(gu)
如果要编制在两个pȝ中都能用的动态链接库Q通常?x)先选择在Windows的VC++提供的调试环境中完成初始的开发,毕竟VC++提供的图形化~辑和调试界面比vi和gcc方便许多。完成测试之后,再进行动态库的程序移植?
通常gcc默认的编译规则比VC++默认的编译规则严|即在VC++下面没有M警告错误的程序在gcc调试中也?x)出现许多警告错误,可以在gcc中采?w选项关闭警告错误?
下面l出E序UL需要遵循的规则以及(qing)l验?
(1) 量不要改变原有动态库头文件的序。通常在C/C++语言中,头文件的序有相当的关系。另外虽然C/C++语言区分大小写,但在包含头文件时QLinux必须与头文g的大写相同Q因为ext2文gpȝҎ(gu)件名是大写敏感Q否则不能正编译,而在Windows下面Q头文g大小写可以正编译?
(2)不同pȝ独有的头文g。在Windowspȝ中,通常?x)包?windows.h头文Ӟ如果调用底层的通信函数Q则?x)包含winsock..h头文件。因此在UL到LinuxpȝӞ要注释掉q些Windowspȝ独有的头文g以及(qing)一些windowspȝ的常量定义说明,增加Linux都底层通信的支持的头文件等?
(3) 数据cd。VC++h许多独有的数据类型,如__int16Q__int32QTRUEQSOCKET{,gcc~译器不支持它们。通常做法是需要将 windows.h和basetypes.h中对q些数据q行定义的语句复制到一个头文g中,再在Linux中包含这个头文g。例如将套接字的cd?SOCKET改ؓ(f)int?
(4)关键字。VC++中具有许多标准C中所没有采用的关键字Q如BOOLQBYTEQDWORDQ__asm{,通常在ؓ(f)了移植方便,量不用它们,如果实在无法避免可以采用#ifdef ?endif为LINUX和W(xu)INDOWS~写两个版本?
(5) 函数原型的修攏V通常如果采用标准的C/C++语言~写的动态库Q基本上不用再重新编写函敎ͼ但对于系l调用函敎ͼ׃两种pȝ的区别,需要改变函数的调用方式{,如在Linux~制的网l通信动态库中,用close()函数代替windows操作pȝ下的closesocket()函数来关闭套接字。另外在Linux下没有文件句柄,要打开文g可用open和fopen函数Q具体这两个函数的用法可参考文献[2]?
(6)makefile 的编写。在windows下面通常由VC++~译器来负责调试Q但gcc需要自己动手编写makefile文gQ也可以参照VC++生成?makefile文g。对于动态库ULQ编译动态库旉要加?shared选项。对于采用数学函敎ͼ如幂U数的程序,在调用动态库是,需要加?lm?
(7)其它一些需要注意的地方
①E序设计l构分析Q对于移植它人编写的动态库E序Q程序结构分析是必不可少的步骤,通常在动态库E序中,不会(x)包含界面{操作,所以相对容易一些?
②在Linux中,Ҏ(gu)件或目录的权限分为拥有者、群l、其它。所以在存取文gӞ要注意对文g是读q是写操作,如果是对文gq行写操作,要注意修Ҏ(gu)件或目录的权限,否则无法Ҏ(gu)件进行写?
③ 指针的用,定义一个指针只l它分配四个字节的内存,如果要对指针所指向的变量赋|必须用malloc函数为它分配内存或不把它定义为指针而定义ؓ(f)变量卛_Q这点在linux下面比windows~译严格。同L(fng)构不能在函数中传|如果要在函数中进行结构传|必须把函C的结构定义ؓ(f)l构指针?
④路径标识W,在Linux下是“/”Q在Windows下是“\”Q注意windows和Linux的对动态库搜烦路径的不同?
⑤~程和调试技巧方面。对不同的调试环境有不同的调试技巧,在这里不多叙q?
5、结束语
本文pȝ分析了windows和Linux动态库实现和用方式,从程序编写、编译、调用以?qing)对操作pȝ依赖{方面综合分析比较了q两U调用方式的不同之处Q根据实际程序移植经验,l出了将VC++~制的Windows动态库UL到Linux下的Ҏ(gu)以及(qing)需要注意的问题Q同时ƈl出了程序示例片断,实际在程序移植过E中Q由于系l的设计{方面,可能ULh需要注意的斚wq比上面复杂Q本文通过ȝ归纳q而ؓ(f)不同操作pȝE序UL提供了有意的l验和技巧?/p>
(本文来源于internetQ感谢原创作?
--------------------------------------------------------------------------
unix具体~译的例子:(x)
源文件ؓ(f)main.c, x.c, y.c, z.c,头文件ؓ(f)x.h,y.h,z.h
# 声称动代q接库,假设名称为libtest.so
gcc x.c y.c z.c -fPIC -shared -o libtest.so
# main.c和动态连接库q行q接生成可执行文?
gcc main.c -L. -ltest -o main
# 输出LD_LIBRARY_PATH环境变量Q一边动态库装蝲器能够找到需要的动态库
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
# 试是否动态连接,如果列出libtest.soQ那么应该是q接正常?
ldd main
-fPICQ表C编译ؓ(f)位置独立的代码,不用此选项的话~译后的代码是位|相关的所以动态蝲入时是?nbsp; q?nbsp; 代码拯的方式来满不同q程的需要,而不能达到真正代码段׃n的目的?
-L.Q表Cq接的库在当前目录中
-ltestQ编译器查找动态连接库时有隐含的命名规则,卛_l出的名字前面加上libQ后面加?so来确定库的名U?
LD_LIBRARY_PATHQ这个环境变量指C动态连接器可以装蝲动态库的\径?
当然如果有root权限的话Q可以修?etc/ld.so.conf文gQ然后调?
/sbin/ldconfig来达到同L(fng)目的Q不q如果没有root权限Q那么只能采用输出LD_LIBRARY_PATH的方法了?/p>
本文来自CSDN博客Q{载请标明出处Q?a >http://blog.csdn.net/yang_lang/archive/2010/10/08/5926486.aspx
Jon Pincus
Microsoft Corporation
摘要Q?/strong>讨论错误处理范例以及(qing)与多个范例相兌的陷阱,q提供了两个单但臛_重要的有关错误情况处理的原则?/p>
有能力的E序员能够编写在未发生异常情冉|正常q行的代码。ɽE序员出cL萃的技能之一是能够编写在发生错误和出?#8220;意外事g”时仍然能l箋q行的代码。然而,术语“意外事g”?x)给ZU错误的印象。如果?zhn)的代码嵌入在一个广泛分布的成功产品中,那么(zhn)应该预料到代码可能发生的各U异常(且可怕)的情c(din)计机耗尽内存Q文件未如?zhn)所愿地存在于应该存在的地方Q从未失败的函数有可能在新版本的操作pȝ中失败,{等Q不一而。如果?zhn)希望代码能l可靠地q行Q那么就需要预见所有这些事件?/p>
本文讨论的特定类别的异常事g是错误情c(din)错误情况ƈ没有一个准的定义。直观地Ԍ它就是指通常能够成功的事情ƈ未成功。内存分配失败就是一个典型示例。通常Q系l有大量内存可以满需求,但是偶尔计算Z可能?x)由于某U原因而特别繁忙,例如Q运行大型的?sh)子表格计算Q或者在 Internet 世界中有人正在对该计机发动拒绝服务d。如果?zhn)要编写Q意类型的重要lg、服务或应用E序Q那么就需要预见到q种情况?/p>
本文采用的编码示例基?C/C++Q但一般原则是独立于编E语a的。即使不使用 C/C++ 代码Q用多U语a的程序员也应该记住这一炏V例如,PC Week 丑֊的黑客竞?(Hackpcweek.com) 的获胜者,是利用某些 Perl CGI 代码中检查返回代码的p|Q来部分地对 Apache 服务器发h凅R有兌一步的信息Q请参阅 http://www.zdnet.com/eweek/stories/general/0,11011,2350744,00.html 上的文章?/p>
M重要的Y仉分都?x)与其他的层和组件进行交互。编写处理错误的代码的第一步,是了解当错误发生时系l的其余部分正在执行哪些操作。本文的其余部分讨论错误处理范例,然后讨论与多个范例相兌的陷阱。本文还包括了两个简单但臛_重要的有关错误情况处理的原则?/p>
无论何时发生错误情况Q都需要考虑三个独立的问题:(x)错误情c(din)报告错误情况以?qing)对错误情况作出响应。通常Q处理这些问题的职责分散在代码的不同lg或层中。例如,系l是否内存不x操作pȝ的工作。内存分配函数的工作是将q种情况报告l它的调用方。例如,VirtualAlloc ?x)返?NULLQMicrosoft 基础cd (MFC) q算Wnew ?x)引?strong>CMemoryException *本页内容
?/a>
错误处理范例
多个 API U定的问?/a>
有关错误情况的两个简单原?/a>
结
?/h3>
错误处理范例
因ؓ(f)不同的层和组仉要相互协作以处理q些错误Q所以第一步是定义Ҏ(gu)词汇Q即Q所有组件共同遵守的U定。遗憄是,?C、C++ 或其他Q何语a中都没有单一且定义完善的U定。相反,却存在许多约定,q且每种U定都有各自的优~点?/p>
下面列出了一些最常用的约定:(x)
q回一?/strong> BOOL g指示成功或失败?/strong>Windows API ?MFC 中的很多调用都返?TRUE 以指C成功,q回 FALSE 以指C失败。这U方法既好又单。但问题在于Q该Ҏ(gu)不对p|q行解释Q也不区分不同类型的成功或失败。(当?Windows API Ӟ(zhn)可以?span class=Apple-converted-space>
q回状态?/strong>遗憾的是Q随着 Windows API 的变化,该约定出C两种不同的样式。COM 函数q回 HRESULTQHRESULT >= 0Q例如,S_OKQ表C成功,HRESULT < 0Q例如,E_FAILQ表C失败。其他函敎ͼ例如Q?strong>Registry 函数Q则q回 WINERROR.H 中定义的错误代码。在该约定中QERROR_SUCCESS (0) 是唯一的成功|所有其他值都表示各种形式的失败。这U不一致有可能造成各种各样的乱。更p糕的是Q?0Q它?Boolean 样式的返回值约定中表示p|Q在此处表示成功。其后果在后面q行讨论。在M事g中,状态返回方法都h优势 ?不同的值可以表CZ同类型的p|Q或者对?HRESULT 而言Q表C成功)?/p>
q回一?/strong> NULL 指针?/strong>C 样式内存分配函数Q例如,HeapAlloc?strong>VirtualAlloc?strong>GlobalAlloc ?span class=Apple-converted-space> mallocQ通常q回一?NULL 指针。另一个示例是 C 标准库的 fopen 函数。与 BOOL q回值相同,q需要其他一些机制来区分不同U类的失败?/p>
q回一?/strong> “ 不可能的?/strong> ” ?/strong>该值通常?0Q对于整敎ͼ?span class=Apple-converted-space> GetWindowsDirectoryQ或 –1Q对于指针或长度Q如 C 标准库的 fgets 函数Q。NULL 指针U定的泛化是它找到某个??例程采用其他方式无法返回该|q且让该DC错误。因为通常没有中心模式Q所以在该Ҏ(gu)扩展到大?API 时会(x)在某U程度上出现问题?/p>
引发 C++ 异常?/strong>对于Ua(b)?C++ E序来说Q该U定可让(zhn)用语a功能来获益。Bobby Schmidt 最q撰写的有关异常?#8220;Deep C++”pd文章详细讨论了这些问题。然而,该Ҏ(gu)与旧式的 C 代码或者与 COM l合可能?x)带来问题。对?COM Ҏ(gu)而言Q引?C++ 异常是非法的。此外,C++ 异常是开销相对较大的一U机制。如果操作本w的价g高,那么q大的开销通常?x)抵消所获得的好处?/p>
引发l构化异常?/strong>q里的注意事Ҏ(gu)?C++ 异常相反。该Ҏ(gu)对于 C 代码而言非常整洁Q但?C++ 的交互性不太好。同P该方法不能与 COM 有效地结合?/p>
?/strong> 如果(zhn)要选用较旧的代码基Q那么?zhn)有时会(x)看?#8220;原生的异常处理机?#8221;。C++ ~译器只是在最q才开始比较好地处理异常。在q去Q开发h员经常基?Windows l构化异常处?(SEH) ?C 库的 setjmp/longjmp 机制来构Z们自q机制。如果?zhn)已经l承了这些代码基中的一个,则需要自担风险,重新~写它可能是最好的选择。否则,最好由l验非常丰富的程序员来处理?/p>
错误处理是Q?API 定义的关键部分。无论?zhn)是要设?API q是使用他h设计?APIQ都是如此。对?API 定义而言Q错误行例与cd义或命名Ҏ(gu)同样重要。例如,MFC API 非常明确地规定了在资源分配失败时哪些函数引发哪些异常Q以?qing)哪些函数返?BOOL 成功/p|调用。API 的设计者明地某些想法植入这一规定Q用户需要理解其意图q按照已l构建的规则q行操作?/p>
如果(zhn)要使用现有?APIQ则必须处理所有现有约定。如果?zhn)要设?APIQ则应该选用能够与?zhn)已经使用?API 盔R应的约定?/p>
如果(zhn)要使用多个 APIQ则通常要用多个约定。某些约定组合可以很好地工作Q因些约定很难淆。但是,某些U定l合却很Ҏ(gu)出错。本文所讨论的许多特定陷阱就是由于存在这些不一致而造成的?/p>
L(fng)和匹配不同的 API U定通常是无法避免的Q但q非常容易出错。一些实例是显而易见的。例如,如果(zhn)尝试在同一个可执行文g中?Windows SEH ?C++ 异常Q则很可能会(x)p|。其他示例更为微妙。其中一个反复出现的特定CZ׃ HRESULT 有关Qƈ且是以下CZ的某U变体:(x) 该示例ؓ(f)何是错误的?FAILED 是一?HRESULT 样式的宏Q因此它?x)检查其参数是否于 0。以下是它的定义Q摘?winerror.hQ:(x) 因ؓ(f) FALSE 被定义ؓ(f) 0Q所?FAILED(FALSE) == 0 是违反直觉的Q这无须多言。而且Q因定义嵌入了强制{换,所以即使?zhn)使用警告U别 4Q也不会(x)获得~译器警告?/p>
当?zhn)处?BOOL Ӟ不应该用宏Q但应该q行昑ּ查:(x) 相反Q当(zhn)处?HRESULT Ӟ则应该始l?SUCCEEDED ?FAILED 宏?/p>
q是一个恶性错误,因ؓ(f)它很Ҏ(gu)被忽略。如?span class=Apple-converted-space> CoDoIt q回 S_OKQ则试成功完成。但是,如果 CoDoIt q回某个其他成功状态,?x)怎样呢?那样Qhr > 0Q所?!hr == 0Qif 试p|Q代码将q回实际上ƈ未发生的错误?/p>
下面是另一个示例:(x) Z有时?x)插话?span id="vnpb1px" class=Apple-converted-space> ISomething::DoIt 在成功时Lq回 S_OKQ因此最后两个代码片D肯定都没有问题。但是,q不是一个安全的假设。COM 接口的说明非常清楚。函数在成功时可以返回Q何成功|因此 ISomething::DoIt 的众多实现者中的Q何一个都可能选择q回某个|例如 S_FALSE。在q种情况下,(zhn)的代码中止运行?/p>
正确的解x案是使用宏,q也是宏存在的原因?/p>
因ؓ(f)已经引出?HRESULT 的主题,所以现在是提醒(zhn)?S_FALSE Ҏ(gu)的大好时机Q?/p>
它是一个成功代码,而不是一个失败代码,因此 SUCCEEDED(S_FALSE) == 1?/p>
它被定义?1Q而不?0Q因?S_FALSE == TRUE?/p>
有许多简单的Ҏ(gu)可以使代码更可靠地处理错误情c(din)相反,Z不愿意做的许多简单事情会(x)使代码在发生错误情况时变得脆弱和不可靠?/p>
没有比这更简单的事情了。几乎所有函数都提供某种表明它们是成功还是失败的指示Q但如果(zhn)不查它们,则这一Ҏ(gu)有Q何用处。这能有多困隑֑Q可以将其视Z个卫生问题。?zhn)知道在吃东西之前应该z手Q但(zhn)可能ƈ不Lq样做。这与检查返回值的道理相同?/p>
下面是一个涉?qing)?span id="jxfn31l" class=Apple-converted-space> GetWindowsDirectory 函数的简单而实用的CZ。MSDN 文清楚地说明了 GetWindowsDirectory 的错误行为:(x) 如果该函数失败,则返回gؓ(f) 0。要获得扩展的错误信息,误?span class=Apple-converted-space> GetLastError 函数?/p>
实际上,文档中写得非常清楚?/p>
下面是一个判?Windows 目录ȝ在哪个驱动器中的代码片段?/p>
如果 GetWindowsDirectory p|Q会(x)发生什么情况呢Q(如果(zhn)不怿 GetWindowsDirectory ?x)失败,q只是?zhn)暂时的观炏V)好,该代码不查返回|因此分配l?cDriveLetter 的值未初始化。未初始化的内存可以hL倹{实际上Q该代码随机选择驱动器。这样做几乎不可能是正确的?/p>
正确的做法是查错误状态?/p>
q种情况q能发生吗?不检查返回值的最常见借口?#8220;我知道那个函数绝对不?x)失?#8221;?strong>GetWindowsDirectory多个 API U定的问?/h3>
extern BOOL DoIt();
BOOL ok;
ok = DoIt(...);
if (FAILED(ok)) // WRONG!!!
return;
#define FAILED(Status) ((HRESULT)(Status)<0)
BOOL ok;
ok = DoIt(...);
if (! ok)
return;
HRESULT hr;
hr = ISomething->DoIt(...);
if (! hr) // WRONG!!!
return;
HRESULT hr;
hr = ISomething->DoIt(...);
if (hr == S_OK) // STILL WRONG!!!
return;
HRESULT hr;
hr = ISomething->DoIt(...);
if (FAILED(hr))
return;
有关错误情况的两个简单原?/h3>
L查返回状?/h4>
Return Values
TCHAR cDriveLetter;
TCHAR szWindowsDir[MAX_PATH];
GetWindowsDirectory(szWindowsDir, MAX_PATH);
cDriveLetter = szWindowsDir[0]; // WRONG!!!
...
TCHAR cDriveLetter;
TCHAR szWindowsDir[MAX_PATH];
if (GetWindowsDirectory(szWindowsDir, MAX_PATH))
{
cDriveLetter = szWindowsDir[0];
...
}
现在 Windows l端服务器出CQ要判断单个用户?Windows 目录变得更ؓ(f)复杂?strong>GetWindowsDirectory 必须完成更多的工作,可能包括分配内存。而且Q因为开发这一函数的开发h员非常负责QQ所以他完成了正的工作q检查内存分配是否成功,如果不成功,则返回描q完整的错误状态?/p>
q就D了另外一些问题:(x)如果 GetWindowsDirectory 在失败时已经它的输出初始化为空字符Ԍ是否?x)有所帮助Q答案是否定的。结果不?x)是未初始化的,但它们仍是_心大意的应用程序所未曾料到的东ѝ假设?zhn)h一个由 cDriveLetter – 'A' ; 索引的数l,那么现在该烦引将H然变ؓ(f)负倹{?/p>
即ɾl端服务器对于?zhn)不是问题Q但同样的情况可能会(x)发生?Windows API 的Q何未来实C。?zhn)希望止正在开发的应用E序在将来版本的操作pȝ或替代实玎ͼ?Embedded NTQ中q行吗?一U良好的?fn)惯是记住以下事实?x)代码l常在其预期的到期日之后l箋生存?/p>
有时Q检查返回值是不够的。请考虑 Windows API ReadFile。?zhn)l常?x)看到如下代码?x)
LONG buffer[CHUNK_SIZE]; ReadFile(hFile, (LPVOID)buffer, CHUNK_SIZE*sizeof(LONG), &cbRead, NULL); if (buffer[0] == 0) // DOUBLY WRONG!!! ...
如果d操作p|Q说明缓冲区的内Ҏ(gu)未初始化的。多数情况下它可能ؓ(f)Ӟ但这一点ƈ不确定?/p>
d文gp|的原因有许多。例如,该文件可能是q程文gQ而网l可能发生故障。即使它是本地文Ӟ盘也可能恰好不合时宜地损坏。如果是q种情况Q则文g的格式可能完全不同于预期格式。他人可能无意中或别有用心地替换了?zhn)认?f)应该存在于某个位|的文gQ或者该文g可能只有一个字节。更为奇怪的事情已经发生了?/p>
要处理这一情况Q?zhn)不但需要检查读取操作是否成功,q必L查以保(zhn)已l读取了正确数量的字节?/p>
LONG buffer[CHUNK_SIZE]; BOOL ok; ok = ReadFile(hFile, (LPVOID)buffer, CHUNK_SIZE*sizeof(LONG), &cbRead, NULL); if (ok && cbRead > sizeof(LONG)) { if (buffer[0] == 0) ... } else // handle the read failure; for example ...
无庸讌Q上qC码有点儿复杂。但是,~写可靠的代码要比编写ƈ不L能够正常工作的代码更为复杂。对上述代码产生性能斚w的疑问是很正常的。虽然添加了几个试Q但在全局上下文(函数调用、磁盘操作,臛_复制 CHUNK_SIZE * sizeof(LONG) 个字节)中,其媄响是极小的?/p>
通常Q每当需要进行返回值检查时QL涉及(qing)C个函数调用,因此性能开销不太重要。在某些情况下,~译器可能会(x)内联该函敎ͼ但是如果发生q种行ؓ(f)Qƈ且由于返回常数而实际上不需要检查返回值时Q编译器?x)将试优化掉?/p>
诚然Q还是有一些特D情况:(x)(zhn)通过删除q回值检查而节省的数 CPU 循环臛_重要Q编译器无法为?zhn)提供帮助Q?zhn)控制了要调用的函数的行?f)。在上述情况下,省略一些返回值检查是有意义的。如果?zhn)认?f)自己处于cM的情形,则应该与其他开发h员进行讨论,重新审视真正的性能折衷Q然后,如果(zhn)仍然确信这样做是正的Q则在代码中每个省略q回值检查的地方加上明确的注释,说明(zhn)的军_q证明它的正性?/p>
无论是?span class=Apple-converted-space> HeapAlloc?strong>VirtualAlloc?strong>IMalloc::Alloc?strong>SysAllocString?strong>GlobalAlloc?strong>malloc q是M C++ q算W?newQ?zhn)都不能想当然地认为内存分配成功。同L(fng)道理也适用于其他各U资源,包括 GDI 对象、文件、注册表等{?/p>
下面是一个很好的独立于^台的错误代码CZQ这些代码是?C 标准库的基础上编写的Q?/p>
char *str; str = (char *)malloc(MAX_PATH); str[0] = 0; // WRONG!!! ...
在此例中Q如果内存耗尽Q则 malloc 返?NULLQ?str 的反引用是 NULL 指针的反引用。这?x)造成讉K冲突Q从而导致程序崩溃。除非?zhn)是在内核模式下运行(例如Q设备驱动程序)Q否则访问冲H将D蓝屏或可利用的安全漏z?/p>
解决Ҏ(gu)非常单。检查返回值是否ؓ(f) NULLQƈ执行正确的操作?/p>
char *str; str = (char *)malloc(MAX_PATH); if (str != NULL) { str[0] = 0; ... }
与返回值检查一P许多人相信实际上不会(x)发生内存分配问题。诚Ӟ该问题ƈ不L发生Q但qƈ不意味着它永q不?x)发生。如果?zhn)让成千上万(或数以百万)的用戯行(zhn)的YӞ即该问题每月仅Ҏ(gu)个用户发生一ơ,后果也是严重的?/p>
许多人相信,在内存耗尽时做什么都是无所谓的。程序应该退出。但是,q在许多斚w都不适用。首先,假设E序在内存耗尽旉出,那么不?x)保存数据文件。其ơ,Z通常期望服务和应用程序能够长期运行,因此它们能够在内存暂时不xl箋正常q行是至关重要的。第三,对于在嵌入式环境中运行的软g而言Q退Z是可行的选择。处理内存分配可能非帔R烦,但这件事情必d?/p>
有时Q意外的 NULL 指针可能不会(x)DE序崩溃Q但q仍然不是g好事情?/p>
HBITMAP hBitmap; HBITMAP hOldBitmap; hBitmap = CreateBitmap(. . .); hOldBitmap = SelectObject(hDC, hBitmap); // WRONG!!! ...
SelectObject 的文在?NULL 位图执行哪些操作斚w含糊不清。这可能不会(x)D崩溃Q但它显然是不可靠的。代码很可能Z某种原因而创Z图,q希望用它来q行一些绘囑ַ作。但是,因ؓ(f)它未能创Z图,所以绘图操作将不会(x)发生。即使代码没有崩溃,q里也明昑֭在一个错误。同P(zhn)需要进行检查?/p>
HBITMAP hBitmap; HBITMAP hOldBitmap; hBitmap = CreateBitmap(. . .); if (hBitmap != NULL) { hOldBitmap = SelectObject(hDC, hBitmap); ... } else ...
当?zhn)使?C++ q算W?new Ӟ事情开始变得更加有。例如,如果(zhn)要使用 MFCQ则全局q算W?new 在内存耗尽时引发一个异常。这意味着(zhn)不能执行以下操作:(x)
int *ptr1; int *ptr2; ptr1 = new int[10]; ptr2 = new int[10]; // WRONG!!!!
如果W二ơ内存分配引发异常,则第一ơ分配的内存泄漏。如果?zhn)的代码嵌入到要长期q行的服务或应用E序中,则这些泄漏会(x)累积h?/p>
只是捕捉异常是不够的Q?zhn)的异常处理代码还必须是正的。不要掉C面这个诱人的陷阱中:(x)
int *ptr1; int *ptr2; try { ptr1 = new int[10]; ptr2 = new int[10]; } catch (CMemoryException *ex) { delete [] ptr1; // WRONG!!! delete [] ptr2; // WRONG!!! }
如果W一ơ内存分配引发了异常Q?zhn)捕捉该异常Q但要删除一个未初始化的指针。如果?zhn)_q运Q这导致即时访问冲H和崩溃。更有可能的是,它将D堆损坏,从而造成数据损坏?或在来难以调试的崩溃。尽力初始化下列变量是值得的:(x)
int *ptr1 = 0; int *ptr2 = 0; try { ptr1 = new int[10]; ptr2 = new int[10]; } catch (CMemoryException *ex) { delete [] ptr1; delete [] ptr2; }
应该指出的是QC++ q算W?new 有许多微妙之处。?zhn)可以用多U不同的方式来修改全局q算W?new 的行为。不同的cd以具有它们自qq算W?newQƈ且如果?zhn)不?MFCQ则可能?x)看C同的默认行ؓ(f)。例如,在内存分配失败时q回 NULLQ而不是引发异常。有兌主题的详l信息,请参?Bobby Schmidt ?#8220;Deep C++”pd文章中有兛_理异常的W?7 部分?/p>
如果(zhn)要~写可靠的代码,则至关重要的一Ҏ(gu)从一开始就考虑如何处理异常事g。?zhn)不能事后再考虑对异怺件的处理。在考虑此类事gӞ错误处理是一个关键的斚w?/p>
错误处理很难正确执行。尽本文只是粗地讨论了这一问题Q但其中介绍的原则奠定了一个强大的基础。请C以下要点Q?/p>
在设计应用程序(?APIQ时Q应预先考虑(zhn)喜Ƣ的错误处理范例?/p>
在?API Ӟ应了解它的错误处理范例?/p>
如果(zhn)处于存在多个错误处理范例的情况下,误惕可能造成混ؕ的根源?/p>
L查返回状态?/p>
L查内存分配?/p>
如果(zhn)执行了上述所有操作,p够编写出可靠的应用程序?/span>
关于C++中异常的争论何其多也Q但往往是一些不合事实的误解。异常曾l是一个难以用好的语言Ҏ(gu),q运的是Q随着C++Cl验的积累,今天我们已经有够的知识L~写异常安全的代码了Q而且~写异常安全的代码一般也不会(x)Ҏ(gu)能造成影响?span lang=EN-US>
使用异常q是q回错误码?q是个争Z休的话题。大家一定听说过q样的说法:(x)只有在真正异常的时候,才用异常。那什么是“真正异常的时?span lang=EN-US>”Q在回答q个问题以前Q让我们先看一看程序设计中的不变式原理?span lang=EN-US>
对象是属性聚合加Ҏ(gu)Q如何判定一个对象的属性聚合是不是处于逻辑上正的状态呢Q这可以通过一pd的断aQ最后下一个结Q这个对象的属性聚合逻辑上是正确的或者是有问题的。这些断a是衡量对象属性聚合对错的不变式?span lang=EN-US>
我们通常在函数调用中Q实施不变式的检查。不变式分ؓ(f)三类Q前条gQ后条g和不变式。前条g是指在函数调用之前,必须满的逻辑条gQ后条g是函数调用后必须满的逻辑条gQ不变式则是整个函数执行中都必须满的条件。在我们的讨ZQ不变式既是前条件又是后条g。前条g是必L的Q如果不满Q那是E序逻辑错误Q后条g则不一定。现在,我们可以用不变式来严格定义异常状况了Q满_条gQ但是无法满_条gQ即为异常状c(din)当且仅当发生异常状冉|Q才抛出异常?span lang=EN-US>
关于何时抛出异常的回{中Qƈ不排斥返回值报告错误,而且q两者是正交的。然而,从我们经验上来说Q完全可以在q两者中加以选择Q这又是Z么呢Q事实上Q当我们做出q种选择Ӟ必然意味着接口语意的改变,在不改变接口的情况下Q其实是无法选择?span lang=EN-US>(试试看,用返回值处理构造函C的错?span lang=EN-US>)。通过不变式区别出正常和异常状况,q可以更好地提炼接口?span lang=EN-US>
对于异常安全的评定,可分Z个别:(x)基本保证、强保证和不?x)失败?span lang=EN-US>
基本保证Q确保出现异常时E序Q对象)处于未知但有效的状态。所谓有效,卛_象的不变式检查全部通过?span lang=EN-US>
Z证:(x)保操作的事务性,要么成功Q程序处于目标状态,要么不发生改变?span lang=EN-US>
不会(x)p|Q对于大多数函数来说Q这是很难保证的。对?span lang=EN-US>C++E序Q至析构函数、释攑և数和swap函数要确保不?x)失败,q是~写异常安全代码的基?span lang=EN-US>
首先从异常情况下资源理的问题开?span lang=EN-US>.很多人可能都q么q过:
Type* obj = new Type;
try{ do_something...}
catch(...){ delete obj; throw;}
不要q么?span lang=EN-US>!q么做只?x)你的代码看上L?span lang=EN-US>,而且?x)降低效?span lang=EN-US>,q也是一直以来异常名C大好的原因之一. 请借助?span lang=EN-US>RAII技术来完成q样的工?span lang=EN-US>:
auto_ptr<Type> obj_ptr(new Type);
do_something...
q样的代码简z、安全而且无损于效率。当你不兛_或是无法处理异常?span lang=EN-US>,请不要试图捕获它。ƈ非?span lang=EN-US>try...catch才能~写异常安全的代?span lang=EN-US>,大部分异常安全的代码都不需?span lang=EN-US>try...catch。我承认Q现实世界ƈ非L如上q的例子那样单,但是q个例子实可以代表很多异常安全代码的做法。在q个例子中,boost::scoped_ptr?span lang=EN-US>auto_ptr一个更适合的替代品?span lang=EN-US>
现在来考虑q样一个构造函敎ͼ(x)
Type() : m_a(new TypeA), m_b(new TypeB){}
假设成员变量m_a?span lang=EN-US>m_b是原始的指针cdQƈ且和Type内的x序一致。这L(fng)代码是不安全的,它存在资源泄漏问题,构造函数的p|回滚机制无法应对q样的问题。如?span lang=EN-US>new TypeB抛出异常,new TypeAq回的资源是得不到释放机?x)?span lang=EN-US>.曄,很多人用q样的方法避免异?span lang=EN-US>:
Type() : m_a(NULL), m_b(NULL){
auto_ptr<TypeA> tmp_a(new TypeA);
auto_ptr<TypeB> tmp_b(new TypeB);
m_a = tmp_a.release();
m_b = tmp_b.release();
}
当然,q样的方法确实是能够实现异常安全的代码的,而且其中实现思想是非常重要?span lang=EN-US>,在如何实现强保证的异常安全代码中?x)采用这U思想.然而这U做法不够彻底,臛_析构函数q是要手动完成的。我们仍然可以借助RAII技术,把这件事做得更ؓ(f)dQ?span lang=EN-US>shared_ptr<TypeA> m_a; shared_ptr<TypeB> m_b;q样,我们可以轻而易丑֜写出异常安全的代?span lang=EN-US>:
Type() : m_a(new TypeA), m_b(new TypeB){}
如果你觉?span lang=EN-US>shared_ptr的性能不能满要求Q可以编写一个接口类?span lang=EN-US>scoped_ptr的智能指针类,在析构函C释放资源卛_。如果类设计成不可复制的Q也可以直接?span lang=EN-US>scoped_ptr。强烈徏议不要把auto_ptr作ؓ(f)数据成员使用,scoped_ptr虽然名字不大好,但是臛_很安全而且不会(x)D混ؕ?span lang=EN-US>
RAII技术ƈ不仅仅用于上qC子中Q所有必L对出现的操作都可以通过q一技术完成而不?span lang=EN-US>try...catch.下面的代码也是常见的Q?span lang=EN-US>
a_lock.lock();
try{ ...} catch(...) {a_lock.unlock();throw;}
a_lock.unlock();
可以q样解决Q先提供一个成Ҏ(gu)作的辅助c:(x)
struct scoped_lock{
explicit scoped_lock(Lock& lock) : m_l(lock){m_l.lock();}
~scoped_lock(){m_l.unlock();}
private:
Lock& m_l;
};
然后Q代码只需q样写:(x)
scoped_lock guard(a_lock);
do_something...
清晰而优雅!l箋考察q个例子,假设我们q不需要成Ҏ(gu)?span lang=EN-US>, 昄Q修?span lang=EN-US>scoped_lock构造函数即可解决问题。然而,往往Ҏ(gu)名称和参C不是那么固定的,怎么办?可以借助q样一个辅助类Q?span lang=EN-US>
template<typename FEnd, typename FBegin>
struct pair_guard{
pair_guard(FEnd fe, FBegin fb) : m_fe(fe) {if (fb) fb();}
~pair_guard(){m_fe();}
private:
FEnd m_fe;
...//止复制
};
typedef pair_guard<function<void () > , function<void()> > simple_pair_guard;
好了Q借助boost?span lang=EN-US>,我们可以q样来编写代码了Q?span lang=EN-US>
simple_pair_guard guard(bind(&Lock::unlock, a_lock), bind(&Lock::lock, a_lock) );
do_something...
我承认,q样的代码不如前面的z和Ҏ(gu)理解Q但是它更灵z?span lang=EN-US>,无论函数名称是什么,都可以拿来结寏V我们可以加强对bind的运用,l合占位W和reference_wrapper,可以处理函数参数、动态绑定变量。所有我们在catch内外的相同工作,交给pair_guardd成即可?span lang=EN-US>
考察前面的几个例子,也许你已l发CQ所谓异常安全的代码Q竟然就是如何避?span lang=EN-US>try...catch的代码,q和直觉g是违背的。有些时候,事情是如此q背直觉。异常是无处不在的,当你不需要关心异常或者无法处理异常的时候,应该避免捕获异常。除非你打算捕获所有异常,否则Q请务必把未处理的异常再ơ抛出?span lang=EN-US>try...catch的方式固然能够写出异常安全的代码Q但是那L(fng)代码无论是清晰性和效率都是难以忍受的,而这正是很多人抨?span lang=EN-US>C++异常的理由。在C++的世界,应该按?span lang=EN-US>C++的法则来行事?span lang=EN-US>
如果按照上述的原则行事,能够实现基本保证了吗Q诚恛__(d)基础设施有了Q但技巧上q不够,让我们l分析不够的部分?span lang=EN-US>
对于一个方法常规的执行q程Q我们在Ҏ(gu)内部可能需要多ơ修改对象状态,在方法执行的中途,对象是可能处于非法状态的Q非法状?span lang=EN-US> != 未知状态)Q如果此时发生异常,对象变得无效。利用前q的手段Q在pair_guard的析构中修复对象是可行的Q但~Z效率Q代码将变得复杂。最好的办法?span lang=EN-US>......是避免这么作Q这么说有点不厚道,但ƈ非毫无道理。当对象处于非法状态时Q意味着此时此刻对象不能安全重入、不能共享。现实一点的做法是:(x)
a.每一ơ修改对象,都确保对象处于合法状?span lang=EN-US>
b.或者当对象处于非法状态时Q所有操作决不会(x)p|?span lang=EN-US>
在接下来的强保证的讨Zl述如何做到q两炏V?span lang=EN-US>
Z证是事务性的Q这个事务性和数据库的事务性有区别Q也有共通性。实现强保证的原则做法是Q在可能p|的过E中计算出对象的目标状态,但是不修改对象,在决不失败的q程中,把对象替换到目标状态。考察一个不安全的字W串赋值方法:(x)
string& operator=(const string& rsh){
if (this != &rsh){
myalloc locked_pool(m_data);
locked_pool.deallocate(m_data);
if (rsh.empty())
m_data = NULL;
else{
m_data = locked_pool.allocate(rsh.size() + 1);
never_failed_copy(m_data, rsh.m_data, rsh.size() + 1);
}
}
return *this;
}
locked_pool是ؓ(f)了锁定内存页。ؓ(f)了讨论的单v见,我们假设只有locked_pool构造函数和allocate是可能抛出异常的Q那么这D代码连基本保证也没有做到。若allocatep|Q则m_data取值将是非法的。参考上面的b条目Q我们可以这样修改代码:(x)
myalloc locked_pool(m_data);
locked_pool.deallocate(m_data); //q入非法状?span lang=EN-US>
m_data = NULL; //立刻再次回到合法状?span lang=EN-US>,且不?x)失?span lang=EN-US>
if(!rsh.empty()){
m_data = locked_pool.allocate(rsh.size() + 1);
never_failed_memcopy(m_data, rsh.m_data, rsh.size() + 1);
}
现在Q如?span lang=EN-US>locked_poolp|Q对象不发生改变。如?span lang=EN-US>allocatep|Q对象是一个空字符Ԍq既不是初始状态,也不是我们预期的目标状态,但它是一个合法状态。我们阐明了实现基本保证所需要的技巧部分,l合前述的基设施Q?span lang=EN-US>RAII的运用)Q完全可以实现基本保证了...?span lang=EN-US>,其实q是有一点疏漏,不过Q那q到最后吧?span lang=EN-US>
l箋Q让上面的代码实现强保证Q?span lang=EN-US>
myalloc locked_pool(m_data);
char* tmp Q?span lang=EN-US> NULL;
if(!rsh.empty()){
tmp = locked_pool.allocate(rsh.size() + 1);
never_failed_memcopy(tmp, rsh.m_data, rsh.size() + 1); //先生成目标状?span lang=EN-US>
}
swap(tmp, m_data); //对象安全q入目标状?span lang=EN-US>
m_alloc.deallocate(tmp); //释放原有资源
Z证的代码多用了一个局部变?span lang=EN-US>tmpQ先计算出目标状态放?span lang=EN-US>tmp中,然后在安全进入目标状态,q个q程我们q没有损׃么东西(代码清晰性,性能{等Q。看上去Q实现强保证q不比基本保证困隑֤,一般而言Q也实如此。不q,别太自信QD一U典型的很难实现Z证的例子Q对于区间操作的Z证:(x)
for (itr = range.begin(); itr != range.end(); ++itr){
itr->do_something();
}
如果某个do_somethingp|了,range处于什么状态?q段代码仍然做到了基本保证,但不是强保证的,Ҏ(gu)实现Z证的基本原则Q我们可以这么做Q?span lang=EN-US>
tmp = range;
for (itr = tmp.begin(); itr != tmp.end(); ++itr){
itr->do_something();
}
swap(tmp, range);
g很简单啊Q呵呵,q样的做法ƈ非不可取Q只是有时候行不通。因为我们额外付Z性能的代P而且Q这个代价可能很大。无论如何,我们阐述了实现强保证的方法,怎么取舍则由(zhn)决定了?span lang=EN-US>
接下来讨论最后一U异常安全保证:(x)不会(x)p|?span lang=EN-US>
通常Q我们ƈ不需要这么强的安全保证,但是我们臛_必须保证三类q程不会(x)p|Q析构函敎ͼ释放cd敎ͼswap。析构和释放函数不会(x)p|Q这?span lang=EN-US>RAII技术有效的基石Q?span lang=EN-US>swap不会(x)p|Q是Z“在决不失败的q程中,把对象替换到目标状?span lang=EN-US>”。我们前面的所有讨论都是徏立在q三c过E不?x)失败的基础上的Q在q里QI补了上面的那个疏漏?span lang=EN-US>
一般而言Q语a内部cd的赋倹{取地址{运是不会(x)发生异常的,上述三类q程逻辑上也是不?x)发生异常的。内部运中Q除法运可能抛出异常。但是地址讉K错通常是一U错误,而不是异常,我们本应该在前条件检查中发现的q一点的。所有不?x)发生异常操作的单篏加,仍然不会(x)D异常?span lang=EN-US>
好了Q现在我们可以ȝ一下编写异常安全代码的几条准则了:(x)
1.只在应该使用异常的地Ҏ(gu)出异?span lang=EN-US>
2.如果不知道如何处理异常,请不要捕?span lang=EN-US>(截留)异常?span lang=EN-US>
3.充分使用RAIIQ旁路异常?span lang=EN-US>
4.努力实现Z证,臛_实现基本保证?span lang=EN-US>
5.保析构函数、释攄函数?span lang=EN-US>swap不会(x)p|?span lang=EN-US>
另外Q还有一些语al节问题Q因为和q个主题有关也一q列出:(x)
1.不要q样抛出异常Q?span lang=EN-US>throw new exception;q将D内存泄漏?span lang=EN-US>
2.自定义类型,应该捕获异常的引用类型:(x)catch(exception& e)?span lang=EN-US>catch(const exception& e)?span lang=EN-US>
3.不要使用异常规范Q即使是I异常规范。编译器q不保证只抛出异常规范允许的异常Q更多内容请参考相关书c?/p>
刚刚发表了《什么是契约》一文,H然发现自己通篇都在写理论,没有实例来证明。所以赶快补充一个反面案例——C++ IOStream。说是反面,不是因ؓ(f)IOStream库设计得不精彩(恰恰相反Q你很难扑ֈ比IOStream设计更ؓ(f)_ֽ的C++库了Q,而是惛_CZ下,在没有契U概늚思想体系里,lg设计ؓ(f)权责不清的错误处理付出多大的代h(hun)?/p>
大家知道QC++ IOStream库非常经典,最先v源于Bjarne Stroustrup的Stream库,之后l过Jerry Schwartz、Martin Carroll、Andy Koenig{h的改q,成ؓ(f)IOStream库,q被q入Bell实验室发行的USD C++库中Q广Z播。后来USD库逐渐消亡了,而IOStream׃获得q泛应用Q得以幸免,q以新的形式被置于标准库中?/p>
对于错误处理Q当IOStream库诞生的时候(大约1985Q?987Q,C++q没有异常机制。因此,Jerry Schwartz发明了这样一套错误处理机Ӟ(x)
?Q经典的IOStream错误处理Q?/p>
ifstream ifs("filename.txt", ios::in);
if (!ifs) { // q里实施了向void*转型的操?br> // 文g打开p|Q实施错误处?br> }
先测试文件是否打开Q再实施具体操作Q这是经典IOStream库的一个惯用法Qidiom)?/p>
我们现在设想用户没有很好地执行这个idiomQ?/p>
int val;
ifstream ifs("filename.txt", ios::in);
ifs >> val;
如果filename.txt打开p|Q会(x)发生什么情况?
如果哪位q有当年的Borland C++ 3.1Q可以试着试一下。我估计是什么也不发生,或者说Q程序处于极端危险的“undefined behavior”状态?/p>
q种情况对C++库开发者来说是不能接受的。因此,管问题的出现是׃用户的错误(他们没有正确地测试流状态)Q但是由于非契约思想体系下的权责不清QIOStream库的开发者开始追求以应对用户错误的lg开发技术。由此,IOStream开始在一个方向上被拖入了复杂性的泥潭之中?/p>
我们看看标准库中的对付这U情况采用什么办法。标准IOStream有一个成员函数叫做exceptions()Q专门用来帮助程序员切换异常模式。缺省情况下Q异常触发ƈ没有打开Q所以情形跟l典IOStream库相同。如果你在操作IOStream之前如下调用Q?/p>
strm.exceptions(std::ios::eofbit | std::ios::failbit |
std::ios::badbit);
则当不处于good状态时Q执行类?strm >> valQ这L(fng)操作Ӟ会(x)抛出异常?/p>
q样做看h不错Q是吗?
我觉得不是。请恕我C++标准的异议,q是我第一ơ正式对C++标准中的设计提出异议?/p>
q种设计带来的缺点,首先是复杂。在Nicolai Josuttis的The C++ Standard Library中,对这个机制整整用?늺来解释,q意Ҏ(gu)。复杂的设计必然带来复杂的用规则,而面对复杂的使用规则Q用h可以投票的,那就是你做你的,我不用!读这帖子的人,谁在实际目中用过exceptions()Q事实上Q我个h是害怕exception甚于xundefined behavior?/p>
而对于用h_(d)你可以不用,却不得不为对它付行性能和空间的代h(hun)。诸位有兴趣Q不妨追t一个IOStream功能的实玎ͼ看看Z支持q个异常QIOStream库的设计q耗费了多大的心力Q而你的CPU又ؓ(f)此耗费了多clock?/p>
~点之二Q是异常本n的问题——即使你抓到了异常,又当如何Q程序可能已l完全离开了发生异常时的执行环境,也许你连异常Z么发生都搞不清楚Q谈何处理?也无非就是通知用户一壎ͼ(x)“我完了,因ؓ(f)一个异常发生在XXXXXXXX处,你要报告的话l我发Email吧?#8221; 是啊Q你q能做什么呢Q?/p>
我们试着用契U观Ҏ(gu)分析q一状况Q如果说“先测试,再?#8221;在传l上是一个idiomQ那么在contract思想里上升ؓ(f)一个契U。对于C++来说Q应该将“处于good状?#8221;作ؓ(f)一个契U,在每一个成员函数里q行查。甚至你q可以设|一个调试期标志Q专门用来核查用h否检查过状态。在必要的操作进行之前,你可以先用断a查用h否检查过状态,满了契U。这样一来,在契U之下,用户被q以正确的方式用组Ӟ从而大q度化组件开发的复杂度?/p>
再来考虑异常。如果真正发生了异常Q在Eiffel中提供了retry机制。Bjarne Stroustrup说过Qretry可以做到Q但是往往没有意义。ؓ(f)什么没有意义呢Q因为C++中没有契U的思想Q异常的产生可能Ҏ(gu)是E序员的bug。在q种情况下,无论retry多少ơ,l果都是一L(fng)p。可是在Eiffel里情形不同。如果各斚w对于契约都做到很好的遵@Q那么真正发生异常的时候,我们大可以比较有把握的说Q这可能是一个很偶然的事件导致的。比如说|络环境下,另一个用户在那一瞬间H然Ҏ(gu)件实施了一个操作,或者硬件的一ơ偶然异常。对于这U情况,“再试一?#8221;成了合情合理的选择。我们很可能异常扼杀在摇之中,从而不l上层模块带来Q何媄响?/p>
谁说契约思想不伟大呢Q?/p>
注:(x)本文转自孟岩的博?http://blog.csdn.net/myan)
假设你现在正在面试,主考不紧不慢地l出下一道题目:(x)“L(fng)C语言写一个类似strcpy的函数。要考虑可能发生的异常情c(din)?#8221; 你会(x)怎么做呢Q很明显Q对方不是在考察你的~程能力Q因为复制字W串实在太容易了。对Ҏ(gu)在考察你的~程风格Q习(fn)惯)Q或者说Q要看看你编码的质量?/p>
下面是多U可能的做法Q?/p>
void
string_copy1(char* dest, const char* source)
{
assert(dest != NULL); /* 使用断言 */
assert(source != NULL);
while (*source != ‘\0′) {
*dest = *source;
++dest;
++source;
}
*dest = ‘\0′;
}
void
string_copy2(char* dest, const char* source)
{
if (dest != NULL && source != NULL) { /* 寚w误消极静?*/
while (*source != ‘\0′) {
*dest = *source;
++dest;
++source;
}
*dest = ‘\0′;
}
}
int
string_copy3(char* dest, const char* source)
{
if (dest != NULL && source != NULL) {
while (*source != ‘\0′) {
*dest = *source;
++dest;
++source;
}
*dest = ‘\0′;
return SUCCESS; /* q回表示正确的?*/
}
else {
errno = E_INVALIDARG; /* 讑֮错误?*/
return FAILED; /* q回表示错误的?*/
}
}
// C++
void
string_copy4(char* dest, const char* source)
{
if (dest == NULL || source == NULL)
throw Invalid_Argument_Error(); /* 抛出异常 */
while (*source != ‘\0′) {
*dest = *source;
++dest;
++source;
}
*dest = ‘\0′;
}
如果你是主考,不知道面对这样四份答P你的评分如何Q当Ӟ你可以心里揣着一?#8220;标准{案”Q?#8220;我者昌Q逆我者亡”。但是如果以认真的态度面对q四份答P我想很多人都?x)难以抉择?/p>
因ؓ(f)q里涉及(qing)C软g开发中的一个带有本质性的N——错误处理?/p>
历来错误处理一直是软g开发者所面(f)的最大困难之一。Bjarne Stroustrup在谈到其原因时说道,能够探察错误的一方不知道如何处理错误Q知道如何处理错误的一Ҏ(gu)有能力探察错误,而直接采用防御性代码来?冻I?x)得程序的正常l构被打乱,从而带来更多的错误。这U困境是非常难以应对的——费心耗力而未必有回报。因此,更多的h采用鸵鸟战术Q对可能发生的错 误视而不见,d自然?/p>
C++、Java和其他语a寚w误处理问题的回答是异常机制。这U机制在正常的程序执行流之外开辟了专门的信道,专门用来在不同程序模块之间报告错误,?决上q错误探察与处理{略分散的矛盾。然而,有了异常处理机制后,开发者开始有一U們Q就是用异常来处理所有的错误。我曄p个问题在 comp.lang.c++.moderated上展开讨论Q结果是发现有相当多的hQ包括Boost开发组里的很多专家Q都认ؓ(f)异常是错误处理的通用?x案?/p>
Ҏ(gu)我不能赞同。ƈ且我认ؓ(f)滥用异常比不用异常的危害更大?/p>
The Pragmatic Programmer是一本在国外E序员中间颇为流行的书,其中在讲到错误处理时Q有一句箴aQ?br>“只在真正异常的状况下使用异常?#8221;
书中举了一个例子,如果你需要当前目录下的一个名?#8220;app.dat”的文Ӟ而这个文件不存在Q那么这不叫异常状况Q这是你应该预料到的、ƈ且显式处?的情c(din)而如果你要到Windows目录下寻找user.dat文gQ却没找刎ͼ那才叫做异常状况——因为每一个正常运行的Windowspȝ都应该有q?个文件?/p>
我非常赞成书中的那句忠告Q可是究竟什么是“真正异常”的状况?书中的这个例子显然只是一个颇h性的、寓a似的故事Q具有所有寓a的共同特点——读h觉得豁然开朗,收获很大Q实际上帮不了你什么忙。这U例子对于我们的实际开发,仍然提供不了真正的帮助?/p>
I竟应该如何看待错误Q怎样才能最好地错误处理Q?/p>
说实话,在这两个问题上,我们所见到的大部分语言都没有给出很好的回答。CU承一贯风|把所有的东西推给开发者考虑QAda发明了异常,但是又ؓ(f)异常所 累(知道阉K亚纳5火箭的处奌Z么失败吗Q)QC++企图Ada的异常机制融合进自己的体pMQ结果异常成了C++中最难以处理的东西;Java?C#昄都没有耐心重新考虑错误处理q桩事,而只是简单的C++的异常机制完善化了事?/p>
与上q这些语a不同QEiffel从一开始就把错误处理放在核心的位置上予以考虑Qƈ?#8220;契约”思想为核心,建立了整个的错误处理思想体系。在我了解的?a里,Eiffel是对q个问题思考最为深M个,因此QEiffel历来享有“高质量系l开发语a”的声誉。(事实上,Bertrand Meyer很不喜欢别hUEiffel?#8220;~程语言”Q他反复QEiffel是一个Software Development Framework。不q本文只涉及(qing)语言Ҏ(gu),所以姑且称Eiffel语言。)
Eiffel把Y仉误生的本质归结?#8220;契约”的破坏。Eiffel认ؓ(f)Q一个Y件能够正常运作,正确完成dQ是需要一pd条g的。这些条件包括客?q行环境良好Q操作者操作正,软g内部功能正确{等。因此,软g的正运行,是环境、操作者与软g本n三方面合作的l果。相应的Q系l的错误Q也是由?三者中有一Ҏ(gu)有正行自q职责而导致的。细化到软g内部Q每个Y仉是由若干不同的模块组成的QY件的错误Q是׃某些模块没有正确履行自己的职 责。要d杜绝软g错误Q只有分清各自模块的责QQƈ且徏立机Ӟ敦促各模块正行自q责QQ然后才有可能做到Bug-free。(鉴于pȝ中错l复 杂的关系Q以?qing)开发者认识能力的局限,我认为真正无错误的系l是不可能的。但是当前一般Y件系l中的质量问题远q比应有的严重。)
如何保证各方恪守职责呢?Eiffel引入了契U(ContractQ这个概c(din)这里的契约与我们通常所说的商业契约很相|有以下几个特点:(x)
1. 契约关系的双Ҏ(gu)q等的,Ҏ(gu)个bussiness的顺利进行负有共同责任,没有哪一方可以只享有权利而不承担义务?br>2. 契约关系l常是相互的Q权利和义务之间往往是互相捆l在一L(fng)Q?br>3. 执行契约的义务在我,而核查契U的权力在hQ?br>4. 我的义务保障的是你的利益Q而你的义务保障的是我的利益;
契U关pd入到软g开发领域,其是面向对象领域之后,在观念上l我们带来了几大冲击Q?/p>
1. 一般的观点Q在软g体系中,E序库和lg库被cL为serverQ而用程序库、组件库的程序被视ؓ(f)client。根据这UC/S关系Q我们往往对库E序 和组件的质量提出很严苛的要求Q强q它们承担本不应该由它们来承担的责QQ而过分纵容client一方,甚至要求库程序去处理明显׃client错误?成的困境。客观上DE序库和lg库的设计和编写异常困难,而且质量隐?zhn)反而更多;同时client一方代码大多松散随意,质量低劣。这U情形,好像在 一个权责不清的企业里,必然?x)养一批尸位素的hQ苦一批Q劳Q怨,不计得失的老黄牛。引入契U观念之后,q种C/S关系被打_(d)大家都是q等的,你需 要我正确提供服务Q那么你必须满我提出的条gQ否则我没有义务“排除万难”C证完成Q务?/p>
2. 一般认为在模块中检查错误状况ƈ且上报,是模块本w的义务。而在契约体制下,对于契约的检查ƈ非义务,实际上是在行权利。一个义务,一个权利,差别极大。例如上面的代码Q?br>if (dest == NULL) { … }
q就是义务,其要点在于,一旦条件不满Q我方(义务方)必须负责以合适手法处理这尬局面,或者返回错误|或者抛出异常。而:(x)
assert(dest != NULL);
q是查契U,履行权利。如果条件不满Q那么错误在Ҏ(gu)而不在我Q我可以立刻“撕毁合同”QŞ工了事,无需做Q何多余动作。这无疑可以大大化程序库和组件库的开发?/p>
3. 契约所核查的,?#8220;Z证正性所必须满的条?#8221;Q因此,当契U被破坏Ӟ只表明一件事QY件系l中有bug。其意义是说Q某些条件在到达我这里时Q必 dl确保ؓ(f)“?#8221;。谁来确保?应该是系l中的其他模块在先期保。如果在我这里发现契U没有被遵守Q那么表明系l中其他模块没有正确履行自己的义务。就 拿上面提到的“打开文g”的例子来_(d)如果有一个模块需要一个FILE*Q而在契约查中发现该指针ؓ(f)NULLQ则意味着有一个模块没有行其义务Q即 “查文件是否存在,保文g以正模式打开Qƈ且保证指针的正确?#8221;。因此,当契U检查失败时Q我们首先要知道q意味着E序员错误,而且要做的不是纠?契约核查方,而是U正契约提供斏V换句话_(d)当你发现:
assert(dest != NULL);
报错Ӟ你要做的不是M改你的string_copy函数Q而是要让M代码在调用string_copy时确保dest指针不ؓ(f)I?/p>
4. 我们以往对待“q程”?#8220;函数”的理解是Q完成某个计Q务的q程Q这一看法只强调了其目标,没有其条件。在q种理解下,我们对于exception 的理解非常模p和宽泛Q只要是无法完成q个计算q程Q均可被视ؓ(f)异常Q也不管是我自己的原因,q是其他人的原因Q典型的权责不清Q。正是因U模p和?泛,“I竟什么时候应该抛出异?#8221;成ؓ(f)没有回答的问题。而引入契U之后,“q程”?#8220;函数”被定义ؓ(f)Q完成契U的q程。基于契U的怺性,如果q个?U的p|是因为其他模块未能行契U,本过E只需报告Q无需以Q何其他方式做出反应。而真正的异常状况?#8220;Ҏ(gu)完全满了契U,而我依然未能如约完成??#8221;的情形。这样以来,我们q“异常”下了一个清晰、可行的定义?/p>
5. 一般来_(d)在面向对象技术中Q我们认?#8220;接口”是唯一重要的东西,接口定义了组Ӟ接口定了系l,接口是面向对象中我们唯一需要关心的东西Q接口不仅是 必要的,而且是充分的。然而,契约观念提醒我们Q仅仅有接口q不充分Q仅仅通过接口q不以传达_的信息,Z正确使用接口Q必考虑契约。只有考虑?U,才可能实现面向对象的目标Q可靠性、可扩展性和可复用性。反q来Q?#8220;没有契约的复用根本就是瞎胡闹。(Bertrand Meyer语)”?/p>
׃q观点可以看出,虽然Eiffel所倡导的Design By Contract在表象上不过是系l化的断aQassertionQ机Ӟ然而在背后Q确实是完全的思想革新。正如Ivar Jacoboson访华时对《程序员》杂志所_(d)(x)“我认为Bertrand Meyer的方向——Design by Contract——是正确的方向,我们都会(x)沿着他的前进。我怿Q大型厂商(微Y、IBMQ当然还有RationalQ都不会(x)对Bertrand Meyer的成坐视不理。所有这些厂商都?x)在q个方向上有所行动?#8221;
@echo off
setlocal
set REPOS=%1
set TXN=%2
rem check that logmessage contains at least 5 characters
svnlook log "%REPOS%" -t "%TXN%" | findstr ".........." > nul
if %errorlevel% gtr 0 goto err
exit 0
:err
echo Empty log message not allowed. Commit aborted! 1>&2
exit 1
REM SVN pre-revprop-change hook allows edit of logmessages from TSVN
setlocal
set REPOS=%1
set REV=%2
set USER=%3
set PROPNAME=%4
set ACTION=%5
if not "%ACTION%"=="M" goto refuse
if not "%PROPNAME%"=="svn:log" goto refuse
goto OK
:refuse
echo Cann't set %PROPNAME%/%ACTION%, only svn:log is allowed 1>&2
endlocal
exit 1
:OK
endlocal
exit 0
VC开发环境之所以提供远E调试的能力Q是因ؓ(f)有些情况下单试会(x)让你崩溃掉。。。比如,调试GUIE序的WM_PAINT消息Q因单步调试Q所以调试器?x)对界面的重l生副作用QHeisenberg不确定性原理)。当然还有些别的情况也适用Q比如程序在试环境q行的好好的Q但是在客户那行为L异常Q这时候如果可以TCPq程q接上去l护的话Q就能通过q程调试的特性在出现状况的系l环境中排错~
下面来说一下具体的做法。先明确下概念,q程调试嘛,自然是两个机器之间调试。程序运行在目标机器上,调试器运行在本机。当Ӟ目标机器上还是要有少许辅助程序才能跟本机的调试器connect上,以便通讯。一般来_(d)只需要copy四个文g到目标机器上p了:(x)MSVCMON.EXE、DM.DLL、TLN0T.DLL和MSDIS110.DLL。这四个文g都能在VC6目录的Common\MSDEV98\Bin目录下面扑ֈ。copyq去之后Q运行msvcom.exeQ看下图片~
有个Settings的按钮,不用。直接点Connectp了~
接着看看本机q边调试器的讄。首先设|好q程调试开养I在Build菜单下有个Debuger Remote Connecting的子菜单Q点之。出CH口Q默认是在Local,我们要选的是Network(TCP/IP)Q然后点讑֮。会(x)弹出一个对话框Q输入目标机器的ip或者机器名Q最后点OKp了?/p>
接下来把工程打开Q设|最后一步。假讄成的可执行程序名为RemoteDebug.exeQ在目标机器上的路径为d:\Prj\Remote.exeQ那么,在本机的Project Settings里面Q选择Debug面的Remote executable path and file name下面的编辑框中输入目标机器中E序的\径:(x)d:\Prj\RemoteDebug.exe。注意,q里写的是从目标机器的角度所看到的\径?/p>
然后~译一下程序,把新~译出来的RemoteDebug.exe复制到目标机器的d:\Prj下面Q就可以在本机像q_一栯试了?/p>
#define _CRTDBG_MAP_ALLOC #include<stdlib.h> #include<crtdbg.h> #include "debug_new.h"
MSDN 如是_(d)(x)“必须保证上面声明的顺序,如果改变了顺序,可能不能正常工作?#8221;至于q是Z么,我们不得而知。MS 的老大们经常这h弄玄虚?br>针对?MFC E序Q再加上周星星的头文Ӟ(x)debug_new.hQ当然如果不加这一句,也能出内存泄漏Q但是你无法定在哪个源E序文g中发生泄漏。Output 输出只告诉你?crtsdb.h 中的某个地方有内存泄漏。我试?REG_DEBUG_NEW 没有起作用。加不加q个宏都可以出发生内存分配泄漏的文件?br>其次Q一旦添加了上面的声明,你就可以通过在程序中加入下面的代码来报告内存泄漏信息了:(x)
_CrtDumpMemoryLeaks();q就q么单。我在周星星的例子代码中加入q些机关后,?VC++ 调试?x)话Q按 F5 调试q行Q?Output H口?Debug 便看到了预期的内存泄漏 dump。该 dump 形式如下Q?/font>
Detected memory leaks! Dumping objects -> c:\Program Files\...\include\crtdbg.h(552) : {45} normal block at 0x00441BA0, 2 bytes long. Data: <AB> 41 42 c:\Program Files\...\include\crtdbg.h(552) : {44} normal block at 0x00441BD0, 33 bytes long. Data: < C > 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: < C > E8 01 43 00 16 00 00 00 00 00 00 00 00 00 00 00 Object dump complete.
更具体的l节请参考本文附带的源代码文件?br>
下面是我看过 MSDN 资料后,针对“如何使用 CRT 调试功能来检内存泄漏?”的问题进行了一番编译和整理Q希望对大家有用。如果你的英文很,那就不用往下看了,直接去读 MSDN 库中的技术原文?br>C/C++ ~程语言的最强大功能之一便是其动态分配和释放内存Q但是中国有句古话:(x)“最大的长处也可能成为最大的q”Q那?C/C++ 应用E序正好印证了这句话。在 C/C++ 应用E序开发过E中Q动态分配的内存处理不当是最常见的问题。其中,最难捉怹最难检的错误之一是内存泄漏Q即未能正确释放以前分配的内存的错误。偶?dng)发生的量内存泄漏可能不?x)引v我们的注意,但泄漏大量内存的E序或泄漏日益增多的E序可能?x)表现出各?各样的征兆:(x)从性能不良Qƈ且逐渐降低Q到内存完全耗尽。更p的是,泄漏的程序可能会(x)用掉太多内存Q导致另外一个程序垮掉,而用户无从查找问题的真正根源。此外,即无害的内存泄漏也可能D及(qing)池鱼?br>q运的是QVisual Studio 调试器和 C q行?(CRT) 库ؓ(f)我们提供了检和识别内存泄漏的有效方法。下面请和我一起分享收获——如何?CRT 调试功能来检内存泄漏?
如何启用内存泄漏机?/font>Q?/font>
VC++ IDE 的默认状态是没有启用内存泄漏机制的Q也是说即使某D代码有内存泄漏Q调试会(x)话的 Output H口?Debug 不?x)输出有兛_存泄漏信息。你必须讑֮两个最基本的机x启用内存泄漏机制?br>
一是用调试堆函数Q?/font>
#define _CRTDBG_MAP_ALLOC #include<stdlib.h> #include<crtdbg.h>
注意Q?include 语句的顺序。如果更Ҏ(gu)序Q所使用的函数可能无法正工作?br>
通过包含 crtdbg.h 头文Ӟ可以?malloc ?free 函数映射到其“调试”版本 _malloc_dbg ?_free_dbgQ这些函C(x)跟踪内存分配和释放。此映射只在调试QDebugQ版本(也就是要定义 _DEBUGQ中有效。发行版本(ReleaseQ用普通的 malloc ?free 函数?br>#define 语句?CRT 堆函数的基础版本映射到对应的“调试”版本。该语句不是必须的,但如果没有该语句Q那么有兛_存泄漏的信息?x)不全?br>
二是在需要检内存泄漏的地方d下面q条语句来输出内存泄漏信息:(x)
_CrtDumpMemoryLeaks();当在调试器下q行E序Ӟ_CrtDumpMemoryLeaks 在 Output H口?Debug 中昄内存泄漏信息。比如:(x)
Detected memory leaks! Dumping objects -> C:\Temp\memleak\memleak.cpp(15) : {45} normal block at 0x00441BA0, 2 bytes long. Data: <AB> 41 42 c:\program files\microsoft visual studio\vc98\include\crtdbg.h(552) : {44} normal block at 0x00441BD0, 33 bytes long. Data: < C > 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: < C > 08 02 43 00 16 00 00 00 00 00 00 00 00 00 00 00 Object dump complete.
如果不?#define _CRTDBG_MAP_ALLOC 语句Q内存泄漏的输出是这L(fng)Q?/font>
Detected memory leaks! Dumping objects -> {45} normal block at 0x00441BA0, 2 bytes long. Data: <AB> 41 42 {44} normal block at 0x00441BD0, 33 bytes long. Data: < C > 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: < C > C0 01 43 00 16 00 00 00 00 00 00 00 00 00 00 00 Object dump complete.Ҏ(gu)q段输出信息Q你无法知道在哪个源E序文g里发生了内存泄漏。下面我们来研究一下输Z息的格式。第一行和W二行没有什么可说的Q从W三行开始:(x)
xx}Q花括弧内的数字是内存分配序P本文例子中是 {45}Q{44}Q{43}Q?blockQ内存块的类型,常用的有三种QnormalQ普通)、clientQ客L(fng)Q或 CRTQ运行时Q;本文例子中是Qnormal blockQ? 用十六进制格式表C的内存位置Q如Qat 0x00441BA0 {; 以字节ؓ(f)单位表示的内存块的大,如:(x)32 bytes longQ? ?16 字节的内容(也是用十六进制格式表C)Q如QData: <AB> 41 42 {;
仔细观察不难发现Q如果定义了 _CRTDBG_MAP_ALLOC Q那么在内存分配序号前面q会(x)昄在其中分配泄漏内存的文g名,以及(qing)文g名后括号中的数字表示发生泄漏的代码行P比如Q?/font>
C:\Temp\memleak\memleak.cpp(15)双击 Output H口中此文g名所在的输出行,便可跛_源程序文件分配该内存的代码行Q也可以选中该行Q然后按 F4Q效果一P Q这样一来我们就很容易定位内存泄漏是在哪里发生的了,因此Q_CRTDBG_MAP_ALLOC 的作用显而易见?br>
_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
q条语句无论E序在什么地斚w出都?x)自动调?_CrtDumpMemoryLeaks。注意:(x)q里必须同时讄两个位域标志Q_CRTDBG_ALLOC_MEM_DF ?_CRTDBG_LEAK_CHECK_DF?br>
讄 CRT 报告模式
默认情况下,_CrtDumpMemoryLeaks 内存泄漏信?dump ?Output H口?Debug , 如果你想这个输出定向到别的地方Q可以?_CrtSetReportMode q行重置。如果你使用某个库,它可能将输出定向到另一位置。此Ӟ只要使用以下语句输Z|设?Output H口卛_Q?/font>
_CrtSetReportMode( _CRT_ERROR, _CRTDBG_MODE_DEBUG );
有关使用 _CrtSetReportMode 的详l信息,请参?MSDN 库关?_CrtSetReportMode 的描q?br>
前面已经说过Q内存泄漏报告中把每一块泄漏的内存分ؓ(f) normalQ普通块Q、clientQ客L(fng)块)?CRT 块。事实上Q需要留心和注意的也是 normal ?clientQ即普通块和客L(fng)块?/font>
除了上述的类型外Q还有下面这两种cd的内存块Q它们不?x)出现在内存泄漏报告中?x)
如何在内存分配序号处讄断点Q?/font>
在内存泄漏报告中Q的文g名和行号可告诉分配泄漏的内存的代码位|,但仅仅依赖这些信息来了解完整的泄漏原因是不够的。因Z个程序在q行Ӟ一D分配内存的代码可能?x)被调用很多ơ,只要有一ơ调用后没有释放内存׃(x)D内存泄漏。ؓ(f)了确定是哪些内存没有被释放,不仅要知道泄漏的内存是在哪里分配的,q要知道泄漏产生的条件。这时内存分配序号就昑־特别有用——这个序号就是文件名和行号之后的花括弧里的那个数字?br>例如Q在本文例子代码的输Z息中Q?#8220;45”是内存分配序P意思是泄漏的内存是你程序中分配的第四十五个内存块:(x)
Detected memory leaks! Dumping objects -> C:\Temp\memleak\memleak.cpp(15) : {45} normal block at 0x00441BA0, 2 bytes long. Data: <AB> 41 42 ...... Object dump complete.
CRT 库对E序q行期间分配的所有内存块q行计数Q包括由 CRT 库自己分配的内存和其它库Q如 MFCQ分配的内存。因此,分配序号?N 的对象即为程序中分配的第 N 个对象,但不一定是代码分配的第 N 个对象。(大多数情况下q如此。)
q样的话Q你便可以利用分配序号在分配内存的位|设|一个断炏V方法是在程序v始附q设|一个位|断炏V当E序在该点中断时Q可以从 QuickWatchQ快速监视)对话框或 WatchQ监视)H口讄一个内存分配断点:(x)
例如Q在 Watch H口中,?Name 栏键入下面的表达式:(x)
_crtBreakAlloc
如果要?CRT 库的多线E?DLL 版本Q?MD 选项Q,那么必须包含上下文操作符Q像q样Q?/font>
{,,msvcrtd.dll}_crtBreakAlloc
现在按下回R键,调试器将计算该值ƈ把结果放?Value 栏。如果没有在内存分配点设|Q何断点,该值将?–1?br>用你惌在其位置中断的内存分配的分配序号替换 Value 栏中的倹{例如输?45。这样就?x)在分配序号?45 的地方中断?nbsp;
在所感兴的内存分配处设|断点后Q可以l调试。这Ӟq行E序时一定要心Q要保证内存块分配的序不会(x)改变。当E序在指定的内存分配处中断时Q可以查?Call StackQ调用堆栈)H口和其它调试器信息以确定分配内存时的情c(din)如果必要,可以从该点l执行程序,以查看对象发生了什么情况,或许可以定未正释攑֯象的原因?br>管通常在调试器中设|内存分配断Ҏ(gu)方便Q但如果愿意Q也可在代码中设|这些断炏Vؓ(f)了在代码中设|一个内存分配断点,可以增加q样一行(对于W四十五个内存分配)Q?/font>
_crtBreakAlloc = 45;
你还可以使用有相同效果的 _CrtSetBreakAlloc 函数Q?/font>
_CrtSetBreakAlloc(45);
如何比较内存状态?
定位内存泄漏的另一个方法就是在关键点获取应用程序内存状态的快照。CRT 库提供了一个结构类?_CrtMemState。你可以用它来存储内存状态的快照Q?/font>
_CrtMemState s1, s2, s3;
若要获取l定点的内存状态快照,可以?_CrtMemCheckpoint 函数传递一?_CrtMemState l构。该函数用当前内存状态的快照填充此结构:(x)
_CrtMemCheckpoint( &s1 );
通过?_CrtMemDumpStatistics 函数传?_CrtMemState l构Q可以在L地方 dump 该结构的内容Q?/font>
_CrtMemDumpStatistics( &s1 );
该函数输出如下格式的 dump 内存分配信息Q?/font>
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.
若要定某段代码中是否发生了内存泄漏Q可以通过获取该段代码之前和之后的内存状态快照,然后使用 _CrtMemDifference 比较q两个状态:(x)
_CrtMemCheckpoint( &s1 );// 获取W一个内存状态快? // 在这里进行内存分? _CrtMemCheckpoint( &s2 );// 获取W二个内存状态快? // 比较两个内存快照的差?if ( _CrtMemDifference( &s3, &s1, &s2) ) _CrtMemDumpStatistics( &s3 );// dump 差异l果
思义Q_CrtMemDifference 比较两个内存状态(前两个参敎ͼQ生成这两个状态之间差异的l果Q第三个参数Q。在E序的开始和l尾攄 _CrtMemCheckpoint 调用Qƈ使用 _CrtMemDifference 比较l果Q是查内存泄漏的另一U方法。如果检到泄漏Q则可以使用 _CrtMemCheckpoint 调用通过二进制搜索技术来分割E序和定位泄漏?br>
l论
管 VC ++ h一套专门调?MFC 应用E序的机Ӟ但本文上q讨论的内存分配很简单,没有涉及(qing)?MFC 对象Q所以这些内容同样也适用?MFC E序。在 MSDN 库中可以扑ֈ很多有关 VC++ 调试斚w的资料,如果你能善用 MSDN 库,怿用不了多时间你有可能成ؓ(f)调试高手?br>
本h水^不高Q谬误在所隑օQ请大家拍砖Q不要客气。顺大家圣诞快乐!
JerryZ ?2004 q^安夜Q?br>
调试Ҏ(gu)和技?br>
作者:(x)非凡
便于调试的代码风|(x)
VC++~译选项Q?/font>
调试Ҏ(gu)Q?/font>
1、?AssertQ原则:(x)量单)
assert只在debug下生效,release下不?x)被~译?br>
例子Q?/font>
char* strcpy(char* dest,char* source) { assert(source!=0); assert(dest!=0); char* returnstring = dest; while((*dest++ = *source++)!= ‘\0’) { ; } return returnstring; }
2、防御性的~程
例子Q?/font>
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++)!= ‘\0’) { ; } return returnstring; }
3、用Trace
以下的例子只能在debug中显C,
例子Q?br>
a)、TRACE
CString csTest Q?“test”Q?TRACEQ?#8220;CString is Qs\n”QcsTestQ;
b)、ATLTRACE
c)、afxDump
CTime time = CTime::GetCurrentTime(); #ifdef _DEBUG afxDump << time << “\n”; #endif
4、用GetLastError来检返回|通过得到错误代码来分析错误原?br>
5、把错误信息记录到文件中
异常处理
E序设计时一定要考虑到异常如何处理,当错误发生后Q不应简单的报告错误q出程序,应当可能的惛_法恢复到出错前的状态或者让E序从头开始运行,q且对于某些错误Q应该能够容错,卛_?dng)R误的存在Q但是程序还是能够正常完成Q务?br>
调试技?/strong>
1、VC++中F5q行调试q行
a)、在output DebugH口中可以看到用TRACE打印的信?br>b)?Call StackH口中能看到E序的调用堆?br>
2、当Debug版本q行时发生崩溃,选择retryq行调试Q通过看Call Stack分析出错的位|及(qing)原因
3、用映文件调?br>
a)、创建映文Ӟ(x)Project settings中link,选中Generate mapfileQ输出程序代码地址Q?MAPINFO: LINESQ得到引出序P(x)/MAPINFO: EXPORTS?br>b)、程序发布时Q应该把所有模块的映射文g都存?br>c)、查看映文Ӟ(x)?#8221; 通过崩溃地址扑և源代码的出错?#8221;文g?br>
4、可以调试的Release版本
Project settings中C++的Debug Info选择为Program DatabaseQLink的Debug中选择Debug Info和Microsoft format?br>
5、查看API的错误码Q在watchH口输入@err可以查看或者@err,hrQ其?#8221;,hr”表示错误码的说明?br>6、Set Next StatementQ该功能可以直接跌{到指定的代码行执行,一般用来测试异常处理的代码?br>7、调试内存变量的变化Q当内存发生变化时停下来?br>
常见错误
1、在函数q回的时候程序崩溃的原因
a)、写自动变量界
b)、函数原型不匚w
2、MFC
a)、用错误的函数原型处理用户定义消息
正确的函数原型ؓ(f)Q?/font>
afx_msg LRESULT OnMyMessage(WPARAM wParam,LPARAM lParam);
3、}慎用TerminateThreadQ用TerminateThread?x)造成资源泄漏Q不C不得Ԍ不要使用?br>
4、用_beginthreadexQ不要用Create Thread来常见线E?br>
参考资料:(x)
《WindowsE序调试?br>
功能强大的vc6调试?br>
作者:(x)yy2better
要成Z位优U的Y件工E师Q调试能力必不可~。本文将较详l介lVC6调试器的主要用法?nbsp;
windowsq_的调试器主要分ؓ(f)两大c:(x)
1 用户模式(user-mode)调试器:(x)它们都基于win32 Debugging APIQ有使用方便的界面,主要用于调试用户模式下的应用E序。这c调试器包括Visual C++调试器、WinDBG、BoundChecker、Borland C++ Builder调试器、NTSD{?nbsp;
2 内核模式(kernel-mode)调试器:(x)内核调试器位于CPU和操作系l之_(d)一旦启动,操作pȝ也会(x)中止q行Q主要用于调试驱动程序或用户模式调试器不易调试的E序。这c调试器包括WDEB386、WinDBG和softice{。其中WinDBG和softice也可以调试用h式代码?nbsp;
国外一位调试高手曾_(d)?0Q调试时间是在用VC++Q其余时间是使用WinDBG和softice。毕竟,调试用户模式代码QVC6调试器的效率是非帔R的。因此,我将首先在本介lVC6调试器的主要用法Q其他调试器的用法及(qing)一些调试技能在后箋文章中阐q?nbsp;
一 位置断点QLocation BreakpointQ?/strong>
大家最常用的断Ҏ(gu)普通的位置断点Q在源程序的某一行按F9p|了一个位|断炏V但对于很多问题Q这U朴素的断点作用有限。譬如下面这D代码:(x)
void CForDebugDlg::OnOK() { for (int i = 0; i < 1000; i++) //A { int k = i * 10 - 2; //B SendTo(k); //C int tmp = DoSome(i); //D int j = i / tmp; //E } }
执行此函敎ͼE序崩溃于E行,发现此时tmp?Q假设tmp本不应该?Q怎么q个时候ؓ(f)0呢?所以最好能够跟t此ơ@环时DoSome函数是如何运行的Q但׃是在循环体内Q如果在E行设|断点,可能需要按F5QGOQ许多次。这h要不停的按,很痛苦。用VC6断点修饰条g可以轻易解x问题。步骤如下?nbsp;
1 Ctrl+B打开断点讄框,如下图:(x)
Figure 1讄高位置断点
2 然后选择D行所在的断点Q然后点击condition按钮Q在弹出对话框的最下面一个编辑框中输入一个很大数目,具体视应用而定Q这?000够了?nbsp;
3 按F5重新q行E序Q程序中断。Ctrl+B打开断点框,发现此断点后跟随一串说明:(x)...487 times remaining。意思是q剩?87ơ没有执行,那就是说执行?13Q?000Q?87Q次时候出错的。因此,我们按步?所Ԍ更改此断点的skipơ数,?000改ؓ(f)513?nbsp;
4 再次重新q行E序Q程序执行了513ơ@环,然后自动停在断点处。这Ӟ我们可以仔l查看DoSome是如何返?的。这P你就避免了手指的痛苦Q节省了旉?nbsp;
再看位置断点其他修饰条g。如Figure 1所C,?#8220;Enter the expression to be evaluated:”下面Q可以输入一些条Ӟ当这些条件满xQ断Ҏ(gu)启动。譬如,刚才的程序,我们需要i?00时程序停下来Q我们就可以输入在编辑框中输?#8220;i==100”?nbsp;
另外Q如果在此编辑框中如果只输入变量名称Q则变量发生改变Ӟ断点才会(x)启动。这Ҏ(gu)一个变量何时被修改很方便,特别对一些大E序?nbsp;
用好位置断点的修饰条Ӟ可以大大方便解决某些问题?nbsp;
?数据断点QData BreakpointQ?/strong>
软g调试q程中,有时?x)发C些数据会(x)莫名其妙的被修改掉(如一些数l的界写导致覆盖了另外的变量)Q找Z处代码导致这块内存被更改是一件棘手的事情Q如果没有调试器的帮助)。恰当运用数据断点可以快速帮你定位何时何处这个数据被修改。譬如下面一D늨序:(x)
#include "stdafx.h" #includeint 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; }
q段E序的输出是
szName1: shenzhen szName1: ase szName2: vckbase
szName1何时被修改呢Q因为没有明昄修改szName1代码。我们可以首先在A行设|普通断点,F5q行E序Q程序停在A行。然后我们再讄一个数据断炏V如下图Q?nbsp;
Figure 2 数据断点
F5l箋q行Q程序停在B行,说明B处代码修改了szName1。B处明明没有修改szName1呀Q但调试器指明是q一行,一般不?x)错Q所以还是静下心来看看程序,哦,你发CQszName2只有4个字节,而strcpy?个字节,所以覆写了szName1?nbsp;
数据断点不只是对变量改变有效Q还可以讄变量是否{于某个倹{譬如,你可以将Figure 2中红圈处改ؓ(f)条g”szName2[0]==''''y''''“,那么当szName2W一个字Wؓ(f)y时断点就?x)启动?nbsp;
可以看出Q数据断点相对位|断点一个很大的区别是不用明指明在哪一行代码设|断炏V?nbsp;
?其他
1 在call stackH口中设|断点,选择某个函数Q按F9讄一个断炏V这样可以从深层ơ的函数调用中迅速返回到需要的函数?nbsp;
2 Set Next StateMent命o(h)Qdebugq程中,右键菜单中的命o(h)Q?nbsp;
此命令的作用是将E序的指令指针(EIPQ指向不同的代码行。譬如,你正在调试上面那D代码,q行在A行,但你不愿意运行B行和C行代码,q时Q你可以在D行,右键Q然?#8220;Set Next StateMent”。调试器׃?x)执行B、C行。只要在同一函数内,此指令就可以随意跛_或蟩后执行。灵zM用此功能可以大量节省调试旉?nbsp;
3 watchH口
watchH口支持丰富的数据格式化功能。如输入0x65,uQ则在右栏显C?01?nbsp;
实时昄windows API调用的错误:(x)在左栏输入@err,hr?nbsp;
在watchH口中调用函数。提醒一下,调用完函数后马上在watchH口中清除它Q否则,单步调试时每一步调试器都会(x)调用此函数?nbsp;
4 messages断点不怎么实用。基本上可以用前面讲q的断点代替?nbsp;
ȝ
调试最重要的还是你要思考,要猜你的程序可能出错的地方Q然后运用你的调试器来证实你的猜。而熟l用上面这些技巧无疑会(x)加快q个q程。最后,大家如果有关于调试方面的问题Q我乐意参与探讨?/font>
[ English | 体中?/strong> | Česky | Dansk | Deutsch | Français | Polski | Руссaий | J體中文 ]
为公众写qY件的人,大概都收到过很拙劣的bugQ计机E序代码中的错误或程序运行时的瑕疵——译者注Q报告,例如Q?/p>
在报告中?#8220;不好?#8221;Q?/p>
所报告内容毫无意义Q?/p>
在报告中用户没有提供_的信息;
在报告中提供?em>错误信息Q?/p>
所报告的问题是׃用户的过p生的Q?/p>
所报告的问题是׃其他E序的错误而生的Q?/p>
所报告的问题是׃|络错误而生的Q?/p>
q便是ؓ(f)什?#8220;技术支?#8221;被认为是一件可怕的工作Q因为有拙劣的bug报告需要处理。然而ƈ不是所有的bug报告都o(h)人生厌:(x)我在业余旉l护自由软gQ有时我?x)收到非常清晰、有帮助q且“有内?#8221;的bug报告?/p>
在这里我?x)尽力阐明如何写一个好的bug报告。我非常希望每一个h在报告bug之前都读一下这短文,当然我也希望用户在给?/em>报告bug之前已经读过q篇文章?/p>
单地_(d)报告bug的目的是Z让程序员看到E序的错误。?zhn)可以亲自CQ也可以l出能导致程序出错的、详的操作步骤。如果程序出错了Q程序员?x)收集额外的信息直到扑ֈ错误的原因;如果E序没有出错Q那么他们会(x)hl箋xq个问题Q收集相关的信息?/p>
在bug报告里,要设法搞清什么是事实Q例如:(x)“我在?sh)脑?#8221;?#8220;XX出现?#8221;Q什么是推测Q例如:(x)“?em>?/em>问题可能是出?#8230;…”Q。如果愿意的话,(zhn)可以省L,但是千万别省略事实?/p>
当?zhn)报告bug的时候(既然(zhn)已l这么做了)Q一定是希望bug得到?qing)时修正。所以此旉对程序员的Q何过Ȁ或亵渎的a语(甚至谩骂Q都是与事无补的——因可能是程序员的错误,也有可能是?zhn)的错误,也许?zhn)有权对他们发火Q但是如果?zhn)能多提供一些有用的信息Q而不是激愤之词)或许bug?x)被更快的修正。除此以外,误住:(x)如果是免费YӞ作者提供给我们已经是出于好心,所以要是太多的人对他们无礼Q他们可能就?#8220;收v”q䆾好心了?/p>
E序员不是弱智:(x)如果E序一炚w不好用,他们不可能不知道。他们不知道一定是因ؓ(f)E序在他们看来工作得很正常。所以,或者是(zhn)作q一些与他们不同的操作,或者是(zhn)的环境与他们不同。他们需要信息,报告bug也是Z提供信息。信息L多好?/p>
许多E序Q特别是自由软gQ会(x)公布一?#8220;已知bug列表”。如果?zhn)扑ֈ的bug在列表里已经有了Q那׃必再报告了,但是如果(zhn)认己掌握的信息比列表中的丰富,那无论如何也要与E序员联pR?zhn)提供的信息可能?x)使他们更单地修复bug?/p>
本文中提到的都是一些指导方针,没有哪一条是必须恪守的准则。不同的E序员会(x)喜欢不同形式的bug报告。如果程序附带了一套报告bug的准则,一定要诅R如果它与本文中提到的规则相抵触Q那么请以它为准?/p>
如果(zhn)不是报告bugQ而是L帮助Q?zhn)应该说明?zhn)曾l到哪里找过{案Q(例如Q我看了W四章和W五章的W二节,但我找不到解决的办法。)q会(x)使程序员了解用户喜欢到哪里去扄案,从而ɽE序员把帮助文档做得更容易用?/p>
报告bug的最好的Ҏ(gu)之一?#8220;演示”l程序员看。让E序员站在电(sh)脑前Q运行他们的E序Q指出程序的错误。让他们看着(zhn)启动电(sh)脑、运行程序、如何进行操作以?qing)程序对?zhn)的输入有何反应?/p>
他们对自己写的Y件了如指掌,他们知道哪些地方不会(x)出问题,而哪些地Ҏ(gu)可能出问题。他们本能地知道应该注意什么。在E序真的出错之前Q他们可能已l注意到某些地方不对Ԍq些都会(x)l他们一些线索。他们会(x)观察E序试中的每一?em>l节“E序不好?#8221;
“演示l我?#8221;
q些可能q不够。也总们觉得还需要更多的信息Q会(x)h重复刚才的操作。他们可能在q期间需要与(zhn)交一下,以便在他们需要的时候让bug重新出现。他们可能会(x)改变一些操作,看看q个错误的生是个别问题q是相关的一c问题。如果?zhn)不走q,他们可能需要坐下来Q拿Z堆开发工P׃几个时?em>好好?/em>研究一下。但是最重要的是在程序出错的时候让E序员在?sh)脑旁。一旦他们看C问题Q他们通常?x)找到原因ƈ开始试着修改?/p>
如今是网l时代,是信息交的时代。我可以点一下鼠标把自己的程序送到俄罗斯的某个朋友那里Q当然他也可以用同样单的Ҏ(gu)l我一些徏议。但是如果我的程序出了什么问题,?em>不可?/em>在他旁边?#8220;演示”是很好的办法Q但是常常做不到?/p>
如果(zhn)必L告bugQ而此时程序员又不在?zhn)w边Q那么?zhn)p惛_法让bug重现在他们面前。当他们亲眼看到错误Ӟp够进行处理了?/p>
切地告诉程序员(zhn)做了些什么。如果是一个图形界面程序,告诉他们(zhn)按了哪个按钮,依照什么顺序按的。如果是一个命令行E序Q精的告诉他们(zhn)键入了什么命令。?zhn)应该可能详l地提供(zhn)所键入的命令和E序的反应?/p>
把?zhn)能想到的所有的输入方式都告诉程序员Q如果程序要d一个文Ӟ(zhn)可能需要发一个文件的拯l他们。如果程序需要通过|络与另一台电(sh)脑通讯Q?zhn)或许不能把那台?sh)脑复制过去,但至可以说一下电(sh)脑的cd和安装了哪些软gQ如果可以的话)?/p>
如果(zhn)给了程序员一长串输入和指令,他们执行以后没有出现错误Q那是因为?zhn)没有l他们够的信息Q可能错误不是在每台计算Z都出玎ͼ(zhn)的pȝ可能和他们的在某些地方不一栗有时候程序的行ؓ(f)可能和?zhn)预想的不一Pq也许是误会(x)Q但是?zhn)会(x)认为程序出错了Q程序员却认是对的?/p>
同样也要描述发生了什么。精的描述(zhn)看C什么。告诉他们ؓ(f)什么?zhn)觉得自己所看到的是错误的,最好再告诉他们Q?zhn)认?f)自己应该看到什么。如果?zhn)只是_(d)(x)“E序出错?#8221;Q那(zhn)很可能漏掉了非帔R要的信息?/p>
如果(zhn)看C错误消息Q一定要仔细、准的告诉E序员,q?em>实很重要。在q种情况下,E序员只要修正错误,而不用去N误。他们需要知道是什么出问题了,pȝ所报的错误消息正好帮助了他们。如果?zhn)没有更好的方法记住这些消息,把它们写下来。只报告“E序Z一个错”是毫无意义的Q除非?zhn)把错误消息一块报上来?/p>
Ҏ(gu)情况下,如果有错误消息号Q?em>一?/em>要把q些L(fng)告诉E序员。不要以为?zhn)看不ZQ何意义,它就没有意义。错误消息号包含了能被程序员L的各U信息,q且很有可能包含重要的线索。给错误消息~号是因为用语言描述计算机错误常o(h)解。用q种方式告诉(zhn)错误的所在是一个最好的办法?/p>
在这U情形下Q程序员的排错工作会(x)十分高效。他们不知道发生了什么,也不可能到现场去观察Q所以他们一直在搜寻有h(hun)值的U烦。错误消息、错误消息号以及(qing)一些莫名其妙的延迟Q都是很重要的线索,像办案时的指纹一样重要,保存好?/p>
如果(zhn)用UNIXpȝQ程序可能会(x)产生一个内核输出(coredumpQ。内核输出是特别有用的线索来源,别扔了它们。另一斚wQ大多数E序员不喜欢收到含有大量内核输出文g的EMAILQ所以在发邮件之前最好先问一下。还有一点要注意Q内核输出文件记录了完整的程序状态,也就是说MU密Q可能当时程序正在处理一些私Z息或U密数据Q都可能包含在内核输出文仉?/p>
当一个错误或bug发生的时候,(zhn)可能会(x)做许多事情。但是大多数Z(x)使事情变的更p。我的一个朋友在学校里误删了Ҏ(gu)有的Word文gQ在找h帮忙之前奚w装了WordQ又q行了一遍碎片整理程序,q些操作对于恢复文g是毫无益处的Q因些操作搞׃盘的文件区块。恐怕在q个世界上没有一U反删除软g能恢复她的文件了。如果她不做M操作Q或许还有一U希望?/p>
q种用户仿佛(jng)一只被逼到墙角的鼬Q黄鼠狼、貂一cȝ动物——译者注Q:(x)背靠墙壁Q面Ҏ(gu)亡的降(f)奋v反扑Q疯狂攻凅R他们认为做点什么L什么都不做强。然而这些在处理计算Y仉题时q不适用?/p>
不要做鼬Q做一只羚。当一只羚面Ҏ(gu)想不到的情况或受到惊吓时Q它?x)一动不动,是ؓ(f)了不吸引M注意Q与此同时也在思考解决问题的最好办法(如果羊有一条技术支持热U,此时占线。)。然后,一旦它扑ֈ了最安全的行动方案,它便d?/p>
当程序出毛病的时候,立刻停止正在做的M操作。不要按M健。仔l地看一下屏q,注意那些不正常的地方Q记住它或者写下来。然后慎重地点击“定” ?#8220;取消”Q选择一个最安全的。学着L一U条件反——一旦电(sh)脑出了问题,先不要动。要xp个问题,x受媄响的E序或者重新启动计机都不好,一个解决问题的好办法是让问题再ơ生。程序员们喜Ƣ可以被重现的问题,快乐的程序员可以更快而且更有效率的修复bug?/p>
q不只是非专业的用户才会(x)写出拙劣的bug报告Q我见过一些非常差的bug报告E序员之手,有些q是非常优秀的程序员?/p>
有一ơ我与另一个程序员一起工作,他一直在找代码中的bugQ他常常遇到一个bugQ但是不?x)解冻I于是叫我帮忙?#8220;Z么毛病了Q?#8221;我问。而他的回{却L一些关于bug的意见。如果他的观Ҏ(gu),那的是一件好事。这意味着他已l完成了工作的一半,q且我们可以一起完成另一半工作。这是有效率q有用的?/p>
但事实上他常常是错的。这׃(x)使我们花上半个小时在原本正确的代码里来回L错误Q而实际上问题出在别的地方。我敢肯定他不会(x)对医生这么做?#8220;大夫Q我得了HydroyoyodyneQ真是怪病——译者)Q给我开个方?#8221;Qh们知道不该对一位医生说q些。?zhn)描述一下症Ӟ哪个地方不舒服,哪里疹{v皮疹、发?#8230;…让医生诊断?zhn)得了什么病Q应该怎样ȝ。否则医生会(x)把?zhn)当做疑心病或_病?zhn)者打发了Q这g没什么不寏V?/p>
做程序员也是一栗即便?zhn)自己?#8220;诊断”有时真的有帮助,也要只说“症状”?#8220;诊断”是可说可不说的,但是“症状”一定要说。同P在bug报告里面附上一份针对bug而做Z改的源代码是有用处的Q但它ƈ不能替代bug报告本n?/p>
如果E序员向(zhn)询问额外的信息Q千万别应付。曾l有一个h向我报告bugQ我让他试一个命令,我知道这个命令不好用Q但我是要看看程序会(x)q回一个什么错误(q是很重要的U烦Q。但是这位老兄Ҏ(gu)没试,他在回复中说“那肯定不好用”Q于是我又花了好些时间才说服他试了一下那个命令?/p>
用户多动动脑{对E序员的工作是有帮助的。即使?zhn)的推断是错误的,E序员也应该感谢(zhn),臛_(zhn)?em>?/em>d助他们,使他们的工作变的更简单。不q千万别忘了报告“症状”Q否则只?x)事情变得更糟?/p>
“间歇性错?#8221;着实让E序员发愁。相比之下,q行一pd单的操作便能D错误发生的问题是单的。程序员可以在一个便于观察的条g下重复那些操作,观察每一个细节。太多的问题在这U情况下不能解决Q例如:(x)E序每星期出一ơ错Q或者偶然出一ơ错Q或者在E序员面前从不出错(E序员一d出错。——译者)。当然还有就是程序的截止日期CQ那肯定要出错?/p>
大多?#8220;间歇性错?#8221;q不是真正的“间歇”。其中的大多数错误与某些地方是有联系的。有一些错误可能是内存泄漏产生的,有一些可能是别的E序在不恰当的时候修Ҏ(gu)个重要文仉成的,q有一些可能发生在每一个小时的前半个小时中Q我实遇到q这U事情)?/p>
同样Q如果?zhn)能bug重现Q而程序员不能Q那很有可能是他们的计算机和(zhn)的计算机在某些地方是不同的Q这U不同引起了问题。我曑ֆq一个程序,它的H口可以L(fng)成一个小球呆在屏q的左上角,它在别的计算Z只能?800x600 的解析度工作Q但是在我的机器上却可以?1024x768 下工作?/p>
E序员想要了解Q何与(zhn)发现的问题相关的事情。有可能的话(zhn)到另一台机器上试试Q多试几ơ,两次Q三ơ,看看问题是不是经常发生。如果问题出现在(zhn)进行了一pd操作之后Q不是?zhn)惌它出现它(yu)׃?x)出现Q这有可能是长旉的运行或处理大文件所D的错误。程序崩溃的时候,(zhn)要可能的C(zhn)都做了些什么,q且如果(zhn)看CQ何图?也别忘了提一下。?zhn)提供的Q何事情都是有帮助的。即使只是概括性的描述Q例如:(x)当后台有EMACSq行ӞE序常常出错Q,q虽然不能提供导致问题的直接U烦Q但是可能帮助程序员重现问题?/p>
最重要的是Q程序员惌定他们正在处理的是一个真正的“间歇性错?#8221;呢,q是一个在另一cȝ定的计算Z才出现的错误。他们想知道有关(zhn)计机的许多细节,以便了解(zhn)的机器与他们的有什么不同。有许多l节都依仗特定的E序Q但是有一件东西?zhn)一定要提供——版本号。程序的版本、操作系l的版本以及(qing)与问题有关的E序的版本?/p>
表意清楚在一份bug报告里是最基本的要求。如果程序员不知道?zhn)说的是什么意思,那?zhn)p没说一栗我收到的bug报告来自世界各地Q有许多是来自非p国家Q他们通常q英文不好而表C歉意。ȝ来说Q这些用户发来的bug报告通常是清晰而且有用的。几乎所有不清晰的bug报告都是来自母语是英语的人,他们L以ؓ(f)只要自己随便说说Q程序员p明白?/p>
因ؓ(f)Qmakefile关系C整个工程的编译规则。一个工E中的源文g不计敎ͼ其按cd、功能、模块分别放在若q个目录中,makefile定义了一pd的规则来指定Q哪些文仉要先~译Q哪些文仉要后~译Q哪些文仉要重新编译,甚至于进行更复杂的功能操作,因ؓ(f)makefile像一个Shell脚本一P其中也可以执行操作系l的命o(h)?
makefile带来的好处就是—?#8220;自动化编?#8221;Q一旦写好,只需要一个make命o(h)Q整个工E完全自动编译,极大的提高了软g开发的效率。make是一个命令工P是一个解释makefile中指令的命o(h)工具Q一般来_(d)大多数的IDE都有q个命o(h)Q比如:(x)Delphi的makeQVisual C++的nmakeQLinux下GNU的make。可见,makefile都成Z一U在工程斚w的编译方法?
现在讲述如何写makefile的文章比较少Q这是我惛_q篇文章的原因。当Ӟ不同产商的make各不相同Q也有不同的语法Q但其本质都是在“文g依赖?#8221;上做文章Q这里,我仅对GNU的makeq行讲述Q我的环境是RedHat Linux 8.0Qmake的版本是3.80。必竟,q个make是应用最为广泛的Q也是用得最多的。而且其还是最遵@于IEEE 1003.2-1992 标准的(POSIX.2Q?
在这文中Q将以C/C++的源码作为我们基Q所以必然涉?qing)一些关于C/C++的编译的知识Q相关于q方面的内容Q还请各位查看相关的~译器的文。这里所默认的编译器是UNIX下的GCC和CC?
~译Ӟ~译器需要的是语法的正确Q函C变量的声明的正确。对于后者,通常是你需要告诉编译器头文件的所在位|(头文件中应该只是声明Q而定义应该放在C/C++文g中)Q只要所有的语法正确Q编译器可以编译出中间目标文g。一般来_(d)每个源文仉应该对应于一个中间目标文ӞO文g或是OBJ文gQ?
链接Ӟ主要是链接函数和全局变量Q所以,我们可以使用q些中间目标文gQO文g或是OBJ文gQ来链接我们的应用程序。链接器q不函数所在的源文Ӟ只管函数的中间目标文ӞObject FileQ,在大多数时候,׃源文件太多,~译生成的中间目标文件太多,而在链接旉要明昑֜指出中间目标文g名,q对于编译很不方便,所以,我们要给中间目标文g打个包,在Windows下这U包?#8220;库文?#8221;QLibrary File)Q也是 .lib 文gQ在UNIX下,是Archive FileQ也是 .a 文g?
ȝ一下,源文仉先会(x)生成中间目标文gQ再׃间目标文件生成执行文件。在~译Ӟ~译器只程序语法,和函数、变量是否被声明。如果函数未被声明,~译器会(x)l出一个警告,但可以生成Object File。而在链接E序Ӟ链接器会(x)在所有的Object File中找d数的实现Q如果找不到Q那到就?x)报链接错误码(Linker ErrorQ,在VC下,q种错误一般是QLink 2001错误Q意思说是说Q链接器未能扑ֈ函数的实现。你需要指定函数的Object File.
好,a归正传,GNU的make有许多的内容Q闲a叙Q还是让我们开始吧?
首先Q我们用一个示例来说明Makefile的书写规则。以便给大家一个感兴认识。这个示例来源于GNU的make使用手册Q在q个CZ中,我们的工E有8个C文gQ和3个头文gQ我们要写一个Makefile来告诉make命o(h)如何~译和链接这几个文g。我们的规则是:(x)
只要我们的Makefile写得够好Q所有的q一切,我们只用一个make命o(h)可以完成,make命o(h)?x)自动智能地?gu)当前的文件修改的情况来确定哪些文仉要重~译Q从而自q译所需要的文g和链接目标程序?
prerequisites是Q要生成那个target所需要的文g或是目标?
command也就是make需要执行的命o(h)。(L的Shell命o(h)Q?
q是一个文件的依赖关系Q也是_(d)targetq一个或多个的目标文件依赖于prerequisites中的文gQ其生成规则定义在command中。说白一点就是说Qprerequisites中如果有一个以上的文g比target文g要新的话Qcommand所定义的命令就?x)被执行。这是Makefile的规则。也是Makefile中最核心的内宏V?
说到底,Makefile的东西就是这样一点,好像我的q篇文也该l束了。呵c(din)还不尽Ӟq是Makefile的主U和核心Q但要写好一个Makefileq不够,我会(x)以后面一点一点地l合我的工作l验l你慢慢到来。内容还多着呢。:(x)Q?
在这个makefile中,目标文gQtargetQ包含:(x)执行文gedit和中间目标文Ӟ*.oQ,依赖文gQprerequisitesQ就是冒号后面的那些 .c 文g?.h文g。每一?.o 文g都有一l依赖文Ӟ而这?.o 文g又是执行文g edit 的依赖文件。依赖关pȝ实质上就是说明了目标文g是由哪些文g生成的,换言之,目标文g是哪些文件更新的?
在定义好依赖关系后,后箋的那一行定义了如何生成目标文g的操作系l命令,一定要以一个Tab键作为开头。记住,makeq不命令是怎么工作的,他只执行所定义的命令。make?x)比较targets文g和prerequisites文g的修Ҏ(gu)期,如果prerequisites文g的日期要比targets文g的日期要斎ͼ或者target不存在的话,那么Qmake׃(x)执行后箋定义的命令?
q里要说明一点的是,clean不是一个文Ӟ它只不过是一个动作名字,有点像C语言中的lable一P其冒号后什么也没有Q那么,make׃?x)自动去找文件的依赖性,也就不会(x)自动执行其后所定义的命令。要执行其后的命令,p在make命o(h)后明昑־指出q个lable的名字。这L(fng)Ҏ(gu)非常有用Q我们可以在一个makefile中定义不用的~译或是和编译无关的命o(h)Q比如程序的打包Q程序的备䆾Q等{?
q就是整个make的依赖性,make?x)一层又一层地L文g的依赖关p,直到最l编译出W一个目标文件。在扑֯的过E中Q如果出现错误,比如最后被依赖的文件找不到Q那么make׃(x)直接退出,q报错,而对于所定义的命令的错误Q或是编译不成功QmakeҎ(gu)不理。make只管文g的依赖性,卻I如果在我找了依赖关系之后Q冒号后面的文gq是不在Q那么对不vQ我׃工作啦?
通过上述分析Q我们知道,像cleanq种Q没有被W一个目标文件直接或间接兌Q那么它后面所定义的命令将不会(x)被自动执行,不过Q我们可以显Cmake执行。即命o(h)—?#8220;make clean”Q以此来清除所有的目标文gQ以侉K~译?
于是在我们编E中Q如果这个工E已被编译过了,当我们修改了其中一个源文gQ比如file.cQ那么根据我们的依赖性,我们的目标file.o?x)被重编译(也就是在q个依性关pd面所定义的命令)Q于是file.o的文件也是最新的啦,于是file.o的文件修Ҏ(gu)间要比edit要新Q所以edit也会(x)被重新链接了Q详见edit目标文g后定义的命o(h)Q?
而如果我们改变了“command.h”Q那么,kdb.o、command.o和files.o都会(x)被重~译Qƈ且,edit?x)被重链接?
比如Q我们声明一个变量,叫objects, OBJECTS, objs, OBJS, obj, 或是 OBJQ反正不什么啦Q只要能够表Cobj文gp了。我们在makefile一开始就q样定义Q?
objects = main.o kbd.o command.o display.o \ insert.o search.o files.o utils.o于是Q我们就可以很方便地在我们的makefile中以“$(objects)”的方式来使用q个变量了,于是我们的改良版makefile变成下面这个样子:(x)
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)
于是如果有新?.o 文g加入Q我们只需单地修改一?objects 变量可以了?
关于变量更多的话题,我会(x)在后l给你一一道来?
只要make看到一个[.o]文gQ它?yu)׃?x)自动的把[.c]文g加在依赖关系中,如果make扑ֈ一个whatever.oQ那么whatever.cQ就?x)是whatever.o的依赖文件。ƈ?cc -c whatever.c 也会(x)被推导出来,于是Q我们的makefile再也不用写得q么复杂。我们的是新的makefile又出炉了?
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)q种Ҏ(gu)Q也是make?#8220;隐晦规则”。上面文件内容中Q?#8220;.PHONY”表示Qclean是个伪目标文件?
关于更ؓ(f)详细?#8220;隐晦规则”?#8220;伪目标文?#8221;Q我?x)在后箋l你一一道来?
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)q种风格Q让我们的makefile变得很简单,但我们的文g依赖关系显得有点凌׃。鱼和熊掌不可兼得。还看你的喜好了。我是不喜欢q种风格的,一是文件的依赖关系看不清楚Q二是如果文件一多,要加入几个新?o文gQ那q不清楚了?
.PHONY : clean clean : -rm edit $(objects)前面说过Q?PHONY意思表Cclean是一?#8220;伪目?#8221;Q。而在rm命o(h)前面加了一个小减号的意思就是,也许某些文g出现问题Q但不要,l箋做后面的事。当Ӟclean的规则不要放在文件的开_(d)不然Q这׃(x)变成make的默认目标,怿谁也不愿意这栗不成文的规矩是—?#8220;clean从来都是攑֜文g的最?#8221;?
上面是一个makefile的概貌,也是makefile的基Q下面还有很多makefile的相关细节,准备好了吗?准备好了来?
最后,q值得一提的是,在Makefile中的命o(h)Q必要以[Tab]键开始?
当然Q你可以使用别的文g名来书写MakefileQ比如:(x)“Make.Linux”Q?#8220;Make.Solaris”Q?#8220;Make.AIX”{,如果要指定特定的MakefileQ你可以使用make?#8220;-f”?#8220;--file”参数Q如Qmake -f Make.Linux或make --file Make.AIX?
include <filename>
在include前面可以有一些空字符Q但是绝不能是[Tab]键开始。include?filename>可以用一个或多个I格隔开。D个例子,你有q样几个MakefileQa.mk、b.mk、c.mkQ还有一个文件叫foo.makeQ以?qing)一个变?(bar)Q其包含了e.mk和f.mkQ那么,下面的语句:(x)
include foo.make *.mk $(bar){h(hun)于:(x)
include foo.make a.mk b.mk c.mk e.mk f.mkmake命o(h)开始时Q会(x)把找寻include所指出的其它MakefileQƈ把其内容安置在当前的位置。就好像C/C++?include指o(h)一栗如果文仉没有指定l对路径或是相对路径的话Qmake?x)在当前目录下首先寻找,如果当前目录下没有找刎ͼ那么Qmakeq会(x)在下面的几个目录下找Q?
如果有文件没有找到的话,make?x)生成一条警告信息,但不?x)马上出现致命错误。它?x)l蝲入其它的文gQ一旦完成makefile的读取,make?x)再重试q些没有扑ֈQ或是不能读取的文gQ如果还是不行,make才会(x)出现一条致命信息。如果你惌make不理那些无法d的文Ӟ而l执行,你可以在include前加一个减?#8220;-”。如Q?
-include <filename>
但是在这里我q是不要使用q个环境变量Q因为只要这个变量一被定义,那么当你使用makeӞ所有的Makefile都会(x)受到它的影响Q这l不是你想看到的。在q里提这个事Q只是ؓ(f)了告诉大Ӟ也许有时候你的Makefile出现了怪事Q那么你可以看看当前环境中有没有定义q个变量?
在Makefile中,规则的顺序是很重要的Q因为,Makefile中只应该有一个最l目标,其它的目标都是被q个目标所q带出来的,所以一定要让make知道你的最l目标是什么。一般来_(d)定义在Makefile中的目标可能?x)有很多Q但是第一条规则中的目标将被确立ؓ(f)最l的目标。如果第一条规则中的目标有很多个,那么Q第一个目标会(x)成ؓ(f)最l的目标。make所完成的也是q个目标?
好了Q还是让我们来看一看如何书写规则?
foo.o : foo.c defs.h # foo模块 cc -c -g foo.c看到q个例子Q各位应该不是很陌生了,前面也已说过Qfoo.o是我们的目标Qfoo.c和defs.h是目标所依赖的源文gQ而只有一个命?#8220;cc -c -g foo.c”Q以Tab键开_(d)。这个规则告诉我们两件事Q?
targets : prerequisites ; command command ...targets是文件名Q以I格分开Q可以用通配W。一般来_(d)我们的目标基本上是一个文Ӟ但也有可能是多个文g?
command是命令行Q如果其不与“target:prerequisites”在一行,那么Q必M[Tab键]开_(d)如果和prerequisites在一行,那么可以用分号做为分隔。(见上Q?
prerequisites也就是目标所依赖的文Ӟ或依赖目标)。如果其中的某个文g要比目标文g要新Q那么,目标p认ؓ(f)?#8220;q时?#8221;Q被认ؓ(f)是需要重生成的。这个在前面已经讲过了?
如果命o(h)太长Q你可以使用反斜框(‘\’Q作为换行符。make对一行上有多个字符没有限制。规则告诉make两g事,文g的依赖关pd如何成成目标文g?
一般来_(d)make?x)以UNIX的标准ShellQ也是/bin/sh来执行命令?
print: *.c lpr -p $? touch print上面q个例子说明了通配W也可以在我们的规则中,目标print依赖于所有的[.c]文g。其中的“$?”是一个自动化变量Q我?x)在后面l你讲述?
objects = *.o上面q个例子Q表CZQ通符同样可以用在变量中。ƈ不是说[*.o]?x)展开Q不Qobjects的值就?#8220;*.o”。Makefile中的变量其实是C/C++中的宏。如果你要让通配W在变量中展开Q也是让objects的值是所有[.o]的文件名的集合,那么Q你可以q样Q?
objects := $(wildcard *.o)q种用法由关键字“wildcard”指出Q关于Makefile的关键字Q我们将在后面讨论?
在一些大的工E中Q有大量的源文gQ我们通常的做法是把这许多的源文g分类Qƈ存放在不同的目录中。所以,当make需要去扑֯文g的依赖关pLQ你可以在文件前加上路径Q但最好的Ҏ(gu)是把一个\径告诉makeQ让make在自动去找?
Makefile文g中的Ҏ(gu)变量“VPATH”是完成q个功能的,如果没有指明q个变量Qmake只会(x)在当前的目录中去扑֯依赖文g和目标文件。如果定义了q个变量Q那么,make׃(x)在当当前目录找不到的情况下,到所指定的目录中LL件了?
VPATH = src:../headers上面的的定义指定两个目录Q?#8220;src”?#8220;../headers”Qmake?x)按照这个顺序进行搜索。目录由“冒号”分隔。(当然Q当前目录永q是最高优先搜索的地方Q?
另一个设|文件搜索\径的Ҏ(gu)是用make?#8220;vpath”关键字(注意Q它是全写的)Q这不是变量Q这是一个make的关键字Q这和上面提到的那个VPATH变量很类|但是它更为灵zR它可以指定不同的文件在不同的搜索目录中。这是一个很灉|的功能。它的用方法有三种Q?
vapth使用Ҏ(gu)中的< pattern>需要包?#8220;%”字符?#8220;%”的意思是匚w零或若干字符Q例如,“%.h”表示所有以“.h”l尾的文件?lt; pattern>指定了要搜烦的文仉Q?lt; directories>则指定了
vpath %.h ../headers该语句表C,要求make?#8220;../headers”目录下搜索所有以“.h”l尾的文件。(如果某文件在当前目录没有扑ֈ的话Q?
我们可以q箋C用vpath语句Q以指定不同搜烦{略。如果连l的vpath语句中出C相同?lt; pattern>Q或是被重复了的< pattern>Q那么,make?x)按照vpath语句的先后顺序来执行搜烦。如Q?
vpath %.c foo vpath % blish vpath %.c bar其表C?#8220;.c”l尾的文Ӟ先在“foo”目录Q然后是“blish”Q最后是“bar”目录?
vpath %.c foo:bar vpath % blish而上面的语句则表C?#8220;.c”l尾的文Ӟ先在“foo”目录Q然后是“bar”目录Q最后才?#8220;blish”目录?
因ؓ(f)Q我们ƈ不生?#8220;clean”q个文g?#8220;伪目?#8221;q不是一个文Ӟ只是一个标{,׃“伪目?#8221;不是文gQ所以make无法生成它的依赖关系和决定它是否要执行。我们只有通过昄地指明这?#8220;目标”才能让其生效。当Ӟ“伪目?#8221;的取名不能和文g名重名,不然其就失去?#8220;伪目?#8221;的意义了?
当然Qؓ(f)了避免和文g重名的这U情况,我们可以使用一个特D的标记“.PHONY”来显C地指明一个目标是“伪目?#8221;Q向make说明Q不是否有q个文gQ这个目标就?#8220;伪目?#8221;?
.PHONY : clean只要有这个声明,不管是否?#8220;clean”文gQ要q行“clean”q个目标Q只?#8220;make clean”q样。于是整个过E可以这样写Q?
.PHONY: clean clean: rm *.o temp伪目标一般没有依赖的文g。但是,我们也可以ؓ(f)伪目标指定所依赖的文件。伪目标同样可以作ؓ(f)“默认目标”Q只要将其放在第一个。一个示例就是,如果你的Makefile需要一口气生成若干个可执行文gQ但你只想简单地敲一个make完事Qƈ且,所有的目标文g都写在一个Makefile中,那么你可以?#8220;伪目?#8221;q个Ҏ(gu):(x)
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我们知道QMakefile中的W一个目标会(x)被作为其默认目标。我们声明了一?#8220;all”的伪目标Q其依赖于其它三个目标。由于伪目标的特性是QL被执行的Q所以其依赖的那三个目标L不如“all”q个目标新。所以,其它三个目标的规则L?x)被册。也pC我们一口气生成多个目标的目的?#8220;.PHONY : all”声明?#8220;all”q个目标?#8220;伪目?#8221;?
随便提一句,从上面的例子我们可以看出Q目标也可以成ؓ(f)依赖。所以,伪目标同样也可成Z赖。看下面的例子:(x)
.PHONY: cleanall cleanobj cleandiff cleanall : cleanobj cleandiff rm program cleanobj : rm *.o cleandiff : rm *.diff“make clean”清除所有要被清除的文g?#8220;cleanobj”?#8220;cleandiff”q两个伪目标有点?#8220;子程?#8221;的意思。我们可以输?#8220;make cleanall”?#8220;make cleanobj”?#8220;make cleandiff”命o(h)来达到清除不同种cL件的目的
上述规则{h(hun)于:(x)bigoutput : text.g generate text.g -big > bigoutput littleoutput : text.g generate text.g -little > littleoutput 其中Q?$(subst output,,$@)中的“$”表示执行一个Makefile的函敎ͼ函数名ؓ(f)substQ后面的为参数。关于函敎ͼ在后面讲述。这里的q个函数是截取字W串的意思,“$@”表示目标的集合,像一个数l,“$@”依次取出目标Qƈ执于命o(h)?
<targets ...>: <target-pattern>: <prereq-patterns ...> <commands> ...
targets定义了一pd的目标文Ӟ可以有通配W。是目标的一个集合?
target-parrtern是指明了targets的模式,也就是的目标集模式?
prereq-parrterns是目标的依赖模式Q它对target-parrtern形成的模式再q行一ơ依赖目标的定义?
q样描述q三个东西,可能q是没有说清楚,q是举个例子来说明一下吧。如果我们的<target-parrtern>定义?#8220;%.o”Q意思是我们?target>集合中都是以“.o”l尾的,而如果我们的<prereq-parrterns>定义?#8220;%.c”Q意思是?lt;target-parrtern>所形成的目标集q行二次定义Q其计算Ҏ(gu)是,?lt;target-parrtern>模式中的“%”Q也是L了[.o]q个l尾Q,qؓ(f)其加上[.c]q个l尾QŞ成的新集合?
所以,我们?#8220;目标模式”或是“依赖模式”中都应该?#8220;%”q个字符Q如果你的文件名中有“%”那么你可以用反斜杠“\”q行转义Q来标明真实?#8220;%”字符?
看一个例子:(x)
objects = foo.o bar.o all: $(objects) $(objects): %.o: %.c $(CC) -c $(CFLAGS) $< -o $@上面的例子中Q指明了我们的目标从$object中获取,“%.o”表明要所有以“.o”l尾的目标,也就?#8220;foo.o bar.o”Q也是变量$object集合的模式,而依赖模?#8220;%.c”则取模式“%.o”?#8220;%”Q也是“foo bar”Qƈ为其加下“.c”的后~Q于是,我们的依赖目标就?#8220;foo.c bar.c”。而命令中?#8220;$<”?#8220;$@”则是自动化变量,“$<”表示所有的依赖目标集(也就?#8220;foo.c bar.c”Q,“$@”表示目标集(也褪恰癴oo.o bar.o”Q。于是,上面的规则展开后等价于下面的规则:(x)
foo.o : foo.c $(CC) -c $(CFLAGS) foo.c -o foo.o bar.o : bar.c $(CC) -c $(CFLAGS) bar.c -o bar.o试想Q如果我们的“%.o”有几百个QU我们只要用q种很简单的“静态模式规?#8221;可以写完一堆规则,实在是太有效率了?#8220;静态模式规?#8221;的用法很灉|Q如果用得好Q那?x)一个很强大的功能。再看一个例子:(x)
files = foo.elc bar.o lose.o $(filter %.o,$(files)): %.o: %.c $(CC) -c $(CFLAGS) $< -o $@ $(filter %.elc,$(files)): %.elc: %.el emacs -f batch-byte-compile $<
$(filter %.o,$(files))表示调用Makefile的filter函数Q过?#8220;$filter”集,只要其中模式?#8220;%.o”的内宏V其的它内容Q我׃用多说了吧。这个例字展CZMakefile中更大的Ҏ(gu)?
main.o : main.c defs.h但是Q如果是一个比较大型的工程Q你必需清楚哪些C文g包含了哪些头文gQƈ且,你在加入或删除头文gӞ也需要小心地修改MakefileQ这是一个很没有l护性的工作。ؓ(f)了避免这U繁重而又Ҏ(gu)出错的事情,我们可以使用C/C++~译的一个功能。大多数的C/C++~译器都支持一?#8220;-M”的选项Q即自动扑֯源文件中包含的头文gQƈ生成一个依赖关pR例如,如果我们执行下面的命令:(x)
cc -M main.c其输出是Q?
main.o : main.c defs.h于是q译器自动生成的依赖关p,q样一来,你就不必再手动书写若q文件的依赖关系Q而由~译器自动生成了。需要提醒一句的是,如果你用GNU的C/C++~译器,你得?#8220;-MM”参数Q不Ӟ“-M”参数?x)把一些标准库的头文g也包含进来?
gcc -M main.c的输出是Q?
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.hgcc -MM main.c的输出则是:(x)
main.o: main.c defs.h那么Q编译器的这个功能如何与我们的Makefile联系在一起呢。因样一来,我们的Makefile也要Ҏ(gu)q些源文仉新生成,让Makefile自已依赖于源文gQ这个功能ƈ不现实,不过我们可以有其它手D|q回地实现这一功能。GNUl织把编译器为每一个源文g的自动生成的依赖关系攑ֈ一个文件中Qؓ(f)每一?#8220;name.c”的文仉生成一?#8220;name.d”的Makefile文gQ[.d]文g中就存放对应[.c]文g的依赖关pR?
于是Q我们可以写出[.c]文g和[.d]文g的依赖关p,q让make自动更新或自成[.d]文gQƈ把其包含在我们的主Makefile中,q样Q我们就可以自动化地生成每个文g的依赖关pM?
q里Q我们给Z一个模式规则来产生[.d]文gQ?
%.d: %.c @set -e; rm -f $@; \ $(CC) -M $(CPPFLAGS) $< > $@.$$$$; \ sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \ rm -f $@.$$$$
q个规则的意思是Q所有的[.d]文g依赖于[.c]文gQ?#8220;rm -f $@”的意思是删除所有的目标Q也是[.d]文gQ第二行的意思是Qؓ(f)每个依赖文g“$<”Q也是[.c]文g生成依赖文gQ?#8220;$@”表示模式“%.d”文gQ如果有一个C文g是name.cQ那?#8220;%”是“name”Q?#8220;$$$$”意ؓ(f)一个随机编PW二行生成的文g有可能是“name.d.12345”Q第三行使用sed命o(h)做了一个替换,关于sed命o(h)的用法请参看相关的用文档。第四行是删除临时文g?
总而言之,q个模式要做的事是在编译器生成的依赖关pM加入[.d]文g的依赖,x依赖关系Q?
main.o : main.c defs.h转成Q?
main.o main.d : main.c defs.h于是Q我们的[.d]文g也会(x)自动更新了,q会(x)自动生成了,当然Q你q可以在q个[.d]文g中加入的不只是依赖关p,包括生成的命令也可一q加入,让每个[.d]文g都包含一个完赖的规则。一旦我们完成这个工作,接下来,我们p把这些自动生成的规则放进我们的主Makefile中。我们可以用Makefile?#8220;include”命o(h)Q来引入别的Makefile文gQ前面讲q)Q例如:(x)
sources = foo.c bar.c include $(sources:.c=.d)上述语句中的“$(sources:.c=.d)”中的“.c=.d”的意思是做一个替换,把变?(sources)所有[.c]的字串都替换成[.d]Q关于这?#8220;替换”的内容,在后面我?x)有更?f)详细的讲q。当Ӟ你得注意ơ序Q因为include是按ơ来载入文gQ最先蝲入的[
每条规则中的命o(h)和操作系lShell的命令行是一致的。make?x)一按顺序一条一条的执行命o(h)Q每条命令的开头必M[Tab]键开_(d)除非Q命令是紧跟在依赖规则后面的分号后的。在命o(h)行之间中的空格或是空行会(x)被忽略,但是如果该空格或I是以Tab键开头的Q那么make?x)认为其是一个空命o(h)?
我们在UNIX下可能会(x)使用不同的ShellQ但是make的命令默认是?#8220;/bin/sh”——U(ku)NIX的标准Shell解释执行的。除非你特别指定一个其它的Shell。Makefile中,“#”是注释符Q很像C/C++中的“//”Q其后的本行字符都被注释?
通常Qmake?x)把其要执行的命令行在命令执行前输出到屏q上。当我们?#8220;@”字符在命令行前,那么Q这个命令将不被make昄出来Q最具代表性的例子是,我们用这个功能来像屏q显CZ些信息。如Q?
@echo 正在~译XXX模块......当make执行Ӟ?x)输?#8220;正在~译XXX模块......”字串Q但不会(x)输出命o(h)Q如果没?#8220;@”Q那么,make输出:(x)
echo 正在~译XXX模块...... 正在~译XXX模块......如果make执行Ӟ带入make参数“-n”?#8220;--just-print”Q那么其只是昄命o(h)Q但不会(x)执行命o(h)Q这个功能很有利于我们调试我们的MakefileQ看看我们书写的命o(h)是执行v来是什么样子的或是什么顺序的?
而make参数“-s”?#8220;--slient”则是全面止命o(h)的显C?
当依赖目标新于目标时Q也是当规则的目标需要被更新Ӟmake?x)一条一条的执行其后的命令。需要注意的是,如果你要让上一条命令的l果应用在下一条命令时Q你应该使用分号分隔q两条命令。比如你的第一条命令是cd命o(h)Q你希望W二条命令得在cd之后的基上运行,那么你就不能把这两条命o(h)写在两行上,而应该把q两条命令写在一行上Q用分号分隔。如Q?
CZ一Q? exec: cd /home/hchen pwd CZ二:(x) exec: cd /home/hchen; pwd
当我们执?#8220;make exec”ӞW一个例子中的cd没有作用Qpwd?x)打印出当前的Makefile目录Q而第二个例子中,cdpv作用了,pwd?x)打印?#8220;/home/hchen”?
make一般是使用环境变量SHELL中所定义的系lShell来执行命令,默认情况下用UNIX的标准Shell—?bin/sh来执行命令。但在MS-DOS下有点特D,因ؓ(f)MS-DOS下没有SHELL环境变量Q当然你也可以指定。如果你指定了UNIX风格的目录Ş式,首先Qmake?x)在SHELL所指定的\径中扑֯命o(h)解释器,如果找不刎ͼ其会(x)在当前盘W中的当前目录中LQ如果再找不刎ͼ其会(x)在PATH环境变量中所定义的所有\径中L。MS-DOS中,如果你定义的命o(h)解释器没有找刎ͼ其会(x)l你的命令解释器加上诸如“.exe”?#8220;.com”?#8220;.bat”?#8220;.sh”{后~?
每当命o(h)q行完后Qmake?x)检每个命令的q回码,如果命o(h)q回成功Q那么make?x)执行下一条命令,当规则中所有的命o(h)成功q回后,q个规则q是成功完成了。如果一个规则中的某个命令出错了Q命令退出码非零Q,那么make׃(x)l止执行当前规则Q这有可能l止所有规则的执行?
有些时候,命o(h)的出错ƈ不表C就是错误的。例如mkdir命o(h)Q我们一定需要徏立一个目录,如果目录不存在,那么mkdir成功执行,万事大吉Q如果目录存在,那么出错了。我们之所以用mkdir的意思就是一定要有这L(fng)一个目录,于是我们׃希望mkdir出错而终止规则的q行?
Z做到q一点,忽略命o(h)的出错,我们可以在Makefile的命令行前加一个减?#8220;-”Q在Tab键之后)Q标Cؓ(f)不管命o(h)Z出错都认为是成功的。如Q?
clean: -rm -f *.oq有一个全局的办法是Q给make加上“-i”或是“--ignore-errors”参数Q那么,Makefile中所有命令都?x)忽略错误。而如果一个规则是?#8220;.IGNORE”作ؓ(f)目标的,那么q个规则中的所有命令将?x)忽略错误。这些是不同U别的防止命令出错的Ҏ(gu)Q你可以Ҏ(gu)你的不同喜欢讄?
q有一个要提一下的make的参数的?#8220;-k”或是“--keep-going”Q这个参数的意思是Q如果某规则中的命o(h)出错了,那么q目该规则的执行,但l执行其它规则?
在一些大的工E中Q我们会(x)把我们不同模块或是不同功能的源文件放在不同的目录中,我们可以在每个目录中都书写一个该目录的MakefileQ这有利于让我们的Makefile变得更加地简z,而不至于把所有的东西全部写在一个Makefile中,q样?x)很隄护我们的MakefileQ这个技术对于我们模块编译和分段~译有着非常大的好处?
例如Q我们有一个子目录叫subdirQ这个目录下有个Makefile文gQ来指明了这个目录下文g的编译规则。那么我们L的Makefile可以q样书写Q?
subsystem: cd subdir && $(MAKE) 其等价于Q? subsystem: $(MAKE) -C subdir定义$(MAKE)宏变量的意思是Q也许我们的make需要一些参敎ͼ所以定义成一个变量比较利于维护。这两个例子的意思都是先q入“subdir”目录Q然后执行make命o(h)? 我们把这个Makefile叫做“LMakefile”QLMakefile的变量可以传递到下的Makefile中(如果你显C的声明Q,但是不会(x)覆盖下层的Makefile中所定义的变量,除非指定?#8220;-e”参数? 如果你要传递变量到下Makefile中,那么你可以用这L(fng)声明Q?如果你不惌某些变量传递到下Makefile中,那么你可以这样声明:(x)export <variable ...>如:(x)unexport <variable ...>CZ一Q? export variable = value 其等价于Q? variable = value export variable 其等价于Q? export variable := value 其等价于Q? variable := value export variable CZ二:(x) export variable += value 其等价于Q? variable += value export variable如果你要传递所有的变量Q那么,只要一个exportp了。后面什么也不用跟,表示传递所有的变量? 需要注意的是,有两个变量,一个是SHELLQ一个是MAKEFLAGSQ这两个变量不管你是否exportQ其L要传递到下层Makefile中,特别是MAKEFILES变量Q其中包含了make的参C息,如果我们执行“LMakefile”时有make参数或是在上层Makefile中定义了q个变量Q那么MAKEFILES变量会(x)是这些参敎ͼq会(x)传递到下层Makefile中,q是一个系l的环境变量? 但是make命o(h)中的有几个参数ƈ不往下传递,它们?#8220;-C”,“-f”,“-h”“-o”?#8220;-W”Q有关Makefile参数的细节将在后面说明)Q如果你不想往下层传递参敎ͼ那么Q你可以q样来:(x)subsystem: cd subdir && $(MAKE) MAKEFLAGS=如果你定义了环境变量MAKEFLAGSQ那么你得确信其中的选项是大安?x)用到的Q如果其中有“-t”,“-n”,?#8220;-q”参数Q那么将?x)有让你意想不到的结果,或许会(x)让你异常地恐慌? q有一个在“嵌套执行”中比较有用的参数Q?#8220;-w”或是“--print-directory”?x)在make的过E中输出一些信息,让你看到目前的工作目录。比如,如果我们的下Umake目录?#8220;/home/hchen/gnu/make”Q如果我们?#8220;make -w”来执行,那么当进入该目录Ӟ我们?x)看刎ͼ?x)make: Entering directory `/home/hchen/gnu/make'.而在完成下层make后离开目录Ӟ我们?x)看刎ͼ?x)make: Leaving directory `/home/hchen/gnu/make'当你使用“-C”参数来指定make下层MakefileӞ“-w”?x)被自动打开的。如果参C?#8220;-s”Q?#8220;--slient”Q或?#8220;--no-print-directory”Q那么,“-w”L失效的? ---++++4.5 定义命o(h)? 如果Makefile中出C些相同命令序列,那么我们可以些相同的命o(h)序列定义一个变量。定义这U命令序列的语法?#8220;define”开始,?#8220;endef”l束Q如Q?define run-yacc yacc $(firstword $^) mv y.tab.c $@ endefq里Q?#8220;run-yacc”是这个命令包的名字,其不要和Makefile中的变量重名。在“define”?#8220;endef”中的两行是命o(h)序列。这个命令包中的W一个命令是q行YaccE序Q因为YaccE序L生成“y.tab.c”的文Ӟ所以第二行的命令就是把q个文gҎ(gu)名字。还是把q个命o(h)包放C个示例中来看看吧?foo.c : foo.y $(run-yacc)我们可以看见Q要使用q个命o(h)包,我们好像用变量一栗在q个命o(h)包的使用中,命o(h)?#8220;run-yacc”中的“$^”是“foo.y”Q?#8220;$@”是“foo.c”Q有兌U以“$”开头的Ҏ(gu)变量Q我们会(x)在后面介l)Qmake在执行命令包Ӟ命o(h)包中的每个命令会(x)被依ơ独立执行?/pre>
]]>
大家已经?fn)惯于微软提供的功能强大的IDEQ已l很考虑手动~连目了,所谓技多不压nQ有I的时候还是随我一块了解一下命令行~译?/p>
C/C++/VC++E序员或有Unix/Linux~程l验应该很熟(zhn),以前我曾写过一文章描q用csc/vbc来进行命令行~译Q今天再介绍一下MS提供的更加快h效的~译工具NMake?
MSDN的描q? Microsoft E序l护实用工具 (NMAKE.EXE) 是一?32 位,Z说明文g中包含的命o(h)生成目的工兗?/p>
NMakeh丰富的选项Q可以完成复杂编译操作。它可以辨别源代码的改动Qƈ选择性的~译Qؓ(f)你节省大量不必要的编译时间?/p>
语法QNMAKE [options] [macros] [targets] [@commandfile]
说明Q其中,options是NMAKE的选项Q?a href="ms-help://MS.VSCC.2003/MS.MSDNQTR.2003FEB.2052/vccore/html/_asug_macros_and_nmake.htm" target=_blank>macros是在命o(h)行中的宏定义Qtargets是NMAKE的目标文件列表,commandfile是包含命令行输入的文本文Ӟ或响应文Ӟ?
NMAKE 使用指定 /F 选项的Makefile(生成文g,通常名字是makefile)Q如果未指定 /F 选项Q则使用当前目录下的Makefile。如果未指定MakefileQ则 NMAKE 使用推理规则生成命o(h)?targets?
NMake本n很简单,与NMAKE配合的是Makefile。Makefile的语法比较复杂,通常需要开发者自己手动编写MakefileQ下一节我们详l讲解Makefile?
上面的options和macros做了MSDN的链接,内容较多Q请自己查询相关,可以?a href="ms-help://MS.VSCC.2003/MS.MSDNQTR.2003FEB.2052/vccore/html/_asug_overview.3a_.nmake_reference.htm" target=_blank>q里q入NMake的MSDN帮助面Q在U帮助点q里?
注:(x)本节内容来自MSDNQ熟(zhn)此节的朋友可以直接跌
Makefile的组成部分包括:(x)成文件包含:(x)
a.描述?/strong>
描述块是后面可跟有命令块的依赖项行:(x)
targets... : dependents... commands...
依赖行指定一或多个目标以?qing)零或多个依赖项。目标必M于行首。用冒号 (:) 目标和依赖分开Q允怋用空格或制表W。若要拆分行Q请在目标或依赖后面用反斜杠 (\ )。如果目标不存在、目标的旉x依赖Ҏ(gu)或者目标是伪目标,?NMAKE 执行命o(h)。如果某依赖Ҏ(gu)其他地方的目标,q且不存在或对于自己的依赖项已过期,?NMAKE 在更新当前依赖项之前更新该依赖项?
如果依赖已q期Q则描述块或推理规则指定要运行的命o(h)块。NMAKE 在运行命令之前显C每个命令,除非使用?/S 选项?strong>.SILENT?strong>!CMDSWITCHES ?@。如果描q块后面没有紧跟命o(h)块,NMAKE 查扑配的推理规则?/p>
命o(h)块包含一个或多个命o(h)Q每个命令位于各自的命o(h)行上。在依赖(或规则)和命令块之间不能出现I。但是可以出现只包含I格或制表符的行Q该行被解释为空命o(h)Qƈ且不出现错误。命令行之间允许有空行?/p>
命o(h)行以一个或多个I格或制表符开始。后面紧跟着换行W的反斜?( \ ) 在命令中被解释ؓ(f)I格Q在行尾使用反斜杠l下一行命令。如果反斜杠后紧跟有其他M字符Q包括空格或制表W)Q则 NMAKE 按原义解释反斜杠?/p>
无论后面是否紧跟有命令块Q前面带分号 (;) 的命令可以出现在依赖行上或推理规则中:(x)
project.obj : project.c project.h ; cl /c project.c
c.?/strong>
宏用另一个字W串替换生成文g中的特定字符丌Ӏ用宏可以Q?
可以定义(zhn)?strong>自己的宏或?NMAKE ?strong>预定义宏?/p>
d.推理规则
推理规则提供命o(h)来更新目标ƈ推理目标的依赖项。推理规则中的扩展名与具有相同基名称的单个目标和依赖匹配。推理规则是用户定义的,或预定义的;预定义的规则可以重新定义?/p>
如果q期的依赖项没有命o(h)Qƈ且如?.SUFFIXES 包含依赖的扩展名,?NMAKE 使用其扩展名与当前或指定目录中的目标和现有文件匹配的规则。如果有多个规则与现有文件匹配,.SUFFIXES 列表确定用哪一个规则;列表优先U从左向x降序排列。如果依赖文件不存在Qƈ且未在另一个描q块中作为目标列出,则推理规则可以从h相同基名U的另一个文件创建缺的依赖V如果描q块的目标没有依赖项或命令,推理规则可以更新目标。即使不存在描述块,推理规则也可以生成命令行目标。即使指定了昑ּ依赖,NMAKE 也可以调用推理依赖项的规则?/p>
e.Ҏ(gu)?/strong>
在描q块之外的行首指定点指o(h)。点指o(h)以句?( . ) 开始,后面跟一个冒?(:)。允怋用空格或制表W。点指o(h)名区分大写q且应ؓ(f)大写?/p>
指o(h) | 作用 |
---|---|
.IGNORE : | 忽略从指定该指o(h)的位|到生成文g末尾之间Q由命o(h)q回的非雉Z码。默认情况下Q如果命令返回非雉Z码,NMAKE 暂停。若要还原错误检查,请?!CMDSWITCHES。若要忽略单个命令的退Z码,请用短划线 (-) 修饰W。若要忽略整个文件的退Z码,请?/I 选项?/td> |
.PRECIOUS : targets | 若更?targets 的命令暂停,则将 targets 保留在磁盘上Q若命o(h)通过删除文g处理中断Q则该指令无效。用一或多个空格或制表W分隔目标名U。默认情况下Q如果通过使用 CTRL+C ?CTRL+BREAK l合键中断生成,NMAKE 删除目标?strong>.PRECIOUS 的每一ơ用都应用于整个生成文Ӟ多次指定是篏计的?/td> |
.SILENT : | 取消从指定该指o(h)的位|到生成文g末尾之间的已执行命o(h)的显C。默认情况下QNMAKE 昄它调用的命o(h)。若要还原回显,请?!CMDSWITCHES。若要取消单个命令的回显Q请使用 @ 修饰W。若要取消整个文件的回显Q请使用 /S 选项?/td> |
.SUFFIXES : list | 列出推理规则匚w的扩展名Q预定义为:(x).exe .obj .asm .c .cpp .cxx .bas .cbl .for .pas .res .rc?/td> |
若要更改 .SUFFIXES 列表序或指定新列表Q请清除此列表ƈ指定新的讄。若要清除此列表Q请不要在冒号后指定扩展名:(x)
.SUFFIXES :
若要其他后~d到列表的末尾Q请指定
.SUFFIXES : suffixlist
其中 suffixlist 是附加后~的列表,׃或多个空格或制表W分隔。若要查?.SUFFIXES 的当前设|,误行选项?/P ?NMAKE?/p>
f.预处理指?/strong>
可以通过使用预处理指令和表达式控?NMAKE ?x)话。预处理指o(h)可以攄在生成文件或 Tools.ini 文g中。用指令可以有条g地处理生成文Ӟ昄错误信息Q包括其他生成文Ӟ取消定义宏以?qing)打开或关闭某些选项?/p>
看了一堆理论,很篏了吧Q下面看一D늮单的MakeFile
# 宏定? SOURCES=AssemblyInfo.cs \ Form1.cs \ Form2.cs \ Form3.cs \ HelloWorld.cs # 引用规则 # 目标: CLRProfiler.exe : $(SOURCES) #<-Q依赖项 # 标志 # 下面是命? csc /t:winexe /out:HelloWorld.exe /r:System.Windows.Forms.dll $(SOURCES) clean: del HelloWorld.exe |
上qC码保存ؓ(f)Makefile(没有后缀)攑֜你的目文g夹下, 然后打开VS2003.NET命o(h)行窗口,q入目Ҏ(gu)在\径,打入NMake回R, ok
CZ2
下面演示一下多个项目时的编译,每个单独的项目创建单独的makefileQ解x案下放一个ȝmakefile
all: # 分别寚w目进行编? cd project1 nmake cd .. cd project2 nmake cd .. cd project3 nmake cd .. # 编译结果汇d当前路径 copy project1\project1.dll copy project2\project2.dll copy project3\project3.exe clean: # 清除~译l果 del project1.dll del project2.dll del project3.exe cd project1 nmake clean cd .. cd project2 nmake clean cd .. cd project3 nmake clean cd .. |
本文单介l了NMAKE的用法,q对Makefile的语法做了介l。篇q所限,既不能面面俱刎ͼ又不能深入剖析,只希望能够让更多Z解此工具。笔者也是刚刚接触,l验不多Q还请各位网友多多拍砖!
W合 | 作用 |
---|---|
^ (caret) | 用于关闭某些字符所h的特D意义,使其只表C字面上的意义。例如:(x)^#abc表示#abcq个字符Ԍ?abc则用于在makefile中加入注释,#在这里ؓ(f)注释标志Q就像C++中的//。另外,在一行的末尾加上^Q可以行尾的回车换行符成ؓ(f)字串的一部分? |
# (number sign) | 注释标志QNMAKE?x)忽略所有从#开始到下一个换行符之间的所有文本。这里要注意的是Q在command lines中不能存在注释。因为对于command linesQNMAKE是将其整行传递给OS的。通常对于command lines的注释都是放在行与行之间? |
\ (backslash) | 用于两行合qؓ(f)一行。将其放在行,NMAKE׃(x)行回R换行W解释ؓ(f)I格QspaceQ?/td> |
% (percent symbol) | 表示其后的字W串Z文g名?/td> |
( (left parentheses) | |
) (right parentheses) | |
{ | |
} | |
! (exclamation symbol) | 命o(h)修饰W?/td> |
@ (at sign) | 命o(h)修饰W?/td> |
- (hyphen) | |
: (colon) | 用于dependent lines和inference rules中,用于分隔target和dependent?/td> |
; (semicolon) | 如果对于一个dependent line只有一条命令,则可以将该命令放在dependent line的后面,二者之间用“Q?#8221;分隔?/td> |
$ (dolor sign) | 用于调用?/td> |
DSW:全称是Developer Studio WorkspaceQ最高别的配置文gQ记录了整个工作I间的配|信息,Ҏ(gu)一个纯文本的文Ӟ在vc创徏新项目的时候自动生?br>DSP:全称是Developer Studio ProjectQ也是一个配|文Ӟ不过她记录的是一个项目的所有配|信息,U文本文?br>OPTQ与DSW、DSP配合使用的配|文Ӟ她记录了与机器硬件有关的信息Q同一个项目在不同的机器上的opt文g内容是不同的
CLWQ记录了跟ClassWizard相关的信息,如果丢失了clw文gQ那么在Class View面板里就没有cM?br>PLGQ实际上是一个超文本文gQ可以用Internet Explorer打开Q记录了Build的过E,是一个日志型文g
RCQ资源描q文Ӟ记录了所有的资源信息Q在资源~辑器里作的修改Q实际上都是对RC文g的修?br>RC2Q附加的资源描述文gQ不能直接资源编辑器修改Q只能手工添加,可以用来d额外的资?br>RESQ经q资源编辑器~译之后的资源文Ӟ以二q制方式存放
SBRQ编译器生成的浏览信息文Ӟ在代码导航的时候非常有用,奚w要在~译时指?FR或?Fr开?br>BSCQBSCMAKE.EXE所有的SBR文g作ؓ(f)输入Q经q处理之后输Z个BSC文gQ在代码D的时候实际用到的是BSC文g
ILKQ当选定渐增型编译连接时Q连接器自动生成ILK文gQ记录连接信?br>PDBQ全U是Program DataBaseQ即E序数据库文Ӟ用来记录调试信息Q是一个相当重要的文gQ没有他Q程序无法正常调?br>LIBQ如果项目输出是Dll的话Q一般会(x)输出一个跟目同名的Lib文gQ记录输出的函数信息
EXPQ同LibQ是跟Dll一L(fng)成的输出文g
PCHQ全U是PreCompiled HeaderQ就是预先编译好的头文gQ在~译时指?Yu开x~译器自动生?/p>