Chapter 10 of 24
10. Pointers
A value that identifies a location
A pointer is a value that can identify the location of another object in memory. It is not the object itself: think of the object as a house and the pointer as an address that lets code find it.
- Declare a pointer with the type of object it may point to, such as
int *p. - Use
&valueto get the address of an existing object. - Use
*pto access the pointed-to value only whenpis valid and points to a live object.
A null pointer represents “no object”; it cannot be dereferenced. Start with pointers to ordinary local variables before applying them to arrays or dynamically allocated memory.
A pointer stores an address. The pointer's type describes what kind of object
it points to.
#include <stdio.h>
int main(void)
{
int value = 42;
int *pointer = &value;
printf("%d\n", *pointer);
return 0;
}
&valueobtains the address ofvalue.int *pointerdeclares a pointer toint.*pointeraccesses the object at the stored address (dereferencing).
Initialize pointers before use. A null pointer points to no object:
#include <stddef.h>
int main(void)
{
int *pointer = NULL;
return pointer == NULL ? 0 : 1;
}
Do not dereference a null, uninitialized, expired, or otherwise invalid
pointer. A pointer must have the correct type and point to a live object before
it is used.
Arrays and pointers are closely related in many expressions, but they are not
the same thing. An array expression often converts to a pointer to its first
element. Pointer arithmetic is only valid within an array object (or one past
its end); the one-past pointer may not be dereferenced.
const and pointers
const int *p: cannot modify the pointed-to integer throughp.int *const p: the pointer itself cannot be changed after initialization.const int *const p: neither can be changed through that declaration.
Function parameters that accept read-only arrays commonly use const.
int main(void)
{
int value = 10;
const int *read_only = &value;
int *const fixed_address = &value;
(void)read_only; // *read_only = 20 is not allowed.
*fixed_address = 20;
return value == 20 ? 0 : 1;
}
The pointer fixed_address itself cannot be assigned a different address
after initialization.
Following an address step by step
If pointer contains the address of value, then *pointer designates the
same object as value. Assigning through it changes that object:
int main(void)
{
int value = 10;
int *pointer = &value;
*pointer = 25;
return value == 25 ? 0 : 1;
}
The pointer itself is a separate variable. Changing pointer to another
valid address does not change value; changing *pointer changes the object
at the address. This distinction is the basis for output parameters, arrays,
linked structures, and dynamic allocation.
Pointer preconditions
Before dereferencing, establish that a pointer is non-null and points to a
live object of the expected type. A null check alone is not enough if a
pointer has already expired or points outside the correct object. Never
return the address of a local automatic variable. When a pointer refers to
one array element, pointer arithmetic may move within that same array or to
one past its end, but not beyond; the one-past pointer is only for comparisons
or iteration termination, not dereferencing.
#include <stdbool.h>
#include <stddef.h>
bool read_value(const int *pointer, int *output)
{
if (pointer == NULL || output == NULL) {
return false;
}
*output = *pointer;
return true;
}
The null checks establish only part of the contract: callers must also
provide pointers to live, correctly typed objects.
Pointers and arrays are connected by the rule that an array expression often
converts to a pointer to its first element. The array still has its own size
and cannot be reassigned; a pointer is an object that stores an address and
can point elsewhere. For each pointer in your program, ask: What object does
it point to? How long does that object live? Who may modify it?
Checkpoint: Trace pointer, *pointer, and value through the example.
State two reasons a pointer might be invalid even if it is not NULL.
Pointer declarations and reading them
The * in a declaration binds to the declarator, not to the base type. Thus
int *first, second; declares first as a pointer to int and second as
an int. For clarity, prefer one declaration per line when pointer types are
involved:
#include <stddef.h>
int *first;
int second;
Read a declaration from the identifier outward. In int *p, p is a
pointer to int. In int **pp, pp is a pointer to a pointer to int.
Parentheses matter: int *rows[4] is an array of four pointers, while
int (*row)[4] is a pointer to an array of four integers.
Pointer arithmetic
Adding one to a pointer advances by one pointed-to element, not one byte:
p + 1 points to the next int when p is an int *. Pointer subtraction
is defined only for pointers into the same array object (or one past it) and
produces a ptrdiff_t. Comparisons between unrelated object pointers do not
provide a portable ordering.
These rules make a pointer plus a length a common way to describe a range.
The beginning points at the first element and the end points one past the
last. A range is empty when begin equals end; the end is never dereferenced.
Do not form a pointer beyond one-past, even if it is not dereferenced.
#include <stddef.h>
int sum_range(const int *begin, const int *end)
{
int total = 0;
while (begin != end) {
total += *begin;
++begin;
}
return total;
}
This requires begin and end to describe a valid range in the same array,
with end at or one past the final element. The accumulator also needs an
application-appropriate overflow policy.
void * and generic object pointers
void * can hold a pointer to an object of any type and can be converted
back to the original object-pointer type. It has no element type, so it
cannot be dereferenced or incremented until converted to an appropriate
object pointer. Standard allocation functions return void *; in C, that
value converts to an object pointer type without a cast. Avoid unnecessary
casts of malloc results because they can hide a missing declaration.
Function pointers are not interchangeable with object pointers. A callback
must use a compatible function type. When a generic library accepts void *
context data, document how long the context must remain valid and how the
callback interprets it.
#include <stdio.h>
int main(void)
{
int value = 42;
void *generic = &value;
int *integer_pointer = generic; // Object-pointer conversion in C.
printf("%d\n", *integer_pointer);
return 0;
}
Convert back to the original compatible object-pointer type before
dereferencing; void * itself has no type to dereference.
Pointer-to-pointer parameters
A pointer-to-pointer is useful when a function must change the caller's
pointer, for example to install a newly allocated object:
#include <stdbool.h>
#include <stdlib.h>
bool make_int(int **output)
{
if (output == NULL) {
return false;
}
int *created = malloc(sizeof *created);
if (created == NULL) {
return false;
}
*created = 0;
*output = created;
return true;
}
The caller can initialize its pointer to NULL, call the function with
&pointer, and free the result if successful. This interface transfers
ownership to the caller; that obligation must be documented. Do not use
T ** merely because an array parameter is present: an array of T is
passed as a T *, while T ** means a pointer to a T *.
Const correctness
Read pointer types from the identifier:
const int *p:pcan be changed to point elsewhere, but the integer
cannot be modified throughp.int *const p:pcannot be changed after initialization, but the
pointed-to integer can be modified.const int *const p: neither the pointer nor the integer can be changed
through this declaration.
Use const on input-only pointers to communicate intent and allow callers
to pass read-only data. const does not make the underlying object immutable
through every possible alias; it restricts modification through that access
path.
#include <stddef.h>
int sum_values(const int values[], size_t length)
{
int sum = 0;
for (size_t i = 0; i < length; ++i) {
sum += values[i];
}
return sum;
}
The parameter prevents this function from modifying array elements through
values. The caller must pass a valid range, and the sum must fit in int.
Exercises: draw a memory diagram for int *p and int **pp; write a
function that swaps two integers; write a function that takes a pointer and a
length and sums the elements without modifying them.
Exercises: swap two integers using pointers; write a function that finds
an element and returns a pointer to it or NULL; explain the difference
between an array and a pointer.