Modern C++ memory allocation

CMP
8 min readAug 1

--

Memory allocation is an important concept for systems programmers to understand, especially when working in environments where resources are limited. This article will go over the differences between the different types of memory allocation and the best practices in modern C++.

Static vs. Dynamic vs. Automatic Memory Allocation

When learning about memory allocation, you may have heard of terms like “the stack” and “the heap”, as well as “static memory” and “dynamic memory”, but what do these terms actually mean?

Memory is split into a few major sections:

  • Text Segment: A read-only section of memory containing executable instructions.
  • Data Segment: A section of memory used to store global and static variables, managed by the compiler. Memory allocated here is known as static memory and the lifetime of these variables are tied to the lifetime of the program.
  • The Stack: A region of memory organized in a last-in-first-out (LIFO) fashion, managed by the operating system and the compiler. The stack pointer (SP) keeps track of the top of The Stack. As functions are called, stack frames are pushed onto The Stack, storing data such as the return address, local variables, and more.

Memory allocated on The Stack is known as automatic memory or “stack memory” and includes local variables for a function. To allocate memory for local variables, the SP simply moves a fixed amount, making stack memory allocation just as fast as pointer arithmetic. To de-allocate the local variables, the SP moves the other direction. This happens automatically with no interference needed by the programmer.

Although The Stack is fast and efficient, the space is fairly limited. Deep recursion or too many nested function calls can fill up The Stack, leading to the famous “stack overflow” error. Because of its limited size, The Stack is not ideal for allocating large chunks of memory or any data structures with dynamic size.

  • The Heap: A region of memory that is not organized in a special way, managed by the OS or the C/C++ runtime library. When memory is requested, a memory allocator finds a suitable block of memory from The Heap and returns a pointer to it, then later marks that block as available once the memory is freed.

Memory allocated on The Heap is typically referred to as dynamic memory and includes data structures like linked lists and trees whose sizes are determined at runtime. This memory must be manually requested by the programmer through functions such as malloc in C or new in C++, then later de-allocated manually using free or delete. This allows for a lot of flexibility compared to The Stack, but is more complicated and can easily lead to issues if not managed correctly.

If memory is allocated but never freed, this is known as a memory leak. Memory leaks waste resources and should be avoided by always freeing memory that has been allocated. Depending on the structure of a program, this can be easier said than done — in fact, memory sometimes gets accidentally freed twice, known as a double free. This results in a memory access violation since the memory was already freed the first time. Another common issue is the dangling pointer, which occurs when a pointer variable is not deleted (set to nullptr) after freeing some previously allocated memory, leading the pointer to point to invalid memory.

C-Style/Old C++ Memory Allocation

Static memory allocation is simple — Simply declare a variable as static inside or outside a function scope, or declare a global variable outside of any functions. This can get a bit complicated with the usage of the extern keyword, but that won’t be discussed in this article.

Automatic memory allocation is also very simple — Simply create local variables when needed and let the SP do the work.

Dynamic memory allocation is much more complicated. As mentioned previously, programmers used to have to use malloc and free to allocate heap memory when coding in C. This would look something like this:

// Allocate a dynamic array of size 10
int n = 10;
int* dynamicArray = (int*)malloc(n * sizeof(int));

// Exit if memory allocation failed
if (dynamicArray == NULL) {
printf("Memory allocation failed...");
return 1;
}
// Fill the array with values 0-9
for (int i = 0; i < n; i++) {
dynamicArray[i] = i;
}
// Print the values of the array
printf("Dynamic array: ");
for (int i = 0; i < n; i++) {
printf("%d ", dynamicArray[i]);
}
printf("\n");
// Free the allocated memory
free(dynamicArray);
return 0;

And now, what if you wanted to make the array even larger? You would have to use realloc:

// Resize the array
int newSize = 20;
int* resizedArray = (int*)realloc(dynamicArray, newSize * sizeof(int));

// Memory allocation failed
if (resizedArray == NULL) {
printf("Memory allocation failed...");
free(dynamicArray); // If you forget this step, you'll have a memory leak!
return 1;
}
// Reassign the pointer
dynamicArray = resizedArray;
// Add new values
for (int i = n; i < newSize; i++) {
dynamicArray[i] = i;
}
// Print the values of the array
printf("Dynamic array (resized): ");
for (int i = 0; i < newSize; i++) {
printf("%d ", dynamicArray[i]);
}
printf("\n");
// Free the allocated memory
free(dynamicArray);
return 0;

This is not fun. Luckily, this process was slightly improved in C++ with the usage of new and delete. A similar example is shown below in (old) C++:

// Allocate a dynamic array of size 10
int n = 10;
int* dynamicArray = new int[n];

// Fill with values 0-9
for (int i = 0; i < n; i++) {
dynamicArray[i] = i;
}
// Print out the values
std::cout << "Dynamic array: ";
for (int i = 0; i < n; i++) {
std::cout << dynamicArray[i] << " ";
}
std::cout << std::endl;
// Free the allocated memory
delete[] dynamicArray;
return 0;

This is a bit cleaner, but you still have to manually free everything that you allocate. What if there was an easier way to perform dynamic memory allocation?

Modern C++ Memory Allocation — Containers

For simple data structures like dynamic arrays, the C++ standard library provides several container types that are very useful and easy to use. These containers automatically handle memory allocation/de-allocation so that the programmer does not have to manually free any memory. Here is the above example but using std::vector as a dynamic array:

// Allocate a dynamic array of size 10
int n = 10;
std::vector<int> dynamicArray{};

// Fill with values 0-9
for (int i = 0; i < n; i++) {
dynamicArray.push_back(i);
}
// Print out the values
std::cout << "Dynamic array: ";
for (int i = 0; i < n; i++) {
std::cout << dynamicArray[i] << " ";
}
std::cout << std::endl;
// No need to manually de-allocate!
return 0;

Modern C++ Memory Allocation — Objects

Standard library containers are fantastic, but what if you need to allocate memory for a custom data structure class? Let’s first look at allocating an object on The Stack:

class MyClass {
public:
// Constructor
MyClass() = default;

// Destructor
~MyClass() = default;
};

int main() {
// Automatically allocate an instance of MyClass
MyClass obj;

// When the local variable goes out of scope,
// it will automatically be de-allocated
return 0;
}

In this example, the MyClass instance was automatically allocated on The Stack as a local variable using its constructor. When that variable goes out of scope when the function exits, the MyClass destructor will run and the local variable will be automatically de-allocated.

Now, let’s look at dynamic memory allocation using new and delete on the MyClass instance:

class MyClass {
public:
// Constructor
MyClass() = default;

// Destructor
~MyClass() = default;
};
int main() {
// Allocate memory for a MyClass object
MyClass* obj = new MyClass();
// De-allocate the MyClass object
delete obj;
}

In this example, an instance of MyClass is allocated on The Heap using new, then freed by using delete. When the caller function calls delete on the MyClass object, the MyClass destructor is invoked.

The question still remains: Is there a way to allocate this object without having to manually free it with delete? What if we could combine dynamic memory allocation with the properties of automatically allocated local variables?

Modern C++ Memory Allocation — Smart Pointers

We know that a stack-allocated object’s destructor will be automatically called when it goes out of scope. We also know that we must call delete on a dynamically allocated object. Can we somehow create a special class whose objects are automatically allocated and that wraps dynamically-allocated objects? Look at the below example:

class MyClassWrapper {
public:
// Constructor
explicit MyClassWrapper(MyClass* myClass) :
m_myClass(myClass) {}

// Destructor
~MyClassWrapper() {
delete m_myClass;
}
private:
MyClass* m_myClass;
};
int main() {
// Automatically allocate MyClassWrapper with a dynamically-allocated MyClass instance
MyClassWrapper myClassWrapper = MyClassWrapper(new MyClass());
// MyClassWrapper is automatically de-allocated at scope exit!
// MyClass is freed within the MyClassWrapper destructor!
return 0;
}

In this example, MyClassWrapper is allocated on The Stack as a local variable. This means that it’s destructor will be called on scope exit. In its constructor, we pass in a dynamically allocated MyClass instance, which gets freed within the destructor for MyClassWrapper!

This is the foundation for C++ smart pointers. Smart pointers were introduced to the standard library in C++11 and improved in C++14. std::unique_ptr is used for exclusive ownership of a resource, std::shared_ptr is used for shared ownership of a resource, and std::weak_ptr is a non-owning observer to resources owned by std::shared_ptrs. Unlike the MyClassWrapper class, C++ smart pointers use templates. Here is a modified version of the above example using smart pointers:

int main() {
// Automatically allocate a std::unique_ptr to hold ownership of a new MyClass instance
std::unique_ptr<MyClass> myClassPtr = std::unique_ptr<MyClass>(new MyClass());

// The dynamically allocated MyClass object is deleted on scope exit along with myClassPtr
return 0;
}

The memory allocation can be further abstracted using make_unique. The auto keyword can make this more readable too:

int main() {
auto myClassPtr = std::make_unique<MyClass>();

return 0;
}

This concept of an object owning a resource as soon as it’s initialized and freeing it when the object is destroyed is called Resource Acquisition is Initialization (RAII). This is an extremely useful and powerful programming paradigm that should be followed as much as possible.

An in-depth comparison of the different smart pointer types, as well as additional uses, are topics for another discussion.

Conclusion

Memory allocation is a complicated yet important topic for systems programmers to understand. If you specifically need global or static variables, then using the correct keyword to allocate memory in the Data Segment is required. In most cases, automatically allocating memory on The Stack by declaring local variables is best, as it’s generally recommended to declare variables in the smallest scope possible. For dynamic data structures, dynamic memory allocation on The Heap is necessary. To prevent memory leaks and other issues, using RAII-mechanisms like smart pointers are advised rather than older mechanisms like new / delete and malloc / free.

--

--