Technical interviews in C evaluate something fundamentally different from interviews in JavaScript, Python, or Go. High-level languages abstract away memory layouts and operating system interactions. In C, interviewers want to see if you understand how hardware executes your code: stack versus heap allocations, CPU cache line alignment, pointer dereferencing, and undefined behavior.
If you are reviewing basic control flow and syntax first, take a look at our foundational guide on mastering C basic programs and the architectural origins in the history and importance of the C language.
Below are six core programming problems frequently asked during systems engineering, embedded firmware, and infrastructure interviews, accompanied by production-grade, compilable C code.
1. In-Place String Reversal with Double Pointers
The Objective: Reverse an ASCII string in-place without allocating a secondary buffer, avoiding stack overflow, and handling null or zero-length inputs gracefully.
Key Concepts: Pointer arithmetic, null termination checking, in-place temporary character swap.
#include <stdio.h>
#include <string.h>
void reverse_string(char *str) {
if (str == NULL || *str == '\0') {
return;
}
char *start = str;
char *end = str + strlen(str) - 1;
while (start < end) {
char temp = *start;
*start = *end;
*end = temp;
start++;
end--;
}
}
int main(void) {
char message[] = "Dropout Developer";
printf("Original: %s\n", message);
reverse_string(message);
printf("Reversed: %s\n", message);
return 0;
}
Common Pitfall: Passing a string literal such as char *literal = "Hello"; reverse_string(literal); causes a segmentation fault on modern operating systems because string literals reside in the read-only data segment (.rodata). Always mutate character arrays allocated on the stack (char arr[]) or heap.
2. Reversing a Singly Linked List (Iterative O(N) Time, O(1) Space)
The Objective: Reverse the pointer chain of a singly linked list in a single pass without allocating additional nodes.
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int data;
struct Node *next;
} Node;
Node *reverse_linked_list(Node *head) {
Node *prev = NULL;
Node *current = head;
Node *next_node = NULL;
while (current != NULL) {
next_node = current->next;
current->next = prev;
prev = current;
current = next_node;
}
return prev;
}
void push_front(Node **head_ref, int new_data) {
Node *new_node = (Node *)malloc(sizeof(Node));
if (new_node == NULL) {
perror("Failed to allocate memory for node");
exit(EXIT_FAILURE);
}
new_node->data = new_data;
new_node->next = *head_ref;
*head_ref = new_node;
}
void print_list(const Node *node) {
while (node != NULL) {
printf("%d -> ", node->data);
node = node->next;
}
printf("NULL\n");
}
void free_list(Node *head) {
Node *current = head;
while (current != NULL) {
Node *temp = current;
current = current->next;
free(temp);
}
}
Why Recursive Reversal is Dangerous: While recursive linked list reversal looks elegant on paper, it consumes O(N) stack frames. If the linked list contains one million elements, recursion triggers an unavoidable stack overflow crash. Production C code uses the iterative approach.
3. Safe Dynamic Array Resizing (realloc Pitfall)
The Objective: Build a dynamically growing vector in C that prevents memory leaks when memory is exhausted.
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int *items;
size_t count;
size_t capacity;
} DynamicIntVector;
int vector_append(DynamicIntVector *vec, int value) {
if (vec->count >= vec->capacity) {
size_t new_capacity = (vec->capacity == 0) ? 4 : vec->capacity * 2;
// Notice: never assign realloc directly to vec->items
int *temp = (int *)realloc(vec->items, new_capacity * sizeof(int));
if (temp == NULL) {
return -1; // Allocation failed; original buffer remains intact
}
vec->items = temp;
vec->capacity = new_capacity;
}
vec->items[vec->count++] = value;
return 0;
}
void vector_destroy(DynamicIntVector *vec) {
if (vec->items != NULL) {
free(vec->items);
vec->items = NULL;
}
vec->count = 0;
vec->capacity = 0;
}
The Critical Bug to Point Out in Interviews: Writing vec->items = realloc(vec->items, new_size); introduces a severe memory leak. If realloc returns NULL, the pointer to the original memory block is overwritten with NULL, making it impossible to free the original allocation.
4. Fast Bit Manipulation: Brian Kernighan's Algorithm
The Objective: Count the number of set bits (1s) in a 32-bit unsigned integer in O(K) iterations, where K is the number of set bits, rather than iterating through all 32 bits.
#include <stdio.h>
#include <stdint.h>
int count_set_bits(uint32_t n) {
int count = 0;
while (n > 0) {
// n & (n - 1) clears the lowest set bit in a single CPU instruction
n = n & (n - 1);
count++;
}
return count;
}
int is_power_of_two(uint32_t n) {
// A power of two has exactly one set bit
return (n > 0) && ((n & (n - 1)) == 0);
}
int main(void) {
uint32_t value = 29; // Binary: 00011101 (4 set bits)
printf("Set bits in %u: %d\n", value, count_set_bits(value));
printf("Is 16 a power of 2? %s\n", is_power_of_two(16) ? "Yes" : "No");
printf("Is 18 a power of 2? %s\n", is_power_of_two(18) ? "Yes" : "No");
return 0;
}
5. Struct Padding and Memory Alignment
Interviewers frequently present a struct definition and ask for its sizeof output on a 64-bit architecture:
#include <stdio.h>
struct Unoptimized {
char a; // 1 byte
// 3 bytes of compiler padding inserted here
int b; // 4 bytes
char c; // 1 byte
// 7 bytes of compiler padding inserted here
double d; // 8 bytes
};
struct Optimized {
double d; // 8 bytes (aligned on 8-byte boundary)
int b; // 4 bytes
char a; // 1 byte
char c; // 1 byte
// 2 bytes of tail padding to reach multiple of 8
};
int main(void) {
printf("Unoptimized size: %zu bytes\n", sizeof(struct Unoptimized)); // Outputs 24 bytes
printf("Optimized size: %zu bytes\n", sizeof(struct Optimized)); // Outputs 16 bytes
return 0;
}
CPUs read memory efficiently when multi-byte values reside at memory addresses that are multiples of their size. By reordering struct fields from largest to smallest, you save substantial cache footprint across large data collections.
6. Type Punning: Inspecting IEEE 754 Float Representation
Directly casting (int)3.14f converts the value mathematically to 3. But what if an interviewer asks you to inspect the raw IEEE 754 32-bit floating point binary pattern? In modern C, the strict aliasing rule prohibits casting *(int *)&float_val. Instead, use a union or memcpy:
#include <stdio.h>
#include <string.h>
#include <stdint.h>
void print_float_bits(float f) {
uint32_t bits;
memcpy(&bits, &f, sizeof(bits)); // Safe under strict aliasing
uint32_t sign = (bits >> 31) & 1;
uint32_t exponent = (bits >> 23) & 0xFF;
uint32_t mantissa = bits & 0x7FFFFF;
printf("Float: %f\n", f);
printf("Sign: %u | Exponent (raw): 0x%X | Mantissa: 0x%X\n", sign, exponent, mantissa);
}
int main(void) {
print_float_bits(3.14159f);
return 0;
}
Summary Checklist for Your C Interview
- Never use gets(): It has no buffer limit checks and was formally removed from ISO C11. Always use
fgets(buffer, sizeof(buffer), stdin). - Pair every malloc with free: Check for NULL immediately after every memory allocation call.
- Understand Pointer Arithmetic:
ptr + 1advances bysizeof(*ptr)bytes, not 1 byte. - Beware of Off-By-One Strings: A string of length N requires an array of size
N + 1to store the terminating null character'\0'.
For more strategies on standing out in software engineering interviews without a Tier-1 degree, check out our guide on breaking the non-traditional path to a developer career.
