Valgrind, the memory management analyser

Programming2: Valgrind in material section

The types of memory and their lifetimes

Binding can refer to associating a variable with a specific memory location. At the start of binding, space in memory is allocated for the variable, and once binding is finished, the memory space is released. The period during which a variable is bound to a specific memory location is called the variable’s lifetime. In C++, a programmer can define variables with different lifetimes, including:

Static variables

static int globalVariable = 10; // Static global variable
void foo() {
      static int staticLocalVariable = 5; // Static local variable
}

In C++, a static variable is bound to its memory location before program execution, and the same binding remains throughout the execution. Global variables and variables introduced with the ’static’ keyword in C++ are examples of static variables. In earlier versions of C++, all variables were considered static.

Stack-dynamic variables inside a function

void bar() {
   int stackVariable = 7; // Stack-dynamic local variable
}

stackVariable does not live after bar().

Explicit heap-dynamic variables

int* dynamicVariable = new int(42); // Explicit heap-dynamic variable
delete dynamicVariable; // Explicit deallocation before program ends

Explicit memory reservation with new or malloc.

void bar() {
   int* dynamicVariable = new int(42); // Explicit heap-dynamic variable
}

No explicit delete or free of heap memory leads to a memory leak. Preferably, deletion in the same scope where the variable was defined.

Implicit heap-dynamic variables

These are not directly supported in C++ like in some scripting languages, such as JavaScript and Python. Binding memory for implicit heap-dynamic variables takes place during runtime when a value is assigned. In JavaScript, you can write:

let myVariable = 2;

where ’myVariable’ is of number type. Later, the variable can be re-assigned to a different type:

myVariable = "hello";

Now, the variable’s type becomes a string. In JavaScript, variables are typically stored on the heap, even when they are within the scope of a function because of the dynamic nature of the language.

Extern variables

C++ also supports external references (e.g., ’extern’ keyword) and persistent data elements located in files, which persist even after the program execution ends. Thus, a variable’s lifetime can be longer than the program’s runtime.

Memory best practices

  1. avoid iterator invalidation:
    • be cautious of iterator invalidation when modifying containers (e.g., insert, erase)
  2. use smart pointers:
    • std::unique_ptr and std::shared_ptr manage memory automatically
#include <memory>
std::unique_ptr<int> uniquePtr = std::make_unique<int>(42);
  1. prefer STL containers and algorithms:
    • such as std::vector, std::map, and STL algorithms whenever possible.
  2. avoid heap memory allocation altogether if possible:
    • minimize use of new, delete, malloc, and free.
    • remember to delete and provide explicit methods to release resources
  3. profile and test:
    • Use Valgrind!

Testing and profiling with Valgrind

If a program is small enough, you can find memory errors just by looking at the code. However, when the size of the program grows, finding errors becomes more difficult.

You can trace the memory management errors with a program called valgrind that must be executed separately: valgrind COMPILED_PROGRAM. It analyzes the code and prints an error message if, for example:

  • your program allocates dynamic memory with new but does not deallocate it with delete
  • your program uses a variable (allocated dynamically or automatically) without a value set to it
  • you try to use dynamically allocated memory that has been deallocated already.

valgrind is installed on linux-desktop.

Permission applied in https://id.tuni.fi/idm/entitlement)

If you want, you also can install valgrind on your own computer, this is easiest in linux:

..code-block:: bash

sudo apt install valgrind

You can execute valgrind in either Qt Creator or via the command line. For the sake of practice, we will in the next exercise, execute valgrind with both ways, so that we learn more than one way of using it.

Note

Valgrind works in Linux desktop, but most probably not in Mac nor Windows (or installing valgrind in these computers is impossible).

Valgrind’s error messages

Some of the error messages refer to the values of 1, 4, 8. These numbers mean the size of the allocated memory in bytes, which is determined by the used type, for example, a character (char) requires only one byte, an integer (int) requires four bytes, and a pointer requires eight bytes.

In the column Location, Valgrind tells a file name and a line number. The location is the one where the error was detected, yet the bug may locate elsewhere, at some earlier point that was executed before the detection of an error.

Next, we will go through the most general error messages. Let’s assume that we have a program like the task list example (wk08/valgrind/task_list_v2/) with the struct List_item.

Use of unitialized value of size 8

If memory is not allocated for an element of a linked list (with new), but you just write, for example, as:

List_item* new_item;

already the editor of Qt informs about an unitialized variable. If you still execute valgrind, you will get the error message in the title above (and perhaps also the error message in the next title).

If you just assign nullptr as:

List_item* new_item = nullptr;

and assign no values in the struct fields, valgrind informs nothing, but the program does work correctly.

If you, besides the above assignment, try to use the element new_item as

List_item* new_item = nullptr;
new_item->task = ...;

the program does nothing, and valgrind informs nothing.

Conditional jump or move depends on uninitialised value(s)

A field of a struct or variable is left uninitialized. For example, in a linked list, a field of the struct may not be properly initialized, such as forgetting to set the value of the next pointer in a node. If an uninitialized field is not the next field, you may encounter errors when trying to print or access the value of that field. If you assign no value (not even nullptr) to the next field, the program may seem to work correctly, but this can lead to unexpected behavior, such as infinite loops when traversing the list. To avoid such issues, ensure that all fields in your structs or variables are properly initialized before use.

Invalid read of size N

Program tries to use (read) a memory cell already deallocated. For example:

delete item_to_be_removed;
...
cout << item_to_be_removed->task << endl;

Here the task field is a string, and thus, there is 8 in the place of N. If the field to be printed was of type int, N would be 4.

Invalid free() / delete / delete[]

It is tried to deallocate a memory cell already deallocated. For example:

delete item_to_be_removed;
...
delete item_to_be_removed;

The program may still work correctly, and thus, the error is hard to detect without valgrind.

If you, between the above lines, assign nullptr as:

delete item_to_be_removed;
item_to_be_removed = nullptr;
...
delete item_to_be_removed;

valgrind informs nothing, and program seems to work correctly. This is because nullptr points to nothing, and thus, nothing can be deallocated.

N (…) bytes in M blocks are definitely lost in loss record X of Y

Memory cells were not deallocated, i.e., the command delete was forgotten.

The program may still work correctly, and thus, the error is hard to detect without valgrind.