Memory Management



Visualizing memory

Below is an example of how the memory looks for a stack/automatic variable:
int foo()
{
    double d = 3.0;
    return 0;
}


Note that d is deallocated when foo's scope ends. When memory is allocated on the heap, the picture looks like this. The pointer lives on the stack and the allocated memory is on the heap.
int foo()
{
    double * dptr = new double;
}


The next example shows a pointer that lives on the heap:
float **ptrToPtr = new float*;
*ptrToPtr = new float;

Using new and delete

Do not use malloc and free from C as they have no concept of object initialization and destruction. Use new and delete instead. Use new to allocate memory and delete to deallocate memory. When using raw pointers every call to new must be paired with a call to delete. The call to new can fail if the system is out of memory, so there has to be some way to handle this situation.

Arrays

One can think of arrays as contiguous memory locations holding the same type of data, similar to how mailboxes are arrayed. When creating arrays on the stack, the size must be known at compile time. Example of stack based arrays:
int ary[5] = {0};


Using arrays on the heap is similar except the size can be determined at runtime. Example:
#include <iostream>
using namespace std;
int main()
{
    int numElems = 0;
    cout << "Enter the number of elements for the array" <<endl;
    cin >> numElems;

    int* aryPtr = new int[numElems];
}
If the user enters in 3, the visualization is as follows:


Each call to the array version of new (new[]) should be paired with the array version of delete (delete[]) as follows:
delete[] aryPtr;
aryPtr = nullptr;

Arrays of objects

Arrays of objects are similar to arrays of primitive types. Example:
class Basic
{
    public: 
        Basic() {cout << "Basic constructor called" << endl;}
        ~Basic() {cout << "Basic destructor called" << endl;}
};
If you allocate an array of 3 Basic objects, the Basic constructor is called 3 times:
Basic* basicAryPtr = new Basic[3]; 
/*Use basicAryPtr here */

delete[] basicAryPtr;
basicAryPtr = nullptr;
Use the array version of delete as usual to deallocate the memory and have the destructor called the same number of times.
If you store pointers to objects on the heap one must call new and delete in a loop as shown below:
int numElems = 3;
Basic **ptrToBasicPtrs = new Basic* [numElems];

for(int k = 0; k < numElems; k++)
{
    ptrToBasicPtrs[k] = new Basic();
}

//Use the objects

for(int k = 0; k < numElems; k++)
{
    delete ptrToBasicPtrs[k];
}

//delete the stack pointer
delete[] ptrToBasicPtrs;
ptrToBasicPtrs = nullptr;
It is a better practice to use STL containers to store smart pointers instead of using arrays to store raw pointers.

Multi-dimensional arrays

Multi dimensional arrays can be thought of as a table with rows and columns. Example:
double matrix[3][3];


However, in memory, the multi dimensional array is stored linearly in row-major order:

Multi-dimensional heap arrays

Multi dimensional arrays are more tricky in that there is more manual memory allocation involved. Here is a picture of how a multidimensional array looks on the heap:



To allocate memory for a 2 dimensional array similar to the one in the picture above, the following must be performed
double** allocateMatrix(int numRows, int numCols)
{
    double** myMat = new double*[numRows];
    for(int i = 0; i < numRows; i++)
    {
        myMat[i] = new double[numCols];
    }
    return myMat;
}
To deallocate memory, the following must be performed:
void deallocateMatrix(double** myMat, int numRows)
{
    for(int i = 0; i < numRows ; i++)
    {
        delete[] myMat[i]; //Delete columns
    }
    delete[] myMat; //delete rows
}
In modern C++ code, prefer vector<T> instead of plain arrays. For two dimensional arrays, use vector<vector<double>> for example.

Pointer casting

Pointers are always the same size, whether they are pointers to ints or doubles for example. Casts can be performed on pointers through the following:
The first is a C-style cast (deprecated, and provides no type safety):
XMLDocument* docPtr = getXMLDocument();
char* charPtr = (char*)docPtr;
With static_cast, the compiler enforces the rule that the types should be related:
XMLDocument *docPtr = getXMLDocument();
char *charPtr = static_cast<char*>(docPtr);

Array pointer relationship

The array name is a pointer to the first element of the array. Array elements can alternatively be accessed as follows:
int myAry[3] = {0};
*(myArray) = 1;
*(myArray+1) = 2;
*(myArray+2) = 3; 
is the same as follows:
myArray[0] = 1;
myArray[1] = 2;
myArray[2] = 3;
When arrays are passed into functions, the compiler treats the array as a pointer. So changes made to the array change the original array instead of a copy.

Smart Pointers

The simplest smart pointer, std::unique_ptr takes single ownership of the resource and deallocates the underlying memory when it goes out of scope or is reset. When ownership is shared, use std::shared_ptr which uses reference counting to keep track of all references to the object and only deallocates memory when the last reference is removed.

Rationale for smart pointers

Even the simplest pairing of new and delete can cause problems. For example, consider the following:
Basic* basicPtr = new Basic();
basicPtr->doWork();
delete basicPtr;
If doWork() throws an exception, the delete statement is not executed, causing a memory leak. A std::unique_ptr should be used in this case. When the pointer goes out of scope or if an exception is thrown, the underlying memory is deallocated. Use make_unique to define a unique_ptr:
auto myPtr = make_unique<Basic>();
auto myPtr2 = make_unique<Basic>(p1,p2); //constructor parameter variant.
Because the uniwue_ptr represents single ownership it cannot be copied. However, the responsibility can be moved as shown in this article link, using std::move.
To represent shared ownership, use std::shared_ptr.
auto owner1 = make_shared<Basic>();
std::shared_ptr<Basic> owner2(owner1);

References:

Gregoire, M. (2018). Professional C++. Indiana, IN: John Wiley & Sons.

Comments

Popular posts from this blog

QTreeView and QTableView dynamic changes

C++ strings and string_view