C++|堆內存之懸垂指針、空指針、內存泄露

小智雅匯 發佈 2022-07-03T18:17:53.489361+00:00

C++ supports three basic types of memory allocation.C++支持三種基本類型的內存分配。Static memory allocation happens for static and global variables.

C++ supports three basic types of memory allocation.

C++支持三種基本類型的內存分配。

Static memory allocation happens for static and global variables. Memory for these types of variables is allocated once when your program is run and persists throughout the life of your program.

靜態內存分配針對靜態和全局變量。這些類型變量的內存在程序運行時分配一次,並在程序的整個生命周期內持續存在。

Automatic memory allocation happens for function parameters and local variables. Memory for these types of variables is allocated when the relevant block is entered, and freed when the block is exited, as many times as necessary.

函數參數和局部變量會自動分配內存。這些類型變量的內存在輸入相關塊時分配,在退出塊時釋放,次數視需要而定。

Both static and automatic allocation have two things in common:

靜態和自動分配有兩個共同點:

The size of the variable / array must be known at compile time.

編譯時必須知道變量/數組的大小。

Memory allocation and deallocation happens automatically (when the variable is instantiated / destroyed).

內存分配和釋放自動發生(當變量實例化/銷毀時)。

Most of the time, this is just fine. However, you will come across situations where one or both of these constraints cause problems, usually when dealing with external (user or file) input.

大多數時候,這很好。但是,通常在處理外部(用戶或文件)輸入時,您會遇到這些約束中的一個或兩個都會導致問題的情況。

For example, we may want to use a string to hold someone’s name, but we do not know how long their name is until they enter it. Or we may want to read in a number of records from disk, but we don’t know in advance how many records there are. Or we may be creating a game, with a variable number of monsters (that changes over time as some monsters die and new ones are spawned) trying to kill the player.

例如,我們可能希望使用字符串來保存某人的名字,但在他們輸入名字之前,我們不知道他們的名字有多長。或者我們可能想從磁碟中讀取一些記錄,但我們事先不知道有多少記錄。或者我們可能正在創建一個遊戲,有不同數量的怪物(隨著時間的推移,隨著一些怪物死亡和新的怪物產生而變化)試圖殺死玩家。

Fortunately, these problems are easily addressed via dynamic memory allocation. Dynamic memory allocation is a way for running programs to request memory from the operating system when needed. This memory does not come from the program’s limited stack memory -- instead, it is allocated from a much larger pool of memory managed by the operating system called the heap. On modern machines, the heap can be gigabytes in size.

幸運的是,這些問題很容易通過動態內存分配解決。動態內存分配是一種在需要時運行程序從作業系統請求內存的方法。該內存不是來自程序有限的堆棧內存,而是從作業系統管理的更大的內存池(稱為堆)中分配的。在現代機器上,堆的大小可以是GB。

Your computer has memory (probably lots of it) that is available for applications to use. When you run an application, your operating system loads the application into some of that memory. This memory used by your application is divided into different areas, each of which serves a different purpose. One area contains your code. Another area is used for normal operations (keeping track of which functions were called, creating and destroying global and local variables, etc…). However, much of the memory available just sits there, waiting to be handed out to programs that request it.

你的電腦有內存(可能很多),可以供應用程式使用。當您運行應用程式時,您的作業系統會將應用程式加載到部分內存中。應用程式使用的內存分為不同的區域,每個區域都有不同的用途。一個區域包含您的代碼。另一個區域用於正常操作(跟蹤調用了哪些函數,創建和銷毀全局和局部變量等)。然而,大部分可用內存就在那裡,等待分發給請求它的程序。

When you dynamically allocate memory, you’re asking the operating system to reserve some of that memory for your program’s use. If it can fulfill this request, it will return the address of that memory to your application. From that point forward, your application can use this memory as it wishes. When your application is done with the memory, it can return the memory back to the operating system to be given to another program.

動態分配內存時,要求作業系統保留一些內存供程序使用。如果它能滿足這個請求,它會將該內存的地址返回給您的應用程式。從那時起,應用程式可以隨心所欲地使用此內存。當應用程式使用內存完成時,它可以將內存返回到作業系統,以提供給另一個程序。

Unlike static or automatic memory, the program itself is responsible for requesting and disposing of dynamically allocated memory.

與靜態或自動內存不同,程序本身負責請求和處理動態分配的內存。

1 Dangling pointers 懸掛指針

C++ does not make any guarantees about what will happen to the contents of deallocated memory, or to the value of the pointer being deleted. In most cases, the memory returned to the operating system will contain the same values it had before it was returned, and the pointer will be left pointing to the now deallocated memory.

C++不保證被釋放內存的內容或被刪除指針的值會發生什麼變化。在大多數情況下,返回到作業系統的內存將包含與返回之前相同的值,指針將指向現在釋放的內存。

A pointer that is pointing to deallocated memory is called a dangling pointer. Indirection through- or deleting a dangling pointer will lead to undefined behavior. Consider the following program:

指向釋放內存的指針稱為懸掛指針。間接尋址或刪除懸空指針將導致未定義的行為。考慮以下程序:

#include <iostream>

int main()
{
    int* ptr = new int ; // dynamically allocate an integer
    *ptr = 7; // put a value in that memory location

    delete ptr; // return the memory to the operating system.  ptr is now a dangling pointer.

    std::cout << *ptr; // Indirection through a dangling pointer will cause undefined behavior
    delete ptr; // trying to deallocate the memory again will also lead to undefined behavior.
    getchar();
    return 0;
}

In the above program, the value of 7 that was previously assigned to the allocated memory will probably still be there, but it’s possible that the value at that memory address could have changed. It’s also possible the memory could be allocated to another application (or for the operating system’s own usage), and trying to access that memory will cause the operating system to shut the program down.

在上述程序中,先前分配給已分配內存的值7可能仍然存在,但該內存地址處的值可能已經更改。內存也可能分配給另一個應用程式(或作業系統自己使用),嘗試訪問該內存將導致作業系統關閉程序。

Deallocating memory may create multiple dangling pointers. Consider the following example:

釋放內存可能會創建多個懸空指針。考慮以下示例:

#include <iostream>

int main()
{
    int* ptr{ new int{} }; // dynamically allocate an integer
    int* otherPtr{ ptr }; // otherPtr is now pointed at that same memory location

    delete ptr; // return the memory to the operating system.  ptr and otherPtr are now dangling pointers.
    ptr = nullptr; // ptr is now a nullptr

    // however, otherPtr is still a dangling pointer!

    return 0;
}

There are a few best practices that can help here.

這裡有一些最佳實踐可以提供幫助。

First, try to avoid having multiple pointers point at the same piece of dynamic memory. If this is not possible, be clear about which pointer 「owns」 the memory (and is responsible for deleting it) and which pointers are just accessing it.

首先,儘量避免多個指針指向同一塊動態內存。如果這不可能,請明確哪個指針「擁有」內存(並負責刪除它)以及哪些指針正在訪問它。

Second, when you delete a pointer, if that pointer is not going out of scope immediately afterward, set the pointer to nullptr. We』ll talk more about null pointers, and why they are useful in a bit.

其次,當刪除指針時,如果該指針沒有立即超出範圍,請將指針設置為nullptr。我們將進一步討論空指針,以及它們為什麼有用。

When requesting memory from the operating system, in rare circumstances, the operating system may not have any memory to grant the request with.

當從作業系統請求內存時,在極少數情況下,作業系統可能沒有任何內存來授予請求。

By default, if new fails, a bad_alloc exception is thrown. If this exception isn’t properly handled (and it won’t be, since we haven’t covered exceptions or exception handling yet), the program will simply terminate (crash) with an unhandled exception error.

默認情況下,如果new失敗,則會引發bad_alloc異常。如果這個異常沒有得到正確處理(而且不會,因為我們還沒有討論異常或異常處理),程序將簡單地終止(崩潰),並出現未處理的異常錯誤。

In many cases, having new throw an exception (or having your program crash) is undesirable, so there’s an alternate form of new that can be used instead to tell new to return a null pointer if memory can’t be allocated. This is done by adding the constant std::nothrow between the new keyword and the allocation type:

在許多情況下,讓new拋出異常(或讓程序崩潰)是不可取的,因此可以使用另一種形式的new來代替,在無法分配內存時告訴new返回空指針。這是通過在新關鍵字和分配類型之間添加常量std::nothrow來實現的:

int* value { new (std::nothrow) int }; // value will be set to a null pointer if the integer allocation fails

In the above example, if new fails to allocate memory, it will return a null pointer instead of the address of the allocated memory.

在上面的示例中,如果new無法分配內存,它將返回一個空指針,而不是已分配內存的地址。

Note that if you then attempt indirection through this pointer, undefined behavior will result (most likely, your program will crash). Consequently, the best practice is to check all memory requests to ensure they actually succeeded before using the allocated memory.

請注意,如果隨後嘗試通過該指針進行間接尋址,將導致未定義的行為(很可能是程序崩潰)。因此,最佳做法是在使用分配的內存之前檢查所有內存請求,以確保它們實際成功。

int* value { new (std::nothrow) int{} }; // ask for an integer's worth of memory
if (!value) // handle case where new returned null
{
    // Do error handling here
    std::cerr << "Could not allocate memory\n";
}

Because asking new for memory only fails rarely (and almost never in a dev environment), it’s common to forget to do this check!

因為向new請求內存很少失敗(在dev環境中幾乎從未失敗),所以忘記執行此檢查是很常見的!

2 Null pointers 空指針

Null pointers (pointers set to nullptr) are particularly useful when dealing with dynamic memory allocation. In the context of dynamic memory allocation, a null pointer basically says 「no memory has been allocated to this pointer」. This allows us to do things like conditionally allocate memory:

空指針(設置為nullptr的指針)在處理動態內存分配時特別有用。在動態內存分配的上下文中,空指針基本上表示「沒有為該指針分配內存」。這使我們可以有條件地分配內存:

// If ptr isn't already allocated, allocate it
if (!ptr)
    ptr = new int;

Deleting a null pointer has no effect. Thus, there is no need for the following:

刪除空指針無效。因此,不需要以下內容:

if (ptr)
    delete ptr;

Instead, you can just write:

相反,你可以只寫:

delete ptr;

If ptr is non-null, the dynamically allocated variable will be deleted. If it is null, nothing will happen.

如果ptr非空,則將刪除動態分配的變量。如果為null,則不會發生任何事情。

A void pointer is a pointer that can point to any type of object, but does not know what type of object it points to. A void pointer must be explicitly cast into another type of pointer to perform indirection. A null pointer is a pointer that does not point to an address. A void pointer can be a null pointer.

空類型(void)指針是一種指針,它可以指向任何類型的對象,但不知道它指向什麼類型的對象。空類型指針必須顯式轉換為另一種類型的指針才能執行間接尋址。空(null)指針是指不指向地址的指針。空類型(void)指針可以是空(null)指針。

3 Memory leaks 內存泄漏

Dynamically allocated memory stays allocated until it is explicitly deallocated or until the program ends (and the operating system cleans it up, assuming your operating system does that). However, the pointers used to hold dynamically allocated memory addresses follow the normal scoping rules for local variables. This mismatch can create interesting problems.

動態分配的內存保持分配狀態,直到顯式取消分配或程序結束(假設您的作業系統這樣做,作業系統會將其清除)。然而,用於保存動態分配的內存地址的指針遵循局部變量的正常作用域規則。這種不匹配可能會產生有趣的問題。

Consider the following function:

考慮以下功能:

void doSomething()
{
    int* ptr{ new int{} };
}

This function allocates an integer dynamically, but never frees it using delete. Because pointers variables are just normal variables, when the function ends, ptr will go out of scope. And because ptr is the only variable holding the address of the dynamically allocated integer, when ptr is destroyed there are no more references to the dynamically allocated memory. This means the program has now 「lost」 the address of the dynamically allocated memory. As a result, this dynamically allocated integer can not be deleted.

此函數動態分配整數,但從不使用delete釋放它。因為指針變量只是普通變量,當函數結束時,ptr將超出範圍。由於ptr是唯一保存動態分配整數地址的變量,當ptr被破壞時,就不再引用動態分配的內存。這意味著程序現在已經「丟失」了動態分配內存的地址。因此,無法刪除此動態分配的整數。

This is called a memory leak. Memory leaks happen when your program loses the address of some bit of dynamically allocated memory before giving it back to the operating system. When this happens, your program can’t delete the dynamically allocated memory, because it no longer knows where it is. The operating system also can’t use this memory, because that memory is considered to be still in use by your program.

這稱為內存泄漏。當程序在將動態分配的內存返回給作業系統之前丟失了某些位的地址時,就會發生內存泄漏。當這種情況發生時,您的程序無法刪除動態分配的內存,因為它不再知道它在哪裡。作業系統也無法使用此內存,因為您的程序仍在使用該內存。

Memory leaks eat up free memory while the program is running, making less memory available not only to this program, but to other programs as well. Programs with severe memory leak problems can eat all the available memory, causing the entire machine to run slowly or even crash. Only after your program terminates is the operating system able to clean up and 「reclaim」 all leaked memory.

內存泄漏會在程序運行時耗盡可用內存,不僅會減少此程序的可用內存,還會減少其他程序的可用內存。具有嚴重內存泄漏問題的程序可能會占用所有可用內存,導致整個機器運行緩慢,甚至崩潰。只有在程序終止後,作業系統才能清理和「回收」所有泄漏的內存。

Although memory leaks can result from a pointer going out of scope, there are other ways that memory leaks can result. For example, a memory leak can occur if a pointer holding the address of the dynamically allocated memory is assigned another value:

雖然指針超出範圍可能導致內存泄漏,但也有其他可能導致內存泄漏的方式。例如,如果為保存動態分配內存地址的指針分配了另一個值,則可能會發生內存泄漏:

int value = 5;
int* ptr{ new int{} }; // allocate memory
ptr = &value; // old address lost, memory leak results

This can be fixed by deleting the pointer before reassigning it:

這可以通過在重新分配指針之前刪除指針來解決:

int value{ 5 };
int* ptr{ new int{} }; // allocate memory
delete ptr; // return memory back to operating system
ptr = &value; // reassign pointer to address of value

Relatedly, it is also possible to get a memory leak via double-allocation:

與此相關,也可能通過雙重分配導致內存泄漏:

int* ptr{ new int{} };
ptr = new int{}; // old address lost, memory leak results

The address returned from the second allocation overwrites the address of the first allocation. Consequently, the first allocation becomes a memory leak!

第二次分配返回的地址覆蓋第一次分配的地址。因此,第一次分配成為內存泄漏!

Similarly, this can be avoided by ensuring you delete the pointer before reassigning.

類似地,可以通過確保在重新分配之前刪除指針來避免這種情況。

void doSomething(bool earlyExit)
{
    int* array{ new int[5] { 9, 7, 5, 3, 1 } }; // allocated memory using new

    if (earlyExit)
        return; // exits the function without deallocating the memory allocated above

    // do stuff here

    delete[] array; // never called
}

However, if array is a std::vector, this won’t happen, because the memory will be deallocated as soon as array goes out of scope (regardless of whether the function exits early or not). This makes std::vector much safer to use than doing your own memory allocation.

然而,如果數組是std::vector,則不會發生這種情況,因為一旦數組超出範圍,內存就會被釋放(無論函數是否提前退出)。這使得std::vector比自己分配內存更安全。

ref

https://www.learncpp.com/cpp-tutorial/dynamic-memory-allocation-with-new-and-delete/

-End-

關鍵字: