Skip to main content

Welcome to a Grasp lesson

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

Lesson 31

Dynamic Memory Management with `new` and `delete`

Recommended resources

Introduction

Hello! In our last lesson, we focused on C++ references, seeing them as a safe and convenient way to create aliases for variables, especially for passing objects to functions without making costly copies. We saw that references, like the variables they alias, typically live on the stack, and their lifetimes are tied to the scope in which they are defined.

This works perfectly when you know the size and lifetime of your data at compile time. But what if you don't? What if you need to create an object whose lifetime must extend beyond the current function call, or if you need to allocate a block of memory whose size is determined by user input?

This is where dynamic memory allocation comes in. Today's lesson directly addresses the learning outcome: to allocate and deallocate memory using the new and delete operators. We will explore how to request memory from a large pool called the heap and why you are responsible for managing it. For someone with your background in Java and Python, which use automatic garbage collection, this concept of manual memory management is one of the most significant distinctions in C++.


1. Revisiting Stack vs. Heap Memory

In the C modules, you were introduced to the concepts of stack and heap memory. Let's refresh and deepen that understanding from a C++ perspective, as it's fundamental to using new and delete correctly.

  • Stack: A region of memory where local variables and function call information are stored. It's managed automatically by the compiler. When a function is called, a "stack frame" is created for its local variables; when the function returns, the frame is destroyed. Allocation is extremely fast (just moving a pointer) but the size is limited and fixed.
  • Heap: A large pool of memory available to the program, managed by the operating system. Allocation is done on-demand at runtime. It's much larger than the stack but slower to allocate from. Crucially, memory allocated on the heap persists until you manually free it.

Since you enjoy understanding the "under the hood" details, the following video gives a superb explanation of how these two memory areas work and why their performance characteristics differ.

Stack vs Heap Memory in C++

Watch this video from The Cherno to get a clear mental model of the stack and heap. Pay attention to his explanation of why stack allocation is so much faster than heap allocation.

Please watch the following segments: 0:00 - 2:47: An introduction to what the stack and heap are and where they reside in memory. 4:05 - 7:35: A visualization of how variables are laid out on the stack and an explanation of the fast 'stack pointer' mechanism. 11:53 - 13:30: A comparison of the performance implications, showing the assembly code for stack vs. heap allocation.

The key takeaway is that the stack is for data with a clear, predictable lifetime tied to a scope, while the heap is for data with a dynamic lifetime that you, the programmer, must manage.


2. The new and delete Operators

In C, you used malloc() and free() for dynamic memory. C++ introduces the type-safe operators new and delete for this purpose. They are preferred in C++ because they understand and work with C++ types, including calling constructors and destructors for objects.

Let's walk through the basics.

new & delete Operators For Dynamic Memory Allocation | C++ Tutorial

This video by Portfolio Courses provides a clear, step-by-step tutorial on the syntax and usage of new and delete. We'll watch it in parts.

First, watch from 1:25 to 4:14. This part demonstrates how to use new to allocate a single variable on the heap and how to use a pointer to access and modify it.

As shown in the video, the process is:

  1. Use the new operator followed by a type to allocate memory on the heap.
  2. new returns a pointer to the start of that memory. You must store this pointer to keep track of the allocation.
  3. Use the dereference operator (*) on the pointer to access the value.
// Allocate an integer on the heap
int* ptr = new int;

// Assign a value to that memory location
*ptr = 10; 

// You can also initialize it at the same time
int* ptr2 = new int(20); // Direct initialization
int* ptr3 = new int{30};  // Uniform initialization

Freeing Memory with delete

For every call to new, there must be a corresponding call to delete. This is the core rule of manual memory management. delete tells the operating system that you are finished with the memory, allowing it to be reused.

new & delete Operators For Dynamic Memory Allocation | C++ Tutorial

Now, let's see how delete works and what happens if you forget to use it.

Watch from 4:14 to 6:50. This segment explains the delete operator and introduces the concept of a memory leak.

A memory leak occurs when you allocate memory on the heap but lose the pointer to it without calling delete. This memory becomes inaccessible for the rest of your program's execution, effectively "leaking" away. This is a serious bug, especially in long-running applications.

// Correct usage
int* p = new int(100);
// ... use the memory pointed to by p ...
delete p; // Return the memory to the system
p = nullptr; // Best practice: set the pointer to null to prevent it from becoming a 'dangling pointer'

3. Allocating Arrays

The real power of dynamic allocation shines when you need an array whose size isn't known at compile time. For this, C++ provides the new[] and delete[] operators.

new & delete Operators For Dynamic Memory Allocation | C++ Tutorial

The next part of the video shows how to allocate and deallocate dynamic arrays.

Watch from 6:50 to 8:15. Pay close attention to the [] syntax used in both new[] and delete[].

This leads to the most critical rule of dynamic array management:

If you allocate with new[], you MUST deallocate with delete[].

Mixing them up (e.g., new[] with delete) leads to undefined behavior. The [] in delete[] tells C++ to correctly clean up all elements of the array.

Manual Array Allocation with New and Delete[]
This diagram illustrates the relationship between the stack and the heap during dynamic array allocation. The pointer `p` exists on the stack, but it holds the memory address of the array, which has been allocated on the heap.
int size;
std::cout << "Enter the size of the array: ";
std::cin >> size;

// Allocate an array of 'size' integers on the heap
int* array_ptr = new int[size];

// Use the array...
for (int i = 0; i < size; ++i) {
    array_ptr[i] = i * 10;
}

// ...

// Deallocate the ENTIRE array when done
delete[] array_ptr;
array_ptr = nullptr;

4. Working with Objects

The new operator is even more powerful when used with classes and structs. A key difference from C's malloc is that new automatically calls the constructor of the object being created, and delete automatically calls the destructor.

This is much closer to what you're used to in Java, where new both allocates memory and initializes the object.

new & delete Operators For Dynamic Memory Allocation | C++ Tutorial

This final segment demonstrates allocating custom objects and introduces a convenient piece of syntax, the arrow operator (->).

Watch from 8:15 to 12:20. This covers creating objects with new, the arrow operator (->) for accessing members via a pointer, and the crucial difference between new/delete and C's malloc/free regarding constructors/destructors.

When you have a pointer to an object, you can access its members using the arrow operator (->). This is simply a convenient shorthand for dereferencing the pointer and then using the dot operator.

object_ptr->member is equivalent to (*object_ptr).member.

struct Player {
    int id;
    void printID() { std::cout << "ID: " << id << std::endl; }
};

Player* player_ptr = new Player{101}; // Create a Player on the heap

player_ptr->printID(); // Use the arrow operator to call a member function

delete player_ptr; // This will call the Player's destructor (if any) before freeing memory
player_ptr = nullptr;

5. Handling Allocation Failures

What happens if the operating system can't grant your memory request (e.g., you ask for a ridiculously large amount)? By default, new will throw an exception of type std::bad_alloc, which will crash your program if unhandled.

There is an alternative that might feel more familiar from a C-style error-checking perspective. You can use the nothrow version of new, which returns nullptr on failure.

Dynamic memory allocation with new and delete

For a detailed explanation of memory leaks, dangling pointers, and how to handle allocation failures, please read the following sections from the excellent Learn C++ tutorial site.

Read the following three short sections: Start at the heading "Dangling pointers" and read to the end of that section. Read the section titled "Operator new can fail". Read the section titled "Memory leaks". Focus on the definitions, the code examples of what can go wrong, and the best practices recommended.

Using std::nothrow allows you to check for allocation success with a simple if statement:




#include <iostream>

int main() {
    // Ask for a huge amount of memory that will likely fail
    int* huge_array = new (std::nothrow) int[1000000000];

    if (huge_array == nullptr) {
        std::cerr << "Memory allocation failed!" << std::endl;
        // Handle the error gracefully
    } else {
        std::cout << "Memory allocated successfully." << std::endl;
        // ... use the memory ...
        delete[] huge_array;
        huge_array = nullptr;
    }

    return 0;
}

6. Practical Exercise

Time to put this into practice. Let's write a program that uses dynamic memory to store a user-defined number of game scores.

  1. Create a file named dynamic_scores.cpp.
  2. In main(), prompt the user to enter the number of scores they want to store.
  3. Read the number into an integer variable, let's call it num_scores.
  4. Dynamically allocate an array of doubles of size num_scores using new[] and store the returned address in a pointer.
  5. Check if the pointer is nullptr. If it is, print an error message and exit.
  6. Use a for loop to prompt the user to enter each score and store them in the dynamic array.
  7. Calculate the average of the scores.
  8. Print the average score to the console.
  9. Crucially, deallocate the array using delete[].
  10. Set the pointer to nullptr.

This exercise covers the full lifecycle of dynamic memory: allocation based on runtime information, usage, and deallocation.


Conclusion

You have now learned the fundamentals of manual memory management in C++ using new and delete. This is a powerful but demanding feature that sets C++ apart from garbage-collected languages.

Key Takeaways:

  • Use new to allocate memory on the heap for data with a dynamic lifetime.
  • Always store the returned pointer to access and later free the memory.
  • For every new, there must be a matching delete.
  • For every new[], there must be a matching delete[]. Mismatching them is a serious error.
  • Forgetting to delete causes memory leaks.
  • In C++, new and delete are superior to malloc and free because they automatically call object constructors and destructors.
  • The arrow operator (->) is a convenient way to access members of an object through a pointer.

While manual memory management is essential to understand, it is also error-prone. Modern C++ offers a much safer and more convenient way to handle dynamic resources using smart pointers, which we will cover in a later module. They automate the process of calling delete, combining the power of the heap with the safety of automatic lifetime management.


Next Up

Now that we've covered a few core C++ features (std::string, references, new/delete), our code files might start to get crowded with function and type names. In the next lesson, we will learn how to use namespaces to organize our code and prevent naming conflicts, a crucial technique for building larger applications.