100 C Interview Questions and Answers (2026)

C is having a comeback. As AI and Python tooling lean harder on fast native code, more teams expect engineers who truly understand pointers, memory, and undefined behavior—not hand-wavy answers. Walk into a 2026 interview shaky on these, and you'll get exposed in the first ten minutes.
This guide gives you 100 questions with concise, interview-ready answers and code where it helps. They're ordered Junior → Mid → Senior, so you build from fundamentals to the deep stuff—structure padding, memory safety, evaluation order, concurrency. Work through them and you'll answer like someone who's shipped real C.
Q1.What is the fundamental difference between a struct and a union in terms of memory layout?
struct and a union in terms of memory layout?A struct gives each member its own storage so they coexist, while a union overlays all members in the same storage so only one is valid at a time.
struct:
Members are laid out sequentially (plus padding); size is roughly the sum of members.
All members hold meaningful values simultaneously.
union:
All members share one block of memory; size equals the largest member (rounded for alignment).
Writing one member overwrites the others; you must track which is active yourself (often a tagged union with an enum).
Reading a member other than the last one written is type punning, with its own rules.
Use case: Unions save space for mutually exclusive data or model variant types.
Q2.What is the difference between const char *p, char * const p, and const char * const p?
const char *p, char * const p, and const char * const p?These three differ in what is constant: the data pointed to, the pointer itself, or both. Read them right-to-left around the * to decide.
const char *p : pointer to constant data: You can change p to point elsewhere, but cannot modify *p.
char * const p : constant pointer to mutable data: You can modify *p, but p must point to the same address forever (must be initialized at declaration).
const char * const p : constant pointer to constant data: Neither p nor *p can change.
Reading trick: the const left of * locks the data; the const right of * locks the pointer.
Q3.What is the difference between an array and a pointer in C?
An array is a contiguous block of named storage; a pointer is a variable that holds an address. They feel similar because an array name decays to a pointer to its first element, but they are distinct types.
Storage and identity:
An array allocates all its elements; the name is not an lvalue you can reassign.
A pointer is its own object holding an address, which you can reassign to point anywhere.
sizeof differs: sizeof(arr) gives total bytes of all elements; sizeof(ptr) gives the pointer size (e.g. 8 bytes).
Decay: In most expressions an array name converts to &arr[0], which is why arr[i] and *(arr+i) are equivalent.
Address relationship: &arr and arr yield the same address but different types (int(*)[N] vs int*).
Q4.What is the difference between void * and a null pointer?
void * and a null pointer?They answer different questions: void * is a type (a pointer to an unspecified type), while a null pointer is a value (a pointer that points to nothing). A void * can be null, and a null pointer can be of any pointer type.
void * is about type: A generic pointer that can hold the address of any object; you must cast before dereferencing.
Null pointer is about value: Written NULL or 0 (or nullptr in C23); it is guaranteed to compare unequal to any valid object address.
They are orthogonal: int *p = NULL; is a null int *; void *q = NULL; is a null void *. Dereferencing either null value is undefined behavior.
Q5.What is pointer arithmetic, and why is ptr + 1 different for a char* vs an int*?
ptr + 1 different for a char* vs an int*?Pointer arithmetic moves a pointer by multiples of the size of the type it points to, not by raw bytes. So ptr + 1 advances by sizeof(*ptr) bytes, which differs between a char* and an int*.
Scaling by element size: char * + 1 moves 1 byte; int * + 1 moves typically 4 bytes (one int).
Why it works this way: It lets arr[i] land on element i regardless of type, since indexing is defined as *(arr + i).
Pointer subtraction: p - q yields the number of elements between them (type ptrdiff_t), not bytes.
Validity rule: Arithmetic is only defined within an array (or one past its end); going further is undefined behavior.
Q6.What is a void pointer, and why can't you perform pointer arithmetic on it directly?
A void * is a typeless generic pointer that can hold any object address. You cannot do arithmetic on it because pointer arithmetic scales by the size of the pointed-to type, and void has no size.
The core reason: ptr + 1 needs sizeof(*ptr), but sizeof(void) is undefined, so the step size is unknown.
The fix: Cast to a concrete type first: ((char *)p) + n advances by n bytes.
Caveat: GCC/Clang permit void * arithmetic as an extension (treating it as char *), but it is non-standard.
Q7.What is an enumeration (enum) and how does it differ from a series of #define constants?
enum) and how does it differ from a series of #define constants?An enum defines a set of named integer constants under one type, processed by the compiler. A #define is a textual substitution done by the preprocessor with no type and no scope, so the enum is usually safer and more maintainable.
What an enum is: A user-defined integer type whose members auto-increment from 0 unless you assign values.
Advantages over #define:
Scope: enum names obey block scope and are visible to the debugger by name; macros are global text.
Auto-numbering: no need to hand-assign sequential values.
Grouping: related constants form one named type that documents intent.
Where #define still wins: Macros can hold non-integer values (floats, strings) and are usable in #if preprocessor conditionals; enum constants cannot.
Q8.Why is C often referred to as a 'Mid-level' language rather than a High-level language?
C is called a mid-level language because it combines high-level structured programming (functions, control flow, types) with low-level capabilities (direct memory access, pointers, bit manipulation) that map closely to the hardware.
High-level traits: Readable syntax, functions, loops, structured control flow, and portability across machines.
Low-level traits:
Pointers, manual memory management, and bitwise operators let you manipulate memory and hardware directly.
Maps cleanly to assembly: most C constructs have a predictable machine-code equivalent.
Practical consequence: Used for OS kernels, drivers, and embedded systems where you need control without writing raw assembly.
Caveat: The same low-level power means no garbage collection or bounds checking: errors like buffer overflows are the programmer's responsibility.
Q9.Is the break statement mandatory in a switch case? What happens if you omit it, and when might you do so intentionally?
break statement mandatory in a switch case? What happens if you omit it, and when might you do so intentionally?No, break is not mandatory. If you omit it, execution "falls through" into the next case's statements, which is usually a bug but is sometimes used intentionally to share code between cases.
Default behavior is fall-through: Once a case matches, control continues executing every subsequent statement until a break or the end of the switch.
Accidental omission is a common bug: Forgetting break runs unintended cases; compilers warn with -Wimplicit-fallthrough.
Intentional fall-through:
Grouping multiple labels that share the same action.
Document it (e.g. a /* fall through */ comment or the C23 [[fallthrough]] attribute).
Q10.Explain 'Short-circuit Evaluation' in C logical expressions.
Short-circuit evaluation means the logical operators && and || stop evaluating as soon as the result is known, so the right operand may never run.
How each operator short-circuits:
&&: if the left operand is false (0), the result is false and the right side is skipped.
||: if the left operand is true (non-zero), the result is true and the right side is skipped.
Why it matters: it's a guarantee, not an optimization:
Lets you guard a risky operation, e.g. if (p != NULL && p->x) never dereferences a null pointer.
Evaluation order is strictly left to right with a sequence point between operands.
The trap: side effects: If the right operand has side effects (a function call, i++), they may not happen when short-circuited.
Q11.What is the difference between pre-increment (++i) and post-increment (i++)?
++i) and post-increment (i++)?Both add 1 to the variable; the difference is the value the expression yields: ++i increments first and returns the new value, while i++ returns the old value and then increments.
Pre-increment ++i: Increments, then the expression evaluates to the updated value.
Post-increment i++: Evaluates to the original value, then increments (conceptually needs to keep a copy of the old value).
When it matters:
Only when the result is used: a = i++; vs a = ++i; assign different values to a.
As a standalone statement, i++; and ++i; are identical.
Performance note: For built-in scalar types compilers make them equal; the distinction matters more for C++ iterators where the copy is real.
Q12.Where are string literals stored in memory, and why is modifying them undefined behavior?
String literals are typically stored in a read-only data segment of the program (often .rodata), which the OS may mark non-writable. Attempting to modify one is undefined behavior because you're writing to memory the standard says is not modifiable.
Where they live: Placed in a read-only segment loaded with the executable; identical literals may even be merged into one copy.
Why modification is UB:
The standard gives literals static lifetime but does not permit modification; on many systems the page faults (segfault).
Literal merging means changing one could corrupt unrelated uses of the same string.
The pointer-vs-array trap:
char *p = "hi"; points into read-only memory: writing through p is UB.
char a[] = "hi"; copies the literal into a writable array, which is safe to modify.
Good practice: Declare literal pointers as const char * so the compiler catches accidental writes.
Q13.What is recursion, and what are the tradeoffs of recursive versus iterative solutions in C?
C?Recursion is a function calling itself to solve smaller instances of a problem until reaching a base case. In C it is elegant for naturally recursive structures but costs stack space and call overhead, so iteration is often preferred for performance and safety.
Two essential parts:
A base case that stops the recursion, and a recursive step that moves toward it.
Missing or wrong base case leads to infinite recursion and a stack overflow.
Cost of recursion:
Each call pushes a stack frame (return address, locals), so deep recursion can exhaust the limited stack.
Function-call overhead is higher than a loop's increment/test.
When recursion wins: Tree/graph traversal, divide-and-conquer (quicksort, mergesort), parsers: code closely mirrors the structure.
When iteration wins:
Linear problems and hot loops: no frame overhead, no stack-depth limit.
Tail-recursive code can sometimes be optimized into a loop by the compiler, but C does not guarantee this.
Q14.What is the const keyword used for, and what does const correctness mean in C?
const keyword used for, and what does const correctness mean in C?The const keyword marks an object as read-only after initialization, and const correctness means consistently using const wherever a value is not meant to change, so the compiler enforces your intent and the interface documents itself.
What it does:
Tells the compiler the object should not be modified through that name; an attempted write is a compile error.
Writing through a cast that drops const on a genuinely const object is undefined behavior.
Pointers and const (read right to left):
const int *p: pointer to const data (data fixed, pointer movable).
int *const p: const pointer to mutable data (pointer fixed, data movable).
const int *const p: both fixed.
Why const correctness matters:
A parameter like const char * promises the function will not modify the caller's data.
Lets callers pass const objects and string literals safely, and can enable compiler optimizations.
Q15.When would you use the extern keyword, and what happens at the linking stage if it's missing?
extern keyword, and what happens at the linking stage if it's missing?You use extern to declare that a global variable or function is defined in another translation unit, giving the current file its type and name without allocating storage. If the definition is missing at link time, you get an unresolved-symbol linker error.
Declaration vs definition:
extern int counter; declares the variable (no storage); int counter; in exactly one file defines it.
Functions are extern by default, so it is mostly used for shared global variables.
Typical usage: Put extern declarations in a header, and the single real definition in one .c file.
What goes wrong at link time:
Only an extern declaration but no definition: linker error like "undefined reference".
A real definition in multiple files (forgetting extern): "multiple definition" error.
Q16.What is the difference between a declaration and a definition?
A declaration introduces a name and its type to the compiler; a definition additionally allocates storage (for objects) or provides the body (for functions). You can declare many times, but define exactly once.
Declaration: tells the compiler something exists:
e.g. extern int x; or a function prototype int f(int); : no storage reserved.
Can appear multiple times across translation units without conflict.
Definition: provides the actual entity:
e.g. int x = 5; allocates storage; int f(int a){ return a; } supplies the body.
A definition is also a declaration, so it satisfies the compiler too.
One Definition Rule: Multiple definitions of the same object/function cause linker errors (duplicate symbol).
Practical use: Put declarations in headers (extern, prototypes); put definitions in one .c file.
Q17.Explain the difference between 'Actual Parameters' and 'Formal Parameters'.
Formal parameters are the variables named in a function's definition; actual parameters (arguments) are the concrete values passed when the function is called. In C, arguments are passed by value, so the formal parameters receive copies.
Formal parameters:
The placeholders in the function header, e.g. int add(int a, int b) : a and b.
They are local variables that exist only during the call.
Actual parameters (arguments): The values supplied at the call site, e.g. add(2, x) : 2 and the value of x.
Pass-by-value in C:
The formal parameter is a copy: modifying it does not affect the caller's variable.
To modify the caller's data, pass a pointer (the address is copied, but it still points to the original).
Q18.Explain the difference between the stack and the heap. When would you choose one over the other?
The stack holds function call frames (local variables, return addresses) and is managed automatically in LIFO order; the heap is a pool for dynamic allocation you manage manually. Use the stack for small, short-lived, fixed-size data and the heap for large, long-lived, or runtime-sized data.
Stack:
Allocation/deallocation is automatic on function entry/exit: very fast (just move a pointer).
Limited in size; deep recursion or huge local arrays cause stack overflow.
Lifetime ends when the function returns, so never return a pointer to a local.
Heap:
Allocated explicitly with malloc/free; lives until you free it, independent of function scope.
Much larger, but slower and prone to fragmentation and leaks.
Choosing:
Stack: size known at compile time, small, and scoped to the function.
Heap: size known only at runtime, large, or must outlive the creating function (e.g. returned data structures).
Q19.Explain the difference between malloc() and calloc() in terms of initialization and performance.
malloc() and calloc() in terms of initialization and performance.Both allocate heap memory, but calloc() zero-initializes the block and takes count/size arguments, while malloc() leaves memory uninitialized (garbage). That zeroing is why calloc() can be slower, though not always.
Initialization:
malloc(n): returns memory with indeterminate contents.
calloc(count, size): returns memory set to all zero bytes.
Signature: calloc() multiplies count by size and checks for overflow, making array allocation safer.
Performance:
malloc() avoids the cost of clearing memory.
calloc() pays for zeroing, but large requests often map fresh OS pages that are already zero, so the cost can be near-free.
Rule of thumb: use calloc() when you need zeroed memory anyway; use malloc() when you'll immediately overwrite the contents.
Q20.What happens to a local variable's memory after a function returns?
A local (automatic) variable lives in the function's stack frame, which is popped when the function returns. The memory isn't erased, but it's no longer reserved, so any pointer to it becomes a dangling pointer and using it is undefined behavior.
Lifetime ends at return: The stack pointer is restored, so the frame's space is free to be reused by the next call.
Values may linger: The bytes often still hold the old value until overwritten, which is why use-after-return bugs sometimes "seem to work."
Returning its address is a bug: Returning &local gives a pointer to reclaimed memory: undefined behavior.
Exceptions: static locals live for the program's lifetime; heap memory from malloc() survives the return until you free() it.
Q21.What is 'Stack Overflow,' and what are the most common conceptual causes in a C program?
A stack overflow happens when the call stack grows past its fixed size limit, so the program writes past the stack's bounds and is killed (usually a segfault). It's about exhausting the stack region, not the heap.
Unbounded or deep recursion: Missing/incorrect base case, or recursion depth proportional to large input.
Huge local objects: Declaring large arrays/structs as locals (e.g. char buf[10*1024*1024]) on the stack.
Dynamic stack growth: Large or unchecked alloca() / VLA sizes blow the frame.
Why it's serious:
The stack has a fixed limit (often a few MB); overflow is undefined behavior and a security risk.
Fixes: bound recursion (or make it iterative), move big buffers to the heap, validate sizes.
Q22.What are 'include guards' and what specific problem do they solve in large-scale header inclusion?
Include guards are a preprocessor pattern that prevents the body of a header from being processed more than once per translation unit, solving the duplicate-definition errors that arise when headers include other headers.
The problem: With nested/diamond includes, a header can be pulled in multiple times, causing redefinition of types, structs, and enums (a hard compile error).
How the guard works: On first inclusion the macro is undefined, so the body compiles and defines the macro; on later inclusions the #ifndef is false and the body is skipped.
The macro name must be unique: Collisions silently hide a header, so use a project/path-based name.
Alternative: #pragma once is simpler and common but non-standard (though widely supported).
Q23.What is the difference between a typedef and a #define for creating aliases?
typedef and a #define for creating aliases?typedef creates a true type alias known to the compiler, while #define is a blind textual replacement done by the preprocessor; for naming types, typedef is almost always the correct choice.
Scope and semantics: typedef obeys C scope rules and is a real type the compiler understands; #define is just text substitution with no scope.
The pointer trap: With #define PTR int*, PTR a, b; makes only a a pointer; a typedef makes both pointers as expected.
Expressiveness: typedef can name complex types like function pointers and arrays that text macros handle poorly.
Use #define for: Constants and conditional-compilation tokens, not type names.
Q24.What are the tradeoffs of using a #define macro versus a const variable?
#define macro versus a const variable?A #define constant is substituted as text with no type and no storage, while a const variable is a real, typed object the compiler checks; const is generally safer, but macros are needed where a true compile-time constant expression is required.
Type safety and debugging: const has a type, respects scope, and is visible to the debugger; a macro is untyped text that produces confusing errors at the expansion site.
Where each is usable: In C, a const int is not a constant expression, so it can't size an array or be a case label; a #define (or enum) can.
Storage and scope: A macro has no address and global reach across the file; a const may occupy storage and follows normal scoping.
Rule of thumb: Prefer const (or enum for integer constants); reach for #define only when you truly need a preprocessor constant expression.
Q25.Why are #define macros considered unsafe compared to inline functions?
#define macros considered unsafe compared to inline functions?Macros are pure text substitution done by the preprocessor: they have no type checking, no scope, and can re-evaluate their arguments, while inline functions behave like real functions (typed, single-evaluation) with the same speed benefit.
No type checking: A macro accepts anything text-substitutable; an inline function enforces parameter and return types.
Multiple argument evaluation: #define MAX(a,b) ((a)>(b)?(a):(b)) evaluates one argument twice, so MAX(i++, j) increments twice. An inline function evaluates each argument exactly once.
Precedence and parenthesization bugs: Forgetting parentheses (#define SQ(x) x*x then SQ(a+b)) produces a+b*a+b. Functions have normal expression semantics.
No scope or debugging: Macros ignore scope and don't appear in the debugger or symbol table; inline functions do.
When macros still win: Generic type-agnostic code, conditional compilation, and stringizing/token-pasting, things functions can't do.
Q26.What is the difference between #include <file.h> and #include "file.h"?
#include <file.h> and #include "file.h"?Both include a header, but the angle-bracket form searches the system/implementation include paths first, while the quoted form searches the current file's directory first before falling back to the system paths.
#include <file.h>:
Used for standard library and system headers.
Searches the implementation-defined system directories (and any added with -I).
#include "file.h":
Used for your own project headers.
Searches the directory of the including file first, then falls back to the system search path if not found.
Practical takeaway: The exact search order is implementation-defined, but the convention is: <> for external/standard headers, "" for local project headers.
Q27.Explain bitwise operations. Why are they critical for systems programming?
Bitwise operators manipulate individual bits of integers, letting you pack flags, control hardware, and do fast arithmetic. They're central to systems programming because hardware registers, protocols, and memory layouts are defined bit by bit.
The operators:
AND &: masks/clears bits and tests if a bit is set.
OR |: sets bits.
XOR ^: toggles bits.
NOT ~: inverts all bits (used to build masks).
Shifts << / >>: move bits, also fast multiply/divide by powers of two.
Why systems code relies on them:
Hardware registers expose control/status as individual bits you set or clear without disturbing neighbors.
Flags pack many booleans into one word, saving memory and enabling single-instruction tests.
Protocols and binary formats define fields at bit offsets that must be extracted/assembled exactly.
Common idioms: Set: reg |= (1u << n); clear: reg &= ~(1u << n); toggle: reg ^= (1u << n); test: reg & (1u << n).
Caveat: Use unsigned types: right-shifting signed negatives and shifting into the sign bit are implementation-defined or undefined.
Q28.Why is it generally considered dangerous to return the address of a local variable from a function?
Because a local (automatic) variable lives on the stack only for the duration of the function call: once the function returns, that stack space is reclaimed, so the returned address becomes a dangling pointer to memory that may be overwritten at any moment.
Lifetime ends at return: Automatic storage is valid only inside its scope; the stack frame is torn down on return.
Undefined behavior: It may appear to work (the value lingers briefly), then break unpredictably when the next call reuses the stack: classic hard-to-find bug.
Safe alternatives: Return by value, allocate on the heap with malloc() (caller frees), use a caller-supplied buffer, or use static storage if a single shared instance is acceptable.
Note: Returning the address of a static local or a string literal is fine: those have longer lifetime.
Q29.What is a dangling pointer, and what happens if you dereference it?
A dangling pointer is a pointer that still holds the address of memory that has already been freed or whose lifetime has ended. Dereferencing it is undefined behavior: the result is unpredictable and unsafe.
How they arise: Using a pointer after free(), or keeping the address of a local variable past its scope.
What dereferencing can do:
Appear to work if the memory hasn't been reused yet (dangerously misleading).
Return garbage, since the slot may now hold unrelated data.
Corrupt live data if you write through it (especially after the allocator reuses the block).
Crash with a segmentation fault, or enable security exploits (use-after-free).
Prevention: Set the pointer to NULL after freeing so misuse fails loudly rather than silently.
Q30.What is a NULL pointer, and how does it differ from an uninitialized pointer?
A NULL pointer is a pointer deliberately set to a special value (guaranteed not to point to any valid object) so it can be tested; an uninitialized pointer holds an indeterminate garbage value and points nowhere predictable. The key difference is intent and testability.
NULL pointer:
A well-defined sentinel value (NULL, typically 0) meaning "points to nothing."
Safe to compare: if (p == NULL) lets you detect and handle the empty case.
Dereferencing it is still undefined behavior, but it usually faults predictably.
Uninitialized pointer:
Contains whatever was on the stack/heap: an unknown garbage address (a wild pointer).
You cannot test it meaningfully; dereferencing may corrupt random memory or crash unpredictably.
Practice: Always initialize pointers, often to NULL, so an unset pointer is detectable rather than wild.
Q31.What is the difference between strcpy and strncpy, and why is the latter often considered safer but still potentially dangerous?
strcpy and strncpy, and why is the latter often considered safer but still potentially dangerous?Both copy a string, but strcpy copies until the null terminator with no length limit, while strncpy copies at most n bytes. strncpy is safer because it bounds the write, but it can leave the destination without a null terminator, which is its hidden danger.
strcpy: Copies bytes until '\0'; if the source is longer than the destination buffer, it overflows it (classic buffer overflow / security hole).
strncpy:
Writes exactly n bytes: if the source is shorter it pads with '\0'; if it is n or longer, it copies n bytes and writes no terminator.
That unterminated result causes later reads (e.g. another strlen) to run off the end.
Safe practice: Manually terminate after copying, or use snprintf / strlcpy which always null-terminate.
Q32.Explain 'Structure Padding' and 'Alignment.' Why does the sizeof a struct often exceed the sum of its members?
sizeof a struct often exceed the sum of its members?Alignment is a hardware requirement that an object of size N sits at an address that is a multiple of its alignment; padding is the unused bytes the compiler inserts between members (and at the end) to satisfy it. That's why sizeof a struct often exceeds the sum of its members.
Alignment:
Each type has an alignment (often equal to its size): a 4-byte int typically must start at an address divisible by 4.
Misaligned access is slower or even a fault on some architectures.
Internal padding: Bytes inserted before a member to align it: a char followed by an int gets 3 padding bytes after the char.
Trailing padding: The struct's total size is rounded up to a multiple of its largest member's alignment so arrays of the struct stay aligned.
Practical levers:
Reordering members largest-to-smallest minimizes padding.
Query with _Alignof / alignof and force with _Alignas.
Q33.What is the difference between 'Deep Copy' and 'Shallow Copy' when dealing with structs containing pointers?
A shallow copy duplicates the struct's bytes, so pointer members point to the same underlying buffer; a deep copy also allocates and copies what those pointers reference, producing fully independent objects.
Shallow copy:
Plain assignment or memcpy copies the pointer value, not the pointee.
Both structs share the same heap memory: a mutation through one is seen by the other.
Danger of double-free and dangling pointers when one copy frees the buffer.
Deep copy:
You malloc new storage and copy the contents so each struct owns its own data.
Safe to free independently; changes don't leak across copies.
Rule of thumb: Any struct owning heap resources needs an explicit deep-copy function; C gives you no automatic copy semantics.
Q34.Explain the difference between Structure Packing and Structure Padding. When is packing required?
They are opposites: padding is bytes the compiler adds to keep members aligned, while packing tells the compiler to remove that padding and lay members back-to-back. Packing is required when the in-memory layout must match an external, byte-exact format.
Structure padding (default): Compiler inserts gaps so each member meets its alignment, trading space for fast aligned access.
Structure packing (opt-in):
Directives like #pragma pack(1) or __attribute__((packed)) force zero (or reduced) padding.
Minimizes size but can produce misaligned members, which slow access or fault on strict architectures.
When packing is required:
Mapping structs onto fixed network packet or file formats where every byte offset is defined.
Overlaying memory-mapped hardware registers.
Otherwise avoid it: the safer portable path is reading/writing fields with explicit offsets or serialization.
Q35.What is the purpose of a void* pointer, and what are the risks of using it?
void* pointer, and what are the risks of using it?A void * is C's generic pointer: it can hold the address of any object type, which enables type-agnostic APIs. The cost is that it carries no type information, so the compiler can no longer check correctness for you.
Its purpose: Generic interfaces: malloc returns void *, memcpy and qsort take void * so they work on any type.
The risks:
No type safety: nothing stops you casting to the wrong type and misreading memory.
Cannot be dereferenced directly; you must cast to a concrete type first.
Cannot do arithmetic on it portably (its element size is unknown).
In C, the cast from void * is implicit, so casting the return of malloc is unnecessary and can mask a missing header.
Q36.Explain the concept of 'Pointer Decay' when passing an array to a function.
Pointer decay is the automatic conversion of an array into a pointer to its first element when passed to a function. The callee receives only an address, losing the array's size and type, which is why you must pass the length separately.
What happens at the call: f(arr) passes &arr[0]; a parameter written int a[] is silently treated as int *a.
The consequence: Inside the function sizeof(a) gives the pointer size, not the array size, so length must be a separate parameter.
What does NOT decay: sizeof, &array, and a string literal used to initialize a char[] keep the full array type.
Q37.Why doesn't C support function overloading, and how do developers typically work around this limitation?
C doesn't support function overloading because its early, simple linker model identifies functions by name alone (no name mangling), so two functions with the same name would collide.
Reason: simple symbol resolution:
The compiler emits a symbol using just the function name, so the linker cannot distinguish add(int) from add(double).
C++ solves this with name mangling (encoding parameter types into the symbol); C keeps the simpler model.
Workaround: distinct names: Suffix the type, e.g. abs(), labs(), fabs() in the standard library.
Workaround: variadic functions: Use <stdarg.h> for variable arguments (like printf).
Workaround: type-generic macros: C11's _Generic selects an implementation based on argument type at compile time.
Q38.What is the difference between signed and unsigned integers, and what happens on overflow for each?
signed and unsigned integers, and what happens on overflow for each?A signed integer can represent negative and positive values (one bit effectively encodes sign), while an unsigned integer represents only non-negative values, doubling the positive range. They differ critically on overflow: unsigned wraps predictably, signed overflow is undefined behavior.
Range difference: A 32-bit int spans about -2.1 billion to +2.1 billion; unsigned int spans 0 to about 4.3 billion.
Unsigned overflow is defined: Arithmetic wraps modulo 2^N, so UINT_MAX + 1 becomes 0. This is guaranteed by the standard.
Signed overflow is undefined behavior: INT_MAX + 1 has no defined result; the compiler may assume it never happens and optimize accordingly, causing surprising behavior.
Practical pitfall: Mixing signed and unsigned in comparisons triggers implicit conversion; a negative int becomes a huge unsigned, breaking loops like for (unsigned i = n; i >= 0; i--) (never ends).
Q39.What is two's complement, and how does C represent negative integers?
C represent negative integers?Two's complement is the standard binary encoding for signed integers: a negative value -n is stored as the bitwise complement of n plus one, so addition and subtraction work with the same hardware as unsigned numbers.
How a value is encoded:
The high (most significant) bit is the sign: 0 for non-negative, 1 for negative.
To negate: invert all bits then add 1 (e.g. for 8-bit, 5 = 0000_0101, so -5 = 1111_1011).
Why it is used:
One unique representation of zero (unlike sign-magnitude or ones' complement).
Add/subtract circuitry is identical to unsigned arithmetic, so no special hardware is needed.
Asymmetric range: An N-bit signed type holds -2^(N-1) to 2^(N-1)-1 (e.g. int8_t: -128 to 127), one more negative value than positive.
C standard note:
C23 mandates two's complement; earlier standards allowed three representations, but two's complement was universal in practice.
Signed overflow is still undefined behavior even though the representation is fixed.
Q40.Explain the three different meanings of the static keyword in C depending on its context.
static keyword in C depending on its context.The static keyword has three distinct meanings in C, all about either lifetime or linkage, depending on where it appears.
On a local variable (inside a function):
Gives it static storage duration: it persists across calls and keeps its value, initialized once.
Still has local scope: only visible inside that function.
On a global variable or function (file scope):
Gives internal linkage: the symbol is private to its translation unit and invisible to the linker elsewhere.
Used to hide helpers and avoid name clashes across files.
In a function parameter array (C99+): Like void f(int a[static 10]): promises the caller passes a pointer to at least 10 elements, enabling optimizations.
Q41.What does the register keyword suggest to the compiler, and is it still relevant in modern C development?
register keyword suggest to the compiler, and is it still relevant in modern C development?The register keyword is a hint asking the compiler to keep a variable in a CPU register for fast access, but modern optimizing compilers ignore the hint and allocate registers far better themselves, so it is essentially obsolete.
What it requests: Suggests frequent use, so store it in a register if possible: just advice, the compiler may ignore it.
The one real constraint: You cannot take the address (&) of a register variable, since it may not live in memory.
Relevance today:
Modern compilers do register allocation automatically, usually better than manual hints.
Effectively a no-op now; C++ deprecated and removed it, though C still allows it.
Q42.What is the difference between a static local variable and a static global variable?
static local variable and a static global variable?Both use the static keyword and both have static storage duration, but they differ in scope and linkage: a static local restricts a function-scoped variable's visibility, while a static global restricts a file-scoped symbol's linkage.
Static local variable:
Declared inside a function; visible only in that function.
Retains its value between calls and is initialized only once.
Static global variable:
Declared at file scope; visible throughout the file but has internal linkage.
Cannot be accessed from other translation units (not even via extern).
Shared trait: Both live for the entire program run and are zero-initialized by default.
Q43.What is the difference between a translation unit and a source file?
A source file is the .c file you write; a translation unit is what the compiler actually compiles: that source file after the preprocessor has run, with all #include headers inserted and macros expanded.
Source file: A single physical file of C code, typically a .c (headers are .h).
Translation unit:
The logical unit of compilation: one .c plus everything its #include directives pull in, after preprocessing.
The compiler turns each translation unit into one object file, which the linker later combines.
Why the distinction matters:
Concepts like internal linkage (static) and the one-definition rule are defined per translation unit, not per source file.
A header is not a translation unit; it only becomes part of one when included.
Q44.What is the difference between extern and static for global variables?
extern and static for global variables?For globals, the keywords control linkage and visibility across files: static restricts a global to its own translation unit (internal linkage), while extern declares a global defined elsewhere so it can be shared across files (external linkage).
static at file scope: internal linkage:
The variable is private to the .c file; other files can't reference it, even with the same name.
Useful for hiding implementation state and avoiding name clashes.
extern: external linkage / declaration:
Declares that a variable is defined in another translation unit, letting files share one instance.
It is a declaration, not a definition: storage is allocated where the variable is defined without extern.
Default for a plain global: A file-scope variable without static already has external linkage; extern in headers lets other files see it.
Q45.How does the auto keyword in C23 differ from its historical meaning in older C standards?
auto keyword in C23 differ from its historical meaning in older C standards?In C23 auto becomes a type-inference keyword (like in C++): it deduces a variable's type from its initializer. Historically auto was just a storage-class specifier meaning "automatic (local) storage," which was the default and therefore useless.
Historical meaning (pre-C23):
A storage-class specifier marking a variable as automatic (stack-allocated, block-scoped).
Redundant: local variables are automatic by default, so almost nobody wrote it.
C23 meaning:
Used as a type, the compiler infers the type from the initializer, so auto x = 3; makes x an int.
Requires an initializer (there is nothing to deduce from otherwise).
Why the change is safe: Since the old use was always redundant, repurposing the keyword breaks essentially no real code.
Q46.What is the difference between 'Internal Linkage' and 'External Linkage'?
Linkage determines whether a name refers to the same entity across translation units. Internal linkage means the name is visible only within its own file; external linkage means it can be referenced from other files at link time.
Internal linkage:
Achieved with static at file scope; the name is unique to that translation unit.
Two files can each define static int x; with no conflict: they are distinct objects.
External linkage:
The default for non-static globals and functions; one definition is shared program-wide.
Other files reach it via an extern declaration.
No linkage: Local variables (block scope, non-extern) have no linkage: each is a separate entity.
Why it matters: Internal linkage encapsulates helpers and avoids polluting the global namespace; external linkage enables sharing across the program.
Q47.What is a memory leak, and how do you detect or prevent it in a C program?
A memory leak is heap memory that was allocated (e.g. via malloc) but never freed and is no longer reachable, so it stays reserved until the program exits. Over time leaks grow the process's memory footprint and can exhaust RAM.
How leaks happen:
Losing the only pointer to a block (overwriting it, returning without freeing, early return on an error path).
Forgetting to free before reassigning realloc results, or losing a node in a linked structure.
Prevention:
Pair every malloc/calloc with a matching free; make ownership clear (who frees what).
Use single-exit cleanup (goto cleanup idiom) so error paths still free resources.
Set freed pointers to NULL to avoid double-free / dangling use.
Detection tools: Valgrind (memcheck) and AddressSanitizer (-fsanitize=address) report leaks and where blocks were allocated.
Q48.What is the difference between a microcontroller and a microprocessor in the context of C programming?
A microprocessor is just a CPU that needs external memory and peripherals; a microcontroller integrates CPU, RAM, flash, and peripherals on one chip. For C, this mostly affects the runtime environment, memory size, and whether you even have an OS or standard library.
Microprocessor (e.g. desktop/server CPU):
Runs under an OS with virtual memory, a full libc, and a large heap.
C code uses malloc(), file I/O, threads freely.
Microcontroller (e.g. AVR, ARM Cortex-M):
Often bare-metal: no OS, tiny RAM (kilobytes), code runs from flash.
You access hardware via memory-mapped registers, often with volatile pointers.
Dynamic allocation is often avoided; you favor static buffers and interrupt handlers.
Practical impact on C: On a microcontroller you mind stack size, avoid heap fragmentation, and may lack a full standard library.
Q49.What is the memory layout of a C program? Explain the text, data, BSS, heap, and stack segments.
A typical C program's address space is divided into segments: read-only code, initialized and uninitialized globals, a heap that grows up, and a stack that grows down. This separation lets the OS set permissions and lets the two dynamic regions grow toward each other.
Text (code) segment: Compiled machine instructions; usually read-only and shareable between processes.
Data segment: Initialized globals and static variables with nonzero initializers; stored in the binary.
BSS segment: Uninitialized (or zero-initialized) globals/statics; takes no file space, zeroed at startup.
Heap: Dynamic memory from malloc()/free(); grows upward toward higher addresses.
Stack:
Function call frames: locals, return addresses, saved registers; grows downward.
LIFO, automatically managed per call/return.
Memory map note: the heap and stack grow toward each other, with command-line args/environment typically at the top.
Q50.How does realloc work, and what are the pitfalls of using it incorrectly?
realloc work, and what are the pitfalls of using it incorrectly?realloc() resizes a previously allocated block, possibly moving it to a new address and returning the new pointer; the old contents are preserved up to the smaller of the old and new sizes.
What it does:
May grow/shrink in place, or allocate a new block, copy the data, and free the old one.
Newly grown bytes are uninitialized (not zeroed).
The classic leak pitfall:
Writing p = realloc(p, n): if it returns NULL, you've lost the original pointer and leaked the old block.
Assign to a temporary, check it, then commit.
Dangling pointers: If the block moves, any other pointers or saved indices into the old memory become invalid.
Edge cases: realloc(NULL, n) behaves like malloc(n); realloc(p, 0) is implementation-defined (may free and return NULL).
Q51.What is size_t, and why should it be used for sizes and array indices?
size_t, and why should it be used for sizes and array indices?size_t is the unsigned integer type returned by sizeof and used by the standard library for object sizes and counts; it is guaranteed large enough to represent the size of any object the platform supports.
Portability: Its width matches the platform's addressing (32-bit vs 64-bit), so it scales without code changes, unlike int.
Correctness with the standard library: malloc(), strlen(), and sizeof all use size_t; matching types avoids truncation and signed/unsigned warnings.
It is unsigned:
A size can never be negative, so the type encodes that invariant.
Trap: counting down past 0 wraps to a huge value, so for (size_t i = n-1; i >= 0; i--) loops forever.
Print with %zu: Use the z length modifier so printf matches the type correctly.
Q52.Can you walk through the four stages of the C compilation process (Preprocessing, Compilation, Assembly, Linking)?
C source becomes an executable through four ordered stages: the preprocessor expands text, the compiler turns C into assembly, the assembler turns assembly into object code, and the linker combines object files and libraries into the final binary.
Preprocessing: Handles directives: #include file insertion, #define macro expansion, and #if conditionals; comments are stripped. Output is a single translation unit (.i).
Compilation: Parses and type-checks the translation unit and emits architecture-specific assembly (.s); most optimization happens here.
Assembly: The assembler translates assembly into machine-code object files (.o) containing a symbol table, with unresolved external references.
Linking: Resolves symbols across object files and libraries, lays out the final image, and produces the executable; this is where "undefined reference" errors appear.
Q53.Explain the difference between a macro and an inline function. When would you prefer one over the other?
inline function. When would you prefer one over the other?A macro is textual substitution done by the preprocessor before compilation, while an inline function is real C code the compiler may expand at the call site; prefer inline functions when you want type safety and prefer macros only for things functions can't do.
Macros (#define):
No type checking, no scope, and can act on types or tokens, not just values.
Pitfalls: double evaluation of arguments and precedence bugs; always parenthesize.
Inline functions:
Full type checking, single argument evaluation, real scope, and debuggable.
inline is a hint; the compiler decides whether to actually inline.
When to choose:
Prefer inline functions for typed computations.
Use macros for compile-time constants, conditional compilation, or generic/type-agnostic constructs.
Q54.Explain the purpose of the constexpr keyword in C23. How does it differ from a #define macro?
constexpr keyword in C23. How does it differ from a #define macro?C23's constexpr lets you define true compile-time constants that are typed, scoped, and known to the compiler, unlike a textual #define which is just blind preprocessor text substitution.
Purpose of constexpr:
Declares an object whose value is a compile-time constant expression, usable where a constant is required (array sizes, case labels, etc.).
In C23 it applies to objects (and enumeration constants), giving a named, immutable value computed at compile time.
Key differences from #define:
Type safety: constexpr int x = 5; has a real type; a macro is just text the compiler never sees as a typed entity.
Scope: constexpr obeys normal block/file scope; macros leak everywhere until #undef.
No double-evaluation or precedence surprises that plague macros (e.g. #define SQ(x) x*x).
Better diagnostics: errors point to a typed declaration, and a non-constant initializer is rejected at compile time.
Relation to const: const means read-only but not necessarily a compile-time constant; constexpr guarantees the value is known at compile time.
Q55.What is a function pointer, and what is a real-world use case for them such as callbacks or jump tables?
A function pointer is a variable that stores the address of a function, letting you call it indirectly and pass behavior around as data. This enables callbacks and jump tables (dispatch by index instead of long if/switch chains).
Syntax: int (*fp)(int, int); declares a pointer to a function taking two int and returning int; assign with fp = &add; and call with fp(2,3).
Callbacks: Pass a function to library code so it can call you back, e.g. the comparator in qsort, event/signal handlers, or threading APIs.
Jump tables: An array of function pointers indexed by a code (opcode, menu choice, state), giving O(1) dispatch and cleaner code than a big switch.
Other uses: Plugin/strategy patterns and emulating polymorphism via structs of function pointers (vtables).
Q56.What is 'Undefined Behavior' in C, and why is it more dangerous than a compiler error?
Undefined behavior (UB) is any operation the C standard places no requirements on: the compiler may do literally anything, and crucially it may assume UB never happens and optimize on that assumption. It is more dangerous than a compile error because nothing forces it to fail, so the bug can hide for years.
Common sources: Out-of-bounds access, dereferencing NULL or freed pointers, signed integer overflow, data races, and reading uninitialized values.
Why it's worse than a compiler error:
A compile error stops the build and points at the line; UB compiles cleanly and runs.
Behavior may vary by compiler, optimization level, or platform, so it can "work" in debug and break in release.
Optimizers assume UB can't occur and may delete checks or reorder code, causing surprising, non-local failures and security holes.
Defense: Enable warnings (-Wall -Wextra), use sanitizers (-fsanitize=undefined,address), and static analysis.
Q57.How does printf handle a variable number of arguments? Explain the concept of va_list.
printf handle a variable number of arguments? Explain the concept of va_list.Functions like printf use variadic arguments via the <stdarg.h> macros: the format string tells the function how many arguments to expect and their types, and va_list walks the stack/registers to fetch them one by one.
Declared with an ellipsis: The prototype is int printf(const char *fmt, ...): at least one named parameter, then variable args.
va_list plus its macros do the work:
va_start(ap, fmt): initializes the cursor after the last named argument.
va_arg(ap, type): reads the next argument; you must supply the correct type.
va_end(ap): cleans up before returning.
The format string drives parsing:
printf scans conversions like %d / %s to know how many args and what types to pull.
There is no type or count safety: a wrong specifier reads garbage or causes undefined behavior.
Default argument promotions apply: char/short promote to int, and float promotes to double, which is why va_arg never uses those narrow types.
Q58.Does the sizeof() operator evaluate the expression passed to it at runtime? Why or why not?
sizeof() operator evaluate the expression passed to it at runtime? Why or why not?No: sizeof is almost always a compile-time operator that yields the size of a type, so it evaluates only the operand's type, not its value. The expression inside is not actually executed.
It works on types, not effects: The compiler computes the size from the static type and discards side effects: sizeof(i++) does not increment i.
Result type is size_t: It is an unsigned compile-time constant, usable in array dimensions and other constant contexts.
The one exception: VLAs: For a variable-length array, the size depends on a runtime length, so sizeof is evaluated at runtime, but the operand's value still isn't "executed" in the side-effect sense.
Q59.Explain the volatile keyword. In what specific scenarios would a compiler's optimization break code if this keyword is missing?
volatile keyword. In what specific scenarios would a compiler's optimization break code if this keyword is missing?volatile tells the compiler that a variable's value can change outside the normal program flow, so it must not cache it in a register or optimize away reads/writes: every access must go to actual memory in program order.
Memory-mapped hardware registers: Reading a device status register has side effects or changes between reads; without volatile the compiler may read once and reuse the stale value.
Variables modified by signal handlers: A flag set in a handler must be volatile sig_atomic_t or the main loop may spin forever on a cached copy.
Shared with another thread (limited): Prevents the optimizer from hoisting a polled flag out of a loop, but volatile is NOT a substitute for _Atomic or memory barriers: it gives no atomicity or ordering guarantees across cores.
How optimization breaks without it: while (flag) {} can become an infinite loop because the compiler reads flag once into a register and never re-reads memory.
Q60.What is the difference between static linking and dynamic linking, and what are the tradeoffs regarding binary size and updateability?
Static linking copies library code into the executable at build time; dynamic linking leaves references that are resolved at load/run time from shared libraries (.so / .dll). The tradeoff is self-containment and speed versus binary size and ease of updates.
Static linking:
Each binary contains its own copy, so it's self-contained and has no runtime dependency mismatch.
Larger binaries and duplicated code across programs; a library bug fix requires relinking and redeploying every binary.
Dynamic linking:
One shared copy in memory across processes: smaller binaries, lower aggregate memory.
Update the shared library once and all programs benefit (security patches); but risks "DLL hell" if versions/ABIs are incompatible or the library is missing.
Performance nuance: Dynamic linking adds load-time symbol resolution and indirection (PLT/GOT); static avoids that but can't share pages.
Rule of thumb: Static for portability and reproducible deploys; dynamic for memory sharing and centralized updates.
Q61.Explain the difference between 'Pass by Value' and 'Pass by Reference' using pointers and the architectural tradeoffs of each.
C is always pass by value: a function receives a copy of each argument. "Pass by reference" is simulated by passing a pointer (a copy of an address), which lets the callee modify the caller's data through that address.
Pass by value:
The callee gets an independent copy; changes don't affect the caller's variable.
Copying large structs costs time and stack space.
"Pass by reference" via pointers:
You pass &var; the callee dereferences (*p) to read or mutate the original.
Cheap for large objects: you copy only a pointer regardless of object size.
Architectural tradeoffs:
Value: safe (no aliasing, no unexpected mutation) but expensive for big data.
Pointer: efficient and enables output parameters, but adds aliasing, null-pointer, and lifetime/ownership risks; use const T * to pass large data cheaply yet read-only.
Q62.What is endianness, and why does it matter when writing C code that communicates over a network or reads binary files?
C code that communicates over a network or reads binary files?Endianness is the order in which a multi-byte value's bytes are stored in memory: big-endian stores the most significant byte first, little-endian the least significant first. It matters because two machines (or a file format) may disagree, so raw bytes read on one can be misinterpreted on another.
The two orders:
Big-endian: 0x12345678 stored as bytes 12 34 56 78.
Little-endian (x86, most ARM): stored as 78 56 34 12.
Why it matters:
Network protocols use big-endian ("network byte order"); a little-endian host sending raw ints garbles them.
Binary file formats fix a byte order; reading on the wrong-endian machine yields wrong values.
How to handle it:
Convert at boundaries with htons()/htonl() and ntohs()/ntohl().
Serialize explicitly byte-by-byte rather than memcpy-ing a struct, which also avoids padding issues.
Q63.What is an Interrupt Service Routine (ISR), and why can't you pass parameters to it or return values from it?
An ISR is a function the CPU calls automatically when a hardware (or software) interrupt fires, via an entry in the interrupt vector table. It takes no parameters and returns no value because the hardware, not a normal caller, invokes it: there's no caller to supply arguments or receive a result.
How it's invoked: On an interrupt the CPU saves context, looks up the ISR address in the vector table, and jumps to it, ending with a special return-from-interrupt instruction (not a normal return).
Why no parameters: There's no call site to set up arguments; the signature must match what the hardware vector expects (typically void f(void)).
Why no return value: Nobody is waiting to consume a result; control simply returns to whatever code was interrupted.
How it communicates instead: Through shared volatile variables, flags, or queues that main code polls or consumes later.
Q64.What is a Watchdog Timer, and how does software interact with it in C?
C?A watchdog timer is a hardware countdown timer that resets the system if it isn't periodically refreshed ("kicked"). It's a safety net: if firmware hangs or loops forever and stops kicking it, the watchdog times out and forces a reboot to recover.
How it works:
You configure a timeout, then healthy code writes to a watchdog register before it expires to restart the count.
If the deadline is missed, the watchdog triggers a hardware reset.
Software interaction in C:
Enable and set the period at init, then periodically "kick" it from the main loop or a trusted task.
Done via MMIO: write a magic value or bit to the watchdog register through a volatile pointer.
Good-practice notes:
Don't kick it blindly from a timer ISR: that defeats the purpose by hiding a hung main loop.
Kick only after verifying key tasks are actually making progress.
Q65.Explain the difference between 'Polling' and 'Interrupt-driven' I/O.
Both are ways to move data between the CPU and a device, but they differ in how the CPU learns the device is ready: polling actively checks, while interrupt-driven I/O lets the device signal the CPU.
Polling (busy-waiting):
The CPU repeatedly reads a status register in a loop until the device is ready.
Simple and deterministic, but wastes CPU cycles spinning when the device is slow.
Good for fast devices or tight, time-critical loops where the wait is short.
Interrupt-driven I/O:
The device raises an interrupt when ready; the CPU does other work meanwhile and jumps to an ISR (interrupt service routine) on the signal.
Efficient CPU usage, but adds latency and overhead from context switching and ISR dispatch.
Better for slow or sporadic events (keyboard, network packets).
Trade-off summary: Polling trades CPU time for simplicity and predictability; interrupts trade complexity for CPU efficiency.
Q66.What is a dangling pointer, and how does it differ from a wild pointer?
A dangling pointer points to memory that was once valid but has since been freed or gone out of scope; a wild pointer is one that was never initialized to point anywhere meaningful. Both are dangerous, but the distinction is about history versus origin.
Dangling pointer:
Held a valid address, but the object was deallocated (free()) or its scope ended.
The pointer value is unchanged but now references invalid memory.
Wild pointer:
Declared but never assigned, so it holds an indeterminate (garbage) value.
Example: int *p; then using *p before assigning it.
Common defense: Initialize pointers to NULL, and set them back to NULL right after free() to turn silent corruption into a detectable crash.
Q67.What is a 'Buffer Overflow' at a conceptual level, and how does C's design contribute to this vulnerability?
A buffer overflow happens when a program writes more data into a buffer than it can hold, spilling into adjacent memory. C is especially prone to this because it performs no automatic bounds checking on array or pointer accesses, trusting the programmer entirely.
The mechanism:
Writing past a buffer's end corrupts neighboring data, other variables, or control structures.
On the stack it can overwrite the saved return address, letting an attacker redirect execution (stack smashing).
Why C enables it:
No bounds checking: arr[i] is just pointer arithmetic; out-of-range indices are undefined behavior, not errors.
Unsafe library functions like strcpy(), gets(), and sprintf() don't track destination size.
C favors performance and low-level control over safety.
Mitigations: Use bounded functions (strncpy(), snprintf()), validate lengths, and rely on OS/compiler defenses (stack canaries, ASLR, NX/DEP).
Q68.What is the difference between nullptr (C23) and the traditional NULL macro?
nullptr (C23) and the traditional NULL macro?Both represent a null pointer, but nullptr (added in C23) is a true typed null-pointer constant, while NULL is a macro that is typically defined as 0 or ((void*)0). nullptr removes the type ambiguity that NULL can cause.
The traditional NULL macro:
Implementation-defined: often ((void*)0) or just 0.
Because it can be an integer constant, it may be mistaken for the number 0 in contexts like variadic functions, causing subtle bugs.
C23 nullptr:
A keyword of its own type nullptr_t, representing the null pointer unambiguously.
It is never an integer, so it can't be confused with 0 and is clearer in type-sensitive contexts (e.g. _Generic, variadic arguments).
Takeaway: Both convey "no object," but nullptr is type-safe and explicit, aligning C with the equivalent C++ feature.
Q69.What is a double free and a use-after-free, and why are they dangerous?
Both are heap-memory bugs involving freed memory: a double free calls free() twice on the same pointer, and a use-after-free dereferences memory after it has been freed. Both are undefined behavior and are prime targets for security exploits.
Double free:
Freeing the same block twice corrupts the allocator's internal free-list/metadata.
Attackers can manipulate that corruption to control later allocations (arbitrary write).
Use-after-free:
The pointer becomes dangling; the memory may be reused for another object, so reads/writes hit unrelated data.
If an attacker reallocates that slot with crafted data (e.g. a fake vtable), it can lead to code execution.
Why dangerous: Behavior is nondeterministic: may crash, corrupt silently, or be exploited.
Defenses:
Set pointers to NULL after freeing (free(NULL) is a safe no-op).
Establish clear ownership and use tools like AddressSanitizer or Valgrind.
Q70.Explain the concept of 'L-value' and 'R-value' in the context of C expressions.
An lvalue is an expression that designates an object (a storage location) and can appear on the left of an assignment; an rvalue is just a value, with no addressable location guaranteed. The distinction governs what can be assigned to and what can have its address taken.
Lvalue:
Refers to an object: variables, *p, arr[i], s.field.
You can take its address with & and assign to it (unless const).
Rvalue:
A pure value: literals like 42, and results of expressions like a + b.
Cannot be assigned to: a + b = 3; is an error.
Conversion: An lvalue decays to an rvalue when its value is read (lvalue-to-rvalue conversion); the reverse does not happen.
Modifiable lvalue: Assignment requires a modifiable lvalue: arrays and const objects are lvalues but not modifiable.
Q71.What is Undefined Behavior (UB)? Give three examples of common C operations that result in UB.
Undefined behavior is program behavior for which the C standard imposes no requirements: the compiler is free to do anything, and crucially it may optimize on the assumption that UB never occurs. This makes UB bugs unpredictable and security-sensitive.
Out-of-bounds access: Reading/writing past an array's end, e.g. a[n] for an n-element array.
Signed integer overflow: Exceeding INT_MAX is UB (unlike unsigned, which wraps modulo 2ⁿ).
Dereferencing invalid pointers: Using a NULL, uninitialized, or freed pointer.
Others worth knowing: Modifying a variable twice without a sequence point, division by zero, and reading uninitialized memory.
Q72.Is the order of evaluation of function arguments defined in C? Why does this matter?
No: the order in which a function's arguments are evaluated is unspecified in C (the compiler may evaluate them in any order, and even interleave them). This matters because arguments with side effects can produce different results on different compilers.
Unspecified, not undefined: Each argument is fully evaluated, but their relative order is a choice the implementation need not document.
Why it matters: Calls like f(i++, i++) give different results per compiler, and because the same i is modified twice with no sequence point between, this is actually UB.
Sequence point at the call: All argument side effects complete before the function body executes, but not in a guaranteed order among themselves.
Practical advice: Avoid side effects in arguments; compute values into separate variables first for portable, predictable code.
Q73.What is a race condition, and how do you use a mutex to prevent one in a multi-threaded C application?
mutex to prevent one in a multi-threaded C application?A race condition occurs when two or more threads access shared data concurrently and at least one writes, so the final result depends on the unpredictable timing of their interleaving. A mutex prevents it by serializing access: only one thread holds the lock and enters the critical section at a time.
The problem:
Operations like count++ are not atomic (read, modify, write), so interleaved threads can lose updates.
The outcome is timing-dependent and non-deterministic, making bugs hard to reproduce.
The mutex fix:
Wrap the critical section with pthread_mutex_lock() and pthread_mutex_unlock() so only the lock holder mutates shared state.
Always unlock on every exit path to avoid leaving the lock held.
Cautions:
Keep critical sections small to limit contention.
Acquire multiple locks in a consistent global order to avoid deadlock.
Q74.What is the difference between memcpy and memmove, and when must you use memmove?
memcpy and memmove, and when must you use memmove?Both copy n bytes between memory regions, but memcpy assumes the source and destination do not overlap, while memmove handles overlap correctly. You must use memmove whenever the two regions might overlap.
memcpy: Faster, but overlapping regions are undefined behavior: it may copy in any order and corrupt data.
memmove: Behaves as if it copies through a temporary buffer, so the result is correct even if ranges overlap (it picks a safe copy direction).
When overlap happens:
Shifting elements within the same array, e.g. memmove(arr, arr+1, n) to remove the first element, where source and destination clearly overlap.
Rule of thumb: distinct buffers, use memcpy; same buffer or any doubt, use memmove.
Q75.How do bit-fields work in a struct, and what are the potential portability issues associated with them?
struct, and what are the potential portability issues associated with them?Q76.What is an opaque pointer (or incomplete type), and how does it help in implementing encapsulation in C?
Q77.What is the difference between typeof and typeof_unqual in C23?
typeof and typeof_unqual in C23?Q78.Explain the use case for _BitInt(N). When would you use it over a standard int or long?
_BitInt(N). When would you use it over a standard int or long?Q79.Why might n++ execute faster than n = n + 1 on certain architectures?
n++ execute faster than n = n + 1 on certain architectures?Q80.What is the difference between virtual and physical memory, and how does the TLB speed up memory access?
TLB speed up memory access?Q81.How does malloc() know how much memory to free when you only pass it a pointer?
malloc() know how much memory to free when you only pass it a pointer?Q82.How does a memory allocator like malloc actually work under the hood, and what are the tradeoffs of different allocation strategies?
malloc actually work under the hood, and what are the tradeoffs of different allocation strategies?Q83.What are the tradeoffs of using alloca() vs. malloc()?
alloca() vs. malloc()?Q84.What is a flexible array member, and when would you use one?
Q85.What do the stringizing (#) and token-pasting (##) operators do in macros?
#) and token-pasting (##) operators do in macros?Q86.What is _Static_assert, and how does it differ from a runtime assert?
_Static_assert, and how does it differ from a runtime assert?Q87.What is a reentrant function, and why is it critical in multi-threaded or interrupt-driven environments?
Q88.How would you implement polymorphism or virtual functions in pure C?
Q89.What does the _Generic keyword (C11) do, and how does it enable pseudo-polymorphism in C?
_Generic keyword (C11) do, and how does it enable pseudo-polymorphism in C?Q90.What is the purpose of setjmp and longjmp? When are they preferred over standard control flow?
setjmp and longjmp? When are they preferred over standard control flow?Q91.What are the limitations of an Interrupt Service Routine (ISR)?
Q92.What is the restrict keyword, and how does it help the compiler optimize code?
restrict keyword, and how does it help the compiler optimize code?Q93.What is Memory-Mapped I/O (MMIO), and how do you access a hardware register in C?
C?Q94.Why is it considered bad practice to use printf() or malloc() inside an Interrupt Service Routine (ISR)?
printf() or malloc() inside an Interrupt Service Routine (ISR)?Q95.What is 'Interrupt Latency,' and what factors contribute to it?
Q96.What is the 'Strict Aliasing Rule' and how does it affect how pointers of different types interact?
Q97.Explain the concept of a 'Sequence Point' and how it relates to expressions like i = i++.
i = i++.Q98.How does C handle integer promotion and type conversion in mixed-type expressions?
Q99.What is undefined behavior in C, and why is i = i++ + ++i considered UB?
i = i++ + ++i considered UB?Q100.Explain the difference between a mutex and a semaphore, and when is a spinlock more efficient than a mutex?
mutex and a semaphore, and when is a spinlock more efficient than a mutex?