C++引入了新的报错处理机制:异常管理(exception handling),它允许我们的代码健壮而简约、能够处理任何意料之外情况的报错。此章将覆盖使用异常的原因、怎么有效的使用异常、和使用异常的弊端,尤其是它们各自对性能的影响。结尾时,我们会对游戏编程中的异常提出一些建议。
5.1管理报错
从计算机的发明开始,程序员就不得不处理报错,从没有过理想的解决方案:不是白花经理和时间来杞人忧天、就是干脆假装看不见。
1.无视它们!
虽然听起来荒谬,但这就是大多数程序的错误处理方法(对大多数错误而言)。比方说,我们当然要检出某个文件是否正常开启,但谁在乎printf有没有成功呢?如果一切正常,倒还好说,但要是一旦出错,程序就有可能崩溃。要是没有足够的内存了呢?要是桌面显示设置的是8比特色彩深度呢?如果没有足够的磁盘空间了呢?或者用户在游戏加载时候把CD-ROM拔出来了呢?在快速且混乱的工具——甚至是内部开发工具中,忽略它们是可行的(取决于公司内部的质量保准预期)。但这显然不是发售版本程序应有的方案,所以我们要找替代方案。
2.使用错误代码(error code)
我们可以用被时间检验过的经典方法:在函数返回值报错,任何可能失败的函数都会返回错误码,或者至少一个布尔值,表明函数是否成功。调用代码负责检查返回值以及正确地处理错误。
理论上,这个方法没问题;实际上,它难以在项目整体中运用。原因有三:首先,代码可读性往往因此降低,原来两句话的函数可能变成了长达三十行的、充满了if-then-else判断句的垃圾,不仅丑陋而且混淆了函数真正的目的。以下代码从数据流中加载了一个静态网格体,第一个函数没做报错检查,而第二个做了,哪个更可读呢?
void Mesh::Load(Stream stream)
{
ParseHeader (stream);
ParseFlags(stream);
ParseVertices(stream);
ParseFaces(stream);
}
int Mesh::Load(Stream stream)
{
int errCode = OK;
errCode = ParseHeader(stream);
if (errCode != OK)
{
FreeHeader();
return errCode;
}
errCode = ParseFlags(stream);
if (errCode != OK)
{
FreeHeader();
return errCode;
}
errCode = ParseVertices(stream);
if (errCode != OK)
{
FreeHeader();
FreeVertices();
return errCode;
}
errCode = ParseFaces(stream);
if (errCode != OK)
{
FreeHeader();
FreeVertces();
FreeFaces();
return errCode;
}
return errCode;
}
源代码自己会说话。其次,检查错误代码不仅丑陋麻烦,也很浪费:要是每个函数调用都被一群判断语句包围,那游戏的性能就会大幅下降。
最后是程序装配的问题:错误代码应当在哪里?是一个所有类都会包含的大文档?还是每个子系统都有各自的一套?它们如何从一串数字编程人类可读的字符串?一定有更好的方案。
3. 扔炸药包(blow up)
另一种方法是双手投降,但用插入(assert)方法来阻止程序崩溃。只要停下了程序,就能报告更多的信息,比如文件名和错误的行号、描述性信息,总比没有好。
4. 使用setjmp()和longjmp()
硬核C程序员可能或熟悉这两个函数,setjmp()允许我们在代码(以及堆栈状态)里预留调用位置,而longjmp()将程序恢复到指定的位置和栈状态。按理说,每当有某种错误时,我们能调用longjmp()来处理,不幸的是,这个方法虽然灵活但和C++不太合拍:它虽然释放了栈,但没有销毁栈中的对象,于是对象的析构函数永远不会被调用,这意味着潜在的内存泄漏或者未释放的资源。如果在出错后只需退出程序,那么问题不大,然而如果需要从报错处恢复并继续运行,此方法会导致内存耗尽从而程序崩溃。
5.使用C++异常
终于到了重点了。目前为止的报错处理机制都有各式的缺点。在看异常的语法之前,不如先看它的常见用法和它相比前几个机制的优点何在。
1.它的原理
当程序碰到预料之外的任何事,都可以扔个异常,使程序跳转到最近的异常处理块。如果整个函数里都没有异常处理块,就释放栈(正确的销毁栈中的每个对象)然后去上个层级寻找,重复这个过程直到找到异常处理块;又或者到了顶部也没找到——这时会触发默认异常处理代码,程序停止。
在异常处理块中,我们想干啥都行:报错、尝试解决、甚至无视它。也可以对症下药,取决于异常的原因,(比如除零和文件损坏这两种问题应当有不同的处理方法),在异常处理块运行之后,程序直接继续运行,而不是回到异常抛出的位置。