Skip to content

Debugging C++ Code

Debugging is an essential skill for any programmer. It involves identifying and fixing errors or bugs in your code. This guide will introduce you to common debugging techniques, tools, and best practices for debugging C++ code.

Here are common types of errors you might encounter while programming:

  1. Syntax Errors: Mistakes in the code that violate the rules of the C++ language. These are usually caught by the compiler.
  2. Runtime Errors: Errors that occur while the program is running, such as division by zero or accessing invalid memory.
  3. Logical Errors: Errors in the logic of the program that produce incorrect results. These are often the hardest to identify and fix.

In the following section, let’s look at some common debugging techniques and tools you can use to debug your C++ code effectively.

One of the simplest ways to debug your code is by adding print statements to check the values of variables at different points in your program.

In small practice programs, it’s common to print debug info with std::cout. In larger programs (or when you’re writing code that will be graded/tested), it’s often better to send debug/error messages to std::cerr so you don’t mix them with your program’s “real” output.

debug_cout.cpp
#include <iostream>
int main() {
// Suppose we want the average of 1..N.
int N = 5;
int sum = 0;
for (int i = 1; i < N; i++) { // BUG: should be i <= N
sum += i;
std::cout << "i=" << i << ", sum=" << sum << std::endl;
}
double avg = static_cast<double>(sum) / N;
std::cout << "N=" << N << ", sum=" << sum << ", avg=" << avg << std::endl;
// Expected avg for N=5 is 3.0, but this prints 2.0.
// The print statements reveal that the loop stops at i=4.
return 0;
}

This is especially useful for logic errors (your program runs, but the output is wrong). Print intermediate values and compare them to what you expect.

If your program is crashing (or you’re printing debug info that you don’t want mixed in with the “real” output), prefer std::cerr.

  • std::cout is for normal program output.
  • std::cerr is for error/debug output.

Also, when a program crashes, some output may still be sitting in a buffer and never make it to the terminal. Using std::cerr and flushing (with std::endl or std::flush) makes it more likely you’ll see your last debug message.

debug_cerr.cpp
#include <iostream>
int main() {
int a = 5;
int b = 0;
std::cerr << "About to divide: a=" << a << ", b=" << b << std::endl; // flushes
// Guard against a crash, but keep the debug print above.
if (b == 0) {
std::cerr << "Error: division by zero" << std::endl;
return 1;
}
int c = a / b;
std::cout << "c: " << c << std::endl;
return 0;
}

Use it when something unexpected happens in your program, and you want to see the values of variables at that point. Don’t forget to remove these print statements once you’ve identified and fixed the bug. You could add a comment where you put the print statement to remind yourself to remove it later.

A debugger is a powerful tool that allows you to step through your code, inspect variables, and control the execution flow. Common debuggers for C++ include GDB (GNU Debugger) and the built-in debugger in IDEs like Visual Studio Code.

Here’s an example using GDB (available on the ENGR servers).

First compile your code with the -g flag to include debugging information:

Terminal window
g++ -g -o run debug_cerr.cpp

Then run GDB with your compiled program:

Terminal window
gdb ./run

This will open an interactive GDB session.

Finally, set breakpoints, run the program, and inspect variables:

Terminal window
(gdb) break main
(gdb) run
(gdb) print a
(gdb) next

To exit GDB, use the quit command:

Terminal window
(gdb) quit

On macOS, you want to use lldb instead of gdb, as gdb is not natively supported. The commands are similar.

A core dump is a file that captures the memory of a program at a specific point in time, usually when the program crashes. You can analyze core dumps using GDB to understand what caused the crash.

First, enable core dumps:

Terminal window
ulimit -c unlimited

Then run your program. If it crashes, it will create a core file that you can analyze with:

Terminal window
gdb ./run core

You will not be able to see core dumps on the ENGR servers, but you can use this technique on your local machine.

Static code analysis tools analyze your code without executing it. They can help identify potential issues such as memory leaks, buffer overflows, and other vulnerabilities. Common tools include cppcheck and Clang Static Analyzer, but VS Code extensions can also provide insights.

On the ENGR servers, clang is installed and so is its static analyzer. You can run:

Terminal window
clang++ --analyze -Xanalyzer -analyzer-output=text debug_cerr.cpp

Alternatively, you can run scan-build (which comes with clang):

Terminal window
scan-build g++ debug_cerr.cpp -o run

This latest command will create an HTML report, which is less practical in the terminal.

For those using VS Code, extensions such as C/C++ and C++ Intellisense can provide real-time feedback on your code. You will see squiggly lines under potential issues, and you can hover over them to see more details.

  1. Understand the Problem: Before you start debugging, make sure you understand the problem and can reproduce it consistently.
  2. Isolate the Issue: Try to isolate the part of the code that is causing the problem. This can make it easier to identify the root cause.
  3. Use Version Control: Use version control systems like Git to keep track of changes in your code. This allows you to revert to a previous state if needed.
  4. Write Test Cases: Writing test cases can help you catch bugs early and ensure that your code behaves as expected.
  5. Stay Calm and Patient: Debugging can be frustrating, but staying calm and patient will help you think more clearly and find solutions more effectively.