C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
151: Implementing a Stack with Linked Lists
When you first move from array-based stacks to linked-list stacks, the logic feels intuitive: you just keep adding nodes to the front. But C's handling of pointers often leads to a very specific, frustrating bug where your stack simply... doesn't change. Or worse, it crashes the next time you try to access it.
Take a look at this pop implementation. It looks logically sound at first glance, but it's broken in a way that will keep you up until 3 AM if you aren't looking for it.
typedef struct Node {
int data;
struct Node* next;
} Node;
void pop(Node* head) {
if (head == NULL) return;
Node* temp = head;
head = head->next;
free(temp);
printf("Popped successfully!\n");
}
int main() {
Node* stack = push(stack, 10);
stack = push(stack, 20);
pop(stack); // We want to remove 20
// Now we check the stack... but wait.
}
The Pass-by-Value Trap
If you run this, you'll notice something weird. You call pop(stack), the function says it worked, but when you look at stack in main, it's still pointing to the node containing 20. Then, the moment you try to access that node, the program segfaults. Why?
Because in C, everything is passed by value. When you pass stack into pop(Node* head), the function creates a local copy of the pointer. Inside the function, head = head->next successfully moves that local copy to the next node. But it does absolutely nothing to the stack pointer back in main.
You've effectively freed the memory that stack is still pointing to. You've created a dangling pointer. This is a classic "C rite of passage."
Updating the Caller with Double Pointers
To fix this, we need to tell the pop function where the actual stack pointer lives in memory, not just what address it currently holds. We do this by passing a pointer to the pointer (Node**).
void pop(Node** head_ref) {
if (*head_ref == NULL) {
printf("Stack Underflow\n");
return;
}
// Store the current head so we can free it
Node* temp = *head_ref;
// Update the actual pointer in main to point to the next node
*head_ref = (*head_ref)->next;
free(temp);
}
// Usage in main:
pop(&stack); // Pass the address of the pointer
By using Node** head_ref, we are dereferencing the pointer to reach back into main's stack frame and modify the original stack variable. Now, when *head_ref = (*head_ref)->next executes, the change persists after the function returns.
Managing the Push Logic
Implementing push is generally safer because you can simply return the new head. I personally prefer this pattern over double pointers for push because it makes the data flow more explicit. You're telling the caller: "Here is the new state of your stack."
Node* push(Node* head, int value) {
Node* newNode = malloc(sizeof(Node));
if (!newNode) return head; // Always check for malloc failure
newNode->data = value;
newNode->next = head; // Link new node to the old top
return newNode; // This becomes the new head
}
The beauty of the linked list approach here is that you're no longer bound by a fixed array size. Your stack can grow until the system runs out of heap memory. Just remember: every malloc in your push must eventually be balanced by a free in your pop, or you'll leak memory like a sieve.
📋 Practical Task
Build a Browser History "Back" Buffer
Create a program that simulates the "Back" button of a web browser using a linked-list stack. Your program should support the following operations via a simple menu loop:
- Visit Page: (Push) Takes a string (URL) and pushes it onto the stack.
- Go Back: (Pop) Removes the current page and displays the URL of the page you've returned to. If the stack becomes empty, notify the user that they have reached the start of their history.
- View Current: (Peek) Displays the URL at the top of the stack without removing it.
Requirements:
1. Use a struct that handles strings (either via char arrays or char*).
2. Implement the pop function using a double pointer (Node**) to ensure the history is updated correctly in the main loop.
3. Ensure all dynamically allocated memory is freed before the program exits.
There are no comments for now.