天天看点

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);

if(!(size >= 100) && afxassertfailedline(__file__,__line__))

}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。下面是一个例子:

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; // 指针副本

assert(isalpha(fill)); // 确保fill为字母

debug_only(if(!isalpha(fill)) fill = 'x'); // 如果fill值非法,则指定为'x'

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

 

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

   如下所示,这些宏直接调用afxassertvalidobject():

   void cmyview::foo(cyourview* pview)

//assert_valid(this); // 验证本身

afxassertvalidobject(this,__file__,__line__);

//assert_valid(pview); // 验证pview

afxassertvalidobject(pview,__file__,__line__);

   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) {}

virtual void assertvalid() const;

void sample::assertvalid() const

cobject::assertvalid(); // 验证基类

assert(!m_name.isempty()); // 验证职员名字

assert(m_salary > 0); // 验证薪水

   显然,依赖于派生类的数据成员,这些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 对象

// 其它操作