Qualcomm Interview Prep β 01. C ProgrammingΒΆ
Scope. Pure C language and the C runtime/memory model β pointers, storage classes, dynamic memory, the program memory map, string/memory routines, data representation, and the compile/link/run pipeline. C++ and OOP live in
02_cpp_oop.md; OS theory (scheduling, virtual memory, IPC) in04_os.md; kernel/driver specifics in07_embedded_linux_kernel.md. Overlaps are cross-linked, not duplicated.How to read each entry. Every question is answered in layers so you can stop at the depth you need: - Q β the question, phrased as interviewers actually ask it. - Frequency β how often it showed up in the 75 collected reports (tier + approximate count). - Concept β the basis β book-style fundamentals with worked examples and, where it helps, an SVG diagram. - The "wh"s β Why it exists (what problem the feature/rule solves), Where you see it (real Qualcomm/embedded/camera situations), and any important caveat. - Answer β a tight, say-it-out-loud interview answer. - Solution / good example β for "how do you handle/avoid/implement X" questions, a complete, copy-pasteable code pattern. - Follow-ups / gotchas β the traps interviewers spring next. - Seen in β the source reports.
Terms in bold-italics like undefined behavior, cast, object, lifetime, alignment are defined in the Β§ Encyclopedia at the bottom β search there for any keyword.
Frequency legend (sample = 75 collected interview reports; counts are approximate and partly from aggregator pages, so treat them as directional): π₯π₯π₯ Very common (~8+ reports) Β· π₯π₯ Common (~4β7) Β· π₯ Occasional (~2β3) Β· β½ Foundational (rarely asked verbatim, but assumed and underpinning everything else).
Why C dominates the Qualcomm loop. Almost every embedded, camera, multimedia, modem, and systems role at Qualcomm is C/C++ at the metal β device drivers, HALs, ISP firmware, RTOS tasks, bring-up code. Interviewers probe whether you truly understand pointers, memory, and the machine, not just syntax. Evidence base: qualcomm_camera_interview_experiences.md.
Table of contentsΒΆ
- A. Pointers β A1 pointer basics Β· A2 dangling pointers Β· A3
void*Β· A4 function pointers Β· A5 pointer arithmetic Β· A6 returning a local's address - B. Storage classes & linkage β B1 storage classes Β· B2
staticΒ· B3const/volatileΒ· B4 where variables live / register spilling - C. Dynamic memory β C1
malloc/calloc/reallocΒ· C2 how allocation works Β· C3 memory leaks - D. Program memory model β D1 memory map Β· D2 stack vs heap Β· D3 segmentation fault
- E. Implement-it routines β E1
memcpy/memmoveΒ· E2strcmpΒ· E3strstrΒ· E4 customsizeofΒ· E5 byte stream - F. Data types & representation β F1 endianness Β· F2 signed vs unsigned Β· F3
structvsunion& padding - G. Compilation, preprocessor & system β G1 library vs system call Β· G2 macros/preprocessor/headers Β· G3
inlineΒ· G4 reading code-output snippets - Β§ Encyclopedia β searchable glossary
- Β§ Last-5-minutes cheat sheet
A. PointersΒΆ
A1 Β· Q: What is a pointer, really? (And how does dereferencing work?)ΒΆ
Frequency: β½ Foundational β every C round assumes this; it underpins ~30+ questions below.
Concept β the basis. Memory is one enormous, byte-addressable array. Every object (any named region of storage β a variable, an array element, a malloc'd block) lives at some address. A pointer is simply a variable whose value is an address, decorated with a type that tells the compiler how many bytes to read and how to interpret them. & ("address-of") produces an address; * ("dereference") follows an address to the object. They are inverses: *(&x) is x.
Example:
int x = 42;
int *p = &x; // p holds the address of x (e.g. 0x1000)
printf("%d\n", *p); // dereference: read the int at 0x1000 β 42
*p = 7; // write through the pointer β x is now 7
char *c = (char*)&x; // a DIFFERENT type viewing the same address
// p + 1 advances 4 bytes (sizeof(int)); c + 1 advances 1 byte (sizeof(char))
Why it exists. A pointer is the C-level name for what the hardware does natively: a CPU loads/stores via addresses held in registers. Pointers give you (1) pass-by-reference (mutate a caller's data, or avoid copying a 10 MB image), (2) dynamic data structures (linked lists, trees β nodes that don't know each other at compile time), (3) dynamic memory (malloc returns a pointer), and (4) direct hardware access (memory-mapped registers). Without pointers you cannot write a driver, an allocator, or an ISP frame queue.
Where you see it (Qualcomm). Passing image/frame buffers between pipeline stages by pointer (never by copy); a camera HAL handing the kernel a pointer to a DMA buffer; walking a linked list of pending capture requests; reading a sensor register at a fixed address.
Answer. "A pointer is a variable that stores the memory address of an object; its type fixes the size and interpretation of the pointee. & takes an address, * dereferences it. Pointers enable pass-by-reference, dynamic data structures, dynamic allocation, and talking to memory-mapped hardware. Arithmetic on a pointer scales by sizeof the pointee."
Follow-ups / gotchas. NULL/nullptr points at nothing β dereferencing is undefined behavior (usually a segmentation fault). A wild pointer (uninitialized) holds garbage. sizeof(p) is the pointer's size (8 bytes on a 64-bit target), not the pointee's. A void* (A3) has no pointee type, so you can't dereference it without a cast.
Seen in: GfG Set 2, GfG ML & System Engineer, LeetCode Engineer (pointers appear in nearly every C round).
A2 Β· Q: What is a dangling pointer and how do you avoid it?ΒΆ
Frequency: π₯ Occasional (~3 reports) β a favorite follow-up to any memory question.
Concept β the basis. A dangling pointer holds an address whose object has already ended its lifetime. The pointer value still looks valid, but the storage it names is no longer yours. Three classic births: (1) using memory after free() (use-after-free); (2) returning the address of a local (automatic storage duration) variable, whose stack frame is reclaimed on return; (3) keeping a pointer to an object whose scope ended.
Example β the three ways to create one:
/* (1) use-after-free */
int *p = malloc(sizeof *p); *p = 5;
free(p); // storage returned to the allocator
printf("%d", *p); // p DANGLES β undefined behavior
/* (2) returning a local's address */
int *bad(void){ int x = 10; return &x; } // x dies on return β caller gets a dangler
/* (3) scope ends */
int *q;
{ int t = 3; q = &t; } // t's lifetime ends at '}'
*q = 9; // dangling
Why it exists / why it matters. C gives you manual lifetime control for speed and determinism β but that means you are responsible for not using storage past its end. A dangling access is among the most dangerous bugs because it is undefined behavior: it often "works" in testing (the freed bytes aren't reused yet) and then corrupts data or crashes in the field. It's also a security hole (use-after-free is a classic exploit primitive).
Where you see it (Qualcomm). Freeing a frame/metadata buffer while another thread or the ISP DMA still references it; returning a pointer to a stack-allocated descriptor from a helper; a callback firing after its context was torn down.
Answer. "A dangling pointer points to memory whose lifetime has ended β freed, or a local that went out of scope β while the pointer still holds the old address. Dereferencing it is undefined behavior. I avoid it by nulling pointers right after free, never returning the address of a local (return by value, heap-allocate, or take a caller buffer), watching scopes, defining clear ownership of who frees what, and catching the rest with AddressSanitizer/Valgrind."
Solution / good example β robust patterns.
/* (a) Null-after-free discipline + a wrapper that does it for you */
#define FREE(p) do { free(p); (p) = NULL; } while (0)
char *buf = malloc(64);
... use buf ...
FREE(buf); // buf is now NULL, so a later *buf faults loudly
if (buf) { /* never taken β safe to test */ }
/* (b) Don't return a local's address β return by value OR take a caller buffer */
void fill(int *out){ *out = 10; } // caller owns 'out'
int main(void){ int v; fill(&v); } // no dangling possible
/* (c) Clear ownership for shared buffers: refcount so the LAST user frees */
typedef struct { void *data; int refs; } Buf;
Buf *acquire(Buf *b){ b->refs++; return b; }
void release(Buf *b){ if (--b->refs == 0) { free(b->data); free(b); } }
Follow-ups / gotchas. Distinguish from a memory leak (pointer lost, memory remains) β dangling is the opposite (memory gone, pointer remains). free(p) does not change p; that's why p = NULL is the discipline. Double-free is the sibling bug (freeing the same block twice β heap corruption).
Seen in: GfG ML & System Engineer ("What are dangling pointers?"), LeetCode SWE (Hyderabad), GfG Set 2.
A3 Β· Q: What is a void pointer and where is it used?ΒΆ
Frequency: π₯ Occasional (~2 reports).
Concept β the basis. A void* is a generic pointer: it can hold the address of any object type, but it carries no pointee type, so you cannot dereference it or do pointer arithmetic on it until you cast it to a concrete type. It is C's mechanism for type-erased, generic code.
Example:
int i = 5; float f = 2.5f;
void *vp;
vp = &i; printf("%d\n", *(int*)vp); // cast back to int* before deref
vp = &f; printf("%.1f\n", *(float*)vp); // same pointer variable, different type
// *vp; // ERROR: cannot dereference a void*
// vp + 1; // ERROR in standard C: arithmetic on void* is not allowed
Why it exists. Generic interfaces need a "pointer to something, I don't care what." malloc returns void* precisely so it can hand back untyped memory that you assign to any pointer type; qsort, memcpy, and callback context parameters all take void* so one implementation serves every type β C's substitute for templates/generics.
Where you see it (Qualcomm). The void *ctx / void *priv cookie in every driver/HAL callback (you store your private struct pointer there and cast it back inside the callback); generic ring-buffer/queue libraries shared across audio, camera, and modem code.
Answer. "A void* is a generic pointer that can point to any type but has no type information, so it must be cast before dereferencing and can't be used in arithmetic. It powers generic interfaces: malloc/free, memcpy, qsort's comparator and elements, and the void *context cookie in driver/HAL callbacks."
Follow-ups / gotchas. In C you should not cast malloc's result (int *p = malloc(...) is correct and avoids hiding a missing <stdlib.h>); in C++ the cast is mandatory. Arithmetic on void* is a GCC extension (treats it as char*), not standard.
Seen in: GfG Set 2 ("Handling generic functions with void pointers"); ubiquitous in driver code.
A4 Β· Q: What are function pointers and where would you use them?ΒΆ
Frequency: π₯π₯ Common (~5 reports).
Concept β the basis. Code lives in memory too, so every function has an address. A function pointer stores that address and lets you call a function indirectly β deciding which function at runtime. Declaration reads inside-out: int (*op)(int,int) is "op is a pointer to a function taking (int,int) and returning int."
Example β declaration & indirect call:
int add(int a,int b){ return a+b; }
int mul(int a,int b){ return a*b; }
int (*op)(int,int);
op = add; printf("%d\n", op(3,4)); // 7 (op(...) == (*op)(...))
op = mul; printf("%d\n", op(3,4)); // 12
struct cam_ops {
int (*open )(void *ctx);
int (*read )(void *ctx, void *buf, int n);
void (*close)(void *ctx);
};
/* framework calls dev->ops->read(ctx, buf, n) without knowing the concrete driver */
Why it exists. It's how C does polymorphism and late binding without classes: callbacks, event handlers, plugin/registration tables, and state machines (an array of function pointers indexed by state replaces a giant switch). It decouples the caller (framework) from the callee (your driver).
Where you see it (Qualcomm). The Linux struct file_operations / V4L2 v4l2_subdev_ops / Android Camera HAL3 camera3_device_ops are all tables of function pointers. ISR vector tables, sensor-driver registration, and codec plugin registries use the same pattern.
Answer. "A function pointer holds a function's address so you can call it indirectly and swap implementations at runtime. Syntax ret (*name)(params). Uses: callbacks (qsort comparator, event handlers), jump tables for state machines, and driver/HAL 'ops' structures β which is exactly how the Linux kernel and Android Camera HAL invoke driver-specific code through a table of function pointers."
Solution / good example β a clean state machine with a jump table:
typedef enum { S_IDLE, S_STREAMING, S_ERROR, S_COUNT } State;
typedef State (*handler)(int event);
State on_idle(int e){ return e ? S_STREAMING : S_IDLE; }
State on_stream(int e){ return e ? S_STREAMING : S_IDLE; }
State on_error(int e){ (void)e; return S_ERROR; }
handler table[S_COUNT] = { on_idle, on_stream, on_error }; // jump table
State step(State s, int e){ return table[s](e); } // O(1) dispatch, no switch
Follow-ups / gotchas. typedef int (*op_t)(int,int); makes declarations readable. You cannot inline a call through a function pointer (target unknown at compile time β ties to G3). An array of function pointers is a jump table replacing a switch.
Seen in: GfG Set 2 ("Function pointers β how, usage, examples"), GfG Set 8 ("Function pointers and applications", "Relationship between function pointers and inline functions"), camera-HAL/driver context.
A5 Β· Q: How does pointer arithmetic work? (What does subtracting two pointers give?)ΒΆ
Frequency: π₯ Occasional (~1β2 reports).
Concept β the basis. Pointer arithmetic is scaled by the pointee size. p + n is the address p + n*sizeof(*p) bytes. Subtracting two pointers into the same array yields the number of elements between them (type ptrdiff_t), not the byte distance. This is exactly why arr[i] is defined as *(arr + i).
Example:
int a[5] = {10,20,30,40,50};
int *p = &a[1]; // β 20
int *q = &a[4]; // β 50
printf("%ld\n", (long)(q - p)); // 3 (elements apart, NOT 12 bytes)
printf("%d\n", *(p + 2)); // 40 == a[3]
char *c = (char*)p;
printf("%ld\n", (long)((char*)q - c));// 12 (raw byte distance, via char*)
Why it exists. Scaling makes array traversal natural and type-safe: ++p always lands on the next element regardless of element size, so the same loop code works for char, int, or a 256-byte struct. It's the basis of how arrays and pointers interrelate.
Where you see it. Iterating a pixel buffer (px + width jumps a whole row), stepping through a descriptor array, computing a member's byte offset (offsetof), and the custom-sizeof trick (E4).
Answer. "Pointer arithmetic scales by sizeof(*p): p+n advances n elements (nΒ·size bytes). Subtracting two pointers into the same array gives the element count between them as a ptrdiff_t. It's only defined within one array (or one-past-the-end); subtracting/comparing unrelated pointers is undefined behavior."
Follow-ups / gotchas. &arr[n] (one-past-the-end) is legal to form but not to dereference. Subtracting pointers from different objects is undefined behavior. Cast to char* to get raw byte distances.
Seen in: GfG Set 2 ("Pointer subtraction").
A6 Β· Q: Can you access a local variable's data after its function returns?ΒΆ
Frequency: π₯ Occasional β appears as a UB/snippet trap.
Concept β the basis. Local (automatic) variables live in the function's stack frame, created on entry and destroyed on return; that memory is then reused by the next call. So a pointer to a local, returned to the caller, dangles (A2). It sometimes "still looks right" because nothing has overwritten it yet β which is precisely what makes the bug insidious.
Example & the fixes:
int *get(void){ int x = 99; return &x; } // BAD: returns address of a local
int main(void){
int *p = get();
foo(); // some other call reuses that stack space
printf("%d\n", *p); // UB: 99, garbage, or crash
}
Why it matters. It's the same root cause as the "return a buffer from a function" question (E5) and a top source of heisenbugs. Understanding it proves you understand lifetime vs scope.
Where you see it (Qualcomm). A helper that builds a descriptor on its stack and returns &desc; returning a pointer into a local char buf[] you formatted with snprintf.
Answer. "No β a local's lifetime ends when the function returns and its stack frame is reclaimed; returning a pointer to it gives the caller a dangling pointer (UB). Instead return by value, heap-allocate (caller frees), have the caller pass a buffer, or β rarely β make it static."
Solution / good example.
/* return by value (best for small data) */
int get_val(void){ int x = 99; return x; }
/* caller-supplied buffer (best in embedded: no hidden malloc) */
void format_id(char *out, size_t n, int id){ snprintf(out, n, "id=%d", id); }
char line[32]; format_id(line, sizeof line, 7); // 'line' lives in caller's frame
/* heap (caller owns/frees) */
int *make(void){ int *p = malloc(sizeof *p); if (p) *p = 99; return p; }
Follow-ups / gotchas. A static local survives but is shared across all calls (not reentrant β dangerous in threads/ISRs; see B2). Returning a pointer to a string literal is fine (literals have static storage in .rodata); returning a pointer to a local char buf[] is not.
Seen in: GfG Set 2 ("Accessing data in called functions after return β auto variables limitation").
B. Storage classes & linkageΒΆ
B1 Β· Q: Explain the storage classes in C.ΒΆ
Frequency: π₯π₯ Common (~5 reports).
Concept β the basis. A storage class controls two orthogonal properties of an identifier: its storage duration (how long the object lives) and its linkage/scope (where its name is visible). C has four storage-class specifiers:
| Specifier | Storage duration | Linkage / scope | Typical use |
|---|---|---|---|
auto |
automatic (stack) | block scope, no linkage | the default for locals β keyword almost never written |
register |
automatic | block scope | hint to keep in a CPU register; you can't take its & |
static |
static (whole program) | internal linkage (file) or none (function-local) | persistent locals; file-private globals/functions |
extern |
static | external linkage | declare a global defined in another translation unit |
Example:
int g = 1; // static duration, EXTERNAL linkage (visible to other files)
static int s = 2; // static duration, INTERNAL linkage (this file only)
void f(void){
auto int a = 0; // automatic; 'auto' is redundant β same as 'int a = 0;'
static int c = 0; // initialized ONCE; persists across calls
register int r = 0;// hint only
c++; // counts how many times f() was called
}
extern int g; // in ANOTHER file: "g is defined elsewhere"
Why it exists. C compiles each .c file (translation unit) separately and links them later. Storage classes are the vocabulary that controls (a) lifetime β automatic for cheap, scope-bound locals vs static for things that must persist; and (b) visibility across files β static to keep a symbol private, extern to share one. This is how C does encapsulation and modular linking before it ever had namespaces.
Where you see it (Qualcomm). static helper functions and config tables kept private to a driver .c; an extern global clock/registers handle shared across a subsystem; register/auto essentially never written by hand today.
Answer. "C has auto (default automatic/stack storage for locals), register (a hint to use a CPU register β can't take its address), static (static duration: the object lives for the whole program; gives file-private internal linkage for globals/functions and persistence for locals), and extern (declares an object with external linkage defined in another translation unit). Together they set storage duration and linkage."
Follow-ups / gotchas. typedef is syntactically a storage-class specifier but creates a type alias, not storage. Default linkage: file-scope = external, block-scope = none. Uninitialized static/global objects are zero-initialized (they live in .bss); uninitialized locals are indeterminate (undefined behavior to read).
Seen in: GfG Set 2 ("Storage classes and their mapping"), GfG Set 8 ("Static variables and functions"), GfG ML & System Engineer.
B2 Β· Q: When do you use the static keyword? (What does it do?)ΒΆ
Frequency: π₯π₯π₯ Very common (~8 reports) β a Qualcomm staple with many follow-ups.
Concept β the basis. static means different things by context, but the unifying idea is "static storage duration and/or restricted linkage."
1. Static local variable β lives for the whole program (not just the call), initialized once, retains its value between calls. For counters, lazy init, last-state caching.
2. Static global variable / function β internal linkage: visible only within its .c file. This is C's private/encapsulation; prevents name clashes across files.
3. (C++) static class member / method β one instance shared by all objects; see 02_cpp_oop.md.
Example β persistence + file privacy:
// counter.c
static int calls = 0; // file-private: other .c files cannot see 'calls'
int next_id(void){
static int id = 100; // initialized once; survives across calls
calls++;
return id++; // returns 100, 101, 102, ...
}
static void helper(void){ } // file-private function (internal linkage)
Why it exists. Two real needs: (a) persistent per-function state without exposing a global (encapsulated counters, memoization, one-time init); (b) namespacing in a language with no namespaces β marking file-local symbols static keeps the global symbol table clean and lets two files each have their own static void reset(void) with no collision.
Where you see it (Qualcomm). static lookup tables and gamma/tone-curve constants private to an ISP module; a static "already initialized?" flag guarding one-time hardware bring-up; file-private helper functions throughout driver code.
Answer. "Three uses: (1) a static local persists across calls and is initialized once β good for counters, caches, lazy init; (2) a static global or function has internal linkage, so it's private to its source file β C's encapsulation and clash-avoidance; (3) in C++ a static member is shared by all instances. The common thread is static storage duration and/or restricted visibility."
Solution / good example β thread-safe one-time init (the gotcha made safe):
#include <pthread.h>
static pthread_once_t once = PTHREAD_ONCE_INIT;
static Hw *g_hw; // file-private singleton
static void do_init(void){ g_hw = hw_bring_up(); }
Hw *get_hw(void){
pthread_once(&once, do_init); // runs do_init exactly once, safely
return g_hw;
}
Follow-ups / gotchas. Static locals are not reentrant / not thread-safe β a shared static inside a function called from multiple threads or an ISR is a classic race (guard with a mutex, pthread_once, or thread-local storage). With a constant initializer the static is set up before main; otherwise (C++ local statics) it's lazy on first reach. Cross-link: races/mutexes β 04_os.md.
Seen in: GfG Set 8, GfG Set 2, GfG ML & System Engineer, AmbitionBox engineer reports.
B3 Β· Q: What do const and volatile mean? (Why does embedded/driver code lean on them?)ΒΆ
Frequency: π₯ Occasional (~2 reports) β but assumed knowledge for any driver/camera role.
Concept β the basis. They are type qualifiers, independent of storage class.
- const = "I promise not to modify this object through this name." A compile-time guarantee for safety/readability; also lets the toolchain place data in ROM.
- volatile = "this object may change outside the normal program flow, so re-read it from memory on every access and never optimize the access away." Essential for memory-mapped hardware registers, variables shared with an ISR, and memory written by DMA β i.e. the camera/driver world.
Example β the canonical embedded combos:
volatile uint32_t *STATUS = (uint32_t*)0x40001000; // hardware status register
while ((*STATUS & 1) == 0) { } // volatile forces a REAL re-read each iteration;
// without it the compiler may read once and spin forever
const int MAX = 100; // read-only constant (can live in ROM)
const volatile uint32_t *TIMER; // read-only to us, but changes on its own (free-running)
Why it exists. Optimizers assume memory only changes when your code changes it. That assumption is false for hardware registers and ISR/DMA-shared memory, where the value can change between two reads with no store in your code. volatile switches the optimizer's assumption off for that object. const exists to encode and enforce immutability (catch accidental writes at compile time, enable ROM placement, and document intent).
Where you see it (Qualcomm). Literally every register definition in a driver is volatile; status/ID registers are const volatile; calibration/tuning tables are const (often in flash); a volatile sig_atomic_t flag set by an ISR and polled by the main loop.
Answer. "const is a compiler-enforced read-only promise β improves safety and allows ROM placement. volatile tells the compiler the value can change unexpectedly (hardware register, ISR-shared variable, DMA buffer), so it must do an actual memory access every time and not cache the value in a register or elide the access. Drivers often combine them: const volatile for a read-only hardware register."
Follow-ups / gotchas. volatile is NOT a concurrency/synchronization primitive β it gives neither atomicity nor memory ordering between threads; use _Atomic/atomics or a mutex for that (cross-link 04_os.md). Pointer placement of const matters: const int *p (pointee is const) vs int * const p (the pointer is const) vs const int * const p (both). Casting away const and then writing a truly-const object is undefined behavior.
Seen in: AmbitionBox/embedded reports; standard expectation for Qualcomm driver/ISP roles.
B4 Β· Q: Where is a variable stored? What happens when you have more live variables than registers?ΒΆ
Frequency: π₯ Occasional β snippet-style (GfG Set 2 / Set 8).
Concept β the basis. Storage location follows storage class (see the memory map, D1): globals/static β .data/.bss; locals β stack (or held in CPU registers when the optimizer can); dynamic β heap; literals/const β .rodata. The compiler keeps "hot" locals in CPU registers for speed, but registers are few (e.g. ~16 general-purpose on x86-64, ~31 on AArch64). When more values are live simultaneously than there are registers, the compiler spills the excess to stack slots and reloads them on demand β this is register allocation / spilling.
Example (conceptual):
int f(int a,int b,int c,int d,int e,int g,int h,int i){
// many values live at once β some are "spilled" to stack and reloaded when used
return a+b+c+d+e+g+h+i;
}
Why it matters. Registers are the fastest storage; spills are extra loads/stores. In hot loops (e.g. per-pixel ISP code) excessive spilling hurts throughput β which is why people minimize live variables and why -O2 aggressively allocates registers.
Where you see it (Qualcomm). Performance tuning of tight DSP/ISP loops; reading disassembly to see whether a critical loop spilled; understanding why a register hint is meaningless to a modern allocator.
Answer. "It depends on storage class: globals/static live in .data (initialized) or .bss (zeroed), locals live on the stack but are often promoted to CPU registers, dynamic objects live on the heap, and constants/literals live in read-only memory. Registers are limited, so when more values are live than there are registers, the compiler spills some to stack slots and reloads them β register allocation."
Seen in: GfG Set 2 / Set 8 ("variable storage when exceeding available registers", "where a program is stored").
C. Dynamic memoryΒΆ
C1 Β· Q: Difference between malloc, calloc, and realloc? What does malloc return?ΒΆ
Frequency: π₯π₯π₯ Very common (~11 reports) β the single most-asked C-memory question.
Concept β the basis. These are the standard heap allocators from <stdlib.h>. The heap is memory you manage manually; the allocator hands out blocks and tracks bookkeeping.
- void *malloc(size_t n) β returns a block of n uninitialized bytes (contents are garbage).
- void *calloc(size_t count, size_t size) β allocates count*size bytes, zero-initialized, and checks the multiplication for overflow.
- void *realloc(void *p, size_t n) β resizes an existing block to n bytes, preserving existing contents; may move the block (so it returns a possibly-new address); realloc(NULL, n) β‘ malloc(n).
All return void* (so they're type-agnostic) or NULL on failure. Every successful allocation must be freed exactly once.
Example:
int *a = malloc(4 * sizeof *a); // 4 ints, UNINITIALIZED (garbage)
int *b = calloc(4, sizeof *b); // 4 ints, all ZERO; overflow-checked
if (!a || !b) { /* allocation failed β handle it */ }
int *bigger = realloc(a, 8 * sizeof *a); // grow to 8 ints, keeping the first 4
if (bigger) a = bigger; // assign to a TEMP first (see gotcha)
free(a); free(b); // exactly once each
Why it exists. Not all sizes/lifetimes are known at compile time β an image is whatever resolution the sensor reports; a request queue grows and shrinks. The heap provides runtime-sized, caller-controlled-lifetime storage. Three functions because the common needs differ: raw fast bytes (malloc), zeroed memory (calloc, also the safe way to multiply sizeΓcount), and grow/shrink (realloc).
Where you see it (Qualcomm). Allocating frame/metadata buffers sized to the current mode; growable arrays of capture requests; building variable-length packets. (Note: hard-real-time and ISR paths often avoid the heap entirely β see "Where you see it" in D2 β and use pools/static buffers for determinism.)
Answer. "malloc(n) returns n uninitialized bytes; calloc(count,size) returns zeroed memory and guards against count*size overflow; realloc(p,n) resizes a block, preserving contents and possibly relocating it. All return void* (implicitly convertible to any object pointer in C) or NULL on failure, and each allocation must be freed exactly once. malloc specifically returns a void* to the start of the block, or NULL if it can't satisfy the request."
Solution / good example β the safe realloc idiom (avoid the leak):
char *tmp = realloc(buf, newsize);
if (!tmp) { /* buf is still valid & must still be freed later */ return -ENOMEM; }
buf = tmp; // only overwrite buf AFTER success
Follow-ups / gotchas. (1) Never write p = realloc(p, n); β on failure it returns NULL and you leak the original block. (2) Don't cast malloc in C. (3) free(NULL) is a safe no-op. (4) malloc(0) may return NULL or a unique freeable pointer (implementation-defined). (5) Always check for NULL. (6) Prefer sizeof *p over sizeof(type) so the size tracks the pointer.
Seen in: GfG Set 8 ("malloc vs calloc"), GfG ML & System Engineer ("How is memory allocated during malloc and calloc?"), LeetCode SWE, AmbitionBox engineer ("What does malloc return?"), and more.
C2 Β· Q: How is memory allocated by malloc, and how do you know whether a piece of memory is in use or free?ΒΆ
Frequency: π₯ Occasional (~2 reports) β the deep follow-up to C1.
Concept β the basis. The allocator (e.g. glibc's ptmalloc) requests big regions from the OS via brk/sbrk (grows the heap segment) or mmap (for large requests) and then sub-divides them. It keeps bookkeeping: typically a small header before each block storing its size and an in-use/free flag, plus free lists / bins of recycled blocks. On malloc it finds a suitable free block (first-fit / best-fit / binned); on free it marks the block free and may coalesce it with adjacent free blocks to fight fragmentation.
From the program's side there is no portable, standard way to ask "is this pointer still valid/allocated?" β that knowledge lives in the allocator's private metadata. So you manage it with discipline and tools.
Example β the only reliable "is it valid" you control:
char *buf = malloc(64);
... use buf ...
free(buf);
buf = NULL; // now you CAN test: if (buf) {valid} else {already freed}
Why it exists. Going to the kernel for every tiny allocation would be far too slow (a syscall each time). So the C library amortizes: grab memory in bulk, hand it out cheaply in user space, recycle freed blocks. The header/free-list machinery is the price of O(1)-ish malloc/free.
Where you see it (Qualcomm). Diagnosing heap fragmentation in a long-running camera/audio daemon; understanding why free doesn't shrink RSS; choosing a slab/pool allocator for fixed-size frame descriptors to avoid fragmentation.
Answer. "malloc doesn't hit the OS each time β the C library grabs large regions via sbrk/mmap and sub-allocates, keeping a per-block header (size + in-use flag) and free lists; free marks a block free and coalesces neighbors. There's no standard API to query whether an arbitrary pointer is currently allocated β that metadata is private β so you track ownership yourself (null pointers after free) and use Valgrind/AddressSanitizer to find leaks, double-frees, and use-after-free."
Follow-ups / gotchas. Writing outside a block corrupts the neighboring header β crashes later, far from the bug (heap corruption). Tools: Valgrind memcheck, ASan. Pools/slabs trade flexibility for determinism and zero fragmentation.
Seen in: GfG ML & System Engineer ("How do you know whether a memory is in use or free?"), GfG Set 2.
C3 Β· Q: What are memory leaks? How do you identify and prevent them?ΒΆ
Frequency: π₯ Occasional (~2 reports).
Concept β the basis. A memory leak is heap memory that is never freed and is no longer reachable β you've lost the last pointer to it, so it can never be returned. It's the mirror image of a dangling pointer (there: memory gone, pointer remains; here: pointer gone, memory remains). Leaks slowly exhaust RAM β fatal for long-running embedded/camera daemons that must run for weeks.
Example β a leak on the error path:
void leaky(void){
char *b = malloc(1024);
if (error()) return; // LEAK: early return without free(b)
free(b);
}
// also: p = malloc(...); p = malloc(...); // first block leaked (pointer overwritten)
Why it matters. Embedded targets have little RAM and no human to restart the process; a slow leak that "doesn't matter" on a desktop is a field failure on a phone after a few days of camera use.
Where you see it (Qualcomm). A capture path that allocates per-frame metadata but skips free on an error branch; a registration that mallocs a node but never removes it on teardown; growth in RSS over a soak test.
Answer. "A memory leak is heap memory that's allocated but never freed and no longer reachable, so it accumulates and can exhaust memory. I find leaks with tools β Valgrind/LeakSanitizer, AddressSanitizer, static analyzers β and by watching RSS grow over a soak test. I prevent them by pairing every malloc with exactly one free, freeing on all paths including errors, clear ownership rules, and RAII in C++."
Solution / good example β single-exit cleanup with goto (the idiomatic C pattern):
int process(void){
char *a = NULL, *b = NULL;
int rc = -1;
if (!(a = malloc(256))) goto out;
if (!(b = malloc(512))) goto out; // if this fails, 'a' is still freed below
... do work ...; rc = 0;
out:
free(b); // free(NULL) is safe
free(a);
return rc; // every path frees everything exactly once
}
Follow-ups / gotchas. The goto cleanup idiom is the standard C way to guarantee cleanup on every error path (the Linux kernel uses it everywhere). Sibling bugs: double-free, use-after-free (A2). Cross-link: tooling + "buffer overflow / stack smashing" detail β 04_os.md.
Seen in: GfG Set 2 ("Mem leaks & corresponding tools"), GfG ML & System Engineer.
D. Program memory modelΒΆ
D1 Β· Q: Explain the memory layout (memory map) of a C program.ΒΆ
Frequency: π₯ Occasional (~1β2 reports) β and the backbone for B4 / C / D2.
Concept β the basis. A running C program's virtual address space is divided into segments. From high to low address: command-line args/env, the stack (grows down), an unmapped gap, the heap (grows up), then the static segments β .bss (uninitialized globals/static, zeroed at load), .data (initialized globals/static), .rodata (constants & string literals, read-only), and .text (machine code, read-only/executable).
Example β where each thing lives:
int g_init = 5; // .data
int g_zero; // .bss
const char *msg = "hi"; // pointer 'msg' in .data; the literal "hi" in .rodata
void f(void){
int local = 1; // stack
static int s = 2; // .data (s is static β NOT on the stack)
int *h = malloc(8); // 'h' is on the stack; the 8 bytes are on the heap
}
Why it exists. Separating segments lets the OS assign different permissions (code read-only/executable, .rodata read-only, stack/heap read-write-noexec) so bugs and exploits are caught by the MMU (e.g. writing to a string literal faults instead of silently corrupting code). Splitting .data (initialized) from .bss (zeroed) also shrinks the executable: .bss stores only a size, not a megabyte of zeros.
Where you see it (Qualcomm). Reading a linker script / map file for an embedded image to see where code and data land in flash/RAM; understanding why a giant static array bloats .bss (RAM) but not the binary; why writing through a bad pointer hits .text and faults.
Answer. "A C program's address space has .text (executable code, read-only), .rodata (constants and string literals), .data (initialized globals/static), .bss (uninitialized globals/static, zeroed at load), the heap (dynamic memory, grows up), and the stack (locals and call frames, grows down), with args/env at the top. The OS gives each segment different permissions, enforced by the MMU."
Follow-ups / gotchas. Uninitialized globals are guaranteed zero (.bss); uninitialized locals are garbage. String literals are read-only β char *s="x"; s[0]='Y'; is undefined behavior (use char s[]="x"; to get a writable copy). Cross-link: virtual memory/paging β 04_os.md.
Seen in: GfG Set 2 ("Memory map of program"), GfG Embedded System ("memory hierarchy / where a program is stored").
D2 Β· Q: Difference between stack memory and heap memory.ΒΆ
Frequency: π₯π₯π₯ Very common (~7 reports).
Concept β the basis. Both store runtime data but differ in who manages them, lifetime, speed, size, and structure.
| Stack | Heap | |
|---|---|---|
| Managed by | compiler (automatic) | programmer (malloc/free) |
| Lifetime | tied to function/scope | until you free it |
| Allocation cost | push/pop a frame β just move the stack pointer (very fast) | search free lists + bookkeeping (slower) |
| Size | small, fixed (β1β8 MB) β stack overflow if exceeded | large (limited by RAM/address space) |
| Fragmentation | none (LIFO) | possible |
| Access pattern | LIFO, cache-friendly | random |
Example:
void f(void){
int s[1000]; // stack: freed automatically when f returns
int *h = malloc(1000*sizeof *h); // heap: lives until free(h)
...
free(h); // you MUST free heap memory
} // s is reclaimed here automatically
Why it exists. Two allocation strategies for two needs. The stack is a perfect fit for the call/return discipline β a function's locals are born on entry and die on return, exactly LIFO, so allocation is a single pointer move and there's never fragmentation. But the stack can't outlive its scope and is small. The heap fills that gap: arbitrary size, caller-chosen lifetime β at the cost of manual management and speed.
Where you see it (Qualcomm). ISR and hard-real-time paths prefer the stack or preallocated pools (heap allocation isn't deterministic and can fragment/fail β unacceptable mid-frame); large/long-lived image buffers go on the heap (or DMA-able pools). A huge local array (int s[1000000]) or deep recursion blows the small stack.
Answer. "Stack memory is compiler-managed: locals and call frames are pushed/popped LIFO, allocation is just moving the stack pointer (fast), and memory is freed automatically when the scope ends β but it's small and fixed, so deep recursion or big arrays overflow it. Heap memory is manually managed with malloc/free, lives until freed, is much larger, but is slower and can fragment. Use the stack for small short-lived data, the heap for large/long-lived/dynamically-sized data β and avoid the heap on real-time/ISR paths."
Follow-ups / gotchas. Returning a pointer to a stack local is the dangling-pointer bug (A6). Stack grows down, heap grows up (D1). Allocation speed and determinism are why embedded code often bans heap use after init. Cross-link: virtual memory β 04_os.md.
Seen in: GfG Set 8 ("Stack vs heap memory with code examples"), LeetCode SWE, multiple AmbitionBox reports.
D3 Β· Q: What is a segmentation fault? What causes it?ΒΆ
Frequency: π₯ Occasional (~2 reports).
Concept β the basis. A segmentation fault (SIGSEGV) is a trap raised by the hardware/OS when a program accesses memory it isn't allowed to β an address outside its mapped segments, or with the wrong permission (e.g. writing to read-only .text/.rodata). The MMU checks every access against the page tables; an illegal one faults.
Example β the classic causes:
int *p = NULL; *p = 5; // (1) null dereference
char *s = "lit"; s[0] = 'X'; // (2) writing to read-only .rodata
int a[3]; a[1000000] = 0; // (3) wild out-of-bounds write
int *q; *q = 7; // (4) uninitialized/wild pointer
free(p); *p = 1; // (5) use-after-free (sometimes faults)
void r(void){ r(); } // (6) infinite recursion β stack overflow
Why it exists. Memory protection is a feature: the MMU + OS isolate processes from each other and catch illegal accesses early, turning silent corruption into an immediate, debuggable crash. Without it a stray write could clobber another process or the kernel.
Where you see it (Qualcomm). Bring-up crashes from a bad register address or an unmapped DMA region; a driver dereferencing a NULL ctx; an off-by-one walking a frame buffer. You debug with a core dump + GDB bt, or AddressSanitizer.
Answer. "A segmentation fault (SIGSEGV) happens when you access memory you don't own or with the wrong permissions; the MMU traps it. Common causes: dereferencing NULL or an uninitialized/wild pointer, writing to a string literal/other read-only memory, out-of-bounds access, use-after-free, and stack overflow from runaway recursion. Debug with a core dump + GDB backtrace or AddressSanitizer."
Solution / good example β turn a crash into a fast diagnosis:
ulimit -c unlimited # enable core dumps
./app # crashes β core file
gdb ./app core -ex bt -ex quit # backtrace points straight at the bad line
# or build with: -fsanitize=address -g β ASan prints the exact faulting access
Follow-ups / gotchas. Not every out-of-bounds access faults β it may silently corrupt adjacent memory (worse). Distinguish SIGSEGV (bad address/permission) from SIGBUS (e.g. misaligned access on a strict-alignment CPU). Cross-link: core dumps/paging β 04_os.md / 07_embedded_linux_kernel.md.
Seen in: GfG ML & System Engineer ("What is a segmentation fault?"), GfG Set 2 (core dumps / error handling).
E. Implement-it routines (write-the-code questions)ΒΆ
E1 Β· Q: Implement memcpy() yourself β including the overlapping-memory case.ΒΆ
Frequency: π₯π₯π₯ Very common (~8 reports) β a signature Qualcomm question.
Concept β the basis. memcpy(dst, src, n) copies n bytes from src to dst. Its contract says the regions must not overlap β if they do, behavior is undefined, because a naive forward copy would overwrite bytes of src before reading them. The library provides memmove for the overlapping case: it picks the copy direction so it never clobbers not-yet-read bytes β copy forward when dst < src, backward when dst > src.
Why it exists / why the overlap rule. Two functions because of a speed/safety trade-off. memcpy may copy in any order and a word/SIMD at a time β fastest, but only correct for disjoint regions. memmove adds the direction check to be overlap-safe β slightly slower. The standard splits them so the common (disjoint) case pays nothing for safety it doesn't need.
Where you see it (Qualcomm). Copying scanlines/tiles within one image buffer (often overlapping β must be memmove); shifting bytes in a ring buffer; assembling packets. ISP/DSP code cares deeply about the aligned word/SIMD fast path.
Answer (code).
#include <stddef.h>
/* memcpy: assumes NO overlap (matches the standard contract). */
void *my_memcpy(void *dst, const void *src, size_t n) {
unsigned char *d = dst; const unsigned char *s = src;
while (n--) *d++ = *s++; // simple byte copy
return dst;
}
/* memmove: correct even if the regions overlap. */
void *my_memmove(void *dst, const void *src, size_t n) {
unsigned char *d = dst; const unsigned char *s = src;
if (d == s || n == 0) return dst;
if (d < s) { while (n--) *d++ = *s++; } // forward: no clobber
else { d += n; s += n; while (n--) *--d = *--s; } // overlap β backward
return dst;
}
word/SIMD register at a time once the pointers are aligned, with a byte loop for the unaligned head/tail β that's where most of the speed comes from.
Follow-ups / gotchas. Use unsigned char* (byte copy; avoids strict aliasing issues). Handle n == 0 and dst == src. If asked "what if they overlap?" β "that's memmove, here's the direction logic." Returning dst matches the standard signature.
Seen in: GfG Set 2 ("implement memcpy() on your own, including overlap situations"), GfG Set 8 ("Implement custom memcpy"), CleverPrep/embedded reports.
E2 Β· Q: Implement strcmp().ΒΆ
Frequency: π₯ Occasional (~2β3 reports).
Concept β the basis. strcmp(a,b) compares two NUL-terminated strings lexicographically and returns < 0, 0, or > 0 for a less than / equal to / greater than b. It walks both in lockstep, stopping at the first differing byte or the terminating '\0'; the result is the difference of the first differing characters compared as unsigned char.
Example: strcmp("apple","apply") compares 'e'(101) vs 'y'(121) β 101β121 = β20 (negative β "apple" < "apply").
Why it exists / why unsigned char. Lexicographic ordering needs a defined sign on the first difference. Comparing as unsigned char makes high/non-ASCII bytes order consistently and the result sign well-defined (signed char could be negative and flip the comparison).
Where you see it. Sorting/looking up strings, parsing protocol tokens, comparing sensor/part IDs β and as a quick "do you handle the '\0' terminator and sign correctly?" probe.
Answer (code).
int my_strcmp(const char *a, const char *b) {
while (*a && (*a == *b)) { a++; b++; }
return (unsigned char)*a - (unsigned char)*b; // 0 if equal; sign on first diff
}
Follow-ups / gotchas. The loop must stop on '\0' (the *a test handles end-of-string). strncmp adds a length bound (safer on non-terminated buffers). Returning just -1/0/1 is fine too if you prefer.
Seen in: GfG Set 2 ("Strcmp, pgm"), embedded reports.
E3 Β· Q: Implement strstr() β find a substring β optimally.ΒΆ
Frequency: π₯ Occasional (~2β3 reports).
Concept β the basis. strstr(hay, needle) returns a pointer to the first occurrence of needle in hay, or NULL. The naive approach tries to match needle at every position of hay β O(nΒ·m) worst case. The optimal approach is KMP (KnuthβMorrisβPratt), O(n+m): precompute a "failure"/LPS (longest-proper-prefix-which-is-also-suffix) table from needle, so on a mismatch you shift by the LPS instead of re-examining hay characters.
Example: searching needle="aab" in hay="aaab" β naive backtracks on each a; KMP uses LPS(aab)=[0,1,0] to skip rescans.
Why it exists / why KMP. Naive matching re-reads hay characters after a partial match fails. KMP's insight: a failed partial match already tells you something about the text you just read, so you can resume without backing up in hay β linear time, crucial for large inputs/streams.
Where you see it. Log/stream scanning, protocol/marker detection, parsing β and as a "do you know an O(n+m) algorithm, not just brute force?" signal.
Answer (code β naive first, then mention KMP).
/* Naive O(n*m): fine to write under time pressure, then discuss KMP. */
char *my_strstr(const char *hay, const char *needle) {
if (!*needle) return (char*)hay; // empty needle β match at start
for (; *hay; hay++) {
const char *h = hay, *n = needle;
while (*h && *n && *h == *n) { h++; n++; }
if (!*n) return (char*)hay; // reached needle's end β full match
}
return NULL;
}
hay once in O(n), shifting by the LPS on mismatch β O(n+m) total, no backtracking in hay." (Full LPS construction β 03_dsa.md.)
Follow-ups / gotchas. Empty needle returns hay. BoyerβMoore is another fast option (sublinear in practice). Be ready to write the LPS table if pushed.
Seen in: GfG Set 2 ("Write your own strstr, optimal way").
E4 Β· Q: Implement your own sizeof operator.ΒΆ
Frequency: π₯ Occasional (~2β3 reports) β a pointer-arithmetic trick.
Concept β the basis. sizeof is a compile-time operator (not a function) yielding an object/type's size in bytes. You can mimic it for a variable using the pointer-arithmetic rule (A5): &x + 1 is the address one element past x; subtracting addresses as char* gives the byte size.
Example: for int x;, (char*)(&x + 1) - (char*)&x = 4 on a typical system.
Why it's a good question. It checks that you understand sizeof is compile-time (no runtime cost), that pointer arithmetic is element-scaled, and that casting to char* converts "elements" into "bytes." It's a concept probe disguised as a puzzle.
Answer (code).
/* For a VARIABLE: (&(x)+1) points one element past x; the byte gap is its size. */
#define my_sizeof(x) ((char*)(&(x) + 1) - (char*)&(x))
int a; /* my_sizeof(a) == 4 */ double d; /* my_sizeof(d) == 8 */
/* For a TYPE (can't take its address): do arithmetic on a null T*. */
#define my_sizeof_type(T) ((char*)((T*)0 + 1) - (char*)(T*)0)
Follow-ups / gotchas. It's a macro β no runtime cost, like real sizeof. The variable form needs &x; the type form uses a null-pointer trick. Real sizeof also handles VLAs and is a constant expression for fixed types. Don't pass side-effecting expressions (macro double-use β see G2).
Seen in: GfG Set 8 ("Custom sizeof operator implementation").
E5 Β· Q: Write a function that returns a "stream of bytes."ΒΆ
Frequency: π₯ Occasional β GfG Set 2 phrasing.
Concept β the basis. "Return a stream/buffer of bytes" tests whether you understand lifetime (A6): you cannot return a pointer to a local array (it dangles). Correct options: (a) heap-allocate and let the caller free; (b) have the caller pass a buffer; (c) use a static buffer (simple but not reentrant).
Why it's asked. It's the constructive flip side of the dangling-pointer question β can you produce a buffer without creating a dangler, and do you reason about ownership (who frees it)?
Where you see it (Qualcomm). APIs that return a packet/descriptor; the pervasive embedded convention "caller provides the buffer, we fill it" (no hidden malloc, predictable memory).
Answer (code) β the two robust patterns.
/* (a) heap: caller must free() β return ownership */
unsigned char *make_bytes(size_t n) {
unsigned char *buf = malloc(n);
if (!buf) return NULL;
for (size_t i = 0; i < n; i++) buf[i] = (unsigned char)i;
return buf; // ok: heap outlives the function
}
/* (b) caller-supplied buffer (preferred in embedded β no hidden allocation) */
void fill_bytes(unsigned char *buf, size_t n) {
for (size_t i = 0; i < n; i++) buf[i] = (unsigned char)i;
}
Follow-ups / gotchas. Returning &local_array[0] is the dangling bug. Always document who owns/frees the buffer. Embedded/driver code overwhelmingly prefers pattern (b).
Seen in: GfG Set 2 ("Write a program to return a stream of bytes from a function").
F. Data types & representationΒΆ
F1 Β· Q: Explain big-endian vs little-endian. How do you detect it, and swap between them?ΒΆ
Frequency: π₯π₯ Common (~4 reports).
Concept β the basis. Endianness is the byte order a CPU uses to store a multi-byte integer. Little-endian stores the least-significant byte at the lowest address (x86, ARM default); big-endian stores the most-significant byte first (network byte order; some MIPS/PowerPC). It matters whenever bytes cross a boundary: files, network packets, and reading multi-byte sensor/hardware registers β a real camera/driver concern.
Example β the 4-byte int 0x01020304 in memory: little-endian bytes are 04 03 02 01; big-endian bytes are 01 02 03 04.
Why it exists. A historical CPU-design choice with no single winner: little-endian makes low-byte access and width-casts convenient; big-endian matches human/"network" left-to-right order. Because both shipped widely, byte order became a portability issue you must handle whenever bytes are serialized (file/network/register) rather than kept as a CPU integer.
Where you see it (Qualcomm). Parsing a multi-byte value out of a sensor/ISP register or a packed image header; sending/receiving network or inter-processor (modemβAP) packets where the wire format is big-endian; bring-up on a big-endian DSP vs little-endian ARM.
Answer (code).
int is_little_endian(void){ unsigned x = 1; return *(char*)&x == 1; } // first byte 1 β LE
uint32_t bswap32(uint32_t v){
return ((v & 0x000000FFu) << 24) | ((v & 0x0000FF00u) << 8) |
((v & 0x00FF0000u) >> 8) | ((v & 0xFF000000u) >> 24);
}
1 in an int and reading its first byte via char*/a union. Swap with shifts/masks or __builtin_bswap32/htonl. It matters for networking, file formats, and reading hardware registers. Network byte order is big-endian β use htons/htonl/ntohl for portability."
Follow-ups / gotchas. A union { uint32_t i; uint8_t b[4]; } is the other standard detection trick. Endianness affects only byte order, not the bit order within a byte, and never single bytes. Bit-fields' layout is also implementation-defined.
Seen in: GfG Set 2 ("Big & Little endian β definitions, representations, write it down, swap them"), embedded reports.
F2 Β· Q: Signed vs unsigned integers β representation, ranges, and overflow.ΒΆ
Frequency: π₯ Occasional (also appears in 09_computer_arch_digital_design.md).
Concept β the basis. Integers are fixed-width. Unsigned uses all bits for magnitude: an n-bit unsigned holds 0 β¦ 2βΏβ1 and wraps modulo 2βΏ on overflow (well-defined). Signed uses two's complement: the top bit is the sign, range β2βΏβ»ΒΉ β¦ 2βΏβ»ΒΉβ1, and signed overflow is undefined behavior in C. Mixing signed and unsigned triggers implicit conversion that can surprise you.
Example β the classic trap:
int a = -1; unsigned b = 1;
if (a > b) puts("yes"); // prints "yes"! a converts to unsigned: -1 β 4294967295 > 1
unsigned int 0β¦4,294,967,295; int β2,147,483,648β¦2,147,483,647.
Why it exists / why two's complement. Two's complement lets the same adder hardware do signed and unsigned addition/subtraction (no separate sign handling), and it has a single representation of zero β so it's universal. Unsigned wraparound is defined because it's just modular arithmetic; signed overflow is left undefined so compilers can optimize assuming it "can't happen."
Where you see it (Qualcomm). Pixel/sample values and sizes are unsigned (uint8_t, size_t); reverse loops and "difference of two counters" are where signed/unsigned bugs bite; register fields are unsigned bit-patterns.
Answer. "Unsigned integers represent 0β¦2βΏβ1 and wrap modulo 2βΏ (defined); signed integers use two's complement, range β2βΏβ»ΒΉβ¦2βΏβ»ΒΉβ1, and signed overflow is undefined behavior. In a mixed expression the signed operand converts to unsigned, which is why (-1 > 1u) is true. I use unsigned for sizes/bit-twiddling (size_t) and stay careful with comparisons and loop counters."
Follow-ups / gotchas. for (size_t i = n; i >= 0; i--) never ends β unsigned can't go below 0. Two's-complement negate = invert-bits + 1. Right-shifting a negative signed int is implementation-defined. INT_MIN has no positive counterpart.
Seen in: GfG Embedded System ("Signed and unsigned integer ranges"); standard C type question.
F3 Β· Q: Difference between struct and union (and what is structure padding)?ΒΆ
Frequency: π₯π₯ Common (~4 reports).
Concept β the basis. A struct lays out its members side by side β its size is (at least) the sum of members, plus padding. A union overlays all members in the same storage β its size is that of its largest member, and only one member is meaningfully active at a time. Padding/alignment: the compiler inserts gaps so each member starts at an address that is a multiple of its alignment (CPUs access aligned data faster, or require it), so member order affects struct size.
Example:
struct S { char c; int i; }; // sizeof ~ 8 : char(1) + 3 pad + int(4)
union U { char c; int i; }; // sizeof = 4 : c and i SHARE the same 4 bytes
union U u; u.i = 0x41424344; // write as int
printf("%c\n", u.c); // read as char β 'D' on little-endian (type-punning)
Why they exist. struct models a record (a fixed bundle of named fields) β the foundation of every data type, register block, and packet header in C. union exists to (a) save memory when only one of several fields is needed at a time (tagged variants), and (b) reinterpret bytes (type punning) β e.g. view a float's bits, or overlay a register as a whole word and as named bit-fields. Padding exists because aligned access is faster/required by the hardware.
Where you see it (Qualcomm). Register definitions as a union of a raw uint32_t and a bit-field struct; packet/descriptor layouts as struct (often __attribute__((packed)) for wire format); tagged message variants as a union. Reordering struct members to shrink padding saves precious bytes in tight buffers.
Answer. "A struct stores members in separate adjacent memory (size = sum + alignment padding); a union overlays all members in shared memory (size = largest), so only one is valid at a time. Unions save memory and enable type-punning/tagged variants; structs model records. Padding is alignment-driven, so member ordering affects struct size."
Follow-ups / gotchas. Reading a different union member than the one written is type punning β legal/implementation-defined in C, undefined behavior in C++ (use memcpy or std::bit_cast there). #pragma pack / __attribute__((packed)) remove padding for wire/register layouts at an access-speed cost. offsetof and sizeof reveal the padding.
Seen in: AmbitionBox engineer ("Difference between Structure vs union"), GfG reports.
G. Compilation, preprocessor & systemΒΆ
G1 Β· Q: Difference between a library call and a system call.ΒΆ
Frequency: π₯π₯ Common (~6 reports).
Concept β the basis. A library call is an ordinary function in user space (printf, strlen, malloc) β it runs in your process at user privilege. A system call is a request to the kernel for a privileged service (I/O, memory, processes): read, write, open, fork, mmap. It crosses the userβkernel boundary via a controlled trap (syscall/svc/int 0x80), switching the CPU to kernel mode β relatively expensive. Library calls often wrap system calls and add buffering/convenience.
Example β printf (library) eventually invokes write (syscall):
printf("hi\n"); // formats into a USER-SPACE buffer (library work) ...
// ... and flushes via the write() SYSTEM CALL into the kernel.
Why the split exists. The kernel owns shared/privileged resources (hardware, other processes' memory) and must protect them. User code can't touch them directly β it must ask via a syscall, which is a guarded doorway with a privilege (mode) switch and argument validation. Libraries sit on top to make this ergonomic and fast (buffering, formatting), so you call printf, not write, by hand.
Where you see it (Qualcomm). ioctl() (a syscall) is how user-space talks to a kernel driver β the bread-and-butter of camera/V4L2/DRM control; understanding syscall cost explains why you batch I/O; strace reveals which syscalls an app makes.
Answer. "A library call is a normal user-space function at user privilege within your process (printf, strlen, malloc). A system call asks the kernel to do something privileged β file/network I/O, memory mapping, process creation β by trapping into kernel mode (read, write, fork, mmap, ioctl), which involves a mode switch and is costlier. Libraries wrap syscalls and add buffering; e.g. printf buffers in user space and calls write underneath."
Follow-ups / gotchas. Mode switch β context switch (a syscall doesn't necessarily switch processes). Buffering explains why a crashing program may "lose" printf output that lacked a \n/fflush. Cross-link: user/kernel boundary, HAL, ioctl β 07_embedded_linux_kernel.md.
Seen in: GfG Set 2 ("Difference between library call and a system call"); recurring in embedded rounds.
G2 Β· Q: Macros & the preprocessor β and how do you write platform-agnostic code?ΒΆ
Frequency: π₯ Occasional (~2 reports).
Concept β the basis. The preprocessor runs before compilation: textual substitution (#define), file inclusion (#include), and conditional compilation (#if/#ifdef/#endif). Macros are pure text replacement β no type checking, no scope β fast and flexible but error-prone. Conditional compilation is how one codebase targets multiple SoCs/OSes.
Example β function-like macro pitfalls and fixes:
#define SQ(x) ((x)*(x)) // ALWAYS parenthesize each arg and the whole body
SQ(a+1) // β ((a+1)*(a+1)) β (without inner parens: a+1*a+1 β)
SQ(i++) // BUG: i incremented TWICE β macro args may be evaluated multiple times
#define MAX(a,b) ((a) > (b) ? (a) : (b)) // same double-evaluation caveat
#ifndef CONFIG_H // include guard: prevents double inclusion
#define CONFIG_H
#if defined(__aarch64__)
#define CACHE_LINE 64
#elif defined(__x86_64__)
#define CACHE_LINE 64
#endif
#endif
Why it exists. Before there were templates/constexpr/inline, the preprocessor was C's only tool for compile-time configuration and code generation. It still owns the jobs no later feature replaced: conditional compilation (per-arch/per-build code), header inclusion with guards, and pulling in declarations. It's "dumb text," which is both its power (works on anything) and its danger (no types/scope).
Where you see it (Qualcomm). #ifdef CONFIG_<SOC> blocks selecting per-chip register maps; feature flags for debug vs release builds; register-bit #defines; __attribute__/likely()/unlikely() wrappers β the kernel and HALs are full of this.
Answer. "The preprocessor does textual work before compilation: #define macros, #include, and conditional compilation. Macros are untyped text substitution β powerful but with pitfalls (parenthesize every argument and the whole expression; never pass side-effecting expressions, since arguments can be evaluated multiple times). Platform-agnostic code uses #if defined(...) to pick per-architecture definitions, header include guards (#ifndef/#define/#endif or #pragma once) to prevent double inclusion, and abstraction layers. I prefer static inline functions or enum/const over macros when I want type safety."
Solution / good example β declaring a shared global the right way (avoids multiple-definition link errors):
/* config.h */ extern int g_frame_count; // DECLARATION (in the header)
/* config.c */ int g_frame_count = 0; // DEFINITION (in exactly one .c)
Follow-ups / gotchas. Header inclusion + variable scope: declare globals extern in headers, define them in exactly one .c (else duplicate-symbol link errors). enum/const are type-safe alternatives to #define constants. inline vs macro β G3.
Seen in: GfG Set 8 ("Platform-agnostic code solutions using macros", "Header file inclusion and variable scope analysis").
G3 Β· Q: What are inline functions? When are they ineffective, and how do they compare to macros / function pointers?ΒΆ
Frequency: π₯ Occasional (~2 reports).
Concept β the basis. inline is a hint to replace a call with the function's body, eliminating call overhead (stack setup, jump, return). Unlike a macro it's a real function: type-checked, scoped, debuggable, and it evaluates each argument exactly once. The compiler may ignore the hint.
When inlining is ineffective/impossible: large bodies (code bloat outweighs the win), recursion, calls made through a function pointer (target unknown at compile time β ties to A4), virtual calls (C++), or when the function's address is taken / it's in another translation unit without LTO.
Example β inline vs macro:
static inline int sq(int x){ return x*x; } // type-checked, x evaluated once
sq(i++); // i incremented exactly once β
#define SQM(x) ((x)*(x))
SQM(i++); // i incremented twice β (macro pitfall, see G2)
Why it exists. To get the speed of a macro (no call overhead) without the macro's hazards (no type checking, double evaluation, no debugging). It's the type-safe successor to function-like macros for small hot helpers.
Where you see it (Qualcomm). Tiny accessors in hot ISP/DSP loops (static inline uint8_t clamp8(int v)); register read/write helpers; anywhere a macro was historically used for speed but you want type safety.
Answer. "inline suggests the compiler substitute the function body at the call site to avoid call overhead, while keeping type safety, scope, and single argument evaluation (unlike a macro). It's a hint the compiler can ignore, and it's ineffective for large or recursive functions, calls through function pointers (target not known at compile time), and virtual calls. Versus macros it's safer and debuggable; versus a normal call it trades a little code size for speed."
Follow-ups / gotchas. "Relationship between function pointers and inline" (a real GfG Set 8 question): calling through a pointer defeats inlining because the target isn't known statically. Modern compilers inline by their own cost model regardless of the keyword, and LTO can inline across files. static inline in a header is the common pattern.
Seen in: GfG Set 8 ("Inline functions: mechanics and optimization benefits; when ineffective; relationship with function pointers").
G4 Β· Q: Predict the output / find the error in this code snippet.ΒΆ
Frequency: π₯π₯ Common β interviewers routinely throw pointer/operator snippets ("ten output-prediction questions involving pointers", "code snippets to debug").
Concept β the basis. These test precise mental modeling: operator precedence, pointer-vs-value, integer promotion, evaluation order, and undefined behavior. Method: (1) note each variable's type and storage; (2) track what each pointer points to; (3) apply precedence/associativity exactly; (4) flag any UB (uninitialized reads, overflow, sequence-point violations).
Example 1 β pre/post-increment with a pointer:
int a[] = {1,2,3}; int *p = a;
printf("%d %d", *p++, *p); // *p++ yields *p (1) then advances p; *p is now 2 β "1 2"
// (note: argument evaluation order is unspecified in general)
int main(void){ char *p; while (i < 50) p++; return p; }
// Errors: 'i' undeclared; 'p' uninitialized (wild) and incremented β UB;
// returning a char* where int is expected (type mismatch).
int x = 5; int y = ++x * 2; // xβ6 first, then y = 12
Why interviewers love it. It's a fast, unfakeable probe: either your mental model of pointers/precedence/UB is correct or it isn't. It mirrors real debugging, where you must read code exactly as the compiler does.
Where you see it (Qualcomm). Reading dense driver/DSP code and bit-twiddling macros; reviewing a colleague's pointer-heavy patch; debugging a heisenbug that's actually UB.
Answer (method, not memorization). "I annotate each variable's type and what each pointer points to, then apply precedence and associativity carefully, watch post/pre-increment timing and integer promotions, and flag undefined behavior β uninitialized/wild pointers, unsequenced modifications like i = i++, signed overflow. For the buggy one I list the concrete errors: undeclared variable, wild uninitialized pointer dereference/increment, and a return-type mismatch."
Follow-ups / gotchas. Argument evaluation order is unspecified, so f(i++, i++) isn't portable. a[i] == *(a+i) == i[a]. i = i++ and a[i] = i++ are UB (unsequenced). State assumptions about int size/endianness when relevant.
Seen in: GfG Set 2 ("Ten output prediction questions involving pointers", "code snippets to debug"), LeetCode SWE/Engineer ("code snippet output"), GfG ML & System Engineer ("Identify the error in the given code").
Β§ Encyclopedia β searchable glossaryΒΆ
Every bold-italic term used above is defined here, alphabetically, with why it exists / where you see it and a micro-example. Use your editor's find (βF / Ctrl-F) to jump to a term.
Address β a number naming a byte of memory. A pointer stores one. Why/where: the CPU loads/stores via addresses; &x yields x's address. printf("%p", (void*)&x);
Alignment β the requirement that an object of size S start at an address that's a multiple of its alignment A (often A=S for scalars). Why: hardware reads aligned data in one bus cycle; misaligned access is slower or faults (SIGBUS). Where: drives padding in structs; DMA buffers must be aligned. _Alignof(int)==4.
Aliasing / strict aliasing β two pointers alias when they refer to the same storage. The strict-aliasing rule says the compiler may assume pointers of incompatible types don't alias, enabling optimization. Why: lets the optimizer keep values in registers across stores. Gotcha: type-punning through mismatched pointer types breaks it β use memcpy or a union, or unsigned char* (which may alias anything).
Automatic storage duration β the lifetime of a local (non-static) variable: created on block entry, destroyed on exit. Lives on the stack. Why: matches call/return; allocation is free. Where: returning its address creates a dangling pointer (A6).
.bss β the segment holding uninitialized (or zero-initialized) globals/statics; zeroed by the loader. Why: storing "10 MB of zeros" as just a size keeps the binary small. static int big[1000000]; lives here.
Cast β an explicit conversion between types: (T)expr. Why: tell the compiler "treat this as a T" β e.g. void*βint* before deref, or intβdouble. Where: required to deref a void*; truncates/extends integers. Caveat: casts can hide bugs (don't cast malloc in C; avoid casting away const/pointer types). C++ refines this into static_cast/reinterpret_cast/const_cast/dynamic_cast β see 02_cpp_oop.md. int *p = (int*)vp;
Coalescing β when free merges a just-freed block with adjacent free blocks into one larger block. Why: fights fragmentation so future large allocations can succeed.
Compilation pipeline β source β preprocess (#include/macros) β compile (β assembly) β assemble (β object file .o) β link (combine objects + libraries β executable). Where: "undefined reference" = a link error (missing definition); "implicit declaration" = a compile error (missing prototype).
Const β a type qualifier meaning read-only through this name (B3). Why: compiler-enforced immutability; enables ROM placement. Caveat: casting it away and writing is undefined behavior.
Context switch β the kernel saving one thread/process's CPU state and restoring another's. Note: a system call is a mode switch (userβkernel), not necessarily a context switch. Full detail β 04_os.md.
Core dump β a file capturing a crashed process's memory/registers. Why/where: ulimit -c unlimited then gdb ./app core + bt pinpoints a segmentation fault.
Dangling pointer β a pointer to storage whose lifetime has ended (freed or out-of-scope). Dereferencing is undefined behavior (A2). Fix: p = NULL; after free.
.data β segment holding initialized globals/statics. int g = 5; lives here.
Dereference β following a pointer to the object it names, via *p (or p->m, p[i]). Gotcha: dereferencing NULL/wild/dangling pointers is UB.
DMA (Direct Memory Access) β hardware moving data to/from memory without the CPU. Why/where: a sensor/ISP DMAs a frame into a buffer; that buffer must be volatile-treated and aligned, and you must not free it while DMA is in flight (β dangling).
Endianness β byte order of multi-byte values (F1). Little = LSB first (x86/ARM); big = network order. Where: serializing to file/network/registers.
Fragmentation β free memory split into many small non-contiguous pieces (external), or unused bytes inside a block (internal, e.g. padding). Why it matters: a long-running camera daemon can fail a large malloc despite enough total free memory β use pools/slabs.
Free list β the allocator's linked structure of recycled free blocks. Why: makes malloc/free fast without asking the OS each time (C2).
Function pointer β a pointer holding a function's address, enabling indirect/late-bound calls (A4). Where: driver/HAL "ops" tables, callbacks, jump tables.
Heap β the region for dynamic, manually-managed memory (malloc/free); grows up; large but slower; can fragment (D2).
Identifier β a name for a variable/function/type. Its visibility is its scope; its cross-file sharing is its linkage.
Implementation-defined behavior β the standard allows choices but requires the implementation to document one (e.g. sizeof(int), right-shift of a negative). Contrast with undefined behavior (no guarantees) and unspecified behavior (a choice, not documented).
Integer promotion / usual arithmetic conversions β small types (char, short) promote to int in expressions, and mixed operands convert to a common type (notably signedβunsigned). Where: the (-1 > 1u) trap (F2); char arithmetic happening in int.
ISR (Interrupt Service Routine) β a function the CPU jumps to on a hardware interrupt. Where: shares variables with the main loop β those must be volatile (and atomic); ISRs must be short and must not call non-reentrant/malloc code.
Lifetime / storage duration β how long an object's storage is valid: automatic (scope), static (whole program), allocated (until free), thread (TLS). Why it's the master concept: dangling pointers, leaks, and "return a buffer" all hinge on it.
Linkage β whether a name refers to the same entity across translation units: external (shared; default for globals), internal (static β file-private), none (locals). Why: controls multi-file visibility and symbol clashes.
lvalue / rvalue β an lvalue names an object you can take the address of / assign to (x, *p, a[i]); an rvalue is a temporary value (x+1, 42). Where: &(x+1) is illegal (rvalue); explains many compiler errors. (C++ adds move semantics on top β 02_cpp_oop.md.)
Macro β preprocessor text substitution #define (G2). Why: compile-time config/codegen; gotchas: no types/scope, double evaluation β parenthesize and avoid side-effects; prefer inline/enum/const.
Memory leak β reachable-no-more heap memory that's never freed (C3). Mirror of a dangling pointer.
Memory-mapped I/O (MMIO) β hardware registers exposed at memory addresses; you read/write them like memory. Why/where: *(volatile uint32_t*)0x4000_1000 controls a peripheral; must be volatile so accesses aren't optimized away.
MMU (Memory Management Unit) β hardware translating virtualβphysical addresses and enforcing per-page permissions. Why/where: gives each process an isolated address space; an illegal access traps as a segmentation fault.
NULL pointer β a pointer guaranteed to compare unequal to any valid object ((void*)0). Why: a sentinel "points to nothing." Dereferencing is UB. C++ prefers nullptr (typed).
Object (C term) β "a region of data storage whose contents can represent values." Note: in C this means any storage (a variable, array element, malloc'd block) β not an OOP class instance (that's 02_cpp_oop.md). Where: the standard's rules (lifetime, aliasing) are phrased over "objects."
Padding β compiler-inserted unused bytes so members meet their alignment (F3). Where: struct sizes; reorder members or __attribute__((packed)) to shrink.
Page / paging β fixed-size (e.g. 4 KB) unit of virtual memory the MMU maps. Detail (page tables, TLB, page faults, thrashing) β 04_os.md.
Pointer β a variable holding an address plus a pointee type (A1).
ptrdiff_t β the signed integer type for the result of subtracting two pointers (A5). Why: portably represents element distances on any platform.
RAII (Resource Acquisition Is Initialization) β C++ idiom tying a resource's lifetime to an object's scope (destructor frees it) β no leaks/dangling. C has no RAII; you use the goto cleanup idiom (C3) instead. Detail β 02_cpp_oop.md.
.rodata β read-only segment for const data and string literals. Where: writing to a string literal faults β that's why char *s="x"; s[0]='Y'; is UB.
Scope β the region of source where a name is visible: block, function, file, function-prototype. Contrast with lifetime (time) and linkage (cross-file).
Segmentation fault (SIGSEGV) β trap on an illegal memory access (D3). Sibling SIGBUS = bus error, e.g. a misaligned access on a strict-alignment CPU.
Sequence point β a point in execution where all prior side effects are complete (e.g. ;, &&, ,, function call). Why: modifying an object twice between sequence points (i = i++) is undefined behavior. (C11 reframes this as "sequenced before/after.")
size_t β the unsigned type for sizes and sizeof results (and array indices). Why: big enough to size any object; gotcha: unsigned, so size_t i; i >= 0 is always true (F2).
Spilling / register allocation β the compiler assigning variables to CPU registers and, when it runs out, spilling extras to the stack (B4). Where: hot-loop performance.
Stack β LIFO region for call frames/locals; auto-managed; fast; small (D2). Overflow on deep recursion/big locals.
Stack frame β the per-call slice of the stack holding a function's locals, saved registers, and return address. Where: destroyed on return β returning a local's address dangles (A6).
Storage class β auto/register/static/extern: sets storage duration + linkage (B1).
String literal β a "..." constant with static lifetime, stored in .rodata (read-only). Gotcha: char *p = "hi"; then writing p[0] is UB; use char a[] = "hi"; for a writable copy.
System call (syscall) β a guarded request into the kernel for a privileged service via a trap/mode switch (G1). Where: read/write/mmap/ioctl; ioctl is the userβdriver channel.
.text β the read-only/executable code segment. Where: writing through a wild pointer into .text faults.
Translation unit β one .c file plus everything it #includes, compiled as a unit into one object file. Why: the scope of internal linkage (static) and where extern resolves across.
Two's complement β the signed-integer encoding (top bit = sign; negate = invert+1) used by virtually all hardware (F2). Why: one representation of zero and shared add/sub hardware with unsigned.
Type punning β reinterpreting an object's bytes as a different type. How safely: union (C, implementation-defined) or memcpy (portable). Where: viewing a float's bits, overlaying a register as word vs bit-fields. UB in C++ via mismatched pointers.
Type qualifier β const / volatile / restrict / _Atomic: refine how an object may be accessed, orthogonal to its type/storage (B3).
Undefined behavior (UB) β the standard imposes no requirements; the program may do anything (work, crash, corrupt, "time-travel" optimize). Why it's central: dangling/wild deref, buffer overflow, signed overflow, data races, i=i++, reading uninitialized memory. Where: most "works on my machine, fails in the field" bugs. Treat UB as "must never happen," not "happens to work."
Unspecified behavior β the standard allows several outcomes and doesn't require documenting which (e.g. argument evaluation order: f(i++, i++)). Less dangerous than UB but still non-portable.
void pointer (void*) β a typeless generic pointer; must be cast before dereference (A3).
volatile β type qualifier forcing a real memory access on every read/write (B3). Where: MMIO registers, ISR/DMA-shared memory. Not a synchronization primitive.
Wild pointer β an uninitialized pointer holding garbage; dereferencing is UB. Fix: initialize to NULL or a valid address at declaration. int *p = NULL;
Word β the natural register/bus width of the CPU (e.g. 4 or 8 bytes). Where: aligned word-at-a-time copies are the memcpy fast path (E1); alignment is usually to the word size.
Β§ Last-5-minutes cheat sheetΒΆ
- Pointer = address + type;
*(&x)==x; arithmetic scales bysizeof(*p); subtracting pointers β element count (ptrdiff_t). - Dangling = memory gone, pointer remains β
p=NULL;afterfree. Leak = pointer gone, memory remains β free on every path (goto cleanup). void*generic, must cast to deref. Function pointer = runtime dispatch (driver/HAL ops tables, jump tables).- Storage classes:
auto(stack) Β·register(hint) Β·static(persist / file-private) Β·extern(declare elsewhere). Qualifiers:const(read-only) Β·volatile(re-read every time β HW regs/ISR/DMA; not a lock). mallocuninitialized Β·calloczeroed + overflow-checked Β·reallocresize (use a TEMP!). All returnvoid*/NULL; free once;free(NULL)safe.- Memory map:
.text/.rodata/.data/.bss/ heapβ / stackβ (MMU-enforced permissions). Stack auto+fast+small; heap manual+slower+large. memcpyno-overlap (fast);memmoveoverlap-safe β fwd ifdst<src, back ifdst>src.- Endianness: little = LSB at low address (x86/ARM); network = big; detect via
*(char*)&one. struct= sum + padding (alignment);union= largest member (shared storage, type-punning).- Signed = two's complement, overflow is UB; unsigned wraps mod 2βΏ; mixed compare converts to unsigned.
- Library call = user space; system call = traps into kernel (mode switch;
ioctl= userβdriver). - Macros = untyped text (parenthesize! no side-effects);
inline= type-safe hint, defeated by function pointers/recursion. - UB (dangling/wild deref, overflow,
i=i++, data race) = "must never happen," not "happens to work."
Evidence base: qualcomm_camera_interview_experiences.md. Frequencies are approximate (sample = 75 reports, some via aggregator pages). Diagrams in assets/. Cross-references: C++/OOP β 02_cpp_oop.md Β· OS/threads/virtual-memory/paging β 04_os.md Β· kernel/drivers/HAL/ioctl β 07_embedded_linux_kernel.md Β· bit/number representation depth β 09_computer_arch_digital_design.md.