Programming · 55 pages
- C language — 15 lessonstutorial
- Lesson 1 — Hello, C
- Lesson 2 — Types & literals
- Lesson 3 — Expressions & operators
- Lesson 4 — Control flow
- Lesson 5 — Functions
- Lesson 6 — Arrays
- Lesson 7 — Pointers
- Lesson 8 — Strings
- Lesson 9 — Structs & unions
- Lesson 10 — Dynamic memory
- Lesson 11 — Preprocessor
- Lesson 12 — Multi-file projects
- Lesson 13 — Standard library
- Lesson 14 — Undefined behavior
- Lesson 15 — Modern C (C11/17/23)
- C++ programming — 15 lessonstutorial
- Lesson 1 — Hello, C++
- Lesson 2 — References & namespaces
- Lesson 3 — Classes & RAII
- Lesson 4 — Inheritance & polymorphism
- Lesson 5 — Templates
- Lesson 6 — STL containers
- Lesson 7 — Iterators & algorithms
- Lesson 8 — Smart pointers
- Lesson 9 — Move semantics
- Lesson 10 — Lambdas
- Lesson 11 — Error handling
- Lesson 12 — Concurrency
- Lesson 13 — Modules (C++20)
- Lesson 14 — Modern C++23
- Lesson 15 — C++ on Cortex-M
- Bare-metal C tutorial — 10 lessonsembedded
- Lesson 1 — Toolchain setup
- Lesson 2 — Bare-metal blink
- Lesson 3 — Registers in C
- Lesson 4 — Memory map
- Lesson 5 — Linker script
- Lesson 6 — Interrupts & ISRs
- Lesson 7 — Bit patterns
- Lesson 8 — Fixed-point math
- Lesson 9 — State machines
- Lesson 10 — Ring buffers + DMA
- Embedded C patterns
- volatile & memory-mapped IO
- CMSIS-Core
- CMSIS-DSP
- Linker scripts
- Startup & vector table
- C++ on Cortex-M
- Rust on Cortex-Mmodern
- Inline assembly
- Bare-metal libc & newlib-nano
- Bootloaders & DFU
- OTA update flow
Lesson 7 — Pointers, the C superpower
Addresses, dereference, NULL, pointer arithmetic, const correctness in all three positions, function pointers as first-class values, and a generic sort using a comparator.
- What you'll learn
- What a pointer is, dereference and address-of, NULL, pointer arithmetic, pointer-to-pointer, the three positions of const, function pointers, a generic sort
- Code provided
- About 130 lines including a generic sort using qsort-style comparator pointers
- Time
- 120-150 minutes
- Pre-reqs
- Lesson 6 — Arrays
Pointers are the feature that separates C from every managed language you have used before. If you understand pointers, the rest of C falls into place. If you do not, every other topic — strings, dynamic memory, structs, function pointers, the standard library — feels like guesswork. This lesson takes pointers slowly and carefully.
What a pointer is
A pointer is a variable whose value is the memory address of another object. That is it. There is nothing magical: a pointer holds a number, and the number happens to be interpretable as the location of something else in memory.
int x = 42;
int *p = &x; /* p holds the address of x */After this code:
xis a 4-byte int (on most systems) at some address — say,0x7ffe9c4a5b3c.pis an 8-byte pointer (on 64-bit) at some other address, holding the value0x7ffe9c4a5b3c.
You can print them:
#include <stdio.h>
int main(void) {
int x = 42;
int *p = &x;
printf("x = %d\n", x);
printf("&x = %p\n", (void *)&x);
printf("p = %p\n", (void *)p);
printf("*p = %d\n", *p);
return 0;
}Output (your addresses will differ):
x = 42
&x = 0x7ffe9c4a5b3c
p = 0x7ffe9c4a5b3c
*p = 42&x is the address-of operator — gives the address of the variable.
*p is the dereference operator — gives the value at the address.
int *p declares p as "pointer to int".
The dereference operator * lets you both read and write through the pointer:
*p = 99; /* now x is 99 */
printf("%d\n", x); /* 99 */Modifying through a pointer is the way to write functions that modify caller state — exactly the swap example from lesson 5.
NULL — the "no pointer" pointer
NULL is a macro defined in <stddef.h> (and many other headers). It represents the "does not point to anything" value of a pointer.
int *p = NULL;
if (p == NULL) {
/* p is not pointing at anything */
}In C, NULL is usually defined as ((void *)0) or just 0. The exact definition is implementation-defined, but every C library defines it. C23 adds nullptr as a proper keyword (lesson 15), which is type-safer than the macro, but for C17 code, NULL is what you write.
Dereferencing a NULL pointer is undefined behavior. On Linux/macOS/Windows it usually segfaults — the OS catches the access to the first page of memory and kills the process. On a microcontroller it may successfully read whatever is at address 0 (often the initial stack pointer or vector table) and proceed with garbage. Always check pointers before dereferencing if there is any chance they could be NULL.
The classic guard:
if (p != NULL) {
use(*p);
}
/* Equivalently — pointers are testable directly */
if (p) {
use(*p);
}C lets you test a pointer in a boolean context directly. if (p) is "if p is non-NULL". Short-circuit && (lesson 3) makes this idiomatic:
if (p != NULL && p->ready) { ... }Pointer arithmetic
Pointers support arithmetic, but the unit is the size of the pointed-to type, not bytes.
int a[5] = { 10, 20, 30, 40, 50 };
int *p = a; /* p points to a[0] */
p + 1 /* points to a[1] — the address is a + sizeof(int) bytes */
p + 3 /* points to a[3] */
*(p + 2) /* equivalent to a[2] — value is 30 */
p[2] /* equivalent — [] is pointer arithmetic in disguise */p + 1 does not add 1 byte. It adds sizeof(*p) bytes. The compiler does this multiplication for you because the unit of an array is the element, not the byte.
double d[3];
double *q = d;
q + 1 /* address advances by sizeof(double) = 8 bytes */
char c[10];
char *r = c;
r + 1 /* address advances by sizeof(char) = 1 byte */Pointer subtraction works similarly: p2 - p1 is the number of elements between them, not bytes. Pointer subtraction is only defined if both pointers point into the same array (or one past the end).
Comparison of pointers (<, <=, >, >=) is only defined when the pointers point into the same array. Cross-object comparison is undefined. Equality (==, !=) is always defined.
The [] operator is defined in terms of pointer arithmetic: a[i] is shorthand for *(a + i). This implies the surprising-but-true 5[a] is the same as a[5] — both compile to *(a + 5). Do not write 5[a] in production code; it is a curiosity, not a feature.
Pointers, arrays, and []
The relationship between pointers and arrays is the single most confusing topic for C beginners. Two facts:
- An array decays to a pointer to its first element in most expressions (lesson 6).
- The
[]operator works on any pointer.
Combine them and a lot of C code suddenly makes sense:
int a[10];
int *p = a; /* a decays to &a[0] */
a[3] = 5; /* same as *(a + 3) */
p[3] = 5; /* same as *(p + 3) — equivalent to a[3] */But arrays and pointers are not the same type:
sizeof(a)is 40 (array size),sizeof(p)is 8 (pointer size).&aisint (*)[10](pointer to array of 10),&pisint **(pointer to pointer).- You cannot assign to an array name:
a = b;is a compile error.
The confusion is mostly limited to function parameters, where the rewriting (int a[10] becomes int *a) is invisible. Once you internalise the "arrays decay; pointers do not have a size" rule, the rest of the syntax stops surprising you.
Pointer to pointer
A pointer is a variable, so you can have a pointer to one:
int x = 42;
int *p = &x;
int **pp = &p;
printf("%d\n", **pp); /* dereference twice — 42 */
*pp = NULL; /* same as p = NULL */Pointers-to-pointers come up in two common patterns.
Modifying a pointer in a caller's frame. If you want a function to change which object a caller's pointer points at, the function needs the address of the pointer:
void allocate_thing(Thing **out) {
*out = malloc(sizeof(Thing)); /* writes into the caller's pointer */
}
int main(void) {
Thing *t = NULL;
allocate_thing(&t);
/* t now points to a new Thing */
}Arrays of pointers (e.g., argv). int main(int argc, char *argv[]) — argv is an array of char *, so its parameter form decays to char **. Each argv[i] is a char * pointing at a string.
Const correctness
const is a type qualifier that says "this value cannot be modified through this name". Where you put it matters.
const int *p; /* pointer to const int — cannot modify *p */
int *const p; /* const pointer to int — cannot modify p itself */
const int *const p; /* const pointer to const int — neither */Read right-to-left. const int *p: "p is a pointer to an int that is const". int *const p: "p is a const pointer to an int".
The most common form is pointer-to-const: the function reads from the pointed-to data but does not write:
size_t my_strlen(const char *s) {
size_t n = 0;
while (s[n] != '\0') n++;
return n;
}const char *s tells the caller "this function will not modify your string", and it tells the compiler to reject any attempt inside the function to write through s. If you accidentally type s[0] = 'X';, the compiler errors out.
Top-level const (the second form, int *const p) is rarer. It says "I will not reassign this pointer to a different object" — useful for documenting that a local pointer should not be rebound.
const is shallow, not deep. const char *p lets you assign p = different_string; (the pointer is not const), but does not let you modify the characters. Conversely, char *const p lets you write through the pointer but not rebind it.
void f(const char *s) {
s = "different"; /* OK — the pointer is not const */
s[0] = 'X'; /* ERROR — the chars are const */
}
void g(char *const s) {
s[0] = 'X'; /* OK — the chars are not const */
s = "different"; /* ERROR — the pointer is const */
}The discipline: const-qualify every pointer parameter that the function does not write through. This is one of the cheapest, most reliable type-safety improvements you can make to a codebase. The compiler enforces it; readers benefit; APIs become self-documenting.
Function pointers
In C, a function has an address, just like a variable. A function pointer holds that address. This sounds exotic; it is the foundation of qsort, bsearch, every callback API, every event handler in embedded code, and every plugin system.
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int (*op)(int, int); /* op is a pointer to a function taking (int, int) returning int */
op = add;
printf("%d\n", op(2, 3)); /* 5 */
op = sub;
printf("%d\n", op(7, 4)); /* 3 */The declaration syntax is famously awkward: int (*op)(int, int). Read from inside out: op is — what is it pointing at? — a function — what are its parameters? — (int, int) — and what is its return type? — int. The parentheses around *op are necessary because * has lower precedence than function-call (); without them, int *op(int, int) would mean "op is a function returning int pointer".
You usually typedef function-pointer types to make them readable:
typedef int (*binop_t)(int, int);
binop_t op = add;
op(2, 3);Calling through a function pointer looks like a normal call: op(2, 3). You can also write (*op)(2, 3) for clarity; both forms work and compile identically.
Function pointers can be stored in arrays, passed to other functions, returned from functions. They are first-class values.
binop_t ops[4] = { add, sub, mul, div };
int result = ops[2](6, 7); /* call mul(6, 7) */This is dispatch by index — the workhorse of state machines, command parsers, and any code that needs to "call one of N things based on a runtime value".
A real example: a generic sort with comparator
The C standard library's qsort takes a comparator function pointer so it can sort any type. Here we write our own simplified version using insertion sort (clear, not fast).
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
typedef int (*cmp_t)(const void *, const void *);
/* Generic insertion sort over an array of `n` elements,
* each of size `size` bytes, ordered by `cmp`. */
static void sort(void *base, size_t n, size_t size, cmp_t cmp) {
uint8_t *a = base; /* byte-pointer view */
uint8_t *tmp = malloc(size);
if (!tmp) return;
for (size_t i = 1; i < n; i++) {
memcpy(tmp, a + i * size, size);
size_t j = i;
while (j > 0 && cmp(a + (j - 1) * size, tmp) > 0) {
memcpy(a + j * size, a + (j - 1) * size, size);
j--;
}
memcpy(a + j * size, tmp, size);
}
free(tmp);
}
/* Comparator: ascending int */
static int cmp_int_asc(const void *a, const void *b) {
int ia = *(const int *)a;
int ib = *(const int *)b;
return (ia > ib) - (ia < ib); /* -1, 0, or +1 */
}
/* Comparator: descending int */
static int cmp_int_desc(const void *a, const void *b) {
return cmp_int_asc(b, a);
}
/* Comparator: ascending string (case-sensitive) */
static int cmp_strp_asc(const void *a, const void *b) {
const char *sa = *(const char *const *)a;
const char *sb = *(const char *const *)b;
return strcmp(sa, sb);
}
int main(void) {
int nums[] = { 5, 3, 8, 1, 9, 2, 4 };
size_t n_nums = sizeof(nums) / sizeof(nums[0]);
sort(nums, n_nums, sizeof(int), cmp_int_asc);
for (size_t i = 0; i < n_nums; i++) printf("%d ", nums[i]);
putchar('\n');
/* prints: 1 2 3 4 5 8 9 */
const char *names[] = { "Charlie", "Alice", "Bob" };
size_t n_names = sizeof(names) / sizeof(names[0]);
sort(names, n_names, sizeof(const char *), cmp_strp_asc);
for (size_t i = 0; i < n_names; i++) printf("%s ", names[i]);
putchar('\n');
/* prints: Alice Bob Charlie */
return 0;
}Several patterns to absorb.
void * is the "any pointer". It does not commit to a pointed-to type. The sort function takes void *base because it does not need to know the element type — it manipulates bytes. The comparator gets const void * arguments and casts them to the right concrete type.
memcpy for type-agnostic copying. Inside sort, we cannot do a[i] = a[i-1] because we do not know the element type. memcpy(a + j * size, a + (j-1) * size, size) copies size bytes — works for any type.
Casting const void * to the real type inside the comparator. *(const int *)a reinterprets the void pointer as a pointer to const int and dereferences. This is safe as long as the caller passes the right type — which is contractual, not enforced.
Comparator returns sign. Standard convention: negative means a < b, zero means equal, positive means a > b. The expression (ia > ib) - (ia < ib) produces -1, 0, or +1 without overflow risk. ia - ib would seem simpler but overflows for far-apart values.
Sorting an array of strings is sorting an array of pointers. Each names[i] is a const char *. The comparator receives const void * pointing to a const char * element, so *(const char *const *)a is "dereference to get the const char * value".
The real qsort from <stdlib.h> has the same signature. We will see it in lesson 13.
Exercises
Exercise 7.1. Without running, predict what this prints:
#include <stdio.h>
int main(void) {
int a[5] = { 10, 20, 30, 40, 50 };
int *p = a;
printf("%d %d %d\n", *p, *(p + 2), p[4]);
printf("%d\n", *(p + 5));
return 0;
}Exercise 7.2. Write a const-correct version of this function. Which arguments should be const?
int sum_array(int *a, int n) {
int s = 0;
for (int i = 0; i < n; i++) s += a[i];
return s;
}Exercise 7.3. Define a function-pointer type event_handler_t taking (uint32_t event_id, void *user_data) and returning void. Write a table of three handlers and a dispatcher that calls the right one based on event_id.
What you learned
- A pointer is a variable that holds an address.
&xtakes an address;*pdereferences. NULLis the "no pointer" value. Dereferencing it is undefined.- Pointer arithmetic moves in units of
sizeof(*p), not bytes.[]is pointer arithmetic. constplacement matters:const int *p(data const),int *const p(pointer const),const int *const p(both).- The convention: const-qualify every pointer parameter the function does not write through.
- Function pointers store the address of a function. Syntax is awkward;
typedefto make readable. void *is the generic pointer — used byqsort,memcpy, dispatchers, allocators.
In lesson 8 we look at strings — a topic that is much trickier in C than in any modern language because there is no string type, just char arrays with a NUL terminator.
Next
| Previous | Next |
|---|---|
| Lesson 6 — Arrays | Lesson 8 — Strings |