{"id":66,"date":"2026-02-08T19:05:52","date_gmt":"2026-02-08T19:05:52","guid":{"rendered":"https:\/\/mattbick.dev\/?p=66"},"modified":"2026-02-08T19:05:52","modified_gmt":"2026-02-08T19:05:52","slug":"relearning-c-after-time-away-building-a-new-data-structure-every-day-day-1-tailless-linked-list","status":"publish","type":"post","link":"https:\/\/mattbick.dev\/?p=66","title":{"rendered":"Relearning C After Time Away \u2014 Building a New Data Structure Every Day (Day 1: Tailless Linked List)"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">I feel like I keep harping on my time as a Computer Engineering student, but I think it is important to keep reiterating the fact that we truly only narrow in on certain languages and lines of thinking, and then we just stop. For ECE students, that means learning a few basic C++ principles, a bit of ARM\/MIPS Assembly, and then a little sprinkle of C whenever we take Operating Systems.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The class that stuck with me the most was not Data Structures, intro to C++, or any of that. Instead, it was Operating Systems. Never before had I really delved deep into systems programming, kernel development, or anything of the sort. However, my professor for this class, Dr. Hartmut Kaiser, was extremely skillful in taking the tiny bit of C++ we learned as ECEs and slowly introducing us to the wild-west of C systems programming and kernel development. He still goes down as THE BEST professor I have had the pleasure of learning under to this day, and it really does go to show just how influential a single class\/professor can be on a student&#8217;s development. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">But that was over a year ago, and since then I have really only been messing with .NET code for my internship and a fair bit of PHP for FFLHub. Throughout all this time, I always wanted to revisit the principles and ideas that I learned in that class. Therefore, I have decided to settle on a simple challenge for myself: <\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>Relearn C by implementing one data structure from scratch every day.<\/strong><\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">No libraries.<br>No header helpers.<br>No hiding memory behavior behind abstractions.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Just raw C, deliberate design, and intentional tradeoffs.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Today is <strong>Day 1<\/strong>, and we\u2019re starting with something classic but deceptively deep:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>A tailless singly linked list.<\/strong><\/p>\n<\/blockquote>\n\n\n\n<h1 class=\"wp-block-heading\">Why Start With a Tailless Linked List?<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Because it forces you to think about:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Pointer mutation<\/li>\n\n\n\n<li>Memory ownership<\/li>\n\n\n\n<li>Traversal cost<\/li>\n\n\n\n<li>Edge cases (empty list, single node list)<\/li>\n\n\n\n<li>Allocation failure handling<\/li>\n\n\n\n<li>Free order correctness<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">And importantly \u2014 it\u2019s incomplete in ways that matter.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We are <strong>not<\/strong> using:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Tail pointer<\/li>\n\n\n\n<li>Size tracking<\/li>\n\n\n\n<li>Container wrapper struct<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">And that\u2019s intentional. Because in later posts, we\u2019ll add those and see exactly just how those things aid us in complexity times and such. But first, a few ground rules and general design principles&#8230; <\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Memory Model \u2014 Caller Owned<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">This implementation uses a <strong>caller-owned memory model<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That means:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The list:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Does NOT automatically allocate nodes<\/li>\n\n\n\n<li>Does NOT automatically free nodes<\/li>\n\n\n\n<li>Does NOT manage payload lifetime beyond copying it into the node<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The caller:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Allocates nodes<\/li>\n\n\n\n<li>Inserts nodes<\/li>\n\n\n\n<li>Removes nodes<\/li>\n\n\n\n<li>Destroys nodes<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Why?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Because this is extremely common in systems programming. It gives maximum flexibility and makes ownership boundaries explicit.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Node Structure:<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Each node stores:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Pointer to payload<\/li>\n\n\n\n<li>Size of payload<\/li>\n\n\n\n<li>Pointer to next node<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">This allows the list to store <strong>heterogeneous data<\/strong> \u2014 not just one fixed type.<\/p>\n\n\n\n<pre data-line=\"\" class=\"wp-block-prismatic-blocks\"><code class=\"language-c\">\/\/For this linked list, I am using a caller owned approach, aka, all memeory is allocated and freed by the caller, not the list or list node itself\nstruct Node {\n        void* data;\n        size_t data_size;\n        struct Node* next;\n};<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Allocation and Freeing Strategy:<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Allocation happens in two steps:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Allocate node struct<\/li>\n\n\n\n<li>Allocate payload buffer<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">If payload allocation fails, we roll back and free the node.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This avoids partial allocations and leaks.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I also implemented a function that allows us to allocate and populate a node with data: <\/p>\n\n\n\n<pre data-line=\"\" class=\"wp-block-prismatic-blocks\"><code class=\"language-c\">\/\/this is how we allocate the memory for nodes\nstruct Node* alloc_node(size_t data_size) {\n\n        \/\/first, we allocate the memory required for the base data size of each node\n        struct Node* node = malloc(sizeof *node);\n        if (!node) return NULL; \/\/if malloc fails, return NULL\n\n        \/\/second, we allocate the memory for the data stored inside our node using our data size passed in\n        node-&gt;data = malloc(data_size);\n\n        \/\/if that malloc fails, we free the previous base allocation and return null\n        if (!node-&gt;data) {\n                free(node);\n                return NULL;\n        }\n\n        \/\/set the data size member\n        node-&gt;data_size = data_size;\n\n        \/\/make sure the next pointer is nll\n        node-&gt;next = NULL;\n\n        \/\/return the allocation pointer\n        return node;\n}\n\n\/\/we simply allocate the node using our function above, then we memcpy the data into the node\nstruct Node* create_node_from_data(void* data, size_t data_size) {\n        struct Node* node = alloc_node(data_size);\n        if (!node) return NULL;\n        memcpy(node-&gt;data, data, data_size);\n        return node;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">And for freeing, we free in <strong>reverse allocation order<\/strong>:<\/p>\n\n\n\n<pre data-line=\"\" class=\"wp-block-prismatic-blocks\"><code class=\"language-c\">\/\/we free a singular node\nvoid free_node(struct Node* node) {\n        if (!node) return; \/\/if node is invalid, return\n\n        \/\/we only want to free the data portion of our node if the data is valid, IE its been allocated properly\n        if (node-&gt;data) {\n                free(node-&gt;data);\n                node-&gt;data = NULL;\n        }\n\n        \/\/only after this we free the memeory allocated for the base node size\n        \/\/we deallocate in REVERSE order of allocation\n        free(node);\n}<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\">Using these functions, we can create general Linked List helper functions that free and destroy the entire list: <\/p>\n\n\n\n<pre data-line=\"\" class=\"wp-block-prismatic-blocks\"><code class=\"language-c\">\/\/free the linked list using our free node function, we step through the linked list\n\/\/we save the next ptr, then free that node, then we use that next ptr to iterate to the next node\nvoid free_list(struct Node* head) {\n        while (head) {\n                struct Node* next = head-&gt;next;\n                free_node(head);\n                head = next;\n        }\n}\n\n\/\/uses the method above then also sets the base pointer to null to avoid a dangling pointer\nvoid list_destroy(struct Node** head) {\n        if (!head || !*head) return;\n\n        free_list(*head);\n        *head = NULL;\n}\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Core Operations Implemented<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">I also wrote some functions to handle the most basic of operations that you would want to perform on a linked list:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Insertion<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>push_front<\/code> \u2014 add a node to the front of the linked list <\/li>\n\n\n\n<li><code>push_back<\/code> \u2014 add a node to the back of the linked list <\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Removal<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>pop_front<\/code> \u2014 remove a node from the front of the linked list <\/li>\n\n\n\n<li><code>pop_back<\/code> \u2014 remove a node from the back of the linked list <\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Access<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>front<\/code> \u2014 grab the head of the linked list <\/li>\n\n\n\n<li><code>back<\/code> \u2014 grab the tail of the linked list <\/li>\n<\/ul>\n\n\n\n<pre data-line=\"\" class=\"wp-block-prismatic-blocks\"><code class=\"language-c\">void push_front(struct Node* new_head, struct Node** head) {\n        \/\/if our new head is invalid, or if our current head pointer is invalid, early exit\n        if (new_head == NULL || head == NULL) return;\n        new_head-&gt;next = *head; \/\/the new heads next pointer must point to the old head\n        *head = new_head;       \/\/now we set the head equal to our new head that we want\n}\n\nvoid pop_front(struct Node** head) {\n        if (head == NULL || *head == NULL) return; \/\/if list is empty or if the head pointer is NULL, early exit\n        struct Node* new_head = (*head)-&gt;next;     \/\/we store the new head ptr, which is the node that occurs after the current head\n        free_node(*head);                          \/\/free the old head node, deallocate its memeory\n        *head = new_head;                          \/\/set the head pointer equal to the new head we want, aka the next node in the list\n}\n\nvoid push_back(struct Node* new_tail, struct Node** head) {\n\n        if (head == NULL || new_tail == NULL) return; \/\/invalid head or tail node, retunrn early\n        \/\/\n        \/\/\/\/if list is empty, then the head is just the node we are appending\n        if (*head == NULL) {\n                *head = new_tail;\n                new_tail-&gt;next = NULL;\n                return;\n        }\n\n        \/\/otherwise, we have to iterate through the entire loop\n        struct Node* cur = (*head);\n\n        \/\/we need to keep iterating until we reach the end, IE the next pointer is NULL\n        while (cur-&gt;next != NULL) cur = cur-&gt;next;\n\n        \/\/once we are at the end, set the next pointer to point to our new tail node\n        cur-&gt;next = new_tail;\n        new_tail-&gt;next = NULL;\n}\n\nvoid pop_back(struct Node** head) {\n        if (head == NULL) return; \/\/invalid header pointer, return early\n\n        if (*head == NULL) return; \/\/list is empty, nothing to pop\n\n        \/\/if we have a one node list, we need to handle that situation since our while loop iterator checks TWO nodes ahead, not one like the above function\n        if ((*head)-&gt;next == NULL) {\n                free_node(*head);\n                *head = NULL;\n                return;\n        }\n\n        \/\/same iteration as the function above, except we check TWO nodes ahead\n        struct Node* prev = (*head);\n        while (prev-&gt;next-&gt;next != NULL) prev = prev-&gt;next;\n\n        \/\/okay so now that we are at the end of the node, we have to free the node\n        free_node(prev-&gt;next);\n        prev-&gt;next = NULL;\n}\n\nstruct Node* front(struct Node* head) {\n        if (head == NULL) return NULL; \/\/invalid head pointer, return NULL, or if the list is empty, return null\n        return head;                   \/\/so yeah even if *head is NULL itll still return NULL here, but whateever its extra safe I guess\n}\n\nstruct Node* back(struct Node* head) {\n        if (head == NULL) return NULL;       \/\/invalid head pointer, return NULL\n        if (head-&gt;next == NULL) return head; \/\/we are already at the endm return head - like before, this is redudant, but oh well maybe the compiler will make something useful out of this\n\n        \/\/now we iterate through the entire list until we reach the end\n        struct Node* cur = head;\n        while (cur-&gt;next != NULL) cur = cur-&gt;next;\n\n        return cur;\n}<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\">Now, the astute among you probably see why I mentioned we are making a TAILLESS linked list. Because we don&#8217;t store the tail of our linked list, any function that has to do with mutating the end of the our linked list has to traverse the entire list first. This yields the following complexity charge for each function we implemented: <\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Complexity Table (Tailless List)<\/h1>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Operation<\/th><th>Complexity<\/th><\/tr><\/thead><tbody><tr><td>push_front<\/td><td>O(1)<\/td><\/tr><tr><td>pop_front<\/td><td>O(1)<\/td><\/tr><tr><td>front<\/td><td>O(1)<\/td><\/tr><tr><td>push_back<\/td><td>O(n)<\/td><\/tr><tr><td>pop_back<\/td><td>O(n)<\/td><\/tr><tr><td>back<\/td><td>O(n)<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>We get a O(n) complexity for every tail associated function. In the next post, we will add a tail and see what this yields (hint: it makes for a super boring chart). <\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\">And to top everything off, I wrote a little test-bed that tests our functions:<\/p>\n\n\n\n<pre data-line=\"\" class=\"wp-block-prismatic-blocks\"><code class=\"language-c\">int main(void) {\n        const int MAX_ITERS = 10000; \/\/ change this one value to scale all tests\n        struct Node* head = NULL;\n\n        printf(&quot;=== Linked List Stress Test (MAX_ITERS=%d) ===\\n\\n&quot;, MAX_ITERS);\n\n        \/\/ -------------------------\n        \/\/ TEST 1: push_front\n        \/\/ -------------------------\n        printf(&quot;TEST 1: push_front %d ints (0..%d)\\n&quot;, MAX_ITERS, MAX_ITERS - 1);\n        for (int i = 0; i &lt; MAX_ITERS; i++) {\n                int v = i;\n                struct Node* n = create_node_from_data(&amp;v, sizeof v);\n                if (!n) {\n                        printf(&quot;alloc failed in TEST 1 at i=%d\\n&quot;, i);\n                        list_destroy(&amp;head);\n                        return 1;\n                }\n                push_front(n, &amp;head);\n        }\n\n        struct Node* f = front(head);\n        struct Node* b = back(head);\n        if (!f || !b) {\n                printf(&quot;FAIL: front\/back returned NULL after push_front\\n&quot;);\n                list_destroy(&amp;head);\n                return 1;\n        }\n\n        int front_v = *(int*) f-&gt;data;\n        int back_v = *(int*) b-&gt;data;\n\n        printf(&quot;Check: front=%d (expected %d), back=%d (expected %d)\\n&quot;,\n               front_v, MAX_ITERS - 1, back_v, 0);\n\n        if (front_v != MAX_ITERS - 1 || back_v != 0) {\n                printf(&quot;FAIL: unexpected front\/back values in TEST 1\\n&quot;);\n                list_destroy(&amp;head);\n                return 1;\n        }\n\n        \/\/ Pop everything and ensure order is descending MAX_ITERS-1 .. 0\n        printf(&quot;Pop all with pop_front and verify descending order...\\n&quot;);\n        for (int expected = MAX_ITERS - 1; expected &gt;= 0; expected--) {\n                struct Node* cur = front(head);\n                if (!cur) {\n                        printf(&quot;FAIL: list became empty early at expected=%d\\n&quot;, expected);\n                        list_destroy(&amp;head);\n                        return 1;\n                }\n\n                int cur_v = *(int*) cur-&gt;data;\n                if (cur_v != expected) {\n                        printf(&quot;FAIL: pop_front order mismatch: got %d expected %d\\n&quot;, cur_v, expected);\n                        list_destroy(&amp;head);\n                        return 1;\n                }\n\n                pop_front(&amp;head);\n        }\n\n        if (head != NULL) {\n                printf(&quot;FAIL: head not NULL after popping all nodes in TEST 1\\n&quot;);\n                list_destroy(&amp;head);\n                return 1;\n        }\n\n        printf(&quot;PASS: TEST 1\\n\\n&quot;);\n\n        \/\/ -------------------------\n        \/\/ TEST 2: push_back\n        \/\/ -------------------------\n        printf(&quot;TEST 2: push_back %d ints (0..%d)\\n&quot;, MAX_ITERS, MAX_ITERS - 1);\n        for (int i = 0; i &lt; MAX_ITERS; i++) {\n                int v = i;\n                struct Node* n = create_node_from_data(&amp;v, sizeof v);\n                if (!n) {\n                        printf(&quot;alloc failed in TEST 2 at i=%d\\n&quot;, i);\n                        list_destroy(&amp;head);\n                        return 1;\n                }\n                push_back(n, &amp;head);\n        }\n\n        f = front(head);\n        b = back(head);\n        if (!f || !b) {\n                printf(&quot;FAIL: front\/back returned NULL after push_back\\n&quot;);\n                list_destroy(&amp;head);\n                return 1;\n        }\n\n        front_v = *(int*) f-&gt;data;\n        back_v = *(int*) b-&gt;data;\n\n        printf(&quot;Check: front=%d (expected %d), back=%d (expected %d)\\n&quot;,\n               front_v, 0, back_v, MAX_ITERS - 1);\n\n        if (front_v != 0 || back_v != MAX_ITERS - 1) {\n                printf(&quot;FAIL: unexpected front\/back values in TEST 2\\n&quot;);\n                list_destroy(&amp;head);\n                return 1;\n        }\n\n        \/\/ Pop everything and ensure order is descending from the back: MAX_ITERS-1 .. 0\n        printf(&quot;Pop all with pop_back and verify descending-from-back order...\\n&quot;);\n        for (int expected = MAX_ITERS - 1; expected &gt;= 0; expected--) {\n                struct Node* cur = back(head);\n                if (!cur) {\n                        printf(&quot;FAIL: list became empty early at expected=%d\\n&quot;, expected);\n                        list_destroy(&amp;head);\n                        return 1;\n                }\n\n                int cur_v = *(int*) cur-&gt;data;\n                if (cur_v != expected) {\n                        printf(&quot;FAIL: pop_back order mismatch: got %d expected %d\\n&quot;, cur_v, expected);\n                        list_destroy(&amp;head);\n                        return 1;\n                }\n\n                pop_back(&amp;head);\n        }\n\n        if (head != NULL) {\n                printf(&quot;FAIL: head not NULL after popping all nodes in TEST 2\\n&quot;);\n                list_destroy(&amp;head);\n                return 1;\n        }\n\n        printf(&quot;PASS: TEST 2\\n\\n&quot;);\n\n        \/\/ -------------------------\n        \/\/ TEST 3: mixed operations with exact expectations\n        \/\/ -------------------------\n        printf(&quot;TEST 3: mixed ops with exact expected front\/back\\n&quot;);\n\n        \/\/ Step A: build 0..MAX_ITERS-1 using push_back\n        for (int i = 0; i &lt; MAX_ITERS; i++) {\n                int v = i;\n                struct Node* n = create_node_from_data(&amp;v, sizeof v);\n                if (!n) {\n                        printf(&quot;alloc failed in TEST 3A at i=%d\\n&quot;, i);\n                        list_destroy(&amp;head);\n                        return 1;\n                }\n                push_back(n, &amp;head);\n        }\n\n        \/\/ Step B: pop_front K times (removes 0..K-1)\n        const int K = MAX_ITERS \/ 10; \/\/ 10% of max iterations\n        for (int expected = 0; expected &lt; K; expected++) {\n                struct Node* cur = front(head);\n                if (!cur) {\n                        printf(&quot;FAIL: empty early during TEST 3B at expected=%d\\n&quot;, expected);\n                        list_destroy(&amp;head);\n                        return 1;\n                }\n                int cur_v = *(int*) cur-&gt;data;\n                if (cur_v != expected) {\n                        printf(&quot;FAIL: TEST 3B pop_front mismatch: got %d expected %d\\n&quot;, cur_v, expected);\n                        list_destroy(&amp;head);\n                        return 1;\n                }\n                pop_front(&amp;head);\n        }\n\n        \/\/ After popping K from front, expected front is K, expected back is MAX_ITERS-1\n        f = front(head);\n        b = back(head);\n        if (!f || !b) {\n                printf(&quot;FAIL: front\/back NULL after TEST 3B\\n&quot;);\n                list_destroy(&amp;head);\n                return 1;\n        }\n        if (*(int*) f-&gt;data != K || *(int*) b-&gt;data != MAX_ITERS - 1) {\n                printf(&quot;FAIL: after TEST 3B expected front=%d back=%d, got front=%d back=%d\\n&quot;,\n                       K, MAX_ITERS - 1, *(int*) f-&gt;data, *(int*) b-&gt;data);\n                list_destroy(&amp;head);\n                return 1;\n        }\n\n        \/\/ Step C: push_front K values (100000..100000+K-1)\n        \/\/ Because push_front reverses order, final front should be 100000+K-1\n        for (int i = 0; i &lt; K; i++) {\n                int v = 100000 + i;\n                struct Node* n = create_node_from_data(&amp;v, sizeof v);\n                if (!n) {\n                        printf(&quot;alloc failed in TEST 3C at i=%d\\n&quot;, i);\n                        list_destroy(&amp;head);\n                        return 1;\n                }\n                push_front(n, &amp;head);\n        }\n\n        f = front(head);\n        b = back(head);\n        if (!f || !b) {\n                printf(&quot;FAIL: front\/back NULL after TEST 3C\\n&quot;);\n                list_destroy(&amp;head);\n                return 1;\n        }\n        int expected_front = 100000 + (K - 1);\n        int expected_back = MAX_ITERS - 1;\n        if (*(int*) f-&gt;data != expected_front || *(int*) b-&gt;data != expected_back) {\n                printf(&quot;FAIL: after TEST 3C expected front=%d back=%d, got front=%d back=%d\\n&quot;,\n                       expected_front, expected_back, *(int*) f-&gt;data, *(int*) b-&gt;data);\n                list_destroy(&amp;head);\n                return 1;\n        }\n\n        \/\/ Step D: pop_back K times (removes MAX_ITERS-1 down to MAX_ITERS-K)\n        for (int j = 0; j &lt; K; j++) {\n                int expected = (MAX_ITERS - 1) - j;\n                struct Node* cur = back(head);\n                if (!cur) {\n                        printf(&quot;FAIL: empty early during TEST 3D at expected=%d\\n&quot;, expected);\n                        list_destroy(&amp;head);\n                        return 1;\n                }\n                int cur_v = *(int*) cur-&gt;data;\n                if (cur_v != expected) {\n                        printf(&quot;FAIL: TEST 3D pop_back mismatch: got %d expected %d\\n&quot;, cur_v, expected);\n                        list_destroy(&amp;head);\n                        return 1;\n                }\n                pop_back(&amp;head);\n        }\n\n        \/\/ After popping K from back, expected back is MAX_ITERS-K-1\n        f = front(head);\n        b = back(head);\n        if (!f || !b) {\n                printf(&quot;FAIL: front\/back NULL after TEST 3D\\n&quot;);\n                list_destroy(&amp;head);\n                return 1;\n        }\n        expected_front = 100000 + (K - 1);\n        expected_back = (MAX_ITERS - K - 1);\n        if (*(int*) f-&gt;data != expected_front || *(int*) b-&gt;data != expected_back) {\n                printf(&quot;FAIL: after TEST 3D expected front=%d back=%d, got front=%d back=%d\\n&quot;,\n                       expected_front, expected_back, *(int*) f-&gt;data, *(int*) b-&gt;data);\n                list_destroy(&amp;head);\n                return 1;\n        }\n\n        \/\/ Cleanup\n        list_destroy(&amp;head);\n        if (head != NULL) {\n                printf(&quot;FAIL: head not NULL after list_destroy at end\\n&quot;);\n                return 1;\n        }\n\n        printf(&quot;PASS: TEST 3\\n\\n&quot;);\n        printf(&quot;ALL TESTS PASSED!\\n&quot;);\n        return 0;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Which yields the following result:<\/p>\n\n\n\n<pre data-line=\"\" class=\"wp-block-prismatic-blocks\"><code class=\"language-\">=== Linked List Stress Test (MAX_ITERS=10000) ===\n\nTEST 1: push_front 10000 ints (0..9999)\nCheck: front=9999 (expected 9999), back=0 (expected 0)\nPop all with pop_front and verify descending order...\nPASS: TEST 1\n\nTEST 2: push_back 10000 ints (0..9999)\nCheck: front=0 (expected 0), back=9999 (expected 9999)\nPop all with pop_back and verify descending-from-back order...\nPASS: TEST 2\n\nTEST 3: mixed ops with exact expected front\/back\nPASS: TEST 3\n\nALL TESTS PASSED!<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Retrospect on this challenge:<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Like I said before, I mainly deal with C#, web-code, etc. We rarely have to actually think about the memory we are allocating because all of that is handled behind the scenes for us whether it be through GC or other means (Rust uses a borrow checker that I want to mess with at some point). <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">C makes you honest.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It makes you think about:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Where memory comes from<\/li>\n\n\n\n<li>Who owns memory<\/li>\n\n\n\n<li>When memory dies<\/li>\n\n\n\n<li>What pointer actually means<\/li>\n\n\n\n<li>How data really moves through systems<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">And honestly \u2014 it\u2019s refreshing.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Closing<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">If you haven\u2019t touched C in a while, I highly recommend doing something like this.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Pick a data structure.<br>Write it from scratch.<br>Test it hard.<br>Then improve it iteratively.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Tomorrow:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">Another data structure.<br>More pointer pain.<br>More learning.<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>I feel like I keep harping on my time as a Computer Engineering student, but I think it is important to keep reiterating the fact that we truly only narrow in on certain languages and lines of thinking, and then we just stop. For ECE students, that means learning a few basic C++ principles, a [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-66","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/mattbick.dev\/index.php?rest_route=\/wp\/v2\/posts\/66","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/mattbick.dev\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mattbick.dev\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/mattbick.dev\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/mattbick.dev\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=66"}],"version-history":[{"count":3,"href":"https:\/\/mattbick.dev\/index.php?rest_route=\/wp\/v2\/posts\/66\/revisions"}],"predecessor-version":[{"id":69,"href":"https:\/\/mattbick.dev\/index.php?rest_route=\/wp\/v2\/posts\/66\/revisions\/69"}],"wp:attachment":[{"href":"https:\/\/mattbick.dev\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=66"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mattbick.dev\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=66"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mattbick.dev\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=66"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}