Debugging & Exception Handling

Debugging

You will inevitably run into bugs while programming. Your program might be crashing, not behaving as expected, or not compiling. Debugging is the process of fixing these issues.

Errors that cause compilation to fail are generally easy to fix. The compiler often tells you where and why it failed. If the solution isn't immediately obvious, you can often find it with a quick Google search.

Crashes and unexpected behaviors are harder to address. The tricky part is finding where the program goes wrong. Once you know where the error is, it is often easy to identify and fix it. This chapter focuses on teachinig you how to find and avoid these bugs. You will learn how to use debugging tools to analyze your code as it runs.

If you need help understanding why a piece of code behaves differently from what you expect, google is your best friend. This is especially true when dealing with external libraries.

Getting information

The most important aspect of debugging is obtaining information about your program's internal state. Sometimes, you only need a simple console output of some variables to find the source of the issue. Outputting data you suspect is faulty while running can pinpoint where the problem occurs. If your program crashes, a text output indicates that the crash happened later in the code.


This program doesn't give us the expected value (47.25) for totalCost.

#include <iostream>

int main() {
    const double price = 50.0; // Price of the item
    const double discount = 0.1; // 10% discount
    const double taxRate = 0.05; // 5% tax
    const double totalCost = price * (1 - discount) + taxRate; // Incorrect calculation

    std::cout << "Total cost: $" << totalCost << std::endl;
    return 0;
}

Separating a calculation into many variables lets us output the variables and identify any segment(s) that don't make sense.

...
    double discountedPrice = price * (1 - discount);
    double taxAmount =  taxRate; // Incorrectly calculating tax on the discounted price
    double totalCost = discountedPrice + taxAmount; 

    std::cout << "Discounted price: $" << discountedPrice << std::endl; // Helps identify what each step computes
    std::cout << "Tax amount: $" << taxAmount << std::endl;
    std::cout << "Total cost: $" << totalCost << std::endl;
...
Discounted price: $45.00
Tax amount: $0.05
Total cost: $45.05

Tax amount: $0.05 seems very low for a 45$ item. You can tell by looking at the line of code for taxAmount that it doesn't consider the discounted price.

double taxAmount = discountedPrice * taxRate; // Now it's fixed

The console closes if your program ends or crashes. This makes reading output to the console impossible. You can avoid this issue by outputting debug information to a file. This way, you won't lose the data if a program closes/crashes.

Note: Some compilers make your console program into a "debug console" if it crashes. This lets you see any output that came before the crash occurred.

The debugger

A debugger lets you see what's happening inside your program. You can use the debugger to step through lines of code manually and inspect variable values. This insight into your program’s execution is invaluable for understanding how the code runs and identifying where it goes wrong.

Practicing the debugger can be boring because it doesn't involve creating new projects. However, learning the basics of the debugger will save you hours of tracking down bugs.

Debug mode

Most compilers let you compile your program in debug mode. A program compiled in debug mode includes information about variable and function names, line numbers, and other data that the compiler usually leaves out for the sake of efficiency. Debug mode lets you use debuggers more efficiently but makes the program slower and larger.

Guides for IDE's

To start debugging in Windows Visual Studio, click the "Local Windows Debugger" button or use the F5 shortcut. This runs the program with the debugger attached. Other IDEs will likely have a "Run and Debug" or "Start Debugging" option. ![image](VS - Local Windows Debugger)

This lesson's video version shows examples of debugging for some common IDEs. You can refer to these links for a more in-depth understanding of your IDE's debugging capabilities, setup, and process: Visual Studio Visual Studio Code Code::Blocks CLion You may have to revisit these links for future sections of this chapter.

Breakpoints

Code execution will freeze when it reaches a line with a breakpoint. You can analyze the values of variables in memory before continuing. In most IDEs, you can set a breakpoint by left-clicking to the left of the line you want it on. A red circle indicates a breakpoint on most IDEs.

Stepping through code

![image](Labeled UI in Visual Studio) Once a breakpoint is triggered, you have a few options to control the execution of your code: Continue: Resumes code execution until the next breakpoint. Step Over: Runs the statement and freezes on the next. Step Into: If the code execution is over a function, the code execution freezes as soon as it enters the function. Step Out: Runs the current function and freezes when it returns to the caller.

Note: Most IDE's will have keybind shortcuts for these operations that are worth learning.

Common issues

Bugs are a part of the development process. Writing code that's easy to read is one of the best things you can do to make debugging easier. This means short single-purpose functions, clear variable and function names, adding useful comments, etc.

You'll intuitively start recognizing bugs as you develop and fix more programs. We'll cover some of the most common bugs to kick-start your intuition.


The type of bug you're experiencing should determine your approach to fixing it.

Logical Errors

If you're getting a wrong value from the program, you should check the values of any variables used to determine the value. Think about the operation priority used, implicit castings, etc. Split formulas into multiple smaller segments to verify each part.

If code execution moves somewhere unexpected, use a debugger to step through code and examine values in conditionals like if and while statements.

Runtime Errors

If your program crashes, run the debugger to see where. You might be trying to perform an impossible operation, like dividing by 0 or dereferencing a nullptr.

You can add safety checks to perform operations safely.

if (denominator != 0)
	// Do stuff
if (pointer_var != nullptr)
	// Do stuff

Typos & Frustrations

The most common causes of bugs are simple typos or rushed code. Using Movie2 instead of Movie1, or writing in1 + in2 * in3 when you should use (in1 + in2) * in3. The only way to address these minor mistakes is to test your code as you write it.

Sometimes, you make more mistakes than usual. You might be tired or losing focus. The best way to find a solution is often to take a break and return with fresh eyes later. If you're getting frustrated while working on a project, there's nothing wrong with taking a break.

Memory Errors

Memory that isn't reserved by the program can be overwritten and changed at any point. Using memory outside of our program's reserved memory will likely cause a crash or give us unexpected results.

Buffer overflow

Buffer overflows happen when you're "over-reaching" the reserved memory. Accessing the 15th element of an array of size five will give you random data or crash.

int yourValue = yourArray[arrIndex];

If your problem occurs on a line like this, arrIndex is likely larger than the array's size.

Dangling pointers

Dangling pointers are pointers that point to a deallocated object.

#include <iostream>

int* example_return() {
	int foo = 5;
	return &foo;
} // `foo` is deallocated when the function ends

int main() {
	int* iptr=example_return();
	std::cout << *iptr; // Trying to get the value of a deallocated location in memory
}

iptr points to the memory that was foo before its deallocation. If nothing has overwritten the address, you might get the right value, but it could also lead to random values or crashes. Use new if you want to keep foo allocated. Remember to use delete to free the allocated memory.

Assign nullptr to pointers when deallocating the memory at their address to prevent them from becoming dangling pointers. Always check if a pointer is nullptr before using it.

#include <iostream>

int* example_return() {
    int* foo = new int(5);
    return foo;
}

int main() {
    int* iptr = example_return();
    if (iptr != nullptr) {
        std::cout << *iptr;
    }
    delete iptr; // Free the allocated memory
    iptr = nullptr; // Prevent dangling pointer
}

Memory leaks

Memory leaks occur when we reserve memory without freeing it, leading to unused reserved memory taking up system resources. Memory leaks cause programs to run out of memory and crash.

for (int i = 0; i < 10000; i++)
	int* ex = new int(5);

This code creates 10,000 int objects without deallocating them, consuming significant memory. [10000*4(size of an int) bytes]

Questions

Questions

Questions

Questions

Questions

Questions