Master these 31 carefully curated interview questions to ace your next C interview.
Pointers are variables that store memory addresses, enabling direct memory manipulation, dynamic allocation, and efficient data passing.
Declaration: int *ptr = &variable. Dereference: *ptr accesses the value. Pointer arithmetic: ptr++ moves by sizeof(type). NULL pointer: points to nothing. Use: dynamic memory, arrays, function pointers, data structures. Common errors: dangling pointers, memory leaks, buffer overflows.
malloc allocates uninitialized memory; calloc allocates and initializes to zero. Both return void* requiring casting.
malloc(size): allocates size bytes, returns uninitialized memory. calloc(count, size): allocates count*size bytes, initializes all to 0. Both return NULL on failure. free() releases memory. realloc() resizes allocation. Always check return value. Memory leaks if free() forgotten.
Structures group related variables of different types under one name, enabling custom data types.
struct Point { int x; int y; }; Access via dot (.) or arrow (->) for pointers. typedef for aliases. Structures can contain other structures, arrays, function pointers. sizeof may include padding for alignment. Bit fields for compact storage.
Stack: automatic, fast, limited, LIFO for local variables. Heap: manual allocation, larger, slower, for dynamic data.
Stack: function call frames, local variables, auto-freed on return, limited size (1-8MB typically), very fast. Heap: malloc/calloc/realloc, must free() manually, larger, slower due to allocation overhead. Stack overflow: deep recursion. Heap fragmentation: many small allocations/frees.
Function pointers store addresses of functions, enabling callbacks, dynamic dispatch, and runtime function selection.
Declaration: int (*fptr)(int, int) = &add. Call: fptr(3, 4). Use: callback functions, strategy pattern, dispatch tables, qsort comparator. typedef simplifies: typedef int (*operation)(int, int). Arrays of function pointers for state machines.
Manual management with malloc/calloc/realloc/free. Programmer responsible for allocation, deallocation, and avoiding leaks.
No garbage collector. Common issues: (1) Memory leaks: allocated but never freed. (2) Dangling pointers: freed memory still referenced. (3) Double free: freeing same memory twice. (4) Buffer overflow: writing beyond allocated bounds. Tools: Valgrind (leak detection), AddressSanitizer (bounds checking). Best practices: set pointers to NULL after free, use RAII-like patterns.
const prevents modification; volatile tells compiler the value may change externally — don't optimize away reads.
const int x = 5: cannot modify. const int *p: can't modify pointed value. int *const p: can't change pointer itself. volatile: used for hardware registers, signal handlers, shared memory — forces re-read every access. const volatile: read-only but externally changeable (read-only hardware register).
Use GDB debugger with backtrace, Valgrind for memory errors, and AddressSanitizer for compile-time detection.
Steps: (1) Compile with -g flag. (2) Run in GDB: bt (backtrace), print variables, breakpoints. (3) Valgrind --tool=memcheck for memory errors. (4) Compile with -fsanitize=address for AddressSanitizer. (5) Common causes: NULL dereference, buffer overflow, use-after-free, stack overflow. (6) Check return values of malloc.
Processes have separate memory spaces; threads share memory within a process. Threads are lighter but need synchronization.
Process: own address space, own resources, IPC needed (pipes, shared memory). Thread: shared address space, shared file descriptors, lighter than processes. Process creation: fork(). Thread creation: pthread_create(). Threads need mutex/semaphore for shared data. Context switch: faster for threads. Use processes for isolation, threads for parallelism.
malloc allocates uninitialized memory block; calloc allocates and zero-initializes memory for an array of elements.
malloc(size_t size): allocates 'size' bytes, returns void pointer, memory contains garbage values. calloc(size_t n, size_t size): allocates n*size bytes, initializes all to zero. Both return NULL on failure — always check. free() deallocates both. realloc() resizes existing allocation. calloc is safer (initialized) but slightly slower. Use malloc when you'll immediately overwrite the memory. Use calloc for arrays/structs where zero-initialization matters. Always free allocated memory to prevent leaks.
Pointers store memory addresses of variables, enabling dynamic memory management, array operations, and function references.
Declaration: int *ptr = &var. Dereference: *ptr accesses value at address. Pointer arithmetic: ptr++ advances by sizeof(type). Uses: dynamic memory (malloc/free), passing by reference to functions, arrays (array name is pointer to first element), linked data structures, function pointers (callbacks). NULL pointer: indicates no valid address. Dangling pointer: points to freed memory (bug). void pointer: generic pointer, must cast before dereferencing. Double pointer: int **pp — pointer to pointer (used for dynamic 2D arrays, modifying pointer in function).
Stack stores local variables with automatic allocation/deallocation; heap stores dynamically allocated memory managed manually.
Stack: LIFO, automatic allocation for local variables and function calls, fixed size (typically 1-8MB), fast access, freed on function return. Heap: manual allocation (malloc/free), larger size, slower access, risk of memory leaks and fragmentation. Stack overflow: too many function calls (deep recursion) or large local arrays. Heap fragmentation: many small allocations/deallocations. Best practices: stack for small, short-lived data; heap for large or long-lived data, dynamic-size data. Global/static variables stored in data segment.
Function pointers store addresses of functions, enabling callbacks, dynamic dispatch, and implementing strategy patterns.
Declaration: int (*func_ptr)(int, int) = &add. Call: result = func_ptr(3, 4) or (*func_ptr)(3, 4). Use cases: callback functions (qsort comparator), state machines, plugin systems, event handlers. Array of function pointers: void (*actions[])(void) = {func1, func2}. typedef simplifies: typedef int (*Operation)(int, int). Passing to function: void process(int (*callback)(int)). Used internally by: signal handlers, atexit(), comparison functions in stdlib. Function pointers enable polymorphism in C without OOP.
volatile tells the compiler a variable may change unexpectedly, preventing optimization that could skip re-reading its value.
Purpose: prevents compiler from caching variable in register, forces re-read from memory on every access. Use cases: (1) Hardware registers (memory-mapped I/O). (2) Variables modified by ISR (interrupt service routines). (3) Variables shared between threads (though not sufficient for thread safety — need mutex). (4) Memory-mapped peripherals in embedded systems. volatile int *port = (int*)0x40000000. Without volatile, compiler may optimize away repeated reads to same address. volatile does NOT provide atomicity or memory ordering — use atomic operations or mutexes for thread safety.
Struct allocates memory for all members; union shares memory among members, size equals largest member.
struct: each member has its own memory, sizeof = sum of members + padding. union: all members share same memory address, sizeof = sizeof(largest member). Only one union member is valid at a time. Use cases: union for variant types (tagged union with enum), memory-efficient data representation, type punning (reinterpreting bits — technically UB). Padding/alignment: compiler adds padding bytes for alignment. #pragma pack or __attribute__((packed)) removes padding (may hurt performance). Bit fields: struct { int flags : 4; } for bit-level control.
The preprocessor processes directives (#include, #define, #ifdef) before compilation, performing text substitution and conditional compilation.
Directives: #include (insert file contents), #define (macro definition), #undef, #ifdef/#ifndef/#endif (conditional compilation), #pragma (compiler hints), #error (compilation error). Macros: #define MAX(a,b) ((a)>(b)?(a):(b)) — text substitution, no type checking. Dangers: double evaluation (#define SQ(x) ((x)*(x)) — SQ(i++) evaluates i++ twice). Stringification: #x converts to string. Concatenation: a##b joins tokens. Include guards: #ifndef HEADER_H #define HEADER_H ... #endif. Modern alternative: _Pragma, inline functions preferred over complex macros.
Memory leaks occur when allocated memory is not freed, detected using Valgrind, AddressSanitizer, or custom tracking.
Causes: forgetting free(), losing pointer to allocated memory, error paths skipping cleanup, circular references (rare in C). Detection: Valgrind (valgrind --leak-check=full ./program), AddressSanitizer (-fsanitize=address), Dr. Memory (Windows). Prevention: (1) Always pair malloc/free. (2) Set pointer to NULL after free. (3) Use RAII-like patterns (cleanup labels, goto for error handling). (4) Smart wrapper functions. (5) Static analysis: cppcheck, Coverity. Best practice: allocate and free in same scope/function when possible. Memory leak in long-running servers causes gradual memory exhaustion.
Undefined behavior means the C standard imposes no requirements — anything can happen, including crashes, wrong results, or appearing to work.
Common UB: (1) Buffer overflow (accessing array out of bounds). (2) Dereferencing NULL/dangling pointer. (3) Signed integer overflow. (4) Use after free. (5) Uninitialized variable read. (6) Modifying string literals. (7) Violating strict aliasing. (8) Data races in multithreaded code. Why dangerous: compiler assumes no UB and optimizes accordingly — removing 'impossible' checks. Detection: -fsanitize=undefined (UBSan), -Wall -Wextra -Werror, static analyzers. Compiler may optimize away security checks when UB is assumed impossible.
Static linking copies library code into the executable; dynamic linking loads shared libraries (.so/.dll) at runtime.
Static: library code embedded in executable, larger binary, no runtime dependency, faster startup (no symbol resolution). Flag: gcc -static main.c -lmath. Dynamic: shared library loaded at runtime, smaller binary, library updates without recompilation, shared memory (multiple programs share one .so). Extensions: .a (static archive), .so (Linux shared), .dll (Windows). Position Independent Code (-fPIC) required for shared libraries. Dynamic loading: dlopen/dlsym for plugins. Trade-offs: static for deployment simplicity, dynamic for security updates and memory efficiency.
Define a node struct with data and next pointer, implement functions for insert, delete, search, and traversal with proper memory management.
struct Node { int data; struct Node *next; }. Operations: (1) Insert head: new->next = head; head = new. (2) Insert tail: traverse to last, last->next = new. (3) Delete: find prev, prev->next = current->next, free(current). (4) Search: traverse comparing data. (5) Always free deleted nodes. (6) Handle empty list edge case. (7) Double pointer (Node **head) for modifying head in function. Variations: doubly linked (prev pointer), circular (last->next = head). Common interview follow-ups: detect cycle (Floyd's tortoise and hare), reverse list, find middle, merge two sorted lists.
Use return codes, errno, setjmp/longjmp for non-local jumps, and cleanup with goto for resource deallocation.
Patterns: (1) Return codes: 0 = success, negative = error. (2) errno: global error variable set by library functions, strerror(errno) for message. (3) Goto cleanup: goto error_cleanup at failure, cleanup label frees resources in reverse order — widely used in Linux kernel. (4) setjmp/longjmp: non-local jump (avoid — hard to reason about). (5) Callback error handlers: function pointer for error reporting. (6) errno should be checked immediately after call. (7) perror() prints error message. Best practice: consistent error handling strategy, always check return values of system calls and memory allocation.
Cache-friendly code accesses memory sequentially, uses data structures with spatial locality, and minimizes cache misses.
CPU cache hierarchy: L1 (fastest, ~64KB), L2 (~256KB), L3 (~8MB). Cache line: 64 bytes loaded at once. Spatial locality: access sequential memory (arrays > linked lists). Temporal locality: reuse recently accessed data. Optimizations: (1) Row-major traversal for 2D arrays (C stores row-major). (2) Structure of Arrays (SoA) vs Array of Structures (AoS) — SoA better when accessing one field across all elements. (3) Align data to cache lines. (4) Avoid false sharing in multithreaded (different threads modifying same cache line). Profile with perf, cachegrind.
Minimize dynamic allocation, use volatile for hardware registers, prefer fixed-size types, and follow MISRA C guidelines.
Embedded constraints: limited RAM/ROM, no OS or minimal RTOS, real-time requirements. Best practices: (1) No malloc — use static allocation and fixed-size buffers. (2) Fixed-width types: uint8_t, int32_t (stdint.h). (3) volatile for hardware registers and ISR-shared variables. (4) Atomic operations for shared data. (5) MISRA C compliance: restrict dangerous constructs. (6) No recursion (unpredictable stack usage). (7) Watchdog timers for recovery. (8) Minimize ISR work (set flag, process in main loop). (9) Static analysis: PC-lint, Polyspace. (10) Unit testing with mock hardware.
Signals are software interrupts sent to processes for events like errors or user actions, handled via signal() or sigaction().
Common signals: SIGINT (Ctrl+C), SIGSEGV (segfault), SIGTERM (terminate), SIGKILL (force kill — uncatchable), SIGALRM (timer), SIGCHLD (child process). Handling: signal(SIGINT, handler) or sigaction (preferred — more control). Handler restrictions: only call async-signal-safe functions (no malloc, printf, mutex). Use volatile sig_atomic_t for flag variables. sigprocmask blocks signals temporarily. raise() sends signal to self. kill(pid, sig) sends to another process. Best practice: set flag in handler, process in main loop.
Use GDB debugger, Valgrind for memory errors, AddressSanitizer for bounds checking, and core dump analysis.
Steps: (1) Compile with debug info: gcc -g -O0 program.c. (2) GDB: gdb ./a.out → run → bt (backtrace at crash). (3) AddressSanitizer: gcc -fsanitize=address — shows exact line of invalid access. (4) Valgrind: valgrind ./a.out — detects use after free, uninitialized read. (5) Core dump: ulimit -c unlimited, analyze with gdb ./a.out core. Common causes: NULL dereference, buffer overflow, use after free, stack overflow, unaligned access. (6) Enable warnings: -Wall -Wextra -Werror catches many issues at compile time.
auto (local, default), static (persists across calls), extern (declared elsewhere), register (CPU register hint).
auto: default for local variables, created on stack, destroyed on function exit. static local: persists between function calls, initialized once. static global/function: limits scope to file (internal linkage). extern: declares variable defined in another file (external linkage). register: suggests CPU register storage (compiler may ignore), cannot take address (&). C11 _Thread_local: per-thread storage. Linkage: internal (static), external (extern/default global), none (local). Static in function: acts as persistent local — useful for counters, caching, one-time initialization.
Memory alignment ensures data is placed at addresses divisible by its size, improving CPU access performance.
Architecture requires natural alignment: int (4 bytes) at address divisible by 4, double (8 bytes) by 8. Struct padding: compiler adds padding bytes between members for alignment. struct { char a; int b; } — sizeof is 8 (not 5) due to 3 bytes padding after a. Minimize padding: order members by size (largest first). _Alignas(n): force alignment. alignof: query alignment requirement. Packed structs: __attribute__((packed)) removes padding — slower due to unaligned access, used in network protocols and file formats.
C99 added VLAs and inline; C11 added threads, atomics, generic selection; C17 is a bugfix release with no new features.
C99: variable-length arrays (VLAs), designated initializers ({.x=1}), inline functions, restrict pointer, bool type, single-line comments (//), mixed declarations and code. C11: _Thread_local, _Atomic, _Generic (type-generic macros), _Static_assert, anonymous structs/unions, aligned_alloc, optional VLAs. C17 (C18): defect fixes only, no new features. C23: constexpr, typeof, improved enums, #embed, digit separators, auto type inference. Most embedded projects use C99 or C11. VLAs controversial — optional in C11+.
Use an array of linked lists (chaining), with a hash function mapping keys to array indices and collision resolution.
Components: (1) Hash function: converts key to index (djb2, FNV-1a for strings). (2) Bucket array: array of linked list heads. (3) Collision resolution: chaining (linked list per bucket) or open addressing (linear probing, quadratic probing, double hashing). Operations: insert O(1) average, search O(1) average, delete O(1) average. Load factor = elements/buckets. Resize when load factor > 0.75 (rehash all elements). Key considerations: good hash distribution, proper sizing (prime numbers for modulo), handling deletions in open addressing (tombstone markers).
restrict tells the compiler a pointer is the only way to access the pointed-to memory, enabling aggressive optimizations.
Syntax: void process(int * restrict a, int * restrict b, int n). Promise: a and b don't alias (point to overlapping memory). Compiler optimizations: vectorization, reordering loads/stores, caching values in registers. Without restrict: compiler must assume pointers might alias and reload values after every store. Used extensively in: memcpy (src and dst must not overlap; use memmove if they might), BLAS/LAPACK numerical libraries, image processing. Violating the restrict contract is undefined behavior. C99+ feature. C++ equivalent: __restrict (compiler extension).
Ready to master C?
Start learning with our comprehensive course and practice these questions.