Let's create the following simple program to introduce the concept of the call stack.
#include <iostream>
using namespace std;
int Health { 150 };
void TakeDamage() {
cout << " - TakeDamage function starting" << endl;
Health -= 50;
cout << " - TakeDamage function complete" << endl;
}
int main() {
cout << "Main function starting" << endl;
TakeDamage();
cout << "We are back in main" << endl;
TakeDamage();
cout << "Main function complete" << endl;
}
The result of running this program can be seen here:
Main function starting
- TakeDamage function starting
- TakeDamage function complete
We are back in main
- TakeDamage function starting
- TakeDamage function complete
Main function complete
The following diagram illustrates the flow of this program through time.
When this program runs, we can imagine that its execution has "height".
The main
function is at the bottom - on the surface. It is the function that gets called first in our program. When main
ends, our program ends.
main
calls the TakeDamage
function. In a sense, when a function calls another function, the calling function is paused, and control shifts to the function that it called.
In this example, we can imagine main
as being paused until TakeDamage
completes.
We could visualise this as a stack of blocks, sitting on top of each other, where each block is created by a call to a function. The function at the top of that stack is the one that is currently being executed.
If that function calls another function, it gets paused, a new block for the new function call is added to the top of the stack, and that new block is now being run.
When a function reaches the end of its execution, its block gets removed from the stack, and the block below it picks up where it was previously paused.
When the block at the very bottom of our stack - the one created by the main
function gets popped off, our stack is empty, and our program quits.
This concept of function calls creating a stack is called a call stack, and the blocks are called stack frames.
A snapshot of the current state of the call stack it is shown to us when we are debugging our code:
Here, we see external code (ie, operating system) at the bottom of the stack. The operating system called our main
function, so its stack frame is on top of the external code. And main
called TakeDamage
, so its stack frame is on top of main
.
Lets add a break point to our code, and explore how we can use the debugger with functions.
When debugging our earlier code, we had only one level of depth - everything was in the main
function. There, we saw how we could advance execution line by line in the debugger using Step Over
Likely, you noticed that many different options appear when we start debugging. Many of those are to allow us to navigate the call stack created by our functions.
The four we want to talk about here are the different ways we can progress our application when debugging - Step Over, Step Into, Step Out, and Continue.
Were we to advance execution using Step Over, we are advancing within the current stack frame. If the line we're debugging happens to call another function, that function will create its own stack frame.
But, with step over, we don't debug that frame - we just let it complete entirely. So, Step Over runs as much code as needed until control shifts back to the frame we're currently in. From there, we can continue debugging.
In our example, if we're in the main
function, and Step Over a line that calls TakeDamage
, the call to TakeDamage
will complete entirely. Once control shifts back to main
, our debugger will pause execution again, and continue to let us step through main
, line by line.
When we're on a line of code that is about to call a function, we have the option to Step Into that function call. In our case, rather than executing the TakeDamage
function completely with Step Over, we could instead use Step Into.
This will add that new function's stack frame to the call stack, and the debugger would immediately take us into that new stack frame, pausing at the first line.
We could then move through the TakeDamage
function, line by line.
Step Out completes execution of the current stack frame. Once it is done, it will get removed from the call stack. Our debugger will then pause at the previous stack frame - the one that called the function we were previously in.
For example, at any point when debugging TakeDamage
, we could Step Out, which would complete execution of the function, remove it from the call stack. Our debugger would then be paused in the main
function, right after the line that called TakeDamage
to generate the stack frame we were previously debugging.
Finally, we have the option to Continue. This will proceed execution until the next breakpoint we have defined. If we have no further breakpoints, we will Continue to the end of our program.
Take some time to add a break point to your code, and experiment with the debugger and how these options navigate around your code.
Establishing an understanding of the call stack concept in this simple program will greatly help understand what is going on in bigger applications, and will be vital to help us debug them when things inevitably go wrong.
When debugging, which action lets us enter a function that is about to be called?
Up next, we'll see how we can use booleans to make our functions more powerful and dynamic
Become a software engineer with C++. Starting from the basics, we guide you step by step along the way