0

This may be a really simple question, but I could not find an answer any place on the internet. I am working on a linked list implementation in C, and am checking the memory usage and de-allocation with valgrind. I have created the following implementation which works properly. The only place that malloc is called is in the push_int_list function, which allows a user to create and insert a struct to any index in the linked list. Yes, I know that it is not a true index like an array, but i am using the same lingo to describe the location of a data point. Furthermore, the push_int_list function will will iterate from the head or tail of the linked list depending on where the struct is to be inserted.

The implementation shown below works correctly; however, when I check the memory allocation and de-allocation with valgrind, it shows that I execute 6 allocs, and 6 frees. As you can see, I only call the push_int_list function 5 times, and therefore should only be executing 5 allocs and 5 frees. If I change the number of push statements, the number of allocs and frees in valgrind is always one more than I actually execute. Why does valgrind always show one more malloc than I actually execute.

#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>

// --------------------------------------------------------------------------------
// Linked list Struct to containt data

typedef struct int_list
{
    int data;
    struct int_list *next;
    struct int_list *previous;
} int_list;
// --------------------------------------------------------------------------------
// Struct to track linked list data

typedef struct
{
    size_t active_length;
    struct int_list *head;
    struct int_list *tail;
} Int;
// --------------------------------------------------------------------------------
// Basic initialization of data tracking struct

void init_int_list(Int *list) {
    list->active_length = 0;
    list->head = NULL;
    list->tail = NULL;
}
// --------------------------------------------------------------------------------
// Function that allows user to push a struct to any indice

int push_int_list(Int *list, int data, size_t index) {
    if (index < 0 || index > list->active_length) {
        fprintf(stderr, "INdex out of range\n");
        return -1;
    }
    struct int_list *dat = malloc(sizeof(int_list));
    if (!dat) {
        fprintf(stderr, "malloc failed in file %s at line %d\n", __FILE__, __LINE__);
        return -1;
    }
    if (list->active_length == 0) {
        // populate struct
        dat->data = data;
        dat->previous = NULL;
        dat->next = NULL;

        // Update linked list tracking struct
        list->head = dat;
        list->tail = dat;
        list->active_length += 1;
    }
    else if (index == 0 && list->active_length > 0) {
        // populate struct
        dat->previous = NULL;
        dat->next = list->head;
        dat->data = data;

        // Update data in the struct occupying hte next index
        (list->head)->previous = dat;

        // Update linked list tracking struct
        list->head = dat;
        list->active_length += 1;
    }
    else if (list->active_length > 0 && index == list->active_length) {
        // populate struct
        dat->data = data;
        dat->next = NULL;
        dat->previous = list->tail;

        // Update struct occupying previous index
        (list->tail)->next = dat;

        // Update linked list tracking struct
        list->tail = dat;
        list->active_length += 1;
    }
    // The next two if statements are mean to reduce the iteration time by half
    // if the index to be inserted is between 0 and active_length
    else if (index < list->active_length / 2) {
        struct int_list *current = list->head;
        struct int_list *previous = NULL;
        for (size_t i = 0; i < index; i++) {
            previous = current;
            current = current->next;
        }
        // populate struct
        dat->data = data;
        dat->next = current;
        dat->previous = previous;

        // Update previous and next struct
        previous->next = dat;
        current->previous = dat;

        // Update linked list tracking struct
        list->active_length += 1;
    }
    else {
        struct int_list *current = list->tail;
        struct int_list *next = NULL;
        for (size_t i = list->active_length; i > index; i--) {
            next = current;
            current = current->previous;
        }
        // Update struct
        dat->data = data;
        dat->next = next;
        dat->previous = current;

        // Update previous and next struct
        next->previous = dat;
        current->next = dat;

        // Update linked list tracking struct
        list->active_length += 1;
    }
    return 1;
}

void free_int_list(Int *list) {
    if (list->active_length > 0) {
        struct int_list *tmp = NULL;
        struct int_list *head = list->head;
        while (head->next != NULL) {
            tmp = head;
            head = tmp->next;
            free(tmp);
        }
        free(head);
    }
}

// Begin code
int main() {
    Int list;
    init_int_list(&list);
    push_int_list(&list, 1, 0);
    push_int_list(&list, 2, 1);
    push_int_list(&list, 3, 2);
    push_int_list(&list, 4, 3);
    push_int_list(&list, 8, 0);

    // Print linked list
    struct int_list *current = list.head;
    for (size_t i = 0; i < list.active_length; i++) {
        printf("%d\n", current->data);
        current = current->next;
    }

    free_int_list(&list);
    return 0;
}

valgrind information shown below

==10074== Memcheck, a memory error detector
==10074== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==10074== Using Valgrind-3.19.0 and LibVEX; rerun with -h for copyright info
==10074== Command: ./test
==10074== 
8
1
2
3
4
==10074== 
==10074== HEAP SUMMARY:
==10074==     in use at exit: 0 bytes in 0 blocks
==10074==   total heap usage: 6 allocs, 6 frees, 1,144 bytes allocated
==10074== 
==10074== All heap blocks were freed -- no leaks are possible
==10074== 
==10074== For lists of detected and suppressed errors, rerun with: -s
==10074== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

I have wondered if it is tracking the instantiation of the Int struct as a malloc, but this is created in stack memory as a static allocation, so I cant see why this would be tracked as a malloc in valgrind!

Paul Floyd
  • 5,530
  • 5
  • 29
  • 43
Jon
  • 1,621
  • 5
  • 23
  • 46
  • I recommend you use a [*debugger*](https://stackoverflow.com/questions/25385173/what-is-a-debugger-and-how-can-it-help-me-diagnose-problems) to step through the code line by line, because then you will clearly see exactly what's happening and when and where and why you do an extra allocation. – Some programmer dude Nov 30 '22 at 02:38
  • And to help with [debugging](https://ericlippert.com/2014/03/05/how-to-debug-small-programs/) and testing, please don't write large parts of code without testing or building. Write only a *small* (*very* small) part of code, build with extra warnings enabled (treated as errors), and test. Only when it build cleanly and all tests succeed, continue with the next very small part. If there's a problem this makes it much easier to figure out what the problem might be. – Some programmer dude Nov 30 '22 at 02:42
  • 1
    Not all allocations necessarily come from you calling `malloc`. The library code that runs before and after your program could allocate and free memory or `printf` could do the same. As long as there isn't a leak I'm not sure it's worth really worrying about. You could test without allocating at all and see what Valgrind says. – Retired Ninja Nov 30 '22 at 02:52
  • 1
    Why not write a program that does nothing but make one call to `malloc`. Run valgrind on that. Did it report 1 allocation? It's possible that the first `malloc` call actually performs its own memory allocations to reserve storage on the heap. So, a simple program like this will test that. A total allocation of 1144 bytes is a bit suspicious for a program that likely only allocates 5 lots of 24 bytes (assuming 64-bit). – paddy Nov 30 '22 at 02:52
  • 1
    @Someprogrammerdude I am very aware of that and have used cmocka to test individual functions and edge cases in this implementation, In addition, I have used a debugger and it is not detecting any extra mallocs. – Jon Nov 30 '22 at 03:07
  • @RetiredNinja your answer confirms what I was thinking, but I did not have any sources to back that assertion up. – Jon Nov 30 '22 at 03:08
  • 2
    @paddy I did do that, and when I wrote the simple program it reported two allocs and two frees. I suspect this may not be an issue with my code, but an underlying implementation of C. Even if it is not with my code, I still want to understand the dynamics of what is going on. – Jon Nov 30 '22 at 03:10
  • Minor bug: `index < 0` is always false. – Lundin Nov 30 '22 at 07:36

1 Answers1

2

You are overthinking the problem. If Valgrind says "no leaks are possible" then you should believe it.

The memory is allocated for your call to printf. Since this memory is allocated in a 'singleton' manner, glibc does not free that memory when running outside of Valgrind. You should be able to confirm that by using gdb and putting breakpoints on malloc and free. Several other functions in glibc allocate singleton buffers.

Valgrind contains a hack to free this memory. When the guest starts it adds a redirection to exit() to call __libc_freeres() and then exit(). The freeres function doesn't get called normally and only exists for tools like Valgrind.

If you run Valgrind with --run-libc-freeres=no then memcheck will ho longer call __libc_freeres and so "detect" this allocation as a "reachable" leak.

Paul Floyd
  • 5,530
  • 5
  • 29
  • 43
  • I figures this had to do with functionality of the C library but I was not sure. Thank you for your answer. – Jon Dec 01 '22 at 18:43