Small implementation of a singly-linked list in C89 I'm very new to C and have extensive experience with C# and very little with C++, but basically none with C.

I wrote a small program that declares a singly-linked list struct, a few methods operating on the list, and then uses it from the main method. Even though I've read about it a lot, this is only my second/third program I've written in C, so please beware.

#include &lt;assert.h&gt;
#include &lt;inttypes.h&gt;
#include &lt;stddef.h&gt;
#include &lt;stdint.h&gt;
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;

typedef struct linked_list_node linked_list_node; // forward declare linked_list_node and add it to the struct/global namespaces

struct linked_list_node
{
    linked_list_node* next;
    int32_t item;
};

typedef struct
{
    linked_list_node* head;
    size_t size;
} linked_list;

void init_list(linked_list* list) { /* Nothing to do here */ }

void add_last(linked_list* list, int32_t item)
{
    assert(list != NULL);

    if (list-&gt;head == NULL)
    {
        // No elements in the list yet.
        list-&gt;head = malloc(sizeof(linked_list_node));
        list-&gt;head-&gt;next = NULL;
        list-&gt;head-&gt;item = item;
    }
    else
    {
        // At least 1 element in the list.
        assert(list-&gt;size &gt; 0);

        // Walk the list until we find the last node.
        linked_list_node* node = list-&gt;head;
        while (node-&gt;next != NULL)
        {
            node = node-&gt;next;
        }

        // Allocate a new last node
        linked_list_node* next = malloc(sizeof(linked_list_node)); // note: no typecast from void* here
        node-&gt;next = next;
        next-&gt;next = NULL; // need to explicitly do this since malloc's memory is uninitialized
        next-&gt;item = item;
    }

    list-&gt;size++;
}

typedef void action(int32_t);

void foreach_list(linked_list* list, action* act)
{
    assert(list != NULL &amp;&amp; act != NULL);

    linked_list_node* node;
    for (node = list-&gt;head;
        node != NULL;
        node = node-&gt;next)
    {
        act(node-&gt;item);
    }
}

// Cleans up all of the memory used up by the list's
// nodes. DOES NOT FREE THE LIST ITSELF.
void cleanup_list(linked_list* list)
{
    assert(list != NULL);

    list-&gt;size = 0;

    // Walk the list, freeing nodes as we go.
    linked_list_node* node, *next;
    for (node = list-&gt;head;
        node != NULL;
        node = next)
    {
        next = node-&gt;next;
        free(node);
    }
}

void print_int32(int32_t to_print)
{
    printf("Found %"PRIi32"!\n", to_print); // format specifier for int32_t
}

int main(void)
{
    linked_list list;
    memset(&amp;list, 0, sizeof(list));
    init_list(&amp;list);

    add_last(&amp;list, 3);
    add_last(&amp;list, 4);
    add_last(&amp;list, 5);

    foreach_list(&amp;list, print_int32);
    printf("Size: %"PRIu64"\n", (uint64_t)list.size); // do not use %zu since this is targeting C89

    cleanup_list(&amp;list);
    return 0;
}


I compiled this under MinGW with C89.

Here is the type of feedback I'm looking for:


Did I introduce and potential sources of UB? For example, is there a flaw in my logic where a NULL pointer can be dereferenced and go undetected by asserts?
Is my memory management logic correct? Did I accidentally malloc something that never got cleared, or did I free anything twice in cleanup_list? Is there a way to make all of the logic less error-prone?
Did I rely on any features specific to GCC to get this to work? For example, will this program also work with other compilers under C89?
Any way I could clean up the code and make it more neat/concise? (I am aware that I should probably have put the list definition/methods into separate header/source files than main.)
init_list

should really initialize the list. Otherwise you are forcing the caller to make an non-obvious memset.

void init_list(linked_list * list) {
    list-&gt;head = 0;
    list-&gt;size = 0;
}


is much more clean.
add_last

is not a particularly good name. Traditionally this operation is called append or push_back.

The function can be streamlined. First, assert your invariant fully (that is either head == 0 and size == 0 or head != 0 and size &gt; 0) and immediately.

Second, once the invariant is deemed correct, allocate the node (you need to do it anyway). It is a good idea to delegate node allocation to a function.

Finally, find the tail (it is also a good candidate to factor out into a function), and attach the node. For example,

void append (linked_list * list, int32_t item)
{
    assert(list_is_good(list));
    linked_list_node * node = create_list_node(item);
    linked_list_node * tail = find_list_tail(list);
    if (tail == NULL) {
        list-&gt;head = node;
    } else {
        tail-&gt;next = node;
    }
    list-&gt;size += 1;
}

malloc may fail

Test the results of malloc. There is not much you can do once it fails, but it is good form anyway.