📚 Chapter 5 🟠 Intermediate → Advanced ⏱ 3–5 hours ⭐ Most important chapter

Pointers &
Dynamic Memory

Memory addresses, the & and * operators, pointer arithmetic, function pointers, and malloc/free — the foundation of every embedded register access and dynamic data structure in C.

🎯 Learning Objectives
Explain why pointers exist and how memory works
Use & and * with full confidence
Perform pointer arithmetic and array traversal
Pass and return pointers safely from functions
Allocate and free memory with malloc/calloc/free
Avoid wild, dangling, and NULL pointer bugs
1Foundations

Why Pointers Exist

Most textbooks open with “a pointer stores an address” and move on. That’s true, but it skips the why. Let’s answer that question properly before writing a single line of pointer code.

The CPU Only Knows Addresses

When you write int x = 10;, the compiler allocates a memory location and labels it x for your convenience. But the CPU itself never thinks in terms of variable names — it only deals with addresses. x is a programmer-facing label; 0x1000 is what the machine actually sees.

📋

Efficient Function Calls

Passing 100 large variables to a function by value means copying all of them. Passing their addresses instead is instant — just a handful of bytes regardless of the data size.

🔌

Hardware Register Access

A GPIO register lives at a fixed address like 0x40021014. The only way to read or write it in C is through a pointer to that exact address.

🧬

Dynamic Memory

When you don’t know how much memory you’ll need until runtime, malloc() returns a pointer to newly reserved heap memory.

🔗

Data Structures

Linked lists, trees, and graphs are built from nodes that point to other nodes. Without pointers, these structures couldn’t exist in C.

The Embedded Reality
Without pointers, embedded programming would be impossible. Every LED, sensor, and peripheral on a microcontroller is controlled by reading and writing specific memory addresses — and pointers are the only way C gives you direct access to those addresses.
2Foundations

Understanding Computer Memory

Think of RAM as a long street of houses. Every house has a unique address and contents inside it. Memory works exactly the same way: every byte has a unique address, and that address holds a value.

RAM — every "house" has an address and contents 1000 1004 1008 1012 10 20 35 90 addresses (top, in red) · values (bottom, in gray)

Every memory location has a fixed address and a value stored at that address. Variables are simply named addresses.

3Foundations

What is a Pointer?

A pointer is a variable that stores the memory address of another variable. Crucially: a pointer stores an address, not the value itself.

C — A pointer in action
int age = 25;        /* normal variable, holds a value      */
int *ptr = &age;     /* pointer, holds age's ADDRESS         */
Variable: age
Address0x1000
Value25
Pointer: ptr
Address0x2000
Value0x1000 (age’s address)
🔕
House Number Analogy
If your friend lives at house number 42, you don’t carry your friend around — you just remember “42”. A pointer remembers the location of data, not the data itself. To get to the data, you follow the address.
4Foundations

Pointer Declaration & Initialization

C — Declaring pointers of every type
int    *ptr;     /* pointer to int    */
char   *ch;      /* pointer to char   */
float  *fp;      /* pointer to float  */
double *dp;      /* pointer to double */

/* Correct initialization */
int x = 10;
int *p = &x;     /* p stores the address of x */

/* WRONG: dereferencing before initialization */
int *bad;
/* *bad = 10;  ← undefined behaviour! bad is uninitialized */
⚠️
Why the Data Type Matters
The compiler needs to know how many bytes to read at the address a pointer holds. int *ptr tells it to read 4 bytes; char *ptr tells it to read 1 byte. The pointer type determines how the pointed-to memory is interpreted — it does not change the size of the pointer itself (which is typically 4 or 8 bytes regardless of what it points to).
Always Initialize Before Use
An uninitialized pointer contains an indeterminate (garbage) address. Dereferencing it is undefined behaviour — it might crash immediately, corrupt unrelated memory, or appear to work by pure chance. Always initialize with a valid address or NULL.
5& and * Operators

Address-of Operator (&)

The & operator returns the memory address of a variable. It is the operator that "looks up" where a variable lives in RAM.

C — Printing an address
#include <stdio.h>

int main()
{
    int x = 50;

    printf("%p\n", (void *)&x);   /* always cast to (void*) for %p */

    return 0;
}
Possible output (varies every run)
0x7ffd83c1a8fc
⚠️
Always Cast to (void*) for %p
The %p format specifier expects a void* argument. Casting explicitly with (void *)&x avoids undefined behaviour and silences compiler warnings. Never use %d to print an address — addresses can exceed the range of int on 64-bit systems.
6& and * Operators

Dereference Operator (*)

The * operator accesses the value stored at the address a pointer holds. It is the operator that "follows" the address back to the actual data.

C — Dereferencing a pointer
#include <stdio.h>

int main()
{
    int x = 25;
    int *ptr = &x;

    printf("%d\n", *ptr);    /* 25 — value AT the address ptr holds */

    *ptr = 50;                /* modifies x through the pointer      */
    printf("%d\n", x);        /* 50 — original variable changed!     */

    return 0;
}
Output
25 50

Five Expressions, Five Meanings

ExpressionMeaningExample value
xValue of x30
&xAddress of x0x1000
ptrAddress stored in ptr (same as &x)0x1000
*ptrValue pointed to by ptr (same as x)30
&ptrAddress of the pointer variable itself0x2000
Modifying Through a Pointer Changes the Original
Because ptr and x refer to the same memory address, assigning through *ptr changes the actual value stored at that address — which is exactly the same memory that x refers to. This is the entire power (and danger) of pointers.
7& and * Operators

Pointer Memory Diagrams & Walkthrough

Let’s trace through a complete example step by step, watching exactly what happens in memory at each line.

C — Step-by-step pointer walkthrough
int num = 100;
int *ptr = &num;
Step 1 num = 100 addr 0x1000 Step 2 &num → 0x1000 Step 3 ptr = &num ptr stores 0x1000 Step 4 *ptr → 100 Final memory state: 0x1000 num 100 address name value 0x2000 ptr 0x1000 address name value (points to num)

Four steps from declaring a variable to reading its value back through a pointer. ptr lives at its own address and stores num’s address as its value.

Common Mistakes with & and *

✗ Confusing ptr with *ptr
int *ptr = &x; printf("%d", ptr); /* prints address, not value! */ /* probably meant *ptr */
✓ Use *ptr for the value
int *ptr = &x; printf("%d", *ptr); /* prints the value of x */
✗ Forgetting & when assigning
int x = 10; int *ptr; ptr = x; /* compiler error/warning: */ /* int assigned to int* */
✓ Use & to get the address
int x = 10; int *ptr; ptr = &x; /* correct: address assigned */
8Arithmetic & Arrays

Pointer Arithmetic

Pointers support a limited form of arithmetic — but the most misunderstood rule in all of C is this: pointer arithmetic is not performed in bytes. It is performed in units of the pointed-to data type.

C — ptr++ does NOT add 1 byte
int arr[3] = {10, 20, 30};
int *ptr = arr;            /* ptr points to arr[0], say at 0x1000 */

printf("%p\n", (void*)ptr);   /* 0x1000 */
ptr++;                          /* moves by sizeof(int) = 4 bytes! */
printf("%p\n", (void*)ptr);   /* 0x1004, NOT 0x1001 */
int array, sizeof(int)=4 — ptr+N jumps N×4 bytes
ptr
0x1000
→ arr[0] = 10
ptr+1
0x1004
→ arr[1] = 20 (+4 bytes)
ptr+2
0x1008
→ arr[2] = 30 (+8 bytes)

Operations Pointers Support

OperationExampleValid?Notes
Incrementptr++YesMoves to next element
Decrementptr--YesMoves to previous element
Add integerptr + 2YesMoves forward 2 elements
Subtract integerptr - 3YesMoves back 3 elements
Pointer differencep2 - p1Yes, if same arrayReturns number of elements between them
Multiplyptr * 2NoCompile error — meaningless
Divideptr / 3NoCompile error — meaningless
C — Pointer difference between array elements
int arr[10];
int *p1 = &arr[2];
int *p2 = &arr[7];

printf("%td", p2 - p1);   /* 5 — number of ELEMENTS, not bytes */
Pointer Subtraction Across Different Arrays is UB
Subtracting pointers that belong to two unrelated arrays (&a[2] - &b[1]) is undefined behaviour. Pointer arithmetic and subtraction are only well-defined within the same array (or one past its end).
9Arithmetic & Arrays

Pointers & Arrays

Pointers and arrays are deeply related in C. In most expressions, an array name represents the address of its first element.

C — Array name decays to a pointer
int numbers[5] = {10, 20, 30, 40, 50};

int *ptr = numbers;        /* equivalent to &numbers[0] */

printf("%p\n", (void*)numbers);       /* same address */
printf("%p\n", (void*)&numbers[0]);   /* same address */
/* numbers and &numbers print the same VALUE, but have different TYPES */
⚠️
Same Address Value, Different Types
numbers has type int* (after decay); &numbers has type int(*)[5] (pointer to the whole array). They print the same address, but numbers+1 moves by 4 bytes while &numbers+1 moves by 20 bytes (the entire array).

Array Indexing IS Pointer Arithmetic

The compiler internally treats numbers[i] as *(numbers+i). These two expressions are exactly equivalent — you can use them interchangeably.

Index notation
printf("%d", numbers[3]); /* Output: 40 */
Pointer notation (identical)
printf("%d", *(numbers+3)); /* Output: 40 */
Arrays Cannot Be Assigned
Unlike pointers, an array name is not a modifiable variable — you cannot write a = b; for two arrays. To copy array contents, copy element by element in a loop, or use memcpy().
10Arithmetic & Arrays

Array Name as Pointer & Traversal

C — Two ways to traverse an array
int numbers[5] = {10, 20, 30, 40, 50};

/* Method 1: index notation */
for(int i = 0; i < 5; i++)
    printf("%d ", numbers[i]);

/* Method 2: pointer notation — moves the pointer itself */
int *ptr = numbers;
for(int i = 0; i < 5; i++)
{
    printf("%d ", *ptr);
    ptr++;
}
/* Both print: 10 20 30 40 50 */

Array vs Pointer — What’s Really Different

PropertyArrayPointer
What it isFixed-size object that owns storageVariable that stores an address
Size decidedAt declaration (compile time)Can be reassigned anytime
Assignable?No (a = b is illegal)Yes
sizeofTotal array size in bytesSize of the pointer itself (4 or 8 bytes)
Owns memory?YesNo — just references it

Embedded Example: Memory-Mapped I/O

C — Reading and writing a hardware register through a pointer
/* GPIO register at a fixed hardware address */
volatile unsigned int *GPIO = (volatile unsigned int *)0x40021014;

unsigned int value = *GPIO;   /* read the register  */
*GPIO = 0x01;                  /* write to the register */
🔌
This Is the Foundation of Embedded Programming
Memory-mapped I/O means hardware peripherals appear as ordinary memory addresses. A pointer to that fixed address lets your C code read sensor values and control actuators directly — no special syntax needed beyond the pointer you already know.
11Pointers & Functions

Pointers as Function Parameters

Pointers become extremely powerful when combined with functions. Instead of copying data, we pass its address — letting the function modify the original variable.

✗ Without pointers — swap fails
void swap(int a, int b) { int t = a; a = b; b = t; } swap(x, y); /* x and y unchanged — copies were swapped */
✓ With pointers — swap works
void swap(int *a, int *b) { int t = *a; *a = *b; *b = t; } swap(&x, &y); /* x and y ARE swapped */
C — Complete swap program
#include <stdio.h>

void swap(int *a, int *b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main()
{
    int x = 10, y = 20;

    swap(&x, &y);

    printf("%d %d\n", x, y);   /* 20 10 */

    return 0;
}
Output
20 10
12Pointers & Functions

Returning Pointers

Functions can return pointers, but there’s a critical safety rule: never return the address of a local variable.

✓ Safe — address came from outside
int *getAddress(int *p) { return p; /* p points to caller's data */ } int value = 100; int *ptr = getAddress(&value); /* valid — value still exists */
✗ Dangerous — returns local address
int *wrong() { int x = 10; return &x; /* x destroyed when fn returns! */ } int *p = wrong(); /* *p is now undefined behaviour */
Function starts x created (stack) return &x address handed back Function ends x destroyed → DANGLING The returned pointer now points to memory that no longer holds a valid object.

A dangling pointer: the address is technically still “valid” as a number, but the data behind it is gone.

Safe Things to Return

  • Global variables — they live for the entire program
  • Static local variables — persist between calls (covered in Ch6)
  • Dynamically allocated memory (malloc) — lives until free()
  • Addresses that were passed into the function from the caller
13Pointers & Functions

Pointer to Pointer (Double Pointer)

Pointers themselves live in memory and have addresses. A pointer to pointer stores the address of another pointer — adding one more level of indirection.

C — Double pointer chain
int x = 10;
int *p = &x;        /* p points to x      */
int **pp = &p;       /* pp points to p     */

printf("%d\n", x);     /* 10 */
printf("%d\n", *p);    /* 10 — value at p's address (x) */
printf("%d\n", **pp);  /* 10 — value at pp's address's address */
x 10 p addr of x pp addr of p pp → p → x → 10 (two levels of indirection)

**pp dereferences twice: once to get to p, once more to get to x.

Why Use Double Pointers?

🧬

Dynamic memory

Functions that allocate memory for the caller need a ** parameter so the caller’s pointer itself can be updated.

🔗

Arrays of pointers

An array of strings (char *names[]) decays to char ** when passed to a function.

⚙️

argv in main()

Command-line arguments are received as char **argv — a pointer to an array of string pointers.

🧠

Linked structures

Modifying the head pointer of a linked list from inside a function requires **head.

14Types & Safety

const with Pointers

const can apply to the pointed-to data, the pointer itself, or both. This is a classic interview topic because the syntax is genuinely tricky — read right-to-left from the variable name to understand it.

C — Four const combinations
int a = 10, b = 20;

/* 1. Pointer to constant — value can't change, pointer can */
const int *ptr1 = &a;
ptr1 = &b;       /* OK — pointer can be reassigned   */
/* *ptr1 = 50;      ERROR — value is read-only via ptr1 */

/* 2. Constant pointer — pointer can't change, value can */
int *const ptr2 = &a;
*ptr2 = 99;       /* OK — value can be changed         */
/* ptr2 = &b;       ERROR — pointer itself is fixed     */

/* 3. Constant pointer to constant — neither can change */
const int *const ptr3 = &a;
/* *ptr3 = 1;       ERROR */
/* ptr3 = &b;       ERROR */

/* 4. Plain pointer — both can change (default) */
int *ptr4 = &a;
*ptr4 = 1;        /* OK */
ptr4 = &b;        /* OK */
DeclarationPointer reassignable?Value modifiable?
int *ptrYesYes
const int *ptrYesNo (read-only)
int *const ptrNo (fixed)Yes
const int *const ptrNoNo
💡
Reading Trick
Read the declaration right to left starting from the pointer name. const int *ptr reads: “ptr is a pointer to an int that is const” — the int is read-only. int *const ptr reads: “ptr is a const pointer to int” — the pointer is fixed.
15Types & Safety

Void Pointer

A void * is a generic pointer that can store the address of any object type. It is the basis of many generic C library functions, including malloc() itself.

C — void* must be cast before dereferencing
int x = 50;
void *ptr = &x;    /* void* can hold ANY pointer type */

/* WRONG — compiler has no idea how many bytes to read */
/* printf("%d", *ptr);   ← compile error               */

/* CORRECT — cast to the real type first, then dereference */
printf("%d\n", *(int *)ptr);   /* 50 */
⚠️
void* Cannot Be Dereferenced Directly
A void* has no associated data type, so the compiler doesn’t know how many bytes to read or how to interpret them. You must cast it to a concrete pointer type — (int *)ptr, (char *)ptr, etc. — before dereferencing.
🔨
Where void* Is Used
malloc() returns void* so it can allocate memory for any type. memcpy(), memset(), and qsort() all use void* parameters to work generically across data types.
16Types & Safety

NULL, Wild & Dangling Pointers

Three pointer states cause the majority of C crashes. Understanding the difference between them is essential for writing safe code.

NULL Pointer
int *ptr = NULL; /* Intentionally points to nothing. Safe — checkable. */ if(ptr != NULL) printf("%d", *ptr);
Wild Pointer
int *ptr; /* Never initialized. Contains garbage address. */ *ptr = 10; /* UB! */
Dangling Pointer
int *ptr = malloc(4); free(ptr); /* ptr still holds the OLD address — but memory is released. */ *ptr = 20; /* UB! */
PropertyWild PointerDangling Pointer
OriginNever initializedWas valid; memory then freed/destroyed
Address containsIndeterminate garbageAn address that was valid
FixAlways initialize: int *ptr = NULL;Set to NULL right after free()
C — Block-scope dangling pointer
int *ptr;

{
    int x = 10;
    ptr = &x;     /* ptr points to x, valid here */
}
/* x no longer exists — ptr is now dangling! */
/* *ptr = 5;  ← undefined behaviour */
The NULL-After-Free Habit
Always pair free(ptr); with ptr = NULL; immediately after. If you accidentally dereference it later, the program crashes predictably at that exact line (a NULL dereference) instead of silently corrupting memory somewhere else.
17Dynamic Memory

Why Dynamic Memory? Heap vs Stack

Every variable we’ve used so far has had a fixed size decided at compile time. But what if you don’t know how much memory you need until the program is running — say, the user enters how many students to store? That’s exactly what dynamic memory allocation solves.

Code Segment Global / Static Data Heap malloc / calloc / free grows upward ↑ free space Stack local vars · grows downward ↓

Heap grows up from low addresses; stack grows down from high addresses. The free space between them is where both can expand.

Stack vs Heap

PropertyStackHeap
AllocationAutomatic (compiler-managed)Manual (programmer-managed)
SpeedVery fastSlower
SizeLimited (often a few MB; KB on MCUs)Usually much larger
LifetimeFunction execution onlyUntil explicitly free()d
Who releases it?Automatic on function returnYou must call free()
⚠️
Why Not Just Use a Giant Array?
If you statically reserve room for 1000 students but only 50 show up, you waste memory. If you reserve room for 1000 but 5000 arrive, the program fails. Static allocation can’t adapt at runtime — dynamic memory can.
18Dynamic Memory

malloc()

malloc() (memory allocate) reserves a block of heap memory of the requested size and returns a void* pointing to it. The memory contents are not initialized — they contain garbage values.

C — malloc syntax and basic use
#include <stdlib.h>

void *malloc(size_t size);

/* Allocate one integer */
int *ptr = (int *)malloc(sizeof(int));

if(ptr == NULL)
{
    printf("Memory allocation failed!\n");
    return 1;
}

*ptr = 100;
printf("%d\n", *ptr);    /* 100 */

/* Allocate an array of 5 integers */
int *arr = malloc(5 * sizeof(int));
for(int i = 0; i < 5; i++)
    arr[i] = i * 10;       /* 0 10 20 30 40 */
Always Check for NULL
malloc() can fail if the heap is exhausted — especially likely on memory-constrained embedded systems. It returns NULL on failure. Dereferencing a NULL pointer without checking is a guaranteed crash.
19Dynamic Memory

calloc()

calloc() (clear allocate) allocates memory for multiple elements and initializes every byte to zero — the key difference from malloc().

C — calloc syntax and comparison
#include <stdlib.h>

void *calloc(size_t count, size_t size);

int *a = malloc(5 * sizeof(int));   /* indeterminate garbage values */
int *b = calloc(5, sizeof(int));    /* all 5 elements = 0           */

printf("%d\n", b[0]);   /* 0 — guaranteed by calloc */
Aspectmalloc()calloc()
Parametersmalloc(size) — one blockcalloc(count, size) — N elements
InitializationNot initialized (garbage)Zero-initialized
SpeedSlightly fasterSlightly slower (zeroing takes time)
Use whenYou will set every value yourselfYou need a clean zero-initialized buffer
20Dynamic Memory

realloc()

realloc() resizes an existing dynamic allocation. If possible, the same block is expanded in place; otherwise, a new block is allocated, the old data is copied over, and the old block is freed automatically.

C — Growing a dynamic array safely
#include <stdlib.h>

int *arr = malloc(5 * sizeof(int));

/* Unsafe — if realloc fails, you lose the original pointer (leak!) */
/* arr = realloc(arr, 10 * sizeof(int)); */

/* Safe pattern: use a temporary pointer */
int *temp = realloc(arr, 10 * sizeof(int));

if(temp != NULL)
{
    arr = temp;     /* success — update arr only now */
}
else
{
    /* arr is STILL VALID — realloc failure doesn't free the original */
    printf("Resize failed, original data intact\n");
}
⚠️
Never Overwrite the Original Pointer Directly
If realloc() fails, it returns NULL but leaves the original block untouched. Writing arr = realloc(arr, ...) directly loses your only reference to the original memory if the call fails — a memory leak. Always assign to a temporary pointer first.
21Dynamic Memory

free() & Memory Leaks

Every block allocated with malloc(), calloc(), or realloc() must eventually be released with free(), or that memory remains reserved for the rest of the program’s life — a memory leak.

C — Complete dynamic array example with proper cleanup
#include <stdio.h>
#include <stdlib.h>

int main()
{
    int n;
    printf("Enter size: ");
    scanf("%d", &n);

    int *arr = malloc(n * sizeof(int));
    if(arr == NULL)
        return 1;

    for(int i = 0; i < n; i++)
        scanf("%d", &arr[i]);

    for(int i = 0; i < n; i++)
        printf("%d ", arr[i]);

    free(arr);     /* release the memory */
    arr = NULL;    /* avoid dangling reference */

    return 0;
}
A Leak You Can’t See
A memory leak doesn’t crash immediately — it slowly consumes available memory until allocations start failing, often much later and far from the buggy line. In long-running embedded systems, a small leak triggered every loop iteration can exhaust RAM within hours.

Heap Fragmentation

Even with enough total free memory, repeated allocation and freeing can split the heap into small, non-contiguous free blocks. A large allocation request can fail even though the sum of free memory exceeds the requested size — this is why many embedded systems avoid dynamic allocation entirely, preferring static buffers or fixed-size memory pools.

🔌
Dynamic Memory in Embedded Systems
Heap fragmentation, allocation failure, and non-deterministic execution time make malloc()/free() risky on resource-constrained microcontrollers. Many real-time embedded systems allocate everything statically at compile time, or use fixed-size memory pools instead of a general-purpose heap.
22Advanced Pointers

Array of Pointers vs Pointer to Array

Two of the most confused declarations in C — they look almost identical but mean completely different things. Parentheses are everything here.

int *p[5]; — Array of Pointers
/* p is an ARRAY of 5 pointers */ int a=10, b=20, c=30; int *p[3]; p[0] = &a; p[1] = &b; p[2] = &c; printf("%d", *p[1]); /* 20 */
int (*p)[5]; — Pointer to Array
/* p is ONE pointer to a whole array */ int arr[5] = {10,20,30,40,50}; int (*p)[5] = &arr; printf("%d", (*p)[2]); /* 30 */ /* note the parentheses: (*p)[2] */
Aspectint *p[5] (Array of Pointers)int (*p)[5] (Pointer to Array)
What is p?An array of 5 separate pointersA single pointer to one 5-element array
Storage5 independent addresses1 address (the whole array’s)
Access syntax*p[i] or p[i](*p)[i] — parentheses required
Common useArray of strings, scattered dataPassing whole arrays/matrices to functions
⚠️
The Parentheses Change Everything
int *p[5] binds [5] to p first (an array), then applies * to each element. int (*p)[5] forces * to bind to p first (a single pointer), then says it points to a [5]-sized array. Drop the parentheses and the meaning flips entirely.
23Advanced Pointers

Function Pointers & Callbacks

Functions have addresses too. A function pointer stores the address of a function, letting you call it indirectly — and pass it around like any other value.

C — Declaring and using a function pointer
#include <stdio.h>

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

int main()
{
    /* Declaration: returns int, takes (int,int) */
    int (*operation)(int, int);

    operation = add;
    printf("%d\n", operation(5, 6));    /* 11 */
    printf("%d\n", (*operation)(5, 6)); /* 11 — also valid syntax */

    operation = sub;
    printf("%d\n", operation(10, 4));   /* 6  */

    return 0;
}

Callback Functions

A callback is a function passed as an argument to another function, which calls it back later. This is the basis of event-driven and interrupt-driven design.

C — Passing a function pointer as a callback
void execute(int (*operation)(int, int))
{
    printf("%d\n", operation(5, 10));
}

int main()
{
    execute(add);   /* pass add() as a callback — output: 15 */
    return 0;
}
🔌
Embedded Callback Pattern
Instead of hard-coding if(button_pressed) { LED_ON(); }, many embedded frameworks register a callback function for “button pressed” events. When the interrupt fires, the framework calls whatever function pointer was registered — letting application code stay decoupled from driver internals.
💡
Use typedef to Simplify Function Pointer Syntax
typedef int (*Operation)(int,int); lets you write Operation op = add; instead of repeating the full function pointer syntax everywhere. Function pointers are used heavily for callbacks, interrupt handlers, menu systems, state machines, and device drivers.
24Advanced Pointers

Command-Line Arguments

Every C program can receive input from the command line through two special parameters to main().

C — argc and argv
#include <stdio.h>

int main(int argc, char *argv[])
{
    printf("Argument count: %d\n", argc);

    for(int i = 0; i < argc; i++)
        printf("argv[%d] = %s\n", i, argv[i]);

    return 0;
}
Running: ./program hello world
Argument count: 3 argv[0] = ./program argv[1] = hello argv[2] = world
ParameterTypeMeaning
argcintArgument count — how many strings were passed, including the program name
argvchar *argv[] (= char **argv)Argument vector — an array of pointers to each argument string
25Advanced Pointers

Advanced Pointer Expressions

Expressions like *ptr++ trip up even experienced programmers because they combine dereference and increment, and precedence determines the order. Postfix ++ has higher precedence than *, but the increment still happens after the value is used.

ExpressionEquivalent toMeaning
*ptr++*(ptr++)Use current value, THEN move pointer forward
(*ptr)++(*ptr)++Increment the VALUE pointed to; pointer stays put
++*ptr++(*ptr)Increment the value first, then return new value
*++ptr*(++ptr)Move pointer forward FIRST, then dereference
C — *ptr++ traced step by step
int arr[] = {10, 20, 30};
int *ptr = arr;

printf("%d\n", *ptr++);   /* prints 10, THEN ptr moves to arr[1] */
printf("%d\n", *ptr);     /* prints 20 — ptr already advanced    */
Output
10 20
⚠️
Memorize the Pattern, Not Just the Rule
Postfix ptr++ always evaluates to the old value of ptr in the expression, then updates ptr as a side effect. Since * uses whatever value ptr++ evaluates to (the old address), *ptr++ always dereferences the current element before moving on.
26Review

Common Mistakes

Basics

✗ Printing address with %d
printf("%d", &x); /* Wrong specifier — UB on 64-bit */
✓ Cast to (void*), use %p
printf("%p", (void *)&x); /* Correct */
✗ Confusing ptr with *ptr
int *ptr = &x; printf("%d", ptr); /* prints address, not value */
✓ *ptr accesses the value
int *ptr = &x; printf("%d", *ptr); /* prints the actual value */

Arithmetic & Arrays

✗ Expecting ptr++ to add 1 byte
int *p = arr; p++; /* moves sizeof(int) bytes, not 1! */
✓ Understand it moves by element
int *p = arr; p++; /* advances to the NEXT int element */
✗ Dereferencing out-of-bounds pointer
int arr[5]; int *p = arr + 10; /* *p is UB — way past the array */
✓ Stay within array bounds
int arr[5]; int *p = arr + 4; /* last valid element — safe */

Declarations

✗ int *p[5] confused with int (*p)[5]
int *p[5]; /* array of 5 pointers */ /* NOT a single pointer to an array! */
✓ Parentheses change meaning entirely
int (*p)[5]; /* one pointer to a */ /* 5-element array */

Dynamic Memory

✗ Not checking malloc's return
int *p = malloc(100); *p = 5; /* crash if p is NULL */
✓ Always verify before use
int *p = malloc(100); if(p != NULL) *p = 5;
✗ Double free
free(ptr); free(ptr); /* UB — undefined behaviour */
✓ NULL after free prevents this
free(ptr); ptr = NULL; free(ptr); /* safe — freeing NULL is a no-op */
✗ Losing the pointer (memory leak)
int *ptr = malloc(sizeof(int)); ptr = NULL; /* original address is now lost forever */
✓ free before reassigning
int *ptr = malloc(sizeof(int)); free(ptr); ptr = NULL;
27Review

Best Practices & Embedded Applications

Always initialize pointers

Use NULL when you don’t have a valid address yet. Never leave a pointer declared without a value.

Check every allocation

malloc, calloc, and realloc can all fail. Always test the result against NULL before use.

NULL pointers after free

This converts a silent dangling-pointer bug into an immediate, debuggable NULL-dereference crash.

Use sizeof(*ptr), not sizeof(type)

malloc(n * sizeof(*ptr)) stays correct automatically even if you later change the pointer’s type.

Use const where data shouldn’t change

Mark read-only pointer parameters as const. The compiler then catches accidental modification for you.

Never return local addresses

Return dynamic memory, global/static variables, or addresses passed in by the caller — never &localVar.

Use typedef for function pointers

typedef int (*Op)(int,int); makes complex declarations far more readable than the raw syntax.

Prefer static allocation in embedded

On resource-constrained MCUs, fixed-size buffers and memory pools avoid fragmentation and non-deterministic allocation time.

Embedded Register Access Patterns

C — Three real peripheral register patterns
/* GPIO output register */
volatile unsigned int *GPIO = (volatile unsigned int *)0x40021014;
*GPIO |= (1 << 5);     /* set bit 5 — turn LED on */

/* UART data register — read incoming byte */
volatile unsigned char *UART = (volatile unsigned char *)0x40013804;
char data = *UART;

/* ADC data register — read sensor value */
volatile unsigned int *ADC = (volatile unsigned int *)0x4001244C;
unsigned int value = *ADC;

/* Common style: wrap in a macro for readability */
#define GPIO_ODR (*(volatile unsigned int *)0x40021014)
GPIO_ODR |= (1 << 5);
⚠️
Why volatile Matters Here
The volatile keyword tells the compiler that this memory location can change outside the program’s control (hardware updates it). Without volatile, the compiler might optimize away repeated reads of the register, assuming the value “can’t have changed” since it wasn’t written by your code — a subtle and dangerous embedded bug.
28Review

Pointer Cheat Sheet & Memory Tricks

ExpressionMeaning
ptrThe address stored in the pointer
*ptrThe value at that address
&ptrThe address of the pointer variable itself
&xThe address of variable x
ptr++ / ptr--Move to next / previous element (by element size)
*(ptr+i)Element at index i — same as ptr[i]
arr[i]Same as *(arr+i)
*ptr++Use current value, then move pointer
(*ptr)++Increment the value pointed to
p2 - p1Number of elements between two pointers (same array)
& vs *
&Address Of
*Value At

“& gives you the address, * gives you what’s there”

Complete Comparison Tables

ComparisonLeftRight
Variable vs PointerStores a value directlyStores an address
malloc vs callocNot zero-initializedZero-initialized
Pointer vs ArrayReassignable; doesn’t own storageFixed-size; owns storage; not assignable
Array of Pointers vs Pointer to Arrayint *p[5] — 5 pointersint (*p)[5] — 1 pointer to array
Wild vs Dangling PointerNever initializedWas valid; memory now released

Pointer Mind Map

Pointers Basics & Arith. Arrays Functions Memory & Safety & * ptr++ ptr+n arr[i]=*(arr+i) pointer traversal pass / return ptrs function pointers malloc calloc free NULL wild dangling Every advanced C topic — structs, linked lists, function callbacks — builds on these four pillars. Remember: A pointer is just a number (an address) with a type attached that tells the compiler how to interpret what’s there.

Four pillars of pointer mastery: basics & arithmetic, arrays, functions, and memory safety.

29Review

Interview Questions

Q1
What is a pointer? Why are pointers important in embedded systems?
A pointer is a variable that stores the memory address of another variable — not the value itself. In embedded systems, pointers are essential because hardware peripherals (GPIO, UART, ADC, timers) are accessed through fixed memory addresses called memory-mapped registers. Without pointers, there would be no way to read or write these registers directly in C.
Q2
What is the difference between ptr and *ptr?
ptr is the address stored in the pointer variable. *ptr dereferences that address to access the actual value stored there. Confusing the two is one of the most common beginner mistakes — printing ptr shows an address, while printing *ptr shows the data.
Q3
Why is pointer arithmetic scaled by the pointed-to type instead of bytes?
When you write ptr++ on an int*, the compiler advances the address by sizeof(int) (typically 4 bytes), not by 1 byte. This is intentional: it lets the pointer move cleanly from one array element to the next regardless of the element’s size, so ptr+i always lands exactly on element i.
Q4
Difference between malloc(), calloc(), and realloc()?
  • malloc(size) allocates one block of memory, contents uninitialized (garbage).
  • calloc(count, size) allocates memory for count elements and zero-initializes every byte.
  • realloc(ptr, newSize) resizes an existing allocation, preserving its content where possible.
Q5
What is the difference between a wild pointer and a dangling pointer?
A wild pointer has never been initialized — it holds an indeterminate, garbage address from the moment it’s declared. A dangling pointer once held a valid address, but the memory it points to has since been released (function returned, or free() was called). Both are dangerous to dereference, but the fixes differ: always initialize for wild pointers, and set to NULL after release for dangling pointers.
Q6
Why should you never return the address of a local variable?
A local variable lives in the function’s stack frame. When the function returns, that stack frame is popped and the memory is reclaimed — the variable no longer exists. The returned address still looks like a valid pointer (it’s just a number), but dereferencing it accesses memory that may now hold completely different data, causing undefined behaviour.
Q7
What is a function pointer? Give a practical use case.
A function pointer stores the address of a function, allowing it to be called indirectly or passed as an argument. Practical uses include: callback functions (passing a function to be called later), interrupt service routine dispatch tables, state machines (each state maps to a handler function), and generic algorithms like qsort() that accept a custom comparison function.
Q8
Why must volatile be used with memory-mapped hardware registers?
Without volatile, the compiler may assume a memory location’s value cannot change unless the program writes to it, and optimize away repeated reads. Hardware registers, however, can change asynchronously (a sensor updates a value, a UART receives a byte). volatile forces the compiler to read the actual memory every single time, preventing it from serving a stale cached value.

Frequently Asked Questions

Why doesn't Java or Python expose raw pointers like C?
Managed languages hide pointer arithmetic and memory addresses to eliminate entire categories of bugs (buffer overflows, dangling pointers, wild pointers) and provide automatic garbage collection. C exposes pointers directly because it’s designed for systems programming, where you need precise control over memory layout, hardware register access, and performance — trade-offs that managed languages deliberately give up.
Is arr the same as &arr?
They produce the same address value but have different types. arr decays to int* (pointer to the first element); &arr has type int(*)[N] (pointer to the whole array). This matters for pointer arithmetic: arr+1 moves by one element's size, while &arr+1 moves by the entire array's size.
Can I do pointer arithmetic on a void pointer?
Standard C does not allow arithmetic on void* because the compiler doesn't know the size of the pointed-to type to scale by. Some compilers offer this as a non-standard extension (treating it like char*), but portable code should cast to a concrete type, such as char*, before performing pointer arithmetic.
30Review

Practice Programs & Chapter Summary

🟢 Easy
  • Declare an int, char, and float pointer. Print the address each holds.
  • Print both the address and value of three different variables.
  • Swap two integers using pointers, and verify both variables changed in main().
  • Allocate memory for a single integer with malloc(), store a value, print it, then free it.
  • Declare a NULL pointer and write an if check before dereferencing it.
🔵 Medium
  • Traverse and print an array using only pointer arithmetic (no [] operator).
  • Reverse an array in-place using two pointers moving toward each other.
  • Read N integers into a dynamically allocated array (size from user input) using malloc(), then free it.
  • Demonstrate a pointer-to-pointer: declare int x, int *p, int **pp, and print x, *p, and **pp.
  • Resize a dynamic array from 5 to 10 elements using realloc(), using the safe temporary-pointer pattern.
🔴 Challenge
  • Without using [] anywhere: read 10 integers, store them in an array, and print them in reverse order using only pointer arithmetic.
  • Implement a simple calculator using an array of function pointers indexed by operator choice.
  • Write your own version of strlen() using only pointer traversal (no indexing, no library calls).
  • Predict and explain the output of: int x=10; int *p=&x; int **pp=&p; **pp=50; printf("%d",x); Walk through every memory change.
  • Implement matrix addition for two 3×3 matrices using only pointer arithmetic to access elements.
✅ What you mastered in Chapter 5
  • A pointer stores a memory address, not a value. & gets an address; * dereferences to get the value.
  • Pointer arithmetic is scaled by the pointed-to type — ptr+1 moves one element forward, not one byte.
  • An array name decays to a pointer to its first element in most expressions; arr[i]*(arr+i).
  • Passing a pointer to a function lets it modify the caller’s original variable — the basis of swap() and output parameters.
  • Never return the address of a local variable; it becomes a dangling pointer the moment the function returns.
  • A double pointer (int **) stores the address of another pointer — used in dynamic allocation, argv, and linked structures.
  • const int * makes the value read-only; int *const makes the pointer itself fixed.
  • void * is a generic pointer that must be cast to a concrete type before dereferencing.
  • Always initialize pointers (to a valid address or NULL); set to NULL immediately after free().
  • malloc() allocates uninitialized memory; calloc() zero-initializes; realloc() resizes; free() releases.
  • Always check allocation results for NULL before use; every malloc/calloc/realloc needs exactly one matching free.
  • int *p[5] is an array of pointers; int (*p)[5] is a single pointer to an entire array — parentheses change everything.
  • Function pointers store the address of a function and enable callbacks, dispatch tables, and state machines.
  • Memory-mapped hardware registers are accessed through volatile pointers — the foundation of embedded register-level programming.
📚 Chapter 6 — Structures, Unions, Enumerations & Typedef
Building custom composite data types for real-world modeling
Structures
Nested Structures
Arrays of Structures
Pointers to Structures
Structure Padding & Alignment
Unions
enum
typedef
Bit-fields
Embedded Examples