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/whileloops,ifstatements. - 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
#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
#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)
#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.
tail pointer (advanced).Step 4: Deleting a Node by Value
#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
#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_listat end ofmain. - 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 exeto 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->datawithout 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
tailpointer 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.