Lesson 06 / 15
Linked Structures
A C implementation of a linked list and a binary search tree; measuring how changing a node through an alias and a shallow copy produce shared visibility, while a deep copy cuts it.
Published Updated
In this lesson
- Implements a linked list and a binary search tree in C with insertion, traversal, and release operations
- Measures, without printing addresses, the node a pointer shares through an alias, using equality and value
- Shows the visibility difference between a shallow copy and a deep copy (shared/copy) on the same list
Contents
The previous lesson measured who releases a block of memory. This course starts with a
different axis: who sees a change when you modify a node. When two different pointers
hold the same block of memory, a change made through one is visible through the other —
the source does not say so, because both variables have the identical declaration
(struct Node *). The linked list is the clearest example of this sharing: the field
that leads to the next node carries a pointer, and whoever holds that pointer sees the
same node.
This lesson builds the linked list and the binary search tree from scratch and compares two of the four classes, shared and copy, on the same structure: accessing and changing a node through an alias produces shared visibility (shared); duplicating the entire list with new nodes cuts that sharing (copy). The previous course’s (Memory and Pointers in C) question of “who releases this node” still applies here — releasing the tree still honors that contract — but the axis this lesson measures is different: not ownership, visibility.
Linked List: Insertion, Traversal, Release
The list is singly linked, and every insertion happens at the head. list_total walks
every node and sums their values; list_free saves the next node before releasing each
one. list_shallow_copy does not allocate a new node — it returns the incoming head as
is, so the list it returns shares the same nodes as the original. list_deep_copy
instead allocates a new node for each one and copies its value; the list it returns owns
an independent chain of memory blocks.
// node.h — singly linked list and basic operations
#ifndef NODE_H
#define NODE_H
struct Node {
int value;
struct Node *next;
};
struct Node *list_insert(struct Node *head, int value);
int list_total(const struct Node *head);
void list_free(struct Node *head);
struct Node *list_shallow_copy(struct Node *head);
struct Node *list_deep_copy(const struct Node *head);
#endif
// node.c — implementation of the list operations
#include <stdlib.h>
#include "node.h"
struct Node *list_insert(struct Node *head, int value) {
struct Node *node = malloc(sizeof *node);
if (node == NULL) {
return head;
}
node->value = value;
node->next = head;
return node;
}
int list_total(const struct Node *head) {
int total = 0;
for (const struct Node *n = head; n != NULL; n = n->next) {
total += n->value;
}
return total;
}
void list_free(struct Node *head) {
while (head != NULL) {
struct Node *next = head->next;
free(head);
head = next;
}
}
struct Node *list_shallow_copy(struct Node *head) {
return head;
}
struct Node *list_deep_copy(const struct Node *head) {
struct Node *new_head = NULL;
struct Node *tail = NULL;
for (const struct Node *n = head; n != NULL; n = n->next) {
struct Node *node = malloc(sizeof *node);
if (node == NULL) {
list_free(new_head);
return NULL;
}
node->value = n->value;
node->next = NULL;
if (tail == NULL) {
new_head = node;
} else {
tail->next = node;
}
tail = node;
}
return new_head;
}
A Node Changed Through an Alias
The list is built with the values 1, 2, 3. The pointer second points at the second
node — not a new node, but one of the list’s own nodes. A change made through second
is reflected in the list’s total, because both point at the same block of memory.
// alias.c — accessing the list's own node through a second pointer
#include <stdio.h>
#include "node.h"
int main(void) {
struct Node *head = NULL;
head = list_insert(head, 3);
head = list_insert(head, 2);
head = list_insert(head, 1);
printf("%s %d\n", "total before changing through the alias", list_total(head));
struct Node *second = head->next;
second->value = 20;
printf("%s %d\n", "total after changing through the alias", list_total(head));
printf("%s %d\n", "is second the same node as head->next", (int)(second == head->next));
list_free(head);
return 0;
}
total before changing through the alias 6 total after changing through the alias 24 is second the same node as head->next 1
DS1. The second variable is of type struct Node *, and so is head — nothing in
the source says anywhere whether second points at the list’s own node or an
independent copy. What decides this is not the declaration but the assignment itself:
second = head->next is not a copy of a value but a copy of a pointer, meaning
second and head->next carry the same address. The line second->value = 20
therefore also changes the total read through head: from 6 to 24. The last line
confirms this without printing an address — the comparison second == head->next gives
1, showing that the two pointers point at the same node. The class is shared: a
second reader (here, the call to list_total through head) sees the change, because
there is a single shared object between them, not two copied objects.
Shallow Copy, Deep Copy
list_shallow_copy does not allocate even a single node — it returns the head it
receives as is. Because of this, the pointer it returns and the original list point at
the same nodes, and a change made through one is visible through the other.
list_deep_copy allocates a new node for each one; the list it returns carries the
values as they were at the moment of copying, but afterward shares no node with the
original.
// copy_comparison.c — shallow copy shares, deep copy cuts the sharing
#include <stdio.h>
#include "node.h"
int main(void) {
struct Node *head = NULL;
head = list_insert(head, 3);
head = list_insert(head, 2);
head = list_insert(head, 1);
struct Node *shallow = list_shallow_copy(head);
head->next->value = 200;
printf("%s %d\n", "second value seen by the shallow copy", shallow->next->value);
struct Node *deep = list_deep_copy(head);
head->next->value = 2000;
printf("%s %d\n", "second value seen by the deep copy", deep->next->value);
list_free(deep);
list_free(head);
return 0;
}
second value seen by the shallow copy 200 second value seen by the deep copy 200
DS2. Both lines print the same number (200), but for different reasons, and that
difference is what this lesson measures. On the first line, shallow points at the same
nodes as head; the assignment head->next->value = 200 also changes
shallow->next->value, because both look at a single object — the class is shared.
On the second line, deep was built by allocating new nodes at the moment of copying
(while the second node still carried the value 200); the assignment
head->next->value = 2000, made after the copy, does not affect deep, because
deep’s nodes are independent memory blocks from head’s. deep->next->value still
carries the 200 from the moment of copying, not 2000 — the class is copy: the change
never reaches a second reader, because there is no shared object between them. Because
the pointer returned by list_shallow_copy is exactly identical to head, this lesson
releases only deep and head separately; releasing shallow as well would release the
same block twice.
Binary Search Tree: Insertion, Traversal, Release
The linked list’s single-next-pointer sharing extends to two pointers (left, right)
in the binary tree, but the principle is the same: when a node is reached through more
than one path, the source does not show which path was used. tree_insert is
recursive, tree_total walks in order (inorder) and sums the values, tree_free
releases the subtrees before releasing the root — the list’s “who releases it” contract
still applies here; this lesson focuses not on that contract but on the tree’s
visibility.
// tree.h — basic binary search tree operations
#ifndef TREE_H
#define TREE_H
struct TreeNode {
int value;
struct TreeNode *left;
struct TreeNode *right;
};
struct TreeNode *tree_insert(struct TreeNode *root, int value);
int tree_total(const struct TreeNode *root);
void tree_free(struct TreeNode *root);
#endif
// tree.c — insertion, inorder traversal, and release
#include <stdlib.h>
#include "tree.h"
struct TreeNode *tree_insert(struct TreeNode *root, int value) {
if (root == NULL) {
struct TreeNode *node = malloc(sizeof *node);
if (node == NULL) {
return NULL;
}
node->value = value;
node->left = NULL;
node->right = NULL;
return node;
}
if (value < root->value) {
root->left = tree_insert(root->left, value);
} else {
root->right = tree_insert(root->right, value);
}
return root;
}
int tree_total(const struct TreeNode *root) {
if (root == NULL) {
return 0;
}
return tree_total(root->left) + root->value + tree_total(root->right);
}
void tree_free(struct TreeNode *root) {
if (root == NULL) {
return;
}
tree_free(root->left);
tree_free(root->right);
free(root);
}
// tree_alias.c — accessing the tree's own node through a second pointer
#include <stdio.h>
#include "tree.h"
int main(void) {
struct TreeNode *root = NULL;
int values[] = {5, 3, 8, 1, 4};
for (size_t i = 0; i < sizeof values / sizeof values[0]; i++) {
root = tree_insert(root, values[i]);
}
printf("%s %d\n", "total of the tree's values before", tree_total(root));
struct TreeNode *left_child = root->left;
left_child->value = 30;
printf("%s %d\n", "total after changing through left_child", tree_total(root));
tree_free(root);
return 0;
}
total of the tree's values before 21 total after changing through left_child 48
DS3. left_child points at the same node as root->left — it did not add a new
branch to the tree; we reached an existing node through a second path (through its own
variable instead of the root’s left field). The assignment left_child->value = 30
raises this node’s value from 3 to 30, and tree_total(root) sees it: from 21 to 48.
The same principle as the second example in the linked list — the class is again
shared. The contract we honor when releasing the tree (tree_free) is a separate
question: who releases each node, the axis of the previous course (Memory and Pointers
in C). This lesson does not reopen that question; it only measures whether a
change is visible through a second pointer.
Partial Shallow Copy Through Struct Assignment
list_shallow_copy and list_deep_copy were two extreme examples: one copied nothing
at all, the other copied every node. But C has a third, more insidious way of copying a
struct: struct assignment (=). When you write struct Node copy = *head;, the
compiler copies every field of the struct that head points at into a new variable —
but if one of “every field” is itself a pointer, that pointer is also copied as a value,
not the place it points at.
// struct_assignment.c — a partial shallow copy through variable-type assignment
#include <stdio.h>
#include "node.h"
int main(void) {
struct Node *head = NULL;
head = list_insert(head, 3);
head = list_insert(head, 2);
head = list_insert(head, 1);
struct Node copy = *head;
printf("%s %d\n", "copy's value field right after the assignment", copy.value);
head->value = 100;
printf("%s %d\n", "copy.value after head->value changes", copy.value);
head->next->value = 200;
printf("%s %d\n", "copy.next->value after changing through the second node", copy.next->value);
list_free(head);
return 0;
}
copy's value field right after the assignment 1 copy.value after head->value changes 1 copy.next->value after changing through the second node 200
DS14. The line struct Node copy = *head; does two different things, both inside
the same assignment: the value field is an int, and the assignment copies it by
value — this is why, after head->value = 100 runs, copy.value still holds its
first value, 1. But the next field is a struct Node *, and the assignment copies it
by value too — the value copied is an address, meaning copy.next and head->next
carry the same address after the assignment. When head->next->value = 200 runs, this
change is also visible through copy.next->value: 200. Within a single assignment
line, the value field produces the copy class while the node that next points at
produces the shared class at the same time — the difference between a “shallow
copy” and a “deep copy” is sometimes not a matter of which function you chose, but of
which fields of a struct carry pointers. list_shallow_copy’s returned pointer was
shared in its entirety; here only the node that next points at is shared, the value
field itself is not — two separate classes within the same struct assignment.
The Bounding Measurement
A pointer’s sharing is invisible in the source. When two list heads or two tree
branches point at the same node, the type of the structure does not say so — the
declaration struct Node * can point at either the list’s own node or an independent
copy, and there is no distinction between them at the source level. In this lesson we
compared pointers in both cases without printing an address: the equality
second == head->next told us whether the two variables looked at the same block of
memory or at different blocks. The difference between list_shallow_copy and
list_deep_copy also shows up not in a signature in the source but in what their
bodies do: one performs no allocation at all, the other allocates a new node for every
node. It is not possible to tell whether a function shares by looking at its return
type; this can only be understood by reading the body or by measuring.
Summary
- The linked list and the binary search tree were built on the same principle: more than one pointer can reach a node at the same time, and the source does not say so.
- Accessing and changing a node through an alias produced the shared class in both
the list and the tree: a second reader (
list_total,tree_total) saw the change. list_shallow_copystayed shared because it returns the head without allocating a node;list_deep_copyproduced the copy class because it allocates a new node for every node.- A partial copy made through struct assignment (
=) can produce the copy class in a struct’s scalar fields and the shared class in what its pointer fields point at, at the same time. - Whether two pointers point at the same node can be measured only through equality
(
==), without printing an address. - Who releases a struct (K02’s question) and who sees a change (this course’s question) are separate axes; this lesson measures the second one.
Next Step
In this lesson we always held a pointer to reach a node, and that pointer was permanent. The next lesson, Ring Buffers and Queues, looks at a structure where what is shared is not the pointer itself but a position (an index): a producer and a consumer use the same array, but one writes while the other reads, and a written value can be overwritten before it is read. The class there is not shared but handoff: the value reaches the reader exactly once, and can then disappear.
To keep your progress and take notes, Log in
My notes
Log in to take notes.