• 讓你的C++代碼變的更加強大

    0
    .NET C/C++ Go ico 11346 次瀏覽

      英文原文:Making Your C++ Code Robust

    • Introduction

      在實際的項目中,當項目的代碼量不斷增加的時候,你會發現越來越難管理和跟蹤各個組件,如其不善,很容易就引入BUG。因此,我們應該掌握一些能讓我們程序更加健壯的方法。

      這篇文章提出了一些建議,能引導我們寫出更加強壯的代碼,以避免產生災難性的錯誤。即使,因為其復雜性和項目團隊結構,你的程序目前不遵循任何編碼規則,按照下面列出的簡單的規則可以幫助您避免大多數的崩潰情況。

    • Background

      先來介紹下作者開發的一些軟件(CrashRpt),你可以http://code.google.com/p/crashrpt/網站上下載源代碼。CrashRpt 顧名思義軟件崩潰記錄軟件(庫),它能夠自動提交你電腦上安裝的軟件錯誤記錄。它通過以太網直接將這些錯誤記錄發送給你,這樣方便你跟蹤軟件問題,并及時修改,使得用戶感覺到每次發布的軟件都有很大的提高,這樣他們自然很高興。

    2011072017333214.png

    圖 1、CrashRpt 庫檢測到錯誤彈出的對話框

      在分析接收的錯誤記錄的時候,我們發現采用下文介紹的方法能夠避免大部分程序崩潰的錯誤。例如,局部變量未初始化導致數組訪問越界,指針使用前未進行檢測(NULL)導致訪問非法區域等。

      我已經總結了幾條代碼設計的方法和規則,在下文一一列出,希望能夠幫助你避免犯一些錯誤,使得你的程序更加健壯。

    • Initializing Local Variables 

      使用未初始化的局部變量是引起程序崩潰的一個比較普遍的原因,例如,來看下面這段程序片段:

    // 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必須以“\0”結尾,結果不會。因此,局部變量初始化時非常重要的,如下正確代碼:

    // 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結構體參數:

    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去初始化那些包括結構體對象的結構體,這樣很容易破壞其內部結構體,從而導致程序崩潰。

    // 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 corrupt the structure
    // memset(&item, 0, sizeof(ItemInfo));
    // Instead use the following
    item.sItemName ="item1";
    item.nItemValue
    =0;
    這里最好是用結構體的構造函數對其成員進行初始化.

    // 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 定義這輛跑車的輪廓區域。

      看看下面代碼,注意觀察我們是如何在使用函數參數之前進行參數檢測:

    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;
    }
    • Validating Pointers

      在指針使用之前,不檢測是非常普遍的,這個可以說是我們引起軟件崩潰最有可能的原因。如果你用一個指針,這個指針剛好是NULL,那么你的程序在運行時,將報出異常。

    CVehicle* pVehicle = GetCurrentVehicle();
    // Validate pointer
    if(pVehicle==NULL)
    {
       
    // Invalid pointer, do not use it!
        return FALSE;
    }
    • Initializing Function Output

      如果你的函數創建了一個對象,并要將它作為函數的返回參數。那么記得在使用之前把他復制為NULL。如不然,這個函數的調用者將使用這個無效的指針,進而一起程序錯誤。如下錯誤代碼:

    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;
    }

    正確的代碼如下;

    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。這樣可以確保程序的沒有那個地方會再使用無效指針。其實就是,訪問一個已經被刪除的對象地址,將引起程序異常。如下代碼展示如何清除一個指針指向的對象:

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

      在釋放一個句柄之前,務必將這個句柄復制偽NULL (0或則其他默認值)。這樣能夠保證程序其他地方不會重復使用無效句柄。看看如下代碼,如何清除一個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
    }

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

    // 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[]:

    // Create an array of objects
    CVehicle* paVehicles = new CVehicle[10];
    delete [] paVehicles;
    // Free pointer to array
    paVehicles = NULL; // Set pointer with NULL
    or
    // 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運行時錯誤。如下代碼展示如何動態申請內存:

    // 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是不能夠檢測我們的程序狀態,錯誤代碼如下:

    #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()來這步檢測操作。正確的代碼如下;

    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 ,程序是否繼續執行下去依賴于該函數的返回結果和返回參數值。

    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。

    #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

      先來看看如下代碼:

    CVehicle* pVehicle = GetCurrentVehicle();

    // Validate pointer
    if(pVehicle==NULL) // Using == operator to compare pointer with NULL
    return FALSE;

    // Do something with the pointer
    pVehicle->Run();

      上面的代碼是正確的,用語指針檢測。但是如果不小心用“=”替換了“==”,如下代碼;

    CVehicle* pVehicle = GetCurrentVehicle();

    // Validate pointer
    if(pVehicle=NULL) // Oops! A mistyping here!
    return FALSE;

    // Do something with the pointer
    pVehicle->Run(); // Crash!!!

      看看上面的代碼,這個的一個失誤將導致程序崩潰。

      這樣的錯誤是可以避免的,只需要將等號左右兩邊交換一下就可以了。如果在修改代碼的時候,你不小心產生這種失誤,這個錯誤在程序編譯的時候將被檢測出來。

    原文:http://blog.csdn.net/xxxluozhen/article/details/6611663

    相似問題

    相關經驗

    相關資訊

    相關文檔

  • sesese色