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.
| Property | Stack | Heap |
|---|---|---|
| Size known at compile time? | Yes — required | No — runtime OK |
| Allocation speed | Instant — just move stack pointer | Slower — allocator searches free list |
| Freed automatically? | Yes — on scope exit | No — you must call free() |
| Typical size limit | 1–8 MB (OS-defined) | Limited only by RAM |
| Risk | Stack overflow on deep recursion or huge arrays | Leaks, double-free, use-after-free |
// 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
| Function | Does | Zeroes? | Use when |
|---|---|---|---|
malloc(n) | Allocate n bytes | No | You'll overwrite all bytes anyway |
calloc(n, s) | Allocate n×s bytes | Yes | Need zero-initialized memory |
realloc(p, n) | Resize block to n bytes | No (new bytes) | Grow/shrink existing allocation |
free(p) | Return memory to system | — | Always, exactly once per allocation |
Golden Rules
- Every
malloc/calloc/reallocmust have exactly one matchingfree. - Always check the return value — allocation can fail and return
NULL. - After
free, set the pointer toNULLto prevent accidental reuse. - Never access memory after
free— use-after-free is undefined behavior. - Never
freethe same pointer twice — double-free is undefined behavior. malloc(0)is implementation-defined — may returnNULLor a non-dereferenceable 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;
// 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
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); }
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.
// 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.
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.
volatile#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. }
// 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)
#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
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.
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.
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! }
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).
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); }
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 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!
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"
| Type | Definition | Consequence | Prevention |
|---|---|---|---|
| Dangling | Points to freed or out-of-scope memory | UB — may corrupt, crash, or appear to work | Set to NULL after free |
| Wild | Uninitialized pointer with garbage value | UB — writes to random address | Always initialize: int *p = NULL; |
| NULL | Explicitly set to address 0 | Dereference → segfault (controlled crash) | Check if (p != NULL) before deref |
// 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[]) { ... }
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.
Structures & Unions
CPUs read memory in aligned chunks. If a field isn't at a naturally aligned address, the compiler inserts invisible padding bytes.
// 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
// 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)
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)
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]);
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.
Bit Manipulation
| Operation | Macro / Idiom | Explanation |
|---|---|---|
| Set bit k | n |= (1U << k) | OR with a mask that has only bit k set |
| Clear bit k | n &= ~(1U << k) | AND with inverted mask |
| Toggle bit k | n ^= (1U << k) | XOR flips the bit |
| Test bit k | ((n >> k) & 1U) | Shift right, AND with 1 |
| Clear lowest set bit | n &= (n - 1) | Kernighan's trick — basis of popcount |
| Isolate lowest set bit | n & (-n) | Two's complement property |
| Power of two check | n && !(n & (n-1)) | True only if exactly one bit set |
#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
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
#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; }
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.
Storage Classes & Scope
Storage classes control three things: lifetime, scope, and linkage.
| Class | Lifetime | Scope | Linkage | Memory |
|---|---|---|---|---|
auto (default) | Function call | Block | None | Stack |
static (local) | Program lifetime | Block | None | Data/BSS |
static (file) | Program lifetime | File | Internal | Data/BSS |
extern | Program lifetime | File | External | Data/BSS |
// 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
// 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
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.
Macros & Preprocessor
The preprocessor runs before the compiler. It does pure text substitution — no type checking, no scoping.
#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!
#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.
// RIGHT — do-while(0) creates a single statement #define LOG(msg) do { \ printf("LOG: %s\n", msg); \ fflush(stdout); \ } while(0)
// # 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;
#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");
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.
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.
| Feature | Macro | Inline Function |
|---|---|---|
| Type checking | None | Full type safety |
| Debuggable | Hard | Yes |
| Argument evaluation | Multiple times (double-eval) | Exactly once |
| Scope | Global — no namespace | Normal C scoping |
| Recursion | Impossible | Possible (not inlined) |
// 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); }
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.
Undefined Behavior
Undefined behavior (UB) means the C standard places no constraints on what the program does.
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.
// 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
| Tool | Detects | How to Enable |
|---|---|---|
| AddressSanitizer | Out-of-bounds, use-after-free, double-free | -fsanitize=address |
| UndefinedBehaviorSanitizer | Signed overflow, null deref, bad shifts | -fsanitize=undefined |
| MemorySanitizer | Uninitialized reads | -fsanitize=memory |
| Valgrind | Leaks, invalid reads/writes | valgrind --leak-check=full ./prog |
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.
Arrays & Strings
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)
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
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 */ }
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.
Quick Interview Tips & C vs C++ Cheat Sheet
| Topic | What Interviewers Really Want |
|---|---|
| Stack vs Heap | Draw the memory layout. Explain lifetime, size limits, automatic vs manual management. |
| Pointer arithmetic + decay | What does p+1 do for int* vs char*? Why does sizeof lose info in functions? |
| Struct padding & alignment | Predict sizeof. Explain why it matters for DMA, network packets, embedded register maps. |
| Dangling pointer vs memory leak | Dangling = freed but pointer still used. Leak = allocated but never freed. |
| #define pitfalls vs inline | Double-eval, no type safety, no scope — always prefer inline in C99+. |
| Undefined behavior examples | Give three: signed overflow, null deref, use-after-free. |
| C way | C++ way | Why C++ is better |
|---|---|---|
malloc / free | new / delete | Calls constructors/destructors. Type-safe. |
malloc + manual | unique_ptr / shared_ptr | RAII — memory freed automatically on scope exit. |
#define PI 3.14 | constexpr double PI = 3.14; | Type-safe, scoped, debuggable. |
void* generic | template<T> | Type-safe generics — no casts needed. |
NULL | nullptr | nullptr has type nullptr_t — won't match int overload. |
strcpy / strcat | std::string | Automatic memory management, no buffer overflows. |
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.