C / Intermediate: Memory and Reliable Components / Lesson 7
Pointers, Object Lifetime, and Aliases
Reason about what a pointer may designate, how long that object exists, and which operations stay within its bounds.
On this page
Lesson goals
- Distinguish pointer values, pointed-to objects, and null.
- Explain scope, storage duration, and lifetime.
- Maintain valid pointer ranges and const access contracts.
An address is only part of validity
A pointer value can designate an object, a permitted one-past position, or no object through a null value. Dereferencing requires more than a nonnull numeric-looking address: the target must be alive, suitably aligned, within bounds, and accessed through an allowed type. A pointer does not carry a universal runtime certificate of those properties.
The unary & operator obtains an object’s address, while unary * accesses the designated object. In int *p, the asterisk belongs to the declarator; in *p = 9 it is an operation. Drawing the pointer object and the target object separately helps distinguish changing p from changing *p.
Mutate the intended object
The first complete example points to a live local int and updates it through that pointer. Both names still refer to the same underlying int object for the observation. The pointer value itself is copied when passed to a function, so a helper can modify the designated object without modifying the caller’s pointer variable.
Aliasing means several expressions can designate the same object. That can be intentional, but a function with two pointer parameters must describe whether they may alias. A swap operation, for example, should either handle both parameters designating the same object or explicitly forbid it in its documented preconditions.
#include <stdio.h>
static void set_to_nine(int *target)
{
*target = 9;
}
int main(void)
{
int value = 4;
int *pointer = &value;
set_to_nine(pointer);
printf("%d %d\n", value, *pointer);
return 0;
}
9 9
The helper requires a live writable int; main supplies one, and both reads observe nine.
Know when the target stops existing
An ordinary block-local object has automatic storage duration, and its lifetime ends when execution leaves its block. Returning its address does not extend that lifetime. A file-scope object or a static local object has static storage duration and exists for the program’s execution, although sharing such state introduces different design concerns.
Allocated storage lasts until it is released by the appropriate deallocation operation. A pointer can remain in scope after its target’s lifetime ends, so visibility does not imply validity. C17 also treats a pointer value as indeterminate when the object it points to, or just past, reaches the end of its lifetime. Do not inspect a dangling pointer as a validation strategy.
Walk one array, not arbitrary memory
Pointer arithmetic is defined within an array object and may produce its one-past pointer. The one-past pointer is useful as a stopping position but may not be dereferenced. Subtracting pointers requires positions in the same array and a representable ptrdiff_t difference. Relational ordering is not a general ordering of unrelated allocated objects.
The traversal below keeps p between the first element and the one-past position. The comparison happens before each dereference, so p == end causes immediate termination. This is equivalent to a bounded index loop. Pointer syntax does not remove the need to carry the array extent as part of the interface.
#include <stdio.h>
int main(void)
{
const int values[] = {2, 4, 6};
const int *end = values + 3;
int total = 0;
for (const int *p = values; p != end; ++p) total += *p;
printf("sum=%d\n", total);
return 0;
}
sum=12
The loop reads three valid elements and never dereferences end.
Express access permissions clearly
const int *p prevents modifying the int through p; int *const p prevents assigning a different pointer value to p. Neither spelling proves that no other alias can change the target. A const-qualified access path is therefore an interface promise about permitted operations, not an automatic snapshot or a concurrency guarantee.
When reviewing a pointer parameter, ask five concrete questions: which object it identifies, how many elements are accessible, whether writing is allowed, how long access may continue, and whether the callee retains the pointer. These questions turn a vague address-based interface into a contract that callers can actually satisfy.
Common errors and fixes
A function returns the address of its local variable.
- Why it happens
- The target lifetime ends at return.
- How to fix it
- Return the value or write into caller-owned storage.
- Prevent it next time
- Record which scope or owner keeps every pointed-to object alive.
A nonnull check is followed by invalid access.
- Why it happens
- Nullness was mistaken for full pointer validity.
- How to fix it
- Reestablish lifetime, extent, alignment, and type guarantees.
- Prevent it next time
- Validate ownership and bounds at the point pointers are created.
The end pointer is read as an element.
- Why it happens
- A stopping position was confused with a valid object.
- How to fix it
- Compare before dereferencing and stop at end.
- Prevent it next time
- Use half-open ranges consistently in documentation and loops.
Check your understanding
1. Explain why returning an int value from a helper is valid while returning the address of its local int is invalid for later dereferencing.
Show worked solution
The returned arithmetic value is copied into the caller’s context before the helper finishes. It does not depend on the local object continuing to exist. An address still designates the local storage whose lifetime ends when the helper returns. The caller must not dereference it. Use a returned value, caller-provided output object, or explicit allocated ownership instead.
2. Modify the traversal idea for an empty logical sequence and state the valid-pointer precondition without forming arithmetic on null.
Show worked solution
For a zero count, return the identity sum before forming an end pointer if null is permitted for empty input. For a nonzero count, require a pointer to the first of at least count readable int objects in one array. Only then form begin + count and traverse. This distinguishes a convenient null-for-empty API convention from a permission to perform arbitrary arithmetic on a null pointer.
Key takeaways
- Pointer validity includes lifetime, bounds, alignment, and access type.
- A one-past position is a boundary, not an element.
- Document retention and aliasing as carefully as parameter types.