Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 3

Dynamic Memory Management in C++

Recommended resources

Welcome to the third lesson in our foundational module on C/C++ systems programming. In our last session, we drew a critical distinction between the stack and the heap, establishing the stack as our default choice for its incredible speed and the heap as the necessary alternative for large, dynamic, or long-lived data. You learned that this choice represents a fundamental trade-off between performance and flexibility.

Today, we transition from the "why" to the "how." We will master the mechanics of heap allocation, which is often referred to as dynamic memory management. Your goal is to become proficient in using the standard C and C++ mechanisms for this and, more importantly, to internalize the significant responsibilities and common pitfalls that come with this power. For anyone aiming to write high-performance, stable systems code, a rigorous understanding of manual memory management is not optional—it is an absolute prerequisite.

The stack and heap are two distinct memory regions. While stack memory is managed automatically, the heap requires manual control using functions like `malloc`, `free`, and operators like `new` and `delete`.

1. Dynamic Memory Management: Two Styles

C and C++ provide two parallel sets of tools for interacting with the heap. While modern C++ code heavily favors its own operators, your work in low-level systems and legacy codebases will inevitably expose you to the C-style functions. It is essential to be fluent in both.

C-Style: malloc, calloc, realloc, and free

The C standard library (<stdlib.h> or <cstdlib> in C++) provides a family of functions for byte-level memory management. The primary function is malloc, for "memory allocation."

Let's watch a detailed walkthrough of how malloc and its counterpart, free, are used to manage memory on the heap.

Pointers and dynamic memory - stack vs heap

This video from mycodeschool provides a clear, practical demonstration of C-style dynamic memory allocation.

Please watch the segment from this section. Pay close attention to these key steps: Including the header: The functions are declared in <stdlib.h>. Requesting memory: malloc takes one argument: the number of bytes to allocate. The sizeof operator is crucial here to ensure portability. The void* return: malloc returns a generic void* pointer, which must be cast to the appropriate pointer type (e.g., int*, double*). Deallocation: Every call to malloc must be paired with a corresponding call to free to release the memory. Failing to do so causes a memory leak.

As you saw, malloc simply reserves a contiguous block of bytes; it does not initialize this memory, which will contain garbage values. Two other functions in this family are also important:

  • calloc(): Stands for "contiguous allocation." It takes two arguments: the number of elements and the size of each element. Its key difference from malloc is that it initializes the allocated memory to zero.
  • realloc(): Used to resize a previously allocated memory block. This is useful for dynamic arrays that need to grow or shrink. A critical safety pattern when using realloc is to assign its return value to a temporary pointer, because if the reallocation fails, realloc returns NULL but does not free the original memory block.

Dynamic Memory Allocation in C

This GeeksforGeeks article provides clear code examples for calloc and realloc.

Please review the section on calloc() to see how it differs from malloc. Then, carefully study the section on realloc(), paying special attention to the second example that uses a temporary pointer (temp) to safely handle potential allocation failures.

C++-Style: new and delete

C++ introduced its own operators, new and delete, to provide a more integrated and type-safe approach to memory management.

// Allocate a single integer on the heap
int* p_int = new int;
*p_int = 10;

// Allocate an array of 50 doubles on the heap
double* p_arr = new double[50];
p_arr[0] = 3.14;

// ... use the allocated memory ...

// Deallocate the single integer
delete p_int;

// Deallocate the array of doubles
delete[] p_arr;

Notice the key differences from the C style:

  • Type Safety: You use new with a type (e.g., new int). The operator returns a pointer of the correct type, so no cast is needed. This eliminates a common source of errors.
  • Object Construction: If you new a class object, its constructor is automatically called after the memory is allocated. Similarly, delete calls the object's destructor before deallocating the memory. This is a cornerstone of the C++ object model, known as RAII (Resource Acquisition Is Initialization).
  • Array vs. Single Object Deletion: You must use delete for single objects allocated with new and delete[] for arrays allocated with new[]. Mismatching these is a serious error that leads to undefined behavior.

new and delete Operators in C++ For Dynamic Memory

This article provides a concise reference for the new and delete operators.

Read the sections covering the new operator for both single elements and arrays. Then, review the section on the delete operator, cementing the crucial delete vs delete[] distinction.


2. Under the Hood: Demystifying the Allocator

In the last lesson, we noted that heap allocation is slow because it involves a search. But what is it searching for? And what happens when we free memory? Your interest in low-level systems warrants a look behind the curtain.

A heap allocator's job is to manage a large region of memory obtained from the operating system. It does this by maintaining bookkeeping information—often a linked list of free blocks—to keep track of which parts of the heap are in use and which are available.

i wrote my own memory allocator in C to prove a point

This video from the "Low Level" channel demonstrates building a simple memory allocator from scratch. It provides an excellent mental model for what malloc and free are doing.

Watch these two key segments: Allocator Metadata: The creator explains the basic data structures (Heap metadata and chunk metadata) used to track memory. This bookkeeping is the source of the overhead. Fragmentation and Coalescing: This part illustrates one of the most significant challenges in memory management: fragmentation. It shows how repeated allocation and deallocation can leave the heap full of small, unusable "holes." It then demonstrates the solution: coalescing, where the allocator merges adjacent free blocks into a single, larger block.

This process of searching, splitting, and coalescing blocks is the complex dance behind every malloc or new call. It's why heap operations are orders of magnitude slower than a simple stack pointer adjustment.


3. The Four Horsemen of Memory Errors

Manual memory management is unforgiving. A small mistake can lead to crashes, silent data corruption, or performance degradation that is difficult to debug. The vast majority of these errors fall into four main categories.

1. Memory Leaks

A memory leak occurs when you allocate memory on the heap but lose the pointer to it without ever calling free or delete. The memory remains allocated and unusable for the lifetime of the program.

void memory_leak_example() {
    // p is a local pointer on the stack
    int* p = new int[100]; 
    
    // some work is done...

} // <-- function returns, 'p' is destroyed. The heap memory it pointed to is now lost.

In a long-running application like a server or a scientific simulation, even a small, repeated leak will eventually exhaust all available system memory and crash the program.
Solution: Be disciplined. Every new must have a corresponding delete. Every malloc must have a free. In modern C++, the preferred solution is to use smart pointers (std::unique_ptr, std::shared_ptr), which automate deallocation.

2. Dangling Pointers

A dangling pointer is one that points to a memory location that has already been deallocated. Using a dangling pointer results in undefined behavior—it might crash, it might read garbage data, or it might silently corrupt another part of your program's memory.

After `free(ptr)` is called, the memory at address 1000 is returned to the heap. However, the `ptr` variable itself still holds the value 1000, making it a "dangling" pointer. Any subsequent attempt to dereference `ptr` is invalid.

Solution: After calling delete or free on a pointer, immediately set the pointer to nullptr. This ensures that any accidental future use of that pointer can be caught as a null pointer dereference, which is a predictable crash rather than silent corruption.

int* p = new int(42);
// ... use p ...
delete p;
p = nullptr; // Good practice!

3. Double Deletion (Double Free)

This occurs when you attempt to delete or free the same memory location twice. This act corrupts the allocator's internal bookkeeping data structures (like the free list we saw earlier), often leading to an immediate crash or bizarre, hard-to-diagnose behavior later in the program.

Solution: The same solution for dangling pointers applies here. Setting a pointer to nullptr after deletion prevents a double delete, as calling delete on a null pointer is guaranteed to be a safe, no-op.

4. Mixing Allocation/Deallocation Mechanisms

The C-style and C++-style mechanisms are not interchangeable.

  • Memory allocated with malloc MUST be freed with free.
  • Memory allocated with new MUST be freed with delete (or delete[]).

Mixing them (e.g., malloc and delete, or new and free) is undefined behavior. It can fail because the allocators might use different heaps or bookkeeping, and critically, it bypasses the C++ constructor/destructor mechanism.

new and delete Operators in C++ For Dynamic Memory

This article summarizes these common errors.

Please read the section titled "Errors Associated with Dynamic Memory". It provides a concise recap of Memory Leaks, Dangling Pointers, Double Deletion, and the danger of mixing new/delete with malloc/free.


Conclusion

In this lesson, you've learned the essential mechanics of managing memory on the heap. We've covered both the traditional C-style functions and the more modern C++ operators, giving you the flexibility to work in any C/C++ environment. More importantly, we've explored what happens "under the hood" and confronted the critical pitfalls that every systems programmer must learn to avoid.

Key Takeaways:

  • Dynamic memory allocation is managed via two main pairs: malloc/free (C-style, byte-oriented, not type-safe) and new/delete (C++-style, type-safe, integrated with object construction/destruction).
  • You must always match the array form new[] with delete[].
  • The heap allocator is a complex piece of software that manages free blocks, and its operations (like searching and coalescing) are the reason for its performance cost.
  • The four primary memory errors are memory leaks, dangling pointers, double deletion, and mixing allocators.
  • A disciplined approach—pairing allocations with deallocations and setting pointers to nullptr after freeing them—is essential for writing robust code.

We've now seen how we get blocks of memory and how primitive types or arrays are stored in them. But how are more complex, user-defined types like structs and classes arranged within these memory blocks? In our next lesson, we will delve into the C++ memory model, exploring data alignment, padding, and the layout of objects in memory—concepts that are critical for performance and for interfacing with hardware like GPUs.