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 Type | Tool Selection | When to Use |
|---|---|---|
| Logic Errors | Debugger | Development phase |
| Memory Errors | Valgrind/ASan | Testing phase |
| Performance Issues | Profiler | Optimization phase |
| Code Quality | Static Analysis | Code 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.