These days got me into solving competitive coding problems for placement preparations. Starting off this regular practice is such a tiresome process. I end up getting frustated most of the time with silly mistakes and the segmentation faults are the worst nightmare.

We're prone to make mistakes while typing code and especially with pointers, it is a pure mess. These tiny mistakes, wrong pointer usages result in undesired output and they have the powers to conceal themselves from your eyesight. "Read the code thrice, all looks good. It should be the machine which is wrong."

Man, I have std::cout

First thing we try to do is to insert print statements inbetween. "I doubt that, really control reaches this line. Let me insert cout<<"test1"". "This variable not seems good, let me print it before and after the expression."

CHj5mr3

We've been doing this since the days we learnt to code and it works. But this is painful and I often end up printing lot of lines to stdout in which I could find the mistakes nowhere. Segmentations faults result in misuse of pointers and finding them needs you to go through your code multiple times.

Let me introduce the magic

Here I present you the concept of debuggers. Debuggers are tools developed in order to aid you in debugging code and much more. Let's stick on to c++ for this tutorial, as debugging concept stays the same for all languages. "So, this is tutorial on how to use a debugger?" Nah, there are plenty availble out there and now let me try to convince you to use one.

Perks of using a debugger

Debuggers such as gdb allow you to attach to a running process, examine it's memory, callstack, view the subsequent m/c instructions, view the registers of that process (beleive me!) and a hell lot more. "oh, wait! I'd rather use std::cout than messing up with registers and memory."

You won't be in need of these indepth usage of debuggers and also most of the IDE's or editors you use, have debuggers built in. IDEs make the learning curve steep and it's worth to give a try. You'll never regret it

Let me list down few simple use cases that are life saviours for competitive coding problems. All the below can be done in a IDE GUI, without pain.

  • Stop the program at any line (breakpoint), and view the values of all local and global values at that point.
  • Run the program, one line at a time to find the control path (step over)
  • View the call stack, with the values of the arguments passed on to that function. This is your handy tool when using recursion as you can see the values with which the recursive funtion is called each time.

These three uses will start you off with a debugger and once you get used to it, you can get yourself dirty with it. Don't underestimate the power of debugger. I'll continue this post with an example for the 3 use-cases.

#1 - Breakpoints

Let's start with breakpoints. A breakpoint stops the flow of execution at that point and let you examine or continue. Consider the below snippet with a segmentation fault. (I'm dereferencing a NULL pointer)

int main() {
  int *p;
  int a = 5;
  p = &a;
  p = NULL;
  cout<<a;
  cout<<*p;
}

This seems lame, but you can find true segmentaion faults by examining at breakpoints. I've attached the screenshot of my debugger (VScode). You're free to use your own.
I added two breakpoints, one at line 9 (left), and another at line 11 (right).

breakpoint

You can see the values of the variables being shown at the side bar. It shows the address to which the pointer points to and also the value at that address. The right image clearly shows that p points to 0x00 which is the very first address (means NULL).

Debuggers have come a long way. When I try to run this program without breakpoints, VSCode automatically shows where the problem is.

sigsegv

Apart from segmentation faults, undesired outputs also can be solved by examining the variables at specific breakpoints.

#2 - Stepping over

Executing your code, one line at a time and examining the values, can get you the control flaws you've made. Let's take another example

int main() {
  for(int i=0; i<5; i++) {
    if(i==2)
      cout<<"oh no! i is 2"; 
      break;
    cout<<i<<endl;
  }
}

I meant to break the loop when i becomes 2. I have this habit of not using {} when there is only one statement. Then I decide to insert a print when breaking. So when I added cout<<"oh no! i is 2";, I by mistake made break come outside if and this is going to break the loop on first run. (I have made this same mistake once, and spent considerable time finding it.) Let's debug this now.

This clearly shows where I went wrong.

#3 - Call stack

Examining call stack shows all the instances of functions called and the parameters passed to them. with many recursive calls, you have no other go than using debuggers.
Let's try the following snippet.

void doIt(int c) {
  if(c) doIt(c--);
}

int main() {
  doIt(2);
}

The above snippet is meant to stop on two calls. But it runs infinitely.
The call stack of the debugger shows

call-stack

Clicking on each call, shows it's local variables. Each call here has c=2 which should have decremented. You get it? It should be --c and not c--.

So, how to start?

The GNU debugger - gdb is the underlying tool and it takes time to get used to it. I don't recommend starting directly with that. For now, start with the debugger that comes with your IDE.
I prefer VScode, but even sublime should do.

Debuggers need symbol table information to work properly, for gcc or g++ this comes by compiling your code with -g flag.

The visual studio Express (not VScode) has the debugger out of the box.
You easily can find tutorials on setting up debugging. Once you set it up, everything is a button away. Feel free to ping me on any clarifications.