Skip to content
BoKSA

Pointers in C

Pointers in C

Introduction

Every variable in your program lives somewhere in memory — at a numbered address, like a house on a street. A normal variable (int a) holds a value (for example 80). A pointer (int *p) holds the address of where some value lives — like writing down "house number 42" instead of storing the furniture itself.

Pointers are not an academic trick. In embedded systems you use them to talk to hardware registers (fixed addresses in the datasheet), walk through arrays, build linked lists, and pass data to functions without copying large blocks. This article explains pointers step by step, including what goes wrong when you misuse them.


Memory addresses in one picture

Picture RAM as a long row of numbered mailboxes. Each mailbox holds one byte (for simplicity). Four consecutive mailboxes might hold one 32-bit int.

1
2
3
4
Address:   9000   9001   9002   9003
           ┌──┬──┬──┬──┐
           │  │  │  │  │   ← variable a (int) might start at 9000
           └──┴──┴──┴──┘

The address of a is 9000 (shown as 0x... in hex on real systems). A pointer to a stores that number so you can find a later.


Declaring pointers

1
2
3
int *p;     /* p can point to an int */
char *q;    /* q can point to a char */
float *r;   /* r can point to a float */

The * in a declaration means "pointer to". Spacing is style: int* p and int *p mean the same thing.

In plain terms

If int a is a teddy bear in a box, then int *p is a note that says which shelf the box is on. The note is not the bear — it tells you where to find the bear.


Two operators: & and *

Operator Name What it does
& Address-of "Where does this variable live?"
* Dereference "What value lives at this address?"
1
2
3
4
int a = 80;
int *p;

p = &a;      /* p now holds the address of a */

After that:

  • p and &a are the same address
  • *p and a are the same value (80)
  • Changing *p = 99 also changes a to 99

Complete example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <stdio.h>

int main(void) {
    int a = 80;
    int *p;

    p = &a;
    printf("Address of a: %p\n", (void *)&a);
    printf("Same in p:    %p\n", (void *)p);
    printf("Value of a:   %i\n", a);
    printf("Value via *p: %i\n", *p);
    return 0;
}

Use %p with a (void *) cast to print addresses portably.


The uninitialized pointer — writing into thin air

1
2
3
4
int a = 80, b = 160;
int *p;

*p = b;   /* DISASTER: p points nowhere known */

Here p was never assigned. It contains garbage — some random address. *p = b tries to write 160 into someone else's memory. The program might crash, corrupt another variable, or appear to work until something unrelated breaks.

Always set a pointer before use:

  • p = &a — point at existing variable
  • p = NULL — point at nothing (safe to test, unsafe to dereference)
  • p = malloc(...) — point at heap block (see Dynamic memory and I/O)

Pointer arithmetic — why p++ is not always +1

When you add 1 to a pointer, C moves forward by one element, not one byte:

1
2
char *q;   /* q++ → next char  → +1 byte */
int *p;    /* p++ → next int   → +4 bytes if int is 4 bytes */

That is why you can walk an array by incrementing a pointer — the type tells the compiler the step size.

1
2
3
4
5
int row_int[10] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
int *p = row_int;   /* points at first element */

for (int k = 0; k < 10; k++)
    printf("%d ", *(p++));

The array name row_int is the address of element 0. That link between arrays and pointers is developed further in Pointers in depth.


Pointers and strings

A string literal like "test" is stored in read-only memory. A char * can point at the first character:

1
2
3
4
5
char *p = "test";

printf("First letter:  %c\n", *p);       /* 't' */
printf("Second letter: %c\n", *(p + 1)); /* 'e' */
printf("End marker:    %d\n", *(p + 4)); /* 0 — null terminator */

C strings end with a hidden '\0' (zero) byte. That is how printf knows where to stop.

Important: char *p = "test" must not be used to modify the string — the literal is read-only. For a mutable string:

1
char buf[] = "test";   /* copy on stack — OK to change buf[0] */

Endianness — reading an integer byte by byte

An int uses several bytes in memory. Endianness is the order those bytes are stored.

1
2
3
4
5
unsigned char *p;
int a = 0xFA27BE93;

p = (unsigned char *)&a;
printf("%02X %02X %02X %02X\n", p[0], p[1], p[2], p[3]);

On a typical little-endian PC or ARM Cortex-M, you see 93 BE 27 FA — least significant byte first. On big-endian systems you would see FA 27 BE 93.

In plain terms

Imagine writing the number 1234 on paper. Little-endian writes digits 4-3-2-1 in memory; big-endian writes 1-2-3-4. The number is the same; only the storage order differs. When two devices talk over a bus, they must agree on the order.

This matters when you parse binary sensor frames or write multi-byte registers. See Data representation.


Why embedded engineers care about pointers

Hardware registers — The datasheet says "GPIO output data register at offset 0x14". You access it through a pointer:

1
2
volatile uint32_t *gpio_odr = (uint32_t *)0x40020014;
*gpio_odr |= (1U << 5);   /* set pin 5 high */

volatile tells the compiler the value can change outside the program (the hardware changes it). Never omit volatile for MMIO.

Efficiency — Passing a pointer to a large struct costs 4 bytes; passing the whole struct copies every field.

Data structures — Linked lists and queues chain nodes through pointers — see Data structures in C.

DMA — The DMA controller needs the start address of your buffer — a pointer.


Relevant topics


Starting points

  1. Draw mailboxes on paper for int a = 80 and int *p = &a — label address and value.
  2. Predict endianness output before running the byte-print program.
  3. Run with AddressSanitizer: gcc -fsanitize=address on the uninitialized pointer example.
  4. Find one volatile ... * register definition in your MCU header file.

Focus points

  • Initialize every pointer before *p.
  • NULL means "points nowhere" — check before dereference.
  • String literals are read-only when pointed to by char *.
  • Endianness bites when two systems exchange binary data.

Key points

  • A pointer stores an address; & gets it, * follows it.
  • Dereferencing a bad pointer causes crashes or silent corruption.
  • Pointer arithmetic moves by sizeof(type), not always one byte.
  • Endianness determines byte order inside multi-byte values.