C++ Debugging & Logging

Finding and fixing errors in your code

๐Ÿ› What is Debugging & Logging?

Debugging helps find and fix errors in code. Logging records program behavior and events. Both are essential for developing reliable C++ applications.


#include <iostream>
using namespace std;

int main() {
    cout << "Debug: Starting program" << endl;
    int x = 10, y = 0;
    cout << "Debug: x=" << x << ", y=" << y << endl;
    return 0;
}
                                    

Debugging Techniques

๐Ÿ–จ๏ธ

Print Debugging

Use cout to track values

cout << "Debug: x = " << x << endl;
โœ…

Assertions

Check conditions during runtime

#include <cassert>
assert(x > 0);
๐Ÿ“

Logging

Record program events

ofstream log("app.log");
log << "Event occurred" << endl;
๐Ÿ”

Debugger Tools

Use IDE debugging features

// Set breakpoints
// Step through code
// Watch variables

๐Ÿ”น Print Debugging

Inserting print statements (std::cout) traces execution flow and variable states. The factorial example shows recursive calls unfolding, revealing the programโ€™s dynamic behavior. While primitive, print debugging is quick and effective for understanding runtime logic, especially when interactive debuggers are unavailable.

#include <iostream>
using namespace std;

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

int main() {
    cout << "DEBUG: Program started" << endl;
    
    int number = 5;
    cout << "DEBUG: Calculating factorial of " << number << endl;
    
    int result = factorial(number);
    
    cout << "DEBUG: Final result = " << result << endl;
    cout << "DEBUG: Program ended" << endl;
    
    return 0;
}

Output:

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

๐Ÿ”น Using Assertions

Assertions assert in C++ validate critical assumptions during development, immediately halting execution when conditions fail to signal bugs early. They check essential preconditions like ensuring divisors are non-zero or array indices remain within bounds. While assertions are active in debug builds to catch errors during development, they're typically disabled in release builds for performance. This makes assertions a powerful complement to robust error handling, helping developers identify logical errors and unexpected states that might otherwise cause silent failures or incorrect results in production environments.

#include <iostream>
#include <cassert>
using namespace std;

double divide(double a, double b) {
    // Assert that b is not zero
    assert(b != 0 && "Division by zero!");
    
    cout << "Dividing " << a << " by " << b << endl;
    return a / b;
}

int arrayAccess(int arr[], int size, int index) {
    // Assert valid array bounds
    assert(index >= 0 && "Index cannot be negative!");
    assert(index < size && "Index out of bounds!");
    
    cout << "Accessing array[" << index << "]" << endl;
    return arr[index];
}

int main() {
    // This will work fine
    cout << "Result: " << divide(10, 2) << endl;
    
    int numbers[] = {1, 2, 3, 4, 5};
    cout << "Value: " << arrayAccess(numbers, 5, 2) << endl;
    
    // Uncomment to see assertion failure
    // cout << divide(10, 0) << endl;  // This will trigger assertion
    
    return 0;
}

Output:

Dividing 10 by 2
Result: 5
Accessing array[2]
Value: 3

๐Ÿ”น Simple Logging System

A custom logger timestamps messages with severity levels (INFO, WARNING). This structures output for monitoring and debugging. Logs persist across sessions, providing a history of application behavior, which is invaluable for diagnosing issues in deployment environments.

#include <iostream>
#include <fstream>
#include <ctime>
#include <string>
using namespace std;

class Logger {
private:
    ofstream logFile;
    
    string getCurrentTime() {
        time_t now = time(0);
        char* timeStr = ctime(&now);
        string result(timeStr);
        result.pop_back(); // Remove newline
        return result;
    }
    
public:
    Logger(const string& filename) {
        logFile.open(filename, ios::app);
    }
    
    ~Logger() {
        if (logFile.is_open()) {
            logFile.close();
        }
    }
    
    void info(const string& message) {
        log("INFO", message);
    }
    
    void warning(const string& message) {
        log("WARNING", message);
    }
    
    void error(const string& message) {
        log("ERROR", message);
    }
    
private:
    void log(const string& level, const string& message) {
        string logEntry = "[" + getCurrentTime() + "] " + level + ": " + message;
        
        // Write to file
        if (logFile.is_open()) {
            logFile << logEntry << endl;
        }
        
        // Also print to console
        cout << logEntry << endl;
    }
};

int main() {
    Logger logger("app.log");
    
    logger.info("Application started");
    
    int x = 10, y = 5;
    logger.info("Variables initialized: x=" + to_string(x) + ", y=" + to_string(y));
    
    if (y == 0) {
        logger.error("Division by zero attempted!");
        return 1;
    }
    
    int result = x / y;
    logger.info("Division result: " + to_string(result));
    
    logger.warning("This is a warning message");
    logger.info("Application ended successfully");
    
    return 0;
}

Output:

[Mon Jan 15 14:30:25 2024] INFO: Application started
[Mon Jan 15 14:30:25 2024] INFO: Variables initialized: x=10, y=5
[Mon Jan 15 14:30:25 2024] INFO: Division result: 2
[Mon Jan 15 14:30:25 2024] WARNING: This is a warning message
[Mon Jan 15 14:30:25 2024] INFO: Application ended successfully

๐Ÿ”น Debugging Best Practices

Effective debugging combines tools: debuggers for inspection, assertions for invariants, and logs for history. Isolating issues via unit tests, reproducing failures, and understanding program state are key. Methodical approaches reduce time spent fixing bugs and improve overall code quality.

Debugging Strategies:

  • Start small - Test small parts of code first
  • Use meaningful variable names - Makes debugging easier
  • Add debug prints strategically - At function entry/exit points
  • Check boundary conditions - Test edge cases
  • Use version control - Track changes that introduce bugs

Common Debugging Tools:

  • GDB - GNU Debugger for command line
  • Visual Studio Debugger - Integrated debugging
  • Code::Blocks Debugger - IDE with debugging support
  • Valgrind - Memory error detection
  • Static analyzers - Find issues without running code

Log Levels:

  • DEBUG - Detailed information for debugging
  • INFO - General information about program flow
  • WARNING - Something unexpected but not critical
  • ERROR - Error conditions that need attention
  • FATAL - Critical errors that stop the program

๐Ÿง  Test Your Knowledge

Which header file is needed for assertions in C++?