Question

How to Use Valgrind to Find Memory Leaks in C on Ubuntu

cvalgrind

Question

How can I use Valgrind to find memory leaks in a C program?

I am using Ubuntu 10.04, and I have a source file named a.c. I want to check whether my program leaks memory and understand how to run Valgrind correctly to identify the problem areas.

Short Answer

By the end of this page, you will know how to compile a C program for debugging, run it with Valgrind, read the leak report, and understand the most common causes of memory leaks in C programs.

Concept

In C, when you allocate memory dynamically using functions such as malloc(), calloc(), or realloc(), you are responsible for freeing that memory with free() when you no longer need it.

A memory leak happens when your program allocates memory but loses the ability to release it later. This matters because:

  • the program wastes memory
  • long-running programs can grow in memory usage over time
  • leaks make software harder to maintain and debug
  • repeated leaks can eventually slow down or crash systems

Valgrind is a debugging tool that runs your program inside an analysis environment. One of its most common tools, Memcheck, tracks memory usage and reports problems such as:

  • memory leaks
  • invalid reads and writes
  • use of uninitialized values
  • freeing memory incorrectly
  • double frees

Valgrind is especially useful in C because the language gives you direct control over memory, but that also means memory bugs are easy to introduce.

To get useful Valgrind output, you should compile your program with debug information using -g. This helps Valgrind show file names and line numbers.

Mental Model

Think of dynamic memory like renting storage boxes.

  • malloc() means you rent a box.
  • Using the pointer means you keep the key to that box.
  • free() means you return the box.
  • A memory leak happens when you lose the key before returning the box.

Valgrind acts like an auditor watching every box you rent and return. At the end, it tells you:

  • which boxes were never returned
  • where you rented them
  • whether you tried to use a box incorrectly

This makes it much easier to trace memory problems back to the exact place in your code.

Take Quiz

Syntax and Examples

To use Valgrind with a C program, the usual workflow is:

  1. Compile with debug symbols.
  2. Run the program through Valgrind.
  3. Read the output.

Install Valgrind

sudo apt-get install valgrind

Compile your program

gcc -g -o a a.c
  • -g adds debug information
  • -o a creates an executable named a

Run with Valgrind

valgrind --leak-check=full ./a

Example program with a leak

#include <stdlib.h>

int main(void) {
    int *numbers = malloc(10 * sizeof(int));
    return 0;
}

Compile and run

gcc -g -o a a.c
valgrind --leak-check=full ./a

What this means

The program allocates memory for 10 integers but never calls free(numbers);. Valgrind will report this as leaked memory.

Fixed version

#include <stdlib.h>

int main(void) {
    int *numbers = malloc(10 * sizeof(int));
    free(numbers);
    return 0;
}

Now the allocated memory is released before the program exits.

Step by Step Execution

Consider this small program:

#include <stdlib.h>

int main(void) {
    char *buffer = malloc(20);
    buffer[0] = 'A';
    buffer[1] = '\0';
    return 0;
}

Step-by-step

  1. malloc(20) asks the system for 20 bytes of memory.
  2. The returned address is stored in buffer.
  3. The program writes 'A' into the first byte.
  4. The program writes the string terminator \0 into the second byte.
  5. The program ends without calling free(buffer).

What Valgrind sees

  • memory was allocated
  • the pointer existed
  • the memory was never freed

Typical command

gcc -g -o a a.c
valgrind --leak-check=full ./a

Typical output shape

==12345== HEAP SUMMARY:
==12345==     in use at exit: 20 bytes in 1 blocks
==12345==   total heap usage: 1 allocs, 0 frees, 20 bytes allocated
==12345==
==12345== 20 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345==    at 0x...: malloc (...)
==12345==    by 0x...: main (a.c:4)

How to read it

  • in use at exit: memory still allocated when the program ended
  • 1 allocs, 0 frees: one allocation happened, but nothing was freed
  • definitely lost: Valgrind is confident this memory was leaked
  • main (a.c:4): the leak originated from line 4 in a.c

That line number is one of the most useful parts of the report.

Real World Use Cases

Memory leak detection matters in many real programs:

  • Command-line tools: a utility may run briefly, but leaks still indicate poor memory management.
  • Servers and daemons: even small leaks become serious if the process runs for days or weeks.
  • Embedded or low-memory systems: leaked memory can quickly exhaust limited RAM.
  • Data processing programs: repeated allocations inside loops can silently leak large amounts of memory.
  • Libraries: if a library leaks memory, every application using it may suffer.

Example scenarios:

  • parsing a file and forgetting to free a buffer
  • building a linked list and never releasing its nodes
  • returning early from a function before cleanup happens
  • overwriting a pointer returned by malloc() and losing the original address
Take Quiz

Real Codebase Usage

In real C codebases, developers often use Valgrind together with coding patterns that reduce leak risk.

Common patterns

  • Pair every allocation with a clear owner

    • If one function allocates memory, be clear about which function frees it.
  • Use guard clauses carefully

    • Early returns are fine, but make sure already-allocated resources are cleaned up first.
  • Centralize cleanup

    • Many C programs use a cleanup section near the end of a function.
#include <stdlib.h>

int process(void) {
    char *a = malloc(100);
    char *b = malloc(200);

    if (a == NULL || b == NULL) {
        free(a);
        free(b);
        return 1;
    }

    free(a);
    free(b);
    return 0;
}
  • Check pointers after allocation

    • malloc() can return NULL.
  • Run Valgrind in testing

    • Developers often run Valgrind on test cases, sample inputs, or integration test binaries.

Helpful options in real projects

valgrind --leak-check=full --show-leak-kinds=all ./a

Useful flags:

  • --leak-check=full: detailed leak information
  • --show-leak-kinds=all: shows different leak categories
  • --track-origins=yes: helpful for uninitialized value errors, though slower

Common Mistakes

Here are common beginner mistakes when using dynamic memory and Valgrind.

1. Forgetting to compile with -g

Without debug symbols, Valgrind output is harder to read.

gcc -o a a.c

Better:

gcc -g -o a a.c

2. Allocating memory and never freeing it

Broken code:

char *name = malloc(50);
name[0] = 'J';
name[1] = '\0';

Fix:

char *name = malloc(50);
if (name != NULL) {
    name[0] = 'J';
    name[1] = '\0';
    free(name);
}

3. Losing the original pointer

Broken code:

int *p = malloc(10 * sizeof(int));
p = NULL;

This leaks memory because the original address is lost before calling free().

4. Freeing memory twice

Broken code:

int *p = malloc(sizeof(int));
free(p);
free(p);

This is not a leak, but it is still a serious memory error. Valgrind can detect it.

5. Returning before cleanup

Broken code:

int func(void) {
    char *data = malloc(100);
    if (data == NULL) {
        return 1;
    }

    return 0;
}

Fix:

int func(void) {
    char *data = malloc(100);
    if (data == NULL) {
        return 1;
    }

    free(data);
    return 0;
}

6. Assuming program exit always makes leaks unimportant

The operating system may reclaim memory when the process exits, but a leak is still a bug. In long-running programs, it becomes much more serious.

Comparisons

Tool or conceptWhat it doesBest use
Valgrind MemcheckDetects leaks and memory misuse while running the programFinding leaks, invalid reads/writes, double frees
Compiler warningsReports suspicious code at compile timeCatching obvious mistakes early
Manual code reviewYou inspect allocation and cleanup logic yourselfUnderstanding ownership and cleanup flow
AddressSanitizerDetects many memory errors with compiler instrumentationFast runtime checks during development

Valgrind vs just reading the code

  • Reading code helps you understand ownership.
  • Valgrind shows what actually happened at runtime.
  • In practice, developers use both.

Leak categories you may see in Valgrind

Leak kindMeaning
definitely lostMemory is leaked and no pointer to it remains
indirectly lostMemory was reachable only through another leaked block
possibly lostValgrind found something suspicious, but cannot be fully certain
still reachableMemory was not freed, but a pointer still existed at exit

For beginners, definitely lost is usually the most important category to fix first.

Take Quiz

Cheat Sheet

Basic workflow

gcc -g -o a a.c
valgrind --leak-check=full ./a

Install

sudo apt-get install valgrind

Useful options

valgrind --leak-check=full --show-leak-kinds=all ./a
valgrind --track-origins=yes ./a

What to look for

  • definitely lost = confirmed leak
  • indirectly lost = leaked through another leaked block
  • still reachable = not freed, but still referenced at exit
  • file and line numbers point to where allocation happened

Good habits

  • compile with -g
  • check every malloc() result
  • free every successful allocation
  • avoid overwriting allocated pointers before freeing them
  • test small programs first, then larger ones

Common allocation functions

malloc(size)
calloc(count, size)
realloc(ptr, new_size)
free(ptr)

Rule of thumb

If you allocate it, you should know who frees it.

FAQ

How do I run Valgrind on a C program?

Compile the program with debug symbols using gcc -g -o a a.c, then run valgrind --leak-check=full ./a.

Why should I compile with -g before using Valgrind?

The -g flag adds debug information so Valgrind can show source file names and line numbers in its output.

What does definitely lost mean in Valgrind?

It means memory was allocated and Valgrind can no longer find any valid pointer to it. This is a real leak.

Can Valgrind find errors other than memory leaks?

Yes. It can also detect invalid memory access, use of uninitialized values, incorrect frees, and double frees.

Does memory still count as leaked if the program exits?

Yes. Even if the operating system reclaims memory after exit, failing to free memory is still considered a bug.

Why does Valgrind feel slow?

Valgrind instruments your program while it runs, so it is much slower than normal execution. That is expected during debugging.

What if Valgrind output does not show line numbers?

You probably did not compile with debug symbols, or the binary was optimized in a way that reduced useful debug information.

Is Valgrind only for large programs?

No. It is very useful for small programs too, especially when learning memory management in C.

Take Quiz

Mini Project

Description

Create a small C program that allocates memory for a dynamic array, uses it, and then frees it correctly. Then run the program with Valgrind to confirm that no memory is leaked. This demonstrates the complete workflow: writing code, compiling with debug symbols, running Valgrind, and interpreting the result.

Goal

Build and test a C program that uses dynamic memory safely and produces a clean Valgrind report.

Requirements

  • Write a C program that allocates memory for an integer array using malloc().
  • Store some values in the array and print them.
  • Free the allocated memory before the program exits.
  • Compile the program with debug symbols.
  • Run the program with Valgrind and confirm there are no leaks.
Take Quiz

Keep learning

Related questions

View more