Dynamic Persistent Memory Allocation in C++

The new operator can be used to create persistent memory at runtime for primitive type values, arrays, and objects.

Listing 11.6 writes a function that passes an array argument, reverses it, and returns the array. Suppose you don’t want to change the original array. You can rewrite the function that passes an array argument and returns a new array that is the reversal of the array argument.

An algorithm for the function can be described as follows:

  1. Let the original array be list.
  2. Declare a new array named result that has the same size as the original array.
  3. Write a loop to copy the first element, second, . . . , and so on in the original array into the last element, second last, . . . , in the new array, as shown in the following diagram:

  1. Return result as a pointer.

The function prototype can be specified like this:

int* reverse(const int* list, int size)

The return value type is an int pointer. How do you declare a new array in Step 2? You may attempt to declare it as

int result[size];

But C++ does not allow the size to be a variable. To avoid this limitation, let us assume that the array size is 6. So, you can declare it as

int result[6];

You can now implement the code in Listing 11.8, but you will soon find out that it is not working correctly.

Listing 11.8 WrongReverse.cpp

1 #include <iostream>
2
using namespace std;
3
4
int* reverse(const int* list, int size)
5 {

6     int result[6];
7
8     
for (int i = 0, j = size – 1; i < size; i++, j–)
9     {
10       result[j] = list[i];
11    }
12
13   
return result;
14 }
15
16
void printArray(const int* list, int size)
17 {
18   
for (int i = 0; i < size; i++)
19    cout << list[i] <<
” “;
20 }
21
22
int main()
23 {
24   
int list[] = {1, 2, 3, 4, 5, 6};
25   
int* p = reverse(list, 6);
26    printArray(p,
6);
27
28   
return 0;
29 }

The sample output is incorrect. Why? The reason is that the array result is stored in the activation record in the call stack. The memory in the call stack does not persist; when the function returns, the activation record used by the function in the call stack are thrown away from the call stack. Attempting to access the array via the pointer will result in erroneous and unpredictable values. To fix this problem, you have to allocate persistent storage for the result array so that it can be accessed after the function returns. We discuss the fix next.          

C++ supports dynamic memory allocation, which enables you to allocate persistent storage dynamically. The memory is created using the new operator. For example,

int* p = new int(4);

Here, new int tells the computer to allocate memory space for an int variable initialized to 4 at runtime, and the address of the variable is assigned to the pointer p. So you can access the memory through the pointer.

You can create an array dynamically. For example,

cout << “Enter the size of the array: “;

int size;

cin >> size;

int* list = new int[size];

Here, new int[size] tells the computer to allocate memory space for an int array with the specified number of elements, and the address of the array is assigned to list. The array created using the new operator is also known as a dynamic array. Note that when you create a regular array, its size must be known at compile time. It cannot be a variable. It must be a constant. For example,

int numbers[40]; // 40 is a constant value

When you create a dynamic array, its size is determined at runtime. It can be an integer variable. For example,

int* list = new int[size]; // size is a variable

The memory allocated using the new operator is persistent and exists until it is explicitly deleted or the program exits. Now you can fix the problem in the preceding example by creat­ing a new array dynamically in the reverse function. This array can be accessed after the function returns. Listing 11.9 gives the new program.

Listing 11.9 CorrectReverse.cpp

1 #include <iostream>
2 using namespace std;
3
4 int* reverse(const int* list, int size)
5 {
6     int* result = new int[size];
7
8     for (int i = 0, j = size – 1; i < size; i++, j–)
9     {
10        result[j] = list[i];
11    }
12
13    return result;
14 }
15
16 void printArray(const int* list, int size)
17 {
18     for (int i = 0; i < size; i++)
19     cout << list[i] << ” “;
20 }
21
22 int main()
23 {
24    int list[] = {1, 2, 3, 4, 5, 6};
25    int* p = reverse(list, 6);
26    printArray(p, 6);
27
28    return 0;
29 }

Listing 11.9 is almost identical to Listing 11.6 except that the new result array is created using the new operator dynamically. The size can be a variable when creating an array using the new operator.

C++ allocates local variables in the stack, but the memory allocated by the new opera­tor is in an area of memory called the freestore or heap. The heap memory remains avail-able until you explicitly free it or the program terminates. If you allocate heap memory heap for a variable while in a function, the memory is still available after the function returns.

The resul t array is created in the function (line 6). After the function returns in line 25, the resul t array is intact. So, you can access it in line 26 to print all the elements in the resul t array.

To explicitly free the memory created by the new operator, use the delete operator for the pointer. For example,

delete p;

The word delete is a keyword in C++. If the memory is allocated for an array, the [] symbol must be placed between the delete keyword and the pointer to the array to release the memory properly. For example,

delete [] list;

After the memory pointed by a pointer is freed, the value of the pointer becomes undefined. Moreover, if some other pointer points to the same memory that was freed, this other pointer is also undefined. These undefined pointers are called dangling pointers. Don’t apply the dereference operator * on dangling pointer. Doing so would cause serious errors.

You might inadvertently reassign a pointer before deleting the memory to which it points. Consider the following code:

1  int* p = new int;

2  *p = 45;

3  p = new int;

Line 1 declares a pointer assigned with a memory address for an int value, as shown in Figure 11.4a. Line 2 assigns 45 to the variable pointed by p, as shown in Figure 11.4b. Line 3 assigns a new memory address to p, as shown in Figure 11.4c. The original memory space that holds value 45 is not accessible, because it is not pointed to by any pointer. This memory cannot be accessed and cannot be deleted. This is a memory leak.

Dynamic memory allocation is a powerful feature, but you must use it carefully to avoid memory leaks and other errors. As a good programming practice, every call to new should be matched by a call to delete.

Source: Liang Y. Daniel (2013), Introduction to programming with C++, Pearson; 3rd edition.

Leave a Reply

Your email address will not be published. Required fields are marked *