A lot of computational physicists with Fortran background ask the same question. (There is a lot of physics code in F95, because it is easier to write efficient math/simulation code in Fortran for current processors, than it is to do the same in C. Vectorization (using SSE2/AVX/AVX2/AVX512 in particular) is much easier for the Fortran compiler than it is to C compiler.
The first time you'll find the true utility of pointers is when you need runtime polymorphism (some variant of subtyping) in your data.
Think of a stack, that can contain different types of items, of varying sizes. You could use an array of
unions that can hold each type of item you want. But you could also make it a linked list of different structures. For example:
#include <stdlib.h>
#include <string.h>
enum {
STACK_STRING = 0,
STACK_INT,
STACK_FLOAT
};
typedef union stack_all stack_all;
typedef struct stack_any stack_any;
struct stack_any {
stack_any *next;
char type;
};
typedef struct {
stack_any *next;
char type; /* = STACK_STRING */
char value[];
} stack_string;
typedef struct {
stack_any *next;
char type; /* = STACK_INT */
int value;
} stack_int;
typedef struct {
stack_any *next;
char type; /* = STACK_FLOAT */
float value;
} stack_float;
union stack_all {
stack_any any;
stack_string s;
stack_int i;
stack_float f;
};
static stack_any *stack = NULL;
static inline stack_any *pop(void)
{
stack_any *item;
if (!stack)
return NULL;
item = stack;
stack = item->next;
item->next = NULL;
return item;
}
static inline void push(stack_any *item)
{
item->next = stack;
stack = item;
}
static inline int push_string(const char *s)
{
const size_t slen = (s) ? strlen(s) : 0;
stack_string *item;
item = malloc(slen + 1 + sizeof (stack_string));
if (!item)
return -1;
item->next = stack;
item->type = STACK_STRING;
if (slen > 0)
memcpy(item->value, s, slen);
item->value[slen] = '\0';
stack = (stack_any *)item;
return 0;
}
static inline int push_int(int i)
{
stack_int *item;
item = malloc(sizeof *item);
if (!item)
return -1;
item->next = stack;
item->type = STACK_INT;
item->value = i;
stack = (stack_any *)item;
return 0;
}
static inline int push_float(float f)
{
stack_float *item;
item = malloc(sizeof *item);
if (!item)
return -1;
item->next = stack;
item->type = STACK_FLOAT;
item->value = f;
stack = (stack_any *)item;
return 0;
}
Now, the reason the
stack = (stack_any *)item; lines are valid, and casting between any
stack_ types is allowed, is twofold: First, all those types need to have the same common initial members. For a stack implemented via a linked list, that means the pointer to the next (older) item in the stack, and the type of the current item in the stack. Second, the compiler needs to have
union stack_all visible. It is an union of all those structure types with the same common initial members. (A lot of C programmers are not aware that the
visibility of the union type suffices; it is
not necessary to use the union type at all. Simply having the union type visible, and the types having the same common initial members, due to
C99 6.5.2.3p5, means you can cast a structure of any of those types to another of those types, and examine the common initial members. Cast to the correct type (used when assigning the fields) is needed to access the type-specific fields.)
(A similar (but older, pre-C99) mechanism was used in POSIX.1 and its predecessors (SVRv4, 4.4BSD) to describe socket addresses (
struct sockaddr). C99 standardized a way to implement those in a backwards compatible manner via this union mechanism; however, because of such a large body of existing code relying the same operation without having such an union visible, most C compilers do not actually need to have that union visible.)
In microcontrollers, you can sometimes see similar queues or stacks used, when commands/requests and results are processed asynchronously, and more than one can be "in flight" at the same time.