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.
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:
- Use the
newoperator followed by a type to allocate memory on the heap. newreturns a pointer to the start of that memory. You must store this pointer to keep track of the allocation.- 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 withdelete[].
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[]](https://qit.software/wp-content/uploads/2023/11/cpp_memory_manual_management_07.png)
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.
- Create a file named
dynamic_scores.cpp. - In
main(), prompt the user to enter the number of scores they want to store. - Read the number into an integer variable, let's call it
num_scores. - Dynamically allocate an array of
doubles of sizenum_scoresusingnew[]and store the returned address in a pointer. - Check if the pointer is
nullptr. If it is, print an error message and exit. - Use a
forloop to prompt the user to enter each score and store them in the dynamic array. - Calculate the average of the scores.
- Print the average score to the console.
- Crucially, deallocate the array using
delete[]. - 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
newto 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 matchingdelete. - For every
new[], there must be a matchingdelete[]. Mismatching them is a serious error. - Forgetting to
deletecauses memory leaks. - In C++,
newanddeleteare superior tomallocandfreebecause 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.