Pointers from Zero
Pointers are the part of C people dread, and they are simpler than their reputation. A pointer is a variable that holds an address - the number of a box in memory. This volume builds that idea up slowly, with every step shown by a program, and finishes with the function pointers that drivers use to call back into your code.
- What a pointer holds, and what it costs in memory
- How & takes an address and * follows one
- Why an array turns into a pointer when you pass it, and what that loses
- Why p + 1 moves four bytes for a uint32_t pointer, and one for a uint8_t
- What const means in each of its three positions in a declaration
- What void pointers and NULL are for, and how to change a pointer through a pointer
- How function pointers build command tables and callbacks
7.1 What a pointer really is
A pointer is a variable that holds an address. That is the whole idea. Everything else in this volume follows from it.
Memory is a row of numbered boxes, as Volume 02 showed. A normal variable holds a value. A pointer holds the number of a box - and that lets you reach the value in it from somewhere else.
Think of a hotel. The guest is the value; the room number is the address. A pointer is a note with a room number on it. Give someone that note and they can knock on the door - without ever knowing the guest's name.
reading = 42
*p = 42 (follow the pointer)
the two addresses match: yes
an int takes 4 bytes, a pointer takes 8
after *p = 77, reading = 77
inside try_to_change, the copy is now 99
after try_to_change, reading = 77
after really_change, reading = 99
after swap(&a, &b): a = 2, b = 1
A pointer has its own size, and it is the same for every type on a given machine. That is 8 bytes
on this 64-bit computer, and usually 4 bytes on a 32-bit microcontroller. A pointer to a uint8_t and
a pointer to a huge struct are the same size, because both just hold an address.
What does a pointer variable actually hold?
Show the answer
Answer: B. A pointer holds an address - the number of a box in memory. Following that address with * reaches the value stored there.
7.2 & and *
& takes an address. * follows one. Those two symbols are the whole of pointer syntax.
| You write | It means |
|---|---|
int *p; |
p is a pointer to an int |
p = &reading; |
put the address of reading into p |
*p |
the value at that address (dereference) |
*p = 77; |
write 77 into the variable p points at |
p == &reading |
do they point at the same place? |
The reason pointers exist
Volume 01 showed that a function cannot change its caller's variable, because it gets a copy. Give it the address instead, and it can:
void try_to_change(int value) /* gets a copy */
{
value = 99; /* changes the copy */
}
void really_change(int *value) /* gets an address */
{
*value = 99; /* changes the original */
}
int reading = 77;
try_to_change(reading); /* reading is still 77 */
really_change(&reading); /* reading is now 99 */
That is why scanf needs an &, and why the classic swap works only with pointers:
void swap(int *a, int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
swap(&x, &y);
Forgetting to give the pointer something to point at:
int *p; /* p holds whatever rubbish was in that memory */
*p = 5; /* writes 5 to a random address */
On a computer this usually crashes. On a microcontroller there is no memory protection, so it may quietly write into a variable, a register, or the stack. Always point a pointer at something before following it.
int value = 10; int *p = &value; *p = *p + 5; What is value now?
Show the answer
Answer: C. *p reads the value at the address, so *p + 5 is 15, and writing that back through *p stores it in value itself. Pointers let you work on the original, not a copy.
7.3 Pointers and arrays
An array name is the address of its first element. Pass an array to a function and it decays into a pointer - which is why the function needs a count as well.
in main, sizeof(data) = 16 (the whole array)
inside the function, sizeof(values) = 8 (a pointer, not the array)
so a function must be told the count: 4
data == &data[0]? yes
data[2] = 30
*(data + 2) = 30
p[2] = 30
*(p + 2) = 30
total = 100
Those four lines print the same value, because a[i] is defined as *(a + i). Indexing is
pointer arithmetic with friendlier syntax.
sizeof gave 16 in main and 8 inside the function. The array became a pointer on the way in, and a
pointer knows nothing about how many elements follow it. This is why every C function that takes an
array takes a length too:
uint32_t total_of(const uint32_t *values, unsigned count);
Going deeper: the array is not a pointer
Inside its own function, an array is not a pointer: sizeof knows its real size, and you cannot
point it somewhere else. It *converts* to a pointer whenever it is used as a value - passed to a
function, assigned, or added to. The distinction matters exactly twice: when you use sizeof, and
when you write &array, which gives a pointer to the whole array rather than to its first element.
Why does a C function that takes an array almost always take a count as well?
Show the answer
Answer: A. Passing an array hands over the address of its first element. The size is not part of a pointer, so the function cannot work out how many elements there are unless you tell it.
7.4 Pointer arithmetic
Pointer arithmetic counts elements, not bytes. Adding 1 to a uint32_t * moves four bytes; adding 1 to a uint8_t * moves one.
a uint32_t pointer steps 4 bytes at a time
a uint8_t pointer steps 1 byte at a time
walking the words: 0x11111111 0x22222222 0x33333333 0x44444444
stop - start = 3 elements
after cursor++, *cursor = 0x22222222
words + 4 is a legal pointer to compare against, but *end is not allowed
that is why loops are written: it != end
end - words = 4
Walking with a pointer
for (const uint32_t *it = words; it != words + 4; it++) {
use(*it);
}
This is how driver code walks a receive buffer. The pointer one past the end is allowed to exist -
that is what makes the != test legal - but following it is not.
Subtracting two pointers into the same array gives a count of elements, not bytes. That is how
ptrdiff_t values and lengths are worked out when walking buffers.
uint16_t buf[8]; uint16_t *p = buf; What address does p + 3 hold, if buf starts at 1000?
Show the answer
Answer: B. Each uint16_t is two bytes, so three elements along is six bytes on: 1006. Pointer arithmetic always counts in elements and lets the compiler do the multiplication.
7.5 const with pointers
const can lock the thing pointed at, the pointer itself, or both. Read the declaration outwards from the name and it is never ambiguous.
| Declaration | What is locked | Reads as |
|---|---|---|
const char *p |
the characters | p is a pointer to const char |
char * const p |
the pointer | p is a const pointer to char |
const char * const p |
both | p is a const pointer to const char |
const char *text -> can move, cannot write. now "two"
char * const fixed -> cannot move, can write. now "One"
const char * const -> neither. still "two"
Try to break any of those promises and the compiler refuses:
error: assignment of read-only location '*text'
error: assignment of read-only variable 'fixed'
error: assignment of read-only location '*both'
Which one you will write most
const char * - a pointer to data you promise not to change - by a long way. It is the standard
way to pass a buffer *into* a function:
void uart_send(const uint8_t *data, uint16_t length); /* will not change your data */
uint16_t uart_receive(uint8_t *buffer, uint16_t size); /* will fill your buffer */
The difference between those two lines tells a reader exactly which way the data flows, before they read a word of the implementation.
You want a function that reads a buffer but must never change it. How do you declare the parameter?
Show the answer
Answer: A. Putting const before the type locks what is pointed at, which is the promise you want to make. A const pointer (the second one) only stops the function moving its own local copy of the pointer, which the caller never sees.
7.6 void pointers, NULL and double pointers
A void pointer is an address with no type attached. NULL is an address that deliberately points at nothing. And a pointer to a pointer is how a function changes where your pointer points.
void copy_bytes(void *dst, const void *src, size_t count)
{
uint8_t *d = dst; /* a void * converts with no cast, in C */
const uint8_t *s = src;
while (count-- > 0u) {
*d++ = *s++;
}
}
A void * can hold the address of anything, which is what makes memcpy work for every type. It
cannot be followed directly: you convert it to a real pointer type first, because the compiler has
to know how many bytes to read.
NULL: the pointer that points nowhere
if (buffer != NULL) {
use(buffer);
}
Following a NULL pointer is undefined behaviour. On a desktop it is a crash with a clear message. On a microcontroller address 0 is often real memory - the start of flash, or the vector table - so the read quietly succeeds and returns nonsense.
Returning a pointer to a local variable:
char *make_label(void)
{
char buffer[16]; /* lives on the stack */
...
return buffer; /* gone the moment this function returns */
}
The caller gets a dangling pointer into a stack frame that the next call will reuse. Return into a buffer the caller owns, or use a static buffer, and say in the comment which one it is.
Pointer to a pointer
To change where a caller's pointer points, you need its address - a uint8_t **:
copy_bytes moved 0xDEADBEEF into a uint32_t: 0xDEADBEEF
maybe is NULL
checking for NULL first is what stops a crash
cursor points at 1
after point_at_second, cursor points at 2
It is the same rule as before, one level up. To change an int, pass int *. To change an
int *, pass int **.
Why must a void pointer be converted before it is followed?
Show the answer
Answer: C. The address alone does not say whether the thing there is one byte or four, or how to interpret it. Converting to a real pointer type supplies that missing information.
7.7 Function pointers and callbacks
A function pointer holds the address of code instead of data. It lets a program decide, while it is running, which function to call.
void led_on(void);
void (*what)(void) = led_on; /* a pointer to a function taking nothing, returning nothing */
what(); /* calls led_on */
typedef void (*action_fn)(void); /* a name for that type, so the rest stays readable */
action_fn next = led_on;
A table of commands
struct command {
const char *name;
action_fn run;
};
static const struct command table[] = {
{ "on", led_on },
{ "off", led_off },
{ "beep", beep },
};
for (unsigned i = 0; i < ARRAY_SIZE(table); i++) {
if (strcmp(word, table[i].name) == 0) {
table[i].run();
}
}
through a function pointer:
led on
beep
running every command in the table:
on ->
led on
off ->
led off
beep ->
beep
the callback added the bytes up to 100
sizeof(action_fn) = 8 bytes on this machine
Adding a new command means adding a row to the table - no new if, no new case. This is how
command shells, protocol handlers and menu systems are written in C.
Callbacks
A callback is a function you hand to someone else, to be called when something happens:
void uart_on_byte(void (*handler)(uint8_t)); /* register it once */
...
uart_on_byte(add_to_checksum); /* now every byte goes there */
Doing heavy work inside a callback that runs from an interrupt. The handler runs with the interrupt still being serviced, so everything else waits. Set a flag or push the byte into a buffer, and do the work in the main loop. Volume 11 covers this properly.
Going deeper: state machines in C
A table of function pointers is also how a state machine is written in C: one function per state, and an array of them indexed by the current state. Volume 12 builds one that way, and the State Machines from Zero course designs the machine itself before you write a line of it.
What does a function pointer hold?
Show the answer
Answer: B. Code lives in memory too, usually in flash. A function pointer holds the address of its first instruction, so calling through the pointer jumps there.
What you learned
- A pointer is a variable that holds an address; its own size is the same for every type.
- & takes an address, * follows one, and *p = x writes into the original variable.
- Pass an address when a function must change something of yours.
- An array decays to a pointer when passed, so functions take a count as well.
- a[i] is exactly *(a + i), and pointer arithmetic counts elements, not bytes.
- const before the type locks the data; const after the star locks the pointer.
- void * is an address with no type; NULL must be checked before it is followed.
- A function pointer holds the address of code, which is how tables and callbacks work.
Key words from this volume
Every word below has a plain-English entry in the glossary.
- Pointer
- Address-of (&)
- Dereference
- Array decay
- Pointer arithmetic
- void pointer
- NULL
- Dangling pointer
- Function pointer
- Callback
Practice
Read the declarations
Say what each of these means, in words.
uint8_t *a;
const uint8_t *b;
uint8_t * const c = &value;
uint8_t **d;
Show the solution
ais a pointer to uint8_t. Both the pointer and what it points at can change.bis a pointer to const uint8_t. It can be moved; the data must not be written through it.cis a const pointer to uint8_t. It cannot be moved; the data can be changed.dis a pointer to a pointer to uint8_t - used when a function must change someone's pointer.
The trick is to start at the name and read outwards: "c is a const pointer to uint8_t."
Write the function
Write a function that takes a buffer and its length, and returns the largest byte in it. Then say why it cannot work out the length itself.
Show the solution
uint8_t largest(const uint8_t *data, unsigned count)
{
uint8_t best = 0u;
for (unsigned i = 0; i < count; i++) {
if (data[i] > best) {
best = data[i];
}
}
return best;
}
It cannot work the length out because data is a pointer, not an array. The size was lost when the
caller passed it. sizeof data inside the function gives the size of a pointer, whatever the
buffer really holds - 4 bytes on a typical microcontroller.
Note the const: this function promises not to change the caller's data, and the compiler enforces
it.
Predict the output
What does this print?
uint8_t data[4] = { 5u, 10u, 15u, 20u };
const uint8_t *p = data + 1;
printf("%u %u %u\n", *p, p[1], *(data + 3));
Show the solution
10 15 20.
p points at data[1], so *p is 10. p[1] is *(p + 1), one element further on, which is
data[2] - 15. And *(data + 3) is data[3], which is 20.
The lesson in it: a pointer can start anywhere in an array, and indexing is always counted from wherever the pointer happens to be.
Fix the dangling pointer
What is wrong with this, and how would you write it instead?
const char *status_text(uint8_t code)
{
char text[16];
snprintf(text, sizeof text, "code %u", code);
return text;
}
Show the solution
text lives on the stack, and its frame is gone the moment the function returns. The caller
receives a pointer into memory that the next function call will reuse.
Three standard fixes, in order of preference:
void status_text(uint8_t code, char *out, size_t size)
{
snprintf(out, size, "code %u", code);
}
const char *status_name(uint8_t code)
{
static const char *names[] = { "idle", "busy", "error" };
return (code < 3u) ? names[code] : "unknown"; /* string literals live for ever */
}
A static char buffer inside the function also works, but only for one caller at a time. Never use
one from an interrupt. gcc catches the simplest version of this bug with
-Werror=dangling-pointer, as Volume 05 showed.
Interview corner
Explain a pointer
"Explain pointers to someone who has never programmed."
Show the solution
"Memory is a long street of numbered houses, and every variable lives in one. A pointer is a piece
of paper with a house number written on it. Give someone the paper and they can go to that house
and change what is inside, even though they never knew the resident's name. In C, & asks for the
house number and * means go to that house. That is why a function that takes a pointer can change
your variable, while one that takes a copy cannot."
Array or pointer?
"Is an array the same as a pointer in C?"
Show the solution
"No, although it converts to one. Inside its own scope an array is a block of memory: sizeof gives its real size, and you cannot point it somewhere else. The moment you pass it to a function it decays into a pointer to its first element, and the size is gone. That is why sizeof inside the function gives the size of a pointer, and why every array function takes a count. The place it matters most is the ARRAY_SIZE macro: it works in the function that declared the array, and silently gives nonsense anywhere else."
Next, Volume 08 groups data together: structs, the padding the compiler inserts between members, unions that read the same bytes two ways, and enums that give numbers names.