我正在尝试重用通用队列模块(push,pop,init等)来处理存储多个不同的结构。
例如,我有以下模块:
的main.c queue.c task1.c task2.c
任务1和任务2都有自己需要排队的独特结构(task_1_t和task_2_t)。我正在考虑将队列实现为uint8的简单数组。然后我可以将结构传递给queue_push函数,作为uint8的数组和sizeof(task_x_t),并将数据复制到队列中。然后,当我弹出时,我可以返回一个指向数组的指针并将其强制转换回正确的task_x_t。
这有意义吗?有没有更好的方法来解决这个问题?
#define MAX_QUEUE_LEN 300
/**
* @brief Queue entry
*/
typedef struct queue_data
{
uint8_t data[MAX_QUEUE_DATA]; /**< Data array */
} queue_data_t;
/**
* @brief Structure to represent a queue
*/
typedef struct seque
{
uint8_t head; /**< Index of head */
uint8_t tail; /**< Index of tail */
uint8_t size; /**< Size of queue */
uint8_t capacity; /**< Max size of queue */
size_t dataSize; /**< Size of data structure to be stored in queue */
queue_data_t data[]; /**< Array of queue entries */
} seque_t;
/**
* @brief Queue error messages
*/
typedef enum
{
QUEUE_OK, /**< Command successful */
QUEUE_ERROR, /**< General error */
QUEUE_EMPTY, /**< Queue is empty */
QUEUE_FULL /**< Queue is full */
} queue_error_t;
/* Function Prototypes */
seque_t * queueInitialize ( uint8_t capacity );
void queueDeInitialize ( seque_t * q );
bool queueIsFull ( seque_t * queue );
bool queueIsEmpty ( seque_t * queue );
uint8_t queueGetSize ( seque_t * queue );
queue_error_t queuePush ( seque_t * queue, void * data, uint16_t len );
queue_error_t queuePop ( seque_t * queue, uint8_t ** item );
queue_error_t queueHead ( seque_t * queue, uint8_t ** item );
void queuePrint ( seque_t * queue );
queue_error_t queueClear ( seque_t * queue );
/**@brief Initializes a queue of given capacity. Size of the queue is initialized as 0
* @details Sets the read and write pointers to 0
*
* @param[in] capacity - number of elements in the queue
*
* @return Pointer to the queue object
*/
seque_t * queueInitialize( uint8_t capacity )
{
uint16_t size = sizeof(seque_t) + (sizeof(queue_data_t) * capacity);
Log_Write(LOG_DEBUG, "Size = %d\r\n", size);
seque_t * queue = (seque_t*) malloc(sizeof(seque_t) + (sizeof(queue_data_t) * capacity));
if (queue == NULL)
{
return NULL;
}
queue->capacity = capacity;
queue->size = 0;
queue->head = 0;
queue->tail = capacity -1;
return queue;
}
/**@brief DeInitializes a queue. Frees the memory allocated for that queue
* @param[in] q - pointer to the queue to free
*/
void queueDeInitialize( seque_t * q )
{
free(q);
}
/**@brief Returns true if the queue is full. The queue is full when it's size
* is equal to it's capacity
*
* @param[in] queue - pointer to the queue to check
*
* @return Whether or not the queue is full
*/
bool queueIsFull( seque_t * queue )
{
return (queue->size == queue->capacity);
}
/**@brief Returns true if the queue is empty. The queue is full when it's size is 0
*
* @param[in] queue - pointer to the queue to check
*
* @return Whether or not the queue is empty
*/
bool queueIsEmpty(seque_t* queue)
{
return queue && (queue->size == 0);
}
/**@brief Returns the size of the queue
*
* @param[in] queue - pointer to the queue to check
*
* @return Number of entries in the queue
*/
uint8_t queueGetSize(seque_t* queue)
{
return queue->size;
}
/**@brief Adds an item to the queue. Changes the tail and size of queue
*
* @param[in] queue - pointer to the queue to push to
* @param[in] item - pointer to start of data to push to queue
* @param[in] len - number of bytes to copy
*
* @return None
*/
queue_error_t queuePush(seque_t * queue, void * data, uint16_t len)
{
if (queueIsFull(queue))
{
return QUEUE_FULL;
}
if (len > sizeof(queue_data_t))
{
len = sizeof(queue_data_t);
}
/* Increment tail counter */
queue->tail = (queue->tail + 1) % queue->capacity;
/* Copy Data */
memcpy(queue->data[queue->tail].data, data, len);
/* Update queue size */
queue->size = queue->size + 1;
return QUEUE_OK;
}
/**@brief Remove an item from the queue. Changes the head of queue
*
* @param[in] queue - pointer to the queue to pop from
*
* @return The popped item
*/
queue_error_t queuePop(seque_t* queue, uint8_t ** item)
{
if (queueIsEmpty(queue))
{
return QUEUE_EMPTY;
}
*item = queue->data[queue->head].data;
queue->head = (queue->head + 1)%queue->capacity;
queue->size = queue->size - 1;
return QUEUE_OK;
}
/**@brief Function to get head of queue
*
* @param[in] queue - pointer to the queue to get the head of
* @param[out] item - double pointer to structure to hold the data
*
* @return Error code
*/
queue_error_t queueHead(seque_t* queue, uint8_t ** item)
{
if (queueIsEmpty(queue))
{
return QUEUE_EMPTY;
}
*item = queue->data[queue->head].data;
return QUEUE_OK;
}
/**@brief Function to clear a queue
*
* @param[in] queue - pointer to the queue to clear
*
* @return Error code
*/
queue_error_t queueClear ( seque_t * queue )
{
queue->size = 0;
queue->head = 0;
queue->tail = queue->capacity -1;
return QUEUE_OK;
}
/**@brief Function to peek at the head of queue
*
* @param[in] queue - pointer to the queue to get the head of
* @param[out] item - pointer to structure to hold the data
*
* @return Error code
*/
queue_error_t queuePeek(seque_t * queue, void * item)
{
if (queueIsEmpty(queue))
{
return QUEUE_EMPTY;
}
/* Copy out the data */
memcpy(item, queue->data[queue->head].data, queue->dataSize);
return QUEUE_OK;
}
答案 0 :(得分:1)
使用void *
来表示C中的多态性。void *
可以指向任何类型的对象,因此您可以编写通用函数,如queue_push
和queue_pop
,它们可以指向要以void *
的形式推送和弹出的对象。 queue_push
/ queue_pop
代码并不关心所指向的类型是什么,调用者知道类型,因此可以在需要时将其强制转换为正确的类型。
在设计此类接口时需要考虑的一些事项,特别是队列,通常具有不同的线程推送而不是弹出,是对象所有权和生命周期。如果在某个函数中初始化本地结构,然后将指向该结构的指针推送到您的队列,谁拥有该对象?对象生存期是进入和退出函数之间的时间,或者是因任何原因重新初始化结构的时间。队列上的指针可能指向已被另一个函数调用覆盖的内存。解决方案是避免将指针推送到临时变量,但要做到这一点,要么将它们设置为静态,这在多线程环境中本质上是危险的,或者在堆上分配内存。
每当从堆中获取指针(malloc
/ calloc
)时,最终必须调用free
将该内存返回到堆中。一种常见的方法是,任何分配对象的人都要对其整个生命周期负责,并且必须安排在某个时刻解除分配。这使得队列接口设计者摆脱困境,因为他们只需编写队列代码并可以将其留给客户端来管理对象的生命周期。它们简单地记录了在队列中跟踪对象的时间段内指向的对象的所有权落入队列代码的事实,即;在queue_push
和queue_pop
来电之间。队列用户必须安排对象生命周期超过该时间段。
作为队列用户,一般的最佳做法是推送对象指针的代码负责分配该内存,弹出它的代码负责解除分配它。这简化了对象生命周期管理,否则需要一些跟踪每个对象的所有权的方法。换句话说,对象是否在队列中被初始化,否则被随机客户端代码等使用/操纵。如果你真的认为你需要它,我可以解释如何进行跟踪。
附录:
好的,既然你已经发布了你的MCVE,我有以下评论:
对于像队列这样的容器,有两种主要的数据存储技术。一种是仅存储指向用户数据的指针,另一种是将数据复制到容器中和从容器中复制数据。你没有一直做任何一个。您memcpy
中的queuePush
,但不是queuePop
或queueHead
。永远不要让客户端代码访问您的内部数据,它只是在寻找麻烦。
你的queueHead
显然是一个偷看功能。它应该将数据复制出来而不从队列中弹出它,或者它应该锁定队列,直到调用者完成对数据的查看。就个人而言,我从不允许客户端代码访问我的内部,从长远来看会导致太多问题。问问自己是否真的需要这个功能,否则,摆脱它。如果你确实保留它并继续在推/弹范例上使用副本,那么也要复制。您可以为希望在弹出之前总是查看的客户端添加queueDiscardHead
函数,并避免额外的副本。