Memory Management and the Stack
Learn about stack allocation, limitations, and transitioning to the Free Store
In this chapter, we'll introduce memory management within C++, starting with stack-allocated memory. In the beginner course, we introduced the concept of the call stack, which is generated by the function calls in our program.
The Call Stack and Debugging Functions
An introduction to how our function calls create a call stack, and how we can navigate it in a debugger.
We can see the call stack in action using a class with a constructor and destructor:
#include <iostream>
class Character {
public:
Character(){
std::cout << "Creating Character\n";
}
~Character(){
std::cout << "Destroying Character\n";
}
};
void SelectCharacter() {
// A new stack frame is created for
// SelectCharacter. Local variable Frodo is
// allocated on the stack
Character Frodo;
// When SelectCharacter ends, Frodo is
// deallocated. Destructor is called as the
// stack frame is removed
}
int main() {
std::cout << "Program Starting\n";
// Call SelectCharacter, creating and
// destroying Frodo
SelectCharacter();
// After SelectCharacter returns, its stack
// frame is removed. Memory used by Frodo is freed
std::cout << "Program Ending\n";
}
Program Starting
Creating Character
Destroying Character
Program Ending
If the concept of constructors and destructors are unfamiliar, I'd recommend reviewing this lesson:
Constructors and Destructors
Learn about special functions we can add to our classes, control how our objects get created and destroyed.
We're already familiar that we can create objects within the local scope of our functions. An example of this is the Frodo
object, created within the SelectCharacter()
function of our previous example.
Stack Allocated Memory
Given we can create objects in our functions, we may have predicted, therefore, that the stack has memory available to store these objects.
This is indeed the case. When we create variables in our functions, we are given the appropriate amount of memory from the stack to store those variables. When the function ends, the stack frame is removed, local variables are deleted, and the memory is freed up for other uses.
However, while the stack is incredibly efficient for managing memory on a function-by-function basis, it is not without its limitations.
Stack Limitation 1: Space
Typically, the size of the stack in a C++ program is determined by the operating system and the settings of the compiler used. For instance, on many systems, the default stack size might be around 1 MB to 2 MB.
This is generally sufficient for most routine operations and function calls. However, it's often not enough to store large objects, or large collections of objects.
Attempting to allocate large data structures on the stack can lead to a stack overflow, where the stack's limit is exceeded, potentially causing the program to crash or behave unpredictably.
Stack Limitation 2: Flexibility
This form of memory management where objects are deleted automatically is often useful, but it does restrict our options
For example, consider a scenario where we want our SelectCharacter()
function to return a pointer to the character:
#include <iostream>
class Character {
public:
~Character() {
std::cout << "Destroying Character\n";
}
std::string Name { "Frodo" };
};
Character* SelectCharacter() {
Character Frodo; // Stack allocated
// Returning a pointer to stack-allocated data
return &Frodo;
}
int main() {
Character* SelectedCharacter {
SelectCharacter()
};
std::cout << "Getting Character Name:\n";
std::cout << SelectedCharacter->Name;
}
The output of this program could be something like the following:
Destroying Character
Getting Character Name:
1I^HHPTI#@H`#@H@* Df.@@
The fact that line 3 was garbage is perhaps predictable given the proceeding output: the Character
has already been destroyed by the time we come to log its name. This is because it was allocated within the SelectCharacter()
function's stack frame.
Once SelectCharacter()
ends, that stack frame is deleted. Therefore, the Frodo
pointer within our main function is pointing at memory that is no longer allocated to our program. Most compilers can detect this, and will issue a warning:
warning: reference to stack memory associated with local variable 'Frodo' returned
Preview: The Free Store
So far, we've explored the workings of stack memory and its limitations. In the next lesson, we'll explore the Free Store, sometimes also called the Heap.
What is the Free Store?
The Free Store is a region of memory that programs use to allocate objects whose lifetime is not tied to the scope of a function. Unlike stack memory, where objects are automatically managed and limited in size, the Free Store allows for dynamic memory allocation.
This means we can allocate memory at runtime, and it's your responsibility to free it when it's no longer needed.
Why Learn About the Free Store?
Understanding the Free Store is crucial because:
- Flexibility in Memory Allocation: It provides a more flexible way to manage memory, especially when dealing with large data or when the size of data is not known at compile time.
- Control Over Object Lifetimes: Objects in the Free Store remain alive until they are explicitly destroyed, giving us more control over their lifetimes.
- Essential for Advanced C++ Features: It lays the foundation for understanding more advanced C++ concepts like smart pointers and dynamic data structures.
What We'll Learn
In our upcoming lesson, we'll learn:
- How to allocate and deallocate memory on the Free Store
- Best practices for managing dynamic memory
- Common pitfalls and how to avoid them
- Real-world examples to solidify our understanding
Dynamic Memory and the Free Store
Learn about dynamic memory in C++, and how to allocate objects to it using new
and delete