C / Intermediate: Memory and Reliable Components / Lesson 8
Dynamic Allocation and Ownership
Design allocation, resizing, and cleanup as one ownership protocol, including every failure path.
On this page
Lesson goals
- Check allocation sizes before multiplication.
- Preserve ownership when realloc fails.
- Separate owners from temporary borrowed pointers.
A resource needs a responsible owner
malloc acquires uninitialized storage and returns a null pointer when the requested allocation cannot be provided. The program must decide which component owns a successful allocation and must eventually release it exactly once. Other pointers may borrow access for a documented period, but borrowing should not silently transfer responsibility for free.
Represent an empty owning object explicitly, often with a null pointer and zero count. Establish the invariant before doing work so cleanup can run after partial initialization. free(NULL) does nothing, which can simplify cleanup, but freeing an interior pointer or the same allocation twice is invalid. Setting one owner pointer to null does not repair other aliases.
Check the size before allocating
For count elements, the byte request count * sizeof *pointer must be representable as size_t before malloc sees it. Check count > SIZE_MAX / sizeof *pointer before multiplying. A wrapped size could allocate less storage than the later element loop assumes. Also decide the policy for a zero count explicitly instead of depending on zero-sized allocation behavior.
The first example allocates three int objects and initializes every element before reading it. No cast of malloc is required in C because void pointers convert to object pointer types; stdlib.h must be included for the correct declaration. This differs from C++, so copying C++ allocation syntax or casting habits can hide mistakes.
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
size_t count = 3;
int *values = NULL;
if (count > SIZE_MAX / sizeof *values) return 1;
values = malloc(count * sizeof *values);
if (values == NULL) return 1;
for (size_t i = 0; i < count; ++i) values[i] = (int)i + 1;
printf("last=%d\n", values[count - 1]);
free(values);
return 0;
}
last=3
On successful allocation the initialized final element is three, and the sole owner releases storage.
Resize without losing the owner
realloc can move an allocation. For a nonzero request, failure returns null and leaves the original allocation available; assign the result to a temporary pointer first. On success, use the returned pointer and treat prior pointers into the old allocation as invalid, even if the returned address looks unchanged. Newly added storage is uninitialized.
Avoid zero-sized realloc requests in a portable C17 interface: the older rules are implementation-defined and awkward for ownership. C23 makes a zero-size realloc request undefined behavior. A separate clear operation that frees storage and resets the owner expresses the intended transition without depending on those version differences.
Keep the failure path intact
The next program begins with two initialized elements, asks for four, and writes the added elements only after resizing succeeds. If the request fails, it frees the still-owned original block and exits. The two counts are fixed small values here; a reusable resize helper must repeat the multiplication-overflow guard for each requested capacity.
Notice that the temporary pointer is not an additional owner on success. Ownership moves back into values, and only values is used for final cleanup. A larger component should make this transfer explicit in its invariant so error handling cannot accidentally free both the temporary and the owning field.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *values = malloc(2 * sizeof *values);
if (values == NULL) return 1;
values[0] = 10; values[1] = 20;
int *temporary = realloc(values, 4 * sizeof *values);
if (temporary == NULL) { free(values); return 1; }
values = temporary;
values[2] = 30; values[3] = 40;
printf("first=%d last=%d\n", values[0], values[3]);
free(values);
return 0;
}
first=10 last=40
The successful path preserves the first two elements and explicitly initializes the added ones.
Design cleanup before success
When a function acquires several resources, list acquisition order and release them in reverse order on failure. A single cleanup block can reduce duplicated logic in C; goto used for that narrow purpose can make ownership easier to review. Keep a variable in a neutral state until it acquires its resource, then update it only at a documented transfer point.
Tests should simulate allocation failure at each acquisition, not only exercise the successful path. Leak and address detectors help find missed releases and invalid accesses, but an unreported run is not a proof of ownership correctness. The stronger argument is an invariant assigning exactly one cleanup responsibility to every live allocation.
Common errors and fixes
realloc failure leaks the original block.
- Why it happens
- The only owner pointer was overwritten with null.
- How to fix it
- Store the result temporarily and commit only on success.
- Prevent it next time
- Review every ownership assignment as a resource transfer.
A large element count produces a tiny allocation.
- Why it happens
- The byte-size multiplication wrapped in size_t.
- How to fix it
- Check the division bound before multiplying.
- Prevent it next time
- Test size guards separately without attempting huge allocations.
A borrowed element pointer fails after growth.
- Why it happens
- realloc invalidated pointers into the prior allocation.
- How to fix it
- Recompute borrowed positions from the new owner and valid indices.
- Prevent it next time
- Document operations that invalidate container pointers.
Check your understanding
1. Describe the state after each failure point in a function that allocates a name and then an integer array.
Show worked solution
Initialize both owner pointers to null. If name allocation fails, release nothing. If the array allocation fails, free the name once. On complete success, either retain both owners in the result or transfer both to the caller; do not also free them in the returning function. A cleanup block may safely free null pointers and each successfully acquired owner.
2. Why is malloc(count * sizeof pointer) the wrong expression for an array of pointed-to objects? Give the corrected rule.
Show worked solution
sizeof pointer measures the pointer object, which may differ from the element size. Use sizeof *pointer to measure the designated type, then reject count > SIZE_MAX / sizeof *pointer before computing the product. Choose a separate zero-count policy. This form also follows a later change of element type without duplicating its name in the allocation expression.
Key takeaways
- Ownership is a protocol covering success, failure, transfer, and release.
- Check byte counts before arithmetic can wrap.
- Reallocation invalidates prior borrowed pointers and requires a commit step.