C

C Cheat Sheet

The language behind operating systems, embedded devices, and most intro "systems" courses. Compiling, variables, pointers, structs, and manual memory management โ€” with copy-paste examples and live Run/Submit exercises.

Jump to: Your first programCompilingVariables & types Format specifiersprintf / scanfOperators Math functionsif / elseswitch Logical operatorsLoopsbreak & continue Nested loopsRandom numbersArrays 2D arraysStringsFunctions Variable scopestatic variablesTernary operator typedefEnumsPointers Pointer arithmeticDouble pointersvoid pointers StructsStruct memory layoutUnions Stack vs. heapDynamic memoryGarbage collection File I/OHeader guardsMulti-file programs ๐Ÿš€ Practice problems

Your first program

Every C program starts execution at main(). Save this as hello.c.

#include <stdio.h>

int main() {
    printf("Hello, world!\n");
    // single-line comment
    /* multi-line
       comment */
    return 0;
}
๐Ÿ“ #include <stdio.h> is a preprocessor directive โ€” it pastes in the standard I/O library before compiling, which is where printf/scanf come from. Your program won't compile without a main().

Compiling

Unlike Python, C is compiled before it runs โ€” a compiler turns your .c file into a native executable.

# compile hello.c into a program called "hello"
gcc hello.c -o hello

# run it
./hello
๐Ÿ“ The exit code from main (usually return 0;) tells the operating system whether your program succeeded. 0 = success, anything else = an error code.

Variables & types

Unlike Python, C is statically typed โ€” you must declare each variable's type, and it can't change.

int age = 25;             // whole numbers, ~4 bytes
float price = 9.99f;       // single-precision decimal, 4 bytes
double pi = 3.14159265;    // double-precision decimal, 8 bytes โ€” use this by default
char grade = 'A';           // ONE character, 1 byte (single quotes)
char name[30] = "";      // C's stand-in for a "string" type (double quotes)

#include <stdbool.h>         // C99+: gives you a real bool
bool is_online = true;    // true=1, false=0
๐Ÿ“ An uninitialized variable holds garbage โ€” whatever bytes happened to already be in that memory. Always initialize (0, 0.0f, '\0', or an empty string) rather than trust a "default." Sizes are machine-dependent-ish โ€” use sizeof(x) to check.

Format specifiers

printf's % codes control exactly how a value gets displayed, and can be dressed up with width, padding, sign, and precision modifiers.

printf("%d\n", 42);        // int
printf("%f\n", 3.14);      // float/double
printf("%c\n", 'A');       // char
printf("%s\n", "hi");      // string

printf("%3d\n", 7);        // "  7"  โ€” min width 3, right-justified
printf("%-3d|\n", 7);      // "7  |" โ€” left-justified
printf("%03d\n", 7);       // "007"  โ€” zero-padded
printf("%+d\n", 7);        // "+7"   โ€” force the sign
printf("%.2f\n", 3.14159);  // "3.14" โ€” 2 digits after the decimal, rounds

printf / scanf

printf prints with format specifiers that must match your variable's type. scanf reads input the same way, but needs the variable's address.

int age;
char grade;
char name[30];

printf("Enter your age: ");
scanf("%d", &age);            // & = "address of" โ€” scanf writes THROUGH a pointer

printf("Enter your grade: ");
scanf(" %c", &grade);         // leading space skips a leftover '\n' in the buffer

printf("Enter your name: ");
fgets(name, sizeof(name), stdin);   // reads a WHOLE line, spaces included
name[strlen(name) - 1] = '\0';    // fgets keeps the '\n' โ€” strip it (needs <string.h>)
โš ๏ธ scanf stops at the first whitespace, so it can't read a full name with a space in it โ€” use fgets for that. And every scanf leaves the trailing \n sitting in the input buffer, which will silently get "read" by the next %c or fgets unless you skip it (a leading space in the format string, or a throwaway getchar()).

Operators

int a = 7, b = 2;
printf("%d\n", a + b);   // 9
printf("%d\n", a / b);   // 3  โ€” integer division truncates!
printf("%d\n", a % b);   // 1  โ€” remainder (modulus)
printf("%f\n", a / (float)b); // 3.5 โ€” cast one side to get real division

int count = 0;
count++;               // increment: count = count + 1
count--;               // decrement: count = count - 1
count += 5;             // augmented assignment: count = count + 5
count *= 2;             // count = count * 2

Math functions

<math.h> adds the functions printf/operators can't do alone.

#include <math.h>

printf("%f\n", sqrt(16));      // 4.0
printf("%f\n", pow(2, 10));    // 1024.0 โ€” 2 to the 10th
printf("%f\n", round(4.5));    // 5.0
printf("%f\n", ceil(4.1));     // 5.0 โ€” rounds up
printf("%f\n", floor(4.9));    // 4.0 โ€” rounds down
โš ๏ธ Forgetting #include <math.h> gives an "implicit declaration" warning โ€” the compiler guesses a signature and things can silently misbehave.

if / else

int age = 20;
if (age >= 18) {
    printf("Adult\n");
} else if (age >= 13) {
    printf("Teen\n");
} else {
    printf("Child\n");
}
โš ๏ธ Conditions are checked top-to-bottom and the first true branch wins โ€” put narrower conditions first. Checking age >= 18 before age >= 65 means your "senior" branch can never trigger, since every senior is already caught by the adult check.

switch

A cleaner alternative to a long if/else if chain when you're comparing one variable against a fixed set of values.

int day = 3;
switch (day) {
    case 1: printf("Monday\n"); break;
    case 2: printf("Tuesday\n"); break;
    case 3: printf("Wednesday\n"); break;
    default: printf("Invalid day\n");
}
โš ๏ธ Forgetting break "falls through" into the next case โ€” execution just keeps going instead of stopping. Sometimes that's intentional, but usually it's a bug. Works with int or char values.

Logical operators

int temp = 22;
if (temp > 0 && temp < 30) {     // AND โ€” BOTH must be true
    printf("Comfortable\n");
}

bool is_weekend = false;
if (is_weekend || temp > 28) {   // OR โ€” AT LEAST ONE true
    printf("Relax today\n");
}

if (!is_weekend) {              // NOT โ€” flips true/false
    printf("Back to work\n");
}

Loops

// for loop โ€” most common for a known number of iterations
for (int i = 0; i < 5; i++) {
    printf("%d\n", i);
}

// while loop โ€” checks the condition BEFORE each run, open-ended
int n = 3;
while (n > 0) {
    printf("%d\n", n);
    n--;
}

// do-while โ€” checks the condition AFTER, so it always runs at least once
int choice;
do {
    printf("Enter 1-3: ");
    scanf("%d", &choice);
} while (choice < 1 || choice > 3);
๐Ÿ“ do-while is the natural fit for "reprompt until valid" input loops, since the body has to run once before there's anything to check.

break & continue

for (int i = 0; i < 10; i++) {
    if (i == 5) break;      // stop the loop entirely
    if (i % 2 == 0) continue; // skip just this iteration
    printf("%d\n", i);         // prints 1, 3
}

Nested loops

A loop inside another loop โ€” the inner counter is conventionally named j so it doesn't collide with the outer i.

// multiplication table, 1-5
for (int i = 1; i <= 5; i++) {
    for (int j = 1; j <= 5; j++) {
        printf("%4d", i * j);   // %4d keeps columns aligned
    }
    printf("\n");
}

Random numbers

#include <stdlib.h>
#include <time.h>

srand(time(NULL));            // seed once, using the current time
int roll = 1 + (rand() % 6);   // 1-6: min + (rand() % (max-min+1))
printf("%d\n", roll);
โš ๏ธ Without srand(time(NULL)), rand() produces the exact same sequence of numbers every time you run the program โ€” it defaults to seed 1.

Arrays

A fixed-size, same-type block of memory. C does not check array bounds for you โ€” reading/writing past the end is undefined behavior.

int nums[5] = {10, 20, 30, 40, 50};
printf("%d\n", nums[0]);      // 10
nums[1] = 99;              // modify in place

int len = sizeof(nums) / sizeof(nums[0]);  // classic way to get array length
for (int i = 0; i < len; i++) {
    printf("%d\n", nums[i]);
}

int scores[5] = {0};       // zero-fill every element (leaving one out zeros ALL of them)

2D arrays

An array of arrays โ€” think rows and columns. Accessed with two indices, arr[row][col].

int grid[3][3] = {
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
};

for (int row = 0; row < 3; row++) {
    for (int col = 0; col < 3; col++) {
        printf("%d ", grid[row][col]);
    }
    printf("\n");
}
๐Ÿ“ The column count is required even when the row count can be inferred from the initializer list.

Strings

C has no string type โ€” a string is just a char array ending with a hidden '\0' (null terminator). String functions live in <string.h>.

#include <string.h>

char name[20] = "Mo";
printf("%s\n", name);            // Mo
printf("%lu\n", strlen(name));  // 2 โ€” length, not counting '\0'

strcat(name, " Salah");       // append โ€” make sure the array is big enough!
printf("%s\n", name);            // Mo Salah

if (strcmp(name, "Mo Salah") == 0) {  // strcmp returns 0 when equal
    printf("Match!\n");
}

// an "array of strings" is really a 2D char array
char fruits[3][10] = {"apple", "banana", "coconut"};
printf("%s\n", fruits[1]);       // banana

Functions

Every function declares its return type and each parameter's type. A prototype up top lets you define the real function body after main().

int add(int a, int b);      // prototype: return type + name + param types + ;

int main() {
    printf("%d\n", add(2, 3));  // 5 โ€” C reads top-down, so it needs to know add() exists first
    return 0;
}

int add(int a, int b) {      // full definition, can live below main()
    return a + b;
}

void greet(char *name) {   // void = returns nothing
    printf("Hi, %s!\n", name);
}
๐Ÿ“ Arguments are what you send in when calling; parameters are what the function declares to receive. Functions can't see each other's local variables โ€” you have to pass everything in explicitly.

Variable scope

int counter = 0;          // GLOBAL โ€” visible to every function, but avoid these when you can

void example() {
    int local_x = 5;    // LOCAL โ€” only exists inside this { } block
}   // local_x is gone here
โš ๏ธ Two variables with the same name can't coexist in the same scope, but they're fine in different functions โ€” each function is its own sealed room. Globals are tempting but make bugs harder to track down, since any function can silently change them; they're mostly fine for constants.

static variables

A static local variable is initialized only once and then persists for the entire life of the program between calls โ€” unlike a normal local, which is recreated (and reset) every time its function runs.

void counter() {
    static int x = 0;   // set up ONCE, ever โ€” not on every call
    x++;
    printf("%d\n", x);
}

int main() {
    counter(); counter(); counter();   // prints 1, 2, 3 โ€” NOT 1, 1, 1
    return 0;
}
๐Ÿ“ Three different variable "lifetimes" in C: automatic (normal locals โ€” die when their { } block ends), static (lives for the whole program, but is still only visible inside the function that declared it), and dynamic (heap memory from malloc โ€” lives until you free it).

Ternary operator

A one-line shorthand for a simple if/else: condition ? valueIfTrue : valueIfFalse.

int age = 20;
char *status = (age >= 18) ? "adult" : "minor";
printf("%s\n", status);   // adult

int n = 7;
printf("%s\n", (n % 2 == 0) ? "even" : "odd");

typedef

Gives an existing type a new nickname, which can make complex declarations more readable.

typedef char String[50];   // now "String" works instead of "char[50]"

String name = "Ada";
printf("%s\n", name);

Enums

A named set of integer constants โ€” clearer than a pile of "magic numbers". Values auto-increment from 0 unless you assign your own.

typedef enum { SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY } Day;

Day today = WEDNESDAY;
if (today == SATURDAY || today == SUNDAY) {
    printf("Weekend!\n");
} else {
    printf("Weekday, day #%d\n", today);  // 3
}

// you can also assign your OWN values instead of auto-incrementing from 0
typedef enum {
    HTTP_BAD_REQUEST = 400,
    HTTP_NOT_FOUND   = 404,
    HTTP_TEAPOT      = 418
} HttpStatus;
๐Ÿ“ A switch is the natural partner for an enum โ€” but C gives no warning if you forget a case (unlike some other languages' exhaustiveness checks), so always keep a default.

Pointers

C's defining feature. A pointer stores a memory address instead of a value. & = "address of", * = "value at this address" (dereference).

int x = 10;
int *p = &x;             // p holds the ADDRESS of x
printf("%d\n", *p);     // 10 โ€” dereference: "the value AT that address"
*p = 20;                 // change x THROUGH the pointer
printf("%d\n", x);      // 20 โ€” x actually changed!

// this is how functions modify their caller's variables (C passes by value otherwise)
void increment(int *n) {
    (*n)++;
}
// calling increment(&x) actually changes x in main
๐Ÿ“ A pointer with no valid address is a "dangling" or "wild" pointer โ€” dereferencing it crashes your program. Set unused pointers to NULL and check before using: if (p != NULL) { ... }.

Pointer arithmetic

An array name decays into a pointer to its first element โ€” arr[i] is really just shorthand for *(arr + i), with the compiler auto-scaling the offset by the element's size.

int nums[5] = {10, 20, 30, 40, 50};
printf("%d\n", nums[2]);      // 30
printf("%d\n", *(nums + 2));   // 30 โ€” the exact same thing
โš ๏ธ Array decay: inside the function where an array is declared, sizeof(arr) gives its true byte size. But once you pass that array into another function, it decays to a bare pointer โ€” sizeof there only reports the pointer's own size (e.g. 8), silently losing the element count. Always pass the length alongside the array.

Double pointers

A pointer can point to another pointer โ€” same address/value idea, just one extra hop. Useful when a function needs to change which address the caller's pointer holds, not just the value at that address.

void allocate_int(int **out, int value) {
    int *fresh = (int *) malloc(sizeof(int));
    if (fresh == NULL) return;
    *fresh = value;
    *out = fresh;        // updates the CALLER's pointer variable itself
}

int main() {
    int *p = NULL;
    allocate_int(&p, 42);   // pass the ADDRESS of the pointer
    printf("%d\n", *p);   // 42
    free(p);
    return 0;
}
โš ๏ธ Inside allocate_int, the new value must be malloc'd (heap), not a plain local variable โ€” a local lives on the stack and disappears the moment the function returns, leaving *out pointing at freed memory.

void pointers

void* is a generic pointer โ€” it discards all type information, so you must cast it back to the correct type yourself before dereferencing. It's how C fakes "generic" containers that can hold any type.

void print_as(void *data, int is_float) {
    if (is_float) printf("%f\n", *(float *)data);
    else          printf("%d\n", *(int *)data);
}

int main() {
    int whole = 7;
    float decimal = 3.14f;
    print_as(&whole, 0);     // 7
    print_as(&decimal, 1); // 3.140000
    return 0;
}
โš ๏ธ Nothing stops you from casting a void* to the wrong type โ€” the compiler trusts you completely, and reading it back as the wrong type gives garbage instead of an error. This flexibility is also how you'd write a type-agnostic swap(void *a, void *b, size_t size): malloc a temporary buffer of size bytes and shuffle the three "hands" with memcpy instead of a typed temp variable.

Structs

A struct groups related variables of different types into one custom type โ€” C's version of a simple object.

struct Point {
    int x;
    int y;
};

int main() {
    struct Point p1 = {3, 4};
    printf("(%d, %d)\n", p1.x, p1.y);   // (3, 4)

    struct Point *ptr = &p1;
    printf("%d\n", ptr->x);          // -> is shorthand for (*ptr).x

    struct Point points[2] = { {0,0}, {5,5} };  // an array of structs
    printf("%d\n", points[1].x);    // 5
    return 0;
}

Struct memory layout

Struct fields sit contiguously in memory in the order you declare them โ€” but sizeof(struct) is often bigger than the sum of its fields, because the compiler inserts invisible padding so each field lands on an aligned address (faster for the CPU to read).

struct Wasteful {   // char(1) + padding(3) + int(4) + char(1) + padding(3) = 12 bytes
    char flag;
    int id;
    char letter;
};

struct Tight {      // int(4) + char(1) + char(1) + padding(2) = 8 bytes
    int id;
    char flag;
    char letter;
};

printf("%lu\n", sizeof(struct Wasteful));  // 12
printf("%lu\n", sizeof(struct Tight));      // 8
๐Ÿ“ Ordering fields largest-to-smallest usually minimizes wasted padding. Exact byte counts are compiler/platform-dependent โ€” always measure with sizeof rather than assume.

Unions

A union holds one of several possible fields at a time, and all of them share the same memory โ€” its size equals its largest member, not the sum of all of them. Set one field, then read a different one, and you reinterpret the same raw bytes as the new type.

typedef enum { KIND_INT, KIND_STRING } Kind;

typedef union {
    int as_int;
    char *as_string;
} Value;

typedef struct {
    Kind kind;
    Value data;
} Object;

Object o;
o.kind = KIND_INT;
o.data.as_int = 42;
printf("%d\n", o.data.as_int);   // 42

printf("%lu\n", sizeof(Value));   // size of its LARGEST member, not both added together
โš ๏ธ A union tagged with a Kind-style field (as above) is the safe, common pattern โ€” the tag tells you which member is actually valid right now. Reading the "wrong" field for what was last set (e.g. writing a signed value and reading it back as unsigned) reinterprets the same bits and can produce wildly different-looking numbers.

Stack vs. heap

Program memory splits into regions. The stack is fast and automatic: every function call pushes a "frame" holding its locals, and returning pops (frees) it โ€” last in, first out. The heap is manually managed (via malloc/free) for data that must outlive the function that created it, or whose size isn't known until runtime.

// STACK: destroyed the moment the function returns
void make_local() {
    int temp = 99;    // lives only inside this function call
}

// HEAP: survives after the function returns โ€” caller owns it now
int *make_heap_int(int value) {
    int *p = (int *) malloc(sizeof(int));
    if (p != NULL) *p = value;
    return p;     // caller is now responsible for free()-ing this
}
โš ๏ธ Returning the address of a local (e.g. return &temp;) is a classic bug โ€” the address still "looks" valid and prints fine, but the stack frame it pointed into is gone, and the next function call will silently reuse and overwrite that same memory. This is also where the phrase "stack overflow" comes from: allocating something too large on the stack (e.g. a huge local array) can exhaust the space set aside for it.

Dynamic memory

C has no garbage collector โ€” memory you allocate, you must free, or it leaks for the life of the program.

#include <stdlib.h>

int *arr = (int *) malloc(5 * sizeof(int));  // room for 5 ints, on the heap
if (arr == NULL) {                        // malloc returns NULL if it fails
    printf("Out of memory\n");
    return 1;
}
for (int i = 0; i < 5; i++) arr[i] = i * i;
printf("%d\n", arr[3]);   // 9
free(arr);                    // give the memory back โ€” every malloc needs a matching free
arr = NULL;                     // good habit: avoid an accidental dangling pointer

// calloc: like malloc but zero-initializes every byte (slightly slower, fewer bugs)
int *zeros = (int *) calloc(5, sizeof(int));

// realloc: resize a previous allocation โ€” check a TEMP pointer first!
int *temp = (int *) realloc(zeros, 10 * sizeof(int));
if (temp != NULL) zeros = temp;   // only overwrite the original once you know it worked
free(zeros);
โš ๏ธ If realloc fails it returns NULL โ€” assigning straight into your original pointer (zeros = realloc(zeros, ...)) would then lose your only reference to the original block, leaking it forever. Always reassign through a temporary pointer first.

Garbage collection concepts

C never collects garbage for you โ€” but it's worth knowing the two classic strategies other languages use under the hood, since the ideas show up any time you're managing shared ownership yourself.

โš ๏ธ Reference counting's blind spot: a cycle (object A references B, and B references A back) means neither count ever reaches 0, even once nothing outside the cycle points to either โ€” a permanent leak that simple ref-counting can't detect. Mark-and-sweep doesn't have this problem, since it only cares whether an object is reachable from a root, not how many things point to it.

File I/O

FILE *f = fopen("notes.txt", "w");   // "w"=write, "r"=read, "a"=append
if (f != NULL) {
    fprintf(f, "Score: %d\n", 95);
    fclose(f);                     // always close what you open
}

char line[100];
f = fopen("notes.txt", "r");
if (f != NULL) {
    while (fgets(line, 100, f)) {
        printf("%s", line);
    }
    fclose(f);
}

Header guards

#include is literal text substitution โ€” it pastes the target file's contents in place. If the same header ever gets included twice (directly or through another header), you get duplicate-definition errors. A header guard fixes that by only letting the contents through once per compile.

// mymath.h
#ifndef MYMATH_H
#define MYMATH_H

int square(int n);

#endif
๐Ÿ“ Most modern compilers also support a one-line shortcut: #pragma once at the top of the header, doing the same job as the #ifndef/#define/#endif trio above.

Multi-file programs

Real programs get split across a .c file (the actual code) and a matching .h file (just the function signatures, so other files know what's available). Each .c file compiles independently into an object file, then a final step links the object files together into one executable.

// mymath.h โ€” the public interface
#pragma once
int square(int n);

// mymath.c โ€” the implementation
#include "mymath.h"
int square(int n) { return n * n; }

// main.c
#include <stdio.h>
#include "mymath.h"
int main() { printf("%d\n", square(5)); return 0; }
# compile each .c into an object file, then link them together
gcc -c mymath.c        # -> mymath.o
gcc -c main.c          # -> main.o
gcc mymath.o main.o -o program

# or the one-line shortcut for small projects
gcc main.c mymath.c -o program

๐Ÿš€ Practice problems

Full runnable programs. Click a box below (or hit its โ–ธ Solve button) to edit, run, and submit against the reference output โ€” same workspace as the Python cheat sheet, just compiled and run in a real C sandbox instead of your browser.

Sum of an array EASY

Print the sum of a fixed array of integers.

#include <stdio.h>

int main() {
    int nums[] = {4, 8, 15, 16, 23, 42};
    int len = sizeof(nums) / sizeof(nums[0]);
    int sum = 0;
    for (int i = 0; i < len; i++) {
        sum += nums[i];
    }
    printf("%d\n", sum);
    return 0;
}

Reverse a string EASY

Print a string reversed, in place, using two pointers.

#include <stdio.h>
#include <string.h>

void reverse(char *s) {
    int i = 0, j = strlen(s) - 1;
    while (i < j) {
        char tmp = s[i];
        s[i] = s[j];
        s[j] = tmp;
        i++; j--;
    }
}

int main() {
    char word[] = "pointers";
    reverse(word);
    printf("%s\n", word);
    return 0;
}

Shopping cart EASY

Read an item name, a price, and a quantity, then print the formatted total.

#include <stdio.h>

int main() {
    char item[30] = "Coffee";
    float price = 4.5f;
    int qty = 3;

    float total = price * qty;
    printf("%d x %s @ $%.2f = $%.2f\n", qty, item, price, total);
    return 0;
}

Weight converter EASY

Menu-driven: convert kilograms to pounds, or pounds to kilograms.

Temperature converter EASY

Convert between Celsius and Fahrenheit based on a character flag.

Prime number checker EASY

Check whether a number is prime using a simple divisor loop.

Leap year checker EASY

Check a list of years to see which ones are leap years.

Even/odd counter EASY

Count how many numbers in a fixed array are even versus odd.

Vowel counter EASY

Count the vowels in a sentence, handling both upper and lower case.

Fibonacci sequence (iterative) EASY

Print the first 10 numbers of the Fibonacci sequence using a loop.

Power function (manual) EASY

Raise a number to a power using a loop instead of pow().

Digit sum EASY

Add up the individual digits of a number.

Reverse a number's digits EASY

Reverse the digits of an integer using modulo and division.

Triangle type classifier EASY

Classify a triangle as equilateral, isosceles, or scalene given three side lengths.

Decimal to binary converter EASY

Convert a decimal integer to its binary representation using an array.

Circle & sphere calculator MEDIUM

Given a radius, compute circle area and sphere surface area & volume.

#include <stdio.h>
#include <math.h>

const double PI = 3.14159265;

int main() {
    double r = 4.0;

    double circle_area = PI * pow(r, 2);
    double sphere_surface = 4 * PI * pow(r, 2);
    double sphere_volume = (4.0 / 3.0) * PI * pow(r, 3);

    printf("Circle area: %.2f\n", circle_area);
    printf("Sphere surface: %.2f\n", sphere_surface);
    printf("Sphere volume: %.2f\n", sphere_volume);
    return 0;
}

Compound interest calculator MEDIUM

Implement A = P(1 + r/n)^(nt) for a given principal, rate, years, and compounding frequency.

#include <stdio.h>
#include <math.h>

int main() {
    double principal = 1000;
    double rate = 5 / 100.0;    // 5% as a decimal
    double years = 10;
    int n = 12;                // compounded monthly

    double amount = principal * pow(1 + rate / n, n * years);
    printf("Final amount: $%.2f\n", amount);
    return 0;
}

Factorial with recursion MEDIUM

Compute 10! using a recursive function.

#include <stdio.h>

long factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

int main() {
    printf("%ld\n", factorial(10));
    return 0;
}

Swap two numbers with pointers MEDIUM

Write a function that swaps two integers by taking pointers to them (the only way to do it in C).

Calculator MEDIUM

Read two numbers and an operator, and switch on the operator โ€” guarding against divide-by-zero.

Rock, paper, scissors MEDIUM

Three functions working together: a computer pick, a user pick, and a winner check.

Number guessing game MEDIUM

Loop until a fixed secret number is guessed, with too-high/too-low hints and a try counter.

Bubble sort MEDIUM

Sort an array of integers in ascending order using bubble sort.

Linear search MEDIUM

Find the index of a target value in an unsorted array.

Binary search MEDIUM

Search a sorted array for a target value by repeatedly halving the range.

GCD and LCM (Euclidean algorithm) MEDIUM

Compute the greatest common divisor and least common multiple of two numbers.

Matrix transpose MEDIUM

Transpose a 2D matrix by swapping rows and columns.

Array rotation MEDIUM

Rotate an array's elements to the left by a given number of positions.

Pascal's triangle MEDIUM

Print the first several rows of Pascal's triangle.

Character frequency counter MEDIUM

Count how often each letter appears in a string using an array as a lookup table.

Struct array + dynamic memory HARD

Build a small array of structs on the heap, fill it, print it, then free it โ€” the full malloc/struct/free lifecycle.

#include <stdio.h>
#include <stdlib.h>

struct Student {
    char name[20];
    int score;
};

int main() {
    int n = 3;
    struct Student *students = (struct Student *) malloc(n * sizeof(struct Student));

    snprintf(students[0].name, 20, "Ada");   students[0].score = 92;
    snprintf(students[1].name, 20, "Linus"); students[1].score = 88;
    snprintf(students[2].name, 20, "Grace"); students[2].score = 95;

    int total = 0;
    for (int i = 0; i < n; i++) {
        printf("%s: %d\n", students[i].name, students[i].score);
        total += students[i].score;
    }
    printf("Average: %d\n", total / n);

    free(students);
    return 0;
}

Banking program HARD

Three functions sharing a running balance: check, deposit, and withdraw, with validation.

#include <stdio.h>

void checkBalance(float balance) {
    printf("Balance: $%.2f\n", balance);
}

float deposit(float amount) {
    if (amount < 0) { printf("Invalid deposit\n"); return 0; }
    return amount;
}

float withdraw(float balance, float amount) {
    if (amount > balance) { printf("Insufficient funds\n"); return 0; }
    return amount;
}

int main() {
    float balance = 100;
    balance += deposit(50);
    balance -= withdraw(balance, 30);
    checkBalance(balance);   // $120.00
    return 0;
}

Quiz game HARD

Parallel arrays for questions and answers, scored by comparing an uppercased guess to an answer key.

#include <stdio.h>
#include <ctype.h>

int main() {
    char questions[][60] = {
        "What color is the sky? A) Blue B) Green",
        "How many days in a week? A) 5 B) 7"
    };
    char answer_key[] = {'A', 'B'};
    char user_guesses[] = {'a', 'b'};   // simulated, lowercase on purpose
    int question_count = sizeof(answer_key) / sizeof(answer_key[0]);
    int score = 0;

    for (int i = 0; i < question_count; i++) {
        printf("%s\n", questions[i]);
        char guess = toupper(user_guesses[i]);
        if (guess == answer_key[i]) score++;
    }
    printf("Score: %d/%d\n", score, question_count);
    return 0;
}

Digital clock HARD

Pull the current time from the system clock and format it with zero-padded, arrow-operator struct access.

Palindrome checker (case-insensitive) HARD

Check whether a word reads the same backward as forward, ignoring case.

Anagram checker HARD

Check whether two words are anagrams of each other using letter-count arrays.

Caesar cipher HARD

Encrypt a message by shifting each letter by a fixed amount, wrapping within the alphabet.

Statistics calculator HARD

Compute the mean, median, and mode of a fixed array of numbers.

Stack simulation with an array HARD

Implement push and pop operations for a stack using a fixed-size array.

Queue simulation with an array HARD

Implement enqueue and dequeue operations for a circular queue using a fixed-size array.

Student record search HARD

Search an array of student structs by name and print the matching record.

Inventory system HARD

Track item quantities and prices in a struct array and compute total inventory value.

Grade calculator with structs HARD

Average each student's scores and assign a letter grade using a helper function.

Traffic light state machine HARD

Model a traffic light cycling through states using a typedef'd enum.

Union-based type tag demo HARD

Store different value types in one struct using a union with a type tag to know which member is active.