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 access — arr[-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).