介紹
在實際的專案中,當專案的程式碼量不斷增加的時候,你會發現越來越難管理和跟蹤其各個元件,如其不善,很容易就引入BUG。因此,我們應該掌握一些能讓我們程式更加健壯的方法。
這篇文章提出了一些建議,能有引導我們寫出更加健壯的程式碼,以避免產生災難性的錯誤。即使、因為其複雜性和專案團隊結構,你的程式目前不遵循任何編碼規則,按照下面列出的簡單的規則可以幫助您避免大多數的崩潰情況。
背景
先來介紹下作者開發一些軟體(CrashRpt),你可以http://code.google.com/p/crashrpt/網站上下載原始碼。CrashRpt 顧名思義軟體崩潰記錄軟體(庫),它能夠自動提交你電腦上安裝的軟體錯誤記錄。它通過乙太網直接將這些錯誤記錄傳送給你,這樣方便你跟蹤軟體問題,並及時修改,使得使用者感覺到每次釋出的軟體都有很大的提高,這樣他們自然很高興。
在分析接收的錯誤記錄的時候,我們發現採用下文介紹的方法能夠避免大部分程式崩潰的錯誤。例如:區域性變數未初始化導致陣列訪問越界,指標使用前未進行檢測(NULL)導致訪問訪問非法區域等。
我已經總結了幾條程式碼設計的方法和規則,在下文一一列出,希望能夠幫助你避免犯一些錯誤,使得你的程式更加健壯。
Initializing Local Variables (區域性變數初始化)
使用未初始化的區域性變數是引起程式崩潰的一個比較普遍的原因,例如、來看下面這段程式片段:
1 2 3 4 5 6 |
// Define local variables BOOL bExitResult; // This will be TRUE if the function exits successfully FILE* f; // Handle to file TCHAR szBuffer[_MAX_PATH]; // String buffer // Do something with variables above... |
上面的這段程式碼存在著一個潛在的錯誤,因為沒有一個區域性變數初始化了。當你的程式碼執行的時候,這些變數將被預設負一些錯誤的數值。例如bExitResult 數值將被負為-135913245 ,szBuffer?必須以“”結尾,結果不會。因此、區域性變數初始化時非常重要的,如下正確程式碼:
1 2 3 4 5 6 7 8 9 10 11 |
// Define local variables // Initialize function exit code with FALSE to indicate failure assumption BOOL bExitResult = FALSE; // This will be TRUE if the function exits successfully // Initialize file handle with NULL FILE* f = NULL; // Handle to file // Initialize string buffer with empty string TCHAR szBuffer[_MAX_PATH] = _T(""); // String buffer // Do something with variables above... |
注意:有人說變數初始化會引起程式效率降低,是的,確實如此,如果你確實非常在乎程式的執行效率,去除區域性變數初始化,你得想好其後果。
Initializing WinAPI Structures
許多Windows API都接受或則返回一些結構體引數,結構體如果沒有正確的初始化,也很有可能引起程式崩潰。大家可能會想起用ZeroMemory巨集或者memset()函式去用0填充這個結構體(對結構體對應的元素設定預設值)。但是大部分Windows API 結構體都必須有一個cbSIze引數,這個引數必須設定為這個結構體的大小。
看看下面程式碼,如何初始化Windows API結構體引數:
1 2 3 4 5 6 7 8 9 10 11 12 |
NOTIFYICONDATA nf; // WinAPI structure memset(&nf,0,sizeof(NOTIFYICONDATA)); // Zero memory nf.cbSize = sizeof(NOTIFYICONDATA); // Set structure size! // Initialize other structure members nf.hWnd = hWndParent; nf.uID = 0; nf.uFlags = NIF_ICON | NIF_TIP; nf.hIcon = ::LoadIcon(NULL, IDI_APPLICATION); _tcscpy_s(nf.szTip, 128, _T("Popup Tip Text")); // Add a tray icon Shell_NotifyIcon(NIM_ADD, &nf); |
注意:千萬不要用ZeroMemory和memset去初始化那些包括結構體物件的結構體,這樣很容易破壞其內部結構體,從而導致程式崩潰.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// Declare a C++ structure struct ItemInfo { // The structure has std::string object inside std::string sItemName; int nItemValue; }; // Init the structure ItemInfo item; // Do not use memset()! It can corrupt the structure // memset(&item, 0, sizeof(ItemInfo)); // Instead use the following item.sItemName = "item1"; item.nItemValue = 0; |
這裡最好是用結構體的建構函式對其成員進行初始化.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// Declare a C++ structure struct ItemInfo { // Use structure constructor to set members with default values ItemInfo() { sItemName = _T("unknown"); nItemValue = -1; } std::string sItemName; // The structure has std::string object inside int nItemValue; }; // Init the structure ItemInfo item; // Do not use memset()! It can corrupt the structure // memset(&item, 0, sizeof(ItemInfo)); // Instead use the following item.sItemName = "item1"; item.nItemValue = 0; |
Validating Function Input
在函式設計的時候,對傳入的引數進行檢測是一直都推薦的。例如、如果你設計的函式是公共API的一部分,它可能被外部客戶端呼叫,這樣很難保證客戶端傳進入的引數就是正確的。
例如,讓我們來看看這個hypotethical DrawVehicle()?函式,它可以根據不同的質量來繪製一輛跑車,這個質量數值(nDrawingQaulity )是0~100。prcDraw?定義這輛跑車的輪廓區域。
看看下面程式碼,注意觀察我們是如何在使用函式引數之前進行引數檢測:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
BOOL DrawVehicle(HWND hWnd, LPRECT prcDraw, int nDrawingQuality) { // Check that window is valid if(!IsWindow(hWnd)) return FALSE; // Check that drawing rect is valid if(prcDraw==NULL) return FALSE; // Check drawing quality is valid if(nDrawingQuality<0 || nDrawingQuality>100) return FALSE; // Now it's safe to draw the vehicle // ... return TRUE; } |
在指標使用之前,不檢測是非常普遍的,這個可以說是我們引起軟體崩潰最有可能的原因。如果你用一個指標,這個指標剛好是NULL,那麼你的程式在執行時,將報出異常。
1 2 3 4 5 6 7 8 9 10 |
CVehicle* pVehicle = GetCurrentVehicle(); // Validate pointer if(pVehicle==NULL) { // Invalid pointer, do not use it! return FALSE; } // Use the pointer |
Initializing Function Output
如果你的函式建立了一個物件,並要將它作為函式的返回引數。那麼記得在使用之前把他複製為NULL。如不然,這個函式的呼叫者將使用這個無效的指標,進而一起程式錯誤。如下錯誤程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 |
int CreateVehicle(CVehicle** ppVehicle) { if(CanCreateVehicle()) { *ppVehicle = new CVehicle(); return 1; } // If CanCreateVehicle() returns FALSE, // the pointer to *ppVehcile would never be set! return 0; } |
正確的程式碼如下;
1 2 3 4 5 6 7 8 9 10 11 12 13 |
int CreateVehicle(CVehicle** ppVehicle) { // First initialize the output parameter with NULL *ppVehicle = NULL; if(CanCreateVehicle()) { *ppVehicle = new CVehicle(); return 1; } return 0; } |
Cleaning Up Pointers to Deleted Objects
在記憶體釋放之後,無比將指標複製為NULL。這樣可以確保程式的沒有那個地方會再使用無效指標。其實就是,訪問一個已經被刪除的物件地址,將引起程式異常。如下程式碼展示如何清除一個指標指向的物件:
1 2 3 4 |
// Create object CVehicle* pVehicle = new CVehicle(); delete pVehicle; // Free pointer pVehicle = NULL; // Set pointer with NULL |
Cleaning Up Released Handles
在釋放一個控制程式碼之前,務必將這個控制程式碼複製偽NULL (0或則其他預設值)。這樣能夠保證程式其他地方不會重複使用無效控制程式碼。看看如下程式碼,如何清除一個Windows API的檔案控制程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
HANDLE hFile = INVALID_HANDLE_VALUE; // Open file hFile = CreateFile(_T("example.dat"), FILE_READ|FILE_WRITE, FILE_OPEN_EXISTING); if(hFile==INVALID_HANDLE_VALUE) { return FALSE; // Error opening file } // Do something with file // Finally, close the handle if(hFile!=INVALID_HANDLE_VALUE) { CloseHandle(hFile); // Close handle to file hFile = INVALID_HANDLE_VALUE; // Clean up handle } |
下面程式碼展示如何清除File *控制程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// First init file handle pointer with NULL FILE* f = NULL; // Open handle to file errno_t err = _tfopen_s(_T("example.dat"), _T("rb")); if(err!=0 || f==NULL) return FALSE; // Error opening file // Do something with file // When finished, close the handle if(f!=NULL) // Check that handle is valid { fclose(f); f = NULL; // Clean up pointer to handle } |
Using delete [] Operator for Arrays
如果你分配一個單獨的物件,可以直接使用new?,同樣你釋放單個物件的時候,可以直接使用delete . 然而,申請一個物件陣列物件的時候可以使用new,但是釋放的時候就不能使用delete ,而必須使用delete[]:
1 2 3 4 5 |
// Create an array of objects CVehicle* paVehicles = new CVehicle[10]; delete [] paVehicles; // Free pointer to array paVehicles = NULL; // Set pointer with NULL |
或者:
1 2 3 4 5 |
// Create a buffer of bytes LPBYTE pBuffer = new BYTE[255]; delete [] pBuffer; // Free pointer to array pBuffer = NULL; // Set pointer with NULL |
Allocating Memory Carefully
有時候,程式需要動態分配一段緩衝區,這個緩衝區是在程式執行的時候決定的。例如、你需要讀取一個檔案的內容,那麼你就需要申請該檔案大小的緩衝區來儲存該檔案的內容。在申請這段記憶體之前,請注意,malloc() or new是不能申請0位元組的記憶體,如不然,將導致malloc() or new函式呼叫失敗。傳遞錯誤的引數給malloc() 函式將導致C執行時錯誤。如下程式碼展示如何動態申請記憶體:
1 2 3 4 5 6 7 8 |
// Determine what buffer to allocate. UINT uBufferSize = GetBufferSize(); LPBYTE* pBuffer = NULL; // Init pointer to buffer // Allocate a buffer only if buffer size > 0 if(uBufferSize>0) pBuffer = new BYTE[uBufferSize]; |
為了進一步瞭解如何正確的分配記憶體,你可以讀下Secure Coding Best Practices for Memory Allocation in C and C++這篇文章。
Using Asserts Carefully
Asserts用語除錯模式檢測先決條件和後置條件。但當我們編譯器處於release模式的時候,Asserts在預編階段被移除。因此,用Asserts是不能夠檢測我們的程式狀態,錯誤程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#include <assert.h> // This function reads a sports car's model from a file CVehicle* ReadVehicleModelFromFile(LPCTSTR szFileName) { CVehicle* pVehicle = NULL; // Pointer to vehicle object // Check preconditions assert(szFileName!=NULL); // This will be removed by preprocessor in Release mode! assert(_tcslen(szFileName)!=0); // This will be removed in Release mode! // Open the file FILE* f = _tfopen(szFileName, _T("rt")); // Create new CVehicle object pVehicle = new CVehicle(); // Read vehicle model from file // Check postcondition assert(pVehicle->GetWheelCount()==4); // This will be removed in Release mode! // Return pointer to the vehicle object return pVehicle; } |
看看上述的程式碼,Asserts能夠在debug模式下檢測我們的程式,在release 模式下卻不能。所以我們還是不得不用if()來這步檢測操作。正確的程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
#include <assert.h> CVehicle* ReadVehicleModelFromFile(LPCTSTR szFileName, ) { CVehicle* pVehicle = NULL; // Pointer to vehicle object // Check preconditions assert(szFileName!=NULL); // This will be removed by preprocessor in Release mode! assert(_tcslen(szFileName)!=0); // This will be removed in Release mode! if(szFileName==NULL || _tcslen(szFileName)==0) return NULL; // Invalid input parameter // Open the file FILE* f = _tfopen(szFileName, _T("rt")); // Create new CVehicle object pVehicle = new CVehicle(); // Read vehicle model from file // Check postcondition assert(pVehicle->GetWheelCount()==4); // This will be removed in Release mode! if(pVehicle->GetWheelCount()!=4) { // Oops... an invalid wheel count was encountered! delete pVehicle; pVehicle = NULL; } // Return pointer to the vehicle object return pVehicle; } |
Checking Return Code of a Function
斷定一個函式執行一定成功是一種常見的錯誤。當你呼叫一個函式的時候,建議檢查下返回程式碼和返回引數的值。如下程式碼持續呼叫Windows API ,程式是否繼續執行下去依賴於該函式的返回結果和返回引數值.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
HRESULT hres = E_FAIL; IWbemServices *pSvc = NULL; IWbemLocator *pLoc = NULL; hres = CoInitializeSecurity( NULL, -1, // COM authentication NULL, // Authentication services NULL, // Reserved RPC_C_AUTHN_LEVEL_DEFAULT, // Default authentication RPC_C_IMP_LEVEL_IMPERSONATE, // Default Impersonation NULL, // Authentication info EOAC_NONE, // Additional capabilities NULL // Reserved ); if (FAILED(hres)) { // Failed to initialize security if(hres!=RPC_E_TOO_LATE) return FALSE; } hres = CoCreateInstance( CLSID_WbemLocator, 0, CLSCTX_INPROC_SERVER, IID_IWbemLocator, (LPVOID *) &pLoc); if (FAILED(hres) || !pLoc) { // Failed to create IWbemLocator object. return FALSE; } hres = pLoc->ConnectServer( _bstr_t(L"ROOT\\CIMV2"), // Object path of WMI namespace NULL, // User name. NULL = current user NULL, // User password. NULL = current 0, // Locale. NULL indicates current NULL, // Security flags. 0, // Authority (e.g. Kerberos) 0, // Context object &pSvc // pointer to IWbemServices proxy ); if (FAILED(hres) || !pSvc) { // Couldn't conect server if(pLoc) pLoc->Release(); return FALSE; } hres = CoSetProxyBlanket( pSvc, // Indicates the proxy to set RPC_C_AUTHN_WINNT, // RPC_C_AUTHN_xxx RPC_C_AUTHZ_NONE, // RPC_C_AUTHZ_xxx NULL, // Server principal name RPC_C_AUTHN_LEVEL_CALL, // RPC_C_AUTHN_LEVEL_xxx RPC_C_IMP_LEVEL_IMPERSONATE, // RPC_C_IMP_LEVEL_xxx NULL, // client identity EOAC_NONE // proxy capabilities ); if (FAILED(hres)) { // Could not set proxy blanket. if(pSvc) pSvc->Release(); if(pLoc) pLoc->Release(); return FALSE; } |
Using Smart Pointers
如果你經常使用用享物件指標,如COM 介面等,那麼建議使用智慧指標來處理。智慧指標會自動幫助你維護物件引用記數,並且保證你不會訪問到被刪除的物件。這樣,不需要關心和控制介面的生命週期。關於智慧指標的進一步知識可以看看Smart Pointers – What, Why, Which??和 Implementing a Simple Smart Pointer in C++這兩篇文章。
如面是一個展示使用ATL’s CComPtr template 智慧指標的程式碼,該部分程式碼來至於MSDN。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
#include <windows.h> #include <shobjidl.h> #include <atlbase.h> // Contains the declaration of CComPtr. int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR pCmdLine, int nCmdShow) { HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); if (SUCCEEDED(hr)) { CComPtr<IFileOpenDialog> pFileOpen; // Create the FileOpenDialog object. hr = pFileOpen.CoCreateInstance(__uuidof(FileOpenDialog)); if (SUCCEEDED(hr)) { // Show the Open dialog box. hr = pFileOpen->Show(NULL); // Get the file name from the dialog box. if (SUCCEEDED(hr)) { CComPtr<IShellItem> pItem; hr = pFileOpen->GetResult(&pItem); if (SUCCEEDED(hr)) { PWSTR pszFilePath; hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath); // Display the file name to the user. if (SUCCEEDED(hr)) { MessageBox(NULL, pszFilePath, L"File Path", MB_OK); CoTaskMemFree(pszFilePath); } } // pItem goes out of scope. } // pFileOpen goes out of scope. } CoUninitialize(); } return 0; } |
Using == Operator Carefully
先來看看如下程式碼;
1 2 3 4 5 6 7 8 |
CVehicle* pVehicle = GetCurrentVehicle(); // Validate pointer if(pVehicle==NULL) // Using == operator to compare pointer with NULL return FALSE; // Do something with the pointer pVehicle->Run(); |
上面的程式碼是正確的,用語指標檢測。但是如果不小心用“=”替換了“==”,如下程式碼;
1 2 3 4 5 6 7 8 |
CVehicle* pVehicle = GetCurrentVehicle(); // Validate pointer if(pVehicle=NULL) // Oops! A mistyping here! return FALSE; // Do something with the pointer pVehicle->Run(); // Crash!!! |
看看上面的程式碼,這個的一個失誤將導致程式崩潰。
這樣的錯誤是可以避免的,只需要將等號左右兩邊交換一下就可以了。如果在修改程式碼的時候,你不小心產生這種失誤,這個錯誤在程式編譯的時候將被檢測出來。
1 2 3 4 5 6 7 8 9 |
// Validate pointer if(NULL==pVehicle) // Exchange left side and right side of the equality operator return FALSE; // Validate pointer if(NULL=pVehicle) // Oops! A mistyping here! But the compiler returns an error message. return FALSE; |