2/08/2006

单步调试每一段代码


尽管断言可以提示可能的问题,但是这意味着程序员需要预料代码中可能存在的问题——而程序员不可能预料到全部问题。最常用的办法还是用调试器来单步调试代码。这个优势是测试人员不具有的,他们只能使用各种各样的输入数据,然后检验输出。


但是单步测试所有的代码?


如果程序员对自己代码很自信,那么他们可以不对编写的代码进行单步测试——但是有多少人能保证自己的代码没有BUG?更不用说单步测试代码的时间和编写代码的时间相比只是九牛一毛了。


我知道习惯良好的程序员会编写异常处理代码。但是,所谓异常就意味着平常这些代码不会被执行到,所以在测试这部分代码时,可以手动编写抛出异常的代码,或者在调试器中修改保存函数返回值的变量的值,以及手动设置当前语句(但是注意这可能使得栈的状态和代码不匹配)。


应该设置断点的位置



  • 构造函数的结尾。每一个成员变量在运行到这里的时候都应该被初始化完毕,如果你在Visual C++中调试时看到一个成员变量具有0xcccccccc这样的值,那么你就忘记初始化这个成员变量了。
  • 函数的开头。如果你的函数工作不正常,检查函数的参数,看看是否符合你的函数的要求。
  • 条件分支语句。注意这不仅包含if语句,也包含隐含逻辑运算的||、&&和?:语句。这些语句的是程序流程分支的位置,而每个分支都需要被单步测试。如果分支之前的语句经过了测试,那么下一次测试的时候可以直接跳到分支的位置——当然也可以不这么跳以测试代码在不同的输入条件下的行为。

有时候需要调试发布版本的代码——调试版本的工作完全正常,但是发布版本的行为和调试版本不同。在单步调试发布版本的代码的时候,一个需要注意的问题是编译器的优化选项会造成单步调试的语句顺序和调试版本下的语句顺序略有不同。如果你觉得优化造成的语句次序颠倒使你无法调试代码,那么你可以关闭优化选项,但是为了尽可能使得调试的程序和发布的程序的行为保持一致,推荐的方法还是尝试习惯这种调试方式。


在Visual C++中,不仅可以在源代码视图进行单步调试,也可以在反汇编视图进行单步调试。在反汇编视图进行单步调试的优点是,这可以分拆?:这样的复杂语句的行为,而不是一次就跳过整个语句。


2/07/2006

Jiangsheng的CSDN Digest (Jan 7 2006)

CSDN 讨论总结系列:





可以让TWebBrowser只下载代码而不执行吗(Delphi 网络通信/分布式开发)




在用WebBrowser.Navigate打开一个网页后可否只让其下载网页的代码,而不执行并显示出来啊,要保证能用(WebBrowser.Document as IHTMLDocument2)调用网页的元素哦




不想显示网页,可以让WebBrowser1范围尽量小,放在按钮后就可以了。虽然这样看不到网页的显示了,但网页中的JS脚本还是会执行,有些还弹出广告窗口。只需要下载文件的话可以用URLDownloadToFile
只需要分析的话可以参考http://www.euromind.com/iedelphi/uilessparser.htm
也可以集成一个浏览器控件,在容器中处理DISPID_AMBIENT_DLCONTROL调用,返回DLCTL_DOWNLOADONLY标志,参考http://www.euromind.com/iedelphi/embeddedwb.htm。




递归开线程疑问(VC/MFC 进程/线程/DLL )




我用递归开线程,用下面的方法输出错误信息,结果是一堆“not enough space”错误
if(_beginthread(find_combination,0,(void*)p_my_data) == -1)
{
printf("strerror says open failed: %s\n", strerror(errno));
return;
}
问问,window里面一个进程理论上能开多少线程?




一个线程的开销包括:
内核模式下的开销(内核堆栈,对象管理所需内存)
用户模式下的开销(线程局部存储、线程环境块、堆栈、CRT、MFC、COM等等等等)

通常,线程数目的瓶颈在于线程自己的堆栈。Visual C++编译器默认设置是每个线程的堆栈大小是1兆。当然,如果你在创建线程时指定较小的堆栈大小,你应该可以创建较多的线程。

但是创建大量线程不是一个好的设计。每个线程创建和销毁的时候,Windows会调用已经加载的动态链接库的DLLMain,传递DLL_THREAD_ATTACH和DLL_THREAD_DETACH作为参数,除非动态库使用DisableThreadLibraryCalls禁用了这个通知。在创建大量线程的时候,这个开销是很大的。对于你这样的用后即弃的线程,你应该使用线程池。一个线程池示例可以在微软知识库找到(http://support.microsoft.com/support/kb/articles/Q197/7/28.asp)。




用 DirectShow 怎样生成 asf / wmv 的文件(VC/MFC 图形处理/算法)




graph 不能 run , 是不是 Asf Writer filter 的属性设置不对或不全 ?




DSCopy (DirectShow) Transcodes one or more files to an ASF file using the DirectShow® WM ASF Writer filter. The input file may be any compressed or uncompressed format supported by DirectShow.

DSPlay (DirectShow) This sample is an interactive audio/video media file player with DRM support. It uses DirectShow's WM ASF Reader filter to play Windows Media files (ASF, WMA, WMV) without DRM protection and files which use DRM at a level of 100 or below. See readme.txt in the sample's directory for more information.

DSSeekFm (DirectShow) This sample demonstrates how to use the DirectShow WM ASF Reader Filter to play ASF content in a DirectShow filter graph, and also how to use the frame seeking support in the Windows Media Format SDK.

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/wmform95/htm/sampleapplications.asp
 




如何读取xml的version等信息(VC/MFC HTML/XML )




如何读取xml的version等信息

我想知道怎么用MSXML,读取一个xml文件的processing instruction

即<?xml version="1.0"?>中的version等信息的值

这些信息可以用createProcessingInstruction来写,为什么找不到读取的方法




That particular line of your file isn't part of the resulting DOM after
you parse the document. But the XML version and encoding used should
be. If I read correctly, the version and encoding strings there will be
available in DOM version 3, which is not currently implemented fully to
my knowledge by any XML parser. However, if you're using Xerces, it has
methods in the Document interface for retrieving those -- and the API
clearly states that it's experimental.

http://groups.google.com/group/comp.text.xml/browse_frm/thread/85c1481c1b0763f0/c2506bd9ce7742b9?lnk=st&q=read+xml+version&rnum=11&hl=en#c2506bd9ce7742b9




一块很古老的板卡,有驱动源程序*.asm,现在怎样用vc++编译这个驱动以及怎么在接下来的程序中调用呢?(其他开发语言 汇编语言 )




以前一个师兄用borlandC++4.0编写了一个程序,只有源程序了,我怎么编译也编译不出来,可能是windows版本的问题(据说可能只有windows3.1之流能够很好的兼容,我用xp装borlandC++4.0就痛苦了好长时间(用兼容方式装成功),但是编译怎么也通不过),所以现在我想重新写这个程序,完成了一点点,碰到一个严重问题:板卡的驱动问题,那个师兄留下了两个*.asm文件,就是板卡的驱动,但是怎么用VC++里面编译,并可以运行这两个*.asm文件呢?以及接下来怎样在程序中进行调用呢?


我以前的师兄是这样使用的:
void far dacs(int ch,int Data);
int far adcs(int Data,int ch);
也就是ad/da卡。
然后再直接调用dacs(int ch,int data)和adcs(int data,int ch)函数就可以了,但是怎么将*.obj或者*.exe文件包括进去呢?



 newstm error LNK2019: unresolved external symbol _adcs referenced in function "public: int __thiscall motorDialog::gostep(void)" (?gostep@motorDialog@@QAEHXZ)
newstm fatal error LNK1120: 1 unresolved externals




How to use the Development Studio or Visual Workbench with MASM
http://support.microsoft.com/kb/q106399/


估计你这两个.asm还是16-bit的代码,
首先需要改成32-bit兼容的。
然后用masm或者tasm汇编成objs,加入到工程中,
然后在程序中加入外部引用(在.cpp或者.h中都可以):
extern "c" void far dacs(int ch,int Data);
extern "c" int far adcs(int Data,int ch);
另外可能调用约定也要改成.model flat,之后所有访问内存的代码都要改用flat模式,因为16-bit Windows的默认调用约定是Pascal。
你先看看.asm中的实现是按什么顺序处理参数、是否自己清除栈框架。你要把函数声明为公用的。一些汇编器默认将所有函数编译成私有的。把生成的obj文件加入到工程(从文件夹拖到file view)。




ATL的线程函数中 FIRE EVENT出错,其它类中FIRE EVENT 就OK ?(VC/MFC 进程/线程/DLL )




ATL和MFC都不是线程安全的
参考http://www.mvps.org/vcfaq/com/11.htm




以不同身份登陆域以获取不同的服务权限 (C++ Builder 基础类)




如何可以做到不转换Windows用户的情况下,以另一个用户的身份访问域服务器,并且结束时可以注销该用户身份。
参看了一些资料,可能要用到LogonUser,DuplicateTokenEx,CreateProcessAsUser这些函数




你可以用CreateProcessAsUser函数另外启动一个进程,之后使用进程间通讯(http://msdn.microsoft.com/library/en-us/ipc/base/interprocess_communications.asp )的方法来控制启动的程序。


参见http://www.xfocus.net/articles/200504/795.html


用LogonUser、CreateProcessAsUser,要SE_TCB_NAME权限,且即使你是administrator,也很难在程序中得到要SE_TCB_NAME权限。你可以在控制面板->管理工具->本地安全设置->用户权利指派中将"以操作系统方式运行"赋给你要设SE_TCB_NAME权限的用户,这样你上面的程序应该可以运行,但有安全隐患,因为SE_TCB_NAME是系统的最高权限。

建议用CreateProcessWithLogon。这需要本地登陆权限


void __fastcall TForm1::Button1Click(TObject *Sender)
{
STARTUPINFOW si;
PROCESS_INFORMATION pi;
memset(&si,0,sizeof(si));
si.cb=sizeof(si);

CreateProcessWithLogonW(L"yourusername",L"yourdomain",L"yourpassword",LOGON_WITH_PROFILE,L"cmd.exe",NULL,CREATE_DEFAULT_ERROR_MODE,
NULL,NULL,&si,&pi);
}
 


或用命令行的runas,
如:
runas /env /user:user@domain.microsoft.com "notepad \"my file.txt\""




分发软件时,需要将msvcr71.dll复制到system32目录下(Delphi 非技术区)




可是这个文件有可能已存在,也有可能该文件存在别的目录下正在被使用




http://support.microsoft.com/default.aspx?scid=kb;en-us;326922




inet控件下载ftp文件后,下载的文件被存放在IE缓存文件夹中,如何自动清除(VB 基础类 )




找到缓存文件夹,清空里面的文件即可


Private Sub Command2_Click()
'需要引用Microsoft Shell Controls And Automation
Dim mShell As New Shell32.Shell
Dim Temporary_Internet_Files As Shell32.Folder
Set Temporary_Internet_Files = mShell.NameSpace(32)
MsgBox Temporary_Internet_Files.Self.Path '这个就是路径
Set Temporary_Internet_Files = Nothing
Set mShell = Nothing
End Sub
 


http://vbnet.mvps.org/index.html?code/internet/deleteurlcache.htm


通过API访问IE Cache :

http://blog.csdn.net/technofantasy/archive/2002/03/29/2962.aspx



2/05/2006

BUG,规范,断言和调试

好长时间没更新BLOG了,向大家拜个晚年先。最近没怎么写代码,转几篇在网易虚拟社区发的文章过来充数。


对于BUG的自信


Donald E. Knuth(高德纳)在TeX: The Program的前言中说:
"我相信,在1985年11月27日,TeX代码里面的最后一个BUG已经被发现和解决了。但是,如果代码中仍旧有BUG,我很高兴付给任何第一个发现BUG的人20.48美元(这是前一个金额的两倍,而且我计划在一年内把它翻倍。你看,我很自信!)" 

想知道后来发生了什么吗?

http://truetex.com/knuthchk.htm可以看到他写出去的支票的金额是从2.56美元开始翻倍的。微基百科中关于这种支票的文章(http://en.wikipedia.org/wiki/Knuth_reward_check)说,截至2001年10月为止,他写出去了超过两千张这样的支票,但是他的BUG支票是如此有名,以至于很多人把他的支票收藏起来而不是拿出去兑现(http://www.tug.org/whatis.html)。

有多少程序员在发布产品的时候可以这样自信地声明产品没有问题?


遗憾的是,现在的程序员经常把发现BUG的责任推给测试人员——“不用担心,测试人员会发现所有BUG的,这是他们的工作”。实际上,测试人员并没有开发人员的条件,他们不可能进行源代码级别的调试,很大程度上只能靠运气——没错,是靠运气,如果一个BUG很容易被发现,程序员不太可能自己没有发现它——来发现BUG。


还有一些人干脆就认为BUG是不可避免的,或者认为不值得这么精益求精(参见网易虚拟社区http://p5.club.163.com/viewArticleByWWW.m?boardId=clanguage&articleId=clanguage_108eacc622169e7&boardOffset=0的讨论),但是实际上防止BUG出现的最好的时机,就是在编写代码的时候。在编写代码一段时间之后,即使是编写者本人也可能需要一段时间来理解代码(如果不习惯写注释的话,这段时间会更长),更别说定位问题所在了。在编写代码时,如果具有良好的习惯,可以免去很多在之后消灭BUG的困难。


规范不是语法


太多人把不要使用goto奉为圣旨,从来不想去打破。他们会争论,goto会造成难以维护的难读的代码,以及使编译器无法进行优化。这两点在很大程度上是真的,但是也有使用goto可以增加程序可读性和效率时候。在这种情况下,遵循“不使用goto语句”规范会产生更糟糕的代码。

一些人喜欢在成员函数后面加const,但是另外一些人没有养成这个习惯。一个直接的结果就是,一些看起来对对象完全没有影响的函数不能在const函数里面使用。这时候应该怎么办?看看Paul DiLascia建议的,把this指针强行转化为一个非const指针(http://www.microsoft.com/msj/archive/S126E.aspx)。如果函数实际上会对对象成员造成影响(例如CToolBar::GetItemRect),这也会带来潜在危险。

为了和ANSI标准之前编写的代码兼容,ANSI C中的memchr函数的声明为

void *memchr(
const void *buf,
int c,
size_t count
);


这里c是一个字符。很明显,标准为了兼容性放弃了明确性和更强的类型检查。如果放弃兼容性,这个函数应该声明为如下形式

void *memchr(
const void *buf,
unsigned
char c,
size_t count
);



微软的很多代码使用一种叫做匈牙利表示法的命名规范。这使得标识符的含义和类型更加明确——但是这是从广义的角度来说的。考虑如下函数声明

char *strcpy(
char *strDestination,
const char *strSource
);



如果严格遵循原始的匈牙利表示法,那么两个参数的声明应该是pch开头。但是以str开头给这两个参数更多含义:它们指向以\0为结束符的字符串。
   
规范是用来在大部分时间里遵循,以及在可以得到更好的结果时打破的。


编译警告的意义


智能化的编译器开始将语法正确的语句列为警告:

while(size-->0);//注意这里有个分号
*pTo++=*pFrom++;


编译器会报告空循环问题。
但是对于以0结尾的字符串复制

while(*pTo++=*pFrom++);


,这样的警告是多余的。
更加常见的警告是在条件判断语句中

if(ch='\0')
EndOfString();


为了绕过这个警告,需要添加额外的运算或者语句,或者更正错误的赋值。

while((*pTo++=*pFrom++)!='\0'){}
if(ch=='\0')


一些程序员甚至将比较语句修改成

if('\0'==ch)


这样作的原因显而易见:为了减少潜在的BUG。如果你的编译器没有这样的警告,那么你可以使用一些工具来检查那些语法正确但是有潜在BUG的代码。LintProject (http://www.codeproject.com/tools/lintproject.asp)就是其中一个。但是,良好的编程习惯还是减少BUG出现的最好的方法。

在觉得警告消息太烦人的时候,不妨想想编译器的开发人员为什么要编写这么多警告消息,而不是仅仅寻求关闭警告的方法。


P.S. Visual C++的默认警告等级是3级。发布软件之前应该改成4级,之后检查所有的编译警告。


无处不在的断言


使用编译器来捕获BUG的主意很好,Visual Studio 2005甚至会报告定义的变量不符合命名规范(Warning 1 CA1709 : Microsoft.Naming : Correct the casing of type name 'welcome'.);但是我敢打赌你检查BUG列表的时候,你会发现只有一小部分BUG会被编译器抓到。很多BUG在程序运行过程中很少会出现,例如内存分配失败的问题

char* strBuffer=new char [length];
MyZeroMemory(strBuffer,length);



这段代码在绝大多数情况下会成功,但是在虚拟内存不足的时候,Windows会报告“您的系统虚拟内存太低,WINDOWS会增加虚拟内存页面文件的大小。在这个过程中,一些应用程序的内存请求会被拒绝”然后开始增加虚拟内存,在这个过程中,new这样的内存分配可能会因为内存不足而失败,而MyZeroMemory则可能会造成访问越界。如果你足够幸运,你会在产品发布之前发现这个BUG,否则,你的用户会代替你发现这个BUG。要是用户刚好没有备份的习惯,丢失了几十分钟甚至是几小时的工作进度,用户会很生气,后果很严重。

编译器不能捕获这种运行时才会出现的错误(顺便说一下,我在CSDN上居然还看到有人抱怨编译器不会报告除0错误);也不能捕获算法中的BUG和检验参数中的数据。但要是你知道怎么做的话,这类问题很容易被发现。你可以用SetProcessWorkingSetSize函数或者msconfig工具减少虚拟内存大小,或者使用Virtual PC之类的虚拟机或者磁盘配额策略来模拟内存和磁盘空间不足的情况。

你有可能想在这种极限情况下调试你的代码,但是大多数时间内,内存分配不会失败,而设置条件断点又太麻烦了。这时候可以在代码里面加上一段用来在内存分配失败时触发调试器的断言代码

void MyZeroMemory(char* strBuffer, int length)
{
assert(strBuffer
!=NULL);
}



如果使用的是MFC或者ATL,建议使用对应的宏ASSERT和ATLASSERT。现在你可以编写健壮的代码使得程序在strBuffer这块内存分配失败时也能够正常运行。

现在的问题是,加入的这些代码增加了应用程序的大小,减慢了运行速度。在解决了内存分配失败造成的程序崩溃的问题之后,有必要在发布的版本中去掉这些断言代码。一个简单的办法是使用预处理标识符:

void MyZeroMemory(char* strBuffer, int length)
{
#ifdef DEBUG
assert(strBuffer
!=NULL);
#endif
}



这样你可以只维护同一份代码。当然,这也意味着调试的代码在发行版中会被去除,所以为了避免不可预料的行为,为了调试而加入的代码应该尽可能少地影响应用程序的行为。

你有可能需要重新定义assert来实现扩展的行为——例如在assert断言失败中断程序时打开源文件并且跳到assert那一行——这时候你可以编写自己的断言函数,然后重定义assert为这个断言函数。

#ifdef DEBUG
/*display a dialog and if the user selected break
, jump to the assert line
*/

void _assert(char*,unsigned int);
#define assert(f)\
if(f) {} else
_assert(__FILE__,__LINE__)
#else
#define assert(f)
#endif


空的if语句块可能看起来有点奇怪,但是这可以避免和宏外的if-else产生冲突。同样,最后一行语句没有结束的分号,因为在使用的时候再加上会更加自然。

assert最有用的地方就是用来检验函数的参数——但是也可以在其他地方起作用。在程序中的断言语句越多,异常的情况就越容易被侦测到。

既然assert是代码,它不可避免的需要注释。即使是自己写的代码,过了六个月之后再来审视也可能需要一点时间来重新理解这部分代码。一个简单的注释可以把这部分时间减少:

void MyZeroMemory(char* strBuffer, int length)
{
/*should not be called when buffer allocation failed*/
#ifdef DEBUG
assert(strBuffer
!=NULL);
#endif
}



在编写完函数之后,应该审视函数中的代码,之后在函数的开头验证函数正常运行所需的条件。如果你在写一个库函数,那么应该在函数的文档中加入函数正常运行所需的条件——否则就会增加使用者发现BUG的难度。举例来说,Windows API的文档不可谓不详尽,但是我在用汇编调用Windows API的时候,也花了很长时间才发现调用Windows API之前栈顶要设置成4的倍数。

注意不要把一些条件当成默认成立的了。assert(sizeof(int)==4);这样的语句在一些人看来很荒谬,但是在Windows开发中通常是32位的long在一些64位平台上已经是64位的了,而在目前还不知道sizeof(int)在什么时候会升位。如果你的代码依赖于int的大小,那么写上这行可以在未来升位之后更快发现问题。

一些保守的程序员在参数错误时会让函数继续运行——返回一个错误码——但是不报告错误。在编写核心模块时这可能很有必要,但是这也经常会把BUG藏起来——在多层函数返回之后时候,错误码经常会丢失或者被替代。尽量不要使用保守编程来替代断言,如果你认为保守编程会造成定位问题的困难,那么就加上断言代码。

在一些时候,校验参数数据似乎是不可能的事情——想象一下那些被设计来搞糊涂解密者的加密算法的中间数据——但是校验这种复杂算法的方法也不是没有。为了确认手算和心算的正确性,我们会使用电子计算器的结果来进行比较,反过来,我们也可以编写另外一个的算法来断言计算结果的正确性。这种方法也可以被用来断言一个函数的汇编版本和C版本的一致性——为了获取最大性能,函数的汇编版本的算法可能和C版本的有很大差异。当然,不是每个函数都有必要用这种方式来验证,实际上,只有极其重要的算法和对性能极其敏感的代码才会需要这种双保险来验证。同样,为了调试而加入的算法也应该尽可能少地影响应用程序的行为。

最后,你不应该在发布程序时从代码中去掉断言语句,而是把它们留在那里以供你升级或者查找BUG时使用。帖来自于网易社区:http://p5.club.163.com/viewArticleByWWW.m?boardId=clanguage&articleId=clanguage_10938d0575c4afe


消灭不可预料的行为


断言并不能抓住所有BUG——它们都是人写出来的,而是人就会犯错误。一些常见的错误包括:



  • 使用状态不确定的资源
  • 在释放资源之后继续访问资源
  • 在资源重新定位之后继续引用旧的资源
  • 申请资源之后丢失对资源的引用
  • 访问时未注意是否越界
  • 忽略错误信息

这些并不是杞人忧天的问题——实际上,这些问题属于日常开发中最常见的问题。这些问题的特点是,它们并不是时常造成程序行为的异常,并且症状不可重复。以内存为例,释放内存之后编译器和操作系统通常不会自动去擦除内存中的内容,所以继续访问内存不太可能造成程序行为的异常——直到内存被重新分配出去,而另一块代码开始重写这块数据。申请资源之后丢失对资源的引用可能只是造成长时间运行之后系统资源不足而已。另外,这些问题都是算法的问题,而编译器并不会替你校验你的算法,你自己也不太可能会怀疑你自己的算法。


不要认为职业的程序员就不会犯这类错误。举几个例子来说明这些问题。



  • 在IE4.0中,MSHTML的HTMLDocument对象的IPersistStreamInit::Load假定传入的IStream流的访问指针已经定位到开头——而在IE5.0中,IPersistStreamInit::Load会自行调用IStream::Seek
  • 在Visual C++ 6.0中,CHTMLView类有字符串资源未释放问题(http://support.microsoft.com/kb/241750)
  • 在Visual C++.Net中,CHTMLView类有两个BUG:


    • 参数传递错误:
      HRESULT CHtmlView::ExecFormsCommand(DWORD dwCommandID, VARIANT* pVarIn,
      VARIANT
      * pVarOut)
      {
      HRESULT hr
      = E_FAIL;


      CComPtr spDoc
      = (IHTMLDocument2*) GetHtmlDocument();
      if (spDoc != NULL)
      {
      CComQIPtr spCmdTarget
      = spDoc;
      if (spCmdTarget != NULL)
      hr
      = spCmdTarget->Exec(&CMDSETID_Forms3, dwCommandID,
      OLECMDEXECOPT_DONTPROMPTUSER, pVarOut, pVarIn);
      }



      return hr;
      }

      COM指针未释放错误:
      void CHtmlView::OnFilePrint()
      {
      // get the HTMLDocument
      if (m_pBrowserApp != NULL)
      {
      CComPtr spDisp
      = GetHtmlDocument();
      if (spDisp != NULL)
      {
      // the control will handle all printing UI
      CComQIPtr spTarget = spDisp;
      if (spTarget != NULL)
      spTarget
      ->Exec(NULL, OLECMDID_PRINT, 0, NULL, NULL);
      }

      }

      }


这些不确定的行为是很大一部分不可重复(因此也很难跟踪)的BUG的根源。举例来说,释放一块内存之后,在操作系统切换到另外一个线程之前,重新分配同一块内存,并且写入数据这个事件发生的可能性十分之小。为了解决这些问题,Visual C++编译器采取了一种保护性的措施,在调试模式下再分配和释放内存时将内存用一般不会用到的值填充,例如0xccccccc,0xdddddddd和0xfefefefe(参见编译器文档中的/RTC参数的说明)。这样你可以减少不可预料的程序行为,强迫BUG重现。如果你的编译器没有这么做,你可以自己编写一个调试模式下专用的内存管理程序进行这样的工作。为什么选择这样的值?在Intel系统中,0xcc的含义是int 3中断(参见http://blogs.msdn.com/oldnewthing/archive/2004/11/11/255800.aspx)——如果不小心执行了这块数据,那么程序会马上中断并且提示用户,其他的值则是典型的非法数据。如果你在为其他环境编写程序。你可能需要查阅一些资料来决定使用什么值来在调试模式下填充内存。


MFC的另一个保护措施是内存泄漏监测器——这也是在每个文件开头要加上#define new DEBUG_NEW的原因——但是这也变更了应用程序的行为。举例来说,为了检查内存泄漏,MFC总是分配比所需要多的内存,然后加入调试信息。如果你的程序有访问越界的代码,那么有可能擦除一部分额外分配的内存,可能的结果就是在调试模式下运行正常,而在发布模式下程序崩溃。当然,这是必须的。如果你的发布版本的程序依赖于这些额外字节,那么你就有麻烦了。


在发布模式下程序的崩溃有助于你发现问题,但是也造成定位问题的困难。你可以在发布模式下加入调试信息(没错,在工程的C++和连接选项中选中Program Database和Generate Debug Info)来生成一个中间版本;MSDN文章Generating and Deploying Debug Symbols with Microsoft Visual C++ 6.0(http://msdn.microsoft.com/library/en-us/dnvc60/html/gendepdebug.asp)甚至教你怎么怎么发布这样的版本,但是也要注意这样的版本和最终发布版本还是可能有区别的——特别是在程序中有BUG的情况下。另一个办法是在调试时将EIP寄存器修改成崩溃信息中的值,这样可以很容易在源代码中定位造成崩溃的代码的位置(参见http://www.codeproject.com/debug/XCrashReportPt1.asp)。


MFC开发中另一个比较有用的定位内存访问越界方法是,将数据封装成对象成员变量,尽量可能让所有类都从CObject派生,并且在代码中大量加上ASSERT_VALID。如果成员变量被越界的访问重写了,那么CObject指向AssertValid的虚函数表会被改写,而ASSERT_VALID会报告这个错误。


不要发布调试版本,这对用户来说并无意义。虽然这么说可能是多此一举,但是我在玩游戏的时候还真看见过断言对话框。调试信息是被设计用来发现问题的,不是用来隐藏问题的。如果你确实需要这么做(微软就定期发布核心模块的调试信息以供软件开发人员定位问题),那么你需要让用户认识到调试版本和最终发布版本的性能差异,例如在程序开始时显示一个消息。


广告时间:


最新Windows SDK不支持Visual C++ 6.0


可能大部分人已经知道了,但是CSDN论坛上仍旧不断出现关于这个兼容性的提问。最新的支持Visual C++ 6.0的版本是2003年2月版,下载地址是http://www.microsoft.com/msdownload/platformsdk/sdkupdate/psdk-full.htm


取消对Visual C++ 6.0的支持的原因是为了支持新的/GS参数。XP SP2和Windows Server 2003 SP 1都增加了很多安全特性,以致于新的Windows SDK中包含的编译器和库文件不再和Visual C++ 6.0兼容。


参见