I'm starting to learn C and thought it would be a good idea to try and implement some data structures. I started with a very simple stack and would like to hear some opinions.
Stack.c
#include <stdio.h>
#include <stdlib.h>
#include "stack.h"
struct Node {
int element;
struct Node* next;
};
struct Node* head;
int size;
int stack_init() {
struct Node* dummy = malloc(sizeof(struct Node));
if(dummy == NULL) {
return -1;
}
head = dummy;
return 0;
}
int push(int value) {
struct Node* tmp = malloc(sizeof(struct Node));
if (tmp == NULL) {
return -1;
}
tmp->element = value;
tmp->next = head;
head = tmp;
size++;
return 0;
}
int pop() {
if(size == 0) {
printf("Stack is empty\n");
}else {
printf("Element #%d ->value: %d has been removed\n", size, head->element);
free(head);
head = head->next;
size--;
}
return 0;
}
void print_stack() {
struct Node* tmp = head;
printf("Stacksize: %d\n", size);
for(int i = 0;i < size;i++) {
printf("Element %d -> %d\n",i,tmp->element);
tmp = tmp->next;
}
printf("#####################\n");
}
int get_size() {
return size;
}
Stack.h
#ifndef STACK_H
#define STACK_H
int stack_init();
int push(int value);
int pop();
void print_stack();
#endif
2 Answers 2
I see a number of things that may help you improve your code.
Use a stack pointer as a parameter
Right now, one can only have one stack at a time. Worse, if one calls stack_init
after some items have already been pushed onto the stack, there will be a memory leak. An alternative scheme would be to have stack_init()
return a pointer to a Stack
and then use that as a parameter for all other calls. That way, one could have multiple stacks simultaneously, making it that much more useful.
Initialize all values before use
In the stack_init
routine, the head
variable is initialized, but neither head->element
nor head->next
are initialized. It's also probably a good idea to explicitly zero the size
. Although it's already zeroed at the moment because it's a file scope variable, if you follow the other advice on this list, you'll have to initialize it yourself.
Provide a way to free memory
Right now, the only way to free memory is to repeatedly call pop()
. Unfortunately, the calling program has no way to know how many items are on the stack or to know when the stack is empty, so it's not going to be able to know how many times to do so. I'd suggest providing at least an explicit isEmpty()
call.
Provide a complete interface
At the moment, there isn't any way to actually use the stack except to call print_stack()
which limits its usefulness in the extreme. I'd suggest that adding a means of examining the top of the stack, as with a call to top()
that returns the value
for that item might be a good idea.
Don't access freed memory
These lines have a problem:
free(head);
head = head->next;
The problem is that after you've freed head
, you dereference it to get the next
pointer. This is undefined behavior -- anything could happen and it probably won't be good! Better would be to save a copy of the pointer, do your housekeeping and then free the copy like this:
struct Node* temp = head;
head = head->next;
free(temp);
Separate printing from data manipulation
There is no way at the moment to use pop
without it printing something. In a real program that use such a stack, that's unlikely to be the desired effect. Better would be to have pop()
just do what it needs to do and leave printing to the calling program.
Return something useful from functions
Most of your non-void functions return something useful, but pop
always returns 0 which isn't very useful.
Encapsulate related data in a structure
The size
and head
elements are closely related items. For that reason (and to accomodate suggestion #1 on this list), I'd recommend combining them into a struct instead like this:
struct Stack {
struct Node* head;
int size;
};
Use a typedef to simplify your code
If you use a typedef
like this:
typedef struct {
struct Node* head;
int size;
} Stack;
the code can refer to simply Stack
instead of struct Stack
. It's not necessary, but it does tend to make things a little easier to write and to read.
-
1\$\begingroup\$ Wow this is amazing. Thank you so much for this detailed review. I think I understand and agree with all of your points. One thing that I don't totally get is the "provide a complete interface point". Why is there currently no way to use the stack? It is possible to call push() and pop(). I absolutely agree that there should be a top() function though. With "Provide a way to free memory" you basically mean some sort of a destroy_stack() function, right? \$\endgroup\$bw0248– bw02482016年10月22日 21:57:42 +00:00Commented Oct 22, 2016 at 21:57
-
\$\begingroup\$ @c_student: Yes, you've got most of it. As for a complete interface, consider that you might be writing an application that can't print like that (such as a GUI application or an embedded system). For such uses, the current interface is incomplete. \$\endgroup\$Edward– Edward2016年10月22日 22:00:56 +00:00Commented Oct 22, 2016 at 22:00
-
\$\begingroup\$ This will become moot when, as advised, you make this properly 'object oriented' by passing pointers to stacks as parameters to all
stack_*()
functions - thereby making it an actual reusable thing, rather than a singleton, where the latter probably isn't what you really want. But more generally, in situations where you legitimately have variables to be shared by various functions within one compilation unit - like your original setup - then you should declare those variables asstatic
. This signals intent, avoids potential symbol conflicts in non-trivial projects, and may aid optimisation. \$\endgroup\$underscore_d– underscore_d2016年10月22日 23:51:35 +00:00Commented Oct 22, 2016 at 23:51
From a memory management point of view, your implementation is a linked list, that is, all items are allocated separately, and the next item is found by following a pointer. As the theoretical advantages of linked lists lie in insertions and deletions from the middle, they don't make for a very good implementation of a stack, where you only really care about adding and removing from one end.
Especially here, when you're only storing a single number, the pointer to the next item is likely to be at least as large as the data stored, and that's not counting the space overhead of malloc. (Let alone the time spent on updating malloc's internal bookkeeping.)
I would suggest storing the values in a single array, and resizing it with realloc()
as necessary. Something to the direction of this quick mock-up:
struct stack_head {
int *data;
unsigned allocated;
unsigned used;
}
struct stack_head *stack_init(unsigned size)
{
struct stack_head *sh = malloc(sizeof(stack_head));
sh->data = calloc(size, sizeof(int));
sh->used = 0;
sh->allocated = size;
return sh;
}
stack_push(struct stack_head *sh, int value)
{
if (sh->used < sh->allocated) {
/* just add to the end of sh->data[] */
} else {
/* need to realloc() the data area */
}
}
-
\$\begingroup\$ This answer raises an important point. Not only would this use less memory, it would also likely be much faster than the existing linked list. \$\endgroup\$Edward– Edward2016年10月23日 13:06:06 +00:00Commented Oct 23, 2016 at 13:06
-
\$\begingroup\$ Great points, but what I don't understand is why the theoretical advantages of linked lists are inserting and deleting in the middle? Isn't inserting and deleting at the head of a linked list O(1) whereas inserting and deleting in the middle would be O(n)? \$\endgroup\$bw0248– bw02482016年10月23日 14:58:25 +00:00Commented Oct 23, 2016 at 14:58
-
\$\begingroup\$ @c_student, you can delete from the middle of a doubly-linked list in constant time, if you have a pointer to the item to be removed and don't need to search for it. Same for adding if you somehow know the position to add to. But in all cases you don't and searching through the list gets expensive because caches make linear access much faster than random access around dynamically allocated objects. \$\endgroup\$ilkkachu– ilkkachu2016年10月23日 17:15:25 +00:00Commented Oct 23, 2016 at 17:15
if (NULL == foo)
instead of(foo == NULL)
. This way you will get compiler errors if you mistype == to =. \$\endgroup\$if
condition \$\endgroup\$