I wrote a generic Queue that could with work any data type you give it. The Queue
can do any the basic operations that you would expect a Queue
can do such as enqueue
, dequeue
, peek
and so on.
I would like you to critique me on:
- My general style
- My ability to properly deallocate memory so that there is no memory leaks
- Properly handling memory allocation failure
- Anything else that you would like to add
Queue.h
#ifndef QUEUE_H_INCLUDED
#define QUEUE_H_INCLUDED
typedef struct Node
{
void *data;
struct Node *next;
}node;
typedef struct QueueList
{
int sizeOfQueue;
size_t memSize;
node *head;
node *tail;
}Queue;
void queueInit(Queue *q, size_t memSize);
int enqueue(Queue *, const void *);
void dequeue(Queue *, void *);
void queuePeek(Queue *, void *);
void clearQueue(Queue *);
int getQueueSize(Queue *);
#endif /* QUEUE_H_INCLUDED */
Queue.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "Queue.h"
void queueInit(Queue *q, size_t memSize)
{
q->sizeOfQueue = 0;
q->memSize = memSize;
q->head = q->tail = NULL;
}
int enqueue(Queue *q, const void *data)
{
node *newNode = (node *)malloc(sizeof(node));
if(newNode == NULL)
{
return -1;
}
newNode->data = malloc(q->memSize);
if(newNode->data == NULL)
{
free(newNode);
return -1;
}
newNode->next = NULL;
memcpy(newNode->data, data, q->memSize);
if(q->sizeOfQueue == 0)
{
q->head = q->tail = newNode;
}
else
{
q->tail->next = newNode;
q->tail = newNode;
}
q->sizeOfQueue++;
return 0;
}
void dequeue(Queue *q, void *data)
{
if(q->sizeOfQueue > 0)
{
node *temp = q->head;
memcpy(data, temp->data, q->memSize);
if(q->sizeOfQueue > 1)
{
q->head = q->head->next;
}
else
{
q->head = NULL;
q->tail = NULL;
}
q->sizeOfQueue--;
free(temp->data);
free(temp);
}
}
void queuePeek(Queue *q, void *data)
{
if(q->sizeOfQueue > 0)
{
node *temp = q->head;
memcpy(data, temp->data, q->memSize);
}
}
void clearQueue(Queue *q)
{
node *temp;
while(q->sizeOfQueue > 0)
{
temp = q->head;
q->head = temp->next;
free(temp->data);
free(temp);
q->sizeOfQueue--;
}
q->head = q->tail = NULL;
}
int getQueueSize(Queue *q)
{
return q->sizeOfQueue;
}
TestQueue.c
#include <stdio.h>
#include <stdlib.h>
#include "Queue.h"
int main()
{
int val;
Queue q;
queueInit(&q, sizeof(int));
for(val = 0; val < 10; val++)
{
enqueue(&q, &val);
printf("The value %d has been enqueued.\n", val + 1);
}
printf("\n");
queuePeek(&q, &val);
printf("The value that is at the front of the queue is %d\n\n", val + 1);
while(getQueueSize(&q) > 0)
{
dequeue(&q, &val);
printf("%d has been dequeued.\n", val + 1);
}
return 0;
}
Output
3 Answers 3
Pattern-less function names. For
Queue.h
, rather see function named obviously make sense together likeQueue_Init()
,Queue_GetSize()
thanqueueInit()
,getQueueSize()
, etc.Comments with
#
preprocessing may not be portable// #endif /* QUEUE_H_INCLUDED */ #endif
There is no need for a
head
andtail
. Alternative, only storetail
and have the tail point to the head of the list. End of list detected whenp->next == tail->next
. This makes your head node one field smaller. Important if code uses lots of queues.Unclear why code uses
int
for the queue size type. A signed type is not needed (could useunsigned
) and on a system wheresize_t
could be much wider thanint
, a queue size likesize_t
is more prudent. Robust code would check for a queue exceed the max value ofsize
inenqueue()
.Good use of
size_t
formemSize
. Good error checking formalloc()
. Good to have test case. IMO, a commented sample usage in the*.h
file is nice. ; the*.h
being the public interface to your good code.Pedantic: Robust would check for
memSize==0
inqueueInit()
as that negates the correctness of themalloc()
checks which should beif(newNode->data == NULL && q->memSize > 0)
.A little documentation goes a long way. suggest a line or two of comment preceding each function declaration in
Queue.h
.Functions like
queuePeek(Queue *q, void *data)
that do not alter*q
should be declaredqueuePeek(const Queue *q, void *data)
. This self documents the unchanging nature ofq
in the function to users and allows some optimizations a compiler may not otherwise employ. It is a check on the implementation of the function too.For completeness, suggest
q->memSize = 0
inclearQueue()
.Change
#include
order. Put"Queue.h"
first as a check thatQueue.h
does not depend on the 3<*.h>
include files - unless that.h
file is coded inQueue.h
.#include "Queue.h" #include <stdio.h> #include <stdlib.h> #include <string.h> // #include "Queue.h"
Indicate failure. If
void dequeue(Queue *q, void *data)
does not have anything in the queue to copy todata
, there is no indication of that here. Perhaps returntrue/false
indicating success. Same forqueuePeek()
.For debugging, zero filling memory before
free()
I have found useful. Errant code tends to fail faster with a 0 pointer/data than with its original data still potentially intact. Faster failing code is easier to debug. YMMV.Opinion: Storing the queue size is of dubious value, unless of course that is the reason for the
queue
type - one with a quicksize
report. Alternative, drop thesize
field and calculate when needed. More often, I have found the need forbool Queue_IsEmpty(const Queue *q)
sufficient than needing a quicksize
and prefer to drop the ever presentsize
field.
-
-
\$\begingroup\$ In regards to number 10, I have to use Queue.h after the stdlib.h else it wont recognize any variable in my header file that have the type size_t. Could you elaborate on 12 with an example? What do you mean by zero filling memory before free? Like setting the pointer to NULL before calling free? \$\endgroup\$Luis Averhoff– Luis Averhoff2016年09月14日 00:18:36 +00:00Commented Sep 14, 2016 at 0:18
-
\$\begingroup\$ @Luis Averhoff The .h file should contain
#include <stdlib.h>
. Your commented problem is precisely why#include "Queue.h"
should be first (#10) as it detected that - but IMO, your fix was wrong. \$\endgroup\$chux– chux2016年09月14日 11:45:45 +00:00Commented Sep 14, 2016 at 11:45 -
\$\begingroup\$ @Luis Averhoff Per #12, recommending
memset(newNode, 0, sizeof *NewNode); free(newNode);
to zero the referenced data pointed to bynewNode
beforefree()
. \$\endgroup\$chux– chux2016年09月14日 11:48:04 +00:00Commented Sep 14, 2016 at 11:48
Node
seems quite a common struct name to me. And you don't use it outside of the Queue
struct. So I think you could define it in the .c file to avoid using up that precious type name.
-
1\$\begingroup\$ Good point. it makes sense to made the node structure private to the queue.c file. \$\endgroup\$Luis Averhoff– Luis Averhoff2016年09月13日 18:17:14 +00:00Commented Sep 13, 2016 at 18:17
#include <stdbool.h>
I would include the above header file and return true
/false
from enqueue
/dequeue
depending whether the operation was successful or not.
Don't copy data
Also, I would not copy the actual objects to the queue nodes. You can simply store the void* pointers in the queue nodes. This will improve performance of your software.
Minor
You could remove #include <stdio.h>
from the .c file since console I/O is not actually used.
Otherwise, your code and API seem plausible to me.
-
\$\begingroup\$ So for queuePeek, I can have it return a void * like this
return q->head->data;
. For enqueue, I can donewNode->data = data;
correct? It seems for dequeue that I'm going to have use memcpy. \$\endgroup\$Luis Averhoff– Luis Averhoff2016年09月13日 15:23:22 +00:00Commented Sep 13, 2016 at 15:23 -
\$\begingroup\$ @LuisAverhoff I would rather stick to your pattern of returning a boolean as an indicator of the success of the called operation. If you, say, peek from an empty queue, don't modify anything pointed by *data, but return false. If not empty, return true and set data appropriately. \$\endgroup\$coderodde– coderodde2016年09月13日 15:26:08 +00:00Commented Sep 13, 2016 at 15:26
-
1\$\begingroup\$ Disagree with "Don't copy data". That idea makes sense if the data to be queued is always a pointer. Yet OP's goal is "work any data type you give it". Example. If my data was type
long double
, trying to copy that or its address into avoid *
would fail. \$\endgroup\$chux– chux2016年09月13日 18:58:51 +00:00Commented Sep 13, 2016 at 18:58 -
1\$\begingroup\$ Pass an
long double*
and only storing the pointer as you suggest obliges the calling code to allocate space and goes against OP's goal of "work any data type you give it". Pedantically your idea does not handle pointer to functions, which may not fit invoid*
- unless calling code allocates for that too. OP's interacts with calling code by passing a reference to the data, not the data itself (which may be of any size). OTOH I have, worked with libraries had the best/worst of both, whenmemSize <= sizeof(void*)
, no need to allocate, just store the value in a unionizedvoid*
. \$\endgroup\$chux– chux2016年09月13日 19:43:59 +00:00Commented Sep 13, 2016 at 19:43 -
1\$\begingroup\$ @Luis Averhoff Re: "I also dont think I need to do memcpy in enqueue either." Consider
{ long double x = 1/7.0; enqueue(q, &x); } .... { long double y; dequeue(q, ....);
How to calldequeue()
to put a value iny
? without amemcpy()
inenqueue()
? The address forx
is long since invalid. \$\endgroup\$chux– chux2016年09月13日 20:04:15 +00:00Commented Sep 13, 2016 at 20:04
int val while(q->sizeOfQueue > 0){dequeue(q, &val)} q->head = q->tail = NULL;
This of course would be in the clearQueue function. \$\endgroup\$