让你的代码变的更加强大(Making your C++ code robust)

本文探讨了在C++编程中提升代码稳健性的多种方法,包括局部变量初始化、验证输入参数、清理已释放对象指针等,以减少程序崩溃的风险。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Making your C++ code robust
  • Introduction

       在实际的项目中,当项目的代码量不断增加的时候,你会发现越来越难管理和跟踪其各个组件,如其不善,很容易就引入BUG。因此、我们应该掌握一些能让我们程序更加健壮的方法。

       这篇文章提出了一些建议,能有引导我们写出更加强壮的代码,以避免产生灾难性的错误。即使、因为其复杂性和项目团队结构,你的程序目前不遵循任何编码规则,按照下面列出的简单的规则可以帮助您避免大多数的崩溃情况。

  • Background

        先来介绍下作者开发一些软件(CrashRpt),你可以http://code.google.com/p/crashrpt/网站上下载源代码。CrashRpt 顾名思义软件崩溃记录软件(库),它能够自动提交你电脑上安装的软件错误记录。它通过以太网直接将这些错误记录发送给你,这样方便你跟踪软件问题,并及时修改,使得用户感觉到每次发布的软件都有很大的提高,这样他们自然很高兴。

图 1、CrashRpt 库检测到错误弹出的对话框

       在分析接收的错误记录的时候,我们发现采用下文介绍的方法能够避免大部分程序崩溃的错误。例如、局部变量未初始化导致数组访问越界,指针使用前未进行检测(NULL)导致访问访问非法区域等。

      我已经总结了几条代码设计的方法和规则,在下文一一列出,希望能够帮助你避免犯一些错误,使得你的程序更加健壮。

  • Initializing Local Variables

     使用未初始化的局部变量是引起程序崩溃的一个比较普遍的原因,例如、来看下面这段程序片段:

  1. // Define local variables 
  2. BOOL bExitResult; // This will be TRUE if the function exits successfully 
  3. FILE* f; // Handle to file 
  4. TCHAR szBuffer[_MAX_PATH];   // String buffer 
  5.    
  6. // Do something with variables above...  

     上面的这段代码存在着一个潜在的错误,因为没有一个局部变量初始化了。当你的代码运行的时候,这些变量将被默认负一些错误的数值。例如bExitResult 数值将被负为-135913245 ,szBuffer 必须以“\0”结尾,结果不会。因此、局部变量初始化时非常重要的,如下正确代码:

  1. // Define local variables 
  2.  
  3. // Initialize function exit code with FALSE to indicate failure assumption 
  4. BOOL bExitResult = FALSE; // This will be TRUE if the function exits successfully 
  5. // Initialize file handle with NULL 
  6. FILE* f = NULL; // Handle to file 
  7. // Initialize string buffer with empty string 
  8. TCHAR szBuffer[_MAX_PATH] = _T("");   // String buffer 
  9. // Do something with variables above...  

    注意:有人说变量初始化会引起程序效率降低,是的,确实如此,如果你确实非常在乎程序的执行效率,去除局部变量初始化,你得想好其后果。

  • Initializing WinAPI Structures

       许多Windows API都接受或则返回一些结构体参数,结构体如果没有正确的初始化,也很有可能引起程序崩溃。大家可能会想起用ZeroMemory宏或者memset()函数去用0填充这个结构体(对结构体对应的元素设置默认值)。但是大部分Windows API 结构体都必须有一个cbSIze参数,这个参数必须设置为这个结构体的大小。

       看看下面代码,如何初始化Windows API结构体参数:

  1. NOTIFYICONDATA nf; // WinAPI structure 
  2. memset(&nf,0,sizeof(NOTIFYICONDATA)); // Zero memory 
  3. nf.cbSize = sizeof(NOTIFYICONDATA); // Set structure size! 
  4. // Initialize other structure members 
  5. nf.hWnd = hWndParent; 
  6. nf.uID = 0;    
  7. nf.uFlags = NIF_ICON | NIF_TIP; 
  8. nf.hIcon = ::LoadIcon(NULL, IDI_APPLICATION); 
  9. _tcscpy_s(nf.szTip, 128, _T("Popup Tip Text")); 
  10.        
  11. // Add a tray icon 
  12. Shell_NotifyIcon(NIM_ADD, &nf); 

      注意:千万不要用ZeroMemory和memset去初始化那些包括结构体对象的结构体,这样很容易破坏其内部结构体,从而导致程序崩溃.

  1. // Declare a C++ structure 
  2. struct ItemInfo 
  3.   std::string sItemName; // The structure has std::string object inside 
  4.   int nItemValue; 
  5. };  
  6.  
  7. // Init the structure 
  8. ItemInfo item; 
  9. // Do not use memset()! It can corrupt the structure 
  10. // memset(&item, 0, sizeof(ItemInfo)); 
  11. // Instead use the following 
  12. item.sItemName = "item1"
  13. item.nItemValue = 0;  
  14.    这里最好是用结构体的构造函数对其成员进行初始化. 
  15.  
  16. // Declare a C++ structure 
  17. struct ItemInfo 
  18.   // Use structure constructor to set members with default values 
  19.   ItemInfo() 
  20.   { 
  21.     sItemName = _T("unknown"); 
  22.     nItemValue = -1; 
  23.   } 
  24.        
  25.   std::string sItemName; // The structure has std::string object inside 
  26.   int nItemValue; 
  27. }; 
  28. // Init the structure 
  29. ItemInfo item; 
  30. // Do not use memset()! It can corrupt the structure 
  31. // memset(&item, 0, sizeof(ItemInfo)); 
  32. // Instead use the following 
  33. item.sItemName = "item1"
  34. item.nItemValue = 0;     
  • Validating Function Input

      在函数设计的时候,对传入的参数进行检测是一直都推荐的。例如、如果你设计的函数是公共API的一部分,它可能被外部客户端调用,这样很难保证客户端传进入的参数就是正确的。

      例如,让我们来看看这个hypotethical DrawVehicle() 函数,它可以根据不同的质量来绘制一辆跑车,这个质量数值(nDrawingQaulity )是0~100。prcDraw 定义这辆跑车的轮廓区域。

      看看下面代码,注意观察我们是如何在使用函数参数之前进行参数检测:

  1. BOOL DrawVehicle(HWND hWnd, LPRECT prcDraw, int nDrawingQuality) 
  2.   { 
  3.     // Check that window is valid 
  4.     if(!IsWindow(hWnd)) 
  5.       return FALSE; 
  6.   
  7.     // Check that drawing rect is valid 
  8.     if(prcDraw==NULL) 
  9.       return FALSE; 
  10.   
  11.     // Check drawing quality is valid 
  12.     if(nDrawingQuality<0 || nDrawingQuality>100) 
  13.       return FALSE; 
  14.     
  15.     // Now it's safe to draw the vehicle 
  16.   
  17.     // ... 
  18.   
  19.     return TRUE; 
  20.   } 
  • Validating Pointers

       在指针使用之前,不检测是非常普遍的,这个可以说是我们引起软件崩溃最有可能的原因。如果你用一个指针,这个指针刚好是NULL,那么你的程序在运行时,将报出异常。

  1. CVehicle* pVehicle = GetCurrentVehicle(); 
  2.  
  3. // Validate pointer 
  4. if(pVehicle==NULL) 
  5.   // Invalid pointer, do not use it! 
  6.   return FALSE; 
  • Initializing Function Output

     如果你的函数创建了一个对象,并要将它作为函数的返回参数。那么记得在使用之前把他复制为NULL。如不然,这个函数的调用者将使用这个无效的指针,进而一起程序错误。如下错误代码:

  1. int CreateVehicle(CVehicle** ppVehicle) 
  2.   { 
  3.     if(CanCreateVehicle()) 
  4.     { 
  5.       *ppVehicle = new CVehicle(); 
  6.       return 1; 
  7.     }     
  8.   
  9.     // If CanCreateVehicle() returns FALSE, 
  10.     // the pointer to *ppVehcile would never be set! 
  11.     return 0; 
  12.   } 
  13.  
  14.       正确的代码如下; 
  15.  
  16.   int CreateVehicle(CVehicle** ppVehicle) 
  17.   { 
  18.     // First initialize the output parameter with NULL 
  19.     *ppVehicle = NULL; 
  20.   
  21.     if(CanCreateVehicle()) 
  22.     { 
  23.       *ppVehicle = new CVehicle(); 
  24.       return 1; 
  25.     }     
  26.   
  27.     return 0; 
  28.   } 
  • Cleaning Up Pointers to Deleted Objects

     在内存释放之后,无比将指针复制为NULL。这样可以确保程序的没有那个地方会再使用无效指针。其实就是,访问一个已经被删除的对象地址,将引起程序异常。如下代码展示如何清除一个指针指向的对象:

  1. // Create object 
  2. CVehicle* pVehicle = new CVehicle(); 
  3. delete pVehicle; // Free pointer 
  4. pVehicle = NULL; // Set pointer with NULL 
  • Cleaning Up Released Handles

      在释放一个句柄之前,务必将这个句柄复制伪NULL (0或则其他默认值)。这样能够保证程序其他地方不会重复使用无效句柄。看看如下代码,如何清除一个Windows API的文件句柄:

  1. HANDLE hFile = INVALID_HANDLE_VALUE;  
  2.  
  3. // Open file 
  4. hFile = CreateFile(_T("example.dat"), FILE_READ|FILE_WRITE, FILE_OPEN_EXISTING); 
  5. if(hFile==INVALID_HANDLE_VALUE) 
  6.   return FALSE; // Error opening file 
  7.  
  8. // Do something with file 
  9.  
  10. // Finally, close the handle 
  11. if(hFile!=INVALID_HANDLE_VALUE) 
  12.   CloseHandle(hFile);   // Close handle to file 
  13.   hFile = INVALID_HANDLE_VALUE;   // Clean up handle 
  14. }  

     下面代码展示如何清除File *句柄:

  1. // First init file handle pointer with NULL 
  2. FILE* f = NULL; 
  3.  
  4. // Open handle to file 
  5. errno_t err = _tfopen_s(_T("example.dat"), _T("rb")); 
  6. if(err!=0 || f==NULL) 
  7.   return FALSE; // Error opening file 
  8.  
  9. // Do something with file 
  10.  
  11. // When finished, close the handle 
  12. if(f!=NULL) // Check that handle is valid 
  13.   fclose(f); 
  14.   f = NULL; // Clean up pointer to handle 
  15. }  
  • Using delete [] Operator for Arrays

     如果你分配一个单独的对象,可以直接使用new ,同样你释放单个对象的时候,可以直接使用delete . 然而,申请一个对象数组对象的时候可以使用new,但是释放的时候就不能使用delete ,而必须使用delete[]:

  1. // Create an array of objects 
  2. CVehicle* paVehicles = new CVehicle[10]; 
  3. delete [] paVehicles; // Free pointer to array 
  4. paVehicles = NULL; // Set pointer with NULL 
  5. or 
  6. // Create a buffer of bytes 
  7. LPBYTE pBuffer = new BYTE[255]; 
  8. delete [] pBuffer; // Free pointer to array 
  9. pBuffer = NULL; // Set pointer with NULL 
  • Allocating Memory Carefully

     有时候,程序需要动态分配一段缓冲区,这个缓冲区是在程序运行的时候决定的。例如、你需要读取一个文件的内容,那么你就需要申请该文件大小的缓冲区来保存该文件的内容。在申请这段内存之前,请注意,malloc() or new是不能申请0字节的内存,如不然,将导致malloc() or new函数调用失败。传递错误的参数给malloc() 函数将导致C运行时错误。如下代码展示如何动态申请内存:

  1. // Determine what buffer to allocate. 
  2. UINT uBufferSize = GetBufferSize();  
  3.  
  4. LPBYTE* pBuffer = NULL; // Init pointer to buffer 
  5.  
  6. // Allocate a buffer only if buffer size > 0 
  7. if(uBufferSize>0) 
  8. pBuffer = new BYTE[uBufferSize]; 

      为了进一步了解如何正确的分配内存,你可以读下Secure Coding Best Practices for Memory Allocation in C and C++这篇文章。

  • Using Asserts Carefully

       Asserts用语调试模式检测先决条件和后置条件。但当我们编译器处于release模式的时候,Asserts在预编阶段被移除。因此,用Asserts是不能够检测我们的程序状态,错误代码如下:

  1. #include <assert.h> 
  2.   
  3. // This function reads a sports car's model from a file 
  4. CVehicle* ReadVehicleModelFromFile(LPCTSTR szFileName) 
  5.    CVehicle* pVehicle = NULL; // Pointer to vehicle object 
  6.  
  7.    // Check preconditions 
  8.    assert(szFileName!=NULL); // This will be removed by preprocessor in Release mode! 
  9.    assert(_tcslen(szFileName)!=0); // This will be removed in Release mode! 
  10.  
  11.    // Open the file 
  12.    FILE* f = _tfopen(szFileName, _T("rt")); 
  13.  
  14.    // Create new CVehicle object 
  15.    pVehicle = new CVehicle(); 
  16.  
  17.    // Read vehicle model from file 
  18.  
  19.    // Check postcondition  
  20.    assert(pVehicle->GetWheelCount()==4); // This will be removed in Release mode! 
  21.  
  22.    // Return pointer to the vehicle object 
  23.    return pVehicle; 

      看看上述的代码,Asserts能够在debug模式下检测我们的程序,在release 模式下却不能。所以我们还是不得不用if()来这步检测操作。正确的代码如下;

  1. CVehicle* ReadVehicleModelFromFile(LPCTSTR szFileName, ) 
  2.    CVehicle* pVehicle = NULL; // Pointer to vehicle object 
  3.  
  4.    // Check preconditions 
  5.    assert(szFileName!=NULL); // This will be removed by preprocessor in Release mode! 
  6.    assert(_tcslen(szFileName)!=0); // This will be removed in Release mode! 
  7.  
  8.    if(szFileName==NULL || _tcslen(szFileName)==0) 
  9.      return NULL; // Invalid input parameter 
  10.  
  11.    // Open the file 
  12.    FILE* f = _tfopen(szFileName, _T("rt")); 
  13.  
  14.    // Create new CVehicle object 
  15.    pVehicle = new CVehicle(); 
  16.  
  17.    // Read vehicle model from file 
  18.  
  19.    // Check postcondition  
  20.    assert(pVehicle->GetWheelCount()==4); // This will be removed in Release mode! 
  21.  
  22.    if(pVehicle->GetWheelCount()!=4) 
  23.    {  
  24.      // Oops... an invalid wheel count was encountered!   
  25.      delete pVehicle;  
  26.      pVehicle = NULL; 
  27.    } 
  28.  
  29.    // Return pointer to the vehicle object 
  30.    return pVehicle; 
  • Checking Return Code of a Function

       断定一个函数执行一定成功是一种常见的错误。当你调用一个函数的时候,建议检查下返回代码和返回参数的值。如下代码持续调用Windows API ,程序是否继续执行下去依赖于该函数的返回结果和返回参数值。

  1. HRESULT hres = E_FAIL; 
  2.     IWbemServices *pSvc = NULL; 
  3.     IWbemLocator *pLoc = NULL; 
  4.      
  5.     hres =  CoInitializeSecurity( 
  6.         NULL,  
  7.         -1,                          // COM authentication 
  8.         NULL,                        // Authentication services 
  9.         NULL,                        // Reserved 
  10.         RPC_C_AUTHN_LEVEL_DEFAULT,   // Default authentication  
  11.         RPC_C_IMP_LEVEL_IMPERSONATE, // Default Impersonation   
  12.         NULL,                        // Authentication info 
  13.         EOAC_NONE,                   // Additional capabilities  
  14.         NULL                         // Reserved 
  15.         ); 
  16.                        
  17.     if (FAILED(hres)) 
  18.     { 
  19.         // Failed to initialize security 
  20.         if(hres!=RPC_E_TOO_LATE)  
  21.            return FALSE; 
  22.     } 
  23.      
  24.     hres = CoCreateInstance( 
  25.         CLSID_WbemLocator,              
  26.         0,  
  27.         CLSCTX_INPROC_SERVER,  
  28.         IID_IWbemLocator, (LPVOID *) &pLoc); 
  29.     if (FAILED(hres) || !pLoc) 
  30.     { 
  31.         // Failed to create IWbemLocator object.  
  32.         return FALSE;                
  33.     } 
  34.     
  35.     hres = pLoc->ConnectServer( 
  36.          _bstr_t(L"ROOT\\CIMV2"), // Object path of WMI namespace 
  37.          NULL,                    // User name. NULL = current user 
  38.          NULL,                    // User password. NULL = current 
  39.          0,                       // Locale. NULL indicates current 
  40.          NULL,                    // Security flags. 
  41.          0,                       // Authority (e.g. Kerberos) 
  42.          0,                       // Context object  
  43.          &pSvc                    // pointer to IWbemServices proxy 
  44.          ); 
  45.      
  46.     if (FAILED(hres) || !pSvc) 
  47.     { 
  48.         // Couldn't conect server 
  49.         if(pLoc) pLoc->Release();      
  50.         return FALSE;   
  51.     } 
  52.     hres = CoSetProxyBlanket( 
  53.        pSvc,                        // Indicates the proxy to set 
  54.        RPC_C_AUTHN_WINNT,           // RPC_C_AUTHN_xxx 
  55.        RPC_C_AUTHZ_NONE,            // RPC_C_AUTHZ_xxx 
  56.        NULL,                        // Server principal name  
  57.        RPC_C_AUTHN_LEVEL_CALL,      // RPC_C_AUTHN_LEVEL_xxx  
  58.        RPC_C_IMP_LEVEL_IMPERSONATE, // RPC_C_IMP_LEVEL_xxx 
  59.        NULL,                        // client identity 
  60.        EOAC_NONE                    // proxy capabilities  
  61.     ); 
  62.     if (FAILED(hres)) 
  63.     { 
  64.         // Could not set proxy blanket. 
  65.         if(pSvc) pSvc->Release(); 
  66.         if(pLoc) pLoc->Release();      
  67.         return FALSE;                
  68.     }  
  • Using Smart Pointers

       如果你经常使用用享对象指针,如COM 接口等,那么建议使用智能指针来处理。智能指针会自动帮助你维护对象引用记数,并且保证你不会访问到被删除的对象。这样,不需要关心和控制接口的生命周期。关于智能指针的进一步知识可以看看Smart Pointers - What, Why, Which?Implementing a Simple Smart Pointer in C++这两篇文章。

       如面是一个展示使用ATL's CComPtr template 智能指针的代码,该部分代码来至于MSDN。

  1. #include <windows.h> 
  2. #include <shobjidl.h>  
  3. #include <atlbase.h> // Contains the declaration of CComPtr. 
  4. int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR pCmdLine, int nCmdShow) 
  5.     HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED |  
  6.         COINIT_DISABLE_OLE1DDE); 
  7.     if (SUCCEEDED(hr)) 
  8.     { 
  9.         CComPtr<IFileOpenDialog> pFileOpen; 
  10.         // Create the FileOpenDialog object. 
  11.         hr = pFileOpen.CoCreateInstance(__uuidof(FileOpenDialog)); 
  12.         if (SUCCEEDED(hr)) 
  13.         { 
  14.             // Show the Open dialog box. 
  15.             hr = pFileOpen->Show(NULL); 
  16.             // Get the file name from the dialog box. 
  17.             if (SUCCEEDED(hr)) 
  18.             { 
  19.                 CComPtr<IShellItem> pItem; 
  20.                 hr = pFileOpen->GetResult(&pItem); 
  21.                 if (SUCCEEDED(hr)) 
  22.                 { 
  23.                     PWSTR pszFilePath; 
  24.                     hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath); 
  25.                     // Display the file name to the user. 
  26.                     if (SUCCEEDED(hr)) 
  27.                     { 
  28.                         MessageBox(NULL, pszFilePath, L"File Path", MB_OK); 
  29.                         CoTaskMemFree(pszFilePath); 
  30.                     } 
  31.                 } 
  32.                 // pItem goes out of scope. 
  33.             } 
  34.             // pFileOpen goes out of scope. 
  35.         } 
  36.         CoUninitialize(); 
  37.     } 
  38.     return 0; 
  39. }   
  • Using == Operator Carefully

       先来看看如下代码;

  1. CVehicle* pVehicle = GetCurrentVehicle(); 
  2.   
  3. // Validate pointer 
  4. if(pVehicle==NULL) // Using == operator to compare pointer with NULL 
  5.    return FALSE;  
  6.  
  7. // Do something with the pointer 
  8. pVehicle->Run(); 

      上面的代码是正确的,用语指针检测。但是如果不小心用“=”替换了“==”,如下代码;

  1. CVehicle* pVehicle = GetCurrentVehicle(); 
  2.  
  3. // Validate pointer 
  4. if(pVehicle=NULL) // Oops! A mistyping here! 
  5.     return FALSE;  
  6.  
  7. // Do something with the pointer 
  8. pVehicle->Run(); // Crash!!!  

        看看上面的代码,这个的一个失误将导致程序崩溃。

       这样的错误是可以避免的,只需要将等号左右两边交换一下就可以了。如果在修改代码的时候,你不小心产生这种失误,这个错误在程序编译的时候将被检测出来。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值