-
C — Debugging with Valgrind
Valgrind is a dynamic analysis tool that runs your program in a controlled environment and reports every memory error — leaks, invalid accesses, and use of uninitialised values — with the exact line where they happened.
Step 1 — Compile with Debug Symbols
Like GDB, Valgrind needs the
-gflag to show source lines instead of raw addresses:gcc -Wall -Wextra -Werror -g main.c -o my_programStep 2 — Run Under Valgrind
valgrind ./my_programThis runs Memcheck (Valgrind’s default tool) and prints a summary at the end. For thorough leak detection use the full set of flags:
valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes -s ./my_programFlag Effect --leak-check=fullreport every individual leaked block, not just totals --show-leak-kinds=allinclude definitely, indirectly, possibly lost, and still reachable --track-origins=yesshow where an uninitialised value was created -sprint a short error summary at the end Reading the Output
A clean run ends with:
LEAK SUMMARY: definitely lost: 0 bytes in 0 blocks indirectly lost: 0 bytes in 0 blocks possibly lost: 0 bytes in 0 blocks still reachable: 0 bytes in 0 blocks suppressed: 0 bytes in 0 blocks ERROR SUMMARY: 0 errors from 0 contextsAny non-zero number is a problem to fix.
Types of Errors
Memory Leak —
definitely lostMemory was allocated but never freed, and no pointer to it remains:
int *arr = malloc(10 * sizeof(int)); // forgot to free(arr)LEAK SUMMARY: definitely lost: 40 bytes in 1 blocksFix: add
free(arr)before the program exits.Invalid Read / Write
Accessing memory outside an allocated block — classic buffer overflow or off-by-one:
int *arr = malloc(3 * sizeof(int)); arr[3] = 99; // index 3 is out of bounds (valid: 0, 1, 2)Invalid write of size 4 at 0x... main (main.c:6) Address 0x... is 0 bytes after a block of size 12 alloc'dFix: check array sizes and loop bounds.
Use of Uninitialised Value
Reading a variable that was declared but never assigned:
int x; if (x > 0) // x has no defined value printf("positive\n");Conditional jump or move depends on uninitialised value(s) at 0x... main (main.c:5)With
--track-origins=yesValgrind also reports wherexwas allocated, making it much easier to trace the root cause.Invalid Free
Freeing a pointer that was already freed, or one that was never malloc’d:
free(ptr); free(ptr); // double freeInvalid free() / delete / delete[] / realloc() at 0x... free (in valgrind) by 0x... main (main.c:8) Address 0x... is 0 bytes inside a block of size 4 free'dFix: set
ptr = NULLimmediately afterfree(ptr).Still Reachable
Memory that was never freed but a valid pointer to it still exists at exit. Technically not a leak (the pointer is accessible), but still sloppy:
char *buf = malloc(64); // buf is never freed, but main() returns with buf still in scopeThese show up under
still reachablein the summary. Free everything before returning frommain.
A Typical Workflow
# Compile gcc -Wall -Wextra -Werror -g main.c utils.c -o my_program # Run with full checks valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes -s ./my_program # If your program takes arguments valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes -s ./my_program arg1 arg2Read the output top to bottom:
- Each error block shows the type, the line, and the call stack
- The LEAK SUMMARY groups leaks by category
- The ERROR SUMMARY gives the total count
Fix errors from the innermost call stack frame up — that is usually the root cause.
Quick Reference
Command / Flag Effect valgrind ./progrun with default Memcheck settings --leak-check=fullreport each leaked block individually --show-leak-kinds=allinclude all leak categories --track-origins=yesshow where uninitialised values come from -sprint error summary at the end definitely lostmalloc’d memory with no surviving pointer — must fix still reachablepointer exists but memory never freed — fix before exit Invalid read/writeout-of-bounds access Uninitialised valuevariable used before being assigned Invalid freedouble free or freeing a non-heap pointer Created on July 2026 -
C — Debugging with GDB
GDB (GNU Debugger) lets you run a C program step by step, inspect memory and variables at any point, and find exactly where and why something goes wrong.
Step 1 — Compile with Debug Symbols
By default GCC strips debug information from the binary. The
-gflag includes it:gcc -Wall -Wextra -Werror -g main.c -o my_programWithout
-g, GDB can still run the program but cannot show source lines, variable names, or meaningful stack frames — just raw addresses.Step 2 — Launch GDB
gdb ./my_programThis opens the GDB prompt
(gdb). The program is loaded but not yet running.If your program takes command-line arguments, use
--argsso GDB does not consume them:gdb --args ./my_program arg1 arg2Everything after the binary name is forwarded to your program when you run
r. Without--args, passing arguments directly togdbwould be interpreted as GDB options.Step 3 — Use GDB TUI for a Visual Layout
gdbtuilaunches GDB with a split-screen terminal UI — source code on top, command prompt below. It is far easier to follow than the plain prompt:gdbtui ./my_program gdbtui --args ./my_program arg1 arg2 # with argumentsThe top pane shows the source file with an arrow pointing at the current line. The bottom pane is the normal GDB command prompt.
Core Commands
r— RunStarts (or restarts) the program. Pass arguments after
rif your program expects them:(gdb) r (gdb) r arg1 arg2The program runs until it hits a breakpoint, crashes, or finishes.
b— BreakpointPauses execution before a specific line or function:
(gdb) b 42 # break at line 42 of the current file (gdb) b main.c:15 # break at line 15 of main.c (gdb) b my_function # break at the entry of my_functionList all breakpoints:
(gdb) info bDelete a breakpoint by its number:
(gdb) delete 1n— Next (step over)Executes the current line and moves to the next one. If the current line is a function call, the entire function runs without stepping into it:
(gdb) ns— Step (step into)Like
n, but if the current line calls a function, GDB steps inside that function:(gdb) sUse
swhen you want to follow execution into a function you wrote. Usenwhen you want to skip over library calls or functions you trust.c— ContinueResumes execution at full speed until the next breakpoint (or the end of the program):
(gdb) cprint— Inspect a ValueEvaluates and prints any expression — variable, pointer, arithmetic:
(gdb) print x (gdb) print *ptr (gdb) print arr[2] (gdb) print a + bYou can also modify a variable on the fly:
(gdb) print x = 10disp— Display (auto-print on every step)displayworks likeprintbut re-evaluates and shows the value automatically after everyn,s, orcthat pauses execution:(gdb) disp x (gdb) disp *ptr (gdb) disp arr[i]List active displays:
(gdb) info displayRemove a display by its number:
(gdb) undisplay 1
Useful Extra Commands
bt— BacktraceShows the full call stack at the current point — essential when diagnosing a crash:
(gdb) bt#0 ft_strlen (s=0x0) at ft_strlen.c:6 #1 ft_putstr (str=0x0) at ft_putstr.c:12 #2 main () at main.c:20Read from bottom to top:
maincalledft_putstrwhich calledft_strlenwhere the crash occurred.p/x— Print in HexadecimalUseful for inspecting raw memory values, addresses, and bitfields:
(gdb) p/x flags (gdb) p/x *ptrwatch— WatchpointPauses execution whenever a variable’s value changes — useful for tracking down unexpected mutations:
(gdb) watch xrefresh— Redraw the TUIThe TUI pane can glitch after terminal output or resizing.
refresh(or its shorthandref) redraws it cleanly:(gdb) refresh (gdb) refRun it any time the source window looks garbled.
q— Quit(gdb) q
A Typical Session
# Compile with debug symbols gcc -Wall -Wextra -Werror -g main.c utils.c -o my_program # Launch with TUI (with optional arguments) gdbtui --args ./my_program arg1 arg2(gdb) b main # break at entry (gdb) r # run until breakpoint (gdb) disp i # watch variable i on every step (gdb) disp *ptr # watch what ptr points to (gdb) n # step over next line (gdb) s # step into the function call (gdb) print result # inspect a value (gdb) c # continue to next breakpoint (gdb) bt # check the call stack after a crash (gdb) q # quit
Quick Reference
Command Effect gcc -ginclude debug symbols in the binary gdbtui ./proglaunch GDB with split source/command UI gdb --args ./prog a bpass arguments to the program, not to GDB rrun (or restart) the program b <line>set a breakpoint at a line or function nnext line — step over function calls sstep — enter function calls ccontinue until next breakpoint print <expr>evaluate and print an expression once disp <expr>auto-print an expression after every step btshow the full call stack p/x <expr>print value in hexadecimal watch <var>pause when a variable changes refresh/refredraw the TUI when it glitches qquit GDB Created on July 2026 -
C — Function Pointers
A function pointer stores the address of a function in memory and lets you call it indirectly. This enables callbacks, generic utilities, and runtime behaviour selection.
Syntax
The declaration mirrors a normal pointer but wraps the name in parentheses and includes the return type and parameter types:
int (*op)(int, int); // pointer to a function: takes two ints, returns intAssigning and calling:
int add(int a, int b) { return (a + b); } int sub(int a, int b) { return (a - b); } int (*op)(int, int); op = &add; printf("%d\n", op(3, 4)); // 7 op = ⊂ printf("%d\n", op(3, 4)); // -1The
&is optional — a function name alone already decays to its address — but writing it makes the intent explicit.Passing a Function Pointer to a Function
This is the callback pattern: a function receives another function as a parameter and calls it internally.
int apply(int a, int b, int (*f)(int, int)) { return (f(a, b)); } printf("%d\n", apply(10, 3, &add)); // 13 printf("%d\n", apply(10, 3, &sub)); // 7void * — Generic Callbacks
When the data type is not known at compile time, use
void *as the parameter type. The caller casts to the right type inside the callback.void apply(void *data, void (*f)(void *)) { f(data); }Example callback printing an
int:void print_int(void *data) { printf("%d\n", *(int *)data); } int n = 42; apply(&n, print_int); // 42Example callback printing a
char *:void print_str(void *data) { printf("%s\n", (char *)data); } apply("hello", print_str); // helloThis pattern is used extensively in linked-list functions like
ft_lstiterandft_lstmap.typedef for Readability
Long function pointer types can be aliased with
typedef:typedef void (*t_callback)(void *); void run(void *data, t_callback f) { f(data); }Dispatch Tables
An array of function pointers acts as a lookup table — a clean alternative to long
if/elseorswitchchains:int add(int a, int b) { return (a + b); } int sub(int a, int b) { return (a - b); } int mul(int a, int b) { return (a * b); } int (*ops[3])(int, int) = {add, sub, mul}; int result = ops[1](10, 4); // calls sub → 6Indexed by an enum or integer, dispatch tables make it easy to extend behaviour without touching existing logic.
Quick Reference
Syntax Meaning int (*f)(int, int)pointer to function returning int, taking two ints f = &addassign address of function addtoff(a, b)call the function through the pointer void (*f)(void *)generic callback — takes any pointer, returns nothing *(int *)datacast void *back toint *then dereferencetypedef void (*t_cb)(void *)alias a function pointer type Created on July 2026 -
C — Linked Lists
A linked list is a chain of nodes where each node holds a value and a pointer to the next node. Unlike arrays, nodes are scattered in heap memory — the links are what connect them.
[value | next] → [value | next] → [value | next] → NULL head tailThe Node Struct
typedef struct s_node { int value; struct s_node *next; } t_node;The
nextpointer usesstruct s_node *(nott_node *) because the typedef is not yet complete at that point in the declaration.For a generic list (storing any type), use
void *for the content:typedef struct s_list { void *content; struct s_list *next; } t_list;Creating a Node
Always allocate on the heap — stack-allocated nodes are destroyed when the function returns:
t_node *new_node(int value) { t_node *node; node = malloc(sizeof(t_node)); if (!node) return (NULL); node->value = value; node->next = NULL; return (node); }Always check the return value of
malloc— if it returnsNULL, allocation failed.Building a List
Add to the Front
Prepending is O(1) — no traversal needed:
void push_front(t_node **head, int value) { t_node *node; node = new_node(value); if (!node) return ; node->next = *head; *head = node; }headis a double pointer so the function can update the caller’s pointer.t_node *list = NULL; push_front(&list, 3); // [3] push_front(&list, 2); // [2] → [3] push_front(&list, 1); // [1] → [2] → [3]Add to the Back
Appending requires traversing to the last node — O(n):
void push_back(t_node **head, int value) { t_node *node; t_node *current; node = new_node(value); if (!node) return ; if (!*head) { *head = node; return ; } current = *head; while (current->next) current = current->next; current->next = node; }Traversing a List
Walk from
headtoNULL, processing each node:void print_list(t_node *head) { t_node *current; current = head; while (current) { printf("%d\n", current->value); current = current->next; } }Never modify
headdirectly while traversing — use a separatecurrentpointer.Searching
Return the first node that matches a value, or
NULLif not found:t_node *find(t_node *head, int target) { while (head) { if (head->value == target) return (head); head = head->next; } return (NULL); }List Size
int ft_lstsize(t_node *head) { int count; count = 0; while (head) { count++; head = head->next; } return (count); }Last Node
t_node *ft_lstlast(t_node *head) { if (!head) return (NULL); while (head->next) head = head->next; return (head); }Deleting a Node
To remove a node from the middle, re-link the previous node’s
nextto skip it:void delete_node(t_node **head, int target) { t_node *current; t_node *prev; current = *head; prev = NULL; while (current) { if (current->value == target) { if (prev) prev->next = current->next; else *head = current->next; // deleting the head free(current); return ; } prev = current; current = current->next; } }Freeing the Entire List
Walk the list and free each node. Advance the pointer before freeing the current node:
void free_list(t_node **head) { t_node *current; t_node *next; current = *head; while (current) { next = current->next; // save next before freeing free(current); current = next; } *head = NULL; // prevent dangling pointer }Never call
current = current->nextafterfree(current)— the memory is invalid.Applying a Function to Every Node
Iterate over the list and call a function on each node’s content:
void ft_lstiter(t_list *lst, void (*f)(void *)) { while (lst) { f(lst->content); lst = lst->next; } }This is the
void *function pointer pattern —fcan be any function that takes avoid *.Doubly Linked Lists
A doubly linked list adds a
prevpointer so you can traverse in both directions:typedef struct s_dnode { int value; struct s_dnode *next; struct s_dnode *prev; } t_dnode;Insertion and deletion are more complex (two pointers to update instead of one), but moving backwards or deleting a node given only its pointer becomes O(1).
Linked List vs Array
Array Linked List Access by index O(1) O(n) Insert at front O(n) O(1) Insert at back O(1) amortised O(n) singly / O(1) with tail pointer Insert in middle O(n) O(n) to find, O(1) to link Memory contiguous — cache friendly scattered — pointer overhead per node Resize realloc needed just add a node Use a linked list when you insert or remove frequently at the front, or when the size changes unpredictably. Use an array when you need fast indexed access.
Quick Reference
Operation Key point Create node malloc(sizeof(t_node)), check for NULL, setnext = NULLPush front O(1) — new node’s next= old head, update headPush back O(n) — traverse to last, set last->next= new nodeTraverse use a currentpointer, never moveheadDelete node re-link prev->nextto skip the node, thenfreeFree list save nextbeforefree, sethead = NULLafterDouble pointer **headrequired when a function must update the caller’s head Created on July 2026 -
C — Structs
A
structgroups variables of different types under one name — useful for representing real-world objects like a point, a player, or a node in a list.
Declaring a Struct
struct s_point { int x; int y; };To use it you must write
struct s_pointevery time, which is verbose.typedefsolves this by creating an alias:typedef struct s_point { int x; int y; } t_point; t_point p; p.x = 10; p.y = 20; printf("(%d, %d)\n", p.x, p.y);A common convention is
s_prefix for the struct tag andt_prefix for the typedef name.Accessing Members
Use
.to access a member through a variable, and->through a pointer:t_point p; t_point *ptr = &p; p.x = 5; // direct access ptr->x = 5; // pointer access — equivalent to (*ptr).xStructs as Function Parameters
Passing a struct by value copies the whole thing. Pass a pointer instead to avoid the copy and to allow the function to modify the original:
void move(t_point *p, int dx, int dy) { p->x += dx; p->y += dy; } t_point pos = {0, 0}; move(&pos, 3, 5); printf("(%d, %d)\n", pos.x, pos.y); // (3, 5)Nested Structs
Structs can contain other structs:
typedef struct s_rect { t_point top_left; t_point bottom_right; } t_rect; t_rect r; r.top_left.x = 0; r.bottom_right.x = 100;Self-Referential Structs — Linked Lists
A struct can hold a pointer to another struct of the same type. This is the foundation of linked lists:
typedef struct s_node { int value; struct s_node *next; } t_node;Each node points to the next one. The last node’s
nextisNULL.t_node *head = malloc(sizeof(t_node)); head->value = 1; head->next = malloc(sizeof(t_node)); head->next->value = 2; head->next->next = NULL;Traversing the list:
t_node *current = head; while (current) { printf("%d\n", current->value); current = current->next; }Structs with malloc
For dynamic allocation, use
sizeofwith the struct type:t_point *p = malloc(sizeof(t_point)); if (!p) return (1); p->x = 10; p->y = 20; free(p);
Quick Reference
Syntax Meaning struct s_foo { ... };declare a struct typedef struct s_foo { ... } t_foo;declare with alias p.xaccess member of a variable ptr->xaccess member through a pointer sizeof(t_foo)size of the struct in bytes struct s_node *nextself-referential pointer (linked list) Created on July 2026 -
C — Makefile
A
Makefileautomates compilation. Instead of retypinggcccommands every time, you runmakeand the build system figures out what needs to be rebuilt.
A Minimal Makefile
NAME = my_program CC = gcc CFLAGS = -Wall -Wextra -Werror SRCS = main.c utils.c OBJS = $(SRCS:.c=.o) all: $(NAME) $(NAME): $(OBJS) $(CC) $(CFLAGS) $(OBJS) -o $(NAME) %.o: %.c $(CC) $(CFLAGS) -c $< -o $@ clean: rm -f $(OBJS) fclean: clean rm -f $(NAME) re: fclean all .PHONY: all clean fclean reHow It Works
Variables
NAME = my_program # output binary name CC = gcc # compiler CFLAGS = -Wall -Wextra -Werror # compiler flags SRCS = main.c utils.c # source files OBJS = $(SRCS:.c=.o) # replace .c with .o automatically$(VAR)expands a variable.$(SRCS:.c=.o)is a substitution reference — it producesmain.o utils.ofrommain.c utils.c.Rules
A rule has the form:
target: prerequisites recipe- target — the file to produce (or a phony label like
clean) - prerequisites — files the target depends on; if any are newer, the recipe runs
- recipe — shell commands to run (must be indented with a real tab, not spaces)
Pattern Rule
%.o: %.c $(CC) $(CFLAGS) -c $< -o $@%is a wildcard.$<is the first prerequisite (the.cfile),$@is the target (the.ofile). This single rule compiles every.cinto its matching.o..PHONY
.PHONY: all clean fclean reTells
makethese targets are not real files. Without this, if a file namedcleanexisted,make cleanwould do nothing.Mandatory Rules
Rule Effect make/make allcompile the program make cleanremove .oobject filesmake fcleanremove .ofiles and the binarymake refull rebuild from scratch ( fcleanthenall)Every C project should have all four.
Incremental Compilation
makeonly recompiles files that changed. If you editutils.c, onlyutils.ois rebuilt —main.ois untouched. This saves time on large projects.Common Mistakes
Spaces instead of tabs — the recipe line must start with a tab character. Most editors show them the same but
makewill refuse:Makefile:10: *** missing separator. Stop.Forgetting
fcleanremoves the binary —cleanonly removes.ofiles.fcleanalso removes$(NAME). Both are required.Not listing all source files — if
SRCSis incomplete, some files won’t compile and you’ll get linker errors about undefined symbols.
Quick Reference
Syntax Meaning $(VAR)expand variable $(SRCS:.c=.o)substitution: replace .cwith.o$@the target of the current rule $<the first prerequisite %.o: %.cpattern rule matching any .c→.o.PHONYdeclare targets that are not files -cflagcompile to .owithout linkingCreated on July 2026 - target — the file to produce (or a phony label like
-
C — Header Files
A header file (
.h) declares the interface of your code — function prototypes, types, constants, and macros — so that multiple.cfiles can share them without duplicating declarations.
Why Header Files
In C, you must declare a function before calling it. When a project grows beyond one file, repeating declarations in every
.cis error-prone. A header file centralises them:project/ ├── main.c ├── math_utils.c └── math_utils.hmath_utils.hdeclares whatmath_utils.cprovides.main.cincludes the header and gains access to those declarations without needing to know the implementation.What Goes in a Header
Put in .hKeep in .cFunction prototypes Function bodies typedef/structdefinitionsLocal variables #defineconstants and macrosstatichelpersexternvariable declarationsVariable definitions Never put function bodies or variable definitions in a header — they would be compiled multiple times if the header is included in several files.
A Minimal Header
/* math_utils.h */ #ifndef MATH_UTILS_H # define MATH_UTILS_H int add(int a, int b); int sub(int a, int b); int clamp(int val, int min, int max); #endifThe three lines around the content are the include guard — explained below.
Include Guards
If a header is included more than once in the same translation unit (directly or through another header), the compiler would see duplicate declarations and error. An include guard prevents this:
#ifndef MATH_UTILS_H // if not yet defined... # define MATH_UTILS_H // ...define this token... /* all your declarations */ #endif // end of guarded blockOn the first inclusion the token is undefined, so the block is processed and the token is defined. On any subsequent inclusion the token already exists, so the entire block is skipped.
The token name is conventionally the filename in uppercase with
.replaced by_.#include— Angle Brackets vs Quotes#include <stdio.h> // system / standard library header #include "my_file.h" // your own header (searched relative to the source file first)Syntax Search path <header.h>system include paths (GCC’s standard library locations) "header.h"current directory first, then system paths Always use quotes for your own headers and angle brackets for standard library ones.
Sharing Types and Constants
Headers are the right place for types and constants that multiple files need:
/* types.h */ #ifndef TYPES_H # define TYPES_H # define BUFFER_SIZE 1024 # define MAX_PLAYERS 4 typedef struct s_player { char name[64]; int score; int lives; } t_player; #endifAny
.cfile that includestypes.hcan uset_player,BUFFER_SIZE, andMAX_PLAYERS.Linking Headers and Sources
The header declares; the source defines. Both must agree on the signature:
/* math_utils.h */ int add(int a, int b);/* math_utils.c */ #include "math_utils.h" int add(int a, int b) { return (a + b); }/* main.c */ #include <stdio.h> #include "math_utils.h" int main(void) { printf("%d\n", add(3, 4)); // 7 return (0); }Compile all
.cfiles together:gcc -Wall -Wextra -Werror main.c math_utils.c -o my_programOr let the Makefile handle it with a pattern rule.
The
externKeywordTo share a variable across files, declare it
externin the header and define it once in exactly one.cfile:/* globals.h */ extern int g_frame_count;/* globals.c */ #include "globals.h" int g_frame_count = 0; // one definitionAny file that includes
globals.hcan read and writeg_frame_count. Global variables should be used sparingly — prefer passing data through function parameters.Nested Includes
Headers can include other headers. Include guards ensure there is no infinite loop or duplication regardless of the inclusion order.
A common pattern is a single top-level header that bundles everything:
/* my_project.h */ #ifndef MY_PROJECT_H # define MY_PROJECT_H # include <stdlib.h> # include <unistd.h> # include "types.h" # include "math_utils.h" #endifThen each
.cfile only needs#include "my_project.h".
Quick Reference
Concept Key point .hfiledeclarations only — no function bodies include guard #ifndef/#define/#endifprevents double inclusion<header.h>system library header "header.h"your own header typedefin.hshare a type across multiple files externin.hdeclare a variable defined elsewhere Makefile compile all .cfiles together — header is included automaticallyCreated on July 2026 -
C — Recursion
A recursive function is a function that calls itself. It is an alternative to loops for problems that can be broken into smaller identical sub-problems.
Structure of a Recursive Function
Every recursive function needs two parts:
- Base case — the condition that stops the recursion
- Recursive case — the call that reduces the problem toward the base case
Without a base case the function calls itself forever until the program crashes.
int factorial(int n) { if (n <= 1) // base case return (1); return (n * factorial(n - 1)); // recursive case }The Call Stack
Each time a function is called, the OS pushes a stack frame onto the call stack. That frame stores the function’s local variables, parameters, and return address.
When a recursive function calls itself, a new frame is pushed on top of the previous one. They stack up until the base case is reached, then they unwind one by one as each call returns.
factorial(4) └─ factorial(3) └─ factorial(2) └─ factorial(1) ← base case, returns 1 returns 2 × 1 = 2 returns 3 × 2 = 6 returns 4 × 6 = 24The call stack has a fixed size. Too many nested calls exhaust it — this is a stack overflow, which causes a segmentation fault.
Practical Examples
Countdown
void countdown(int n) { if (n < 0) return ; printf("%d\n", n); countdown(n - 1); }Print String in Reverse
Recursion naturally defers work until the stack unwinds:
void print_reverse(char *str) { if (*str == '\0') return ; print_reverse(str + 1); write(1, str, 1); // printed on the way back up }Sum of an Array
int sum(int *arr, int size) { if (size == 0) return (0); return (arr[0] + sum(arr + 1, size - 1)); }Recursion vs Iteration
Recursion Iteration Readability often cleaner for tree/list problems clearer for simple counters Memory uses call stack (risk of overflow) uses a fixed loop variable Performance function call overhead per step generally faster Norminette rule allowed whileonly (forforbidden)Recursion is the natural fit for problems with a recursive structure: trees, linked lists, path-finding, and divide-and-conquer algorithms.
Common Mistakes
Missing base case — infinite recursion, stack overflow:
int bad(int n) { return (n * bad(n - 1)); // never stops }Base case never reached — e.g. decrementing when input is already negative:
int bad(int n) { if (n == 0) // never hit if n starts negative return (0); return (bad(n - 1)); }Always verify your base case covers every possible entry value.
Quick Reference
Concept Key point Base case stops the recursion — must always be reachable Recursive case calls itself with a smaller / simpler input Stack frame memory pushed per call — holds locals and return address Stack overflow too many nested calls exhaust the call stack Unwind return values propagate back up through each frame Created on July 2026 -
C — Basics
This post covers the C foundations — everything from compiling your first file to managing memory manually.
Compilation
C is a compiled language. You write source code (
.c), then convert it to an executable using a compiler like GCC.gcc main.c -o my_program # compile main.c into an executable called my_program ./my_program # run itCommon flags:
gcc -Wall -Wextra -Werror main.c -o my_programFlag Effect -Wallenable all standard warnings -Wextraenable extra warnings -Werrortreat warnings as errors Code must compile with all three. No warning tolerance.
Variables and Types
C is statically typed — every variable has a fixed type declared at creation.
int age = 25; float price = 9.99; double pi = 3.14159265; char letter = 'A'; char name[] = "Sam";Type Size Use char1 byte single character or small integer int4 bytes whole numbers float4 bytes decimal numbers (less precise) double8 bytes decimal numbers (more precise) Constants are declared with
#defineorconst:#define MAX 100 const int LIMIT = 50;Standard Output
C has no
printbuilt-in. You usewrite(low-level) orprintf(formatted):#include <unistd.h> write(1, "Hello\n", 6); // file descriptor 1 = stdout#include <stdio.h> printf("Hello, %s! You are %d years old.\n", name, age);Common format specifiers:
Specifier Type %dinteger %ffloat %cchar %sstring %ppointer address A common exercise is to rewrite
printfyourself asft_printf.Conditionals
if (age >= 18) { printf("Adult\n"); } else if (age >= 13) { printf("Teenager\n"); } else { printf("Child\n"); }Comparison and logical operators:
Operator Meaning ==equal !=not equal ><greater / less than >=<=greater or equal / less or equal &&logical AND ||logical OR !logical NOT Bitwise & Bit Shift Operators
Bitwise operators work directly on the binary representation of integers, bit by bit.
Operator Name Example Result &AND 5 & 31|OR 5 | 37^XOR 5 ^ 36~NOT ~5-6<<left shift 1 << 38>>right shift 8 >> 14How They Work
Each integer is a sequence of bits. The operators act on each pair of bits independently:
5 = 0101 3 = 0011 ---- & AND: 0001 = 1 | OR: 0111 = 7 ^ XOR: 0110 = 6~flips every bit (bitwise NOT). Because of two’s complement,~5gives-6.Bit Shifts
<<shifts bits to the left (multiplies by powers of 2).>>shifts bits to the right (divides by powers of 2).int x = 1; // 0001 x = x << 1; // 0010 = 2 x = x << 2; // 1000 = 8 x = x >> 1; // 0100 = 4Shifting left by
nis equivalent to multiplying by2^n, and is faster than actual multiplication:int n = 3; int result = n << 2; // 3 * 4 = 12Practical Uses
Check if a number is odd:
if (n & 1) printf("odd\n");Set a bit (turn on bit at position
i):flags = flags | (1 << i);Clear a bit (turn off bit at position
i):flags = flags & ~(1 << i);Toggle a bit:
flags = flags ^ (1 << i);Check if a specific bit is set:
if (flags & (1 << i)) printf("bit %d is set\n", i);This pattern is common in systems programming, graphics (color channels), game states, and flags.
Loops
while
int i = 0; while (i < 5) { printf("%d\n", i); i++; }for
for (int i = 0; i < 5; i++) printf("%d\n", i);The Norminette forbids
forloops — usewhileinstead.do…while
Executes at least once before checking the condition:
int i = 0; do { printf("%d\n", i); i++; } while (i < 5);The Norminette also forbids
do…whileloops.Functions
Functions in C must be declared before they are used (either defined above
main, or via a prototype):int add(int a, int b); // prototype int main(void) { printf("%d\n", add(3, 4)); return (0); } int add(int a, int b) { return (a + b); }A function that returns nothing uses
void:void greet(char *name) { printf("Hello, %s\n", name); }Pointers
A pointer stores the memory address of another variable, not its value.
int x = 42; int *p = &x; // p holds the address of x printf("%d\n", x); // 42 — the value printf("%p\n", p); // 0x... — the address printf("%d\n", *p); // 42 — dereference: value at addressSyntax Meaning &xaddress of x *pvalue at address stored in p int *pdeclare p as a pointer to int Pointers are how C passes variables by reference to functions:
void increment(int *n) { *n = *n + 1; } int main(void) { int x = 5; increment(&x); printf("%d\n", x); // 6 }Arrays
An array is a contiguous block of memory holding multiple values of the same type:
int scores[5] = {10, 20, 30, 40, 50}; printf("%d\n", scores[0]); // 10 printf("%d\n", scores[4]); // 50The name of an array is a pointer to its first element:
int *p = scores; // equivalent to &scores[0] printf("%d\n", *p); // 10 printf("%d\n", *(p + 2)); // 30Strings
In C, a string is an array of
charending with a null terminator'\0':char name[] = "Sam"; // equivalent to: {'S', 'a', 'm', '\0'}Common string operations (from
<string.h>):#include <string.h> strlen(str); // length (not counting '\0') strcpy(dest, src); // copy src into dest strcat(dest, src); // append src to dest strcmp(s1, s2); // compare: 0 if equalA common exercise is to rewrite all of these yourself as
ft_strlen,ft_strcpy, etc.Pointer Arithmetic on Strings
char *s = "Hello"; while (*s) { write(1, s, 1); s++; }s++advances the pointer one byte — the next character.Dynamic Memory
Stack memory (local variables) is freed automatically. Heap memory must be managed manually with
mallocandfree.#include <stdlib.h> int *arr = malloc(5 * sizeof(int)); // allocate 5 ints on the heap if (!arr) return (1); // always check for NULL arr[0] = 10; arr[1] = 20; free(arr); // release the memory arr = NULL; // avoid dangling pointerRules:
- Every
mallocmust have a matchingfree - Never use memory after freeing it
- Never free the same pointer twice
2D Arrays with malloc
char **grid = malloc(3 * sizeof(char *)); int i = 0; while (i < 3) { grid[i] = malloc(4 * sizeof(char)); i++; } // ... use grid[row][col] ... // free each row, then the array itself i = 0; while (i < 3) { free(grid[i]); i++; } free(grid);Norminette
The Norminette is a strict code style checker. Key rules:
- Functions must be 25 lines maximum
- No more than 5 function parameters
- No
forloops (onlywhile) - Variable declarations at the top of the scope only
- 4-space tabs, specific brace placement
norminette my_file.c # check style compliance
Quick Reference
Concept Meaning gcc -Wall -Wextra -Werrorcompile with full warnings int *p = &xpointer to variable x *pdereference — value at address mallocallocate heap memory freerelease heap memory '\0'null terminator ending a string norminettestyle checker Created on July 2026 - Every
-
Shell Tips — Commands & Bash Scripting
The terminal is one of the most powerful tools in a developer’s toolkit. Whether you’re on Linux, macOS, or WSL on Windows, mastering a handful of Shell commands and understanding how to write Bash scripts can dramatically speed up your workflow.
Essential Commands
ls — List Directory Contents
lsshows what’s in the current directory. The flags make it genuinely useful:ls # basic list ls -l # long format: permissions, owner, size, date ls -a # show hidden files (starting with .) ls -la # combine both: long format + hidden filesThe
-lacombo is the one you’ll use 95% of the time. It gives you the full picture of a directory including dotfiles like.zshrc,.gitignore, etc.cd — Change Directory
cd /path/to/dir # absolute path cd Documents # relative path cd .. # go up one level cd ~ # go to home directory cd - # go back to previous directorycd -is underrated — it toggles between the last two directories you visited, great when you’re jumping between two project folders.cp — Copy Files or Directories
cp file.txt backup.txt # copy a file cp -r folder/ backup_folder/ # copy a directory recursively cp *.png ~/Pictures/ # copy all .png files to PicturesAlways use
-r(recursive) when copying directories, otherwisecpwill refuse.mv — Move or Rename
mv old_name.txt new_name.txt # rename a file mv file.txt ~/Documents/ # move a file mv folder/ ../other_folder/ # move a directorymvdoes double duty: moving and renaming are the same operation. There’s no dedicated rename command in Shell.chmod — Change File Permissions
Permissions control who can read, write, or execute a file. They apply to three groups: owner, group, and others.
chmod +x script.sh # make a file executable chmod 755 script.sh # rwxr-xr-x (owner full, others read+execute) chmod 644 file.txt # rw-r--r-- (owner read+write, others read only)There is also a symbolic mode using
+,-, and=to add, remove, or set permissions, andu(user/owner),g(group),o(others),a(all) to target who:chmod +x script.sh # give execute to everyone chmod -x script.sh # remove execute from everyone chmod +r file.txt # give read to everyone chmod -w file.txt # remove write from everyone chmod u+x script.sh # give execute to owner only chmod g-w file.txt # remove write from group chmod o-r file.txt # remove read from others chmod a+r file.txt # give read to all (same as +r) chmod u+rwx,go+rx file # owner full, group and others read+execute chmod o= file.txt # remove all permissions from othersSymbol Meaning +add permission -remove permission =set exact permissions (removes others) uowner (user) ggroup oothers aall (u+g+o) The numeric mode (
755,644) is the most common in practice. Each digit is a sum of three base values:Value Permission 4 read (r) 2 write (w) 1 execute (x) Add them together to combine permissions:
Value Permissions 7 rwx (4+2+1) 6 rw- (4+2) 5 r-x (4+1) 4 r— (4) 0 --- alias — Create Command Shortcuts
aliaslets you define a shortcut for any command or chain of commands:alias ll="ls -la" alias gs="git status" alias ..="cd .." alias ...="cd ../.."The problem: aliases defined in the terminal only last for the current session.
Making Aliases Permanent with .zshrc
To persist aliases across sessions, add them to your shell config file. For Zsh (default on macOS and most modern Linux distros), that’s
~/.zshrc:# Open .zshrc with your editor nano ~/.zshrc # Add your aliases at the bottom alias ll="ls -la" alias gs="git status" alias gp="git push" alias ..="cd .." # Reload the config without restarting the terminal source ~/.zshrcFor Bash users the file is
~/.bashrcinstead.
Writing a Bash Script
A Bash script is a plain text file containing a sequence of Shell commands. The first line — the shebang — tells the OS which interpreter to use.
The Shebang Line
#!/bin/bashThis must be the very first line of the file. It points to the Bash binary, so the script runs with Bash regardless of what shell the user has active.
A Practical Example
Here’s a script that creates a timestamped backup of a directory:
#!/bin/bash SOURCE="$HOME/Projects" DEST="$HOME/Backups" TIMESTAMP=$(date +"%Y-%m-%d_%H-%M") BACKUP_NAME="backup_$TIMESTAMP" mkdir -p "$DEST/$BACKUP_NAME" cp -r "$SOURCE/" "$DEST/$BACKUP_NAME/" echo "Backup created: $DEST/$BACKUP_NAME"Save it as
backup.sh, then make it executable and run it:chmod +x backup.sh ./backup.shKey Script Concepts
Variables — no spaces around
=:NAME="Sam" echo "Hello, $NAME"Conditionals:
if [ -f "file.txt" ]; then echo "File exists" else echo "File not found" fiLoops:
for file in *.txt; do echo "Processing $file" doneFunctions:
greet() { echo "Hello, $1" } greet "Sam"
Quick Reference
Command Purpose lsList directory contents cdChange directory cpCopy files or directories mvMove or rename files chmodChange file permissions aliasCreate a command shortcut source ~/.zshrcReload shell config #!/bin/bashShebang — declare Bash interpreter Created on July 2026