Introduction
The ferry route list is three pieces of information working together: a pointer to the routes, how many routes are being used, and how many slots are available. This lets a C program create an array that can grow when more routes are added.
A C array does not store its own length. The programmer must keep track of how much memory was reserved and how many elements have actually been placed in it. In this route list, the program uses a pointer to the allocated memory, a count of routes currently stored, and a capacity describing how many route structures fit before growth is required.
The ferry-ticket CLI project calls that small piece of state RouteList:
typedef struct {
FerryRoute *routes;
int count;
int capacity;
} RouteList;
This is not a special container built into C. It is a convention enforced by the functions that initialize, append to, search, sort, and free the list.
Four pointer operations to keep separate
Suppose route is one structure value and route_ptr points to it:
FerryRoute route;
FerryRoute *route_ptr = &route;
route_ptr->available_seats = 20;
(*route_ptr).available_seats = 20;
route is the value itself. &route is the address-of operator: it produces a pointer to that value. *route_ptr is the indirection or dereference operator: it accesses the FerryRoute object at that address. For a structure pointer, route_ptr->member is shorthand for (*route_ptr).member.
The arrow is therefore not a different kind of pointer. It is a readable member-access operation applied to a pointer. The project passes a RouteList * into mutating route-management functions so those functions can update the caller's count, capacity, and routes fields with list->count and similar expressions.
The heap is the area of memory used for storage requested while the program is running. Memory allocated on the heap remains available until the program releases it with free(). This is why a dynamic route list must have an explicit cleanup step.
What the three RouteList fields mean
The list structure and the allocation it points to have different identities:
routespoints to the first element of a contiguous heap allocation, or is null when no allocation exists. Contiguous means that the route records are stored beside one another in one block of memory.countis the number of initializedFerryRoutevalues that the program may read. Valid elements are indexes0throughcount - 1.capacityis the number ofFerryRouteslots allocated. It may be greater thancountbecause unused slots are reserved for future appends.
The distinction prevents two common mistakes. Reading index count is not valid merely because it is below capacity, and allocating a new array does not automatically update count. The list functions must maintain both values deliberately.
Appending a route
The repository's route-list implementation follows the usual grow-on-demand shape. When there is room, it copies a new route value into routes[count] and increments count. When count == capacity, it requests a larger allocation with realloc() before writing the new element.
A focused version of that reasoning looks like this:
bool route_list_append(RouteList *list, FerryRoute route) {
if (list->count == list->capacity) {
int new_capacity = list->capacity * 2;
FerryRoute *grown = realloc(
list->routes,
(size_t)new_capacity * sizeof *list->routes
);
if (grown == NULL) {
return false;
}
list->routes = grown;
list->capacity = new_capacity;
}
list->routes[list->count] = route;
list->count += 1;
return true;
}
The snippet explains the invariant; it is intentionally shorter than the project's complete error and user-interface code. The important order is: obtain a successful allocation, publish the new pointer and capacity, copy the route into a slot, then increase the count.
The repository's route_list_append stores a FerryRoute structure value. That copies the structure's fields into the array; it does not make the list own arbitrary pointers that might be inside those fields. If a future FerryRoute contains heap-owned members, its copy and destruction rules would need to be revisited.
Why a temporary pointer matters for realloc()
realloc() may extend the existing block, or allocate a new block, copy the preserved bytes, and free the old block. It returns the new base address. If allocation fails, it returns null and leaves the old block valid.
If a pointer is used after the object or allocation it refers to is no longer valid, the program has undefined behavior. This means that the C language no longer gives the program a reliable result. The old element pointers after a moving realloc() must therefore not be used.
That is why the result should first go into grown:
FerryRoute *grown = realloc(list->routes, new_bytes);
if (grown == NULL) {
/* list->routes still owns the old allocation */
return false;
}
list->routes = grown;
Directly assigning list->routes = realloc(...) loses the only pointer to the old block when the call fails. The bytes are still allocated, but the program can no longer free or use them: a memory leak and a failed append have become harder to recover from.
On success, even an in-place success invalidates the old pointer value as a handle to the allocation according to the C library contract. More obviously, if the block moved, every pointer into the old array is stale:
The project should therefore treat list->routes as the authoritative base pointer after growth. A pointer returned earlier by a lookup such as route_list_get_by_id() must not be retained across an append that may resize the array. Look the route up again after growth.
Who owns the allocation?
In this project, route_list_init() establishes the list's initial storage and route_list_free() releases the route allocation. That gives the RouteList a simple ownership rule: the list owns the block held in routes, and the code that creates the list is responsible for eventually calling its free function.
free() releases storage allocated by malloc(), realloc(), or related allocation functions. Calling free(NULL) is harmless, which makes a cleanup function easier to write, but using a pointer after free() or freeing the same allocation twice is undefined behavior. A useful cleanup convention is to set routes to NULL and reset count and capacity after freeing, so a second cleanup can be detected or made harmless.
Ownership is not inferred from the type. C will not free a FerryRoute array when RouteList goes out of scope, and it will not know whether a pointer returned from a lookup is borrowed or owned. Those rules belong in the API's documentation and calling code.
Searching returns a borrowed pointer
The project includes route_list_get_by_id(), which scans the initialized portion of the array and returns a pointer to the matching route or NULL when no match exists. Conceptually:
for (int i = 0; i < list->count; i++) {
if (list->routes[i].id == route_id) {
return &list->routes[i];
}
}
return NULL;
That pointer is useful because a caller can update the route without copying the whole structure. It is also a borrowed pointer: the RouteList still owns the element, and the pointer becomes invalid if the list is freed or if a later realloc() moves the array.
This is a source-based interpretation of the function's role. It is not a promise that every future version of the project will preserve pointer stability; a dynamic contiguous array cannot make that promise across relocation.
Sorting swaps structure values
The ferry project provides manual sorting functions for price and destination. The price sort compares numeric fields, while the destination sort uses strcmp(). Both use a bubble-sort-style pass and exchange adjacent routes when they are in the wrong order.
The exchange helper has the pointer shape expected for two structure values:
void swap_routes(FerryRoute *a, FerryRoute *b) {
FerryRoute temporary = *a;
*a = *b;
*b = temporary;
}
Here a and b point to elements inside the same allocation. Dereferencing copies the complete structure value, so the array slots change places while the allocation itself stays where it is. The swap does not allocate memory and does not change count or capacity.
If a caller holds a pointer to one of those elements, the pointer still addresses the same slot, but the route value in that slot may now be a different route. That is a different hazard from realloc() relocation: the address remains valid, while the identity of the value at that address changes.
Before and after capacity growth
An append with spare capacity changes only the initialized boundary:
| State | count |
capacity |
Meaning |
|---|---|---|---|
| Before append | 2 | 4 | Two route values, two unused slots |
| After append | 3 | 4 | The new route occupies index 2 |
| Before growth append | 4 | 4 | No free slot remains |
| After successful growth | 5 | 8 | Values are preserved; four new slots are available |
The particular growth factor is an implementation choice. Doubling is common because it makes repeated appends practical, but the essential rule is that the new capacity must be large enough for the requested element and the code must handle allocation failure.
What this project demonstrates
- Documented/source behavior:
RouteListstores aFerryRoute *,count, andcapacity; route management initializes, appends, searches, sorts, and frees that storage. - Reproducible demonstration: appending a fifth value to a full four-slot list can relocate the four existing values into a larger allocation; after growth, re-read the base pointer before using the array.
- Design reflection: a contiguous dynamic array is a reasonable learning structure for this CLI because indexing and swapping route values are straightforward, but its pointer invalidation rules should be made explicit in any larger API.
Key takeaways
- A structure value and a pointer to that structure are different things;
->accesses a member through the pointer. counttracks initialized routes, whilecapacitytracks allocated slots.realloc()may move the allocation, so receive its result in a temporary pointer and publish it only after success.- Pointers into a dynamic array can become invalid after relocation; look up routes again after a growth operation.
RouteListowns the allocation held byroutesand must eventually release it withfree().- Sorting swaps route values in place; it does not resize the array, but a pointer to a slot may now refer to a different route.
- C does not track array length or ownership automatically. The list functions and their callers must maintain those contracts.