Mastering Pointers in C: 50 Advanced Interview Questions with Detailed Answers for Senior & Principal Embedded / Systems Software Engineers

Mastering Pointers in C: 50 Advanced Interview Questions with Detailed Answers for Senior & Principal Embedded / Systems Software Engineers

Prepared for Chintan Gala · Connectivity SWE (XR) pipeline · C / C++ · Embedded Firmware

How to Use This Handbook

This handbook covers pointers from fundamentals to the staff/principal layer across five chapters of ten questions each. Every question is written to be answered out loud in an interview: a precise explanation, a memory diagram or worked code where it helps, and — in the callout boxes — a 30–60 second spoken answer, the firmware relevance, common mistakes, best practices, and the follow-up questions an interviewer is likely to ask next.

How to practise: First pass: read each question and answer aloud. Second pass: cover the answer and reconstruct from the code alone. For every Embedded box, substitute a story from your own SATCOM / bring-up / driver work — specificity beats the generic version every time. Keep a running list of the follow-ups you stumble on; those are your highest-value review items before a phone screen.


Chapter 1 — Pointer Fundamentals

The fundamentals decide whether the rest of the interview goes well. Senior interviewers probe these to check that you reason about memory, not just syntax — so answer in terms of addresses, object lifetime, and representation, and reach for a memory diagram whenever you can.

Q1. What Is a Pointer?

A pointer is an object whose value is the address of another object (or function) in the program’s address space. It does not hold the pointed-to value; it holds where that value lives. The pointer also carries a type, which tells the compiler the size and interpretation of the object at that address.

int   num = 100;
int  *ptr = #

printf("%d
",  num);         /* 100              */
printf("%p
", (void*)ptr);   /* address of num   */
printf("%d
", *ptr);         /* 100 (deref)      */

/* Memory diagram:
   num  [ 100 ]  at addr 0x7ffc
   ptr  [ 0x7ffc ]  stores num's address */

INTERVIEW ANSWER (30–60s): A pointer stores the memory address of another object rather than the value itself. Its type tells the compiler how to interpret the bytes at that address, enabling dereferencing and pointer arithmetic. Pointers give us pass-by-reference, dynamic allocation, data structures, and direct hardware-register access.

EMBEDDED RELEVANCE: The most common pointer in firmware is a memory-mapped register: volatile uint32_t *GPIOA_ODR = (volatile uint32_t*)0x40020014u;

COMMON MISTAKES: Saying a pointer “is” the value (it is the address). Forgetting void* cannot be dereferenced. Printing with %d instead of %p.

Q2. Why Do We Need Pointers?

  • Pass-by-reference: C passes arguments by value, so modifying a caller’s object requires a pointer (swap, out-parameters, scanf).
  • Dynamic memory: malloc/free return a heap handle — an address.
  • Data structures: linked lists, trees, and graphs link nodes by address.
  • Hardware access: memory-mapped I/O is dereferencing a fixed address.
  • Efficiency: passing a pointer to a large struct copies 8 bytes instead of the whole object.
  • Callbacks / polymorphism: function pointers select behavior at run time (ISR tables, driver ops).

Q3. Difference Between a Variable, an Address, and a Pointer

A variable is a named object with storage; its address is where that storage sits; a pointer is a separate variable that stores an address. &num is an rvalue (computed, not stored); ptr = &num copies that address into a real variable that can be reseated.

Q4. How Are Pointers Stored in Memory?

int   x;
int  *p  = &x;   /* 8 bytes on 64-bit, 4 bytes on Cortex-M */
int **pp = &p;   /* pointer to pointer */

printf("%zu
", sizeof  p);  /* 8 — pointer width */
printf("%zu
", sizeof *p);  /* 4 — sizeof(int)   */

EMBEDDED RELEVANCE: On a 32-bit Cortex-M, every pointer is 4 bytes regardless of target type. This matters when sizing structs sent over the wire or packed into flash. Endianness follows the core (ARM little-endian by default).

Q5. NULL vs Wild vs Dangling vs Void Pointer

int *a = NULL;                /* null:     safe to test, never deref */
int *b;                       /* wild:     garbage — most dangerous  */
int *c = malloc(4); free(c);  /* dangling: valid then freed          */
void *g = &a;                 /* void*:    generic, cast before use  */

BEST PRACTICE: Initialize every pointer to NULL. Set to NULL immediately after free. Never return the address of a local variable.

EMBEDDED RELEVANCE: Without an MMU, address 0 may be real flash. NULL deref then silently reads valid bytes instead of trapping — far harder to debug.

LIKELY FOLLOW-UP: Is dereferencing NULL always a crash? No — on bare-metal MCUs it may silently corrupt logic.

Q6. Pointer Declaration Syntax

int  *p;            /* pointer to int                          */
int  *p, q;         /* p is int*, q is int  ← classic trap!   */
int **pp;           /* pointer to pointer to int               */
int  *arr[10];      /* array of 10 pointers to int             */
int (*parr)[10];    /* pointer to an array of 10 int           */
int (*fp)(int);     /* pointer to function taking int -> int  */

Q7. Pointer Initialization

int  x = 10;
int *p = &x;                 /* (1) address of an existing object */
int *q = malloc(sizeof *q);  /* (2) heap allocation (check NULL!) */
int *r = NULL;               /* (3) explicit null sentinel        */

if (q) { *q = 5; free(q); q = NULL; }  /* NULL after free */

Q8. sizeof(pointer) vs sizeof(*pointer)

double *p;
sizeof  p;    /* 8 — pointer's own width (platform address size) */
sizeof *p;    /* 8 — sizeof(double)                             */

char *c;
sizeof  c;    /* 8 — still a pointer                            */
sizeof *c;    /* 1 — sizeof(char)                               */

/* CORRECT allocation: */
T *ptr = malloc(n * sizeof *ptr);  /* self-correcting if type changes */

COMMON MISTAKE: malloc(sizeof(p)) allocates pointer-width bytes regardless of element type. sizeof(arr)/sizeof(arr[0]) only works where the array is declared — inside a function the array has decayed to a pointer.

Q9. Why Does Dereferencing NULL Crash?

On hosted systems, address 0 is in an unmapped guard page. The MMU raises a hardware fault, which the OS reports as SIGSEGV. On Cortex-M without an MPU configured to protect page 0, address 0 is the vector table — dereferencing NULL reads real data and silently corrupts program logic.

Q10. Pointer Alignment

uint8_t  buf[8];
uint32_t *p = (uint32_t*)(buf + 1);  /* misaligned — UB, may fault on ARM */
uint32_t  v = *p;                    /* UNDEFINED BEHAVIOR                 */

/* Safe: use memcpy for unaligned byte buffers */
uint32_t safe;
memcpy(&safe, buf + 1, sizeof safe);  /* always correct */

/* Runtime alignment check: */
assert(((uintptr_t)p % alignof(uint32_t)) == 0);

EMBEDDED RELEVANCE: Alignment bites in: casting uint8_t* packet buffers to structs, DMA descriptors requiring 32-byte alignment, and __attribute__((packed)) structs. Always use memcpy to extract values from unaligned wire-format buffers — never a raw cast.


Chapter 2 — Pointer Arithmetic

The single idea to internalize: arithmetic is in units of the pointed-to type, and it is only defined inside one array (plus one-past-the-end). Everything below follows from that.

Q11. Pointer Arithmetic Rules

C defines exactly four pointer-arithmetic operations, only within the bounds of a single array:

  • p + n and p - n: move by n elements (n × sizeof(*p) bytes).
  • p - q: distance in elements between two pointers into the same array; type is ptrdiff_t.
  • p == q, p < q, etc.: defined for pointers into the same array (or one-past-end).
  • A pointer to one-past-the-last element is valid to form and compare, but not to dereference. Going further is undefined behavior.
int a[5] = {10,20,30,40,50};
int *p = a;          /* &a[0]                      */
p += 3;              /* now &a[3], value 40         */
int *end = a + 5;    /* one-past-end: valid to form */
ptrdiff_t d = end - p;   /* 2 elements             */

Q12. Why Does ptr+1 Differ for int*, char*, double*?

/* +1 means "next element", not "next byte" */
/* The compiler multiplies by sizeof(*ptr)  */

char   *c;   c+1  /* +1 byte  */
int    *i;   i+1  /* +4 bytes */
double *d;   d+1  /* +8 bytes */

int    a[3]; int    *ip = a;
char   b[3]; char   *cp = b;
printf("%td
", (char*)(ip+1) - (char*)ip);  /* 4 */
printf("%td
", (char*)(cp+1) - (char*)cp);  /* 1 */

EMBEDDED RELEVANCE: Register maps exploit this: a volatile uint32_t* walked with +1 steps to the next 32-bit register, matching a peripheral’s word-addressed layout without manual byte math.

LIKELY FOLLOW-UP: How do you advance by raw bytes? Cast to char*/uint8_t*, add, cast back (respecting alignment).

Q13. Pointer Subtraction

int a[8];
int *p = &a[2], *q = &a[6];
ptrdiff_t n = q - p;                    /* 4 elements, not 16 bytes */
ptrdiff_t bytes = (char*)q - (char*)p;  /* 16 bytes                 */

COMMON MISTAKES: Storing result in int on 64-bit (distance can exceed INT_MAX). Expecting bytes — you get elements unless you cast to char* first. Print ptrdiff_t with %td, size_t with %zu.

Q14. Comparing Pointers

int a[5];
int *p = &a[1], *q = &a[4];
if (p < q)    { /* defined: same array */ }
if (p != NULL) { /* defined: equality  */ }

int x, y;
if (&x < &y) { /* UB: unrelated objects */ }

Q15. Array Name vs Pointer

int a[10];
int *p = a;            /* decay: p = &a[0]            */
sizeof a;              /* 40: whole array              */
sizeof p;              /* 8:  a pointer                */
/* a = p;   error: array is not assignable            */
p = a + 1;             /* fine: p is a real variable   */

EMBEDDED RELEVANCE: This decay is why you must pass a length alongside a buffer to a driver API — the callee only receives a pointer and cannot recover the array size.

Q16. &arr vs arr

int a[5];
printf("%p %p
", (void*)a, (void*)&a); /* same address! */

a   + 1;   /* int*       -> +4 bytes  (next element) */
&a  + 1;   /* int(*)[5]  -> +20 bytes (next whole array) */

INTERVIEW ANSWER: arr and &arr point at the same byte but have different types: arr is a pointer to the first int, &arr is a pointer to the entire array. arr+1 steps by one int; &arr+1 steps by the entire array.

Q17. Pointer Arithmetic on Multidimensional Arrays

/* int m[2][3]: row-major in memory            */
/* [m00 m01 m02 | m10 m11 m12]                 */
/* m      : int(*)[3]   m+1  -> +3 ints (next row) */
/* m[i]   : int*        m[i]+j -> &m[i][j]    */

int m[2][3] = {{1,2,3},{4,5,6}};
int *flat = &m[0][0];
flat[1*3 + 2];        /* m[1][2] == 6 */
*(*(m+1)+2);          /* m[1][2] == 6 */

Q18. Negative Indexing

int a[5] = {0,1,2,3,4};
int *p = &a[3];
p[-1];    /* a[2] == 2 : valid, still in bounds */
p[-3];    /* a[0] == 0 : valid                  */
/* a[-1] : UB, before the array                 */

/* Fun fact: a[i] == *(a+i) == *(i+a) == i[a]  */
/* So 3[a] is legal C (never good style though) */

Q19. Pointer Overflow

int a[4];
int *p = a + 4;     /* OK: one-past-end, valid to form  */
int *q = a + 5;     /* UB: beyond one-past-end          */
int *r = a - 1;     /* UB: before the array             */

COMMON MISTAKES: Loop guard for (p = a; p <= a + n; ++p) steps one element too far. Computing a - 1 as a sentinel. Static analyzers (Coverity, MISRA checkers) flag out-of-bounds pointer formation because it is silent in firmware.

Q20. Pointer Arithmetic Interview Problems

/* Predict the output */
int a[] = {1,2,3,4,5};
int *p = a + 2;
printf("%d %d
", *p, *(p-1));   /* 3 2 */
printf("%d
", *(a + (sizeof(a)/sizeof(a[0])) - 1)); /* 5 */

/* Reverse a buffer in place with two pointers */
void reverse(int *a, size_t n) {
    int *lo = a, *hi = a + n - 1;
    while (lo < hi) {
        int t = *lo; *lo++ = *hi; *hi-- = t;
    }
}

/* strlen by pointer subtraction */
size_t my_strlen(const char *s) {
    const char *p = s;
    while (*p) ++p;
    return (size_t)(p - s);
}

INTERVIEW TIP: When asked to “walk a buffer,” narrate invariants out loud — “lo and hi stay in bounds, they only cross once” — interviewers score the reasoning, not just the working code.


Chapter 3 — Arrays & Pointers

Arrays and pointers are deeply related but not the same type. Most confusion comes from array-to-pointer decay; once you can say exactly when it happens and what survives it, this whole chapter follows.

Q21. Is an Array a Pointer?

No. An array is a fixed-size block of contiguous elements with its own sizeof. A pointer is a separate object holding one address. They feel interchangeable only because an array name decays to a pointer to its first element in most expressions — but sizeof and &array reveal the difference.

/* int a[3]:   [ e0 ][ e1 ][ e2 ]   (12 bytes, no extra)  */
/* int *p:     [ addr ]             ( 8 bytes on 64-bit)  */

/* Three contexts where array does NOT decay:              */
/* sizeof a, &a, and _Alignof a                           */

COMMON MISTAKE: extern int a[]; and extern int *a; are NOT interchangeable across translation units — they generate different machine code and will corrupt access if mismatched.

Q22. Why Can’t an Array Be Assigned?

int a[3], b[3] = {1,2,3};
/* a = b;            error: array type is not assignable */
memcpy(a, b, sizeof a);   /* correct: copy element bytes */

BEST PRACTICE: Wrap an array in a struct if you genuinely want value semantics — structs (including those containing arrays) are assignable and copy by value.

Q23. char *str vs char str[]

char  a[] = "hi";   /* writable copy on the stack       */
char *p   = "hi";   /* points to read-only literal      */

a[0] = 'H';          /* OK                               */
/* p[0] = 'H';        UB: modifying a string literal     */

sizeof a;            /* 3 (incl. )                     */
sizeof p;            /* 8 (a pointer)                    */

BEST PRACTICE: Declare literal pointers as const char * so the compiler rejects accidental writes at compile time.

EMBEDDED RELEVANCE: On an MCU the literal sits in flash (.rodata); writing through a char* to it either faults or is silently ignored depending on the memory controller.

Q24. Pointer to an Array

int a[5] = {0,1,2,3,4};
int (*p)[5] = &a;     /* pointer to array of 5 int     */
(*p)[2];              /* a[2] == 2                     */
p + 1;                /* +20 bytes (one whole array)   */

int  *q   = a;        /* pointer to element            */
q + 1;                /* +4  bytes (one element)       */

Q25. Array of Pointers

const char *days[] = { "Mon", "Tue", "Wednesday" };
days[2];           /* -> "Wednesday" (own length)     */

/* days: [ p0 ][ p1 ][ p2 ]
            |     |     |
            v     v     v
         "Mon" "Tue" "Wednesday"  (separate blocks)   */

/* Ragged 2-D via array of row pointers: */
int *grid[3];
for (int i = 0; i < 3; ++i)
    grid[i] = malloc(cols[i] * sizeof(int));

EMBEDDED RELEVANCE: Driver op tables and command dispatch are arrays of function pointers — the same idea, where each slot points to a handler rather than a buffer.

Q26. Pointer to a Multidimensional Array

int m[2][3] = {{1,2,3},{4,5,6}};
int (*p)[3] = m;      /* m decays to int(*)[3]   */
p[1][2];              /* 6                       */

void print2d(int (*a)[3], int rows) {
    for (int i=0;i<rows;i++)
        for (int j=0;j<3;j++)
            printf("%d ", a[i][j]);
}

Q27. Passing Arrays to Functions

void f(int a[10]);   /* EXACTLY the same as: */
void f(int *a);      /* the [10] is ignored  */

/* CORRECT: always pass length explicitly */
void sum(const int *a, size_t n) {
    long s = 0;
    for (size_t i = 0; i < n; i++) s += a[i];
}

EMBEDDED RELEVANCE: This is exactly the (uint8_t *buf, size_t len) signature pattern in every UART/SPI/I2C driver. The length must travel with the pointer.

Q28. sizeof(arr) Inside a Function

void f(int arr[]) {
    printf("%zu
", sizeof arr);   /* 8: pointer! NOT array size */
}
int main(void) {
    int a[10];
    printf("%zu
", sizeof a);     /* 40: array   */
    f(a);
    /* WRONG: len = sizeof(arr)/sizeof(arr[0]) inside f = 8/4 = 2 */
}

INTERVIEW TIP: GCC’s -Wsizeof-pointer-div warns about exactly this mistake — mentioning it shows depth.

Q29. Flexible Array Member

typedef struct {
    size_t  len;
    uint8_t data[];      /* C99 flexible array member */
} packet_t;

/* Allocate header + n bytes in ONE allocation */
packet_t *p = malloc(sizeof *p + n);
p->len = n;
memcpy(p->data, src, n);
free(p);                              /* single free */

EMBEDDED RELEVANCE: This is the standard shape for network/protocol frames and IPC messages in firmware: a header struct plus a trailing payload, allocated and DMA’d as one contiguous block.

COMMON MISTAKE: Forgetting to add payload size in malloc. Using old “struct hack” (data[1]) — the C99 data[] form is correct and well-defined.

Q30. Dynamic Arrays

typedef struct { int *a; size_t len, cap; } vec_t;

int vec_push(vec_t *v, int x) {
    if (v->len == v->cap) {
        size_t nc = v->cap ? v->cap * 2 : 4;
        int *t = realloc(v->a, nc * sizeof *t);
        if (!t) return -1;        /* old block still valid */
        v->a = t; v->cap = nc;
    }
    v->a[v->len++] = x;
    return 0;
}

CRITICAL MISTAKE: v->a = realloc(v->a, ...) directly — on failure you overwrite the only pointer with NULL and leak the block. Always capture realloc’s result in a temporary.

EMBEDDED RELEVANCE: On MCUs, heap growth and fragmentation are real risks. Many firmware codebases pre-size or use fixed pools instead. Be ready to discuss that trade-off.


Chapter 4 — Function Pointers

Function pointers are where pointers become behavior, not just data — and they map directly onto firmware patterns interviewers love: vector tables, driver op-structs, dispatch tables, callbacks. Get the syntax fluent, then anchor every answer in one of those real uses.

Q31. What Are Function Pointers?

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

int (*op)(int, int) = add;   /* &add and add are equivalent */
int r = op(2, 3);            /* calls add -> 5             */
r = (*op)(2, 3);             /* identical: explicit deref  */

INTERVIEW ANSWER: A function pointer holds the address of a function’s code, so calling through it dispatches to whichever function it currently points at. It is the mechanism for run-time selection of behavior in C — callbacks, plug-in handlers, state machines, and the C equivalent of virtual methods.

EMBEDDED RELEVANCE: The interrupt vector table is literally an array of function pointers in flash; the CPU loads the handler’s address from it on each exception. Driver “classes” in C are structs of function pointers (ops tables).

Q32. Function Pointer Syntax Explained

int (*p)(int);        /* p: pointer to fn(int) -> int       */
int *q(int);          /* q: fn(int) -> int*  (NOT a ptr!)   */

typedef int (*binop_t)(int, int);
binop_t op = add;     /* clean, readable                    */

void (*table[4])(void);           /* array of 4 fn pointers  */
int  (*(*pf)(void))(int);         /* fn->ptr to fn(int)->int */

BEST PRACTICE: Always typedef function-pointer types used in APIs; it makes ops-struct members and callback parameters self-documenting.

Q33. Callback Functions

typedef void (*evt_cb)(int event, void *ctx);

void register_handler(evt_cb cb, void *ctx);  /* library side */

static void on_rx(int ev, void *ctx) {
    my_state_t *s = ctx;          /* recover typed context */
    s->count++;
}
register_handler(on_rx, &my_state);

INTERVIEW ANSWER: A callback is a function pointer handed to lower-level code so it can call back into the caller’s logic when an event occurs. A robust callback signature includes an opaque void* context pointer so the handler can access its own state without relying on globals.

COMMON MISTAKES: Capturing state in globals instead of void *ctx — breaks reentrancy and multi-instance use. Doing heavy work inside an ISR callback rather than deferring to a task.

Q34. State Machine Using Function Pointers

typedef struct fsm fsm_t;
typedef void (*state_fn)(fsm_t *);
struct fsm { state_fn state; /* ...context... */ };

static void s_idle(fsm_t *m);
static void s_run(fsm_t  *m);

static void s_idle(fsm_t *m) { if (start) m->state = s_run; }
static void s_run (fsm_t *m) { if (done)  m->state = s_idle; }

void fsm_tick(fsm_t *m) { m->state(m); }   /* dispatch */

INTERVIEW ANSWER: I model the current state as a function pointer to that state’s handler; ticking the machine just calls through the pointer, and each handler sets the next state. It replaces a sprawling switch with direct dispatch, isolates each state for unit testing, and makes transitions explicit.

EMBEDDED RELEVANCE: Protocol stacks and device bring-up sequences (link training, power states) are natural fits — each state is a small handler, and the table of states doubles as documentation.

Q35. ISR Vector Table

typedef void (*isr_t)(void);

__attribute__((section(".isr_vector")))
const isr_t vectors[] = {
    (isr_t)&_estack,   /* [0] initial MSP        */
    Reset_Handler,     /* [1] reset              */
    NMI_Handler,       /* [2]                    */
    HardFault_Handler, /* [3]                    */
    /* ... peripheral IRQs ... */
};

INTERVIEW ANSWER: The vector table is an array of function pointers stored at a fixed location — on Cortex-M, the base of flash — where each slot is the address of an exception or IRQ handler. On an interrupt the core reads the corresponding entry and branches to it. Slot 0 is special: it holds the initial stack pointer, not a handler.

LIKELY FOLLOW-UPS: Unused vectors? Weak aliases to a default infinite-loop handler. Why move table to RAM? To patch vectors at run time or support bootloader/app handoff via VTOR.

Q36. Jump Tables

typedef int (*cmd_fn)(const uint8_t *args, size_t n);

static const cmd_fn dispatch[CMD_COUNT] = {
    [CMD_READ]  = cmd_read,
    [CMD_WRITE] = cmd_write,
    [CMD_PING]  = cmd_ping,
};

int handle(uint8_t id, const uint8_t *a, size_t n) {
    if (id >= CMD_COUNT || !dispatch[id]) return -EINVAL;
    return dispatch[id](a, n);
}

INTERVIEW ANSWER: A jump table is an array of function pointers indexed by a command/opcode for O(1) dispatch — constant time regardless of command count, unlike a chain of comparisons. C99 designated initializers keep it readable.

CRITICAL MISTAKE: Indexing without a bounds check — an out-of-range opcode calls garbage (a classic security hole).

EMBEDDED RELEVANCE: AT-command parsers, SCPI handlers, and protocol message routers in firmware are usually jump tables.

Q37. qsort Comparator

int cmp_int(const void *a, const void *b) {
    int x = *(const int*)a, y = *(const int*)b;
    return (x > y) - (x < y);   /* no overflow, unlike x - y */
}

int v[] = {4,1,3,2};
qsort(v, 4, sizeof v[0], cmp_int);

COMMON MISTAKES: Returning x – y for ints — overflows for large/negative values. Use (x>y)-(x<y) instead. Mismatching the comparator signature — the cast from void* must match the real element type.

Q38. Driver Abstraction (Ops Struct)

typedef struct {
    int (*init) (void *ctx);
    int (*read) (void *ctx, uint8_t *buf, size_t n);
    int (*write)(void *ctx, const uint8_t *buf, size_t n);
} dev_ops_t;

typedef struct { const dev_ops_t *ops; void *ctx; } device_t;

static inline int dev_read(device_t *d, uint8_t *b, size_t n) {
    return d->ops->read(d->ctx, b, n);   /* virtual dispatch */
}

INTERVIEW ANSWER: I model a driver interface as a struct of function pointers — init/read/write — paired with an opaque context. Upper layers depend only on that interface and dispatch through the pointers, while each device fills in its own functions. It is the C version of a vtable, precisely how kernels expose uniform driver interfaces over diverse hardware.

LIKELY FOLLOW-UP: Keep the ops table in flash by making it const; only the per-instance context lives in RAM.

Q39. Bootloader Command Table

typedef int (*boot_cmd)(const uint8_t *p, size_t n);

static const struct { uint8_t id; boot_cmd fn; } cmds[] = {
    { 0x01, cmd_get_version },
    { 0x21, cmd_write_flash },
    { 0x43, cmd_erase },
    { 0x92, cmd_go },
};

int dispatch(uint8_t id, const uint8_t *p, size_t n) {
    for (size_t i=0; i<sizeof cmds/sizeof cmds[0]; i++)
        if (cmds[i].id == id) return cmds[i].fn(p, n);
    return -1;
}

CRITICAL SECURITY NOTE: Because it controls flash, the dispatcher must validate the command id, declared length vs received bytes, and target address range before calling. An unvalidated jump is a remote-code-execution risk. No authentication on flash-write/erase/go commands is catastrophic.

Q40. Generic apply / map (Live Coding Question)

/* Transform each int in place */
void apply(int *a, size_t n, int (*op)(int)) {
    for (size_t i = 0; i < n; ++i) a[i] = op(a[i]);
}

static int square(int x) { return x * x; }
apply(v, n, square);

/* Fully generic version with context */
void for_each(void *base, size_t n, size_t sz,
              void (*fn)(void *elem, void *ctx), void *ctx) {
    char *p = base;
    for (size_t i = 0; i < n; ++i) fn(p + i*sz, ctx);
}

INTERVIEW TIP: If asked to generalize, reach for the void* + size + callback + ctx signature unprompted — it shows you recognize the standard-library idiom (same shape as qsort/bsearch) rather than reinventing it.

LIKELY FOLLOW-UP: How would you add early termination? Have the callback return a status and stop the loop on non-zero.


Chapter 5 — Advanced Pointer Topics

This is the staff/principal layer: qualifiers that change what the compiler may assume (const, volatile, restrict), the aliasing rules that govern optimization, and the memory-mapped-I/O patterns that define embedded connectivity work. Strong answers here separate “writes C” from “understands the machine and the optimizer.”

Q41. Double Pointers

/* Allocate into the caller's pointer */
int make(int **out) {
    int *p = malloc(sizeof *p);
    if (!p) return -1;
    *p = 42; *out = p;        /* reseat caller's pointer */
    return 0;
}

/* Delete from a list without head/special cases */
void remove_if(node_t **pp, bool (*pred)(node_t*)) {
    while (*pp) {
        node_t *cur = *pp;
        if (pred(cur)) { *pp = cur->next; free(cur); }
        else pp = &cur->next;
    }
}

INTERVIEW ANSWER: A double pointer lets a function change the caller’s pointer, not just what it points to — essential for out-parameters that allocate, and for linked-list edits where you hold the address of the link to update. The list-deletion idiom of walking a pointer-to-pointer removes the special case for the head node entirely.

LIKELY FOLLOW-UP: Why does void alloc(int *p) fail to allocate for the caller? It updates a local copy of the pointer; the caller never sees it. You need int **.

Q42. Triple Pointers

/* Grow a ragged 2-D array: must modify caller's int** */
int add_row(int ***grid, size_t *rows, size_t cols) {
    int **t = realloc(*grid, (*rows + 1) * sizeof *grid);
    if (!t) return -1;
    t[*rows] = calloc(cols, sizeof t[*rows][0]);
    *grid = t; (*rows)++;
    return 0;
}

BEST PRACTICE: If you reach three stars, wrap the structure in a typedef’d struct (e.g. matrix_t holding int **data; size_t rows, cols;) and pass a pointer to that instead. It is clearer and self-documenting.

Q43. Void Pointers

void *p = malloc(n);          /* any type fits */
int  *ip = p;                 /* implicit in C */

void store(void *dst, const void *src, size_t n) {
    memcpy(dst, src, n);      /* byte-wise, type-agnostic */
}

INTERVIEW ANSWER: void* is a typeless generic pointer that can address any object — how the standard library writes type-agnostic routines like memcpy and qsort. You cannot dereference or do arithmetic without casting. In C, conversions to/from void* are implicit; casting malloc’s result is therefore unnecessary and historically masked missing prototypes.

COMMON MISTAKE: Doing arithmetic on void* — not standard (GCC extension treats element size as 1).

Q44. The restrict Keyword

void add(int *restrict d, const int *restrict a,
         const int *restrict b, size_t n) {
    for (size_t i = 0; i < n; ++i) d[i] = a[i] + b[i];
}
/* compiler may assume d does not overlap a or b,
   enabling vectorization and fewer reloads.           */

INTERVIEW ANSWER: restrict tells the compiler that, while the pointer is live, the data it references is reached only through that pointer — no other pointer aliases it. With that guarantee the optimizer can cache values in registers and vectorize. It is a contract: if the pointers actually alias, the behavior is undefined. memcpy is restrict; memmove is not.

EMBEDDED RELEVANCE: In DSP and packet-processing loops, marking input/output buffers restrict is a real, measurable speedup — a good performance answer for a low-power connectivity role where cycles equal energy.

Q45. const with Pointers — All Combinations

int x = 1, y = 2;

int       *p;               /* mutable ptr, mutable data       */
const int *p1 = &x;         /* data const:  *p1 = 9 illegal    */
int *const p2 = &x;         /* ptr const:   p2 = &y illegal    */
const int *const p3 = &x;   /* both const                      */

p1 = &y;     /* OK: pointer rebindable                         */
*p2 = 9;     /* OK: data writable                              */

/* Read right-to-left rule:
   const int *p    -> "pointer to const int"  (data fixed)
   int *const p    -> "const pointer to int"  (target fixed)
   const int*const -> both fixed                               */

EMBEDDED RELEVANCE: Marking lookup tables and register-description structs const places them in flash (.rodata) instead of RAM — a concrete win on memory-constrained parts.

BEST PRACTICE: Use const T * for read-only buffer parameters. It documents intent and lets callers pass const data.

Q46. Volatile Pointers

volatile uint32_t *reg = (volatile uint32_t*)0x40020000u;
while (!(*reg & STATUS_RDY)) { }   /* re-reads every time   */

/* shared with ISR */
static volatile bool data_ready;
while (!data_ready) { }            /* not optimized away    */

INTERVIEW ANSWER: volatile forces the compiler to issue each read and write to the object literally, because its value can change outside normal program flow — a peripheral register, an ISR-updated flag, or shared memory. Without it the compiler may hoist the read out of a loop and spin forever on a stale cached value. It guarantees non-elision, but not atomicity and not multi-core memory visibility.

COMMON MISTAKE: Assuming volatile makes accesses atomic or provides memory barriers — it does neither. volatile is necessary but not sufficient for concurrency.

LIKELY FOLLOW-UP: volatile vs _Atomic? volatile controls elision/ordering of accesses to one location; _Atomic provides indivisible operations and a memory model for concurrency.

Q47. Memory-Mapped Registers

typedef struct {
    volatile uint32_t CR;     /* 0x00 control  */
    volatile uint32_t SR;     /* 0x04 status   */
    volatile uint32_t DR;     /* 0x08 data     */
} uart_regs_t;

#define UART1 ((uart_regs_t*)0x40011000u)

UART1->CR |= UART_EN;
while (!(UART1->SR & TXE)) { }
UART1->DR = byte;

INTERVIEW ANSWER: I describe the peripheral as a volatile-qualified register struct overlaid at its base address, so field accesses become precise, correctly-sized, non-cached loads and stores to hardware. The struct layout must match the datasheet exactly — offsets, widths, and reserved gaps — and volatile guarantees the compiler does not reorder or drop the accesses the hardware depends on.

COMMON MISTAKES: Wrong field width or missing reserved gap shifts every subsequent register. Read-modify-write on a register an ISR also touches, without protection — lost updates.

Q48. Pointer Aliasing

/* Strict-aliasing violation: reading a float as int */
float f = 3.14f;
int  *ip = (int*)&f;     /* UB to deref as int          */

/* Correct type-pun: */
int bits;
memcpy(&bits, &f, sizeof bits);  /* well-defined           */

/* Union type-pun (also valid in C, not C++): */
union { float f; int i; } u = { .f = 3.14f };
int v = u.i;  /* defined in C99/C11                      */

INTERVIEW ANSWER: Aliasing is when two pointers can reach the same memory. The optimizer assumes worst-case aliasing unless restrict or the type rules say otherwise. Strict aliasing lets it assume pointers of incompatible types don’t overlap — so reinterpreting a float through an int* is undefined behavior, the cause of bugs that only appear with optimization on. The safe way to type-pun is memcpy or a union.

KEY INSIGHT: char*/unsigned char* may legally alias anything — useful for byte-level inspection. Know -fno-strict-aliasing exists, but treat it as a workaround, not a fix.

LIKELY FOLLOW-UP: Why might code work at -O0 but fail at -O2? Higher optimization actually relies on strict aliasing; the latent UB only then changes behavior.

Q49. Common Pointer Bugs — Complete Taxonomy

  • Wild pointer (uninitialized): initialize to NULL; enable -Wmaybe-uninitialized.
  • NULL dereference: check allocation results and parameters; MPU guard on page 0 for MCUs.
  • Use-after-free / dangling: NULL the pointer after free; tools: ASan, Valgrind.
  • Double free: single ownership discipline; NULL after free.
  • Buffer overrun / off-by-one: carry explicit lengths; use one-past-end bounds; ASan/MISRA.
  • Memory leak: match every malloc with a free; track ownership.
  • Returning address of a local: stack frame gone on return; -Wreturn-local-addr catches it.
  • Strict-aliasing / misaligned cast: use memcpy; respect alignment.
  • Pointer/array length lost across call: always pass length with the buffer.

INTERVIEW ANSWER: I group them: lifetime bugs (use-after-free, dangling, returning local’s address), initialization bugs (wild, NULL deref), bounds bugs (overruns, off-by-one), ownership bugs (leaks, double-free), and type bugs (aliasing, misalignment). My defenses are: initialize to NULL and NULL after free, carry explicit lengths, single clear ownership, and run AddressSanitizer/Valgrind plus static analysis in CI.

EMBEDDED RELEVANCE: On targets without ASan: MPU guard regions, canary/stack-painting, heap integrity checks, and static analyzers (Coverity, MISRA). Mentioning the bare-metal toolset signals real firmware experience.

INTERVIEW TIP: Pair each bug with a prevention AND a detection tool: “Initialize to NULL (prevent), AddressSanitizer in CI (detect)” — that’s the answer shape interviewers reward.

Q50. Embedded Pointer Challenge Questions

A grab-bag of the harder embedded pointer prompts commonly seen in connectivity/firmware interviews:

/* 1. Set, clear, and toggle bit 5 of register at 0x40021000 */
volatile uint32_t *R = (volatile uint32_t*)0x40021000u;
*R |=  (1u << 5);   /* set    */
*R &= ~(1u << 5);   /* clear  */
*R ^=  (1u << 5);   /* toggle */

/* 2. Why does the optimizer break a non-volatile poll loop?
   Without volatile, the compiler reads SR once, caches it
   in a register, and spins on the stale copy forever.
   volatile forces a fresh read each iteration.             */

/* 3. Safely parse a 4-byte big-endian field from unaligned buffer */
uint32_t be32(const uint8_t *p) {   /* no cast/deref of u32* */
    return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16)
         | ((uint32_t)p[2] <<  8) |  (uint32_t)p[3];
}

INTERVIEW ANSWER: The recurring themes are: use volatile for MMIO and ISR-shared state; do bit ops with set/clear/toggle masks; never cast an unaligned byte buffer to a wider type — assemble the value byte-by-byte or memcpy; and always pair buffers with lengths. Each of these is a place where ignoring the rule appears to work in testing and fails in the field.

EMBEDDED RELEVANCE: These are exactly the micro-skills a connectivity/bring-up role exercises daily — register pokes, endian-safe parsing of 802.11/BT frames, and ISR-safe shared state. Answer them crisply and you signal hands-on firmware fluency, not just textbook C.

LIKELY FOLLOW-UP: When is read-modify-write on a register unsafe? When an ISR or another core can touch the same register between the read and the write; guard it or use a bit-band/atomic set-clear register if the hardware provides one.


This handbook is part of the Senior & Principal Embedded / Systems Software Engineer interview preparation series. Companion editions covering C++, Embedded & Firmware, Connectivity & Wireless, Debugging, System Design, and Behavioral topics are available separately.

Leave a Reply

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