5
\$\begingroup\$

Here is some code I've written. I am welcoming any critiques, whether it is style/formatting, correctness (I believe it is correct) -- I would appreciate any advice on formatting unit tests, and any other thing that people may think of.

#include <string.h>
#include <stdlib.h>
#include <stdbool.h>

/*
 * all functions expect a null-terminated C-string for argument "str"
 */

/*
 * a node in the linked list
 */

struct ll_node
{
  char* str;
  struct ll_node *prev;
  struct ll_node *next;
};

/*
 * the "manager" of the linked list
 */

struct ll_node_manager
{
  struct ll_node *head;
  struct ll_node *tail;
};

/*
 * initialize the linked list
 */

void
init_ll (struct ll_node_manager *ptr)
{
  ptr->head = NULL;
  ptr->tail = NULL;
}

/*
 * insert node at head of linked list
 * return false on failure (allocation error) or true otherwise
 */

bool
insert_head_ll (struct ll_node_manager *ptr, char *str)
{
  if (!ptr->head)
    {
      ptr->head = malloc(sizeof(struct ll_node));
      if (!ptr->head)
        return false;
      ptr->tail = ptr->head;
      ptr->head->str = strdup(str);
      if (!ptr->head->str)
        return false;
      ptr->head->prev = NULL;
      ptr->head->next = NULL;
    }
  else
    {
      struct ll_node *tmp = malloc(sizeof(struct ll_node));
      if (!tmp)
        return false;
      tmp->str = strdup(str);
      if (!tmp->str)
        return false;
      tmp->prev = NULL;
      tmp->next = ptr->head;
      ptr->head->prev = tmp;
      ptr->head = tmp;
    } 
  return true;
}

/*
 * insert node at tail of linked list
 * return false on failure (allocation error) or true otherwise
 */

bool
insert_tail_ll(struct ll_node_manager *ptr, char *str)
{
  if (!ptr->tail)
    {
      ptr->head = malloc(sizeof(struct ll_node));
      if (!ptr->head)
        return false;
      ptr->tail = ptr->head;
      ptr->head->str = strdup(str);
      if (!ptr->head->str)
        return false;
      ptr->head->prev = NULL;
      ptr->head->next = NULL;
    }
  else
    {
      struct ll_node *tmp = malloc(sizeof(struct ll_node));
      if (!tmp)
        return false;
      tmp->str = strdup(str);
      if (!tmp->str)
        return false;
      tmp->prev = ptr->tail;
      tmp->next = NULL;;
      ptr->tail->next = tmp;
      ptr->tail = tmp;
    }
  return true;
}

/*
 * return true if node with value of "str" contained in linked list
 * return false otherwise
 */
 
bool
ll_contains(struct ll_node_manager *ptr, char *str)
{
  for (struct ll_node *tmp = ptr->head;
       tmp; tmp = tmp->next)
    if (strcmp(tmp->str, str) == 0)
      return true; 
  return false;
}

/*
 * return true if successful (removed head)
 * return false if not (did not exist)
 */

bool
remove_head_ll(struct ll_node_manager *ptr)
{
  if (!ptr->head)
    return false;
  struct ll_node *tmp = ptr->head;
  ptr->head = ptr->head->next;
  if (!ptr->head)
    ptr->tail = ptr->head;
  free(tmp->str);
  free(tmp);
  return true;
} 

/*
 * return true if successful (removed tail)
 * return false if not (did not exist)
 */

bool
remove_tail_ll(struct ll_node_manager *ptr)
{
  if (!ptr->tail)
    return false;
  struct ll_node *tmp = ptr->tail;
  ptr->tail = ptr->tail->prev;
  if (!ptr->tail)
    ptr->head = ptr->tail;
  free(tmp->str);
  free(tmp);
  return true;
}
  
/*
 * remove first node (traverse from head) with value of "str"
 * return true if successful (node found and removed)
 * return false if not (node not found)
 */

bool
remove_node_ll(struct ll_node_manager *ptr, char *str)
{
  for (struct ll_node *tmp = ptr->head;
       tmp; tmp = tmp->next)
      if (strcmp(tmp->str, str) == 0)
        {
          tmp->prev->next = tmp->next;
          tmp->next->prev = tmp->prev;
          free(tmp->str);
          free(tmp);
          return true;
        }
  return false;
}

I am a relative beginner. A second year Computer Science student in university. I'm practicing implementing data structures for my own knowledge.

\$\endgroup\$
2
  • \$\begingroup\$ It's going to be hard to advise on formatting unit tests, since you haven't shown us the tests. \$\endgroup\$ Commented Jun 13, 2023 at 7:47
  • \$\begingroup\$ I don’t know how to write unit tests and was asking for guidance @TobySpeight \$\endgroup\$ Commented Jun 13, 2023 at 21:55

4 Answers 4

7
\$\begingroup\$

In addition to @Deduplicator good answer.

Header file

Form a header file that defines needed types and declares functions for this double-linked list routines.

It is important to present what the public needs versus what the implementation needs.

Example struct ll_node needed in the implementation, yet the public does not need it.

Name space

Code uses names like struct ll_node_manager, init_ll(), insert_head_ll(), .... Consider greater uniformity and put the ll in the same place. I recommend ll_ at the start of all.

ll likely to collide single linked list code. Perhaps dl?

Formatting

Do not manually format. Use an auto formatter. Hopefully your IDE provides one, tailorable to your group's standard.

Comments

Rather than all lower case, write more like standard prose.

// insert node at head of linked list
// Insert node at head of linked list.

Use const when referenced data does not change

// bool remove_node_ll(struct ll_node_manager *ptr, char *str)
bool remove_node_ll(struct ll_node_manager *ptr, const char *str)

Next

Consider a generic apply function like below that applies the function to each node's str in the list, perhaps stopping early if f() returns non-zero.

Useful for printing and all sorts of applications.

int apply_ll(struct ll_node_manager *ptr, int (*f)(void *state, char *str), void *state)

Sample improvement/simplification

  • Use of const.
  • Removed leak on allocation failure.
  • Allocate to the referenced object, not type.
  • Reduces code repeat.
// Return success flag.
bool insert_tail_ll(struct ll_node_manager *ptr, const char *str) {
  struct ll_node *node = malloc(sizeof *node);
  if (node) {
    node->str = strdup(str);
    if (node->str) {
      node->next = NULL;
      node->prev = ptr->tail;
      if (ptr->tail) {
        // As tail exists, append this node to the tail.
        ptr->tail->next = node;
      } else {
        // As tail does not exist, let head point to this node too.
        ptr->head = node;
      }
      ptr->tail = node; // Update tail.
      return true;
    }
    free(node);
  }
  return false;
}

Another approach is to do all allocating up front. I like this when there are 3 or more things to allocate. The core of the function is 2 less indents in than the above.

bool insert_tail_ll(struct ll_node_manager *ptr, const char *str) {
  struct ll_node *node = malloc(sizeof *node);
  char *str2 = strdup(str);
  if (node == NULL || str2 == NULL) {
    free(node);
    free(str2);
    return false;
  }

  ...  // The core of the function.

  return true;
}
\$\endgroup\$
8
  • \$\begingroup\$ Nice demonstration how to work with having to create the element separately. Also thanks for promoting standardization, both in the interface and the presentation, I really let that all fall under the table. \$\endgroup\$ Commented Jun 13, 2023 at 20:50
  • \$\begingroup\$ Thank you for your comment. For formatting, I use emacs, not an IDE. For comments, why do you say not all lowercase? I will take your advice for dl even though I don’t plan to write a singly-linked list. Lastly, it seems like the code for the first sample improvement doesn’t handle failure of strdup, which should be one extra free. \$\endgroup\$ Commented Jun 13, 2023 at 22:51
  • \$\begingroup\$ @user129393192 Code comments are for people. For the same reason, your question used sentence case: it conveys more information to people. Namespace: consider not only yourself but what others may like when the use your code. When strdup() returns NULL, there is no need to free it. Otherwise, please re-state " first sample improvement doesn’t handle failure of strdup, which should be one extra free". \$\endgroup\$ Commented Jun 14, 2023 at 0:02
  • \$\begingroup\$ You are right. I was mistaken. As for IDE/Emacs, I was asking about my general C style with regards to formatting. And as for the header you mentioned, if it is solely for the public, it is even necessary to include it in the *.c file? \$\endgroup\$ Commented Jun 14, 2023 at 0:45
  • 1
    \$\begingroup\$ @user129393192 Sounds good. Avoid manually formatting. It is not productive and tends to be inconsistent. Really not worth your valuable time. \$\endgroup\$ Commented Jun 14, 2023 at 3:22
6
\$\begingroup\$
  • remove_node_ll is buggy. If str matches tmp->str at the very first iteration (tmp is head), tmp->prev->next dereferences a NULL pointer. Ditto if the match happens at the tail node.

  • strdup may fail. You shall check its return value;

  • DRY. The code which creates a node is repeated 4 times. Make it a function:

      struct ll_node * create_node(char * str)
      {
          struct ll_node * node = malloc(sizeof(*node));
          if (node == NULL) {
              return NULL;
          }
          node->str = strdup(str);
          if (node->str == NULL) {
              free(node);
              return NULL;
          }
          node->prev = NULL;
          node->next = NULL;
          return node;
      }
    
  • Streamline.

    In insert_head_ll the newly created node becomes head no matter what. Consider

      bool insert_head_ll(ll_node_manager * ptr, char * str)
      {
          struct ll_node * node = create_node(str);
          if (node == NULL) {
              return false;
          }
          node->next = ptr->head;
          if (ptr->head != NULL) {
              ptr->head->prev = node;
          } else {
              ptr->tail = node;
          }
          ptr->head = node;
          return true;
      }
    

    Ditto for insert_tail_ll.

\$\endgroup\$
3
  • 1
    \$\begingroup\$ create_node() node is a good idea. Consider making it const char * str. \$\endgroup\$ Commented Jun 13, 2023 at 19:43
  • \$\begingroup\$ The code for creating a node is repeated four times because the code for inserting a node is repeated four times. They could be reduced to a simple insert_node_before and forwarders. Anyway, nice finding yet another bug caused by too many special cases. \$\endgroup\$ Commented Jun 13, 2023 at 20:44
  • 2
    \$\begingroup\$ @chux-ReinstateMonica Of course you are right. My bad. Edited. \$\endgroup\$
    – vnp
    Commented Jun 13, 2023 at 21:13
6
\$\begingroup\$
  1. Avoid special cases.

    They increase complexity and reduce efficiency.

    In your specific case, link it up as a full circle (anchor in the list-type, and all the nodes, you might have to define a links-type to get it done), and almost all special cases are gone. As an example, an empty list is just a list only containing the anchor.

    The only disadvantage of such circular linked lists is that they are not bitwise-movable, and unless they know their length not trivially fixable if moved such anyway.

  2. This also allows you to add an efficient iterator-interface (begin, end, advance, value), consolidating all insertion, removal, search, and enumeration.

  3. Coalesce allocations.

    Managing dynamic memory is expensive, so minimize the need.
    Instead of the first element being a pointer to a string, make the last a flexible array member (See also on cppreference).

  4. strdup(), though removed by coalescing allocations, is the only API not yet part of standard C you used (TR 24731-2 didn't make it to C17). Still, it is part of POSIX and should be with us in C23.

  5. You currently have a memory-leak if allocating the node succeeds, but not allocating its data. Coalescing both obviously fixes that.

  6. Consider declaring the same identifier as a typedef-name that you used as a tag-name. It's common and convenient.

  7. If you have a ctor (init_ll()), also define a dtor (destroy_ll()). Take care to avoid recursion here.

  8. Avoid sizeof(TYPE). sizeof *expr is far less error-prone and more refactor-friendly.

typedef struct dl_node {
    struct dl_node *next;
    struct dl_node *prev;
    char data[];
} dl_node;

typedef struct dl_list {
    dl_node anchor; // no dummy payload due to FAM
    size_t n
} dl_list;

static dl_node* dl_begin(dl_list* a) {
    return a->anchor.next;
}

static dl_node* dl_end(dl_list* a) {
    return &a->anchor;
}

static size_t dl_size(dl_list* a) {
    return a->n;
}

static dl_node* dl_next(dl_node* a) {
    return a->next;
}

static dl_node* dl_data(dl_node* a) {
    return a->data;
}

void dl_fixup(dl_list* a) {
    if (a->n)
        a->anchor.next->prev = a->anchor.prev->next = &a->anchor;
    else
        a->anchor.next = a->anchor.prev = &a->anchor;
}

void dl_init(dl_list* a) {
    a->n = 0;
    a->anchor.next = a->anchor.prev = &a->anchor;
}

void dl_destroy(dl_list* a) {
    for (dl_node *p = dl_begin(a), *tmp; p != dl_end(a); p = tmp) {
        tmp = p->next;
        free(p);
    }
}

bool dl_replacen(dl_node* p, const char* s, size_t n) {
    p = realloc(p, sizeof *p + n + 1);
    if (!p)
        return false;
    p->next->prev = p->prev->next = p;
    memcpy(p->data, s, n);
    p->data[n] = '\0';
    return true;
}

bool dl_replace(dl_node* p, const char* s) {
    return dl_replacen(p, s, strlen(s));
}

bool dl_insertn(dl_list* a, dl_node* p, const char* s, size_t n) {
    dl_node* new = malloc(sizeof *new + n + 1);
    if (!new)
        return false;
    new->next = p;
    new->prev = p->prev;
    ++a->n;
    new->next->prev = new->prev->next = new;
    memcpy(new->data, s, n);
    new->data[n] = '\0';
    return true;
}

bool dl_insert(dl_list* a, dl_node* p, const char* s) {
    return dl_insertn(a, p, s, strlen(s));
}

bool dl_insert_head(dl_list* a, const char* s) {
    return dl_insert(a, dl_begin(a), s);
}

bool dl_insert_tail(dl_list* a, const char* s) {
    return dl_insert(a, dl_end(a), s);
}

dl_node* dl_findn(dl_node* a, dl_node* b, const char* s) {
    for (; a != b; a = dl_next(a))
        if (!strcmp(s, dl_data(a))
            return a;
    return NULL;
}

dl_node* dl_find(dl_list* a, const char* s) {
    return dl_findn(dl_begin(a), dl_end(a), s);
}
\$\endgroup\$
12
  • \$\begingroup\$ What do you mean by "use a full circle"? I couldn't make any sense of it. The item about an "efficient iterator-interface" is unclear to me as well, could you add some more details? Is managing dynamic memory necessarily expensive? How could recursion make its way into destroy_ll? \$\endgroup\$ Commented Jun 13, 2023 at 12:55
  • \$\begingroup\$ 1. A full circle: All full nodes and the anchor node are connected in a circle, thus the empty list has exactly one element, the anchor, and need not be special-cased. 2. Think about an array: You can point to each element, and behind the last. The same is possible with the changed list, which you can than leverage in all observing and manipulating functions. As an example, look at C++ iterators. 3. Managing dynamic memory is the most expensive operation your code uses by far. Anyway, doing nothing is generally much faster than doing something specific. \$\endgroup\$ Commented Jun 13, 2023 at 13:28
  • \$\begingroup\$ 7. If you consider the node owning the next one, and being responsible for destroying it. \$\endgroup\$ Commented Jun 13, 2023 at 13:30
  • \$\begingroup\$ Thanks for your comment. Isn’t flexible array members undefined (accessing array out of bounds) — I read another thread that said so, though implementations support. As for the anchor, I’ve heard that called a “dummy node” before. From my understanding, you are saying to always have that dummy node. \$\endgroup\$ Commented Jun 13, 2023 at 23:01
  • \$\begingroup\$ Also, why do you say for point 8? To elaborate on my other comment, what I mean is that the padding at the end might overlap with the array, which could be undefined. Doesn't occur in my case, but see here \$\endgroup\$ Commented Jun 14, 2023 at 0:13
0
\$\begingroup\$

Other people have already given good feedback, so rather than talk a whole bunch, I'll let the code speak. This is how I might refactor the code if I were going to use this.

I changed the names of head and tail, just because I find firstItem/lastItem clearer, but that's just style of course. Other than that, I tried to keep the interface mostly the same as your original. One exception is that ll_contains() has the opportunity to double as a find-specified-node function for basically free, so I would just use if (ll_find_node()) (i.e. is not NULL). Of course if you really want an ll_contains() that returns bool you can just do bool ll_contains(const char *str) { return ll_find_node(str) != NULL; }

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>             // memset()
#include <assert.h>

/*
 * all functions expect a null-terminated C-string for argument "str"
 */

// represents a node in the linked list
struct ll_node
{
        char *str;
        struct ll_node *next, *prev;
};

// represents a linked list
typedef struct llist
{
        struct ll_node *firstItem;
        struct ll_node *lastItem;
} llist;

// initialize the given linked list
void ll_init(struct llist *list) {
        memset(list, 0, sizeof(struct llist));
}

// convenience function to allocate and initialize a linked list
struct llist *ll_create() {
        llist *list = malloc(sizeof(llist));
        ll_init(list);
        return list;
}

// internal shared function to create a new node with the given str
static struct ll_node *ll_create_node(const char *str) {
        assert(str != NULL);

        struct ll_node *newItem = malloc(sizeof(struct ll_node));
        if (!newItem) return NULL;

        newItem->str = strdup(str);
        if (!newItem->str) {
                free(newItem);
                return NULL;
        }

        return newItem;
}

// shared function to free a node
static void ll_node_destroy(struct ll_node *node) {
        free(node->str);
        #ifndef NDEBUG
                node->str = NULL;               // may help with debugging if use-after-free
        #endif

        free(node);
}

// remove all items from the given list and make the list empty
void ll_clear(struct llist *list) {
        struct ll_node *node = list->firstItem;
        while(node) {
                struct ll_node *next = node->next;
                ll_node_destroy(node);
                node = next;
        }

        ll_init(list);
}

// convenience function to free the given list and any items it contained
void ll_destroy(struct llist *list) {
        ll_clear(list);
        free(list);
}

// return the number of items in the given list
int ll_count(struct llist *list) {
        int count = 0;
        struct ll_node *node = list->firstItem;
        while(node) {
                node = node->next;
                count++;
        }

        return count;
}

// insert a node at head (the beginning) of linked list
// return false on failure (allocation error) or true otherwise
bool ll_insert_head(struct llist *list, const char *str) {
        struct ll_node *newItem = ll_create_node(str);
        if (!newItem) return false;

        newItem->str = strdup(str);
        if (!newItem->str) {
                free(newItem);
                return false;
        }

        newItem->prev = NULL;
        newItem->next = list->firstItem;

        if (list->firstItem)
                list->firstItem->prev = newItem;
        else    // this must be the first item
                list->lastItem = newItem;

        list->firstItem = newItem;
        return true;
}

// insert node at tail (the end) of linked list
// return false on failure (allocation error) or true otherwise
bool ll_insert_tail(struct llist *list, const char *str) {
        struct ll_node *newItem = ll_create_node(str);
        if (!newItem) return false;

        // (note: I'm checking lastitem here and not first as a micro-optimization:
        // this way if the "if" is true, the compiler may be able to re-use the
        // lastItem value it already loaded)
        if (list->lastItem)
                list->lastItem->next = newItem;
        else    // if no last, must be the first
                list->firstItem = newItem;

        newItem->prev = list->lastItem;
        newItem->next = NULL;
        list->lastItem = newItem;
        return true;
}

// if a node with the matching str value exists in the linked list,
// return a pointer to it, else return NULL (this can also be used to
// find if an item exists, just do "if (ll_find_node(...))")
struct ll_node *ll_find_node(struct llist *list, char *str) {
        struct ll_node *node = list->firstItem;
        while(node) {
                if (!strcmp(node->str, str))
                        return node;

                node = node->next;
        }

        return NULL;
}

// remove and delete the first node in the list
// return true if successful (removed head)
// return false if not (list was empty)
bool ll_remove_head(struct llist *list) {
        struct ll_node *deleteme = list->firstItem;
        if (deleteme) {
                list->firstItem = deleteme->next;
                if (list->firstItem)
                        list->firstItem->prev = NULL;
                else
                        list->lastItem = NULL;

                ll_node_destroy(deleteme);
                return true;
        }

        return false;
}

// remove and delete the last node in the list
// returns true if successful (removed tail)
// returns false if not (did not exist)
bool ll_remove_tail(struct llist *list) {
        struct ll_node *deleteme = list->lastItem;
        if (deleteme) {
                list->lastItem = deleteme->prev;
                if (list->lastItem)
                        list->lastItem->next = NULL;
                else
                        list->firstItem = NULL;

                ll_node_destroy(deleteme);
                return true;
        }

        return false;
}

// remove the given node from the list, and delete it
void ll_remove_node(struct llist *list, struct ll_node *node) {
        assert(node != NULL);

        if (node == list->firstItem)
                list->firstItem = list->firstItem->next;
        else if (node->prev)
                node->prev->next = node->next;

        if (node == list->lastItem)
                list->lastItem = list->lastItem->prev;
        else
                node->next->prev = node->prev;

        ll_node_destroy(node);
}

// remove the first node (traverse from head) with a str matching the given string.
// return true if successful (node found and removed)
// return false if not (node not found)
bool ll_find_remove_node(struct llist *list, char *str) {
        struct ll_node *node = ll_find_node(list, str);
        if (node) {
                ll_remove_node(list, node);
                return true;
        }

        return false;
}

// dump all items in the list
void ll_dump(struct llist *list) {
        int nitems = ll_count(list);
        printf("\nLIST CONTENTS (%d item%s; "
                        "first = <%p:'%s'>   last = <%p:'%s'>)\n",
                        nitems,
                        (nitems != 1) ? "s" : "",
                        list->firstItem,
                        list->firstItem ? list->firstItem->str : "(empty)",
                        list->lastItem,
                        list->lastItem ? list->lastItem->str : "(empty)");

        struct ll_node *node = list->firstItem;
        if (node) puts("");             // spacing

        while(node) {
                printf("  <%p:'%s'>\t next=%p prev=%p\n",
                                node,
                                node ? node->str : "",
                                node->next, node->prev);

                node = node->next;
        }

        puts("--");
}

int main(int argc, char **argv) {
        printf("\nTest part 1:\n");

        llist *list = ll_create();
        ll_insert_tail(list, "delete this item");
        ll_insert_tail(list, "find this item");
        ll_insert_tail(list, "myLastItem");
        ll_insert_head(list, "myFirstItem");

        ll_dump(list);

        ll_find_remove_node(list, "delete this item");
        ll_dump(list);

        if (ll_find_node(list, "find this item"))
                printf("Yay! expected item exists!!\n");
        else
                printf("Uh-oh, didn't find it!\n");

        printf("\n\nTest part 2:\n");

        ll_clear(list);
        ll_insert_head(list, "remove this last item");
        ll_insert_head(list, "List cleared and replaced with this");
        ll_insert_head(list, "remove this first item");
        ll_dump(list);

        ll_remove_head(list);
        ll_dump(list);

        ll_remove_tail(list);
        ll_dump(list);

        ll_destroy(list);
        return 0;
}
\$\endgroup\$
1
  • \$\begingroup\$ Nice effort. Some ideas: 1) Easier to review and less maintain to size to the referenced object than the type: e.g. newItem = malloc(sizeof newItem[0]). 2) Consider handling pointer == NULL rather than failing an assert. e.g. return failure indication. 3) Use const when able, e.g.: ll_find_node(struct llist *list, const char *str), ll_dump(const struct llist *list). 4) int count is reasonable, yet a wider type would cover more cases with little performance impact. \$\endgroup\$ Commented Jun 14, 2023 at 23:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.