Dynamic memory allocation is a powerful concept in programming that allows us to allocate memory at runtime. Unlike static memory allocation, where memory is allocated at compile-time, dynamic memory allocation offers flexibility in managing memory resources based on a program’s specific needs. In this lesson, we’ll explore the syntax of declaring dynamic memory for a single variable, discuss the role of pointers in pointing to memory on the heap, compare static and dynamic memory management, and explore the benefits of dynamic memory allocation for arrays.

Dynamic memory allocation for a single variable

To dynamically allocate memory for a single variable, the new operator is used in combination with the desired data type.

Syntax of dynamic memory allocation

The syntax is as follows:

new dataType{initialValue};

This expression will create a memory block of a specific size (determined by dataType) that is allocated on the heap and initialized with the provided initial value (initialValue). Upon successful execution, the new operator returns the address of the heap’s allocated memory.

But how can we access the allocated heap memory through the returned address from the new instruction?

We store the memory address in a pointer, allowing us to manipulate and retrieve data from the allocated memory block. To store the address, we write the following line of code:

dataType* pointerName = new dataType(initialValue);

In this line of code, pointerName is a pointer variable of the type dataType, which will store the address of the dynamically allocated memory.

Deallocating dynamic memory

In contrast to memory managed by the compiler and runtime environment for static memories like simple variables and static arrays, dynamically allocated memory persists in memory space until explicitly deleted by the programmer or until the operating system terminates the program.

To ensure efficient memory management and prevent memory wastage, it is essential to explicitly deallocate dynamically allocated memory using the delete operator. By doing so, we release the memory block on the heap, allowing it to be reused for other purposes. Here’s the syntax to deallocate the memory:

delete ptrName;

By using this designated syntax, we instruct the program to release the dynamically allocated memory and mark it as available for future allocations. Proper deallocation of dynamically allocated memory is crucial to avoid memory leaks and optimize the utilization of system resources.

Remember: It's important to note that the responsibility of deallocating dynamically allocated memory lies with the programmer. Failure to properly deallocate memory can result in memory leaks, where memory is occupied but no longer accessible, leading to inefficient memory usage and potential performance issues.

Please observe the following key points:

  • When deallocating dynamic memory using the delete operator, it’s important to note that the delete instruction does not destroy the pointer variable itself (ptrName), which resides on the stack. Instead, it frees the memory block located on the heap that the pointer points to.

  • Attempting to use the delete operator with a pointer that points to memory residing in the stack or anywhere other than the heap can lead to logical errors and potentially crash the program. This is because the delete operator is specifically designed to deallocate memory on the heap, not memory allocated on the stack or elsewhere.

  • To clarify, when we use the delete operator with a pointer that correctly points to dynamically allocated memory on the heap, it releases the memory block and marks it as available for future allocations. However, the pointer variable itself remains in the memory (which could be both on the stack as a local variable or on the heap), and it may still contain the address of the deallocated memory. For programmers, it is important to handle such scenarios by either setting the pointer to nullptr or reassigning it to a valid memory address.

Look at the following animation to understand how memory is allocated dynamically and freed from the heap.

Press + to interact
canvasAnimation-image
1 of 9

Example: Dynamically allocated memory variable

For a better understanding, let’s look at the following code, which demonstrates the use of dynamic memory allocation and pointers in C++.

Press + to interact
#include <iostream>
using namespace std;
int main()
{
int * alpha = new int {23} ;
cout<< "`&alpha: `"<<&alpha<< " pointing towards : " <<alpha << " has value: "<< *alpha << endl;
delete alpha;
cout << "\t <<after deletion>> \n";
cout<< "`&alpha: `"<<&alpha<< " pointing towards : " <<alpha ;
// The following line is an illegal memory access now
cout << " has value: "<< *alpha << endl; // Logical Error
alpha = nullptr;
cout << "\t <<after updating the `alpha`>> \n";
cout<< "`&alpha: `"<<&alpha<< " pointing towards : " <<alpha ;
// This following line will have an error
// Will cause segmentation fault
cout << " has value: "<< *alpha << endl; // Logical Error
return 0;
}
  • Line 5–11: We declare a pointer variable alpha of type int* and dynamically allocate memory for an integer with the value 23 using the new keyword. The code then prints the memory address of alpha, the value it points to, and *alpha itself. After that, the memory allocated for alpha is freed using the delete keyword.

  • Line 11: There’s a logical error here because the program is now trying to access a memory location that is no longer the property of the program.

  • Line 13–19: It updates the value of alpha to nullptr and prints the updated memory address and value, which is null. Finally, an attempt is made to dereference the null pointer and print its value, resulting in a logical error (dereferencing the nullptr).

Dynamic memory allocation: Power and flexibility

Dynamic memory allocation plays a vital role in overcoming the limitations of static memory allocation at compile-time. Static arrays have a fixed size, making resizing impossible after creation. However, dynamic memory allocation allows us to overcome this limitation by creating arrays with variable sizes at runtime. We can resize dynamic arrays as needed.

To allocate contiguous memory blocks of a specific size on the heap, we can use the new keyword along with the desired data type and size enclosed in square brackets. The syntax is as follows:

dataType* arrayName = new dataType[size]{Initialization list};

Upon successful execution, the new instruction returns the base address of the allocated memory. Storing this memory address in a pointer allows us to manipulate and retrieve data from the dynamically allocated memory blocks. We can utilize pointer arithmetic or the subscript operator ([]) to access individual elements within the allocated memory. For deleting the heap array, the pointer must hold the address of the same memory that was allocated (if the pointer is displaced and asked to release the heap anywhere within the range of the allocated dynamic array, then the compiler may generate an undefined behavior).

The syntax for deleting an array is shown below.

dataType* arrayName = new dataType[size]{Initialization_list};
.
.
.
delete [] arrayName;

The following animation visually illustrates the process of memory allocation (including storing the address in a pointer) and deallocation.

Press + to interact
canvasAnimation-image
1 of 8

Let’s look at an example of creating a dynamically allocated array in such a way that we can regrow its size at runtime.

Example: Regrow the dynamic array

The following code demonstrates a dynamic array implementation that allows for the resizing of the array as elements are inserted. The main purpose of the code is to showcase the process of dynamically allocating memory on the heap, managing array capacity, and resizing the array when needed.

Press + to interact
void regrow(int *& arr, int size, int & capacity)
{
capacity++;
int *ha = new int [capacity]; // ha: Heap Array
for(int i = 0; i < size; i++)
ha[i] = arr[i];
delete [] arr;
arr = ha;
}
void insert_element(int * &arr, int & size, int &capacity)
{
if(capacity <= size)
{
regrow(arr, size, capacity);
}
arr[size++] = rand() % 50;
}
int main()
{
int capacity = 10;
int * arr = new int[capacity] {2, 5, 6, 10, 4, 12, 15, 3, 8};
int size = 9;
print("Intialized array: ", arr, size);
cout << "The size of array after initialization: " << size << '\n';
cout << "The capacity of array after initialization: " << capacity << '\n';
insert_element(arr, size, capacity);
cout << "The size of array after inserting 10th element: " << size << '\n';
cout << "The capacity of array after inserting 10th element: " << capacity << '\n';
print("Array after 10th an element: ", arr, size);
insert_element(arr, size, capacity);
cout << "The size of array after inserting 11th element: " << size << '\n';
cout << "The capacity of array after inserting 11th element: " << capacity << '\n';
print("Array after 11th an element: ", arr, size);
return 0;
}
  • Lines 1–9: The regrow function is responsible for increasing the capacity of the array. It takes in the array by reference (int *& arr), along with the current size (int size) and capacity (int & capacity) of the array. Inside the function, the capacity is incremented by one, and a new memory block of the updated capacity is allocated on the heap using the new operator. The elements from the original array are copied to the new memory block, and then the original memory block is deallocated using delete [] arr. Finally, the pointer arr is updated to point to the new memory block.

Note: If we don't pass arr by reference, the regrow function won't work because arr will be a new local variable and any change in arr won't change anything in the pointer, which was passed as an argument. However, line 7 will delete all the data of the original array (which was passed to this function as an argument through the base address).

  • Lines 11–20: The insert_element() function inserts an element into the array. If size is less than or equal to capacity, it calls regrow() to increase the capacity and then inserts the random number at the next available position into the resized array.

  • Lines 24–37: In the main() function, an array with an initial capacity of 10 is created and initialized with some values. The array’s size and capacity are printed. Elements are then inserted into the array using dynamic resizing of the array. The final size and capacity of the array, along with its contents, are printed.

In the above code example, we have shown the dynamic resizing of an array using the regrow() and insert_element() functions, allowing for flexible memory management and accommodating additional elements beyond the initial capacity.

Example: Manipulating data in a duplicated array

The following code addresses the problem of calculating the median of a set of class marks while ensuring the original data remains intact and read-only. Let’s look at the implementation to understand how we achieve this.

Press + to interact
void sortAsc(int arr[], int size)
{
for(int i = 0; i<size - 1; i++)
{
for(int j = 0; j < size - i - 1; j++)
{
if(arr[j]>arr[j+1])
{
swap(arr[j], arr[j+1]);
}
}
}
}
void fillRandomly(int * arr, int &size, int capacity)
{
for(int i = 0; i<capacity; i++ )
{
arr[i]= rand() % 100;
size++;
}
}
// calculating the median
float median(const int * arr, int size)
{
int * sRecord = new int[size];
for(int i = 0; i< size;i++)
{
sRecord[i]= arr[i];
}
sortAsc(sRecord, size);
float med;
med = (sRecord[size/2] + sRecord[(size-1)/2])/ 2.0;
/*
// This is equivalent to
if(size % 2)
med = sRecord[size/2];
else
med = (sRecord[size/2] + sRecord[size/2 - 1])/ 2.0;
*/
delete [] sRecord;
return med;
}
int main()
{
int capacity;
srand(time(0));
// How many students class
cin>>capacity;
int size = 0;
int * classMarks = new int[capacity];
fillRandomly(classMarks, size, capacity);
print("Content of the array(Before median calculation): ",classMarks, size);
float med = median(classMarks, size);
cout << "The median of given data is : ";
cout << med<<'\n';
print("Content of the array(Before median calculation): ",classMarks, size);
return 0;
}

Enter the input below

Note: Write the number of students in the input section. For example, 10.

  • Lines 1–13: The sortAsc() function implements the bubble sort algorithm to sort the array of class marks in ascending order. It uses nested loops to compare adjacent elements and swap them if they are out of order. This sorting operation modifies the array in place, rearranging the elements in ascending order.

  • Lines 14–21: The fillRandomly() function populates the array of class marks with random values. It takes a pointer to the array, the current size, and the capacity as parameters. The function uses a loop to assign random values (using the rand() function) to each index of the array. The size variable is incremented accordingly to reflect the number of elements in the array.

  • Lines 23–44: The median() function calculates the median of the class marks. It creates a new dynamic array called sRecord and copies the original data into it. The sortAsc() function is then called to sort the copied array in ascending order. After sorting, the median is calculated based on the size of the array. If the size is odd, the median is the middle element; if the size is even, the median is the average of the two middle elements. The dynamically allocated memory for sRecord is appropriately deallocated to prevent memory leaks, and the calculated median is returned as a floating-point value.

  • Lines 46–55: In the main() function, the user is prompted to input the number of students in the class (capacity). The fillRandomly() function is then called to populate the classMarks array with random marks. The content of the array before the median calculation is printed. Next, the median() function is invoked to calculate the median of the class marks, which are stored in the med variable. The median is then displayed on the screen. Finally, the content of the array is printed again, demonstrating that the original data has remained unmodified.

Benefits of dynamic memory allocation for arrays

Dynamic memory allocation for arrays offers several advantages over static arrays, making it a popular choice in many programming scenarios.

  1. Flexibility: With dynamic memory allocation, arrays can be created with sizes determined at runtime, allowing programs to adapt to varying data requirements. This flexibility enables efficient memory utilization and enhances the scalability of programs.

  2. Memory efficiency: Dynamic arrays only consume memory resources proportional to their actual size, avoiding memory wastage often encountered with fixed-size static arrays. This efficiency becomes particularly crucial when dealing with large or dynamically changing data sets.

  3. Dynamic resize: Dynamic arrays can be resized during program execution. If the size of a dynamic array needs to change, a new memory block can be allocated with the desired size, and the data from the old array can be copied to the new location. This resizing capability allows programs to adapt to dynamic data structures or changing input sizes without the need for fixed, preallocated memory.

  4. Avoiding memory wastage: Static arrays require a fixed amount of memory to be allocated, regardless of the actual data size needed at runtime. This can result in memory wastage if the allocated space is larger than necessary. Dynamic arrays, on the other hand, can be allocated precisely to fit the required data size, eliminating any wastage of memory.

  5. Runtime flexibility: Dynamic arrays offer runtime flexibility in terms of their lifetime. While static arrays are limited to the scope in which they are defined, dynamic arrays can persist beyond the scope of a function or program. This allows data to be shared between different parts of the program or accessed from multiple functions, enhancing the program’s flexibility and enabling more complex data structures and algorithms.

  6. Memory management control: Dynamic memory allocation provides explicit control over memory management. Since the programmer is responsible for deallocating the dynamically allocated memory using the delete operator, it allows for fine-grained memory control and can help prevent memory leaks or unnecessary memory consumption.

To summarize, dynamic memory allocation is a powerful feature in programming that allows for flexible and efficient memory management. By dynamically allocating memory at runtime, programmers can create variables and arrays of varying sizes, adapt to changing data requirements, and avoid memory wastage. Dynamic memory allocation, coupled with pointers, provides a mechanism to access and manipulate dynamically allocated memory. By understanding the syntax and benefits of dynamic memory allocation, programmers can leverage this feature to create more flexible, scalable, and memory-efficient programs. However, exercising caution and ensuring proper memory deallocation is important to prevent memory leaks and ensure optimal memory usage.

In the upcoming lesson, we’ll explore the complexities of potential issues that arise when handling dynamic memory and the essential precautions needed to avoid unwanted consequences.