Hello! Welcome to your next lesson on C++ foundations.
In our last lesson, we defined our first custom type, the TelemetryEvent class, which bundled data and behavior. We learned the difference between struct and class, and the importance of public vs. private for creating clean interfaces. All the objects we created, like TelemetryEvent my_event;, were allocated on the stack and were automatically destroyed when they went out of scope.
This leads to a critical question for our telemetry engine: What if we need data to live longer than the function that creates it? What if we need to allocate a very large buffer for events, potentially larger than the stack can handle?
Today's lesson answers these questions by exploring the two fundamental memory regions in a C++ program: the stack and the heap. You will learn to explain the difference between stack and heap allocation and manage memory with new and delete. Understanding where your data lives and how to control its lifetime is not just an academic exercise; for a high-performance system like the one you're building, it is a non-negotiable, core engineering skill.
Memory in a C++ Program: A Tale of Two Regions
When your C++ program runs, the operating system gives it a block of virtual memory. For our purposes, we'll focus on two key areas within that block: the stack and the heap. They have fundamentally different characteristics, management styles, and performance profiles.

Let's start by getting a clear conceptual model of these two memory regions and their trade-offs.
The video 'Stack vs Heap Memory in C++' by The Cherno offers an excellent overview. It explains where these memory regions are, their relative sizes, and the core differences in how they are used.
Please watch the first part of the video from 0:00 to 1:44. Focus on understanding that both stack and heap are just regions in your computer's RAM, but they are managed very differently.
The Stack: Fast, Ordered, and Automatic
You've already been using the stack without even knowing it! Every local variable you declare inside a function is, by default, allocated on the stack.
void myFunction() {
int a = 5; // 'a' is on the stack
TelemetryEvent event; // 'event' is on the stack
} // 'a' and 'event' are automatically destroyed here
Key Characteristics of the Stack:
- Automatic Management: The compiler handles allocation and deallocation for you. When a variable goes out of scope (e.g., the function it was declared in returns), its memory is automatically freed.
- Extremely Fast: Allocating memory on the stack is incredibly fast. It typically involves just moving a single pointer (the "stack pointer") by the number of bytes you need. It's often a single CPU instruction.
- LIFO (Last-In, First-Out): The stack works like a stack of plates. When you call a function, a "stack frame" containing its local variables is pushed onto the top. When the function returns, its frame is popped off.
- Fixed Size: The stack has a relatively small, fixed size (e.g., a few megabytes). If you try to allocate too much memory on it (e.g., a very large array), you'll get a stack overflow error.
The speed of stack allocation is a crucial point for performance-critical code. Let's see a deeper explanation of why it's so fast.
Let's return to the same video by The Cherno. This segment brilliantly demonstrates the speed of stack allocation by showing the simple machine code it generates.
Please watch the following segments: 4:48 - 7:35: This part visualizes how stack variables are laid out contiguously in memory and explains that allocation is just a simple, fast movement of the stack pointer. 13:20 - 15:00: This is the key takeaway. The video compares the generated assembly code for stack vs. heap allocation, showing that stack allocation is just one simple instruction. This directly relates to your goal of building a 'zero-overhead' system.
The Heap: Flexible, Large, and Manual
What if you need data to persist beyond a single function's scope, or you need to allocate a large amount of memory? This is where the heap comes in. The heap is a large pool of memory available to your program for creating objects with dynamic lifetimes.
Key Characteristics of the Heap:
- Manual Management: You, the programmer, are in complete control. You explicitly request memory, and you must explicitly release it when you're done.
- Slower Allocation: Requesting memory from the heap is significantly slower than stack allocation. It involves searching for a free block of memory of the right size, some bookkeeping, and potentially even asking the operating system for more memory, which is a very expensive operation.
- Flexible Lifetime: An object allocated on the heap exists until you explicitly destroy it, regardless of which scope it was created in.
- Large Size: The heap is much larger than the stack, limited primarily by the available RAM on your system.
Managing Heap Memory with new and delete
To work with the heap in C++, you use the new and delete operators.
- The
newoperator allocates memory on the heap for an object, runs its constructor, and returns a pointer to that memory. - The
deleteoperator takes a pointer, runs the object's destructor, and returns the memory to the system.
Let's watch a detailed walkthrough of this process.
new & delete Operators For Dynamic Memory Allocation | C++ Tutorial
The video 'new & delete Operators' from Portfolio Courses provides a very clear, step-by-step guide to the syntax and concepts of manual memory management.
Watch these segments to understand the mechanics: 1:25 - 4:14: How to use new to allocate a single object and how to use the pointer to access its value. 4:14 - 6:38: The critical importance of delete and a clear illustration of what a memory leak is. 8:15 - 10:36: How to use new and delete with your own custom classes (like our TelemetryEvent), including the use of the arrow (->) operator to access members via a pointer.
Let's apply this to our TelemetryEvent class.
// Previous lesson: Stack allocation
void createOnStack() {
TelemetryEvent stack_event; // Object created on the stack
stack_event.set(1, 101, "Stack event");
stack_event.print();
} // stack_event is automatically destroyed here.
// New concept: Heap allocation
TelemetryEvent* createOnHeap() {
// 'new' allocates memory on the heap and returns a pointer.
TelemetryEvent* event_ptr = new TelemetryEvent();
return event_ptr; // The object on the heap continues to exist!
}
int main() {
// We receive a pointer to an object that lives on the heap.
TelemetryEvent* heap_event = createOnHeap();
// Use the arrow operator (->) to access members via a pointer.
heap_event->set(2, 202, "Heap event");
heap_event->print();
// CRUCIAL: We are responsible for deleting the memory we allocated.
// If we don't, this is a memory leak.
delete heap_event;
heap_event = nullptr; // Good practice to null out pointers after deleting
return 0;
}
This article provides another excellent visualization of how a pointer on the stack refers to an object on the heap.
C++ Memory Management. Visualizing memory
The article 'C++ Memory Management. Visualizing memory' provides simple, clear diagrams showing the relationship between stack pointers and heap data.
Read the short sections 'Visualizing memory' and 'Using new and delete'. Focus on the diagrams that show the dptr variable (a pointer) living on the stack, while the memory it points to is on the heap.
The Cardinal Rule: new must be paired with delete
For every new, there must be a matching delete. If you fail to delete memory that you've allocated, your program "leaks" memory. It holds onto memory it no longer needs, and for a long-running application like a profiler, this can be fatal, eventually consuming all available RAM and crashing the system.
Allocating Arrays on the Heap
Our profiler will need to handle many events, not just one. Often, we'll want to allocate a buffer to hold an array of them. You can do this on the heap using a special form of new and delete.
new Type[count]allocates an array ofcountobjects.delete[] pointerdeallocates an array of objects.
It is absolutely critical to match delete[] with new[]. Using the wrong form (delete on an array or delete[] on a single object) leads to undefined behavior.
// Allocate an array of 1000 TelemetryEvents on the heap
size_t event_count = 1000;
TelemetryEvent* event_buffer = new TelemetryEvent[event_count];
// We can access elements like a normal array
event_buffer[0].set(1, 101, "First event");
event_buffer[999].set(1000, 201, "Last event");
// ... do work with the buffer ...
// CRUCIAL: Use the array version of delete.
delete[] event_buffer;
event_buffer = nullptr;
This is a pattern you'll see often in high-performance C++ where you need to manage blocks of memory directly.
Test your understanding!
You are writing a function that needs to process a list of log messages. The number of messages is determined at runtime. Your task is to correctly allocate, use, and free the memory.
- Write a function
processLogs(int log_count)that does the following:- Allocates an array of
TelemetryEventobjects on the heap. The size of the array should belog_count. - Initializes the very first event (
index 0) in the array with some sample data. - Prints the first event to the console.
- Correctly frees all the allocated memory before the function returns.
- Allocates an array of
Show answer
#include <iostream>
#include <cstdint>
// Assuming the TelemetryEvent class from the previous lesson is defined here.
class TelemetryEvent {
uint64_t timestamp;
uint32_t event_id;
const char* message;
public:
void set(uint64_t ts, uint32_t id, const char* msg) {
timestamp = ts;
event_id = id;
message = msg;
}
void print() const {
std::cout << "Event ID " << event_id
<< " at " << timestamp
<< ": " << message << std::endl;
}
};
void processLogs(int log_count) {
if (log_count <= 0) {
return; // Nothing to do
}
// 1. Allocate an array of 'log_count' events on the heap.
TelemetryEvent* log_buffer = new TelemetryEvent[log_count];
std::cout << "Allocated buffer for " << log_count << " logs." << std::endl;
// 2. Initialize the first event.
log_buffer[0].set(12345, 1, "System Initialized");
// 3. Print the first event.
std::cout << "First log message: ";
log_buffer[0].print();
// 4. CRUCIAL: Free the allocated array memory.
delete[] log_buffer;
log_buffer = nullptr; // Good practice
std::cout << "Log buffer freed." << std::endl;
}
int main() {
processLogs(10);
return 0;
}
Conclusion
You've now gained control over one of the most fundamental aspects of systems programming: memory layout and lifetime. This knowledge separates high-level application scripting from low-level systems engineering.
Key Takeaways:
- Stack: Used for local variables with scope-based lifetimes. Allocation is automatic and extremely fast. Size is limited.
- Heap: Used for data with a dynamic lifetime that can outlive its creation scope. Allocation is manual and slower, but the memory pool is much larger.
newanddelete: Your tools for manual heap management.newallocates and returns a pointer;deletefrees the memory.new[]anddelete[]: The required pairing for allocating and freeing arrays on the heap. Mismatching them is a serious error.- Memory Leaks: The result of calling
newornew[]without a correspondingdeleteordelete[]. They are a critical bug in long-running programs.
In this lesson, we saw the power and the danger of manual memory management. It's powerful because it gives you control, but dangerous because it's easy to make mistakes like forgetting delete. So, how can we get the best of both worlds?
Our next lesson, "Implement classes with constructors and destructors to manage object lifecycle," is the first step towards the solution. You'll learn how to use special class functionsโconstructors and destructorsโto automate the setup and cleanup of your objects, making your code safer and more robust. This will lay the groundwork for a core C++ philosophy: RAII (Resource Acquisition Is Initialization), which you will use extensively.