C interviews test low-level thinking: pointers, memory management, the preprocessor, bit manipulation, and systems programming. This guide covers the 50 most common questions — with clear answers and runnable examples.
Quick reference
| Topic | Most asked questions |
|---|---|
| Pointers | Pointer arithmetic, double pointers, function pointers, void* |
| Memory | malloc/calloc/realloc/free, stack vs heap, memory leaks |
| Strings | string.h functions, buffer overflows, null terminator |
| Structs | padding, bit fields, typedef, flexible arrays |
| Preprocessor | #define vs const, macros, include guards, #pragma |
| Bit manipulation | AND/OR/XOR/NOT, shift operators, common tricks |
| Functions | pass by value vs pointer, variadic functions, recursion |
| Standard library | stdio, stdlib, string.h, math.h |
Pointers
1. What is a pointer in C?
A pointer is a variable that stores the memory address of another variable. Pointers enable dynamic memory allocation, pass-by-reference semantics, and efficient array/string manipulation.
int x = 10;
int *p = &x; // p holds the address of x
printf("%d\n", x); // 10 — value of x
printf("%p\n", p); // 0x7fff... — address stored in p
printf("%d\n", *p); // 10 — dereferencing: value at address
*p = 20; // modifies x through pointer
printf("%d\n", x); // 20
Key operators:
&— address-of operator*— dereference operator (in expression context) or pointer declaration (in type context)
2. What is the difference between *p++, (*p)++, and *(p++)?
Operator precedence determines the result:
int arr[] = {10, 20, 30};
int *p = arr;
// *p++ — post-increment the pointer, dereference original address
int a = *p++; // a = 10, p now points to arr[1]
// (*p)++ — dereference first, then increment the value
p = arr;
int b = (*p)++; // b = 10, arr[0] is now 11, p unchanged
// *(p++) — same as *p++ (post-increment has same precedence as *)
p = arr;
int c = *(p++); // c = 10, p now points to arr[1]
| Expression | What increments | Value returned |
|---|---|---|
*p++ |
Pointer | *p before increment |
(*p)++ |
Pointed-to value | *p before increment |
++*p |
Pointed-to value | *p after increment |
*++p |
Pointer | *p after increment |
3. What is a double pointer (pointer to pointer)?
A double pointer stores the address of another pointer. Common uses: dynamic 2D arrays, modifying a pointer from a function.
int x = 5;
int *p = &x;
int **pp = &p; // pointer to pointer
printf("%d\n", **pp); // 5 — double dereference
// Modifying a pointer from a function
void allocate(int **ptr) {
*ptr = malloc(sizeof(int));
**ptr = 42;
}
int *q = NULL;
allocate(&q);
printf("%d\n", *q); // 42
free(q);
4. What is a void pointer (void *)?
A void * is a generic pointer that can hold the address of any type. It cannot be dereferenced directly — you must cast it first.
void *vp;
int x = 10;
double d = 3.14;
vp = &x;
printf("%d\n", *(int *)vp); // 10
vp = &d;
printf("%f\n", *(double *)vp); // 3.140000
// malloc returns void *
int *arr = malloc(5 * sizeof(int)); // implicit cast in C (explicit in C++)
void * is how malloc, memcpy, and qsort achieve generic behavior in C.
5. What is the difference between a pointer to a function and a function pointer array?
// Function pointer declaration
int (*add)(int, int);
int sum(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
add = sum;
printf("%d\n", add(2, 3)); // 5
// Array of function pointers (dispatch table)
int (*ops[2])(int, int) = {sum, mul};
printf("%d\n", ops[0](2, 3)); // 5
printf("%d\n", ops[1](2, 3)); // 6
Function pointer arrays are the C equivalent of virtual dispatch — used in embedded systems, state machines, and plugin architectures.
6. What is pointer arithmetic?
You can add/subtract integers from pointers. The result moves the pointer by n * sizeof(pointed_type) bytes.
int arr[] = {10, 20, 30, 40};
int *p = arr;
printf("%d\n", *(p + 1)); // 20 (moves 4 bytes forward)
printf("%d\n", *(p + 2)); // 30
// Pointer difference
int *q = &arr[3];
ptrdiff_t diff = q - p; // 3 (not 12 bytes — element count)
printf("%td\n", diff);
Pointer arithmetic is only valid within a single array object (or one past the end). Arithmetic on unrelated pointers is undefined behavior.
Memory management
7. What is the difference between malloc, calloc, realloc, and free?
| Function | Purpose | Initialization |
|---|---|---|
malloc(size) |
Allocate size bytes |
Uninitialized (garbage) |
calloc(n, size) |
Allocate n * size bytes |
Zero-initialized |
realloc(ptr, size) |
Resize allocation | New bytes uninitialized |
free(ptr) |
Release memory | — |
// malloc
int *a = malloc(5 * sizeof(int));
if (!a) { perror("malloc"); exit(1); }
// calloc — elements are zeroed
int *b = calloc(5, sizeof(int)); // b[0]..b[4] == 0
// realloc
a = realloc(a, 10 * sizeof(int));
if (!a) { /* original memory NOT freed on failure */ exit(1); }
free(a);
free(b);
Common mistakes:
- Not checking for NULL return
- Using memory after
free(use-after-free) - Double-freeing a pointer
- Storing
reallocresult in the original pointer (leaks on failure)
8. What is the difference between stack and heap memory?
| Property | Stack | Heap |
|---|---|---|
| Allocation | Automatic (function entry/exit) | Manual (malloc/free) |
| Size | Limited (~1–8 MB typical) | Limited by RAM |
| Speed | Very fast (just moves stack pointer) | Slower (OS allocation) |
| Scope | Local to function | Persists until freed |
| Fragmentation | None | Possible |
| Overflow risk | Stack overflow (deep recursion) | Memory leak / OOM |
void example() {
int stack_var = 10; // stack
int *heap_var = malloc(4); // heap
*heap_var = 20;
free(heap_var); // required
} // stack_var freed automatically
9. What is a memory leak? How do you detect and prevent it?
A memory leak occurs when allocated heap memory is never freed, causing the program's memory usage to grow indefinitely.
// Leak: ptr reassigned without freeing
int *ptr = malloc(100);
ptr = malloc(200); // first allocation lost — LEAK
// Fix
int *ptr = malloc(100);
free(ptr);
ptr = malloc(200);
free(ptr);
Detection tools:
- Valgrind (
valgrind --leak-check=full ./prog) — reports all leaks - AddressSanitizer (
gcc -fsanitize=address) — runtime detection - Electric Fence — catches buffer overruns and use-after-free
Prevention patterns:
- Every
mallochas a correspondingfree - Use goto/label cleanup pattern in C for multiple exit paths
- RAII-equivalent: wrapper macros that auto-free on scope exit (GLib style)
10. What is a dangling pointer?
A dangling pointer points to memory that has been freed or gone out of scope.
int *p = malloc(sizeof(int));
*p = 10;
free(p);
printf("%d\n", *p); // UNDEFINED BEHAVIOR — dangling pointer
// Fix: set to NULL after free
free(p);
p = NULL;
if (p) { /* safe */ }
| Type | Cause |
|---|---|
| Freed heap | free(p) then use p |
| Out-of-scope stack | Return pointer to local variable |
| Reallocated | realloc moved the block |
Strings
11. How are strings represented in C?
C strings are null-terminated arrays of char. The null terminator ('\0', value 0) marks the end.
char s1[] = "hello"; // {'h','e','l','l','o','\0'} — 6 bytes
char *s2 = "hello"; // string literal — read-only in most implementations
char s3[10] = "hello"; // 10 bytes, null-padded
printf("%zu\n", strlen(s1)); // 5 (excludes '\0')
printf("%zu\n", sizeof(s1)); // 6 (includes '\0')
printf("%zu\n", sizeof(s2)); // pointer size (4 or 8)
Key distinction: sizeof gives allocation size; strlen gives string length (scanning for '\0').
12. What are the common string functions in <string.h>?
| Function | Purpose | Safe alternative |
|---|---|---|
strlen(s) |
Length (excl. \0) |
— |
strcpy(dst, src) |
Copy string | strncpy / strlcpy |
strcat(dst, src) |
Concatenate | strncat / strlcat |
strcmp(a, b) |
Compare (0=equal, <0, >0) | strncmp |
strchr(s, c) |
Find first char | — |
strstr(s, sub) |
Find substring | — |
memcpy(dst, src, n) |
Copy n bytes | — |
memset(dst, c, n) |
Fill n bytes | — |
snprintf(buf, n, fmt, ...) |
Safe formatted write | Prefer over sprintf |
char src[] = "world";
char dst[20];
strcpy(dst, "hello ");
strcat(dst, src);
printf("%s\n", dst); // hello world
// Safe version with size limit
char buf[10];
snprintf(buf, sizeof(buf), "%s", "longer than ten characters");
printf("%s\n", buf); // "longer th" — truncated safely
13. What is a buffer overflow and how do you prevent it?
A buffer overflow writes past the end of a buffer, corrupting adjacent memory or enabling exploits.
// UNSAFE
char buf[8];
scanf("%s", buf); // reads unlimited input — overflow possible
gets(buf); // NEVER USE gets() — removed in C11
// SAFE
char buf[8];
scanf("%7s", buf); // limit to 7 chars + null terminator
fgets(buf, sizeof(buf), stdin); // preferred: includes newline, null-safe
Prevention:
- Always specify buffer size in format strings or function arguments
- Use
snprintfinstead ofsprintf - Use
strncpy/strncat(orstrlcpy/strlcaton BSD/macOS) - Enable stack canaries (
-fstack-protector) and ASLR - Use static analysis (Clang-Tidy, PVS-Studio)
Structs and data types
14. What is structure padding?
The compiler inserts padding bytes between struct members to satisfy alignment requirements, which can make a struct larger than the sum of its members.
struct A {
char c; // 1 byte
// 3 bytes padding
int i; // 4 bytes
char d; // 1 byte
// 3 bytes padding
};
printf("%zu\n", sizeof(struct A)); // 12
// Reorder to minimize padding
struct B {
int i; // 4 bytes
char c; // 1 byte
char d; // 1 byte
// 2 bytes padding
};
printf("%zu\n", sizeof(struct B)); // 8
Use #pragma pack(1) or __attribute__((packed)) to remove padding (risk: unaligned access penalty on some CPUs).
15. What are bit fields in C?
Bit fields allow packing multiple boolean/small-integer values into a single word.
struct Flags {
unsigned int read : 1;
unsigned int write : 1;
unsigned int execute : 1;
unsigned int unused : 5; // padding to byte boundary
};
struct Flags f = {1, 0, 1, 0};
printf("%d %d %d\n", f.read, f.write, f.execute); // 1 0 1
printf("%zu\n", sizeof(f)); // 4 (implementation-defined)
Use cases: hardware register maps, network protocol headers, compact flag storage.
16. What is the difference between struct, union, and enum?
// struct: all members exist, total size = sum of members + padding
struct Point { int x; int y; }; // 8 bytes
// union: members share memory, size = largest member
union Data { int i; float f; char c; }; // 4 bytes
union Data d;
d.i = 42; // OK
d.f = 1.5; // overwrites d.i — only last written is valid
// enum: named integer constants
enum Color { RED = 0, GREEN = 1, BLUE = 2 };
enum Color c = GREEN;
Unions are commonly used for type-punning (with caveats about strict aliasing) and tagged unions (variant types).
Preprocessor
17. What is the difference between #define and const?
| Aspect | #define |
const |
|---|---|---|
| Type checking | None — text substitution | Yes — type-safe |
| Debugger visibility | No (replaced before compile) | Yes |
| Scope | From definition to end of file | Block/file scope |
| Memory | No storage | Storage (optimized away often) |
| Expressions | Can define macros | Values only |
#define MAX 100 // text replacement: MAX → 100
const int MAX = 100; // typed constant
// Prefer const for simple values
// Use #define for token manipulation, include guards, conditional compilation
18. What are include guards and why are they needed?
Include guards prevent a header file from being included multiple times in the same translation unit, which would cause redefinition errors.
// myheader.h
#ifndef MYHEADER_H
#define MYHEADER_H
struct Point { int x; int y; };
void print_point(struct Point p);
#endif /* MYHEADER_H */
Modern alternative: #pragma once (not standard, but supported by all major compilers):
#pragma once
struct Point { int x; int y; };
19. What are variadic macros?
Macros that accept a variable number of arguments using ... and __VA_ARGS__.
#include <stdio.h>
#define DEBUG(fmt, ...) \
fprintf(stderr, "[DEBUG] %s:%d: " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)
DEBUG("value = %d", 42);
// Output: [DEBUG] main.c:10: value = 42
##__VA_ARGS__ removes the preceding comma when no extra args are passed (GCC extension, widely supported).
Bit manipulation
20. What are the bitwise operators in C?
| Operator | Name | Example |
|---|---|---|
& |
AND | 5 & 3 = 1 (0101 & 0011 = 0001) |
| |
OR | 5 | 3 = 7 (0101 | 0011 = 0111) |
^ |
XOR | 5 ^ 3 = 6 (0101 ^ 0011 = 0110) |
~ |
NOT | ~5 = -6 (two's complement) |
<< |
Left shift | 5 << 1 = 10 |
>> |
Right shift | 20 >> 2 = 5 |
// Common bit tricks
int x = 0b1010;
// Set bit n
x |= (1 << 2); // set bit 2: 0b1010 | 0b0100 = 0b1110
// Clear bit n
x &= ~(1 << 1); // clear bit 1: 0b1110 & 0b1101 = 0b1100
// Toggle bit n
x ^= (1 << 3); // toggle bit 3
// Test bit n
int is_set = (x >> 2) & 1;
// Check power of two
int is_pow2 = n > 0 && (n & (n - 1)) == 0;
// Swap without temp
a ^= b; b ^= a; a ^= b;
21. How do you count set bits (popcount)?
// Naive O(n) per bit
int count_bits(unsigned int n) {
int count = 0;
while (n) {
count += n & 1;
n >>= 1;
}
return count;
}
// Brian Kernighan's trick: O(set bits)
int count_bits_fast(unsigned int n) {
int count = 0;
while (n) {
n &= n - 1; // clears lowest set bit
count++;
}
return count;
}
// GCC/Clang built-in (maps to single CPU instruction)
int c = __builtin_popcount(42);
Functions and scope
22. What is the difference between pass by value and pass by pointer in C?
C is strictly pass-by-value — a copy of the argument is made. To "pass by reference," you pass a pointer.
// Pass by value — original unchanged
void double_val(int x) { x *= 2; }
// Pass by pointer — modifies original
void double_ptr(int *x) { *x *= 2; }
int n = 5;
double_val(n); printf("%d\n", n); // 5 — unchanged
double_ptr(&n); printf("%d\n", n); // 10 — modified
For large structs, passing by pointer is more efficient even when modification isn't needed (pass const struct Foo *).
23. What are static variables and functions?
The static keyword has two distinct meanings:
// 1. static local variable: persists across function calls
void counter() {
static int count = 0; // initialized once; retains value
count++;
printf("%d\n", count);
}
counter(); // 1
counter(); // 2
counter(); // 3
// 2. static at file scope: limits linkage to current file
// (only visible within the same .c file)
static int helper_var = 42;
static void helper_func(void) { /* internal */ }
Static functions are the C equivalent of private in C++/Java.
24. What is extern in C?
extern declares that a variable or function is defined in another translation unit.
// file1.c
int global_var = 10;
// file2.c
extern int global_var; // declaration — no new storage allocated
void use_it() {
printf("%d\n", global_var); // accesses file1.c's global_var
}
| Keyword | Purpose |
|---|---|
extern |
Declare without defining (use from another file) |
static (file scope) |
Define with internal linkage (hide from other files) |
| (no keyword) | Define with external linkage (visible to all files) |
25. What is a recursive function? What are its risks?
// Fibonacci (exponential — O(2^n))
int fib(int n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
// Factorial — O(n) recursive
long long fact(int n) {
if (n <= 1) return 1;
return n * fact(n - 1);
}
Risks:
- Stack overflow — each call consumes stack frame (~100–1000 bytes); deep recursion crashes
- No tail-call optimization guaranteed in C (GCC optimizes
-O2with-foptimize-sibling-calls) - Performance — function call overhead vs iteration
Rule of thumb: Prefer iteration for >1000 depth; use dynamic programming to memoize recursive solutions.
Storage classes and qualifiers
26. What are the four storage classes in C?
| Storage class | Scope | Lifetime | Default init |
|---|---|---|---|
auto |
Block | Block duration | Garbage |
register |
Block | Block duration | Garbage (hint to compiler) |
static |
Block or file | Program duration | Zero |
extern |
File | Program duration | Zero |
auto is the default for local variables. register is a hint to store in a CPU register (ignored by modern compilers).
27. What does the volatile qualifier do?
volatile tells the compiler not to optimize reads/writes to a variable because it may change unexpectedly (hardware register, ISR, shared memory).
// Without volatile, compiler may cache value in register
volatile int *hardware_reg = (volatile int *)0xDEADBEEF;
// Reads always go to memory
while (*hardware_reg == 0) { /* wait for hardware */ }
Common uses:
- Memory-mapped I/O registers
- Variables modified by interrupt service routines
- Variables in signal handlers
- Shared memory (though prefer
_Atomicorpthreadprimitives for concurrent code)
28. What does the const qualifier do?
const int x = 10; // x is read-only
// Pointer and const combinations
const int *p1 = &x; // pointer to const int — *p1 can't change
int * const p2 = &x; // const pointer to int — p2 can't change (address)
const int * const p3 = &x; // both pointer and value are const
// In function parameters: communicate "won't modify"
void print_str(const char *s) { /* s content is read-only */ }
const is about the programmer's intent and enables compiler warnings + optimizations. It does not make data immutable at runtime.
Arrays and memory layout
29. What is the relationship between arrays and pointers?
An array name decays to a pointer to its first element in most expressions. However, arrays are not pointers.
int arr[] = {1, 2, 3, 4, 5};
int *p = arr; // arr decays to &arr[0]
// Equivalent access
arr[2] == *(arr + 2) == p[2] == *(p + 2) // all true
// Key differences
sizeof(arr) == 20 // 5 * 4 — full array size
sizeof(p) == 8 // pointer size only
// Array of arrays (2D)
int mat[3][4];
// mat[i][j] == *(*(mat + i) + j)
Array decay is why passing an array to a function loses size information — always pass the length separately or use a struct wrapper.
30. How do you dynamically allocate a 2D array?
int rows = 3, cols = 4;
// Method 1: Array of pointers (non-contiguous rows)
int **mat = malloc(rows * sizeof(int *));
for (int i = 0; i < rows; i++)
mat[i] = malloc(cols * sizeof(int));
mat[1][2] = 42;
for (int i = 0; i < rows; i++) free(mat[i]);
free(mat);
// Method 2: Single contiguous block (cache-friendly)
int (*mat2)[cols] = malloc(rows * cols * sizeof(int));
mat2[1][2] = 42;
free(mat2);
Method 2 (pointer to VLA) is cache-friendlier and requires only one free. Method 1 is more flexible for jagged arrays.
Type system and conversions
31. What is implicit type conversion (promotion) in C?
char c = 65;
int i = c; // char promoted to int — 65
float f = i; // int converted to float — 65.0
double d = f; // float promoted to double
// Integer arithmetic promotion
short a = 10, b = 20;
int result = a + b; // a and b both promoted to int before addition
// Potential pitfall
unsigned int u = 10;
int s = -1;
if (s < u) { ... } // WRONG — s converted to unsigned, becomes huge positive number
Integer promotion rules (simplified):
char/short/bool→int- In mixed signed/unsigned: unsigned wins
- In mixed float/int: float wins
32. What is the difference between int, long, long long, and their sizes?
Sizes are implementation-defined but have minimums:
| Type | Minimum bits | Typical LP64 (Linux/macOS 64-bit) |
|---|---|---|
char |
8 | 8 |
short |
16 | 16 |
int |
16 | 32 |
long |
32 | 64 |
long long |
64 | 64 |
size_t |
— | 64 |
ptrdiff_t |
— | 64 |
Use <stdint.h> for exact widths: int8_t, int16_t, int32_t, int64_t, uint32_t, etc.
#include <stdint.h>
int32_t a = 2147483647;
uint64_t b = 18446744073709551615ULL;
Standard library and I/O
33. What is the difference between printf and fprintf?
#include <stdio.h>
printf("Hello\n"); // prints to stdout
fprintf(stdout, "Hello\n"); // same as printf
fprintf(stderr, "Error: %s\n", msg); // prints to stderr (for errors/diagnostics)
fprintf(file_ptr, "Value: %d\n", x); // prints to any FILE*
Format specifiers:
| Specifier | Type |
|---|---|
%d, %i |
int |
%u |
unsigned int |
%ld |
long |
%lld |
long long |
%f |
float/double |
%lf |
double (in scanf) |
%c |
char |
%s |
char * (string) |
%p |
pointer |
%zu |
size_t |
%x |
hex (lowercase) |
34. How does scanf differ from fgets?
// scanf — stops at whitespace, unsafe for strings
char name[50];
scanf("%s", name); // reads one word only, no width check
scanf("%49s", name); // safer: limits to 49 chars
// fgets — reads a full line including spaces, null-terminates
fgets(name, sizeof(name), stdin);
// Note: fgets includes the '\n' in the buffer if buffer is large enough
name[strcspn(name, "\n")] = '\0'; // strip newline
Prefer fgets + sscanf for parsing user input — it handles whitespace correctly and avoids buffer overflows.
Compilation and linking
35. What are the stages of C compilation?
Source (.c) → Preprocessor → Compiler → Assembler → Linker → Executable
| Stage | Tool | Input → Output | Flag to stop |
|---|---|---|---|
| Preprocessing | cpp | .c → .i |
gcc -E |
| Compilation | cc1 | .i → .s |
gcc -S |
| Assembly | as | .s → .o |
gcc -c |
| Linking | ld | .o → executable |
(none) |
gcc -E main.c -o main.i # preprocessed
gcc -S main.c -o main.s # assembly
gcc -c main.c -o main.o # object file
gcc main.o -o main # link
gcc -Wall -O2 main.c -o main # typical full build
36. What is the difference between a header file and a source file?
Header (.h) Source (.c)
────────────────────── ──────────────────────
Declarations Definitions
Function prototypes Function bodies
Type definitions (struct) Variable storage
Macros malloc/logic
extern variable declarations extern variable definitions
Headers are #included into multiple .c files. Definitions must appear exactly once across all translation units (One Definition Rule). Guard against including definitions in headers to avoid multiple-definition linker errors.
Common patterns and algorithms
37. How do you reverse a string in C?
#include <string.h>
void reverse_string(char *s) {
int left = 0, right = strlen(s) - 1;
while (left < right) {
char tmp = s[left];
s[left] = s[right];
s[right] = tmp;
left++;
right--;
}
}
char s[] = "hello";
reverse_string(s);
printf("%s\n", s); // olleh
38. How do you implement a linked list in C?
#include <stdlib.h>
typedef struct Node {
int data;
struct Node *next;
} Node;
// Prepend to list
Node *push(Node *head, int val) {
Node *node = malloc(sizeof(Node));
node->data = val;
node->next = head;
return node;
}
// Print list
void print_list(Node *head) {
for (Node *cur = head; cur; cur = cur->next)
printf("%d -> ", cur->data);
printf("NULL\n");
}
// Free list
void free_list(Node *head) {
while (head) {
Node *next = head->next;
free(head);
head = next;
}
}
// Usage
Node *list = NULL;
list = push(list, 3);
list = push(list, 2);
list = push(list, 1);
print_list(list); // 1 -> 2 -> 3 -> NULL
free_list(list);
39. How do you implement a stack using an array?
#define MAX 100
typedef struct {
int data[MAX];
int top;
} Stack;
void init(Stack *s) { s->top = -1; }
int is_full(Stack *s) { return s->top == MAX - 1; }
int is_empty(Stack *s) { return s->top == -1; }
void push(Stack *s, int val) {
if (!is_full(s)) s->data[++(s->top)] = val;
}
int pop(Stack *s) {
if (!is_empty(s)) return s->data[(s->top)--];
return -1; // error
}
int peek(Stack *s) { return s->data[s->top]; }
40. What is qsort and how do you use it?
qsort from <stdlib.h> sorts any array using a user-supplied comparator.
#include <stdlib.h>
int cmp_int(const void *a, const void *b) {
return (*(int *)a - *(int *)b); // ascending
}
int cmp_str(const void *a, const void *b) {
return strcmp(*(char **)a, *(char **)b);
}
int arr[] = {5, 2, 8, 1, 9};
qsort(arr, 5, sizeof(int), cmp_int);
// arr is now {1, 2, 5, 8, 9}
// String array sort
char *words[] = {"banana", "apple", "cherry"};
qsort(words, 3, sizeof(char *), cmp_str);
// words is now {"apple", "banana", "cherry"}
The comparator must return negative/zero/positive for less/equal/greater.
Advanced topics
41. What is typedef and when should you use it?
// Without typedef
struct Point { int x; int y; };
struct Point p; // must write "struct" every time
// With typedef
typedef struct Point { int x; int y; } Point;
Point p; // cleaner
// Function pointer typedef
typedef int (*Comparator)(const void *, const void *);
Comparator cmp = cmp_int;
// Common idiom: opaque type
typedef struct _Handle Handle; // forward declaration in header
// users never see the struct internals
42. What is the restrict keyword?
restrict (C99) tells the compiler that a pointer is the only way to access the object it points to, enabling optimizations by eliminating aliasing concerns.
// Without restrict: compiler must assume a and b might alias
void add(int *a, const int *b, int n) {
for (int i = 0; i < n; i++) a[i] += b[i];
}
// With restrict: no aliasing — compiler can use SIMD/vectorize
void add_r(int * restrict a, const int * restrict b, int n) {
for (int i = 0; i < n; i++) a[i] += b[i];
}
Misusing restrict (passing overlapping pointers) is undefined behavior. Standard library functions like memcpy use restrict; memmove does not.
43. What is inline in C?
inline is a hint to the compiler to expand the function body at the call site, eliminating function call overhead.
inline int max(int a, int b) {
return a > b ? a : b;
}
Modern compilers (-O2) inline automatically based on heuristics. __attribute__((always_inline)) forces inlining; __attribute__((noinline)) prevents it.
Note: In C99/C11, inline functions with external linkage need one non-inline definition in a .c file, or use static inline.
44. What are flexible array members?
C99 allows the last member of a struct to be an incomplete array, enabling variable-length trailing data with a single malloc.
typedef struct {
int length;
int data[]; // flexible array member — no size specified
} IntArray;
// Allocate for length=5
IntArray *arr = malloc(sizeof(IntArray) + 5 * sizeof(int));
arr->length = 5;
arr->data[0] = 10;
arr->data[4] = 50;
free(arr);
This is the idiomatic C pattern for variable-length structures, used extensively in the Linux kernel.
45. What is setjmp/longjmp and when is it used?
Non-local jumps that transfer control across function boundaries — C's way to implement exceptions.
#include <setjmp.h>
jmp_buf buf;
void risky() {
// something went wrong
longjmp(buf, 1); // jump back to setjmp, returning 1
}
int main() {
int ret = setjmp(buf); // returns 0 first time, non-zero on longjmp
if (ret == 0) {
risky();
} else {
printf("caught error: %d\n", ret);
}
}
Caution: longjmp bypasses destructors and free — use carefully to avoid leaks. Not safe across signal handlers in all implementations.
C standard versions
46. What are the major differences between C89, C99, and C11?
| Feature | C89/ANSI C | C99 | C11 |
|---|---|---|---|
// comments |
No | Yes | Yes |
bool |
No | <stdbool.h> |
Yes |
| Variable declarations | Top of block only | Anywhere | Anywhere |
| VLAs (variable-length arrays) | No | Yes (optional in C11) | Optional |
restrict |
No | Yes | Yes |
stdint.h |
No | Yes | Yes |
__VA_ARGS__ macros |
No | Yes | Yes |
inline |
No | Yes | Yes |
_Bool, _Complex |
No | Yes | Yes |
_Generic (type selection) |
No | No | Yes |
_Static_assert |
No | No | Yes |
_Atomic |
No | No | Yes |
Threads (<threads.h>) |
No | No | Yes (optional) |
Use gcc -std=c11 -Wall -Wextra -pedantic for modern portable C.
Error handling
47. How is error handling done in C?
C has no exceptions. Conventions:
// Convention 1: return error code, output via pointer
int divide(int a, int b, int *result) {
if (b == 0) return -1; // error
*result = a / b;
return 0; // success
}
// Convention 2: return -1/NULL, check errno
#include <errno.h>
FILE *f = fopen("file.txt", "r");
if (!f) {
perror("fopen"); // prints: fopen: No such file or directory
return 1;
}
// Convention 3: global errno for POSIX functions
int fd = open("file.txt", O_RDONLY);
if (fd < 0) {
fprintf(stderr, "open failed: %s\n", strerror(errno));
}
Always check return values of memory allocation and I/O functions.
Debugging and tools
48. What debugging tools do C programmers use?
| Tool | Purpose | Usage |
|---|---|---|
gdb |
Interactive debugger | gdb ./prog then run, bt, print |
valgrind |
Memory error detection | valgrind --leak-check=full ./prog |
AddressSanitizer |
Fast memory error detection | gcc -fsanitize=address,undefined |
UndefinedBehaviorSanitizer |
UB detection | gcc -fsanitize=undefined |
strace |
System call trace | strace ./prog |
ltrace |
Library call trace | ltrace ./prog |
gprof |
Profiling | gcc -pg then gprof |
perf |
Linux performance counters | perf stat ./prog |
clang-tidy |
Static analysis | clang-tidy main.c |
cppcheck |
Static analysis | cppcheck main.c |
# Typical debugging build
gcc -g -O0 -Wall -Wextra -fsanitize=address,undefined -o prog main.c
gdb ./prog
(gdb) run
(gdb) backtrace
(gdb) print variable_name
Anti-patterns
49. What are common C programming mistakes?
| Anti-pattern | Problem | Fix |
|---|---|---|
gets(buf) |
Buffer overflow, removed in C11 | Use fgets |
Not checking malloc return |
Null dereference | Always check if (!ptr) |
int i = strlen(s) |
Signed/unsigned mismatch | Use size_t i |
Forgetting to free |
Memory leak | Every malloc needs free |
free(ptr) without ptr = NULL |
Dangling pointer double-free | Set ptr = NULL after free |
| Returning pointer to local | Dangling pointer | Use heap allocation |
scanf("%s", buf) |
No size limit | Use scanf("%99s", buf) |
char *s = "literal"; s[0] = 'x' |
Write to read-only segment (UB) | Use char s[] = "literal" |
if (ptr = NULL) |
Assignment in condition | Use if (ptr == NULL) or if (!ptr) |
| Signed integer overflow | Undefined behavior | Use unsigned or check before op |
Comparison
50. How does C compare to C++ and other languages?
| Feature | C | C++ | Rust | Python |
|---|---|---|---|---|
| Memory management | Manual | Manual + RAII | Ownership/borrow | GC |
| OOP | No (struct + fn pointers) | Yes (classes, vtable) | Traits | Yes |
| Generics | Macros / void* | Templates | Generics | Duck typing |
| Error handling | Return codes / errno | Exceptions + codes | Result<T,E> |
Exceptions |
| Runtime overhead | None | Minimal | None | Interpreter |
| Standard library | Small (libc) | Large (STL) | Large (std) | Very large |
| Compilation speed | Fast | Slow (templates) | Slow | N/A |
| Learning curve | Medium | High | High | Low |
| Best for | OS/embedded/systems | Systems + applications | Safe systems | Scripting/ML |
C remains the lingua franca of systems programming — the Linux kernel, CPython, SQLite, Redis, and most embedded firmware are written in C.
Common mistakes
| Mistake | Explanation |
|---|---|
| Off-by-one in arrays | arr[n] is out of bounds; valid range is arr[0]..arr[n-1] |
Comparing char with EOF |
char c; c = getchar() — EOF is -1, may be truncated to 0xFF; use int c |
String comparison with == |
== compares pointers, not content; use strcmp |
| Modifying string literals | char *s = "hello"; s[0] = 'H'; is UB; use char s[] = "hello" |
| Integer division truncation | 5 / 2 == 2 (not 2.5); cast to float first |
| Implicit function declaration | In C99+ all functions must be declared before use |
Missing break in switch |
Falls through to next case silently |
sizeof on decayed array |
void f(int a[]) { sizeof(a); } gives pointer size, not array size |
C vs related languages
| Language | Relation to C |
|---|---|
| C++ | Superset of C (mostly); adds classes, templates, RAII |
| Objective-C | Strict superset of C; used for Apple OS before Swift |
| Go | Designed by C authors; GC, goroutines, no manual memory |
| Rust | Systems language like C; no GC; compile-time memory safety |
| Zig | Modern C replacement; comptime, no macros, better error handling |
| Python (CPython) | Interpreter written in C; C extensions via Python/C API |
FAQ
Q: Is C still worth learning in 2025? A: Yes — it's required for OS development, embedded systems, game engines, database internals, compilers, and understanding how higher-level languages work. C is also the FFI base for almost every language runtime.
Q: What is undefined behavior (UB) in C? A: Operations where the C standard imposes no requirements on the result — the compiler may emit anything. Common UBs: signed integer overflow, dereferencing null/dangling pointers, accessing out-of-bounds array elements, use-after-free, reading uninitialized variables.
Q: What is the difference between char *s = "hello" and char s[] = "hello"?
A: char *s = "hello" is a pointer to a string literal (usually read-only). char s[] = "hello" copies the string into a local array (writable). Modifying through a char * literal pointer is undefined behavior.
Q: What is two's complement and why does it matter?
A: Two's complement is the standard way CPUs represent signed integers. In two's complement, -1 is 0xFFFFFFFF for a 32-bit int. C11 mandates two's complement for all signed integers. It makes addition/subtraction hardware the same for signed and unsigned, but overflow of signed integers is still UB in C.
Q: Should I use int or size_t for loop counters over arrays?
A: Use size_t (from <stddef.h> or <stdlib.h>) when iterating over arrays or dealing with object sizes — it matches sizeof/strlen return types and avoids signed/unsigned comparison warnings. Use int for mathematical loops where negative values are meaningful.
Q: What is the difference between malloc(0) and a null pointer?
A: malloc(0) is implementation-defined — it may return either NULL or a unique non-null pointer that must not be dereferenced. Always check the return and treat a zero-size allocation as an edge case to handle explicitly.