BrainBit Infotech Teaching · Programming Fundamentals · 20 days

C Programming

The language the operating system, the database and the Python interpreter are all written in. Learn it once, and every language after it makes more sense.

20 days 11 modules 6 tests 6 practice sets 1 capstone works offline
Press → or Space to begin · F fullscreen · O overview
Where this course sits

Why learn C first?

  • Closest mainstream language to the machine. Every variable, pointer and array maps directly onto memory you can reason about — nothing hides between your code and the hardware.
  • The world runs on it. The Windows, Linux and macOS kernels, MySQL and PostgreSQL, and the CPython interpreter that runs your Python code are themselves written in C.
  • Every later language borrows its syntax. C++, Java, C#, JavaScript and Go all read like dialects of C once you know it — braces, semicolons, for loops, function calls.
  • It teaches memory. Nothing else forces you to learn what a variable actually is, because nothing else lets you get it wrong.
Where C sits in your education.
  1. School — the first programming language most syllabi teach
  2. College — the first-semester programming paper
  3. College — the foundation the Data Structures paper builds on
💡
Everything in Modules 9–11 of this course — structures, files, linked lists, stacks and queues — is exactly what a first Data Structures course assumes you already know.
11 modules · 6 tests · 6 practice sets · 1 capstone

The road ahead

ModuleDaysYou will learnCheckpoint
11–2FoundationsTest 1
23–4Data & operatorsTest 1
35Input & outputTest 1 · Days 1–5
46–7Control flowTest 2 · Days 6–7
58–9Functions & recursionTest 3
610–11Arrays & stringsTest 3 · Days 8–11
712–13PointersTest 4
814Dynamic memoryTest 4 · Days 12–14
915–16Structures, unions & enumsTest 5
1017Files & the preprocessorTest 5 · Days 15–17
1118–20Data structures in CTest 6 · Days 18–20
Capstone—Student Record System — full CRUD console appFinal exam
💡
Every code window in this deck has a ▶ Run button that hides the output. Predict what it prints before you click — that is where the learning happens.
Day-wise plan · part 1 of 2

Days 1–10

DayModuleWhat today covers
1Foundations What C is, the compilation pipeline, setup, your first program
2Foundations Algorithms & flowcharts, tokens & identifiers, comments & style, the four kinds of error
3Data & operators Data types & sizeof, variables, constants, literals & escape sequences, format specifiers
4Data & operators Arithmetic, relational, logical & bitwise operators, precedence, type conversion
5Input & output printf & scanf in depth, character I/O, the input-buffer trap, a marksheet practical
6Control flow if / else-if ladders, nested if, switch & fall-through, why goto is avoided
7Control flow while & do-while, for — anatomy, break & continue, nested loops, dry-running
8Functions & recursion Declaring, defining & calling functions, parameters, call by value, scope, storage classes
9Functions & recursion How recursion works, the call stack, recursion vs iteration
10Arrays & strings Array memory model, declaring & traversing, linear & binary search, bubble, selection & insertion sort
Day-wise plan · part 2 of 2

Days 11–20

DayModuleWhat today covers
11Arrays & strings 2D arrays & matrices, strings as char arrays, the null terminator, string.h, writing your own string functions
12Pointers What a pointer is, & and *, pointer arithmetic, pointers & arrays, call by reference
13Pointers Pointers & strings, arrays of pointers, pointer to pointer, pointer to function
14Dynamic memory Stack vs heap, malloc/calloc/realloc/free, checking the return, leaks & dangling pointers
15Structures, unions & enums Declaring & initialising structs, arrays & nested structs, struct pointers & ->
16Structures, unions & enums typedef, union, enum, padding & sizeof
17Files & the preprocessor File I/O with fopen/fread/fwrite, seeking, macros, headers & multi-file builds
18Data structures in C What a data structure is, self-referential structs, building & traversing a linked list
19Data structures in C Stacks & queues, array & linked implementations, balanced parentheses, circular queue
20Data structures in C Doubly linked lists, Big-O without the maths, comparing array/list/stack/queue
Before you start

How to use this deck

KeyAction
→ / SpaceNext slide
←Previous slide
Home / EndFirst / last slide
O / EscSlide overview
FFullscreen
▶
Predict, then click. Every code window's output is hidden behind a ▶ Run button. Say out loud what it will print before you reveal it — guessing wrong is exactly where the learning happens.
Practice & test sets. Six untimed practice sets (12 long-answer questions each, 48 marks) sit before six timed tests (15 short-answer questions, 15-minute clock, model answers reveal at 00:00). Every practice set has Print worksheet and Print answer key buttons built in.
🎓
Where you can stop. Teaching C as a school first language? Stop after Day 14 — pointers and dynamic memory close the loop on everything a first course needs. Modules 9–11 (structures, files, data structures) are the college syllabus.
Before Day 1

Install a compiler, then prove it works

terminal
$ gcc --version
gcc.exe (MinGW-W64 x86_64-ucrt-posix-seh) 16.1.0

$ gcc hello.c -o hello && ./hello
Hello, world!
          
  • Windows — install WinLibs or MinGW-w64, add its bin folder to PATH
  • Ubuntu / Debian — sudo apt install build-essential
  • macOS — install the Xcode Command Line Tools: xcode-select --install
  • Prefer a full IDE instead? Code::Blocks ships a compiler and editor together
  • Any plain text editor works — VS Code, Notepad++, even Notepad
💡
Verify before Day 1. Run gcc --version in a terminal. If it prints a version number, you are ready. gcc hello.c -o hello compiles; ./hello runs the result.
What gcc actually does

One command, four stages

terminal
$ gcc -E hello.c -o hello.i     # stop after preprocessing
$ gcc -S hello.c -o hello.s     # stop after compiling
$ gcc -c hello.c -o hello.o     # stop after assembling
$ gcc    hello.c -o hello       # run all four stages
          
  1. Preprocessor — expands #include and #define, strips comments. hello.c → hello.i
  2. Compiler — translates C into assembly for your CPU. hello.i → hello.s
  3. Assembler — turns assembly into machine code, an object file. hello.s → hello.o
  4. Linker — joins your object file with library code (such as printf's implementation) into one executable. hello.o → hello.exe
💡
Plain gcc hello.c -o hello runs all four stages silently. The flags above let you stop and inspect any one of them — useful the first time a linker error looks nothing like the C you wrote.
01

Foundations

Day 01 · Module 1 · Foundations

Today we cover

By the end of today you will have typed, compiled and run a C program on your own machine, and be able to name every line of it.
History & context

A language built to write an operating system

  • Dennis Ritchie designed C at Bell Labs in 1972, so that the UNIX operating system — until then written in assembly, one instruction per line of hardware — could be rewritten in something a human could read.
  • Compiled vs interpreted. A compiled language translates your whole source file into machine code before it runs, producing a standalone program. An interpreted language such as Python reads and executes source line by line, every time, through another program.
  • Because C compiles once to machine code, a C program starts faster and sits closer to the hardware — exactly why an operating system is written in it, not in an interpreted language.
Where C runs today, more than 50 years later.
  • The Windows, Linux and macOS kernels — the core of every operating system you use
  • Embedded systems — the firmware inside a washing machine, a car's ECU, a smart card
  • Databases — MySQL and PostgreSQL are written in C
  • The CPython interpreter — the program that runs every Python program you write is itself written in C
💡
Every language you learn after this one either runs on something written in C, or borrows its syntax from it.
Reading the shape

Every C program has this shape

hello.c
#include <stdio.h>

int main(void)
{
    printf("Hello, world!\n");
    return 0;
}
          
  1. Preprocessor directive. A line starting with # runs before compilation — #include <stdio.h> pulls in the standard input/output declarations, including printf, before your code compiles.
  2. int main(void) is where every C program starts. int is what it returns, void means it takes no input, and every C program has exactly one main — execution always begins there.
  3. The block. The matching { and } mark the body of main — everything the function does, grouped together.
  4. A statement. printf("Hello, world!\n"); is one complete instruction. In C, a statement always ends with a semicolon.
  5. return 0; hands control back to the operating system with the value 0, the C convention for "this program succeeded." Any other value signals failure — exactly why main is declared to return an int rather than nothing at all.
Your first program

Three lines that do something

hello.c
#include <stdio.h>

int main(void)
{
    printf("Hello, world!\n");
    return 0;
}
          
  1. #include <stdio.h> pastes the declarations of printf into your file before compiling. Without it the compiler has never heard of printf.
  2. int main(void) is where every C program starts. The operating system calls it.
  3. printf writes to the screen. \n is one character — a newline.
  4. return 0; tells the OS the program succeeded. Any other number means it failed.
💡
Predict before you click. Every code window in this deck hides its output behind ▶ Run. Say out loud what it will print first — that is where the learning happens.
From source file to running program

Compile once, run it any time

greet.c
#include <stdio.h>

int main(void)
{
    char name[20];

    printf("Enter your name: ");
    scanf("%19s", name);

    printf("Hello, %s! Welcome to C.\n", name);
    return 0;
}
          
terminal
$ gcc greet.c -o greet
$ ./greet
Enter your name: Asha
Hello, Asha! Welcome to C.
          
  • char name[20] declares an array — a block of 20 characters reserved for the name. scanf("%19s", name) reads into it, capped at 19 characters to leave room for the terminator. Arrays get their own module later in the course.
  • gcc greet.c -o greet compiles it exactly like hello.c was compiled on the last slide, writing one new file — gcc does not care whether a program reads input or not.
  • ./greet runs it. Nothing else is left behind, unless you ask for it, the way -c asked for just the object file two slides back.
💡
Re-run gcc after every edit. Editing .c source does not touch the compiled file sitting next to it — you must compile again before the change takes effect.
Day 02 · Module 1 · Foundations

Today we cover

By the end of today you can plan a short program on paper with a flowchart, name its parts correctly, and read a compiler error message instead of fearing it — then use all of it in Practical 1.
Plan before you type

Solve it on paper first

An algorithm is a finite, ordered sequence of steps that solves a problem — written in plain language, before a single line of C exists. A flowchart draws that sequence as a diagram, using five standard symbols:

SymbolShapeMeans
TerminalOvalStart or End
Input/OutputParallelogramRead a value in, print a value out
ProcessRectangleA calculation or assignment
DecisionDiamondA yes/no branch
Flow lineArrowThe order steps run in
Worked example — largest of three numbers, as pseudocode.
  1. Start
  2. Read three numbers a, b, c
  3. If a > b and a > c, then a is the largest
  4. Else if b > c, then b is the largest
  5. Else c is the largest
  6. Print the largest
  7. End
💡
Notice this already reads like C — if, else if, else. That is not an accident: pseudocode is meant to translate almost directly. You will write this exact logic in C on Day 6.
The vocabulary of C

Six kinds of token

  • Keywords — words reserved by C itself, such as int and return. You cannot use one as a variable name.
  • Identifiers — names you invent, for variables, functions and more.
  • Constants — fixed values such as 42 or 3.14.
  • String literals — text in double quotes, such as "Hello".
  • Operators — symbols that act on values: +, =, ==.
  • Punctuators — structural symbols: ;, { }, ( ), ,.
The 32 original C keywords — the ones you will actually use. Every one of these is off-limits as an identifier.
autobreakcasecharconstcontinuedefaultdodoubleelseenumexternfloatforgotoifintlongregisterreturnshortsignedsizeofstaticstructswitchtypedefunionunsignedvoidvolatilewhile

Later standards added 12 more, specialist ones you will not need in this course — the underscore-capital keywords such as _Bool, _Atomic and _Generic (C99 and C11), bringing C11's full count to 44.

💡
Identifier rules: start with a letter or underscore, then any mix of letters, digits and underscores — no spaces, no leading digit. total_marks and _temp are valid; 2nd_year is not. Convention: lower snake_case for variables, ALL_CAPS for constants.
Code is read more than it is written

Comments say why, not what

style.c
/* Multi-line comment: everything between
   these markers is ignored by the compiler. */

// Single-line comment: ignored to end of line

int total_marks = 0;   // running total, not final

/* Bad:  // add one to x        -- repeats the code   */
/* Good: // one more retry after a timeout -- explains why */
          
  • /* ... */ can span many lines and cannot be nested. // comments out only to the end of that line.
  • A good comment explains why, not what. "add one to x" tells you nothing the code did not already say. "one more retry after a timeout" tells you something the code cannot.
  • Indent consistently. Every statement inside { } gets one more level of indent — four spaces is the convention used throughout this deck.
  • One statement per line. a = 1; b = 2; compiles, but it hides a bug from your own eyes the moment one of them needs a fix.
💡
You will not remember why you wrote a tricky line the way you did — not next week, and certainly not next year. The comment is a note to your future self.
Reading what went wrong

Four ways a program can fail

broken.c
$ gcc broken.c -o broken
broken.c: In function 'main':
broken.c:6:5: error: expected ',' or ';' before 'printf'
    6 |     printf("%d\n", total);
      |     ^~~~~~
          
nolinker.c
$ gcc nolinker.c -o nolinker
...ld.exe: undefined reference to `showMenu'
collect2.exe: error: ld returned 1 exit status
          
KindCaught whenWhat you see
Compile-timegcc reads your sourceAn error: with an exact line number, like the left
Linkergcc joins the pieces into one programAn undefined reference from ld — the function exists in name only
Runtimewhile the program is runninggcc said nothing at all. The program crashes or hangs the moment it runs — dividing by zero is a classic example
LogicalneverIt compiles, links and runs cleanly — and prints a confidently wrong answer
💡
The one nobody warns you about is logical. No tool catches it — you catch it by working the answer out by hand and comparing. That habit is worth more than memorising error messages.
Module 1 practical

Bill an electricity meter, slab by slab

bill.c · 1/2
#include <stdio.h>
int main(void)
{
    int units;
    float charge, tax, total;
    printf("Enter units consumed: ");
    scanf("%d", &units);
    if (units <= 100)
        charge = units * 4.50f;
    else if (units <= 200)
        charge = 100*4.50f + (units-100)*6.00f;
    else
        charge = 100*4.50f + 100*6.00f
                + (units-200)*8.50f;
          
bill.c · 2/2
    tax = charge * 0.05f;
    total = charge + tax;
    printf("\n---- Electricity Bill ----\n");
    printf("%-16s%9d\n", "Units:", units);
    printf("%-16s%9.2f\n", "Charge (Rs):", charge);
    printf("%-16s%9.2f\n", "Tax (Rs):", tax);
    printf("%-16s%9.2f\n", "Total (Rs):", total);
    return 0;
}
          
  • The slab rates. The first 100 units cost ₹4.50 each, the next 100 cost ₹6.00 each, and every unit beyond 200 costs ₹8.50. A flat 5% tax applies to the charge, not to the units.
  • if / else if picks which slab formula applies — this is a preview of Day 6, where conditionals are taught properly. For now, read it as plain English: "if units is at most 100, do this; otherwise if at most 200, do this; otherwise do this."
  • %-16s left-aligns a label in a 16-character field; %9.2f right-aligns a number in 9 characters with 2 decimal places — together they line up every rupee amount in a column, the way a real bill does.
💡
Verified at the boundaries. 100 units charges only the first slab (₹450.00), 150 units crosses into the second (₹750.00), and 250 units reaches the third (₹1475.00) — each run through gcc -Wall -Wextra with zero warnings before this number was trusted.
02

Data & Operators

Day 03 · Module 2 · Data & operators

Today we cover

By the end of today you can declare a variable of the right type for any value, and explain the difference between a constant the preprocessor pastes in and one the compiler actually knows about.
What a variable costs

Every type has a size, in bytes

sizes.c
#include <stdio.h>

int main(void)
{
    printf("char        %zu byte\n",  sizeof(char));
    printf("short       %zu bytes\n", sizeof(short));
    printf("int         %zu bytes\n", sizeof(int));
    printf("long        %zu bytes\n", sizeof(long));
    printf("long long   %zu bytes\n", sizeof(long long));
    printf("float       %zu bytes\n", sizeof(float));
    printf("double      %zu bytes\n", sizeof(double));
    printf("long double %zu bytes\n", sizeof(long double));
    return 0;
}
          
  • sizeof is a compile-time operator, not a function call — it returns a size_t, an unsigned type, which is why it is printed with %zu rather than %d.
  • These numbers are machine-specific. This run is from 64-bit Windows, gcc 16.1.0 (MinGW-w64 x86_64-ucrt-posix-seh). Here long is 4 bytes; on 64-bit Linux the identical program prints 8 bytes for long, because the two platforms pick different data models for the same type.
  • The C standard only guarantees a minimum range, never an exact size — char at least 8 bits, short and int at least 16 bits, long at least 32 bits, long long at least 64 bits. Never hard-code a size in your own programs — always ask sizeof.
💡
Run sizeof on your own machine before trusting any number on this slide — that is the entire point of the operator.
Naming a box in memory

A variable is a named box of a fixed type

vars.c
#include <stdio.h>

int main(void)
{
    int rollNo = 12;              /* declared and initialised together */
    int marks1, marks2, marks3;   /* declared together, values later */

    marks1 = 78;
    marks2 = 85;
    marks3 = 91;

    int total = marks1 + marks2 + marks3;   /* declared where first needed */

    printf("Roll No: %d\n", rollNo);
    printf("Marks: %d, %d, %d\n", marks1, marks2, marks3);
    printf("Total: %d\n", total);
    return 0;
}
          
  • Declaration reserves the box and fixes its type — int rollNo means "a box exactly big enough for an int, named rollNo."
  • Initialisation gives it a value at the same moment it is declared, = 12. You can also declare several variables of the same type in one line, int marks1, marks2, marks3;, and assign each one later.
  • C lets you declare a variable anywhere before its first use, not only at the top of the block — total is declared right where it is first needed, not earlier.
💡
An uninitialised local variable holds garbage, not zero — whatever bits happened to already be sitting in that memory. Always initialise before you read.
Two ways to say "never changes"

One is text, the other is typed

constants.c
#include <stdio.h>
#define PI 3.14159      /* preprocessor: text substitution */

int main(void)
{
    const float gst = 0.18f;   /* compiler-checked, typed, scoped */
    float radius = 5.0f;
    float area = PI * radius * radius;
    float taxedPrice = 1000 * (1 + gst);

    printf("Area of circle = %.2f\n", area);
    printf("Price with GST = %.2f\n", taxedPrice);
    return 0;
}
          
#defineconst
Handled byPreprocessor, Stage 1Compiler
Has a type?No — plain textYes — typed
Has scope?No — whole file from that pointYes — block or file, like a normal variable
Compiler checks it?Never sees the nameFully type-checked
💡
Tied to Day 1's compilation pipeline. #define PI 3.14159 is erased by the preprocessor before the compiler ever runs — every PI in the file is replaced by the literal text 3.14159. const float gst is a real variable the compiler tracks, with a type and a scope, that simply refuses any code that tries to assign to it again.
Characters that don't look like characters

A backslash means "not what it looks like"

escapes.c
#include <stdio.h>

int main(void)
{
    printf("Tab:\tbetween\ttwo\twords\n");
    printf("Newline splits->\nhere\n");
    printf("Backslash: \\  Quote: \"quoted\"  Apostrophe: \'A\'\n");
    printf("Bell/alert: [\a] Carriage return: [\r]->overwritten\n");
    printf("Backspace demo: AB\bC\n");
    return 0;
}
          
  • A literal is a value written directly in source — 42, 3.14, 'A' and "hi" are all literals.
  • \n newline, \t horizontal tab, \\ a literal backslash, \" a quote mark inside a string, \' an apostrophe inside a char literal.
  • \a rings the terminal bell (silent or invisible on most modern terminals), \r returns the cursor to the start of the line without advancing it, \b backs the cursor up one column — each is exactly one character, however many symbols it takes to write it.
💡
Why the compiler needs \" and \\ at all: a bare " inside a string literal would end the string early, and a bare \ always starts an escape sequence — both must be escaped to appear as themselves.
Telling printf and scanf what type is coming

Match the specifier to the type

SpecifierTypeReads/prints
%dintSigned decimal integer
%uunsigned intUnsigned decimal integer
%ldlongSigned long integer
%ffloatDecimal, 6 places by default
%lfdoubleOnly for scanf — printf uses %f for both
%ccharA single character
%schar[]A string, up to the null terminator
%x / %ointHexadecimal / octal
%zusize_tWhat sizeof returns
Precision and width.
  • %.2f prints a float or double to exactly 2 decimal places — used on every rupee amount so far in this deck.
  • %9.2f right-aligns that same number in a field 9 characters wide, as Practical 1's bill did.
  • %-16s left-aligns a string in a field 16 characters wide.
💡
A mismatched specifier is undefined behaviour, not an error. Passing an int to %f compiles and may print nonsense, or crash — gcc -Wall warns about most mismatches, which is one more reason to always build with it.
Day 04 · Module 2 · Data & operators

Today we cover

By the end of today you can read any expression built from C's operators and predict exactly what it evaluates to, in exactly what order.
The two mistakes everyone makes first

Integer division truncates; ++ has two forms

ops.c · 1/2
#include <stdio.h>

int main(void)
{
    int a = 7, b = 2;
    float fa = 7.0f, fb = 2.0f;

    printf("int division   7/2 = %d\n", a / b);
    printf("float division 7.0/2.0=%.1f\n", fa / fb);
    printf("modulus        7 %% 2 = %d\n", a % b);

    int c = 10;
    c += 5;  printf("c += 5  -> %d\n", c);
    c -= 3;  printf("c -= 3  -> %d\n", c);
    c *= 2;  printf("c *= 2  -> %d\n", c);
    c /= 4;  printf("c /= 4  -> %d\n", c);
          
ops.c · 2/2
    int x = 5;
    printf("x = %d\n", x);
    printf("x++ evaluates to %d\n", x++);
    printf("after x++,  x = %d\n", x);
    printf("++x evaluates to %d\n", ++x);
    printf("after ++x,  x = %d\n", x);
    printf("x-- evaluates to %d\n", x--);
    printf("after x--,  x = %d\n", x);
    printf("--x evaluates to %d\n", --x);
    printf("after --x,  x = %d\n", x);
    return 0;
}
          
  • Truncation. 7 / 2 throws away the remainder and gives 3, not 3.5, because both operands are int. Make even one operand a float and the division happens in floating point.
  • % the modulus operator gives the remainder, and only works on integers — 7 % 2 is 1.
  • c += 5 is shorthand for c = c + 5. The same pattern works for -= *= /= %=.
  • Pre vs post. x++ (post-increment) hands back the old value, then increments. ++x (pre-increment) increments first, then hands back the new value — the output above shows both stages of each, so the difference is not just a rule, it is a number.
💡
The mistake you will actually make: writing average = total / count; with all three as int, then wondering why a class average of 82.4 printed as 82. Fix it the way Practical 1's tax line did — cast, or declare one operand as float.
Every future if is built from these

A comparison is just a 0 or a 1

OperatorMeans
==Equal to
!=Not equal to
> / <Greater than / less than
>= / <=Greater or equal / less or equal
&&Logical AND — true only if both sides are true
||Logical OR — true if either side is true
!Logical NOT — flips true to false
C has no separate boolean type here. Every relational or logical expression evaluates to the int 1 for true or 0 for false.
Worked example — exam eligibility.

attendance = 78, marks = 42

attendance >= 75 && marks >= 40

→ 1 && 1 → 1 (eligible)

💡
Short-circuit evaluation. In a && b, C never even evaluates b if a is already false — the answer is already known. The same applies to || when the left side is already true. This is why p != NULL && *p == 5 is safe: if p is null, the unsafe second half never runs.
Operating on individual bits

Six operators, one bit at a time

bits.c · 1/2
#include <stdio.h>

void printBits(unsigned int n)
{
    for (int bit = 7; bit >= 0; bit--)
        printf("%d", (n >> bit) & 1);
}

int main(void)
{
    unsigned int a = 12;   /* 0x0C */
    unsigned int b = 10;   /* 0x0A */

    printf("a = 12 = "); printBits(a); printf("\n");
    printf("b = 10 = "); printBits(b); printf("\n\n");
          
bits.c · 2/2
    printf("a & b  = "); printBits(a & b);  printf("  (%d)\n", a & b);
    printf("a | b  = "); printBits(a | b);  printf("  (%d)\n", a | b);
    printf("a ^ b  = "); printBits(a ^ b);  printf("  (%d)\n", a ^ b);
    printf("~a     = "); printBits(~a);     printf("  (%d)\n", ~a);
    printf("a << 2 = "); printBits(a << 2); printf("  (%d)\n", a << 2);
    printf("a >> 2 = "); printBits(a >> 2); printf("  (%d)\n", a >> 2);

    unsigned int mask = 0x0F;
    printf("\nmask = 0x0F = "); printBits(mask); printf("\n");
    printf("a & mask (low nibble) = %d\n", a & mask);
    return 0;
}
          
  • printBits prints only the low 8 bits of its argument, using (n >> bit) & 1 to peel off one bit at a time — that expression is this whole slide in miniature.
  • & 1 only where both bits are 1 — useful for masking, as the last line shows.
  • | 1 where either bit is 1. ^ (XOR) 1 where the bits differ.
  • ~ flips every bit, including bits outside the 8 this slide prints — which is why ~a reads as a large negative number, -13, once all 32 bits of the int are accounted for.
  • << / >> shift every bit left or right by the given count, filling with 0 — each shift by 1 is a multiply or divide by 2, so a << 2 is a * 4.
💡
Do not confuse & with &&, or | with ||. The single-character versions work bit by bit on numbers; the doubled versions work on truth values and short-circuit. 0x0F is the same value as decimal 15 — hex is just easier to read as four bits at a time.
The reference table

Highest binds first

LevelOperatorsMeaningAssociativity
1 (highest)() [] -> . postfix ++ --Call, index, member access, post-inc/decLeft → right
2prefix ++ -- unary +/- ! ~ * & sizeof (type)Unary: pre-inc/dec, not, complement, deref, address-of, castRight → left
3* / %MultiplicativeLeft → right
4+ -AdditiveLeft → right
5<< >>Bitwise shiftLeft → right
6< <= > >=RelationalLeft → right
7== !=EqualityLeft → right
8&Bitwise ANDLeft → right
9^Bitwise XORLeft → right
10|Bitwise ORLeft → right
11&&Logical ANDLeft → right
12||Logical ORLeft → right
13?:Ternary conditionalRight → left
14= += -= *= /= %= &= ^= |= <<= >>=AssignmentRight → left
15 (lowest),CommaLeft → right
💡
Read this table top to bottom, not left to right. A higher level always binds tighter: in 2 + 3 * 4, * (level 3) runs before + (level 4), giving 14, not 20. When in doubt, add parentheses — they cost nothing and remove all ambiguity, for you and for the next person reading the code.
When C changes a value's type for you — or you force it

Implicit happens quietly; explicit you ask for

cast.c · 1/2
#include <stdio.h>

int main(void)
{
    int i = 5;
    float f = 2.0f;
    float result = i / f;
    printf("5 / 2.0f = %.2f\n", result);

    double pi = 3.14159;
    int truncated = (int) pi;
    printf("(int) 3.14159 = %d (fraction lost)\n",
           truncated);
          
cast.c · 2/2
    int big = 130;
    signed char small = (signed char) big;
    printf("(signed char) 130 = %d (overflowed)\n",
           small);

    int marks = 8, total = 10;
    printf("(float)marks/total*100 = %.1f%%\n",
           (float) marks / total * 100);
    printf("marks/total*100 (no cast) = %d%%\n",
           marks / total * 100);
    return 0;
}
          
  • Implicit conversion. i / f mixes an int and a float — C silently promotes i to float before dividing, so the division happens in floating point without you asking.
  • Explicit casting. (int) pi forces a double down to an int, truncating the fraction. (float) marks / total forces the division into floating point before * 100 runs — without it, integer division truncates first and the whole calculation is ruined.
  • Visible data loss. signed char only holds -128 to 127. Casting 130 into it wraps around to -126 — the value is not clamped, it overflows.
💡
Cast the operand, not the result. (float) marks / total converts before dividing; (float)(marks / total) would cast an already-truncated integer and still be wrong. Where you put the parentheses changes the answer.
Module 2 practical

Simple interest, and three ways to swap

interest.c
#include <stdio.h>

int main(void)
{
    float principal, rate, time, interest;

    printf("Enter principal (Rs): ");
    scanf("%f", &principal);
    printf("Enter rate (%% per year): ");
    scanf("%f", &rate);
    printf("Enter time (years): ");
    scanf("%f", &time);

    interest = (principal * rate * time) / 100;

    printf("\nSimple interest = Rs %.2f\n", interest);
    printf("Amount payable   = Rs %.2f\n",
           principal + interest);
    return 0;
}
          
swap.c
#include <stdio.h>

int main(void)
{
    int a, b, temp;
    printf("Enter a and b: ");
    scanf("%d %d", &a, &b);
    printf("Before: a=%d, b=%d\n\n", a, b);

    temp = a; a = b; b = temp;             /* 1: temp */
    printf("Temp var: a=%d, b=%d\n", a, b);

    a = a + b; b = a - b; a = a - b;       /* 2: +/- */
    printf("Arithmetic: a=%d, b=%d\n", a, b);

    a = a ^ b; b = a ^ b; a = a ^ b;       /* 3: XOR */
    printf("XOR: a=%d, b=%d\n", a, b);
    return 0;
}
          
💡
Be honest about the last two. The arithmetic swap and the XOR swap are interview curiosities, not code you should actually write — the temp-variable version is clearer, no slower on any modern compiler, and does not risk overflow. Worse, the XOR swap breaks if a and b are the same variable (swap(&x, &x)): the first line a = a ^ b becomes x = x ^ x, which is always 0 — the variable is silently zeroed instead of left unchanged. Know the trick for the viva; use the temp variable in real code.
03

Input & Output

Day 05 · Module 3 · Input & output

Today we cover

By the end of today you can format any output to the exact width and precision you want, read input safely by checking what scanf actually did, and explain why the buffer trap happens before it happens to you.
Width, precision and the flags

Making printf line up a table

fmt.c
#include <stdio.h>

int main(void)
{
    printf("%-12s%8s\n", "Item", "Price");
    printf("%-12s%8.2f\n", "Pen", 12.5);
    printf("%-12s%8.2f\n", "Notebook", 45.0);
    printf("%-12s%8.2f\n", "Eraser", 3.75);
    printf("\n");
    printf("[%5d]\n", 42);
    printf("[%-5d]\n", 42);
    printf("[%05d]\n", 42);
    printf("[%+d] [%+d]\n", 42, -42);
    printf("[%8.3f]\n", 3.14159);
    return 0;
}
          
  • Width. The number right after %, as in %8s or %8.2f, sets the minimum field width. C pads with spaces on the left by default — that is what right-aligns the "Price" column above.
  • - flag left-aligns inside that width instead. %-12s pads the item name with spaces on the right, so every row's price column starts in the same place.
  • Precision. %.2f fixes exactly two digits after the decimal point, however many the value actually has — 45.0 becomes 45.00.
  • 0 flag pads with leading zeros instead of spaces, and only makes sense together with a width: %05d on 42 gives 00042.
  • + flag always shows the sign, even on a positive number — useful wherever the sign itself is the information, such as a profit/loss figure.
💡
Order matters. A specifier is written % flags, then width, then .precision, then the type letter — %-8.2f is the - flag, width 8, precision .2, type f. Get the order wrong and printf does not warn you — it just prints something else.
Telling scanf where to store, and asking whether it worked

scanf needs an address, not a value

readnum.c
#include <stdio.h>

int main(void)
{
    int age, ok;

    printf("Enter your age: ");
    ok = scanf("%d", &age);

    if (ok == 1) {
        printf("scanf converted %d item(s). Age read as %d.\n", ok, age);
    } else {
        printf("scanf converted %d item(s). That was not a number.\n", ok);
    }

    return 0;
}
          
  • The &. scanf("%d", &age) passes the address of age, not its value — scanf must be told where in memory to store what it reads. Forgetting the & is one of the most common first-month C bugs, and it often does not even fail to compile, only crash or store garbage.
  • The return value. scanf returns the number of items it successfully converted, not whether the call "worked" in general. Almost nobody is taught this, and it is the only reliable way to notice that the user typed something that was not a number.
  • Above, ok stores that return value, so the program can tell real input from bad input instead of silently trusting whatever ended up in age.
💡
An unchecked scanf is a trap for later. If scanf("%d", &age) fails to convert, it leaves age completely untouched — whatever was already in that memory before the call. A program that never checks the return value will happily carry on with that garbage value as if it were real input.
Reading and writing one character at a time

getchar and putchar

chario.c
#include <stdio.h>

int main(void)
{
    int ch;

    printf("Type text, end with Ctrl+D (Ctrl+Z on Windows):\n");
    while ((ch = getchar()) != EOF) {
        putchar(ch);
    }
    printf("\n-- end of input --\n");
    return 0;
}
          
  • getchar reads exactly one character from standard input and hands it back as an int, not a char.
  • Why an int. EOF is a special value outside the range of every possible char (typically -1). Storing getchar's result in a char can make a real character look like EOF and end the loop early — always declare the variable as int.
  • putchar writes one character to standard output — together with getchar this is the simplest possible echo loop, one character in, one character out.
💡
EOF is not a key on the keyboard. It is the signal "no more input is coming" — typed as Ctrl+D on Linux/macOS or Ctrl+Z then Enter on Windows, or reached automatically when reading from a file or a pipe.
The bug every beginner hits, once

Why the very next fgets reads nothing

buffer_bad.c
#include <stdio.h>

int main(void)
{
    int n;
    char name[40];

    printf("How many students? ");
    scanf("%d", &n);

    printf("Class teacher's name: ");
    fgets(name, sizeof(name), stdin);

    printf("Students: %d, Teacher: %s\n", n, name);
    return 0;
}
          
buffer_fixed.c
#include <stdio.h>

int main(void)
{
    int n, ch;
    char name[40];

    printf("How many students? ");
    scanf("%d", &n);

    while ((ch = getchar()) != '\n' && ch != EOF)
        ;

    printf("Class teacher's name: ");
    fgets(name, sizeof(name), stdin);

    printf("Students: %d, Teacher: %s\n", n, name);
    return 0;
}
          
💡
What actually happens. Typing 30 and pressing Enter sends "30\n" to standard input. scanf("%d", &n) reads only the digits and leaves that '\n' sitting in the buffer, unread. The very next input call, fgets, does not wait for the teacher's name at all — it immediately reads that leftover newline as a complete, empty line, and returns. The fix is the drain loop: while ((ch = getchar()) != '\n' && ch != EOF); reads and throws away every character up to and including that stray newline, so the following fgets starts clean. Do this after every scanf that will be followed by fgets.
Module 3 practical

Five subjects, one aligned report card

marksheet.c · 1/2
#include <stdio.h>

int main(void)
{
    char name[40];
    int eng, math, sci, sst, hindi, total;
    float pct;
    char grade;

    printf("Student name: ");
    fgets(name, sizeof(name), stdin);

    printf("Marks (out of 100 each) - English Maths Science SST Hindi: ");
    scanf("%d %d %d %d %d", &eng, &math, &sci, &sst, &hindi);

    total = eng + math + sci + sst + hindi;
    pct = (float) total / 5;
          
marksheet.c · 2/2
    if (pct >= 90)      grade = 'A';
    else if (pct >= 75) grade = 'B';
    else if (pct >= 60) grade = 'C';
    else if (pct >= 33) grade = 'D';
    else                grade = 'F';

    printf("\n---- Mark Sheet ----\n");
    printf("Name: %s", name);
    printf("%-10s%6d\n", "English", eng);
    printf("%-10s%6d\n", "Maths", math);
    printf("%-10s%6d\n", "Science", sci);
    printf("%-10s%6d\n", "SST", sst);
    printf("%-10s%6d\n", "Hindi", hindi);
    printf("%-10s%6d\n", "Total", total);
    printf("Percentage: %.2f%%\n", pct);
    printf("Grade: %c\n", grade);

    return 0;
}
          
  • fgets reads the name first, before any scanf call, so there is no leftover newline to trap it — exactly the fix from the previous slide, applied by simply choosing the right order.
  • One scanf, five values. "%d %d %d %d %d" reads all five marks in a single call; the spaces between specifiers let scanf skip over any amount of whitespace the user types between numbers.
  • %-10s%6d left-aligns the subject name in a 10-character field and right-aligns its mark in a 6-character field — the same width trick from printf in depth, now building a real report card.
💡
The percentage cast. pct = (float) total / 5; casts total before dividing, exactly as Practical 2's tax line did — without it, five integer marks divided by the integer 5 would truncate the percentage to a whole number.
04

Control Flow

Day 06 · Module 4 · Control flow

Today we cover

By the end of today you can build a correct else-if ladder, brace every nested if on sight, and explain exactly what a missing break does inside a switch.
Branching on a condition

The ladder checks top to bottom, and stops

grade_wrong.c
#include <stdio.h>

int main(void)
{
    int marks = 82;

    if (marks >= 33)
        printf("Grade: D\n");
    else if (marks >= 60)
        printf("Grade: C\n");
    else if (marks >= 75)
        printf("Grade: B\n");
    else if (marks >= 90)
        printf("Grade: A\n");
    else
        printf("Grade: F\n");

    return 0;
}
          
grade.c
#include <stdio.h>

int main(void)
{
    int marks = 82;

    if (marks >= 90)
        printf("Grade: A\n");
    else if (marks >= 75)
        printf("Grade: B\n");
    else if (marks >= 60)
        printf("Grade: C\n");
    else if (marks >= 33)
        printf("Grade: D\n");
    else
        printf("Grade: F\n");

    return 0;
}
          
💡
An else-if ladder stops at the first true condition and never looks at the rest. With marks = 82, testing >= 33 first is true immediately, so the ladder prints D and never even reaches the >= 90 or >= 75 branches that were actually meant to catch it. The fix is not a different condition — it is the same conditions in the opposite order, most restrictive first, so a mark of 82 only passes the >= 90 test if it is truly 90 or above.
Two bugs almost every beginner writes once

One equals sign, one missing brace

trap_assign.c
#include <stdio.h>

int main(void)
{
    int x = 0;

    if (x = 5) {
        printf("condition was true, x is now %d\n", x);
    } else {
        printf("condition was false, x is now %d\n", x);
    }

    return 0;
}
          
trap_else.c
#include <stdio.h>

int main(void)
{
    int a = 3, b = -7;

    if (a > 0)
        if (b > 0)
            printf("both positive\n");
    else
        printf("a is not positive\n");

    printf("done\n");

    return 0;
}
          
💡
= is not ==. if (x = 5) does not compare — it assigns 5 to x and then tests that assignment's value, which is always 5, always true. GCC warns, but the program still compiles and runs, silently wrong; the fix is if (x == 5). An else always binds to the nearest unmatched if. Above, with a = 3 (positive) and b = -7 (not), the writer meant the else to answer the outer if (a > 0), but with no braces it binds to the inner if (b > 0) instead — so the program wrongly claims a is not positive. Brace every nested if, always, even a one-line body.
Grouping cases on purpose

Falling through is a choice, not an accident

menu.c
#include <stdio.h>

int main(void)
{
    int day = 6;

    switch (day) {
        case 1: case 2: case 3: case 4: case 5:
            printf("Weekday - school is open\n");
            break;
        case 6: case 7:
            printf("Weekend - school is closed\n");
            break;
        default:
            printf("Not a valid day number\n");
    }

    return 0;
}
          
  • Stacked case labels. case 1: through case 5: with no statement between them all fall into the same printf — this is fall-through used deliberately, to group days that share an outcome.
  • break leaves the switch the moment a matching group has run its statement, so execution never spills into the next group below it.
  • default catches any value that matched no case at all — here, anything outside 1–7. It does not need its own break since nothing follows it.
💡
A missing break is C's default, and it is almost always a bug. Leave one out between two groups that were not meant to share code, and control quietly falls into the next case's statements too — no error, no warning by default, just the wrong output. Every case needs a break (or a return) unless the fall-through is intentional and, ideally, commented.
A jump instruction, dressed as a keyword

It works. That is not the same as good

goto.c
#include <stdio.h>

int main(void)
{
    int i = 1;

top:
    if (i > 5)
        goto done;
    printf("%d ", i);
    i++;
    goto top;

done:
    printf("\ndone\n");
    return 0;
}
          
goto_better.c
#include <stdio.h>

int main(void)
{
    int i;

    for (i = 1; i <= 5; i++) {
        printf("%d ", i);
    }
    printf("\ndone\n");

    return 0;
}
          
💡
Same output, very different program. goto.c rebuilds a loop by hand out of a label and a jump — it works, but nothing in the text tells you it is a loop until you trace the jumps yourself, and in a longer program goto can jump anywhere, forwards or backwards, in or out of blocks, which is exactly what makes hand-built control flow like this hard to read and easy to break. goto_better.c says the same thing with a for loop, whose start, end and step are visible in one line. This course avoids goto by convention, the way almost every real C codebase does — not because it is banned outright, but because the two cases where it genuinely earns its place are narrow and worth naming: escaping a deeply nested loop, where a single break or continue only affects the innermost loop and reaching an outer one otherwise needs a flag variable checked at every level; and centralised cleanup, where a function that has acquired several resources jumps every failure to one cleanup: label instead of repeating the release code at each exit point — a pattern C leans on precisely because it has no destructors or finally. Outside those two, reach for the structured loop every time.
Day 07 · Module 4 · Control flow

Today we cover

By the end of today you can pick the right loop for a job without guessing, trace what a loop does line by line on paper, and build a menu that keeps asking until the user quits.
Checking before the body runs, or after

The same sum, three shapes

loops.c
#include <stdio.h>

int main(void)
{
    int n = 5, i, sum;

    sum = 0; i = 1;
    while (i <= n) { sum += i; i++; }
    printf("while    : sum 1 to %d = %d\n", n, sum);

    sum = 0; i = 1;
    do { sum += i; i++; } while (i <= n);
    printf("do-while : sum 1 to %d = %d\n", n, sum);

    sum = 0;
    for (i = 1; i <= n; i++) sum += i;
    printf("for      : sum 1 to %d = %d\n", n, sum);

    return 0;
}
          
  • while is entry-controlled — it tests the condition before every iteration, including the first. If the condition is false to start with, the body never runs at all.
  • do-while is exit-controlled — the body runs first, and the condition is only tested after, at the bottom. The body always runs at least once, even if the condition was false from the start.
  • All three loops above compute the identical sum — the difference is never what they compute here, only when the condition gets checked.
💡
Picture n = 0. while (i <= n) would test 1 <= 0, find it false immediately, and skip the body completely — the while and for versions would both print a sum of 0 without ever entering the loop. do-while cannot do that: its body always executes once before the condition is even looked at. That one guarantee is exactly why Practical 4's menu, later today, is built on do-while — a menu must be shown at least once, before there is anything to check.
Three clauses, one line

Init; condition; update

for_anatomy.c
#include <stdio.h>

int main(void)
{
    int i, j;

    for (i = 1, j = 5; i <= 5; i++, j--) {
        printf("i=%d j=%d\n", i, j);
    }

    for (i = 0; i < 10; i += 2) {
        printf("%d ", i);
    }
    printf("\n");

    return 0;
}
          
  • Init i = 1, j = 5 runs exactly once, before the loop starts. The comma lets you initialise more than one variable — here, two counters moving in opposite directions.
  • Condition i <= 5 is tested before every iteration, first loop included. The moment it is false, the loop ends and the body does not run again.
  • Update i++, j-- runs after each iteration's body, right before the condition is checked again — not once, not at the start.
  • The second loop shows the update clause is not stuck at i++: i += 2 steps by two just as easily.
💡
All three clauses are optional; the two semicolons are not. for (;;) is a legal, deliberate infinite loop with no init, condition or update at all — you will see it later paired with a break inside. Forgetting the update clause by accident, though, is the most common way a beginner writes an infinite loop unintentionally: the condition never changes, so it is never false.
Three shapes, one decision

Let the problem pick the loop

LoopUse it whenOne-line example
forThe number of iterations is known before the loop starts — a fixed count, an array length, a row of a table.for (i = 0; i < n; i++)
whileYou loop until some condition changes, and you cannot say in advance how many times that will take.while (scanf("%d", &n) == 1)
do-whileThe body must run at least once, before there is anything to check — a menu, or a prompt that has to appear before its own input is validated.do { ... } while (choice != 5);
💡
Any of the three can technically do any of these jobs — that is not the point. Reaching for the shape whose built-in guarantee matches the problem tells the next reader, immediately, what kind of loop this is without them tracing the condition by hand. A for loop over a known range, a while loop that waits for a sentinel, a do-while menu — each one announces its own shape before you read a single line of its body.
Leaving early, or skipping ahead

break exits; continue skips

break_demo.c
#include <stdio.h>

int main(void)
{
    int n = 37, i;

    for (i = 2; i <= n / 2; i++) {
        if (n % i == 0) {
            printf("First factor found: %d\n", i);
            break;
        }
    }
    if (i > n / 2) {
        printf("No factor found up to n/2 - %d is prime\n", n);
    }

    return 0;
}
          
continue_demo.c
#include <stdio.h>

int main(void)
{
    int i;

    printf("Odd numbers from 1 to 10: ");
    for (i = 1; i <= 10; i++) {
        if (i % 2 == 0) {
            continue;
        }
        printf("%d ", i);
    }
    printf("\n");

    return 0;
}
          
💡
break leaves the loop immediately and jumps to the statement right after it — above, the moment a factor is found, there is no reason to keep dividing, so the search stops there. continue does not leave the loop; it skips straight to the next iteration, so the rest of that one pass through the body is abandoned but the loop itself carries on — above, an even i is skipped, not printed, and the loop moves on to the next value. Both jump control flow around the normal top-to-bottom read of the body, so use them where they genuinely simplify a search or a filter, not as a habit.
One loop for the row, one for the column

Four patterns, one idea

patterns.c · 1/2
#include <stdio.h>

int main(void)
{
    int n = 5, i, j, num;

    printf("Right triangle:\n");
    for (i = 1; i <= n; i++) {
        for (j = 1; j <= i; j++) printf("*");
        printf("\n");
    }

    printf("\nInverted triangle:\n");
    for (i = n; i >= 1; i--) {
        for (j = 1; j <= i; j++) printf("*");
        printf("\n");
    }
          
patterns.c · 2/2
    printf("\nPyramid:\n");
    for (i = 1; i <= n; i++) {
        for (j = 1; j <= n - i; j++) printf(" ");
        for (j = 1; j <= 2 * i - 1; j++) printf("*");
        printf("\n");
    }

    printf("\nFloyd's triangle:\n");
    num = 1;
    for (i = 1; i <= n; i++) {
        for (j = 1; j <= i; j++) printf("%d ", num++);
        printf("\n");
    }

    return 0;
}
          
  • Outer loop, inner loop. In every pattern here the outer for (i ...) picks the row, and the inner for (j ...) decides what fills that row — the inner loop runs to completion once for every single value of i.
  • The pyramid uses two inner loops. One prints the leading spaces, the other prints the stars — run one after the other on the same row, not nested inside each other.
  • num++ in Floyd's triangle keeps counting upward across every row and never resets — the one pattern here where the printed value is not derived from i or j directly.
💡
The pyramid's space count is the part everyone gets wrong first. Row i needs n - i leading spaces, not i — the last row (i = n) needs zero spaces and the widest row of stars, so the space count must shrink as i grows, exactly opposite to the star count, which is 2i - 1 and grows.
Tracing a loop by hand, one iteration at a time

Reversing 1234, step by step

reverse.c
#include <stdio.h>

int main(void)
{
    int n = 1234, digit, rev = 0;

    while (n != 0) {
        digit = n % 10;
        rev = rev * 10 + digit;
        n = n / 10;
    }

    printf("Reversed number = %d\n", rev);

    return 0;
}
          
ndigitrev
1234—0
12344
12343
12432
014321
💡
The first row is the state before the loop body has run even once — n still holds the full number and rev is still 0. Every row after that shows n, digit and rev once that pass's three statements have all executed, in order: peel off the last digit, fold it into rev, then chop it off n. The condition n != 0 decides whether there is a next row at all — the moment n reaches 0 the loop stops, with rev holding the fully reversed number. This is exactly the table a dry-run question on an exam paper expects.
Module 4 practical

A menu that keeps asking, until you quit

calc.c · 1/2
#include <stdio.h>

int main(void)
{
    int choice, ok;
    float a, b;
    char junk;

    do {
        printf("\n1.Add 2.Sub 3.Mul 4.Div 5.Quit\nChoice: ");
        ok = scanf("%d", &choice);
        if (ok != 1) {
            printf("Not a number.\n");
            while ((junk = getchar()) != '\n' && junk != EOF)
                ;
            continue;
        }
          
calc.c · 2/2
        if (choice == 5) { printf("Goodbye.\n"); break; }
        if (choice < 1 || choice > 5) { printf("Choose 1-5.\n"); continue; }
        printf("Enter two numbers: ");
        scanf("%f %f", &a, &b);
        switch (choice) {
            case 1: printf("Result: %.2f\n", a + b); break;
            case 2: printf("Result: %.2f\n", a - b); break;
            case 3: printf("Result: %.2f\n", a * b); break;
            case 4:
                if (b == 0)
                    printf("Cannot divide by zero.\n");
                else
                    printf("Result: %.2f\n", a / b);
                break;
        }
    } while (choice != 5);

    return 0;
}
          
  • do-while shows the menu before checking anything — exactly the case from the earlier slide where the body has to run at least once, before there is any input to validate.
  • Checking scanf's return value. ok = scanf("%d", &choice) and then if (ok != 1) is the same check from Day 5 — typing x instead of a number is caught here instead of quietly leaving choice holding garbage, and the drain loop clears the bad input from the buffer before the menu shows again.
  • The division guard. if (b == 0) inside case 4 stops a crash before it happens — dividing by zero on a float would not even crash, it would silently print inf, so the guard is what makes the program trustworthy.
💡
Out-of-range and not-a-number are two different problems, checked two different ways. choice < 1 || choice > 5 catches a valid number that is simply the wrong one, like 9; the earlier ok != 1 check catches input that was never a number at all. Confusing the two, or checking only one, is exactly how a "robust" menu still crashes on the first bad keystroke.
05

Functions & Recursion

Day 08 · Module 5 · Functions

Today we cover

By the end of today you can split a program across several functions, predict exactly which variables each one can see, and explain why a swap function that looks correct still fails.
One idea, one name, one place

Write it once, call it as often as you like

why.c
#include <stdio.h>

float average(float a, float b)
{
    return (a + b) / 2.0f;
}

int main(void)
{
    printf("Average of 10, 20 = %.2f\n", average(10, 20));
    printf("Average of 55, 65 = %.2f\n", average(55, 65));
    printf("Average of 8, 12 = %.2f\n", average(8, 12));
    return 0;
}
          
  • No repetition. Without average, the same (a + b) / 2.0f expression would be typed out, and possibly mistyped, three separate times.
  • One name for one idea. average(10, 20) reads as what it does, not how it does it — the caller does not need to know the formula at all.
  • Testable in isolation. A function can be checked on its own, with known inputs and a known expected output, before it is ever wired into a bigger program.
💡
A main that keeps growing is a warning sign. Every time the same few lines get copied and pasted with small changes, that is a function waiting to be written. Smaller, named pieces are easier to read, easier to test, and easier to fix — a bug in average only needs fixing in one place, not three.
Three different moments, three different jobs

C reads top to bottom, once

funcs.c
#include <stdio.h>

int cube(int n);

int main(void)
{
    int x = 5;
    printf("cube(%d) = %d\n", x, cube(x));
    return 0;
}

int cube(int n)
{
    return n * n * n;
}
          
  • Declaration (prototype). int cube(int n); above main tells the compiler cube's name, return type and parameter types — nothing about how it works yet.
  • Definition. The block after main, with a body in { }, is where cube actually does its work.
  • Call. cube(x) inside main is where the function actually runs, using whatever value x holds at that moment.
💡
Why the prototype is needed at all. C compiles top to bottom in a single pass. By the time it reaches cube(x) inside main, it has not yet seen cube's real definition below — without the prototype, the compiler would not know cube exists, what it returns, or what to pass it. Delete that one line and try compiling: GCC refuses, with implicit declaration of function 'cube'.
The vocabulary examiners actually ask for

One is a name, the other is a value

params.c
#include <stdio.h>

int power(int base, int exp);

int main(void)
{
    int b = 2, e = 10;
    printf("%d^%d = %d\n", b, e, power(b, e));
    return 0;
}

int power(int base, int exp)
{
    int result = 1;
    for (int i = 0; i < exp; i++)
        result *= base;
    return result;
}
          
  • Parameter. base and exp in int power(int base, int exp) are parameters — the named variables a function's definition declares to receive values.
  • Argument. b and e passed at power(b, e) are arguments — the actual values supplied at the call.
  • Order matters, names do not. The argument b is copied into parameter base only because it is first; the parameter could have been named anything at all.
💡
Exam vocabulary. "Parameter" and "argument" are often used loosely in conversation, but Indian board and university papers mark them as distinct terms: the parameter lives in the function's own definition, the argument is what the caller hands over. Get the direction right — a parameter receives, an argument supplies.
What actually gets copied

C copies, always

byvalue.c
#include <stdio.h>

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

int main(void)
{
    int x = 10, y = 20;
    swap(x, y);
    printf("x = %d, y = %d\n", x, y);
    return 0;
}
          
  • swap receives copies of x and y, stored in its own local parameters a and b.
  • Inside swap, a and b really do swap — but they are swapping two copies that live only for the duration of the call, then vanish.
  • x and y in main were never touched. The function looks correct and compiles cleanly, yet it fails at the one job it was written for.
💡
Remember this failure — it is the whole reason pointers exist. Every argument in C is passed by value: the function gets a copy, never the original variable. There is no way to write a working swap with plain int parameters, no matter how it is rearranged. In Module 7, swap will be rewritten to take addresses instead of values — that one change is what finally lets a function reach back and modify its caller's variables.
Where a name is visible

A local name hides a global one

scope.c
#include <stdio.h>

int count = 100;

void show(void)
{
    int count = 5;
    printf("inside show : local count = %d\n", count);
}

int main(void)
{
    printf("in main, before call : count = %d\n", count);
    show();
    printf("in main, after call  : count = %d\n", count);
    count = 200;
    printf("in main, after change: count = %d\n", count);
    return 0;
}
          
  • Global scope. int count = 100;, declared outside every function, is visible to every function in the file, from the point it is declared onward.
  • Local scope. show's own int count = 5; exists only inside show, from that line until show's closing brace — then it is gone.
  • Shadowing. Inside show, the local count hides the global one completely; the global is unreachable there by that name at all.
💡
The global was never touched by show. show's count = 5 creates and modifies its own local copy; when main checks its global count right after the call, it is still 100, unaffected. Only main's own assignment, count = 200, changes the global — because main has no local count of its own to shadow it with.
How long a variable lives, and who can see it

Four keywords, two questions

KeywordStorage durationScopeLinkage
autoCreated on entry to the block, destroyed on exit — the default for every local variableLocal to its blockNone — every block's own copy is a separate object
staticCreated once, survives for the whole program runLocal to its block (or to the file, if declared outside every function)None inside a function; internal if declared at file scope — shared only within that one file
registerCreated on entry, destroyed on exit — a hint to keep it in a fast CPU registerLocal to its blockNone
externSame as the object it refers to — almost always the whole program runFrom its declaration to the end of the file (or block) it appears inExternal — refers to the same one object in every file that declares it

Default value if never explicitly initialised: auto and register hold garbage; static and extern are guaranteed 0.

💡
Scope and linkage are not the same question. Scope asks where in the source a name is visible; linkage asks whether two declarations of that name, possibly in different files, refer to the same object. An extern declaration's own scope is just the file it sits in — it is linkage, not scope, that lets it reach across files to the one real definition.
⚠
register in practice. Modern compilers allocate registers far better than a programmer's guess, so the hint is largely ignored today — it survives mostly in older code and exam papers. The one restriction the standard still enforces: you cannot take the address of a register variable. int *p = &x; on a register int x fails to compile; GCC refuses it outright with error: address of register variable 'x' requested.
storage.c
#include <stdio.h>

void track(void)
{
    static int calls = 0;
    int local = 0;
    calls++;
    local++;
    printf("call: static calls = %d, local = %d\n", calls, local);
}

int main(void)
{
    track();
    track();
    track();
    return 0;
}
          
💡
local resets to 0 on every call, since it is auto by default. calls is created only once, the first time track runs — every later call finds it already holding whatever it was left at, which is exactly how it keeps counting up.
helper.c
#include <stdio.h>

int total = 0;

void add_to_total(int n)
{
    total += n;
    printf("added %d, running total = %d\n", n, total);
}
          
main.c
#include <stdio.h>

extern int total;
void add_to_total(int n);

int main(void)
{
    add_to_total(10);
    add_to_total(25);
    printf("final total in main = %d\n", total);
    return 0;
}
          
💡
This is the course's first multi-file build. total is defined exactly once, in helper.c. main.c never defines it — its extern int total; only tells the compiler "this variable exists somewhere, trust me, link it in later." Compiling both files together, gcc main.c helper.c -o app, is what lets the linker resolve that promise, matching main.c's extern declaration to helper.c's real definition so both files share the one total.
Day 09 · Module 5 · Functions

Today we cover

By the end of today you can write a recursive function with a correct base case, trace its call stack by hand, and explain when a loop would have been the better choice.
A function that calls itself

Every call needs a way to stop

recurse.c · 1/2
#include <stdio.h>

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

int fibonacci(int n)
{
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

int gcd(int a, int b)
{
    if (b == 0) return a;
    return gcd(b, a % b);
}
          
recurse.c · 2/2
int main(void)
{
    printf("factorial(6) = %ld\n", factorial(6));
    printf("fibonacci(8) = %d\n", fibonacci(8));
    printf("gcd(48, 18) = %d\n", gcd(48, 18));
    return 0;
}
          
  • Base case. The condition that stops the chain — n <= 1 for factorial and fibonacci, b == 0 for gcd. It answers directly, with no further call.
  • Recursive case. Everything else — the function calls itself with an argument that is closer to the base case than the one it received.
  • fibonacci calls itself twice per call, branching into a tree rather than a single chain; factorial and gcd each call themselves once, in a straight line.
💡
Every recursive function needs a base case — no exceptions. Delete the if (n <= 1) return 1; line from factorial and the calls never stop: factorial(1) calls factorial(0), which calls factorial(-1), and so on forever. Each call still needs its own space in memory to run, and that space runs out — the program crashes with a stack overflow. The exact message is platform-specific, but the cause is always the same: a base case that was missing, or unreachable.
Where a pending call waits

Four frames for factorial(4)

CallReturnsValue
factorial(4)4 * factorial(3)— waiting
factorial(3)3 * factorial(2)— waiting
factorial(2)2 * factorial(1)— waiting
factorial(1)base case1
factorial(2)2 * 12
factorial(3)3 * 26
factorial(4)4 * 624

Seven rows, but only four frames on the stack: factorial(4), (3) and (2) each appear twice — once pushed, once popped — while factorial(1), the base case, resolves instantly and needs only one row.

Pushing. Each call to factorial opens a new stack frame — its own private copy of n and its own place to resume once the call it is waiting on returns. factorial(4) cannot finish its multiplication until factorial(3) returns a value, so it waits, and the stack grows: four frames deep before anything returns.
Unwinding. factorial(1) hits the base case and returns 1 immediately, no further call needed. That answer lets factorial(2)'s pending multiplication finally run, which lets factorial(3)'s run, which lets factorial(4)'s run — each return pops one frame off the stack.
💡
Every frame costs real memory. Four frames for factorial(4) is nothing, but a recursive call with no base case, or one whose base case is never reached, keeps pushing new frames forever — the stack has a fixed size, and once it is exhausted the program crashes with a stack overflow. Depth is not free.
Two honest trade-offs, not a verdict

Same answer, different cost

fact_rec.c
long factorial(int n)
{
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}
          
fact_iter.c
long factorial(int n)
{
    long result = 1;
    for (int i = 2; i <= n; i++)
        result *= i;
    return result;
}
          
💡
Identical output, very different memory use. fact_rec.c opens a new stack frame for every call — six frames for factorial(6), growing with n. fact_iter.c uses one result variable and one loop, in constant stack space no matter how large n gets. For a plain countdown to a base case like this, the loop is strictly cheaper and just as clear — there is no real argument for the recursive version here.
hanoi.c
#include <stdio.h>

void hanoi(int n, char from, char aux, char to)
{
    if (n == 0) return;
    hanoi(n - 1, from, to, aux);
    printf("Move disc %d from %c to %c\n", n, from, to);
    hanoi(n - 1, aux, from, to);
}

int main(void)
{
    hanoi(3, 'A', 'B', 'C');
    return 0;
}
          
  • hanoi(n, ...) moves n - 1 discs out of the way, moves disc n itself, then moves those n - 1 discs back onto it — the code is the problem's own definition, almost word for word.
  • Writing this iteratively is possible, but it needs an explicit stack data structure to track what the call stack was doing for free — the recursive version is shorter and, once you trust it, easier to read.
💡
This is where recursion earns its keep. Tower of Hanoi has no natural loop — the problem itself is defined in terms of a smaller version of itself, so a recursive solution mirrors it directly. Neither shape is universally better: pick recursion when the problem's own definition is recursive, and a loop when it genuinely is not, remembering that recursion always costs a stack frame per call and can overflow on deep input.
Module 5 practical

One header, one library, three files

mathlib.h
#ifndef MATHLIB_H
#define MATHLIB_H

int is_prime(int n);
int gcd(int a, int b);
long power(int base, int exp);
long factorial(int n);
int reverse_digits(int n);

#endif
          
mathlib.c · 1/2
#include "mathlib.h"

int is_prime(int n)
{
    int i;
    if (n < 2) return 0;
    for (i = 2; i * i <= n; i++)
        if (n % i == 0) return 0;
    return 1;
}

int gcd(int a, int b)
{
    if (b == 0) return a;
    return gcd(b, a % b);
}
          
mathlib.c · 2/2
long power(int base, int exp)
{
    if (exp == 0) return 1;
    return base * power(base, exp - 1);
}

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

int reverse_digits(int n)
{
    int rev = 0;
    while (n != 0) { rev = rev * 10 + n % 10; n /= 10; }
    return rev;
}
          
main.c
#include <stdio.h>
#include "mathlib.h"

int main(void)
{
    printf("is_prime(29) = %d\n", is_prime(29));
    printf("is_prime(30) = %d\n", is_prime(30));
    printf("gcd(84, 30) = %d\n", gcd(84, 30));
    printf("power(2, 10) = %ld\n", power(2, 10));
    printf("factorial(7) = %ld\n", factorial(7));
    printf("reverse_digits(9876) = %d\n", reverse_digits(9876));
    return 0;
}
          
  • The include guard. #ifndef / #define / #endif in mathlib.h stops the same declarations being read twice if the header is ever included from more than one place.
  • Three files, one program. mathlib.h declares; mathlib.c defines; main.c only ever #includes the header, never the .c file.
💡
This is the shape every real C project uses. gcc main.c mathlib.c -o mathutil compiles both source files and links them together; the compiler needs mathlib.h just to know each function's signature while compiling main.c, and the linker needs mathlib.c's object code to supply the bodies. This exact pattern — a header of declarations, a source file of definitions, and callers that only see the header — is what the capstone project will be built from.
06

Arrays & Strings

Day 10 · Module 6 · Arrays & strings

Today we cover

By the end of today you can traverse and search an array, explain the comparison-count difference between linear and binary search, and trace a bubble, selection or insertion sort pass by hand.
Where the elements actually live

One block, laid out back to back

ElementAddressOffset from base
a[0]20000
a[1]20044
a[2]20088
a[3]201212
a[4]201616

Illustrative addresses for int a[5] with base address 2000 and sizeof(int) == 4 — real addresses depend on the machine and change on every run.

The array is one block. int a[5] reserves 5 × sizeof(int) bytes, contiguous in memory, all at once. a itself names the address of a[0] — the base address.
Indexing is arithmetic. To reach a[i], C computes base address + i × sizeof(int). That is not a metaphor — it is exactly the calculation the compiler generates, which is why a[i] and *(a + i) are two spellings of the identical operation.
💡
This is the bridge to Module 7. Right now, a + i is described in words — "the address i elements past the base". Once pointers are introduced, that description becomes literal: a decays to a pointer to its first element, and a + i is real pointer arithmetic that the compiler scales by sizeof(int) automatically. Everything on this slide is exact, not approximate — pointers do not change the model, they just give you the vocabulary to write it directly.
Syntax, initialisation, and counting elements

Declare it, fill it, walk it

arr.c
#include <stdio.h>

int main(void)
{
    int marks[5] = {78, 65, 90, 42, 88};   /* size fixed by the compiler */
    int scores[5] = {50, 60};              /* rest are zero-initialised */
    int n = sizeof(marks) / sizeof(marks[0]);
    int i;
    for (i = 0; i < n; i++)
        printf("marks[%d] = %d\n", i, marks[i]);
    for (i = 0; i < 5; i++)
        printf("scores[%d] = %d\n", i, scores[i]);
    printf("Number of elements in marks = %d\n", n);
    return 0;
}
          
  • Declaration. int marks[5] reserves five ints. The size in the brackets must be a constant known at compile time for an ordinary array like this.
  • Partial initialisation. int scores[5] = {50, 60} sets the first two elements and zero-fills the rest — C guarantees this, it is not garbage.
  • Counting with sizeof. sizeof(marks) / sizeof(marks[0]) divides the array's total size by one element's size, giving the element count without hard-coding 5 anywhere.
⚠
That sizeof trick only works here. It relies on marks still being a real array in the scope where it is used. Pass it into a function and it decays to a plain pointer — sizeof on a pointer gives the pointer's own size, not the array's, a mistake that silently produces the wrong count. Module 7 explains exactly why.
Same question, two different costs

Sorted data lets you skip half

search.c · 1/3
#include <stdio.h>

int linear_search(int a[], int n, int key, int *cmp)
{
    int i;
    *cmp = 0;
    for (i = 0; i < n; i++) {
        (*cmp)++;
        if (a[i] == key) return i;
    }
    return -1;
}
          
search.c · 2/3
int binary_search(int a[], int n, int key, int *cmp)
{
    int lo = 0, hi = n - 1;
    *cmp = 0;
    while (lo <= hi) {
        int mid = (lo + hi) / 2;
        (*cmp)++;
        if (a[mid] == key) return mid;
        if (a[mid] < key) lo = mid + 1;
        else hi = mid - 1;
    }
    return -1;
}
          
search.c · 3/3
int main(void)
{
    /* binary search requires a sorted array */
    int arr[10] = {3, 8, 12, 19, 24, 31, 40, 55, 67, 78};
    int n = 10, key, pos, cmp;
    key = 67;
    pos = linear_search(arr, n, key, &cmp);
    printf("linear : key %d -> index %d, comparisons %d\n", key, pos, cmp);
    pos = binary_search(arr, n, key, &cmp);
    printf("binary : key %d -> index %d, comparisons %d\n", key, pos, cmp);
    key = 3;
    pos = linear_search(arr, n, key, &cmp);
    printf("linear : key %d -> index %d, comparisons %d\n", key, pos, cmp);
    pos = binary_search(arr, n, key, &cmp);
    printf("binary : key %d -> index %d, comparisons %d\n", key, pos, cmp);
    return 0;
}
          
  • Linear search checks every element in order until it finds the key or runs out — it needs no sorting, but a key near the end costs almost n comparisons.
  • Binary search only works on a sorted array. It checks the middle element and, since the array is sorted, immediately discards the half that cannot contain the key.
  • Searching for 67 in this 10-element array: linear takes 9 comparisons (it is near the end), binary takes only 3. Searching for 3, right at the start, linear takes just 1 — but binary still takes 3, because it always halves the range no matter where the key sits.
💡
O(n) versus O(log n), in words. Linear search's cost grows in direct proportion to the array's size — double the elements, and the worst case roughly doubles too. Binary search's cost grows with how many times the array can be halved before one element is left — for 10 elements that is about 4 halvings, for 10,000 elements it is only about 14. That gap is why binary search is worth the one-time cost of keeping the data sorted.
Watch the array change, pass by pass

Two ways to sort by swapping

sorts.c · 1/3
#include <stdio.h>

void print_arr(int arr[], int n)
{
    int i;
    for (i = 0; i < n; i++)
        printf("%d ", arr[i]);
    printf("\n");
}
          
sorts.c · 2/3
void bubble_sort(int arr[], int n)
{
    int i, j, t;
    for (i = 0; i < n - 1; i++) {
        for (j = 0; j < n - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                t = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = t;
            }
        }
        printf("After pass %d: ", i + 1);
        print_arr(arr, n);
    }
}
          
sorts.c · 3/3
void selection_sort(int arr[], int n)
{
    int i, j, min, t;
    for (i = 0; i < n - 1; i++) {
        min = i;
        for (j = i + 1; j < n; j++) {
            if (arr[j] < arr[min])
                min = j;
        }
        t = arr[i];
        arr[i] = arr[min];
        arr[min] = t;
        printf("After pass %d: ", i + 1);
        print_arr(arr, n);
    }
}
          
  • Bubble sort. Each pass walks the array comparing neighbours and swapping the out-of-order ones, so the largest unsorted value "bubbles" to its final position at the end of every pass.
  • Selection sort. Each pass scans the remaining unsorted part for its minimum, then swaps it directly into place — exactly one swap per pass, no matter how unsorted the rest is.
  • Both need at most n - 1 passes, and both sort this six-element array fully by pass 4 — but selection sort still runs pass 5 even though nothing is left to fix, since it has no way to notice early that the array is already sorted.
💡
Printing after every pass is the whole point. A sort's final output only ever shows the finished array; watching it change pass by pass is what makes bubble sort's neighbour-swapping and selection sort's find-the-minimum visibly different mechanisms, even though they end at the same sorted array.
Building a sorted section one item at a time

Like sorting cards in your hand

sorts.c
void insertion_sort(int arr[], int n)
{
    int i, j, key;
    for (i = 1; i < n; i++) {
        key = arr[i];
        j = i - 1;
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key;
        printf("After pass %d: ", i);
        print_arr(arr, n);
    }
}
          
  • The sorted section grows by one. Before pass i begins, arr[0..i-1] is already sorted. The pass takes arr[i] as the key and shifts every larger element in the sorted section one step right to make room for it.
  • Pass 3 does nothing visible. 37 is already bigger than everything to its left, so the while loop's condition is false immediately — the array does not change, but the sorted section has still grown to include it.
  • This is the same motion as sorting playing cards by hand: pick up the next card, and slide it left past every card bigger than it.
💡
Insertion sort is fast on nearly-sorted data. If the array were already sorted, every while condition would fail on its first check, and the whole sort would cost only n - 1 comparisons total — far cheaper than bubble or selection sort's fixed number of passes. That is why it is the sort real libraries fall back to for small or almost-sorted inputs.
Day 11 · Module 6 · Arrays & strings

Today we cover

By the end of today you can work with a 2D array, explain exactly why a string needs its terminator, and write a small library of your own string functions.
An array of arrays — rows and columns

Two indices, one block of memory

matrix.c · 1/3
#include <stdio.h>

#define N 3

void print_matrix(int m[N][N])
{
    int i, j;
    for (i = 0; i < N; i++) {
        for (j = 0; j < N; j++)
            printf("%4d", m[i][j]);
        printf("\n");
    }
}
          
matrix.c · 2/3
void matrix_add(int a[N][N], int b[N][N], int sum[N][N])
{
    int i, j;
    for (i = 0; i < N; i++)
        for (j = 0; j < N; j++)
            sum[i][j] = a[i][j] + b[i][j];
}
void matrix_multiply(int a[N][N], int b[N][N], int prod[N][N])
{
    int i, j, k;
    for (i = 0; i < N; i++) {
        for (j = 0; j < N; j++) {
            prod[i][j] = 0;
            for (k = 0; k < N; k++) prod[i][j] += a[i][k] * b[k][j];
        }
    }
}
          
matrix.c · 3/3
int main(void)
{
    int a[N][N] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
    int b[N][N] = {{9, 8, 7}, {6, 5, 4}, {3, 2, 1}};
    int sum[N][N], prod[N][N];
    matrix_add(a, b, sum);
    matrix_multiply(a, b, prod);
    printf("Matrix A\n");   print_matrix(a);
    printf("Matrix B\n");   print_matrix(b);
    printf("A + B\n");      print_matrix(sum);
    printf("A x B\n");      print_matrix(prod);
    return 0;
}
          
  • Declaration. int a[N][N] is an array of N arrays, each holding N ints — still one contiguous block, stored row by row.
  • Addition pairs up elements at the same [i][j] position — one nested loop, one add per cell.
  • Multiplication is different: prod[i][j] is the sum of row i of a multiplied element-by-element against column j of b — a third, innermost loop that addition never needed.
💡
Three nested loops, three jobs. The outer two loops in matrix_multiply pick which cell of prod is being filled; the innermost loop is the actual dot product for that one cell. Forgetting to reset prod[i][j] = 0 before that innermost loop is the single most common matrix-multiplication bug — without it, every cell keeps adding onto whatever garbage was already there.
C has no separate string type

A string is just a char array

strings.c
#include <stdio.h>

int main(void)
{
    /* three ways to declare a string */
    char s1[] = "Hello";
    char s2[20] = "Hello";
    char s3[] = {'H', 'e', 'l', 'l', 'o', '\0'};
    printf("s1 = %s\n", s1);
    printf("s2 = %s\n", s2);
    printf("s3 = %s\n", s3);
    return 0;
}
          
  • char s1[] = "Hello"; lets the compiler count the letters and size the array itself — the most common style.
  • char s2[20] = "Hello"; reserves a fixed 20-byte buffer, useful when the string needs room to grow later, for example with strcat.
  • char s3[] = {'H','e','l','l','o','\0'}; builds the same string one character at a time — and makes visible something the other two forms hide.
💡
All three are the identical five-letter string in memory. "Hello" as a string literal is really six characters, not five — the compiler silently appends a '\0' after the letters you typed, which is exactly why s3's explicit array had to include it by hand. Every C function that takes a string, including printf's %s, relies on that '\0' being there to know where the string ends.
The one byte that makes a char array a string

sizeof counts bytes, strlen counts letters

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

int main(void)
{
    char s1[] = "Hello";
    char s2[20] = "Hello";
    printf("sizeof(s1) = %zu\n", sizeof(s1));
    printf("strlen(s1) = %zu\n", strlen(s1));
    printf("sizeof(s2) = %zu\n", sizeof(s2));
    printf("strlen(s2) = %zu\n", strlen(s2));
    return 0;
}
          
  • sizeof(s1) == 6. Five letters plus the compiler-added '\0' — sizeof reports the array's full reserved size, decided at compile time.
  • strlen(s1) == 5. strlen walks the array counting bytes until it hits the first '\0', then stops — it never counts the terminator itself.
  • s2's gap. sizeof(s2) == 20 but strlen(s2) == 5 — the extra 15 bytes are reserved but unused, exactly the room s2 was given to grow into later.
⚠
What happens if the terminator is missing. char bad[5] = {'H','e','l','l','o'}; is legal C — it fills the array exactly full, with no room left for a '\0'. Passing bad to strlen or printf("%s", bad) is undefined behaviour: both keep reading byte after byte past the end of the array, looking for a zero byte that was never placed there, until they happen to hit one somewhere in whatever memory follows. The output is not wrong in a predictable way — it is not reproducible at all, and on a bad day it reads memory the program was never meant to touch. This is not a demonstration to run; it is the reason every hand-built string must end in '\0'.
The standard library's own string toolbox

Don't write what's already written — and tested

FunctionDoes
strlen(s)Returns the number of characters before the '\0'
strcpy(dest, src)Copies src, terminator included, into dest
strcat(dest, src)Appends src onto the end of dest, after its existing '\0'
strcmp(a, b)Compares two strings; returns 0 if equal, negative if a sorts first, positive otherwise
stringh.c
#include <stdio.h>
#include <string.h>

int main(void)
{
    char first[20] = "Nandan";
    char full[40];
    printf("strlen(first) = %zu\n", strlen(first));
    strcpy(full, first);
    strcat(full, " Kumar");
    printf("after strcpy + strcat: %s\n", full);
    printf("strcmp(\"cat\", \"cat\") = %d\n", strcmp("cat", "cat"));
    printf("strcmp(\"apple\", \"banana\") = %d\n", strcmp("apple", "banana"));
    if (strcmp(full, "Nandan Kumar") == 0)
        printf("full matches \"Nandan Kumar\"\n");
    return 0;
}
          
  • full must be big enough. strcpy and strcat trust the caller completely — neither checks that dest has room. full[40] is sized generously on purpose.
  • strcmp's return value is a sign, not a distance. A negative value means "less than" — here, -1 for this particular call — not "one character different"; only == 0 should ever be trusted as an exact meaning.
⚠
Why strcmp(a, b) == 0, never a == b. a == b compares two addresses, not the text they point to — two different arrays holding the identical word are still different addresses, so == would wrongly say "not equal". strcmp is what actually compares the characters. This exact mistake — comparing strings with == — is one of the most common bugs a student meeting C after Python or Java writes.
What the library function is really doing

Four functions, walk the '\0' yourself

strlib.c · 1/4
#include <stdio.h>
#include <string.h>

int my_strlen(const char *s)
{
    int len = 0;
    while (s[len] != '\0')
        len++;
    return len;
}
          
strlib.c · 2/4
void my_strcpy(char *dest, const char *src)
{
    int i = 0;
    while (src[i] != '\0') {
        dest[i] = src[i];
        i++;
    }
    dest[i] = '\0';
}

int my_strcmp(const char *a, const char *b)
{
    int i = 0;
    while (a[i] != '\0' && a[i] == b[i])
        i++;
    return (unsigned char) a[i] - (unsigned char) b[i];
}
          
  • my_strlen counts up from index 0 until it meets '\0' — there is nothing cleverer inside the real strlen either.
  • my_strcpy copies characters one at a time, then adds the terminator after the loop ends — forgetting that last line is the most common bug in a hand-written copy.
  • my_strcmp walks both strings together, stopping at the first mismatch or the first terminator; the byte difference at that point is the return value's sign.
strlib.c · 3/4
void my_strrev(char *s)
{
    int i = 0, j = my_strlen(s) - 1;
    while (i < j) {
        char t = s[i];
        s[i] = s[j];
        s[j] = t;
        i++;
        j--;
    }
}
          
strlib.c · 4/4
int main(void)
{
    char src[] = "BrainBit Infotech", dest[20];
    printf("my_strlen = %d, strlen = %zu\n", my_strlen(src), strlen(src));
    my_strcpy(dest, src);
    printf("my_strcpy -> %s\n", dest);
    printf("my_strcmp(cat,dog) = %d, strcmp = %d\n",
           my_strcmp("cat", "dog"), strcmp("cat", "dog"));
    my_strrev(dest);
    printf("my_strrev -> %s\n", dest);
    return 0;
}
          
  • my_strrev swaps from both ends inward, using my_strlen itself to find the last index — a small proof that these hand-written functions can lean on each other.
  • Every my_* result on the right matches its string.h counterpart exactly — that agreement is the actual proof these implementations are correct, not just plausible-looking.
💡
Why write these at all, when string.h already exists? Writing your own strlen, strcpy and strcmp is one of the most common exam and interview exercises in C, precisely because it forces the '\0'-walking mental model that every other string operation in this course depends on. In real code, always reach for string.h — it is tested far more thoroughly than a version written once for an exam.
A 2D char array, read as a list of words

Rows of text, not rows of numbers

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

int main(void)
{
    char days[7][10] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
    int i;
    for (i = 0; i < 7; i++)
        printf("days[%d] = %s\n", i, days[i]);
    printf("sizeof(days) = %zu, sizeof(days[0]) = %zu\n",
           sizeof(days), sizeof(days[0]));
    for (i = 0; i < 7; i++) {
        if (strcmp(days[i], "Wed") == 0)
            printf("Found \"Wed\" at index %d\n", i);
    }
    return 0;
}
          
  • char days[7][10] is a 2D char array — seven rows, each a 10-byte buffer holding one short string, the same shape as the matrices earlier today.
  • days[i] names row i on its own, which decays to a plain char * — that is why it can be handed straight to printf's %s or to strcmp.
  • Fixed-width rows waste space. "Sun" only needs 4 bytes, but every row reserves 10 — sizeof(days) == 70 confirms all 7 × 10 bytes are reserved regardless of how short each word actually is.
💡
Searching an array of strings needs strcmp, not ==. days[i] == "Wed" would compare two addresses and almost always be false, even on a genuine match — the loop here correctly uses strcmp(days[i], "Wed") == 0 instead, the same rule from the string.h slide, now applied inside a loop.
Module 6 practical

One menu, three programs

practical6.c · 1/6
#include <stdio.h>
#include <string.h>
#define N 2

void read_matrix(int m[N][N])
{
    int i, j;
    for (i = 0; i < N; i++)
        for (j = 0; j < N; j++)
            scanf("%d", &m[i][j]);
}
          
practical6.c · 2/6
void multiply_matrices(int a[N][N], int b[N][N], int prod[N][N])
{
    int i, j, k;
    for (i = 0; i < N; i++) {
        for (j = 0; j < N; j++) {
            prod[i][j] = 0;
            for (k = 0; k < N; k++) prod[i][j] += a[i][k] * b[k][j];
        }
    }
}
          
  • Option 1 — matrix multiply. read_matrix and multiply_matrices are the same logic from this morning's matrix slide, just given their own names.
  • Splitting a big task into small, named helpers is exactly the Module 5 lesson — a practical this size is where that habit starts paying off.
practical6.c · 3/6
void print_matrix(int m[N][N])
{
    int i, j;
    for (i = 0; i < N; i++) {
        for (j = 0; j < N; j++) printf("%4d", m[i][j]);
        printf("\n");
    }
}
void matrix_menu(void)
{
    int a[N][N], b[N][N], prod[N][N];
    printf("Enter %dx%d matrix A:\n", N, N);   read_matrix(a);
    printf("Enter %dx%d matrix B:\n", N, N);   read_matrix(b);
    multiply_matrices(a, b, prod);
    printf("A x B =\n");
    print_matrix(prod);
}
          
practical6.c · 4/6
void palindrome_check(void)
{
    char s[100];
    int i, j, is_pal = 1;
    printf("Enter a word: ");
    scanf("%99s", s);
    for (i = 0, j = strlen(s) - 1; i < j; i++, j--) {
        if (s[i] != s[j]) {
            is_pal = 0;
            break;
        }
    }
    printf("\"%s\" is %sa palindrome\n", s, is_pal ? "" : "not ");
}
          
  • matrix_menu only prints the prompts and calls the three helpers — it reads as a short summary of what option 1 does.
  • Option 2 — palindrome check. Two indices walk inward from both ends at once; the moment any pair of characters disagrees, is_pal is set to 0 and break stops the loop immediately — there is no point checking the rest.
practical6.c · 5/6
void word_count(void)
{
    char line[200];
    int i, count = 0, in_word = 0;
    printf("Enter a line of text: ");
    scanf(" %199[^\n]", line);
    for (i = 0; line[i] != '\0'; i++) {
        if (line[i] == ' ') {
            in_word = 0;
        } else if (!in_word) {
            in_word = 1;
            count++;
        }
    }
    printf("Word count = %d\n", count);
}
          
practical6.c · 6/6
int main(void)
{
    int choice;
    do {
        printf("\n1. Matrix multiply  2. Palindrome check  3. Word count  4. Exit\n");
        printf("Enter choice: ");
        scanf("%d", &choice);
        switch (choice) {
            case 1: matrix_menu();       break;
            case 2: palindrome_check();  break;
            case 3: word_count();        break;
            case 4: printf("Exiting...\n"); break;
            default: printf("Invalid choice\n");
        }
    } while (choice != 4);
    return 0;
}
          
  • Option 3 — word count. in_word is a flag, not a counter — count only increases on the character where a run of non-spaces begins, so several letters in a row still count as one word.
  • do-while and switch together make the classic menu shape from Practical 4 — the menu always shows at least once, and each option is one case.
💡
One file, three unrelated problems, one consistent shape. Matrix multiplication, a palindrome check and a word count share no logic at all — what they share is the discipline this whole module has been building: declare the right array or string, traverse it correctly, and hand the real work to a small, named function. That is the actual skill Days 10 and 11 were teaching.
07

Pointers

Day 12 · Module 7 · Pointers

Today we cover

By the end of today you can declare and dereference a pointer, explain why a[i] and *(a + i) are the same operation, and write a function that actually changes its caller's variables.
A new kind of variable

A variable that stores an address

Ordinary variablePointer variable
int num = 42;int *p = &num;
Holds a value — 42Holds an address — where num lives
Lives at some address tooPoints at that address

Every variable in a running program occupies memory and therefore has an address, whether or not anything ever looks at it.

The postal analogy. A house has an address. A variable is the house; its address is a number like 2000. A pointer is a slip of paper with that number written on it — not the house itself, just directions to it.
Declaration syntax. int *p; declares p as "a pointer to int". The * here is part of the type, not an operator yet — it tells C what kind of thing p points at, which it needs to know before doing arithmetic on p or reading the right number of bytes through it.
💡
A pointer is always typed. int *, char * and double * are different types. The address they store might look the same, but the type tells C how many bytes to read or write through it, and how far to move for "the next one" — the subject of the next two slides.
Two operators, opposite directions

Take an address, then follow it back

ptr.c
#include <stdio.h>

int main(void)
{
    int num = 42;
    int *p = &num;

    printf("num  = %d\n", num);
    printf("&num = %p\n", (void *)&num);
    printf("p    = %p\n", (void *)p);
    printf("*p   = %d\n", *p);

    *p = 100;
    printf("after *p = 100, num = %d\n", num);

    return 0;
}
          
  • & — address-of. &num is not a value of num's type — it is the address where num lives, and that address is exactly what gets stored into p.
  • * — dereference. *p means "go to the address p holds, and read (or write) the int there". *p = 100; changes num itself, through p.
  • p and &num print identically — the same address, because p was set to point at num.
💡
Your addresses will differ. 0000006DE0FFF834 is where this program happened to put num on this run. Run it again, or on another machine, and the digits change — the operating system places a program's stack at a different spot almost every time. What must never change is that p's line matches &num's line, and that *p equals num.
+1 does not mean "add one byte"

The step is sizeof the type

ptrmath.c · 1/2
#include <stdio.h>

void walk_ints(int nums[])
{
    int *ip;
    printf("int array, stride = sizeof(int) = %zu\n", sizeof(int));
    for (ip = nums; ip < nums + 4; ip++)
        printf("  %p -> %d\n", (void *)ip, *ip);
}

void walk_chars(char chars[])
{
    char *cp;
    printf("char array, stride = sizeof(char) = %zu\n", sizeof(char));
    for (cp = chars; cp < chars + 4; cp++)
        printf("  %p -> %c\n", (void *)cp, *cp);
}
          
ptrmath.c · 2/2
int main(void)
{
    int nums[4] = {10, 20, 30, 40};
    char chars[4] = {'A', 'B', 'C', 'D'};
    walk_ints(nums);
    walk_chars(chars);
    return 0;
}
          
  • ip++ jumps by 4. ip is an int *. B40 → B44 → B48 → B4C — each step is 4 bytes, exactly sizeof(int), so ip always lands on the next whole int.
  • cp++ jumps by 1. cp is a char *. B3C → B3D → B3E → B3F — one byte at a time, because sizeof(char) is 1.
  • Same +1 in the source, two completely different byte jumps — the compiler scales the step by the pointer's own type.
💡
This is why pointers need a type at all. p + 1 never means "one byte later" — it means "one element later", whatever size that element is. Get the type wrong (say, a char * walking what is really an int array) and every step lands one byte short of where it should. As on the previous slide, the exact digits are only for this run — run it again and every address shifts, but the 4-byte and 1-byte strides between consecutive addresses never do.
The most misunderstood rule in C

An array is not a pointer

arrdecay.c
#include <stdio.h>

void show_size(int *a)
{
    printf("inside show_size, sizeof(a) = %zu\n", sizeof(a));
}

int main(void)
{
    int a[5] = {1, 2, 3, 4, 5};
    printf("inside main,       sizeof(a) = %zu\n", sizeof(a));
    show_size(a);
    return 0;
}
          
  • Inside main, a is the real array. sizeof(a) is 20 — 5 × sizeof(int), the whole block. a here genuinely names 5 integers.
  • Passed to a function, it decays. Inside show_size, sizeof(a) is 8 — the size of one pointer on this toolchain. Only the base address survived the trip; the "how many" information did not.
  • Writing the parameter as int a[] instead of int *a changes nothing — C treats them as identical. In a parameter list, [] is only ever a pointer in disguise.
💡
Closing the loop from Day 10. The Array memory model slide described a[i] as an address calculation: base address + i × sizeof(int). That description is now literal — a decays to a pointer to its first element, a + i is real pointer arithmetic, and a[i] == *(a + i) exactly, always. Decay is what makes the two spellings identical — but decay only happens when an array is used as a pointer (passed to a function, assigned, compared); sizeof applied right where the array was declared still sees the whole block. That is exactly why this slide's own two numbers differ.
Fixing Day 8's swap, for good

Still call by value — of an address

swap2.c
#include <stdio.h>

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

int main(void)
{
    int x = 10, y = 20;
    printf("before: x = %d, y = %d\n", x, y);
    swap(&x, &y);
    printf("after:  x = %d, y = %d\n", x, y);
    return 0;
}
          
  • Same program, fixed. This is the exact swap from Day 8's Call by value slide (byvalue.c), which printed x = 10, y = 20 — unchanged. Only the parameter types and the call site changed.
  • main passes addresses. &x and &y are still just values — addresses, copied into a and b exactly as before. C never stopped being call by value.
  • swap dereferences to reach back. *a and *b follow those copied addresses to the original x and y in main, so this time the swap is real — the output proves it.
💡
"Call by reference" is not a separate mechanism in C. Some languages have real reference parameters built in; C does not. What C offers is a pointer, passed by value like everything else, then dereferenced by hand. Learn it this way now, and int ** later in this module will not confuse you — it is just a pointer to a pointer, passed by value, dereferenced twice.
Three ways a pointer can go wrong

Not the same problem — three problems

KindWhat it holdsHow it happens
NULLAddress 0, deliberatelyint *p = NULL; — "points at nothing", on purpose
WildGarbage — whatever bytes were already thereint *p; — declared, never given an address
DanglingThe address of something that no longer existsPoints at a local variable whose function already returned

Only NULL is safe to check for. A wild or dangling pointer looks exactly like a valid one — nothing about its value says "do not use this".

NULL is the one you can defend against. Because NULL is a known, fixed value, code can test for it before dereferencing — the whole point of the guard below.
A dangling pointer, concretely. A function declares int local = 5; and returns &local. The moment the function returns, local's memory is no longer reserved for it — the returned address now points at a stack slot the program is free to reuse for the very next call. The pointer's value looks perfectly valid; what it points at is gone.
💡
Module 8 is built on this idea. Dynamic memory adds a related danger: a pointer left holding the address of memory that was explicitly freed. Same failure shape as dangling — the pointer looks fine, the memory behind it is gone — just a different reason it became gone. Recognise the shape now.
nullptr.c · 1/2
#include <stdio.h>
#include <stddef.h>

void print_value(int *p)
{
    if (p != NULL)
        printf("value = %d\n", *p);
    else
        printf("pointer is NULL, nothing to print\n");
}
          
nullptr.c · 2/2
int main(void)
{
    int num = 7;
    int *ok = &num;
    int *empty = NULL;
    print_value(ok);
    print_value(empty);
    return 0;
}
          
  • The guard. if (p != NULL) before every dereference is the one habit that defends against a NULL pointer. It costs one comparison and prevents a crash.
  • It does not defend against wild or dangling pointers — those hold addresses that are not NULL, just wrong. The guard only catches the one case the language gives a clean signal for.
Day 13 · Module 7 · Pointers

Today we cover

By the end of today you can choose correctly between a string-literal pointer and a character array, read char *argv[] on sight, and pass a function as an argument to another function.
Two ways to spell a string, one safe to edit

A pointer to a literal, or a copy of one

strptr.c
#include <stdio.h>

int main(void)
{
    char *lit = "hello";
    char arr[] = "hello";

    printf("lit = %s   sizeof(lit) = %zu (a pointer)\n", lit, sizeof(lit));
    printf("arr = %s   sizeof(arr) = %zu (the array, strlen + 1)\n", arr, sizeof(arr));

    arr[0] = 'H';
    printf("arr after arr[0] = 'H': %s\n", arr);

    return 0;
}
          
  • char *lit = "hello"; lit is a pointer, 8 bytes on this toolchain, holding the address of the string literal "hello" — which C is free to place in read-only memory.
  • char arr[] = "hello"; arr is a real array, 6 bytes (h e l l o \0) — its own private, writable copy of those characters.
  • arr[0] = 'H'; is safe — arr owns that memory, and the output proves it changed.
💡
Never write lit[0] = 'H';. This program deliberately never attempts it. lit points at a string literal, and the C standard allows the compiler to store literals in read-only memory. Writing to one is undefined behaviour — not "always crashes", not "always fine". Depending on the platform it may crash immediately, silently corrupt something else, or appear to work today and fail somewhere else. The only correct rule: if a string needs to change, put it in an array — never leave it as a raw literal pointer.
main's own parameters

argv is exactly that

argv.c
#include <stdio.h>

int main(int argc, char *argv[])
{
    int i;
    printf("argc = %d\n", argc);
    for (i = 0; i < argc; i++)
        printf("argv[%d] = %s\n", i, argv[i]);
    return 0;
}
          
  • char *argv[] is an array of pointers to char — each element is a separate string, and the strings need not be the same length or laid out together.
  • argc counts how many of those pointers are valid: the program's own invocation plus every word typed after it on the command line.
  • Different from Module 6's array of strings (char names[][20]), where every row is the same fixed width in one contiguous block. Here each argv[i] can point anywhere, to a string of any length.
💡
You have been using this since Day 1. int main(void) is shorthand for "I do not need the command-line arguments". The full signature, int main(int argc, char *argv[]), was always available — and argv[0]'s exact form is implementation-defined: here it is a full path, elsewhere it might just be ./argv. Only the count and the words typed are guaranteed.
One more layer of indirection

A pointer that points at a pointer

ptrptr.c
#include <stdio.h>

int main(void)
{
    int num = 5;
    int *p = &num;
    int **pp = &p;

    printf("num  = %d\n", num);
    printf("*p   = %d\n", *p);
    printf("**pp = %d\n", **pp);

    **pp = 50;
    printf("after **pp = 50, num = %d\n", num);

    return 0;
}
          
  • int **pp = &p; pp stores the address of p, not of num. pp is a pointer to int *.
  • One * at a time. *pp follows pp to reach p itself — a pointer. **pp follows once more, from p to num.
  • **pp = 50; reaches all the way back to num, exactly as *p = ... would — it just took two hops instead of one.
💡
This is Call by reference, one layer deeper. If a function needs to change a variable, it takes a pointer to it. If a function needs to change a pointer that its caller holds — make it point somewhere new — it needs a pointer to that pointer. int ** is not a new rule; it is the old rule applied twice.
Passing behaviour, not just data

A function you can hand to another function

fnptr.c · 1/2
#include <stdio.h>

int ascending(int a, int b)  { return a > b; }
int descending(int a, int b) { return a < b; }

void sort_ints(int arr[], int n, int (*cmp)(int, int))
{
    int i, j, t;
    for (i = 0; i < n - 1; i++)
        for (j = 0; j < n - i - 1; j++)
            if (cmp(arr[j], arr[j + 1])) {
                t = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = t;
            }
}
          
fnptr.c · 2/2
void print_arr(int arr[], int n)
{
    int i;
    for (i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");
}
int main(void)
{
    int marks[5] = {68, 92, 45, 77, 83};
    sort_ints(marks, 5, ascending);
    printf("ascending:  ");
    print_arr(marks, 5);
    sort_ints(marks, 5, descending);
    printf("descending: ");
    print_arr(marks, 5);
    return 0;
}
          
  • int (*cmp)(int, int) declares cmp as a pointer to a function taking two ints and returning int. The parentheses around *cmp matter — without them it would declare a function returning a pointer, not a pointer to a function.
  • sort_ints does not know or care which comparison it is doing — it calls cmp(arr[j], arr[j+1]) and swaps if that call is true. Pass ascending or descending and the same sort produces the opposite order.
💡
This is exactly how the standard library's qsort works. qsort sorts any array of any type because it is handed a comparison function pointer — it never needs to know what "ascending" means for your data, only that your function can compare two elements of it.
Module 7 practical

A sort and a copy, rebuilt on pointers

practical7.c · 1/4
#include <stdio.h>
#define N 6

void swap(int *a, int *b)
{
    int t = *a;
    *a = *b;
    *b = t;
}
void bubble_sort(int arr[], int n)
{
    int i, j;
    for (i = 0; i < n - 1; i++)
        for (j = 0; j < n - i - 1; j++)
            if (arr[j] > arr[j + 1])
                swap(&arr[j], &arr[j + 1]);
}
          
practical7.c · 2/4
void print_arr(int arr[], int n)
{
    int i;
    for (i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");
}

void my_strcpy(char *dest, const char *src)
{
    while (*src != '\0') {
        *dest = *src;
        dest++;
        src++;
    }
    *dest = '\0';
}
          
  • bubble_sort's swap takes pointers. The same swap(int *a, int *b) from Call by reference — called here as swap(&arr[j], &arr[j + 1]), addresses of two array elements.
  • my_strcpy walks with pointers, not indices. No [i] anywhere — dest++ and src++ advance both pointers one char at a time until *src is the terminator, then the terminator itself is copied.
practical7.c · 3/4
void sort_demo(void)
{
    int marks[N] = {68, 92, 45, 77, 83, 59};
    printf("before: ");
    print_arr(marks, N);
    bubble_sort(marks, N);
    printf("after:  ");
    print_arr(marks, N);
}
void strcpy_demo(void)
{
    char src[50], dest[50];
    printf("Enter a string: ");
    scanf(" %49[^\n]", src);
    my_strcpy(dest, src);
    printf("copied: %s\n", dest);
}
          
practical7.c · 4/4
int main(void)
{
    int choice;
    do {
        printf("\n1. Bubble sort (pointer swap)  2. my_strcpy  3. Exit\n");
        printf("Enter choice: ");
        scanf("%d", &choice);
        switch (choice) {
            case 1: sort_demo();    break;
            case 2: strcpy_demo();  break;
            case 3: printf("Exiting...\n"); break;
            default: printf("Invalid choice\n");
        }
    } while (choice != 3);
    return 0;
}
          
  • Two completely different jobs — sorting numbers, copying text — and both now move through memory the same way: an address, a step, a check for "am I done", repeated.
💡
This is the whole module, in one file. swap reaches back through a pointer to change its caller's data — Call by reference. my_strcpy treats a string as what it always was — a run of memory walked one address at a time, not a row of numbered boxes. Indexing with [i] was never a different thing from pointer arithmetic; it was always this, spelled more comfortably.
08

Dynamic memory

Day 14 · Module 8 · Dynamic memory

Today we cover

By the end of today you can write a program that asks the user how much data there is, allocates exactly that much, uses it, and frees it — with no fixed-size array anywhere in the file.
Two kinds of memory

The size has to be a guess — unless you ask at run time

The problem. int marks[50]; commits you before the program has met a single student. 30 students wastes 20 slots; 60 students does not fit. The size is fixed when the program is compiled, but it is only known when the program is run.
StackHeap
Ordinary local variables and arraysWhatever you ask malloc for
Size fixed at compile timeSize decided at run time
Freed automatically when the function returnsFreed only when you call free
Small — typically 1–8 MB for the whole programLarge — limited by the machine, not the language
Fast: the stack pointer just movesSlower: the allocator has to find a gap
Dies with its frameLives until freed — can outlive the function that made it
Day 8 put the call stack on a slide. That is the same stack. Each call pushes a frame holding that call's locals; each return pops it. This is exactly why returning a pointer to a local array is broken — the frame it lived in is gone the moment you return.
The heap is the answer to that. Memory taken from the heap belongs to no frame. A function can allocate it, return the pointer, and the caller can keep using it long after that function has returned — which is precisely what a function like strdup, or any "build me a thing" function, does.
💡
The trade you are making. The heap buys you a size decided at run time and a lifetime you control. It charges you for it: you are now responsible for handing every block back. The stack never sends a bill because it never gave you the choice.
The four functions

Ask, use, give back

dyn.c · ask and check
#include <stdio.h>
#include <stdlib.h>

int n, i;

printf("How many numbers? ");
scanf("%d", &n);

int *a = malloc(n * sizeof(int));
if (a == NULL) {
    printf("Out of memory\n");
    return 1;
}
dyn.c · use and give back
for (i = 0; i < n; i++)
    a[i] = (i + 1) * (i + 1);

for (i = 0; i < n; i++)
    printf("%d ", a[i]);
printf("\n");

free(a);        /* hand the memory back */
a = NULL;       /* and stop pointing at it */
CallWhat it does
malloc(n * sizeof(int))One block of n ints, contents unknown
calloc(n, sizeof(int))Same block, every byte set to zero
realloc(p, newsize)Grows or shrinks an existing block, copying what fits
free(p)Returns the block. Does not change p
  • All four live in <stdlib.h>.
  • malloc takes a count in bytes, not elements — hence n * sizeof(int), never plain n.
  • It returns void *, which assigns to any pointer type. In C you do not cast it; in C++ you must.
  • Writing sizeof(*a) instead of sizeof(int) means the line stays correct if a's type ever changes.
💡
a is used exactly like an array — a[i] works, the loop is the loop you already write. That is not a coincidence: Module 7 showed that a[i] is *(a + i). The only new thing here is where the memory came from and who has to hand it back.
czero.c · malloc vs calloc
int *m = malloc(5 * sizeof(int));
int *c = calloc(5, sizeof(int));

printf("malloc: ");
for (i = 0; i < 5; i++) printf("%d ", m[i]);

printf("\ncalloc: ");
for (i = 0; i < 5; i++) printf("%d ", c[i]);
printf("\n");

free(m);
free(c);
The malloc row is whatever was left in that memory — it will differ on your machine and between runs. Never read an allocation before you have written to it. calloc zeroes every byte for you, which costs a little time and removes a whole class of bug.
grow.c · realloc, done right
if (count == cap) {
    cap = cap * 2;
    int *bigger = realloc(a, cap * sizeof(int));
    if (bigger == NULL) {
        free(a);          /* a is still valid */
        printf("Out of memory\n");
        return 1;
    }
    a = bigger;           /* only now overwrite a */
    printf("  [grown to %d slots]\n", cap);
}
a[count] = value;
count++;
Never write a = realloc(a, ...). If it fails it returns NULL, you have just overwritten the only pointer to the old block, and the data is both lost and unfreeable. Catch the result in a second pointer, check it, and assign only on success. realloc may move the block, so the old pointer must not be used again afterwards.
The guard that is never optional

Every allocation can fail

guard.c · inside main
/* an absurd request, so malloc really does fail */
long long huge = 1000000000LL * 1000LL;

int *a = malloc((size_t)huge * sizeof(int));

if (a == NULL) {
    printf("malloc failed - nothing was allocated\n");
    printf("a = %p\n", (void *)a);
    return 1;
}

printf("allocated\n");
free(a);
On failure malloc returns NULL. It does not stop the program, print anything, or raise an error. It hands back a null pointer and carries on — so the very next a[0] = 1; writes through a null pointer and the program crashes several lines away from the real cause.
The shape to memorise. Allocate, check, then use — never allocate and use.
the pattern
int *a = malloc(n * sizeof(int));
if (a == NULL) {
    /* report and get out */
    return 1;
}
/* only now is a safe to use */
💡
Why it fails in real programs, not just contrived ones. A negative or garbage n read from the user converts to an enormous size_t; a long-running program that leaks eventually exhausts the heap; a small embedded device has very little heap to begin with. The check costs three lines and turns an unexplainable crash into a message.
Three ways this goes wrong

The same program, leaking and fixed

leak.c · broken
void report(int n)
{
    int *a = malloc(n * sizeof(int));
    if (a == NULL) return;

    for (int i = 0; i < n; i++) a[i] = i;
    printf("%d ", a[n - 1]);
    /* no free - n*4 bytes lost, every call */
}
noleak.c · fixed
void report(int n)
{
    int *a = malloc(n * sizeof(int));
    if (a == NULL) return;

    for (int i = 0; i < n; i++) a[i] = i;
    printf("%d ", a[n - 1]);

    free(a);      /* the owner releases it */
    a = NULL;     /* and stops pointing at it */
}
Both print the same thing. That is the whole difficulty with memory bugs — the broken one is not slower, not wrong, and not louder. It simply loses 4,000 bytes per call, and only a program that runs for hours, or a tool that counts, ever notices.
MistakeWhat you wroteWhat happens
Memory leakAllocated, never freedThe block is never reusable. A loop or long-running service eventually exhausts the heap
Double freefree(p); free(p);Corrupts the allocator's bookkeeping. Usually an abort, sometimes a silent, exploitable mess
Dangling pointerUsed p after free(p)Reads or writes memory that now belongs to something else — undefined behaviour
Losing the pointerp = malloc(..); p = malloc(..);The first block's address is gone, so it can never be freed
💡
Three habits cover all four rows. (1) Every malloc gets a matching free — write the free before you write the code in between. (2) Set the pointer to NULL right after freeing: a dangling pointer becomes a null pointer, a double free becomes harmless, because free(NULL) is explicitly defined to do nothing. (3) Decide who owns each block — the code that allocates it is the code that frees it. When you need a machine to check, run the program under valgrind on Linux, or rebuild it with gcc -fsanitize=address -g and let the compiler's own sanitizer report the leak with the line number.
Pointers to pointers, put to work

A grid whose rows and columns are both decided at run time

dyn2d.c · building it
int rows = 3, cols = 4, i, j;

/* an array of ROW POINTERS ... */
int **grid = malloc(rows * sizeof(int *));
if (grid == NULL) return 1;

/* ... then a row behind each one */
for (i = 0; i < rows; i++) {
    grid[i] = malloc(cols * sizeof(int));
    if (grid[i] == NULL) return 1;
}
dyn2d.c · using and freeing it
for (i = 0; i < rows; i++)
    for (j = 0; j < cols; j++)
        grid[i][j] = (i + 1) * (j + 1);

/* free the rows FIRST, then the array */
for (i = 0; i < rows; i++)
    free(grid[i]);
free(grid);
int **grid is Module 7's pointer-to-pointer, with a reason to exist. grid points at a block of rows pointers; each of those points at its own block of cols ints. grid[i][j] therefore means "follow grid to row i, follow that to column j" — two hops, not one.
Free in the reverse order of allocation. Free grid first and you have thrown away the only copies of the row addresses — every row is leaked and unreachable. Rows first, then the array of pointers. Allocation goes outside-in; freeing goes inside-out.
int m[3][4];int **grid
SizeFixed when compiledRead at run time
LayoutOne contiguous blockrows+1 separate blocks
RowsAll the same lengthCan differ (a "jagged" array)
CleanupAutomaticrows+1 calls to free
💡
This is how char *argv[] is built. The shell allocates one array of pointers, then one string behind each. It is also the shape of an array of names, a list of lines read from a file, and — in Module 11 — the linked structures you will build next.
Module 8 practical

A marks report with no fixed-size array in the file

practical8.c · 1/5
#include <stdio.h>
#include <stdlib.h>

/* Fill the array from the keyboard. */
void read_marks(float *marks, int n)
{
    for (int i = 0; i < n; i++) {
        printf("Marks of student %d: ", i + 1);
        scanf("%f", &marks[i]);
    }
}

float total_of(const float *marks, int n)
{
    float sum = 0.0f;
    for (int i = 0; i < n; i++)
        sum += marks[i];
    return sum;
}
practical8.c · 2/5
int index_of_highest(const float *marks, int n)
{
    int best = 0;
    for (int i = 1; i < n; i++)
        if (marks[i] > marks[best])
            best = i;
    return best;
}

int index_of_lowest(const float *marks, int n)
{
    int worst = 0;
    for (int i = 1; i < n; i++)
        if (marks[i] < marks[worst])
            worst = i;
    return worst;
}
practical8.c · 3/5
void print_report(const float *marks, int n)
{
    float avg = total_of(marks, n) / n;
    int hi = index_of_highest(marks, n);
    int lo = index_of_lowest(marks, n);
    int above = 0;

    for (int i = 0; i < n; i++)
        if (marks[i] > avg) above++;

    printf("\n--- Report for %d students ---\n", n);
    printf("Average : %.2f\n", avg);
    printf("Highest : %.2f (student %d)\n", marks[hi], hi + 1);
    printf("Lowest  : %.2f (student %d)\n", marks[lo], lo + 1);
    printf("Above average: %d\n", above);
}
practical8.c · 4/5
int main(void)
{
    int n;

    printf("Number of students: ");
    if (scanf("%d", &n) != 1 || n <= 0) {
        printf("Not a valid count.\n");
        return 1;
    }

    /* The size is known only now, so allocate now. */
    float *marks = malloc(n * sizeof(float));
    if (marks == NULL) {
        printf("Could not allocate.\n");
        return 1;
    }
practical8.c · 5/5
    read_marks(marks, n);
    print_report(marks, n);

    free(marks);
    marks = NULL;
    return 0;
}
  • main stays short because every step is a named function — read n, allocate, fill, report, free. That is the Module 5 habit, still paying.
  • The helper functions take const float *marks: they read the array, so const lets the compiler stop them from writing to it by accident.
  • The count n travels with the pointer into every function, because a pointer carries no length — the Module 6 lesson, unchanged.
💡
Read the file for what is missing. There is no [50], no "maximum students" constant, no wasted slots and no upper limit — 5 students or 5,000, the program allocates exactly what it needs and frees it. That, and not the syntax, is what malloc is for.
09

Structures

Day 15 · Module 9 · Structures

Today we cover

By the end of today you can design a record for a real-world thing, hold a hundred of them in an array, and write functions that read and change them without copying anything.
The problem a struct solves

Three arrays that must be kept in step by hand

whystruct.c · the old way
int   roll[N]     = {101, 102, 103};
char  name[N][20] = {"Asha", "Rahul", "Fatima"};
float marks[N]    = {87.5f, 74.0f, 93.5f};

/* Swap student 0 and 1 - and remember all THREE */
int   ti = roll[0];  roll[0]  = roll[1];  roll[1]  = ti;
float tf = marks[0]; marks[0] = marks[1]; marks[1] = tf;
/* the names were forgotten: the records are now wrong */

for (int i = 0; i < N; i++)
    printf("%d %-8s %.1f\n", roll[i], name[i], marks[i]);
Look at the first two rows. Roll 102 now belongs to Asha and carries Rahul's marks. Nothing crashed, nothing warned — the data is simply wrong, because the three arrays are only related by a convention the programmer has to remember.
The three values are one thing. A student has a roll number, a name and marks. Splitting them across three arrays stores the pieces but throws away the fact that they belong together, and then asks you to maintain that fact by hand in every loop, every swap and every function call.
A struct is a type you define. int and float are given to you; a struct lets you build struct Student out of them and then use it the way you use any other type — declare one, put it in an array, pass it to a function, return it.
ArrayStruct
Many values of one typeOne value made of many types
Reached by index — a[2]Reached by name — s.marks
Cannot be assigned as a wholeCan be assigned as a whole
Decays to a pointer when passedIs copied when passed
💡
The two are used together, not instead of each other. The shape that actually solves this slide is an array of structs — one array, each element a complete student. Swapping two of them is then a single three-line swap that cannot forget a field.
Defining your own type

Declare the shape, then make variables of it

st.c · the shape
struct Student {      /* the shape - no memory yet */
    int roll;
    char name[30];
    float marks;
};                    /* the semicolon is required */
st.c · inside main
struct Student s1 = {101, "Asha Menon", 87.5f};
struct Student s2;

s2.roll = 102;
snprintf(s2.name, sizeof(s2.name), "Rahul Verma");
s2.marks = 74.0f;

printf("%d  %-14s %.1f\n", s1.roll, s1.name, s1.marks);
printf("%d  %-14s %.1f\n", s2.roll, s2.name, s2.marks);

struct Student copy = s1;   /* whole-struct assignment */
copy.marks = 91.0f;

printf("%d  %-14s %.1f\n", copy.roll, copy.name, copy.marks);
printf("s1 is untouched: %.1f\n", s1.marks);
  • The declaration creates no memory. struct Student { … }; describes a shape. Memory appears only when you declare a variable of that type.
  • The trailing semicolon. }; — forgetting it produces a confusing error on the next line, which is how most people meet it.
  • The members are reached with a dot. s1.roll, s1.marks. Each behaves exactly like an ordinary variable of its own type.
  • Brace initialisation is positional — {101, "Asha Menon", 87.5f} fills roll, name, marks in declaration order. Any member left out is set to zero.
  • A whole struct can be assigned with =, which copies every member, the character array included. This is the one thing an array itself cannot do.
Why snprintf and not s2.name = "Rahul Verma";? Because name is an array, and an array is not assignable — Module 6's rule, unchanged by being inside a struct. The text has to be copied into it, with strcpy, snprintf, or a scanf straight into s2.name.
💡
copy = s1; is a real copy, not a second name for it. Changing copy.marks leaves s1.marks at 87.5. Remember this when you pass a struct to a function — the function gets exactly this kind of copy.
The record-keeping shape

One array, each element a whole record

stars.c · the array
struct Student cls[N] = {
    {101, "Asha",   87.5f},
    {102, "Rahul",  74.0f},
    {103, "Fatima", 93.5f},
    {104, "Vikram", 61.0f},
    {105, "Neha",   82.5f}
};
stars.c · one pass over the array
float total = 0.0f;
int top = 0;

printf("ROLL  NAME     MARKS\n");
for (int i = 0; i < N; i++) {
    printf("%-5d %-8s %5.1f\n",
           cls[i].roll, cls[i].name, cls[i].marks);
    total += cls[i].marks;
    if (cls[i].marks > cls[top].marks)
        top = i;
}

printf("\nClass average : %.2f\n", total / N);
printf("Topper        : %s (%.1f)\n",
       cls[top].name, cls[top].marks);
  • cls[i] is one complete student; cls[i].marks is that student's marks. Index first, then dot.
  • The nested braces mirror the shape: an outer pair for the array, an inner pair per element.
  • top holds an index, not a copy — so cls[top].name and cls[top].marks are guaranteed to be the same student. That is the bug from the previous slide, made impossible.
Swapping two records is now one swap. struct Student t = cls[0]; cls[0] = cls[1]; cls[1] = t; moves every member together. No field can be forgotten, because no field is handled separately.
💡
This is the shape of almost every small C program that keeps records — a marksheet, a bank, an inventory, a library. Module 8's malloc replaces [N] when the count is not known in advance: struct Student *cls = malloc(n * sizeof(struct Student)); and everything else on this slide stays exactly the same.
A struct inside a struct

Reach in one dot at a time

nested.c · the two types
struct Date {
    int day;
    int month;
    int year;
};

struct Employee {
    int id;
    char name[25];
    struct Date joined;   /* a struct inside a struct */
    float salary;
};
nested.c · inside main
struct Employee e = {
    7001, "Priya Nair",
    {14, 8, 2023},        /* inner braces build the Date */
    58000.0f
};

printf("Joined  : %02d/%02d/%d\n",
       e.joined.day, e.joined.month, e.joined.year);

e.joined.year = 2024;     /* reach in one dot at a time */
printf("Revised : %02d/%02d/%d\n",
       e.joined.day, e.joined.month, e.joined.year);
  • e.joined is a whole struct Date; e.joined.year is the int inside it. Each dot takes you one level down.
  • The inner type must be declared first. struct Employee cannot contain a struct Date the compiler has not seen yet.
  • e.joined = (struct Date){1, 1, 2025}; replaces the whole date at once, because a struct is assignable as a whole.
A struct cannot contain itself. struct Node { struct Node next; }; is impossible — the size would be infinite. It can contain a pointer to itself, struct Node *next;, because a pointer's size is known. That single distinction is what makes linked lists possible, and it is where Module 11 begins.
💡
Nesting is how real records are built. An address inside a customer, a date inside an order, a point inside a rectangle. Each inner struct stays a reusable type in its own right — the same struct Date serves the joining date, the date of birth and the invoice date.
Module 7, applied to your own type

One operator that means “follow, then take”

stptr.c · the two notations
struct Student s = {101, "Asha", 87.5f};
struct Student *p = &s;

printf("p->roll     = %d\n", p->roll);
printf("(*p).roll   = %d\n", (*p).roll);
printf("sizeof(s)   = %zu bytes\n", sizeof(s));
printf("sizeof(p)   = %zu bytes\n", sizeof(p));
They are the same thing. p->roll is defined to mean exactly (*p).roll — follow the pointer to the struct, then take the member. -> exists only because the parentheses are unavoidable and everybody got tired of typing them.
You haveWrite
A struct — struct Student s;s.roll
A pointer — struct Student *p;p->roll
An array elementcls[i].roll
A pointer into an arrayp->roll, then p++
Why *p.roll does not work. . binds tighter than *, so *p.roll parses as *(p.roll) — "take the member roll of the pointer p", which is meaningless, because a pointer has no members. Hence (*p).roll, and hence ->.
28 against 8. The struct is 28 bytes; a pointer to it is 8 on a 64-bit machine, whatever the struct holds. That single comparison is the argument of the next slide.
💡
You will write -> constantly from here on. Every linked list, tree and queue in Module 11 is a chain of struct pointers, and every step through one is a ->. It is worth saying out loud once: "arrow means follow the pointer, then take the field."
Copy, or address?

A struct is copied — unless you pass its address

stptr.c · by value
/* Takes a COPY - 28 bytes pushed per call,
   and the caller's student is untouched. */
void by_value(struct Student s)
{
    s.marks = 100.0f;
    printf("  inside by_value  : %.1f\n", s.marks);
}
stptr.c · by pointer
/* Takes an ADDRESS - 8 bytes, and it can
   change the caller's data. */
void by_pointer(struct Student *p)
{
    p->marks = 100.0f;
    printf("  inside by_pointer: %.1f\n", p->marks);
}
Both functions really did set 100.0. The difference is what they set it on. by_value changed its own private copy, which was destroyed on return. by_pointer followed the address back to main's student and changed that. This is Day 8's failed swap and Day 12's fix, in new clothes.
The rule in practice. Pass const struct Student * when the function only reads — it avoids copying 28 bytes and the const makes the compiler enforce "read only". Pass struct Student * when it must change the record. Pass by value only for genuinely small structs you deliberately want a copy of.
struct Student sstruct Student *p
What is passedA copy of all 28 bytesAn 8-byte address
Member accesss.marksp->marks
Can change the caller's recordNoYes — unless declared const
Cost grows with the structYesNo — always one address
💡
Returning a struct is allowed, unlike returning an array — struct Date today(void) works fine and hands back a copy. The array inside a struct travels with it, which is why a struct can be returned when a bare array cannot.
Day 16 · Module 9 · Structures

Today we cover

By the end of today you can write Student s; instead of struct Student s;, explain when a union is the right tool and when it is a trap, and predict roughly what sizeof will say about a struct you designed.
A shorter name for a type

Dropping the word struct

td.c · the typedefs
/* A name for a plain type ... */
typedef unsigned int uint;

/* ... and, far more usefully, for a struct. */
typedef struct {
    int   roll;
    char  name[20];
    float marks;
} Student;

/* The parameter type is now just "Student". */
void show(const Student *s)
{
    printf("%d  %-8s %.1f\n", s->roll, s->name, s->marks);
}
td.c · using it
int main(void)
{
    uint count = 2;
    Student a = {101, "Asha", 87.5f};   /* no "struct" */
    Student b = {102, "Rahul", 74.0f};
    show(&a);
    show(&b);
    printf("%u students\n", count);
    return 0;
}
typedef creates no new type. It creates a new name for an existing one. uint and unsigned int are the same type in every respect; the compiler cannot tell them apart and will not warn if you mix them.
Without typedefWith
struct Student s;Student s;
struct Student *p;Student *p;
void f(struct Student *s)void f(Student *s)
The tag can be kept as well. typedef struct Student { … } Student; gives you both struct Student and Student. You need the tag when the struct refers to itself: a node's next pointer must be written struct Node *next; inside the braces, because the typedef name does not exist yet at that point.
You have used one already. size_t, FILE and time_t are all typedefs from the standard library — which is exactly why you write FILE *fp; and not struct FILE *fp;.
💡
Use it for structs; be sparing elsewhere. Hiding unsigned int behind uint saves six characters and costs the reader the knowledge of what the type really is. Hiding a pointer inside a typedef — typedef struct Node *NodePtr; — is worse: readers stop seeing that a pointer is involved at all.
Members that share one space

Same syntax as a struct, opposite storage

un.c · same members, two ways
struct SData { int i; float f; char c; };
union  UData { int i; float f; char c; };
un.c · inside main
union UData u;

printf("sizeof(struct SData) = %zu\n", sizeof(struct SData));
    printf("sizeof(union  UData) = %zu\n", sizeof(union UData));

    u.i = 65;
    printf("\nwrote u.i = 65\n");
    printf("  u.i = %d\n", u.i);
    printf("  u.c = %c\n", u.c);

    u.f = 3.14f;
    printf("\nwrote u.f = 3.14  (same bytes, overwritten)\n");
    printf("  u.f = %.2f\n", u.f);
    printf("  u.i = %d\n", u.i);
    return 0;
}
structunion
MembersEach has its own memoryAll share one block
SizeSum of members, plus paddingSize of the largest member
Valid at onceAll of themOne — the one written last
Use forA record with several fieldsA value that is one of several kinds
Read the output carefully. After u.i = 65, reading u.c gives 'A' — because 65 is the code for 'A' and c is looking at the same first byte. After u.f = 3.14, u.i reports 1078523331: not a conversion, just the bit pattern of 3.14 read as though it were an integer.
The rule. Read back only the member you wrote last. Reading any other one gives you a reinterpretation of raw bytes, which is a bug in every program that did not mean to do it deliberately.
💡
What a union is actually for. A value that is one of several kinds at a time — a token that is either a number or a name, a setting that is either an int or a string. The standard shape pairs it with an enum tag inside a struct, so the program can always say which member is the live one. Unions also appear in embedded code to view one block of memory two ways, which is exactly the trick this slide's output shows.
Naming a fixed set of values

Names instead of bare numbers

en.c
enum Weekday { MON = 1, TUE, WED, THU, FRI, SAT, SUN };

void schedule(enum Weekday d)
{
    switch (d) {
        case SAT:
        case SUN:
            printf("%d: holiday\n", d);
            break;
        case WED:
            printf("%d: lab day\n", d);
            break;
        default:
            printf("%d: class day\n", d);
    }
}
en.c · driving it
int main(void)
{
    printf("MON = %d, TUE = %d, SUN = %d\n", MON, TUE, SUN);
    for (enum Weekday d = MON; d <= SUN; d++)
        schedule(d);
    return 0;
}
  • The values are just ints. Numbering starts at 0 unless you say otherwise; here MON = 1 sets the start and the rest follow — TUE is 2, SUN is 7.
  • Any value can be pinned — enum Status { OK = 0, WARN = 10, FAIL = 20 }; — and the ones after it carry on from there.
  • The names are constants, not strings. printf("%d", MON) prints 1; there is no built-in way to print the word "MON". A parallel array of strings is the usual answer.
Why not #define MON 1? Both give you a name for 1, but an enum declares a set that belongs together, gives the debugger something to show you, and lets the compiler warn when a switch forgets one of its values. The preprocessor's #define has no idea the seven days are related — Module 10 returns to this.
💡
The pairing to remember. An enum for the kind, a switch for the behaviour. The bank practical two slides from here uses enum TxnKind { DEPOSIT = 1, WITHDRAW }; so that a transaction record says DEPOSIT and not 1 — and so the statement printer cannot mistake one for the other.
Why the numbers do not add up

Same members, different order, different size

pad.c · the two layouts
#include <stddef.h>   /* for offsetof */

struct Loose {
    char  a;      /* 1 byte, then 3 wasted */
    int   b;      /* 4 */
    char  c;      /* 1 byte, then 3 wasted */
};

struct Tight {
    int   b;      /* 4 */
    char  a;      /* 1 */
    char  c;      /* 1, then 2 wasted */
};
pad.c · inside main
printf("sizeof(struct Loose) = %zu\n", sizeof(struct Loose));
printf("sizeof(struct Tight) = %zu\n", sizeof(struct Tight));
printf("sum of the members   = %zu\n",
       sizeof(char) + sizeof(int) + sizeof(char));

printf("\nLoose offsets: a=%zu b=%zu c=%zu\n",
       offsetof(struct Loose, a), offsetof(struct Loose, b),
       offsetof(struct Loose, c));
printf("Tight offsets: b=%zu a=%zu c=%zu\n",
       offsetof(struct Tight, b), offsetof(struct Tight, a),
       offsetof(struct Tight, c));
The members are identical. One char, one int, one char — six bytes of actual data either way. Rearranging them changes the size by a third, because of what the compiler puts between them.
  • Alignment. A 4-byte int must sit at an address that is a multiple of 4 — the CPU reads it in one go that way. So in Loose, a takes byte 0 and b cannot start until byte 4: three bytes are skipped.
  • Tail padding. The struct's own size is rounded up to a multiple of its largest member's alignment, so that an array of them keeps every element aligned. That is the 3 wasted bytes after Loose.c and the 2 after Tight.c.
  • The offsets prove it — offsetof reports where each member really starts. In Tight, a and c sit side by side at 4 and 5 with nothing between them.
These exact numbers are this compiler's. They come from 64-bit GCC on Windows, where int is 4 bytes and aligns to 4. Another compiler, another platform, or a packing option can give different sizes — the rule is portable, the numbers are not. Never assume a struct's size; ask with sizeof.
💡
Declare members largest-first and padding mostly disappears by itself. It rarely matters for a handful of records — but for an array of a million, the Loose layout costs 4 MB more than Tight for exactly the same data, and every one of those wasted bytes is read into cache along with the real ones.
Module 9 practical

A bank built from struct, enum and pointers

bank.c · 1/5
#define MAX_ACC 10
#define MAX_TXN 20

enum TxnKind { DEPOSIT = 1, WITHDRAW };

struct Txn {
    enum TxnKind kind;
    float amount;
    float balance_after;
};

struct Account {
    int   number;
    char  holder[25];
    float balance;
    struct Txn history[MAX_TXN];
    int   txn_count;
};
bank.c · 2/5
int find_account(const struct Account *acc, int count, int number)
{
    for (int i = 0; i < count; i++)
        if (acc[i].number == number)
            return i;
    return -1;
}

void record(struct Account *a, enum TxnKind kind, float amount)
{
    if (a->txn_count >= MAX_TXN)
        return;
    a->history[a->txn_count].kind = kind;
    a->history[a->txn_count].amount = amount;
    a->history[a->txn_count].balance_after = a->balance;
    a->txn_count++;
}
bank.c · 3/5
void deposit(struct Account *a, float amount)
{
    if (amount <= 0) {
        printf("Amount must be positive.\n");
        return;
    }
    a->balance += amount;
    record(a, DEPOSIT, amount);
    printf("Deposited %.2f. Balance = %.2f\n", amount, a->balance);
}
bank.c · 4/5
void withdraw(struct Account *a, float amount)
{
    if (amount <= 0) {
        printf("Amount must be positive.\n");
        return;
    }
    if (amount > a->balance) {
        printf("Insufficient funds. Balance is only %.2f\n",
               a->balance);
        return;
    }
    a->balance -= amount;
    record(a, WITHDRAW, amount);
    printf("Withdrew %.2f. Balance = %.2f\n", amount, a->balance);
}
bank.c · 5/5
void statement(const struct Account *a)
{
    printf("\nAccount %d - %s\n", a->number, a->holder);
    printf("TYPE      AMOUNT   BALANCE\n");
    for (int i = 0; i < a->txn_count; i++)
        printf("%-9s %8.2f %9.2f\n",
               a->history[i].kind == DEPOSIT ? "DEPOSIT" : "WITHDRAW",
               a->history[i].amount,
               a->history[i].balance_after);
    printf("Closing balance: %.2f\n", a->balance);
}

/* main holds the array, reads the menu choice, looks the
   account up with find_account, and hands &acc[i] to the
   function that does the work. */
  • Every piece of Module 9 is here. A struct holding another struct's array (struct Txn history[] inside struct Account), an array of structs, an enum for the transaction kind, and -> on every line that touches an account.
  • Nothing is passed by value. deposit and withdraw take struct Account * because they must change the record; statement and find_account take const struct Account * because they must not.
  • The guard is the point of the exercise. if (amount > a->balance) refuses the withdrawal and returns without touching the balance — the account is never left in a state that the history cannot explain.
💡
Extend it three ways. Replace the fixed acc[MAX_ACC] with a malloced array read from the user (Module 8), save the accounts to a file so the balances survive a restart (Module 10, next), and turn the transaction history into a linked list so it is never full (Module 11). The bank is the running example those three modules extend.
10

Files & the preprocessor

Day 17 · Module 10 · Files & the preprocessor

Today we cover

By the end of today you can write a program that saves its records to a file and finds them again the next time it runs — and explain what the compiler saw after the preprocessor finished with your source.
The handle and the four steps

Open, check, use, close

the shape of every file program
#include <stdio.h>

FILE *fp = fopen("notes.txt", "r");   /* 1. open  */

if (fp == NULL) {                     /* 2. CHECK */
    perror("notes.txt");
    return 1;
}

/* 3. use it - fprintf, fgets, fread, fwrite ... */

fclose(fp);                           /* 4. close */
FILE * is a handle, not the file. fopen asks the operating system to open the file and hands back a pointer to a FILE structure holding the position, the buffer and the error flags. You never look inside it — you only pass it to the f… functions. FILE is a typedef, which is why it needs no struct keyword.
ModeMeaningIf the file is missing
"r"Read from the startFails — returns NULL
"w"Write — empties the file firstCreates it
"a"Append — always writes at the endCreates it
"r+"Read and write, from the startFails
"w+"Read and write, empties firstCreates it
"a+"Read anywhere, write at the endCreates it
Add b for binary — "rb", "wb", "ab". On Windows this matters: in text mode a written \n becomes two bytes on disk and is translated back on reading. For a file of structs that silent translation corrupts the data, so binary files must always be opened with b.
💡
"w" deletes the file's contents the moment it opens. Not when you write — when you open. A program that opens a file with "w" to "check something" has already destroyed it. When you mean "add to this file", the mode is "a". And fclose is what flushes the buffer: data you "wrote" may still be sitting in memory, so a program that skips fclose and crashes writes nothing at all.
Readable files

The same printf you know, aimed at a file

write.c
FILE *fp = fopen("notes.txt", "w");
if (fp == NULL) {
    printf("Could not open notes.txt\n");
    return 1;
}

fprintf(fp, "Asha 87.5\n");
fprintf(fp, "Rahul 74.0\n");
fputs("Fatima 93.5\n", fp);

fclose(fp);
printf("notes.txt written\n");
readit.c
char line[100];
int n = 0;

FILE *fp = fopen("notes.txt", "r");
if (fp == NULL) return 1;

/* fgets stops at the newline OR at size-1
   characters, and returns NULL at the end. */
while (fgets(line, sizeof(line), fp) != NULL) {
    n++;
    printf("%d: %s", n, line);
}

fclose(fp);
printf("%d lines read\n", n);
ConsoleFile
printf(…)fprintf(fp, …)
scanf(…)fscanf(fp, …)
puts(s)fputs(s, fp)
getchar()fgetc(fp)
Why fgets and not fscanf("%s"). %s stops at the first space, so "Asha Menon" arrives as two separate reads. fgets takes the whole line, and it is given the buffer's size, so it cannot overflow. Read the line with fgets, then pick it apart with sscanf — the pattern the practical uses.
fgets keeps the newline. That is why the loop prints "%d: %s" with no \n of its own. To strip it: line[strcspn(line, "\n")] = '\0';
💡
A text file is just bytes a human can read. Nothing separates notes.txt from what you would type into Notepad — which is the great advantage: you can open it, read it, fix it by hand, mail it, put it in Excel. The cost is size and precision, which is what the next slide is about.
Whole structs, straight to disk

The bytes in memory, written as they are

bin.c · write then read back
struct Student out[3] = {
    {101, "Asha",   87.5f},
    {102, "Rahul",  74.0f},
    {103, "Fatima", 93.5f}
};
struct Student in[3];

FILE *fp = fopen("students.dat", "wb");   /* b = binary */
if (fp == NULL) return 1;
fwrite(out, sizeof(struct Student), 3, fp);
fclose(fp);

fp = fopen("students.dat", "rb");
if (fp == NULL) return 1;
size_t got = fread(in, sizeof(struct Student), 3, fp);
fclose(fp);
Both take four arguments — fwrite(where, size of one, how many, fp). They return the number of items actually handled, not bytes, so got == 3 is the check that the read succeeded. A short count means the file ended early.
TextBinary
One record here~20 readable bytesExactly 28 — sizeof
Human-readableYesNo
Float precisionOnly what you printedExact, bit for bit
Writing a structField by fieldOne fwrite
Jump to record nMust scan the linesn * sizeof(rec)
Portable between machinesYesNo
💡
The portability warning is the real one. A binary file is a photograph of your memory, padding and all — so it is only readable by a program built with the same compiler, the same struct definition and the same word size. Change one field of the struct and every old file becomes unreadable. Use binary for a program's own working data; use text when anything else will ever read it.
Random access

Reach record 3 without reading 1 and 2

seek.c
FILE *fp = fopen("students.dat", "rb");
if (fp == NULL) return 1;

fseek(fp, 0, SEEK_END);     /* jump to the end ...      */
size = ftell(fp);           /* ... and ask where that is */
printf("file size    : %ld bytes\n", size);
printf("record count : %ld\n",
       size / (long)sizeof(struct Student));

/* Read ONLY the 3rd record. */
fseek(fp, 2L * (long)sizeof(struct Student), SEEK_SET);
printf("offset now   : %ld\n", ftell(fp));

fread(&s, sizeof(struct Student), 1, fp);
printf("record 3     : %d %s %.1f\n", s.roll, s.name, s.marks);

rewind(fp);                 /* back to byte 0 */
printf("after rewind : %ld\n", ftell(fp));
fclose(fp);
CallWhat it does
fseek(fp, n, SEEK_SET)Move to byte n from the start
fseek(fp, n, SEEK_CUR)Move n bytes from where you are
fseek(fp, n, SEEK_END)Move n bytes from the end
ftell(fp)Report the current byte offset
rewind(fp)Back to byte 0, and clear the error flags
  • The arithmetic is the whole idea. Every record is the same size, so record n begins at n * sizeof(struct Student). Record 3 is index 2, and 2 × 28 = 56 — exactly the offset the program reports.
  • SEEK_END then ftell gives you the file's size, and dividing by the record size gives the record count without a loop.
  • n is a long. Casting sizeof keeps the multiplication honest on a large file.
💡
This is what a database does, in miniature. Fixed-size records plus an offset calculation means finding record 900,000 costs exactly what finding record 3 costs. It also means you can overwrite one record in place: fseek to it, fwrite the new version, and nothing else in the file moves. Text files cannot do that — changing one line means rewriting everything after it.
When the file is not there

The check you cannot skip, and the message the system gives you

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

int main(void)
{
    FILE *fp = fopen("no_such_file.txt", "r");

    if (fp == NULL) {
        /* perror prints your message, then ": ", then
           the system's description of what went wrong. */
        perror("no_such_file.txt");
        exit(1);
    }

    printf("opened - this line never runs\n");
    fclose(fp);
    return 0;
}
The second half of that line is not yours. You supplied "no_such_file.txt"; perror added ": No such file or directory" by reading errno, the global the library sets when a call fails. A wrong-permissions file would produce ": Permission denied" from the identical line of code.
CheckMeaning
fp == NULLThe open failed — missing, locked, or no permission
feof(fp)The end of the file has been reached
ferror(fp)A read or write failed — a real error, not the end
Return of freadFewer items than asked for — short read
Why while (!feof(fp)) is wrong. feof only becomes true after a read has already failed at the end, so the loop runs one extra time with stale data. Test the read itself instead — while (fgets(…) != NULL) or while (fread(…) == 1) — which is what every loop in this module does.
💡
Three lines, every single time. Opening a file is the most likely thing in a small C program to fail, because it depends on the world outside the program — a typo in a name, a folder that moved, a file someone else has open. exit(1) ends the program with a non-zero status, which is how a script or a shell knows it failed.
Before the compiler runs

A text-substitution pass that knows no C at all

Day 1's pipeline, revisited. The preprocessor is the first stage: it reads your .c file, obeys every line starting with #, and hands the compiler one long expanded file. It does not understand types, expressions or scope — it moves text around. Every confusing macro bug comes from that one fact.
DirectiveWhat it does
#include <stdio.h>Pastes that file in here. Angle brackets: search the system folders
#include "marks.h"Same, but look in this folder first
#define PI 3.14159Replace every later PI with that text
#undef PIForget that definition
#ifdef / #ifndefKeep the block only if a name is / is not defined
#if / #else / #endifChoose which block the compiler even sees
see it for yourself
$ gcc -E hello.c > hello.i     # stop after preprocessing
$ wc -l hello.c                # 6 lines you wrote
$ wc -l hello.i                # ~800 - stdio.h pasted in
No semicolons, and the line is the statement. A directive ends at the end of the line, not at a ;. Writing #define MAX 100; makes every MAX expand to 100;, so int a[MAX]; becomes int a[100;]; and the error appears nowhere near the #define.
#define versus const. const int MAX = 100; is a real variable with a type the compiler checks and a name the debugger knows. #define MAX 100 is a find-and-replace with no type at all. Prefer const for values, and keep #define for array sizes, conditional compilation and include guards — the jobs const cannot do.
💡
By convention macro names are SHOUTED. MAX_ACC, PASS_MARK, FILENAME. It is only a convention, and the compiler does not enforce it — but it is the reader's only warning that a name is text substitution rather than a variable, so it is worth keeping.
The classic preprocessor bug

Two macros, one character apart, different answers

macro_bad.c · broken
#define SQUARE(x) x * x

int a = 2, b = 3;

printf("SQUARE(5)     = %d\n", SQUARE(5));
printf("SQUARE(a + b) = %d\n", SQUARE(a + b));
printf("expected      = %d\n", (a + b) * (a + b));
macro_good.c · fixed
#define SQUARE(x) ((x) * (x))

int a = 2, b = 3;

printf("SQUARE(5)     = %d\n", SQUARE(5));
printf("SQUARE(a + b) = %d\n", SQUARE(a + b));
printf("expected      = %d\n", (a + b) * (a + b));
Where 11 came from. The preprocessor does not evaluate a + b and square the result — it substitutes the text. SQUARE(a + b) becomes a + b * a + b, and now C's ordinary precedence applies: 2 + (3 × 2) + 3 = 11. Nothing warned, nothing crashed, and both programs compiled cleanly — the only symptom is a wrong number. Wrapping every argument, ((x) * (x)), forces the addition to happen first and the answer back to 25.
The second trap: double evaluation. SQUARE(i++) expands to ((i++) * (i++)) — i is incremented twice, and the result is undefined behaviour on top. A macro's argument must never be an expression with a side effect, and there is nothing the macro's author can do about it.
The rules, then. Parenthesise every argument and the whole body. Shout the name. Keep it to one expression. And in modern C, prefer a small function — int square(int x) { return x * x; } has none of these problems, is type-checked, and the compiler will usually inline it anyway.
💡
When a macro still wins. When it must work for any type at once — #define MAX(a,b) ((a) > (b) ? (a) : (b)) works on ints, floats and chars, where a function would need one version each. That genuine advantage is why macros survive, and why their traps are worth knowing rather than avoiding.
More than one .c file

Declarations in the header, bodies in the source

marks.h
#ifndef MARKS_H
#define MARKS_H

#define PASS_MARK 40

/* declarations only */
float average(const float *a, int n);
int   passed(const float *a, int n);

#endif /* MARKS_H */
marks.c
#include "marks.h"

float average(const float *a, int n)
{
    float sum = 0.0f;
    for (int i = 0; i < n; i++)
        sum += a[i];
    return sum / n;
}

int passed(const float *a, int n)
{
    int count = 0;
    for (int i = 0; i < n; i++)
        if (a[i] >= PASS_MARK)
            count++;
    return count;
}
use.c
#include <stdio.h>
#include "marks.h"
#include "marks.h"   /* twice on purpose */

int main(void)
{
    float m[5] = {87.5f, 34.0f, 93.5f,
                  61.0f, 38.5f};

    printf("pass mark : %d\n", PASS_MARK);
    printf("average   : %.2f\n", average(m, 5));
    printf("passed    : %d of 5\n", passed(m, 5));
    return 0;
}
The include guard. The first time marks.h is pasted in, MARKS_H is not defined, so the body is kept — and defines MARKS_H. The second #include finds it already defined and the preprocessor throws the whole block away. Without it, use.c would declare everything twice, which for a struct or typedef is a compile error. Real headers include each other constantly, so every header gets one.
Compile both, and only name the .c files. gcc use.c marks.c -o use — the header is never passed to gcc, because it has already been pasted into both sources by the preprocessor. Leave marks.c off the command and the compiler is happy but the linker fails with "undefined reference to average": it saw the promise in the header and never found the body. That is Day 2's linker error, with a cause you can now name.
💡
The header is the contract. It tells every other file what exists and how to call it; the .c file decides how. Change a body and only that one file recompiles; change the header and everything that includes it must. That single split is how a program grows past one file without becoming unreadable — and it is exactly what <stdio.h> has been doing for you since Day 1.
Module 10 practical

Records that are still there tomorrow

practical10.c · 1/4
#define FILENAME "students.txt"

struct Student {
    int   roll;
    char  name[25];
    float marks;
};
practical10.c · 2/4
/* "a" creates the file if missing and always
   writes at the end. */
int add_record(const struct Student *s)
{
    FILE *fp = fopen(FILENAME, "a");
    if (fp == NULL) {
        perror(FILENAME);
        return 0;
    }
    fprintf(fp, "%d,%s,%.1f\n",
            s->roll, s->name, s->marks);
    fclose(fp);
    return 1;
}
practical10.c · 3/4
int list_records(void)
{
    struct Student s;
    char line[100];
    int n = 0;
    float total = 0.0f;

    FILE *fp = fopen(FILENAME, "r");
    if (fp == NULL) {
        printf("No records yet.\n");
        return 0;
    }
practical10.c · 4/4
    printf("\nROLL  NAME                MARKS\n");
    while (fgets(line, sizeof(line), fp) != NULL) {
        if (sscanf(line, "%d,%24[^,],%f",
                   &s.roll, s.name, &s.marks) != 3)
            continue;          /* skip a bad line */
        printf("%-5d %-19s %5.1f\n",
               s.roll, s.name, s.marks);
        total += s.marks;
        n++;
    }
    fclose(fp);

    if (n > 0)
        printf("%d records, average %.2f\n",
               n, total / n);
    return n;
}
Read the line, then pick it apart. fgets takes the whole line however many spaces the name has; sscanf then splits it on the commas. %24[^,] means "up to 24 characters that are not a comma" — which is how "Asha Menon" survives intact where %s would have stopped at the space.
  • Run it twice. The second run lists the records from the first — that is the whole point of the module, and the first program in this course whose output depends on what happened yesterday.
  • Comma-separated text is a real format. This file is a CSV: Excel will open it, a Python script will read it, and you can fix a typo in Notepad. That is what text buys you over fwrite.
  • Every fopen is checked and every one is closed, in both functions, including on the error paths — the file equivalent of Module 8's malloc/free discipline.
💡
Two exercises worth doing. Add a "search by roll" option — it is list_records with an if. Then try "delete a record", and discover the real limitation of a text file: you cannot remove a line in place. The standard answer is to copy every line you are keeping into a temporary file, then remove the original and rename the temporary one over it.
11

Data structures in C

Day 18 · Module 11 · Data structures

Today we cover

By the end of today you can build a linked list from nothing, add and remove items anywhere in it, and free the whole thing without leaking a single node.
Choosing the shape of your data

A way of holding data that makes some operations cheap

You have used one since Day 10. An array is a data structure: values laid out side by side, reached by index. It makes "give me item 500" instant and "insert at the front" expensive, because everything after has to shuffle up. A different layout makes a different set of operations cheap. That trade is the whole subject.
ArrayLinked list
One contiguous blockSeparate nodes, joined by pointers
Size fixed at creationGrows one node at a time, no limit
a[500] is instantItem 500 means 500 steps
Insert at front: shift everythingInsert at front: change one pointer
No memory per item wasted8 extra bytes per node for the link
Neighbours are next to each otherNodes can be anywhere in memory
The three questions to ask of any structure. How do I add something? How do I find something? How do I remove something? The answers, and what each costs, are what separate an array from a list from a stack from a queue — and they are all this module is about.
Two families. A linear structure lays its items in a single line: arrays, linked lists, stacks, queues — the ones on this course. A non-linear one branches: trees and graphs, where a node has several successors. Everything you learn here is what those are built from.
💡
Why this module comes last. A linked list needs a struct that holds a pointer to its own type (Module 9), memory allocated while the program runs (Module 8), and a pointer walked with -> until it hits NULL (Module 7). Nothing here is a new language feature. It is the three you already have, finally pointed at a problem worth their trouble.
The declaration everything rests on

A struct that holds a pointer to its own type

the node
struct Node {
    int data;
    struct Node *next;   /* a pointer to its own type */
};
impossible — will not compile
struct Node {
    int data;
    struct Node next;    /* NOT a pointer - infinite size */
};
Why one works and the other cannot. To lay out struct Node the compiler must know its size. If a Node contained a whole Node, that one would contain another, for ever — the size has no answer. A pointer to a Node is 8 bytes whatever it points at, and the compiler knows that before it knows anything else about the struct. The error message is field 'next' has incomplete type.
the chain it builds
head
 |
 v
+------+------+    +------+------+    +------+------+
|  10  |  o---+--> |  20  |  o---+--> |  30  | NULL |
+------+------+    +------+------+    +------+------+
 data   next        data   next        data   next
  • head is just a pointer to the first node — not a node itself, and not the list. Lose head and the whole chain is unreachable and unfreeable.
  • NULL in the last node's next is the end marker. Every loop in this module is "walk until the pointer is NULL" — the same job the '\0' does in a string.
  • An empty list is head == NULL. Not a special case to be avoided — it is the state every list starts in, and every function has to handle it.
💡
If you use a typedef, keep the tag. typedef struct Node { int data; struct Node *next; } Node; — inside the braces the name Node does not exist yet, so the field must still be written struct Node *next;. This is the case Module 9 warned about, and it is the one place it really bites.
One node at a time

Every node is a malloc, and every malloc is checked

list.c · make_node
struct Node *make_node(int value)
{
    struct Node *n = malloc(sizeof(struct Node));
    if (n == NULL) {
        printf("Out of memory\n");
        exit(1);
    }
    n->data = value;
    n->next = NULL;      /* a new node ends the list */
    return n;
}
a list of three, by hand
struct Node *head = make_node(10);
head->next       = make_node(20);
head->next->next = make_node(30);

/* head -> 10 -> 20 -> 30 -> NULL */
  • The node lives on the heap, so it outlives the function that made it. This is exactly the Module 8 point: a node built in a local variable would die on return, and the pointer handed back would dangle.
  • n->next = NULL; is not optional. A fresh malloc returns uninitialised memory, so next holds garbage. Walk a list whose last node has a garbage next and the loop follows it into memory that is not yours.
  • Returning struct Node * is why the caller can write head = make_node(10); — the function allocates, the caller owns.
Why exit(1) here rather than returning NULL? Because a teaching program has nothing sensible to do when the heap is exhausted, and checking one return value in one place keeps the list code readable. Real library code returns NULL and makes every caller handle it — which is more correct and would double the length of every slide that follows.
💡
Chaining by hand once is worth doing. Type those three lines and print head->next->next->data. Seeing 30 come out of a chain of arrows makes every function on the next three slides read as a shortcut for something you already did manually.
Three places, three functions

Rewire two pointers, in the right order

insert_front
struct Node *insert_front(
        struct Node *head, int value)
{
    struct Node *n = make_node(value);
    n->next = head;
    return n;      /* n is the new head */
}
insert_end
struct Node *insert_end(
        struct Node *head, int value)
{
    struct Node *n = make_node(value);
    if (head == NULL)
        return n;          /* empty list */

    struct Node *p = head;
    while (p->next != NULL)
        p = p->next;       /* walk to the last */
    p->next = n;
    return head;
}
insert_at (after position)
struct Node *insert_at(
        struct Node *head, int pos, int value)
{
    if (pos <= 1 || head == NULL)
        return insert_front(head, value);

    struct Node *p = head;
    for (int i = 1; i < pos - 1
                 && p->next != NULL; i++)
        p = p->next;

    struct Node *n = make_node(value);
    n->next = p->next;   /* link forward FIRST */
    p->next = n;         /* then link in */
    return head;
}
The order of those two lines is the whole bug. In insert_at, n->next = p->next; must come before p->next = n;. Do it the other way round and p->next has already been overwritten, so n->next points at n itself — the rest of the list is lost and any walk through it loops for ever.
Why every one of them returns the head. Inserting at the front makes a new first node, so the caller's head must change — and C passes by value, so the function cannot change the caller's variable by itself. Returning the head and writing head = insert_front(head, 5); is the simplest fix. The alternative is a struct Node **head parameter, which is Module 7's pointer-to-pointer doing the same job.
💡
Front is free, end is not. insert_front does the same tiny amount of work whether the list holds three nodes or three million. insert_end has to walk the whole chain first. Keeping a second pointer to the last node — a tail pointer — makes appending as cheap as prepending, and is the first optimisation anyone makes to this code.
Finding, and unlinking

Keep a trailing pointer, or you cannot unlink anything

list.c · find it, one step behind
struct Node *delete_value(struct Node *head, int value)
{
    struct Node *p = head, *prev = NULL;

    while (p != NULL && p->data != value) {
        prev = p;              /* one step behind */
        p = p->next;
    }
    if (p == NULL) {
        printf("%d not found\n", value);
        return head;
    }
list.c · unlink, then free
    if (prev == NULL)          /* deleting the head */
        head = p->next;
    else
        prev->next = p->next;  /* jump over p */

    free(p);
    printf("%d deleted\n", value);
    return head;
}
list.c · search
int search(const struct Node *head, int value)
{
    int pos = 1;
    for (const struct Node *p = head;
         p != NULL; p = p->next, pos++)
        if (p->data == value)
            return pos;
    return -1;
}
  • Three cases, and all three must work — the value is not there at all, the value is in the head, the value is somewhere after it. The prev == NULL test is what separates the second from the third.
  • free(p) comes last. Unlink first, then free — touching p->next after free(p) is a dangling read, and it is the commonest way this function is written wrong.
  • Searching is a walk, not a jump. There is no list[4] — finding position 4 means following four pointers. This is the price a list pays for its cheap inserts.
💡
Binary search is impossible here, even on a sorted list. It needs to jump straight to the middle, and a linked list has no middle it can reach without walking. A structure's shape decides which algorithms it can host — which is why the choice of structure matters more than the cleverness of the code inside it.
Walking the chain, and taking it down

Save the next pointer before you free the node

list.c · display
void display(const struct Node *head)
{
    printf("list: ");
    for (const struct Node *p = head; p != NULL; p = p->next)
        printf("%d -> ", p->data);
    printf("NULL\n");
}
list.c · free_all
void free_all(struct Node *head)
{
    struct Node *next;
    while (head != NULL) {
        next = head->next;   /* save it BEFORE freeing */
        free(head);
        head = next;
    }
}
broken — reads freed memory
while (head != NULL) {
    free(head);
    head = head->next;   /* head is already gone */
}
The broken version often appears to work. free does not wipe the memory or change your pointer — it just marks the block reusable. The old next value is usually still sitting there, so the loop limps along until the allocator reuses that block and the value turns to rubbish. It is undefined behaviour that passes its first test, which is the worst kind.
The traversal idiom, once and for all. for (p = head; p != NULL; p = p->next). Every list function on these slides is that loop with a different body: printing, counting, comparing, summing. Learn the loop and the rest is detail.
const struct Node * on the read-only ones. display and search promise not to modify the list, and the compiler holds them to it. free_all takes a plain pointer, because destroying the list is precisely its job.
💡
One malloc per node means one free per node. A 1,000-node list needs 1,000 calls to free, and the only way to reach node 1,000 is through node 999 — so losing head halfway through leaks everything after it. free_all is not tidying up; it is the only path those nodes will ever have back.
Day 19 · Module 11 · Data structures

Today we cover

By the end of today you can implement a stack and a queue from scratch, and explain which one a problem needs by asking a single question: which end does the next item have to come out of?
Last in, first out

One index, called top, does everything

stackarr.c · the structure
#define MAX 5

struct Stack {
    int items[MAX];
    int top;       /* index of the top; -1 when empty */
};

void init(struct Stack *s)  { s->top = -1; }
int  is_empty(const struct Stack *s) { return s->top == -1; }
int  is_full(const struct Stack *s)  { return s->top == MAX - 1; }
stackarr.c · push and pop
void push(struct Stack *s, int value)
{
    if (is_full(s)) {
        printf("overflow - cannot push %d\n", value);
        return;
    }
    s->items[++s->top] = value;
}

int pop(struct Stack *s)
{
    if (is_empty(s)) {
        printf("underflow - nothing to pop\n");
        return -1;
    }
    return s->items[s->top--];
}
  • LIFO — last in, first out. 10, 20, 30 went in; 30, 20, 10 came out. A stack of plates: you take the one you put down last.
  • ++s->top and s->top-- are doing real work. Pre-increment on the way in (move up, then store), post-decrement on the way out (read, then move down). Day 4's operators, earning their keep.
  • Both errors must be refused, not crashed on. Pushing onto a full array would write past its end; popping an empty one would read index −1. Guarding these two is most of what a correct stack is.
💡
You have been standing on one since Day 8. The call stack is a stack: each call pushes a frame, each return pops the most recent one. That is exactly why recursion unwinds in reverse order, and why an endless recursion is called a stack overflow — the same word this slide's guard prints.
The same rules, no ceiling

The head is the top

stacklist.c · push
/* push = insert at front, pop = delete from front,
   so both are one step and there is no MAX. */
struct Node *push(struct Node *top, int value)
{
    struct Node *n = malloc(sizeof(struct Node));
    if (n == NULL) exit(1);
    n->data = value;
    n->next = top;
    return n;
}
stacklist.c · pop
struct Node *pop(struct Node *top, int *out)
{
    if (top == NULL) {
        printf("underflow - nothing to pop\n");
        *out = -1;
        return NULL;
    }
    struct Node *next = top->next;
    *out = top->data;
    free(top);
    return next;
}
Identical output, different machinery. The array version refused a sixth push; this one never will, until the heap itself runs out. Same interface, same LIFO behaviour, different trade.
Array stackLinked stack
CapacityFixed MAXLimited only by the heap
OverflowMust be guardedCannot happen
Memory per itemJust the valueValue + 8-byte link
SpeedFaster — no allocationA malloc per push
Use whenYou know the maximumYou do not
💡
int *out is call by reference, again. pop already returns the new top, so the popped value needs a second way out — an address the caller supplies. Module 7's technique, used because a C function returns exactly one thing and this one has two answers.
A stack doing real work

Push what opens, pop what closes

paren.c · balanced()
int balanced(const char *s)
{
    char stack[MAX];
    int top = -1;

    for (int i = 0; s[i] != '\0'; i++) {
        char c = s[i];

        if (c == '(' || c == '[' || c == '{') {
            stack[++top] = c;              /* push */
        } else if (c == ')' || c == ']' || c == '}') {
            if (top == -1)                 /* closed, nothing open */
                return 0;
            char open = stack[top--];      /* pop */
            if (!matches(open, c))
                return 0;
        }
    }
    return top == -1;    /* everything opened was closed */
}
Why a stack is the right structure. Brackets must close in the reverse of the order they opened — which is precisely LIFO. The innermost open bracket is always the one on top, so it is always the one the next closer must match. No other structure gives you that for free.
Three ways it can fail, all handled. A closer with nothing open (top == -1); a closer against the wrong opener (!matches); and openers left over at the end (top != -1), which is what catches "((()". Miss the last one and the function calls an unfinished expression balanced.
💡
This is a real compiler doing a real job. The error "expected } at end of input" that GCC has shown you all course comes from exactly this algorithm, run over your source file. Editors highlight matching brackets the same way. It is also the standard interview question — and now you can say why the stack is not a clever trick but the only structure that fits the problem.
First in, first out

Two indices, and a % that makes the array bend round

cqueue.c · the structure
#define MAX 5

struct Queue {
    int items[MAX];
    int front, rear, count;
};

void init(struct Queue *q)
{ q->front = 0; q->rear = -1; q->count = 0; }
cqueue.c · enqueue
void enqueue(struct Queue *q, int value)
{
    if (q->count == MAX) {
        printf("full - cannot add %d\n", value);
        return;
    }
    q->rear = (q->rear + 1) % MAX;     /* wrap around */
    q->items[q->rear] = value;
    q->count++;
}
cqueue.c · dequeue
int dequeue(struct Queue *q)
{
    if (q->count == 0) {
        printf("empty - nothing to remove\n");
        return -1;
    }
    int value = q->items[q->front];
    q->front = (q->front + 1) % MAX;   /* wrap around */
    q->count--;
    return value;
}
Watch rear in the last two lines. It goes 4, then 0, then 1 — wrapping into the slots that 10 and 20 vacated. A plain, non-circular queue would have reported "full" there, with two free slots sitting unused at the front. That waste is the entire reason the circular version exists.
Why keep a count? Because once the indices wrap, front == rear is true both when the queue is empty and when it is full — the two states become indistinguishable. Counting the items removes the ambiguity. The alternative is to deliberately waste one slot so the two cases never collide.
💡
FIFO is fairness. A printer queue, keystrokes waiting to be processed, customers at a counter, packets on a network card — anywhere "whoever asked first gets served first" is the rule, the structure underneath is a queue. A stack would serve the most recent arrival first, which is exactly the wrong behaviour for all of them.
Day 20 · Module 11 · Data structures

Today we cover

By the end of today you can look at a problem and say which structure it wants, and defend the answer in terms of what each operation costs rather than what you happen to have used before.
One more pointer

A chain you can walk backwards

dll.c · the node and the link
struct Node {
    int data;
    struct Node *prev;   /* the only new field */
    struct Node *next;
};

struct Node *insert_end(struct Node *head, int value)
{
    struct Node *n = make_node(value);
    if (head == NULL)
        return n;

    struct Node *p = head;
    while (p->next != NULL)
        p = p->next;
    p->next = n;
    n->prev = p;         /* the backward link */
    return head;
}
dll.c · walking backwards
const struct Node *p = head;
while (p->next != NULL)     /* find the last node */
    p = p->next;

printf("backward: ");
for (; p != NULL; p = p->prev)
    printf("%d ", p->data);
The chain drawn out.
two links per node
NULL <-+      +-----+      +-----+
       |      |     |      |     |
    +--+--+ +-+---+-++  +--+--+--+
    | 10  |->| 20  |->  | 30  | NULL
    +-----+<-+-----+<-  +-----+
  • Deleting gets easier. In a singly linked list you must remember the previous node while walking. Here the node carries its own prev, so any node can unlink itself — the reason editors and browsers use one for undo and back/forward.
  • Every operation now maintains two links. Forget to set prev on an insert and the forward walk still looks perfect while the backward walk breaks — a bug that hides until someone goes back.
  • The cost: 8 more bytes per node, and twice the pointer bookkeeping in every function.
💡
The other variant worth knowing. In a circular list the last node's next points back at the head instead of NULL, so a walk never ends — useful for round-robin turns and playlists on repeat. Combine the two and you get a circular doubly linked list, which is what many real schedulers use.
What "efficient" means

How the cost grows when the data gets bigger

OperationCostWhy
Array — read a[i]O(1)One multiplication and one memory read, whatever i is
Array — insert at frontO(n)Every later element shifts up one place
Array — linear searchO(n)May have to look at every element
Sorted array — binary searchO(log n)Halves what is left with each comparison
List — insert at frontO(1)Make a node, point it at the old head
List — insert at endO(n)Must walk the whole chain to find the last node
List — searchO(n)No indexing; follow the links one at a time
Stack — push / popO(1)Only ever touches the top
Queue — enqueue / dequeueO(1)Only ever touches the two ends
Bubble sortO(n²)A pass for every element, and a comparison for every element in it
Read O(n) as "grows in step with the data". Double the items and it takes about twice as long. O(1) means the time does not depend on the size at all — one item or a million, the same work. O(n²) means doubling the data quadruples the time, which is why bubble sort is fine for 20 marks and hopeless for 20,000 records.
nO(1)O(log n)O(n)O(n2)
101310100
1,0001101,0001,000,000
1,000,0001201,000,0001012
  • Constants are deliberately ignored. O(n) and O(3n) are both written O(n), because the shape of the growth is what decides whether a program survives a bigger input — not whether it is three times quicker on today's.
  • It is about the worst case, unless someone says otherwise. Linear search might find the value first try; O(n) is the promise that it will never be worse than looking at everything.
💡
Small n makes all of this irrelevant. For twenty records, use whatever is clearest to read — the difference is microseconds. Big-O earns its keep at the point where the data grows and a program that was instant last year now takes a minute. Knowing which line of the table you are standing on tells you whether that will happen.
Choosing

Four structures, and the question each one answers

ArrayLinked listStackQueue
SizeFixedGrows freelyEitherEither
Reach item iO(1)O(n)Top onlyFront only
Insert frontO(n)O(1)——
Insert endO(1) if roomO(n)—O(1)
RemoveO(n)O(1) once foundO(1) topO(1) front
Order outAnyAnyLIFOFIFO
Memory overheadNoneA pointer per nodeDependsDepends
Stacks and queues are not rivals of the other two — they are restrictions. Each is built on an array or a list, and then deliberately refuses access to everything but one or two ends. That refusal is the feature: a structure that cannot be touched in the middle cannot be corrupted in the middle.
If the problem says…Reach for
"Marks of 50 students, fixed"Array
"Jump straight to record 900"Array
"However many the user enters"Linked list
"Insert and delete constantly"Linked list
"Undo the last action"Stack
"Check matching brackets"Stack
"Reverse something"Stack
"Serve in the order they arrived"Queue
"Print jobs, requests, buffering"Queue
"Go back and forward"Doubly linked list
💡
The question to ask, every time: how will this data be used? Not how it will be stored — how it will be reached. Mostly read by position, rarely changed? Array. Constantly growing and shrinking? List. Only ever the most recent item? Stack. Only ever the oldest? Queue. Get that answer right and the code writes itself; get it wrong and you spend the rest of the project fighting the shape you chose.
Module 11 practical

A phone book that stays in alphabetical order

phonebook.c · 1/6
struct Contact {
    char name[30];
    char phone[15];
    struct Contact *next;
};
phonebook.c · 2/6 · build the node
struct Contact *add_contact(struct Contact *head,
        const char *name, const char *phone)
{
    struct Contact *n = malloc(sizeof(struct Contact));
    if (n == NULL) return head;
    snprintf(n->name,  sizeof(n->name),  "%s", name);
    snprintf(n->phone, sizeof(n->phone), "%s", phone);
    n->next = NULL;
phonebook.c · 3/6 · link it in order
    /* goes before the head? */
    if (head == NULL || strcmp(name, head->name) < 0) {
        n->next = head;
        return n;
    }
    /* walk until the next name is bigger */
    struct Contact *p = head;
    while (p->next != NULL
           && strcmp(name, p->next->name) > 0)
        p = p->next;

    n->next = p->next;
    p->next = n;
    return head;
}
phonebook.c · 4/6 · search
const struct Contact *find_contact(
        const struct Contact *head, const char *name)
{
    for (const struct Contact *p = head; p != NULL; p = p->next)
        if (strcmp(p->name, name) == 0)
            return p;
    return NULL;
}
phonebook.c · 5/6 · delete
struct Contact *delete_contact(struct Contact *head,
                               const char *name)
{
    struct Contact *p = head, *prev = NULL;

    while (p != NULL && strcmp(p->name, name) != 0) {
        prev = p;
        p = p->next;
    }
    if (p == NULL) {
        printf("%s not found\n", name);
        return head;
    }
    if (prev == NULL) head = p->next;
    else              prev->next = p->next;

    free(p);
    return head;
}
phonebook.c · 6/6 · cleanup
void free_all(struct Contact *head)
{
    struct Contact *next;
    while (head != NULL) {
        next = head->next;
        free(head);
        head = next;
    }
}

/* main runs the menu, then calls free_all(head)
   once before returning - every node released. */
  • Rahul was entered first and still came out second. The list is never sorted, because it is never unsorted — each contact is linked into its alphabetical place as it arrives. Doing the same with an array would mean shifting every later element on every insert.
  • strcmp decides the order. Module 6's function, returning a negative number when the first string sorts earlier — which is exactly the test insert_at needed.
  • Every node is freed. delete_contact frees the one it unlinks, and free_all takes down whatever is left before main returns.
💡
This is the whole course in one file. A struct for the record (Module 9), a pointer to its own type for the chain (Module 11), malloc and free for the nodes (Module 8), -> on every line (Module 7), strcmp and snprintf for the strings (Module 6), functions with clear jobs (Module 5) and a menu loop (Module 4). Add Module 10's file saving and it is a program someone could actually use.
★

Capstone — Student Record System

One program, three files, and every single thing this course taught. Records held in a linked list, saved to a binary file, and brought back the next time it runs.

FeatureWhat it usesFrom
A student recordstruct, typedef, array memberModule 9
Any number of studentsmalloc, free, linked listModules 8, 11
Add, delete, searchPointers, ->, strcmpModules 6, 7
Sort by totalRelinking nodes, comparisonModules 6, 11
Totals, averages, gradesLoops, if-chains, floatModules 2, 4
Saving and loadingfopen, fwrite, freadModule 10
The menudo-while, switchModule 4
Three files, one programHeader, include guard, prototypesModules 5, 10
The contract

One header every file agrees on

student.h · the type
#ifndef STUDENT_H
#define STUDENT_H

#define MAX_NAME 40
#define SUBJECTS 5
#define DATA_FILE "students.dat"

typedef struct Student {
    int    roll;
    char   name[MAX_NAME];
    float  marks[SUBJECTS];
    struct Student *next;   /* the tag is needed here */
} Student;
student.h · the prototypes
Student       *add_student(Student *head, int roll,
                           const char *name, const float *marks);
Student       *delete_student(Student *head, int roll);
const Student *find_by_roll(const Student *head, int roll);
const Student *find_by_name(const Student *head, const char *name);
Student       *sort_by_total(Student *head);
void           free_all(Student *head);

float total_of(const Student *s);
float average_of(const Student *s);
char  grade_of(const Student *s);
void  print_report(const Student *head);

int      save_to_file(const Student *head);
Student *load_from_file(void);

#endif /* STUDENT_H */
  • The header holds no code — only the type, the three constants and thirteen promises. student.c keeps those promises; main.c relies on them and never needs to see how.
  • The struct Student tag is kept, even though the typedef gives the short name Student. Inside the braces that short name does not exist yet, so the self-link must be written struct Student *next; — the exact case Module 9 flagged.
  • const marks the read-only ones. find_by_roll, print_report and save_to_file promise not to change the list, and the compiler enforces it. delete_student and free_all take plain pointers, because changing is their job.
  • Four functions return Student * — each can change which node is the head, and C cannot change the caller's variable for them.
Build it with:
two sources, one header
$ gcc -Wall -Wextra -std=c11 main.c student.c -o srms
$ ./srms
💡
Read the header first, always. Handed an unfamiliar C project, the headers tell you what exists and what it is called before you read a single line of implementation. That is why this file is the first slide of the capstone and not the last.
student.c · the list

Add, delete, find, sort

student.c · add_student · fill the node
Student *add_student(Student *head, int roll,
                     const char *name, const float *marks)
{
    Student *n = malloc(sizeof(Student));
    if (n == NULL) {
        printf("Out of memory.\n");
        return head;
    }
    n->roll = roll;
    snprintf(n->name, sizeof(n->name), "%s", name);
    for (int i = 0; i < SUBJECTS; i++)
        n->marks[i] = marks[i];
    n->next = NULL;
student.c · add_student · link it in
    /* keep the list sorted by roll number */
    if (head == NULL || roll < head->roll) {
        n->next = head;
        return n;
    }
    Student *p = head;
    while (p->next != NULL && p->next->roll < roll)
        p = p->next;
    n->next = p->next;
    p->next = n;
    return head;
}
student.c · sort_by_total · detach
/* Insertion sort by RELINKING nodes - no data is
   copied, only pointers are rearranged. */
Student *sort_by_total(Student *head)
{
    Student *sorted = NULL;

    while (head != NULL) {
        Student *node = head;
        head = head->next;      /* detach the first */
        node->next = NULL;
student.c · sort_by_total · place it
        if (sorted == NULL
            || total_of(node) > total_of(sorted)) {
            node->next = sorted;
            sorted = node;
        } else {
            Student *p = sorted;
            while (p->next != NULL
                   && total_of(p->next) >= total_of(node))
                p = p->next;
            node->next = p->next;
            p->next = node;
        }
    }
    return sorted;
}
Two different sorted orders, on purpose. add_student keeps the list in roll-number order as records arrive, so the report reads like a register. sort_by_total rebuilds it in rank order, highest first. Both are the same technique — walk until the next node should come after the new one, then splice — with a different comparison.
Sorting moves pointers, not records. Nothing is copied and nothing is reallocated; the nodes stay exactly where malloc put them and only the next fields change. On an array of 64-byte records a sort has to move the records themselves — which is the trade Module 11's comparison table was describing.
💡
delete_student, find_by_roll and find_by_name are the Module 11 functions unchanged — a trailing prev pointer for the delete, a straight traversal for each search, strcmp for the name. Nothing in this capstone is a technique you have not already written once.
student.c · persistence

Save the record, never the pointer

student.c · what goes on disk
/* The record WITHOUT the next pointer. An address is
   meaningless in the next run, so it is never saved. */
struct Record {
    int   roll;
    char  name[MAX_NAME];
    float marks[SUBJECTS];
};
student.c · save_to_file
FILE *fp = fopen(DATA_FILE, "wb");
if (fp == NULL) { perror(DATA_FILE); return 0; }

int n = 0;
for (const Student *p = head; p != NULL; p = p->next) {
    struct Record r;
    r.roll = p->roll;
    snprintf(r.name, sizeof(r.name), "%s", p->name);
    for (int i = 0; i < SUBJECTS; i++)
        r.marks[i] = p->marks[i];

    if (fwrite(&r, sizeof r, 1, fp) != 1) {
        perror(DATA_FILE);
        fclose(fp);
        return 0;
    }
    n++;
}
fclose(fp);
student.c · load_from_file
FILE *fp = fopen(DATA_FILE, "rb");
if (fp == NULL)          /* no file yet is not an error */
    return NULL;

Student *head = NULL;
struct Record r;
int n = 0;

while (fread(&r, sizeof r, 1, fp) == 1) {
    head = add_student(head, r.roll, r.name, r.marks);
    n++;
}
fclose(fp);
return head;
This is the one idea worth taking away. A linked list cannot be written to a file as it stands, because every node holds an address — and an address from this run means nothing in the next one. So the list is flattened on the way out: each node becomes a plain struct Record, and the links are thrown away. On the way back in, add_student rebuilds them from scratch. The structure lives in memory; only the data goes to disk.
Two records made 128 bytes. 4 for the int, 40 for the name, 20 for five floats — 64 exactly, with no padding, because each member already sits on a 4-byte boundary. Module 9's padding slide, showing up in a real file size.
💡
fread in the loop condition is the correct idiom — while (fread(&r, sizeof r, 1, fp) == 1) tests the read itself, so it stops on the last real record rather than processing a stale one. A missing file simply returns NULL: the first run has nothing to load, and that is not a failure.
main.c · the whole thing running

Load, loop, save, free

main.c · load, then loop
int main(void)
{
    Student *head = load_from_file();   /* pick up where
                                           we left off */
    int choice, roll;
    char name[MAX_NAME];

    do {
        printf("\n=== Student Record System ===\n");
        printf("1. Add   2. Delete   3. Find by roll   "
               "4. Find by name\n");
        printf("5. Sort by total   6. Report   "
               "7. Save   8. Exit\n");
        printf("Enter choice: ");
        if (scanf("%d", &choice) != 1)
            break;
main.c · dispatch, save, free
        switch (choice) {
            case 1: read_and_add(&head); break;
            case 2: /* ... delete_student ... */ break;
            /* 3-7: find, sort, report, save */
            case 8:
                save_to_file(head);   /* save on the way out */
                printf("Goodbye.\n");
                break;
            default: printf("Invalid choice.\n");
        }
    } while (choice != 8);

    free_all(head);     /* every node released */
    return 0;
}
  • The second run is the capstone. The program was closed, the process ended, every malloced byte was handed back — and the records came back anyway, in roll order, rebuilt into a fresh linked list from 128 bytes on disk.
  • Note what the second run does not show. The sort from the first run is gone, because sorting rearranged pointers and the file only ever held records. add_student rebuilt the list in roll order on load. That is not a bug — it is the file layer doing exactly what it promised.
  • read_and_add(&head) takes the address of the head pointer — a Student **, so the helper can update main's own variable. Module 7's pointer-to-pointer, in the one place it earns its keep.
💡
Two lines carry the whole design. load_from_file() on the first line of main and free_all(head) on the last. Between them the program works entirely in memory and never thinks about the disk again — which is why the list code and the file code never got tangled together.
Your turn

Ten ways to make it yours

ExtensionWhat it exercises
Subject names, not just marks[5]Arrays of strings, a second struct
Edit an existing recordSearch, then write through the pointer
Search by partial namestrstr instead of strcmp
Class topper and subject topperTraversal with a running best
Pass/fail count and class averageOne pass, several accumulators
Export the report as CSVfprintf to a text file
Refuse a duplicate roll numberfind_by_roll before add_student
Undo the last deleteA stack of removed records
A doubly linked listA prev pointer, both walks
Split student.c into list + report + fileThree headers, one build command
Start with the duplicate-roll check. It is four lines, it uses a function that already exists, and it turns the program from a demonstration into something that defends itself against its user. Then do the CSV export — opening your own report in Excel is a disproportionately satisfying five minutes.
Then try the hard one. Splitting student.c into three files with three headers is not more code — it is the same code, reorganised, and it forces you to decide what each part is allowed to know about the others. That decision is most of what software design is, and it is the last thing this syllabus can teach you with a compiler.
Keep it. This program is a legitimate answer to "show me something you have written" — it has a data structure, a file format, a clean header and no leaks. Put it in a folder with a README saying how to build it, and it is the first entry in a portfolio.
💡
Every extension above is deliberately reachable with what you already know — not one of them needs a feature this course did not cover. That is the real measure of the twenty days: the syllabus is finished, and the problems you can now attack are not.
Everything, on one slide

The whole language, as far as this course goes

syntax, types & I/O
#include <stdio.h>
int main(void) { return 0; }

int i;  float f;  double d;  char c;  long l;
unsigned u;  const int MAX = 100;

printf("%d %f %c %s %p %zu\n", i, f, c, s, p, n);
scanf("%d", &i);          /* & on everything but a string */
scanf(" %39[^\n]", name); /* a whole line with spaces    */

sizeof(int)     /* bytes */
(float)a / b    /* cast before dividing two ints */
a % b           /* remainder - ints only */
a++  ++a  +=  -=  *=  /=
==  !=  <  >  <=  >=  &&  ||  !
cond ? yes : no
control flow & functions
if (a > b) { } else if (a == b) { } else { }

switch (n) { case 1: ...; break; default: ...; }

while (c) { }      do { } while (c);
for (int i = 0; i < n; i++) { }
break;   continue;

int add(int a, int b) { return a + b; }
void show(const int *a, int n);   /* prototype */

static int counter;   /* keeps its value  */
extern int shared;    /* defined elsewhere */

int fact(int n) {                 /* recursion */
    if (n <= 1) return 1;         /* base case */
    return n * fact(n - 1);
}
arrays, strings, pointers & memory
int a[5] = {1, 2, 3};      /* rest are 0 */
int m[3][4];               /* m[row][col] */
char s[20] = "hello";      /* '\0' at the end */

strlen(s)  strcpy(d,s)  strcmp(a,b)  strcat(d,s)

int *p = &n;    *p = 5;    p++;   /* moves sizeof(int) */
a[i]  ==  *(a + i)
void swap(int *a, int *b);  swap(&x, &y);

int *v = malloc(n * sizeof(int));
if (v == NULL) return 1;
v = NULL;   free(v);    /* free FIRST, then NULL */
calloc(n, size)   realloc(p, size)
structs, files & lists
struct Student { int roll; float marks; };
struct Student s = {101, 87.5f};
s.roll;    p->roll;    /* p->x is (*p).x */
typedef struct { ... } Student;
union U { int i; float f; };   /* shared bytes */
enum Day { MON = 1, TUE, WED };

FILE *fp = fopen("f.txt", "r");   /* r w a rb wb ab */
if (fp == NULL) { perror("f.txt"); return 1; }
fprintf(fp, ...);  fgets(line, sizeof line, fp);
fwrite(&r, sizeof r, 1, fp);  fread(&r, sizeof r, 1, fp);
fseek(fp, n, SEEK_SET);  ftell(fp);  fclose(fp);

struct Node { int data; struct Node *next; };
for (p = head; p != NULL; p = p->next)
Define the following terms

Every word a paper can ask you to define

TermOne-sentence definition
TokenThe smallest meaningful unit of a C program — keyword, identifier, constant, string literal, operator or punctuator
IdentifierA programmer-chosen name for a variable, function or type
KeywordA word reserved by the language itself, such as int or while, that cannot be used as an identifier
LiteralA fixed value written directly in the source, such as 42 or "hi"
ConstantA value that cannot change after it is set, declared with const or #define
VariableA named piece of memory whose value can change while the program runs
Data typeWhat kind of value a variable holds and how many bytes it occupies
OperatorA symbol that performs an operation on one or more operands
ExpressionAny combination of values, variables and operators that produces a value
StatementA complete instruction, ended by a semicolon
CompilerThe program that translates C source into machine code
PreprocessorThe first stage, which obeys the # directives by substituting text before compilation
LinkerThe stage that joins object files and libraries into one executable
Object fileThe compiled but not yet linked output of one source file
Header fileA .h file of declarations, included by the sources that need them
Function prototypeA declaration giving a function's name, return type and parameters, but no body
ParameterThe variable named in a function's definition
ArgumentThe actual value passed to a function when it is called
Call by valuePassing a copy, so the function cannot change the caller's variable
Call by referencePassing an address, so the function can change what it points at
ScopeThe region of the program in which a name is visible
Storage classHow long a variable lives and where it is visible — auto, static, extern, register
RecursionA function calling itself to solve a smaller version of the same problem
Base caseThe condition at which a recursive function stops calling itself
ArrayA fixed-size, contiguous collection of elements of one type
IndexThe position of an element in an array, counted from 0
TermOne-sentence definition
StringA character array ending in the null character
Null terminatorThe '\0' byte that marks the end of a C string
PointerA variable that stores the memory address of another variable
DereferenceFollowing a pointer with * to reach the value it points at
Pointer arithmeticAdding to or subtracting from a pointer, which moves it by whole elements, not bytes
Dangling pointerA pointer still holding the address of memory that has been freed or has gone out of scope
Memory leakHeap memory that was allocated but can no longer be freed, because nothing points at it any more
Stack (memory)The region holding local variables and call frames, freed automatically on return
HeapThe region from which malloc allocates, freed only when you call free
StructureA user-defined type grouping several members, each with its own storage
UnionLike a structure, but all members share one block of memory — only the last written is valid
EnumerationA user-defined type naming a fixed set of integer constants
typedefA new name for an existing type — it creates no new type
PaddingBytes the compiler inserts between members so each is correctly aligned
File pointerA FILE * handle returned by fopen, holding the position and state of an open file
StreamThe sequence of bytes flowing to or from a file or device
MacroA name defined with #define that the preprocessor replaces with its text
Include guardThe #ifndef / #define / #endif pair that stops a header being inserted twice
Data structureA way of organising data that makes particular operations on it cheap
NodeOne element of a linked structure, holding data and a pointer to the next
Linked listA chain of separately allocated nodes, each pointing to the next, ending at NULL
LIFOLast in, first out — the order a stack removes items in
FIFOFirst in, first out — the order a queue removes items in
TraversalVisiting every element of a structure exactly once
Time complexityHow the work an operation does grows as the amount of data grows, written in Big-O
Twenty days, done

Everything you have actually built

DayYou can now…
1Compile and run a C program, and explain every line✓
2Write an algorithm, name the token kinds, read an error✓
3Choose a data type and use the right format specifier✓
4Use every operator, and cast before dividing two ints✓
5Read input safely and format output into columns✓
6Branch with if chains and switch✓
7Write any loop, nest them, and print patterns✓
8Split a program into functions with prototypes✓
9Write a recursive function with a correct base case✓
10Search and sort an array three ways✓
DayYou can now…
11Work with 2D arrays and handle strings correctly✓
12Declare, dereference and do arithmetic on pointers✓
13Use call by reference, and pointers to functions✓
14Allocate at run time and free without leaking✓
15Design a record with struct and hold many of them✓
16Use typedef, union, enum, and predict sizeof✓
17Save data to a file and load it back next run✓
18Build, search and free a linked list✓
19Implement a stack and a circular queue✓
20Choose the right structure and justify it in Big-O✓
💡
Plus what the course made you do, not just read: 11 practicals, 6 practice sets worth 48 marks each, 6 timed tests, and a three-file capstone that saves its data and leaks nothing. Every program on every slide was compiled with -Wall -Wextra and run before it was shown to you.
After the syllabus

Four doors, and this course is the key to all of them

Embedded systems & firmware. Microcontrollers, Arduino, automotive and medical devices — C is still the language, often the only one. Everything you learned about pointers, sizeof, memory and bit operations is the daily work, and the discipline about malloc matters more, not less, on a chip with 2 KB of RAM.
Operating systems & systems programming. Linux, Windows kernels, databases, network stacks and the Python interpreter itself are written in C. The next step is processes, threads, sockets and system calls — all of which are C functions taking the pointers and structs you now read fluently.
Data structures & competitive programming. Module 11 is the doorway: trees, graphs, hashing, heaps and the algorithms over them are the DSA paper in every syllabus and the whole of the technical interview. You have the two things that make them learnable — pointers and dynamic memory.
C++, Rust and the rest. C++ is C with objects and templates bolted on, and it assumes you know this. Java, C#, JavaScript, Go and Python all borrow C's syntax, so their loops and conditions will already read as familiar. Rust exists specifically to solve the dangling-pointer and memory-leak problems you met in Module 8 — and nothing it does makes sense until you have been bitten by them in C.
💡
C is the language you learn once and read for ever. It has barely changed in fifty years; the hello.c from Day 1 would have compiled in 1990 and will compile in 2050. That stability is why it is the first language on so many syllabi — and why the twenty days behind you do not expire.
BrainBit Infotech Teaching · C Programming · Complete

You’re ready.

You started twenty days ago not knowing what a compiler was. You have written a bill calculator, a mark sheet, a menu calculator, a matrix multiplier, a bank, a phone book and a record system that remembers its data between runs — in the language the operating system under your fingers is written in.

20days
11modules
11practicals
1capstone
The only thing left is to keep writing it. Open the capstone, pick one line from “Extend it”, and make the program a little more yours than it was yesterday. That is the whole method, and it does not change from here.
BrainBit Infotech Teaching · C Programming · Final test

Now prove it.

Fifty questions across all eleven modules — twenty-five that ask you to read code and say what it prints, twenty-five that ask you why. Forty minutes. No notes, no compiler.

50questions
40minutes
50%to pass
1certificate
Score 50% or more and you earn a certificate from BrainBit Infotech Private Limited, with a number your college or employer can check for themselves. Sit it at brainbit-teaching.com/tests/?test=c-final.

All slides — click to jump (press O or Esc to close)

BrainBit Infotech Teaching· C Programming
→ / Space · next
←  ·  back  ·  F  ·  fullscreen