C++ Valgrind: A Practical Memory Debugging Guide

C++ Valgrind: A Practical Memory Debugging Guide

이 글의 핵심

A hands-on guide to C++ Valgrind for memory debugging and profiling.

Introduction

Valgrind is a powerful toolkit for memory leaks, bugs, and profiling in C/C++ programs. On Linux and macOS it is essential for tracking down memory issues.


1. Installation and basic usage

Installation

# Ubuntu/Debian
sudo apt-get install valgrind

# macOS
brew install valgrind

# Check version
valgrind --version

Basic usage

# Compile (debug symbols)
g++ -g program.cpp -o program

# Run Valgrind
valgrind --leak-check=full ./program

# More detail
valgrind --leak-check=full --show-leak-kinds=all ./program

# Track origins of uninitialized values
valgrind --track-origins=yes ./program

2. Detecting memory leaks

Example 1: Memory leak

// leak.cpp
#include <iostream>

int main() {
    int* ptr = new int(42);
    
    std::cout << *ptr << std::endl;
    
    return 0;
}
g++ -g leak.cpp -o leak
valgrind --leak-check=full ./leak

Sample output:

==12345== HEAP SUMMARY:
==12345==     in use at exit: 4 bytes in 1 blocks
==12345==   total heap usage: 1 allocs, 0 frees, 4 bytes allocated
==12345== 
==12345== 4 bytes in 1 blocks are definitely lost
==12345==    at 0x4C2E0EF: operator new(unsigned long)
==12345==    by 0x400B2C: main (leak.cpp:5)

Example 2: Uninitialized memory

// uninit.cpp
#include <iostream>

int main() {
    int x;
    
    if (x > 0) {
        std::cout << "positive" << std::endl;
    }
    
    return 0;
}
g++ -g uninit.cpp -o uninit
valgrind --track-origins=yes ./uninit

Sample output:

==12345== Conditional jump or move depends on uninitialised value(s)
==12345==    at 0x400B2C: main (uninit.cpp:6)

Example 3: Invalid memory access

// invalid.cpp
#include <iostream>

int main() {
    int arr[10];
    
    for (int i = 0; i <= 10; ++i) {
        arr[i] = i;
    }
    
    return 0;
}
g++ -g invalid.cpp -o invalid
valgrind ./invalid

Sample output:

==12345== Invalid write of size 4
==12345==    at 0x400B2C: main (invalid.cpp:7)

3. Valgrind tools

Memcheck

valgrind --tool=memcheck ./program

Cachegrind

valgrind --tool=cachegrind ./program
cg_annotate cachegrind.out.12345

Callgrind

valgrind --tool=callgrind ./program
kcachegrind callgrind.out.12345

Helgrind

valgrind --tool=helgrind ./program

Massif

valgrind --tool=massif ./program
ms_print massif.out.12345

4. Common issues

Issue 1: Performance

# Valgrind is very slow (often 10–50×)
# Use only in development/testing

# Test with small inputs
valgrind --leak-check=full ./program < small_input.txt

Issue 2: False positives

# Generate suppression rules
valgrind --gen-suppressions=all ./program > my.supp

# Use suppressions
valgrind --suppressions=my.supp ./program

Example my.supp:

{
   <system_library_leak>
   Memcheck:Leak
   fun:malloc
   fun:system_function
}

Issue 3: Missing debug info

# ❌ No debug symbols
g++ program.cpp -o program
valgrind ./program

# ✅ Use -g
g++ -g program.cpp -o program
valgrind ./program

Issue 4: Optimization level

# ❌ Heavy optimization
g++ -O3 -g program.cpp

# ✅ Lower optimization for clearer stacks
g++ -O0 -g program.cpp

5. Interpreting output

Leak kinds

# Definite leak
definitely lost: 100 bytes in 5 blocks

# Indirect leak
indirectly lost: 50 bytes in 2 blocks

# Possible leak
possibly lost: 20 bytes in 1 blocks

# Still reachable at exit
still reachable: 30 bytes in 3 blocks

Quick reference

KindMeaningAction
definitely lostTrue leakFix
indirectly lostLeak via parent pointerFix parent leak
possibly lostPossible leakInvestigate
still reachableStill pointed to at exitOften OK; free on shutdown if strict

6. Practical examples

Example 1: Smart pointers

// smart_ptr.cpp
#include <memory>
#include <iostream>

int main() {
    auto ptr = std::make_unique<int>(42);
    
    std::cout << *ptr << std::endl;
    
    return 0;
}
g++ -g smart_ptr.cpp -o smart_ptr
valgrind --leak-check=full ./smart_ptr

Sample output:

==12345== HEAP SUMMARY:
==12345==     in use at exit: 0 bytes in 0 blocks
==12345==   total heap usage: 1 allocs, 1 frees, 4 bytes allocated
==12345== 
==12345== All heap blocks were freed -- no leaks are possible

Example 2: Vector of raw pointers

// vector_leak.cpp
#include <vector>
#include <iostream>

int main() {
    std::vector<int*> vec;
    
    for (int i = 0; i < 5; i++) {
        vec.push_back(new int(i));
    }
    
    return 0;
}
g++ -g vector_leak.cpp -o vector_leak
valgrind --leak-check=full ./vector_leak

Fix:

// vector_fixed.cpp
#include <vector>
#include <iostream>

int main() {
    std::vector<int*> vec;
    
    for (int i = 0; i < 5; i++) {
        vec.push_back(new int(i));
    }
    
    for (auto ptr : vec) {
        delete ptr;
    }
    
    return 0;
}

Summary

Key takeaways

  1. Valgrind: Memory debugging and profiling framework
  2. Memcheck: Memory error and leak detection
  3. Cachegrind: Cache profiling
  4. Helgrind: Threading errors
  5. Cost: Often 10–50× slower

Tool comparison

ToolPurposeTypical slowdown
MemcheckMemory errors10–50×
CachegrindCache analysis20–100×
CallgrindCall graphs20–100×
HelgrindThread errors20–50×
MassifHeap profiling~20×

Practical tips

  • Compile with -g
  • Use --leak-check=full
  • Test with small inputs
  • Use suppression files for known false positives
  • Prefer smart pointers

Next steps

  • C++ Sanitizers
  • C++ GDB
  • C++ memory management

  • C++ debugging techniques | GDB, sanitizers, leaks, multithreading