忘記釋放記憶體(記憶體洩漏)

程式設計最佳實踐是釋放由你自己的程式碼直接分配的任何記憶體,或通過呼叫內部或外部函式(如 strdup() 等庫 API)隱式釋放 。無法釋放記憶體可能會導致記憶體洩漏,這可能會累積到程式(或系統)無法使用的大量浪費記憶體中,從而可能導致崩潰或未定義的行為。如果在迴圈或遞迴函式中重複發生洩漏,則更可能發生問題。洩漏程式執行的時間越長,程式失敗的風險就越大。有時候會出現問題; 其他時候,經常數小時甚至數年不會出現問題。記憶體耗盡故障可能是災難性的,具體取決於具體情況。

以下無限迴圈是洩漏的一個示例,它最終會通過呼叫 getline() 來消耗可用的記憶體洩漏,getline() 是一個隱式分配新記憶體的函式,而不釋放該記憶體。

#include <stdlib.h>
#include <stdio.h>

int main(void)
{
    char *line = NULL;
    size_t size = 0;

    /* The loop below leaks memory as fast as it can */

    for(;;) { 
        getline(&line, &size, stdin); /* New memory implicitly allocated */

        /* <do whatever> */

        line = NULL;
    }

    return 0;
}

相反,下面的程式碼也使用 getline() 函式,但這次,正確釋放分配的記憶體,避免洩漏。

#include <stdlib.h>
#include <stdio.h>

int main(void)
{
    char *line = NULL;
    size_t size = 0;

    for(;;) {
        if (getline(&line, &size, stdin) < 0) {
            free(line);
            line = NULL;

            /* Handle failure such as setting flag, breaking out of loop and/or exiting */
        }

        /* <do whatever> */

        free(line);
        line = NULL;

    }

    return 0;
}

洩漏記憶並不總是會產生切實的後果,也不一定是功能性問題。雖然最佳實踐要求在策略要點和條件下嚴格釋放記憶體,以減少記憶體佔用並降低記憶體耗盡的風險,但也有例外。例如,如果程式的持續時間和範圍有限,則可能會認為分配失敗的風險太小而無法擔心。在這種情況下,繞過顯式釋放可能被認為是可接受的。例如,大多數現代作業系統在程式終止時自動釋放程式消耗的所有記憶體,無論是由於程式故障,系統呼叫 exit(),程序終止還是到達 main() 結束。

如果記憶體不足,則分配可能會失敗,並且應在適當的呼叫堆疊級別考慮處理故障。getline(),如上所示是一個有趣的用例,因為它是一個庫函式,它不僅分配它留給呼叫者釋放的記憶體,而且可能由於多種原因而失敗,所有這些都必須加以考慮。因此,在使用 C API 時,必須閱讀 文件(手冊頁) 並特別注意錯誤條件和記憶體使用情況,並注意哪個軟體層承擔了釋放返回記憶體的負擔。

另一種常見的記憶體處理方法是在釋放這些指標引用的記憶體後立即將記憶體指標設定為 NULL,因此可以隨時測試這些指標的有效性(例如,檢查為 NULL /非 NULL),因為訪問釋放的記憶體可能導致嚴重問題,例如獲取垃圾資料(讀取操作)或資料損壞(寫入操作)和/或程式崩潰。在大多數現代作業系統中,釋放記憶體位置 0(NULL)是 NOP(例如它是無害的),正如 C 標準所要求的那樣 - 因此通過將指標設定為 NULL,如果指標沒有雙重釋放記憶體的風險被傳遞給 free()。請記住,雙重釋放記憶體可能會導致非常耗時,混亂且難以診斷故障。