C is a general-purpose, procedural, statically typed, compiled programming language that compiles to native machine code, provides manual memory management via malloc/free, direct memory access through pointers, and no runtime overhead — the foundational systems programming language of operating systems, kernels, compilers, and embedded systems.
📑 C Reference — All Topics
Stack vs heap, malloc/free, pointer arithmetic, NULL.
Defining structs, arrays, strings as char arrays.
What it is, the most common causes, how to avoid it.
The Linux kernel and most of Windows/macOS still run substantially on this.
No official toolchain — GCC and Clang are both free and both production-grade.
sizeof(int) isn't guaranteed to be 4 — only a MINIMUM size is promised.
C had no boolean type at all until C99 — 0 was false, everything else true.
No pass-by-reference exists — pass a pointer, or nothing you do sticks.
No string type exists — just a char array that happens to end in a zero byte.
Every member starts at the SAME address — write one, read a different one, legally.
Every malloc needs exactly one matching free — miss it, and it's gone forever.
A macro without enough parentheses is a classic, real, easy-to-hit bug.
#include isn't a real import — it's the preprocessor copy-pasting text.
fopen returns NULL on failure, not an exception — and nothing forces you to check.
& vs && both compile — and they do genuinely different things.
qsort doesn't know how to compare YOUR data — you pass it a pointer to a function.
No networking, no JSON, not even a real string — deliberately, by design.
Each .c file compiles with zero visibility into any other file's internals.
A recipe indented with spaces instead of a real tab produces a cryptic error, every time.
A segfault alone tells you nothing — GDB is how you find the line and the pointer.
A leak or overflow can compile clean and run silently — until one of these tools finds it.
A struct holding a pointer to another instance of itself — the root idea behind every C linked structure.
C makes no tail-call optimization guarantee — deep recursion can genuinely overflow.
printf trusts the format string entirely — a mismatch is undefined behavior, not a caught error.
A C enum is just a named integer — the compiler won't stop you assigning 9999.
const int *p and int * const p mean completely different things.
No try/catch exists — if you don't check the return value, the error just vanishes.
C11 standardized threads.h — but pthreads is what nearly everyone actually uses.
For a decade, one book WAS the spec — until ANSI finally formalized C in 1989.
C is the language that sits closest to the hardware — no garbage collector, no objects, no runtime, just your code, the CPU, and the memory. Every other language was either written in C or heavily influenced by it.
What C is
C is a general-purpose, procedural, statically typed, compiled programming language created by Dennis Ritchie at Bell Labs between 1969 and 1973 to rewrite the Unix operating system. C compiles directly to native machine code — no virtual machine, no interpreter, no garbage collector. The programmer manages memory explicitly with malloc and free. In exchange, C delivers performance as close to hand-written assembly as a high-level language can get, complete control over memory layout, and near-universal portability across every CPU architecture.
C is the language of operating systems (Linux kernel, macOS/iOS XNU kernel, Windows NT), embedded systems, database engines (SQLite, PostgreSQL internals), interpreters (CPython, Ruby MRI), compilers (GCC, Clang), and virtually every piece of systems software that runs the world.
#include <stdio.h> /* standard I/O — printf, scanf */
#include <stdlib.h> /* malloc, free, exit */
int main(void) {
printf("Hello, World!\n");
/* Variables — types are explicit, no inference */
int count = 10;
double price = 29.99;
char letter = 'A';
char name[] = "Priya"; /* char array (string) */
/* printf format codes */
printf("Name: %s, Count: %d, Price: %.2f\n", name, count, price);
return 0; /* 0 = success; non-zero = error */
}Types and sizes
C's type sizes are platform-defined, not fixed — int is at least 16 bits but usually 32 on 64-bit systems. For fixed-width types, use <stdint.h>: int8_t, int16_t, int32_t, int64_t, and their unsigned variants. size_t is the correct type for memory sizes and array indices — it is pointer-sized (32-bit on 32-bit systems, 64-bit on 64-bit).
#include <stdio.h>
#include <stdint.h> /* fixed-width types */
int main(void) {
/* Platform-dependent sizes */
printf("int: %zu bytes\n", sizeof(int)); /* usually 4 */
printf("long: %zu bytes\n", sizeof(long)); /* 4 or 8 */
printf("double: %zu bytes\n", sizeof(double)); /* 8 */
printf("char*: %zu bytes\n", sizeof(char*)); /* 4 or 8 */
/* Fixed-width — always these sizes */
int32_t x = 2147483647; /* exactly 32 bits */
uint64_t big = 18446744073709551615ULL; /* max uint64 */
/* size_t — correct type for sizes and indices */
size_t n = 100;
int arr[n]; /* VLA — variable length array, C99+ */
return 0;
}Pointers — the central concept
A pointer is a variable that holds a memory address. int* p declares a pointer to an int. &x gets the address of x. *p dereferences the pointer — reads or writes the value at the stored address. Pointers are what make C both powerful and dangerous: they enable efficient data structures, direct hardware access, and pass-by-reference — but an invalid pointer access produces undefined behaviour.
#include <stdio.h>
int main(void) {
int x = 42;
int *p = &x; /* p holds the address of x */
printf("x = %d\n", x); /* 42 */
printf("p = %p\n", p); /* address like 0x7fff... */
printf("*p = %d\n", *p); /* 42 — dereference: value at p */
*p = 100; /* modify x through the pointer */
printf("x = %d\n", x); /* 100 */
/* Pointer arithmetic */
int arr[] = {10, 20, 30, 40};
int *q = arr; /* q points to arr[0] */
printf("%d\n", *(q + 2)); /* 30 — arr[2] */
return 0;
}Manual memory management
C has no garbage collector. Dynamic memory comes from the heap via malloc (allocate) and must be returned via free (deallocate). Failing to free allocated memory is a memory leak. Freeing memory and then using it is a use-after-free. Freeing the same pointer twice is a double-free. All three are undefined behaviour — they may crash immediately, produce wrong results, or enable security exploits.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
/* Allocate array on heap */
int n = 10;
int *arr = malloc(n * sizeof(int)); /* request n ints */
if (arr == NULL) {
fprintf(stderr, "malloc failed\n");
return 1;
}
/* Use the memory */
for (int i = 0; i < n; i++) arr[i] = i * i;
printf("%d\n", arr[5]); /* 25 */
free(arr); /* return memory to heap */
arr = NULL; /* prevent dangling pointer use */
/* calloc: allocate + zero-initialise */
char *buf = calloc(100, sizeof(char));
strncpy(buf, "Hello", 99);
printf("%s\n", buf);
free(buf);
return 0;
}Strings in C
C has no built-in string type. A C string is a null-terminated array of char: the sequence ends with a '\0' (NUL) byte. strlen(s) counts bytes up to (not including) the NUL. strcpy, strcat, sprintf: all dangerous if the destination buffer is too small. Use strncpy, strncat, snprintf with explicit size limits. Buffer overflows — writing past the end of a string buffer — are the most common class of C security vulnerability.
Structs, unions, and enums
C organises data with structs (named groups of fields), unions (multiple fields sharing the same memory — only one valid at a time), and enums (named integer constants). Structs are C's only mechanism for grouping related data — there are no classes. Functions operating on structs are defined separately and receive a pointer to the struct as a parameter, forming the basis of C's OOP-like patterns used in the Linux kernel and other large C codebases.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char name[64];
double balance;
} BankAccount;
/* "Method" — takes pointer to struct */
void deposit(BankAccount *acc, double amount) {
if (amount > 0) acc->balance += amount;
}
void print_account(const BankAccount *acc) {
printf("Account(%s: %.2f)\n", acc->name, acc->balance);
}
int main(void) {
BankAccount acc;
strncpy(acc.name, "Priya", sizeof(acc.name) - 1);
acc.balance = 1000.0;
deposit(&acc, 500.0);
print_account(&acc); /* Account(Priya: 1500.00) */
/* Heap-allocated struct */
BankAccount *heap_acc = malloc(sizeof(BankAccount));
strncpy(heap_acc->name, "Rahul", sizeof(heap_acc->name) - 1);
heap_acc->balance = 2000.0;
free(heap_acc);
return 0;
}Function pointers and callbacks
Functions are not first-class values in C, but function pointers enable passing functions as arguments — a form of callbacks and polymorphism. The standard library's qsort and bsearch accept a comparison function pointer. Many event-driven C systems (libuv, GTK, callbacks in embedded firmware) use function pointers as their core abstraction.
#include <stdio.h>
#include <stdlib.h>
int compare_ints(const void *a, const void *b) {
return (*(int*)a - *(int*)b); /* comparison for qsort */
}
int main(void) {
int arr[] = {5, 2, 8, 1, 9, 3};
int n = sizeof(arr) / sizeof(arr[0]);
/* qsort takes a function pointer */
qsort(arr, n, sizeof(int), compare_ints);
for (int i = 0; i < n; i++) printf("%d ", arr[i]);
/* 1 2 3 5 8 9 */
/* Declare a function pointer variable */
int (*cmp)(const void*, const void*) = compare_ints;
printf("\ncmp ptr: %p\n", (void*)cmp);
return 0;
}The preprocessor
The C preprocessor runs before compilation, performing text substitution. #include pastes file contents. #define defines macros — text replacements before parsing. #ifdef/#ifndef/#endif conditionally include code. Macros are powerful but dangerous — they don't respect scope or types. Prefer const variables over #define for constants and inline functions over function-like macros.
#include <stdio.h>
#define MAX_SIZE 100 /* constant macro — prefer const int */
#define MIN(a,b) ((a)<(b)?(a):(b)) /* function macro — has pitfalls */
/* MIN(x++, y++) — x or y incremented TWICE! Use inline function instead */
/* Header guard — prevent double-inclusion */
#ifndef MYHEADER_H
#define MYHEADER_H
typedef struct { int x, y; } Point;
int distance(Point a, Point b);
#endif /* MYHEADER_H */
/* Conditional compilation */
#ifdef DEBUG
#define LOG(msg) fprintf(stderr, "[DEBUG] %s\n", msg)
#else
#define LOG(msg) ((void)0) /* no-op in release */
#endifUndefined behaviour: the compiler's contract
ISO C defines many operations as undefined behaviour (UB): signed integer overflow, null pointer dereference, out-of-bounds array access, use-after-free, reading uninitialised variables, data races. When UB occurs, the C standard gives compilers permission to generate any code. Compilers assume UB never occurs and optimise accordingly — meaning UB can cause code to be silently deleted, produce security holes, or behave correctly during testing but fail in production. Use -fsanitize=address,undefined (AddressSanitizer + UBSan) to detect UB at runtime during development.
int arr[10]: passing arr to a function passes a pointer to the first element. sizeof(arr) inside the declaring function gives the total bytes; sizeof(arr) inside any other function gives the pointer size (8 bytes on 64-bit). Always pass the array length as a separate parameter.The C abstract machine and the as-if rule
ISO C defines a program's behaviour in terms of an abstract machine — a hypothetical computer with a specific memory model, sequence points, and execution rules. The compiler is free to generate any code as long as the observable behaviour (I/O, volatile accesses, and the program's return value) matches what the abstract machine would produce. This is the as-if rule (ISO C17 §5.1.2.3). The as-if rule allows compilers to reorder, eliminate, and transform operations freely — constant folding, dead code elimination, loop unrolling — as long as the observable output is identical. When UB is present, the abstract machine's behaviour is undefined, so the as-if rule applies to nothing — compilers may produce literally anything.
Memory models: stack, heap, BSS, text, data
A C process's virtual memory is divided into segments: text — executable code (read-only, shared between processes running the same binary); data — initialised global/static variables (int x = 5; at file scope); BSS (Block Started by Symbol) — uninitialised global/static variables, zero-initialised by the OS loader; stack — local variables, function call frames, grows downward; heap — dynamic allocations via malloc, grows upward. Stack allocation is O(1) — just decrement the stack pointer. Heap allocation is managed by the allocator (jemalloc, tcmalloc, glibc malloc) — typically O(1) amortised but with fragmentation overhead.
C17, C23 and the standards timeline
C89/C90: the original ANSI/ISO standard. Declarations must precede statements; no bool, no inline. C99: added inline, mixed declarations/code, variable-length arrays (VLAs), _Bool, stdint.h, stdbool.h, designated initialisers, compound literals. C11: _Generic type-generic expressions, _Static_assert, anonymous structs/unions, thread support (threads.h), atomic operations (stdatomic.h). C17: defect fixes only — no new features. C23 (published 2024): nullptr keyword, binary literals 0b1010, #embed for embedding binary data, typeof, constexpr, improved attributes.
ISO/IEC 9899:2018 — C17 standard. iso.org/standard/74528.html. Kernighan, B. W. & Ritchie, D. M. (1988). The C Programming Language (2nd ed.). Prentice Hall — K&R. The reference implementation of C for most purposes. Open standard draft: open-std.org/JTC1/SC22/WG14/.