Opaque type is a type where we only have the declaration of the type and not the entire data structure. Let's say we have a queue structure defined like below. This is in the file queue.c
.
struct queue {
int head, tail;
int capacity;
int *elems;
};
To define the queue as an opaque type, we only declare it in the header file queue.h
like below.
typedef struct queue Queue;
With this we can work with the pointer to the queue. And not the concrete type Queue. Any further functions on queue will be like follows.
void initQueue(Queue **queue, int capacity);
void printQueueDetails(Queue *queue);
void enqueue(Queue *queue, int elem);
int dequeue(Queue *queue);
void dealloc(Queue *queue);
The main purpose of this is for encapsulation. Here we don't know what the queue struct
is like. What are the member elements of it. These are implementation details and with this we can change the implementation of the queue struct without affecting the callers.
For better understanding, here is the full program of a fixed sized queue on the heap.
// queue.h
#ifndef __QUEUE_H__
#define __QUEUE_H__
#define CAPACITY 100
typedef struct queue Queue;
void initQueue(Queue **queue, int capacity);
void printQueueDetails(Queue *queue);
void enqueue(Queue *queue, int elem);
int dequeue(Queue *queue);
void dealloc(Queue *queue);
#endif
// queue.c
#include <stdio.h>
#include <stdlib.h>
#include "queue.h"
struct queue {
int head, tail;
int capacity;
int *elems;
};
void handleError(Queue *queue) {
if (queue == NULL || queue->elems == NULL) {
printf("Error initializing queue\n");
exit(EXIT_FAILURE);
}
}
void initQueue(Queue **queue, int capacity) {
*queue = malloc(sizeof(Queue));
(*queue)->head = -1;
(*queue)->tail = -1;
(*queue)->capacity = capacity;
(*queue)->elems = malloc(capacity * sizeof(int));
handleError(*queue);
}
void printQueueDetails(Queue *queue) {
printf("Queue capacity: %d\nHead: %d - Tail %d\n", queue->capacity, queue->head, queue->tail);
for (int i = 0; i <= queue->tail; i++) {
printf("%d\n", queue->elems[i]);
}
}
void enqueue(Queue *queue, int elem) {
if (queue->tail == queue->capacity) {
printf("Queue is full.\nTail: %d, capacity %d\n", queue->tail, queue->capacity);
} else {
queue->elems[queue->tail + 1] = elem;
queue->tail = queue->tail + 1;
if (queue->head == -1) {
queue->head = 0;
}
}
}
int dequeue(Queue *queue) {
if (queue->head == -1) {
printf("The queue is empty\n");
return -1;
}
int elem = queue->elems[queue->head];
if (queue->head == queue->tail) {
queue->head = -1;
queue->tail = -1;
} else {
queue->head = queue->head + 1;
}
return elem;
}
void dealloc(Queue *queue) {
free(queue->elems);
free(queue);
printf("Dealloc completed\n");
}
We can use the queue
like below.
// qtest.c
#include <stdio.h>
#include "queue.h"
int main() {
Queue *queue = NULL;
initQueue(&queue, 100);
enqueue(queue, 1);
printQueueDetails(queue);
dealloc(queue);
return 0;
}
We can compile this using the below command.
clang queue.c qtest.c -o qtest
./qtest
Queue capacity: 100
Head: 0 - Tail 0
1
Dealloc completed