Theory-Pointer Basics in Embedded C: Addresses, Dereferencing, NULL and Register Pointers-Free Embedded C Course in Hyderabad

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

MODULE 9 · LESSON 9.1 · THEORY

Pointer Basics in Embedded C: Addresses, Dereferencing, NULL and Register Pointers

What an address is, the & and * operators, declaring pointers, why the pointer type matters, pointer size on the Cortex-M4, NULL and wild pointers, aliasing and a first look at register pointers. Keep this in mind whenever you apply pointer basics in embedded C.

50 min
Read Time
Beginner
Level
M9 · L1
Module / Lesson

What You Will Learn

✓ What an address is and how memory is organised as numbered bytes
✓ The two operators: address-of & and dereference *
✓ Declaring pointers and reading declarations correctly
✓ Why the pointer type decides how many bytes are accessed
✓ Pointer size: 4 bytes on the Cortex-M4, 8 on a 64-bit PC
✓ NULL, uninitialised and dangling pointers
✓ Two pointers to one object (aliasing)
✓ A first look at pointers to peripheral registers

Key Terms

PointerAddressAddress-of operatorDereferenceNULLPointer typeLittle-endiansizeofAliasingDangling pointerWild pointerRegister pointervolatileuintptr_tMemory-mapped I/O

1. Memory Is a Row of Numbered Bytes

A microcontroller sees its memory as one long row of bytes, each with a number called its address. On the STM32F407 the SRAM starts at address 0x20000000 and the Flash at 0x08000000; peripherals such as GPIOD appear at 0x40020C00. Every variable occupies one or more consecutive bytes, so each has three properties: a name, a value and an address. Many bugs in pointer basics in embedded C start here.

Variables in SRAM (illustrative layout)
0x20000000
uint8_t flag = 1 (1 byte)
0x20000004
uint32_t count = 500 (4 bytes)
0x20000008
uint16_t level = 90 (2 bytes)

A pointer is a variable whose value is the address of another variable. Knowing the address, the program can read or change that variable indirectly, without using its name. Pointers are the foundation of arrays, strings, callbacks, data structures and, in embedded work, every access to a peripheral register. This is the core rule of pointer basics in embedded C.

sizes.c
#include <stdio.h> #include <stdint.h> int main(void) { printf(“sizeof(uint8_t) = %zu\n”, sizeof(uint8_t)); printf(“sizeof(uint16_t) = %zu\n”, sizeof(uint16_t)); printf(“sizeof(uint32_t) = %zu\n”, sizeof(uint32_t)); printf(“sizeof(int *) = %zu (PC: 64-bit pointers; Cortex-M4: 4)\n”, sizeof(int *)); printf(“sizeof(char *) = %zu\n”, sizeof(char *)); printf(“sizeof(void *) = %zu\n”, sizeof(void *)); return 0; }
Output (verified by compiling and running)
sizeof(uint8_t) = 1 sizeof(uint16_t) = 2 sizeof(uint32_t) = 4 sizeof(int *) = 8 (PC: 64-bit pointers; Cortex-M4: 4) sizeof(char *) = 8 sizeof(void *) = 8

All pointer types have the same size on a given machine: the width of an address. The STM32F407 has a 32-bit address space (4 GB), so a pointer is 4 bytes; the 64-bit PC used to run these programs shows 8. The explanations below hold on both; where a number differs, the text says so. Reviewers check pointer basics in embedded C closely.

2. The Two Operators: & and *

Two operators do all the work. The address-of operator &x gives the address of x. The dereference (indirection) operator *p gives the object that p points to. They are opposites: *(&x) is x again. Interviewers test pointer basics in embedded C on exactly this point.

ExpressionMeaningExample result
int x = 10;x holds the value 10x = 10
int *p = &x;p holds the address of xp points to x
*pthe int at that address10
*p = 25;writes 25 to x through px = 25
&x == pcomparison of addresses1 (true)
addr_deref.c
#include <stdio.h> int main(void) { int x = 10; int *p = &x; /* p holds the address of x */ printf(“x = %d\n”, x); printf(“*p = %d (the int p points to)\n”, *p); printf(“&x == p : %s\n”, (&x == p) ? “yes” : “no”); *p = 25; /* write through the pointer */ printf(“after *p = 25: x = %d\n”, x); x = x + 1; /* write through the name */ printf(“after x + 1 : *p = %d\n”, *p); return 0; }
Output (verified by compiling and running)
x = 10 *p = 10 (the int p points to) &x == p : yes after *p = 25: x = 25 after x + 1 : *p = 26

The variable and the pointer see the same memory: a change through either is visible through the other. The exact address is not printed because it changes between runs and platforms; only relations such as &x == p are meaningful. Master pointer basics in embedded C by practising this case.

p points to x
p
value = address of x
→ points to →
x
value = 10, then 25, then 26
Two meanings of *: In a declaration (int *p;) the star says “p is a pointer”. In an expression (*p = 5;) it means “the object p points to”. Multiplication is a third use.

3. Declaring Pointers

A pointer declaration names the type of object it points to. Read it from the variable name outward. Remember this when you review pointer basics in embedded C in code reviews.

DeclarationReads as
int *p;p is a pointer to int
uint8_t *buf;buf is a pointer to uint8_t (a byte pointer)
char *s;s is a pointer to char
const int *q;q is a pointer to const int (Lesson 8.2)
int *a, b;a is a pointer to int, b is a plain int
decl_trap.c
#include <stdio.h> int main(void) { int *a, b; /* the star belongs to a only */ int v = 7; a = &v; b = v; printf(“sizeof(a) = %zu, sizeof(b) = %zu\n”, sizeof a, sizeof b); int *c, *d; /* a star for each pointer */ c = &v; d = &v; printf(“*c = %d, *d = %d\n”, *c, *d); return 0; }
Output (verified by compiling and running)
sizeof(a) = 8, sizeof(b) = 4 *c = 7, *d = 7
Style rule: Declare one pointer per line (int *a; then int b;). The star binds to the name, not to the type, so int* a, b; misleads the reader.

4. The Pointer Type Decides What Is Read

A pointer holds only an address; the type tells the compiler how many bytes to access and how to interpret them. The same address read through three different pointer types yields three different values. On both the Cortex-M4 and an x86 PC the byte order is little-endian: the least significant byte is stored at the lowest address.

type_decides.c
#include <stdio.h> #include <stdint.h> int main(void) { uint32_t word = 0x12345678u; /* stored as bytes 78 56 34 12 (little-endian) */ uint8_t *p8 = (uint8_t *)&word; uint16_t *p16 = (uint16_t *)&word; uint32_t *p32 = &word; printf(“*p32 = 0x%08X\n”, (unsigned)*p32); printf(“*p16 = 0x%04X\n”, (unsigned)*p16); printf(“*p8 = 0x%02X\n”, (unsigned)*p8); for (int i = 0; i < 4; i++) printf(“byte %d = 0x%02X\n”, i, (unsigned)p8[i]); return 0; }
Output (verified by compiling and running)
*p32 = 0x12345678 *p16 = 0x5678 *p8 = 0x78 byte 0 = 0x78 byte 1 = 0x56 byte 2 = 0x34 byte 3 = 0x12
One word, read through three pointer types
+0: 0x78
+1: 0x56
+2: 0x34
+3: 0x12
uint8_t* reads the first box • uint16_t* reads the first two • uint32_t* reads all four

Reading the same memory through a pointer of a different type is the basis of byte-level access to buffers and registers (Lesson 9.5 treats casting and alignment in depth). It also means a pointer of the wrong type silently reads the wrong number of bytes: the compiler warns about incompatible types, so never silence the warning with a cast unless you understand the consequences.

5. Initialising Pointers: NULL and Wild Pointers

An uninitialised local pointer holds garbage, a wild pointer: dereferencing it reads or writes a random address. On the STM32F407 that can hit a peripheral register, corrupt another variable, or cause a BusFault. Always initialise a pointer, either to a valid address or to NULL, the null pointer constant (a pointer value that points to no object, written 0 or NULL).

null_check.c
#include <stdio.h> #include <stddef.h> static int read_or_default(const int *p, int fallback) { return (p != NULL) ? *p : fallback; /* test before the dereference */ } int main(void) { int value = 42; int *valid = &value; int *none = NULL; printf(“valid : %d\n”, read_or_default(valid, –1)); printf(“none : %d\n”, read_or_default(none, –1)); if (!none) printf(“a NULL pointer is false in a condition\n”); return 0; }
Output (verified by compiling and running)
valid : 42 none : -1 a NULL pointer is false in a condition

A NULL pointer converts to false in a condition, so if (p) and if (p != NULL) are equivalent. Dereferencing NULL is undefined behavior; on a Cortex-M4 address 0 is readable (it is an alias of the start of Flash), so a stray read of NULL often succeeds silently and returns the first word of the vector table, which makes the bug harder to find than the crash on a PC.

wild.c
int main(void) { int *p; return *p; /* p was never set */ }
GCC warns about a possibly uninitialised pointer
wild.c: In function ‘main’: wild.c:4:12: warning: ‘p’ is used uninitialized [-Wuninitialized] 4 | return *p; /* p was never set */ | ^~ wild.c:3:10: note: ‘p’ was declared here 3 | int *p; | ^

6. Several Pointers, One Object (Aliasing)

Nothing stops two pointers from holding the same address. A write through one is then visible through the other. This is useful (a handle shared between modules, a hardware register accessed from two places) and dangerous (a function that assumes its two pointer parameters are distinct, see the XOR swap in Lesson 8.2).

alias.c
#include <stdio.h> int main(void) { int level = 5; int *a = &level; int *b = &level; /* a and b alias the same int */ *a = 20; printf(“*b = %d (changed through a)\n”, *b); *b += 3; printf(“level = %d, *a = %d\n”, level, *a); printf(“a == b : %s\n”, (a == b) ? “yes” : “no”); return 0; }
Output (verified by compiling and running)
*b = 20 (changed through a) level = 23, *a = 23 a == b : yes

7. A First Look at Register Pointers

The most important pointer in embedded C is a pointer to a peripheral register. The STM32F407 reference manual gives a fixed address for each register; the program turns that number into a pointer and dereferences it. Writing GPIOD_ODR then writes the output data register of port D (base 0x40020C00, ODR at offset 0x14).

register_macro.c (for the real board)
#include <stdint.h> #define GPIOD_BASE 0x40020C00UL #define GPIOD_ODR (*(volatile uint32_t *)(GPIOD_BASE + 0x14UL)) void green_led_on(void) { GPIOD_ODR |= (1u << 12); /* PD12: green LED */ }

Read the macro from the inside: the integer 0x40020C14 is cast to “pointer to volatile uint32_t”, and the outer * dereferences it, so the macro behaves like an ordinary variable that happens to live in the peripheral space. volatile tells the compiler that every access matters. On a PC this address does not exist, so the next program uses an ordinary variable as a stand-in register with the same macro shape.

register_sim.c
#include <stdio.h> #include <stdint.h> static volatile uint32_t sim_odr = 0; /* stands in for GPIOD->ODR */ #define GPIOD_ODR_SIM (*(&sim_odr)) /* same shape: dereference of an address */ int main(void) { GPIOD_ODR_SIM |= (1u << 12); /* PD12 on */ GPIOD_ODR_SIM |= (1u << 14); /* PD14 on */ printf(“ODR = 0x%08X\n”, (unsigned)GPIOD_ODR_SIM); GPIOD_ODR_SIM &= ~(1u << 12); /* PD12 off */ printf(“ODR = 0x%08X\n”, (unsigned)GPIOD_ODR_SIM); return 0; }
Output (verified by compiling and running)
ODR = 0x00005000 ODR = 0x00004000

Lesson 9.6 builds this idea out into register structs and safe access macros. For now remember the principle: a register is just a memory location with a fixed address, and a pointer reaches it.

8. Common Pointer Mistakes

MistakeEffectFix
Uninitialised pointerWrite to a random addressInitialise to a valid address or NULL
Dereferencing NULLUndefined behavior (silent read on Cortex-M)Check before use
Returning the address of a localDangling pointerReturn a value, a static, or fill a caller buffer
Wrong pointer typeWrong number of bytes readMatch the type to the data
int *a, b; misreadb is not a pointerOne declaration per line
Assigning an integer to a pointerCompiler warning, wrong addressCast deliberately: (volatile uint32_t *)0x40020C14
mist.c
int *make(void) { int local = 5; return &local; /* dangling */ } int main(void) { int *p = 1000; /* integer to pointer */ float f = 2.0f; int *q = &f; /* wrong pointer type */ return (int)(p == q) + *make(); }
GCC on pointer mistakes
mist.c: In function ‘make’: mist.c:4:12: warning: function returns address of local variable [-Wreturn-local-addr] 4 | return &local; /* dangling */ | ^~~~~~ mist.c: In function ‘main’: mist.c:9:14: warning: initialization of ‘int *’ from ‘int’ makes pointer from integer without a cast [-Wint-conversion] 9 | int *p = 1000; /* integer to pointer */ | ^~~~ mist.c:11:14: warning: initialization of ‘int *’ from incompatible pointer type ‘float *’ [-Wincompatible-pointer-types] 11 | int *q = &f; /* wrong pointer type */ | ^

GCC reports each of these (the exact wording varies between versions). Treat pointer warnings as errors in firmware: -Werror=incompatible-pointer-types -Werror=int-conversion -Werror=return-local-addr.

9. Best Practices for Pointer Basics in Embedded C

  • Initialise every pointer at its declaration (to an address or NULL).
  • Declare one pointer per line and keep the star next to the name.
  • Use fixed-width types for what is pointed to (uint8_t *, uint32_t *).
  • Check pointers received from outside for NULL before dereferencing.
  • Use const for read-only data and volatile for registers.
  • Never print or compare pointer values in portable logic; use uintptr_t if you must convert to an integer.
  • Do not return the address of a local variable.
  • Keep the warnings on: pointer warnings usually mean real bugs.

Key Takeaways — Lesson 9.1

  • A pointer is a variable that stores an address; & produces an address and * follows one.
  • The pointer type decides how many bytes are accessed and how they are interpreted.
  • A pointer is 4 bytes on the Cortex-M4 (8 on a 64-bit PC).
  • Initialise pointers; NULL means “no object” and must be checked before use.
  • Two pointers can alias one object; writes are visible through both.
  • A peripheral register is a fixed address reached through a (volatile) pointer.

Frequently Asked Questions

What is a pointer in C?
A variable that stores the memory address of another object. You read or write the object by dereferencing the pointer with the * operator.
What is the difference between & and * in C?
&x gives the address of x. *p gives the object stored at the address in p. They are inverse operations.
How big is a pointer on the STM32F407?
Four bytes, because the Cortex-M4 has a 32-bit address space. On a 64-bit PC a pointer is eight bytes.
What is a NULL pointer?
A pointer that points to no object. It is written NULL (or 0), evaluates to false in a condition and must never be dereferenced.
Why do embedded programs use pointers to registers?
Peripheral registers are memory locations at fixed addresses. Casting the address to a volatile pointer and dereferencing it lets C code read and write the hardware directly.

Ready to practise?

Next: worked examples with swaps, byte access, buffers, NULL checks and simulated GPIO registers.

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *