C Pointers Interview Questions and Answers (40 Questions with Code Examples)
Pointers are the single most reliable way for an interviewer to separate someone who has memorised C syntax from someone who understands how a program sits in memory. This guide collects 40 C pointers interview questions for embedded and firmware roles. Each answer is short, shows code you can compile, and calls out the mistake interviewers usually probe for. Examples assume a standard C compiler such as GCC or Clang; where behaviour depends on the target (word size, alignment, endianness) the answer says so.
Topics Covered
- Pointer Basics (Q1–Q8)
- Arrays and Pointer Arithmetic (Q9–Q15)
- Pointers and Functions (Q16–Q21)
- const, volatile and restrict (Q22–Q26)
- Dynamic Memory (Q27–Q32)
- Pointers in Embedded Code (Q33–Q38)
- Tricky Cases and Undefined Behaviour (Q39–Q40)
A pointer is a variable whose value is the address of another object or function. The pointer's type tells the compiler how many bytes to read when you dereference it and how far to move when you do arithmetic on it.
int x = 10;
int *p = &x; /* p holds the address of x */
printf("%d\n", *p); /* prints 10 */
*p = 20; /* x is now 20 */
p + 1 move by sizeof(*p) bytes.It is implementation-defined. It is typically 4 bytes on 32-bit targets such as Cortex-M and 8 bytes on 64-bit targets such as AArch64 or x86-64. All object pointers on a given platform usually have the same size, but the C standard does not guarantee that a function pointer is the same size as a data pointer. Always use sizeof instead of assuming.
printf("%zu %zu\n", sizeof(char *), sizeof(double *)); /* equal on common platforms */
A null pointer is a pointer guaranteed not to point to any valid object. Dereferencing it is undefined behaviour. On a hosted OS you usually get a segmentation fault. On a bare-metal MCU there may be no MMU, and address 0 can be valid memory (for example the vector table), so the bug can silently read or corrupt data instead of crashing.
int *p = NULL;
if (p != NULL) {
*p = 1; /* guarded */
}
0 or NULL in source code, but the machine representation does not have to be all-zero bits. Comparing against NULL is always correct.A wild pointer is declared but never initialised, so it holds an indeterminate value. A dangling pointer was valid once but the object it pointed to no longer exists, for example after free() or when a function returns.
int *wild; /* uninitialised: wild */ int *d = malloc(sizeof *d); free(d); /* d is now dangling */ d = NULL; /* defensive habit */
void * is a generic object pointer. Any object pointer converts to it and back without a cast in C. You cannot dereference it or (in standard C) do arithmetic on it, because the size of the target is unknown. GCC allows arithmetic on void * as an extension, but it is not portable.
void swap_bytes(void *a, void *b, size_t n)
{
unsigned char *x = a, *y = b, t;
while (n--) { t = *x; *x++ = *y; *y++ = t; }
}
memcpy, qsort comparators, and RTOS APIs that pass a context pointer to a task or callback.*p++, (*p)++ and *++p?| Expression | Meaning |
|---|---|
*p++ | Read *p, then advance the pointer (postfix binds tighter than dereference) |
(*p)++ | Increment the pointed-to value; pointer unchanged |
*++p | Advance the pointer first, then read |
int a[] = {10, 20, 30};
int *p = a;
int v = *p++; /* v = 10, p -> a[1] */
A T ** stores the address of a T *. The classic reason is letting a function change the caller's pointer, because C passes arguments by value.
int alloc_buf(uint8_t **out, size_t n)
{
*out = malloc(n);
return (*out != NULL) ? 0 : -1;
}
uint8_t *buf;
if (alloc_buf(&buf, 64) == 0) { /* use buf */ free(buf); }
Equality (==, !=) works for any two pointers of compatible type. Relational comparison (<, >) is only defined when both pointers point into the same array (or one past its end) or the same struct. Comparing unrelated pointers is undefined by the standard, even though it "works" on flat-memory targets.
An array is not a pointer, but in most expressions an array name decays to a pointer to its first element. a[i] is defined as *(a + i). The exceptions are sizeof, unary &, and string-literal initialisation of a char array.
int a[4] = {1, 2, 3, 4};
printf("%zu\n", sizeof a); /* 16 with 4-byte int */
int *p = a;
printf("%zu\n", sizeof p); /* pointer size, not 16 */
sizeof on an array parameter return the pointer size?A parameter declared as int a[10] is adjusted by the compiler to int *a. The length is lost, so pass it explicitly.
void f(int a[10]) { printf("%zu\n", sizeof a); } /* prints sizeof(int *) */
void g(const int *a, size_t n); /* correct pattern */
p + n advances by n * sizeof(*p) bytes. Subtracting two pointers into the same array yields a ptrdiff_t counted in elements, not bytes.
uint32_t buf[8]; uint32_t *a = &buf[1], *b = &buf[5]; ptrdiff_t n = b - a; /* 4 elements */ size_t bytes = (char *)b - (char *)a; /* 16 bytes */
int *p[5] and int (*p)[5]?Read declarations from the identifier outward. int *p[5] is an array of 5 pointers to int. int (*p)[5] is a pointer to an array of 5 ints.
int m[3][5]; int (*row)[5] = m; /* row[i][j] works; row + 1 skips 5 ints */
void f(int (*m)[5], size_t rows).char *s = "abc" and char s[] = "abc"?char s[] = "abc" creates a modifiable 4-byte array initialised from the literal. char *s = "abc" points at a string literal that typically lives in read-only memory; modifying it is undefined behaviour. On MCUs the literal is usually in flash, so a write may fault or be silently ignored.
char a[] = "abc"; a[0] = 'X'; /* OK */ char *p = "abc"; /* p[0] = 'X'; undefined behaviour */ const char *q = "abc"; /* preferred spelling */
a[i] the same as i[a]?Yes, because a[i] means *(a + i) and addition is commutative. It compiles, but never write it in real code.
size_t my_strlen(const char *s)
{
const char *p = s;
while (*p) p++;
return (size_t)(p - s);
}
Interviewers often follow up with "what if s is NULL?" and "what if the buffer is not terminated?". The standard strlen does not protect against either; bounded variants such as strnlen help in the second case.
A function pointer stores the address of a function so it can be called indirectly.
int add(int a, int b) { return a + b; }
int (*op)(int, int) = add; /* or &add */
int r = op(2, 3); /* 5 */
typedef int (*binop_t)(int, int);
binop_t table[] = { add };
Interrupt vector tables, driver ops tables (open, read, write style structs), state machines, callback registration, and bootloaders jumping to an application entry point.
typedef void (*isr_t)(void);
static isr_t vector[16];
void register_isr(unsigned n, isr_t fn) { if (n < 16) vector[n] = fn; }
The callee receives copies. Passing addresses lets it modify the originals.
void swap(int *a, int *b)
{
int t = *a; *a = *b; *b = t;
}
The local lives on the stack frame, which is gone after return, so the pointer dangles. Fixes: return a static object (not thread-safe), allocate with malloc, or have the caller pass a buffer.
char *bad(void) { char b[16] = "hi"; return b; } /* wrong */
int good(char *out, size_t n) { return snprintf(out, n, "hi"); }
Cast the integer address to a function pointer type. A bootloader on a Cortex-M typically reads the application's reset vector from its vector table and jumps to it after loading the stack pointer.
void (*app)(void) = (void (*)(void))0x08008000u; /* illustrative address */ app();
A callback is a function pointer handed to another module to be called later. Because C has no closures, APIs take a void *ctx alongside the function pointer.
typedef void (*done_cb)(int status, void *ctx); int start_transfer(const uint8_t *d, size_t n, done_cb cb, void *ctx);
const int *p, int *const p and const int *const p.| Declaration | Can change *p? | Can change p? |
|---|---|---|
const int *p | No | Yes |
int *const p | Yes | No |
const int *const p | No | No |
volatile mean on a pointer target?volatile tells the compiler that each access to the object is observable and must not be removed, merged or reordered with other volatile accesses. It is needed for memory-mapped registers and for data changed by an ISR or hardware. For more on why this matters in kernel code, see our article on why volatile is used in the Linux kernel and device drivers.
volatile uint32_t *const UART_SR = (volatile uint32_t *)0x40011000u;
while ((*UART_SR & 0x80u) == 0) { /* wait for TXE */ }
const and volatile?Yes. Your code may not write to it, but hardware can change it. A read-only status register is the standard example: const volatile uint32_t *STATUS.
restrict promise?C99 restrict is a promise from the programmer that, during the pointer's lifetime, the object it accesses is accessed only through that pointer (or pointers derived from it). It lets the compiler optimise more aggressively. Breaking the promise is undefined behaviour, which is why memcpy (restrict) is unsafe for overlapping buffers while memmove is safe.
void add_arrays(int *restrict dst, const int *restrict a, const int *restrict b, size_t n);
const?The cast itself is legal, but writing through the result is undefined behaviour if the original object was defined const. It is acceptable only when the object was never really const, for example to adapt to a legacy API that forgot a const in its signature and does not modify the data.
malloc, calloc and realloc differ?| Function | Behaviour |
|---|---|
malloc(n) | Allocates n bytes, contents uninitialised |
calloc(c, n) | Allocates c*n bytes, zero-filled, with overflow checking on the multiplication |
realloc(p, n) | Resizes a block; may move it; returns NULL on failure and leaves the old block valid |
realloc?void *tmp = realloc(buf, new_size);
if (tmp == NULL) {
/* buf is still valid: handle error, free(buf) if giving up */
} else {
buf = tmp;
}
Writing buf = realloc(buf, n) leaks the original block when allocation fails.
free twice on the same pointer?Undefined behaviour; typically heap corruption or an abort from the allocator. free(NULL) is explicitly safe, so setting the pointer to NULL after freeing turns an accidental second free into a no-op.
malloc?Fragmentation, non-deterministic allocation time, and unchecked failure paths are all hard to accept on long-running or safety-related systems. Common alternatives are static allocation, fixed-block memory pools, and allocating everything during initialisation. For the memory-region basics see our related guide Top 40 Embedded Systems Interview Questions.
On a host, use Valgrind (--leak-check=full) or AddressSanitizer/LeakSanitizer (-fsanitize=address). On a target, wrap the allocator to count live blocks, track high-water marks, and use the RTOS heap statistics (FreeRTOS exposes xPortGetFreeHeapSize() and xPortGetMinimumEverFreeHeapSize() with its heap implementations).
sizeof be used with the variable instead of the type in malloc?struct dev *d = malloc(sizeof *d); /* stays correct if the type changes */
No cast is required in C, and casting hides a missing #include <stdlib.h> on very old compilers.
#define GPIOA_ODR (*(volatile uint32_t *)0x40020014u) GPIOA_ODR |= (1u << 5);
A structure overlay is cleaner for a block of registers:
typedef struct {
volatile uint32_t CR;
volatile uint32_t SR;
volatile uint32_t DR;
} uart_regs_t;
#define UART1 ((uart_regs_t *)0x40011000u)
UART1->CR |= 1u;
ioremap, readl/writel) see our post on memory-mapped I/O vs port-mapped I/O.Padding and alignment must match the register layout (use fixed-width types and, if needed, explicit reserved fields and static asserts on offsets), bit-field ordering is implementation-defined, and each access must keep the correct width. Prefer shifts and masks over bit-fields for registers.
Reading a 32-bit value from an address that is not a multiple of 4. Cortex-M0/M0+ will raise a HardFault; Cortex-M3 and above permit it for most loads but not all instructions, and many other architectures either fault or are slow. Casting a uint8_t * into a packet buffer to uint32_t * is a common source of this bug.
uint32_t read_u32(const uint8_t *p)
{
uint32_t v;
memcpy(&v, p, sizeof v); /* safe and usually compiled to one load */
return v;
}
uint16_t x = 0x0001; int little = (*(uint8_t *)&x == 1);
Accessing any object through a char-type pointer is always allowed, which is why this works. Casting to a different non-character type would violate strict aliasing.
The compiler may assume that pointers of unrelated types do not refer to the same memory. Code such as *(uint32_t *)&some_float breaks that assumption, and optimised builds may produce surprising results. Use memcpy, a union (permitted in C), or compile with -fno-strict-aliasing as a last resort.
Use a single-producer, single-consumer ring buffer where the ISR only writes the head and the main loop only writes the tail, mark the indices volatile, and make sure index updates are atomic for the word size. If more than one writer exists, disable interrupts around the update or use atomics.
typedef struct {
uint8_t buf[64];
volatile uint8_t head, tail; /* power-of-two size: mask instead of modulo */
} ring_t;
static inline int ring_put(ring_t *r, uint8_t b)
{
uint8_t next = (r->head + 1) & 63;
if (next == r->tail) return -1; /* full */
r->buf[r->head] = b;
r->head = next;
return 0;
}
volatile: it is not a synchronisation primitive.struct node { int v; struct node *next; };
struct node *reverse(struct node *head)
{
struct node *prev = NULL;
while (head) {
struct node *nx = head->next;
head->next = prev;
prev = head;
head = nx;
}
return prev;
}
Walk through a three-node list on paper. Interviewers care that you save next before overwriting it and that you handle the empty list.
int a[] = {1, 2, 3, 4};
int *p = a;
printf("%d %d %d\n", *p++, *p++, *p); /* unspecified */
The order in which function arguments are evaluated is unspecified, and modifying p twice without a sequence point between the modifications is not well defined in this form. The correct answer is "do not write this". A safe version splits it into separate statements.
int x = *p++;
int y = *p++;
int z = *p;
printf("%d %d %d\n", x, y, z); /* 1 2 3 */
Practise Pointers the Way Interviewers Test Them
The Embedded System Interview Prep Bootcamp covers C, data structures, RTOS, Linux kernel and drivers, with a mock interview and resume review.
View Interview Prep Bootcamp →Frequently Asked Questions
Q: Why are pointers so important in embedded interviews?
Embedded code talks to hardware through addresses, passes buffers between interrupts and tasks, and runs without an OS safety net. Weak pointer knowledge shows up as bugs that are hard to debug on a target.
Q: How many pointer questions should I prepare?
Quality matters more than count. Be able to explain, with code, the topics in Q1, Q9, Q12, Q16, Q22, Q23, Q28, Q33, Q35 and Q39 without hesitation, then widen out.
Q: Is pointer arithmetic on void * allowed?
Not in standard C. GCC accepts it as an extension that treats the size as 1. Cast to char * or uint8_t * instead.
Q: Do I need to know C++ smart pointers for embedded C roles?
Not for C roles. For C++ roles it helps to know unique_ptr and how it avoids the leaks discussed in Q28 and Q29.
Q: Where should I practise next?
Compile the snippets here with -Wall -Wextra -fsanitize=address,undefined, then continue with our 50 must-know Embedded C interview questions.
