What is linkage in C, and how do
staticandexternaffect it?Linkage decides whether one name in different places means the same object or function. External linkage is shared across files (the default for functions and file-level variables), internal is one file (
static), and locals have none./* config.c */ int max_users = 100; /* external linkage, definition */ static int retries = 3; /* internal: invisible to other files */ /* main.c */ extern int max_users; /* declaration, refers to config.c's object */Two files defining the same non-static global fail to link (GCC 10 and later default to
-fno-common, so even uninitialized ones conflict).What is the strict aliasing rule?
The compiler may assume pointers to different types do not alias: accessing an object through an lvalue of an incompatible type is undefined behavior, except through its signed or unsigned variant or a character type (
char,signed char,unsigned char). The full list is in cppreference's strict aliasing section.float f = 1.0f; uint32_t bits = *(uint32_t *) &f; /* undefined: float read through uint32_t */ uint32_t ok; memcpy(&ok, &f, sizeof ok); /* defined, and compiled to a single move */At
-O2GCC enables-fstrict-aliasing, so code that worked at-O0can read stale values. Reinterpret bytes withmemcpyor a union (allowed in C).How do you write type-generic code in C?
With
void *plus an element size and callbacks (asqsortdoes), with macros, or with C11's_Generic, which picks an expression at compile time from its argument's type.C#include <stdio.h> #define type_name(x) _Generic((x), \ int: "int", \ double: "double", \ char *: "char *", \ default: "other") static void print_int(int v) { printf("%d\n", v); } static void print_double(double v) { printf("%.2f\n", v); } static void print_str(const char *v) { printf("%s\n", v); } #define print_val(x) _Generic((x), \ int: print_int, \ double: print_double, \ char *: print_str)(x) int main(void) { char name[] = "coddy"; printf("%s %s %s\n", type_name(42), type_name(2.5), type_name(name)); print_val(42); print_val(2.5); print_val(name); return 0; }It prints
int double char *, then42,2.50andcoddy.print_valselects a function and then calls it, because every_Genericbranch must compile for any argument.How do variadic functions like
printfwork, and how do you write one?Declare at least one named parameter (C23 also allows
f(...)), then..., and read the rest withva_start,va_argandva_end. The function cannot know the count or types, so a count or format string must tell it.C#include <stdio.h> #include <stdarg.h> int sum(int count, ...) { va_list ap; va_start(ap, count); int total = 0; for (int i = 0; i < count; i++) total += va_arg(ap, int); va_end(ap); return total; } int main(void) { printf("%d\n", sum(3, 10, 20, 30)); printf("%d\n", sum(5, 1, 2, 3, 4, 5)); return 0; }It prints 60 and 15. Arguments are promoted, so
va_arg(ap, char)orfloatis wrong (useintanddouble), and a wrong type is undefined behavior.In
f() + g(), which function is called first?It is unspecified: either may run first, and the compiler need not document it or be consistent. Function arguments are the same. Only
&&,||,?:and the comma operator fix the order. Iffandgmodify the same global it stays unspecified, since calls never interleave, buti++ + i++is undefined behavior. When order matters, use separate statements.How do you detect integer overflow in C?
Check before the operation: signed overflow is undefined behavior, so a check after it such as
if (a + b < a)may be optimized away. Compare against<limits.h>limits, or use the GCC and Clang builtins.C#include <stdio.h> #include <limits.h> #include <stdbool.h> bool safe_add(int a, int b, int *out) { if ((b > 0 && a > INT_MAX - b) || (b < 0 && a < INT_MIN - b)) return false; *out = a + b; return true; } int main(void) { int r; printf("%s\n", safe_add(INT_MAX, 1, &r) ? "ok" : "overflow"); printf("%s\n", safe_add(100, 23, &r) ? "ok" : "overflow"); printf("builtin: %s\n", __builtin_add_overflow(INT_MAX, 1, &r) ? "overflow" : "ok"); return 0; }It prints
overflow,okandbuiltin: overflow. C23 standardizes this asckd_addin<stdckdint.h>.Practice it: Reverse the Digits, Factorial
How does
inlinework in C, and how is it different from C++?In C99 and later,
inlinewithoutexternin a header is only an inline definition: if the compiler emits a call, one.cfile must provide the external definition withextern inline, or-O0builds fail with "undefined reference". So headers usestatic inline./* vec.h */ static inline int clamp(int x, int lo, int hi) { return x < lo ? lo : x > hi ? hi : x; }C++ merges duplicate inline definitions at link time, so plain
inlineworks there.What are
setjmpandlongjmp, and when would you use them?setjmp(env)saves the execution context and returns 0; a laterlongjmp(env, val)jumps back, andsetjmpreturnsval. It is a non-local goto, the closest C has to exceptions.C#include <stdio.h> #include <setjmp.h> static jmp_buf on_error; static int error_code; void parse(int depth) { if (depth == 3) { error_code = 42; longjmp(on_error, 1); /* abandon the whole call chain */ } parse(depth + 1); } int main(void) { if (setjmp(on_error) == 0) { parse(0); printf("not reached\n"); } else { printf("recovered with code %d\n", error_code); } return 0; }It prints
recovered with code 42.setjmpis allowed only in a condition like this or as a statement, soint code = setjmp(env);is undefined behavior (see setjmp on cppreference). Nothing in between is cleaned up, and locals modified aftersetjmpmust bevolatileto have a defined value.Should you use
#defineorconstfor constants in C?Use
enumfor integer constants andconstfor other typed values. In C aconst intis not a constant expression, socase n:and a file-scopestatic int arr[n];are not valid C; that is whereenumor#defineis needed. Inside a function,int arr[n];compiles, but as a variable length array. C23 addsconstexprfor objects.How would you implement a growable array (like a C++ vector) in C?
Keep a pointer, a length and a capacity, and when full
reallocto double the capacity, so appends are amortized O(1); a fixed increment makesnappends O(n²).C#include <stdio.h> #include <stdlib.h> typedef struct { int *data; size_t len, cap; } IntVec; int vec_push(IntVec *v, int x) { if (v->len == v->cap) { size_t cap = v->cap ? v->cap * 2 : 4; int *p = realloc(v->data, cap * sizeof *p); if (!p) return -1; /* v->data is still valid */ v->data = p; v->cap = cap; } v->data[v->len++] = x; return 0; } int main(void) { IntVec v = {0}; for (int i = 1; i <= 10; i++) { vec_push(&v, i * i); printf("len %zu cap %zu\n", v.len, v.cap); } printf("last = %d\n", v.data[v.len - 1]); free(v.data); return 0; }The capacity goes 4, 8, 16 and the last element is 100. Pointers into
datago stale after a growth.Write a binary search in C and point out the classic bug.
Keep a range
[lo, hi]that must contain the target and halve it, O(log n). The classic bug is(lo + hi) / 2, which overflows whenlo + hiexceedsINT_MAX; writelo + (hi - lo) / 2.C#include <stdio.h> int binary_search(const int *a, int n, int target) { int lo = 0, hi = n - 1; while (lo <= hi) { int mid = lo + (hi - lo) / 2; if (a[mid] == target) return mid; if (a[mid] < target) lo = mid + 1; else hi = mid - 1; } return -1; } int main(void) { int a[] = {2, 5, 8, 12, 16, 23, 38, 56}; printf("%d %d %d\n", binary_search(a, 8, 23), binary_search(a, 8, 2), binary_search(a, 8, 7)); return 0; }It prints
5 0 -1. See the binary search visualizer.Practice it: Binary Search, Search in Rotated Sorted Array, Square Root (Integer)
Advanced C Interview Questions
Linkage, aliasing, generic code, variadic functions and the corners of the standard that cause real bugs.