Prepare for C programming interview questions grouped by experience level.
C Interview Question & Answers
0-2 Years
C is a general-purpose, procedural programming language, originally developed in the early 1970s, that remains genuinely relevant because it gives a programmer fine, direct control over memory and hardware, making it the language of choice for operating systems, embedded systems, and other genuinely performance-critical, low-level software.
The preprocessor handles directives like #include, expanding them into actual code. The compiler translates that expanded source code into object code. The linker combines that object code with any needed libraries into a final executable file, which the operating system then actually runs.
A header file, typically ending in .h, declares functions, variables, or macros that can genuinely be shared across multiple source files. #include <stdio.h> brings in declarations for genuinely standard input and output functions, like printf, without needing to actually rewrite those declarations yourself.
A compile-time error is caught by the compiler before the program ever actually runs, like a missing semicolon or a genuine type mismatch. A runtime error only actually surfaces while the program is executing, like dividing by zero or accessing invalid memory, which the compiler generally has no way to catch in advance.
int main() is the entry point every C program genuinely starts executing from. It's required because the operating system needs one single, consistent, known place to actually start running the program, and by convention, that specific place is always genuinely the main function.
printf() genuinely writes formatted output to the standard output stream, typically the console. scanf() genuinely reads formatted input from the standard input stream, typically whatever the user actually types at the console, and both rely on a genuine format string specifying the expected data type.
int for whole numbers, float and double for decimal numbers (double offering greater precision), char for a single character, and void, used specifically to represent the genuine absence of a type, like a function returning nothing at all.
float typically uses 4 bytes and offers roughly 6 to 7 decimal digits of precision. double typically uses 8 bytes and offers roughly 15 to 16 decimal digits, meaningfully more precise, which is why double is the more common default choice for most calculations unless memory usage is genuinely a tight, real constraint.
A signed integer can represent both negative and positive values, splitting its available range between the two. An unsigned integer can only represent non-negative values, but in exchange gets a larger positive range for the same number of bits, since it doesn't need to reserve any of that range for negative numbers.
sizeof(int) returns the genuine number of bytes a specific data type (or a variable) actually occupies in memory, which can genuinely vary somewhat depending on the specific compiler and platform, which is exactly why sizeof is used rather than genuinely hardcoding an assumed size.
Type casting converts a value from one data type to another. Implicit casting happens automatically, like an int being promoted to a double during a mixed arithmetic expression. Explicit casting is written directly by the programmer, like (int)someDouble, needed when converting in a direction that could genuinely lose data.
A local variable is declared inside a function and only exists during that specific function's execution. A global variable is declared outside every function, accessible from anywhere in the entire file (and other files, if declared extern), and it persists for the entire lifetime of the program.
= is the assignment operator, genuinely assigning a value to a variable. == is the equality comparison operator, genuinely checking whether two values are equal. Confusing them, like writing if (x = 5) instead of if (x == 5), is a genuinely common mistake because the assignment version still genuinely compiles and runs, just with an unintended, incorrect result.
&& is the genuine logical AND operator, used in a conditional expression and evaluating whether both operands are genuinely true, with short-circuit evaluation. & is the genuine bitwise AND operator, operating on the actual individual bits of its operands, a genuinely different operation entirely despite the similar-looking symbol.
A for loop is typically used when you know the number of iterations upfront. A while loop checks its condition before each iteration, so it might not run at all if the condition starts false. A do-while loop checks its condition after each iteration, guaranteeing the loop body runs at least once.
break genuinely exits the loop entirely, stopping all further iterations. continue genuinely skips just the rest of the current iteration and moves on to the next one, without exiting the loop as a whole.
A switch statement compares one value against several possible fixed cases. Each case label must genuinely be a constant integer expression (or a character), which is a genuine restriction that if-else doesn't share, since if-else can evaluate any genuinely arbitrary boolean condition.
The ternary operator, condition ? valueIfTrue : valueIfFalse, provides a genuinely compact, inline alternative to a simple if-else statement. int max = (a > b) ? a : b; assigns the genuinely larger of a and b to max in one single, concise expression.
int add(int a, int b); is a function declaration, telling the compiler the function's name, its return type, and its parameters, without providing the actual implementation yet. int add(int a, int b) { return a + b; } is the actual function definition, providing the real, complete implementation.
C passes arguments genuinely by value by default, so a change made inside a function has no effect on the original variable outside it. You genuinely simulate pass by reference by explicitly passing a pointer to the variable, letting the function actually modify the original value through that pointer.
Recursion is a function calling itself to solve a smaller version of the same problem. Every correct recursive function genuinely needs a base case, the condition where it stops calling itself and returns directly, and a recursive case that genuinely makes progress toward that base case with each call.
A function prototype declares a function's genuine signature, its return type and parameters, before it's actually called in the code, typically placed at the top of a file or in a header. It's genuinely necessary so the compiler can actually verify calls to that function are correct even before it encounters the genuine, full function definition later in the file.
A void function is genuinely declared with a return type of void, meaning it doesn't actually return a value at all. It's used for a function whose genuine purpose is performing an action, like printing output, rather than actually computing and returning a specific result.
A regular local variable's genuine value is lost every time the function returns, and it's genuinely reinitialized on the next call. A static local variable genuinely retains its value between separate calls to that same function, initialized only genuinely once, the very first time the function is actually called.
int numbers[5] = {1, 2, 3, 4, 5}; declares an array of 5 integers with those specific given values. Array elements are accessed using a genuinely zero-based index, so numbers[0] genuinely refers to the very first element.
A C-style string is genuinely a plain array of characters, terminated by a special null character, '\0', which marks the genuine end of the string. Functions like strlen() genuinely rely on finding that null terminator to actually know where the string's real content stops.
An array's declared size is the genuine total number of characters it can actually hold, including space for the terminating null character. The string's actual length, as returned by strlen(), counts only the genuine visible characters before that null terminator, not including the terminator itself.
strcpy() genuinely copies a string without any bound on the destination buffer's size, which can cause a genuine buffer overflow if the source string is longer than the destination can actually hold. strncpy() genuinely accepts a maximum number of characters to copy, providing a genuinely safer, bounded alternative.
strlen(str) genuinely counts the number of characters in the string before its terminating null character, which is the genuinely standard function provided by the string.h header specifically for this purpose.
An array genuinely represents a fixed block of contiguous memory, and its name, in most expressions, decays into a pointer to its very first element. A pointer is genuinely just a variable holding a memory address, and unlike an array, it can genuinely be reassigned to point somewhere else entirely.
sizeof applied directly to the array name returns the array's genuine total size in bytes, all its elements combined. Once that array has decayed into a plain pointer, like when passed into a function, sizeof on that pointer returns only the size of the pointer itself, typically 4 or 8 bytes, not the original array's actual full size, which is a genuinely common source of confusion for someone newer to C.
A pointer is a variable that genuinely stores the actual memory address of another variable, rather than storing a value itself directly. int* ptr; declares a pointer to an int. Assigning it with ptr = &someInt; makes it actually point directly to someInt's own real memory location.
& is the address-of operator, genuinely returning a variable's actual memory address. * is the dereference operator, genuinely accessing the actual value stored at whatever address a pointer currently holds.
A null pointer, typically represented as NULL, points to genuinely nothing at all, no valid, actual memory address. It matters because attempting to dereference a null pointer causes undefined behavior, typically an actual program crash, which is exactly why checking a pointer against NULL before actually using it is a genuinely important, common habit.
In most expressions, an array's name genuinely decays into a pointer to its very first element. arr[i] is genuinely equivalent to *(arr + i), which is exactly why pointer arithmetic works so naturally with arrays in C, since the two concepts are genuinely so closely, deliberately related.
Pointer arithmetic lets you move a pointer forward or backward by an actual specific number of elements, not raw bytes, adjusted automatically based on the pointer's own underlying data type's actual size. If ptr points to the first element of an int array, ptr + 1 correctly points to the very next int, automatically advancing by exactly 4 bytes on a typical, standard system.
3-6 Years
A pointer to a pointer, declared as int** ptr, genuinely stores the address of another pointer. A genuinely practical use case is a function needing to actually modify a caller's own pointer variable itself, like a function that genuinely allocates memory and needs to update the caller's own pointer to point at that newly allocated block.
A function pointer genuinely stores the address of a function, letting you actually call that function indirectly through the pointer, or pass it as an argument to another function. It solves the genuine problem of needing to parameterize behavior itself, beyond just data, similar in spirit to a callback in a genuinely higher-level language.
int (*funcPtr)(int, int); declares a pointer to a function taking two int parameters and returning an int. Assigning funcPtr = add; (where add is an genuinely already-defined function matching that exact signature) lets you actually call it indirectly through funcPtr(a, b).
A two-dimensional array, like int arr[3][4], is genuinely stored as one single, contiguous block of memory, and accessing arr[i][j] is genuinely equivalent to a specific pointer arithmetic calculation combining both the row and column offsets to actually locate that exact element.
const int* ptr means the genuine value ptr points to can't be modified through ptr, though ptr itself can genuinely be reassigned to point elsewhere. int* const ptr means ptr itself genuinely can't be reassigned once set, though the value it points to can genuinely still be modified through it.
The stack stores local variables and function call information, automatically managed, growing and shrinking as functions are actually called and return. The heap is used for dynamically allocated memory, requested explicitly with malloc() and released explicitly with free(), and it genuinely persists until you actually free it yourself.
malloc(size) genuinely allocates a specified number of bytes on the heap and returns a pointer to that genuinely allocated block. If it genuinely fails, typically because the system has run out of available memory, it returns NULL instead, which is exactly why checking the returned pointer against NULL before actually using it is a genuinely important habit.
malloc() genuinely allocates a block of memory without initializing its actual contents, leaving it filled with genuinely whatever garbage values happened to already be there. calloc() genuinely allocates memory and also initializes every byte to zero, at the cost of a small amount of genuinely additional overhead for that zeroing step.
A memory leak happens when memory is dynamically allocated with malloc() but never actually released with a matching free(), so that memory genuinely stays reserved and unusable for the rest of the program's entire run, even though nothing in the program can actually reach or use it anymore.
A dangling pointer points to memory that's already been freed. It's genuinely dangerous because using it afterward, reading or writing through it, causes undefined behavior, since that specific memory might already have been reused for something entirely different by then.
A struct groups genuinely several different, related variables together into a single, custom data type, letting you actually treat a logically related collection of data, like a point's x and y coordinates, as one single, cohesive unit rather than several genuinely separate, unrelated variables.
The arrow operator, ptr->member, genuinely dereferences the pointer and accesses the specified member in one single, combined step, equivalent to writing (*ptr).member, but genuinely more concise and commonly used in practice.
A union genuinely allocates enough memory to hold its genuinely largest member, and every member genuinely shares that exact same memory location, so setting one member genuinely overwrites any value previously stored in another. A struct instead genuinely allocates separate, independent memory for every single member.
typedef creates a genuinely new, alternative name for an existing type, like typedef struct Point Point;, letting you actually write Point p; instead of the more genuinely verbose struct Point p; every single time you need to declare a variable of that type.
A self-referential structure genuinely contains a pointer to another instance of its own exact same type, like a linked list node holding a pointer to the genuinely next node. This is exactly the fundamental building block behind implementing a linked list, a tree, or another genuinely similar linked data structure in C.
A bit field lets you specify the exact number of bits a struct member should actually occupy, like unsigned int flag : 1;, packing several small values tightly into a genuinely smaller amount of total memory than each would individually take as a full, separate int. It's used when memory is genuinely tight and several boolean or small numeric values need to be stored compactly together.
The preprocessor handles directives beginning with #, like #include and #define, genuinely processing and transforming the source code textually before the actual compiler ever sees it, expanding a macro or genuinely including another file's full contents directly into the current file.
#define PI 3.14159 genuinely defines a macro, and every genuine occurrence of PI in the code is textually replaced with 3.14159 by the preprocessor before actual compilation happens, rather than PI genuinely behaving as an actual, typed variable at all.
A function-like macro, like #define SQUARE(x) ((x) * (x)), is genuinely, textually substituted by the preprocessor with no actual function call overhead at runtime, but it can genuinely produce a subtle, unexpected bug if called with an expression having a side effect. An actual function genuinely evaluates its argument exactly once, avoiding that specific class of bug entirely.
An include guard, using #ifndef, #define, and #endif around a header file's own contents, genuinely prevents that same header from actually being included more than once in the exact same file, which would otherwise cause a genuine duplicate declaration error if the same header got included, directly or indirectly, more than once.
FILE* fp = fopen('data.txt', 'r'); genuinely opens the file in read mode, returning a pointer to a FILE structure that genuinely represents the open file stream, used in every subsequent operation actually reading from or writing to that file.
'r' mode genuinely opens a file for reading, and it genuinely fails if the file doesn't actually exist. 'w' mode genuinely opens a file for writing, creating it if it doesn't exist, and genuinely, completely truncating (erasing) its existing content if it already does exist.
fgets(buffer, sizeof(buffer), fp) genuinely reads a single line from the file pointed to by fp into the given buffer, stopping at a newline character or once the genuine buffer size limit is actually reached, whichever genuinely comes first.
fopen() returns NULL if it genuinely fails to open the file, for instance if the file doesn't actually exist or the program genuinely lacks permission to access it. Using that NULL pointer without checking it first would genuinely cause undefined behavior, typically an actual program crash.
6-8 Years
A double free happens when free() is genuinely called more than once on the exact same pointer, which causes undefined behavior, since the memory allocator's own internal bookkeeping genuinely becomes corrupted, potentially leading to a genuinely hard-to-diagnose crash or a real security vulnerability later on, possibly far removed from the actual, original double-free call itself.
A buffer overflow happens when a program writes data genuinely beyond the actual bounds of an allocated buffer, corrupting adjacent memory. C is genuinely susceptible because it doesn't automatically check array or buffer bounds at all, leaving that genuine responsibility entirely up to the programmer, unlike a language with genuinely built-in bounds checking.
Memory alignment requires certain data types to genuinely start at a memory address that's a multiple of their own size, for actual hardware efficiency reasons. A compiler genuinely inserts padding bytes within a struct to actually satisfy this alignment, which is exactly why sizeof a struct can genuinely be larger than the naive sum of its individual members' own sizes.
A tool like Valgrind genuinely tracks every allocation and deallocation during a program's execution, reporting any block of memory that was genuinely allocated but never actually freed by the time the program exits, pinpointing exactly where that specific leaked allocation genuinely originated in the code.
Undefined behavior means the language standard genuinely places no requirement at all on what the program actually does once it occurs, accessing an out-of-bounds array index, for instance, could crash immediately, silently produce a wrong result, or appear to genuinely work correctly on one compiler while failing entirely on another.
A stack overflow happens when the call stack genuinely grows beyond its allocated size, most commonly caused by a genuinely deep or infinite recursion with a missing or unreachable base case, where each recursive call genuinely adds another frame to the stack until it eventually, actually runs out of available space.
A wild pointer is genuinely uninitialized, holding whatever random, garbage value happened to already be at that memory location before it was ever actually assigned. A dangling pointer once genuinely pointed to a valid, actual block of memory that has since been freed. Both are dangerous for the same underlying reason, but a wild pointer never actually pointed to valid memory in the first place, while a dangling pointer genuinely did at some earlier point.
struct Node { int data; struct Node* next; }; defines a genuinely self-referential structure holding a data value and a pointer to the next node in the list, the fundamental genuine building block every linked list operation is actually built on top of.
Allocate a genuinely new node with malloc(), set its data field, point its next field to the genuinely current head of the list, and then update the head pointer itself to actually point to this genuinely new node, making it the new, actual first element.
Maintain an array along with a genuine top index tracking the current number of elements. Push genuinely increments top and adds the new element at that position. Pop genuinely reads the element at the current top and decrements it, following the genuine last-in-first-out behavior a stack requires.
A naive array-based queue genuinely wastes space at the front of the array as elements are dequeued over time. A circular queue genuinely wraps the front and rear indices back around to the beginning of the array once they reach its end, reusing that genuinely freed space rather than letting it sit permanently unused.
Traverse the list, and for each node, genuinely save a pointer to the next node before actually calling free() on the current one, since freeing a node and then trying to access its own next field afterward would genuinely be a use-after-free error.
8-10 Years
Bitwise operators, &, |, ^, ~, <<, >>, operate genuinely directly on the individual bits of an integer value. A genuinely practical use case is efficiently packing several boolean flags into a genuinely single integer, using individual bits as flags rather than allocating a genuinely separate variable for each one.
const genuinely tells the compiler a value shouldn't be modified through that specific variable, enabling certain compiler optimizations. volatile genuinely tells the compiler a value might change unexpectedly, outside the program's own normal control flow, like from hardware or a genuinely separate interrupt handler, preventing the compiler from actually optimizing away a read that appears redundant but genuinely isn't.
Header files (.h) genuinely contain function prototypes, struct definitions, and macro declarations. Source files (.c) genuinely contain the actual implementation. Other source files genuinely #include the relevant header to actually use those declared functions, and the linker combines everything together at the genuinely final build step.
extern declares that a genuine variable or function is actually defined in a different source file, letting the current file reference it without genuinely creating a duplicate definition of its own, which the linker would otherwise genuinely reject as a conflicting, duplicate symbol.
A variadic function, like printf, accepts a genuinely variable number of arguments. Using va_list, va_start, va_arg, and va_end from stdarg.h, you can actually iterate through those genuinely variable arguments one at a time, though the function needs some other genuine way, like a format string, to actually know how many arguments were genuinely passed and what type each one actually is.
An error genuinely prevents the code from compiling at all. A warning genuinely lets the code compile anyway, but flags something that's likely, though not certainly, a genuine mistake, like comparing a signed and an unsigned integer. Treating warnings seriously catches a genuinely large class of subtle bug before it ever actually becomes a real, production issue.
Inline assembly lets you embed genuinely raw assembly language instructions directly within C code, used for a genuinely rare, specific case requiring direct hardware access or an optimization the compiler genuinely can't express through standard C alone, common in embedded systems or genuinely low-level driver code.
A system call requests a genuine service directly from the operating system's own kernel, like reading a file or allocating memory, whereas a regular function call genuinely executes entirely within the current process's own user-space code, with no genuine involvement from the kernel at all.
fork() genuinely creates a new process by duplicating the calling process entirely. exec() genuinely replaces the current process's own memory image with a completely different program, and the two are commonly genuinely used together, forking a child process and then having that specific child call exec() to actually run a genuinely different program.
Embedded C targets genuinely resource-constrained hardware, like a microcontroller, with limited memory and processing power, often with no genuine operating system underneath at all. It commonly requires directly manipulating genuinely specific hardware registers and being far more deliberate about memory usage than a typical desktop application would ever genuinely need to be.
A memory-mapped register lets you actually control a specific piece of hardware by reading from or writing to a genuinely specific memory address, typically accessed in C through a pointer cast to that specific address, like *(volatile unsigned int*)0x40000000 = 1;, with volatile genuinely ensuring the compiler doesn't optimize away that access.
An ISR is a genuinely special function that runs in response to a hardware interrupt, pausing whatever the main program was genuinely doing to actually handle that specific event immediately. ISRs should genuinely be kept as short and fast as possible, since a genuinely lengthy ISR can delay other important, time-sensitive processing elsewhere in the system.
Stick to genuinely standard C (avoiding compiler-specific extensions where possible), avoid relying on a specific data type's exact size (using types like int32_t from stdint.h instead of assuming int is genuinely always 32 bits), and isolate any genuinely necessary platform-specific code behind a clear, well-defined interface.
10+ Years
I'd weigh the actual, concrete performance and memory-control requirement, is it genuinely a hard, real constraint, against the real cost of a team needing to actually learn and carefully maintain C code going forward, since C's own manual memory management and genuinely greater risk of a subtle memory bug carry a real, ongoing cost well beyond just the initial writing of it.
Migrate incrementally, starting with the genuinely highest-risk areas, code that's caused actual real memory-related bugs before, rather than attempting to rewrite the entire, complete codebase all at once in one single, large, disruptive pass. Leaning on real, existing test coverage and a tool like Valgrind to actually catch a regression early matters just as much here as for genuinely any other large-scale change.
I check whether ownership of every dynamically allocated block of memory is genuinely clear, who's actually responsible for eventually calling free() on it, whether every malloc() return value is genuinely checked against NULL, and whether the design accounts for a genuine, realistic failure mode rather than only the happy path.
Automate what can genuinely be automated, static analysis tools flagging a common, real C pitfall directly in code review, enforced in CI so standards aren't purely a matter of individual opinion. For the more genuinely subtle design conventions that resist full automation, I'd document the handful of decisions that actually matter most.
I'd weigh the genuinely real reduction in an entire class of memory-safety bug that a language like Rust provides against the real cost of a team's own existing deep C expertise and any genuinely large, existing C codebase that would still need to be maintained regardless. I'd pilot the newer language on a genuinely smaller, lower-risk new component first.
A crash dump, analyzed with a debugger like gdb, often reveals the exact actual call stack at the specific real moment of the crash, which is a genuinely strong starting point. Memory corruption bugs, an out-of-bounds write or a genuine use-after-free, are common actual causes of exactly this kind of intermittent, inconsistent crash.
Track memory usage over time specifically to catch a genuine, slow memory leak before it eventually causes an actual real out-of-memory crash, alongside standard latency and error-rate metrics that would apply to genuinely any production service, regardless of the language it's actually written in.
Treat the library's actual public header file as a genuine contract. Adding a genuinely new function is generally safe. Changing an existing function's actual signature can genuinely break other code linking against it, which is exactly why C library versioning needs real, careful, deliberate attention.
Mitigate first: schedule a genuinely regular, planned restart of the actual service as an immediate, short-term stopgap, buying real, needed time to actually properly diagnose the real, underlying root cause. Then use a genuine memory profiling tool to identify exactly what's actually accumulating and never being properly released.
Start from actual load testing at realistic traffic patterns rather than a purely theoretical, rough calculation, since real bottlenecks in a C application, often tied to a specific memory allocation pattern or a genuinely inefficient algorithm, often only actually surface under genuinely real, sustained load.
This is a judgment question interviewers use to see how you reason under genuine uncertainty, not to test a specific textbook fact. A strong answer names the actual constraint that forced the decision, the realistic options that were genuinely on the table, why you picked one knowing it wasn't guaranteed to be right, and what you'd do differently with what you know now.
I'd walk through an actual piece of their code together, showing concretely how a NULL pointer dereference could genuinely occur if malloc() ever actually failed and returned NULL, rather than simply telling them checking the return value is genuinely important in the abstract. Seeing the real, concrete failure mode tends to build that habit far more effectively.
I wouldn't push it as an abstract best practice. I'd run the tool against the existing codebase first and show the genuinely concrete, real bugs it actually found, letting that specific, tangible evidence make the case rather than arguing for static analysis purely in the abstract.
I'd bring actual, real benchmark data comparing the two directly, rather than a purely general, abstract belief about raw, unchecked access always being genuinely faster in a way that matters. Grounding the discussion in real, actual measured numbers, and the real, concrete cost of a bug if the bounds check is genuinely skipped, resolves this kind of disagreement far faster than continuing to debate it in the abstract.
I'd translate the risk into terms leadership already tracks: the cost of a specific past crash or security issue traced back to manual memory handling, and how much longer a typical change in that specific area now takes compared to a genuinely well-audited part of the same codebase. Framed as a reliability and velocity issue with a real, already-incurred cost behind it, it competes far better for prioritization than framed as a general code-quality concern.




