001 Notes
C Interview Notes
C.1 — Implement sizeof
sizeofWhat sizeof actually is
sizeof actually isint n = read_int();
int a[n]; // VLA
size_t s = sizeof(a); // runtime: n * sizeof(int)
size_t t = sizeof(int[n]); // runtime
sizeof expr // parens optional
sizeof(type) // parens required around a type name
int x = 5;
sizeof(x = 10); // x stays 5; only the type of (x=10) is examined
Naive emulation for an expression
#define SIZEOF(x) ((size_t)((char*)(&(x) + 1) - (char*)&(x)))
Emulation for a type
#define SIZEOF_TYPE(T) ((size_t)((char*)((T*)0 + 1) - (char*)((T*)0)))
Why you cannot fully replicate sizeof
sizeofArray-decay gotcha
void f(int a[10]) { // a is actually int*
sizeof(a); // sizeof(int*), NOT 10*sizeof(int)
}
int b[10];
sizeof(b); // 10*sizeof(int), good
sizeof(b)/sizeof(b[0]); // common 'array length' macro
#define ARRAY_SIZE(a) (sizeof(a)/sizeof((a)[0]))
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]) + __must_be_array(arr))
Counter-examples to internalize
| Expression | Result | Why |
|---|---|---|
sizeof(char) | 1 | by definition |
sizeof('a') in C | sizeof(int) | character constants are int in C |
sizeof('a') in C++ | 1 | char in C++ |
sizeof("hi") | 3 | includes the '\0' |
sizeof(struct {}) | 0 in C (GCC ext), 1 in C++ | empty struct UB-ish in std C; C++ guarantees ≥1 |
sizeof(void) | error in std C, 1 in GCC | extension |
C.2 — Implement memcpy
memcpyStep 0 — naive byte loop
void *memcpy(void *dst, const void *src, size_t n) {
unsigned char *d = dst;
const unsigned char *s = src;
while (n--) *d++ = *s++;
return dst;
}
Step 1 — word-at-a-time when aligned
void *memcpy(void *dst, const void *src, size_t n) {
unsigned char *d = dst;
const unsigned char *s = src;
/* Copy bytes until d is word-aligned */
while (n && ((uintptr_t)d & (sizeof(long) - 1))) {
*d++ = *s++; n--;
}
/* Copy long-at-a-time if s is also aligned */
if (!((uintptr_t)s & (sizeof(long) - 1))) {
long *ld = (long*)d;
const long *ls = (const long*)s;
while (n >= sizeof(long)) {
*ld++ = *ls++;
n -= sizeof(long);
}
d = (unsigned char*)ld;
s = (const unsigned char*)ls;
}
/* Tail */
while (n--) *d++ = *s++;
return dst;
}
Step 2 — SIMD (SSE/AVX)
#include <immintrin.h>
void *memcpy(void *dst, const void *src, size_t n) {
unsigned char *d = dst;
const unsigned char *s = src;
while (n >= 32) {
__m256i v = _mm256_loadu_si256((const __m256i*)s);
_mm256_storeu_si256((__m256i*)d, v);
s += 32; d += 32; n -= 32;
}
while (n--) *d++ = *s++;
return dst;
}
Step 3 — what glibc actually does
if (n <= 32) {
__m128i a = _mm_loadu_si128((const __m128i*)s);
__m128i b = _mm_loadu_si128((const __m128i*)(s + n - 16));
_mm_storeu_si128((__m128i*)d, a);
_mm_storeu_si128((__m128i*)(d + n - 16), b);
return dst;
}
Step 4 — non-temporal stores for huge copies
while (n >= 64) {
__m256i a = _mm256_loadu_si256((const __m256i*)s);
__m256i b = _mm256_loadu_si256((const __m256i*)(s + 32));
_mm256_stream_si256((__m256i*)d, a); // bypasses cache
_mm256_stream_si256((__m256i*)(d + 32), b);
s += 64; d += 64; n -= 64;
}
_mm_sfence(); // ensure stores visible
Decision tree
| Size | Best strategy |
|---|---|
| < 8 B | inline by-byte or overlapping loads |
| 8 B – L1 size | SIMD or unrolled word loop with cached stores |
| > L3 | non-temporal stores, no cache pollution |
| Memory-mapped IO | volatile loop, no SIMD reordering |
C.3 — Implement typeof
typeofIdiomatic use — type-safe MAX
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = 5;
int y = MAX(x++, 10); // x++ evaluated twice — bug
#define MAX(a, b) ({ \
__typeof__(a) _a = (a); \
__typeof__(b) _b = (b); \
_a > _b ? _a : _b; \
})
#define max(x, y) ({ \
typeof(x) _max1 = (x); \
typeof(y) _max2 = (y); \
(void) (&_max1 == &_max2); \
_max1 > _max2 ? _max1 : _max2; })
Without GCC extensions — _Generic (C11)
_Generic (C11)#define abs(x) _Generic((x), \
int: abs, \
long: labs, \
long long: llabs, \
float: fabsf, \
double: fabs, \
long double:fabsl)(x)
typeof gotchas
int a[10];
typeof(a) b; // int[10], OK
typeof(a) *p = &a; // int(*)[10] — array pointer
const int x = 5;
typeof(x) y; // const int — const propagates
C.4 — Implement NULL
NULLWhat NULL is
NULL is/* Common C definition */
#define NULL ((void*)0)
/* C++ pre-11 */
#define NULL 0
/* C++11 onward */
#define NULL nullptr
The (void*)0 vs 0 debate
(void*)0 vs 0 debateWhy nullptr exists (C++)
nullptr exists (C++)void f(int);
void f(char*);
f(0); // calls f(int) — surprising for null-pointer intent
f(NULL); // calls f(int) when NULL is 0
f(nullptr); // calls f(char*) — unambiguous
Is NULL guaranteed to be all-zero bits?
Common patterns
if (p) /* null-pointer check */
if (p != NULL) /* equivalent, explicit */
if (!p) /* null-pointer check */
C.5 — Implement malloc
mallocStep 0 — bump allocator (no free)
#include <unistd.h>
void *malloc(size_t n) {
void *p = sbrk(n);
return p == (void*)-1 ? NULL : p;
}
void free(void *p) { (void)p; }
Step 1 — free list with coalescing
typedef struct block {
size_t size; /* low bit = in-use */
struct block *next; /* free list link, only when free */
} block_t;
#define ALIGN 8
#define HEADER sizeof(size_t)
#define ALIGN_UP(n) (((n) + ALIGN - 1) & ~(ALIGN - 1))
static block_t *freelist = NULL;
static void *heap_end = NULL;
void *malloc(size_t n) {
n = ALIGN_UP(n);
block_t **prev = &freelist, *b;
for (b = freelist; b; prev = &b->next, b = b->next) {
if ((b->size & ~1UL) >= n) {
*prev = b->next; /* unlink */
b->size |= 1; /* mark in-use */
return (char*)b + HEADER;
}
}
/* grow heap */
b = sbrk(n + HEADER);
if (b == (void*)-1) return NULL;
b->size = n | 1;
return (char*)b + HEADER;
}
void free(void *p) {
if (!p) return;
block_t *b = (block_t*)((char*)p - HEADER);
b->size &= ~1UL;
b->next = freelist;
freelist = b;
}
| size_hdr | payload .... | size_ftr |
Step 2 — size classes (slab / segregated lists)
class[0]: 16 B
class[1]: 32 B
class[2]: 48 B
...
class[k]: 8*(k+2) B
Step 3 — mmap for large allocations
if (n >= MMAP_THRESHOLD) { /* ~128 KB in glibc */
void *p = mmap(NULL, n + page_size, ...);
/* record n in page header */
return p + page_size;
}
Step 4 — per-thread cache (tcmalloc / jemalloc)
Real-world numbers
Gotchas to mention
C.6 — Memory-saving struct design
Padding mechanics
struct bad {
char a; /* 1 */
/* 7 bytes pad */
long b; /* 8 */
char c; /* 1 */
/* 7 bytes pad — tail, so arrays stay aligned */
};
/* sizeof = 24 */
struct good {
long b; /* 8 */
char a; /* 1 */
char c; /* 1 */
/* 6 bytes pad */
};
/* sizeof = 16 */
Bitfields
struct flags {
unsigned valid:1;
unsigned dirty:1;
unsigned mode:3;
unsigned :0; /* force alignment of next field */
unsigned id:24;
};
__attribute__((packed)) — what it does and what it costs
__attribute__((packed)) — what it does and what it costsstruct __attribute__((packed)) net_hdr {
uint8_t ver;
uint32_t addr; /* no padding inserted */
};
Unions for tagged variants
struct value {
enum { INT, STR, FLT } tag;
union {
int i;
char *s;
float f;
} u;
};
Reducing pointer count
struct node {
uint32_t left; /* index, not pointer */
uint32_t right;
} nodes[1 << 28]; /* up to 256M nodes — addressable by 32-bit index */
Hot/cold splitting
struct task {
/* hot: touched every scheduler tick */
uint64_t runtime;
int state;
/* cold: rarely touched */
char name[64];
struct credential *cred;
};
Empty-base optimization (C++) and zero-size structs
Counter-example: don’t sacrifice readability for 4 bytes
struct user {
char name[32];
int id;
bool admin;
/* 3 bytes pad */
};
C.7 — Implement offsetof
offsetofClassic implementation
#define offsetof(t, m) ((size_t)(&((t*)0)->m))
Strictly conforming variant
#define offsetof(t, m) ((size_t)((char*)&(((t*)1)->m) - (char*)1))
What the compilers actually use
#define offsetof(t, m) __builtin_offsetof(t, m)
What it’s used for
C.8 — Implement container_of
container_of#define container_of(ptr, type, member) ({ \
const __typeof__(((type*)0)->member) *__mptr = (ptr); \
(type *)((char*)__mptr - offsetof(type, member)); \
})
What it does
struct list_head { struct list_head *next, *prev; };
struct task {
int pid;
struct list_head run;
};
/* You have a list_head*, you want the struct task* */
struct task *t = container_of(node, struct task, run);
Why the __typeof__ dance
__typeof__ dance#define container_of(ptr, type, member) \
((type*)((char*)(ptr) - offsetof(type, member)))
container_of(some_unrelated_ptr, struct task, run);
Why intrusive containers exist
+--------------------+ +--------------------+
| struct task | | struct task |
| int pid | | int pid |
| list_head run -----+ +---- list_head run |
+--------------------+ | | +--------------------+
+--+
Generic intrusive list
#define list_for_each_entry(pos, head, member) \
for (pos = container_of((head)->next, __typeof__(*pos), member); \
&pos->member != (head); \
pos = container_of(pos->member.next, __typeof__(*pos), member))
C.9 — Macros vs inline functions
What they are
Side-effects bug
#define SQR(x) ((x) * (x))
int n = 3;
int r = SQR(n++); /* expands to ((n++) * (n++)) — UB */
static inline int sqr(int x) { return x * x; }
int r = sqr(n++); /* n incremented once, value 9 used */
Precedence bug
#define SQR(x) (x) * (x)
int y = SQR(1+2); /* (1+2)*(1+2) = 9 — OK */
int z = SQR(1+2) + 1; /* (1+2)*(1+2) + 1 = 10 — OK */
/* but: */
#define SQR(x) x*x
int w = SQR(1+2); /* 1+2*1+2 = 5 — wrong */
Multi-statement macro pattern
#define LOG(msg) do { \
fprintf(stderr, "%s\n", msg); \
fflush(stderr); \
} while (0)
When macros are the right tool
When inline is the right tool
Performance — does inline actually inline?
inline actually inline?Debugging
C.10 — extern, static, declaration vs definition, linkage rules
extern, static, declaration vs definition, linkage rulesDeclaration vs definition
extern int x; /* declaration */
int x = 5; /* definition (also a declaration) */
int f(int); /* declaration */
int f(int n) { ... } /* definition */
int x; /* tentative definition at file scope */
Linkage — the three kinds
| Linkage | Means | Default for |
|---|---|---|
| External | Visible across TUs; one definition program-wide | file-scope objects/functions |
| Internal | Visible only within this TU | file-scope objects/functions declared static |
| None | Visible only within block scope | block-scope objects |
extern — three different jobs
extern — three different jobsstatic at file scope vs block scope
static at file scope vs block scope/* file scope */
static int counter; /* internal linkage — not visible from other TUs */
static int helper(void); /* internal linkage — TU-private helper */
void f(void) {
static int n; /* block scope, static storage duration */
n++; /* persists across calls */
}
What’s the default?
| Where | Default storage | Default linkage |
|---|---|---|
| File-scope object | static (entire program lifetime) | external |
| File-scope function | n/a | external |
Block-scope object without static | automatic (stack) | none |
Block-scope object with static | static | none |
| Function parameter | automatic | none |
Multiple-definition errors
/* a.c */ int x = 1;
/* b.c */ int x = 2; /* link error: multiple definition of 'x' */
/* a.c */ static int x = 1;
/* b.c */ static int x = 2; /* OK — each is internal */
/* header.h */ extern int x;
/* a.c */ int x = 1;
/* b.c */ #include "header.h" /* uses a.c's x */
Common subtle bugs
/* shared.h */ int counter; /* tentative definition */
/* fast.h */ static inline int sqr(int x) { return x*x; }
extern int x = 5; /* this is a definition, not a declaration */
C.11 — The static keyword
static keyword1. File-scope variable — internal linkage
static int g_count; /* not visible outside this TU */
2. File-scope function — internal linkage
static void helper(void) { ... } /* TU-private */
3. Block-scope variable — static storage duration
int next_id(void) {
static int n = 0; /* initialized once, persists */
return ++n;
}
4. Array parameter — minimum size hint (C99)
void f(int a[static 10]) { ... } /* caller promises a has >= 10 elements */
5. C++ class static member
struct S {
static int n; /* one per class, not per instance */
};
int S::n = 0; /* definition in exactly one TU */
Linkage vs storage duration — keep them separate
C.12 — The restrict keyword (C99)
restrict keyword (C99)Contract
Why it exists — autovectorization
void add(int *a, int *b, int *c, size_t n) {
for (size_t i = 0; i < n; i++) a[i] = b[i] + c[i];
}
void add(int *restrict a, int *restrict b, int *restrict c, size_t n) {
for (size_t i = 0; i < n; i++) a[i] = b[i] + c[i];
}
Standard library uses
void *memcpy(void *restrict dest, const void *restrict src, size_t n);
Multiple restrict pointers
restrict pointersint x;
int *restrict p = &x;
int *restrict q = &x; /* UB if both are used to write */
int *restrict p = arr;
int *q = p + 5; /* OK — q is derived from p */
*q = 1; *p = 2; /* both writes go through p's family */
restrict on struct fields
restrict on struct fieldsWhen to use
When to avoid
C.13 — Implement strcpy, strncpy
strcpy, strncpystrcpy
strcpychar *strcpy(char *dst, const char *src) {
char *p = dst;
while ((*p++ = *src++))
;
return dst;
}
Word-at-a-time strcpy
#define HAS_ZERO_BYTE(w) (((w) - 0x0101010101010101ULL) & ~(w) & 0x8080808080808080ULL)
strncpy — and why it’s so dangerous
strncpy — and why it’s so dangerouschar *strncpy(char *dst, const char *src, size_t n) {
size_t i;
for (i = 0; i < n && src[i] != '\0'; i++)
dst[i] = src[i];
for (; i < n; i++)
dst[i] = '\0'; /* pad with zeros */
return dst;
}
The “safe” alternative — strlcpy (BSD)
strlcpy (BSD)size_t strlcpy(char *dst, const char *src, size_t size) {
size_t srclen = strlen(src);
if (size > 0) {
size_t copy = srclen < size - 1 ? srclen : size - 1;
memcpy(dst, src, copy);
dst[copy] = '\0';
}
return srclen; /* returns total length src — lets caller detect truncation */
}
snprintf as a safe alternative
snprintf as a safe alternativesnprintf(dst, size, "%s", src);
Counter-example — overlap
char buf[] = "hello";
strcpy(buf + 1, buf); /* UB — overlapping source and dest */
C.14 — Implement itoa, atoi
itoa, atoiatoi — basic
atoi — basicint atoi(const char *s) {
while (*s == ' ' || (*s >= '\t' && *s <= '\r')) s++;
int sign = 1;
if (*s == '-') { sign = -1; s++; }
else if (*s == '+') s++;
int n = 0;
while (*s >= '0' && *s <= '9')
n = n * 10 + (*s++ - '0');
return sign * n;
}
strtol is the right tool
strtol is the right toollong strtol(const char *restrict s, char **restrict endptr, int base);
long strtol(const char *s, char **endptr, int base) {
/* skip whitespace */
while (isspace((unsigned char)*s)) s++;
int neg = 0;
if (*s == '-') { neg = 1; s++; }
else if (*s == '+') s++;
/* auto-detect base */
if (base == 0) {
if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) { base = 16; s += 2; }
else if (s[0] == '0') base = 8;
else base = 10;
} else if (base == 16 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) {
s += 2;
}
long acc = 0;
long cutoff = neg ? LONG_MIN : LONG_MAX;
long cutlim = cutoff % base;
cutoff /= base;
if (neg) { cutlim = -cutlim; cutoff = -cutoff; }
int any = 0, overflow = 0;
for (;;) {
int c = (unsigned char)*s;
int d;
if (isdigit(c)) d = c - '0';
else if (isalpha(c)) d = (c | 0x20) - 'a' + 10;
else break;
if (d >= base) break;
if (overflow || acc > cutoff || (acc == cutoff && d > cutlim))
overflow = 1;
else { acc = acc * base + d; any = 1; }
s++;
}
if (endptr) *endptr = (char*)(any ? s : s - !!neg);
if (overflow) { errno = ERANGE; return neg ? LONG_MIN : LONG_MAX; }
return neg ? -acc : acc;
}
itoa — non-standard, here’s the idiom
itoa — non-standard, here’s the idiomchar *itoa(int value, char *buf, int base) {
char tmp[36];
int i = 0;
unsigned uval = value < 0 && base == 10 ? -(unsigned)value : (unsigned)value;
do {
int d = uval % base;
tmp[i++] = d < 10 ? '0' + d : 'a' + d - 10;
uval /= base;
} while (uval);
char *p = buf;
if (value < 0 && base == 10) *p++ = '-';
while (i--) *p++ = tmp[i];
*p = '\0';
return buf;
}
Optimized integer formatting
C.15 — Pointer arithmetic
Rules
Equivalence a[i] = *(a + i)
a[i] = *(a + i)E1[E2] ≡ *((E1) + (E2))
Type of subtraction
int arr[100];
int *p = &arr[20], *q = &arr[5];
ptrdiff_t d = p - q; /* 15 */
One-past-the-end
Array decay
void f(int *p);
int arr[10];
f(arr); /* arr decays to &arr[0] */
f(&arr[0]); /* same thing */
int arr[10];
int (*pa)[10] = &arr;
pa + 1; /* address arr + 40 (10 ints) */
&arr[0] + 1; /* address arr + 4 (1 int) */
void * arithmetic
void * arithmeticvoid *p = ...;
p++; /* error in standard C; GCC extension treats as 1 byte */
char *cp = p;
cp += n; /* portable byte arithmetic */
Negative offsets / underflow
int arr[10];
int *p = &arr[0];
p--; /* UB — underflows below array */
Pointer-to-pointer
int **pp = ...;
pp + 1; /* moves by sizeof(int*) */
Implementation note — fat pointers
C.16 — Why arrays start from 0
Mechanical: pointer arithmetic
Mathematical (Dijkstra EWD831, 1982)
a) 2 ≤ i < 6
b) 1 < i ≤ 5
c) 2 ≤ i ≤ 5
d) 1 < i < 6
Historical
Languages that buck the trend
C.17 — Alignment
What alignment is
Why hardware cares
C alignment primitives
#include <stdalign.h>
alignas(16) char buf[64]; /* buf is 16-aligned */
size_t a = alignof(double); /* alignment requirement of double */
void *p;
posix_memalign(&p, 4096, size); /* page-aligned */
p = aligned_alloc(64, size); /* C11, cache-line-aligned */
Struct alignment
struct s {
char a;
double b; /* offsetof(b) = 8 — padding inserted */
};
alignof(struct s); /* 8 — max of member alignments */
sizeof(struct s); /* 16 — multiple of alignment, so arrays stay aligned */
struct alignas(64) cacheline_aligned {
int x;
}; /* always 64-aligned, sizeof multiple of 64 */
Cache-line alignment — false sharing
struct counters {
long a;
long b;
} c;
struct counters {
alignas(64) long a;
alignas(64) long b;
};
Unaligned loads — gotcha
char buf[8] = ...;
uint64_t v = *(uint64_t*)buf; /* may be UB if buf not 8-aligned */
uint64_t v;
memcpy(&v, buf, 8); /* compiler optimizes to a load */
Page alignment
C.18 — Condition variable vs mutex vs spinlock vs atomic
Atomic
#include <stdatomic.h>
_Atomic int counter;
atomic_fetch_add(&counter, 1);
Spinlock
typedef _Atomic int spinlock_t;
void lock(spinlock_t *l) {
while (atomic_exchange_explicit(l, 1, memory_order_acquire))
while (atomic_load_explicit(l, memory_order_relaxed))
__builtin_ia32_pause(); /* x86 PAUSE */
}
void unlock(spinlock_t *l) {
atomic_store_explicit(l, 0, memory_order_release);
}
Mutex
#include <pthread.h>
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&m);
/* ... */
pthread_mutex_unlock(&m);
Condition variable
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t c = PTHREAD_COND_INITIALIZER;
int ready = 0;
/* consumer */
pthread_mutex_lock(&m);
while (!ready)
pthread_cond_wait(&c, &m); /* atomically unlocks m, sleeps */
/* uses shared state, holds m */
pthread_mutex_unlock(&m);
/* producer */
pthread_mutex_lock(&m);
ready = 1;
pthread_cond_signal(&c);
pthread_mutex_unlock(&m);
Choosing — decision table
| Need | Use |
|---|---|
| Counter / flag, no other state | atomic |
| Lock-free data structure | atomic with CAS |
| Critical section <100 ns, can’t sleep | spinlock |
| Critical section >100 ns or may block | mutex |
| Wait for predicate / event | mutex + CV (or eventfd, futex_wait) |
| Many readers, rare writer | rwlock or RCU |
Performance summary (typical x86-64)
| Op | Latency uncontended | Contended |
|---|---|---|
| Cached load | 1 ns | — |
| Atomic increment | 5–10 ns | 50–200 ns (cache bounce) |
| Spinlock | 5 ns | depends on hold time |
| Mutex | 25 ns | 1–5 μs (with syscall) |
| Condition variable wake | — | 5–10 μs (syscall + wake) |
Memory order matters
Counter-example: spinlock under preemption
spin_lock(&L); /* user thread acquires */
/* preempted! */
spin_lock(&L); /* another user thread spins until the kernel reschedules the first */
Comments