A linked list is the first data structure most C programmers implement themselves. Unlike arrays, it does not require a fixed size upfront and makes insertion at the head O(1). Building one teaches you pointers, structs, and manual memory management all at once.

The Node Structure

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

typedef struct Node {
    int         value;
    struct Node *next;   /* pointer to the next node */
} Node;

Each node holds a value and a pointer to the next node. The last node’s next pointer is NULL. Note the use of struct Node inside the definition — the Node typedef is not yet available when the struct body is being parsed, so you must use the tagged name. See our article on C structs and typedef for the full explanation.

Creating a Node

Node *create_node(int value) {
    Node *node = malloc(sizeof(Node));
    if (node == NULL) {
        fprintf(stderr, "malloc failedn");
        exit(1);
    }
    node->value = value;
    node->next  = NULL;
    return node;
}

Each node is allocated on the heap. For the difference between heap and stack allocation, see our guide on malloc vs calloc vs realloc.

Inserting at the Head

/* Returns the new head of the list */
Node *insert_head(Node *head, int value) {
    Node *node   = create_node(value);
    node->next   = head;   /* new node points to old head */
    return node;           /* new node is the new head */
}

Inserting at the Tail

Node *insert_tail(Node *head, int value) {
    Node *node = create_node(value);

    if (head == NULL) return node;   /* empty list */

    Node *curr = head;
    while (curr->next != NULL) {
        curr = curr->next;
    }
    curr->next = node;
    return head;
}

Traversing and Printing

void print_list(Node *head) {
    Node *curr = head;
    while (curr != NULL) {
        printf("%d", curr->value);
        if (curr->next != NULL) printf(" -> ");
        curr = curr->next;
    }
    printf(" -> NULLn");
}

Deleting a Node by Value

/* Returns new head (may change if head is deleted) */
Node *delete_value(Node *head, int value) {
    if (head == NULL) return NULL;

    /* Deleting the head */
    if (head->value == value) {
        Node *new_head = head->next;
        free(head);
        return new_head;
    }

    /* Find the node before the one to delete */
    Node *curr = head;
    while (curr->next != NULL && curr->next->value != value) {
        curr = curr->next;
    }

    if (curr->next != NULL) {
        Node *to_delete = curr->next;
        curr->next = to_delete->next;
        free(to_delete);
    }

    return head;
}

Freeing the Entire List

void free_list(Node *head) {
    Node *curr = head;
    while (curr != NULL) {
        Node *next = curr->next;   /* save next before freeing */
        free(curr);
        curr = next;
    }
}

Save curr->next before calling free(curr). After free, accessing curr->next is undefined behaviour — the node no longer belongs to you.

Complete Working Example

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

typedef struct Node {
    int value;
    struct Node *next;
} Node;

Node *create_node(int v) {
    Node *n = malloc(sizeof(Node));
    if (!n) { perror("malloc"); exit(1); }
    n->value = v;
    n->next  = NULL;
    return n;
}

Node *insert_head(Node *head, int v) {
    Node *n  = create_node(v);
    n->next  = head;
    return n;
}

void print_list(Node *head) {
    for (Node *c = head; c; c = c->next)
        printf("%d%s", c->value, c->next ? " -> " : " -> NULLn");
    if (!head) printf("NULLn");
}

void free_list(Node *head) {
    while (head) { Node *n = head->next; free(head); head = n; }
}

int main() {
    Node *list = NULL;

    for (int i = 1; i <= 5; i++)
        list = insert_head(list, i * 10);

    print_list(list);   /* 50 -> 40 -> 30 -> 20 -> 10 -> NULL */

    free_list(list);
    return 0;
}

Singly vs Doubly Linked List

Singly linked Doubly linked
Memory per node 1 pointer 2 pointers
Insert at head O(1) O(1)
Insert at tail O(n) O(1) with tail pointer
Delete by pointer O(n) — need prev node O(1)
Traverse backward Not possible O(n)

TL;DR

  • Each node is heap-allocated — free every node when done
  • The last node’s next is always NULL
  • Save next before calling free() during traversal
  • Head insertion is O(1); tail insertion is O(n) without a tail pointer
  • Use double pointers (Node**) if you want functions to modify the head in place