Before discussing dynamic memory allocation, let’s first look at the two basic types of memory allocation in C++:
- Static memory allocation is used for static and global variables. The memory for these variables is allocated all at once when the program runs and lasts for the entire lifecycle of the program.
- Automatic memory allocation is used for function parameters and local variables. The memory for these variables is allocated when entering the relevant scope and released upon exiting the scope, and can be done multiple times as needed.
- The third point, which is the topic of this article, is dynamic memory allocation.
The first two allocation methods have two common points:
- The size of the variable/array must be known at compile time.
- Memory allocation and deallocation are done automatically.
In most cases, this logic may not pose significant issues. However, there are some exceptions, for example, suppose we need to store a person’s name in a string, but we do not know how long it will be until the input is finished. Similarly, when reading data records of unknown size from disk, the size is also unknown until the reading is complete.
In game development, for instance, the number of monsters in a game may change over time, and such data is also dynamically changing, making it difficult to meet the condition of being known at compile time.
You might think of using pre-allocated fixed-size capacity to solve these issues, which is not unfeasible, but it carries the risk of space waste and memory overflow.
For example, if we allocate space for 25 characters for each name, but the average name length is only 12 characters, we are using more than twice the space actually needed.
Most conventional variables (including fixed arrays) are allocated in a memory section called the stack. The stack memory available to the program is usually quite small; for example, the default stack size in <span>Visual Studio</span> as a tool for game development is 1MB. If you exceed this number, a stack overflow will occur, and the operating system may terminate the program.
For many programs, keeping memory within the
<span>1MB</span>range is a consideration, especially in the field of graphics development.
Dynamic Memory Allocation for Single Variables
To dynamically allocate a single variable, use the <span>new</span> operator:
new int;
The new operator creates an object in that memory and returns a pointer containing the allocated memory address. Typically, we assign the return value to our pointer variable for later access to the allocated memory.
int* ptr { new int };
Then use dereferencing to access the memory and assign a value.
*ptr = 9;
Note that since heap-allocated objects require first obtaining the object’s address from the pointer and then accessing the object’s value through that address, this is somewhat slower than accessing stack-allocated objects, where the compiler knows the address of the stack-allocated object and can directly access that address to get the value.
Basic Working Principle of Dynamic Memory Allocation
A computer has a certain amount of memory available for applications to use. When you run an application, the operating system loads that application into a portion of memory.
This portion of memory used by the application is divided into different areas, each serving different tasks, one of which stores your program code. Another area ensures the program operates correctly, such as recording function calls, creating and destroying global and local variables, etc.
Nevertheless, there is generally a certain amount of memory that remains idle, waiting for requests from programs. When you perform dynamic memory allocation, you are requesting the operating system for this portion of idle memory, hoping to request a part of it for your program’s use. If the system memory meets the requirements, it will return a memory address to your program based on your request.
From that moment on, your program owns and can freely use this portion of memory, and when the program no longer needs it, it can return this memory to the operating system to be on standby for other programs’ requests.
Unlike static or automatic memory, dynamic memory allocation is managed by the program itself, meaning that the management of dynamic memory is no longer controlled by the operating system but is handed over to the program.
Core Summary
Allocation and deallocation of stack objects are done automatically.
We do not need to manually handle memory addresses — the code generated by the compiler will automatically take care of this.
Allocation and deallocation of heap objects are not done automatically.
We must manually participate. This means we need a clear method to reference a specific heap object so that we can manually destroy it at the appropriate time.
This way of referencing heap objects is through memory addresses.
When we use the new operator, it returns a pointer pointing to the memory address of the newly allocated object.
We typically store this return value in a pointer variable so that we can access the object through this address later and destroy it when needed.
When you dynamically allocate a variable, you can initialize this variable either directly or uniformly:
int* ptr1 { new int(6) };
int* ptr2 { new int { 6 } };
When we finish using the dynamically allocated variable, we need to explicitly tell C++ to release this portion of memory for reuse. For a single variable, this can be done using the non-array form of the <span>delete</span> keyword.
delete ptr;
ptr = nullptr;
Note:
<span>delete</span>operator does not actually delete anything; it merely returns the memory to the operating system. The operating system can then freely reallocate this memory to another application.- Although it looks like we are deleting a variable syntactically, that is not the case! The pointer variable still has its previous scope and can be reassigned like any other variable (for example, assigned to nullptr).
- delete can only be used to free memory dynamically allocated through new (or new[]). If you use delete on a pointer to a regular variable or a pointer that has already been freed, it may lead to program crashes, undefined behavior, or memory corruption.
C++ does not guarantee what happens to the contents of freed memory, nor does it guarantee what the value of a deleted pointer will become.
In most cases, the memory returned to the operating system still retains its original data, and that pointer still points to the already freed memory address.
A pointer that points to freed memory is called a dangling pointer. Dereferencing or deleting a dangling pointer will lead to undefined behavior.
#include <iostream>
int main()
{
int* ptr{ new int }; // Dynamically allocate memory for an integer
*ptr = 7; // Write a value into this memory
delete ptr; // Return this memory to the operating system. At this point, ptr becomes a dangling pointer.
std::cout << *ptr; // Dereferencing a dangling pointer will lead to undefined behavior.
delete ptr; // Attempting to free this already freed memory will also lead to undefined behavior.
return 0;
}
In the above program, the value 7 previously allocated to memory may still exist, but it is also possible that the value at that memory address has changed.
Additionally, it is possible that this memory has already been allocated to other applications (or used by the operating system itself), and if you try to access this memory, the operating system may terminate the program as a result.
If multiple pointers point to the same freed memory (i.e., multiple dangling pointers), any erroneous access by any pointer will lead to undefined behavior.
#include <iostream>
int main()
{
int* ptr{ new int{} }; // Dynamically allocate memory for an integer
int* otherPtr{ ptr }; // otherPtr now points to the same memory address as ptr
delete ptr; // Return this memory to the operating system. At this point, both ptr and otherPtr are dangling pointers.
ptr = nullptr; // ptr is now a nullptr
// However, otherPtr is still a dangling pointer!
return 0;
}
To address the above issues, here are some avoidance suggestions:
- Avoid having multiple pointers point to the same dynamically allocated memory. If unavoidable, clearly identify which pointer owns this memory (for easier subsequent release) and which pointers can only access this memory.
- When deleting a pointer, if that pointer does not immediately go out of scope after deletion, it should be set to
<span>nullptr</span>.
Cases of new Operator Allocation Failure
- When requesting memory from the operating system, in rare cases, the operating system may not have enough memory to satisfy the request.
- By default, if new fails to allocate memory, it throws a bad_alloc exception. If this exception is not handled correctly, the program will crash due to the unhandled exception.
- In many cases, throwing an exception (or crashing the program) is not the desired outcome, so there is an alternative form of new that can be used to tell new to return a null pointer when it cannot allocate memory.
In this case, you can achieve this by adding the constant std::nothrow between the new keyword and the allocation type.
int* val { new(std::nothrow) int };
In the above example, if new cannot allocate memory, it will return a null pointer instead of the address of allocated memory.
Note that if you subsequently try to dereference this pointer, it will lead to undefined behavior (most likely causing the program to crash). Therefore, best practice is to check for null pointers before using allocated memory to ensure they were actually successful.
int* val { new (std::nothrow) int{} }; // Request to allocate memory of size for an integer
if (!val) // Handle the case where new returns a null pointer
{
// Handle the error here
std::cerr << "Unable to allocate memory\n";
}
Null Pointers and Dynamic Memory Allocation
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 essentially indicates “this pointer has not been allocated memory”.
This allows us to conditionally allocate memory like this:
// If ptr has not yet allocated memory, perform allocation
if (!ptr)
ptr = new int;
- Deleting a null pointer has no effect, so there is no need to check if the pointer is null before performing a delete operation. delete ptr; will safely ignore a null pointer and will only perform memory release when the pointer points to valid memory.
if (ptr) // If ptr is not a null pointer
delete ptr; // Delete it
// Otherwise do nothing
The above writing is redundant and can be written like this:
delete ptr;
Best Practices:
Using delete on a null pointer is allowed, and there is no need for conditional checks on the delete statement.
Memory Leaks
- When you use new to dynamically allocate memory, this memory will remain allocated until you explicitly use delete to free it. Even after the function execution is complete, the memory will not be automatically released until you manually release it or the operating system recovers the memory when the program ends.
- A pointer as a local variable has a limited scope, meaning it will become invalid as the function execution ends. However, the dynamically allocated memory pointed to (if not released) will continue to exist until you explicitly release it. This mismatch between the pointer’s lifecycle and the dynamic memory’s lifecycle can lead to issues such as memory leaks or dangling pointers.
Consider the following function:
void doSomething()
{
int* ptr{ new int{} };
}
This function dynamically allocates memory for an integer but never uses delete to release it. Because the pointer variable is like a regular variable, ptr will go out of scope when the function ends.
And because ptr is the only variable holding the address of the dynamically allocated integer, when ptr is destroyed, the address of the dynamically allocated memory is lost. This means the program now “loses” the address to the dynamically allocated memory.
As a result, this dynamically allocated integer cannot be deleted.
This is called a memory leak.
A memory leak occurs when your program loses the address of a dynamically allocated memory without releasing it. When this happens, the program cannot delete the dynamically allocated memory because it no longer knows where the memory is.
The operating system also cannot use this memory because it is considered still in use by the program.
Memory leaks will consume available memory during program execution, reducing the memory available to this program and other programs. Programs with severe memory leak issues may consume all available memory, causing the entire machine to slow down or even crash.
Only when the program terminates can the operating system clean up and “reclaim” all leaked memory.
Memory leaks will consume available memory during program execution, reducing the memory available to this program and other programs. Programs with severe memory leak issues may consume all available memory, causing the entire machine to slow down or even crash.
Only when the program terminates can the operating system clean up and “reclaim” all leaked memory.
int value = 5;
int* ptr{ new int{} }; // Allocate memory
ptr = &value; // Old address lost, leading to memory leak
This issue can be fixed by deleting the pointer before reassigning it:
int value{ 5 };
int* ptr{ new int{} }; // Allocate memory
delete ptr; // Return memory to the operating system
ptr = &value; // Reassign pointer to the address of value
Reallocating memory by using new again and assigning the address to the same pointer ptr will overwrite the previously stored address, thus losing the reference to the first allocated memory, leading to a memory leak.
int* ptr{ new int{} };
ptr = new int{}; // Old address lost, leading to memory leak
Before reallocating memory, ensure to release the original memory (using delete) to prevent losing the original memory address and causing leaks.
This article is beautified and formatted using Welight’s “Ink and Wash” theme, https://waer.ltd