在实际工作中,我们写的函数除了能完成要求的功能之外,常常需要对函数的输入进行检查、判断函数是否正确执行(比如有除0操作)、执行结果是否符合预期。如果运行出错,错误原因是什么?以及如何针对不同错误进行处理?
通常,我们有 3 种方式把错误信息传递给函数的调用者。1
方法1:通过函数返回值来告知调用者是否出错。
很多Windows的API就是通过返回值为0表示API调用成功,而返回值不为0表示在API调用的过程中出错了。微软为不同的非零返回值定义了不同的意义,调用者可以根据这些返回值判断出错的原因。
很多SDK的接口也会采用返回值的方式来传递错误信息,例如:有一个初始化的函数int32_t XXXSDK_Create( sdk_type_e nType, int32_t& nPDLLHandle )
,返回值如果非0,可以在SDK文档的错误码清单中找到对应错误原因。
void Init()
{
int result = XXXSDK_Create(XXXSDK_CORE_SDK_TYPE,m_nPDLLHandle);
if (result == 0)
{
//成功,TODO……
}
else
{
//失败打印错误码,TODO……
}
}
SDK文档中的错误码类似这样:
1005| XXXSDK_CORE_ERROR_INIT_FAIL| 初始化失败
1009| XXXSDK_CORE_ERROR_INVALID_PARAM| 无效的参数
1010| XXXSDK_CORE_ERROR_TIMEOUT| 操作超时
…………
这种方式最大的问题是使用不便,因为函数不能直接把计算结果通过返回值赋值给其他变量,同时也不能把这个函数计算的结果直接作为参数传递给其他函数。
方法2:设置一个全局变量保存错误信息。
此时我们可以在返回值中传递计算结果了。这种方法比第一种方法使用起来更加方便,因为调用者可以直接把返回值赋值给其他变量或者作为参数传递给其他函数。
Windows的很多API运行出错之后,也会设置一个全局变量。我们可以通过调用函数GetLastError()
分析这个表示错误的全局变量,从而得知出错的原因。
DWORD GetLastError(VOID)
获取调用线程的最后一个错误代码值。最后一个错误代码基于每个线程进行维护。多个线程不会覆盖彼此的最后错误代码。- 通过调用
SetLastError(DWORD dwErrCode)
函数来设置这个错误码。当函数的返回值指示此类调用将返回有用的数据时,应立即调用GetLastError
函数。这是因为某些函数在成功时会把最后一个错误码置为零。
FormatMessage与GetLastError结合使用,可以显示返回的错误消息。下面是一个示例:2
void ReportError()
{
LPTSTR lpMessage;
DWORD dwErrCode = GetLastError();
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM,
NULL, // no source buffer needed
dwErrCode, // error code for this message
NULL, // default language ID
(LPTSTR)&lpMessage, // allocated by fcn
NULL, // minimum size of buffer
NULL); // no inserts
MessageBox(NULL, lpMessage, TEXT("File Error"), MB_ICONSTOP | MB_OK );
LocalFree(lpMessage); // Free the memory allocated by FormatMessage
}
下面的示例包括一个错误处理功能,该功能可以打印错误消息并终止该进程。lpszFunction参数是设置的最后一个错误代码的函数的名称。3
#include <windows.h>
#include <strsafe.h>
void ErrorExit(LPTSTR lpszFunction)
{
// Retrieve the system error message for the last-error code
LPVOID lpMsgBuf;
LPVOID lpDisplayBuf;
DWORD dw = GetLastError();
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
dw,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR) &lpMsgBuf,
0, NULL );
// Display the error message and exit the process
lpDisplayBuf = (LPVOID)LocalAlloc(LMEM_ZEROINIT,
(lstrlen((LPCTSTR)lpMsgBuf) + lstrlen((LPCTSTR)lpszFunction) + 40) * sizeof(TCHAR));
StringCchPrintf((LPTSTR)lpDisplayBuf,
LocalSize(lpDisplayBuf) / sizeof(TCHAR),
TEXT("%s failed with error %d: %s"),
lpszFunction, dw, lpMsgBuf);
MessageBox(NULL, (LPCTSTR)lpDisplayBuf, TEXT("Error"), MB_OK);
LocalFree(lpMsgBuf);
LocalFree(lpDisplayBuf);
ExitProcess(dw);
}
void main()
{
// Generate an error
if(!GetProcessId(NULL))
ErrorExit(TEXT("GetProcessId"));
}
系统错误码是在WinError.h
中定义的,有时是由非系统软件返回的。错误码的描述一般不会非常具体,需要结合具体应用,仔细进行排查。
- windows系统错误码详见:system-error-codes
- 其他的windows错误处理方法见:error-handling-functions
但这个方式有个问题:调用者很容易就会忘记去检查全局变量,因此在调用出错的时候忘记做相应的错误处理,从而留下安全隐患。
方法3:异常捕获和处理。
当函数运行出错的时候,我们就抛出一个异常,我们还可以根据不同的出错原因定义不同的异常类型。因此函数的调用者根据异常的类型就能知道出错的原因,从而做相应的处理。
C++ 异常处理涉及到三个关键字:try、catch、throw。4
- throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
- catch: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。
- try: try 块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个 catch 块。
通过显式划分程序正常运行的代码块(try模块)和处理异常的代码块(catch模块),逻辑比较清晰。
除以零的异常处理代码示例:4
#include <iostream>
using namespace std;
double division(int a, int b)
{
if( b == 0 )
{
//抛出异常
throw "Division by zero condition!";
}
return (a/b);
}
int main ()
{
int x = 50;
int y = 0;
double z = 0;
try
{
//保护代码
z = division(x, y);
cout << z << endl;
}
catch (const char* msg) //捕获异常
{
//处理异常
cerr << msg << endl;
}
return 0;
}
由于我们抛出了一个类型为 const char* 的异常,因此,当捕获该异常时,我们必须在 catch 块中使用 const char*。
小结
三种方式各有优劣。在自己封装SDK的时候,可以用返回值给出错误码;在处理系统错误码时,可以获取全局变量;在需要针对不同错误分类处理时,可以用try、catch、throw。