Dynamic Memory
Introduction to dynamic memory allocations in C++ and usage of its underlying operators
Overview:
Have you ever declared an array and you couldn't decide what the size of the array should be because the size of the array could vary during the runtime of the program and you just declare an array of size 100 and feel like you're squandering all the unused memory space? Follow along this tutorial to solve this problem using dynamic memory allocation.
What is Dynamic Memory Allocation?
A more intuitive solution to the problem above would be to declare variables that would request just the right amount of memory space from the operating system when needed. This is exactly what we are going to learn in this tutorial. The correct term for this process is dynamic memory allocation. There are some underlying ideas and operators we need to learn to properly apply dynamic memory in our C++ programs. Let's start by learning a little bit more about the physical memory spaces.
Heaps and Stacks:
- Stack is an application layer where local variables and function parameters are stored during the runtime of the program. Keep in mind that pointer variables also follow the same rules as local variables and are thus stored in the stack.
- Heap is another application layer where dynamically allocated memories are stored.
- A stack is a linear data structure where the last allocated memory is the first one to be deallocated. The deallocation is automatic which makes using stacks faster than using heaps.
- The heap, on the other hand, is not strictly linear. The memory space in heaps is virtually larger that in stacks which makes it convenient for dynamically allocated memory to use memory spaces as needed. Dynamically allocated memory can be accessed by using pointers, which makes it slower to use than using stacks. The allocation and deallocation of dynamic memory should be done manually using operators we will learn next.
new and new[ ] operators:
Dynamic memory is allocated using the new unary operator. new is followed by a data type specifier and if an array is required, the size of the array is defined within brackets [ ]. One problem with dynamic memory allocation is that we cannot create new variable names on the fly. Consequently, the declaration of dynamic memory allocation returns a pointer with the address of the newly allocated memory.
int * foo = new int [5];
In the above example, foo now has the address of the first element of an integer array with 5 elements. The array in this case is stored in the heap, however, the pointer foo is stored in the stack.
Wait I don't see anything new. How is it different from a regular array?
At first glance yes, all we have from the above code is a pointer pointing to the first element of the array. However, the difference lies between the brackets. Instead of a constant number, we can now use a variable. This means that we don't need to decide how big an array should be before compilation and can now be done during runtime. For example:
int var = 5;
int * foo = new int [var];
Now, we can use variables instead of constants to define the size of an array.
This all seems too good to be true.
Well, your incredulity is not incorrect. Dynamic memory allocation comes with its own set of caveats. The good news is, it also comes with relevant workarounds.
The dynamic memory requested by our program is allocated by the system in the memory heap. However, computer memory is a limited resource, and it can be exhausted. Therefore, there are no guarantees that all memory allocations requested using the new operator are going to be granted by the operating system.
One workaround for this problem is to use the nothrow object declared in the new header as an argument for new:
int * foo = new (nothrow) int [5];
If the allocation of memory fails, the nothrow object assigns a nullptr to foo and we can handle errors accordingly:
if(foo == nullptr){
// handle error
} else {
// continue with foo
}
delete and delete [ ] operators:
Dynamically allocated memories are stored in heaps. Whenever the program execution completes, the pointer pointing to the dynamic memory in the stack is destroyed but the dynamically allocated memory space will still be in the heap. This means that there are memory spaces allocated but no pointers to reference it. This leads to garbage memory because they are just occupying memory and not being used.
Garbage collection can be avoided by deallocating the dynamically allocated memory before the program is terminated. This can be done by using the delete operator.
delete[] foo;
Deallocation doesn’t necessarily have to be done before the termination of a program. It can be done as soon as we are done working with a dynamically allocated memory. This helps free the memory space for other memory requirements in the same program.
Perfect, now that you know the underlying topics of using a dynamic memory, we can now look at a fun example:
Fun example:
#
#
using namespace std;
int main() {
int * ptr;
int size;
cout << "How many numbers would you like to type? ";
cin >> size;
ptr = new (nothrow) int[size];
if(ptr == nullptr) {
cout << "Error: memory could not be allocated" << "\n";
exit(1);
} else {
for(int i = 0; i < size; i++) {
cout << "Enter number: ";
cin >> ptr[i];
}
cout << "You entered the following numbers: " << "\n";
for(int i = 0; i< size; i++) {
cout << ptr[i] << "\n";
}
delete[] ptr;
}
return 0;
}
This is one of the simplest programs you can write using dynamic memory allocation. The idea of it is pretty straightforward, we will ask the user the number of numbers they want to enter and then declare an array with input as its size and then let the user enter that many numbers. To clarify, let's discuss the important lines of the program in more detail:
#
Here, we are including the new header. This is required for us to use the nothrow object in our program.
int * ptr;
Here, we are declaring a pointer variable ptr.
cout << "How many numbers would you like to type? ";
cin >> size;
Here, we are asking the user to input the size of the array and storing it in a variabe size.
ptr = new (nothrow) int[size];
There is a lot going on in this line. First, we are using the new keyword to declare an integer array with the variable size and store the returned address to the pointer variable ptr. Remember that ptr is stored in the stack, whereas, the new dynamically allocated memory exists in the heap. We also have the nothrowobject as an argument to the new keyword. This will assign nullptr to the pointer variable ptr if the memory could not be allocated.
if(ptr == nullptr) {
cout << "Error: memory could not be allocated" << "\n";
exit(1);
}
Here we are checking to see if ptr is nullptr or not. If it is, then we can be certain that the requested memory could not be assigned and can throw an error and handle the program accordingly, which in this case, we are simply exiting the program. If the memory was successfully assigned, we can now go to the next line with the pointer pointing to the new dynamic memory we just created.
else {
for(int i = 0; i < size; i++) {
cout << "Enter number: ";
cin >> ptr[i];
}
}
This block of code asks a number for the same number of times the user requested. Notice that we are storing the input to the ptr because it is the only reference to the dynamically allocated memory for the array.
for(int i = 0; i< size; i++) {
cout << ptr[i] << "\n";
}
This block of code is pretty obvious. It prints each number added by the user to the console, which can be accessed with ptr.
delete[] ptr;
This line of code is crucial because it deallocates the dynamically allocated memory preventing garbage memory collection.
The output of the code will look like:
How many numbers would you like to type? 3
Enter number: 100
Enter number: 200
Enter number: 300
You entered the following numbers:
1
2
3
In a nutshell:
When memory required by a program varies on each runtime, the most intuitive solution would be to use dynamic memory allocation. In C++, dynamically allocated memory is stored in memory heaps, which we can create using the new operator. The new operator returns an address to the allocated memory which we can access using pointer variables. Any dynamically allocated memory must be deallocated manually to prevent garbage memory collection.