Making your c ++ code robust
In actual projects, when the amount of code in the project increases, you will find it increasingly difficult to manage and track its components. If it is not good, it is easy to introduce bugs. Therefore, we should master some methods that make our programs more robust.
This article provides some suggestions to guide us to write stronger code to avoid catastrophic errors. Even because of its complexity and the structure of the project team, your program does not currently comply with any encoding rules. The simple rules listed below can help you avoid most crashes.
Let's first introduce the author's development of some software (crashrpt). You can download the source code at http://code.google.com/p/crashrpt/website. Crashrpt, as its name implies, is a software crash record software (database) that automatically submits software error records installed on your computer. It directly sends these error records to you over Ethernet, so that you can track software problems and modify them in time, so that users can feel that the software released each time has greatly improved, in this way, they are naturally very happy.
Figure 1. dialog box popped up when the crashrpt database detects an error
When analyzing the received error records, we found that using the methods described below can avoid most program crash errors. For example, if the local variable is not initialized, the array access is out of bounds, and the pointer is not detected before use (null), leading to access to illegal regions.
I have summarized several code design methods and rules, which are listed in the following sections, hoping to help you avoid mistakes and make your program more robust.
- Initializing local variables
Using uninitialized local variables is a common cause of program crash. For example, let's look at the following program snippet:
// 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...
The above code has a potential error because no local variable is initialized. When your code runs, these variables will be negative by default. For example, the bexitresult value will be negative-135913245, And the szbuffer must end with "\ 0", but the result will not. Therefore, local variables are very important during initialization. The following code is correct:
// 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...
Note: Some people say that variable initialization will lead to lower program efficiency. Yes, it is true. If you really care about program execution efficiency and remove local variable initialization, you have to think about the consequences.
- Initializing winapi Structures
Many windows APIs accept or return some struct parameters. If the struct is not correctly initialized, the program may crash. You may remember to use the zeromemory macro or memset () function to fill the struct with 0 (set the default value for the elements corresponding to the struct ). However, most Windows API structures must have a cbsize parameter, which must be set to the size of the struct.
Let's take a look at the following code to initialize the Windows API struct parameters:
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);
Note: Do not use zeromemory and memset to initialize the struct including the struct object. This can easily destroy the internal struct and cause program crash.
// Declare a C ++ structure struct iteminfo {STD: String sitemname; // The structure has STD: String object inside int nitemvalue ;}; // init the structure iteminfo item; // do not use memset ()! It can reset upt the structure // memset (& item, 0, sizeof (iteminfo); // instead use the following item. sitemname = "Item1"; item. nitemvalue = 0; it is best to use the constructor of the struct to initialize its members. // 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 reset upt the structure // memset (& item, 0, sizeof (iteminfo); // instead use the following item. sitemname = "Item1"; item. nitemvalue = 0;
- Validating function input
During function design, it is always recommended to check input parameters. For example, if the function you designed is part of a public API, it may be called by an external client, so it is difficult to ensure that the parameters passed by the client are correct.
For example, let's take a look at this hypotethical drawvehicle () function, which can draw a sports car based on different quality. The ndrawingqaulity is 0 ~ 100. Prcdraw defines the outline area of the sports car.
Take a look at the following code and observe how we perform parameter detection before using function parameters:
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; }
Before using pointers, non-detection is very common. This can be said to be the most likely cause of software crash. If you use a pointer and the pointer is null, an exception will be reported during running of your program.
CVehicle* pVehicle = GetCurrentVehicle(); // Validate pointer if(pVehicle==NULL) { // Invalid pointer, do not use it! return FALSE; }
- Initializing function output
If your function creates an object and uses it as the return parameter of the function. So remember to copy it to null before use. Otherwise, the caller of this function will use this Invalid Pointer and cause a program error. The following error code:
Int createvehicle (cvehicle ** ppvehicle) {If (cancreatevehicle () {* ppvehicle = new cvehicle (); return 1 ;}// if cancreatevehicle () returns false, // The pointer to * ppvehcile wocould never be set! Return 0;} the correct code is as follows; 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
After the memory is released, the pointer is copied to null. In this way, we can ensure that invalid pointers are no longer used in the program. In fact, accessing a deleted object address will cause program exceptions. The following code clears an object pointed to by a pointer:
// Create object CVehicle* pVehicle = new CVehicle(); delete pVehicle; // Free pointer pVehicle = NULL; // Set pointer with NULL
- Cleaning up released handles
Before releasing a handle, copy the handle to a pseudo null value (0 or other default values ). This ensures that invalid handles are not reused elsewhere in the program. See the following code to clear the file handle of a Windows API:
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 }
The following code clears the file * handle:
// 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
If you allocate a separate object, you can use new directly. When you release a single object, you can use Delete directly. however, you can use new when applying for an object array object, but you cannot use Delete when releasing it. Instead, you must use Delete []:
// Create an array of objects CVehicle* paVehicles = new CVehicle[10]; delete [] paVehicles; // Free pointer to array paVehicles = NULL; // Set pointer with NULLor // 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
Sometimes, the program needs to dynamically allocate a buffer, which is determined when the program is running. For example, if you want to read the content of a file, you need to apply for a buffer of the file size to save the content of the file. Before applying for the memory, note that malloc () or new cannot apply for a 0-byte memory. Otherwise, malloc () or new function calls will fail. Passing an incorrect parameter to the malloc () function will cause a C runtime error. The following code shows how to dynamically apply for memory:
// 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];
To learn more about how to properly allocate memory, read the secure coding best practices for memory allocation in C and C ++ article.
Asserts language debugging mode detection prerequisites and post conditions. However, when our compiler is in the release mode, asserts is removed during the pre-encoding phase. Therefore, asserts cannot detect the program status. The error code is as follows:
#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; }
Looking at the above Code, asserts can detect our programs in debug mode, but not in release mode. Therefore, we still have to use if () for this step. The correct code is as follows;
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
It is a common mistake to determine whether a function is successfully executed. When you call a function, we recommend that you check the return code and return parameter values. The following code continuously calls the Windows API. Whether the program continues to execute depends on the returned results and returned parameter values of the function.
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; }
If you often use exclusive object pointers, such as the COM interface, we recommend that you use smart pointers for processing. Smart pointers automatically help you maintain the object reference count and ensure that you do not access the deleted object. In this way, you do not need to care about and control the interface lifecycle. For more information about smart pointers, see smart Pointers-what, why, and which? And implementing a simple smart pointer in C ++.
For example, it is a code that shows how to use the ATL's ccomptr template smart pointer.
#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
Let's take a look at the following code;
CVehicle* pVehicle = GetCurrentVehicle(); // Validate pointer if(pVehicle==NULL) // Using == operator to compare pointer with NULL return FALSE; // Do something with the pointer pVehicle->Run();
The above code is correct, and the terms pointer detection. However, if "=" is replaced with "=", the following code is used;
CVehicle* pVehicle = GetCurrentVehicle(); // Validate pointer if(pVehicle=NULL) // Oops! A mistyping here! return FALSE; // Do something with the pointer pVehicle->Run(); // Crash!!!
Look at the code above. This mistake will cause the program to crash.
This error can be avoided. You only need to swap the equal signs between the two sides. If you accidentally make this mistake when modifying the code, this error will be detected during program compilation.