👑 Premium Sign in Sign up

Language lab · Fundamentals

🔧 C

Concept → trace → write code → run tests → explain why → improve → mini-project. Start in the browser if you like; set up your own computer when you are ready.

💻 Runs on your computerOfficial download & docs ↗

📖 Topics with real code (38)

Every verified output below was produced by running the code with gcc (GCC) 14.4.0 on 6 Nov 2026. Open a topic to edit the code and run it right here in your browser.

🌱 Beginner (10)

Variables and data types

C types have a fixed size in memory: int, long, float, double, char, and bool from stdbool.h. printf needs the right format: %d int, %ld long, %f double, %c char. sizeof shows the bytes.

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

int main(void) {
    int age = 15;
    long population = 1428000000L;
    double height = 1.62;
    bool is_student = true;
    char grade = 'A';
    const char *name = "Asha";
    printf("%s %d %.2f %d %c\n", name, age, height, is_student, grade);
    printf("population %ld\n", population);
    printf("sizes: int=%zu double=%zu char=%zu bool=%zu\n", sizeof(int), sizeof(double), sizeof(char), sizeof(bool));
    int marks = atoi("88") + 2;
    double avg = (double)7 / 2;
    printf("%d %.1f %d %d\n", marks, avg, 7 / 2, (int)9.99);
    return 0;
}

✅ Verified output

Asha 15 1.62 1 A
population 1428000000
sizes: int=4 double=8 char=1 bool=1
90 3.5 3 9
Operators and boolean logic

In C, 0 is false and any other number is true. && and || short-circuit. Integer division drops the fraction; % is the remainder; bitwise & | ^ << work on bits.

#include <stdio.h>

int main(void) {
    int a = 17, b = 5;
    printf("%d %d %d %d %d\n", a + b, a - b, a * b, a / b, a % b);
    int has_ticket = 1, is_vip = 0;
    printf("enter: %d either: %d not: %d\n", has_ticket && !is_vip, has_ticket || is_vip, !has_ticket);
    printf("6&3=%d 6|3=%d 6^3=%d 1<<4=%d\n", 6 & 3, 6 | 3, 6 ^ 3, 1 << 4);
    int x = 5;
    x += 3; x *= 2; x++;
    printf("x=%d, 10 > 3 is %d\n", x, 10 > 3);
    return 0;
}

✅ Verified output

22 12 85 3 2
enter: 1 either: 1 not: 0
6&3=2 6|3=7 6^3=5 1<<4=16
x=17, 10 > 3 is 1
Conditionals: if, else, switch

if/else picks a branch; switch jumps to a matching case and needs break to stop falling through; ?: is a short if/else.

#include <stdio.h>

int main(void) {
    int marks = 78;
    const char *grade;
    if (marks >= 90) grade = "A+";
    else if (marks >= 75) grade = "A";
    else if (marks >= 35) grade = "Pass";
    else grade = "Fail";
    printf("Grade: %s\n", grade);

    int day = 6;                     /* 0 = Sunday */
    switch (day) {
        case 0:
        case 6:
            printf("Weekend\n");
            break;
        default:
            printf("School day\n");
    }
    printf("%s\n", marks >= 35 ? "passed" : "try again");
    return 0;
}

✅ Verified output

Grade: A
Weekend
passed
Loops: for, while, do-while

for counts, while checks first, do-while checks last so it runs at least once. break and continue control the loop.

#include <stdio.h>

int main(void) {
    for (int i = 1; i <= 5; i++) {
        if (i == 3) continue;
        printf("for %d\n", i);
    }
    int n = 10;
    while (n > 0) {
        if (n < 4) break;
        printf("while %d\n", n);
        n -= 3;
    }
    int k = 100;
    do { printf("do-while runs once: %d\n", k); } while (k < 0);
    for (int r = 1; r <= 3; r++) {
        for (int c = 1; c <= 3; c++) printf("%3d", r * c);
        printf("\n");
    }
    return 0;
}

✅ Verified output

for 1
for 2
for 4
for 5
while 10
while 7
while 4
do-while runs once: 100
  1  2  3
  2  4  6
  3  6  9
Functions and pass-by-value

A function needs a declared return type and parameter types. C passes copies of values, so a function cannot change the caller's variable unless it gets a pointer.

#include <stdio.h>

int area(int length, int width) { return length * width; }
double average(const int nums[], int count) {
    int sum = 0;
    for (int i = 0; i < count; i++) sum += nums[i];
    return (double)sum / count;
}
void try_change(int v) { v = 99; }
void really_change(int *v) { *v = 99; }

int main(void) {
    int nums[] = {4, 9, 2, 7};
    printf("area %d, average %.2f\n", area(5, 3), average(nums, 4));
    int x = 1;
    try_change(x);
    printf("after copy: %d\n", x);
    really_change(&x);
    printf("after pointer: %d\n", x);
    return 0;
}

✅ Verified output

area 15, average 5.50
after copy: 1
after pointer: 99
Integer sizes, limits and overflow

Each integer type has a fixed size. Going past the maximum of an unsigned type wraps around; for signed types it is undefined behaviour, so check before adding.

#include <stdio.h>
#include <limits.h>
#include <stdint.h>
int main(void) {
    printf("char %zu, short %zu, int %zu, long long %zu bytes\n", sizeof(char), sizeof(short), sizeof(int), sizeof(long long));
    printf("INT_MAX = %d, INT_MIN = %d\n", INT_MAX, INT_MIN);
    unsigned char u = 250;
    u += 10;                                   /* wraps: 260 - 256 */
    printf("unsigned char 250 + 10 = %u\n", u);
    int a = INT_MAX, b = 1;
    if (a > INT_MAX - b) printf("a + b would overflow: refused\n");
    uint32_t big = 4000000000u;
    printf("uint32_t holds %u, int64_t holds %lld\n", big, (long long)INT64_MAX);
    return 0;
}

✅ Verified output

char 1, short 2, int 4, long long 8 bytes
INT_MAX = 2147483647, INT_MIN = -2147483648
unsigned char 250 + 10 = 4
a + b would overflow: refused
uint32_t holds 4000000000, int64_t holds 9223372036854775807
Booleans with stdbool.h

C has no built-in true/false keywords before C23; include stdbool.h. Any non-zero value counts as true in a condition.

#include <stdio.h>
#include <stdbool.h>
bool is_leap(int y) { return (y % 4 == 0 && y % 100 != 0) || y % 400 == 0; }
int main(void) {
    int years[] = {1900, 2000, 2024, 2025};
    for (int i = 0; i < 4; i++)
        printf("%d leap? %s\n", years[i], is_leap(years[i]) ? "true" : "false");
    printf("!0 = %d, !5 = %d, 3 && 0 = %d, 3 || 0 = %d\n", !0, !5, 3 && 0, 3 || 0);
    return 0;
}

✅ Verified output

1900 leap? false
2000 leap? true
2024 leap? true
2025 leap? false
!0 = 1, !5 = 0, 3 && 0 = 0, 3 || 0 = 1
printf and scanf formats

printf prints with format codes (%d int, %f float, %c char, %s string, %x hex). Widths and precision line up tables. sscanf reads from a string the same way scanf reads input.

#include <stdio.h>
int main(void) {
    printf("|%5d|%-5d|%05d|\n", 42, 42, 42);
    printf("|%.2f|%8.3f|%e|\n", 3.14159, 2.5, 123456.0);
    printf("|%c|%s|%x|%o|%%|\n", 'A', "GayaGnan", 255, 8);
    int day, month, year;
    char name[20];
    if (sscanf("Asha 15/08/2025", "%19s %d/%d/%d", name, &day, &month, &year) == 4)
        printf("%s: day %d, month %d, year %d\n", name, day, month, year);
    return 0;
}

✅ Verified output

|   42|42   |00042|
|3.14|   2.500|1.234560e+05|
|A|GayaGnan|ff|10|%|
Asha: day 15, month 8, year 2025
Nested loops: patterns and tables

A loop inside a loop runs fully for each outer turn: perfect for grids, multiplication tables and star patterns.

#include <stdio.h>
int main(void) {
    for (int row = 1; row <= 4; row++) {
        for (int s = 0; s < 4 - row; s++) putchar(' ');
        for (int k = 0; k < 2 * row - 1; k++) putchar('*');
        putchar('\n');
    }
    for (int i = 1; i <= 3; i++) {
        for (int j = 1; j <= 5; j++) printf("%4d", i * j);
        printf("\n");
    }
    return 0;
}

✅ Verified output

   *
  ***
 *****
*******
   1   2   3   4   5
   2   4   6   8  10
   3   6   9  12  15
break, continue and loop labels with goto

break leaves the nearest loop, continue skips to the next turn. To leave two loops at once, use a flag (or, rarely, goto to a clean-up label).

#include <stdio.h>
int main(void) {
    for (int i = 1; i <= 10; i++) {
        if (i % 3 == 0) continue;
        if (i > 8) break;
        printf("%d ", i);
    }
    printf("\n");
    int grid[3][3] = {{1, 2, 3}, {4, 42, 6}, {7, 8, 9}};
    int found = 0;
    for (int r = 0; r < 3 && !found; r++)
        for (int c = 0; c < 3; c++)
            if (grid[r][c] == 42) { printf("42 at row %d col %d\n", r, c); found = 1; break; }
    return 0;
}

✅ Verified output

1 2 4 5 7 8 
42 at row 1 col 1

🌿 Intermediate (16)

Arrays and sorting

An array is a fixed block of same-type values. Its size is sizeof(arr)/sizeof(arr[0]). qsort sorts with a compare function. C does not check bounds: never index past the end.

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

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

int main(void) {
    int marks[] = {72, 95, 64, 88, 91};
    size_t n = sizeof(marks) / sizeof(marks[0]);
    qsort(marks, n, sizeof(int), cmp);
    printf("sorted:");
    for (size_t i = 0; i < n; i++) printf(" %d", marks[i]);
    printf("\nfirst %d last %d count %zu\n", marks[0], marks[n - 1], n);
    int grid[2][3] = {{1, 2, 3}, {4, 5, 6}};
    int total = 0;
    for (int r = 0; r < 2; r++) for (int c = 0; c < 3; c++) total += grid[r][c];
    printf("2D total %d\n", total);
    return 0;
}

✅ Verified output

sorted: 64 72 88 91 95
first 64 last 95 count 5
2D total 21
Strings (char arrays)

A C string is an array of char ending with '\0'. Use string.h: strlen, strcpy/strncpy, strcat, strcmp. snprintf formats safely into a buffer of known size.

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

int main(void) {
    char name[32] = "GayaGnan";
    printf("%s has %zu letters\n", name, strlen(name));
    strncat(name, " labs", sizeof(name) - strlen(name) - 1);
    printf("%s\n", name);
    for (size_t i = 0; name[i]; i++) name[i] = (char)toupper((unsigned char)name[i]);
    printf("%s, compare: %d\n", name, strcmp("apple", "banana") < 0);
    char buf[40];
    snprintf(buf, sizeof buf, "Total: %.2f for %d items", 49.5 * 3, 3);
    printf("%s\n", buf);
    const char *w = "level";
    int pal = 1;
    for (size_t i = 0, j = strlen(w) - 1; i < j; i++, j--) if (w[i] != w[j]) pal = 0;
    printf("palindrome: %d\n", pal);
    return 0;
}

✅ Verified output

GayaGnan has 8 letters
GayaGnan labs
GAYAGNAN LABS, compare: 1
Total: 148.50 for 3 items
palindrome: 1
Structs and arrays of structs

struct groups related fields into one type. typedef gives it a short name. Pass a pointer to a struct to change it; use -> to reach fields through a pointer.

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

typedef struct {
    char name[20];
    int marks;
} Student;

void bonus(Student *s, int extra) { s->marks += extra; }

int main(void) {
    Student cls[] = {{"Asha", 88}, {"Ravi", 95}, {"Meena", 72}};
    int n = sizeof cls / sizeof cls[0];
    bonus(&cls[2], 5);
    int best = 0;
    for (int i = 0; i < n; i++) {
        printf("%-6s %d\n", cls[i].name, cls[i].marks);
        if (cls[i].marks > cls[best].marks) best = i;
    }
    printf("top: %s, struct size %zu bytes\n", cls[best].name, sizeof(Student));
    return 0;
}

✅ Verified output

Asha   88
Ravi   95
Meena  77
top: Ravi, struct size 24 bytes
Dynamic memory: malloc, realloc, free

malloc asks the heap for memory at run time; realloc grows it; free gives it back. Always check for NULL and free exactly once.

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

int main(void) {
    int cap = 2, len = 0;
    int *list = malloc(cap * sizeof *list);
    if (!list) return 1;
    for (int v = 10; v <= 50; v += 10) {
        if (len == cap) {
            cap *= 2;
            int *bigger = realloc(list, cap * sizeof *list);
            if (!bigger) { free(list); return 1; }
            list = bigger;
            printf("grew to %d\n", cap);
        }
        list[len++] = v;
    }
    for (int i = 0; i < len; i++) printf("%d ", list[i]);
    printf("\nlen %d cap %d\n", len, cap);
    free(list);
    list = NULL;                       /* avoid a dangling pointer */
    return 0;
}

✅ Verified output

grew to 4
grew to 8
10 20 30 40 50 
len 5 cap 8
Error handling with return codes and errno

C has no exceptions. Functions return a status, and library calls set errno. strtol with an end pointer tells you whether the whole text was a number.

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

int parse_int(const char *s, long *out) {
    char *end;
    errno = 0;
    long v = strtol(s, &end, 10);
    if (errno || *end != '\0' || end == s) return -1;
    *out = v;
    return 0;
}

int main(void) {
    const char *inputs[] = {"42", "abc", "12x"};
    for (int i = 0; i < 3; i++) {
        long v;
        if (parse_int(inputs[i], &v) == 0) printf("%s -> %ld\n", inputs[i], v);
        else printf("%s -> not a number\n", inputs[i]);
    }
    FILE *f = fopen("/no/such/dir/file.txt", "r");
    if (!f) printf("fopen failed: %s\n", strerror(errno));
    return 0;
}

✅ Verified output

42 -> 42
abc -> not a number
12x -> not a number
fopen failed: No such file or directory
Files: fprintf and fgets

fopen opens a file ("w" write, "r" read). fprintf writes formatted text; fgets reads a line at a time. Always fclose.

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

int main(void) {
    const char *path = "/tmp/marks.csv";
    FILE *f = fopen(path, "w");
    if (!f) return 1;
    fprintf(f, "name,marks\nAsha,88\nRavi,95\n");
    fclose(f);

    f = fopen(path, "r");
    char line[64];
    int total = 0, count = 0;
    fgets(line, sizeof line, f);              /* skip header */
    while (fgets(line, sizeof line, f)) {
        char *name = strtok(line, ",");
        int marks = atoi(strtok(NULL, ",\n"));
        printf("%s -> %d\n", name, marks);
        total += marks; count++;
    }
    fclose(f);
    remove(path);
    printf("average %.1f\n", (double)total / count);
    return 0;
}

✅ Verified output

Asha -> 88
Ravi -> 95
average 91.5
2D arrays and matrix multiplication

A 2D array is rows of columns stored one after another in memory. Pass it to a function with the column count in the type.

#include <stdio.h>
#define N 3
void mul(int a[N][N], int b[N][N], int out[N][N]) {
    for (int i = 0; i < N; i++)
        for (int j = 0; j < N; j++) {
            out[i][j] = 0;
            for (int k = 0; k < N; k++) out[i][j] += a[i][k] * b[k][j];
        }
}
int main(void) {
    int a[N][N] = {{1, 2, 0}, {0, 1, 0}, {2, 0, 1}}, id[N][N] = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}, c[N][N];
    mul(a, a, c);
    for (int i = 0; i < N; i++) printf("%d %d %d\n", c[i][0], c[i][1], c[i][2]);
    mul(a, id, c);
    printf("A x I == A? %s\n", c[2][0] == 2 && c[0][1] == 2 ? "yes" : "no");
    return 0;
}

✅ Verified output

1 4 0
0 1 0
4 4 1
A x I == A? yes
String library: strlen, strcpy, strcmp, strtok

string.h works on NUL-terminated char arrays. Always leave room for the final '\0'; prefer snprintf and strncpy-style limits to avoid overflows.

#include <stdio.h>
#include <string.h>
#include <ctype.h>
int main(void) {
    char s[64];
    snprintf(s, sizeof s, "%s", "maths,science,english");
    printf("length %zu\n", strlen(s));
    printf("compare: %d %d\n", strcmp("apple", "banana") < 0, strcmp("same", "same"));
    for (char *tok = strtok(s, ","); tok; tok = strtok(NULL, ",")) {
        tok[0] = (char)toupper((unsigned char)tok[0]);
        printf("[%s]", tok);
    }
    printf("\n%s\n", strstr("GayaGnan Labs", "Gnan"));
    return 0;
}

✅ Verified output

length 21
compare: 1 0
[Maths][Science][English]
Gnan Labs
Enums and switch state machine

An enum names a set of integer constants. A switch on the current state plus an event is a simple, testable state machine.

#include <stdio.h>
typedef enum { RED, GREEN, YELLOW } Light;
const char *names[] = {"RED", "GREEN", "YELLOW"};
Light next(Light s) {
    switch (s) {
        case RED: return GREEN;
        case GREEN: return YELLOW;
        case YELLOW: return RED;
    }
    return RED;
}
int main(void) {
    Light s = RED;
    int secs[] = {30, 25, 5};
    for (int i = 0; i < 6; i++) {
        printf("%-6s for %2d s\n", names[s], secs[s]);
        s = next(s);
    }
    return 0;
}

✅ Verified output

RED    for 30 s
GREEN  for 25 s
YELLOW for  5 s
RED    for 30 s
GREEN  for 25 s
YELLOW for  5 s
Unions and bit fields

A union stores different types in the same memory. Bit fields pack several small values into one integer, as in hardware registers.

#include <stdio.h>
#include <stdint.h>
typedef struct {
    unsigned ready : 1;
    unsigned error : 1;
    unsigned mode : 2;
    unsigned count : 4;
} Status;
typedef union { float f; uint32_t bits; } FloatBits;
int main(void) {
    Status st = {1, 0, 2, 9};
    printf("ready=%u error=%u mode=%u count=%u (size %zu bytes)\n", st.ready, st.error, st.mode, st.count, sizeof st);
    FloatBits fb = {.f = 1.0f};
    printf("1.0f is stored as 0x%08X\n", fb.bits);
    fb.f = -2.5f;
    printf("-2.5f sign bit = %u\n", fb.bits >> 31);
    return 0;
}

✅ Verified output

ready=1 error=0 mode=2 count=9 (size 4 bytes)
1.0f is stored as 0x3F800000
-2.5f sign bit = 1
Linked list: insert, search, delete

Each node points to the next. Inserting or removing in the middle only changes pointers; free every node when done.

#include <stdio.h>
#include <stdlib.h>
typedef struct Node { int val; struct Node *next; } Node;
Node *push(Node *head, int v) { Node *n = malloc(sizeof *n); n->val = v; n->next = head; return n; }
Node *removeVal(Node *head, int v) {
    Node **pp = &head;
    while (*pp && (*pp)->val != v) pp = &(*pp)->next;
    if (*pp) { Node *dead = *pp; *pp = dead->next; free(dead); }
    return head;
}
void print(const Node *n) { for (; n; n = n->next) printf("%d -> ", n->val); printf("NULL\n"); }
int main(void) {
    Node *list = NULL;
    for (int i = 1; i <= 5; i++) list = push(list, i * 10);
    print(list);
    list = removeVal(list, 30);
    list = removeVal(list, 50);
    print(list);
    while (list) { Node *n = list->next; free(list); list = n; }
    return 0;
}

✅ Verified output

50 -> 40 -> 30 -> 20 -> 10 -> NULL
40 -> 20 -> 10 -> NULL
Stack and queue with arrays

A stack pushes and pops at one end (LIFO). A circular queue reuses its array by wrapping the indices with %.

#include <stdio.h>
#define CAP 4
int stack[CAP], top = 0;
int q[CAP], head = 0, tail = 0, size = 0;
int enqueue(int v) { if (size == CAP) return 0; q[tail] = v; tail = (tail + 1) % CAP; size++; return 1; }
int dequeue(int *v) { if (!size) return 0; *v = q[head]; head = (head + 1) % CAP; size--; return 1; }
int main(void) {
    for (int i = 1; i <= 3; i++) stack[top++] = i;
    while (top) printf("pop %d\n", stack[--top]);
    for (int i = 1; i <= 5; i++) if (!enqueue(i)) printf("queue full, %d waits\n", i);
    int v;
    dequeue(&v); printf("served %d\n", v);
    enqueue(5);
    while (dequeue(&v)) printf("served %d\n", v);
    return 0;
}

✅ Verified output

pop 3
pop 2
pop 1
queue full, 5 waits
served 1
served 2
served 3
served 4
served 5
Binary search tree

Smaller keys go left, larger go right, so search, insert and in-order (sorted) printing follow the tree's shape.

#include <stdio.h>
#include <stdlib.h>
typedef struct T { int key; struct T *l, *r; } T;
T *insert(T *t, int k) {
    if (!t) { t = calloc(1, sizeof *t); t->key = k; return t; }
    if (k < t->key) t->l = insert(t->l, k); else t->r = insert(t->r, k);
    return t;
}
int height(T *t) { if (!t) return 0; int a = height(t->l), b = height(t->r); return 1 + (a > b ? a : b); }
void inorder(T *t) { if (!t) return; inorder(t->l); printf("%d ", t->key); inorder(t->r); }
void freeTree(T *t) { if (!t) return; freeTree(t->l); freeTree(t->r); free(t); }
int main(void) {
    int keys[] = {50, 30, 70, 20, 40, 60, 80, 35};
    T *root = NULL;
    for (int i = 0; i < 8; i++) root = insert(root, keys[i]);
    inorder(root);
    printf("\nheight %d\n", height(root));
    freeTree(root);
    return 0;
}

✅ Verified output

20 30 35 40 50 60 70 80 
height 4
qsort and bsearch with comparators

The C library sorts and searches any array if you give it a comparison function that returns negative, zero or positive.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct { char name[12]; int marks; } Student;
int byMarksDesc(const void *a, const void *b) { return ((const Student *)b)->marks - ((const Student *)a)->marks; }
int cmpInt(const void *a, const void *b) { int x = *(const int *)a, y = *(const int *)b; return (x > y) - (x < y); }
int main(void) {
    Student s[] = {{"Asha", 91}, {"Ravi", 78}, {"Meena", 85}, {"Kiran", 65}};
    qsort(s, 4, sizeof s[0], byMarksDesc);
    for (int i = 0; i < 4; i++) printf("%d. %-6s %d\n", i + 1, s[i].name, s[i].marks);
    int a[] = {42, 7, 19, 3, 88, 21};
    qsort(a, 6, sizeof a[0], cmpInt);
    int key = 19, *hit = bsearch(&key, a, 6, sizeof a[0], cmpInt);
    printf("19 found at index %td\n", hit - a);
    return 0;
}

✅ Verified output

1. Asha   91
2. Meena  85
3. Ravi   78
4. Kiran  65
19 found at index 2
Preprocessor: macros and conditional compilation

#define makes constants and macros; parenthesise macro arguments. #ifdef includes code only when a symbol is defined, which is how debug logging is switched on and off.

#include <stdio.h>
#define SQUARE_BAD(x) x * x
#define SQUARE(x) ((x) * (x))
#define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0]))
#define DEBUG 1
#if DEBUG
#define LOG(msg) printf("[debug %s:%d] %s\n", __func__, __LINE__, msg)
#else
#define LOG(msg)
#endif
int main(void) {
    printf("SQUARE_BAD(2+3) = %d, SQUARE(2+3) = %d\n", SQUARE_BAD(2 + 3), SQUARE(2 + 3));
    int marks[] = {91, 78, 85, 65, 70};
    printf("%zu marks\n", ARRAY_LEN(marks));
    LOG("finished counting");
    return 0;
}

✅ Verified output

SQUARE_BAD(2+3) = 11, SQUARE(2+3) = 25
5 marks
[debug main:15] finished counting
const, static and scope

const promises not to change a value. A static local keeps its value between calls; a static global is private to its file.

#include <stdio.h>
static int calls = 0;                 /* visible only in this file */
int next_ticket(void) {
    static int ticket = 100;          /* kept between calls */
    calls++;
    return ++ticket;
}
double average(const int *xs, int n) {   /* const: this function will not change xs */
    long sum = 0;
    for (int i = 0; i < n; i++) sum += xs[i];
    return (double)sum / n;
}
int main(void) {
    for (int i = 0; i < 3; i++) printf("ticket %d\n", next_ticket());
    const int marks[] = {91, 78, 85};
    printf("average %.2f after %d calls\n", average(marks, 3), calls);
    return 0;
}

✅ Verified output

ticket 101
ticket 102
ticket 103
average 84.67 after 3 calls

🌳 Advanced (12)

Recursion and memoization

A recursive function calls itself until a base case. A static array caches results, so fib(80) is instant; unsigned long long holds the large value.

#include <stdio.h>

unsigned long long memo[100];

unsigned long long fib(int n) {
    if (n < 2) return (unsigned long long)n;
    if (memo[n]) return memo[n];
    return memo[n] = fib(n - 1) + fib(n - 2);
}

int binary_search(const int a[], int lo, int hi, int target) {
    if (lo > hi) return -1;
    int mid = lo + (hi - lo) / 2;
    if (a[mid] == target) return mid;
    return a[mid] < target ? binary_search(a, mid + 1, hi, target) : binary_search(a, lo, mid - 1, target);
}

int main(void) {
    for (int i = 0; i < 10; i++) printf("%llu ", fib(i));
    printf("| fib(80) = %llu\n", fib(80));
    int a[] = {2, 5, 8, 12, 16, 23, 38};
    printf("index of 23: %d\n", binary_search(a, 0, 6, 23));
    return 0;
}

✅ Verified output

0 1 1 2 3 5 8 13 21 34 | fib(80) = 23416728348467685
index of 23: 5
Function pointers and callbacks

A function pointer stores which function to call. Passing one lets a function such as apply or qsort run your code: C's version of higher-order functions.

#include <stdio.h>

typedef int (*op_t)(int, int);
int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
int apply_all(const int *v, int n, int start, op_t op) {
    int acc = start;
    for (int i = 0; i < n; i++) acc = op(acc, v[i]);
    return acc;
}
void for_each(int *v, int n, void (*fn)(int *)) { for (int i = 0; i < n; i++) fn(&v[i]); }
void triple(int *x) { *x *= 3; }

int main(void) {
    int v[] = {1, 2, 3, 4};
    op_t ops[] = {add, mul};
    const char *names[] = {"sum", "product"};
    for (int i = 0; i < 2; i++) printf("%s = %d\n", names[i], apply_all(v, 4, i, ops[i]));
    for_each(v, 4, triple);
    printf("tripled: %d %d %d %d\n", v[0], v[1], v[2], v[3]);
    return 0;
}

✅ Verified output

sum = 10
product = 24
tripled: 3 6 9 12
Threads with pthreads and a mutex

Each thread runs a function at the same time. Without the mutex, two threads could update total together and lose a count; with it the answer is always 4000.

#include <stdio.h>
#include <pthread.h>

long total = 0;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void *worker(void *arg) {
    int id = *(int *)arg;
    for (int i = 0; i < 1000; i++) {
        pthread_mutex_lock(&lock);
        total++;
        pthread_mutex_unlock(&lock);
    }
    return (void *)(long)id;
}

int main(void) {
    pthread_t t[4];
    int ids[4] = {1, 2, 3, 4};
    for (int i = 0; i < 4; i++) pthread_create(&t[i], NULL, worker, &ids[i]);
    for (int i = 0; i < 4; i++) {
        void *ret;
        pthread_join(t[i], &ret);
        printf("thread %ld done\n", (long)ret);
    }
    printf("total %ld\n", total);
    return 0;
}

✅ Verified output

thread 1 done
thread 2 done
thread 3 done
thread 4 done
total 4000
Pointers and pointer arithmetic

A pointer holds a memory address. *p reads the value there; p + 1 moves to the next element (by the size of the type). Arrays decay to a pointer to their first element.

#include <stdio.h>

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

int main(void) {
    int arr[] = {10, 20, 30, 40};
    int *p = arr;                         /* same as &arr[0] */
    printf("*p=%d *(p+2)=%d p[3]=%d\n", *p, *(p + 2), p[3]);
    printf("elements apart: %td\n", &arr[3] - &arr[0]);
    int sum = 0;
    for (int *q = arr; q < arr + 4; q++) sum += *q;
    printf("sum via pointer walk %d\n", sum);
    int x = 1, y = 2;
    swap(&x, &y);
    printf("swapped x=%d y=%d\n", x, y);
    int **pp = &p;
    printf("pointer to pointer: %d\n", **pp);
    return 0;
}

✅ Verified output

*p=10 *(p+2)=30 p[3]=40
elements apart: 3
sum via pointer walk 100
swapped x=2 y=1
pointer to pointer: 10
Bitwise operations and masks

& | ^ ~ << >> work on bits: set, clear, toggle and test flags, count set bits, and check for a power of two.

#include <stdio.h>
#include <stdint.h>
#define BIT(n) (1u << (n))
int popcount(uint32_t x) { int c = 0; while (x) { x &= x - 1; c++; } return c; }
int main(void) {
    uint8_t reg = 0;
    reg |= BIT(0) | BIT(3);           /* set */
    reg ^= BIT(1);                    /* toggle */
    reg &= (uint8_t)~BIT(0);          /* clear */
    printf("reg = 0x%02X, bit3 is %s\n", reg, (reg & BIT(3)) ? "on" : "off");
    printf("popcount(0xF0F0) = %d\n", popcount(0xF0F0));
    for (unsigned n = 60; n <= 64; n++) printf("%u%s ", n, (n & (n - 1)) == 0 ? "(pow2)" : "");
    printf("\nswap nibbles of 0xAB -> 0x%X\n", ((0xAB & 0x0F) << 4) | (0xAB >> 4));
    return 0;
}

✅ Verified output

reg = 0x0A, bit3 is on
popcount(0xF0F0) = 8
60 61 62 63 64(pow2) 
swap nibbles of 0xAB -> 0xBA
Hash table with chaining

A hash function picks a bucket for each key; keys that collide are kept in a short linked list inside that bucket.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUCKETS 8
typedef struct E { char key[16]; int val; struct E *next; } E;
E *table[BUCKETS];
unsigned hash(const char *s) { unsigned h = 5381; while (*s) h = h * 33 + (unsigned char)*s++; return h % BUCKETS; }
void put(const char *k, int v) {
    unsigned b = hash(k);
    for (E *e = table[b]; e; e = e->next) if (!strcmp(e->key, k)) { e->val = v; return; }
    E *e = malloc(sizeof *e); snprintf(e->key, sizeof e->key, "%s", k); e->val = v; e->next = table[b]; table[b] = e;
}
int get(const char *k, int *v) { for (E *e = table[hash(k)]; e; e = e->next) if (!strcmp(e->key, k)) { *v = e->val; return 1; } return 0; }
int main(void) {
    const char *names[] = {"asha", "ravi", "meena", "kiran", "zara", "dev"};
    for (int i = 0; i < 6; i++) put(names[i], 60 + i * 7);
    put("ravi", 99);
    int v;
    if (get("ravi", &v)) printf("ravi -> %d\n", v);
    printf("bob found? %d\n", get("bob", &v));
    for (int b = 0; b < BUCKETS; b++) { int n = 0; for (E *e = table[b]; e; e = e->next) n++; printf("bucket %d: %d\n", b, n); }
    return 0;
}

✅ Verified output

ravi -> 99
bob found? 0
bucket 0: 0
bucket 1: 0
bucket 2: 2
bucket 3: 2
bucket 4: 1
bucket 5: 0
bucket 6: 0
bucket 7: 1
Graph BFS with an adjacency matrix

Breadth-first search uses a queue to visit nodes in order of distance, so it finds the fewest hops from the start to every node.

#include <stdio.h>
#define V 6
int main(void) {
    int adj[V][V] = {0};
    int edges[][2] = {{0, 1}, {0, 2}, {1, 3}, {2, 3}, {3, 4}, {4, 5}};
    for (int i = 0; i < 6; i++) { adj[edges[i][0]][edges[i][1]] = adj[edges[i][1]][edges[i][0]] = 1; }
    int dist[V], q[V], head = 0, tail = 0;
    for (int i = 0; i < V; i++) dist[i] = -1;
    dist[0] = 0; q[tail++] = 0;
    while (head < tail) {
        int u = q[head++];
        for (int w = 0; w < V; w++)
            if (adj[u][w] && dist[w] < 0) { dist[w] = dist[u] + 1; q[tail++] = w; }
    }
    for (int i = 0; i < V; i++) printf("node %d: %d hops\n", i, dist[i]);
    return 0;
}

✅ Verified output

node 0: 0 hops
node 1: 1 hops
node 2: 1 hops
node 3: 2 hops
node 4: 3 hops
node 5: 4 hops
Dynamic programming: coin change

Build a table of answers for every smaller amount; each entry uses the best earlier one. Here: fewest coins for every amount up to the target.

#include <stdio.h>
#include <limits.h>
int main(void) {
    int coins[] = {1, 5, 10, 20}, n = 4, target = 63;
    int best[64], used[64];
    best[0] = 0;
    for (int a = 1; a <= target; a++) {
        best[a] = INT_MAX;
        for (int c = 0; c < n; c++)
            if (coins[c] <= a && best[a - coins[c]] + 1 < best[a]) { best[a] = best[a - coins[c]] + 1; used[a] = coins[c]; }
    }
    printf("%d rupees needs %d coins:", target, best[target]);
    for (int a = target; a > 0; a -= used[a]) printf(" %d", used[a]);
    printf("\n");
    return 0;
}

✅ Verified output

63 rupees needs 6 coins: 1 1 1 20 20 20
Generic swap with void pointers and memcpy

void * can point to any type. Together with a size, memcpy lets one function work on ints, doubles or structs.

#include <stdio.h>
#include <string.h>
void swap(void *a, void *b, size_t size) {
    unsigned char tmp[64];
    memcpy(tmp, a, size);
    memcpy(a, b, size);
    memcpy(b, tmp, size);
}
typedef struct { char name[8]; int age; } P;
int main(void) {
    int x = 1, y = 2;
    double d1 = 1.5, d2 = -3.25;
    P p1 = {"Asha", 15}, p2 = {"Ravi", 16};
    swap(&x, &y, sizeof x);
    swap(&d1, &d2, sizeof d1);
    swap(&p1, &p2, sizeof p1);
    printf("%d %d | %.2f %.2f | %s %d\n", x, y, d1, d2, p1.name, p1.age);
    return 0;
}

✅ Verified output

2 1 | -3.25 1.50 | Ravi 16
Variadic functions with stdarg

A function can take a variable number of arguments with ..., read them with va_list, and must know how many there are (a count or a format string).

#include <stdio.h>
#include <stdarg.h>
double mean(int count, ...) {
    va_list ap;
    va_start(ap, count);
    double sum = 0;
    for (int i = 0; i < count; i++) sum += va_arg(ap, double);
    va_end(ap);
    return count ? sum / count : 0;
}
void logf_(const char *level, const char *fmt, ...) {
    va_list ap;
    va_start(ap, fmt);
    printf("[%s] ", level);
    vprintf(fmt, ap);
    printf("\n");
    va_end(ap);
}
int main(void) {
    printf("%.2f %.2f\n", mean(3, 90.0, 80.0, 70.0), mean(2, 1.5, 2.5));
    logf_("INFO", "%d students, top score %d", 42, 98);
    return 0;
}

✅ Verified output

80.00 2.00
[INFO] 42 students, top score 98
Endianness and byte order

Multi-byte numbers are stored lowest byte first (little-endian) on most CPUs. Network protocols use big-endian, so code converts explicitly.

#include <stdio.h>
#include <stdint.h>
uint32_t to_big_endian(uint32_t x) {
    return ((x & 0xFF) << 24) | ((x & 0xFF00) << 8) | ((x >> 8) & 0xFF00) | (x >> 24);
}
int main(void) {
    uint32_t v = 0x11223344;
    unsigned char *b = (unsigned char *)&v;
    printf("in memory: %02X %02X %02X %02X\n", b[0], b[1], b[2], b[3]);
    printf("this CPU is %s-endian\n", b[0] == 0x44 ? "little" : "big");
    printf("big-endian value: 0x%08X\n", to_big_endian(v));
    unsigned char packet[4] = {0x00, 0x00, 0x01, 0xF4};
    uint32_t port = (uint32_t)packet[0] << 24 | packet[1] << 16 | packet[2] << 8 | packet[3];
    printf("decoded from the wire: %u\n", port);
    return 0;
}

✅ Verified output

in memory: 44 33 22 11
this CPU is little-endian
big-endian value: 0x44332211
decoded from the wire: 500
Ring buffer for streaming data

A ring buffer keeps the last N samples in a fixed array, overwriting the oldest: the standard way to buffer sensor or UART data on a microcontroller.

#include <stdio.h>
#define N 5
typedef struct { int data[N]; int head, count; } Ring;
void put(Ring *r, int v) { r->data[r->head] = v; r->head = (r->head + 1) % N; if (r->count < N) r->count++; }
double avg(const Ring *r) { long s = 0; for (int i = 0; i < r->count; i++) s += r->data[i]; return r->count ? (double)s / r->count : 0; }
int main(void) {
    Ring r = {0};
    int readings[] = {20, 22, 21, 35, 23, 24, 22, 21};
    for (int i = 0; i < 8; i++) {
        put(&r, readings[i]);
        printf("sample %d -> moving average %.1f\n", readings[i], avg(&r));
    }
    return 0;
}

✅ Verified output

sample 20 -> moving average 20.0
sample 22 -> moving average 21.0
sample 21 -> moving average 21.0
sample 35 -> moving average 24.5
sample 23 -> moving average 24.2
sample 24 -> moving average 25.0
sample 22 -> moving average 25.0
sample 21 -> moving average 25.0

🛠️ Set up and verify

  1. Install
    winget install MSYS2.MSYS2   (then in MSYS2: pacman -S mingw-w64-ucrt-x86_64-gcc)
    xcode-select --install
    sudo apt install build-essential gdb
  2. Editor: Visual Studio Code with C/C++ (Microsoft), CodeLLDB or the GDB debugger.
  3. Create hello.c
    #include <stdio.h>
    
    int main(void) {
        printf("Hello, GayaGnan!\n");
        return 0;
    }
  4. Run it and check the version
    gcc hello.c -o hello && ./hello
    gcc --version
  5. Health check: paste what your terminal printed.

Install one language at a time. These commands only install or print versions; nothing here deletes files. Versions change: check the official page and note the version you installed.

🧭 The 14-lab path

  • 00 · Setup and verify — install the runtime and editor, run hello world, print the version, fix PATH
  • 01 · Syntax and trace — follow each line and the value of every variable
  • 02 · Logic — conditionals and decision trees
  • 03 · Loops — trace tables and loop challenges
  • 04 · Functions — break problems down and test each piece
  • 05 · Data structures — lists, maps, stacks, queues and trees you can see
  • 06 · Debugging — bug cases with a hint ladder
  • 07 · Algorithms — search, sort and Big-O comparison
  • 08 · Files and data — read and write fixture files
  • 09 · API or database — talk to a mock API or a sandbox database
  • 10 · Testing — write tests and read failing assertions
  • 11 · Security — secure coding and dependency hygiene for this language
  • 12 · Project — one portfolio mini-project
  • 13 · Assessment — quiz, coding test, rubric and an explain-your-answer prompt

📚 Modules

  1. types
  2. operators
  3. branches
  4. loops
  5. functions
  6. arrays
  7. strings
  8. pointers
  9. pointer arithmetic
  10. structs
  11. dynamic memory
  12. files
  13. compile and link
  14. debugging
  15. complexity

🏗️ Mini-projects

  • Terminal quiz
  • Array algorithm visualiser
  • Contact book with synthetic records

Projects use synthetic data only.