Pointers
What a pointer holds, taking addresses and dereferencing, null and dangling pointers, void pointers, const with pointers, pointers to pointers and function pointers.
The idea of a pointer, and a first example with
& and *, is on
memory management, next to the stack and the heap. This page goes further: the
rules, the mistakes and the tools that catch them. Pointers and
arrays are so closely tied in C that they share a page of their own:
arrays and pointer arithmetic.
Addresses and dereferencing
Every object in a running program sits somewhere in memory, and its
address is the number of its first byte. The
address-of operator
& gives that address: &watts is
"where watts lives". A variable declared as
int *p is a pointer to int: it can hold
the address of an int.
Dereferencing is following the
pointer to the object it points to, with the unary
* operator: *p is that
int itself, which you can read and also assign to.
#include <stdio.h>
int main(void)
{
int watts = 1234;
int *p = &watts; /* p holds the address of watts */
printf("watts = %d\n", watts);
printf("*p = %d\n", *p);
*p = 980; /* write through the pointer */
printf("watts = %d\n", watts);
printf("sizeof p = %zu, sizeof *p = %zu\n", sizeof p, sizeof *p);
printf("p points to watts: %d\n", p == &watts);
return 0;
}
watts = 1234 *p = 1234 watts = 980 sizeof p = 8, sizeof *p = 4 p points to watts: 1
*p follows it.
On our test machine, a 64-bit Linux machine (ARM64), every pointer
is 8 bytes, whatever it points to. The examples on this site never
print the address itself: printf("%p", (void *)p)
works, but the operating system places the program at a random
address on every run (address space layout randomization, ASLR), so
the number changes each time. Comparing pointers with
== is stable, and is what programs actually need.
Reading pointer declarations
The * in a declaration belongs to the name, not to the
type. int *p, q; declares one pointer
p and one plain int q, which
surprises almost everyone once:
#include <stdio.h>
int main(void)
{
int *p, q; /* p is a pointer, q is a plain int */
q = 5;
p = &q;
printf("sizeof p = %zu, sizeof q = %zu, *p = %d\n", sizeof p, sizeof q, *p);
return 0;
}
sizeof p = 8, sizeof q = 4, *p = 5
Declare one pointer per line, or write * on each name.
The type of a pointer matters: *p on an
int * reads 4 bytes as an int, on a
double * it reads 8 bytes as a double.
Mixing them up is refused:
#include <stdio.h>
int main(void)
{
int kwh = 350;
double *p = &kwh;
printf("%f\n", *p);
return 0;
}
example.c:6:17: error: initialization of ‘double *’ from incompatible pointer type ‘int *’ [-Werror=incompatible-pointer-types]
gcc 12 warns about this even without any flags; since GCC 14 it is
an error by default. A cast such as
(double *)&kwh silences the compiler but not the
problem: reading an int through a
double * is undefined behavior.
Changing the caller's variables
C passes arguments by value (see
functions and header files), so a function cannot change a variable it receives. It can
change a variable whose address it receives.
Pass by pointer is that
technique: the caller passes &x, the function takes
an int * parameter and writes through it. The pointer
itself is still copied, but the copy points at the caller's
variable. This is how a C function returns several results:
#include <stdio.h>
/* Splits a power reading in watts into kW and the remaining watts. */
void split_power(int watts, int *kw, int *rest)
{
*kw = watts / 1000;
*rest = watts % 1000;
}
int main(void)
{
int kw, rest;
split_power(1234, &kw, &rest);
printf("%d kW and %d W\n", kw, rest);
return 0;
}
1 kW and 234 W
A common shape combines both: the return value says whether the function succeeded, and the result goes through a pointer. This parser reads the power line of a smart meter's P1 telegram and stores the value in whole watts, an integer, as the P1 page recommends. It relies on DSMR's fixed format of exactly three decimals:
#include <stdio.h>
/* Reads "1-0:1.7.0(01.234*kW)" into watts.
Returns 1 on success, 0 if the line does not match. */
int parse_power(const char *line, int *watts)
{
int kw, frac;
if (sscanf(line, "1-0:1.7.0(%d.%3d*kW)", &kw, &frac) != 2) {
return 0;
}
*watts = kw * 1000 + frac;
return 1;
}
int main(void)
{
const char *lines[] = { "1-0:1.7.0(01.234*kW)", "1-0:2.7.0(00.000*kW)" };
for (int i = 0; i < 2; i++) {
int watts;
if (parse_power(lines[i], &watts)) {
printf("%s -> %d W\n", lines[i], watts);
} else {
printf("%s -> not a power line\n", lines[i]);
}
}
return 0;
}
1-0:1.7.0(01.234*kW) -> 1234 W 1-0:2.7.0(00.000*kW) -> not a power line
sscanf itself works the same way: it needs the
addresses of the variables to fill, which is why every argument
after the format has an &. Forgetting one passes a
number where an address is expected. gcc checks
scanf-style formats and refuses it; without the check,
the program would write to whatever address that number happens to
be:
scan.c:6:13: error: format ‘%d’ expects argument of type ‘int *’, but argument 2 has type ‘int’ [-Werror=format=]
6 | scanf("%d", watts);
| ~^ ~~~~~
| | |
| | int
| int *
Null pointers
A null pointer is a pointer
value that is guaranteed not to point at any object. It is written
NULL (a macro from <stddef.h>,
<stdio.h> and other headers), or
nullptr in C23. Functions use it to say "nothing":
malloc returns NULL when there is no
memory, fopen when a file cannot be opened, and your
own lookup functions when nothing was found. In a condition a null
pointer counts as false, so if (m) means
if (m != NULL).
#include <stdio.h>
#include <string.h>
struct meter {
const char *id;
int kwh;
};
static struct meter meters[] = {
{ "M-30004", 350 },
{ "M-30007", 1500 },
};
/* Returns the meter with this id, or NULL when there is none. */
struct meter *find_meter(const char *id)
{
for (size_t i = 0; i < sizeof meters / sizeof meters[0]; i++) {
if (strcmp(meters[i].id, id) == 0) {
return &meters[i];
}
}
return NULL;
}
int main(void)
{
const char *wanted[] = { "M-30004", "M-39999" };
for (int i = 0; i < 2; i++) {
struct meter *m = find_meter(wanted[i]);
if (m == NULL) {
printf("%s: unknown meter\n", wanted[i]);
continue;
}
printf("%s: %d kWh\n", m->id, m->kwh);
}
return 0;
}
M-30004: 350 kWh M-39999: unknown meter
m->kwh is short for (*m).kwh: the
member of the struct that m points to (see
structs, unions and enums). Leave out the check and the second lookup dereferences a null
pointer. That is undefined behavior; on Linux the lowest addresses
are not mapped, so the processor raises a fault and the kernel ends
the program with the signal SIGSEGV. The shell reports it as a
segmentation fault, and the line printed before the crash is the
only clue in the output:
#include <stdio.h>
#include <string.h>
struct meter {
const char *id;
int kwh;
};
static struct meter meters[] = {
{ "M-30004", 350 },
{ "M-30007", 1500 },
};
struct meter *find_meter(const char *id)
{
for (size_t i = 0; i < sizeof meters / sizeof meters[0]; i++) {
if (strcmp(meters[i].id, id) == 0) {
return &meters[i];
}
}
return NULL;
}
int main(void)
{
const char *wanted[] = { "M-30004", "M-39999" };
for (int i = 0; i < 2; i++) {
struct meter *m = find_meter(wanted[i]);
printf("%s: %d kWh\n", wanted[i], m->kwh); /* no NULL check */
}
return 0;
}
M-30004: 350 kWh Segmentation fault
A segmentation fault is what service desks and admins most often see of a C bug: a daemon that disappears, exit status 139 in a script, a core dump in the logs. How to find the line responsible with a debugger is shown on debugging C. NULL pointer dereference is number 13 in the CWE Top 25 of 2025, MITRE's list of the most dangerous software weaknesses (see undefined behavior and safer C).
Dangling pointers
A dangling pointer holds the
address of an object whose lifetime has ended. The address looks
perfectly normal, which is what makes it dangerous: reading through
it may return the old value, a new value that has moved in, or
crash. There are three ways to get one: returning the address of a
local variable, keeping the address of a variable after its block
ends, and using memory after free (see
dynamic memory).
The first is easy for the compiler to see, and gcc refuses it:
#include <stdio.h>
int *latest_reading(void)
{
int kwh = 350;
return &kwh;
}
int main(void)
{
printf("%d\n", *latest_reading());
return 0;
}
example.c:6:12: error: function returns address of local variable [-Werror=return-local-addr]
The second is not caught at compile time. Here
peak keeps the address of a variable that exists only
during one round of the loop. Built normally on our test machine,
the program printed 200, the second day's value, because the loop
reused the same memory, and exited as if nothing was wrong.
AddressSanitizer stops it at the bad read:
#include <stdio.h>
int main(void)
{
int *peak = NULL;
for (int day = 1; day <= 2; day++) {
int kwh = day * 100;
if (peak == NULL) {
peak = &kwh;
}
}
printf("peak: %d kWh\n", *peak); /* kwh no longer exists */
return 0;
}
SUMMARY: AddressSanitizer: stack-use-after-scope example.c:13 in main
The rule: a pointer must never outlive what it points to. Return
values rather than addresses of locals, let the caller pass in the
memory to fill (as split_power above does), or allocate
on the heap and document who frees it. Setting a pointer to
NULL once its target is gone turns a silent dangling
pointer into a loud null pointer, but only for that one copy of the
address.
const and pointers
const can protect two different things: the object a
pointer points to, or the pointer itself. Read the declaration from
the name outward, right to left:
| Declaration | Read as | *p = 1 |
p = &other |
|---|---|---|---|
int *p |
pointer to int | allowed | allowed |
const int *p (same as
int const *p)
|
pointer to const int | error | allowed |
int *const p |
const pointer to int | allowed | error |
const int *const p |
const pointer to const int | error | error |
#include <stdio.h>
int main(void)
{
int kwh = 350;
int limit = 1000;
const int *reader = &kwh; /* may move, may not write */
int *const fixed = &kwh; /* may write, may not move */
*fixed = 400; /* fine: changes kwh */
reader = &limit; /* fine: now points at limit */
printf("kwh %d, *reader %d\n", kwh, *reader);
return 0;
}
kwh 400, *reader 1000
Writing through a pointer to const does not compile:
#include <stdio.h>
int main(void)
{
int kwh = 350;
const int *p = &kwh;
*p = 400;
printf("%d\n", kwh);
return 0;
}
example.c:8:8: error: assignment of read-only location ‘*p’
The most useful form is const on parameters, as in
parse_power(const char *line, int *watts) above: the
prototype promises that line is only read, while
watts will be written. The compiler holds the function
to that promise, and the reader of the prototype knows which
arguments change. Note that const int * only means "not
changed through this pointer"; the object itself may still
change in other ways.
void pointers
A void pointer,
void *, holds an address without saying what type of
object is there. Any object pointer converts to
void * and back without a cast, which makes it C's way
of writing generic code: malloc returns a
void *, and memcpy,
memset and qsort take them, because they
work on any kind of data. This function prints the bytes of any
object:
#include <stdio.h>
#include <string.h>
/* Prints any block of memory as hexadecimal bytes. */
static void dump(const char *label, const void *data, size_t size)
{
const unsigned char *bytes = data; /* void * converts without a cast */
printf("%-8s", label);
for (size_t i = 0; i < size; i++) {
printf(" %02X", bytes[i]);
}
printf("\n");
}
int main(void)
{
int watts = 1234;
unsigned short crc = 0xF1F4;
char unit[] = "kW";
dump("watts", &watts, sizeof watts);
dump("crc", &crc, sizeof crc);
dump("unit", unit, sizeof unit);
return 0;
}
watts D2 04 00 00 crc F4 F1 unit 6B 57 00
The output shows two things that are easy to forget. 1234 is 0x04D2, but its lowest byte comes first in memory, because this machine is little-endian (see the standard library and portability). And the string "kW" takes three bytes: the last one is the terminating zero (see strings and buffer overflows).
Because the compiler does not know what a void *
points to, it cannot dereference one or do arithmetic with it.
Convert it to a real pointer type first:
#include <stdio.h>
int main(void)
{
int watts = 1234;
void *p = &watts;
printf("%d\n", *p);
return 0;
}
example.c:8:20: error: dereferencing ‘void *’ pointer [-Werror]
Pointers to pointers
A pointer is a variable too, so it has an address. A
pointer to pointer, such
as int **pp, holds the address of a pointer;
*pp is that pointer and **pp the object it
points to. You meet them for two reasons. The first is a function
that must change the caller's pointer, not just the object
behind it: pass by pointer, one level up. This parser moves the
caller's position in a P1 line forward after each number it reads:
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
/* Reads a number at *cursor and moves the caller's cursor past it. */
static long next_number(const char **cursor)
{
char *end;
long value = strtol(*cursor, &end, 10);
*cursor = end; /* changes the caller's pointer */
while (**cursor != '\0' && !isdigit((unsigned char)**cursor)) {
(*cursor)++; /* skip separators */
}
return value;
}
int main(void)
{
const char *line = "0-0:96.7.21(00002)";
const char *pos = line;
printf("numbers:");
while (*pos != '\0') {
printf(" %ld", next_number(&pos));
}
printf("\n");
return 0;
}
numbers: 0 0 96 7 21 2
The parentheses in (*cursor)++ matter:
*cursor++ would move the local
cursor instead of the caller's pointer. The second
reason is an array of pointers, such as an array of strings. Passed
to a function, it becomes a pointer to its first element, which is a
pointer: char **. That is the type of
argv in int main(int argc, char **argv).
Function pointers
A function also has an address, and a function pointer stores it so
that the code to run can be chosen at run time. The syntax is the
hardest part:
int (*order)(const void *, const void *) declares
order as a pointer to a function taking two
const void * and returning int. The
parentheses around *order are required; without them it
declares a function returning a pointer.
The standard library's qsort sorts any array, because
you pass it the comparison as a function pointer. Its comparison
function receives two const void * and returns a
negative number, zero or a positive number:
#include <stdio.h>
#include <stdlib.h>
static int ascending(const void *a, const void *b)
{
int x = *(const int *)a;
int y = *(const int *)b;
return (x > y) - (x < y); /* -1, 0 or 1, and never overflows */
}
static int descending(const void *a, const void *b)
{
return ascending(b, a);
}
static void print_all(const char *label, const int *values, size_t n)
{
printf("%-11s", label);
for (size_t i = 0; i < n; i++) {
printf(" %d", values[i]);
}
printf("\n");
}
int main(void)
{
int kwh[] = {130, 95, 1500, 120};
size_t n = sizeof kwh / sizeof kwh[0];
int (*order)(const void *, const void *) = ascending;
qsort(kwh, n, sizeof kwh[0], order);
print_all("ascending:", kwh, n);
order = descending;
qsort(kwh, n, sizeof kwh[0], order);
print_all("descending:", kwh, n);
return 0;
}
ascending: 95 120 130 1500 descending: 1500 130 120 95
A typedef gives a function pointer type a readable
name. Firmware and network code often keep a table that maps an
input to the function that handles it, a dispatch table; here OBIS
codes from a P1 telegram are routed to handlers:
#include <stdio.h>
#include <string.h>
typedef void (*handler)(const char *value);
static void on_power(const char *value) { printf("power now: %s\n", value); }
static void on_voltage(const char *value) { printf("voltage: %s\n", value); }
struct route {
const char *obis;
handler handle;
};
static const struct route routes[] = {
{ "1-0:1.7.0", on_power },
{ "1-0:32.7.0", on_voltage },
};
int main(void)
{
const char *obis[] = { "1-0:32.7.0", "1-0:1.7.0", "0-0:96.14.0" };
const char *values[] = { "231.4*V", "01.234*kW", "0002" };
for (int i = 0; i < 3; i++) {
int handled = 0;
for (size_t r = 0; r < sizeof routes / sizeof routes[0]; r++) {
if (strcmp(routes[r].obis, obis[i]) == 0) {
routes[r].handle(values[i]);
handled = 1;
}
}
if (!handled) {
printf("ignored: %s\n", obis[i]);
}
}
return 0;
}
voltage: 231.4*V power now: 01.234*kW ignored: 0-0:96.14.0
Callbacks work the same way: a framework stores your function's address and calls it later, for example when a pin changes (see GPIO, PWM and interrupts). A function pointer that is null, or that was overwritten by a memory bug, sends the program to a wrong address; that is one reason why memory-safety bugs in C can be so much worse than a wrong number.