C++2: Stepping, Running, and Jumping (?)

nubb
6 min readJul 2, 2024

--

Welcome back, Medium. Last week, I went over some C++ beginner topics like the preprocessor, header files and undefined behaviour. Today, I’ll be going over some core debugging methods like stepping and breakpoints, as well as how the call stack works.

(source)

stopAtEntry: “true”

For context on that heading choice. stopAtEntry is a field inside of your launch.json file in Visual Studio Code. You’ll need to create a new debug configuration to open the file and edit the field, which can be done by opening the command palette (Ctrl + SHIFT + P) and selecting C/C++ Add Debug Configuration. The reason behind this is that when we do debug, it will run the program until it hits a point where it can no longer do so, either at a field asking for user input (std::cin) or at a breakpoint, which can be added on any line by clicking left of the line number on a specific line (or by going to Run → Toggle Breakpoint), and will pause the debugging process.

This is where we can use stepping, which is a feature of most modern debuggers. There are 3 forms of stepping with a debugger:

  • Stepping in, which executes the line the debugger is currently held at, either by stopAtEntry being set to true or by a breakpoint halting progress.
  • Stepping over, which can be used to skip function calls if you don’t wish to step in to them manually and execute them line-by-line.
  • Stepping out, which exits a function entirely and doesn’t execute further. This can be used if you accidently step into a function you aren’t trying to debug.

In my opinion, stepping in is the most powerful of these tools since you can properly examine the execution of your program to diagnose its issues (since that’s what a debugger is being used for — to remove bugs).

There are 3 other notable features of debugging that aren’t so apparent, those being:

  • Run to cursor, which can be done by right-clicking on a given line and selecting the run to cursor command. This will cause the debugger to execute until it reaches the line you’re aiming to reach.
  • Continue, which just runs the program until completion unless it reaches a breakpoint or some other point pausing the program.
  • Jumping or “set next statement”, which can be used similarly to run to cursor by right-clicking on a line and selecting the relevant option. This will go to the selected line and ignore all other lines. It’s basically run to cursor but it doesn’t execute everything until it reaches the given line.

One thing to note is that using jumping is rather dangerous in that if you have variable declarations or assignments between your current point and the desired “jump point”, your program will inevitably produce some form of undefined behaviour (straight up crashing, invalid variable values, etc.)

Anyways, the last thing I’ll quickly go over is the call stack. Whenever we run a function call, it gets pushed to the top of the call stack. When a function finishes executing, it looks at the call stack and finds out where it last left off.

Consider the following program:

#include <iostream>

int multiply(int x, int y)
{
return x*y;
}

int readNumber()
{
std::cout << "Number: ";

int x{};
std::cin >> x;

return x;
}

int main()
{
int x { readNumber() };
int y { readNumber() };

int z { multiply(x, y) };
std::cout << "Final product: " << z;

return 0;
}

Let’s run this with Visual Studio Code’s debugger and see what happens. When we first start the debugger (by pressing F5 or going to Run → Start Debugging), we can see the call stack in the bottom left corner.

(after starting debugger)

Our debugger is currently waiting for us to continue and provide instructions to it. Let’s step into the function readNumber() and watch the call stack change.

(ignore the jank variable value in the Watch window, VSC is totally wrong xD)

The call stack has placed a bookmark where we did the function call to remember where it must go to after the current function is completed.

At this point, you’ll need to restart the debugger/program to run it properly. std::cin does not function properly with stepping as far as I can tell (trying to input anything just doesn’t work for me at least).

Now that we’re back at square one, go ahead and step over the function calls until you return to line 23. Go ahead and step in to the multiply(int x, int y) function. Once again, the call stack sets a bookmark at the function call so it can return back to it and keep executing the program from there. After the function call is complete, the (function) call gets thrown off the top of the stack. The main() call remains since main() is still running. Once our program terminates, main() is thrown off the stack.

If you wanna have a bit of fun with the call stack, consider this program:

#include <iostream>

void foo()
{
foo(); // what happens here?
}

int main()
{
std::cout << "let the ignorance commence";
foo();

return 0;
}

What might happen here? Let’s run it and find out for ourselves! :D

C:\Users\(username)\Desktop\Programming Projects and Scripts\cpp-projects\main.cpp: In function 'void foo()':
C:\Users\(username)\Desktop\Programming Projects and Scripts\cpp-projects\main.cpp:3:6: error: infinite recursion detected [-Werror=infinite-recursion]
3 | void foo()
| ^~~
C:\Users\(username)\Desktop\Programming Projects and Scripts\cpp-projects\main.cpp:5:7: note: recursive call
5 | foo(); // what happens here?
| ~~~^~
cc1plus.exe: all warnings being treated as errors

Build finished with error(s).

If you look carefully, I have all warnings treated as errors set up for my compiler. I highly recommend doing this so your compiler can catch semantic errors like these — with this one being a recursion loop.

If we think about this, our function will start at main(), so our call stack will look something like this:

CALL STACK 
main() main.cpp [11:1]

When we enter foo(), it’ll look like this:

CALL STACK 
main() main.cpp [11:1]
foo() main.cpp [5:1]

Wait a minute, if we step in or step over in this function, foo() will execute again, but the previous call never gets pushed off the stack, since we never left the function body!

This will inevitably create an infinite loop and subsequent stack overflow, where we exceed the call stack’s depth, which ultimately crashes everything. I personally have experienced this with a particular recursive program last fall in my CS class, which had me 4x the stack limit to ~4000 calls. If we do exceed the stack limit in C++, the stack basically blows up and says “Dang, we’re out of space to contain this. We need to stop now and warn the user!” This will send a segmentation fault to the user, which can be interpreted as “Wowzah, you screwed up big time. Debug your mess please.”

Real talk though, a seg fault is basically saying that your attempting to:

  • Access read-only memory and change it (illegal, return seg fault)
  • Accessing freed (non-existent to a degree) variables (illegal, can introduce undefined behaviour)

Those are two examples of scenarios where you might get a seg fault. I personally have never encountered one, but if you guys have (and want to tell me how it happened), feel free to do so in the comments.

-Werror

That’s all I’ve got for this week. Next week, I’ll be introducing you guys to more data types of C++, like doubles and chars. I’ll be doing my research and learning as per usual until then.

Yesterday was Canada Day, so to anyone that celebrated, I hope you guys had a great day (happy July 4th to an American readers too)! I personally didn’t do anything special, though I did go outside for a bit and touched grass.

Anyways, I’ll see you guys again soon. Have a great rest of your day, Medium!

If you enjoyed this blog post, consider following me or subscribing to support me!

Check out my GitHub and Portfolio!

--

--

nubb

Some guy programming and telling the world about it || Check out my projects here: https://github.com/nubbsterr