1.1

Memory Management

Every C program has two places to store data: the stack (automatic, fixed size at compile time) and the heap (dynamic, you control lifetime). Understanding both deeply is the foundation of writing correct, efficient C.

Stack vs Heap — When to Use Which
PropertyStackHeap
Size known at compile time?Yes — requiredNo — runtime OK
Allocation speedInstant — just move stack pointerSlower — allocator searches free list
Freed automatically?Yes — on scope exitNo — you must call free()
Typical size limit1–8 MB (OS-defined)Limited only by RAM
RiskStack overflow on deep recursion or huge arraysLeaks, double-free, use-after-free
C · stack vs heap
// Stack — size must be fixed at compile time
int arr[100];          // OK — 100 is a compile-time constant
// int arr[n];         // VLA (C99) — risky for large n, avoid

// Heap — size decided at runtime
int n;
scanf("%d", &n);
int *arr = malloc(n * sizeof(int));   // allocate exactly what we need
if (!arr) { /* always check! */ exit(1); }
// ... use arr ...
free(arr);
arr = NULL;    // prevent dangling pointer
malloc / calloc / realloc / free
FunctionDoesZeroes?Use when
malloc(n)Allocate n bytesNoYou'll overwrite all bytes anyway
calloc(n, s)Allocate n×s bytesYesNeed zero-initialized memory
realloc(p, n)Resize block to n bytesNo (new bytes)Grow/shrink existing allocation
free(p)Return memory to systemAlways, exactly once per allocation

Golden Rules

  • Every malloc / calloc / realloc must have exactly one matching free.
  • Always check the return value — allocation can fail and return NULL.
  • After free, set the pointer to NULL to prevent accidental reuse.
  • Never access memory after free — use-after-free is undefined behavior.
  • Never free the same pointer twice — double-free is undefined behavior.
  • malloc(0) is implementation-defined — may return NULL or a non-dereferenceable pointer.
Safe realloc Pattern — Never Lose Your Original Pointer

If realloc fails, it returns NULL but does not free the original. Writing back into the same variable immediately leaks the old block.

Wrong — memory leak on failure

p = realloc(p, new_size);
// if NULL returned, old p is lost forever!

Correct — safe pattern

void *tmp = realloc(p, new_size);
if (!tmp) { free(p); p = NULL; return; }
p = tmp;

C · realloc edge cases
// realloc(NULL, n) == malloc(n)  — useful for "grow on demand" patterns
int *p = NULL;
p = realloc(p, 100);   // equivalent to malloc(100)

// Shrinking: usually in-place (fast, same address)
buf = realloc(buf, 100);

// Expanding: may copy to new address if not enough contiguous space
char *tmp = realloc(buf, 10000);
if (!tmp) { free(buf); return -1; }
buf = tmp;    // buf may now be a completely different address
Memory Layout — The Full Map
High Address (e.g. 0xFFFF...) ┌──────────────────────────────────────────┐ │ Stack │ ← Local variables, function frames │ ↓ grows down │ Freed automatically on scope exit ├──────────────────────────────────────────┤ │ (free / unmapped space) │ ├──────────────────────────────────────────┤ │ Heap │ ← malloc / calloc / realloc │ ↑ grows up │ You manage lifetime manually ├──────────────────────────────────────────┤ │ BSS │ ← Uninitialized globals/statics (zeroed) ├──────────────────────────────────────────┤ │ Data │ ← Initialized globals/statics ├──────────────────────────────────────────┤ │ Text (Code) │ ← Compiled instructions (read-only) └──────────────────────────────────────────┘ Low Address (e.g. 0x0000...)
C · where each variable lives
int g_init = 42;          // Data segment
int g_uninit;              // BSS (zeroed at startup)
static int s_var = 5;     // Data segment

int main(void) {
    int local = 10;         // Stack
    char *buf = malloc(100); // buf ptr on Stack; 100 bytes on Heap
    const char *s = "Hi";   // "Hi" in read-only Text segment!
    // s[0] = 'h';            // SEGFAULT — writing to read-only text!
    free(buf);
}
Heap Fragmentation

Over time, repeated malloc/free can leave the heap with many small holes. Even if total free bytes are enough, a large contiguous allocation may fail.

concept · external fragmentation
// Memory state after many alloc/free cycles:
// [USED 50B][FREE 30B][USED 40B][FREE 30B][USED 60B]
//  Total free = 60B, but you need 60B contiguous → FAILS!

// Internal fragmentation: asked for 100B, allocator gave 128B
// (rounded up for alignment) — 28B are wasted inside the block.
Embedded / Long-Running Systems

Fragmentation matters most in systems running for hours or days. Solutions: use memory pools, arena allocators, or fixed-size block allocators to avoid fragmentation entirely.

Memory-Mapped I/O and volatile
C · volatile for hardware registers
#define GPIO_PORT (*(volatile unsigned int *)0x40021000)

void toggle_led(void) {
    GPIO_PORT = 0x01;   // LED ON
    GPIO_PORT = 0x00;   // LED OFF
    // Without volatile: compiler may delete the first write!
    // volatile forces EVERY read/write to actually happen.
}
Alignment — Natural Alignment & __attribute__((aligned))
C · alignment and __attribute__((aligned))
// Natural alignments (on x86-64)
// char  → 1-byte   short → 2-byte   int → 4-byte   long → 8-byte

printf("%zu\n", alignof(int));      // 4
printf("%zu\n", alignof(double));   // 8

// Force alignment (GCC/Clang)
struct __attribute__((aligned(64))) CacheLine {
    int data[16];
};
// Used in: DMA buffers, SIMD data, avoid false sharing

// Aligned malloc (C11)
void *buf = aligned_alloc(64, 256);
free(buf);

#define IS_ALIGNED(ptr, n) (((uintptr_t)(ptr) & ((n)-1)) == 0)
Memory Pool — Deterministic Allocation
C · simple fixed-size memory pool
#define POOL_COUNT 32
#define BLOCK_SIZE 64

typedef struct {
    uint8_t  data[POOL_COUNT][BLOCK_SIZE];
    uint8_t  used[POOL_COUNT];
} MemPool;

void* pool_alloc(MemPool *pool) {
    for (int i = 0; i < POOL_COUNT; i++) {
        if (!pool->used[i]) {
            pool->used[i] = 1;
            return pool->data[i];
        }
    }
    return NULL;
}

void pool_free(MemPool *pool, void *ptr) {
    int i = ((uint8_t*)ptr - pool->data[0]) / BLOCK_SIZE;
    pool->used[i] = 0;   // O(1)
}
// Benefits: deterministic time, no fragmentation, no system call
Interview Questions & Answers

Q: What is the difference between malloc and calloc?
A: malloc(n) allocates n bytes — contents are uninitialized (garbage). calloc(n, s) allocates n×s bytes and zero-initializes all of them. Use calloc when you need a zeroed buffer; malloc is slightly faster when you'll overwrite all bytes anyway.

Q: What happens if realloc returns NULL? Show the safe pattern.
A: realloc failure does NOT free the original pointer — you still own it. Never do p = realloc(p, n) — on failure you leak the original. Safe: void *tmp = realloc(p, n); if (!tmp) { free(p); return; } p = tmp;

Q: Where do global variables live? What about static locals?
A: Initialized globals and static locals live in the Data segment. Uninitialized globals and zero-initialized statics live in BSS (zeroed automatically at program start). Both survive for the entire program lifetime.

Q: Why do we need volatile in embedded systems?
A: The compiler may optimize away "redundant" reads/writes if it sees no C-level side effects. Hardware registers change externally — volatile forces every read and write to actually happen, in order.

Q: Why are memory pools used in embedded systems instead of malloc?
A: malloc is non-deterministic, can fragment over time, and can fail at any point. Memory pools pre-allocate a fixed block — O(1) allocation, no fragmentation, predictable timing. Essential for RTOS tasks and MISRA-C compliant safety-critical code.

1.2

Pointers

A pointer is a variable that holds the memory address of another variable. The most tested C/C++ interview topic. Master every variant below.

Pointer Basics and Arithmetic
C · pointer fundamentals
int x = 42;
int *p = &x;      // p holds address of x
// p  → the address (e.g. 0x1000)
// *p → the value at that address (42)

*p = 99;         // modifies x through the pointer — now x == 99

// Pointer arithmetic is in units of the POINTED TYPE
int arr[] = {10, 20, 30, 40, 50};
int *q = arr;     // &arr[0]
// If sizeof(int)==4 and q is at address 1000:
// q+1 = 1004 (adds sizeof(int)=4 bytes)
printf("%d\n", *(q + 2));    // 30

// Array decay: array name → pointer to first element in most expressions
void print_size(int arr[]) {
    printf("%zu\n", sizeof(arr)); // sizeof(int*) = 8, NOT array size!
}
Array Decay Trap

When an array is passed to a function, it decays to a pointer and sizeof information is lost. Always pass the length separately: void process(int *arr, size_t len).

Double Pointer (Pointer to Pointer)
C · double pointer
int x = 42;
int  *p  = &x;     // p holds address of x
int **pp = &p;     // pp holds address of p — pp → p → x = 42
printf("%d\n", **pp);   // 42

// Real use: function allocates memory and sets caller's pointer
void alloc_array(int **out, size_t n) {
    *out = (int *)malloc(n * sizeof(int));
    if (*out) {
        for (size_t i = 0; i < n; i++) (*out)[i] = 0;
    }
}

int main(void) {
    int *arr = NULL;
    alloc_array(&arr, 10);   // pass address of the pointer
    free(arr);
}
Function Pointers and Callbacks
C · function pointers
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

// Declaration: return_type (*name)(param_types)
int (*op)(int, int);
op = add;   op(3, 4);  // 7
op = sub;   op(3, 4);  // -1

// Cleaner with typedef:
typedef int (*MathFn)(int, int);
MathFn fn = add;
fn(10, 5);   // 15

// Callback pattern — sort with any comparator
void sort(int *arr, int n, int (*cmp)(int, int)) {
    for (int i = 0; i < n-1; i++)
        for (int j = 0; j < n-i-1; j++)
            if (cmp(arr[j], arr[j+1]) > 0) {
                int tmp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = tmp;
            }
}
void* — Generic Pointer
C · void* generic swap
void swap(void *a, void *b, size_t size) {
    unsigned char tmp[size];
    memcpy(tmp, a, size);
    memcpy(a, b, size);
    memcpy(b, tmp, size);
}
int x = 10, y = 20;
swap(&x, &y, sizeof(int));   // x=20, y=10 — works with ANY type!
const with Pointers — Four Forms
C · const pointer combinations
int x = 10, y = 20;

const int *p1 = &x;      // ptr to const int — *p1 immutable, p1 can change
// *p1 = 99;  ERROR
p1 = &y;                 // OK

int *const p2 = &x;      // const ptr to int — *p2 mutable, p2 can't change
*p2 = 99;               // OK
// p2 = &y;  ERROR

const int *const p3 = &x; // const ptr to const int — neither can change

// Mnemonic: "const applies to what's on its LEFT"
Dangling, Wild, and NULL Pointers
TypeDefinitionConsequencePrevention
DanglingPoints to freed or out-of-scope memoryUB — may corrupt, crash, or appear to workSet to NULL after free
WildUninitialized pointer with garbage valueUB — writes to random addressAlways initialize: int *p = NULL;
NULLExplicitly set to address 0Dereference → segfault (controlled crash)Check if (p != NULL) before deref
Pointer to Array vs Array of Pointers
C · pointer to array vs array of pointers
// ARRAY OF POINTERS — each element is a pointer
int a = 1, b = 2, c = 3;
int *arr_of_ptrs[3] = {&a, &b, &c};

// POINTER TO ARRAY — one pointer to an entire array
int arr[3] = {1, 2, 3};
int (*ptr_to_arr)[3] = &arr;   // ptr_to_arr + 1 jumps sizeof(int[3]) = 12 bytes

// Real use: pointer to row of 2D array
int matrix[3][4];
int (*row)[4] = matrix;     // row++ advances by 16 bytes (next row)

// argv is a classic array of pointers:
int main(int argc, char *argv[]) { ... }
Interview Questions & Answers

Q: What is the difference between const int *p and int *const p?
A: Read right-to-left. const int *p = pointer to const int — the value *p cannot change, but p can point elsewhere. int *const p = const pointer to int — p cannot point elsewhere, but *p can change. const int *const p = neither can change.

Q: When would you use a double pointer? Give a real use case.
A: When a function needs to modify a pointer itself. Example: alloc_array(int **out, int n) allocates memory and assigns it to *out — the caller's pointer gets updated. Also used for 2D arrays: int **matrix = malloc(rows * sizeof(int*)).

Q: What is a function pointer? How is it declared?
A: A function pointer holds the address of a function. Syntax: int (*fp)(int, int) = &add; — called as fp(3,4). Used for callbacks (qsort), dispatch tables, and runtime-selectable behaviour.

Q: What is the difference between a dangling pointer and a wild pointer?
A: Wild pointer = uninitialized pointer (random garbage address). Dangling pointer = was valid but now points to freed/out-of-scope memory. Both are UB on dereference. Prevention: always initialize to NULL, set to NULL after free.

Q: What is the strict aliasing rule?
A: You may not access an object through a pointer to an incompatible type. Violation: float f; int *p = (int*)&f; *p. The compiler assumes int* and float* can't alias. Safe alternatives: union (C99), memcpy.

1.3

Structures & Unions

Struct Padding and Alignment

CPUs read memory in aligned chunks. If a field isn't at a naturally aligned address, the compiler inserts invisible padding bytes.

struct Example { char a; int b; char c; }; Offset: 0 1 2 3 4 5 6 7 8 9 10 11 [ a |pad|pad|pad| b (int) | c |pad|pad|pad] sizeof(struct Example) = 12 (not 6!)
C · padding comparison
// Wasteful — 12 bytes
struct Wasteful {
    char c;          // 1 byte + [3 pad]
    int  i;          // 4 bytes
    char d;          // 1 byte + [3 pad]
};                   // total: 12

// Efficient — 8 bytes (reordered largest to smallest)
struct Efficient {
    int  i;          // 4 bytes
    char c;          // 1 byte
    char d;          // 1 byte + [2 pad]
};                   // total: 8

// Rule: order members largest → smallest to minimize padding
Packed Structs — Exact Layout for Protocols
C · packed struct
// GCC/Clang attribute:
struct __attribute__((packed)) Packet {
    char     type;    // 1 byte (NO padding)
    uint32_t id;     // 4 bytes (may be unaligned!)
    char     flags;   // 1 byte
};                    // sizeof = 6

// MSVC / portable alternative:
#pragma pack(push, 1)
struct Packet2 { char type; int id; char flags; };
#pragma pack(pop)
Bit Fields
C · bit fields
struct Flags {
    unsigned int enabled  : 1;   // 1 bit  → 0 or 1
    unsigned int mode     : 3;   // 3 bits → 0–7
    unsigned int priority : 4;   // 4 bits → 0–15
    unsigned int reserved : 24;  // padding to fill int
};
// sizeof = 4 (one int, tightly packed)

// Limitations:
// - Bit order depends on compiler + CPU endianness
// - Cannot take address of a bit field (&f.enabled is ILLEGAL)
Unions — Shared Memory, One Member Valid at a Time
C · union basics and type punning
union Data {
    int   i;          // 4 bytes
    float f;          // 4 bytes
    char  bytes[4];   // 4 bytes
};
// sizeof(union Data) = 4  (size of LARGEST member)
// All members SHARE the same 4 bytes — only ONE is "valid" at a time

// Type punning — inspect float's raw bytes:
union { float f; unsigned char b[4]; } u;
u.f = 3.14f;
printf("%02x %02x %02x %02x\n", u.b[0], u.b[1], u.b[2], u.b[3]);
Interview Questions & Answers

Q: Predict sizeof for struct { char c; int i; char d; }. Answer?
A: 12 bytes. char c at offset 0 (1 byte) + 3 padding to align int to 4-byte boundary + int i at offset 4 (4 bytes) + char d at offset 8 (1 byte) + 3 padding to make total a multiple of 4. Total = 12.

Q: How do you minimize struct size?
A: Order members from largest to smallest alignment: double (8), int/float (4), short (2), char (1). This minimises padding because each member is already naturally aligned where the previous one ended.

Q: What is a union? How is it different from a struct?
A: A union allocates memory equal to its largest member — all members share the same location, only one valid at a time. A struct allocates separate memory for each member with padding. Union size = largest member only.

Q: Why can't you take the address of a bit field?
A: Addresses must point to full bytes. A bit field may occupy part of a byte — there is no address for "bit 3 of byte 5." The & operator doesn't have sub-byte granularity.

1.4

Bit Manipulation

Core Bit Operations
OperationMacro / IdiomExplanation
Set bit kn |= (1U << k)OR with a mask that has only bit k set
Clear bit kn &= ~(1U << k)AND with inverted mask
Toggle bit kn ^= (1U << k)XOR flips the bit
Test bit k((n >> k) & 1U)Shift right, AND with 1
Clear lowest set bitn &= (n - 1)Kernighan's trick — basis of popcount
Isolate lowest set bitn & (-n)Two's complement property
Power of two checkn && !(n & (n-1))True only if exactly one bit set
C · bit manipulation macros
#define SET_BIT(n, k)    ((n) |=  (1U << (k)))
#define CLEAR_BIT(n, k)  ((n) &= ~(1U << (k)))
#define TOGGLE_BIT(n, k) ((n) ^=  (1U << (k)))
#define TEST_BIT(n, k)   (((n) >> (k)) & 1U)

unsigned int n = 0b1000;
SET_BIT(n, 1);    // n = 0b1010
CLEAR_BIT(n, 3); // n = 0b0010
Count Set Bits — Brian Kernighan's Algorithm
C · popcount O(set bits)
int count_bits(unsigned int n) {
    int count = 0;
    while (n) {
        n &= (n - 1);   // clears the LOWEST set bit each iteration
        count++;
    }
    return count;
}
// Built-in (GCC): __builtin_popcount(n) — compiled to single instruction
Endianness and Network Byte Order
Value: 0x12345678 Big Endian (network standard, most significant byte first): Address: [0x00] [0x01] [0x02] [0x03] Value: [ 12 ] [ 34 ] [ 56 ] [ 78 ] Little Endian (x86/x64 standard, least significant byte first): Address: [0x00] [0x01] [0x02] [0x03] Value: [ 78 ] [ 56 ] [ 34 ] [ 12 ]
C · endianness conversion
#include <arpa/inet.h>
uint32_t host = 0x12345678;
uint32_t net  = htonl(host);   // Host To Network Long (→ Big Endian)
uint32_t back = ntohl(net);   // Network To Host Long

// Detect endianness at runtime:
int is_little_endian(void) {
    union { uint32_t i; uint8_t c[4]; } test = { .i = 0x01020304 };
    return test.c[0] == 0x04;
}
Interview Questions & Answers

Q: Write a macro to set, clear, and toggle a bit at position k.
A: #define SET_BIT(n,k) ((n) |= (1U << (k))) — OR with mask. #define CLEAR_BIT(n,k) ((n) &= ~(1U << (k))) — AND with inverted mask. #define TOGGLE_BIT(n,k) ((n) ^= (1U << (k))) — XOR flips the bit.

Q: How do you check if a number is a power of 2 using bit ops?
A: n && !(n & (n-1)). Powers of 2 have exactly one bit set. n-1 has all bits below that bit set, and ANDing gives 0. The n&& check handles n=0.

Q: What is endianness? Why does it matter in networking?
A: Byte ordering of multi-byte integers. Little-endian (x86): least significant byte at lowest address. Network protocols use big-endian. Use htons/htonl (host to network), ntohs/ntohl (network to host) — otherwise 0x1234 on x86 appears as 0x3412 to the receiver.

1.5

Storage Classes & Scope

Storage classes control three things: lifetime, scope, and linkage.

ClassLifetimeScopeLinkageMemory
auto (default)Function callBlockNoneStack
static (local)Program lifetimeBlockNoneData/BSS
static (file)Program lifetimeFileInternalData/BSS
externProgram lifetimeFileExternalData/BSS
static — Two Very Different Uses
C · static inside function vs file scope
// USE 1: static inside a function — value persists across calls
void counter(void) {
    static int count = 0;   // initialized ONCE, lives in Data segment
    count++;
    printf("%d ", count);   // 1, 2, 3, ...
}

// USE 2: static at file scope — internal linkage (hidden from other files)
static int secret = 42;   // ONLY visible in this .c file
static void helper(void) { ... }  // private function
extern — Declaration vs Definition
C · extern across files
// shared.h — declaration (no storage allocated)
extern int global_count;    // "this variable EXISTS somewhere"

// main.c — definition (storage allocated here)
int global_count = 0;

// other.c
#include "shared.h"
void increment(void) { global_count++; }

// Key rule: DECLARE in header (extern), DEFINE in exactly ONE .c file
Interview Questions & Answers

Q: What does static mean inside a function vs at file scope?
A: Inside a function: the variable persists across calls — initialized once, lives in the Data segment for the program's lifetime. At file scope: internal linkage — invisible to other translation units.

Q: What is the difference between a declaration and a definition?
A: Declaration says "this thing exists" — no storage allocated (extern int x;). Definition says "this thing lives here" — allocates storage (int x = 0;). Multiple declarations OK; only one definition allowed.

Q: A static local variable is initialized to 0 — true or false?
A: True. Static local variables (and all static/global variables) are guaranteed to be zero-initialized at program start, before main() runs.

1.6

Macros & Preprocessor

The preprocessor runs before the compiler. It does pure text substitution — no type checking, no scoping.

Object-Like and Function-Like Macros
C · macro basics
#define PI       3.14159
#define MAX_SIZE 1024

// ALWAYS parenthesize arguments AND the whole expression
#define SQ(x)     ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))

// WITHOUT outer parens:
#define BAD_SQ(x) x * x
BAD_SQ(1 + 2);       // expands to 1+2*1+2 = 5  ✗ WRONG!
Double-Evaluation Bug and the Fix
C · double evaluation
#define SQ(x) ((x) * (x))
int i = 5;
int r = SQ(i++);    // ((i++) * (i++)) — i incremented TWICE! UB!

// FIX: use an inline function instead
inline int sq(int x) { return x * x; }
r = sq(i++);        // i passed by value, incremented ONCE. Safe.
Multi-Statement Macro — do { } while(0) Pattern
C · safe multi-statement macro
// RIGHT — do-while(0) creates a single statement
#define LOG(msg) do { \
    printf("LOG: %s\n", msg); \
    fflush(stdout); \
} while(0)
Preprocessor Operators — # and ##
C · stringification and token pasting
// # turns argument into a string literal
#define DBG(var)  printf(#var " = %d\n", var)
int count = 42;
DBG(count);    // prints "count = 42"

// ## glues two tokens together
#define CONCAT(a, b) a##b
int CONCAT(my, var) = 10;  // int myvar = 10;
assert and static_assert
C · assertions
#include <assert.h>
assert(x > 0);    // crashes with message if false; ZERO cost with -DNDEBUG

// static_assert: evaluated at COMPILE TIME — no runtime cost ever
_Static_assert(sizeof(int) == 4, "int must be 4 bytes");
_Static_assert(CHAR_BIT == 8,    "requires 8-bit bytes");
Interview Questions & Answers

Q: What is the double-evaluation bug in macros? How do you fix it?
A: #define SQ(x) ((x)*(x)) — called as SQ(i++), expands to ((i++)*(i++)) — i incremented twice, result is UB. Fix: use an inline function — argument evaluated exactly once, then passed by value.

Q: Why use do { } while(0) in multi-statement macros?
A: It wraps multiple statements into a single syntactic statement. Without it, an if-body macro with two statements can break: if(err) LOG("x"); else handle(); — the else may attach to the wrong statement.

Q: When would you prefer static_assert over assert?
A: static_assert evaluates at compile time — the program won't even compile if it fails. Use it for struct size checks, platform assumptions (CHAR_BIT==8, sizeof(int)==4). Zero runtime cost ever. Regular assert only catches failures at runtime.

1.7

Inline Functions

The inline keyword suggests the compiler replace a function call with the function's body directly — eliminating call overhead. It is a hint, not a command.

Inline vs Macro — Always Prefer Inline
FeatureMacroInline Function
Type checkingNoneFull type safety
DebuggableHardYes
Argument evaluationMultiple times (double-eval)Exactly once
ScopeGlobal — no namespaceNormal C scoping
RecursionImpossiblePossible (not inlined)
Forcing and Preventing Inlining
C · compiler attributes
// Force inlining regardless of compiler judgment:
__attribute__((always_inline)) inline void must_inline(void) { ... }

// Prevent inlining (for profiling / call shows in stack trace):
__attribute__((noinline)) void never_inline(void) { ... }

// static inline — most common pattern for header utilities
static inline int clamp(int v, int lo, int hi) {
    return v < lo ? lo : (v > hi ? hi : v);
}
Interview Questions & Answers

Q: What does inline mean? Is it a guarantee?
A: inline is a hint to the compiler to replace the function call with the function body. It is NOT a guarantee — the compiler may ignore it for large functions, recursive functions, functions called via pointer, or when optimisation is disabled (-O0).

Q: When will the compiler refuse to inline a function?
A: Function body too large, function is recursive, function is called via pointer (target unknown), optimisation disabled (-O0), or compiler decides inlining would hurt instruction cache performance.

1.8

Undefined Behavior

Undefined behavior (UB) means the C standard places no constraints on what the program does.

Why UB is Dangerous

UB doesn't just crash — it can silently produce wrong results, be optimized away, or work in debug builds but break in release. The compiler is allowed to delete your null-check if it deduces the pointer must be non-null.

Common UB Examples
C · undefined behavior patterns
// 1. Use after free
int *p = malloc(sizeof(int));
free(p);
*p = 100;   // UNDEFINED BEHAVIOR

// 2. Dangling pointer to stack variable
int *get_local(void) {
    int x = 42;
    return &x;   // WRONG — x gone after return
}

// 3. Out-of-bounds access
int arr[5];
arr[5] = 99;   // UB — one past end

// 4. Signed integer overflow
int x = INT_MAX;
x++;            // UB — optimizer may assume this never happens!

// 5. Sequence point violation
int r = i++ + i++;  // UB — modification order unspecified
Tools for Detecting UB
ToolDetectsHow to Enable
AddressSanitizerOut-of-bounds, use-after-free, double-free-fsanitize=address
UndefinedBehaviorSanitizerSigned overflow, null deref, bad shifts-fsanitize=undefined
MemorySanitizerUninitialized reads-fsanitize=memory
ValgrindLeaks, invalid reads/writesvalgrind --leak-check=full ./prog
Interview Questions & Answers

Q: What is undefined behavior? Give three examples.
A: UB = the C standard places no constraints on what the program does. Three examples: (1) signed integer overflow — int x = INT_MAX; x++; (2) null pointer dereference — int *p = NULL; *p; (3) use-after-free — free(p); *p = 1;

Q: Why is signed integer overflow UB but unsigned overflow is not?
A: The C standard intentionally leaves signed overflow undefined so compilers can optimize aggressively. Unsigned overflow is well-defined: wraps modulo 2^n — used in hash functions, cryptography, and circular buffers.

Q: What tools would you use to find memory bugs in a C program?
A: AddressSanitizer (-fsanitize=address), UBSanitizer (-fsanitize=undefined), MemorySanitizer (-fsanitize=memory), Valgrind (--leak-check=full). Plus compiler warnings: -Wall -Wextra -Wshadow.

1.9

Arrays & Strings

Array vs Pointer
C · array vs pointer
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;

sizeof(arr);           // 20 — compiler knows full array size (5 × 4)
sizeof(p);             // 8  — sizeof pointer

// Reassignment:
p = some_other_array;  // OK — pointers are reassignable
// arr = other;        // ERROR — array name is NOT an lvalue

// sizeof loss in functions:
void fn(int arr[]) {
    sizeof(arr);   // 8 — decays to pointer! Size is LOST.
}
// Always pass array length separately: fn(int *arr, size_t n)
C Strings — Null-Terminator Rules
C · string fundamentals
char str[] = "Hello";
// str = ['H']['e']['l']['l']['o']['\0']  — 6 bytes including null!

strlen(str);    // 5 — does NOT include '\0'
sizeof(str);    // 6 — includes '\0'
sizeof(p);      // 8 — sizeof pointer, NOT string length!

// String literal is READ-ONLY:
char *lit = "Hello";
lit[0] = 'J';   // SEGFAULT — read-only text segment

// Writable copy:
char buf[] = "Hello";   // copies to stack — writable
String Function Pitfalls
C · safe string handling
char dest[5];

// UNSAFE: strcpy — no bounds checking
strcpy(dest, "Hello World!");   // BUFFER OVERFLOW!

// SAFER: strncpy — copies at most n bytes, but may NOT null-terminate!
strncpy(dest, src, sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0';   // always guarantee null-termination

// DANGEROUS: gets() — NEVER use! Removed in C11.
// SAFE: fgets(buf, sizeof(buf), stdin);

// strcmp — compare strings (NOT ==)
if (strcmp(a, b) == 0) { /* strings equal */ }
Interview Questions & Answers

Q: What is the difference between sizeof(arr) and strlen(arr)?
A: sizeof(arr) — compile-time, returns total bytes including null terminator (char arr[] = "Hello" → 6). strlen(arr) — runtime, counts chars until '\0', does NOT include it (→ 5). sizeof on a pointer gives pointer size (8), not string length.

Q: Why is strcpy dangerous?
A: No length parameter — copies until null terminator regardless of destination size. Source longer than destination → buffer overflow (stack smashing, exploitable). Safe: strncpy(dest, src, sizeof(dest)-1); dest[sizeof(dest)-1] = '\0'.

Q: Why can't you compare strings with == in C?
A: == compares pointer addresses, not string content. Always use strcmp(a, b) == 0.

Tips

Quick Interview Tips & C vs C++ Cheat Sheet

Most Asked C Topics
TopicWhat Interviewers Really Want
Stack vs HeapDraw the memory layout. Explain lifetime, size limits, automatic vs manual management.
Pointer arithmetic + decayWhat does p+1 do for int* vs char*? Why does sizeof lose info in functions?
Struct padding & alignmentPredict sizeof. Explain why it matters for DMA, network packets, embedded register maps.
Dangling pointer vs memory leakDangling = freed but pointer still used. Leak = allocated but never freed.
#define pitfalls vs inlineDouble-eval, no type safety, no scope — always prefer inline in C99+.
Undefined behavior examplesGive three: signed overflow, null deref, use-after-free.
C vs C++ Cheat Sheet
C wayC++ wayWhy C++ is better
malloc / freenew / deleteCalls constructors/destructors. Type-safe.
malloc + manualunique_ptr / shared_ptrRAII — memory freed automatically on scope exit.
#define PI 3.14constexpr double PI = 3.14;Type-safe, scoped, debuggable.
void* generictemplate<T>Type-safe generics — no casts needed.
NULLnullptrnullptr has type nullptr_t — won't match int overload.
strcpy / strcatstd::stringAutomatic memory management, no buffer overflows.
One-Line Answers for Speed Rounds

malloc vs calloc: malloc doesn't zero; calloc does.
dangling vs wild: dangling = freed; wild = never initialized.
NULL vs nullptr: NULL is 0 (int); nullptr is nullptr_t (type-safe).
static in function: value persists across calls.
static at file scope: hidden from other translation units.
restrict: promise of no aliasing → enables vectorisation.
volatile: every access must actually happen — no caching.
struct vs union: struct = all members; union = one at a time.
sizeof vs strlen: sizeof includes '\0'; strlen does not.
inline guarantee?: No — it's a hint to the compiler.