调试和测试变得容易(第一部分)






4.21/5 (14投票s)
2003年3月10日
4分钟阅读

75689

522
一些宏和技巧,可以减少 bug 的出现,并使用类使单元测试轻松而简单
引言
您是否经常发现自己在调试代码,一切看起来都合乎逻辑,但就是不起作用?初学者经常会遇到这种情况。嗯,我最近发现这种情况少了很多,这得益于我学到的一些很棒的技巧。我想和大家分享,并希望它们也能帮助您。
背景
我用 VC++ 编程,但经常发现自己为各种事情编写控制台实用程序。VC++ 和 MFC 有一些很棒的宏(C++ #define
)可以帮助您调试。但如果您在项目中使用的是非 MFC,那您就陷入困境了。不过,那些宏并没有什么神奇之处,并且很容易为您的非 MFC 项目(甚至非 VC++ 项目)重现。
在我看来,代码中存在 bug 的最主要原因之一是,对特定代码段应该(和不应该)做什么的假设是错误的。所以解决办法是在代码中记录您的假设,并让您的代码在不按预期工作时,或者在使用(或误用)不当的方式时给出警告。
一些理论(我希望它不会枯燥)- Bertrand Meyer 引入了“契约式设计”的概念,正是为了做到这一点(以及更多,但我们不想在本文中深入探讨这些细节)。但是 C++ 并不像 Eiffel 那样支持契约。那么我们该怎么办呢?我们尝试找出实现契约的方法。
我说的这个契约到底是什么?我不需要什么法律术语来编程。您也不需要。基本上,函数的(或方法的)契约表明了它为了正确工作而期望满足的条件,调用者施加给它的条件,最后,还有一些不应被违反的全局条件。
函数为正确工作而必须满足的条件是它的前置条件或“REQUIRE”条件。例如,“参数不应为 null
”是一个非常常见的要求。调用者施加的特定函数的条件必须得到满足。这是为了“ENSURE”结果符合预期。例如,如果函数涉及文件操作,则操作确实已完成,未被中止。全局条件通常称为不变式或“ASSERT”断言。
在调试或开发时,我们希望在契约被违反时收到通知。因为当契约被违反时,它表明结果不一定正确,有问题。最简单的方法之一是将消息打印到控制台,例如“前置条件不满足”、“断言失败”或“某个变量的值为 X,而该断言失败”。嗯,这里有一些宏可以做到这一点。
#define DBGOUT cout //you could just as easily put
//name of a ofstream here and log everything to a file.
#define REQUIRE(cond) \
if (!(cond))\
{\
DBGOUT << "\nPrecondition \t" << #cond << "\tFAILED\t\t" \
<< __FILE__ << "(" << __LINE__ << ")";\
};\
//precondition tracing macro
#define ENSURE(cond)\
if (!(cond)) \
{\
DBGOUT << "\nPostcondition \t" << #cond << "\tFAILED\t\t" \
<< __FILE__ << "(" << __LINE__ << ")";\
};\
//postcondition tracing macro
#define ASSERT(cond) \
if (!(cond)) \
{\
DBGOUT << "\nAssertion \t" << #cond << "\tFAILED\t\t" \
<< __FILE__ << "(" << __LINE__ << ")";\
};\
//invariant tracing macro
#define TRACE(data) \
DBGOUT << "\nTrace \t" << #data << " : " << data \
<< "\t\t" << __FILE__ << "(" << __LINE__ << ")";
//dump some variables value.
#define WARN(str) \
DBGOUT << "\nWarning \t" << #str << "\t\t" \
<< __FILE__ << "(" << __LINE__ << ")";
//print some warning indicating that some code is being executed which
//shouldn't really be executed.
Using the Code
让我们以一个除以两个数的函数为例,看看这些宏会有什么帮助。
int div (int a, int b)
{
//preconditions
REQUIRE(b != 0)
int result = a/b;
//postconditions
ENSURE(b*result <= a)
return a/b;
}
上面的例子看起来可能很简单,但考虑更复杂的函数,并正确使用上述宏,您至少可以找出最常见的错误原因。TRACE
和 WARN
宏在弄清楚为什么某事物不符合预期时特别有用,而 ENSURE
、REQUIRE
和 ASSERT
将让您确信,如果您的函数获得正确的输入,它们将产生正确的输出。
稍微复杂一点的例子
//to parse some string in numbers (thousand) to its equivalent integer(1000)
int Parse(char* str)
{
//preconditions
REQUIRE(str != NULL) //null strings cannot be parsed
int result;
//some table lookup is done here
//assert that index is within array bounds
ASSERT (lookupindex < tablesize)
//postconditions
ENSURE(result >= 0) // the caller doesnt expect negative answers
return result;
}
误用
- 这些宏有一些缺点和误用。首先,它们会减慢处理速度。所以,您通常不希望它们出现在发布代码中,只在开发或调试时使用。在这种情况下,您可以有条件地定义宏,使其在调试时生成代码,但在发布时则不生成(请参阅 zip 文件,它在 dbgmacros.h 中是这样定义的)。
- 其次,宏期望测试不变条件,并且不期望在其中进行任何处理。例如,
ASSERT(i++!=10)
- 这段代码在您的发布版本中最有可能失败。请不要在宏中进行处理。只检查条件。
关注点
为了更深入地了解,我强烈建议您深入研究契约式设计的理念。它对面向对象系统的设计和实现有着巨大的影响。
我将在本系列的下一部分中提供类似的宏,以简化您的测试,可能在下周。
历史
- 2003年3月10日:第一次修订
许可证
本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。
作者可能使用的许可证列表可以在此处找到。