Consider a function
pop() that detaches and returns a pointer to the first element in the list:
struct node *pop(struct node **list)
{
struct node *first;
/* Safety check, will not trigger in normal code */
if (list == NULL)
return NULL;
/* If the list is empty, return NULL. */
if (*list == NULL)
return NULL;
/* Detach the first element in the list: */
first = *list; /* Keep track of the original first element, */
*list = first->next; /* advance the list to start at the second element, */
first->next = NULL; /* and isolate the originally first element from the list. */
/* Return the now detached, originally first element in the list. */
return first;
}
The caller does need to free the popped element. If you just wish to discard the first element in the list, you can safely always do
free(pop(&mylist));, because
free(NULL); does nothing (and is safe to do).
The above is exactly equivalent to
struct node *pop(struct node **list)
{
struct node *first;
if (!list)
return NULL;
if (!*list)
return NULL;
first = *list;
*list = (*list)->next;
first->next = NULL;
return first;
}
in case you wonder.
Please also note how important descriptive comments are to the maintenance and understanding of the code.
Because I myself did not learn this when I learned to program on my own, I am still struggling with this. Do not repeat my mistake!
Learn to write comments that describe your intent as a programmer; to describe what you intend the code to do.
Writing comments that describe
what the code does –– like "increment x by 5" –– are less than useless, because as programmers, we can read the code itself, we do not need to read that twice. (This means that the second comment,
"If the list is empty, return NULL", is borderline; if I had written it as
"If *list is NULL, return NULL", it would definitely be useless and annoying comment. Consider the difference between those two wordings, to see what I mean.)
It is the higher, more concept-level things, that the comments should describe.
I haven't implemented dynamic lists in that way in ages. While this is certainly correct and flexible, this kind of implementation is - in general - pretty inefficient cache-wise.
Yes, but this is a necessary step to
grok this stuff before one can advance to the cache-efficient and dedicated dynamic allocator stuff.
That is, I personally consider this thread as an example of how one would go about learning this stuff in C down to the "intuitive" level (where one uses these structures, even if they have their downsides, without much cognitive effort or worries), before advancing to the more interesting, more complicated dynamic memory stuff: step by step, one step at a time.
You certainly wouldn't want to tell someone who wasn't yet completely familiar with this kind of pointery stuff in C to implement and use their own specialized dynamic memory allocators, would you?

(Although, I must admit a good book on data structures in C –– see old threads in this sub-forum! –– would probably benefit OP more than this thread.)
Some of the actually real-world useful data structures, like
binary heaps, specifically min- and max-heaps implemented as an array, do not necessarily use pointers at all internally; but to make
use of them, you end up needing to
grok how pointers work in C anyway. (That is, do not expect that to be "easier" than any pointer stuff: it
looks deceptively simple, but the rules of how to implement it a way that works reliably with other code, is not simple. There are lots and lots of implementation choices, but only a small subset of them work together well as a whole!)
My favourite example of this is a min-heap of event timestamps, to implement timeouts or timed events. Each element in the binary heap contains only a timestamp of the event, and a pointer or index to the event slot in an array or list of events. Each event slot contains a pointer or index back to the element; the two are linked bidirectionally to each other. As the heap is a min-heap, the root always contains the next event to occur. When it occurs, it is removed from the heap, and the event slot updated to indicate the event has elapsed. When the heap is modified, elements are percolated towards the root, and their links in the event slot updated to match.
I use that in real-world code, especially in systems programming (daemons and services written in C in a full blown OS like Linux or Windows), with a dedicated thread handling the timeout stuff. It runs only very rarely, most of the time waiting for the next event or an event cancelation/addition, and does not use much resources (CPU or memory) to do what it does; so it is a very, very efficient way to do it. On POSIXy systems, it can even interrupt blocking I/O calls in specific threads (delivering an internal POSIX signal).
However, the way the event in the heap and in the heap slot are two-way linked to each other, does mean one has to be very, very careful to make sure they remain consistent. The fact that they also need to be thread-safe, makes for a lot of options in locking options, some of them "interesting". (I do not actually know of a way to do this locklessly in a multithreaded environment). That "interesting" means that while some of the options look perfectly fine in a separate exercise or unit test, in real life use they cause a cascade of increasingly undesirable neighboring code. The simplest one, a single mutually exclusive lock (mutex) governing access to both the heap and the event slot list/array, works surprisingly well; anything more fine-grained needs a lot of applied experience to actually turn out better in real world work loads.
(Why binary min-heap for this? As Mark Allen Weiss shows in
Algorithm Analysis and Data Structures in C, for uniformly random keys (timestamps), the number of percolation events (moving a heap entry towards or outwards from the root, level by level) on average is approximately
e ≃ 2.718 per key addition or deletion, with an absolute maximum of log
2N events for
N existing entries in the heap. This makes it a very efficient choice. It also shows how important
algorithm analysis is when choosing algorithms. Yet, unless you have the necessary skills to implement those algorithms in C, the analysis part is useless to you. The two –– implementing and analysis ––, should in my opinion, be developed almost in tandem, starting on the more mathematical analysis part as soon as one can implement all the basic abstract data structures in C.)