Skip to content
Learni
View all tutorials
Langage C

How to Implement a Linked List in C in 2026

Lire en français

Introduction

In 2026, C remains essential for embedded systems, kernels, and high-performance apps where manual memory management is crucial. Unlike modern languages like Rust or Go, C lacks built-in collections—you must implement your own data structures. The linked list (singly linked list) is a core intermediate tool, enabling O(1) insertions and deletions at the head, perfect for queues or dynamic stacks.

This tutorial walks you step-by-step through creating a linked list for integers: defining the Node struct, dynamic allocation with malloc, head/tail insertions, traversal, deletion, and full cleanup. Each step includes a complete, runnable program compilable with gcc. You'll learn to dodge memory leaks, a classic C trap. By the end, you'll have a battle-tested implementation every C pro bookmarks. Ready to code? (142 words)

Prerequisites

  • Basic C knowledge: variables, for/while loops, if statements.
  • Strong grasp of pointers and structs (struct).
  • Tools: GCC installed (gcc --version), editor like VS Code or Vim.
  • Compilation: gcc file.c -o exe && ./exe.
  • Estimated time: 20 minutes.

Step 1: Node Structure and Creating a Node

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

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

Node* create_node(int value) {
    Node* new_node = (Node*)malloc(sizeof(Node));
    if (new_node == NULL) {
        fprintf(stderr, "Erreur allocation mémoire\n");
        return NULL;
    }
    new_node->data = value;
    new_node->next = NULL;
    return new_node;
}

int main() {
    Node* head = create_node(42);
    if (head != NULL) {
        printf("Nœud créé : %d\n", head->data);
        free(head);
    }
    return 0;
}

This program defines the Node struct with int data and a next pointer. The create_node function dynamically allocates with malloc, initializes values, and checks for allocation failure. In main, it creates a standalone node, prints it, and frees it right away. Compile with gcc linked_list_creation.c -o creation && ./creation to test. Pitfall: always check malloc != NULL.

Understanding the Basics: Pointers and Allocation

Think of a node like a train car: data holds the cargo, next hooks to the next one. malloc reserves heap space (not stack) and returns a pointer. Skip free and you get a memory leak! This step lays the foundation: an empty list is head = NULL. Next up: head insertion, like adding a car to the front.

Step 2: Head Insertion

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

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

Node* create_node(int value) {
    Node* new_node = (Node*)malloc(sizeof(Node));
    if (new_node == NULL) {
        return NULL;
    }
    new_node->data = value;
    new_node->next = NULL;
    return new_node;
}

Node* insert_head(Node* head, int value) {
    Node* new_node = create_node(value);
    if (new_node == NULL) return head;
    new_node->next = head;
    return new_node;
}

void print_list(Node* head) {
    Node* current = head;
    while (current != NULL) {
        printf("%d -> ", current->data);
        current = current->next;
    }
    printf("NULL\n");
}

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

int main() {
    Node* head = NULL;
    head = insert_head(head, 10);
    head = insert_head(head, 20);
    head = insert_head(head, 30);
    print_list(head);
    free_list(head);
    return 0;
}

Adds insert_head: creates a new node, links its next to the old head, and returns the new head. print_list traverses with a while loop until NULL. free_list cleans up iteratively without recursion. Run it: outputs "30 -> 20 -> 10 -> NULL". O(1) efficiency, great for stacks.

Head Insertion: Why O(1)?

Head insertion is straightforward—no need to traverse the list. Analogy: adding a page to the front of a notebook. We also introduce print_list for visualization (-> simulates links) and free_list to prevent leaks. Note: head is passed by value, so return the new head for reassignment.

Step 3: Tail Insertion (End of List)

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

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

Node* create_node(int value) {
    Node* new_node = (Node*)malloc(sizeof(Node));
    if (new_node == NULL) return NULL;
    new_node->data = value;
    new_node->next = NULL;
    return new_node;
}

Node* insert_head(Node* head, int value) {
    Node* new_node = create_node(value);
    if (new_node == NULL) return head;
    new_node->next = head;
    return new_node;
}

Node* insert_tail(Node* head, int value) {
    Node* new_node = create_node(value);
    if (new_node == NULL || head == NULL) return head;
    Node* current = head;
    while (current->next != NULL) {
        current = current->next;
    }
    current->next = new_node;
    return head;
}

void print_list(Node* head) {
    Node* current = head;
    while (current != NULL) {
        printf("%d -> ", current->data);
        current = current->next;
    }
    printf("NULL\n");
}

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

int main() {
    Node* head = NULL;
    head = insert_tail(head, 10);
    head = insert_tail(head, 20);
    head = insert_tail(head, 30);
    print_list(head);
    free_list(head);
    return 0;
}

insert_tail traverses to the last node (O(n)), then links the new one. Handles empty lists. Test: "10 -> 20 -> 30 -> NULL". Useful for FIFO queues. Pitfall: don't crash on NULL head; return unchanged here.

Tail vs Head Insertion

  • Head: O(1), ideal for LIFO stacks.
  • Tail: O(n), simple without doubly linked lists.
For O(1) tail inserts, add a tail pointer (advanced).

Step 4: Deleting a Node by Value

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

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

Node* create_node(int value) {
    Node* new_node = (Node*)malloc(sizeof(Node));
    if (new_node == NULL) return NULL;
    new_node->data = value;
    new_node->next = NULL;
    return new_node;
}

Node* insert_head(Node* head, int value) {
    Node* new_node = create_node(value);
    if (new_node == NULL) return head;
    new_node->next = head;
    return new_node;
}

Node* delete_by_value(Node* head, int value) {
    if (head == NULL) return NULL;
    if (head->data == value) {
        Node* temp = head;
        head = head->next;
        free(temp);
        return head;
    }
    Node* current = head;
    while (current->next != NULL && current->next->data != value) {
        current = current->next;
    }
    if (current->next != NULL) {
        Node* temp = current->next;
        current->next = temp->next;
        free(temp);
    }
    return head;
}

void print_list(Node* head) {
    Node* current = head;
    while (current != NULL) {
        printf("%d -> ", current->data);
        current = current->next;
    }
    printf("NULL\n");
}

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

int main() {
    Node* head = NULL;
    head = insert_head(head, 30);
    head = insert_head(head, 20);
    head = insert_head(head, 10);
    printf("Avant : "); print_list(head);
    head = delete_by_value(head, 20);
    printf("Après suppression 20 : "); print_list(head);
    free_list(head);
    return 0;
}

delete_by_value handles head and mid/end cases: saves next before free to avoid dangling pointers. O(n) traversal. Test deletes 20: "30 -> 10 -> NULL". Key: always free the removed node!

Step 5: Complete Program with Interactive Menu

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

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

Node* create_node(int value) {
    Node* new_node = (Node*)malloc(sizeof(Node));
    if (new_node == NULL) return NULL;
    new_node->data = value;
    new_node->next = NULL;
    return new_node;
}

Node* insert_head(Node* head, int value) {
    Node* new_node = create_node(value);
    if (new_node == NULL) return head;
    new_node->next = head;
    return new_node;
}

Node* insert_tail(Node* head, int value) {
    Node* new_node = create_node(value);
    if (new_node == NULL || head == NULL) return insert_head(head, value);
    Node* current = head;
    while (current->next != NULL) current = current->next;
    current->next = new_node;
    return head;
}

Node* delete_by_value(Node* head, int value) {
    if (head == NULL) return NULL;
    if (head->data == value) {
        Node* temp = head; head = head->next; free(temp); return head;
    }
    Node* current = head;
    while (current->next && current->next->data != value) current = current->next;
    if (current->next) {
        Node* temp = current->next; current->next = temp->next; free(temp);
    }
    return head;
}

void print_list(Node* head) {
    if (head == NULL) { printf("Liste vide\n"); return; }
    Node* current = head;
    while (current) {
        printf("%d -> ", current->data);
        current = current->next;
    }
    printf("NULL\n");
}

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

int main() {
    Node* head = NULL;
    int choice, value;
    do {
        printf("\n1. Insert tête | 2. Insert queue | 3. Supprimer | 4. Afficher | 0. Quitter\nChoix: ");
        scanf("%d", &choice);
        switch(choice) {
            case 1: printf("Valeur: "); scanf("%d", &value); head = insert_head(head, value); break;
            case 2: printf("Valeur: "); scanf("%d", &value); head = insert_tail(head, value); break;
            case 3: printf("Valeur à supp: "); scanf("%d", &value); head = delete_by_value(head, value); break;
            case 4: print_list(head); break;
        }
    } while (choice != 0);
    free_list(head);
    return 0;
}

Interactive program with switch menu tests all operations. insert_tail falls back to head for empty lists. do-while loop runs until 0. Robust for testing. Run and play: insert 1,2,3, delete 2.

Best Practices

  • Always check malloc: if (!ptr) return NULL; prevents segfaults.
  • Free systematically: Call free_list at end of main.
  • Save next before free: temp = current->next; free(current);.
  • Handle empty lists: if (head == NULL) return; everywhere.
  • Compile with warnings: gcc -Wall -Wextra -std=c11 file.c -o exe to catch issues.

Common Errors to Avoid

  • Memory leaks: Forget free → Use Valgrind (valgrind ./exe) to detect.
  • Dangling pointers: Access after free → segfault.
  • NULL dereference: head->data without check → crash.
  • Infinite loops: while (current->next) without != NULL.
  • scanf issues: Add fflush(stdin); if inputs glitch (rare in modern C).

Next Steps

  • Implement doubly linked or circular lists.
  • Optimize with tail pointer for O(1) tail inserts.
  • Use in projects: minimal JSON parser or queue simulator.
  • Resources: K&R C (2nd ed.), Beej's Guide to C.
  • Check out our Learni courses on embedded C and systems.