This is my first attempt at writing a C program, it a generic stack that can grow accordingly. It appears to work correctly, however I am worried that is just a fluke and I could be doing something very wrong.
I would greatly appreciate any feedback/criticism.
I am aware I should split this out into a .h and .c file but for demonstration purposes I have listed it as one.
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
typedef struct Stack
{
void** data;
int capacity;
int count;
} Stack;
void stack_init(Stack *stack, int capacity)
{
stack->data = (void**)malloc(capacity * sizeof(void*));
stack->capacity = capacity;
stack->count = 0;
}
void stack_push(Stack *stack, void* entry)
{
if (stack->count >= stack->capacity)
{
stack->capacity *= 2;
stack->data = (void**)realloc(stack->data, stack->capacity * sizeof(void*));
}
stack->data[stack->count] = entry;
stack->count++;
}
void* stack_pop(Stack *stack)
{
stack->count--;
return stack->data[stack->count];
}
bool stack_is_empty(Stack *stack)
{
return (stack->count == 0);
}
int main()
{
Stack myStack;
stack_init(&myStack, 2);
for (int i = 0; i < 2; i++)
{
stack_push(&myStack, i);
}
while (!stack_is_empty(&myStack))
{
printf("%d\n", (int)stack_pop(&myStack));
}
return 0;
}
3 Answers 3
I suggest that you do more error checking:
malloc
orrealloc
may fail.I can initialize the stack with negative capacity.
I can pop more elements than I pushed.
In all these cases, your implementation silently swallows the errors and accesses invalid addresses.
As πάντα ῥεῖ said in comments, if you want to store generic elements that exceed void*
in size, you need to allocate memory for them and let the void*
point to them, e.g.:
typedef struct {
// ...
} BigStruct;
// pushing
BigStruct* data = malloc(sizeof(BigStruct));
// initialize *data
stack_push(&stack, data);
// popping
BigStruct* data = stack_pop(&stack);
// use *data
free(data);
Some small issues:
It is not common to cast the result of
malloc
orrealloc
in C.If the stack is initialized to capacity 0, then the stack will never grow and elements will be stored at invalid positions. A solution is to adjust the capacity to 1 automatically.
There are two main problems with the code that must be addressed:
- You store pointers to data in an array, rather than actual data. This is not useful, since pointers may point to data that goes out of scope or otherwise becomes obsolete. When writing a container class such as a stack/LIFO, one should store so-called "hard copies" of the data passed.
The code is not valid C. You implicitly convert from an
int
which is data, to avoid*
which is a pointer. Details here: "Pointer from integer/integer from pointer without a cast" issues. To prevent non-C from compiling without errors then set gcc/clang/icc to always compile with-std=c11 -pedantic-errors
.Apart from the implicit conversion itself being invalid, there is no guarantee that you can safely convert from
int
tovoid*
and back even if you use a cast. This conversion relies on poorly specified behavior.
Because of these two remarks, the code cannot be fixed, it must be rewritten from scratch. To rewrite this into a proper stack container, it must be rewritten to use bytes of raw data (uint8_t
) and make hard copies of the data passed.
Other remarks:
- When popping from the stack, check
count
to see if there actually are any items left! typedef struct Stack { ... } Stack;
The "struct tag in this case is superfluous, you can just writetypedef struct { ... } Stack;
.int main()
is an obsolete form of main() declaration, always useint main (void)
instead (or the version with argv+argc).- You don't use const correctness. Functions that do not change the passed
Stack*
shouldconst
qualify it:bool stack_is_empty(const Stack *stack)
. - When storing the size of an array, the integer type
size_t
is more correct to use thanint
, sinceint
may not be large enough and in addition it is signed. return (stack->count == 0);
No parenthesis needed here, it's just clutter.(void**)malloc
etc. No cast needed here, it's just clutter.- When calling malloc and realloc, always check if they succeeded by checking the result against NULL. -Calling realloc repeatedly like this is inefficient. That's why a stack is usually implemented as a double linked linked list. You may not have learnt about these yet, but they are the most suitable way to implement containers that often add/remove items at the top or bottom, at the case of slower access time for items located at a random place inside the container. You will not use this stack like that, so a linked list would be more suitable.
-
1\$\begingroup\$ "Calling realloc repeatedly like this is very inefficient. That's why a stack is usually implemented as a double linked linked list. You may not have learnt about these yet, but they are the most suitable way to implement containers that often add/remove items at the top or bottom." Hmm ... C++ vectors do this, and usually outperform linked lists. Am I missing something special about
realloc
? \$\endgroup\$L. F.– L. F.2020年03月04日 08:13:02 +00:00Commented Mar 4, 2020 at 8:13 -
1\$\begingroup\$ C++ vectors usually outperform linked lists, but it does depend on the access patterns. I expect the optimum data structure for a stack is more like a rope: a linked list of chunks; each chunk big enough to accommodate a decent number of items. No reallocation, but very few allocations, and very little pointer chaining. \$\endgroup\$Toby Speight– Toby Speight2020年03月04日 09:04:27 +00:00Commented Mar 4, 2020 at 9:04
-
1\$\begingroup\$ @L.F.
realloc
or heap allocation in general is slow. Therefore, the implementation of std::vector uses an allocation scheme along the lines of: "allocate align * n bytes upon creation, when running out of memory reallocate align * 2n", doubling the size each time. This reduces the amount of calls. Similar algorithms are often used in C too when designing a string ADT or whatever. Of course a std::vector or any array outperforms a linked list in access time, since it has random access. Arrays vs linked lists is very basic computer science stuff. \$\endgroup\$Lundin– Lundin2020年03月04日 10:21:56 +00:00Commented Mar 4, 2020 at 10:21 -
2\$\begingroup\$ Isn't OP doing what you are describing?
stack->capacity *= 2;
Also don't forget that linked lists have space implications. \$\endgroup\$L. F.– L. F.2020年03月04日 10:33:25 +00:00Commented Mar 4, 2020 at 10:33 -
1\$\begingroup\$ @Lundin Wouldn't linked list perform even worse for lots of pushing and popping, since you need to allocate memory from heap for every push, even if you are not reaching a new maximum size? \$\endgroup\$JiK– JiK2020年03月04日 13:06:53 +00:00Commented Mar 4, 2020 at 13:06
Ease of clean-up
Consider a function to empty a Stack
and free its allocated memory.
void stack_clean_up(Stack *stack) {
free(stack->data);
stack->data = NULL;
stack->capacity = 0;
stack->count = 0;
}
int foo() {
Stack myStack;
....
// Preceding code all done, time to clean up
stack_clean_up(&myStack);
return 0;
}
-
\$\begingroup\$ Thanks, on my second revision I did add one like this. codereview.stackexchange.com/questions/238380/… \$\endgroup\$NoOdle– NoOdle2020年03月05日 10:15:47 +00:00Commented Mar 5, 2020 at 10:15
double
or any kind ofstruct
this code will fail spectacularly.. What you're doing is wrong.void*
looses any type information, including the size of the original type. Thussizeof(void*)
for allocation is futile from the beginning. You're allocating space for a pointer size. \$\endgroup\$void*
space to store anint
under the assumption thatsizeof(int) <= sizeof(void*)
. The practice of (ab)using the genericvoid*
data itself to store the value instead of pointing to an external data block (and thus worrying about lifetime) is common in C. \$\endgroup\$int
values only, and makes the stack interface quite confusing. \$\endgroup\$