Why this matters
Passing structs by value is fine for small ones, but it copies every byte and the function cannot change the caller's struct. Real C code almost always handles structs through pointers: the standard library's FILE *, the kernel's struct task_struct *, every node of every linked list and tree. Arrays of structs are the C equivalent of a table of records, and sorting them is an everyday task. This lesson combines what you know about pointers, the heap and structs into the patterns used for the rest of the track.
The arrow operator
With a pointer to a struct, you could reach a member by dereferencing first: (*p).x. The parentheses are required because . binds tighter than *. That is so common and so clumsy that C has a shorthand:
p->x /* exactly the same as (*p).x */
Read -> as "the member of what p points to". Memory-wise, p->x means: take the address in p, add the offset of x, and access the bytes there.
stack (anywhere: stack, heap, global)
+---------------------+ +----------------+----------------+
| p = 0x7ffd...a0 ---+----------> | x = 1 | y = 2 |
+---------------------+ +----------------+----------------+
p->x = *(p + 0) p->y = bytes at p + 4
Use . on a struct, -> on a pointer to a struct. gcc tells you when you mix them up:
main.c:3:82: error: 'p' is a pointer; did you mean to use '->'?
main.c:3:104: error: invalid type argument of '->' (have 'struct point')
Passing structs by pointer
Two reasons to pass a pointer instead of the struct:
- To modify the caller's struct. The function receives the address, so changes land in the original.
- To avoid copying a large struct. A pointer is 8 bytes no matter how big the struct is.
When a function only reads, declare the parameter const struct item *. You get the speed of a pointer with the promise of a value: the compiler rejects any attempt to modify through it, and the reader of the signature knows the struct will not change.
Arrays of structs and qsort
An array of structs is a contiguous run of structs, each sizeof(struct item) bytes, padding included:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct item {
char name[16];
int price; /* in cents */
int qty;
};
int item_total(const struct item *it)
{
return it->price * it->qty;
}
void restock(struct item *it, int amount)
{
it->qty += amount;
}
int by_total_desc(const void *a, const void *b)
{
const struct item *x = a, *y = b;
return item_total(y) - item_total(x);
}
int main(void)
{
struct item stock[] = {
{ "pen", 150, 10 },
{ "notebook", 400, 2 },
{ "stapler", 900, 3 },
};
size_t n = sizeof stock / sizeof stock[0];
restock(&stock[1], 5);
qsort(stock, n, sizeof stock[0], by_total_desc);
for (size_t i = 0; i < n; i++)
printf("%-10s %3d x %4d = %5d\n", stock[i].name, stock[i].qty,
stock[i].price, item_total(&stock[i]));
return 0;
}
notebook 7 x 400 = 2800
stapler 3 x 900 = 2700
pen 10 x 150 = 1500
Things to notice:
stock[i]is a struct, sostock[i].name;&stock[i]is a pointer to it, which is whatrestockanditem_totaltake.stock + iis the same pointer, by pointer arithmetic: adding 1 to astruct item *advances bysizeof(struct item)bytes, 24 here.sizeof stock / sizeof stock[0]counts the elements. This only works on a real array, not on a pointer (an array parameter is a pointer, as you saw in the pointers module).qsortknows nothing about your struct. It shuffles blocks ofsizeof stock[0]bytes and asks your comparator which of two blocks comes first. The comparator receivesconst void *pointers to two elements; convert them toconst struct item *and compare. Return a negative number if the first should come first, positive if second, zero if equal.
Warning:
return y_total - x_total;is a common comparator shortcut, and it is fine for small values like these. With large or negative values the subtraction can overflow, which is undefined behavior and gives wrong orderings. The safe form isreturn (a > b) - (a < b);or explicitifcomparisons.
stock (72 bytes)
+-------------------------+-------------------------+-------------------------+
| "notebook" 400 7 | "stapler" 900 3 | "pen" 150 10 |
+-------------------------+-------------------------+-------------------------+
^ stock[0], stock + 0 ^ stock + 1 (24 bytes on) ^ stock + 2
Structs on the heap
When the number of structs is only known at run time, or a struct must outlive the function that creates it, allocate it on the heap. If the struct itself owns heap memory, provide a create/destroy pair as in the ownership lesson:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct person {
char *name; /* owned: heap copy */
int age;
};
/* Returns a new person, or NULL. Release with person_free. */
struct person *person_new(const char *name, int age)
{
struct person *p = malloc(sizeof *p);
if (p == NULL)
return NULL;
p->name = malloc(strlen(name) + 1);
if (p->name == NULL) {
free(p);
return NULL;
}
strcpy(p->name, name);
p->age = age;
return p;
}
void person_free(struct person *p)
{
if (p == NULL)
return;
free(p->name);
free(p);
}
int main(void)
{
struct person *ada = person_new("Ada", 36);
if (ada == NULL)
return 1;
printf("%s is %d\n", ada->name, ada->age);
ada->age++;
printf("%s is now %d\n", ada->name, ada->age);
person_free(ada);
return 0;
}
Ada is 36
Ada is now 37
stack heap
+-------+ +-------------------+ +---+---+---+----+
| ada --+------> | name ---------------------> | A | d | a | \0 |
+-------+ | age = 37 | +---+---+---+----+
+-------------------+
struct person (16 bytes) separate allocation
person_free frees the inner allocation first and then the struct, children before parent. Freeing p first and then reading p->name would read freed memory.
ada->age++ works because -> binds tighter than ++: it increments the member.
Deep and shallow copies
struct person b = *ada; copies the struct: b.age is independent, but b.name is the same address as ada->name. Both refer to one string. Free one and the other dangles; free both and it is a double free. If you need an independent copy, write a function that allocates a new string too (a deep copy):
struct person *person_clone(const struct person *src)
{
return person_new(src->name, src->age);
}
When you design a struct, decide whether each pointer member is owned (the struct frees it and copies need deep copies) or borrowed (it points at something someone else manages), and write that in a comment next to the member.
Common mistakes
.and->confused: see the gcc messages above.*p.xinstead ofp->xor(*p).x: the.applies first. Error:'p' is a pointer; did you mean to use '->'?- Allocating the size of the pointer:
malloc(sizeof(struct person *))gives 8 bytes, not a struct.malloc(sizeof *p)is always right. - Comparator converting to the wrong type:
qsortpasses pointers to elements. For an array of structs that isconst struct item *. For an array of pointers to structs it would beconst struct item *const *, a classic source of garbage sorts. - Shallow copies of owning structs, then double frees.
Recap
p->mis(*p).m. Dot for structs, arrow for pointers to structs.- Pass structs by pointer to modify them or avoid copies; use
const struct T *for read-only access. - Arrays of structs are contiguous;
&arr[i]andarr + ipoint at element i.qsortsorts them with a comparator that castsconst void *back to the struct pointer type. - Heap structs that own memory get create/destroy functions that free inner allocations first.
- Copying a struct copies pointers, not what they point to: decide ownership per member, and write deep-copy functions when needed.
Going further: change the leaderboard exercise to store an array of struct player * (each player allocated separately) and sort that. Work out what type the comparator's arguments really point to before you write it.