C

C is the layer below everything. The Linux kernel, CPython, the JVM, the V8 engine, most embedded firmware — all C. It gives you direct memory access, minimal runtime overhead, and zero abstractions you didn’t ask for. The cost is that you manage everything yourself: allocation, deallocation, bounds checking, null handling. Undefined behavior is not an edge case to avoid — it’s a constant threat that requires deliberate discipline to manage.

Learning C doesn’t make you a better Java developer the way people claim. But it does make the rest of computing stop feeling like magic.


🟢 Junior

Types and Memory Sizes

C’s integer types have platform-dependent sizes. Use <stdint.h> types when size matters.

#include <stdint.h>
#include <stdio.h>

int8_t  a = 127;         // exactly 8 bits, signed
uint8_t b = 255;         // exactly 8 bits, unsigned
int32_t c = 2147483647;  // exactly 32 bits
int64_t d = 9223372036854775807LL;

printf("size of int: %zu bytes\n", sizeof(int));  // platform-dependent: 4 on most
printf("size of long: %zu bytes\n", sizeof(long)); // 4 on Windows 64-bit, 8 on Linux 64-bit

printf format specifiers: %d (int), %u (unsigned), %f (float/double), %s (string), %p (pointer), %zu (size_t). Wrong format specifier for the type is undefined behavior.

Pointers

A pointer holds the memory address of a value. & takes the address of a variable. * dereferences a pointer to access the value at that address.

int x = 42;
int *ptr = &x;    // ptr contains the address of x

printf("%d\n", *ptr);  // 42 — dereference
*ptr = 100;            // modifies x through the pointer
printf("%d\n", x);     // 100

// Pointer arithmetic
int arr[] = {10, 20, 30, 40, 50};
int *p = arr;           // array name decays to pointer to first element
printf("%d\n", *(p+2)); // 30 — equivalent to arr[2]
p++;                    // advance by sizeof(int) bytes
printf("%d\n", *p);     // 20

A NULL pointer points to nothing. Dereferencing it is undefined behavior — typically a segfault.

Strings

C strings are null-terminated arrays of char. The null terminator '\0' marks the end.

#include <string.h>

char name[] = "Alice";            // 6 bytes: 'A','l','i','c','e','\0'
char buf[64] = {0};               // zero-initialized buffer

strlen(name);                     // 5 — does not count '\0'
strcpy(buf, name);                // copies including '\0'
strncpy(buf, name, sizeof(buf)-1); // safer — limits copy length
strncat(buf, " Smith", sizeof(buf)-strlen(buf)-1);

strcmp("abc", "abc");  // 0 — equal
strcmp("abc", "abd");  // negative — 'c' < 'd'

Never use gets() — it has no bounds checking and is a buffer overflow vulnerability. Use fgets() instead.

Functions and Stack

Function parameters are passed by value — C always copies. Pass a pointer to modify the caller’s variable.

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int x = 1, y = 2;
swap(&x, &y);
// x == 2, y == 1

The call stack stores local variables, return addresses, and parameters. Stack memory is automatically reclaimed when the function returns. Never return a pointer to a local variable — it becomes dangling immediately.


🟡 Medior

Dynamic Memory: malloc, calloc, realloc, free

Heap memory persists until explicitly freed with free().

#include <stdlib.h>
#include <string.h>

int n = 100;
int *arr = malloc(n * sizeof(int));    // uninitialized
int *zero = calloc(n, sizeof(int));    // zero-initialized

if (!arr || !zero) {
    perror("malloc failed");
    exit(EXIT_FAILURE);
}

// Grow the array
arr = realloc(arr, n * 2 * sizeof(int));
if (!arr) { /* original arr is freed by realloc on failure */ exit(1); }

memset(arr + n, 0, n * sizeof(int));   // zero the new half

free(arr);
free(zero);
arr = NULL;   // set to NULL after free — prevents use-after-free bugs
zero = NULL;

Every malloc/calloc/realloc must be matched by exactly one free. Double-free is undefined behavior.

Structs and Typedef

Structs group related data. In C (unlike C++), you must write struct before the type name, unless you use typedef.

typedef struct {
    int id;
    char name[64];
    double balance;
} Account;

typedef struct Node Node;
struct Node {
    int value;
    Node *next;  // self-referential — must use struct tag, not typedef
};

Account a = { .id = 1, .name = "Alice", .balance = 100.0 };
printf("Account %d: %s $%.2f\n", a.id, a.name, a.balance);

Struct padding: the compiler inserts padding bytes to align members to their natural alignment. A struct with a char followed by an int is likely 8 bytes, not 5. Use __attribute__((packed)) (GCC) or #pragma pack only when interfacing with hardware or network protocols.

File I/O

#include <stdio.h>

FILE *f = fopen("data.bin", "rb");
if (!f) { perror("fopen"); return 1; }

uint32_t header;
size_t nread = fread(&header, sizeof(header), 1, f);
if (nread != 1) { /* handle error */ }

fseek(f, 0, SEEK_END);          // seek to end
long size = ftell(f);            // get file size
fseek(f, 0, SEEK_SET);          // seek back to start

fclose(f);                       // always close

For text files, use fopen("file.txt", "r") and fscanf/fprintf/fgets. Binary files use "rb"/"wb".

The Preprocessor

The preprocessor runs before compilation, performing text substitution.

#define MAX_USERS 256
#define MIN(a, b) ((a) < (b) ? (a) : (b))   // always parenthesize macro args

// Include guards prevent double-inclusion
#ifndef MY_HEADER_H
#define MY_HEADER_H

typedef struct { int x, y; } Point;
void draw_point(Point p);

#endif  // MY_HEADER_H

Macros have no type safety and no scope. Prefer const int MAX = 256; and inline functions over macros in modern C.

Conditional compilation:

#ifdef DEBUG
    printf("debug: x = %d\n", x);
#endif

#if defined(_WIN32)
    // Windows-specific code
#elif defined(__linux__)
    // Linux-specific code
#endif

🔴 Senior

Undefined Behavior in C

The C standard defines certain programs as having undefined behavior (UB) — the compiler may generate any code, including code that silently gives wrong results, crashes, or corrupts memory.

Signed integer overflow — unlike unsigned (which wraps modulo 2^N), signed overflow is UB. The compiler may assume it does not happen.

Out-of-bounds array accessarr[-1] or arr[N] where N is the size. UB, not a guaranteed bounds error.

Strict aliasing violation — accessing the same memory through incompatible pointer types is UB. Use memcpy or char* (which may alias anything) for type punning.

Dereferencing a NULL or dangling pointer — UB. Common result is a segfault, but the standard allows anything.

Compile with -fsanitize=address,undefined to detect many of these at runtime during development.

Function Pointers and Callbacks

A function pointer holds the address of a function, enabling callbacks and dispatch tables.

typedef int (*CompareFn)(const void *, const void *);

int compare_int(const void *a, const void *b) {
    return *(const int*)a - *(const int*)b;
}

int arr[] = {5, 3, 8, 1, 9};
qsort(arr, 5, sizeof(int), compare_int);

// Generic dispatch table
typedef void (*HandlerFn)(int);

HandlerFn handlers[256] = { 0 };  // NULL = unregistered
handlers['A'] = handle_alpha;
handlers['1'] = handle_digit;

void dispatch(char c, int data) {
    if (handlers[(unsigned char)c]) {
        handlers[(unsigned char)c](data);
    }
}

Memory Alignment and restrict

restrict tells the compiler that two pointers do not alias the same memory, enabling vectorization:

void add_vectors(float * restrict dst,
                 const float * restrict a,
                 const float * restrict b,
                 size_t n) {
    for (size_t i = 0; i < n; ++i) {
        dst[i] = a[i] + b[i];  // compiler can use SIMD (AVX, SSE)
    }
}

_Alignas and alignof control alignment for SIMD types or DMA buffers:

#include <stdalign.h>

alignas(32) float vec[8];   // 32-byte aligned for AVX

Valgrind and AddressSanitizer

Two essential tools for finding memory bugs in C:

Valgrind (valgrind --leak-check=full ./program): detects memory leaks, invalid reads/writes, use of uninitialized values. Slows the program ~20×.

AddressSanitizer (gcc -fsanitize=address): catches buffer overflows, use-after-free, double-free, stack corruption at near-native speed. Use for automated testing.

Undefined Behavior Sanitizer (-fsanitize=undefined): catches signed overflow, misaligned access, null pointer dereference, strict aliasing violations.

Run all three in CI. Fix every warning — they are not style issues, they are bugs.

Senior Gotchas

sizeof on a pointer returns the pointer size (8 on 64-bit), not the array size. When you pass an array to a function, it decays to a pointer and size information is lost. Always pass the size explicitly.

void process(int *arr, size_t n) { ... }   // correct
void process(int arr[]) { ... }             // same as int* — n is unknown

char may be signed or unsigned depending on the platform. Comparing char values against EOF (-1 from getchar()) requires storing the result in an int, not a char.

realloc on failure returns NULL but does not free the original pointer — if you store the result directly back in the original pointer, you lose it and leak memory.

Integer promotions: expressions like uint8_t a = 200; uint8_t b = 100; int c = a + b; promote both operands to int before adding — c is 300, not 44 (which would be the result of wrapping at 8 bits).