断言语句指定一个你希望在程序中某个时刻为真的条件。 如果该条件不为 true,则断言失败,程序执行中断,并显示 “断言失败”对话框 。
Visual Studio 支持基于以下构造的C++断言语句:
MFC 程序的 MFC 断言。
ATLASSERT 用于使用 ATL 的程序。
使用 C 运行时库的程序的 CRT 断言。
ANSI 断言函数 用于其他 C/C++ 程序。
可以使用断言来捕获逻辑错误、检查操作结果以及测试那些本应处理的错误条件。
本主题内容
断言的工作原理
当调试器因 MFC 或 C 运行时库断言而停止时,如果源可用,调试器将导航到发生断言的源文件中的点。 断言消息同时显示在 “输出”窗口 和“ 断言失败 ”对话框中。 如果要保存该断言消息以供将来引用,可以将断言消息从 “输出 ”窗口复制到文本窗口。 “输出”窗口也可能包含其他错误消息。 仔细检查这些消息,因为它们提供了断言失败原因的线索。
使用断言检测开发过程中的错误。 作为一个规则,对每个假设使用一个断言。 例如,如果假设参数不是 NULL,请使用断言来测试该假设。
调试和发布版本中的断言
仅当定义了 _DEBUG 时,断言语句才会编译。 否则,编译器会将断言视为 null 语句。 因此,断言语句不会在最终发布程序中产生开销或性能成本,并允许避免使用 #ifdef 指令。
使用断言的副作用
向代码添加断言时,请确保断言没有副作用。 例如,考虑以下用于修改 nM 值的断言:
ASSERT(nM++ > 0); // Don't do this!
ASSERT由于表达式未在程序的 Release 版本中求值,nM因此在调试和发布版本中将具有不同的值。 若要避免 MFC 中出现此问题,可以使用 VERIFY 宏而不是 ASSERT.
VERIFY 计算所有版本中的表达式,但不检查 Release 版本中的结果。
特别小心在断言语句中使用函数调用,因为评估函数可能会产生意外的副作用。
ASSERT ( myFnctn(0)==1 ) // unsafe if myFnctn has side effects
VERIFY ( myFnctn(0)==1 ) // safe
VERIFY 在调试和发布版本中调用 myFnctn ,因此可以接受使用。 但是,使用 VERIFY 会在发布版本中带来不必要的函数调用开销。
CRT 断言
CRTDBG.H头文件定义断言检查的_ASSERT宏和_ASSERTE宏。
| Macro | 结果 |
|---|---|
_ASSERT |
如果指定的表达式的计算结果为 FALSE,则文件名和行号 。_ASSERT |
_ASSERTE |
与_ASSERT相同,并加上断言表达式的字符串表示形式。 |
_ASSERTE 功能更强大,因为它会报告结果为 FALSE 的断言表达式。 这可能足以识别问题,而无需引用源代码。 但是,应用程序的调试版本将包含为每个使用 _ASSERTE 断言的表达式定义的字符串常量。 如果使用许多 _ASSERTE 宏,则这些字符串表达式占用大量内存。 如果这证明是个问题,则用于 _ASSERT 节省内存。
定义 _DEBUG 后,宏 _ASSERTE 定义如下:
#define _ASSERTE(expr) \
do { \
if (!(expr) && (1 == _CrtDbgReport( \
_CRT_ASSERT, __FILE__, __LINE__, #expr))) \
_CrtDbgBreak(); \
} while (0)
如果断言表达式的计算结果为 FALSE, 则调用_CrtDbgReport 报告断言失败(默认使用消息对话框)。 如果在消息对话框中选择 “重试 ”,则返回 1, _CrtDbgReport 并通过 _CrtDbgBreak 调用调试器 DebugBreak。
如果需要暂时禁用所有断言,请使用 _CtrSetReportMode。
检查堆损坏
以下示例使用 _CrtCheckMemory 来检查堆是否存在损坏:
_ASSERTE(_CrtCheckMemory());
检查指针有效性
以下示例使用 _CrtIsValidPointer 来验证给定的内存范围是否对读取或写入有效。
_ASSERTE(_CrtIsValidPointer( address, size, TRUE );
以下示例使用 _CrtIsValidHeapPointer 验证指向本地堆中的内存的指针(由 C 运行时库的此实例创建的和托管的堆) - DLL 可以有自己的库实例,因此,它自己的堆位于应用程序堆之外)。 此断言不仅捕获 null 或超出边界的地址,还捕获指向静态变量、堆栈变量和任何其他非本地内存的指针。
_ASSERTE(_CrtIsValidHeapPointer( myData );
检查内存块
以下示例使用 _CrtIsMemoryBlock 来验证内存块是否位于本地堆中并且具有有效的块类型。
_ASSERTE(_CrtIsMemoryBlock (myData, size, &requestNumber, &filename, &linenumber));
MFC 断言
MFC 定义断言检查的 ASSERT 宏。 它还定义了用于检查继承自CObject的对象内部状态的MFC ASSERT_VALID和CObject::AssertValid方法。
如果 MFC ASSERT 宏的参数计算结果为零或 false,则宏将停止程序执行并提醒用户;否则,执行将继续。
断言失败时,消息对话框会显示源文件的名称和断言的行号。 如果在对话框中选择“重试”,则对 AfxDebugBreak 的调用会导致执行中断到调试器。 此时,可以检查调用堆栈并使用其他调试器设施来确定断言失败的原因。 如果已启用 实时调试,并且调试器尚未运行,则对话框可以启动调试器。
以下示例演示如何用于 ASSERT 检查函数的返回值:
int x = SomeFunc(y);
ASSERT(x >= 0); // Assertion fails if x is negative
可以将 ASSERT 与 IsKindOf 函数一起使用,以提供函数参数的类型检查:
ASSERT( pObject1->IsKindOf( RUNTIME_CLASS( CPerson ) ) );
宏 ASSERT 在发布版本中不生成任何代码。 如果需要在 Release 版本中计算表达式,请使用 VERIFY 宏而不是 ASSERT。
MFC 的 ASSERT_VALID 和 CObject::AssertValid
CObject::AssertValid 方法提供对象的内部状态的运行时检查。 虽然从CObject派生类时不需要重写AssertValid,但通过这样做可以使您的类更加可靠。
AssertValid 应对所有对象的成员变量执行断言,以验证它们是否包含有效值。 例如,它应检查指针成员变量是否不为 NULL。
以下示例演示如何声明函数 AssertValid :
class CPerson : public CObject
{
protected:
CString m_strName;
float m_salary;
public:
#ifdef _DEBUG
// Override
virtual void AssertValid() const;
#endif
// ...
};
重写 AssertValid 时,请在执行自己的检查之前调用基类的AssertValid版本。 然后使用 ASSERT 宏检查派生类特有的成员,如下所示:
#ifdef _DEBUG
void CPerson::AssertValid() const
{
// Call inherited AssertValid first.
CObject::AssertValid();
// Check CPerson members...
// Must have a name.
ASSERT( !m_strName.IsEmpty());
// Must have an income.
ASSERT( m_salary > 0 );
}
#endif
如果您的任何成员变量存储对象,可以使用ASSERT_VALID宏来测试其内部有效性(如果其类中重写了AssertValid)。
例如,请考虑一个类,该类 CMyData将 CObList 存储在其中一个成员变量中。
CObList变量m_DataList存储对象的CPerson集合。 缩写声明 CMyData 如下:
class CMyData : public CObject
{
// Constructor and other members ...
protected:
CObList* m_pDataList;
// Other declarations ...
public:
#ifdef _DEBUG
// Override:
virtual void AssertValid( ) const;
#endif
// And so on ...
};
AssertValid重载在CMyData中如下所示:
#ifdef _DEBUG
void CMyData::AssertValid( ) const
{
// Call inherited AssertValid.
CObject::AssertValid( );
// Check validity of CMyData members.
ASSERT_VALID( m_pDataList );
// ...
}
#endif
CMyData 使用该 AssertValid 机制测试存储在其数据成员中的对象的有效性。 重写AssertValid,在CMyData中为其自身的m_pDataList成员变量调用ASSERT_VALID宏。
有效性测试不会在此级别停止,因为该类 CObList 也会覆写 AssertValid。 此替代对列表的内部状态执行其他有效性测试。 因此,对 CMyData 对象的有效性测试会导致对存储 CObList 列表对象的内部状态进行额外的有效性测试。
通过更多工作,还可以为 CPerson 列表中存储的对象添加有效性测试。 可以从CObList派生一个类CPersonList并重写AssertValid。 在重写中,你将调用 CObject::AssertValid,然后遍历列表,并对列表中存储的每个 CPerson 对象调用 AssertValid。 已在本主题开头显示的CPerson类已经重写AssertValid。
在构建用于调试的程序时,这是一种强大的机制。 在进行发布构建时,该机制会自动关闭。
AssertValid 的限制
触发的断言指示对象绝对不正确,执行将停止。 但是,缺少断言仅表示找不到问题,但不能保证该对象良好。
使用断言
捕捉逻辑错误
可以根据程序的逻辑设置一个条件的断言,该条件必须为 true。 除非发生逻辑错误,否则断言不起作用。
例如,假设你在容器中模拟气体分子,而变量 numMols 表示分子总数。 此数字不能小于零,因此可以包括如下所示的 MFC 断言语句:
ASSERT(numMols >= 0);
或者,您可以加入一个如下所示的 CRT 断言:
_ASSERT(numMols >= 0);
当程序正常运行时,这些语句不会执行任何操作。 但是,如果逻辑错误导致 numMols 小于零,则断言会停止执行程序并显示 断言失败对话框。
检查结果
断言对于测试一些其结果不易通过快速视觉检查显而易见的操作非常有用。
例如,请考虑以下代码,它根据由mols指向的链表的内容更新iMols变量。
/* This code assumes that type has overloaded the != operator
with const char *
It also assumes that H2O is somewhere in that linked list.
Otherwise we'll get an access violation... */
while (mols->type != "H2O")
{
iMols += mols->num;
mols = mols->next;
}
ASSERT(iMols<=numMols); // MFC version
_ASSERT(iMols<=numMols); // CRT version
所计数iMols的分子数必须始终小于或等于分子总数。 numMols 循环的视觉检查并不表明这一点一定是这种情况,因此在循环之后使用断言语句来测试该条件。
查找未经处理的错误
可以在代码中任何错误应已被处理的位置使用断言来测试错误条件。 在以下示例中,图形例程返回错误代码或零以成功。
myErr = myGraphRoutine(a, b);
/* Code to handle errors and
reset myErr if successful */
ASSERT(!myErr); -- MFC version
_ASSERT(!myErr); -- CRT version
如果错误处理代码正常工作,则应在到达断言之前处理错误并将其 myErr 重置为零。 如果有 myErr 另一个值,则断言失败,程序将停止,并显示 “断言失败”对话框 。
但是,断言语句不能替代错误处理代码。 以下示例展示了一条可能在最终发布的代码中引起问题的断言语句:
myErr = myGraphRoutine(a, b);
/* No Code to handle errors */
ASSERT(!myErr); // Don't do this!
_ASSERT(!myErr); // Don't do this, either!
此代码依赖于断言语句来处理错误条件。 因此,最终发布代码中 myGraphRoutine 返回的任何错误代码都将未被处理。