In the beginner course, we introduced the concept of the call stack, which is generated by the function calls in our program.
We saw how we could create local variables within our functions. We may have predicted, therefore, that the stack has some memory available to store things required by our functions.
This is indeed the case - the stack has a small amount of memory available to it. 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.
We can see this in action using a class with a constructor and destructor:
#include <iostream>
using namespace std;
class Character {
public:
Character() {
cout << "Creating Character" << endl;
}
~Character() {
cout << "Destroying Character" << endl;
}
};
void SelectCharacter() {
Character Frodo;
}
int main() {
cout << "Program Starting" << endl;
SelectCharacter();
cout << "Program Ending" << endl;
}
The output is as follows:
Program Starting
Creating Character
Destroying Character
Program Ending
If the concept of constructors and destructors are unfamiliar, I’d recommend reviewing this lesson:
This automated form of memory management is very useful, but it does have some limitations.
For an example of the second limitation, consider a scenario where we want our SelectCharacter
function to return a pointer to the character:
#include <iostream>
using namespace std;
class Character {
public:
~Character() {
cout << "Destroying Character" << endl;
}
string Name { "Frodo" };
};
Character* SelectCharacter() {
Character Frodo;
return &Frodo;
}
int main() {
Character* SelectedCharacter { SelectCharacter() };
cout << "Getting Character Name:" << endl;
cout << SelectedCharacter->Name << endl;
}
On my machine, this program outputs the following:
Destroying Character
Getting Character Name:
1�I�^H�H�PTI�#@H�`#@H�@�* �D�f.�@�@`H=�@`t�H�t
The fact that line 3 was nonsense is perhaps predictable given the proceeding output. The Character
has already been destroyed because it was allocated within the SelectCharacter
function’s stack frame.
So, the Frodo
pointer within our main function is pointing at memory that is no longer allocated to our program.
This is going to cause issues but, hopefully, the compiler will have warned us of this. Clang generated the following warning:
warning: reference to stack memory associated
with local variable 'Frodo' returned
Updating the previous example to return Frodo
by value rather than by reference would have worked as expected:
Character SelectCharacter() {
Character Frodo;
return Frodo;
}
int main() {
Character SelectedCharacter { SelectCharacter() };
cout << "Getting Character Name:" << endl;
cout << SelectedCharacter.Name << endl;
}
Getting Character Name:
Frodo
Destroying Character
When we return something from a function, that data is moved to the stack frame that called our function. In this example, it’s the main
function that receiving Frodo
When we return Frodo
by value, we’re moving the Frodo
object. Now, the Frodo
object is not destroyed when SelectCharacter
ends. It is instead moved to main
, and only destroyed once main
ends.
Previously, when we returned a pointer, we’re moving just the pointer to the main
function. The Frodo
object is still within the SelectCharacter
stack frame. Therefore, it gets destroyed with the stack frame, and the pointer that was moved to main
is no longer useful.
We cover move semantics in more detail later in this course.
To get around these limitations, there is another place in memory where we can store data. This is called the free store.
There, we have much more control over the lifecycle of our objects. Most notably, our functions can place objects there, and they remain in place until we explicitly delete them.
We’ll see how we can use the free store in the next lesson.
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.