Coming Soon

This lesson is currently being developed

Basic debugging tactics

Learn practical techniques for debugging C++ programs.

Debugging C++ Programs
Chapter
Beginner
Difficulty
45min
Estimated Time

What to Expect

Comprehensive explanations with practical examples

Interactive coding exercises to practice concepts

Knowledge quiz to test your understanding

Step-by-step guidance for beginners

Development Status

In Progress

Content is being carefully crafted to provide the best learning experience

Preview

Early Preview Content

This content is still being developed and may change before publication.

3.4 — Basic debugging tactics

In this lesson, you'll learn essential debugging tactics that every C++ programmer should master. These are practical, hands-on techniques you can use immediately to find and fix bugs in your code.

Print statement debugging

The most fundamental debugging technique is adding print statements to see what's happening in your program. While simple, it's incredibly effective and widely used.

Basic print debugging

#include <iostream>

int calculateFactorial(int n)
{
    std::cout << "DEBUG: calculateFactorial called with n = " << n << std::endl;
    
    if (n <= 1)
    {
        std::cout << "DEBUG: Base case reached, returning 1" << std::endl;
        return 1;
    }
    
    int result = n * calculateFactorial(n - 1);
    std::cout << "DEBUG: n = " << n << ", result = " << result << std::endl;
    
    return result;
}

int main()
{
    int value = 5;
    std::cout << "Calculating factorial of " << value << std::endl;
    
    int factorial = calculateFactorial(value);
    
    std::cout << "Final result: " << factorial << std::endl;
    
    return 0;
}

Output:

Calculating factorial of 5
DEBUG: calculateFactorial called with n = 5
DEBUG: calculateFactorial called with n = 4
DEBUG: calculateFactorial called with n = 3
DEBUG: calculateFactorial called with n = 2
DEBUG: calculateFactorial called with n = 1
DEBUG: Base case reached, returning 1
DEBUG: n = 2, result = 2
DEBUG: n = 3, result = 6
DEBUG: n = 4, result = 24
DEBUG: n = 5, result = 120
Final result: 120

Strategic print placement

#include <iostream>

// Bug: This function should find the average of positive numbers only
double calculateAverageOfPositives(int arr[], int size)
{
    std::cout << "DEBUG: Starting calculateAverageOfPositives with size = " << size << std::endl;
    
    int sum = 0;
    int count = 0;
    
    for (int i = 0; i < size; ++i)
    {
        std::cout << "DEBUG: Checking arr[" << i << "] = " << arr[i] << std::endl;
        
        if (arr[i] > 0)
        {
            sum += arr[i];
            count++;
            std::cout << "DEBUG: Added " << arr[i] << " to sum. New sum = " << sum << ", count = " << count << std::endl;
        }
        else
        {
            std::cout << "DEBUG: Skipped " << arr[i] << " (not positive)" << std::endl;
        }
    }
    
    std::cout << "DEBUG: Final sum = " << sum << ", count = " << count << std::endl;
    
    if (count == 0)
    {
        std::cout << "DEBUG: No positive numbers found, returning 0" << std::endl;
        return 0.0;
    }
    
    double average = static_cast<double>(sum) / count;
    std::cout << "DEBUG: Calculated average = " << average << std::endl;
    
    return average;
}

int main()
{
    int numbers[] = {-2, 3, -1, 4, 0, 5, -3, 2};
    int size = sizeof(numbers) / sizeof(numbers[0]);
    
    double avg = calculateAverageOfPositives(numbers, size);
    
    std::cout << "Average of positive numbers: " << avg << std::endl;
    
    return 0;
}

Output:

DEBUG: Starting calculateAverageOfPositives with size = 8
DEBUG: Checking arr[0] = -2
DEBUG: Skipped -2 (not positive)
DEBUG: Checking arr[1] = 3
DEBUG: Added 3 to sum. New sum = 3, count = 1
DEBUG: Checking arr[2] = -1
DEBUG: Skipped -1 (not positive)
DEBUG: Checking arr[3] = 4
DEBUG: Added 4 to sum. New sum = 7, count = 2
DEBUG: Checking arr[4] = 0
DEBUG: Skipped 0 (not positive)
DEBUG: Checking arr[5] = 5
DEBUG: Added 5 to sum. New sum = 12, count = 3
DEBUG: Checking arr[6] = -3
DEBUG: Skipped -3 (not positive)
DEBUG: Checking arr[7] = 2
DEBUG: Added 2 to sum. New sum = 14, count = 4
DEBUG: Final sum = 14, count = 4
DEBUG: Calculated average = 3.5
Average of positive numbers: 3.5

Conditional debug output

#include <iostream>

// Debug flag to control output
const bool DEBUG_MODE = true;

void debugPrint(const std::string& message)
{
    if (DEBUG_MODE)
    {
        std::cout << "[DEBUG] " << message << std::endl;
    }
}

bool isPrime(int number)
{
    debugPrint("isPrime called with number = " + std::to_string(number));
    
    if (number < 2)
    {
        debugPrint("Number less than 2, returning false");
        return false;
    }
    
    if (number == 2)
    {
        debugPrint("Number is 2, returning true");
        return true;
    }
    
    if (number % 2 == 0)
    {
        debugPrint("Number is even (and > 2), returning false");
        return false;
    }
    
    // Check odd divisors up to sqrt(number)
    for (int i = 3; i * i <= number; i += 2)
    {
        debugPrint("Testing divisor: " + std::to_string(i));
        
        if (number % i == 0)
        {
            debugPrint("Found divisor " + std::to_string(i) + ", returning false");
            return false;
        }
    }
    
    debugPrint("No divisors found, returning true");
    return true;
}

int main()
{
    int testNumbers[] = {17, 18, 19, 20};
    
    for (int num : testNumbers)
    {
        bool prime = isPrime(num);
        std::cout << num << " is " << (prime ? "prime" : "not prime") << std::endl;
        std::cout << "---" << std::endl;
    }
    
    return 0;
}

Code commenting and annotation

Use comments to document your debugging process and mark suspicious areas.

Marking suspicious code

#include <iostream>

int findSecondLargest(int arr[], int size)
{
    // TODO: This function has a bug - investigate edge cases
    
    if (size < 2)
    {
        // FIXME: Should this return an error value instead of 0?
        return 0;  // Not enough elements
    }
    
    int largest = arr[0];
    int secondLargest = arr[1];  // BUG: What if arr[1] > arr[0]?
    
    // INVESTIGATE: Does this loop handle all cases correctly?
    for (int i = 2; i < size; ++i)
    {
        if (arr[i] > largest)
        {
            secondLargest = largest;  // Move old largest to second
            largest = arr[i];         // Update largest
        }
        else if (arr[i] > secondLargest)
        {
            secondLargest = arr[i];   // Update second largest
        }
        // QUESTION: What if arr[i] equals largest? Should we update secondLargest?
    }
    
    return secondLargest;
}

// Fixed version with comments explaining the logic
int findSecondLargest_fixed(int arr[], int size)
{
    if (size < 2)
    {
        return -1;  // Error indicator - not enough elements
    }
    
    // Initialize properly: handle case where second element is larger than first
    int largest = (arr[0] > arr[1]) ? arr[0] : arr[1];
    int secondLargest = (arr[0] > arr[1]) ? arr[1] : arr[0];
    
    // Process remaining elements
    for (int i = 2; i < size; ++i)
    {
        if (arr[i] > largest)
        {
            secondLargest = largest;
            largest = arr[i];
        }
        else if (arr[i] > secondLargest && arr[i] != largest)
        {
            // Fixed: Only update if not equal to largest (handles duplicates)
            secondLargest = arr[i];
        }
    }
    
    return secondLargest;
}

int main()
{
    // Test cases to reveal the bug
    int test1[] = {5, 2, 8, 1, 9};        // Normal case
    int test2[] = {10, 5};                // Second element smaller
    int test3[] = {3, 15, 8, 12};         // Second element larger
    int test4[] = {7, 7, 7, 7};           // All elements the same
    int test5[] = {5};                    // Single element (edge case)
    
    std::cout << "Testing original (buggy) version:" << std::endl;
    std::cout << "Test 1 [5,2,8,1,9]: " << findSecondLargest(test1, 5) << std::endl;
    std::cout << "Test 2 [10,5]: " << findSecondLargest(test2, 2) << std::endl;
    std::cout << "Test 3 [3,15,8,12]: " << findSecondLargest(test3, 4) << std::endl;
    std::cout << "Test 4 [7,7,7,7]: " << findSecondLargest(test4, 4) << std::endl;
    std::cout << "Test 5 [5]: " << findSecondLargest(test5, 1) << std::endl;
    
    std::cout << "\nTesting fixed version:" << std::endl;
    std::cout << "Test 1 [5,2,8,1,9]: " << findSecondLargest_fixed(test1, 5) << std::endl;
    std::cout << "Test 2 [10,5]: " << findSecondLargest_fixed(test2, 2) << std::endl;
    std::cout << "Test 3 [3,15,8,12]: " << findSecondLargest_fixed(test3, 4) << std::endl;
    std::cout << "Test 4 [7,7,7,7]: " << findSecondLargest_fixed(test4, 4) << std::endl;
    std::cout << "Test 5 [5]: " << findSecondLargest_fixed(test5, 1) << std::endl;
    
    return 0;
}

Output:

Testing original (buggy) version:
Test 1 [5,2,8,1,9]: 8
Test 2 [10,5]: 5
Test 3 [3,15,8,12]: 12
Test 4 [7,7,7,7]: 7
Test 5 [5]: 0

Testing fixed version:
Test 1 [5,2,8,1,9]: 8
Test 2 [10,5]: 5
Test 3 [3,15,8,12]: 12
Test 4 [7,7,7,7]: 7
Test 5 [5]: -1

Using assertions

Assertions help catch bugs by verifying that certain conditions hold true during execution.

Basic assertions

#include <iostream>
#include <cassert>

double divide(double numerator, double denominator)
{
    // Assertion to catch division by zero during development
    assert(denominator != 0.0 && "Division by zero is not allowed");
    
    return numerator / denominator;
}

int getArrayElement(int arr[], int size, int index)
{
    // Assertions to catch array bounds errors
    assert(arr != nullptr && "Array pointer cannot be null");
    assert(index >= 0 && "Index cannot be negative");
    assert(index < size && "Index out of bounds");
    
    return arr[index];
}

int main()
{
    std::cout << "Testing assertions..." << std::endl;
    
    // Valid operations
    double result1 = divide(10.0, 2.0);
    std::cout << "10 / 2 = " << result1 << std::endl;
    
    int numbers[] = {1, 2, 3, 4, 5};
    int element = getArrayElement(numbers, 5, 2);
    std::cout << "numbers[2] = " << element << std::endl;
    
    // Uncomment these to see assertions fail:
    // double result2 = divide(10.0, 0.0);  // Assertion will fail
    // int badElement = getArrayElement(numbers, 5, 10);  // Assertion will fail
    
    return 0;
}

Output:

Testing assertions...
10 / 2 = 5
numbers[2] = 3

Custom assertion macros

#include <iostream>

// Custom debug assertion that can be easily disabled
#ifdef DEBUG
    #define DEBUG_ASSERT(condition, message) \
        if (!(condition)) { \
            std::cout << "ASSERTION FAILED: " << message << std::endl; \
            std::cout << "File: " << __FILE__ << ", Line: " << __LINE__ << std::endl; \
            abort(); \
        }
#else
    #define DEBUG_ASSERT(condition, message) // Do nothing in release builds
#endif

// Enable debug mode for this example
#define DEBUG

class BankAccount
{
private:
    double balance;
    
public:
    BankAccount(double initialBalance) : balance(initialBalance)
    {
        DEBUG_ASSERT(initialBalance >= 0, "Initial balance cannot be negative");
    }
    
    void withdraw(double amount)
    {
        DEBUG_ASSERT(amount > 0, "Withdrawal amount must be positive");
        DEBUG_ASSERT(amount <= balance, "Insufficient funds for withdrawal");
        
        balance -= amount;
        
        DEBUG_ASSERT(balance >= 0, "Balance should never be negative after withdrawal");
    }
    
    void deposit(double amount)
    {
        DEBUG_ASSERT(amount > 0, "Deposit amount must be positive");
        
        balance += amount;
    }
    
    double getBalance() const
    {
        return balance;
    }
};

int main()
{
    std::cout << "Testing BankAccount with assertions..." << std::endl;
    
    BankAccount account(100.0);
    
    std::cout << "Initial balance: $" << account.getBalance() << std::endl;
    
    account.deposit(50.0);
    std::cout << "After deposit: $" << account.getBalance() << std::endl;
    
    account.withdraw(30.0);
    std::cout << "After withdrawal: $" << account.getBalance() << std::endl;
    
    // Uncomment to trigger assertions:
    // account.withdraw(-10.0);  // Negative amount
    // account.withdraw(200.0);  // More than balance
    
    return 0;
}

Output:

Testing BankAccount with assertions...
Initial balance: $100
After deposit: $150
After withdrawal: $120

Code simplification

When debugging, sometimes it helps to temporarily simplify your code to isolate the problem.

Isolating problematic sections

#include <iostream>

// Original complex function with multiple potential issues
/*
int complexCalculation(int a, int b, int c)
{
    int result = 0;
    
    // Multiple operations that could have bugs
    if (a > 0 && b > 0 && c > 0)
    {
        result = (a * b + c * c) / (a + b - c);
    }
    else if (a < 0 || b < 0)
    {
        result = a * a + b * b - c;
    }
    else
    {
        result = a + b * c / 2;
    }
    
    return result * (result > 100 ? 2 : 1);
}
*/

// Simplified version - break down the operations
int complexCalculation_debug(int a, int b, int c)
{
    std::cout << "DEBUG: Input values: a=" << a << ", b=" << b << ", c=" << c << std::endl;
    
    int result = 0;
    
    // Simplify the conditions to understand the logic flow
    bool allPositive = (a > 0 && b > 0 && c > 0);
    bool anyNegative = (a < 0 || b < 0);
    
    std::cout << "DEBUG: allPositive=" << allPositive << ", anyNegative=" << anyNegative << std::endl;
    
    if (allPositive)
    {
        std::cout << "DEBUG: Taking positive path" << std::endl;
        
        // Break down complex calculation
        int numerator = a * b + c * c;
        int denominator = a + b - c;
        
        std::cout << "DEBUG: numerator=" << numerator << ", denominator=" << denominator << std::endl;
        
        if (denominator == 0)
        {
            std::cout << "DEBUG: Division by zero detected!" << std::endl;
            return 0;  // Handle division by zero
        }
        
        result = numerator / denominator;
    }
    else if (anyNegative)
    {
        std::cout << "DEBUG: Taking negative path" << std::endl;
        result = a * a + b * b - c;
    }
    else
    {
        std::cout << "DEBUG: Taking default path" << std::endl;
        // Simplified to avoid potential integer division issues
        result = a + (b * c) / 2;
    }
    
    std::cout << "DEBUG: Before final multiplication: result=" << result << std::endl;
    
    // Simplify final calculation
    if (result > 100)
    {
        std::cout << "DEBUG: Applying 2x multiplier" << std::endl;
        result *= 2;
    }
    
    std::cout << "DEBUG: Final result=" << result << std::endl;
    return result;
}

int main()
{
    // Test cases to isolate which path causes issues
    std::cout << "=== Test Case 1: All positive ===" << std::endl;
    int result1 = complexCalculation_debug(5, 3, 2);
    std::cout << "Result: " << result1 << std::endl;
    
    std::cout << "\n=== Test Case 2: Division by zero case ===" << std::endl;
    int result2 = complexCalculation_debug(3, 2, 5);  // denominator = 3+2-5 = 0
    std::cout << "Result: " << result2 << std::endl;
    
    std::cout << "\n=== Test Case 3: Negative values ===" << std::endl;
    int result3 = complexCalculation_debug(-2, 4, 3);
    std::cout << "Result: " << result3 << std::endl;
    
    return 0;
}

Output:

=== Test Case 1: All positive ===
DEBUG: Input values: a=5, b=3, c=2
DEBUG: allPositive=1, anyNegative=0
DEBUG: Taking positive path
DEBUG: numerator=19, denominator=6
DEBUG: Before final multiplication: result=3
DEBUG: Final result=3
Result: 3

=== Test Case 2: Division by zero case ===
DEBUG: Input values: a=3, b=2, c=5
DEBUG: allPositive=1, anyNegative=0
DEBUG: Taking positive path
DEBUG: numerator=31, denominator=0
DEBUG: Division by zero detected!
DEBUG: Before final multiplication: result=0
DEBUG: Final result=0
Result: 0

=== Test Case 3: Negative values ===
DEBUG: Input values: a=-2, b=4, c=3
DEBUG: allPositive=0, anyNegative=1
DEBUG: Taking negative path
DEBUG: Before final multiplication: result=13
DEBUG: Final result=13
Result: 13

Testing edge cases

Systematic testing of edge cases often reveals bugs that normal testing misses.

Comprehensive edge case testing

#include <iostream>
#include <string>

std::string getGrade(double percentage)
{
    if (percentage >= 97) return "A+";
    if (percentage >= 93) return "A";
    if (percentage >= 90) return "A-";
    if (percentage >= 87) return "B+";
    if (percentage >= 83) return "B";
    if (percentage >= 80) return "B-";
    if (percentage >= 77) return "C+";
    if (percentage >= 73) return "C";
    if (percentage >= 70) return "C-";
    if (percentage >= 67) return "D+";
    if (percentage >= 65) return "D";
    return "F";
}

void testGetGrade()
{
    std::cout << "=== Comprehensive Edge Case Testing ===" << std::endl;
    
    // Test boundary values
    double testCases[] = {
        // Normal cases
        100.0, 95.0, 85.0, 75.0, 65.0, 50.0,
        // Boundary cases
        97.0, 96.9,    // A+ boundary
        93.0, 92.9,    // A boundary
        90.0, 89.9,    // A- boundary
        65.0, 64.9,    // D boundary
        // Edge cases
        0.0,           // Zero
        -5.0,          // Negative
        110.0,         // Over 100%
        // Floating-point precision cases
        96.99999,
        93.00001
    };
    
    for (double testCase : testCases)
    {
        std::string grade = getGrade(testCase);
        std::cout << "Grade for " << testCase << "%: " << grade << std::endl;
    }
    
    // Test special floating-point values
    std::cout << "\n=== Special Cases ===" << std::endl;
    std::cout << "Grade for NaN: " << getGrade(0.0/0.0) << std::endl;  // This may not work as expected
}

int main()
{
    testGetGrade();
    return 0;
}

Binary search debugging

When you have a large codebase, use binary search to isolate the problematic section.

Example of binary search debugging approach

#include <iostream>

// Large function with multiple sections - one has a bug
void processLargeDataset()
{
    std::cout << "Starting large dataset processing..." << std::endl;
    
    // Section 1: Data validation (comment out to test if bug is here)
    std::cout << "Section 1: Validating data..." << std::endl;
    // ... lots of validation code ...
    std::cout << "Section 1 complete" << std::endl;
    
    // Section 2: Data transformation (comment out to test if bug is here)
    std::cout << "Section 2: Transforming data..." << std::endl;
    // ... lots of transformation code ...
    std::cout << "Section 2 complete" << std::endl;
    
    // Section 3: Data analysis (comment out to test if bug is here)
    std::cout << "Section 3: Analyzing data..." << std::endl;
    // ... lots of analysis code ...
    // BUG: Division by zero hidden somewhere in this section
    std::cout << "Section 3 complete" << std::endl;
    
    // Section 4: Report generation (comment out to test if bug is here)
    std::cout << "Section 4: Generating reports..." << std::endl;
    // ... lots of reporting code ...
    std::cout << "Section 4 complete" << std::endl;
    
    std::cout << "Processing complete!" << std::endl;
}

// Binary search approach: Test sections in halves
void binarySearchDebug()
{
    std::cout << "=== Binary Search Debugging Demo ===" << std::endl;
    
    // First, test first half (sections 1-2)
    std::cout << "Testing first half (sections 1-2):" << std::endl;
    std::cout << "Section 1: Validating data..." << std::endl;
    std::cout << "Section 1 complete" << std::endl;
    std::cout << "Section 2: Transforming data..." << std::endl;
    std::cout << "Section 2 complete" << std::endl;
    std::cout << "First half completed without issues" << std::endl;
    
    std::cout << "\nTesting second half (sections 3-4):" << std::endl;
    std::cout << "Section 3: Analyzing data..." << std::endl;
    std::cout << "Section 3 complete" << std::endl;
    std::cout << "Section 4: Generating reports..." << std::endl;
    std::cout << "Section 4 complete" << std::endl;
    std::cout << "Second half completed without issues" << std::endl;
    
    // If bug was in second half, now test sections 3 and 4 individually
    // If bug was in first half, test sections 1 and 2 individually
    // Continue until you isolate the exact problematic section
}

int main()
{
    binarySearchDebug();
    return 0;
}

Summary

These basic debugging tactics form the foundation of effective debugging:

  1. Print statement debugging: Add output to see program flow and variable values
  2. Code commenting: Mark suspicious areas and document your investigation
  3. Assertions: Catch invalid conditions during development
  4. Code simplification: Break complex operations into simpler steps
  5. Edge case testing: Test boundary conditions and unusual inputs
  6. Binary search debugging: Isolate problems in large codebases

Remember that debugging is often iterative - you may need to combine multiple tactics and repeat the process several times to find and fix a bug completely.

In the next lesson, you'll learn more advanced debugging tactics that build upon these fundamentals.

Quiz

  1. What are the advantages and disadvantages of print statement debugging?
  2. When should you use assertions in your code?
  3. How does binary search debugging help with large codebases?
  4. What types of edge cases should you always test?
  5. Why is code simplification useful during debugging?

Practice exercises

  1. Add debug output to this buggy function:

    int countVowels(const std::string& text)
    {
        int count = 0;
        for (char c : text)
        {
            if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u')
                count++;
        }
        return count;
    }
    
  2. Add assertions to this function to catch potential problems:

    double calculateMean(double values[], int count)
    {
        double sum = 0;
        for (int i = 0; i < count; ++i)
        {
            sum += values[i];
        }
        return sum / count;
    }
    
  3. Create comprehensive test cases for this function, including edge cases:

    bool isValidEmail(const std::string& email)
    {
        return email.find('@') != std::string::npos && 
               email.find('.') != std::string::npos;
    }
    
  4. Practice binary search debugging: If you have a program with multiple functions and one is causing incorrect output, describe how you would use binary search to isolate the problem.

Continue Learning

Explore other available lessons while this one is being prepared.

View Course

Explore More Courses

Discover other available courses while this lesson is being prepared.

Browse Courses

Lesson Discussion

Share your thoughts and questions

💬

No comments yet. Be the first to share your thoughts!

Sign in to join the discussion