关闭

调试

标签: mfc工具build编译器编程文档
903人阅读 评论(1) 收藏 举报
概述
调试是一个程序员最基本的技能,其重要性甚至超过学习一门语言。不会调试的程序员就意味着他即使会一门语言,却不能编制出任何好的软件。
这里我简要的根据自己的经验列出调试中比较常用的技巧,希望对大家有用。
本文约定,在选择菜单时,通过/表示分级菜单,例如File/Open表示顶级菜单File的子菜单Open。

设置
为了调试一个程序,首先必须使程序中包含调试信息。一般情况下,一个从AppWizard创建的工程中包含的Debug Configuration自动包含调试信息,但是是不是Debug版本并不是程序包含调试信息的决定因素,程序设计者可以在任意的Configuration中增加调试信息,包括Release版本。
为了增加调试信息,可以按照下述步骤进行: 

打开Project settings对话框(可以通过快捷键ALT+F7打开,也可以通过IDE菜单Project/Settings打开) 

选择C/C++页,Category中选择general ,则出现一个Debug Info下拉列表框,可供选择的调试信息 方式包括: 


命令行 Project settings 说明
无 None 没有调试信息
/Zd Line Numbers Only 目标文件或者可执行文件中只包含全局和导出符号以及代码行信息,不包含符号调试信息
/Z7 C 7.0- Compatible 目标文件或者可执行文件中包含行号和所有符号调试信息,包括变量名及类型,函数及原型等
/Zi Program Database 创建一个程序库(PDB),包括类型信息和符号调试信息。

/ZI Program Database for Edit and Continue 除了前面/Zi的功能外,这个选项允许对代码进行调试过程中的修改和继续执行。这个选项同时使#pragma设置的优化功能无效


选择Link页,选中复选框"Generate Debug Info",这个选项将使连接器把调试信息写进可执行文件和DLL 
如果C/C++页中设置了Program Database以上的选项,则Link incrementally可以选择。选中这个选项,将使程序可以在上一次编译的基础上被编译(即增量编译),而不必每次都从头开始编译。 
断点
断点是调试器设置的一个代码位置。当程序运行到断点时,程序中断执行,回到调试器。断点是 最常用的技巧。调试时,只有设置了断点并使程序回到调试器,才能对程序进行在线调试。

设置断点:可以通过下述方法设置一个断点。首先把光标移动到需要设置断点的代码行上,然后 

按F9快捷键 
弹出Breakpoints对话框,方法是按快捷键CTRL+B或ALT+F9,或者通过菜单Edit/Breakpoints打开。打开后点击Break at编辑框的右侧的箭头,选择 合适的位置信息。一般情况下,直接选择line xxx就足够了,如果想设置不是当前位置的断点,可以选择Advanced,然后填写函数、行号和可执行文件信息。 
去掉断点:把光标移动到给定断点所在的行,再次按F9就可以取消断点。同前面所述,打开Breakpoints对话框后,也可以按照界面提示去掉断点。

条件断点:可以为断点设置一个条件,这样的断点称为条件断点。对于新加的断点,可以单击Conditions按钮,为断点设置一个表达式。当这个表达式发生改变时,程序就被中断。底下设置包括"观察数组或者结构的元素个数",似乎可以设置一个指针所指向的内存区的大小,但是我设置一个比较的值但是改动范围之外的内存区似乎也导致断点起效。最后一个设置可以让程序先执行多少次然后才到达断点。

数据断点:数据断点只能在Breakpoints对话框中设置。选择"Data"页,就显示了设置数据断点的对话框。在编辑框中输入一个表达式,当这个 表达式的值发生变化时,数据断点就到达。一般情况下,这个表达式应该由运算符和全局变量构成,例如:在编辑框中输入 g_bFlag这个全局变量的名字,那么当程序中有g_bFlag= !g_bFlag时,程序就将停在这个语句处。

消息断点:VC也支持对Windows消息进行截获。他有两种方式进行截获:窗口消息处理函数和特定消息中断。
在Breakpoints对话框中选择Messages页,就可以设置消息断点。如果在上面那个对话框中写入消息处理函数的名字,那么每次消息被这个函数处理,断点就到达(我觉得如果采用普通断点在这个函数中截获,效果应该一样)。如果在底下的下拉 列表框选择一个消息,则每次这种消息到达,程序就中断。


Watch
VC支持查看变量、表达式和内存的值。所有这些观察都必须是在断点中断的情况下进行。
观看变量的值最简单,当断点到达时,把光标移动到这个变量上,停留一会就可以看到变量的值。
VC提供一种被成为Watch的机制来观看变量和表达式的值。在断点状态下,在变量上单击右键,选择Quick Watch, 就弹出一个对话框,显示这个变量的值。
单击Debug工具条上的Watch按钮,就出现一个Watch视图(Watch1,Watch2,Watch3,Watch4),在该视图中输入变量或者表达式,就可以观察变量或者表达式的值。注意:这个表达式不能有副作用,例如++运算符绝对禁止用于这个表达式中,因为这个运算符将修改变量的值,导致软件的逻辑被破坏。

Memory
由于指针指向的数组,Watch只能显示第一个元素的值。为了显示数组的后续内容,或者要显示一片内存的内容,可以使用memory功能。在 Debug工具条上点memory按钮,就弹出一个对话框,在其中输入地址,就可以显示该地址指向的内存的内容。

Varibles
Debug工具条上的Varibles按钮弹出一个框,显示所有当前执行上下文中可见的变量的值。特别是当前指令涉及的变量,以红色显示。

寄存器
Debug工具条上的Reigsters按钮弹出一个框,显示当前的所有寄存器的值。

进程控制
VC允许被中断的程序继续运行、单步运行和运行到指定光标处,分别对应快捷键F5、F10/F11和CTRL+F10。各个快捷键功能如下: 
  


快捷键 说明

F5 继续运行

F10 单步,如果涉及到子函数,不进入子函数内部

F11 单步,如果涉及到子函数,进入子函数内部

CTRL+F10 运行到当前光标处。


Call Stack
调用堆栈反映了当前断点处函数是被那些函数按照什么顺序调用的。单击Debug工具条上的Call stack就显示Call Stack对话框。在CallStack对话框中显示了一个调用系列,最上面的是当前函数,往下依次是调用函数的上级函数。单击这些函数名可以跳到对应的函数中去。

其他调试手段
系统提供一系列特殊的函数或者宏来处理Debug版本相关的信息,如下: 


宏名/函数名 说明

TRACE 使用方法和printf完全一致,他在output框中输出调试信息

ASSERT 它接收一个表达式,如果这个表达式为TRUE,则无动作,否则中断当前程序执行。对于系统中出现这个宏 导致的中断,应该认为你的函数调用未能满足系统的调用此函数的前提条件。例如,对于一个还没有创建的窗口调用SetWindowText等。

VERIFY 和ASSERT功能类似,所不同的是,在Release版本中,ASSERT不计算输入的表达式的值,而VERIFY计算表达式的值。


关注
一个好的程序员不应该把所有的判断交给编译器和调试器,应该在程序中自己加以程序保护和错误定位,具体措施包括: 

对于所有有返回值的函数,都应该检查返回值,除非你确信这个函数调用绝对不会出错,或者不关心它是否出错。 
一些函数返回错误,需要用其他函数获得错误的具体信息。例如accept返回INVALID_SOCKET表示accept失败,为了查明 具体的失败原因,应该立刻用WSAGetLastError获得错误码,并针对性的解决问题。 
有些函数通过异常机制抛出错误,应该用TRY-CATCH语句来检查错误 
程序员对于能处理的错误,应该自己在底层处理,对于不能处理的,应该报告给用户让他们决定怎么处理。如果程序出了异常, 却不对返回值和其他机制返回的错误信息进行判断,只能是加大了找错误的难度。 
另外:VC中要编制程序不应该一开始就写cpp/h文件,而应该首先创建一个合适的工程。因为只有这样,VC才能选择合适的编译、连接 选项。对于加入到工程中的cpp文件,应该检查是否在第一行显式的包含stdafx.h头文件,这是Microsoft Visual Studio为了加快编译速度而设置的预编译头文件。在这个#include "stdafx.h"行前面的所有代码将被忽略,所以其他头文件应该在这一行后面被包含。
对于.c文件,由于不能包含stdafx.h,因此可以通过Project settings把它的预编译头设置为"不使用",方法是: 

弹出Project settings对话框 
选择C/C++ 
Category选择Precompilation Header 
选择不使用预编译头。

MFC 断言的应用(转载)

一、关于断言

   所谓的断言就是可以肯定为正确的一个陈述语句。

   假设某个函数需要一个指向文档对象的指针作为参数,但却错误地使用了一个视图指针来调用该函数。如果函数继续使用该错误地址,轻则程序运行得不到正确的结果,重则破坏视图数据。由于这种错误往往要到使用视图数据时才会发现,因而要找出导致错误的根本原因就要付出相当大的代价了。

   只要在函数开始部分加入断言检查,检验指针是否真正指向一个文档对象,就可以完全地避免此类问题的产生。

   二、ASSERT宏

   ASSERT宏能够计算作为参数传递的表达式值。如果表达式为真,则执行继续。否则,程序显示一个消息并中断。此时可以选择忽略错误、终止程序或进入调试器。下面是如何在函数中应用ASSERT宏验证参数的一个例子:


void foo(char* p,int size)
{
ASSERT(p != 0); // 验证缓冲区指针
ASSERT((size >= 100); // 确认缓冲区大小至少为100字节
// foo 函数的其它计算过程
}

   如果没有定义_DEBUG预处理符,则该语句不会真正生成代码。Visual C++会在调试模式编译时自动定义_DEBUG,而在发行模式下,该预处理符是不存在的。如果定义了_DEBUG,则上述两个断言生成的代码类如:

//ASSERT(p != 0);
do
{
if(!(p != 0) && AfxAssertFailedLine(__FILE__, __LINE__))
AfxDebugBreak();
} while(0);

//ASSERT((size >= 100);
do
{
if(!(size >= 100) && AfxAssertFailedLine(__FILE__,__LINE__))
AfxDebugBreak();
}while(0);

   do-while结构在一个单独的语句块之内封装了整个断言操作。if语句计算断言表达式,如果值为0则调用AfxAssertFailedLine()。AfxAssertFailedLine()显示消息框并提供 “Abort,Retry,or Ignore”选择,如果选择Retry则调用AfxDebugBreak(),并由此激活调试器。

   和AfxAssertFailedLine()的简单目的(即显示对话框以供选择)相比,它的实现显得非常复杂。该函数的源代码在afxasert.cpp内,可以发现它使用了一些特殊的函数以确保消息框正确显示。举个例子,如果断言失败是在程序发送WM_QUIT消息之后的某个位置,则AfxAssertFailedLine()为了显示消息框必须临时地从队列中删除这个消息。

   传递给AfxAssertFailedLine()的参数__FILE__ 和 __LINE__分别为代表程序文件名和当前行号的预处理符号。它们由ANSI标准定义,由编译器自动生成具体数值。

   三、VERIFY宏

   由于ASSERT仅在程序的调试版起作用,测试表达式总是被动的。也就是说,它们不能包含赋值、增量、减量等真正改变数据的操作。但有时候我们需要验证一个主动表达式,比如赋值语句。这时可以使用VERIFY代替ASSERT。下面是一个例子:


void foo(char* p,int size)
{
char* q; // 指针的副本
VERIFY(q = p); // 拷贝指针并执行验证
ASSERT((size >= 100); // 确保缓冲区大小至少为100字节
// 执行 foo 的其它操作
}

   在调试模式下ASSERT和VERIFY是相同的。但在发行模式下,VERIFY能够继续对表达式求值(但不再进行断言检验),而ASSERT语句在效果上就如同已经删除了一样。

   尽管在MFC源代码中可以找到一些应用VERIFY的例子,但ASSERT用得更为普遍。一些程序员总是完全避免使用VERIFY,因为他们已经习惯于使用被动断言。请记住,如果在ASSERT语句中使用了主动表达式,编译器不会发出任何警告。在发行模式下编译时该表达式会被直接删除,从而导致程序运行的错误。由于发行版程序不含调试信息,这种类型的错误是很难找到原因的。

   四、DEBUG_ONLY宏

   可以认为DEBUG_ONLY宏是ASSERT宏的一种特殊格式,它用于计算表达式而不执行断言检查。这在只为调试目的而加入某语句时很有用,如:


void foo(char* p,int size,char fill)
{
char* q; // 指针副本
VERIFY(q = p); // 拷贝指针并执行验证
ASSERT((size >= 100); // 确认缓冲区大小至少为100字节
ASSERT(isalpha(fill)); // 确保fill为字母
DEBUG_ONLY(if(!isalpha(fill)) fill = 'X'); // 如果fill值非法,则指定为'X'
// 执行 foo 的其它操作
}

   在上例中,即使第三个参数非法,调试过程仍可继续。事实上,很少有程序员喜欢用这个宏。他们更习惯于使用传统的方法,即使用_DEBUG预处理符使得调试代码只在调试时被编译:

 
void foo(char* p,int size,char fill)
{
char* q; // 指针副本
VERIFY(q = p); // 拷贝指针并执行验证
ASSERT((size >= 100); // 确认缓冲区大小至少为100字节
ASSERT(isalpha(fill)); // 确认fill为字母
#ifdef _DEBUG
if(!isalpha(fill)) fill = 'X'); // 如果fill值非法则使其为'X'
#endif
// 执行foo的其它操作
}

   五、ASSERT_VALID宏

   ASSERT在执行简单验证时很有用,但对于C++对象,特别是由CObject派生的对象,则有更好的方法来实现类似操作。作为一般规则,我们应在开始使用每一个对象之前检查数据讹误。ASSERT_VALID宏使得对CObject的派生类实现该操作非常简单,其过程如下所示:

  
void CMyView::foo(CYourView* pView) // CMyView 和 CYourView 从CObject 继承
{
ASSERT_VALID(this); // 验证本身
ASSERT_VALID(pView); // 验证pView
// 执行foo的其它操作
}

   如下所示,这些宏直接调用AfxAssertValidObject():
   void CMyView::foo(CYourView* pView)
{
//ASSERT_VALID(this); // 验证本身
AfxAssertValidObject(this,__FILE__,__LINE__);
//ASSERT_VALID(pView); // 验证pView
AfxAssertValidObject(pView,__FILE__,__LINE__);
// 执行foo的其它操作
}

   AfxAssertValidObject()的定义可以在objcore.cpp文件找到,它是一个没有正式说明文档的MFC函数。AfxAssertValidObject()首先检查指针非空(NULL)且指向一个合法的内存地址,其大小符合由相关的CRuntimeClass对象指定的数值。如果这些测试失败,则AfxAssertValidObject()的行为就像一个普通断言错误- -即调用AfxAssertFailedLine(),有可能还要调用AfxDebugBreak();否则,AfxAssertValidObject()调用虚函数AssertValid(),可以覆盖后者以执行其它的完整性检查。

   如果使用应用向导或类向导生成基于MFC的类,通常会得到AssertValid()的骨架,最好改写这些骨架代码以增加最基本的完整性检查。下面是一个典型的例子,类Sample从CObject继承,假定它含有职员名字及其薪水:
class Sample : public CObject
{
protected:
CString m_Name; // 职员名字
double m_Salary; // 薪水

public:
Sample(LPCTSTR name,double salary) : m_Name(name), m_Salary(salary) {}
#ifdef _DEBUG
virtual void AssertValid() const;
#endif

#ifdef _DEBUG
void Sample::AssertValid() const
{
CObject::AssertValid(); // 验证基类
ASSERT(!m_Name.IsEmpty()); // 验证职员名字
ASSERT(m_Salary > 0); // 验证薪水
}
#endif

   显然,依赖于派生类的数据成员,这些AssertValid()函数可以更为复杂。由于该函数只在调试版起作用,因而无需担心由此产生的时间开销。然而,在某些热点如CView::OnDraw()函数,MFC会频繁地验证其CDocument指针,此时在文档对象内应该避免冗长的测试。当然,如果正确地使用了文档/视图结构,许多应用数据将会保存在CDocument的派生类对象中,此时妥协和折衷还是必要的。

   六、ASSERT_KINDOF宏

   许多时候只需要验证指针确实引用了所希望的对象类型,以确保可以安全地访问该对象的成员。ASSERT_KINDOF能够完成该检查,而且时间开销比ASSERT_VALID要少。其调用形式如下:

   void Sample::DoSomething(CMyDocument* pDoc) const{ASSERT_KINDOF(CMyDocument,pDoc);}

   ASSERT_KINDOF要求所检查的指针指向从CObject派生的对象,而且该对象实现MFC运行时类信息声明。这种声明通常是通过在类声明中包含DECLARE_DYNAMIC宏,在实现文件包含IMPLEMENT_DYNAMIC宏来实现的。应用向导(或类向导)能够为大多数文档和视图类自动生成该声明。

   下面是由ASSERT_KINDOF生成的真正代码:
void Sample::DoSomething(CMyDocument* pDoc) const
{
//ASSERT_KINDOF(CMyDocument,pDoc) 生成以下代码
ASSERT(pDoc->IsKindOf(RUNTIME_CLASS(CMyDocument)));
// 现在可以确认 pDoc 指向 CMyDocument 对象
// 其它操作
}

细谈VC程序调试的若干方法(转载)

记得我刚开始用VC编程时,多次遇到程序编译链接都没有任何错误提示时运行却发生意外错误或者陷入死机状态,对此我真是茫然不知所措。后来经过项目工程的“洗礼”,我学到了一些解决这类错误的方法同时也将自己积累的经验运用到开发工作中,从而避免了重蹈覆辙,编程效率得以明显提高。如果你用VC而不精通解决上述种种莫名其妙故障的调试技术,那么软件的进度和质量将无法保障。VC调试之重要不言而喻。

  其实VC所依赖的开发平台Microsoft Developer Studio本身提供的调试功能并不弱,每当我们创建一个新的VC工程项目时,默认状态就是Debug(调试)版本,在"组建"(Build)菜单下的Configurations中可以看到除了调试版本还可以设置成发行(Release)版本。调试版本由于包含了大量信息,所以它生成的可执行程序容量会远远大于发行版。

  具体地,调试版本主要增加了两个内容:其一,会执行编译命令_D_DEBUG,使头文件的调试语句ifdef及其代码附加到程序中;其二,在可执行文件中加入的调试信息使开发人员能够观察变量,进行单步执行等。在VC"组建"(Build)菜单下的"开始调试"中有4条专用的调试命令:Go,Step into,Run to Cursor,Attach to process…。在运行程序源代码时用Go命令(而不是Execute)才能处于调试状态, Go命令会使程序运行变得缓慢下来,但可以更好地控制运行程序,我们可以在任何时刻中断程序、单步执行、查看变量、检查调用栈。

  有必要详细介绍一下VC的调试功能:首先,再次强调要用Go命令运行一个将要调试的程序;如果要中止调试状态下的运行程序可以点击Stop Debugging命令,还可以通过Break选项以可恢复方式中断调试程序的运行流程(用Restart选项可以重新开始运行程序);Step Into选项表示每次只执行一行语句(单步执行),但如果当前代码是调用一个函数,那么Step Into表示进入该函数,全部函数语句执行完后返回,而Step Over则是跳出这个函数;Step To Cursor选项表示程序将执行到光标所在的可执行语句行上;在调试多线程程序时,可以在线程函数或主应用程序线程中设置断点,还可以用Break选项结束线程后用Threads选项查看运行线程列表,也可以选择悬挂和恢复每个线程;在设置断点后,在VC "查看"菜单的"调试窗口"中可以查看变量、内存、调用堆栈、寄存器以及反汇编语句。在程序中设置断点的方法是,点击要设置的代码行并点击设置代码的工具栏按钮,会出现在代码行最左边的一个小黑点即是断点标志,这时再选Go程序会在执行到端点处停下来,如果要继续执行可以再选Go。

  通过选择VC"工具"菜单下的"源浏览器"可以生成一个.BSC文件,使用浏览器可以从中发现多种信息:程序中任何一个变量、函数、类或宏在何处定义及引用;可以列出所有声明的函数类、变量、宏;可以发现调用一个指定函数的所有函数;可以找到一个指定类的派生来源或者它派生出哪些类。

在使用微软程序开发库MSDN时,我们会发现其中的VC示例经常采用看似多余的ASSERT语句,其作用就是使程序具有"维护"性。对于Debug版本的VC程序,在遇到布尔值为FALSE的ASSERT语句处停止,并显示Assertion Failed对话;如果设置为发布版,所有ASSERT语句都会被预处理程序删除。一个地道的VC编程员,应该有意在自己的代码中通过"维护"特征去检测任何设定,诸如输入参数、循环范围和变量值的设定。

  在安装好VC系统之后,在VC之外的程序组中有一个程序Tracer是一个跟踪工具,在激活它后使用Go运行VC代码,在输出窗口就能够看到程序运行过程中的内部过程,包括DLL调用等,你如果看不到任何输出,可以转到菜单"查看"(View)点击"输出"(Output)。

  其实,MFC自身就提供有错误查找和TRACE语句,而TRACE语句的语法与printf非常类似,所以我们可以在程序中直接加入这条跟踪命令,如下所示:

// Example for TRACE
int i = 1;
char sz[] = "one";
TRACE( "Integer = %d, String = %s/n", i, sz );
// Output: 'Integer = 1, String = one'


  在Developer Studio中还提供了一个ERRLOOK工具,程序员只要输入错误号就能得到系统出错信息或模块错误内容。

  MFC从Cobject派生的每个类都包含一个Dump函数,该函数可把当前状态转储(Dumping)到输出窗口,这在某些调试过程中会有用,以下代码是Dump函数的用法:

// Example for CObject::Dump
void CAge::Dump( CDumpContext &dc ) const
{
 CObject::Dump( dc );
 dc << "Age = " << m_years;
}


  在MFC中还有一个非常有用的类是CMemoryState,我们可以在程序的任何部分使用这个类检测内存冲突,并得到内存冲突的确切位置。CMemoryState类有3个成员函数:CheckPoint可将堆的当前状态存入类的实体;Difference可以比较两个实体包含的堆之间的差异;DumpStatistics用于标准化转储所有被CheckPoint捕获后分配到堆的对象,如CheckPoint未被调用实体未被初始化时,该函数将转储当前堆的所有内容。以下代码表示了CMemoryState类的使用方法:

// Example for CMemoryState::CMemoryState,
// Includes all CMemoryState functions
CMemoryState msOld, msNew, msDif;
msOld.Checkpoint();
CAge* page1 = new CAge( 21 );
CAge* page2 = new CAge( 22 );
msOld.DumpAllObjectsSince();
msNew.Checkpoint();
msDif.Difference( msOld, msNew );
msDif.DumpStatistics();


  代码运行的结果为:

Dumping objects ->
{2} a CObject at $190A
{1} a CObject at $18EA
Object dump complete.
0 bytes in 0 Free Blocks
8 bytes in 2 Object Blocks
0 bytes in 0 Non-Object Blocks
Largest number used: 8 bytes
Total allocations: 8 bytes


  在MFC类和VC中本身就有"异常情况"这个概念,并在此基础上形成它们处理系统错误和意外的主要机制。比如当系统内存分配殆尽时,你的运行程序就会收到内存异常的消息。这样就给了程序员消除异常的机会。

  MFC中的异常情况主要有:CArchiveException表示档案文件载入或保存时出错,CDBException属于数据库错误,CFileException为文件错误,CMemoryException为调用new时发生分配错误,CNotSupportedException表示指定操作不被支持,COleException表示在调用OLE操作时出错,COleDispatchException表示在OLE自动操作时出错,CResourceException表示资源找不到或无法创建,CUserException用于通知用户错误。

  MFC还包含一系列以Afx-为词头的调试函数:AfxAbort可以在发生致命错误时异常终止程序,AfxCheckMemory可以检查堆和剩余缓冲池的受损部分;AfxDoForAllClasses重声明所有CObject的派生类;AfxDoForAllObject重声明堆上所有CObject派生的对象;AfxEnableMemoryTracking启用或禁止内存追踪;AfxIsMemoryBlock用于确认指针所指内存有效;AfxIsValidAddress用于确认地址是驻留在程序的内存区域内;AfxIsValidString用于确认地址所指字符串有效;AfxSetAllocHook用于内存分配前进行检测;AfxTraceEnabled启动或禁止输出跟踪,AfxTraceFlags则进一步定制跟踪特征。

在我们随手编制的VC程序中,普遍存在着会发生内存泄漏的隐患,有些问题程序的痼疾症状是在处理数据量激增时陷入瘫痪,更糟的要发现内存泄漏并不容易。首先,我们要明确VC中内存泄漏的含义:简单说就是一个程序申请得到了一段内存却没有及时释放。比如用new在堆中分配了一个对象或对象组却并没有调用delete操作。灵活的指针技术使内存泄漏的原因变得复杂化,比如改变了保存在一变量中的指针的值后未能删除指针所指向的内存区;当内存泄漏是来自一个带有指针类型成员变量的类时会更加困难,因为当调用分配指针时并没有复制构造函数/析构函数或运算符。

  为了防止发生内存泄漏这样棘手的故障,在VC编程时应当注意遵循几个规范:其一,如果一个类包含有指针并且分配了指针值,那么就需要构造相应的析构函数以删除该指针;其二,如果一个函数分配了一块内存并把该内存块返回给调用它的函数使用,那么它返回的必须是一个指针而非一个引用,因为引用不能被程序删除;其三,即使一个函数分配了一段内存并在同一函数的稍后部分删除了该内存段,也要尽可能将内存块分配到堆栈中;最后,就是决不要试图改变一个指针值,除非已经删除指针所指的对象或通过数组指向了该指针所指向的内存,而且也不要对new返回的指针进行加1运算。

  每当编写VC程序时,我们都会处于一个琳琅满目的集成开发环境(IDE)中,现实的真相是我们很多人在这里编程多年,对开发环境了解并不全面和细微。记得王朔的小说中有句话说"穿了多年的外套在不穿时才发现它原来还有一个兜!"。我们在安装VC时,得到的IDE即Developer Studio, VC其实是Developer Studio下激活的一个组件而已,比如微软的VJ++也是基于Developer Studio。很少有技术书籍会一一介绍Developer Studio界面元素,也许聪明的程序员轻易就能识别其含义,全部猜对界面图符的含义并非易事。可是它们对我们了解开发信息很重要,也与调试程序有关联。

  在Developer Studio下会生成多个文件去保存项目的所有信息:一个是以.DSW为扩展名的项目工作区文件,它包含项目中所有文件的名称、文件所在目录、编译器和连接器的选项以及项目工作的其它信息;以.DSP为扩展名的也是项目记录文件,.OPT是工作区选项文件,它包含Developer Studio的所有个人设置 - 包括颜色、字体、工具栏、哪个文件被打开以及MDI窗口如何被定位和最新调试中的断点等。在打开项目工作区文件时其它文件随即会自动打开。在Developer Studio下可以按类查看代码,其中的ClassView显示了应用程序中所有的类,每个类下显示了成员函数和数据成员,在成员函数旁有粉红图标,数据成员旁是蓝绿色图标,保护类成员的图标旁有一枚钥匙,私有类成员则有一个挂锁图标。

  当然,在开发环境下最主要的工作是输入编辑程序源代码,源代码会显示"语法着色"。在缺省情况下,代码为黑色,夹以绿色的注释和蓝色的关键字(指VC所保留的public、private、new和int等等)。--这些地球人都知道,但是为了调试需要,我们还可以指定颜色去显示字符串、数字和运算符。定义方法是通过Tools菜单下的Options对话框中的Format选项卡设置。

  在Developer Studio提供的诸多菜单项中,我们往往对少数菜单避而不用,因为不了解它们的作用唯恐好奇心会造成乱子;还有多个菜单项都可以达到目的,至于它们之间的微小差别则不甚了了。比如在编译和调试时常用到Build菜单组,它具有和应用程序编译、运行调试相关的多项操作:其中的Compile菜单会编译当前的聚焦文件;Build菜单会编译和链接所有在项目中修改的文件;Build All会编译链接项目中所有文件,包括最近编译后没有修改过的文件;Batch Build用于包含有Debug和Release配置的项目;Clean会删除所有的中间和输出文件,因而项目目录下仅包含源文件;Debugger Remote Connection用于远程调试,即在一台机器运行程序而在另一台调试;Set Active Configuration可设置某个配置为激活状态(Debug或Release);Profiler能够识别应用程序的瓶颈,即找到降低程序执行速度的代码和有关模块,为此需要在"工程/设置/Link"下勾选Profiling。

  VC中的警告信息是有级别的。在"工程/设置"下的"C/C++"选项卡中的警告级别Warning Level缺省值是3,如果改为更为严格的4级,往往会产生更多的警告类错误。在C/C++选项卡中还提供了代码优化栏Optimizations,在你完成漫漫调试之旅准备正式发布前夕,你应当改动此项,它提供了一些适合建立发行版应用程序的优化设置。

  其实,在多年以前,在软件业中针对开发具有一定规模软件项目的情况就出现了软件工程理论,以此来指导软件人员树立团队协作意识,进而保证软件项目的协调性及进度质量等。笔者在从事VC开发中对此很有感触,而且觉得对于每个开发人员自己也应当具备一定的工程素养。比如,我们在开发初期时,应当对所编写的有价值的源代码及时备份,即使有些代码在后续阶段似乎没有用处了也不妨“敝帚自珍”。在VC编程中我经常遇到的问题是,一个不算小的编写了很久的程序,在稍微扩展一点功能时出现了故障,而最挠头的时花了很多时间也无法排故,所幸的是对原来的程序我还有备份,可以重新再来--功能还要加,只是还一种编程思路去实现相同的功能。说实在的,本人在VC开发中,如果要总结“解决了多少问题”,不如说很多时候是采用"游击战"巧妙地"绕过"了一些棘手的问题。--尤其在软件交工处于倒计时的开发后期出现的故障,往往你已经没有时间去找到故障,而是用前一个完好的VC Project去尝试另一条捷径。

 
0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:56413次
    • 积分:1060
    • 等级:
    • 排名:千里之外
    • 原创:50篇
    • 转载:3篇
    • 译文:0篇
    • 评论:8条
    文章分类
    最新评论