I'm a C beginner. I'm studying and implementing some data structures. I'd like to get feedback about my style and decisions I made.
This is a Singly Linked List. I decided to keep things as simple as possible writing just the basic operations for the list.
In main
there is some test code.
All the code is in one file because I'd like to store all these implementations in single files (as snippets) for future reference.
By the way this is the first time I post here, please don't be kind.
/*
* Singly Linked List C implementation.
*
* Linear collection of data. Each element (node) contains data and a link to
* the next node.
* The first element is often called "head" and (a pointer to it) must be
* preserved since without it, it's not possible to traverse the list.
* The last element is linked (points) to nothing (NULL).
*
* ----------- ----------- ----------- ----------- ------
* | 17 | --|---->| 3 | --|---->| 22 | --|---->| 11 | --|---->|NULL|
* ----------- ----------- ----------- ----------- ------
* ^
* HEAD
*
* Search: O(n): search operations are performed in linear time.
* Insert: O(1): since all new nodes will replace the head.
* Delete: O(n): since we are deleting the first occurrence of a specific data.
*/
#include <stdlib.h>
typedef struct node
{
int data;
struct node *next;
} node;
/*
* insert: creates and inserts a new node (storing data) in the list pointed by
* the pointer stored at address head.
* Returns the pointer of the new node or NULL if the process failed.
*/
node *insert(node **head, int data)
{
if (head == NULL) {
return NULL;
}
node *new = malloc(sizeof(*new));
if (new != NULL) {
new->data = data;
new->next = *head;
*head = new;
}
return new;
}
/*
* delete: delete the first occurrence of data (i.e the first node which has
* data in its data field) in the list pointed by the pointer stored at
* address head.
* Returns 1 if a node has been deleted, 0 otherwise.
*/
int delete(node **head, int data)
{
if (head == NULL) {
return 0;
}
node *prev = NULL;
node *curr = *head;
while (curr != NULL) {
if (curr->data == data) {
if (prev == NULL) {
*head = curr->next;
} else {
prev->next = curr->next;
}
free(curr);
return 1;
}
prev = curr;
curr = curr->next;
}
return 0;
}
/*
* destroy: delete all nodes in the list pointed by the pointer stored at
* address head, then sets *head to NULL implying an empty list.
*/
void destroy(node **head)
{
if (head == NULL) {
return;
}
node *curr = *head;
node *next;
while (curr != NULL) {
next = curr->next;
free(curr);
curr = next;
}
*head = NULL;
}
/*
* find: returns 1 if there is at least one occurrence of data in the list
* pointed by head, 0 otherwise.
*/
int find(node *head, int data)
{
node *trav = head;
while (trav != NULL) {
if (trav->data == data) {
return 1;
}
trav = trav->next;
}
return 0;
}
#include <stdio.h>
void print_list(node *head);
int main(void)
{
int n;
node *head = NULL;
for (n = 0; n < 10; n++) {
insert(&head, n);
}
print_list(head);
printf("Deleting all elements...\n");
for (n = 0; n < 10; n++) {
delete(&head, n);
}
print_list(head);
printf("Inserting other elements...\n");
insert(&head, 25);
insert(&head, 49);
insert(&head, 57);
insert(&head, 22);
insert(&head, 90);
insert(&head, 27);
insert(&head, 22);
print_list(head);
for (n = 20; n < 30; n++) {
printf("Looking for %i: ", n);
if (find(head, n)) {
printf("%i is in the list\n", n);
} else {
printf("%i is not in the list\n", n);
}
}
printf("\n");
n = 57;
printf("Deleting first occurrence of %i...\n", n);
delete(&head, n);
print_list(head);
n = 22;
printf("Deleting first occurrence of %i...\n", n);
delete(&head, n);
print_list(head);
printf("Deleting head...\n");
printf("Current head: pointer: %p data: %i\n", head, head->data);
delete(&head, head->data);
printf("Head after deletion: pointer: %p data: %i\n", head, head->data);
print_list(head);
n = 25;
printf("Deleting last node...\n");
delete(&head, n);
print_list(head);
printf("Trying functions with NULL head or not stored data...\n");
printf("insert(NULL, 33) returns %p\n", insert(NULL, 33));
printf("find(NULL, 90) returns %i\n", find(NULL, 90));
printf("delete(NULL, 90) returns %i\n", delete(NULL, 90));
node *null_h = NULL;
printf("\nnode *null_h = NULL;\ndelete(&null_h, 90) returns %i\n\n",
delete(&null_h, 90));
printf("delete(&head, 33) returns %i\n", delete(&head, 33));
printf("destroy(NULL)\n");
destroy(NULL);
print_list(head);
printf("Destroying the list\n");
destroy(&head);
print_list(head);
}
void print_list(node *head)
{
printf("List: ");
if (head == NULL) {
printf("is empty");
} else {
node *trav = head;
while (trav != NULL) {
printf(" %i ", trav->data);
trav = trav->next;
}
}
printf("\n\n");
return;
}
6 Answers 6
Include all library headers near the beginning of the file
#include <stdlib.h>
#include <stdio.h>
This has the following advantages:
- Gives readers a quick glance at the libraries used throughout the source file.
- Results in more comprehensible error messages when your declarations clash with those in an included header.
- Avoids potential surprises if a header file alters macro definitions or compiler settings that affect subsequent code.
Avoid identifiers that clash with keywords of similar languages
new
and delete
are C++ keywords and C code is commonly included in C++ programs. Therefore it would be better to avoid these and other identifiers that happen to be C++ keywords.
You can use something like new_node
to refer to a newly created node structure instead.
See the next section for a better name for delete
.
Use more descriptive function names
insert
, delete
and destroy
(and other verbs) aren't very descriptive because they're ambiguous on what to [verb]. Use something like [verb]+[noun], e. g. insert_element
, delete_element
, destroy_list
. You already do that with print_list
.
Use puts
or fputs
to print fixed strings
puts("Deleting all elements...");
or
fputs("Deleting all elements...\n", stdout);
You can use puts
or fputs
instead of printf
to print fixed strings. The former are simpler than the latter and have no drawback in the absence of format arguments. Additionally, you don't need to quote %
characters.
Some compilers (e. g. GCC) will optimise calls to printf
with fixed strings without format specifiers to calls to puts
/fputs
.
Watch out that puts
appends a new-line character at the end of the string while fputs
doesn't.
-
\$\begingroup\$ Yeah, the headers, since I want to store these implementations as snippets for future reference, I'd like that only the headers required by the implementation are at the top.
stdio.h
is not necessary for the list itself, (neitherprint_list
nor thatmain
), I added it only for test purpose, that's why it is at the bottom. Probably I should have specified it better, my bad, I'm sorry. Definitely I'm going to change those trivial and ambiguous names and thank you for theputs
advice, I'll use it from now on! \$\endgroup\$MarcoLucidi– MarcoLucidi2017年06月11日 15:32:13 +00:00Commented Jun 11, 2017 at 15:32
Hm... is the use of names like new
and delete
intentional? While perfectly legal, it might provoke confusion (in a "mixed" project), or even become a hindrance one day when re-using library code.
Suggestion: maybe find
could return a node*
(or NULL
). So one could easily use the return value for editing the node value, deleting the node, or inserting before the node.
-
7\$\begingroup\$ To be clear, this is because
new
anddelete
are keywords in C++. In a pure C project, there's nothing inherently wrong with them. \$\endgroup\$Edward– Edward2017年06月10日 20:52:20 +00:00Commented Jun 10, 2017 at 20:52 -
1\$\begingroup\$ Silly me, the thing is that I never programmed in C++ nor in a "mixed" project of course. Definitely I'm going to change those trivial and ambiguous names. Thank you for the
find
suggestion, maybe I'll implement in that way, but I guess you mean insert after the node, since I have only the next pointer. Even deleting a node would be tricky knowing only the pointer returned byfind
. Am I wrong? \$\endgroup\$MarcoLucidi– MarcoLucidi2017年06月11日 14:50:55 +00:00Commented Jun 11, 2017 at 14:50 -
\$\begingroup\$ @MarcoLucidi Then you've got a good taste in naming functions ;) If you return
trav
infind
, one could use it to change the "found" value. Less obvious: "insert before" is possible, too (by the way, it's the standard behaviour in C++). I'll refer to the found item as current here, as innode *current=find(head, val)
. Of course,insert
can't change the pointer to current... but you can copy "current" data/next into the new item, then the value to be inserted into "current", and let current->next point to the new item. \$\endgroup\$jvb– jvb2017年06月11日 17:38:12 +00:00Commented Jun 11, 2017 at 17:38 -
\$\begingroup\$ @jvb ahh yeah, that's fair! Though doing that could "invalidate" a variable storing the pointer of a specific node (because of the data change), but I guess is an uncommon case. \$\endgroup\$MarcoLucidi– MarcoLucidi2017年06月11日 17:59:06 +00:00Commented Jun 11, 2017 at 17:59
a typedef is handy to define a linked list type:
typedef node* llist;
and use that in place of **node
.
There is an advantage to NOT storing any data in the Head of a list, instead putting a sentinel value in it instead (like -1, or INT_MAX) and defining a few utility functions.
is_head(node *n) { return (n->data == INT_MAX); }
is_last(node*n) { return (n->next == NULL); }
is_empty(llist l){ return ( (*l)->next == NULL);}
You just have to remember to skip over the head nodeto head->next every time you are accessing the list.
It is notable that your insert function inserts at the beginning and not at the end of the list. This is how a stack works, but stacks use push() and pop() , removing the beginning. Most singly linked-lists append data to the end with their next pointer set to NULL, unless stack behavior is wanted, and keep the list head stable for the life of the list. Changing the head node to a new struct every insert, while no problem for the computer is more likely to be a problem later on if you send your list into a function which changes it, and sends back a different address.
It looks as if you are duplicating the find function inside the delete function. It may be more useful for find() to return a node* to the node found or NULL
if not found, and have delete take the list and the node to delete as parameters.
node* n = find( 100);
if(n) delete(list, n);
The thing about a singly-linked list that has an advantage over an array
is that insertion and deletion leave all the other nodes in the same place
and any variables pointing to any individual node are still valid.
To insert or delete data into the middle of an array requires resizing it and shifting every item after the insert or delete, and all the data after is in a different memory location, unless you only insert at the very end.
So you need to write a couple of node
functions:
insert_after() /* after a node */
insert_before() /* before a node */
append() /* insert at end */
prepend() /* insert after head */
set_data() /* change the data, leave the node */
get_data() /* get the data from the node */
push() /* stack prepend to top*/
pop() /* get_data and delete from top */
swap() /* if you want to sort a list. */
-
2\$\begingroup\$ I have to disagree with the very first suggestion in this answer. It is almost never a good idea to hide a pointer behind a typedef. It just creates confusion and invites bugs. \$\endgroup\$Cody Gray– Cody Gray2017年06月11日 05:43:15 +00:00Commented Jun 11, 2017 at 5:43
-
\$\begingroup\$ I thought about not storing anything in the head but it looked like a wasted node and for simplicity I stated that
NULL
represents an empty list. Inserting a node at the end, would require to traverse the list at each insertion since I'm not storing the tail (again for simplicity). In order to makedelete
work that way I have to traverse the list twice since I'll lose the pointer to the previous node (Am I right?). I'll write those functions when and if I'll need them because this is meant to be a minimal implementation. \$\endgroup\$MarcoLucidi– MarcoLucidi2017年06月11日 16:07:55 +00:00Commented Jun 11, 2017 at 16:07 -
\$\begingroup\$ A deletion of a node involves 3 node pointers. parent->next , current , current->next There's only one way to do it usually; its in all the books. Why reinvent the wheel? Some of these things you learn by copying. \$\endgroup\$Chris Reid– Chris Reid2017年06月12日 20:38:49 +00:00Commented Jun 12, 2017 at 20:38
-
\$\begingroup\$ @ChrisReid I think I'm aware about the pointers involved in a deletion. I'm asking, following your suggestion, where do I take
parent
to changeparent->next
without loop through the list again? (by the way, that's why I duplicated a bit offind
indelete
). \$\endgroup\$MarcoLucidi– MarcoLucidi2017年06月13日 09:01:32 +00:00Commented Jun 13, 2017 at 9:01
As the
insert(node **head, int data)
function receives pointer to thehead
node as its first argument you could rather usebool
orint
as a return value, to check whether the operation was successful.unless you are doing the other way around (depending on returne value to modify
head
node), the return value (node *
) has the same significance of any other type that holdsstatus
. so why not useint
. (like the other subroutines).As others have mentioned indentation of
4 spaces
is optimal for statements inside a block.Make good use of the return values if they deserve.
minor
I'd prefer not to make single statement as block. Both are the same it's just a choice you have.
-
\$\begingroup\$ Yeah, probably would be better return a
bool
(orint
) for success or failure ininsert
. Thank you for the suggestions. \$\endgroup\$MarcoLucidi– MarcoLucidi2017年06月11日 16:48:55 +00:00Commented Jun 11, 2017 at 16:48
- Put all your
#include
s at the top of your source file. - Use variable names that are full words - let the IDE do the repetitive typing via autocompletion.
destroy()
seems kind of weird as a name - why notfree_list()
?- In
print_list
and other places,trav
, for "traverse", should also be named ascurrent
, as in the rest of the code. - It is standard practice to put the declaration (interface) in a separate header file - however, it seems you're aware of this and you've avoided doing this deliberately for good reason, so I won't dwell on this further.
- Your indentation is non-standard: 4 spaces to a tab is considered standard by most programmers.
- You do not follow a consistent bracing style between functions and statement blocks - use the same for both.
- You sometimes leave a useless line break after the body of the
if
- if this is intentional, be consistent. - An explicit
return
is not required at the end of a method of return typevoid
, as inprint_list
. - Try not to use
new
ordelete
as variable names - it makes your code uncompilable by a C++ compiler as they are C++ keywords which occur in identifier positions in your code, although the rest of the code is undoubtedly valid C++, too. - Use the Boolean data type to indicate
true
/false
conditions - in a C99-compliant (or above) compiler (say,gcc
with thestd=c99
option passed to it), the typebool
and the macro literalstrue
andfalse
are available from the header filestdbool.h
.
Given all the above, there are no logical issues with your code that I can find.
-
\$\begingroup\$ Thank you for all your points, I'd like to reply to few of them: 4. I used
trav
when only a pointer was sufficient,prev
,curr
andnext
when multiple pointers were required; 6. 7. I try to follow the "Linux Kernel style"; 8. is intentional when the line after theif
is long or a bit "cryptic" or when the first character is*
, since, without a full blank line it doesn't look "clean" (to me) on my editor. \$\endgroup\$MarcoLucidi– MarcoLucidi2017年06月11日 16:37:38 +00:00Commented Jun 11, 2017 at 16:37 -
1\$\begingroup\$ 4. My advice about using whole names for variables still holds; I was not aware of your specific issues, so I gave generalized advice. \$\endgroup\$Tamoghna Chowdhury– Tamoghna Chowdhury2017年06月12日 09:32:09 +00:00Commented Jun 12, 2017 at 9:32
in printList(), this line:
if (head == NULL) {
is not correct for determining if the list is empty. After all, that is the address of the head
variable in main(), not the contents of head
. I.E. the pointer needs to be dereference to get the contents of 'head'. Suggest:
if ( NULL == *head ) {
Note. placing the literal on the right in such a comparison is highly prone to the keypunch error of typing = instead of == not being caught by you. By placing the literal on the left, the compiler will flag such a keypunch error rather than you having to develop lots of gray hairs trying to debug the problem.
-
\$\begingroup\$ The declaration for
print_list
isvoid print_list(node *head);
.head
is a simple pointer, not a pointer to pointer. With your suggestion, the code doesn't even compile (have you try it?). If you notice, the functions that "write" the list (i.e add or delete nodes) take a pointer to pointer, while functions that only need to "read" the list, take a simple pointer. \$\endgroup\$MarcoLucidi– MarcoLucidi2017年06月12日 09:30:50 +00:00Commented Jun 12, 2017 at 9:30 -
\$\begingroup\$ @MarcoLucidi, regarding: my statement about the checking of the POINTER to head, in printlist(), , the parameter
head
is defined as*head
. So the actual contents of the pointer is NOT the contents of the callers'head
, but rather the address of the callers'head
, so checking that address for 'NULL' is an error while checking the contents ofhead
because it 'could' be NULL. \$\endgroup\$user3629249– user36292492017年06月16日 07:42:23 +00:00Commented Jun 16, 2017 at 7:42
main
? Can you be more specific? \$\endgroup\$insert
anddestroy
simply returning when not handed a list. \$\endgroup\$insert
returnsNULL
(failure), probably I should changedestroy
to return abool
(orint
) though. Anyway, what do you suggest for "not swallow errors"? \$\endgroup\$head
againstNULL
because if that will ever happen would be a programming error indeed, hard to detect, and so, leave the program crash while dereferencing, would be better in order to spot the error. Did I understand well? \$\endgroup\$