Articles
C Pointer Interview Questions: Arrays, Lifetimes, and Undefined Behavior
Practice C pointer interview questions on arrays, pointer arithmetic, malloc, object lifetime, and undefined behavior with worked examples.
A strong answer to a C pointer interview question starts with four checks: type, object, lifetime, and bounds. Before predicting an output, identify what the pointer points to, whether that object still exists, and whether the operation stays inside its valid range. If the code has undefined behavior, there is no portable output to calculate.
This guide uses original examples for C11/C17-style reasoning. It is useful for systems, embedded, and general software interviews. It separates language guarantees from common machine behavior so that you can explain why an answer is correct, rather than repeat what one compiler printed.
Key takeaways
- An array is storage for elements; a pointer is a separate object that holds a pointer value. Array-to-pointer conversion does not make them the same type.
- A one-past pointer can be formed, but it cannot be dereferenced.
- A non-null address does not prove the pointed-to object is still alive.
- Allocation, ownership, cleanup, and failure handling belong in the answer, not in an optional afterthought.
- Warnings and sanitizers help find mistakes. A clean run is not a proof that every execution is defined.
Is an array the same thing as a pointer in C?
No. Consider three integers and a pointer to the first element:
int values[3] = {10, 20, 30};
int *p = values;
Here, values is an array of three integers. In the initializer for p, it converts to a pointer to its first element. The pointer does not contain a copy of the array, and it does not carry the element count. Changing p can make it point somewhere else; assigning a new pointer value to the array name is not an array assignment operation.
| Expression | Meaning | Interview trap |
|---|---|---|
| sizeof values | Total size of the array | Not the size of a pointer |
| sizeof p | Size of the pointer object | Does not reveal the allocation length |
| values[1] | Second integer, 20 | Equivalent to *(values + 1) |
| &values | Pointer to the whole array | Type is int (*)[3], not int * |
For this non-VLA array, sizeof values / sizeof values[0] gives three. Inside a function declared with void inspect(int values[3]), however, the parameter is adjusted to pointer type. The bracketed three does not let the function recover the caller's array length. Pass a length explicitly and define whether a null pointer is allowed when that length is zero.
How does pointer arithmetic work?
Adding one to an int * advances by one integer element, not one byte. The portable rule is expressed in elements of an array, not in assumptions about numeric addresses. Use this example:
int a[4] = {3, 6, 9, 12};
int *p = a + 1;
int x = *p; /* 6 */
int y = *(p + 2); /* 12 */
int *end = a + 4; /* valid one-past pointer */
Forming end is permitted. Reading *end is not. Nor is it valid to form a - 1 just because you promise not to dereference it. Pointer subtraction is defined for positions in the same array, including one past, when the difference is representable as ptrdiff_t. Do not subtract unrelated allocations to estimate their distance.
A useful spoken answer is: “The pointer starts at index one, the requested offset is two, so the access is at index three of a four-element array.” That gives the interviewer a bounds argument, not just a number.
How do pointers to arrays differ from pointers to elements?
The declaration int *p defines a pointer to an integer. The declaration int (*p)[4] defines a pointer to an array of four integers. If int a[4] exists, int (*row)[4] = &a; is the matching declaration. Advancing row by one advances past an entire four-integer array. The expression (*row)[2] accesses its third element.
This distinction becomes practical with two-dimensional arrays. For int matrix[2][4], the expression matrix normally converts to a pointer to its first row, with type int (*)[4]. It is not an int **. A pointer-to-pointer layout usually refers to separate pointer objects; a rectangular array contains its rows directly. Casting one to the other does not repair the layout.
When does a pointer become dangling?
A dangling pointer is associated with an object whose lifetime has ended. Returning the address of an automatic local variable is the standard interview example:
int *bad_result(void) {
int result = 42;
return &result; /* result's lifetime ends on return */
}
The caller cannot safely read the former local object through that returned pointer. The memory may look unchanged in a debugger, but unchanged bytes do not extend the object's lifetime. A better interface returns the integer by value, writes through a caller-provided output pointer, or returns dynamically allocated storage with a clear cleanup contract.
Static storage has a different lifetime, but using a static local as the repair creates shared state and may introduce reentrancy or concurrency problems. In an interview, choose the repair that fits the API rather than treating static as a universal escape hatch.
How should malloc, free, and realloc be explained?
Start by naming the owner. If your function allocates and returns a buffer, say who frees it, how its length is communicated, and what happens on failure. Include the correct headers, check allocation results, and avoid multiplying an untrusted count by an element size without checking for overflow.
#include <stdint.h>
#include <stdlib.h>
int *make_values(size_t n) {
if (n == 0 || n > SIZE_MAX / sizeof(int))
return NULL;
int *p = malloc(n * sizeof *p);
if (p == NULL)
return NULL;
for (size_t i = 0; i < n; ++i)
p[i] = 0;
return p; /* caller owns the allocation */
}
This interface deliberately treats a zero-length request as no allocation. That is an API choice, not a general claim about every allocator. For a nonzero resize, assign realloc's result to a temporary pointer first. On failure the old allocation remains valid; overwriting its only pointer would lose access to it. On success, use the returned pointer and discard old aliases, even if the address appears unchanged.
Calling free(NULL) is harmless, but freeing the same allocation twice is not. Setting one variable to null after freeing can prevent accidental reuse through that variable; it does not invalidate every other alias. A pointer into the middle of a buffer is also not the pointer you should pass to free.
What do const pointer declarations mean?
| Declaration | Can reassign p? | Can modify the integer through p? |
|---|---|---|
| const int *p | Yes | No |
| int *const p | No | Yes, if the object is modifiable |
| const int *const p | No | No |
Read the declaration in terms of the pointer and the pointed-to object separately. Const access through one pointer does not automatically make an originally mutable object immutable through every other alias. Conversely, casting away const does not grant permission to modify an object that was actually defined as const.
Apply the same care to strings. char text[] = "cat"; creates a modifiable array, including a terminating null character. const char *text = "cat"; points to a string literal that must not be modified. A literal's storage duration does not make it writable.
What should I say when a question has undefined behavior?
Name the first invalid operation and stop the output prediction there. Out-of-bounds access, using freed storage, and modifying a string literal are useful examples. Another common trap is an expression such as i = i++ + 1;, whose unsequenced modifications make it undefined under the C rules used here.
Do not say “it always crashes” or select whichever result your laptop prints. Undefined behavior means the language does not require a particular result. Distinguish it from implementation-defined behavior, where an implementation documents its choice, and unspecified behavior, where the language allows alternatives without requiring one fixed choice.
A practical debugging and interview checklist
- Write each pointer's complete type, including const and array dimensions.
- Name the object it refers to and the valid element range.
- Track ownership and every point where lifetime can end.
- Separate expressions that only form pointers from expressions that access objects.
- Test empty, one-element, last-element, and one-past cases.
- Enable compiler warnings and suitable address/undefined-behavior sanitizers during practice.
For a mock interview, take fifteen minutes to implement a bounded array search, then explain every dereference. On the next attempt, add an allocation failure path and a caller-owned output parameter. Finish by reviewing a deliberately broken version and identifying the earliest fault. These exercises turn vocabulary into a method you can use under pressure.
Quick answers to common follow-ups
Does a null check make a pointer safe? No. It only rejects the null value; a non-null pointer may be dangling, misaligned, or outside the object's bounds.
Why use a pointer to a pointer? It lets a function update the caller's pointer variable when that is the intended interface. It does not make all multidimensional arrays compatible with that parameter.
Can I use sizeof to find a heap buffer's length? Not from the pointer alone. Store the length as part of the interface or data structure.
Reference: The public C11 committee draft is the language reference for the conversion, lifetime, and pointer-operation rules. Examples and preparation exercises above are original.