C++ Notes: Practical Techniques for Memory Leak Detection

Master the skills of identifying, troubleshooting, and preventing C++ memory leaks, learn to use practical tools to quickly locate issues, and establish good memory management habits.

🎯 Use Cases

  • Memory Leak Detection: Continuous memory growth during program execution
  • Code Review: Preventive checks for potential memory issues
  • Performance Optimization: Addressing excessive memory usage issues
  • Tool Usage: Mastering the basic operations of memory detection tools
  • Debugging Techniques: Quickly locating and fixing memory leaks
  • Best Practices: Establishing memory-safe programming habits

📚 Basics of Memory Leaks

What is a Memory Leak

// Typical memory leak example
void memory_leak_example() {
    int* ptr = new int(42);    // Allocate memory
    // Forget to delete ptr;        // Memory leak!
    
    std::vector* vec = new std::vector(1000);
    // Forget to delete vec;        // Container memory leak!
}

Hazards of Memory Leaks

  • Memory Exhaustion: Long-running programs lead to insufficient system memory
  • Performance Degradation: Frequent memory allocations cause fragmentation
  • Program Crashes: Severe cases lead to resource exhaustion
  • Server Stability: Affects long-running service programs

🔍 Common Memory Leak Patterns

1. Forgetting to Release Dynamically Allocated Memory

// ❌ Incorrect example
class BadExample {
private:
    int* data_;
    
public:
    BadExample() : data_(new int[100]) {}
    
    // No destructor! Memory leak
    // ~BadExample() { delete[] data_; }  // This line should be here
};

// ✅ Correct example
class GoodExample {
private:
    std::unique_ptr data_;  // Automatically manages memory
    
public:
    GoodExample() : data_(std::make_unique(100)) {}
    // No need for manual destructor, smart pointer handles it automatically
};

2. Exception Safety Issues

// ❌ Exception-unsafe code
void unsafe_function() {
    int* ptr = new int(42);
    
    risky_operation();      // If an exception is thrown here
    
    delete ptr;            // This line will never execute!
}

// ✅ Exception-safe code
void safe_function() {
    auto ptr = std::make_unique(42);  // RAII automatically manages
    
    risky_operation();      // Even if an exception is thrown, ptr will be automatically cleaned up
}

3. Circular Reference Issues

// ❌ Circular reference with smart pointers
class Node {
public:
    std::shared_ptr next;
    std::shared_ptr prev;    // Circular reference!
};

void create_cycle() {
    auto node1 = std::make_shared();
    auto node2 = std::make_shared();
    
    node1->next = node2;
    node2->prev = node1;    // Circular reference, memory leak!
}

// ✅ Break the cycle using weak_ptr
class SafeNode {
public:
    std::shared_ptr next;
    std::weak_ptr prev;      // Use weak_ptr
};

4. Pointers in Containers Not Released

// ❌ Container storing raw pointers
std::vector pointers;

void fill_vector() {
    for (int i = 0; i < 100; ++i) {
        pointers.push_back(new int(i));  // Allocate memory
    }
    // When the container is destroyed, only the vector itself is released, not the memory pointed to by the pointers!
}

// ✅ Use smart pointer containers
std::vector<std::unique_ptr> smart_pointers;

void fill_vector_safe() {
    for (int i = 0; i < 100; ++i) {
        smart_pointers.push_back(std::make_unique(i));
    }
    // When the container is destroyed, all unique_ptr automatically release memory
}</std::unique_ptr

5. Forgetting to Pair Allocations and Deallocations

// ❌ Mismatched allocation/deallocation
void mismatched_allocation() {
    int* single = new int(42);
    int* array = new int[100];
    
    delete[] single;    // Error! Should use delete
    delete array;       // Error! Should use delete[]
}

// ✅ Correct pairing
void correct_allocation() {
    int* single = new int(42);
    int* array = new int[100];
    
    delete single;      // new corresponds to delete
    delete[] array;     // new[] corresponds to delete[]
}

🔧 Memory Leak Detection Tools

1. Valgrind (Linux/macOS)

The most powerful memory detection tool

Basic Usage

# Add debug information during compilation
g++ -g -O0 program.cpp -o program

# Use Valgrind for detection
valgrind --tool=memcheck --leak-check=full ./program

Output Interpretation

==12345== HEAP SUMMARY:
==12345==     in use at exit: 400 bytes in 1 blocks
==12345==   total heap usage: 2 allocs, 1 frees, 404 bytes allocated
==12345== 
==12345== 400 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345==    at 0x4C2E0EF: operator new(unsigned long) (in vgpreload_memcheck)
==12345==    by 0x108654: main (program.cpp:10)

Key Information Interpretation:

  • <span>400 bytes in 1 blocks are definitely lost</span>: Memory leak confirmed
  • <span>at 0x108654: main (program.cpp:10)</span>: Leak occurred at line 10 of program.cpp

2. AddressSanitizer (GCC/Clang)

Built-in detection tool in modern compilers

Usage

# Enable AddressSanitizer during compilation
g++ -fsanitize=address -g program.cpp -o program

# Run the program
./program

Practical Usage Example

// leak_test.cpp
#include 

int main() {
    int* leak = new int(42);  // Intentionally leaking
    // delete leak;           // Commented out release code
    
    return 0;
}
# Compile and run
g++ -fsanitize=address -g leak_test.cpp -o leak_test
./leak_test

# Output similar to:
# =================================================================
# ==ERROR: LeakSanitizer: detected memory leaks
# 
# Direct leak of 4 byte(s) in 1 object(s) allocated from:
#     #0 0x7f8b8c9b7602 in operator new(unsigned long)
#     #1 0x55a8b0c01149 in main leak_test.cpp:4

3. Visual Studio Diagnostic Tools (Windows)

Memory detection on Windows platform

Enable CRT Debug Heap

#ifdef _DEBUG
#define _CRTDBG_MAP_ALLOC
#include 
#endif

int main() {
#ifdef _DEBUG
    _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
#endif
    
    // Your program code
    int* leak = new int(42);
    
    return 0;
}

4. Custom Simple Memory Tracker

#include 
#include <map>
#include 

class MemoryTracker {
private:
    static std::map allocations_;
    static std::mutex mutex_;
    static size_t total_allocated_;

public:
    static void record_allocation(void* ptr, size_t size) {
        std::lock_guard lock(mutex_);
        allocations_[ptr] = size;
        total_allocated_ += size;
        std::cout << "Allocated " << size << " bytes at " << ptr 
                  << " (Total: " << total_allocated_ << ")" << std::endl;
    }
    
    static void record_deallocation(void* ptr) {
        std::lock_guard lock(mutex_);
        auto it = allocations_.find(ptr);
        if (it != allocations_.end()) {
            total_allocated_ -= it->second;
            std::cout << "Freed " << it->second << " bytes at " << ptr 
                      << " (Total: " << total_allocated_ << ")" << std::endl;
            allocations_.erase(it);
        }
    }
    
    static void print_leaks() {
        std::lock_guard lock(mutex_);
        if (!allocations_.empty()) {
            std::cout << "Memory leaks detected:" << std::endl;
            for (const auto& pair : allocations_) {
                std::cout << "  " << pair.second << " bytes at "
                          << pair.first << std::endl;
            }
        } else {
            std::cout << "No memory leaks detected!" << std::endl;
        }
    }
};

// Static member definition
std::map MemoryTracker::allocations_;
std::mutex MemoryTracker::mutex_;
size_t MemoryTracker::total_allocated_ = 0;

// Overload global new/delete (for debugging only)
#ifdef DEBUG_MEMORY
void* operator new(size_t size) {
    void* ptr = malloc(size);
    MemoryTracker::record_allocation(ptr, size);
    return ptr;
}

void operator delete(void* ptr) noexcept {
    MemoryTracker::record_deallocation(ptr);
    free(ptr);
}
#endif</map>

🛠️ Practical Troubleshooting Techniques

1. Segmented Testing Method

// Comment out segments of the program to gradually locate the leak
int main() {
    // First segment of code
    // function1();
    
    // Second segment of code  
    // function2();
    
    // Third segment of code
    function3();  // If only this segment has a leak, the problem is in function3
    
    return 0;
}

2. RAII Wrapper

// Create RAII wrappers for legacy code
template
class RAIIWrapper {
private:
    T* ptr_;
    
public:
    explicit RAIIWrapper(T* p) : ptr_(p) {}
    ~RAIIWrapper() { delete ptr_; }
    
    T* get() const { return ptr_; }
    T& operator*() const { return *ptr_; }
    T* operator->() const { return ptr_; }
    
    // Disable copy
    RAIIWrapper(const RAIIWrapper&) = delete;
    RAIIWrapper& operator=(const RAIIWrapper&) = delete;
};

// Usage example
void legacy_code_wrapper() {
    RAIIWrapper wrapper(new int(42));
    // Even if an exception is thrown, wrapper will automatically delete on destruction
    risky_operation();
}

3. Smart Pointer Conversion Strategy

// Gradually convert raw pointers to smart pointers
class ModernClass {
private:
    // Step 1: Change member variables to smart pointers
    std::unique_ptr data_;
    // int* data_;  // Old code
    
public:
    ModernClass(size_t size) 
        : data_(std::make_unique(size)) {
        // : data_(new int[size]) {  // Old code
    }
    
    // Step 2: Provide compatibility interface
    int* get_data() const { return data_.get(); }
    
    // Step 3: Gradually replace all usage points
    void process() {
        // work_with_raw_pointer(data_.get());  // Transition period
        work_with_smart_pointer(data_);         // Final goal
    }
};

🚀 Prevention Strategies

1. Prefer Smart Pointers

// ✅ Recommended memory management method
class RecommendedPractice {
private:
    std::unique_ptr resource_;
    std::shared_ptr shared_resource_;
    std::vector<std::unique_ptr> items_;
    
public:
    RecommendedPractice() 
        : resource_(std::make_unique()),
          shared_resource_(std::make_shared()) {}
    
    void add_item(std::unique_ptr item) {
        items_.push_back(std::move(item));
    }
    
    // No need for manual destructor, smart pointers manage automatically
};</std::unique_ptr

2. Use Containers Instead of Raw Arrays

// ❌ Avoid using raw arrays
void avoid_raw_arrays() {
    int* raw_array = new int[100];  // Needs manual management
    // ... Use array
    delete[] raw_array;
}

// ✅ Use standard containers
void use_containers() {
    std::vector container(100);  // Automatically manages memory
    // ... Use container
    // Automatically released at the end of the function
}

3. Exception-Safe Resource Management

// Resource Acquisition Is Initialization (RAII)
class SafeFileHandler {
private:
    std::unique_ptr file_;
    
public:
    explicit SafeFileHandler(const char* filename) 
        : file_(fopen(filename, "r"), &fclose) {
        if (!file_) {
            throw std::runtime_error("Failed to open file");
        }
    }
    
    FILE* get() const { return file_.get(); }
    
    // Automatically closes the file on destruction
};

4. Factory Function Pattern

// Use factory functions to create objects
template
std::unique_ptr make_safe(Args&&... args) {
    return std::make_unique(std::forward(args)...);
}

// Usage example
auto obj = make_safe(param1, param2);
// Automatically manages memory, no need for manual delete

🔍 Case Study Analysis

Case 1: Memory Leak in Image Processing Program

// Problematic code
class ImageProcessor {
private:
    unsigned char* image_data_;
    int width_, height_;
    
public:
    bool load_image(const std::string& filename) {
        // Load image data
        image_data_ = new unsigned char[width_ * height_ * 3];
        // ... Loading logic
        return true;
    }
    
    void process_image() {
        if (!image_data_) return;
        
        // Create temporary buffer
        unsigned char* temp = new unsigned char[width_ * height_ * 3];
        
        // Process image...
        if (some_error_condition) {
            return;  // Leak! Forget to release temp
        }
        
        // Update image data
        delete[] image_data_;
        image_data_ = temp;
    }
};
// Fixed code
class SafeImageProcessor {
private:
    std::vector image_data_;
    int width_, height_;
    
public:
    bool load_image(const std::string& filename) {
        image_data_.resize(width_ * height_ * 3);
        // ... Loading logic
        return true;
    }
    
    void process_image() {
        if (image_data_.empty()) return;
        
        // Use vector to automatically manage memory
        std::vector temp(width_ * height_ * 3);
        
        // Process image...
        if (some_error_condition) {
            return;  // Safe! temp automatically releases
        }
        
        // Move semantics to avoid copy
        image_data_ = std::move(temp);
    }
};

Case 2: Memory Leak in Connection Pool

// Problematic code
class ConnectionPool {
private:
    std::queue available_connections_;
    std::set all_connections_;
    
public:
    Connection* get_connection() {
        if (available_connections_.empty()) {
            auto* conn = new Connection();  // Potential leak
            all_connections_.insert(conn);
            return conn;
        }
        
        auto* conn = available_connections_.front();
        available_connections_.pop();
        return conn;
    }
    
    void return_connection(Connection* conn) {
        available_connections_.push(conn);
    }
    
    // Destructor may be forgotten to implement!
};
// Fixed code
class SafeConnectionPool {
private:
    std::queue<std::shared_ptr> available_connections_;
    std::vector<std::shared_ptr> all_connections_;
    
public:
    std::shared_ptr get_connection() {
        if (available_connections_.empty()) {
            auto conn = std::make_shared();
            all_connections_.push_back(conn);
            return conn;
        }
        
        auto conn = available_connections_.front();
        available_connections_.pop();
        return conn;
    }
    
    void return_connection(std::shared_ptr conn) {
        available_connections_.push(conn);
    }
    
    // No need for manual destructor, smart pointers manage automatically
};</std::shared_ptr</std::shared_ptr

📋 Memory Leak Check List

Code Review Points

  • Dynamic Allocation Check: Each <span>new</span> has a corresponding <span>delete</span>
  • Array Allocation Check: <span>new[]</span> corresponds to <span>delete[]</span>
  • Exception Safety Check: Is memory correctly released on exception paths
  • Smart Pointer Usage: Prefer using <span>unique_ptr</span> and <span>shared_ptr</span>
  • Circular Reference Check: Are there circular references with <span>shared_ptr</span>
  • Container Cleanup: Are pointers in containers correctly released

Tool Usage Check

  • Compilation Options: Use <span>-fsanitize=address</span> for compilation
  • Debug Information: Include <span>-g</span> option during compilation
  • Memory Tools: Regularly use Valgrind or similar tools for detection
  • Unit Testing: Write dedicated tests for memory management

Design Principles Check

  • RAII Principle: Resource Acquisition Is Initialization
  • Smart Pointer Priority: Avoid using raw pointers for memory management
  • Container Priority: Use STL containers instead of raw arrays
  • Exception Safety: Ensure strong exception safety

💡 Best Practices Summary

✅ Recommended Practices

  1. 1. Smart Pointers First: Prefer using <span>unique_ptr</span> and <span>shared_ptr</span>
  2. 2. RAII Principle: Bind resource management to object lifecycle
  3. 3. Containers Instead of Arrays: Use <span>std::vector</span> and other containers
  4. 4. Tool Assistance: Regularly use memory detection tools
  5. 5. Exception Safety: Ensure resources are correctly released on exception paths
  6. 6. Circular Reference Handling: Use <span>weak_ptr</span> to break <span>shared_ptr</span> cycles

❌ Avoid Traps

  1. 1. Forgetting to Pair Releases: <span>new/delete</span> and <span>new[]/delete[]</span> must be paired
  2. 2. Exception Path Leaks: Be cautious of memory release on exception throws
  3. 3. Smart Pointer Circular References: Be careful of circular references with <span>shared_ptr</span>
  4. 4. Container Pointer Leaks: Be particularly careful when storing raw pointers in containers
  5. 5. Mixed Memory Management: Do not mix <span>malloc/free</span> and <span>new/delete</span>

🤔 Thought Questions

  1. 1. How to detect and fix memory leaks in the following code?
std::vector create_strings() {
    std::vector result;
    for (int i = 0; i < 10; ++i) {
        result.push_back(new std::string("item" + std::to_string(i)));
    }
    return result;
}
  1. 2. Why can smart pointers effectively prevent memory leaks? What is their principle?
  2. 3. In what situations can memory leaks still occur even when using smart pointers?

#C++ Memory Leak, #Memory Management, #Smart Pointers, #Valgrind, #AddressSanitizer, #RAII, #Exception Safety, #Debugging Techniques

This article focuses on practical techniques for troubleshooting C++ memory leaks. It is recommended to combine memory detection tools in actual projects and develop good memory management habits, as prevention is better than cure.

C++ Notes: Practical Techniques for Memory Leak Detection

Click to follow~

Leave a Comment