Skip to content
BoKSA

Pointers in Depth

Pointers in Depth

Introduction

You know that a pointer holds an address. Now the question is what you do with that power when functions, arrays, and callbacks enter the picture.

Most beginner bugs in embedded C come from this area: "I passed my variable to a function but it did not change", "my array works in main but not inside the function", or "this pointer worked once and then random things broke". This article explains why those happen and how experienced firmware code avoids them.


Why bother with pointers at all?

Without pointers, every function argument is a copy. That is fine for a single int, but imagine copying a 200-byte sensor packet on every function call — slow and wasteful on a small MCU. Pointers let functions look at the same memory you already have.

Situation Without pointers With pointers
Swap two variables Need clumsy workarounds Pass addresses
Large struct Copy entire struct Pass one address
Modify caller data Impossible by value *p = new_value
Hardware register Cannot "pass" hardware Pass or use fixed address
Callback on interrupt Function pointer

Call by value — the photocopy problem

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void swap(int p, int q) {
    int temp = p;
    p = q;
    q = temp;
}

int main(void) {
    int a = 10, b = 20;
    swap(a, b);
    /* a is still 10, b is still 20 */
}

When you call swap(a, b), C makes photocopies of a and b into p and q. Swapping the photocopies does not change the originals on your desk.

In plain terms

You gave your friend a copy of your house key diagram. They redraw their copy — your real key does not move.

Call by reference — give the real address

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void swap(int *p, int *q) {
    int temp = *p;
    *p = *q;
    *q = temp;
}

int main(void) {
    int a = 10, b = 20;
    swap(&a, &b);
    /* a is 20, b is 10 */
}

&a means "address of a". Inside swap, *p reads and writes the real a in main.

This pattern appears everywhere: HAL_UART_Receive(&huart, buf, len, timeout) — the driver needs your buffer's address so it can fill it in place.


Arrays and functions — why size disappears

When you pass an array to a function, C does not pass the whole array. It passes a pointer to the first element only. The function has no idea how long the array is unless you tell it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
double average(int p[], int n) {
    double sum = 0.0;
    for (int k = 0; k < n; k++)
        sum += p[k];
    return sum / n;
}

int main(void) {
    int row[10] = {2, 6, 5, 8, 7, 9, 1, 4, 10, 3};
    printf("%.4f\n", average(row, 10));
}

These declarations mean the same thing to the compiler:

1
2
double average(int p[], int n);
double average(int *p, int n);

p[k] is simply shorthand for *(p + k) — the value at offset k from the start.

Modifying the caller's buffer

1
2
3
4
5
6
7
void makecapitals(char *p) {
    while (*p != '\0') {
        if (*p >= 'a' && *p <= 'z')
            *p = *p + 'A' - 'a';
        p++;
    }
}

The pointer p starts at the first character of the caller's string and walks forward. Every *p = ... changes the original buffer in main. There is no copy.

Lesson: if a function should fill or change your data, pass a pointer (and the buffer size for safety).


Returning more than one value

A C function can only return one value directly. For quotient and remainder, use output parameters:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void div_int(int num1, int num2, int *quotient, int *remainder) {
    *quotient = num1 / num2;
    *remainder = num1 % num2;
}

int main(void) {
    int a = 76, b = 10, q, r;
    div_int(a, b, &q, &r);
    printf("quotient %d, remainder %d\n", q, r);
}

You pass addresses of q and r; the function writes through them. HAL functions often use this style: HAL_GPIO_ReadPin(...) returns state; more complex APIs return status through pointer arguments.


Returning pointers — the dangling trap

Wrong: address of a local variable

1
2
3
4
5
6
int *getMax(int m, int n) {
    if (m > n)
        return &m;
    else
        return &n;
}

m and n exist only inside getMax. When the function returns, that stack space is reused for something else. The pointer you return points at a mailbox that already has new mail — dangling pointer.

Right: point at caller's variables

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
int *getMax(int *m, int *n) {
    if (*m > *n)
        return m;
    else
        return n;
}

int main(void) {
    int x = 100, y = 200;
    int *max = getMax(&x, &y);
    printf("max: %d\n", *max);
}

x and y live in main — they stay valid. Alternatively return data through an argument, use static storage carefully (not re-entrant), or heap memory you manage.


Pointer to pointer — when one level is not enough

Sometimes you need a pointer to a pointer:

1
2
3
char ch;
char *pch = &ch;      /* points to ch */
char **ppch = &pch;   /* points to pch */

Array of strings is the classic case:

1
char *seasons[] = {"Winter", "Spring", "Summer", "Autumn"};

seasons is an array of four pointers. Each pointer points at the first letter of a string. Passing to a function:

1
2
3
4
5
6
void doprint(char **q) {
    for (int k = 0; k < 4; k++)
        printf("%s\n", q[k]);
}

doprint(seasons);

q[k] is the k-th string. *(q[k] + 2) would be the third character of that string — useful for parsing command tokens in a UART CLI.


Function pointers — choosing which function runs

A function pointer stores the address of a function, not data. You can call it like a normal function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
int do_add(int a, int b) { return a + b; }
int do_mult(int a, int b) { return a * b; }

int main(void) {
    int (*pf)(int, int);

    pf = do_add;
    printf("%d\n", pf(6, 5));   /* 11 */

    pf = do_mult;
    printf("%d\n", pf(6, 5));   /* 30 */
}

Read the type inside-out: pf is a pointer to a function that takes two int and returns int.

Embedded uses:

  • ISR registration — vector table holds addresses of handlers
  • HAL callbacksvoid (*callback)(void) runs when transfer completes
  • State machines — table of functions, one per state action
  • RTOS — task entry point is a function pointer

In plain terms

Instead of pressing a fixed button on a remote, you program which button the remote calls. At runtime you can switch between "add mode" and "multiply mode" by changing the function pointer.


Relevant topics


Starting points

  1. Prove call-by-value failure with swap — then fix with pointers.
  2. Write makecapitals and pass a buffer from fgets — see bytes change in debugger.
  3. Explain aloud why return &m is illegal for a local m.
  4. Sketch a tiny state machine with three function pointers in an array.

Focus points

  • Always pass array length — the compiler does not do it for you.
  • Never return pointers to locals unless you understand lifetime.
  • Function pointer signature must match exactly.
  • Use const when the function must not modify data: void print(const char *s).

Key points

  • Call by value copies; pointers let functions change caller data.
  • Array parameters are pointers — size is lost, pass n explicitly.
  • Output parameters return multiple values through addresses.
  • Function pointers enable callbacks and runtime dispatch — common in HAL and RTOS.