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
nextis always NULL - Save
nextbefore callingfree()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