Skip to content

C++ Debugging Techniques

Overview

Debugging is an essential skill in software development, used to discover and fix errors in programs. This chapter introduces various C++ debugging techniques, including debugger usage, logging, static analysis, dynamic analysis, and more.

🔍 Debugger Basics

GDB Debugger

cpp
#include <iostream>
#include <vector>
#include <algorithm>

class DebugExample {
private:
    std::vector<int> data_;
    
public:
    void addNumbers(const std::vector<int>& numbers) {
        for (int num : numbers) {
            data_.push_back(num);
        }
    }
    
    int findMax() {
        if (data_.empty()) {
            return -1;  // Potential error: should throw exception
        }
        return *std::max_element(data_.begin(), data_.end());
    }
    
    double calculateAverage() {
        if (data_.empty()) {
            return 0.0;  // Potential error: division by zero
        }
        
        int sum = 0;
        for (int num : data_) {
            sum += num;
        }
        
        return static_cast<double>(sum) / data_.size();
    }
    
    void sortData() {
        std::sort(data_.begin(), data_.end());
    }
    
    void printData() const {
        std::cout << "Data: ";
        for (size_t i = 0; i < data_.size(); ++i) {
            std::cout << data_[i];
            if (i < data_.size() - 1) {
                std::cout << ", ";
            }
        }
        std::cout << std::endl;
    }
    
    // Intentional error example
    int buggyFunction(int index) {
        // Missing bounds check
        return data_[index];  // Potential out-of-bounds access
    }
    
    void memoryLeakExample() {
        int* ptr = new int[100];
        // Forgot to delete[] ptr; - Memory leak
        
        if (data_.size() > 50) {
            return;  // Early return, causes memory leak
        }
        
        delete[] ptr;
    }
};

// Debug example program
int main() {
    DebugExample example;
    
    // Add some data
    std::vector<int> numbers = {5, 2, 8, 1, 9};
    example.addNumbers(numbers);
    
    // Print data
    example.printData();
    
    // Calculate max and average
    std::cout << "Max value: " << example.findMax() << std::endl;
    std::cout << "Average: " << example.calculateAverage() << std::endl;
    
    // Sort and print
    example.sortData();
    example.printData();
    
    // Potentially crashing call
    try {
        int value = example.buggyFunction(10);  // Out-of-bounds access
        std::cout << "Value: " << value << std::endl;
    } catch (...) {
        std::cout << "Exception occurred" << std::endl;
    }
    
    return 0;
}

/*
GDB debugging command examples:

Compile: g++ -g -o debug_example debug_example.cpp

Debug commands:
gdb ./debug_example

(gdb) break main          # Set breakpoint at main function
(gdb) run                 # Run program
(gdb) step                # Step execution
(gdb) next                # Execute next line
(gdb) print numbers       # Print variable values
(gdb) list                # Display source code
(gdb) backtrace           # Display call stack
(gdb) info locals         # Display local variables
(gdb) watch data_         # Set watchpoint
(gdb) continue            # Continue execution
*/

Visual Studio Debugging

cpp
#include <iostream>
#include <string>
#include <map>

class StudentManager {
private:
    std::map<int, std::string> students_;
    static int next_id_;
    
public:
    int addStudent(const std::string& name) {
        int id = next_id_++;
        students_[id] = name;
        return id;
    }
    
    bool removeStudent(int id) {
        auto it = students_.find(id);
        if (it != students_.end()) {
            students_.erase(it);
            return true;
        }
        return false;
    }
    
    std::string getStudent(int id) const {
        auto it = students_.find(id);
        if (it != students_.end()) {
            return it->second;
        }
        return "";  // Potential issue: empty string vs exception
    }
    
    void listStudents() const {
        std::cout << "Student list:" << std::endl;
        for (const auto& pair : students_) {
            std::cout << "ID: " << pair.first 
                      << ", Name: " << pair.second << std::endl;
        }
    }
    
    // Debug info output
    void debugInfo() const {
        std::cout << "=== Debug Info ===" << std::endl;
        std::cout << "Total students: " << students_.size() << std::endl;
        std::cout << "Next ID: " << next_id_ << std::endl;
        
        // Memory address info
        std::cout << "Container address: " << &students_ << std::endl;
        
        // Detailed content
        for (const auto& pair : students_) {
            std::cout << "Student[" << pair.first << "] = \"" 
                      << pair.second << "\" (address: " << &pair.second << ")" << std::endl;
        }
    }
};

int StudentManager::next_id_ = 1;

/*
Visual Studio debugging tips:

1. Breakpoint types:
   - F9: Toggle breakpoint
   - Conditional breakpoint: Right-click breakpoint -> Condition
   - Data breakpoint: Break when variable value changes

2. Debug windows:
   - Locals window: Display variables in current scope
   - Watch window: Custom watch expressions
   - Call Stack: Display function call chain
   - Memory window: View raw memory contents

3. Debug shortcuts:
   - F5: Start debugging/Continue
   - F10: Step Over
   - F11: Step Into
   - Shift+F11: Step Out
   - Ctrl+F5: Run without debugging

4. Advanced features:
   - Edit and Continue: Modify code while debugging
   - Diagnostic Tools: Memory and CPU usage
   - IntelliTrace: Historical debugging
*/

📝 Logging

Simple Logging System

cpp
#include <iostream>
#include <fstream>
#include <sstream>
#include <chrono>
#include <iomanip>

enum class LogLevel {
    DEBUG = 0,
    INFO = 1,
    WARNING = 2,
    ERROR = 3,
    CRITICAL = 4
};

class Logger {
private:
    LogLevel min_level_;
    std::ofstream file_stream_;
    bool console_output_;
    
    std::string levelToString(LogLevel level) {
        switch (level) {
            case LogLevel::DEBUG: return "DEBUG";
            case LogLevel::INFO: return "INFO";
            case LogLevel::WARNING: return "WARNING";
            case LogLevel::ERROR: return "ERROR";
            case LogLevel::CRITICAL: return "CRITICAL";
            default: return "UNKNOWN";
        }
    }
    
    std::string getCurrentTime() {
        auto now = std::chrono::system_clock::now();
        auto time_t = std::chrono::system_clock::to_time_t(now);
        auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
            now.time_since_epoch()) % 1000;
        
        std::stringstream ss;
        ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H:%M:%S");
        ss << '.' << std::setfill('0') << std::setw(3) << ms.count();
        return ss.str();
    }
    
public:
    Logger(LogLevel min_level = LogLevel::INFO, 
           const std::string& filename = "", 
           bool console = true) 
        : min_level_(min_level), console_output_(console) {
        if (!filename.empty()) {
            file_stream_.open(filename, std::ios::app);
        }
    }
    
    ~Logger() {
        if (file_stream_.is_open()) {
            file_stream_.close();
        }
    }
    
    template<typename... Args>
    void log(LogLevel level, const std::string& format, Args... args) {
        if (level < min_level_) {
            return;
        }
        
        std::stringstream ss;
        ss << "[" << getCurrentTime() << "] "
           << "[" << levelToString(level) << "] ";
        
        // Simple formatting (should use better formatting library in actual projects)
        formatString(ss, format, args...);
        
        std::string message = ss.str() + "\n";
        
        if (console_output_) {
            std::cout << message;
        }
        
        if (file_stream_.is_open()) {
            file_stream_ << message;
            file_stream_.flush();
        }
    }
    
private:
    void formatString(std::stringstream& ss, const std::string& format) {
        ss << format;
    }
    
    template<typename T, typename... Args>
    void formatString(std::stringstream& ss, const std::string& format, T&& t, Args... args) {
        size_t pos = format.find("{}");
        if (pos != std::string::npos) {
            ss << format.substr(0, pos) << t;
            formatString(ss, format.substr(pos + 2), args...);
        } else {
            ss << format;
        }
    }
};

// Global logger instance
Logger g_logger(LogLevel::DEBUG, "app.log", true);

// Convenience macros
#define LOG_DEBUG(...) g_logger.log(LogLevel::DEBUG, __VA_ARGS__)
#define LOG_INFO(...) g_logger.log(LogLevel::INFO, __VA_ARGS__)
#define LOG_WARNING(...) g_logger.log(LogLevel::WARNING, __VA_ARGS__)
#define LOG_ERROR(...) g_logger.log(LogLevel::ERROR, __VA_ARGS__)
#define LOG_CRITICAL(...) g_logger.log(LogLevel::CRITICAL, __VA_ARGS__)

// Usage example
void demonstrateLogging() {
    LOG_INFO("Program started");
    
    int user_id = 12345;
    std::string username = "alice";
    
    LOG_DEBUG("User login attempt: ID={}, Username={}", user_id, username);
    
    bool login_success = true;
    if (login_success) {
        LOG_INFO("User {} (ID: {}) logged in successfully", username, user_id);
    } else {
        LOG_WARNING("User {} login failed", username);
    }
    
    // Simulate some operations
    for (int i = 0; i < 5; ++i) {
        LOG_DEBUG("Processing operation {}", i + 1);
        
        if (i == 3) {
            LOG_WARNING("Operation {} requires extra time", i + 1);
        }
    }
    
    // Error case
    try {
        throw std::runtime_error("Simulated error");
    } catch (const std::exception& e) {
        LOG_ERROR("Caught exception: {}", e.what());
    }
    
    LOG_INFO("Program ended");
}

Assertions and Conditional Debugging

cpp
#include <cassert>
#include <iostream>

// Custom assertion macro
#ifdef DEBUG
    #define ASSERT(condition, message) \
        do { \
            if (!(condition)) { \
                std::cerr << "Assertion failed: " << #condition \
                          << " File: " << __FILE__ \
                          << " Line: " << __LINE__ \
                          << " Message: " << message << std::endl; \
                abort(); \
            } \
        } while(0)
    
    #define DEBUG_PRINT(x) std::cout << "DEBUG: " << x << std::endl
    
    #define DEBUG_CODE(code) do { code } while(0)
#else
    #define ASSERT(condition, message) do { } while(0)
    #define DEBUG_PRINT(x) do { } while(0)
    #define DEBUG_CODE(code) do { } while(0)
#endif

// Debug helper class
class DebugHelper {
private:
    static int allocation_count_;
    static int deallocation_count_;
    
public:
    static void* debug_malloc(size_t size, const char* file, int line) {
        void* ptr = malloc(size);
        allocation_count_++;
        DEBUG_PRINT("Allocated memory: " << size << " bytes, address: " << ptr 
                   << " (" << file << ":" << line << ")");
        return ptr;
    }
    
    static void debug_free(void* ptr, const char* file, int line) {
        if (ptr) {
            deallocation_count_++;
            DEBUG_PRINT("Freed memory: address: " << ptr 
                       << " (" << file << ":" << line << ")");
            free(ptr);
        }
    }
    
    static void printMemoryStats() {
        std::cout << "Memory statistics:" << std::endl;
        std::cout << "  Allocations: " << allocation_count_ << std::endl;
        std::cout << "  Deallocations: " << deallocation_count_ << std::endl;
        std::cout << "  Leaks: " << (allocation_count_ - deallocation_count_) << std::endl;
    }
};

int DebugHelper::allocation_count_ = 0;
int DebugHelper::deallocation_count_ = 0;

#ifdef DEBUG
    #define DEBUG_MALLOC(size) DebugHelper::debug_malloc(size, __FILE__, __LINE__)
    #define DEBUG_FREE(ptr) DebugHelper::debug_free(ptr, __FILE__, __LINE__)
#else
    #define DEBUG_MALLOC(size) malloc(size)
    #define DEBUG_FREE(ptr) free(ptr)
#endif

// Usage example
class Vector3D {
private:
    double x_, y_, z_;
    
public:
    Vector3D(double x = 0, double y = 0, double z = 0) : x_(x), y_(y), z_(z) {
        DEBUG_PRINT("Vector3D constructor: (" << x_ << ", " << y_ << ", " << z_ << ")");
    }
    
    double magnitude() const {
        double mag = sqrt(x_ * x_ + y_ * y_ + z_ * z_);
        ASSERT(mag >= 0, "Vector length cannot be negative");
        return mag;
    }
    
    Vector3D normalize() const {
        double mag = magnitude();
        ASSERT(mag > 0, "Cannot normalize zero vector");
        
        DEBUG_CODE({
            double old_mag = mag;
            Vector3D result(x_ / mag, y_ / mag, z_ / mag);
            double new_mag = result.magnitude();
            ASSERT(abs(new_mag - 1.0) < 1e-10, "Normalized vector length should be 1");
        });
        
        return Vector3D(x_ / mag, y_ / mag, z_ / mag);
    }
    
    void print() const {
        std::cout << "(" << x_ << ", " << y_ << ", " << z_ << ")" << std::endl;
    }
};

void demonstrateDebugging() {
    LOG_INFO("Starting debug demo");
    
    // Memory debugging
    void* ptr1 = DEBUG_MALLOC(100);
    void* ptr2 = DEBUG_MALLOC(200);
    
    DEBUG_FREE(ptr1);
    // Intentionally don't free ptr2 to demonstrate memory leak detection
    
    // Vector operation debugging
    Vector3D v1(3, 4, 0);
    v1.print();
    
    DEBUG_PRINT("Vector length: " << v1.magnitude());
    
    Vector3D normalized = v1.normalize();
    normalized.print();
    
    // Attempt to normalize zero vector (should trigger assertion)
    DEBUG_CODE({
        try {
            Vector3D zero(0, 0, 0);
            // Vector3D norm_zero = zero.normalize();  // This would trigger assertion
        } catch (...) {
            LOG_ERROR("Caught exception");
        }
    });
    
    DebugHelper::printMemoryStats();
    
    LOG_INFO("Debug demo ended");
}

🔧 Static Analysis Tools

Code Quality Checking

cpp
// Examples of problems static analysis tools can detect

#include <iostream>
#include <vector>
#include <memory>

class ProblematicCode {
public:
    // 1. Memory leak
    void memoryLeak() {
        int* ptr = new int(42);
        // Forgot to delete ptr; - static analyzer will warn
    }
    
    // 2. Uninitialized variable
    int useUninitializedVariable() {
        int x;  // Uninitialized
        return x * 2;  // Use uninitialized variable
    }
    
    // 3. Array bounds
    void arrayBounds() {
        int arr[5] = {1, 2, 3, 4, 5};
        int value = arr[10];  // Out-of-bounds access
        std::cout << value << std::endl;
    }
    
    // 4. Null pointer dereference
    void nullPointerDereference() {
        int* ptr = nullptr;
        *ptr = 42;  // Null pointer dereference
    }
    
    // 5. Resource management issues
    void resourceManagement() {
        FILE* file = fopen("test.txt", "r");
        if (file) {
            // Read file
            char buffer[100];
            fread(buffer, 1, 100, file);
            // Forgot to fclose(file) on some paths
            if (buffer[0] == 'A') {
                return;  // Resource leak
            }
            fclose(file);
        }
    }
    
    // 6. Dead code
    int deadCode() {
        return 42;
        std::cout << "This line never executes" << std::endl;  // Dead code
    }
    
    // 7. Type conversion issues
    void typeConversion() {
        double d = 3.14159;
        int i = d;  // Implicit type conversion, may lose precision
        
        void* ptr = malloc(100);
        int* int_ptr = (int*)ptr;  // C-style cast, unsafe
        // Should use: int* int_ptr = static_cast<int*>(ptr);
    }
};

// Improved code
class ImprovedCode {
public:
    // 1. Use smart pointers to avoid memory leaks
    void properMemoryManagement() {
        auto ptr = std::make_unique<int>(42);
        // Automatic memory deallocation
    }
    
    // 2. Initialize variables
    int useInitializedVariable() {
        int x = 0;  // Explicit initialization
        return x * 2;
    }
    
    // 3. Use containers and bounds checking
    void safeBounds() {
        std::vector<int> vec = {1, 2, 3, 4, 5};
        try {
            int value = vec.at(10);  // Safe bounds checking
            std::cout << value << std::endl;
        } catch (const std::out_of_range& e) {
            std::cout << "Out of bounds access: " << e.what() << std::endl;
        }
    }
    
    // 4. Check pointer validity
    void safePointerUse() {
        int* ptr = nullptr;
        if (ptr != nullptr) {
            *ptr = 42;
        } else {
            std::cout << "Pointer is null, skipping operation" << std::endl;
        }
    }
    
    // 5. RAII resource management
    void properResourceManagement() {
        std::ifstream file("test.txt");
        if (file.is_open()) {
            std::string line;
            std::getline(file, line);
            // File automatically closed on destruction
        }
    }
    
    // 6. Explicit type conversion
    void explicitTypeConversion() {
        double d = 3.14159;
        int i = static_cast<int>(d);  // Explicit type conversion
        
        void* ptr = malloc(100);
        int* int_ptr = static_cast<int*>(ptr);  // C++ style cast
        free(ptr);
    }
};

/*
Common static analysis tools:

1. Clang Static Analyzer
   clang --analyze source.cpp

2. Cppcheck
   cppcheck --enable=all source.cpp

3. PVS-Studio (Commercial)
   pvs-studio-analyzer analyze

4. PC-lint Plus (Commercial)
   pclp64 source.cpp

5. Visual Studio Code Analysis
   /analyze compilation option

Static analysis configuration example:
.clang-tidy:
Checks: '-*,readability-*,performance-*,bugprone-*'
WarningsAsErrors: 'readability-*'
*/

🚀 Dynamic Analysis Tools

Valgrind and AddressSanitizer

cpp
#include <iostream>
#include <vector>
#include <memory>
#include <cstring>

class MemoryAnalysisExample {
public:
    // Memory leak example
    void memoryLeakExample() {
        int* leaked_memory = new int[100];
        // Intentionally don't free - Valgrind will detect
        std::cout << "Allocated memory but not freed" << std::endl;
    }
    
    // Buffer overflow example
    void bufferOverflowExample() {
        char buffer[10];
        strcpy(buffer, "This is a very long string that will cause buffer overflow");
        // AddressSanitizer will detect this error
        std::cout << buffer << std::endl;
    }
    
    // Use freed memory
    void useAfterFreeExample() {
        int* ptr = new int(42);
        delete ptr;
        std::cout << *ptr << std::endl;  // Use freed memory
    }
    
    // Array bounds access
    void arrayBoundsExample() {
        std::vector<int> vec(10, 0);
        vec[15] = 42;  // Out-of-bounds access - may not crash immediately, but tools will detect
    }
    
    // Correct memory management example
    void correctMemoryManagement() {
        // Use smart pointers
        auto smart_ptr = std::make_unique<int[]>(100);
        
        // Use standard containers
        std::vector<int> safe_vector(100);
        
        // Safe string operations
        std::string safe_string = "This is safe string operation";
        
        std::cout << "Safe memory operations complete" << std::endl;
    }
};

/*
Valgrind usage:

Compile (no optimization):
g++ -g -O0 -o memory_test memory_test.cpp

Run Valgrind:
valgrind --tool=memcheck --leak-check=full --track-origins=yes ./memory_test

AddressSanitizer usage:

Compile:
g++ -g -fsanitize=address -fno-omit-frame-pointer -o asan_test memory_test.cpp

Run:
./asan_test

Other useful tools:
1. ThreadSanitizer (detect thread errors): -fsanitize=thread
2. UndefinedBehaviorSanitizer: -fsanitize=undefined
3. MemorySanitizer (detect uninitialized memory): -fsanitize=memory
*/

int main() {
    std::cout << "=== C++ Debugging Techniques Demo ===" << std::endl;
    
    // Logging demo
    demonstrateLogging();
    
    // Debug code demo
    demonstrateDebugging();
    
    // Memory analysis example
    MemoryAnalysisExample example;
    example.correctMemoryManagement();
    
    // Note: The following code will cause errors, only for demonstrating tool detection capabilities
    // example.memoryLeakExample();
    // example.bufferOverflowExample();
    
    return 0;
}

Summary

Debugger Categories

  • Debuggers: GDB, Visual Studio Debugger
  • Static Analysis: Clang Static Analyzer, Cppcheck
  • Dynamic Analysis: Valgrind, AddressSanitizer
  • Logging Systems: Custom Logger, spdlog

Debugging Strategies

Problem TypeTool SelectionWhen to Use
Logic ErrorsDebuggerDevelopment phase
Memory ErrorsValgrind/ASanTesting phase
Performance IssuesProfilerOptimization phase
Code QualityStatic AnalysisCode review

Best Practices

  • Prevention First: Write defensive code
  • Tool Combination: Use multiple tools together
  • Continuous Integration: Integrate analysis tools into CI/CD
  • Logging: Appropriate log levels and formats
  • Code Review: Combine manual review and tool analysis

Debugging techniques are essential skills for C++ developers. Mastering various debugging tools and techniques can significantly improve development efficiency and code quality.

Content is for learning and research only.