Here's yet another implementation of a singly linked list. You may base your review on the following questions:
Review goals:
- Is the API well thought of?
- Does this design include undefined behavior, allow unnecessary flexibility, or make unnecessary assumptions? Are there arbitrary details in the design?
- Does the design handle special cases, such as an empty list?
- Does any function do more than one task? Do all functions follow the Single Responsibility Principle?
- Can any of the expressions in my code overflow / underflow?
- Have I used any C idioms in a questionable way?
- Am I referencing memory that I have no right to touch? Do you see any memory leaks in the code?
- Do you approve of my doxygen-styled comments? I was introduced to it in my last post.
- What features is my list missing?
- Was I able to successfully abstract away the implementation?
- Are the tests well thought of? I believed the process of testing one's code must be complicated, and never did it. But it turned out to be quite rewarding and uncomplicated.
- How can I make the list more generic?
- How can I improve my code?
Code:
list.h:
#ifndef LIST_H
#define LISH_H 1
/* The header is meant for users of the code. So in there I document the interface:
* how to use it, preconditions and postconditions, etcetera.
*
* The .c file is for maintainers. In there, I document the implementation:
* how things work internally, and why they work that way.
*/
struct ll_node;
/**
* @brief ll_tally() shall count the number of elements in the list.
* @param head - A pointer to the head of the list.
* @return Upon successful return, ll_tally() returns number
* of elements present.
*/
extern intmax_t ll_tally (const struct ll_node *head);
/**
* @brief ll_push_node() shall push a new node to the front of the list.
* @param head - A pointer to a pointer to the head of the list.
* @param data - The value of the element to initialize the node with.
* @return Upon successful return, ll_push node returns true. Otherwise it
* returns false to indicate an allocation failure.
* @warning The caller is responsible for ensuring that the head pointer is
* not NULL. Failing to comply could result in unexpected program
* termination and potential loss of data.
*/
extern bool ll_push_node (struct ll_node **head, intmax_t data);
/**
* @brief ll_delete() shall free all the elements in the list. Allows head to be NULL
* to mimic free (NULL), in which case no operation is performed.
* @param head - A pointer to the head of the list.
* @return This function returns nothing.
*/
extern void ll_delete (struct ll_node *head);
/**
* @brief ll_build_head() shall build a linked list by inserting nodes at the
* head of the list.
* @param size - The initial number of nodes in the list.
* @param data[size] - An optional array to initialize the values of the elements
* of the nodes with. If data is NULL pointer, the elements
* of all nodes are initialized to 0.
* @return Upon successful return, ll_build_head() shall return a pointer to the
* head of the list. Otherwise, it shall return a NULL pointer to indicate
* a memory allocation failure.
*/
extern struct ll_node *ll_build_head (size_t size,
const intmax_t data[size]);
/**
* @brief ll_build_tail() shall build a linked list by inserting the first
* node at the head, and henceforth at the tail.
* @param size - the initial number of nodes in the list.
* @param data[size] - an optional array to initialize the values of the
* elements of the nodes with. If data is a NULL pointer,
* the elements of the nodes are initialized to 0.
* @return Upon successful return, ll_build_tail() shall return a pointer
* to the head of the list. Otherwise, it shall return a NULL pointer
* to indicate a memory allocation failure.
*/
extern struct ll_node *ll_build_tail (size_t size,
const intmax_t data[size]);
/**
* @brief ll_print() prints the value of all the elements associated with a node.
* @param head - A pointer to the head of the linked list. Allows head
* to be NULL, in which case no operation is performed.
* @return Upon successful return, ll_print return the number of bytes
* written.
*/
extern int ll_print (const struct ll_node *head);
/**
* @brief ll_find_node() shall search the list for the node at index index.
* @param head - a pointer to the head of the list.
* @index index - the index of the node to return.
* @return Upon successful return, ll_find_node() shall return a pointer
* the node. Otherwise, it returns a NULL pointer to indicate failure.
* A NULL pointer would also be returned for an empty list, i.e.
* a NULL head pointer.
*/
extern struct ll_node *ll_find_node (struct ll_node *head, size_t index);
/**
* @brief ll_pop_node() pops the first node pointed to by head.
* @param head - a pointer to pointer to the head of the list.
* @return Upon successful return, ll_pop_node() shall free the
* first node and return the value of the element associated
* with the node pointed to by head.
* @warning The caller is responsible for ensuring that the head pointer and
* the pointer pointed to by head is not NULL. Failing to comply
* could result in unexpected program termination and potential loss
* of data.
*/
extern intmax_t ll_pop_node (struct ll_node **head);
/**
* @brief ll_update_node() shall update the value of the element of the first
* node that matches the value of old_data.
* @param head - A pointer to the head of the list.
* @param old_data - The value of the element associated with a node to update.
* @param new_data - THe new value to initialize the element with.
* @return ll_update_node() returns nothing.
* @warning The caller is responsible for ensuring that the head pointer is
* not NULL. Failing to comply could result in unexpected program
* termination and potential loss of data.
*/
extern void ll_update_node (struct ll_node *head, intmax_t old_data,
intmax_t new_data);
/**
* @brief ll_append_node() shall append a new node to the end of the list.
* @param head - A pointer to the head of the node.
* @param data - The value to initialize the element associated with
* the new node.
* @return Upon successful return, ll_append_node() returns the value of head
* that was passed in. ELse, it returns a NULL pointer to indicate
* allocation failure.
*/
extern void *ll_append_node (struct ll_node *head, intmax_t data);
/**
* @brief ll_insert_pos() shall insert a new node at position index
* of the list.
* @param head - A pointer to pointer to the head of the list.
* @param index - The index of the node to insert at.
* @param data - The value to initialize the element associated with
* the new node.
* @return Upon successful return, ll_insert_pos() shall return true.
* Otherwise, it returns false to indicate a memory allocation failure.
* @warning The caller is responsible for ensuring that the head pointer is
* not NULL. Failing to comply could result in unexpected program
* termination and potential loss of data.
*/
extern bool ll_insert_pos (struct ll_node **head, size_t index,
intmax_t data);
/**
* @brief ll_search() shall search each node of the list pointed to by head
* for data.
* @param head - A pointer to the head of the list.
* @param data - The value to search for.
* @return Upon successful return, ll_search() returns true. Otherwise it returns
* false to indicate failure.
*/
extern bool ll_search (const struct ll_node *head, intmax_t data);
/**
* @brief ll_pop_end() pops the node at the end of the list.
* @param head - A pointer to a pointer to the head of the list.
* @return Upon successful return, ll_pop_end() frees the node and
* returns the value of the element associated with the node.
* @warning The caller is responsible for ensuring that both head and
* the pointer pointed to by head is not NULL. Failure to comply
* could result in unexpected program termination and potential
* loss of data.
*/
extern intmax_t ll_pop_end (struct ll_node **head);
/**
* @brief ll_pop_pos() pops the node at index index of the list.
* @param head - A pointer to a pointer to the head of the node.
* @param index - The index of the node to pop.
* @return Upon successful return, ll_pop_pos() frees the node at
* index index and returns the value associated with that node.
* Otherwise, it returns INTMAX_MIN to indicate failure.
*/
extern intmax_t ll_pop_pos (struct ll_node **head, size_t index);
/**
* @brief ll_get_data() shall obtain the value of the element associated with
* the node pointed to by head.
* @param head - A pointer to a node.
* @return Upon successful return, ll_get_data() returns the value of the
* node's element.
* @warning The caller is responsible for assuring a NULL pointer is not passed
* in. Failure to comply could result in unexpected program termination
* and potential loss of data.
*/
extern intmax_t ll_get_data (const struct ll_node *head);
/**
* @brief ll_set_data() shall update the value of the element associated with
* the node pointed to by head.
* @param head - A pointer to a node.
* @param data - The new value.
* @return ll_set_data() returns nothing.
* @warning The caller is responsible for assuring a NULL pointer is not passed
* in. Failure to comply could result in enexpected program termination
* and potential loss of data.
*/
extern void ll_set_data (struct ll_node *head, intmax_t data);
#endif
list.c:
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdint.h>
#include <assert.h>
#include "list.h"
struct ll_node {
intmax_t data;
struct ll_node *next;
};
extern intmax_t ll_tally (const struct ll_node *head)
{
intmax_t count;
for (count = 0; head; head = head->next) {
count++;
}
return count;
}
extern void ll_update_node (struct ll_node *head, intmax_t old_data,
intmax_t new_data)
{
assert (head);
while (head) {
if (head->data == old_data) {
head->data = new_data;
return;
}
head = head->next;
}
}
extern bool ll_insert_pos (struct ll_node **head, size_t index,
intmax_t data)
{
assert (head);
if (!(*head) || !index) {
return ll_push_node (head, data);
}
size_t count = 0;
struct ll_node *current = *head;
while (current && (count++ < index)) {
current = current->next;
}
struct ll_node *new_node = realloc (0, sizeof *new_node);
if (!new_node) {
return false;
}
new_node->data = data;
new_node->next = current->next;
current->next = new_node;
return true;
}
extern bool ll_search (const struct ll_node *head, intmax_t data)
{
while (head) {
if (head->data == data) {
return true;
}
head = head->next;
}
return false;
}
extern intmax_t ll_get_data (const struct ll_node *head)
{
assert (head);
return head->data;
}
extern void ll_set_data (struct ll_node *head, intmax_t data)
{
assert (head);
head->data = data;
}
/* Should the return type and the parameter type of struct ll_node be const-qualified? */
extern struct ll_node *ll_find_node (struct ll_node *head, size_t index)
{
size_t count = 0;
while (head) {
if (count++ == index) {
return head;
}
head = head->next;
}
return 0;
}
extern bool ll_push_node (struct ll_node **head, intmax_t data)
{
assert (head);
struct ll_node *new_node = realloc (0, sizeof *new_node);
if (!new_node) {
return false;
}
new_node->data = data;
new_node->next = *head;
*head = new_node;
return true;
}
extern struct ll_node *ll_build_tail (size_t size,
const intmax_t data[size])
{
struct ll_node *head = 0;
struct ll_node *tail = 0;
bool ret_val = ll_push_node (&head, data ? data[0] : 0);
if (!ret_val) {
return 0;
}
tail = head;
for (size_t i = 1; i < size; i++) {
bool ret_val = ll_push_node (&(tail->next), data ? data[i] : 0);
if (!ret_val) {
ll_delete (head);
return 0;
}
tail = tail->next;
}
return head;
}
extern intmax_t ll_pop_end (struct ll_node **head)
{
assert (head && *head);
struct ll_node *prev = 0;
struct ll_node *current = *head;
while (current->next) {
prev = current;
current = current->next;
}
prev->next = current->next;
intmax_t result = current->data;
free (current);
return result;
}
/* XXX: Change to_be_popped's name */
extern intmax_t ll_pop_pos (struct ll_node **head, size_t index)
{
assert (head && *head);
if (!index) {
return ll_pop_node (head);
}
struct ll_node *prev = *head;
struct ll_node *current = ll_find_node (prev, index);
/* While this reduces the range of legally usable values in the
* list, it makes positive and negative range equally sized.
*/
if (!current) {
return INTMAX_MIN;
}
while (prev->next != current) {
prev = prev->next;
}
prev->next = current->next;
intmax_t result = current->data;
free (current);
return result;
}
extern void *ll_append_node (struct ll_node *head, intmax_t data)
{
while (head->next) {
head = head->next;
}
struct ll_node *new_node = realloc (0, sizeof *new_node);
if (!new_node) {
return 0;
}
new_node->data = data;
new_node->next = 0;
head->next = new_node;
return head;
}
extern struct ll_node *ll_build_head (size_t size,
const intmax_t data[size])
{
struct ll_node *head = 0;
for (size_t i = 0; i < size; i++) {
if (!ll_push_node (&head, data ? data[i] : 0)) {
ll_delete (head);
}
}
return head;
}
extern intmax_t ll_pop_node (struct ll_node **head)
{
assert (head && *head);
struct ll_node *current = *head;
intmax_t data = current->data;
*head = current->next;
free (current);
return data;
}
extern int ll_print (const struct ll_node *head)
{
assert (head);
int ret_val = 0;
for (; head; head = head->next) {
if (ret_val > 0) {
ret_val += fputc ('-', stdout);
}
ret_val += printf (" %jd ", head->data);
}
ret_val += fputc ('\n', stdout);
return ret_val;
}
extern void ll_delete (struct ll_node *head)
{
while (head) {
struct ll_node *current = head;
head = head->next;
free (current);
}
}
Unit tests:
I used the criterion framework for the tests. tests.c:
#include <criterion/criterion.h>
#include <stdint.h>
#include "../src/list.h"
#define SIZE 10
struct ll_node *head = 0;
void setup (void)
{
intmax_t limits[SIZE];
for (intmax_t i = 0; i < SIZE; i++) {
limits[i] = i;
}
head = ll_build_head (SIZE, limits);
cr_assert (head);
}
void tear_down (void)
{
ll_delete (head);
}
TestSuite (list_tests,.init = setup,.fini = tear_down);
Test (list_tests, ll_push_node)
{
cr_assert (ll_push_node (&head, -18));
cr_assert (ll_push_node (&head, 4929320493214));
cr_assert (ll_push_node (&head, 0));
cr_assert (ll_push_node (&head, -3021302324));
cr_assert (ll_push_node (&head, 450340));
}
Test (list_tests, ll_print)
{
cr_assert (ll_print (head) > 0);
}
Test (list_tests, ll_find_node)
{
struct ll_node *current = ll_find_node (head, 0);
cr_assert (current && ll_get_data (current) == 9);
current = ll_find_node (head, 9);
cr_assert (current && !ll_get_data (current));
cr_assert (!ll_find_node (0, 4));
}
Test (list_tests, ll_set_data)
{
struct ll_node *current = ll_find_node (head, 0);
ll_set_data (current, 1);
cr_assert (ll_pop_node (&head) == 1);
}
Test (list_tests, ll_pop_node)
{
cr_assert (ll_pop_node (&head) == 9);
cr_assert (!ll_search (head, 9));
}
Test (list_tests, ll_tally)
{
cr_assert (ll_tally (head) == SIZE);
cr_assert (!ll_tally (0));
}
Test (list_tests, ll_update_node)
{
ll_update_node (head, 9, 90);
}
Test (list_tests, ll_append_node)
{
head = ll_append_node (head, 30);
}
Test (list_tests, ll_insert_pos)
{
cr_assert (ll_insert_pos (&head, 0, 333));
cr_assert (ll_search (head, 333));
cr_assert (ll_insert_pos (&head, 10, 444));
cr_assert (ll_search (head, 444));
}
Test (list_tests, ll_pop_end)
{
cr_assert (ll_pop_end (&head) == 0);
cr_assert (!ll_search (head, 0));
}
Test (list_tests, ll_pop_pos)
{
cr_assert (ll_pop_pos (&head, 0) == 9);
cr_assert (!ll_search (head, 9));
cr_assert (ll_pop_pos (&head, 1) == 7);
cr_assert (!ll_search (head, 7));
cr_assert (ll_pop_pos (&head, 2) == 5);
cr_assert (!ll_search (head, 5));
}
Test (list_tests, ll_search)
{
cr_assert (!ll_search (0, 4));
cr_assert (!ll_search (head, 3912932));
cr_assert (!ll_search (head, -893821));
cr_assert (ll_search (head, 9));
cr_assert (ll_search (head, 4));
cr_assert (!ll_search (0, 0));
cr_assert (!ll_search (head, 3928310888308831190123209));
}
Test (list_tests1, ll_build_tail)
{
intmax_t limits[SIZE] = { 0, 4, 30, 20, 1 };
head = ll_build_tail (SIZE, limits);
cr_assert (head);
ll_update_node (head, 30, 35);
cr_assert (ll_search (head, 35));
struct ll_node *current = ll_find_node (head, 2);
cr_assert (current && ll_get_data (current) == 35);
ll_delete (head);
head = ll_build_tail (SIZE, 0);
cr_assert (head && !ll_pop_node (&head));
ll_delete (head);
}
Dynamic testing:
And here is valgrind's dump:
==85== Memcheck, a memory error detector
==85== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==85== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==85== Command: tests/bin/tests
==85==
==85== error calling PR_SET_PTRACER, vgdb might block
9 - 8 - 7 - 6 - 5 - 4 - 3 - 2 - 1 - 0
[====] Synthesis: Tested: 13 | Passing: 13 | Failing: 0 | Crashing: 0
==85==
==85== HEAP SUMMARY:
==85== in use at exit: 0 bytes in 0 blocks
==85== total heap usage: 762 allocs, 762 frees, 115,093 bytes allocated
==85==
==85== All heap blocks were freed -- no leaks are possible
==85==
==85== For lists of detected and suppressed errors, rerun with: -s
==85== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
Compilation:
It compiled with zero warnings.
make
mkdir lib
mkdir obj
gcc-10 -std=c17 -no-pie -g3 -ggdb -Wall -Wextra -Warray-bounds -Wconversion -Wmissing-braces -Wno-parentheses -Wpedantic -Wstrict-prototypes -Wwrite-strings -Winline -c src/list.c -o obj/list.o
ar -cvrs lib/list.a obj/list.o
a - obj/list.o
intmax_t? \$\endgroup\$