Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

C From The Ground Up

A progressive C learning workbook from basics to systems programming.

Each lesson is a small, readable program with the explanation in the comments and the code inline. The workbook is meant to stay close to the code that is actually here. This is not a polished application suite or portfolio showcase. It is a progression of lessons, exercises, and a few larger workbook-style projects.

How This Book Works

Every chapter follows the same pattern:

  1. A prose explanation of the concepts being taught
  2. The full, runnable C source file with detailed comments
  3. Compile-and-run instructions

The original .c files in the repository are the single source of truth. The comments are the lesson content. Read the chapter prose for the overview, then study the source to see it in action.

Structure

The book is organized into five progressive parts:

  • Part 1: The Beginner Path – Core language fundamentals from Hello World through strings
  • Part 2: The Intermediate Path – Pointers, structs, dynamic memory, file I/O, and command-line arguments
  • Part 3: The Advanced Path – Projects and deeper topics including function pointers, recursion, linked lists, bit manipulation, and a line-based text editor
  • Part 4: The Expert Path – Systems programming with sockets, process management, hash tables, and multithreading
  • Part 5: Expert Systems – Multi-file projects, external libraries, terminal UI with ncurses, data parsing, and a capstone text adventure

Quick Start

Most lessons are single-file programs. Pick one .c file, compile it, and run it:

cc -Wall -Wextra -std=c23 \
  "Part 1 - The Beginner Path_ Core Concepts/1_hello_world.c" \
  -o /tmp/1_hello_world

/tmp/1_hello_world

If your compiler does not support -std=c23 yet, use -std=c17.

Build Notes

  • The repo-level verification baseline prefers -std=c23 and falls back to -std=c17 when a compiler does not yet accept C23.
  • Most lessons compile with cc -Wall -Wextra -Wpedantic -Wstrict-prototypes -std=c23 lesson.c -o lesson_name.
  • Lessons 26 through 30 use POSIX APIs (sockets, fork, waitpid, pthread).
  • Lesson 30 needs -pthread.
  • Lesson 32 needs -lm.
  • Lessons 33 and 35 need -lncurses or -lncursesw, depending on your system.

Verification

The supported verification flow is:

mdbook build
CC=clang sh scripts/smoke_check.sh
CC=gcc sh scripts/smoke_check.sh

If your local GCC is versioned, use that compiler name instead, for example CC=gcc-15 sh scripts/smoke_check.sh.

GitHub Actions mirrors that verification path with mdbook build plus the smoke check under both Clang and GCC.

License

This project is licensed under the MIT License.

Hello, World!

Welcome to your first lesson in C! The journey to mastering C is challenging but incredibly rewarding. It all begins with this single step.

WHAT IS A COMPILER? A COMPILER is a special program that translates the human-readable C code you write into the machine code that your computer can execute. For this course, we will use GCC (GNU Compiler Collection).

The #include is a PREPROCESSOR DIRECTIVE. It’s a command that runs before the main compilation process starts. It tells the compiler to find the file specified in the angle brackets < > and copy its entire content into this file right here.

We are including stdio.h, which stands for “Standard Input/Output Header”. This header file contains declarations for essential functions used for input and output operations, including the printf function we will use below.

This is the MAIN FUNCTION. Every complete C program MUST have exactly one function named main. The execution of the program officially begins at the top of the main function.

Let’s break down its signature: int main(void)

  • int: This is the RETURN TYPE. It specifies that the main function will “return” an integer value to the operating system when it finishes. This integer is an “exit code” that signals whether the program ran successfully.

  • main: This is the required NAME for the starting function.

  • (void): This specifies the function’s PARAMETERS (the inputs it takes). void is a special keyword that means “nothing”. So, (void) tells us that this function takes no arguments or inputs.

printf() is a FUNCTION that “prints formatted” output to your console (the text window where you’ll run this program). This function is made available to us because we included <stdio.h>.

The text inside the double quotes, "Hello, World!\n", is a STRING LITERAL. This is the data we are passing as an argument to the printf function.

The \n at the end is a special “escape sequence” that represents a NEWLINE character. It tells the console to move the cursor to the next line after printing “Hello, World!”.

Finally, the semicolon ; at the end marks the end of a C STATEMENT. Most lines of executable code in C must end with a semicolon.

The return statement ends the execution of the main function. It sends a value back to the operating system. By convention, return 0; signals that the program executed successfully without any errors.

Full Source

/**
 * @file 1_hello_world.c
 * @brief Part 1, Lesson 1: Hello, World!
 * @author dunamismax
 * @date 06-15-2025
 *
 * This file is your very first C program and lesson.
 * The lesson is taught through the comments in this file. Read them
 * from top to bottom to understand what's happening.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * Welcome to your first lesson in C! The journey to mastering C is challenging
 * but incredibly rewarding. It all begins with this single step.
 *
 * WHAT IS A COMPILER?
 * A COMPILER is a special program that translates the human-readable C code you
 * write into the machine code that your computer can execute. For this course,
 * we will use GCC (GNU Compiler Collection).
 */

// --- Part 1: Preprocessor Directives ---

/*
 * The `#include` is a PREPROCESSOR DIRECTIVE. It's a command that runs
 * *before* the main compilation process starts. It tells the compiler to find
 * the file specified in the angle brackets `< >` and copy its entire content
 * into this file right here.
 *
 * We are including `stdio.h`, which stands for "Standard Input/Output Header".
 * This header file contains declarations for essential functions used for
 * input and output operations, including the `printf` function we will use below.
 */
#include <stdio.h>

// --- Part 2: The Main Function ---

/*
 * This is the MAIN FUNCTION. Every complete C program MUST have exactly one
 * function named `main`. The execution of the program officially begins at the
 * top of the `main` function.
 *
 * Let's break down its signature: `int main(void)`
 *
 * - `int`: This is the RETURN TYPE. It specifies that the `main` function will
 *   "return" an integer value to the operating system when it finishes. This
 *   integer is an "exit code" that signals whether the program ran successfully.
 *
 * - `main`: This is the required NAME for the starting function.
 *
 * - `(void)`: This specifies the function's PARAMETERS (the inputs it takes).
 *   `void` is a special keyword that means "nothing". So, `(void)` tells us
 *   that this function takes no arguments or inputs.
 */
int main(void)
{ // The opening brace `{` marks the beginning of the function's code block (its body).

    // --- Part 3: Statements and Functions ---

    /*
     * `printf()` is a FUNCTION that "prints formatted" output to your console
     * (the text window where you'll run this program). This function is made
     * available to us because we included `<stdio.h>`.
     *
     * The text inside the double quotes, `"Hello, World!\n"`, is a STRING LITERAL.
     * This is the data we are passing as an argument to the `printf` function.
     *
     * The `\n` at the end is a special "escape sequence" that represents a NEWLINE
     * character. It tells the console to move the cursor to the next line after
     * printing "Hello, World!".
     *
     * Finally, the semicolon `;` at the end marks the end of a C STATEMENT.
     * Most lines of executable code in C must end with a semicolon.
     */
    printf("Hello, World!\n");

    /*
     * The `return` statement ends the execution of the `main` function. It sends
     * a value back to the operating system. By convention, `return 0;` signals
     * that the program executed successfully without any errors.
     */
    return 0;

} // The closing brace `}` marks the end of the function's body.

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * Congratulations on writing your first C program!
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * 1. Open a terminal or command prompt.
 * 2. Navigate to the directory where you saved this file.
 * 3. Use the GCC compiler to translate this source code into an executable program.
 *    The command below does the following:
 *    - `gcc`: Invokes the compiler.
 *    - `-Wall -Wextra`: Enables all and extra warning flags to catch potential errors.
 *    - `-std=c11`: Specifies that we are using the C11 standard of the language.
 *    - `-o 1_hello_world`: Specifies the output file name should be `HelloWorld`.
 *    - `1_hello_world.c`: Specifies the input source file to compile.
 *
 *    `gcc -Wall -Wextra -std=c11 -o HelloWorld HelloWorld.c`
 *
 * 4. Run the newly created executable:
 *    - On Linux/macOS:   `./1_hello_world`
 *    - On Windows:       `1_hello_world.exe`
 *
 * You should see "Hello, World!" printed to your console.
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 1_hello_world 1_hello_world.c
./1_hello_world

Variables and Data Types

In the last lesson, we printed a fixed message. To write useful programs, we need a way to store and manipulate data that can change. This is where variables come in.

WHAT IS A VARIABLE? A VARIABLE is a named storage location in the computer’s memory. Think of it as a labeled box where you can store a piece of information. You give it a name, and you can change the contents of the box (the value) as your program runs.

WHAT IS A DATA TYPE? A DATA TYPE specifies the type of data that a variable can hold. C is a “statically-typed” language, which means you must explicitly tell the compiler the data type of every variable you create. This helps the compiler allocate the correct amount of memory and ensures you use the variable correctly.

The process of creating a variable is called DECLARATION. The process of giving a variable its first value is called INITIALIZATION.

An INTEGER is a whole number (no decimal point). The int data type is used to store them.

Below, we DECLARE a variable named player_score of type int. This tells the compiler: “Reserve a spot in memory for an integer, and I will call it player_score.”

Now we INITIALIZE the variable by assigning it a value using the assignment operator =.

To print the value of a variable, we use printf with a FORMAT SPECIFIER. A FORMAT SPECIFIER is a placeholder in the string that tells printf what type of data to expect and how to format it.

%d is the format specifier for a signed decimal integer (int). The variable player_score is passed as a second argument to printf, and its value will replace %d in the output.

To store numbers with decimal points, we use FLOATING-POINT types. The most common one is double, which stands for “double-precision”. It can store numbers with much greater precision than its counterpart, float. For this course, we will prefer double for all decimal numbers.

%f is the format specifier for a double (and float).

By default, %f might print many extra zeros. You can control the number of decimal places by modifying the format specifier. For example, %.2f tells printf to print only 2 digits after the decimal point.

The char data type is used to store a single CHARACTER. This can be a letter, a number, or a symbol.

A CHARACTER LITERAL is enclosed in SINGLE quotes (e.g., ‘A’). This is different from a string literal, which uses double quotes.

%c is the format specifier for a char.

You can print multiple variables in a single printf call. Just make sure the format specifiers and the variables are listed in the same order.

Full Source

/**
 * @file 2_variables_and_data_types.c
 * @brief Part 1, Lesson 2: Variables and Data Types
 * @author dunamismax
 * @date 06-15-2025
 *
 * This file introduces the fundamental concepts of variables and data types.
 * We learn how to store information in memory and print it to the console.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * In the last lesson, we printed a fixed message. To write useful programs,
 * we need a way to store and manipulate data that can change. This is where
 * variables come in.
 *
 * WHAT IS A VARIABLE?
 * A VARIABLE is a named storage location in the computer's memory. Think of it
 * as a labeled box where you can store a piece of information. You give it a
 * name, and you can change the contents of the box (the value) as your
 * program runs.
 *
 * WHAT IS A DATA TYPE?
 * A DATA TYPE specifies the type of data that a variable can hold. C is a
 * "statically-typed" language, which means you must explicitly tell the
 * compiler the data type of every variable you create. This helps the compiler
 * allocate the correct amount of memory and ensures you use the variable correctly.
 *
 * The process of creating a variable is called DECLARATION.
 * The process of giving a variable its first value is called INITIALIZATION.
 */

#include <stdio.h>

int main(void)
{
    // The main function body is where we'll declare and use our variables.

    // --- Part 1: The Integer (int) ---

    /*
     * An INTEGER is a whole number (no decimal point). The `int` data type is
     * used to store them.
     *
     * Below, we DECLARE a variable named `player_score` of type `int`.
     * This tells the compiler: "Reserve a spot in memory for an integer,
     * and I will call it `player_score`."
     */
    int player_score; // Declaration

    /*
     * Now we INITIALIZE the variable by assigning it a value using the
     * assignment operator `=`.
     */
    player_score = 1250; // Initialization

    /*
     * To print the value of a variable, we use `printf` with a FORMAT SPECIFIER.
     * A FORMAT SPECIFIER is a placeholder in the string that tells `printf`
     * what type of data to expect and how to format it.
     *
     * `%d` is the format specifier for a signed decimal integer (`int`).
     * The variable `player_score` is passed as a second argument to `printf`,
     * and its value will replace `%d` in the output.
     */
    printf("Player score: %d\n", player_score);

    // You can also declare and initialize a variable in a single statement.
    int number_of_lives = 3;
    printf("Number of lives: %d\n", number_of_lives);
    printf("\n"); // Print a blank line for separation

    // --- Part 2: The Double-Precision Floating-Point Number (double) ---

    /*
     * To store numbers with decimal points, we use FLOATING-POINT types.
     * The most common one is `double`, which stands for "double-precision".
     * It can store numbers with much greater precision than its counterpart, `float`.
     * For this course, we will prefer `double` for all decimal numbers.
     *
     * `%f` is the format specifier for a `double` (and `float`).
     */
    double item_price = 19.99; // Declaration and initialization
    printf("The price of the magic sword is: $%f\n", item_price);

    /*
     * By default, `%f` might print many extra zeros. You can control the
     * number of decimal places by modifying the format specifier. For example,
     * `%.2f` tells `printf` to print only 2 digits after the decimal point.
     */
    printf("The price, formatted neatly, is: $%.2f\n", item_price);
    printf("\n");

    // --- Part 3: The Character (char) ---

    /*
     * The `char` data type is used to store a single CHARACTER. This can be
     * a letter, a number, or a symbol.
     *
     * A CHARACTER LITERAL is enclosed in SINGLE quotes (e.g., 'A'). This is
     * different from a string literal, which uses double quotes.
     *
     * `%c` is the format specifier for a `char`.
     */
    char player_grade = 'A'; // Declaration and initialization
    printf("Player's performance grade: %c\n", player_grade);

    // --- Part 4: Putting It All Together ---

    /*
     * You can print multiple variables in a single `printf` call. Just make
     * sure the format specifiers and the variables are listed in the same order.
     */
    printf("Summary: Player with %d lives and a grade of '%c' has a score of %d.\n",
           number_of_lives, player_grade, player_score);

    return 0; // Signal successful execution
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * In this lesson, you learned about three fundamental data types:
 * - `int` for whole numbers.
 * - `double` for numbers with decimal points.
 * - `char` for single characters.
 *
 * You also learned how to DECLARE variables, INITIALIZE them with values, and
 * print them using `printf` with FORMAT SPECIFIERS (`%d`, `%f`, `%c`).
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * 1. Open a terminal or command prompt.
 * 2. Navigate to the directory where you saved this file.
 * 3. Use the GCC compiler to create an executable file:
 *    `gcc -Wall -Wextra -std=c11 -o 2_variables_and_data_types 2_variables_and_data_types.c`
 * 4. Run the executable:
 *    - On Linux/macOS:   `./2_variables_and_data_types`
 *    - On Windows:       `2_variables_and_data_types.exe`
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 2_variables_and_data_types 2_variables_and_data_types.c
./2_variables_and_data_types

User Input

So far, our programs have been one-way streets: they run and print things without any input from the outside world. To build truly useful tools, we need our programs to be interactive.

THE scanf FUNCTION The counterpart to printf (for printing) is scanf (for scanning/reading). The scanf function reads formatted input from the standard input, which is usually your keyboard.

Like printf, it uses FORMAT SPECIFIERS to know what type of data to expect. However, it has one very important new requirement we will explore.

To read input, we first need a variable to store that input in. We DECLARE an integer variable user_age. Notice we do not initialize it, because its value will be provided by the user.

It’s good practice to print a “prompt” to the user, so they know what you’re asking for.

Now, we call scanf. This is the most important part of the lesson.

  • "%d": This is the format specifier. It tells scanf to look for and read an integer from the keyboard input.

  • &user_age: This is new! The & symbol is the ADDRESS-OF OPERATOR. It gets the memory address of the user_age variable.

WHY DO WE NEED &? You must tell scanf where in memory to store the data it reads. Simply providing user_age would only give scanf the variable’s current value (which is garbage at this point). By providing &user_age, you are giving it the location of the box, so it can put the user’s input directly into it.

The pattern is the same for other data types. We just need to change the variable type and the format specifier.

IMPORTANT: For scanf, the format specifier for a double is %lf (which stands for “long float”). This is DIFFERENT from printf, which uses %f for a double. This is a common point of confusion!

scanf can sometimes behave in unexpected ways because of how it handles whitespace (like spaces, tabs, and newlines).

When you typed your GPA and pressed Enter, the \n (newline character) was left behind in the INPUT BUFFER. A subsequent scanf call for a character (%c) would read that leftover newline instead of waiting for you to type a new character!

THE FIX: To fix this, we put a single space before the %c in the format string: " %c". That leading space tells scanf to “skip any and all leading whitespace characters before reading the character”. It’s a crucial trick.

Full Source

/**
 * @file 3_user_input.c
 * @brief Part 1, Lesson 3: User Input
 * @author dunamismax
 * @date 06-15-2025
 *
 * This lesson demonstrates how to make programs interactive by reading
 * input from the user's keyboard.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * So far, our programs have been one-way streets: they run and print things
 * without any input from the outside world. To build truly useful tools,
 * we need our programs to be interactive.
 *
 * THE `scanf` FUNCTION
 * The counterpart to `printf` (for printing) is `scanf` (for scanning/reading).
 * The `scanf` function reads formatted input from the standard input, which is
 * usually your keyboard.
 *
 * Like `printf`, it uses FORMAT SPECIFIERS to know what type of data to expect.
 * However, it has one very important new requirement we will explore.
 */

#include <stdio.h>

int main(void)
{
    // --- Part 1: Reading an Integer ---

    /*
     * To read input, we first need a variable to store that input in.
     * We DECLARE an integer variable `user_age`. Notice we do not initialize it,
     * because its value will be provided by the user.
     */
    int user_age;

    /*
     * It's good practice to print a "prompt" to the user, so they know
     * what you're asking for.
     */
    printf("Please enter your age (as a whole number): ");

    /*
     * Now, we call `scanf`. This is the most important part of the lesson.
     *
     * - `"%d"`: This is the format specifier. It tells `scanf` to look for
     *   and read an integer from the keyboard input.
     *
     * - `&user_age`: This is new! The `&` symbol is the ADDRESS-OF OPERATOR.
     *   It gets the memory address of the `user_age` variable.
     *
     * WHY DO WE NEED `&`?
     * You must tell `scanf` *where* in memory to store the data it reads.
     * Simply providing `user_age` would only give `scanf` the variable's
     * *current value* (which is garbage at this point). By providing `&user_age`,
     * you are giving it the *location* of the box, so it can put the user's
     * input directly into it.
     */
    scanf("%d", &user_age);

    // Let's print the value to confirm we received it correctly.
    printf("Thank you. You entered %d.\n", user_age);
    printf("\n"); // Add a blank line for spacing.

    // --- Part 2: Reading a Double ---

    /*
     * The pattern is the same for other data types. We just need to change
     * the variable type and the format specifier.
     *
     * IMPORTANT: For `scanf`, the format specifier for a `double` is `%lf`
     * (which stands for "long float"). This is DIFFERENT from `printf`,
     * which uses `%f` for a `double`. This is a common point of confusion!
     */
    double user_gpa;
    printf("Please enter your GPA (e.g., 3.8): ");
    scanf("%lf", &user_gpa); // Use %lf for reading a double!

    printf("Your GPA is %.2f.\n", user_gpa); // Use %f for printing a double.
    printf("\n");

    // --- Part 3: A Common `scanf` Pitfall and Reading a Character ---

    /*
     * `scanf` can sometimes behave in unexpected ways because of how it handles
     * whitespace (like spaces, tabs, and newlines).
     *
     * When you typed your GPA and pressed Enter, the `\n` (newline character)
     * was left behind in the INPUT BUFFER. A subsequent `scanf` call for a
     * character (`%c`) would read that leftover newline instead of waiting for
     * you to type a new character!
     *
     * THE FIX: To fix this, we put a single space before the `%c` in the format
     * string: `" %c"`. That leading space tells `scanf` to "skip any and all
     * leading whitespace characters before reading the character". It's a crucial trick.
     */
    char favorite_letter;
    printf("Please enter your favorite letter: ");
    scanf(" %c", &favorite_letter); // The space before %c is very important!

    printf("Your favorite letter is '%c'.\n", favorite_letter);

    return 0;
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * In this lesson, you learned the basics of making your programs interactive.
 *
 * Key Takeaways:
 * - The `scanf()` function reads formatted input from the keyboard.
 * - `scanf()` requires the ADDRESS-OF OPERATOR `&` before the variable name to know
 *   *where* to store the input.
 * - Format specifiers for `scanf()` can sometimes be different from `printf()`.
 *   - For `double`: Use `%lf` with `scanf`, but `%f` with `printf`.
 * - `scanf()` can leave newline characters in the input buffer, which can cause
 *   problems. The `" %c"` trick (with a leading space) is used to solve this
 *    when reading characters.
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * 1. Open a terminal or command prompt.
 * 2. Navigate to the directory where you saved this file.
 * 3. Use the GCC compiler to create an executable file:
 *    `gcc -Wall -Wextra -std=c11 -o 3_user_input 3_user_input.c`
 * 4. Run the executable:
 *    - On Linux/macOS:   `./3_user_input`
 *    - On Windows:       `3_user_input.exe`
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 3_user_input 3_user_input.c
./3_user_input

Basic Operators

In programming, an OPERATOR is a special symbol that performs an operation on one or more values or variables (called OPERANDS). For example, in 5 + 3, the + is the operator and 5 and 3 are the operands.

We will explore three main categories of operators in this lesson:

  1. Arithmetic Operators: For performing mathematical calculations.
  2. Relational Operators: For comparing two values.
  3. Logical Operators: For combining multiple comparisons.

/ (Division) - This one is tricky! When you divide two integers in C, the result is also an integer. The fractional part is simply thrown away (truncated). This is called INTEGER DIVISION.

To get a precise decimal result, at least one of the operands must be a floating-point type (like double). We can achieve this by “casting” one of the integers to a double.

% (Modulus) This operator gives you the REMAINDER of an integer division. It is incredibly useful in many programming scenarios.

RELATIONAL OPERATORS are used to compare two values. The result of a relational operation is a “truth” value. In C, there is no built-in boolean type like true or false. Instead, C uses integers:

  • 0 represents FALSE.
  • Any non-zero integer represents TRUE (typically 1).

LOGICAL OPERATORS are used to combine or modify logical (true/false) expressions.

Just like in math (PEMDAS/BODMAS), operators in C have a default order of evaluation called OPERATOR PRECEDENCE. For example, * and / are evaluated before + and -.

You can use parentheses () to force a specific order of evaluation, just like in math. It’s often a good idea to use parentheses to make your code clearer, even when not strictly necessary.

Full Source

/**
 * @file 4_basic_operators.c
 * @brief Part 1, Lesson 4: Basic Operators
 * @author dunamismax
 * @date 06-15-2025
 *
 * This lesson covers the fundamental operators in C used for performing
 * calculations and making comparisons.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * In programming, an OPERATOR is a special symbol that performs an operation on
 * one or more values or variables (called OPERANDS). For example, in `5 + 3`,
 * the `+` is the operator and `5` and `3` are the operands.
 *
 * We will explore three main categories of operators in this lesson:
 * 1. Arithmetic Operators: For performing mathematical calculations.
 * 2. Relational Operators: For comparing two values.
 * 3. Logical Operators: For combining multiple comparisons.
 */

#include <stdio.h>

int main(void)
{
    // Let's declare some variables to work with.
    int a = 10;
    int b = 3;

    // --- Part 1: Arithmetic Operators ---

    printf("--- Part 1: Arithmetic Operators ---\n");
    printf("Initial values: a = %d, b = %d\n\n", a, b);

    // `+` (Addition)
    printf("a + b = %d\n", a + b);

    // `-` (Subtraction)
    printf("a - b = %d\n", a - b);

    // `*` (Multiplication)
    printf("a * b = %d\n", a * b);

    /*
     * `/` (Division) - This one is tricky!
     * When you divide two integers in C, the result is also an integer.
     * The fractional part is simply thrown away (truncated). This is called
     * INTEGER DIVISION.
     */
    printf("Integer Division (a / b) = %d\n", a / 3); // 10 / 3 is 3, not 3.333

    /*
     * To get a precise decimal result, at least one of the operands must be a
     * floating-point type (like `double`). We can achieve this by "casting"
     * one of the integers to a double.
     */
    double precise_division = (double)a / b;
    printf("Floating-Point Division ((double)a / b) = %f\n", precise_division);

    /*
     * `%` (Modulus)
     * This operator gives you the REMAINDER of an integer division. It is
     * incredibly useful in many programming scenarios.
     */
    printf("Modulus (a %% b) = %d\n", a % b); // 10 divided by 3 is 3 with a remainder of 1.
    // Note: To print a literal '%' character in printf, you must type '%%'.
    printf("\n");

    // --- Part 2: Relational Operators ---

    /*
     * RELATIONAL OPERATORS are used to compare two values. The result of a
     * relational operation is a "truth" value. In C, there is no built-in
     * boolean type like `true` or `false`. Instead, C uses integers:
     * - `0` represents FALSE.
     * - Any non-zero integer represents TRUE (typically `1`).
     */
    printf("--- Part 2: Relational Operators ---\n");
    printf("The results below will be 1 for TRUE and 0 for FALSE.\n\n");

    // `==` (Equal to) - VERY IMPORTANT:
    // A single `=` is for assignment. A double `==` is for comparison.
    // This is one of the most common beginner mistakes!
    printf("Is a == 10? %d\n", a == 10); // True (1)
    printf("Is a == b?  %d\n", a == b);  // False (0)

    // `!=` (Not equal to)
    printf("Is a != b?  %d\n", a != b); // True (1)

    // `>` (Greater than)
    printf("Is a > b?   %d\n", a > b); // True (1)

    // `<` (Less than)
    printf("Is a < b?   %d\n", a < b); // False (0)

    // `>=` (Greater than or equal to)
    printf("Is a >= 10? %d\n", a >= 10); // True (1)

    // `<=` (Less than or equal to)
    printf("Is b <= 3?  %d\n", b <= 3); // True (1)
    printf("\n");

    // --- Part 3: Logical Operators ---

    /*
     * LOGICAL OPERATORS are used to combine or modify logical (true/false) expressions.
     */
    printf("--- Part 3: Logical Operators ---\n");
    int is_player_alive = 1; // 1 for true
    int has_key = 0;         // 0 for false
    printf("Player is alive: %d, Player has key: %d\n\n", is_player_alive, has_key);

    // `&&` (Logical AND) - evaluates to true ONLY if BOTH operands are true.
    printf("Can open door (is_player_alive && has_key)? %d\n", is_player_alive && has_key);

    // `||` (Logical OR) - evaluates to true if AT LEAST ONE operand is true.
    printf("Is player alive OR has key? %d\n", is_player_alive || has_key);

    // `!` (Logical NOT) - inverts the truth value of its operand.
    printf("The opposite of has_key (!has_key) is: %d\n", !has_key);
    printf("\n");

    // --- Part 4: Operator Precedence ---
    printf("--- Part 4: Operator Precedence ---\n");
    /*
     * Just like in math (PEMDAS/BODMAS), operators in C have a default order
     * of evaluation called OPERATOR PRECEDENCE. For example, `*` and `/` are
     * evaluated before `+` and `-`.
     */
    int result1 = 5 + 2 * 10;             // 2*10 is done first, then 5 is added.
    printf("5 + 2 * 10 = %d\n", result1); // Prints 25, not 70.

    /*
     * You can use parentheses `()` to force a specific order of evaluation,
     * just like in math. It's often a good idea to use parentheses to make
     * your code clearer, even when not strictly necessary.
     */
    int result2 = (5 + 2) * 10;             // (5+2) is done first.
    printf("(5 + 2) * 10 = %d\n", result2); // Prints 70.

    return 0;
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * In this lesson, you learned about C's fundamental operators:
 * - Arithmetic (`+`, `-`, `*`, `/`, `%`) for calculations.
 * - Relational (`==`, `!=`, `>`, `<`, `>=`, `<=`) for comparisons.
 * - Logical (`&&`, `||`, `!`) for combining true/false conditions.
 *
 * You also learned about integer division, the modulus operator, and how to
 * use parentheses `()` to control the order of operations.
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * 1. Open a terminal or command prompt.
 * 2. Navigate to the directory where you saved this file.
 * 3. Use the GCC compiler to create an executable file:
 *    `gcc -Wall -Wextra -std=c11 -o 4_basic_operators 4_basic_operators.c`
 * 4. Run the executable:
 *    - On Linux/macOS:   `./4_basic_operators`
 *    - On Windows:       `4_basic_operators.exe`
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 4_basic_operators 4_basic_operators.c
./4_basic_operators

Conditional Statements

Our programs so far have been linear; they execute every line of code from top to bottom in the same order every time. To create powerful and intelligent programs, we need them to be able to make decisions.

CONDITIONAL STATEMENTS allow us to control the flow of our program’s execution. They use the relational and logical operators from the previous lesson to evaluate conditions. If a condition is true (evaluates to a non-zero value), one block of code is executed. If it’s false (evaluates to 0), that block is skipped, and optionally, another block is executed.

The primary tool for this is the if statement.

The if statement is the simplest form of a conditional. It executes a block of code ONLY if its condition is true.

Syntax: if (condition) { // Code to execute if ‘condition’ is true }

The else statement provides an alternative block of code to execute when the if condition is false.

Syntax: if (condition) { // Code to execute if ‘condition’ is true } else { // Code to execute if ‘condition’ is false }

To check for multiple, mutually exclusive conditions, you can chain else if statements. The program will check each condition in order. As soon as it finds a true condition, it runs that block and skips the rest of the entire chain. The final else is optional and acts as a “catch-all” if none of the preceding conditions were true.

Note on logical combinations: We can use logical operators (&&, ||) to create more complex conditions. For example, to check if a grade is a B (80-89):

Full Source

/**
 * @file 5_conditional_statements.c
 * @brief Part 1, Lesson 5: Conditional Statements
 * @author dunamismax
 * @date 06-15-2025
 *
 * This lesson introduces conditional statements, which allow a program
 * to make decisions and execute different code blocks based on specific conditions.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * Our programs so far have been linear; they execute every line of code from
 * top to bottom in the same order every time. To create powerful and intelligent
 * programs, we need them to be able to make decisions.
 *
 * CONDITIONAL STATEMENTS allow us to control the flow of our program's execution.
 * They use the relational and logical operators from the previous lesson to
 * evaluate conditions. If a condition is true (evaluates to a non-zero value),
 * one block of code is executed. If it's false (evaluates to 0), that block
 * is skipped, and optionally, another block is executed.
 *
 * The primary tool for this is the `if` statement.
 */

#include <stdio.h>

int main(void)
{
    // --- Part 1: The `if` Statement ---

    /*
     * The `if` statement is the simplest form of a conditional. It executes a
     * block of code ONLY if its condition is true.
     *
     * Syntax:
     * if (condition) {
     *     // Code to execute if 'condition' is true
     * }
     */
    int score = 100;

    printf("--- Part 1: The 'if' Statement ---\n");
    printf("Initial score: %d\n", score);

    // This condition `score == 100` is true, so the printf inside the braces will run.
    if (score == 100)
    {
        printf("Congratulations on a perfect score!\n");
    }

    // This condition `score > 100` is false, so this block is skipped.
    if (score > 100)
    {
        printf("This message will not be printed.\n");
    }
    printf("\n");

    // --- Part 2: The `if-else` Statement ---

    /*
     * The `else` statement provides an alternative block of code to execute
     * when the `if` condition is false.
     *
     * Syntax:
     * if (condition) {
     *     // Code to execute if 'condition' is true
     * } else {
     *     // Code to execute if 'condition' is false
     * }
     */
    int number_to_check;

    printf("--- Part 2: The 'if-else' Statement ---\n");
    printf("Enter a whole number: ");
    scanf("%d", &number_to_check);

    // We use the modulus operator `%` to check for an even number.
    // If a number divided by 2 has a remainder of 0, it's even.
    if (number_to_check % 2 == 0)
    {
        printf("%d is an even number.\n", number_to_check);
    }
    else
    {
        printf("%d is an odd number.\n", number_to_check);
    }
    printf("\n");

    // --- Part 3: The `if-else if-else` Chain ---

    /*
     * To check for multiple, mutually exclusive conditions, you can chain
     * `else if` statements. The program will check each condition in order.
     * As soon as it finds a true condition, it runs that block and skips the
     * rest of the entire chain. The final `else` is optional and acts as a
     * "catch-all" if none of the preceding conditions were true.
     */
    int user_grade;

    printf("--- Part 3: The 'if-else if-else' Chain ---\n");
    printf("Enter your numerical grade (0-100): ");
    scanf("%d", &user_grade);

    if (user_grade >= 90)
    {
        printf("Your letter grade is A.\n");
    }
    else if (user_grade >= 80)
    {
        printf("Your letter grade is B.\n");
    }
    else if (user_grade >= 70)
    {
        printf("Your letter grade is C.\n");
    }
    else if (user_grade >= 60)
    {
        printf("Your letter grade is D.\n");
    }
    else
    {
        printf("Your letter grade is F.\n");
    }

    /*
     * Note on logical combinations:
     * We can use logical operators (`&&`, `||`) to create more complex conditions.
     * For example, to check if a grade is a B (80-89):
     */
    if (user_grade >= 80 && user_grade < 90)
    {
        printf("This is a solid B.\n");
    }

    return 0;
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * In this lesson, you've learned how to give your program a brain.
 *
 * Key Takeaways:
 * - The `if` statement executes code only if a condition is true.
 * - The `if-else` statement provides an alternative path for when the condition is false.
 * - The `if-else if-else` chain allows you to test a series of conditions in order,
 *   executing only the first one that is true.
 * - These constructs are the fundamental building blocks for creating logic
 *   and making your programs smart and responsive.
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * 1. Open a terminal or command prompt.
 * 2. Navigate to the directory where you saved this file.
 * 3. Use the GCC compiler to create an executable file:
 *    `gcc -Wall -Wextra -std=c11 -o 5_conditional_statements 5_conditional_statements.c`
 * 4. Run the executable:
 *    - On Linux/macOS:   `./5_conditional_statements`
 *    - On Windows:       `5_conditional_statements.exe`
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 5_conditional_statements 5_conditional_statements.c
./5_conditional_statements

Loops

Imagine you needed to print “Hello!” five times. You could write: printf(“Hello!\n”); printf(“Hello!\n”); printf(“Hello!\n”); printf(“Hello!\n”); printf(“Hello!\n”);

This is tedious and not scalable. What if you needed to do it 500 times? This is where LOOPS come in. A LOOP is a programming structure that repeats a sequence of instructions until a specific condition is met. Each single execution of the loop’s body is called an ITERATION.

C has three main types of loops:

  1. The for loop
  2. The while loop
  3. The do-while loop

The for loop is ideal when you know exactly how many times you want to repeat a block of code. It has a very specific structure with three parts inside its parentheses, separated by semicolons.

for (INITIALIZATION; CONDITION; POST-ITERATION) { // Code to be repeated }

  1. INITIALIZATION: A statement that runs only ONCE before the loop starts. It’s typically used to declare and initialize a counter variable.
  2. CONDITION: An expression that is checked BEFORE each iteration. If the condition is true, the loop body runs. If it’s false, the loop terminates.
  3. POST-ITERATION: A statement that runs AFTER each iteration. It’s typically used to increment or decrement the counter variable. i++ is shorthand for i = i + 1.

The while loop is simpler. It repeats a block of code AS LONG AS its condition remains true. It’s useful when you don’t know the exact number of iterations beforehand.

while (condition) { // Code to be repeated }

The condition is checked BEFORE each iteration.

CRITICAL: You must ensure that something inside the loop’s body will eventually make the condition false. Otherwise, you will create an INFINITE LOOP, and your program will never end!

The do-while loop is a variation of the while loop. The key difference is that the condition is checked AT THE END of the loop’s body.

This means a do-while loop is GUARANTEED to execute at least once.

This makes it perfect for situations like input validation, where you need to get the input first before you can check if it’s valid.

Full Source

/**
 * @file 6_loops.c
 * @brief Part 1, Lesson 6: Loops
 * @author dunamismax
 * @date 06-15-2025
 *
 * This lesson introduces loops, which are control structures that allow
 * a block of code to be executed repeatedly.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * Imagine you needed to print "Hello!" five times. You could write:
 * printf("Hello!\n");
 * printf("Hello!\n");
 * printf("Hello!\n");
 * printf("Hello!\n");
 * printf("Hello!\n");
 *
 * This is tedious and not scalable. What if you needed to do it 500 times?
 * This is where LOOPS come in. A LOOP is a programming structure that repeats a
 * sequence of instructions until a specific condition is met. Each single
 * execution of the loop's body is called an ITERATION.
 *
 * C has three main types of loops:
 * 1. The `for` loop
 * 2. The `while` loop
 * 3. The `do-while` loop
 */

#include <stdio.h>

int main(void)
{
    // --- Part 1: The `for` Loop ---

    /*
     * The `for` loop is ideal when you know exactly how many times you want to
     * repeat a block of code. It has a very specific structure with three parts
     * inside its parentheses, separated by semicolons.
     *
     * for (INITIALIZATION; CONDITION; POST-ITERATION) {
     *     // Code to be repeated
     * }
     *
     * 1. INITIALIZATION: A statement that runs only ONCE before the loop starts.
     *    It's typically used to declare and initialize a counter variable.
     * 2. CONDITION: An expression that is checked BEFORE each iteration. If the
     *    condition is true, the loop body runs. If it's false, the loop terminates.
     * 3. POST-ITERATION: A statement that runs AFTER each iteration. It's
     *    typically used to increment or decrement the counter variable. `i++` is
     *    shorthand for `i = i + 1`.
     */
    printf("--- Part 1: The 'for' Loop (Countdown) ---\n");

    // Let's create a countdown from 5 down to 1.
    // Initialization: `int i = 5`
    // Condition: `i > 0` (loop as long as i is greater than 0)
    // Post-iteration: `i--` (decrement i by 1 after each loop; shorthand for i = i - 1)
    for (int i = 5; i > 0; i--)
    {
        printf("%d...\n", i);
    }
    printf("Blast off!\n\n");

    // --- Part 2: The `while` Loop ---

    /*
     * The `while` loop is simpler. It repeats a block of code AS LONG AS its
     * condition remains true. It's useful when you don't know the exact number
     * of iterations beforehand.
     *
     * while (condition) {
     *     // Code to be repeated
     * }
     *
     * The condition is checked BEFORE each iteration.
     *
     * CRITICAL: You must ensure that something inside the loop's body will
     * eventually make the condition false. Otherwise, you will create an
     * INFINITE LOOP, and your program will never end!
     */
    printf("--- Part 2: The 'while' Loop (Simple Menu) ---\n");

    int menu_choice = 0;

    // Loop as long as the user has not chosen option 4 to quit.
    while (menu_choice != 4)
    {
        printf("Menu:\n");
        printf("1. Start New Game\n");
        printf("2. Load Game\n");
        printf("3. Options\n");
        printf("4. Quit\n");
        printf("Enter your choice: ");

        scanf("%d", &menu_choice); // Get user input. This updates the loop variable.

        printf("You chose: %d\n\n", menu_choice);
    }
    printf("Thanks for playing!\n\n");

    // --- Part 3: The `do-while` Loop ---

    /*
     * The `do-while` loop is a variation of the `while` loop. The key difference
     * is that the condition is checked AT THE END of the loop's body.
     *
     * This means a `do-while` loop is GUARANTEED to execute at least once.
     *
     * This makes it perfect for situations like input validation, where you need
     * to get the input first *before* you can check if it's valid.
     */
    printf("--- Part 3: The 'do-while' Loop (Input Validation) ---\n");
    int secret_number;

    do
    {
        printf("Please enter a number between 1 and 10: ");
        scanf("%d", &secret_number);

        // This check happens AFTER the user has already entered a number.
        if (secret_number < 1 || secret_number > 10)
        {
            printf("Invalid input. Please try again.\n");
        }

    } while (secret_number < 1 || secret_number > 10); // The condition is checked here.

    printf("You successfully entered the number %d.\n", secret_number);

    return 0;
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * In this lesson, you learned how to make your program perform repetitive tasks.
 *
 * Key Takeaways:
 * - Loops are used to execute a block of code multiple times.
 * - The `for` loop is great for when you know the number of iterations in advance.
 *   It consists of an initializer, a condition, and a post-iteration step.
 * - The `while` loop is great for when you want to loop as long as a condition is
 *   true, and the number of iterations isn't fixed.
 * - The `do-while` loop is similar to `while`, but it always executes its body at
 *   least once because the condition is checked at the end.
 *
 * Mastering loops is a massive step towards writing complex and useful programs.
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * 1. Open a terminal or command prompt.
 * 2. Navigate to the directory where you saved this file.
 * 3. Use the GCC compiler to create an executable file:
 *    `gcc -Wall -Wextra -std=c11 -o 6_loops 6_loops.c`
 * 4. Run the executable:
 *    - On Linux/macOS:   `./6_loops`
 *    - On Windows:       `6_loops.exe`
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 6_loops 6_loops.c
./6_loops

Functions

As programs grow larger, putting all the code inside the main function becomes messy, hard to read, and difficult to maintain. If you have a task that needs to be performed multiple times, you don’t want to copy and paste the same block of code over and over.

A FUNCTION is a named, self-contained block of code that performs a specific task. Think of printf and scanf–they are functions that someone else wrote to perform the tasks of printing and scanning. Now, you will learn to write your own.

Using functions allows us to follow the “DRY” principle: Don’t Repeat Yourself.

There are three key parts to using a function:

  1. The DECLARATION (also called the PROTOTYPE): Informs the compiler about the function’s existence, its name, what type of value it returns, and what parameters it accepts.
  2. The DEFINITION: The actual code that makes up the function’s body.
  3. The CALL: The statement that executes the function.

A FUNCTION PROTOTYPE is a declaration that tells the compiler what a function looks like before it’s actually defined. This is crucial because the compiler reads files from top to bottom. If main tries to call a function that the compiler hasn’t seen yet, it will generate an error. Prototypes solve this.

A prototype consists of:

The return type of the function.

The name of the function.

The data types of the parameters it accepts, in order.

  • A semicolon at the end.

The main function will now act as a manager, calling our other functions to perform their specific tasks.

Below main, we provide the full DEFINITIONS for our functions. A definition includes the function’s header (which looks like the prototype, but without the semicolon) and its body inside curly braces {}.

This is an example of a function that takes no arguments and returns no value. void as a return type means it doesn’t send any value back. (void) as the parameter list means it doesn’t accept any input.

This function accepts one piece of data: an integer. Inside this function, that integer is known by the PARAMETER name character_level. A PARAMETER is a special local variable that holds the value of the ARGUMENT passed during the call.

This function takes two integers as input and, importantly, it has a RETURN VALUE. The return keyword does two things:

  1. It immediately ends the execution of this function.
  2. It sends the value that follows it back to wherever the function was called.

Full Source

/**
 * @file 7_functions.c
 * @brief Part 1, Lesson 7: Functions
 * @author dunamismax
 * @date 06-15-2025
 *
 * This lesson introduces functions, which are the fundamental building blocks
 * for organizing code into reusable, modular pieces.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * As programs grow larger, putting all the code inside the `main` function
 * becomes messy, hard to read, and difficult to maintain. If you have a task
 * that needs to be performed multiple times, you don't want to copy and paste
 * the same block of code over and over.
 *
 * A FUNCTION is a named, self-contained block of code that performs a specific
 * task. Think of `printf` and `scanf`--they are functions that someone else wrote
 * to perform the tasks of printing and scanning. Now, you will learn to write
 * your own.
 *
 * Using functions allows us to follow the "DRY" principle: Don't Repeat Yourself.
 *
 * There are three key parts to using a function:
 * 1. The DECLARATION (also called the PROTOTYPE): Informs the compiler about the
 *    function's existence, its name, what type of value it returns, and what
 *    parameters it accepts.
 * 2. The DEFINITION: The actual code that makes up the function's body.
 * 3. The CALL: The statement that executes the function.
 */

#include <stdio.h>

// --- Part 1: Function Declarations (Prototypes) ---

/*
 * A FUNCTION PROTOTYPE is a declaration that tells the compiler what a function
 * looks like *before* it's actually defined. This is crucial because the compiler
 * reads files from top to bottom. If `main` tries to call a function that the
 * compiler hasn't seen yet, it will generate an error. Prototypes solve this.
 *
 * A prototype consists of:
 * - The return type of the function.
 * - The name of the function.
 * - The data types of the parameters it accepts, in order.
 * - A semicolon at the end.
 */

// Prototype for a simple function that takes nothing and returns nothing.
void print_welcome_message(void);

// Prototype for a function that takes an integer as input and returns nothing.
// The name `character_level` inside the parentheses is optional in a prototype,
// but can be good for documentation.
void display_character_level(int character_level);

// Prototype for a function that takes two integers and returns an integer.
int calculate_damage(int strength, int weapon_power);

// --- Part 2: The `main` Function (The Caller) ---

/*
 * The `main` function will now act as a manager, calling our other functions
 * to perform their specific tasks.
 */
int main(void)
{
    // FUNCTION CALL: To execute a function, you write its name followed by
    // parentheses. This is a call to `print_welcome_message`.
    print_welcome_message();

    int player_level = 15;
    // This call passes the *value* of `player_level` (which is 15) as an
    // ARGUMENT to the `display_character_level` function.
    display_character_level(player_level);

    // Here, we call `calculate_damage` and store its result.
    // The values 20 and 8 are the ARGUMENTS we pass.
    int total_damage = calculate_damage(20, 8);

    printf("With 20 strength and a weapon power of 8...\n");
    printf("Total calculated damage is: %d\n", total_damage);

    return 0; // End the main program
}

// --- Part 3: Function Definitions (The Implementation) ---

/*
 * Below `main`, we provide the full DEFINITIONS for our functions. A definition
 * includes the function's header (which looks like the prototype, but without
 * the semicolon) and its body inside curly braces `{}`.
 */

/**
 * @brief Prints a standard welcome message to the console.
 *
 * This is an example of a function that takes no arguments and returns no value.
 * `void` as a return type means it doesn't send any value back.
 * `(void)` as the parameter list means it doesn't accept any input.
 */
void print_welcome_message(void)
{
    printf("--- Welcome to the Function Demo! ---\n\n");
}

/**
 * @brief Displays a character's level.
 * @param character_level The level to be displayed.
 *
 * This function accepts one piece of data: an integer. Inside this function,
 * that integer is known by the PARAMETER name `character_level`. A PARAMETER is a
 * special local variable that holds the value of the ARGUMENT passed during the call.
 */
void display_character_level(int character_level)
{
    printf("Your character is level %d.\n\n", character_level);
}

/**
 * @brief Calculates damage based on strength and weapon power.
 * @param strength The character's strength attribute.
 * @param weapon_power The power rating of the equipped weapon.
 * @return The total calculated damage as an integer.
 *
 * This function takes two integers as input and, importantly, it has a
 * RETURN VALUE. The `return` keyword does two things:
 * 1. It immediately ends the execution of this function.
 * 2. It sends the value that follows it back to wherever the function was called.
 */
int calculate_damage(int strength, int weapon_power)
{
    int calculated_value = strength + (weapon_power * 2);
    return calculated_value; // Send this value back to the caller (`main`)
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * In this lesson, you've unlocked one of the most powerful organizational tools in C.
 *
 * Key Takeaways:
 * - Functions let you break down large problems into smaller, manageable, and
 *   reusable pieces of code.
 * - A FUNCTION PROTOTYPE (declaration) tells the compiler a function exists before it's defined.
 * - A FUNCTION DEFINITION contains the actual code that runs.
 * - A FUNCTION CALL executes the function.
 * - PARAMETERS are the variables in a function's definition that accept input.
 * - ARGUMENTS are the actual values you pass to a function when you call it.
 * - The `return` keyword sends a value back from the function to the caller.
 * - A `void` return type means the function does not return any value.
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * 1. Open a terminal or command prompt.
 * 2. Navigate to the directory where you saved this file.
 * 3. Use the GCC compiler to create an executable file:
 *    `gcc -Wall -Wextra -std=c11 -o 7_functions 7_functions.c`
 * 4. Run the executable:
 *    - On Linux/macOS:   `./7_functions`
 *    - On Windows:       `7_functions.exe`
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 7_functions 7_functions.c
./7_functions

Arrays

So far, we have only used variables that can hold one value at a time. For example, int score = 100;. But what if you need to store the scores for an entire class of 30 students? Declaring 30 separate variables (score1, score2, etc.) would be extremely inefficient.

An ARRAY is a data structure that can store a fixed-size, sequential collection of elements of the SAME data type. Think of it as a row of numbered boxes, where each box holds a value of the same type.

To declare an array, you specify the data type of its elements, the name of the array, and the number of elements it can hold (its size) in square brackets [].

The following line declares an array named high_scores that can hold exactly 5 integers.

ACCESSING ARRAY ELEMENTS: You access individual elements of an array using an INDEX in square brackets. C uses ZERO-BASED INDEXING. This is a critical concept. It means the first element is at index 0, the second at index 1, and so on. For an array of size 5, the valid indices are 0, 1, 2, 3, and 4.

Accessing an index outside of this range (like high_scores[5]) leads to UNDEFINED BEHAVIOR, a common source of bugs and crashes in C programs.

Manually assigning each element is tedious. You can initialize an array when you declare it using an initializer list in curly braces {}.

If you provide an initializer list, you can let the compiler figure out the size of the array by leaving the square brackets empty.

The real power of arrays is realized when you combine them with loops. A for loop is perfect for iterating through an array, performing an action on each element.

Full Source

/**
 * @file 8_arrays.c
 * @brief Part 1, Lesson 8: Arrays
 * @author dunamismax
 * @date 06-15-2025
 *
 * This lesson introduces arrays, a fundamental data structure for storing
 * collections of elements of the same type.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * So far, we have only used variables that can hold one value at a time. For
 * example, `int score = 100;`. But what if you need to store the scores for
 * an entire class of 30 students? Declaring 30 separate variables (`score1`,
 * `score2`, etc.) would be extremely inefficient.
 *
 * An ARRAY is a data structure that can store a fixed-size, sequential
 * collection of elements of the SAME data type. Think of it as a row of
 * numbered boxes, where each box holds a value of the same type.
 */

#include <stdio.h>

int main(void)
{
    // --- Part 1: Declaring and Initializing an Array ---

    /*
     * To declare an array, you specify the data type of its elements, the name
     * of the array, and the number of elements it can hold (its size) in
     * square brackets `[]`.
     *
     * The following line declares an array named `high_scores` that can hold
     * exactly 5 integers.
     */
    int high_scores[5]; // An array of 5 integers.

    /*
     * ACCESSING ARRAY ELEMENTS:
     * You access individual elements of an array using an INDEX in square brackets.
     * C uses ZERO-BASED INDEXING. This is a critical concept. It means the
     * first element is at index 0, the second at index 1, and so on.
     * For an array of size 5, the valid indices are 0, 1, 2, 3, and 4.
     *
     * Accessing an index outside of this range (like `high_scores[5]`) leads to
     * UNDEFINED BEHAVIOR, a common source of bugs and crashes in C programs.
     */
    printf("--- Part 1: Assigning and Accessing Array Elements ---\n");
    high_scores[0] = 980; // Assign 980 to the first element
    high_scores[1] = 950; // Assign 950 to the second element
    high_scores[2] = 875;
    high_scores[3] = 820;
    high_scores[4] = 799; // The fifth (and last) element

    printf("The highest score is: %d\n", high_scores[0]);
    printf("The third highest score is: %d\n", high_scores[2]);
    printf("\n");

    // --- Part 2: Initializer Lists ---

    /*
     * Manually assigning each element is tedious. You can initialize an array
     * when you declare it using an initializer list in curly braces `{}`.
     *
     * If you provide an initializer list, you can let the compiler figure out
     * the size of the array by leaving the square brackets empty.
     */
    printf("--- Part 2: Using an Initializer List ---\n");
    double temperatures[] = {72.5, 75.0, 69.8, 80.1, 85.3}; // Size is 5
    char grades[] = {'A', 'B', 'A', 'C', 'B', 'A'};         // Size is 6

    printf("The first temperature recorded was: %.1f\n", temperatures[0]);
    printf("The fourth student's grade was: %c\n", grades[3]);
    printf("\n");

    // --- Part 3: Iterating Over an Array ---

    /*
     * The real power of arrays is realized when you combine them with loops.
     * A `for` loop is perfect for iterating through an array, performing an
     * action on each element.
     */
    printf("--- Part 3: Iterating Over an Array with a 'for' loop ---\n");
    printf("All recorded temperatures:\n");

    // We will loop from index 0 up to (but not including) 5.
    // The loop counter `i` is used as the array index.
    for (int i = 0; i < 5; i++)
    {
        printf("  Day %d: %.1f\n", i + 1, temperatures[i]);
    }
    printf("\n");

    // Let's use a loop to calculate the sum of all high scores.
    int total_score = 0;
    for (int i = 0; i < 5; i++)
    {
        total_score = total_score + high_scores[i];
    }
    printf("Sum of all high scores: %d\n", total_score);

    return 0;
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * In this lesson, you've learned to manage collections of data.
 *
 * Key Takeaways:
 * - An ARRAY is a fixed-size collection of elements of the same type.
 * - C uses ZERO-BASED INDEXING, where the first element is at index 0.
 * - Accessing an array element is done with `array_name[index]`.
 * - You can initialize an array with a list of values like `int arr[] = {1, 2, 3};`.
 * - Loops, especially `for` loops, are essential for processing each element in an array.
 *
 * NOTE: For now, we manually kept track of the array's size (e.g., `i < 5`). In
 * future lessons, we will learn more robust ways to handle array sizes.
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * 1. Open a terminal or command prompt.
 * 2. Navigate to the directory where you saved this file.
 * 3. Use the GCC compiler to create an executable file:
 *    `gcc -Wall -Wextra -std=c11 -o 8_arrays 8_arrays.c`
 * 4. Run the executable:
 *    - On Linux/macOS:   `./8_arrays`
 *    - On Windows:       `8_arrays.exe`
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 8_arrays 8_arrays.c
./8_arrays

Strings

Up to this point, we’ve only seen string “literals” (text in double quotes) used directly in printf. We haven’t stored or manipulated them.

THE BIG IDEA: C does not have a built-in string data type like many other languages. Instead, a C STRING is simply an ARRAY of characters (char).

But how does the computer know where the string ends in the array?

THE NULL TERMINATOR C strings follow a crucial convention: they are “null-terminated”. This means a special character, the NULL TERMINATOR \0, is placed after the last actual character in the array to mark the end of the string.

So, the string “Hello” is stored in a char array as: [‘H’, ‘e’, ‘l’, ‘l’, ‘o’, ‘\0’] It requires 6 characters of storage, not 5! This is a very important concept.

All standard C functions that work with strings rely on this \0 character.

The easiest way to declare and initialize a string is to use a string literal. The compiler automatically adds the null terminator \0 for you.

The %s format specifier in printf is used for strings. When printf receives a char array with %s, it prints characters one by one until it finds the null terminator \0.

Because strings are so common, C provides a standard library of functions to work with them. We included <string.h> at the top to get access.

strcpy() - String Copy You CANNOT assign one string to another using =. char another_string[20]; another_string = message; <– THIS IS WRONG!

You must copy the characters from the source to the destination. WARNING: The destination array must be large enough to hold the source string, including its null terminator!

strcat() - String Concatenate (join) This function appends one string to the end of another. WARNING: Again, the destination string must have enough space for the combined result!

strcmp() - String Compare You CANNOT compare strings using ==. if (str1 == str2) <– THIS IS WRONG! It compares memory locations, not content.

strcmp() compares two strings lexicographically (like in a dictionary). It returns:

  • 0 if the strings are identical.
  • A negative value if the first string comes before the second.
  • A positive value if the first string comes after the second.

Full Source

/**
 * @file 9_strings.c
 * @brief Part 1, Lesson 9: Strings
 * @author dunamismax
 * @date 06-15-2025
 *
 * This lesson covers how to work with strings, which are one of the most
 * common types of data in programming.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * Up to this point, we've only seen string "literals" (text in double quotes)
 * used directly in `printf`. We haven't stored or manipulated them.
 *
 * THE BIG IDEA: C does not have a built-in `string` data type like many other
 * languages. Instead, a C STRING is simply an ARRAY of characters (`char`).
 *
 * But how does the computer know where the string ends in the array?
 *
 * THE NULL TERMINATOR
 * C strings follow a crucial convention: they are "null-terminated". This means
 * a special character, the NULL TERMINATOR `\0`, is placed after the last
 * actual character in the array to mark the end of the string.
 *
 * So, the string "Hello" is stored in a `char` array as:
 * ['H', 'e', 'l', 'l', 'o', '\0']
 * It requires 6 characters of storage, not 5! This is a very important concept.
 *
 * All standard C functions that work with strings rely on this `\0` character.
 */

#include <stdio.h>
#include <string.h> // We must include this new header for string manipulation functions.

int main(void)
{
    // --- Part 1: Declaring and Printing Strings ---

    /*
     * The easiest way to declare and initialize a string is to use a string
     * literal. The compiler automatically adds the null terminator `\0` for you.
     */
    char greeting[] = "Hello, C programmer!";

    printf("--- Part 1: Declaring and Printing Strings ---\n");
    /*
     * The `%s` format specifier in `printf` is used for strings. When `printf`
     * receives a `char` array with `%s`, it prints characters one by one
     * until it finds the null terminator `\0`.
     */
    printf("Our greeting string: %s\n", greeting);

    // Let's prove the null terminator exists. We'll manually initialize one.
    // This is the "hard way" but illustrates the concept perfectly.
    char name[] = {'A', 'l', 'i', 'c', 'e', '\0'};
    printf("A manually created name: %s\n\n", name);

    // --- Part 2: The `<string.h>` Library ---

    /*
     * Because strings are so common, C provides a standard library of functions
     * to work with them. We included `<string.h>` at the top to get access.
     */
    printf("--- Part 2: String Functions ---\n");

    // `strlen()` - String Length
    // This function counts the number of characters in a string *before* the
    // null terminator.
    char message[] = "This is a test.";
    // The length is 15, even though the array size is 16 (to hold the `\0`).
    printf("The message is: \"%s\"\n", message);
    printf("The length of the message (strlen) is: %zu\n", strlen(message));
    // Note: strlen returns a special unsigned integer type `size_t`. We use
    // the `%zu` format specifier to print it correctly and avoid warnings.
    printf("\n");

    // --- Part 3: Copying and Concatenating Strings ---

    printf("--- Part 3: Copying and Concatenating ---\n");

    /*
     * `strcpy()` - String Copy
     * You CANNOT assign one string to another using `=`.
     * `char another_string[20]; another_string = message;` <-- THIS IS WRONG!
     *
     * You must copy the characters from the source to the destination.
     * WARNING: The destination array must be large enough to hold the source
     * string, including its null terminator!
     */
    char destination[50]; // A buffer to copy into.
    char source[] = "Some text to be copied.";

    strcpy(destination, source); // Copies `source` into `destination`.
    printf("Result of strcpy: %s\n", destination);

    /*
     * `strcat()` - String Concatenate (join)
     * This function appends one string to the end of another.
     * WARNING: Again, the destination string must have enough space for the
     * combined result!
     */
    char full_greeting[50] = "Welcome, "; // Initialize with enough space.
    char user[] = "dunamismax";

    strcat(full_greeting, user); // Appends `user` to `full_greeting`.
    printf("Result of strcat: %s\n\n", full_greeting);

    // --- Part 4: Comparing Strings ---

    printf("--- Part 4: Comparing Strings ---\n");

    /*
     * `strcmp()` - String Compare
     * You CANNOT compare strings using `==`.
     * `if (str1 == str2)` <-- THIS IS WRONG! It compares memory locations, not content.
     *
     * `strcmp()` compares two strings lexicographically (like in a dictionary).
     * It returns:
     *   - 0 if the strings are identical.
     *   - A negative value if the first string comes before the second.
     *   - A positive value if the first string comes after the second.
     */
    char pass1[] = "password123";
    char pass2[] = "Password123"; // Case matters!
    char pass3[] = "password123";

    if (strcmp(pass1, pass2) == 0)
    {
        printf("\"%s\" and \"%s\" are the same.\n", pass1, pass2);
    }
    else
    {
        printf("\"%s\" and \"%s\" are different.\n", pass1, pass2);
    }

    if (strcmp(pass1, pass3) == 0)
    {
        printf("\"%s\" and \"%s\" are the same.\n", pass1, pass3);
    }

    return 0;
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * Congratulations on completing Part 1! Strings are a huge concept.
 *
 * Key Takeaways:
 * - A C string is a `char` array ending with a null terminator `\0`.
 * - The `%s` format specifier is used to print strings.
 * - The `<string.h>` header provides essential functions for strings.
 * - `strlen()` gets the length (not including `\0`).
 * - `strcpy()` copies one string to another.
 * - `strcat()` joins one string to the end of another.
 * - `strcmp()` compares the content of two strings.
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * 1. Open a terminal or command prompt.
 * 2. Navigate to the directory where you saved this file.
 * 3. Use the GCC compiler to create an executable file:
 *    `gcc -Wall -Wextra -std=c11 -o 9_strings 9_strings.c`
 * 4. Run the executable:
 *    - On Linux/macOS:   `./9_strings`
 *    - On Windows:       `9_strings.exe`
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 9_strings 9_strings.c
./9_strings

Pointers

Welcome to Part 2 of your C journey! You’ve mastered the fundamentals, and now we’re diving deeper. This lesson introduces POINTERS, arguably the most powerful and unique feature of the C language.

Don’t be intimidated! The concept is simple: Instead of holding a value (like 5, or ‘c’), a POINTER holds a memory ADDRESS. It’s like having a note that says “the data you want is over there” instead of holding the data itself.

WHY ARE POINTERS IMPORTANT? Pointers allow for highly efficient code and are essential for many advanced topics, including dynamic memory allocation, building complex data structures (like the linked lists we’ll see later), and allowing functions to modify the original variables you pass to them. Mastering pointers is mastering C.

Full Source

/**
 * @file 10_pointers.c
 * @brief Part 2, Lesson 10: Pointers
 * @author dunamismax
 * @date 06-15-2025
 *
 * This file serves as the lesson and demonstration for pointers.
 * It explains the core concepts of memory addresses, pointer declaration,
 * the address-of operator, and the dereference operator through structured
 * comments and provides a runnable example of their implementation.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * Welcome to Part 2 of your C journey! You've mastered the fundamentals, and now
 * we're diving deeper. This lesson introduces POINTERS, arguably the most
 * powerful and unique feature of the C language.
 *
 * Don't be intimidated! The concept is simple:
 * Instead of holding a value (like 5, or 'c'), a POINTER holds a memory ADDRESS.
 * It's like having a note that says "the data you want is over there" instead of
 * holding the data itself.
 *
 * WHY ARE POINTERS IMPORTANT?
 * Pointers allow for highly efficient code and are essential for many advanced
 * topics, including dynamic memory allocation, building complex data structures
 * (like the linked lists we'll see later), and allowing functions to modify
 * the original variables you pass to them. Mastering pointers is mastering C.
 */

#include <stdio.h> // We need this for printf and to use NULL

// --- Part 1: Variables and Their Memory Addresses ---

// Every variable you declare is stored somewhere in your computer's memory (RAM).
// Each location in memory has a unique ADDRESS, like a house number on a street.

// The ADDRESS-OF operator (`&`) gives you the memory address of a variable.

// --- Part 2: What is a Pointer? ---

// A POINTER is a special kind of variable that is designed to store a memory ADDRESS.

// DECLARING A POINTER:
// You declare a pointer by specifying the type of data it will point to,
// followed by an asterisk (*), and then the pointer's name.
//
// SYNTAX: data_type *pointer_name;
//
// Example: `int *p_number;` declares a pointer named `p_number` that is
//          intended to hold the address of an `int` variable.
//
// It's a very good practice to initialize pointers to NULL. NULL is a special
// value that means "this pointer doesn't point to anything valid." This helps
// prevent bugs from using a pointer that points to a random, garbage address.

// --- Part 3: The Dereference Operator (*) ---

// The asterisk `*` has a second job. When used with an existing pointer variable
// (not in its declaration), it acts as the DEREFERENCE operator.
//
// DEREFERENCING means "go to the address stored in this pointer and get the
// value that is located there."

int main(void)
{
    // --- DEMONSTRATION 1: Getting a Variable's Address ---

    int score = 94; // A regular integer variable. It has a value (94) and an address.

    printf("--- Part 1: Variable Addresses ---\n");
    printf("The value of the 'score' variable is: %d\n", score);

    // We use the address-of operator (&) to get its memory location.
    // The `%p` format specifier is used to print addresses in a standard hexadecimal format.
    printf("The memory address of 'score' is: %p\n\n", (void *)&score);

    // --- DEMONSTRATION 2: Declaring and Using a Pointer ---

    printf("--- Part 2 & 3: Pointers in Action ---\n");

    // Declare a pointer that can hold the address of an integer.
    // We initialize it to NULL for safety.
    int *p_score = NULL;

    printf("Initial value of p_score (a safe pointer): %p\n", (void *)p_score);

    // Now, let's make the pointer 'point to' our 'score' variable.
    // We assign the address of 'score' to the pointer 'p_score'.
    p_score = &score;

    printf("Value of p_score after assignment: %p (Notice it's score's address!)\n", (void *)p_score);
    printf("Address of the pointer itself (p_score): %p\n\n", (void *)&p_score); // Pointers have their own address too!

    // --- DEMONSTRATION 3: Dereferencing a Pointer ---

    printf("--- Part 4: Dereferencing ---\n");
    // To get the value stored at the address the pointer holds, we DEREFERENCE it with *.
    printf("Value at the address p_score points to: %d\n\n", *p_score);

    // --- DEMONSTRATION 4: Modifying a Variable via its Pointer ---
    // This is where the true power of pointers starts to show. We can change the
    // original 'score' variable's value using only the pointer.

    printf("--- Part 5: Modifying Data via Pointer ---\n");
    printf("Original score: %d\n", score);

    printf("Changing value through the pointer...\n");
    *p_score = 100; // "Go to the address stored in p_score and set the value there to 100."

    // Let's check the original 'score' variable now.
    printf("New score: %d\n", score); // It has been changed!

    return 0;
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * Key Takeaways:
 *
 * 1.  Every variable has a value AND a memory ADDRESS.
 * 2.  The ADDRESS-OF operator `&` gets the memory address of a variable.
 * 3.  A POINTER is a variable that stores a memory address.
 * 4.  The DEREFERENCE operator `*` (when used on an existing pointer) goes to the
 *     pointer's stored address and retrieves the value at that location.
 * 5.  Pointers allow you to indirectly read and modify the variables they point to.
 * 6.  Initializing pointers to `NULL` is a critical safety practice.
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * 1. Open a terminal or command prompt.
 * 2. Navigate to the directory where you saved this file.
 * 3. Use the GCC compiler to create an executable file:
 *    `gcc -Wall -Wextra -std=c11 -o 10_pointers 10_pointers.c`
 * 4. Run the executable:
 *    - On Linux/macOS:   `./10_pointers`
 *    - On Windows:       `10_pointers.exe`
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 10_pointers 10_pointers.c
./10_pointers

Pointers and Arrays

In the previous lesson, you learned what a POINTER is: a variable that stores a memory address. Now, we will explore one of the most important relationships in C: the connection between pointers and arrays.

THE BIG REVEAL: The name of an array, by itself, acts as a CONSTANT POINTER to the first element of that array.

This means an array’s name IS a memory address. This is why we didn’t need the & operator when using scanf with a string (which is a char array) back in Lesson 3. The array’s name already provided the address!

This relationship enables a powerful technique called POINTER ARITHMETIC, which allows us to move between elements of an array using a pointer.

Full Source

/**
 * @file 11_pointers_and_arrays.c
 * @brief Part 2, Lesson 11: Pointers and Arrays
 * @author dunamismax
 * @date 06-15-2025
 *
 * This file serves as the lesson and demonstration for the relationship
 * between pointers and arrays. It explains that an array name is a pointer
 * to the first element and introduces the concept of POINTER ARITHMETIC.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * In the previous lesson, you learned what a POINTER is: a variable that stores
 * a memory address. Now, we will explore one of the most important relationships
 * in C: the connection between pointers and arrays.
 *
 * THE BIG REVEAL:
 * The name of an array, by itself, acts as a CONSTANT POINTER to the first
 * element of that array.
 *
 * This means an array's name IS a memory address. This is why we didn't need the
 * `&` operator when using `scanf` with a string (which is a `char` array) back
 * in Lesson 3. The array's name already provided the address!
 *
 * This relationship enables a powerful technique called POINTER ARITHMETIC, which
 * allows us to move between elements of an array using a pointer.
 */

#include <stdio.h>

int main(void)
{
    // Let's create an array of integers.
    // In memory, these values are stored right next to each other in a
    // single, unbroken block. This is called CONTIGUOUS memory.
    int grades[] = {85, 92, 78, 99, 67};
    int count = 5; // Number of elements in the array

    // --- Part 1: The Array Name is a Pointer ---

    printf("--- Part 1: The Array Name as a Pointer ---\n");

    // Let's prove that the array name 'grades' is just an address.
    // We will print the value of 'grades' and the address of its first element.
    printf("Value of 'grades' (the array name): %p\n", (void *)grades);
    printf("Address of the first element (&grades[0]): %p\n", (void *)&grades[0]);
    printf("They are the same!\n\n");

    // --- Part 2: Pointer Arithmetic ---

    printf("--- Part 2: Pointer Arithmetic ---\n");

    // Since the array name is a pointer, we can assign it to a real pointer variable.
    int *p_grades = grades; // Note: no `&` is needed!

    // Let's access the first element using the pointer.
    printf("First element using grades[0]: %d\n", grades[0]);
    printf("First element using *p_grades: %d\n\n", *p_grades);

    // Now for the magic. What happens if we add 1 to the pointer?
    // POINTER ARITHMETIC is smart. Adding `1` to an `int` pointer doesn't just
    // add 1 to the address. It moves the address forward by the size of ONE ELEMENT.
    // In this case, it moves forward by `sizeof(int)` bytes.

    // `*(p_grades + 1)` means "go to the address of the first element, move
    // forward by the size of one integer, and then dereference that new address."
    printf("Second element using grades[1]: %d\n", grades[1]);
    printf("Second element using *(p_grades + 1): %d\n\n", *(p_grades + 1));

    printf("Third element using grades[2]: %d\n", grades[2]);
    printf("Third element using *(p_grades + 2): %d\n\n", *(p_grades + 2));

    // --- Part 3: Equivalence of p[i] and *(p + i) ---

    printf("--- Part 3: Equivalence of Subscript and Pointer Notation ---\n");

    // The C language guarantees that for any pointer or array `p` and integer `i`,
    // the expression `p[i]` is EXACTLY THE SAME as `*(p + i)`.
    // The square bracket notation `[]` is just "syntactic sugar" to make pointer
    // arithmetic look cleaner and more familiar.

    printf("Accessing the fourth element (index 3):\n");
    printf("Using array notation grades[3]: %d\n", grades[3]);
    printf("Using pointer notation *(grades + 3): %d\n\n", *(grades + 3));

    // This even works on our pointer variable `p_grades`!
    printf("Accessing the fifth element (index 4) via our pointer variable:\n");
    printf("Using pointer notation *(p_grades + 4): %d\n", *(p_grades + 4));
    printf("Using array notation p_grades[4]: %d\n\n", p_grades[4]);

    // --- Part 4: Looping Through an Array with a Pointer ---

    printf("--- Part 4: Looping Through The Array ---\n");
    printf("Grades: ");
    for (int i = 0; i < count; i++)
    {
        // We will use pointer arithmetic to access each element
        printf("%d ", *(grades + i));
    }
    printf("\n");

    return 0;
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * Key Takeaways:
 *
 * 1.  The name of an array is a constant pointer to its first element.
 * 2.  `array_name` and `&array_name[0]` evaluate to the same memory address.
 * 3.  POINTER ARITHMETIC lets you move between elements. Adding `n` to a pointer
 *     moves it forward by `n * sizeof(element_type)` bytes in memory.
 * 4.  The standard array subscript notation `array[i]` is just a more convenient
 *     way of writing the pointer arithmetic expression `*(array + i)`. They are
 *     functionally identical.
 * 5.  This deep relationship is what makes passing arrays to functions so efficient
 *     in C--you're just passing a single address, not copying the whole array.
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * 1. Open a terminal or command prompt.
 * 2. Navigate to the directory where you saved this file.
 * 3. Use the GCC compiler to create an executable file:
 *    `gcc -Wall -Wextra -std=c11 -o 11_pointers_and_arrays 11_pointers_and_arrays.c`
 * 4. Run the executable:
 *    - On Linux/macOS:   `./11_pointers_and_arrays`
 *    - On Windows:       `11_pointers_and_arrays.exe`
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 11_pointers_and_arrays 11_pointers_and_arrays.c
./11_pointers_and_arrays

Structs

So far, our variables have held a single piece of data (like an int or a double), and our arrays have held multiple pieces of the SAME type of data.

But what if we want to represent a more complex, real-world object? Think of a student. A student has a name (a string), an ID number (an integer), and a GPA (a double). These are all different data types, but they all logically belong together.

This is where a STRUCT (short for structure) comes in. A STRUCT is a user-defined data type that groups together related variables of potentially different types into a single unit.

Full Source

/**
 * @file 12_structs.c
 * @brief Part 2, Lesson 12: Structs
 * @author dunamismax
 * @date 06-15-2025
 *
 * This file serves as the lesson and demonstration for structs.
 * It explains how to define and use custom data types to group related
 * variables, and how to access their members using both the dot (.) and
 * arrow (->) operators.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * So far, our variables have held a single piece of data (like an `int` or a
 * `double`), and our arrays have held multiple pieces of the SAME type of data.
 *
 * But what if we want to represent a more complex, real-world object?
 * Think of a student. A student has a name (a string), an ID number (an integer),
 * and a GPA (a double). These are all different data types, but they all
 * logically belong together.
 *
 * This is where a STRUCT (short for structure) comes in. A STRUCT is a
 * user-defined data type that groups together related variables of potentially
 * different types into a single unit.
 */

#include <stdio.h>
#include <string.h> // We need this for strcpy() to handle strings

// --- Part 1: Defining a Struct ---

// We define the blueprint for our new data type using the `struct` keyword.
// This definition doesn't create any variables yet; it just tells the compiler
// what a "Student" looks like. It's like creating a blueprint for a house.
// By convention, struct names often start with a capital letter.
struct Student
{
    // These are the MEMBERS of the struct.
    char name[50];
    int student_id;
    double gpa;
};

// --- Part 2: Pointers to Structs ---

// Just like with any other data type (`int *`, `char *`), we can have pointers
// to structs. A pointer to a struct stores the memory address of a struct
// variable.
//
// When we want to access a struct's member through a pointer, we can't use the
// dot (.) operator directly. We use the ARROW OPERATOR (->).

int main(void)
{
    // --- DEMONSTRATION 1: Creating and Using a Struct Variable ---
    printf("--- Part 1: Creating and Accessing a Struct ---\n");

    // Now we can declare a variable of our new `struct Student` type.
    // This is like building an actual house from the blueprint.
    struct Student student1;

    // To access the members of a struct variable, we use the DOT OPERATOR (.).
    // Let's assign some values to student1's members.
    // For strings, we must use `strcpy` from <string.h> as we can't assign to an array directly.
    strcpy(student1.name, "Jane Doe");
    student1.student_id = 12345;
    student1.gpa = 3.8;

    // Now let's print the data to see our struct in action.
    printf("Student Name: %s\n", student1.name);
    printf("Student ID:   %d\n", student1.student_id);
    printf("Student GPA:  %.2f\n\n", student1.gpa); // .2f prints float/double to 2 decimal places

    // --- DEMONSTRATION 2: Using a Pointer to a Struct ---
    printf("--- Part 2: Pointers to Structs and the Arrow Operator ---\n");

    // Declare a pointer that can point to a `struct Student` variable.
    struct Student *p_student = NULL;

    // Assign the address of `student1` to our pointer.
    p_student = &student1;

    // Now we can access the members of `student1` through the pointer `p_student`.
    // We use the ARROW OPERATOR (->) for this. It's clean and intuitive.
    printf("Accessing data via pointer:\n");
    printf("Name: %s\n", p_student->name);
    printf("ID:   %d\n", p_student->student_id);
    printf("GPA:  %.2f\n\n", p_student->gpa);

    // We can also modify the original struct's data using the pointer.
    printf("Modifying GPA through the pointer...\n");
    p_student->gpa = 4.0;

    // Let's check the original student1 variable.
    printf("Original student1's new GPA: %.2f\n\n", student1.gpa); // It has been changed!

    // --- The Long Way: Dereferencing and Dot Operator ---
    // The arrow operator `->` is just "syntactic sugar" (a convenient shortcut).
    // The expression `p_student->gpa` is exactly equivalent to `(*p_student).gpa`.
    // This means "first, DEREFERENCE the pointer to get the actual struct, then
    // use the DOT OPERATOR on that struct."
    // The parentheses are required because the dot operator has higher precedence
    // than the dereference operator (*).
    printf("--- Bonus: The Long Way to Access Members via Pointer ---\n");
    printf("Accessing ID with (*p_student).student_id: %d\n", (*p_student).student_id);
    printf("See? It's the same, but `->` is much easier to read and type!\n");

    return 0;
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * Key Takeaways:
 *
 * 1.  A STRUCT lets you create your own custom data types by grouping multiple
 *     variables (called MEMBERS) into one logical unit.
 * 2.  You first define the struct's blueprint, then you can declare variables
 *     of that new type.
 * 3.  Use the DOT OPERATOR (`.`) to access members of a struct variable directly.
 *     Example: `my_struct.member`
 * 4.  Use the ARROW OPERATOR (`->`) to access members of a struct through a pointer.
 *     Example: `my_pointer_to_struct->member`
 * 5.  The arrow operator `p->m` is a convenient shortcut for `(*p).m`.
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * 1. Open a terminal or command prompt.
 * 2. Navigate to the directory where you saved this file.
 * 3. Use the GCC compiler to create an executable file:
 *    `gcc -Wall -Wextra -std=c11 -o 12_structs 12_structs.c`
 * 4. Run the executable:
 *    - On Linux/macOS:   `./12_structs`
 *    - On Windows:       `12_structs.exe`
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 12_structs 12_structs.c
./12_structs

Dynamic Memory Allocation

Until now, the size of all our variables and arrays had to be known when we wrote the program (at COMPILE-TIME). For example, int numbers[10]; creates an array that can hold exactly 10 integers. It can’t hold 9, and it can’t hold 11.

But what if we don’t know how much memory we need until the program is running? What if we want to ask the user “How many students do you want to enter?” and then create an array of just the right size?

This is where DYNAMIC MEMORY ALLOCATION comes in. It’s the ability to request memory from the operating system at RUNTIME.

THE STACK AND THE HEAP Local variables (like int x; inside main) are stored on a memory region called the STACK. The STACK is fast and memory is managed for you automatically. When a function ends, its variables on the stack are destroyed.

Dynamically allocated memory comes from a different, large pool of memory called the HEAP. The HEAP is more flexible, but there’s a catch: YOU are responsible for managing it. When you are done with memory from the HEAP, you MUST give it back to the system yourself.

Full Source

/**
 * @file 13_dynamic_memory_allocation.c
 * @brief Part 2, Lesson 13: Dynamic Memory Allocation
 * @author dunamismax
 * @date 06-15-2025
 *
 * This file serves as the lesson and demonstration for dynamic memory allocation.
 * It explains the concepts of the stack vs. the heap, and introduces the
 * functions `malloc()`, `free()`, and the `sizeof` operator.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * Until now, the size of all our variables and arrays had to be known when we
 * wrote the program (at COMPILE-TIME). For example, `int numbers[10];` creates
 * an array that can hold exactly 10 integers. It can't hold 9, and it can't hold 11.
 *
 * But what if we don't know how much memory we need until the program is running?
 * What if we want to ask the user "How many students do you want to enter?" and
 * then create an array of just the right size?
 *
 * This is where DYNAMIC MEMORY ALLOCATION comes in. It's the ability to request
 * memory from the operating system at RUNTIME.
 *
 * THE STACK AND THE HEAP
 * Local variables (like `int x;` inside `main`) are stored on a memory region
 * called the STACK. The STACK is fast and memory is managed for you automatically.
 * When a function ends, its variables on the stack are destroyed.
 *
 * Dynamically allocated memory comes from a different, large pool of memory
 * called the HEAP. The HEAP is more flexible, but there's a catch:
 * YOU are responsible for managing it. When you are done with memory from the
 * HEAP, you MUST give it back to the system yourself.
 */

#include <stdio.h>
#include <stdlib.h> // `malloc` and `free` are declared in the Standard Library header

// --- Part 1: `malloc` - Memory Allocation ---
// The `malloc()` function is used to request a block of memory from the HEAP.
//
// - It takes one argument: the number of BYTES you want to allocate.
// - It returns a "void pointer" (`void*`) to the start of that memory block.
//   A `void*` is a generic pointer; we must CAST it to the specific pointer
//   type we need (e.g., `int*`, `char*`).
// - If `malloc()` cannot find enough free memory, it returns `NULL`. You MUST
//   always check for this!

// --- Part 2: `sizeof` - Getting The Size of a Type ---
// How do we know how many bytes an `int` or a `struct` takes up? The size can
// be different on different computers. The `sizeof` operator solves this.
// `sizeof(int)` will give us the number of bytes for a single integer.
// This makes our code portable and readable.
//
// The common pattern for `malloc` is:
// pointer = (type *)malloc(number_of_elements * sizeof(type));

// --- Part 3: `free` - Giving Memory Back ---
// When you are finished with memory you allocated with `malloc()`, you MUST
// release it using the `free()` function.
//
// If you don't, you create a MEMORY LEAK. The program holds onto memory it
// no longer needs. In a long-running program, this can eventually use up all
// available memory and crash the program or even the system.
//
// `free()` takes one argument: the pointer you got from `malloc()`.

int main(void)
{
    int num_students;
    double *gpa_list = NULL; // A pointer to hold the address of our dynamic array.
                             // It's good practice to initialize it to NULL.

    printf("How many student GPAs do you want to store? ");
    scanf("%d", &num_students);

    // Let's prevent the user from entering a non-positive number.
    if (num_students <= 0)
    {
        printf("Invalid number of students. Exiting.\n");
        return 1; // Exit with an error code.
    }

    // --- DEMONSTRATION: The `malloc`, use, `free` cycle ---

    // 1. ALLOCATE: Request memory from the heap.
    // We want space for `num_students` doubles.
    printf("Allocating memory for %d doubles...\n", num_students);
    gpa_list = (double *)malloc(num_students * sizeof(double));

    // 2. VALIDATE: ALWAYS check if malloc succeeded.
    if (gpa_list == NULL)
    {
        printf("Error: Memory allocation failed!\n");
        printf("The operating system could not provide the requested memory.\n");
        return 1; // Exit with an error code.
    }
    printf("Memory allocated successfully at address: %p\n\n", (void *)gpa_list);

    // 3. USE: Now we can use `gpa_list` just like a regular array.
    // Let's fill it with some sample data.
    for (int i = 0; i < num_students; i++)
    {
        gpa_list[i] = 2.5 + (i * 0.2); // Just some dummy values
    }

    // And print it back out to prove it works.
    printf("The stored GPAs are:\n");
    for (int i = 0; i < num_students; i++)
    {
        printf("Student %d GPA: %.2f\n", i + 1, gpa_list[i]);
    }
    printf("\n");

    // 4. FREE: We are done with the memory. We MUST return it.
    printf("Freeing the allocated memory...\n");
    free(gpa_list);

    // IMPORTANT: After freeing, the `gpa_list` pointer is now a DANGLING POINTER.
    // It points to memory that we no longer own. Using it can cause a crash.
    // It's a critical safety practice to set the pointer to NULL immediately
    // after freeing it to prevent accidental use.
    gpa_list = NULL;

    printf("Memory has been released and pointer has been set to NULL.\n");

    return 0;
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * Key Takeaways:
 *
 * 1.  DYNAMIC MEMORY ALLOCATION allows you to request memory at RUNTIME from the HEAP.
 * 2.  The `malloc()` function allocates a block of bytes and returns a pointer to it.
 * 3.  The `sizeof` operator should always be used with `malloc` to ensure you
 *     request the correct number of bytes, making your code portable.
 * 4.  You MUST check if `malloc` returned `NULL`, which indicates allocation failure.
 * 5.  When you are done with the memory, you MUST release it with `free()`. Failure
 *     to do so results in a MEMORY LEAK.
 * 6.  After calling `free(ptr)`, set `ptr = NULL;` to prevent using a
 *     DANGLING POINTER, which is a common and dangerous bug.
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * 1. Open a terminal or command prompt.
 * 2. Navigate to the directory where you saved this file.
 * 3. Use the GCC compiler to create an executable file:
 *    `gcc -Wall -Wextra -std=c11 -o 13_dynamic_memory_allocation 13_dynamic_memory_allocation.c`
 * 4. Run the executable:
 *    - On Linux/macOS:   `./13_dynamic_memory_allocation`
 *    - On Windows:       `13_dynamic_memory_allocation.exe`
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 13_dynamic_memory_allocation 13_dynamic_memory_allocation.c
./13_dynamic_memory_allocation

File I/O

All our programs so far have had a temporary existence. When the program ends, any data it was holding in variables, arrays, or structs is lost forever.

To make our programs truly useful, we need PERSISTENCE–the ability to save data to a storage device (like a hard drive) and load it back later. This is achieved through FILE I/O (Input/Output).

The Core Concepts of File I/O

  1. The FILE Pointer: When you work with a file in C, you use a special pointer type called FILE *. This is a struct defined in <stdio.h> that holds all the information C needs to manage the connection to a file (its location, your current position in it, etc.). You never need to know what’s inside the FILE struct; you just use a pointer to it.

  2. fopen() - Opening a File: This function establishes a connection, or “stream,” to a file on your disk. It takes two arguments: the filename and a “mode” string.

    • “w”: WRITE mode. Creates a new file. If the file already exists, its contents are completely ERASED and overwritten.
    • “r”: READ mode. Opens an existing file for reading. If the file doesn’t exist, fopen() will fail.
    • “a”: APPEND mode. Opens a file to add new data to the END of it. If the file doesn’t exist, it will be created. fopen() returns a FILE * on success or NULL on failure. YOU MUST ALWAYS CHECK FOR NULL!
  3. fclose() - Closing a File: This is the essential cleanup step. It finalizes any pending writes to the disk and tells the operating system you are done with the file, releasing resources. You MUST close every file you open.

  4. Writing and Reading Functions:

    • fprintf(): Works just like printf(), but its first argument is a FILE *. It writes formatted text TO a file.
    • fgets(): Reads a line of text FROM a file. It’s safer than other functions because you specify the maximum number of characters to read, preventing buffer overflows.

Full Source

/**
 * @file 14_file_io.c
 * @brief Part 2, Lesson 14: File I/O
 * @author dunamismax
 * @date 06-15-2025
 *
 * This file serves as the lesson and demonstration for File I/O (Input/Output).
 * It explains how to write data to files and read data from them, allowing
 * programs to save and load information permanently.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * All our programs so far have had a temporary existence. When the program ends,
 * any data it was holding in variables, arrays, or structs is lost forever.
 *
 * To make our programs truly useful, we need PERSISTENCE--the ability to save
 * data to a storage device (like a hard drive) and load it back later. This is
 * achieved through FILE I/O (Input/Output).
 *
 * --- The Core Concepts of File I/O ---
 *
 * 1. The `FILE` Pointer:
 *    When you work with a file in C, you use a special pointer type called `FILE *`.
 *    This is a struct defined in `<stdio.h>` that holds all the information C
 *    needs to manage the connection to a file (its location, your current
 *    position in it, etc.). You never need to know what's inside the `FILE`
 *    struct; you just use a pointer to it.
 *
 * 2. `fopen()` - Opening a File:
 *    This function establishes a connection, or "stream," to a file on your disk.
 *    It takes two arguments: the filename and a "mode" string.
 *    - "w": WRITE mode. Creates a new file. If the file already exists, its
 *           contents are completely ERASED and overwritten.
 *    - "r": READ mode. Opens an existing file for reading. If the file doesn't
 *           exist, `fopen()` will fail.
 *    - "a": APPEND mode. Opens a file to add new data to the END of it. If the
 *           file doesn't exist, it will be created.
 *    `fopen()` returns a `FILE *` on success or `NULL` on failure. YOU MUST
 *    ALWAYS CHECK FOR `NULL`!
 *
 * 3. `fclose()` - Closing a File:
 *    This is the essential cleanup step. It finalizes any pending writes to the
 *    disk and tells the operating system you are done with the file, releasing
 *    resources. You MUST close every file you open.
 *
 * 4. Writing and Reading Functions:
 *    - `fprintf()`: Works just like `printf()`, but its first argument is a
 *      `FILE *`. It writes formatted text TO a file.
 *    - `fgets()`: Reads a line of text FROM a file. It's safer than other
 *      functions because you specify the maximum number of characters to read,
 *      preventing buffer overflows.
 */

#include <stdio.h>
#include <stdlib.h> // For exit()

int main(void)
{
    // A FILE pointer, always initialize to NULL.
    FILE *file_pointer = NULL;
    char *filename = "my_first_file.txt"; // The name of the file we will work with.

    // --- DEMONSTRATION 1: Writing to a File ---
    printf("--- Part 1: Writing to a file ---\n");

    // Step 1: Open the file in WRITE ("w") mode.
    file_pointer = fopen(filename, "w");

    // Step 2: ALWAYS check if the file opened successfully.
    if (file_pointer == NULL)
    {
        // stderr is the "standard error" stream, a good place for error messages.
        fprintf(stderr, "Error: Could not open file '%s' for writing.\n", filename);
        exit(1); // exit() terminates the program immediately.
    }

    // Step 3: Write to the file using fprintf().
    printf("Writing data to %s...\n", filename);
    fprintf(file_pointer, "Hello, File!\n");
    fprintf(file_pointer, "This is the second line.\n");
    fprintf(file_pointer, "Player: %s, Score: %d\n", "Max", 100);

    // Step 4: Close the file.
    fclose(file_pointer);
    file_pointer = NULL; // Good practice to nullify the pointer after closing.
    printf("Successfully wrote to and closed the file.\n\n");

    // --- DEMONSTRATION 2: Reading from a File ---
    printf("--- Part 2: Reading from a file ---\n");

    // This buffer will hold the lines we read from the file.
    char line_buffer[256];

    // Step 1: Open the same file, but now in READ ("r") mode.
    file_pointer = fopen(filename, "r");

    // Step 2: Check for errors again. The file might have been deleted, or
    // we may not have permission to read it.
    if (file_pointer == NULL)
    {
        fprintf(stderr, "Error: Could not open file '%s' for reading.\n", filename);
        exit(1);
    }

    // Step 3: Read from the file line-by-line using fgets().
    // `fgets()` reads until it hits a newline, the end of the file, or the
    // buffer size limit is reached. It returns NULL when there is nothing left to read.
    printf("Reading data from %s:\n", filename);
    while (fgets(line_buffer, sizeof(line_buffer), file_pointer) != NULL)
    {
        // Print the line we just read from the file to the console.
        printf("%s", line_buffer);
    }

    // Step 4: Close the file.
    fclose(file_pointer);
    printf("\nSuccessfully read and closed the file.\n");

    return 0;
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * Key Takeaways:
 *
 * 1.  File I/O allows your program's data to be PERSISTENT.
 * 2.  The `FILE *` is your handle to an open file.
 * 3.  The basic workflow is always: OPEN -> VALIDATE -> READ/WRITE -> CLOSE.
 * 4.  `fopen()` opens a file with a specific mode (`"w"`, `"r"`, `"a"`). You MUST
 *     check its return value for `NULL` to handle errors.
 * 5.  `fprintf()` is like `printf()` but writes to a file stream.
 * 6.  `fgets()` is a safe and reliable way to read text line-by-line from a file.
 * 7.  `fclose()` is mandatory to save all changes and release system resources.
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * 1. Open a terminal or command prompt.
 * 2. Navigate to the directory where you saved this file.
 * 3. Use the GCC compiler to create an executable file:
 *    `gcc -Wall -Wextra -std=c11 -o 14_file_io 14_file_io.c`
 * 4. Run the executable:
 *    - On Linux/macOS:   `./14_file_io`
 *    - On Windows:       `14_file_io.exe`
 *
 * After running, check the directory! You will find a new file named
 * `my_first_file.txt` containing the text from the program.
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 14_file_io 14_file_io.c
./14_file_io

Command-Line Arguments

You’ve already been using command-line arguments without even realizing it! When you compile a program like this: gcc -o my_program my_program.c

The strings -o, my_program, and my_program.c are all command-line arguments passed to the gcc program. They tell gcc what to do.

This is an incredibly powerful way to make your programs flexible and scriptable. Instead of prompting the user for input with scanf, you can have them provide the input when they run the program.

To access these arguments, we must use a different signature for our main function.

The main Function with Arguments

int main(int argc, char *argv[])

Let’s break this down:

  1. int argc: argc stands for “argument count”. It is an integer that holds the number of command-line arguments that were passed to your program. This count ALWAYS includes the name of the program itself as the first argument. So, argc will always be at least 1.

  2. char *argv[]: argv stands for “argument vector” (a vector is just another name for an array). It is an ARRAY OF STRINGS (or more accurately, an array of character pointers). Each element in this array is one of the command-line arguments.

    • argv[0] is always the name of the program executable.
    • argv[1] is the first argument the user actually typed.
    • argv[2] is the second argument, and so on.
    • argv[argc - 1] is the last argument.

The C standard also guarantees that argv[argc] will be a NULL pointer.

Full Source

/**
 * @file 15_command_line_arguments.c
 * @brief Part 2, Lesson 15: Command-Line Arguments
 * @author dunamismax
 * @date 06-15-2025
 *
 * This file serves as the lesson and demonstration for using command-line arguments.
 * It explains the special `main` function signature that allows a program to
 * receive and process input directly from the command line when it's executed.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * You've already been using command-line arguments without even realizing it!
 * When you compile a program like this:
 * `gcc -o my_program my_program.c`
 *
 * The strings `-o`, `my_program`, and `my_program.c` are all command-line
 * arguments passed to the `gcc` program. They tell `gcc` what to do.
 *
 * This is an incredibly powerful way to make your programs flexible and
 * scriptable. Instead of prompting the user for input with `scanf`, you can
 * have them provide the input when they run the program.
 *
 * To access these arguments, we must use a different signature for our `main` function.
 *
 * --- The `main` Function with Arguments ---
 *
 * `int main(int argc, char *argv[])`
 *
 * Let's break this down:
 *
 * 1. `int argc`:
 *    `argc` stands for "argument count". It is an integer that holds the number
 *    of command-line arguments that were passed to your program. This count
 *    ALWAYS includes the name of the program itself as the first argument. So,
 *    `argc` will always be at least 1.
 *
 * 2. `char *argv[]`:
 *    `argv` stands for "argument vector" (a vector is just another name for an array).
 *    It is an ARRAY OF STRINGS (or more accurately, an array of character pointers).
 *    Each element in this array is one of the command-line arguments.
 *
 *    - `argv[0]` is always the name of the program executable.
 *    - `argv[1]` is the first argument the user actually typed.
 *    - `argv[2]` is the second argument, and so on.
 *    - `argv[argc - 1]` is the last argument.
 *    - The C standard also guarantees that `argv[argc]` will be a `NULL` pointer.
 */

#include <stdio.h>
#include <stdlib.h> // We need this for atoi() to convert a string to an integer

// Note the new main function signature!
int main(int argc, char *argv[])
{
    // --- DEMONSTRATION 1: Printing all arguments ---
    printf("--- Part 1: Inspecting argc and argv ---\n");

    // Let's first see what `argc` contains.
    printf("Argument count (argc): %d\n", argc);

    // Now, let's loop through the `argv` array and print each argument.
    printf("Arguments (argv):\n");
    for (int i = 0; i < argc; i++)
    {
        printf("  argv[%d]: \"%s\"\n", i, argv[i]);
    }
    printf("\n");

    // --- DEMONSTRATION 2: Using the arguments ---
    printf("--- Part 2: A Practical Example ---\n");

    // A common task is to check if the user provided the correct number of arguments.
    // Let's create a simple greeter program that expects a name and an age.
    // We expect 3 arguments:
    // 1. The program name (e.g., ./15_command_line_arguments)
    // 2. A name (e.g., "Alice")
    // 3. An age (e.g., "30")
    if (argc != 3)
    {
        // If the count is wrong, print a "usage" message to standard error.
        // This is a standard practice for command-line tools.
        fprintf(stderr, "Usage: %s <name> <age>\n", argv[0]);
        fprintf(stderr, "Example: %s Alice 30\n", argv[0]);
        return 1; // Return 1 to indicate an error occurred.
    }

    // If we get here, we have the correct number of arguments.
    // `argv[1]` will be the name (which is already a string).
    char *name = argv[1];

    // `argv[2]` will be the age, but it's a STRING (e.g., "30").
    // We need to convert it to an integer using `atoi()` (ASCII to Integer).
    int age = atoi(argv[2]);

    printf("Hello, %s!\n", name);
    printf("You are %d years old.\n", age);
    printf("In 10 years, you will be %d.\n", age + 10);

    return 0; // Success!
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * Key Takeaways:
 *
 * 1.  Command-line arguments make your programs powerful and scriptable.
 * 2.  Use `int main(int argc, char *argv[])` to accept arguments.
 * 3.  `argc` is the total count of arguments, including the program's own name.
 * 4.  `argv` is an array of strings containing the actual arguments.
 * 5.  `argv[0]` is always the program's name.
 * 6.  Always check `argc` to ensure the user has provided the correct number of
 *     inputs before you try to access them in `argv`.
 * 7.  Arguments are always passed as strings. You may need to convert them to
 *     numbers using functions like `atoi()` (for integers) or `atof()` (for floats).
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * 1. Open a terminal or command prompt.
 * 2. Navigate to the directory where you saved this file.
 * 3. Use the GCC compiler to create an executable file:
 *    `gcc -Wall -Wextra -std=c11 -o 15_command_line_arguments 15_command_line_arguments.c`
 *
 * 4. Run the executable WITH DIFFERENT ARGUMENTS to see what happens:
 *
 *    Try running with no arguments (this will fail our check):
 *    - On Linux/macOS:   `./15_command_line_arguments`
 *    - On Windows:       `15_command_line_arguments.exe`
 *
 *    Try running with the correct arguments:
 *    - On Linux/macOS:   `./15_command_line_arguments Bob 42`
 *    - On Windows:       `15_command_line_arguments.exe Bob 42`
 *
 *    Try running with too many arguments:
 *    - On Linux/macOS:   `./15_command_line_arguments Charlie 25 Extra`
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 15_command_line_arguments 15_command_line_arguments.c
./15_command_line_arguments

Simple Calculator

Welcome to Part 3! It’s time to start building bigger things. This lesson is our first real PROJECT: a simple calculator that works from the command line.

THE GOAL: We want to be able to run our program like this: ./16_simple_calculator 10 + 22 And have it print the result: 10.00 + 22.00 = 32.00

SKILLS WE WILL USE:

  1. Command-Line Arguments (argc, argv): To get the numbers and the operator from the user when they run the program.
  2. Data Conversion: The arguments in argv are STRINGS. We need to convert the number strings (“10”, “22”) into actual numbers (double) so we can do math with them. We’ll use the atof() function for this.
  3. Conditional Logic (if, switch): To check if the user provided the correct input and to decide which mathematical operation to perform.
  4. Error Handling: We will provide a helpful USAGE MESSAGE if the user makes a mistake and handle potential math errors like division by zero.

Full Source

/**
 * @file 16_simple_calculator.c
 * @brief Part 3, Lesson 16: Simple Calculator
 * @author dunamismax
 * @date 06-15-2025
 *
 * This file is our first full PROJECT. It combines several concepts we've
 * learned--command-line arguments, data type conversion, and conditional
 * logic--to build a complete, useful command-line tool.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * Welcome to Part 3! It's time to start building bigger things. This lesson is
 * our first real PROJECT: a simple calculator that works from the command line.
 *
 * THE GOAL:
 * We want to be able to run our program like this:
 * `./16_simple_calculator 10 + 22`
 * And have it print the result: `10.00 + 22.00 = 32.00`
 *
 * SKILLS WE WILL USE:
 * 1. Command-Line Arguments (`argc`, `argv`): To get the numbers and the
 *    operator from the user when they run the program.
 * 2. Data Conversion: The arguments in `argv` are STRINGS. We need to convert
 *    the number strings ("10", "22") into actual numbers (`double`) so we can
 *    do math with them. We'll use the `atof()` function for this.
 * 3. Conditional Logic (`if`, `switch`): To check if the user provided the
 *    correct input and to decide which mathematical operation to perform.
 * 4. Error Handling: We will provide a helpful USAGE MESSAGE if the user makes a
 *    mistake and handle potential math errors like division by zero.
 */

#include <stdio.h>
#include <stdlib.h> // Required for atof() and exit()
#include <string.h> // Required for strcmp()

// We need argc and argv to get input from the command line.
int main(int argc, char *argv[])
{
    double num1, num2, result;
    char operator;

    // --- Step 1: Validate the Input ---
    // We need exactly four arguments:
    // argv[0]: program name
    // argv[1]: first number
    // argv[2]: operator
    // argv[3]: second number
    if (argc != 4)
    {
        // If the count is wrong, print a helpful usage message to standard error.
        fprintf(stderr, "Usage: %s <number1> <operator> <number2>\n", argv[0]);
        fprintf(stderr, "Example: %s 10.5 / 2\n", argv[0]);
        exit(1); // Exit with an error code.
    }

    // --- Step 2: Convert and Store Arguments ---

    // `atof` (ASCII to float) converts a string to a double.
    num1 = atof(argv[1]);
    num2 = atof(argv[3]);

    // The operator is a string like "+". We only need the first character.
    // We check if the string is longer than one character, which would be invalid.
    if (strlen(argv[2]) != 1)
    {
        fprintf(stderr, "Error: Invalid operator '%s'. Operator must be a single character.\n", argv[2]);
        exit(1);
    }
    operator = argv[2][0];

    // --- Step 3: Perform the Calculation ---
    // A `switch` statement is perfect for checking the operator and deciding
    // which calculation to perform.
    switch (operator)
    {
    case '+':
        result = num1 + num2;
        break; // `break` exits the switch statement.

    case '-':
        result = num1 - num2;
        break;

    case '*':
        // Some shells interpret * as a wildcard. Users may need to type '*'
        // to pass it correctly.
        result = num1 * num2;
        break;

    case '/':
        // This is an important EDGE CASE. We cannot divide by zero.
        if (num2 == 0)
        {
            fprintf(stderr, "Error: Division by zero is not allowed.\n");
            exit(1);
        }
        result = num1 / num2;
        break;

    default:
        // If the operator is not one of the above, it's invalid.
        fprintf(stderr, "Error: Unknown operator '%c'.\n", operator);
        fprintf(stderr, "Supported operators are: + - * /\n");
        exit(1);
    }

    // --- Step 4: Display the Result ---
    // If we get here, the calculation was successful.
    // We use %.2f to format the numbers to two decimal places for clean output.
    printf("%.2f %c %.2f = %.2f\n", num1, operator, num2, result);

    return 0; // Success!
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * Congratulations! You've just built your first complete, functional command-line
 * tool. You've seen how to take user input, validate it, process it, and
 * produce a result, all while handling potential errors gracefully.
 *
 * Key Achievements in this Project:
 * - Combined multiple C features into a working application.
 * - Used `argc` and `argv` for real-world program input.
 * - Converted string data to numeric data for calculations (`atof`).
 * - Implemented robust error checking for both user input and math rules.
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * 1. Open a terminal or command prompt.
 * 2. Navigate to the directory where you saved this file.
 * 3. Use the GCC compiler to create an executable file:
 *    `gcc -Wall -Wextra -std=c11 -o 16_simple_calculator 16_simple_calculator.c`
 *
 * 4. Run the executable with different arguments:
 *
 *    Addition:
 *    ./16_simple_calculator 5 + 3
 *
 *    Subtraction with decimals:
 *    ./16_simple_calculator 100.5 - 50.2
 *
 *    Multiplication (you might need quotes around '*' in some terminals like bash):
 *    ./16_simple_calculator 7 '*' 6
 *
 *    Division:
 *    ./16_simple_calculator 20 / 4
 *
 *    Division by zero (this will trigger our error message):
 *    ./16_simple_calculator 10 / 0
 *
 *    Incorrect usage (this will trigger our usage message):
 *    ./16_simple_calculator 10 plus 5
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 16_simple_calculator 16_simple_calculator.c
./16_simple_calculator

Student Record System

This project is where everything comes together. We will build a complete application that can:

  1. Add new student records.
  2. Display all existing records.
  3. Save the records to a file so the data isn’t lost when the program closes.
  4. Load the records from the file when the program starts.

This is a foundational pattern for almost any data-management application.

KEY ARCHITECTURE CONCEPTS:

  • MODULARITY: We will break the program into small, single-purpose functions (e.g., add_student, save_to_file). This makes the code much easier to read, debug, and maintain. The main function will act as a control center.
  • DATA PERSISTENCE: By using File I/O, our application’s data will persist between runs.
  • USER INTERFACE: We’ll create a simple text-based menu to interact with the user.
  • ERROR HANDLING: The program will gracefully handle file errors and invalid user input.

so we can modify the original student_count in main.

const is used to signal that this function will not change the data.

Full Source

/**
 * @file 17_student_record_system.c
 * @brief Part 3, Project 17: Student Record System
 * @author dunamismax
 * @date 06-15-2025
 *
 * This project builds a menu-driven database application to manage student
 * records. It demonstrates the power of combining structs, arrays, functions,
 * and file I/O to create a persistent data management tool.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * This project is where everything comes together. We will build a complete
 * application that can:
 * 1. Add new student records.
 * 2. Display all existing records.
 * 3. Save the records to a file so the data isn't lost when the program closes.
 * 4. Load the records from the file when the program starts.
 *
 * This is a foundational pattern for almost any data-management application.
 *
 * KEY ARCHITECTURE CONCEPTS:
 * - MODULARITY: We will break the program into small, single-purpose functions
 *   (e.g., `add_student`, `save_to_file`). This makes the code much easier to
 *   read, debug, and maintain. The `main` function will act as a control center.
 * - DATA PERSISTENCE: By using File I/O, our application's data will persist
 *   between runs.
 * - USER INTERFACE: We'll create a simple text-based menu to interact with the user.
 * - ERROR HANDLING: The program will gracefully handle file errors and invalid
 *   user input.
 */

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

// --- Global Constants and Type Definitions ---
#define MAX_STUDENTS 100
#define MAX_NAME_LEN 50
#define INPUT_BUFFER_SIZE 128
#define FILENAME "students.db"

// Our main data structure for a single student.
typedef struct
{
    int id;
    char name[MAX_NAME_LEN];
    double gpa;
} Student;

// --- Function Prototypes ---
// Declaring all our functions here provides a nice overview of the program's
// capabilities and allows us to call them from `main` before they are defined.
void display_menu(void);
void add_student(Student students[], int *count);
void print_all_records(const Student students[], int count);
void save_to_file(const Student students[], int count);
void load_from_file(Student students[], int *count);
void clear_stream_remainder(FILE *stream);
void clear_input_buffer(void);
int read_line_from_stream(FILE *stream, char *buffer, size_t size);
int read_line(char *buffer, size_t size);
int parse_int_value(const char *text, int *value);
int parse_double_value(const char *text, double *value);
int is_blank_string(const char *text);
int parse_student_record(char *line, Student *student);

// --- Main Function: The Program's Control Center ---
int main(void)
{
    Student all_students[MAX_STUDENTS];
    int student_count = 0;
    int choice = 0;
    char input[INPUT_BUFFER_SIZE];

    // Start by loading any existing records from our database file.
    load_from_file(all_students, &student_count);

    // This is the main application loop. It continues until the user chooses to exit.
    while (1)
    {
        display_menu();
        if (read_line(input, sizeof(input)) == 0)
        {
            printf("\nEnd of input detected. Exiting program.\n");
            break;
        }

        if (!parse_int_value(input, &choice))
        {
            printf("Invalid input. Please enter a number.\n");
            continue; // Skip the rest of the loop and start over.
        }

        switch (choice)
        {
        case 1:
            add_student(all_students, &student_count);
            break;
        case 2:
            print_all_records(all_students, student_count);
            break;
        case 3:
            save_to_file(all_students, student_count);
            break;
        case 4:
            printf("Exiting program. Goodbye!\n");
            exit(0); // exit(0) terminates the program successfully.
        default:
            printf("Invalid choice. Please try again.\n");
        }
        printf("\n"); // Add a space for readability before the next menu.
    }

    return 0;
}

// --- Function Implementations ---

/**
 * @brief Displays the main menu options to the user.
 */
void display_menu(void)
{
    printf("--- Student Record System ---\n");
    printf("1. Add Student\n");
    printf("2. Display All Records\n");
    printf("3. Save Records to File\n");
    printf("4. Exit\n");
    printf("Enter your choice: ");
}

/**
 * @brief Consumes characters until the end of the current line.
 */
void clear_stream_remainder(FILE *stream)
{
    int c;
    while ((c = fgetc(stream)) != '\n' && c != EOF)
    {
        // Consume characters until newline or end-of-file.
    }
}

void clear_input_buffer(void)
{
    clear_stream_remainder(stdin);
}

/**
 * @brief Reads a full line from the provided stream into a buffer.
 * @return 1 on success, 0 on EOF/error, and -1 if the line was too long.
 */
int read_line_from_stream(FILE *stream, char *buffer, size_t size)
{
    size_t length;

    if (fgets(buffer, size, stream) == NULL)
    {
        return 0;
    }

    length = strcspn(buffer, "\n");
    if (buffer[length] == '\n')
    {
        buffer[length] = '\0';
        return 1;
    }

    clear_stream_remainder(stream);
    buffer[0] = '\0';
    return -1;
}

int read_line(char *buffer, size_t size)
{
    return read_line_from_stream(stdin, buffer, size);
}

/**
 * @brief Parses a whole-number input and rejects trailing junk.
 */
int parse_int_value(const char *text, int *value)
{
    char *endptr;
    long parsed;

    while (isspace((unsigned char)*text))
    {
        text++;
    }

    if (*text == '\0')
    {
        return 0;
    }

    errno = 0;
    parsed = strtol(text, &endptr, 10);
    if (errno != 0 || endptr == text || parsed < INT_MIN || parsed > INT_MAX)
    {
        return 0;
    }

    while (isspace((unsigned char)*endptr))
    {
        endptr++;
    }

    if (*endptr != '\0')
    {
        return 0;
    }

    *value = (int)parsed;
    return 1;
}

/**
 * @brief Parses a floating-point value and rejects trailing junk.
 */
int parse_double_value(const char *text, double *value)
{
    char *endptr;
    double parsed;

    while (isspace((unsigned char)*text))
    {
        text++;
    }

    if (*text == '\0')
    {
        return 0;
    }

    errno = 0;
    parsed = strtod(text, &endptr);
    if (errno != 0 || endptr == text)
    {
        return 0;
    }

    while (isspace((unsigned char)*endptr))
    {
        endptr++;
    }

    if (*endptr != '\0')
    {
        return 0;
    }

    *value = parsed;
    return 1;
}

int is_blank_string(const char *text)
{
    while (*text)
    {
        if (!isspace((unsigned char)*text))
        {
            return 0;
        }
        text++;
    }

    return 1;
}

/**
 * @brief Parses one database record from the on-disk format.
 */
int parse_student_record(char *line, Student *student)
{
    char *first_comma = strchr(line, ',');
    char *second_comma;

    if (!first_comma)
    {
        return 0;
    }

    second_comma = strchr(first_comma + 1, ',');
    if (!second_comma || strchr(second_comma + 1, ',') != NULL)
    {
        return 0;
    }

    *first_comma = '\0';
    *second_comma = '\0';

    if (!parse_int_value(line, &student->id) || student->id <= 0)
    {
        return 0;
    }

    if (strlen(first_comma + 1) >= MAX_NAME_LEN ||
        strchr(first_comma + 1, ',') != NULL ||
        is_blank_string(first_comma + 1))
    {
        return 0;
    }
    strcpy(student->name, first_comma + 1);

    if (!parse_double_value(second_comma + 1, &student->gpa) ||
        student->gpa < 0.0 || student->gpa > 4.0)
    {
        return 0;
    }

    return 1;
}

/**
 * @brief Adds a new student record to the array.
 * @param students The array of student records.
 * @param count A pointer to the current number of students. We use a pointer
 *              so we can modify the original `student_count` in `main`.
 */
void add_student(Student students[], int *count)
{
    Student new_student;
    char input[INPUT_BUFFER_SIZE];

    if (*count >= MAX_STUDENTS)
    {
        printf("Database is full. Cannot add more students.\n");
        return;
    }

    printf("Enter Student ID: ");
    if (read_line(input, sizeof(input)) != 1 ||
        !parse_int_value(input, &new_student.id) ||
        new_student.id <= 0)
    {
        printf("Invalid student ID. Record was not added.\n");
        return;
    }

    printf("Enter Student Name (commas are not allowed): ");
    if (read_line(new_student.name, sizeof(new_student.name)) != 1)
    {
        printf("Name was too long or could not be read. Record was not added.\n");
        return;
    }

    if (strchr(new_student.name, ',') != NULL)
    {
        printf("Names cannot contain commas because the save file uses commas as separators.\n");
        return;
    }

    if (is_blank_string(new_student.name))
    {
        printf("Student name cannot be empty. Record was not added.\n");
        return;
    }

    printf("Enter Student GPA: ");
    if (read_line(input, sizeof(input)) != 1 ||
        !parse_double_value(input, &new_student.gpa) ||
        new_student.gpa < 0.0 || new_student.gpa > 4.0)
    {
        printf("Invalid GPA. Enter a value between 0.0 and 4.0. Record was not added.\n");
        return;
    }

    students[*count] = new_student;
    printf("Student added successfully.\n");
    (*count)++; // Increment the total number of students.
}

/**
 * @brief Prints all student records to the console in a formatted table.
 * @param students The array of student records.
 * @param count The current number of students.
 *              `const` is used to signal that this function will not change the data.
 */
void print_all_records(const Student students[], int count)
{
    if (count == 0)
    {
        printf("No records to display.\n");
        return;
    }

    printf("\n--- All Student Records ---\n");
    printf("ID   | Name                                               | GPA\n");
    printf("-----|----------------------------------------------------|------\n");
    for (int i = 0; i < count; i++)
    {
        // %-4d: left-align integer in 4 spaces
        // %-50s: left-align string in 50 spaces
        // %5.2f: right-align double in 5 spaces, with 2 decimal places
        printf("%-4d | %-50s | %5.2f\n", students[i].id, students[i].name, students[i].gpa);
    }
    printf("------------------------------------------------------------------\n");
}

/**
 * @brief Saves the entire array of student records to a file.
 */
void save_to_file(const Student students[], int count)
{
    FILE *file = fopen(FILENAME, "w"); // "w" for write, will overwrite existing file.
    if (file == NULL)
    {
        fprintf(stderr, "Error: Could not open file '%s' for writing.\n", FILENAME);
        return;
    }

    for (int i = 0; i < count; i++)
    {
        // We save in a simple comma-separated format, so names must not contain commas.
        fprintf(file, "%d,%s,%.2f\n", students[i].id, students[i].name, students[i].gpa);
    }

    fclose(file);
    printf("Successfully saved %d record(s) to %s.\n", count, FILENAME);
}

/**
 * @brief Loads student records from a file into the array.
 */
void load_from_file(Student students[], int *count)
{
    FILE *file = fopen(FILENAME, "r"); // "r" for read.
    char line[INPUT_BUFFER_SIZE * 2];
    int loaded = 0;
    int skipped = 0;
    int line_status;

    if (file == NULL)
    {
        // This is not an error if it's the first time running the program.
        printf("No existing database file found. Starting fresh.\n");
        return;
    }

    while ((line_status = read_line_from_stream(file, line, sizeof(line))) != 0)
    {
        if (line_status < 0)
        {
            skipped++;
            continue;
        }

        if (line[0] == '\0')
        {
            continue;
        }

        if (*count >= MAX_STUDENTS)
        {
            skipped++;
            continue;
        }

        if (parse_student_record(line, &students[*count]))
        {
            (*count)++;
            loaded++;
        }
        else
        {
            skipped++;
        }
    }

    fclose(file);
    printf("Successfully loaded %d record(s) from %s.\n", loaded, FILENAME);
    if (skipped > 0)
    {
        printf("Skipped %d invalid record(s) while loading.\n", skipped);
    }
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * Congratulations! You've built a complete database application. This project is a
 * major milestone and demonstrates a solid understanding of C's most important
 * features for building practical software.
 *
 * Key Project Achievements:
 * - A modular design using functions for each major feature.
 * - Persistent data storage using a simple comma-separated file format.
 * - A clean, interactive user menu for program control.
 * - Robust handling of user input and file system operations.
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * 1. Open a terminal or command prompt.
 * 2. Navigate to the directory where you saved this file.
 * 3. Use the GCC compiler to create an executable file:
 *    `gcc -Wall -Wextra -std=c11 -o 17_student_record_system 17_student_record_system.c`
 *
 * 4. Run the executable:
 *    - On Linux/macOS:   `./17_student_record_system`
 *    - On Windows:       `17_student_record_system.exe`
 *
 * Try adding a few students, saving, exiting the program, and running it again.
 * You'll see your records are loaded back in automatically!
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 17_student_record_system 17_student_record_system.c
./17_student_record_system

Function Pointers

You have mastered pointers to DATA (like int* or struct Student*), which store the memory address of a variable.

C takes this concept one step further: you can also have pointers to CODE. A FUNCTION POINTER is a variable that stores the memory address of a function.

This might sound strange, but it’s an incredibly powerful feature. It allows you to treat functions like any other variable:

  • You can pass a function as an argument to another function.
  • You can store functions in an array.
  • You can change which function gets called at runtime.

The most common use case is creating CALLBACKS, where you provide a generic function with a specific “helper” function for it to call back to complete its task.

Full Source

/**
 * @file 18_function_pointers.c
 * @brief Part 3, Lesson 18: Function Pointers
 * @author dunamismax
 * @date 06-15-2025
 *
 * This file serves as the lesson and demonstration for function pointers.
 * It explains how to declare, assign, and use pointers to functions,
 * enabling powerful techniques like callbacks and dynamic function calls.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * You have mastered pointers to DATA (like `int*` or `struct Student*`), which
 * store the memory address of a variable.
 *
 * C takes this concept one step further: you can also have pointers to CODE.
 * A FUNCTION POINTER is a variable that stores the memory address of a function.
 *
 * This might sound strange, but it's an incredibly powerful feature. It allows
 * you to treat functions like any other variable:
 * - You can pass a function as an argument to another function.
 * - You can store functions in an array.
 * - You can change which function gets called at runtime.
 *
 * The most common use case is creating CALLBACKS, where you provide a generic
 * function with a specific "helper" function for it to call back to complete its task.
 */

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

// --- Part 1: The Functions We'll Point To ---
// Let's define two simple functions. It's crucial that they have the
// exact same SIGNATURE (return type and parameter types).

int add(int a, int b)
{
    return a + b;
}

int subtract(int a, int b)
{
    return a - b;
}

// --- Part 2: The Callback Function ---
// Here is a generic function that takes two numbers and a function pointer
// as arguments. It will use the provided function to perform a calculation.
//
// The third parameter `int (*operation_func)(int, int)` is the function pointer.
// It says: "I expect the address of a function that takes two ints and returns an int."
void perform_calculation(int x, int y, int (*operation_func)(int, int))
{
    // First, check if the pointer is valid (not NULL)
    if (operation_func == NULL)
    {
        printf("Error: No operation function provided.\n");
        return;
    }
    int result = operation_func(x, y); // Call the function through the pointer
    printf("Performing calculation... Result is: %d\n", result);
}

int main(void)
{
    // --- DEMONSTRATION 1: Declaring and Using a Function Pointer ---
    printf("--- Part 1: Basic Function Pointer Usage ---\n");

    // This is the syntax for declaring a function pointer.
    // It must match the signature of the functions it will point to.
    // return_type (*pointer_name)(parameter_types);
    int (*p_calculate)(int, int) = NULL;

    // The parentheses around `*p_calculate` are ESSENTIAL.
    // Without them, `int *p_calculate(int, int);` would declare a
    // function named `p_calculate` that returns an `int*`.

    // ASSIGNMENT: Assign the address of the `add` function to the pointer.
    // The name of a function, like an array name, decays into a pointer.
    p_calculate = add; // You can also use `&add`, but it's not necessary.

    // CALLING: Call the function through the pointer.
    int sum = p_calculate(10, 5);
    printf("Calling via pointer (add): %d\n", sum);

    // Now, point the SAME pointer to a DIFFERENT function.
    p_calculate = subtract;

    // And call it again. The pointer now invokes different code.
    int difference = p_calculate(10, 5);
    printf("Calling via pointer (subtract): %d\n\n", difference);

    // --- DEMONSTRATION 2: Using Function Pointers as Callbacks ---
    printf("--- Part 2: Callbacks in Action ---\n");

    int a = 20;
    int b = 12;

    // Here, we call `perform_calculation` and pass the `add` function
    // as the callback. `perform_calculation` will "call back" to `add`.
    printf("Passing 'add' function as a callback:\n");
    perform_calculation(a, b, add);

    // Now we call the exact same generic function, but we give it a
    // different tool to work with.
    printf("Passing 'subtract' function as a callback:\n");
    perform_calculation(a, b, subtract);

    return 0;
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * Key Takeaways:
 *
 * 1.  A FUNCTION POINTER stores the memory address of a function.
 * 2.  The declaration syntax is critical: `return_type (*pointer_name)(params);`.
 *     The parentheses around the pointer name are mandatory.
 * 3.  You can assign the name of a compatible function directly to the pointer.
 * 4.  This allows for incredibly flexible code, primarily by enabling CALLBACKS.
 * 5.  A callback is when you pass a function (A) as an argument to another
 *     function (B), and B then calls A to perform a specific part of its task.
 *     This allows you to customize the behavior of generic functions.
 *
 * This is an advanced topic, but mastering it opens the door to designing
 * highly modular and reusable C programs.
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * 1. Open a terminal or command prompt.
 * 2. Navigate to the directory where you saved this file.
 * 3. Use the GCC compiler to create an executable file:
 *    `gcc -Wall -Wextra -std=c11 -o 18_function_pointers 18_function_pointers.c`
 * 4. Run the executable:
 *    - On Linux/macOS:   `./18_function_pointers`
 *    - On Windows:       `18_function_pointers.exe`
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 18_function_pointers 18_function_pointers.c
./18_function_pointers

Recursion

So far, when you’ve needed to repeat a task, you’ve used a loop (for or while). Today, we’re going to learn a powerful and elegant alternative: RECURSION.

WHAT IS RECURSION? RECURSION is the process of a function calling itself.

Think of it like a set of Russian nesting dolls. You open a doll to find a slightly smaller doll inside. You open that one to find an even smaller one, and so on, until you reach the smallest, solid doll that can’t be opened.

In programming, a recursive function works the same way. It has two essential parts:

  1. THE BASE CASE: This is the condition that STOPS the recursion. It’s the “smallest doll” that doesn’t call itself again. Without a base case, a recursive function would call itself forever, leading to a crash. This is the MOST IMPORTANT part of any recursive function.

  2. THE RECURSIVE STEP: This is where the function calls itself, but with a modified argument that brings it one step closer to the base case. It’s the act of “opening the doll to find the next smaller one.”

// DO NOT UNCOMMENT AND RUN THIS FUNCTION. // It is an example of what NOT to do. void infinite_recursion(void) { printf(“This will run forever… and then crash!\n”); infinite_recursion(); // No base case, no modification. Uh oh. }

Full Source

/**
 * @file 19_recursion.c
 * @brief Part 3, Lesson 19: Recursion
 * @author dunamismax
 * @date 06-15-2025
 *
 * This file serves as the lesson and demonstration for recursion. It explains
 * the concept of a function calling itself, the importance of a base case,
 * and how recursion can be an elegant alternative to loops for some problems.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * So far, when you've needed to repeat a task, you've used a loop (`for` or `while`).
 * Today, we're going to learn a powerful and elegant alternative: RECURSION.
 *
 * WHAT IS RECURSION?
 * RECURSION is the process of a function calling itself.
 *
 * Think of it like a set of Russian nesting dolls. You open a doll to find a
 * slightly smaller doll inside. You open that one to find an even smaller one,
 * and so on, until you reach the smallest, solid doll that can't be opened.
 *
 * In programming, a recursive function works the same way. It has two essential parts:
 *
 * 1. THE BASE CASE: This is the condition that STOPS the recursion. It's the
 *    "smallest doll" that doesn't call itself again. Without a base case, a
 *    recursive function would call itself forever, leading to a crash. This is
 *    the MOST IMPORTANT part of any recursive function.
 *
 * 2. THE RECURSIVE STEP: This is where the function calls itself, but with a
 *    modified argument that brings it one step closer to the base case. It's
 *    the act of "opening the doll to find the next smaller one."
 */

#include <stdio.h>

// --- Part 1: A Simple Recursive Countdown ---
// This function will count down from a number `n` to 1, then print "Blastoff!".
// It's a simple way to see the base case and recursive step in action.
void countdown(int n)
{
    // BASE CASE: If n is 0 or less, we stop the recursion.
    if (n <= 0)
    {
        printf("Blastoff!\n");
        return; // Stop.
    }

    // RECURSIVE STEP:
    printf("%d...\n", n);
    countdown(n - 1); // The function calls itself with a smaller number.
}

// --- Part 2: A Classic Example - Factorial ---
// The factorial of a non-negative integer `n`, denoted by `n!`, is the
// product of all positive integers less than or equal to `n`.
// Example: 5! = 5 * 4 * 3 * 2 * 1 = 120
//
// This problem has a natural recursive definition:
// - The factorial of 0 is 1. (Base Case)
// - The factorial of any other number `n` is `n` times the factorial of `n-1`. (Recursive Step)
//
// We use `long long` for the return type because factorials grow very quickly!
long long factorial(int n)
{
    // BASE CASE: The factorial of 0 (or 1) is 1.
    if (n <= 1)
    {
        return 1;
    }

    // RECURSIVE STEP: n multiplied by the factorial of the number below it.
    return n * factorial(n - 1);
}

// --- Part 3: The Danger - Infinite Recursion and Stack Overflow ---
// What happens if you forget the base case? The function will call itself
// endlessly. Each time a function is called, a small amount of memory is used
// on the "call stack". If the recursion never stops, it will eventually use up
// all the available stack memory, and the program will crash with an error
// called a STACK OVERFLOW.

/*
// DO NOT UNCOMMENT AND RUN THIS FUNCTION.
// It is an example of what NOT to do.
void infinite_recursion(void)
{
    printf("This will run forever... and then crash!\n");
    infinite_recursion(); // No base case, no modification. Uh oh.
}
*/

int main(void)
{
    printf("--- DEMONSTRATION 1: Countdown from 5 ---\n");
    countdown(5);

    printf("\n--- DEMONSTRATION 2: Calculating Factorial of 10 ---\n");
    int number = 10;
    long long fact_result = factorial(number);
    printf("The factorial of %d is %lld.\n", number, fact_result);

    // Let's try another one.
    number = 15;
    fact_result = factorial(number);
    printf("The factorial of %d is %lld.\n", number, fact_result);

    return 0;
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * Key Takeaways:
 *
 * 1.  Recursion is a problem-solving technique where a function calls itself.
 * 2.  It's a powerful alternative to loops for problems that can be broken down
 *     into smaller, self-similar sub-problems.
 * 3.  Every recursive function MUST have a BASE CASE to prevent infinite recursion.
 * 4.  Every recursive function MUST have a RECURSIVE STEP that moves it closer
 *     to the base case.
 * 5.  Forgetting or having an incorrect base case leads to a STACK OVERFLOW crash.
 *
 * While loops are often more efficient in C, recursion can provide incredibly
 * elegant and readable solutions for certain types of problems, especially when
 * dealing with tree-like data structures we'll see later.
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * 1. Open a terminal or command prompt.
 * 2. Navigate to the directory where you saved this file.
 * 3. Use the GCC compiler to create an executable file:
 *    `gcc -Wall -Wextra -std=c11 -o 19_recursion 19_recursion.c`
 * 4. Run the executable:
 *    - On Linux/macOS:   `./19_recursion`
 *    - On Windows:       `19_recursion.exe`
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 19_recursion 19_recursion.c
./19_recursion

Linked Lists

So far, our primary way of storing a collection of items has been the ARRAY. Arrays are great, but they have one major limitation: they have a fixed size. If you declare int my_array[100];, you can’t store 101 items without creating a new, larger array and copying everything over.

Welcome to the LINKED LIST, a data structure that solves this problem.

WHAT IS A LINKED LIST? A LINKED LIST is a sequence of data elements, which are connected together via links. Each element, called a NODE, contains two things:

  1. The DATA itself (e.g., a number, a struct, etc.).
  2. A POINTER to the next node in the sequence.

Think of it like a train. Each Node is a train car. It holds some cargo (the data) and has a coupling that connects it to the next car (the next pointer). All we need to know to find the entire train is where the first car is. This first-car pointer is called the HEAD.

so we can modify the head pointer in the main function.

Full Source

/**
 * @file 20_linked_lists.c
 * @brief Part 3, Lesson 20: Linked Lists
 * @author dunamismax
 * @date 06-15-2025
 *
 * This file serves as the lesson and demonstration for a singly linked list.
 * It shows how to build one of the most fundamental dynamic data structures
 * from scratch using structs, pointers, and dynamic memory allocation.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * So far, our primary way of storing a collection of items has been the ARRAY.
 * Arrays are great, but they have one major limitation: they have a fixed size.
 * If you declare `int my_array[100];`, you can't store 101 items without
 * creating a new, larger array and copying everything over.
 *
 * Welcome to the LINKED LIST, a data structure that solves this problem.
 *
 * WHAT IS A LINKED LIST?
 * A LINKED LIST is a sequence of data elements, which are connected together via
 * links. Each element, called a NODE, contains two things:
 * 1. The DATA itself (e.g., a number, a struct, etc.).
 * 2. A POINTER to the next node in the sequence.
 *
 * Think of it like a train. Each `Node` is a train car. It holds some cargo (the
 * data) and has a coupling that connects it to the next car (the `next` pointer).
 * All we need to know to find the entire train is where the first car is. This
 * first-car pointer is called the HEAD.
 */

#include <stdio.h>
#include <stdlib.h> // For malloc() and free()

// --- Part 1: The Building Block - The Node ---

// This is the blueprint for a single "car" in our train.
// It's a SELF-REFERENTIAL struct because it contains a pointer to itself.
typedef struct Node
{
    int data;          // The data this node holds
    struct Node *next; // A pointer to the next node in the list
} Node;

// --- Part 2: Core List Operations ---
// We will write functions to handle the list's main operations.

/**
 * @brief Creates a new node, allocates memory for it, and initializes its fields.
 * @param data The integer data to store in the new node.
 * @return A pointer to the newly created node.
 */
Node *create_node(int data)
{
    // Allocate memory for one Node on the HEAP.
    Node *new_node = (Node *)malloc(sizeof(Node));
    if (new_node == NULL)
    {
        fprintf(stderr, "Error: Memory allocation failed!\n");
        exit(1);
    }
    new_node->data = data; // Set the data.
    new_node->next = NULL; // The new node doesn't point to anything yet.
    return new_node;
}

/**
 * @brief Inserts a new node at the beginning of the list.
 * @param head_ref A pointer to the head pointer. We need this double pointer
 *                 so we can modify the `head` pointer in the main function.
 * @param data The data for the new node.
 */
void insert_at_beginning(Node **head_ref, int data)
{
    // 1. Create the new node.
    Node *new_node = create_node(data);

    // 2. Point the new node's `next` to what `head` is currently pointing to.
    new_node->next = *head_ref;

    // 3. Update `head` to point to our new node, making it the new first node.
    *head_ref = new_node;
}

/**
 * @brief Prints all the elements of the list from beginning to end.
 * @param head A pointer to the first node of the list.
 */
void print_list(Node *head)
{
    Node *current = head; // Start at the beginning.

    while (current != NULL)
    {
        printf("%d -> ", current->data);
        current = current->next; // Move to the next node.
    }
    printf("NULL\n"); // The end of the list points to nothing.
}

/**
 * @brief Frees all memory allocated for the list to prevent memory leaks.
 * @param head_ref A pointer to the head pointer.
 */
void free_list(Node **head_ref)
{
    Node *current = *head_ref;
    Node *temp;

    while (current != NULL)
    {
        temp = current;          // Save the current node.
        current = current->next; // Move to the next one.
        free(temp);              // Free the saved node.
    }

    // Finally, set the original head pointer in main() to NULL.
    *head_ref = NULL;
}

int main(void)
{
    // The HEAD pointer. This is our only entry point into the list.
    // An empty list is represented by a NULL head pointer.
    Node *head = NULL;

    printf("Building the linked list by inserting at the beginning...\n");

    // Insert elements. Since we insert at the beginning, the last one
    // we insert will be the first one in the list.
    insert_at_beginning(&head, 30);
    insert_at_beginning(&head, 20);
    insert_at_beginning(&head, 10);

    printf("The current list is:\n");
    print_list(head);
    printf("\n");

    printf("Adding another element, 5...\n");
    insert_at_beginning(&head, 5);

    printf("The final list is:\n");
    print_list(head);
    printf("\n");

    // CRITICAL STEP: Always clean up memory when you're done.
    printf("Freeing all nodes in the list...\n");
    free_list(&head);

    printf("List after freeing:\n");
    print_list(head); // Should print "NULL"

    return 0;
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * Key Takeaways:
 *
 * 1.  A LINKED LIST is a dynamic data structure made of NODES linked by pointers.
 * 2.  A NODE contains DATA and a POINTER to the next node.
 * 3.  The HEAD pointer is the entry point to the entire list. If `head` is `NULL`,
 *     the list is empty.
 * 4.  Nodes are created on the HEAP using `malloc()`, which gives us the flexibility
 *     to grow or shrink the list at runtime.
 * 5.  Because we use `malloc()`, we are responsible for using `free()` on every
 *     single node to prevent memory leaks.
 * 6.  To modify the head pointer from within a function, we must pass its
 *     address (a pointer to a pointer, or `Node **`).
 *
 * You have just built one of the most fundamental data structures in all of
 * computer science. Understanding linked lists is key to tackling more complex
 * structures like trees, graphs, and hash tables.
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * 1. Open a terminal or command prompt.
 * 2. Navigate to the directory where you saved this file.
 * 3. Use the GCC compiler to create an executable file:
 *    `gcc -Wall -Wextra -std=c11 -o 20_linked_lists 20_linked_lists.c`
 * 4. Run the executable:
 *    - On Linux/macOS:   `./20_linked_lists`
 *    - On Windows:       `20_linked_lists.exe`
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 20_linked_lists 20_linked_lists.c
./20_linked_lists

Bit Manipulation

C is a language that operates very close to the hardware. All data in your computer–integers, characters, etc.–is ultimately stored as a sequence of BITS (binary digits), which are either 0 or 1.

C provides a special set of BITWISE OPERATORS that allow you to manipulate these individual bits directly. This is a low-level feature that is incredibly powerful for tasks like:

  • Controlling hardware devices where each bit in a register has a meaning.
  • Writing highly efficient code for specific algorithms.
  • Storing multiple boolean (true/false) flags in a single byte.

The Bitwise Operators

& (Bitwise AND): Sets a bit to 1 if BOTH corresponding bits are 1. | (Bitwise OR): Sets a bit to 1 if EITHER corresponding bit is 1. ^ (Bitwise XOR): Sets a bit to 1 if the corresponding bits are DIFFERENT. ~ (Bitwise NOT): Inverts all the bits (0s become 1s and 1s become 0s). << (Left Shift): Shifts all bits to the left by a specified number of places.

(Right Shift): Shifts all bits to the right by a specified number of places.

Full Source

/**
 * @file 21_bit_manipulation.c
 * @brief Part 3, Lesson 21: Bit Manipulation
 * @author dunamismax
 * @date 06-15-2025
 *
 * This file serves as the lesson and demonstration for bit manipulation. It
 * introduces the bitwise operators and shows how to directly manipulate the
 * individual bits of data, a powerful technique for optimization and control.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * C is a language that operates very close to the hardware. All data in your
 * computer--integers, characters, etc.--is ultimately stored as a sequence of
 * BITS (binary digits), which are either 0 or 1.
 *
 * C provides a special set of BITWISE OPERATORS that allow you to manipulate
 * these individual bits directly. This is a low-level feature that is incredibly
 * powerful for tasks like:
 * - Controlling hardware devices where each bit in a register has a meaning.
 * - Writing highly efficient code for specific algorithms.
 * - Storing multiple boolean (true/false) flags in a single byte.
 *
 * --- The Bitwise Operators ---
 *
 * &  (Bitwise AND):     Sets a bit to 1 if BOTH corresponding bits are 1.
 * |  (Bitwise OR):      Sets a bit to 1 if EITHER corresponding bit is 1.
 * ^  (Bitwise XOR):     Sets a bit to 1 if the corresponding bits are DIFFERENT.
 * ~  (Bitwise NOT):     Inverts all the bits (0s become 1s and 1s become 0s).
 * << (Left Shift):      Shifts all bits to the left by a specified number of places.
 * >> (Right Shift):     Shifts all bits to the right by a specified number of places.
 */

#include <stdio.h>
#include <stdint.h> // For uint8_t, an unsigned 8-bit integer type.

// --- A Helper Function to Visualize Bits ---
// To understand bit manipulation, you need to SEE the bits. This function
// will print the binary representation of a byte (an 8-bit number).
// We use `uint8_t` for a clear, 8-bit unsigned integer.
void print_binary(uint8_t byte)
{
    printf("0b"); // "0b" prefix indicates a binary number
    // We loop from the most significant bit (7) down to the least (0).
    for (int i = 7; i >= 0; i--)
    {
        // `(byte >> i)` shifts the bit we're interested in to the 0th position.
        // `& 1` then isolates that bit. If it's 1, the result is 1; otherwise, 0.
        printf("%d", (byte >> i) & 1);
    }
}

int main(void)
{
    // Let's use two small numbers for our demonstration.
    uint8_t a = 5; // Binary: 00000101
    uint8_t b = 9; // Binary: 00001001

    printf("--- Part 1: Basic Bitwise Operations ---\n");
    printf("a = %2d = ", a);
    print_binary(a);
    printf("\n");
    printf("b = %2d = ", b);
    print_binary(b);
    printf("\n\n");

    // Bitwise AND
    uint8_t result_and = a & b; // 00000101 & 00001001 = 00000001 (1)
    printf("a & b = %2d = ", result_and);
    print_binary(result_and);
    printf("\n");

    // Bitwise OR
    uint8_t result_or = a | b; // 00000101 | 00001001 = 00001101 (13)
    printf("a | b = %2d = ", result_or);
    print_binary(result_or);
    printf("\n");

    // Bitwise XOR
    uint8_t result_xor = a ^ b; // 00000101 ^ 00001001 = 00001100 (12)
    printf("a ^ b = %2d = ", result_xor);
    print_binary(result_xor);
    printf("\n");

    // Bitwise NOT
    uint8_t result_not_a = ~a; // ~00000101 = 11111010 (250)
    printf("  ~a  = %3d = ", result_not_a);
    print_binary(result_not_a);
    printf("\n\n");

    // --- Part 2: Shift Operations ---
    printf("--- Part 2: Shift Operations ---\n");
    uint8_t c = 12; // Binary: 00001100

    printf("c = %2d = ", c);
    print_binary(c);
    printf("\n");

    // Left Shift (equivalent to multiplying by 2^n)
    uint8_t left_shifted = c << 2; // 12 * 2^2 = 12 * 4 = 48 (00110000)
    printf("c << 2 = %2d = ", left_shifted);
    print_binary(left_shifted);
    printf("\n");

    // Right Shift (equivalent to integer division by 2^n)
    uint8_t right_shifted = c >> 1; // 12 / 2^1 = 12 / 2 = 6 (00000110)
    printf("c >> 1 = %2d = ", right_shifted);
    print_binary(right_shifted);
    printf("\n\n");

    // --- Part 3: Practical Use - BITMASKS for Flags ---
    // A very common use case is to store multiple on/off states in a single byte.
    // Each bit position represents a different flag.

    printf("--- Part 3: Practical Bitmasking ---\n");
    const uint8_t FLAG_READ_ACCESS = 1 << 0;    // 00000001 (1)
    const uint8_t FLAG_WRITE_ACCESS = 1 << 1;   // 00000010 (2)
    const uint8_t FLAG_EXECUTE_ACCESS = 1 << 2; // 00000100 (4)
    const uint8_t FLAG_IS_ADMIN = 1 << 7;       // 10000000 (128)

    uint8_t user_permissions = 0; // Start with no permissions.
    printf("Initial permissions: ");
    print_binary(user_permissions);
    printf("\n");

    // 1. SETTING a bit (giving permission) using OR
    user_permissions = user_permissions | FLAG_READ_ACCESS;
    user_permissions = user_permissions | FLAG_WRITE_ACCESS;
    printf("After giving R/W:    ");
    print_binary(user_permissions);
    printf("\n");

    // 2. CHECKING a bit (do they have permission?) using AND
    if (user_permissions & FLAG_WRITE_ACCESS)
    {
        printf("User has WRITE access.\n");
    }
    if (!(user_permissions & FLAG_EXECUTE_ACCESS))
    {
        printf("User does NOT have EXECUTE access.\n");
    }
    if (!(user_permissions & FLAG_IS_ADMIN))
    {
        printf("User is NOT an administrator.\n");
    }

    // 3. CLEARING a bit (revoking permission) using AND with NOT
    user_permissions = user_permissions & ~FLAG_WRITE_ACCESS;
    printf("After revoking W:    ");
    print_binary(user_permissions);
    printf("\n");

    // 4. TOGGLING a bit (flipping its state) using XOR
    user_permissions = user_permissions ^ FLAG_READ_ACCESS; // Turn it off
    user_permissions = user_permissions ^ FLAG_READ_ACCESS; // Turn it back on
    printf("After toggling R twice:");
    print_binary(user_permissions);
    printf("\n");

    return 0;
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * Key Takeaways:
 *
 * 1.  Bitwise operators (`&`, `|`, `^`, `~`, `<<`, `>>`) allow you to directly
 *     manipulate the 0s and 1s that make up numeric data types.
 * 2.  A BITMASK is a value (often a constant) used to target specific bits in another
 *     variable for setting, clearing, or checking.
 * 3.  The common patterns for bitmasking are:
 *     - Set bit:      `flags = flags | MASK;`
 *     - Clear bit:    `flags = flags & ~MASK;`
 *     - Check bit:    `if (flags & MASK) { ... }`
 *     - Toggle bit:   `flags = flags ^ MASK;`
 * 4.  Shift operators provide a very fast way to perform multiplication and
 *     division by powers of two.
 *
 * This is a low-level but essential skill for any serious C programmer.
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * 1. Open a terminal or command prompt.
 * 2. Navigate to the directory where you saved this file.
 * 3. Use the GCC compiler to create an executable file:
 *    `gcc -Wall -Wextra -std=c11 -o 21_bit_manipulation 21_bit_manipulation.c`
 * 4. Run the executable:
 *    - On Linux/macOS:   `./21_bit_manipulation`
 *    - On Windows:       `21_bit_manipulation.exe`
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 21_bit_manipulation 21_bit_manipulation.c
./21_bit_manipulation

Preprocessor Directives

Before your C code is ever seen by the actual compiler, it goes through a first pass by a program called the C PREPROCESSOR. The preprocessor’s job is simple: it’s a text-replacement tool that scans your code for lines starting with a hash symbol (#) and acts on them. These are called PREPROCESSOR DIRECTIVES.

You’ve been using these since day one without necessarily knowing the details. Let’s explore the most important ones.

Full Source

/**
 * @file 22_preprocessor_directives.c
 * @brief Part 3, Lesson 22: Preprocessor Directives
 * @author dunamismax
 * @date 06-15-2025
 *
 * This file serves as the lesson and demonstration for the C preprocessor.
 * It explains key directives like #include, #define, and conditional
 * compilation, which are essential for managing larger codebases.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * Before your C code is ever seen by the actual compiler, it goes through a
 * first pass by a program called the C PREPROCESSOR. The preprocessor's job is
 * simple: it's a text-replacement tool that scans your code for lines starting
 * with a hash symbol (`#`) and acts on them. These are called PREPROCESSOR DIRECTIVES.
 *
 * You've been using these since day one without necessarily knowing the details.
 * Let's explore the most important ones.
 */

// --- Part 1: `#include` Revisited ---
// You know that `#include` pastes the contents of another file into this one.
// But there's a subtle difference between the two forms:

#include <stdio.h> // `<...>` : Use for standard system libraries. The preprocessor
                   //           searches for these in a list of standard system directories.

// #include "my_header.h" // `"..."` : Use for your own header files. The preprocessor
//           searches for this file first in the CURRENT directory,
//           and then in the standard system directories.
// This is crucial for multi-file projects.

// --- Part 2: `#define` for Constants and Macros ---
// `#define` is used to create MACROS. A macro is a fragment of code that has
// been given a name. Whenever the name is used, it is replaced by the contents
// of the macro. This is direct text substitution.

// 1. Object-like Macros (Constants)
// This tells the preprocessor: "anywhere you see PI, replace it with 3.14159".
// By convention, macro constants are in ALL_CAPS.
#define PI 3.14159
#define GREETING "Hello, World!"

// 2. Function-like Macros
// These macros can take arguments.
// THE PARENTHESES RULE: When creating function-like macros, ALWAYS surround
// each argument and the ENTIRE expression with parentheses. This prevents
// operator precedence bugs after the text substitution happens.

// BAD EXAMPLE:
#define BAD_SQUARE(x) x *x
// If you call `BAD_SQUARE(2 + 3)`, it expands to `2 + 3 * 2 + 3`, which is `2 + 6 + 3 = 11`. WRONG!

// GOOD EXAMPLE (Follow this rule!):
#define SQUARE(x) ((x) * (x))
// Now, `SQUARE(2 + 3)` expands to `((2 + 3) * (2 + 3))`, which is `5 * 5 = 25`. CORRECT!

// --- Part 3: Conditional Compilation ---
// These directives allow you to include or exclude parts of your code from
// compilation based on certain conditions. This is extremely useful.

// A common use is for debugging messages. We can define a `DEBUG` macro.
#define DEBUG

int main(void)
{
    printf("--- Part 1 & 2: Using #define Macros ---\n");
    double radius = 5.0;
    double area = PI * SQUARE(radius); // Preprocessor expands this before compilation

    printf("%s\n", GREETING);
    printf("The area of a circle with radius %.2f is %.2f\n", radius, area);

    int val = 2 + 3;
    printf("The square of (2 + 3) is %d\n\n", SQUARE(val));

    printf("--- Part 3: Conditional Compilation in Action ---\n");

// This block of code will only be included if the DEBUG macro is defined.
#ifdef DEBUG
    printf("DEBUG: This is a debug message.\n");
    printf("DEBUG: The value of 'area' is %f.\n", area);
#endif

// You can also use #ifndef (if not defined).
#ifndef RELEASE_MODE
    printf("This is not a release build.\n");
#endif

// You can also check values with #if
#if (10 > 5)
    printf("#if (10 > 5) is true, so this line is compiled.\n");
#endif

    return 0;
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * Key Takeaways:
 *
 * 1.  The PREPROCESSOR runs before the compiler, performing text-based operations
 *     on your code based on directives starting with `#`.
 * 2.  `#include <...>` is for system libraries; `#include "..."` is for your own files.
 * 3.  `#define` creates macros for constants or simple functions. ALWAYS use parentheses
 *     in function-like macros to avoid bugs: `#define NAME(arg) ((arg)...)`.
 * 4.  Conditional compilation (`#ifdef`, `#ifndef`, `#if`, `#else`, `#endif`) lets you
 *     include or exclude code, which is perfect for debugging or platform-specific features.
 *
 * THE MOST IMPORTANT USE OF CONDITIONAL COMPILATION: HEADER GUARDS
 *
 * When you start creating your own `.h` files, you'll run into a "double inclusion"
 * problem. To prevent this, every header file you create MUST be wrapped in
 * a "header guard", which looks like this:
 *
 *   #ifndef MY_UNIQUE_HEADER_NAME_H
 *   #define MY_UNIQUE_HEADER_NAME_H
 *
 *   // All your header file content goes here...
 *
 *   #endif // MY_UNIQUE_HEADER_NAME_H
 *
 * This ensures that even if the file is `#include`d ten times, its content is
 * only processed by the compiler ONCE. We will use this in future lessons.
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * 1. Open a terminal or command prompt.
 * 2. Navigate to the directory where you saved this file.
 * 3. Use the GCC compiler to create an executable file:
 *    `gcc -Wall -Wextra -std=c11 -o 22_preprocessor_directives 22_preprocessor_directives.c`
 * 4. Run the executable:
 *    - On Linux/macOS:   `./22_preprocessor_directives`
 *    - On Windows:       `22_preprocessor_directives.exe`
 *
 * To see conditional compilation work, try commenting out `#define DEBUG` at the
 * top, then recompile and run. The debug messages will disappear!
 *
 * Alternatively, you can define macros from the command line with the -D flag.
 * Remove `#define DEBUG` from the file, then compile like this:
 * `gcc -Wall -Wextra -std=c11 -DDEBUG -o 22_preprocessor_directives 22_preprocessor_directives.c`
 * The debug messages will reappear!
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 22_preprocessor_directives 22_preprocessor_directives.c
./22_preprocessor_directives

Unions and Enums

Today we’re looking at two special kinds of data types: enum and union. While they look a bit like structs, they solve very different problems.

  • ENUM (Enumeration): Its purpose is to make your code more READABLE and SAFER. An enum creates a set of named integer constants. Instead of using “magic numbers” like 0, 1, 2 to represent states or types, you can use meaningful names like SUCCESS, PENDING, FAILURE.

  • UNION: Its purpose is MEMORY EFFICIENCY. A union allows you to store different data types in the SAME memory location, but only one at a time. While a struct allocates space for ALL its members, a union only allocates enough space for its LARGEST member.

Full Source

/**
 * @file 23_unions_and_enums.c
 * @brief Part 3, Lesson 23: Unions and Enums
 * @author dunamismax
 * @date 06-15-2025
 *
 * This file serves as the lesson and demonstration for enums and unions.
 * It explains how these specialized data types are used for creating readable
 * constants and for memory-efficient data storage.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * Today we're looking at two special kinds of data types: `enum` and `union`.
 * While they look a bit like `structs`, they solve very different problems.
 *
 * - ENUM (Enumeration):
 *   Its purpose is to make your code more READABLE and SAFER. An `enum` creates a
 *   set of named integer constants. Instead of using "magic numbers" like 0, 1, 2
 *   to represent states or types, you can use meaningful names like `SUCCESS`,
 *   `PENDING`, `FAILURE`.
 *
 * - UNION:
 *   Its purpose is MEMORY EFFICIENCY. A `union` allows you to store different
 *   data types in the SAME memory location, but only one at a time. While a
 *   `struct` allocates space for ALL its members, a `union` only allocates
 *   enough space for its LARGEST member.
 */

#include <stdio.h>
#include <stdint.h> // For fixed-width integers like uint8_t

// --- Part 1: Enums for Readable Code ---
// Let's define an enumeration for different status types in a system.
typedef enum
{
    STATE_PENDING,    // Automatically assigned the value 0
    STATE_PROCESSING, // Automatically assigned the value 1
    STATE_SUCCESS,    // Automatically assigned the value 2
    STATE_FAILURE     // Automatically assigned the value 3
} SystemState;

// --- Part 2: Unions for Memory Efficiency ---
// This union can hold ONE of three types, but not at the same time.
typedef union
{
    int i;
    double d;
    char c;
} DataValue;

// --- Part 3: The Tagged Union - The Best of Both Worlds ---
// A problem with unions is that you don't know WHICH member is currently valid.
// The solution is a TAGGED UNION. We wrap the union in a struct that also
// contains an enum "tag" to tell us what kind of data is currently stored.

typedef enum
{
    TYPE_INT,
    TYPE_DOUBLE,
    TYPE_CHAR
} DataType;

typedef struct
{
    DataType type;   // The tag that tells us what's in the union
    DataValue value; // The union holding the actual data
} TaggedValue;

// A helper function to print our tagged union safely.
void print_tagged_value(TaggedValue tv)
{
    switch (tv.type)
    {
    case TYPE_INT:
        printf("TaggedValue is an INT: %d\n", tv.value.i);
        break;
    case TYPE_DOUBLE:
        printf("TaggedValue is a DOUBLE: %f\n", tv.value.d);
        break;
    case TYPE_CHAR:
        printf("TaggedValue is a CHAR: '%c'\n", tv.value.c);
        break;
    default:
        printf("TaggedValue has an unknown type.\n");
    }
}

int main(void)
{
    printf("--- Part 1: Using Enums ---\n");
    SystemState current_state = STATE_PROCESSING;

    // The code is much more readable than `if (current_state == 1)`
    if (current_state == STATE_PROCESSING)
    {
        printf("Current system state is: PROCESSING\n\n");
    }

    printf("--- Part 2: Using Unions ---\n");
    DataValue data;

    printf("Size of the DataValue union: %zu bytes\n", sizeof(DataValue));
    printf("Size of a double on this system: %zu bytes\n", sizeof(double));
    printf("The union's size matches its largest member!\n\n");

    // Store an int in the union
    data.i = 123;
    printf("Stored an int: data.i = %d\n", data.i);

    // Now store a double. THIS OVERWRITES THE INT'S MEMORY.
    data.d = 987.654;
    printf("Stored a double: data.d = %f\n", data.d);

    // The integer value is now corrupted because the same memory was reused.
    printf("The int value is now garbage: data.i = %d\n\n", data.i);

    printf("--- Part 3: Using Tagged Unions ---\n");

    // Create a tagged value to hold an integer
    TaggedValue int_val;
    int_val.type = TYPE_INT;
    int_val.value.i = 42;
    print_tagged_value(int_val);

    // Create another tagged value to hold a double
    TaggedValue double_val;
    double_val.type = TYPE_DOUBLE;
    double_val.value.d = 3.14159;
    print_tagged_value(double_val);

    return 0;
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * Key Takeaways:
 *
 * 1.  ENUMS create named integer constants to make your code more readable and
 *     less error-prone than using "magic numbers." They are perfect for `switch`
 *     statements or representing a fixed set of states.
 *
 * 2.  UNIONS allow multiple members to share the same memory location. This saves
 *     memory when you know you only need to store one of several possible data
 *     types at any given time.
 *
 * 3.  The most powerful pattern is the TAGGED UNION, where a `struct` contains
 *     both a `union` and an `enum` tag. The tag tells you which field of the
 *     union is currently active, allowing you to use it safely.
 *
 * Understanding when to use these specialized types is a mark of a proficient
 * C programmer.
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * 1. Open a terminal or command prompt.
 * 2. Navigate to the directory where you saved this file.
 * 3. Use the GCC compiler to create an executable file:
 *    `gcc -Wall -Wextra -std=c11 -o 23_unions_and_enums 23_unions_and_enums.c`
 * 4. Run the executable:
 *    - On Linux/macOS:   `./23_unions_and_enums`
 *    - On Windows:       `23_unions_and_enums.exe`
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 23_unions_and_enums 23_unions_and_enums.c
./23_unions_and_enums

Static and Extern Variables

As programs grow, we need more control over our variables and functions. We need to answer two key questions:

  1. LIFETIME: How long does a variable exist in memory?
  2. LINKAGE (VISIBILITY): Which parts of our code are allowed to see it?

The static and extern keywords are our tools for managing this.

static - The “Private” Keyword The static keyword has two different meanings depending on where you use it, but both are related to making things more “private” or “persistent”.

extern - The “Public Declaration” Keyword The extern keyword is used to DECLARE a global variable that is DEFINED in another file. It tells the compiler, “Don’t worry, this variable exists somewhere else. The linker will find it and connect everything.”

Full Source

/**
 * @file 24_static_and_extern_variables.c
 * @brief Part 3, Lesson 24: Static and Extern Variables
 * @author dunamismax
 * @date 06-15-2025
 *
 * This file serves as the lesson and demonstration for storage classes,
 * specifically the `static` and `extern` keywords. It explains how these
 * control a variable's lifetime, scope, and linkage across files.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * As programs grow, we need more control over our variables and functions. We need
 * to answer two key questions:
 *
 * 1. LIFETIME: How long does a variable exist in memory?
 * 2. LINKAGE (VISIBILITY): Which parts of our code are allowed to see it?
 *
 * The `static` and `extern` keywords are our tools for managing this.
 *
 * `static` - The "Private" Keyword
 * The `static` keyword has two different meanings depending on where you use it,
 * but both are related to making things more "private" or "persistent".
 *
 * `extern` - The "Public Declaration" Keyword
 * The `extern` keyword is used to DECLARE a global variable that is DEFINED in
 * another file. It tells the compiler, "Don't worry, this variable exists somewhere
 * else. The linker will find it and connect everything."
 */

#include <stdio.h>

// --- Part 1: `static` Inside a Function (Controlling LIFETIME) ---
// When used inside a function, `static` changes a variable's storage duration.
// - A normal (auto) local variable is created on the stack when the function is
//   called and destroyed when the function exits.
// - A `static` local variable is created only once and exists for the ENTIRE
//   lifetime of the program. It RETAINS ITS VALUE between function calls.

void regular_counter(void)
{
    int count = 0; // This variable is recreated and reset to 0 every time.
    count++;
    printf("Regular counter is at: %d\n", count);
}

void static_counter(void)
{
    // This variable is initialized to 0 only the VERY FIRST time.
    // In all subsequent calls, it keeps its previous value.
    static int count = 0;
    count++;
    printf("Static counter is at: %d\n", count);
}

// --- Part 2: `static` at File Scope (Controlling LINKAGE) ---
// When used on a global variable or a function, `static` changes its linkage
// from "external" to "internal".
// - An EXTERNAL linkage variable (the default) can be seen and used by other .c files.
// - An INTERNAL linkage variable is private to this file. No other .c file can see it.
//   This is a key tool for ENCAPSULATION, or hiding implementation details.

int g_public_variable = 100; // EXTERNAL linkage. Visible to other files.

static int g_private_variable = 42; // INTERNAL linkage. Private to this file.

// This also applies to functions.
static void private_helper_function(void)
{
    printf("This is a private helper function. It cannot be called from other files.\n");
}

// --- Part 3: `extern` for Sharing Globals ---
// Imagine we have a variable defined in another file, `globals.c`:
//
//   // In globals.c:
//   int g_shared_counter = 500;
//
// To use `g_shared_counter` in this file, we must DECLARE it with `extern`.
// This tells the compiler that we intend to use it, but the actual variable
// is defined elsewhere. For this single-file example, we will define it at
// the bottom of this file to show how the declaration works.

extern int g_late_defined_variable; // A DECLARATION, not a definition.

int main(void)
{
    printf("--- Part 1: `static` for Variable Lifetime ---\n");
    printf("Calling counters three times:\n");
    regular_counter();
    static_counter();
    printf("---\n");
    regular_counter();
    static_counter();
    printf("---\n");
    regular_counter();
    static_counter();
    printf("\n");

    printf("--- Part 2: `static` for File-Scope Linkage ---\n");
    printf("The public global variable is: %d\n", g_public_variable);
    printf("The private static variable is: %d\n", g_private_variable);
    private_helper_function();
    printf("\n");

    printf("--- Part 3: Using an `extern` Variable ---\n");
    printf("The late-defined variable declared with 'extern' is: %d\n", g_late_defined_variable);

    return 0;
}

// This is the DEFINITION for the `extern` variable declared earlier.
// Because the `main` function has already seen the `extern` declaration, this works.
// In a real project, this definition would be in a separate .c file.
int g_late_defined_variable = 999;

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * Key Takeaways:
 *
 * 1. The `static` keyword has two meanings based on context:
 *    - INSIDE a function: Creates a variable that persists for the program's
 *      entire lifetime and retains its value between calls.
 *    - OUTSIDE a function: Creates a global variable or function that is private
 *      to the current file (internal linkage).
 *
 * 2. By default, global variables and functions have EXTERNAL linkage, meaning
 *    they can be shared across multiple .c files.
 *
 * 3. The `extern` keyword is used to DECLARE a global variable that is DEFINED
 *    in another file, allowing you to share variables across your project.
 *
 * These concepts are the bedrock of creating well-structured, multi-file C projects.
 * You use `static` to hide details within a file and `extern` (usually via a header file)
 * to expose the public parts.
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * 1. Open a terminal or command prompt.
 * 2. Navigate to the directory where you saved this file.
 * 3. Use the GCC compiler to create an executable file:
 *    `gcc -Wall -Wextra -std=c11 -o 24_static_and_extern_variables 24_static_and_extern_variables.c`
 * 4. Run the executable:
 *    - On Linux/macOS:   `./24_static_and_extern_variables`
 *    - On Windows:       `24_static_and_extern_variables.exe`
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 24_static_and_extern_variables 24_static_and_extern_variables.c
./24_static_and_extern_variables

Simple Text Editor

Welcome to your Part 3 capstone project! This is where we put everything together to build a substantial application: a command-line text editor.

Our editor, which we’ll call sled (Simple Line Editor), will be able to open a file, display its contents, add/delete lines, and save the changes.

THE CORE DATA STRUCTURE: The heart of our editor is a DOUBLY-LINKED LIST. This is an enhancement of the linked list from lesson 20. In a doubly-linked list, each node points not only to the NEXT node but also to the PREVIOUS one. This makes operations like inserting and deleting in the middle of the list much more efficient.

Each node in our list will represent one line of text in the file.

SKILLS INTEGRATED:

  • Structs: To define the Line node for our list.
  • Pointers: Extensively used for linking nodes (next, prev) and managing data.
  • Dynamic Memory (malloc/free): To create and destroy lines as needed.
  • File I/O (fopen, fgets, fprintf): To load and save the file.
  • Command-Line Arguments (argc, argv): To specify which file to edit.
  • Functions: To create a clean, modular, and readable program structure.

Full Source

/**
 * @file 25_simple_text_editor.c
 * @brief Part 3, Capstone Project: Simple Text Editor
 * @author dunamismax
 * @date 06-15-2025
 *
 * This capstone project builds a functional, line-based text editor.
 * It integrates many concepts from the course: doubly-linked lists, dynamic
 * memory allocation, file I/O, command-line arguments, and string manipulation.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * Welcome to your Part 3 capstone project! This is where we put everything
 * together to build a substantial application: a command-line text editor.
 *
 * Our editor, which we'll call `sled` (Simple Line Editor), will be able to
 * open a file, display its contents, add/delete lines, and save the changes.
 *
 * THE CORE DATA STRUCTURE:
 * The heart of our editor is a DOUBLY-LINKED LIST. This is an enhancement of the
 * linked list from lesson 20. In a doubly-linked list, each node points not
 * only to the NEXT node but also to the PREVIOUS one. This makes operations like
 * inserting and deleting in the middle of the list much more efficient.
 *
 * Each node in our list will represent one line of text in the file.
 *
 * SKILLS INTEGRATED:
 * - Structs: To define the `Line` node for our list.
 * - Pointers: Extensively used for linking nodes (`next`, `prev`) and managing data.
 * - Dynamic Memory (`malloc`/`free`): To create and destroy lines as needed.
 * - File I/O (`fopen`, `fgets`, `fprintf`): To load and save the file.
 * - Command-Line Arguments (`argc`, `argv`): To specify which file to edit.
 * - Functions: To create a clean, modular, and readable program structure.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h> // For a true/false `modified` flag

// --- Data Structures and Global State ---

// The node for our doubly-linked list. Each node is one line of text.
typedef struct Line
{
    char *text;        // Dynamically allocated text of the line
    struct Line *prev; // Pointer to the previous line
    struct Line *next; // Pointer to the next line
} Line;

// Global pointers to the start and end of our list (the "text buffer")
Line *head = NULL;
Line *tail = NULL;
int line_count = 0;
bool modified = false; // Flag to track if the file has been changed

// --- Function Prototypes ---
void load_file(const char *filename);
void save_file(const char *filename);
void free_buffer(void);
void print_lines(void);
void insert_line(int line_number, const char *text);
void delete_line(int line_number);
void append_line(const char *text);
void print_help(void);

// --- Main Function: The Editor's Control Loop ---

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
        return 1;
    }

    char *filename = argv[1];
    load_file(filename);

    char input_buffer[1024];
    printf("Simple Line Editor. Type 'h' for help, 'q' to quit.\n");

    while (true)
    {
        printf("> ");
        if (fgets(input_buffer, sizeof(input_buffer), stdin) == NULL)
        {
            break; // Exit on EOF (Ctrl+D)
        }

        // Parse command and arguments
        char command = input_buffer[0];
        char *argument = input_buffer + 2;
        int line_num = atoi(argument);

        switch (command)
        {
        case 'p':
            print_lines();
            break;
        case 'a': // Append
            // Remove newline from argument
            argument[strcspn(argument, "\n")] = 0;
            append_line(argument);
            break;
        case 'i': // Insert
        {
            char *text_start = strchr(argument, ' ');
            if (text_start)
            {
                *text_start = '\0'; // Split line number from text
                text_start++;
                text_start[strcspn(text_start, "\n")] = 0;
                insert_line(atoi(argument), text_start);
            }
            else
            {
                printf("Usage: i <line_number> <text>\n");
            }
            break;
        }
        case 'd': // Delete
            delete_line(line_num);
            break;
        case 's':
            save_file(filename);
            break;
        case 'h':
            print_help();
            break;
        case 'q':
            if (modified)
            {
                printf("File has unsaved changes. Save first? (y/n): ");
                if (fgets(input_buffer, sizeof(input_buffer), stdin) != NULL &&
                    (input_buffer[0] == 'y' || input_buffer[0] == 'Y'))
                {
                    save_file(filename);
                }
            }
            printf("Exiting.\n");
            free_buffer();
            return 0;
        default:
            printf("Unknown command. Type 'h' for help.\n");
        }
    }

    free_buffer();
    return 0;
}

// --- Function Implementations ---

/**
 * @brief Prints the help menu.
 */
void print_help(void)
{
    printf("--- sled Help ---\n");
    printf("p              - Print all lines\n");
    printf("a <text>       - Append a new line of text to the end\n");
    printf("i <num> <text> - Insert text before line <num>\n");
    printf("d <num>        - Delete line <num>\n");
    printf("s              - Save the file\n");
    printf("h              - Show this help message\n");
    printf("q              - Quit the editor\n");
}

/**
 * @brief Allocates a new line node.
 */
Line *create_line(const char *text)
{
    Line *new_line = (Line *)malloc(sizeof(Line));
    if (!new_line)
    {
        perror("malloc failed for new line");
        exit(1);
    }
    // Allocate space for the text and copy it over.
    new_line->text = malloc(strlen(text) + 1);
    if (!new_line->text)
    {
        perror("malloc failed for line text");
        exit(1);
    }
    strcpy(new_line->text, text);
    new_line->prev = NULL;
    new_line->next = NULL;
    return new_line;
}

/**
 * @brief Appends a new line to the end of the text buffer.
 */
void append_line(const char *text)
{
    Line *new_line = create_line(text);
    if (tail == NULL)
    { // List is empty
        head = tail = new_line;
    }
    else
    {
        tail->next = new_line;
        new_line->prev = tail;
        tail = new_line;
    }
    line_count++;
    modified = true;
}

/**
 * @brief Loads the content of a file into the linked-list buffer.
 */
void load_file(const char *filename)
{
    FILE *file = fopen(filename, "r");
    if (file == NULL)
    {
        printf("New file: '%s'\n", filename);
        return; // It's okay if the file doesn't exist yet
    }

    char buffer[1024];
    while (fgets(buffer, sizeof(buffer), file))
    {
        buffer[strcspn(buffer, "\n")] = 0; // Remove trailing newline
        append_line(buffer);
    }
    fclose(file);
    modified = false; // Freshly loaded, so not modified yet
}

/**
 * @brief Saves the current buffer content to the file.
 */
void save_file(const char *filename)
{
    FILE *file = fopen(filename, "w");
    if (file == NULL)
    {
        perror("Could not open file for writing");
        return;
    }

    Line *current = head;
    while (current != NULL)
    {
        fprintf(file, "%s\n", current->text);
        current = current->next;
    }
    fclose(file);
    printf("File '%s' saved.\n", filename);
    modified = false;
}

/**
 * @brief Frees all memory used by the text buffer.
 */
void free_buffer(void)
{
    Line *current = head;
    while (current != NULL)
    {
        Line *next = current->next;
        free(current->text); // Free the text string first
        free(current);       // Then free the node itself
        current = next;
    }
    head = tail = NULL;
    line_count = 0;
}

/**
 * @brief Prints all lines with line numbers.
 */
void print_lines(void)
{
    Line *current = head;
    int i = 1;
    while (current != NULL)
    {
        printf("%4d: %s\n", i++, current->text);
        current = current->next;
    }
}

/**
 * @brief Inserts a line at a specific position.
 */
void insert_line(int line_number, const char *text)
{
    if (line_number < 1 || line_number > line_count + 1)
    {
        printf("Error: Invalid line number.\n");
        return;
    }
    if (line_number == line_count + 1)
    {
        append_line(text);
        return;
    }

    Line *new_line = create_line(text);
    Line *current = head;
    for (int i = 1; i < line_number; i++)
    {
        current = current->next;
    }

    // Re-wire the pointers
    new_line->next = current;
    new_line->prev = current->prev;

    if (current->prev == NULL)
    { // Inserting at the head
        head = new_line;
    }
    else
    {
        current->prev->next = new_line;
    }
    current->prev = new_line;

    line_count++;
    modified = true;
}

/**
 * @brief Deletes a line at a specific position.
 */
void delete_line(int line_number)
{
    if (line_number < 1 || line_number > line_count)
    {
        printf("Error: Invalid line number.\n");
        return;
    }

    Line *to_delete = head;
    for (int i = 1; i < line_number; i++)
    {
        to_delete = to_delete->next;
    }

    // Bypass the node to be deleted
    if (to_delete->prev)
        to_delete->prev->next = to_delete->next;
    else
        head = to_delete->next; // Deleting the head

    if (to_delete->next)
        to_delete->next->prev = to_delete->prev;
    else
        tail = to_delete->prev; // Deleting the tail

    free(to_delete->text);
    free(to_delete);
    line_count--;
    modified = true;
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * Congratulations! You have built a complex, useful application from the ground up.
 * This project required careful management of memory, pointers, and program state,
 * exercising nearly every major C concept we have learned.
 *
 * Key Project Achievements:
 * - A DOUBLY-LINKED LIST implementation for efficient line editing.
 * - Robust FILE I/O for data persistence.
 * - A clean COMMAND-LINE INTERFACE for user interaction.
 * - Careful DYNAMIC MEMORY MANAGEMENT to prevent leaks.
 * - A modular structure that separates concerns into different functions.
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * 1. Open a terminal or command prompt.
 * 2. Navigate to the directory where you saved this file.
 * 3. Use the GCC compiler to create an executable file:
 *    `gcc -Wall -Wextra -std=c11 -o 25_simple_text_editor 25_simple_text_editor.c`
 *
 * 4. Run the executable with a file name:
 *    - On Linux/macOS:   `./25_simple_text_editor my_document.txt`
 *    - On Windows:       `25_simple_text_editor.exe my_document.txt`
 *
 * Once inside, try the commands:
 * > a This is the first line.
 * > a This is the second.
 * > p
 * > i 2 This line goes in the middle.
 * > p
 * > d 1
 * > p
 * > s
 * > q
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 25_simple_text_editor 25_simple_text_editor.c
./25_simple_text_editor

Simple Socket Server and Client

The Client

Welcome to the world of network programming! In this lesson and the next, we will build a complete, working client-server application. This is your gateway to understanding how programs communicate over the internet.

THE CLIENT-SERVER MODEL Most network communication follows a CLIENT-SERVER model.

  • A SERVER is a program that runs continuously, LISTENS for incoming connections on a specific port, and provides a service.
  • A CLIENT is a program that actively initiates a connection to a server to request that service.

Think of a web browser (client) connecting to a web server (like google.com) to request a webpage. This file is the CLIENT.

WHAT IS A SOCKET? A SOCKET is an endpoint for communication between two programs over a network. You can think of it like a telephone for your program. You create a socket, ‘dial’ the server’s address and port, and then you can send and receive data through it.

We will be using TCP/IP sockets (specifically, AF_INET and SOCK_STREAM), which provide a reliable, connection-oriented communication channel. This means data arrives in order and without errors, like a phone call.

THE CLIENT’S JOURNEY A typical client program follows these steps:

  1. Create a SOCKET.
  2. Specify the server’s IP address and PORT number.
  3. CONNECT to the server.
  4. SEND data (a message).
  5. RECEIVE a response.
  6. CLOSE the connection.

Let’s build it!

The socket() function creates a communication endpoint and returns a file descriptor for it.

ARGUMENTS:

  1. domain: AF_INET specifies the IPv4 protocol family.
  2. type: SOCK_STREAM specifies a TCP socket (reliable, connection-oriented).
  3. protocol: 0 tells the OS to choose the proper protocol (TCP for SOCK_STREAM).

struct sockaddr_in is a structure used to store addresses for the AF_INET family.

  • sin_family: The address family. Must match the one used in socket().
  • sin_port: The port number. We must use htons() (Host TO Network Short) to convert the port number into NETWORK BYTE ORDER. This ensures that computers with different byte ordering can communicate correctly.
  • sin_addr.s_addr: The IP address. inet_addr() converts the string IP (e.g., “127.0.0.1”) into the correct binary format in network byte order.

The connect() function establishes a connection to the server. It’s a BLOCKING call; it will wait until the connection is made or an error occurs.

ARGUMENTS:

  1. The client’s socket descriptor.
  2. A pointer to the server’s address struct. Note the cast to (struct sockaddr *). This is a C convention for generic socket functions.
  3. The size of the address structure.

The send() function transmits data to the connected socket. It returns the number of bytes sent, or -1 on error.

The recv() function receives data from a socket. It’s a BLOCKING call; the program will pause here until data arrives. It returns the number of bytes received, 0 if the connection was closed, or -1 on error.

Client Source

/**
 * @file 26_simple_socket_client.c
 * @brief Part 4, Lesson 26: Simple Socket Client
 * @author dunamismax
 * @date 06-15-2025
 *
 * This file implements the client side of our basic TCP client-server application.
 * This program will connect to a running server, send it a message, and
 * print the server's response to the console.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * Welcome to the world of network programming! In this lesson and the next, we
 * will build a complete, working client-server application. This is your gateway
 * to understanding how programs communicate over the internet.
 *
 * THE CLIENT-SERVER MODEL
 * Most network communication follows a CLIENT-SERVER model.
 * - A SERVER is a program that runs continuously, LISTENS for incoming
 *   connections on a specific port, and provides a service.
 * - A CLIENT is a program that actively initiates a connection to a server to
 *   request that service.
 *
 * Think of a web browser (client) connecting to a web server (like google.com)
 * to request a webpage. This file is the CLIENT.
 *
 * WHAT IS A SOCKET?
 * A SOCKET is an endpoint for communication between two programs over a network.
 * You can think of it like a telephone for your program. You create a socket,
 * 'dial' the server's address and port, and then you can send and receive
 * data through it.
 *
 * We will be using TCP/IP sockets (specifically, AF_INET and SOCK_STREAM), which
 * provide a reliable, connection-oriented communication channel. This means data
 * arrives in order and without errors, like a phone call.
 *
 * THE CLIENT'S JOURNEY
 * A typical client program follows these steps:
 * 1. Create a SOCKET.
 * 2. Specify the server's IP address and PORT number.
 * 3. CONNECT to the server.
 * 4. SEND data (a message).
 * 5. RECEIVE a response.
 * 6. CLOSE the connection.
 *
 * Let's build it!
 */

// --- Required Headers ---
// We need several headers for network programming.
#include <stdio.h>      // For standard I/O, like printf() and perror()
#include <stdlib.h>     // For exit() and atoi()
#include <string.h>     // For string manipulation, like strlen() and memset()
#include <unistd.h>     // For close()
#include <sys/socket.h> // The main header for socket programming functions
#include <arpa/inet.h>  // For functions like inet_addr() and htons()

// This is the function signature we use when we want to accept command-line arguments.
int main(int argc, char *argv[])
{
    // --- Step 0: Validate Command-Line Arguments ---
    // Our client needs to know where the server is and what message to send.
    // We expect: ./program_name <SERVER_IP> <PORT> <MESSAGE>
    if (argc != 4)
    {
        // `fprintf` is like `printf`, but it lets us specify the output stream.
        // `stderr` is the "standard error" stream, the conventional place for errors.
        fprintf(stderr, "Usage: %s <Server IP> <Port> <Message>\n", argv[0]);
        return 1; // Exit with a non-zero status to indicate an error.
    }

    // Parse the arguments from the command line.
    char *server_ip = argv[1];
    int port = atoi(argv[2]); // `atoi` converts a string to an integer.
    char *message = argv[3];

    // --- Part 1: Create a Socket ---

    // A SOCKET DESCRIPTOR is an integer that uniquely identifies a socket, much like
    // a FILE DESCRIPTOR identifies an open file.
    int client_socket;

    /*
     * The `socket()` function creates a communication endpoint and returns a
     * file descriptor for it.
     *
     * ARGUMENTS:
     * 1. domain: AF_INET specifies the IPv4 protocol family.
     * 2. type:   SOCK_STREAM specifies a TCP socket (reliable, connection-oriented).
     * 3. protocol: 0 tells the OS to choose the proper protocol (TCP for SOCK_STREAM).
     */
    printf("Creating client socket...\n");
    client_socket = socket(AF_INET, SOCK_STREAM, 0);

    // `socket()` returns -1 on failure. `perror` prints a descriptive system error.
    if (client_socket == -1)
    {
        perror("Could not create socket");
        return 1;
    }
    printf("Socket created.\n");

    // --- Part 2: Configure Server Address ---

    // We need a structure to hold the server's address information.
    struct sockaddr_in server_addr;

    /*
     * `struct sockaddr_in` is a structure used to store addresses for the AF_INET family.
     *
     * - sin_family: The address family. Must match the one used in `socket()`.
     * - sin_port: The port number. We must use `htons()` (Host TO Network Short)
     *   to convert the port number into NETWORK BYTE ORDER. This ensures that
     *   computers with different byte ordering can communicate correctly.
     * - sin_addr.s_addr: The IP address. `inet_addr()` converts the string IP
     *   (e.g., "127.0.0.1") into the correct binary format in network byte order.
     */
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(port);
    server_addr.sin_addr.s_addr = inet_addr(server_ip);

    // --- Part 3: Connect to the Server ---

    /*
     * The `connect()` function establishes a connection to the server.
     * It's a BLOCKING call; it will wait until the connection is made or an
     * error occurs.
     *
     * ARGUMENTS:
     * 1. The client's socket descriptor.
     * 2. A pointer to the server's address struct. Note the cast to
     *    `(struct sockaddr *)`. This is a C convention for generic socket functions.
     * 3. The size of the address structure.
     */
    printf("Connecting to server at %s:%d...\n", server_ip, port);
    if (connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
    {
        perror("Connection failed");
        close(client_socket);
        return 1;
    }
    printf("Connected to server.\n");

    // --- Part 4: Send and Receive Data ---

    // Now that we're connected, we can send our message.
    /*
     * The `send()` function transmits data to the connected socket.
     * It returns the number of bytes sent, or -1 on error.
     */
    printf("Sending message: \"%s\"\n", message);
    if (send(client_socket, message, strlen(message), 0) < 0)
    {
        perror("Send failed");
        close(client_socket);
        return 1;
    }

    // Now we wait for the server's reply.
    char server_reply[2000];
    int recv_size;

    // It's good practice to clear the buffer before receiving data into it.
    memset(server_reply, 0, sizeof(server_reply));

    /*
     * The `recv()` function receives data from a socket.
     * It's a BLOCKING call; the program will pause here until data arrives.
     * It returns the number of bytes received, 0 if the connection was closed,
     * or -1 on error.
     */
    recv_size = recv(client_socket, server_reply, 2000, 0);
    if (recv_size < 0)
    {
        perror("Receive failed");
        close(client_socket);
        return 1;
    }

    printf("Server reply: %s\n", server_reply);

    // --- Part 5: Close the Socket ---

    // Always clean up! Closing the socket releases the resources.
    close(client_socket);
    printf("Connection closed.\n");

    return 0;
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * This is the CLIENT part of a two-part application. You must compile and run
 * the server first.
 *
 * 1. Open a terminal and compile the client:
 *    `gcc -Wall -Wextra -std=c11 -o 26_simple_socket_client 26_simple_socket_client.c`
 *
 * 2. In a DIFFERENT terminal, compile and run the server (which we will build next):
 *    `gcc -Wall -Wextra -std=c11 -o 26_simple_socket_server 26_simple_socket_server.c`
 *    `./26_simple_socket_server 8888`
 *
 * 3. Go back to the FIRST terminal (for the client) and run it, providing the
 *    server's IP, port, and a message. If the server is on the same machine,
 *    the IP is 127.0.0.1 (localhost).
 *
 *    `./26_simple_socket_client 127.0.0.1 8888 "Hello from the C client!"`
 *
 *    You should see the client connect, send the message, and then print the
 *    server's reply. The server's terminal will show the message it received.
 */

The Server

This is the SERVER component of our application. A server’s primary role is to listen for and accept connections from clients.

While the client actively initiates contact, the server is passive. It sets up a ‘shop’ at a known address (IP and PORT) and waits for customers (clients) to arrive.

THE SERVER’S JOURNEY A typical TCP server program follows these steps, which are different from the client’s:

  1. Create a SOCKET. (Same as the client)
  2. BIND the socket to a specific IP address and port number. This is how clients will find it.
  3. LISTEN on that port for incoming connection requests. This puts the socket into a “listening state.”
  4. ACCEPT a connection from a client. This creates a NEW socket dedicated to communicating with that specific client. The original socket remains listening for other clients.
  5. RECEIVE and SEND data with the connected client using the new socket.
  6. CLOSE the client’s connection and, eventually, the main listening socket.

Key server-specific functions are bind(), listen(), and accept().

INADDR_ANY is a special constant that tells the socket to bind to all available network interfaces on the machine (e.g., Wi-Fi, Ethernet, etc.). This is the standard way to configure a server so it can accept connections from both the local machine (127.0.0.1) and from other machines on the network.

The bind() function assigns the address specified by server_addr to the socket descriptor server_socket. This is a critical step for a server.

The listen() function puts the server socket into a passive mode, where it waits for the client to approach the server to make a connection. The second argument (3) is the BACKLOG, which is the maximum number of pending connections that can be queued up before the server starts refusing new ones.

The accept() function is a BLOCKING call. The program will pause here and wait until a client connects. When a connection is accepted, it creates a NEW SOCKET descriptor (client_socket) for this specific communication channel. The original server_socket remains open and listening for more connections. It also fills the client_addr struct with the client’s information.

Server Source

/**
 * @file 26_simple_socket_server.c
 * @brief Part 4, Lesson 26: Simple Socket Server
 * @author dunamismax
 * @date 06-15-2025
 *
 * This file implements the server side of our basic TCP client-server application.
 * This program will wait for a client to connect, receive a message,
 * send a reply, and then shut down.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * This is the SERVER component of our application. A server's primary role is
 * to listen for and accept connections from clients.
 *
 * While the client actively initiates contact, the server is passive. It sets
 * up a 'shop' at a known address (IP and PORT) and waits for customers (clients)
 * to arrive.
 *
 * THE SERVER'S JOURNEY
 * A typical TCP server program follows these steps, which are different from
 * the client's:
 *
 * 1. Create a SOCKET. (Same as the client)
 * 2. BIND the socket to a specific IP address and port number. This is how clients
 *    will find it.
 * 3. LISTEN on that port for incoming connection requests. This puts the socket
 *    into a "listening state."
 * 4. ACCEPT a connection from a client. This creates a NEW socket dedicated to
 *    communicating with that specific client. The original socket remains
 *    listening for other clients.
 * 5. RECEIVE and SEND data with the connected client using the new socket.
 * 6. CLOSE the client's connection and, eventually, the main listening socket.
 *
 * Key server-specific functions are `bind()`, `listen()`, and `accept()`.
 */

// --- Required Headers ---
#include <arpa/inet.h>  // For address structures and functions
#include <errno.h>      // For errno during numeric parsing
#include <stdio.h>      // For standard I/O
#include <stdlib.h>     // For exit() and strtol()
#include <string.h>     // For string manipulation
#include <sys/socket.h> // The main header for socket programming
#include <unistd.h>     // For close(), write()

int main(int argc, char *argv[])
{
    // --- Step 0: Validate Command-Line Arguments ---
    // The server needs to know which port to listen on.
    if (argc != 2)
    {
        fprintf(stderr, "Usage: %s <Port>\n", argv[0]);
        return 1;
    }

    char *endptr = NULL;
    errno = 0;
    long parsed_port = strtol(argv[1], &endptr, 10);
    if (errno != 0 || endptr == argv[1] || *endptr != '\0' || parsed_port < 1 || parsed_port > 65535)
    {
        fprintf(stderr, "Error: Port must be a whole number between 1 and 65535.\n");
        return 1;
    }
    int port = (int)parsed_port;

    // --- Part 1: Create the Server Socket ---

    int server_socket, client_socket;

    // Create the socket. Same as the client.
    server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket == -1)
    {
        perror("Could not create server socket");
        return 1;
    }
    printf("Server socket created.\n");

    // --- Part 2: Bind the Socket to an IP and Port ---

    struct sockaddr_in server_addr, client_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    memset(&client_addr, 0, sizeof(client_addr));

    // Prepare the sockaddr_in structure for the server.
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(port); // The port to listen on.

    /*
     * INADDR_ANY is a special constant that tells the socket to bind to all
     * available network interfaces on the machine (e.g., Wi-Fi, Ethernet, etc.).
     * This is the standard way to configure a server so it can accept connections
     * from both the local machine (127.0.0.1) and from other machines on the
     * network.
     */
    server_addr.sin_addr.s_addr = INADDR_ANY;

    /*
     * The `bind()` function assigns the address specified by `server_addr` to
     * the socket descriptor `server_socket`. This is a critical step for a server.
     */
    printf("Binding socket to port %d...\n", port);
    if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
    {
        perror("Bind failed");
        close(server_socket);
        return 1;
    }
    printf("Bind successful.\n");

    // --- Part 3: Listen for Connections ---

    /*
     * The `listen()` function puts the server socket into a passive mode, where
     * it waits for the client to approach the server to make a connection.
     * The second argument (3) is the BACKLOG, which is the maximum number of
     * pending connections that can be queued up before the server starts
     * refusing new ones.
     */
    if (listen(server_socket, 3) < 0)
    {
        perror("Listen failed");
        close(server_socket);
        return 1;
    }
    printf("Server listening on port %d...\n", port);
    printf("Waiting for incoming connections...\n");

    // --- Part 4: Accept Incoming Connections ---

    socklen_t client_addr_size = sizeof(client_addr);

    /*
     * The `accept()` function is a BLOCKING call. The program will pause here
     * and wait until a client connects.
     * When a connection is accepted, it creates a NEW SOCKET descriptor
     * (`client_socket`) for this specific communication channel.
     * The original `server_socket` remains open and listening for more connections.
     * It also fills the `client_addr` struct with the client's information.
     */
    client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_addr_size);

    if (client_socket < 0)
    {
        perror("Accept failed");
        close(server_socket);
        return 1;
    }

    // `inet_ntoa` converts the client's network address into a human-readable string.
    printf("Connection accepted from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

    // --- Part 5: Communicate with the Client ---

    char client_message[2000];
    int read_size;

    // Clear the buffer before reading into it.
    memset(client_message, 0, sizeof(client_message));

    // Receive a message from the client using the NEW socket descriptor.
    // `recv` is also a BLOCKING call.
    read_size = recv(client_socket, client_message, 2000, 0);
    if (read_size > 0)
    {
        printf("Client message: %s\n", client_message);

        // Prepare and send a reply back to the client.
        const char *reply_message = "Message received. Thank you!";
        // `write()` is another function for sending data, similar to `send()`.
        write(client_socket, reply_message, strlen(reply_message));
        printf("Reply sent to client.\n");
    }
    else if (read_size == 0)
    {
        printf("Client disconnected.\n");
    }
    else
    {
        perror("Receive failed");
    }

    // --- Part 6: Close the Sockets ---

    // This simple server handles one client and then shuts down.
    // A real-world server would loop back to `accept()` to handle more clients.
    // We must close BOTH the client-specific socket and the main server socket.
    close(client_socket);
    close(server_socket);
    printf("Sockets closed. Server shutting down.\n");

    return 0;
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * This is the SERVER. It must be running BEFORE you run the client.
 *
 * 1. Open a terminal and compile the server:
 *    `gcc -Wall -Wextra -std=c11 -o 26_simple_socket_server 26_simple_socket_server.c`
 *
 * 2. Run the server, providing a port number for it to listen on.
 *    A common choice for testing is a high-numbered port like 8888.
 *    `./26_simple_socket_server 8888`
 *
 *    The server will start and print "Waiting for incoming connections...".
 *    It is now paused, waiting for a client.
 *
 * 3. Open a SECOND terminal and run the compiled client program as described
 *    in the client's source file.
 *
 *    `./26_simple_socket_client 127.0.0.1 8888 "This is a test!"`
 *
 *    You will see the output in both terminals as they communicate.
 */

How to Compile and Run

Build both programs:

cc -Wall -Wextra -std=c11 -o socket_server 26_simple_socket_server.c
cc -Wall -Wextra -std=c11 -o socket_client 26_simple_socket_client.c

Run the server in one terminal:

./socket_server 8080

Connect with the client in another terminal:

./socket_client 127.0.0.1 8080

Build Your Own grep

PROJECT: BUILD YOUR OWN grep

grep (which stands for Global Regular Expression Print) is one of the most famous and widely used command-line utilities in the Unix world. Its job is to search for a specific pattern of text inside files and print the lines that contain a match.

Our goal is to build a simplified version of grep. It won’t support complex regular expressions, but it will search for a fixed string within a file and print any matching lines. This is a fantastic project because it combines:

  1. COMMAND-LINE ARGUMENTS: To get the search pattern and the filename from the user.
  2. FILE I/O: To open and read the target file.
  3. STRING MANIPULATION: To search for the pattern within each line of the file.
  4. ERROR HANDLING: To manage cases where the user provides wrong input or the file doesn’t exist.

THE PLAN:

  1. Get two arguments from the command line: the search pattern and the filename.
  2. Open the filename for reading.
  3. Read the file one line at a time.
  4. For each line, check if it contains the pattern.
  5. If it does, print that line to the console.
  6. Close the file and exit.

Let’s get started!

Full Source

/**
 * @file 27_build_your_own_grep.c
 * @brief Part 4, Lesson 27: Build Your Own grep
 * @author dunamismax
 * @date 06-15-2025
 *
 * This file is a project that combines many of our previously learned skills
 * to build a simplified version of the famous `grep` command-line utility.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * PROJECT: BUILD YOUR OWN `grep`
 *
 * `grep` (which stands for Global Regular Expression Print) is one of the most
 * famous and widely used command-line utilities in the Unix world. Its job is
 * to search for a specific pattern of text inside files and print the lines
 * that contain a match.
 *
 * Our goal is to build a simplified version of `grep`. It won't support complex
 * regular expressions, but it will search for a fixed string within a file and
 * print any matching lines. This is a fantastic project because it combines:
 *
 * 1. COMMAND-LINE ARGUMENTS: To get the search pattern and the filename from the user.
 * 2. FILE I/O: To open and read the target file.
 * 3. STRING MANIPULATION: To search for the pattern within each line of the file.
 * 4. ERROR HANDLING: To manage cases where the user provides wrong input or the
 *    file doesn't exist.
 *
 * THE PLAN:
 * 1. Get two arguments from the command line: the search `pattern` and the `filename`.
 * 2. Open the `filename` for reading.
 * 3. Read the file one line at a time.
 * 4. For each line, check if it contains the `pattern`.
 * 5. If it does, print that line to the console.
 * 6. Close the file and exit.
 *
 * Let's get started!
 */

// --- Required Headers ---
#include <stdio.h>
#include <stdlib.h>
#include <string.h> // We need this for strstr()

// The main function signature for programs that accept command-line arguments.
int main(int argc, char *argv[])
{
    // --- Step 1: Validate Command-Line Arguments ---

    // `argc` is the count of arguments. We expect exactly 3:
    // argv[0]: The program name (e.g., ./27_build_your_own_grep)
    // argv[1]: The search pattern (e.g., "main")
    // argv[2]: The filename (e.g., "27_build_your_own_grep.c")
    if (argc != 3)
    {
        fprintf(stderr, "Usage: %s <pattern> <filename>\n", argv[0]);
        return 1; // Return an error code.
    }

    // Store the arguments in clearly named variables for readability.
    char *pattern = argv[1];
    char *filename = argv[2];

    // --- Step 2: Open the File ---

    // We declare a FILE POINTER. This pointer will hold the reference to our open file.
    FILE *file_pointer;

    // `fopen()` attempts to open the file.
    // The first argument is the path to the file.
    // The second argument, "r", specifies that we want to open it in READ mode.
    file_pointer = fopen(filename, "r");

    // If `fopen()` fails (e.g., the file doesn't exist), it returns NULL.
    // We must always check for this!
    if (file_pointer == NULL)
    {
        // `perror()` is a great function for printing file-related errors.
        // It prints your custom message, followed by a colon, and then the
        // system's human-readable error message for why the operation failed.
        perror("Error opening file");
        return 1;
    }

    // --- Step 3 & 4: Read File Line by Line and Search ---

    char line_buffer[2048]; // A buffer to hold one line of text from the file.

    printf("Searching for \"%s\" in file \"%s\":\n\n", pattern, filename);

    // This `while` loop is the standard, safe way to read a file line-by-line.
    // `fgets()` reads one line (or up to 2047 characters + null terminator)
    // from `file_pointer` and stores it in `line_buffer`.
    // It returns NULL when it reaches the end of the file, which ends the loop.
    while (fgets(line_buffer, sizeof(line_buffer), file_pointer) != NULL)
    {
        // --- The Core Logic: Searching for a Substring ---

        // `strstr()` is the key function here. It searches for the first
        // occurrence of the `pattern` string within the `line_buffer` string.
        // - If it finds a match, it returns a POINTER to the beginning of the match.
        // - If no match is found, it returns NULL.
        if (strstr(line_buffer, pattern) != NULL)
        {
            // A match was found! Print the entire line.
            // We use `printf("%s", ...)` and not `printf("%s\n", ...)` because
            // `fgets()` already includes the newline character `\n` at the
            // end of the line it reads.
            printf("%s", line_buffer);
        }
    }

    // --- Step 5: Clean Up ---

    // It is crucial to close the file when you are done with it.
    // `fclose()` releases the file handle back to the operating system.
    fclose(file_pointer);

    return 0; // Success!
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * Congratulations! You've just built your own version of `grep`. This project is a
 * major milestone. It demonstrates your ability to write a complete, useful
 * command-line tool that interacts with the file system, processes user input,
 * and performs core logic using string manipulation.
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * 1. Open a terminal and compile the program:
 *    `gcc -Wall -Wextra -std=c11 -o 27_build_your_own_grep 27_build_your_own_grep.c`
 *
 * 2. Run it! A great first test is to make it search for something in its own source code.
 *    Let's search for every line containing the word "main":
 *    `./27_build_your_own_grep main 27_build_your_own_grep.c`
 *
 * 3. Try another one. Search for every line containing the `strstr` function:
 *    `./27_build_your_own_grep strstr 27_build_your_own_grep.c`
 *
 * 4. For a more realistic example, create a sample data file:
 *    `echo "Hello world, this is a test." > data.txt`
 *    `echo "Another line with some more data." >> data.txt`
 *    `echo "The world is full of interesting tests." >> data.txt`
 *    `echo "A final line without the keyword." >> data.txt`
 *
 *    Now search this new file for the word "world":
 *    `./27_build_your_own_grep world data.txt`
 *
 *    You should see the first and third lines printed to your console.
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 27_build_your_own_grep 27_build_your_own_grep.c
./27_build_your_own_grep

Hash Table Implementation

PROJECT: IMPLEMENT A HASH TABLE

So far, we’ve seen arrays (fast access by index, slow search) and linked lists (slow search, flexible size). Now, we will build a data structure that offers the best of both worlds: the HASH TABLE (also known as a Hash Map).

WHAT IS A HASH TABLE? A Hash Table is a data structure that maps KEYS to VALUES. You give it a key (e.g., a person’s name), and it can very quickly find the associated value (e.g., their phone number). The goal is to achieve an average time complexity of O(1) – constant time – for insertion, deletion, and searching. This is incredibly fast and is why hash tables are used everywhere, from databases to compiler symbol tables to the dictionaries/maps in modern languages like Python and JavaScript.

HOW DOES IT WORK?

  1. THE HASH FUNCTION: The magic is in the HASH FUNCTION. This is a function that takes a key (which can be a string, number, etc.) and converts it into an integer index. This index tells us where to store the value in an underlying array. A good hash function distributes keys evenly across the array to minimize collisions.

  2. THE ARRAY (TABLE): The hash table uses a simple array as its backbone. The index generated by the hash function corresponds to a “bucket” or “slot” in this array.

  3. COLLISION HANDLING: What if two different keys hash to the same index? This is called a COLLISION. It’s inevitable. We will handle collisions using a method called SEPARATE CHAINING. This means that each slot in our array won’t hold the value directly. Instead, each slot will be a pointer to the HEAD of a LINKED LIST. If a collision occurs, we just add the new key-value pair as a new node in that linked list.

OUR IMPLEMENTATION PLAN:

  1. Define an Entry struct (a node in our linked list for a key-value pair).
  2. Define a HashTable struct (which holds the main array of entry pointers).
  3. Write a hash_function to convert string keys to array indices.
  4. Implement ht_create to initialize the table.
  5. Implement ht_insert to add or update key-value pairs.
  6. Implement ht_search to retrieve a value by its key.
  7. Implement ht_delete to remove a key-value pair.
  8. Implement ht_free to clean up all allocated memory.

This is a simple hash function. It sums the ASCII values of the characters in the key and then uses the modulo operator to ensure the result is within the bounds of our table size.

A good hash function is crucial for performance, but this is fine for learning.

Full Source

/**
 * @file 28_hash_table_implementation.c
 * @brief Part 4, Lesson 28: Hash Table Implementation
 * @author dunamismax
 * @date 06-15-2025
 *
 * This file is a major project: we will implement a Hash Table, one of the
 * most important and powerful data structures in all of computer science.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * PROJECT: IMPLEMENT A HASH TABLE
 *
 * So far, we've seen arrays (fast access by index, slow search) and linked lists
 * (slow search, flexible size). Now, we will build a data structure that offers
 * the best of both worlds: the HASH TABLE (also known as a Hash Map).
 *
 * WHAT IS A HASH TABLE?
 * A Hash Table is a data structure that maps KEYS to VALUES. You give it a key
 * (e.g., a person's name), and it can very quickly find the associated value
 * (e.g., their phone number). The goal is to achieve an average time complexity
 * of O(1) -- constant time -- for insertion, deletion, and searching. This is
 * incredibly fast and is why hash tables are used everywhere, from databases to
 * compiler symbol tables to the dictionaries/maps in modern languages like
 * Python and JavaScript.
 *
 * HOW DOES IT WORK?
 * 1. THE HASH FUNCTION: The magic is in the HASH FUNCTION. This is a function
 *    that takes a key (which can be a string, number, etc.) and converts it
 *    into an integer index. This index tells us where to store the value in
 *    an underlying array. A good hash function distributes keys evenly across
 *    the array to minimize collisions.
 *
 * 2. THE ARRAY (TABLE): The hash table uses a simple array as its backbone. The
 *    index generated by the hash function corresponds to a "bucket" or "slot"
 *    in this array.
 *
 * 3. COLLISION HANDLING: What if two different keys hash to the same index?
 *    This is called a COLLISION. It's inevitable. We will handle collisions
 *    using a method called SEPARATE CHAINING. This means that each slot in our
 *    array won't hold the value directly. Instead, each slot will be a pointer
 *    to the HEAD of a LINKED LIST. If a collision occurs, we just add the new
 *    key-value pair as a new node in that linked list.
 *
 * OUR IMPLEMENTATION PLAN:
 * 1. Define an `Entry` struct (a node in our linked list for a key-value pair).
 * 2. Define a `HashTable` struct (which holds the main array of entry pointers).
 * 3. Write a `hash_function` to convert string keys to array indices.
 * 4. Implement `ht_create` to initialize the table.
 * 5. Implement `ht_insert` to add or update key-value pairs.
 * 6. Implement `ht_search` to retrieve a value by its key.
 * 7. Implement `ht_delete` to remove a key-value pair.
 * 8. Implement `ht_free` to clean up all allocated memory.
 */

// --- Required Headers ---
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// --- Part 1: Data Structures and Constants ---

#define TABLE_SIZE 10 // A small size for easy demonstration

// A single key-value entry. This is also a node in a linked list.
typedef struct Entry
{
    char *key;
    char *value;
    struct Entry *next;
} Entry;

// The Hash Table itself.
typedef struct HashTable
{
    // The table is an array of pointers to Entry structs.
    // This is an array of linked list heads.
    Entry **entries;
} HashTable;

// --- Part 2: The Hash Function ---

/*
 * This is a simple hash function. It sums the ASCII values of the characters
 * in the key and then uses the modulo operator to ensure the result is
 * within the bounds of our table size.
 *
 * A good hash function is crucial for performance, but this is fine for learning.
 */
unsigned int hash_function(const char *key)
{
    unsigned long int value = 0;
    unsigned int i = 0;
    unsigned int key_len = strlen(key);

    // Sum the ASCII values of the characters
    for (; i < key_len; ++i)
    {
        value = value * 37 + key[i]; // A slightly better algorithm (djb2-ish)
    }

    // Return the value modulo the table size
    return value % TABLE_SIZE;
}

// --- Part 3: Core Hash Table Operations ---

/**
 * @brief Creates and initializes a new hash table.
 * @return A pointer to the newly created HashTable, or NULL on failure.
 */
HashTable *ht_create(void)
{
    // Allocate memory for the HashTable structure itself.
    HashTable *hashtable = malloc(sizeof(HashTable));
    if (hashtable == NULL)
    {
        return NULL;
    }

    // Allocate memory for the array of Entry pointers ("buckets").
    // We use calloc to initialize all pointers in the array to NULL.
    hashtable->entries = calloc(TABLE_SIZE, sizeof(Entry *));
    if (hashtable->entries == NULL)
    {
        free(hashtable); // Clean up partially allocated memory
        return NULL;
    }

    return hashtable;
}

/**
 * @brief Creates a new key-value entry.
 */
Entry *create_entry(const char *key, const char *value)
{
    Entry *entry = malloc(sizeof(Entry));
    entry->key = malloc(strlen(key) + 1);
    entry->value = malloc(strlen(value) + 1);

    strcpy(entry->key, key);
    strcpy(entry->value, value);
    entry->next = NULL;

    return entry;
}

/**
 * @brief Inserts a key-value pair into the hash table. Updates value if key exists.
 */
void ht_insert(HashTable *hashtable, const char *key, const char *value)
{
    unsigned int index = hash_function(key);
    Entry *current_entry = hashtable->entries[index];

    // Traverse the linked list at this index to see if the key already exists.
    while (current_entry != NULL)
    {
        if (strcmp(current_entry->key, key) == 0)
        {
            // Key found, so update the value.
            free(current_entry->value); // Free the old value
            current_entry->value = malloc(strlen(value) + 1);
            strcpy(current_entry->value, value);
            return; // We're done.
        }
        current_entry = current_entry->next;
    }

    // Key not found. Create a new entry and insert it at the head of the list.
    Entry *new_entry = create_entry(key, value);
    new_entry->next = hashtable->entries[index];
    hashtable->entries[index] = new_entry;
}

/**
 * @brief Searches for a key in the hash table.
 * @return The value associated with the key, or NULL if the key is not found.
 */
char *ht_search(HashTable *hashtable, const char *key)
{
    unsigned int index = hash_function(key);
    Entry *entry = hashtable->entries[index];

    // Traverse the linked list at the calculated index.
    while (entry != NULL)
    {
        if (strcmp(entry->key, key) == 0)
        {
            // Key found! Return its value.
            return entry->value;
        }
        entry = entry->next;
    }

    // Key not found.
    return NULL;
}

/**
 * @brief Deletes a key-value pair from the hash table.
 */
void ht_delete(HashTable *hashtable, const char *key)
{
    unsigned int index = hash_function(key);
    Entry *entry = hashtable->entries[index];
    Entry *prev = NULL;

    // Traverse the list to find the entry to delete
    while (entry != NULL && strcmp(entry->key, key) != 0)
    {
        prev = entry;
        entry = entry->next;
    }

    // If entry is NULL, the key wasn't found.
    if (entry == NULL)
    {
        return;
    }

    // Relink the list
    if (prev == NULL)
    {
        // The item to delete is the head of the list
        hashtable->entries[index] = entry->next;
    }
    else
    {
        // The item is in the middle or at the end
        prev->next = entry->next;
    }

    // Free the memory for the deleted entry
    free(entry->key);
    free(entry->value);
    free(entry);
}

/**
 * @brief Frees all memory used by the hash table.
 */
void ht_free(HashTable *hashtable)
{
    for (int i = 0; i < TABLE_SIZE; ++i)
    {
        Entry *entry = hashtable->entries[i];
        while (entry != NULL)
        {
            Entry *temp = entry;
            entry = entry->next;
            free(temp->key);
            free(temp->value);
            free(temp);
        }
    }

    free(hashtable->entries);
    free(hashtable);
}

/**
 * @brief A helper function to print the contents of the hash table.
 */
void ht_print(HashTable *hashtable)
{
    printf("\n--- Hash Table Contents ---\n");
    for (int i = 0; i < TABLE_SIZE; ++i)
    {
        printf("Bucket[%d]: ", i);
        Entry *entry = hashtable->entries[i];
        if (entry == NULL)
        {
            printf("~empty~\n");
            continue;
        }
        while (entry != NULL)
        {
            printf(" -> [\"%s\": \"%s\"]", entry->key, entry->value);
            entry = entry->next;
        }
        printf("\n");
    }
    printf("---------------------------\n");
}

// --- Main Function for Demonstration ---
int main(void)
{
    printf("Creating a new hash table.\n");
    HashTable *ht = ht_create();

    printf("\nInserting key-value pairs...\n");
    ht_insert(ht, "name", "John Doe");
    ht_insert(ht, "age", "30");
    ht_insert(ht, "city", "New York");
    // These two keys will likely cause a collision with TABLE_SIZE 10
    ht_insert(ht, "country", "USA");
    ht_insert(ht, "language", "C");

    ht_print(ht);

    printf("\nSearching for keys...\n");
    char *name = ht_search(ht, "name");
    char *job = ht_search(ht, "job"); // This key doesn't exist

    printf("Value for 'name': %s\n", name ? name : "Not Found");
    printf("Value for 'job': %s\n", job ? job : "Not Found");

    printf("\nDeleting key 'age'...\n");
    ht_delete(ht, "age");
    ht_print(ht);

    printf("\nUpdating key 'city'...\n");
    ht_insert(ht, "city", "Los Angeles"); // This should update the existing entry
    ht_print(ht);

    printf("\nFreeing all hash table memory...\n");
    ht_free(ht);
    printf("Memory freed successfully.\n");

    return 0;
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * Congratulations on building a complete hash table! You've implemented one of the
 * most fundamental and high-performance data structures. You used dynamic memory,
 * pointers to pointers, structs, linked lists (for collision handling), and
 * string manipulation. This is a massive achievement.
 *
 * You now understand the core principles behind the dictionaries, maps, and objects
 * that power modern, high-level programming languages.
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * 1. Open a terminal and compile the program:
 *    `gcc -Wall -Wextra -std=c11 -o 28_hash_table_implementation 28_hash_table_implementation.c`
 *
 * 2. Run the executable:
 *    `./28_hash_table_implementation`
 *
 * 3. Observe the output. Pay close attention to how the `ht_print` function shows
 *    the state of the table after each operation, especially how collisions
 *    result in linked lists within a single bucket.
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 28_hash_table_implementation 28_hash_table_implementation.c
./28_hash_table_implementation

Tiny Shell

PROJECT: BUILD A TINY SHELL

A SHELL is a command-line interpreter. It’s the program that gives you a prompt (like $ or > ), reads your input (like ls -l), and tells the operating system to execute that command. In this lesson, we will build one.

This project is the culmination of many systems programming concepts. It’s how a computer can run a program from within another program. The core of any shell revolves around a simple, powerful loop:

  1. READ: Read a command from the user.
  2. PARSE: Break the command into a program name and its arguments.
  3. FORK: Create a new PROCESS (a copy of the shell).
  4. EXEC: The new process REPLACES itself with the command to be run.
  5. WAIT: The original shell process waits for the command to finish.

THE KEY SYSTEM CALLS To accomplish this, we will use three of the most important functions in Unix/Linux programming:

  • fork(): This creates a new child process, which is an exact duplicate of the parent process (our shell). It’s like cloning our program. The parent gets the child’s process ID, and the child gets 0.

  • execvp(): (The “exec” family of functions). This function REPLACES the current process’s memory space with a new program. When the child process calls execvp, it ceases to be a copy of our shell and BECOMES the ls program (or whatever was requested). execvp is special because it searches the system’s PATH for the executable, just like a real shell.

  • waitpid(): This tells the parent process (our shell) to pause and wait until a specific child process has finished executing. This ensures our shell prompt doesn’t reappear until the command’s output is complete.

Let’s build our own bash!

Full Source

/**
 * @file 29_tiny_shell.c
 * @brief Part 4, Lesson 29: Build Your Own Tiny Shell
 * @author dunamismax
 * @date 06-15-2025
 *
 * This file implements a basic command-line shell, a program that reads
 * commands from the user and executes them. This is a capstone project for
 * understanding process management in a Unix-like environment.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * PROJECT: BUILD A TINY SHELL
 *
 * A SHELL is a command-line interpreter. It's the program that gives you a
 * prompt (like `$` or `> `), reads your input (like `ls -l`), and tells the
 * operating system to execute that command. In this lesson, we will build one.
 *
 * This project is the culmination of many systems programming concepts. It's how
 * a computer can run a program from within another program. The core of any
 * shell revolves around a simple, powerful loop:
 *
 * 1. READ: Read a command from the user.
 * 2. PARSE: Break the command into a program name and its arguments.
 * 3. FORK: Create a new PROCESS (a copy of the shell).
 * 4. EXEC: The new process REPLACES itself with the command to be run.
 * 5. WAIT: The original shell process waits for the command to finish.
 *
 * THE KEY SYSTEM CALLS
 * To accomplish this, we will use three of the most important functions in
 * Unix/Linux programming:
 *
 * - fork(): This creates a new child process, which is an exact duplicate of
 *   the parent process (our shell). It's like cloning our program. The parent
 *   gets the child's process ID, and the child gets 0.
 *
 * - execvp(): (The "exec" family of functions). This function REPLACES the
 *   current process's memory space with a new program. When the child process
 *   calls `execvp`, it ceases to be a copy of our shell and BECOMES the `ls`
 *   program (or whatever was requested). `execvp` is special because it
 *   searches the system's PATH for the executable, just like a real shell.
 *
 * - waitpid(): This tells the parent process (our shell) to pause and wait
 *   until a specific child process has finished executing. This ensures our
 *   shell prompt doesn't reappear until the command's output is complete.
 *
 * Let's build our own `bash`!
 */

// --- Required Headers ---
#include <stdio.h>
#include <stdlib.h>
#include <string.h>   // For strtok()
#include <unistd.h>   // For fork(), execvp()
#include <sys/wait.h> // For waitpid()

// --- Constants ---
#define MAX_LINE 80 // The maximum length of a command
#define MAX_ARGS 20 // The maximum number of arguments

// --- Function to Parse Input ---
// This function takes a raw command line string and breaks it into tokens.
// It populates the `args` array and is a crucial pre-processing step.
void parse_input(char *line, char **args)
{
    int i = 0;
    // `strtok` splits a string by a delimiter. We use space, tab, and newline.
    // The first call to `strtok` uses the line; subsequent calls use NULL
    // to continue tokenizing the same string.
    char *token = strtok(line, " \t\n");

    while (token != NULL && i < MAX_ARGS - 1)
    {
        args[i] = token;
        i++;
        token = strtok(NULL, " \t\n");
    }
    // The `execvp` function requires the argument array to be terminated by a NULL pointer.
    args[i] = NULL;
}

void discard_remaining_input(void)
{
    int ch;

    while ((ch = getchar()) != '\n' && ch != EOF)
    {
        // Consume the rest of an overlong command so it is not treated as a new one.
    }
}

int main(void)
{
    char *args[MAX_ARGS]; // Array to hold parsed command arguments
    char line[MAX_LINE];  // Buffer to hold the raw input line
    int should_run = 1;   // Flag to control the main loop

    printf("Welcome to TinyShell! Type 'exit' to quit.\n");

    while (should_run)
    {
        printf("> ");
        // We must flush stdout to ensure the prompt `>` appears before `fgets` waits for input.
        fflush(stdout);

        // --- Step 1: Read Input ---
        if (fgets(line, sizeof(line), stdin) == NULL)
        {
            // If fgets returns NULL, it's an end-of-file (Ctrl+D) or an error.
            printf("\nExiting TinyShell.\n");
            break;
        }

        if (strchr(line, '\n') == NULL && !feof(stdin))
        {
            discard_remaining_input();
            printf("Command too long. Maximum length is %d characters.\n", MAX_LINE - 2);
            continue;
        }

        // --- Step 2: Parse Input ---
        parse_input(line, args);

        // If the user just hits Enter, args[0] will be NULL. Loop again.
        if (args[0] == NULL)
        {
            continue;
        }

        // --- Handle Built-in Commands ---
        // We handle `exit` ourselves; it's not an external program.
        if (strcmp(args[0], "exit") == 0)
        {
            should_run = 0;
            continue;
        }

        // --- Step 3: Fork a Child Process ---
        pid_t pid = fork();

        if (pid < 0)
        {
            // Forking failed. This is a serious error.
            perror("fork failed");
            exit(1);
        }
        else if (pid == 0)
        {
            // --- This is the CHILD PROCESS ---
            // --- Step 4: Execute the Command ---

            // `execvp` takes the program name (args[0]) and the full argument vector.
            // If it is successful, it NEVER returns. The child process becomes `ls` (or whatever).
            if (execvp(args[0], args) == -1)
            {
                // If `execvp` returns, an error occurred (e.g., command not found).
                perror("execvp failed");
                exit(1); // IMPORTANT: Terminate the child process on failure.
            }
        }
        else
        {
            // --- This is the PARENT PROCESS ---
            // --- Step 5: Wait for the Child to Complete ---
            int status;
            // `waitpid` waits for the process with the given `pid` to finish.
            // The `&status` argument can be used to get info on how the child exited.
            waitpid(pid, &status, 0);
        }
    }

    return 0;
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * You have successfully built a functional command-line shell! It can run almost
 * any standard command-line program on your system. You have directly manipulated
 * processes, the fundamental unit of execution in an operating system.
 *
 * This Read-Parse-Fork-Exec-Wait loop is the foundation of not just shells, but
 * also IDE "Run" buttons, web servers that spawn worker processes, and many other
 * powerful system tools.
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * 1. Open a terminal and compile the program:
 *    `gcc -Wall -Wextra -std=c11 -o 29_tiny_shell 29_tiny_shell.c`
 *
 * 2. Run your new shell:
 *    `./29_tiny_shell`
 *
 * 3. You will see a `>` prompt. Try running some commands!
 *    > ls
 *    > ls -l
 *    > pwd
 *    > echo "Hello from my own shell!"
 *    > whoami
 *
 * 4. When you are finished, type `exit` to quit your shell and return to the
 *    regular system shell.
 *    > exit
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 29_tiny_shell 29_tiny_shell.c
./29_tiny_shell

Multithreaded File Analyzer

PROJECT: HARNESS MULTI-CORE POWER WITH THREADS

So far, all our programs have run in a single sequence of execution. They do one thing at a time. This is called single-threaded execution. But modern CPUs have multiple cores, each capable of running instructions independently. How can we take advantage of this power? The answer is MULTITHREADING.

PROCESS vs. THREAD

  • A PROCESS is a running instance of a program (like our tiny_shell). It has its own dedicated memory space, file descriptors, etc. fork() creates a new process.
  • A THREAD is a “lightweight” path of execution within a single process. Multiple threads within the same process SHARE the same memory space. This makes communication between them easy (they can just access the same variables), but it also introduces a huge challenge: managing that shared access safely.

THE CHALLENGE: RACE CONDITIONS Imagine two threads trying to increment the same global counter (global_count++). This operation is not ATOMIC (it’s not one single instruction). It’s really:

  1. Read the value of global_count from memory into a CPU register.
  2. Increment the value in the register.
  3. Write the new value back to memory.

A RACE CONDITION occurs if Thread 1 reads the value (say, 5), but before it can write back 6, Thread 2 also reads the value (still 5). Both threads will compute 6 and write it back. The counter should be 7, but it’s only 6! We lost an update.

THE SOLUTION: MUTEXES A MUTEX (MUTual EXclusion) is a lock. It’s a programming primitive that ensures only one thread can access a “critical section” of code at a time. The workflow is:

  1. Thread A acquires the lock. No other thread can acquire it until A is done.
  2. Thread A safely modifies the shared data.
  3. Thread A releases the lock.
  4. Thread B, which was waiting, can now acquire the lock and do its work.

OUR PLAN: We will write a program that counts the characters, words, and lines in a large file. To speed it up, we will:

  1. Read the entire file into one large memory buffer.
  2. Divide the buffer into chunks, one for each thread.
  3. Launch multiple threads, each running a worker function to analyze its chunk.
  4. Each thread will calculate its local counts.
  5. After finishing, each thread will lock a global mutex, add its local counts to a global total, and then unlock the mutex.
  6. The main thread will wait for all worker threads to complete and then print the result.

We will use the POSIX Threads (pthreads) library, the standard for C.

Full Source

/**
 * @file 30_multithreaded_file_analyzer.c
 * @brief Part 4, Lesson 30: Multithreaded File Analyzer
 * @author dunamismax
 * @date 06-15-2025
 *
 * This file is a project that harnesses the power of multiple CPU cores
 * by using threads to analyze a large file in parallel.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * PROJECT: HARNESS MULTI-CORE POWER WITH THREADS
 *
 * So far, all our programs have run in a single sequence of execution. They do
 * one thing at a time. This is called single-threaded execution. But modern
 * CPUs have multiple cores, each capable of running instructions independently.
 * How can we take advantage of this power? The answer is MULTITHREADING.
 *
 * PROCESS vs. THREAD
 * - A PROCESS is a running instance of a program (like our `tiny_shell`). It has
 *   its own dedicated memory space, file descriptors, etc. `fork()` creates a new process.
 * - A THREAD is a "lightweight" path of execution *within* a single process.
 *   Multiple threads within the same process SHARE the same memory space. This
 *   makes communication between them easy (they can just access the same variables),
 *   but it also introduces a huge challenge: managing that shared access safely.
 *
 * THE CHALLENGE: RACE CONDITIONS
 * Imagine two threads trying to increment the same global counter (`global_count++`).
 * This operation is not ATOMIC (it's not one single instruction). It's really:
 * 1. Read the value of `global_count` from memory into a CPU register.
 * 2. Increment the value in the register.
 * 3. Write the new value back to memory.
 *
 * A RACE CONDITION occurs if Thread 1 reads the value (say, 5), but before it
 * can write back 6, Thread 2 also reads the value (still 5). Both threads will
 * compute 6 and write it back. The counter should be 7, but it's only 6! We lost an update.
 *
 * THE SOLUTION: MUTEXES
 * A MUTEX (MUTual EXclusion) is a lock. It's a programming primitive that ensures
 * only one thread can access a "critical section" of code at a time. The workflow is:
 * 1. Thread A acquires the lock. No other thread can acquire it until A is done.
 * 2. Thread A safely modifies the shared data.
 * 3. Thread A releases the lock.
 * 4. Thread B, which was waiting, can now acquire the lock and do its work.
 *
 * OUR PLAN:
 * We will write a program that counts the characters, words, and lines in a large
 * file. To speed it up, we will:
 * 1. Read the entire file into one large memory buffer.
 * 2. Divide the buffer into chunks, one for each thread.
 * 3. Launch multiple threads, each running a worker function to analyze its chunk.
 * 4. Each thread will calculate its *local* counts.
 * 5. After finishing, each thread will lock a global mutex, add its local counts
 *    to a global total, and then unlock the mutex.
 * 6. The main thread will wait for all worker threads to complete and then print the result.
 *
 * We will use the POSIX Threads (pthreads) library, the standard for C.
 */

// --- Required Headers ---
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h> // The main header for POSIX Threads
#include <ctype.h>   // For isspace()

// --- Constants and Global Data ---
#define NUM_THREADS 4

// This struct will hold the final, combined results. This is our SHARED DATA.
typedef struct
{
    long long total_chars;
    long long total_words;
    long long total_lines;
} GlobalCounts;

GlobalCounts g_counts = {0, 0, 0}; // Initialize global counts
pthread_mutex_t g_mutex;           // The global MUTEX to protect g_counts

// This struct holds the information we need to pass to each thread.
typedef struct
{
    char *data_chunk; // Pointer to the start of this thread's data
    long chunk_size;  // How many bytes this thread should process
    int starts_inside_word; // True when this chunk begins in the middle of a word
} ThreadData;

// --- The Worker Function ---
// This is the function that each thread will execute.
void *analyze_chunk(void *arg)
{
    ThreadData *data = (ThreadData *)arg;

    // --- Step 1: Perform analysis on local variables ---
    // We do NOT want to lock the mutex for every character we count.
    // That would be extremely slow and defeat the purpose of threading.
    // Instead, each thread calculates its own sub-total.
    long long local_chars = 0;
    long long local_words = 0;
    long long local_lines = 0;
    int in_word = data->starts_inside_word; // Continue an in-progress word across chunks

    for (long i = 0; i < data->chunk_size; i++)
    {
        char c = data->data_chunk[i];
        local_chars++;

        if (c == '\n')
        {
            local_lines++;
        }

        if (isspace((unsigned char)c))
        {
            in_word = 0;
        }
        else if (in_word == 0)
        {
            in_word = 1;
            local_words++;
        }
    }

    // --- Step 2: Lock the mutex and update the global state ---
    // This is the CRITICAL SECTION. Only one thread can be in here at a time.
    pthread_mutex_lock(&g_mutex);

    g_counts.total_chars += local_chars;
    g_counts.total_words += local_words;
    g_counts.total_lines += local_lines;

    pthread_mutex_unlock(&g_mutex); // Release the lock!

    return NULL;
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
        return 1;
    }

    // --- Read entire file into memory ---
    FILE *file = fopen(argv[1], "rb");
    if (!file)
    {
        perror("Error opening file");
        return 1;
    }

    if (fseek(file, 0, SEEK_END) != 0)
    {
        perror("Error seeking to end of file");
        fclose(file);
        return 1;
    }

    long file_size = ftell(file);
    if (file_size < 0)
    {
        perror("Error determining file size");
        fclose(file);
        return 1;
    }

    if (fseek(file, 0, SEEK_SET) != 0)
    {
        perror("Error rewinding file");
        fclose(file);
        return 1;
    }

    char *file_buffer = NULL;
    if (file_size > 0)
    {
        file_buffer = malloc((size_t)file_size);
    }

    if (file_size > 0 && !file_buffer)
    {
        fprintf(stderr, "Could not allocate memory for file\n");
        fclose(file);
        return 1;
    }

    if (file_size > 0 && fread(file_buffer, 1, (size_t)file_size, file) != (size_t)file_size)
    {
        fprintf(stderr, "Error reading file\n");
        free(file_buffer);
        fclose(file);
        return 1;
    }
    fclose(file);
    printf("Successfully read %ld bytes from %s.\n", file_size, argv[1]);

    if (file_size == 0)
    {
        printf("File is empty. Nothing to analyze.\n");
        printf("\n--- Analysis Complete ---\n");
        printf("Total Characters: %lld\n", g_counts.total_chars);
        printf("Total Words:      %lld\n", g_counts.total_words);
        printf("Total Lines:      %lld\n", g_counts.total_lines);
        printf("-------------------------\n");
        return 0;
    }

    // --- Initialize Threads and Mutex ---
    pthread_t threads[NUM_THREADS];
    ThreadData thread_args[NUM_THREADS];
    pthread_mutex_init(&g_mutex, NULL); // Initialize the mutex

    long chunk_size = file_size / NUM_THREADS;
    for (int i = 0; i < NUM_THREADS; i++)
    {
        long chunk_start = i * chunk_size;

        thread_args[i].data_chunk = file_buffer + chunk_start;
        thread_args[i].chunk_size = (i == NUM_THREADS - 1) ? (file_size - chunk_start) : chunk_size;
        thread_args[i].starts_inside_word =
            (chunk_start > 0 && !isspace((unsigned char)file_buffer[chunk_start - 1]));

        printf("Launching thread %d to process %ld bytes.\n", i, thread_args[i].chunk_size);
        // `pthread_create` starts a new thread executing `analyze_chunk`
        // and passes it a pointer to its `thread_args`.
        pthread_create(&threads[i], NULL, analyze_chunk, &thread_args[i]);
    }

    // --- Wait for all threads to complete ---
    for (int i = 0; i < NUM_THREADS; i++)
    {
        // `pthread_join` blocks the main thread until the specified thread finishes.
        pthread_join(threads[i], NULL);
        printf("Thread %d finished.\n", i);
    }

    // --- Clean up and Print Results ---
    pthread_mutex_destroy(&g_mutex); // Always destroy the mutex
    free(file_buffer);

    printf("\n--- Analysis Complete ---\n");
    printf("Total Characters: %lld\n", g_counts.total_chars);
    printf("Total Words:      %lld\n", g_counts.total_words);
    printf("Total Lines:      %lld\n", g_counts.total_lines);
    printf("-------------------------\n");

    return 0;
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * You've just written a parallel program! This is a huge step into high-performance
 * computing. You used POSIX threads to split a task, managed shared data with a
 * mutex to prevent race conditions, and successfully reaped the results.
 *
 * This pattern (split data, process in parallel, safely combine results) is a
 * cornerstone of concurrent programming.
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * Compiling threaded programs requires linking the pthread library. You must add
 * the `-pthread` flag to your GCC command.
 *
 * 1. Compile the program:
 *    `gcc -Wall -Wextra -std=c11 -pthread -o 30_multithreaded_file_analyzer 30_multithreaded_file_analyzer.c`
 *
 * 2. To see the benefit, you need a large file. You can create one with this command
 *    (this creates a ~100MB file of random data, which won't have many lines/words
 *    but is good for testing raw character counting performance):
 *    `dd if=/dev/urandom of=large_test_file.txt bs=1M count=100`
 *    (On Windows, you'll need to find a large text file online to download).
 *
 * 3. Run the analyzer on the large file:
 *    `./30_multithreaded_file_analyzer large_test_file.txt`
 *
 * 4. You can also run it on its own source code for a more readable result:
 *    `./30_multithreaded_file_analyzer 30_multithreaded_file_analyzer.c`
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -pthread -o 30_multithreaded_file_analyzer 30_multithreaded_file_analyzer.c
./30_multithreaded_file_analyzer <filename>

Makefiles for Multi-File Projects

Welcome to a pivotal moment in your C journey. Until now, we have worked with single source files. While great for learning, real-world applications are almost always split into multiple files for organization, reusability, and clarity.

Typing out long gcc commands for every file becomes tedious and error-prone. Imagine a project with 50 files. This is where the make utility comes in.

What is a Makefile?

A make program reads a special file, by default named Makefile, to understand how to build your project. A Makefile contains a set of rules. Each rule has three parts: a target, its dependencies, and a recipe.

target: dependencies
    <tab>recipe (the command to run)
  • Target: The file we want to build (e.g., an executable or an object file).
  • Dependencies: The files the target needs in order to be built.
  • Recipe: The command(s) to execute to create the target.

Important: The recipe lines must start with a TAB character, not spaces.

The Project Structure

This lesson is intentionally split across multiple files to demonstrate how different C files work together:

  1. main.c – The entry point of the program. Contains the main function.
  2. helper.h – The header file that declares the helper functions.
  3. helper.c – The source file that defines the helper functions.
  4. Makefile – The instruction manual for the compiler.

helper.h

/**
 * @file helper.h
 * @brief Part 5, Lesson 31: Header File for the Helper Module
 * @author dunamismax
 * @date 06-15-2025
 *
 * This is a HEADER FILE. Its purpose is to declare the functions and types
 * that another part of the program (in this case, `helper.c`) provides.
 * It acts as a public "interface" or "contract".
 */

/*
 * =====================================================================================
 * |                            - LESSON: HEADER FILES -                               |
 * =====================================================================================
 *
 * WHAT IS A HEADER FILE?
 * In a multi-file project, we need a way for one `.c` file to know about the
 * functions available in another `.c` file. A HEADER FILE (with a `.h` extension)
 * serves this purpose. It contains the DECLARATIONS of functions, but not their
 * actual code (the DEFINITIONS).
 *
 * Any `.c` file that wants to use these functions can simply `#include` this
 * header file.
 *
 * --- The Include Guard ---
 *
 * What happens if a header file gets included more than once in the same
 * compilation process? It can lead to errors ("redeclaration of '...''").
 * To prevent this, we use an INCLUDE GUARD. It's a standard preprocessor trick.
 *
 * `#ifndef HELPER_H`   // "If HELPER_H is NOT defined..."
 * `#define HELPER_H`   // "...then define it..."
 *
 *  // ... all the content of the header file goes here ...
 *
 * `#endif`             // "...and this is the end of the 'if' block."
 *
 * The first time the compiler sees this file, `HELPER_H` is not defined, so it
 * processes everything inside. It also defines `HELPER_H`. The second time it
 * sees this file, `HELPER_H` *is* defined, so the preprocessor skips everything
 * between `#ifndef` and `#endif`.
 *
 * The name `HELPER_H` should be unique to the file. A common convention is to
 * use the filename in all caps, replacing the `.` with an `_`.
 */

#ifndef HELPER_H
#define HELPER_H

// --- Function Prototypes ---

// This is a FUNCTION PROTOTYPE or FUNCTION DECLARATION.
// It tells the compiler everything it needs to know to *call* the function:
// its return type, its name, and the types of its parameters.
// The actual code for this function is in `helper.c`.
void say_hello_from_helper(void);

#endif // HELPER_H

helper.c

/**
 * @file helper.c
 * @brief Part 5, Lesson 31: Implementation File for the Helper Module
 * @author dunamismax
 * @date 06-15-2025
 *
 * This is a SOURCE FILE that provides the implementation for the
 * functions declared in `helper.h`.
 */

/*
 * =====================================================================================
 * |                          - LESSON: SOURCE FILES -                                 |
 * =====================================================================================
 *
 * WHAT IS A SOURCE IMPLEMENTATION FILE?
 * This `.c` file contains the actual code, the DEFINITIONS, for the functions
 * that were promised (declared) in the corresponding header file (`helper.h`).
 *
 * By separating the declaration (`.h`) from the definition (`.c`), we create
 * modular, reusable code. Another programmer could use our `helper` module by
 * just reading `helper.h`; they wouldn't need to know the details of our code here.
 *
 * --- Including Local Headers ---
 * Notice we use `#include "helper.h"` with double quotes. This is a signal
 * to the compiler to look for the file in the CURRENT directory first, before
 * searching the system's standard library paths.
 *
 *  - `#include <stdio.h>`: Use angle brackets for system libraries.
 *  - `#include "helper.h"`: Use double quotes for your own project's header files.
 */

#include <stdio.h>
#include "helper.h" // We need to include our own header file.

/**
 * @brief Prints a message to demonstrate a function call from a separate file.
 *
 * This is the DEFINITION of the function that was declared in `helper.h`.
 * It contains the actual block of code that will be executed when the
 * function is called.
 */
void say_hello_from_helper(void)
{
    printf("Hello from the helper module! This message comes from helper.c.\n");
}

main.c

/**
 * @file main.c
 * @brief Part 5, Lesson 31: Using a Makefile and Multiple Source Files
 * @author dunamismax
 * @date 06-15-2025
 *
 * This file demonstrates how to use a function from a separate module
 * (`helper.c`) by including its header file (`helper.h`).
 */

/*
 * =====================================================================================
 * |                      - LESSON: PUTTING IT ALL TOGETHER -                          |
 * =====================================================================================
 *
 * Welcome to our first multi-file project!
 *
 * The goal of this lesson is to see how different C files work together.
 *
 * 1. `main.c` (This file): This is the entry point of our program. It contains
 *    the `main` function. It needs to call a function from our "helper" module.
 *
 * 2. `helper.h` (Header file): This file *declares* the functions our helper
 *    module provides. By including it here (`#include "helper.h"`), we tell
 *    the compiler "Trust me, a function named `say_hello_from_helper` exists
 *    somewhere. Here's what it looks like."
 *
 * 3. `helper.c` (Source file): This file *defines* the `say_hello_from_helper`
 *    function, providing the actual code for it.
 *
 * 4. `Makefile`: This is the instruction manual for the compiler. It tells `gcc`
 *    how to compile `main.c` and `helper.c` separately into "object files",
 *    and then how to "link" them together into a single, final executable.
 */

#include <stdio.h>
#include "helper.h" // Include the declarations from our helper module.

/**
 * @brief The main entry point of the program.
 *
 * This program demonstrates linking multiple source files. It calls a function
 * defined in `helper.c`.
 *
 * @return 0 on successful execution.
 */
int main(void)
{
    printf("Message from main.c: Calling a function from another file...\n");

    // This function call is possible because we included `helper.h`.
    // The compiler knows the function's "signature" from the header.
    // The LINKER (part of the compilation process guided by the Makefile)
    // is responsible for connecting this call to the actual function code
    // in `helper.c`.
    say_hello_from_helper();

    printf("Message from main.c: The helper function has completed.\n");

    return 0;
}

/*
 * =====================================================================================
 * |                                 - COMPILATION -                                   |
 * =====================================================================================
 *
 * NOTE: Do NOT compile this file directly with a simple `gcc` command.
 *
 * This is a multi-file project. To compile and run it, you must use the
 * `make` utility with the provided `Makefile`.
 *
 * Open your terminal in this directory and run:
 *
 *   `make`
 *
 * Then run the resulting executable:
 *
 *   `./31_project_main`
 *
 * For a detailed explanation, read the comments in the `Makefile`.
 */

The Makefile

The Makefile ties it all together. It uses variables, rules, and phony targets to automate the build process.

# Makefile
# @brief Part 5, Lesson 31: Makefiles for Multi-File Projects
# @author dunamismax
# @date 06-15-2025
#
# This file is the lesson on Makefiles. Read the comments from top to bottom
# to learn how to automate the compilation of complex C projects.

# =====================================================================================
# |                                   - LESSON START -                                  |
# =====================================================================================
#
# Welcome to a pivotal moment in your C journey! Until now, we've worked with
# single source files. While great for learning, real-world applications are almost
# always split into multiple files for organization, reusability, and clarity.
#
# Typing out long `gcc` commands for every file becomes tedious and error-prone.
# Imagine a project with 50 files! This is where the `make` utility comes in.
#
# WHAT IS A MAKEFILE?
# A `make` program reads a special file, by default named `Makefile`, to understand
# how to build your project. A Makefile contains a set of RULES. Each rule has
# three parts: a TARGET, its DEPENDENCIES, and a RECIPE.
#
#   target: dependencies
#       <tab>recipe (the command to run)
#
# - TARGET: The file we want to build (e.g., an executable or an object file).
# - DEPENDENCIES: The files the target needs in order to be built.
# - RECIPE: The command(s) to execute to create the target.
#
# IMPORTANT: The lines with recipes MUST start with a TAB character, not spaces!

# --- Part 1: Using Variables ---
#
# To make our Makefile clean and easy to modify, we use VARIABLES.
# It's convention to use all caps for variable names.

# The C compiler we want to use.
CC = gcc

# The flags we pass to the compiler. These are the same flags we've been using.
# CFLAGS stands for "C Compiler Flags".
CFLAGS = -Wall -Wextra -std=c11

# The name of the final executable file we want to build.
TARGET = 31_project_main

# --- Part 2: Defining the Rules ---
#
# Now we define the rules for building our project. `make` reads these rules
# and figures out what needs to be compiled or re-compiled.

# The first rule in a Makefile is the default rule. When you just type `make`
# in the terminal, this is the target that `make` will try to build. We want
# it to build our entire project.
#
# This rule says: To make the TARGET (`31_project_main`), you first need
# `main.o` and `helper.o`.
# The recipe then links these two object files together to create the final executable.
$(TARGET): main.o helper.o
	$(CC) $(CFLAGS) -o $(TARGET) main.o helper.o

# This rule says: To make `main.o`, you need `main.c` and `helper.h`.
# If `main.c` or `helper.h` has changed since the last time `main.o` was built,
# `make` will run the recipe. The `-c` flag tells GCC to COMPILE ONLY, creating
# an "object file" (`.o`) without linking.
main.o: main.c helper.h
	$(CC) $(CFLAGS) -c main.c

# This is the rule for our helper module.
# To make `helper.o`, you need `helper.c` and `helper.h`.
helper.o: helper.c helper.h
	$(CC) $(CFLAGS) -c helper.c

# --- Part 3: Phony Targets ---
#
# Sometimes we want a target that isn't an actual file. For example, a "clean"
# target to delete all the compiled files. These are called PHONY targets.
# We declare them with `.PHONY` to tell `make` that this target name doesn't
# correspond to a file it should expect to exist.

.PHONY: all clean

# A common convention is to have an `all` target that is just another name for
# the default target. It doesn't do anything new but provides a clear command.
all: $(TARGET)

# This rule cleans up our directory.
# The `rm` command removes files. The `-f` flag means "force", so it won't
# complain if the files don't exist.
# We remove the object files (`*.o`) and the final executable (`$(TARGET)`).
clean:
	rm -f $(TARGET) *.o

# =====================================================================================
# |                                    - LESSON END -                                   |
# =====================================================================================
#
# HOW TO USE THIS MAKEFILE:
#
# 1. Open a terminal in the directory containing this Makefile and the .c/.h files.
#
# 2. To build the entire project:
#    `make`
#    (or `make all`)
#
#    You will see the commands from the recipes being executed. An executable
#    named `31_project_main` will be created.
#
# 3. To run the program:
#    - On Linux/macOS:   `./31_project_main`
#    - On Windows:       `31_project_main.exe`
#
# 4. To clean up all generated files (the executable and object files):
#    `make clean`
#
# Try it! Run `make`, then run the program. Then run `make` again. Notice how
# `make` says everything is "up to date"? Now, modify `helper.c` (e.g., add a
# comment) and run `make` again. See how it only re-compiles what's necessary?
# This is the power of `make`!

How to Compile and Run

Build the project:

make

Run the program:

./31_project_main

Clean up generated files:

make clean

Try it: run make, then run the program. Then run make again. Notice how make says everything is “up to date.” Now modify helper.c (for example, add a comment) and run make again. See how it only re-compiles what changed? That is the power of make.

Key Takeaways

  • Real-world C projects are split into multiple source files for organization.
  • Header files (.h) declare function signatures so other files can use them.
  • Source files (.c) define the actual function implementations.
  • The #include "header.h" directive (with quotes) includes your own headers.
  • A Makefile automates the compile-and-link process so you do not have to type long gcc commands by hand.
  • make only re-compiles files that have changed, saving time on large projects.
  • Use make clean to remove generated build artifacts.

Linking External Libraries

In our last lesson, we learned how to build a project from multiple source files that WE wrote. But what if we want to use code written by someone else? This is where LIBRARIES come in.

WHAT IS AN EXTERNAL LIBRARY? A LIBRARY is a collection of pre-written, pre-compiled code that provides specific functionality. For example, there are libraries for advanced math, graphics, networking, sound, and much more. Using a library saves you from having to “reinvent the wheel.”

A library typically consists of two parts:

  1. HEADER FILES (.h): These contain the function declarations, just like our helper.h did. They tell our compiler WHAT functions are available.
  2. A BINARY LIBRARY FILE (.so on Linux, .dylib on macOS, .dll on Windows): This is the pre-compiled code. It contains the actual function definitions.

THE LINKER The LINKER is a crucial part of the compilation process. After the compiler turns your source code into object files, the linker’s job is to connect everything. It resolves the function calls in your code to the function definitions, whether they are in your own object files (like in lesson 31) or in an external library file (like in this lesson).

Full Source

/**
 * @file 32_linking_external_libraries.c
 * @brief Part 5, Lesson 32: Linking External Libraries
 * @author dunamismax
 * @date 06-15-2025
 *
 * This file serves as the lesson and demonstration for linking against
 * external, pre-compiled libraries. It explains the core concepts
 * through structured comments and provides a runnable example.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * In our last lesson, we learned how to build a project from multiple source
 * files that WE wrote. But what if we want to use code written by someone else?
 * This is where LIBRARIES come in.
 *
 * WHAT IS AN EXTERNAL LIBRARY?
 * A LIBRARY is a collection of pre-written, pre-compiled code that provides
 * specific functionality. For example, there are libraries for advanced math,
 * graphics, networking, sound, and much more. Using a library saves you from
 * having to "reinvent the wheel."
 *
 * A library typically consists of two parts:
 * 1. HEADER FILES (`.h`): These contain the function declarations, just like
 *    our `helper.h` did. They tell our compiler WHAT functions are available.
 * 2. A BINARY LIBRARY FILE (`.so` on Linux, `.dylib` on macOS, `.dll` on Windows):
 *    This is the pre-compiled code. It contains the actual function definitions.
 *
 * THE LINKER
 * The LINKER is a crucial part of the compilation process. After the compiler
 * turns your source code into object files, the linker's job is to connect
 * everything. It resolves the function calls in your code to the function
 * definitions, whether they are in your own object files (like in lesson 31)
 * or in an external library file (like in this lesson).
 */

// --- Part 1: Including the Header ---

// To use a library, you must first include its header file. The C standard
// library provides a powerful math library. Its header is `<math.h>`.
// This gives us access to declarations for functions like `sqrt()` (square root)
// and `pow()` (power).
#include <stdio.h>
#include <math.h>

// --- Part 2: The Main Function ---

int main(void)
{
    // The code below uses functions declared in `<math.h>`.
    // However, including the header only tells the compiler that these functions
    // exist. It doesn't tell the linker WHERE to find the compiled code for them.
    // We'll solve that problem during compilation.

    // --- Using sqrt() ---
    // The `sqrt()` function takes a `double` and returns its square root.
    double number_for_sqrt = 81.0;
    double square_root_result = sqrt(number_for_sqrt);

    printf("Using sqrt(): The square root of %.2f is %.2f.\n",
           number_for_sqrt, square_root_result);

    // --- Using pow() ---
    // The `pow()` function takes two `double` arguments (a base and an exponent)
    // and returns the base raised to the power of the exponent.
    double base = 2.0;
    double exponent = 10.0;
    double power_result = pow(base, exponent);

    printf("Using pow(): %.2f to the power of %.2f is %.2f.\n",
           base, exponent, power_result);

    // --- Using sin() ---
    // Let's try a trigonometric function. `sin()` takes an angle in RADIANS.
    // M_PI is a constant defined in <math.h> for the value of Pi (π).
    double angle_radians = M_PI / 2.0; // 90 degrees
    double sin_result = sin(angle_radians);

    printf("Using sin(): The sine of PI/2 radians is %.2f.\n", sin_result);

    return 0;
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * HOW TO COMPILE AND RUN THIS CODE (THE IMPORTANT PART!):
 *
 * Because we are using functions from an external math library, we must explicitly
 * tell the linker to include that library's code in our final executable.
 *
 * 1. Open a terminal or command prompt.
 * 2. Navigate to the directory where you saved this file.
 * 3. Use the GCC compiler with a special flag to link the math library:
 *
 *    `gcc -Wall -Wextra -std=c11 -o 32_linking_external_libraries 32_linking_external_libraries.c -lm`
 *
 * THE -lm FLAG
 * This is the key to this lesson. The `-l` flag is used to tell the linker
 * which library to link. The name of the math library is `m` (from its file,
 * which is often `libm.so`). So, `-lm` means "link the math library".
 *
 * CONVENTION: The `-l` flags should always come at the END of your command,
 * after your source files.
 *
 * WHAT HAPPENS IF YOU FORGET `-lm`?
 * Try compiling with: `gcc ... -o output 32_linking_external_libraries.c`
 * You will get an "undefined reference to `sqrt`" error (and to `pow`, `sin`).
 * This is the linker telling you, "I see you're calling a function named `sqrt`,
 * but I have no idea where its actual code is!" The `-lm` flag solves this by
 * telling the linker to look inside the math library.
 *
 * 4. Run the executable:
 *    - On Linux/macOS:   `./32_linking_external_libraries`
 *    - On Windows:       `32_linking_external_libraries.exe`
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 32_linking_external_libraries 32_linking_external_libraries.c -lm
./32_linking_external_libraries

Advanced Terminal UI

So far, our programs have been linear: we printf some text, scanf some input, and the text scrolls up and away forever. It’s functional, but not very interactive. Today, we take control of the entire terminal screen.

WHAT IS NCURSES? NCURSES is a powerful external library that allows you to create complex Terminal User Interfaces (TUIs). It gives you precise control over:

  • Cursor position: Move the cursor anywhere on the screen.
  • Text attributes: Display text in bold, underlined, or in color.
  • Windows: Divide the screen into independent sections.
  • Input: Read single keystrokes (like arrow keys) without waiting for Enter.

NCURSES is the foundation for many classic terminal applications, like the text editor vim, the system monitor htop, and many more. This lesson will introduce you to the basic building blocks.

IMPORTANT: This program will take over your terminal screen. It will look different from our previous programs!

Every ncurses program needs a standard setup and cleanup routine.

The endwin() function is CRITICAL. It must be called before your program exits to restore the terminal to its normal, sane state. If you forget this, your terminal prompt might be broken!

NCURSES uses a VIRTUAL SCREEN. When you “print” something, it’s drawn to an in-memory representation of the screen first. Nothing appears on the actual terminal until you call refresh().

Full Source

/**
 * @file 33_advanced_terminal_ui.c
 * @brief Part 5, Lesson 33: Advanced Terminal UI with ncurses
 * @author dunamismax
 * @date 06-15-2025
 *
 * This file serves as the lesson and demonstration for creating a rich
 * Terminal User Interface (TUI) using the ncurses library.
 * It explains the core concepts through structured comments and
 * provides a runnable example of their implementation.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * So far, our programs have been linear: we `printf` some text, `scanf` some
 * input, and the text scrolls up and away forever. It's functional, but not
 * very interactive. Today, we take control of the entire terminal screen.
 *
 * WHAT IS NCURSES?
 * NCURSES is a powerful external library that allows you to create complex
 * Terminal User Interfaces (TUIs). It gives you precise control over:
 *  - Cursor position: Move the cursor anywhere on the screen.
 *  - Text attributes: Display text in bold, underlined, or in color.
 *  - Windows: Divide the screen into independent sections.
 *  - Input: Read single keystrokes (like arrow keys) without waiting for Enter.
 *
 * NCURSES is the foundation for many classic terminal applications, like the
 * text editor `vim`, the system monitor `htop`, and many more. This lesson
 * will introduce you to the basic building blocks.
 *
 * IMPORTANT: This program will take over your terminal screen. It will look
 * different from our previous programs!
 */

// To use ncurses, you must include its header file.
#include <ncurses.h>
#include <unistd.h> // We'll use sleep() for a small delay.

// --- Function Prototypes ---
// Good practice to declare our functions before main.
void initialize_ncurses(void);
void cleanup_ncurses(void);
void draw_ui(void);

// --- The Main Function ---

int main(void)
{
    initialize_ncurses();

    draw_ui();

    // After drawing, we wait for the user to press any key before exiting.
    // getch() is the ncurses function to get a single character of input.
    getch();

    cleanup_ncurses();

    return 0;
}

// --- Part 1: Initialization and Cleanup ---
/*
 * Every ncurses program needs a standard setup and cleanup routine.
 */
void initialize_ncurses(void)
{
    // initscr() is the FIRST ncurses function you must call. It determines the
    // terminal type, allocates memory, and effectively "takes over" the screen.
    initscr();

    // noecho() prevents keys pressed by the user from being automatically
    // displayed on the screen. This gives us full control over what appears.
    noecho();

    // cbreak() makes input available to the program immediately, without
    // requiring the user to press Enter.
    cbreak();

    // keypad() enables the reading of function keys like F1, F2, and arrow keys.
    // `stdscr` is the default WINDOW that represents the entire screen.
    keypad(stdscr, TRUE);

    // --- Color Initialization ---
    // To use colors, we must check if the terminal supports them.
    if (has_colors() == FALSE)
    {
        // If not, we have to exit gracefully.
        endwin(); // This restores the terminal to its normal state.
        printf("Your terminal does not support color\n");
        // exit(1); // We don't have this yet, so we'll let main return.
    }

    // If colors are supported, we must start the color system.
    start_color();

    // Now we can define COLOR PAIRS. A pair is a foreground/background combination.
    // We give each pair a number. Pair 0 is reserved for the default terminal colors.
    // init_pair(pair_number, foreground_color, background_color);
    init_pair(1, COLOR_YELLOW, COLOR_BLACK); // Yellow text on a black background
    init_pair(2, COLOR_CYAN, COLOR_BLACK);   // Cyan text on a black background
    init_pair(3, COLOR_WHITE, COLOR_BLUE);   // White text on a blue background
}

/*
 * The `endwin()` function is CRITICAL. It must be called before your program
 * exits to restore the terminal to its normal, sane state. If you forget this,
 * your terminal prompt might be broken!
 */
void cleanup_ncurses(void)
{
    endwin();
}

// --- Part 2: Drawing to the Screen ---
/*
 * NCURSES uses a VIRTUAL SCREEN. When you "print" something, it's drawn to an
 * in-memory representation of the screen first. Nothing appears on the actual
 * terminal until you call `refresh()`.
 */
void draw_ui(void)
{
    // Clear the screen
    clear();

    // --- Basic Drawing on `stdscr` ---
    // `mvprintw(row, col, "text")` moves the cursor to (row, col) and prints.
    // (0, 0) is the top-left corner.
    mvprintw(0, 0, "Welcome to the NCURSES Interactive Environment!");

    // Let's use our color pairs. `attron` turns an attribute ON.
    attron(COLOR_PAIR(1) | A_BOLD); // Turn on Yellow (Pair 1) and BOLD text.
    mvprintw(2, 2, "This text is Yellow and Bold!");
    attroff(COLOR_PAIR(1) | A_BOLD); // Turn the attributes OFF.

    mvprintw(4, 2, "Normal text again.");

    // The REFRESH command. This flushes the virtual screen buffer and updates
    // the physical terminal to show what we've drawn.
    refresh();

    sleep(2); // Pause for 2 seconds to let the user see the first part.

    // --- Creating a WINDOW ---
    // A WINDOW is a rectangular subsection of the screen that you can draw on
    // independently. This is key for organized UIs.

    int height = 10, width = 60;
    int start_y = 6, start_x = 5;

    // `newwin()` creates a new window.
    WINDOW *info_win = newwin(height, width, start_y, start_x);

    // `box(win, 0, 0)` draws a border around the window using default characters.
    box(info_win, 0, 0);

    // Let's draw inside our new window.
    // We'll use the window-specific version of printw: `wprintw`.
    // But first, let's change the window's color scheme.
    wbkgd(info_win, COLOR_PAIR(3)); // Set background for the window

    // Note: coordinates for drawing are relative to the WINDOW's top-left corner.
    // (1, 1) here is one character down and one right from the window's border.
    mvwprintw(info_win, 1, 1, "This is a new window!");

    attron(COLOR_PAIR(2)); // Use a different color for this next part
    mvwprintw(info_win, 3, 3, "You can draw here without affecting the main screen.");
    mvwprintw(info_win, 5, 3, "Press any key to exit this application.");
    attroff(COLOR_PAIR(2));

    // To display the window, we must refresh IT.
    wrefresh(info_win);
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * Just like we used `-lm` for the math library, we need to tell the linker to
 * include the ncurses library. The library's name is `ncurses`.
 *
 * 1. Open a terminal or command prompt.
 * 2. Navigate to the directory where you saved this file.
 * 3. Use the GCC compiler, adding the `-lncurses` flag AT THE END:
 *
 *    `gcc -Wall -Wextra -std=c11 -o 33_advanced_terminal_ui 33_advanced_terminal_ui.c -lncurses`
 *
 * THE -lncurses FLAG
 * This is the crucial part. `-l` tells the linker to look for a library, and
 * `ncurses` is the name of the library we want. Forgetting this flag will
 * result in "undefined reference" errors for all the ncurses functions like
 * `initscr`, `mvprintw`, etc.
 *
 * 4. Run the executable:
 *    - On Linux/macOS:   `./33_advanced_terminal_ui`
 *    - On Windows:       `33_advanced_terminal_ui.exe`
 *
 *    Your terminal will clear and show the UI we just created! Press any
 *    key to return to your normal command prompt.
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 33_advanced_terminal_ui 33_advanced_terminal_ui.c -lncurses
./33_advanced_terminal_ui

Parsing Data Files

So far, our programs have either been non-persistent or have saved data in a simple, unstructured way. Real-world applications, however, almost always need to read structured data–user profiles, game levels, configuration settings, etc. The process of reading this data and converting it into a useful format (like a struct) is called PARSING.

Today, we will learn how to parse a simple Comma-Separated Values (CSV) file. Each line in the file will represent a single record, with fields separated by commas.

THE GOAL: We will write a program that reads a file named users.dat. Each line of this file will contain a user’s ID, username, level, and active status. Our program will parse each line and load the data into an array of struct User.

We will explore two primary methods for parsing strings: strtok and sscanf. As you will see, sscanf is often safer and more robust.

sscanf is often the best tool for this job. It’s like scanf, but it reads from a string instead of the keyboard. It’s powerful because it can parse complex patterns, and its return value tells you how many items were successfully assigned, which makes error checking very easy.

| - (For Reference) Parsing with strtok() - |

strtok is another function for splitting a string. It works by finding a delimiter, replacing it with a null terminator (\0), and returning a pointer to the start of the token.

WHY IS IT NOT PREFERRED?

  1. It MODIFIES the original string. This can be destructive and surprising.
  2. It is STATEFUL. You call it once with the string, then with NULL for subsequent tokens. This makes the code more complex and means you can’t use it to parse two strings at once (it is not “re-entrant” or thread-safe).

While sscanf is generally better, seeing a strtok implementation is useful as you will encounter it in existing C code.

Example strtok loop:

// Important: strtok modifies the string, so we’d need a copy. // char line_copy[MAX_LINE_LENGTH]; // strcpy(line_copy, line);

char *token = strtok(line, “,”); // Get first token if (token) user.id = atoi(token);

token = strtok(NULL, “,”); // Get next token if (token) strcpy(user.username, token);

… and so on. As you can see, it’s more verbose and requires more care.

Full Source

/**
 * @file 34_parsing_data_files.c
 * @brief Part 5, Lesson 34: Parsing Structured Data Files
 * @author dunamismax
 * @date 06-15-2025
 *
 * This file serves as the lesson and demonstration for reading and parsing
 * structured data files, like CSVs or configuration files.
 * It explains the core concepts through structured comments and
 * provides a runnable example of their implementation.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * So far, our programs have either been non-persistent or have saved data in a
 * simple, unstructured way. Real-world applications, however, almost always
 * need to read structured data--user profiles, game levels, configuration
 * settings, etc. The process of reading this data and converting it into a
 * useful format (like a `struct`) is called PARSING.
 *
 * Today, we will learn how to parse a simple Comma-Separated Values (CSV) file.
 * Each line in the file will represent a single record, with fields separated
 * by commas.
 *
 * THE GOAL:
 * We will write a program that reads a file named `users.dat`. Each line of
 * this file will contain a user's ID, username, level, and active status.
 * Our program will parse each line and load the data into an array of `struct User`.
 *
 * We will explore two primary methods for parsing strings: `strtok` and `sscanf`.
 * As you will see, `sscanf` is often safer and more robust.
 */

#include <stdio.h>
#include <stdlib.h> // For atoi() in the strtok example
#include <string.h> // For strtok(), strcpy()

#define MAX_LINE_LENGTH 256
#define MAX_USERS 10

// --- Part 1: The Data Structure ---
// First, we need a struct to hold the data for a single user.
// This structure directly mirrors the format of a line in our data file.
typedef struct
{
    int id;
    char username[50];
    int level;
    int is_active; // 1 for active, 0 for inactive
} User;

// --- Function Prototypes ---
void parse_with_sscanf(FILE *file);
void print_user(const User *user);

// --- The Main Function ---
// Our program will expect the name of the data file as a command-line argument.
int main(int argc, char *argv[])
{
    // A program that needs a file to work should always check for it.
    if (argc != 2)
    {
        fprintf(stderr, "Usage: %s <datafile>\n", argv[0]);
        return 1;
    }

    FILE *file = fopen(argv[1], "r");
    if (file == NULL)
    {
        // `perror` is a useful function from <stdio.h> that prints our message
        // followed by a system error message for why the operation failed.
        perror("Error opening file");
        return 1;
    }

    printf("--- Parsing data file using sscanf() ---\n");
    parse_with_sscanf(file);

    fclose(file);
    return 0;
}

// --- Part 2: Parsing with sscanf() ---
/*
 * `sscanf` is often the best tool for this job. It's like `scanf`, but it reads
 * from a string instead of the keyboard. It's powerful because it can parse
 * complex patterns, and its return value tells you how many items were
 * successfully assigned, which makes error checking very easy.
 */
void parse_with_sscanf(FILE *file)
{
    char line[MAX_LINE_LENGTH];
    User users[MAX_USERS];
    int user_count = 0;

    // We read the file line by line using `fgets`. This is safe because it
    // prevents buffer overflows by never reading more than `MAX_LINE_LENGTH`
    // characters at a time.
    while (fgets(line, sizeof(line), file) != NULL && user_count < MAX_USERS)
    {
        User current_user;

        // This is the core of the sscanf method. Let's break down the format string:
        // "%d"       - Reads an integer (the ID).
        // ","        - Matches and discards a literal comma.
        // "%49[^,]"  - This is the clever part for reading the username.
        //   `%[...]` is a scanset. It reads characters as long as they are in the set.
        //   `^` negates the set, so `[^,]` reads characters as long as they are NOT a comma.
        //   `49` is a width limit to prevent buffer overflow on `username[50]`.
        // ",%d,%d"   - Reads comma, integer, comma, integer for level and status.
        //
        // CRITICAL: We pass POINTERS to the variables where sscanf should store
        // the data. For integers, we use the address-of operator `&`. For the
        // character array `username`, the name itself decays to a pointer.
        int items_scanned = sscanf(line, "%d,%49[^,],%d,%d",
                                   &current_user.id,
                                   current_user.username,
                                   &current_user.level,
                                   &current_user.is_active);

        // We expect to scan 4 items. If we don't, the line was malformed.
        if (items_scanned == 4)
        {
            users[user_count] = current_user;
            user_count++;
        }
        else
        {
            fprintf(stderr, "Warning: Malformed line skipped: %s", line);
        }
    }

    // Now, let's print out the data we successfully parsed.
    printf("\nSuccessfully parsed %d user records:\n", user_count);
    for (int i = 0; i < user_count; i++)
    {
        print_user(&users[i]);
    }
}

/**
 * @brief A helper function to print the contents of a User struct.
 * @param user A pointer to the User struct to be printed.
 */
void print_user(const User *user)
{
    // The formatting specifiers `%-5d` and `%-15s` left-align the output
    // in a field of a certain width, creating nice columns.
    printf("  User ID: %-5d | Username: %-15s | Level: %-3d | Active: %s\n",
           user->id,
           user->username,
           user->level,
           user->is_active ? "Yes" : "No"); // Using a ternary operator for clean output.
}

/*
 * =====================================================================================
 * |                    - (For Reference) Parsing with strtok() -                      |
 * =====================================================================================
 *
 * `strtok` is another function for splitting a string. It works by finding a
 * delimiter, replacing it with a null terminator (`\0`), and returning a pointer
 * to the start of the token.
 *
 * WHY IS IT NOT PREFERRED?
 * 1. It MODIFIES the original string. This can be destructive and surprising.
 * 2. It is STATEFUL. You call it once with the string, then with NULL for
 *    subsequent tokens. This makes the code more complex and means you can't
 *    use it to parse two strings at once (it is not "re-entrant" or thread-safe).
 *
 * While `sscanf` is generally better, seeing a `strtok` implementation is useful
 * as you will encounter it in existing C code.
 *
 * Example `strtok` loop:
 *
 *   // Important: `strtok` modifies the string, so we'd need a copy.
 *   // char line_copy[MAX_LINE_LENGTH];
 *   // strcpy(line_copy, line);
 *
 *   char *token = strtok(line, ","); // Get first token
 *   if (token) user.id = atoi(token);
 *
 *   token = strtok(NULL, ","); // Get next token
 *   if (token) strcpy(user.username, token);
 *
 *   ... and so on. As you can see, it's more verbose and requires more care.
 */

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * HOW TO COMPILE AND RUN THIS CODE:
 *
 * This program requires a data file to read. You must create it first.
 *
 * 1. CREATE THE DATA FILE:
 *    In the same directory as this C file, create a new file named `users.dat`.
 *    Copy and paste the following content into it:
 *
 *    101,alpha_coder,15,1
 *    102,bit_wizard,22,1
 *    205,kernel_hacker,45,0
 *    this is a bad line and should be skipped
 *    310,logic_lord,33,1
 *    404,syntax_sorcerer,99,1
 *
 * 2. COMPILE THE C FILE:
 *    Open a terminal and navigate to the directory.
 *    `gcc -Wall -Wextra -std=c11 -o 34_parsing_data_files 34_parsing_data_files.c`
 *
 * 3. RUN THE EXECUTABLE:
 *    You must provide the name of the data file as a command-line argument.
 *    - On Linux/macOS:   `./34_parsing_data_files users.dat`
 *    - On Windows:       `34_parsing_data_files.exe users.dat`
 *
 *    The program will read `users.dat`, parse its contents, print a warning for
 *    the bad line, and then display the structured data it successfully extracted.
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 34_parsing_data_files 34_parsing_data_files.c
./34_parsing_data_files

Capstone: Text Adventure

Welcome to the final project. This is it.

Every lesson, from printf to pointers, from malloc to Makefiles, has been leading to this. You are about to build a complete, interactive application from the ground up. This project doesn’t introduce many new C features; instead, it challenges you to synthesize everything you’ve already learned.

CONCEPTS YOU WILL USE IN THIS PROJECT:

  • Foundational Logic: Variables, if/else, for/while loops, functions.
  • Data Structures: structs to model the game world, enums for clarity.
  • Memory Management: malloc and free to dynamically create the world.
  • Pointers: The entire game world is connected by POINTERS. This is the ultimate demonstration of their power.
  • File I/O: The game world is loaded from an external data file (world.map).
  • Parsing: sscanf is used to parse the world data file.
  • Command-Line Arguments: The program takes the map file as an argument.
  • External Libraries: We use the ncurses library for an advanced terminal UI.
  • Build Tooling: Lesson 31 introduced Makefiles, but this capstone stays in one source file so you can build it directly with a compiler command.
  • Advanced Techniques: We’ll even use an array of structs with function pointers for a clean command handling system.

You are no longer a beginner. You are a C developer. Let’s build something awesome.

This function demonstrates dynamic allocation, file I/O, and robust parsing.

and calls it.

This is CRITICAL for preventing memory leaks. A C developer must always clean up their own mess.

Full Source

/**
 * @file 35_capstone_awesome_text_adventure.c
 * @brief Part 5, Lesson 35: Capstone - Awesome Text Adventure
 * @author dunamismax
 * @date 06-15-2025
 *
 * This file contains the source code for the final capstone lesson.
 * It pulls together many earlier topics into a single ncurses-based
 * text adventure that loads a map file at runtime.
 */

/*
 * =====================================================================================
 * |                                   - LESSON START -                                  |
 * =====================================================================================
 *
 * Welcome to the final project. This is it.
 *
 * Every lesson, from `printf` to pointers, from `malloc` to Makefiles, has been
 * leading to this. You are about to build a complete, interactive application
 * from the ground up. This project doesn't introduce many new C features;
 * instead, it challenges you to synthesize everything you've already learned.
 *
 * CONCEPTS YOU WILL USE IN THIS PROJECT:
 * - Foundational Logic: Variables, `if`/`else`, `for`/`while` loops, functions.
 * - Data Structures: `struct`s to model the game world, `enum`s for clarity.
 * - Memory Management: `malloc` and `free` to dynamically create the world.
 * - Pointers: The entire game world is connected by POINTERS. This is the ultimate
 *   demonstration of their power.
 * - File I/O: The game world is loaded from an external data file (`world.map`).
 * - Parsing: `sscanf` is used to parse the world data file.
 * - Command-Line Arguments: The program takes the map file as an argument.
 * - External Libraries: We use the `ncurses` library for an advanced terminal UI.
 * - Build Tooling: Lesson 31 introduced Makefiles, but this capstone stays in
 *   one source file so you can build it directly with a compiler command.
 * - Advanced Techniques: We'll even use an array of structs with function
 *   pointers for a clean command handling system.
 *
 * You are no longer a beginner. You are a C developer. Let's build something awesome.
 */

// --- Standard and External Library Includes ---
#include <ctype.h>  // For tolower()
#include <errno.h>  // For robust numeric parsing
#include <limits.h> // For INT_MIN and INT_MAX
#include <ncurses.h> // For the advanced Terminal User Interface (TUI)
#include <stdio.h>
#include <stdlib.h> // For malloc, free, exit
#include <string.h> // For string manipulation functions

// --- Game Constants and Enums ---
#define MAX_DIRECTIONS 4
#define MAX_ROOMS 100
#define MAX_LOG_MESSAGES 10
#define INPUT_BUFFER_SIZE 100
#define ROOM_DESCRIPTION_SIZE 512
#define WORLD_LINE_BUFFER_SIZE 1024

// Using an enum makes direction-related code much more readable and safe.
typedef enum
{
    NORTH,
    SOUTH,
    EAST,
    WEST
} Direction;

// --- Core Data Structures ---
// These structs are the blueprint for our entire game world.

// Forward declaration is needed because GameState and Room reference each other.
struct GameState;

typedef struct Room
{
    int id;
    char description[ROOM_DESCRIPTION_SIZE];
    // This is the key: an array of POINTERS to other Room structs.
    // This is how we form the "graph" of our world map.
    struct Room *exits[MAX_DIRECTIONS];
} Room;

typedef struct Player
{
    Room *current_room; // A pointer to the room the player is currently in.
} Player;

typedef struct
{
    char *command;
    // A function pointer! This lets us create a clean, data-driven command system.
    void (*handler)(struct GameState *game, char *argument);
} Command;

// The main struct to hold the entire state of our running game.
typedef struct GameState
{
    Player player;
    Room *all_rooms[MAX_ROOMS];
    int num_rooms;
    int game_should_close;

    // For the ncurses UI
    WINDOW *main_win;
    WINDOW *status_win;
    WINDOW *input_win;
    char *log_messages[MAX_LOG_MESSAGES];
    int log_count;
} GameState;

// --- Function Prototypes ---
// Grouping prototypes makes the code easier to navigate.

// Game Lifecycle
void game_loop(GameState *game);
void cleanup(GameState *game);

// World Loading & Parsing
GameState *load_world(const char *filename);
int parse_room(char *line, GameState *game);
int parse_link(char *line, GameState *game);
Room *find_room_by_id(GameState *game, int id);
const char *direction_to_string(Direction d);
int read_world_line(FILE *file, char *buffer, size_t size);
char *skip_whitespace(char *text);
int parse_int_token(char **cursor, int *value);
int has_only_trailing_whitespace(char *text);
char *duplicate_string(const char *text);

// Command Handling
void parse_and_execute_command(GameState *game, char *input);
void handle_quit(GameState *game, char *argument);
void handle_look(GameState *game, char *argument);
void handle_go(GameState *game, char *argument);
void handle_help(GameState *game, char *argument);

// UI Functions (ncurses)
void ui_init(GameState *game);
void ui_draw(GameState *game);
void ui_get_input(GameState *game, char *buffer);
void ui_log(GameState *game, const char *message);
void ui_cleanup(GameState *game);

// --- Main Program Entry ---
int read_world_line(FILE *file, char *buffer, size_t size)
{
    size_t length;

    if (fgets(buffer, size, file) == NULL)
    {
        return 0;
    }

    length = strcspn(buffer, "\n");
    if (buffer[length] == '\n')
    {
        buffer[length] = '\0';
        return 1;
    }

    if (!feof(file))
    {
        int ch;

        while ((ch = fgetc(file)) != '\n' && ch != EOF)
        {
            // Discard the rest of an overlong line so the next read starts cleanly.
        }
        buffer[0] = '\0';
        return -1;
    }

    return 1;
}

char *skip_whitespace(char *text)
{
    while (isspace((unsigned char)*text))
    {
        text++;
    }

    return text;
}

int parse_int_token(char **cursor, int *value)
{
    char *endptr;
    long parsed;

    *cursor = skip_whitespace(*cursor);
    if (**cursor == '\0')
    {
        return 0;
    }

    errno = 0;
    parsed = strtol(*cursor, &endptr, 10);
    if (errno != 0 || endptr == *cursor || parsed < INT_MIN || parsed > INT_MAX)
    {
        return 0;
    }

    *value = (int)parsed;
    *cursor = endptr;
    return 1;
}

int has_only_trailing_whitespace(char *text)
{
    text = skip_whitespace(text);
    return *text == '\0';
}

char *duplicate_string(const char *text)
{
    size_t length = strlen(text) + 1;
    char *copy = malloc(length);

    if (!copy)
    {
        return NULL;
    }

    memcpy(copy, text, length);
    return copy;
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        // We use fprintf to stderr for error messages.
        fprintf(stderr, "Usage: %s <world_map_file>\n", argv[0]);
        return 1;
    }

    // --- Initialization ---
    // Load the world from the file specified on the command line.
    GameState *game = load_world(argv[1]);
    if (game == NULL)
    {
        // load_world will have already printed an error.
        return 1;
    }

    ui_init(game); // Initialize the ncurses interface

    // Set the player's starting location.
    game->player.current_room = find_room_by_id(game, 0);
    if (game->player.current_room == NULL)
    {
        ui_cleanup(game);
        cleanup(game);
        fprintf(stderr, "Error: Map has no starting room (ID 0).\n");
        return 1;
    }

    // --- Main Game Loop ---
    ui_log(game, "Welcome to the Awesome Text Adventure! Type 'help' for commands.");
    handle_look(game, NULL); // Give the player their initial view.
    game_loop(game);

    // --- Cleanup ---
    // This is crucial for both ncurses and memory management.
    ui_cleanup(game);
    cleanup(game);

    printf("Thank you for playing!\n");
    return 0;
}

// =====================================================================================
// |                            - WORLD LOADING & PARSING -                            |
// =====================================================================================

/**
 * @brief Loads the entire game world from a data file.
 * This function demonstrates dynamic allocation, file I/O, and robust parsing.
 */
GameState *load_world(const char *filename)
{
    // Dynamically allocate the main GameState struct.
    GameState *game = malloc(sizeof(GameState));
    int line_number = 0;
    int line_status;
    if (!game)
        return NULL;
    // ALWAYS initialize allocated memory.
    memset(game, 0, sizeof(GameState));

    FILE *file = fopen(filename, "r");
    if (!file)
    {
        perror("Error opening world file");
        free(game);
        return NULL;
    }

    char line[WORLD_LINE_BUFFER_SIZE];

    // --- TWO-PASS LOADING ---
    // This is a common and robust technique for loading interconnected data.
    // Pass 1: Read all room definitions and allocate memory for them.
    while ((line_status = read_world_line(file, line, sizeof(line))) != 0)
    {
        line_number++;
        if (line_status < 0)
        {
            fprintf(stderr, "Error: world file line %d is too long.\n", line_number);
            fclose(file);
            cleanup(game);
            return NULL;
        }

        if (strncmp(line, "room", 4) == 0)
        {
            if (!parse_room(line, game))
            {
                fprintf(stderr, "Error parsing room definition on line %d.\n", line_number);
                fclose(file);
                cleanup(game);
                return NULL;
            }
        }
    }

    // Rewind the file pointer to the beginning for the second pass.
    rewind(file);
    line_number = 0;

    // Pass 2: Read all link definitions and connect the already-created rooms.
    while ((line_status = read_world_line(file, line, sizeof(line))) != 0)
    {
        line_number++;
        if (line_status < 0)
        {
            fprintf(stderr, "Error: world file line %d is too long.\n", line_number);
            fclose(file);
            cleanup(game);
            return NULL;
        }

        if (strncmp(line, "link", 4) == 0)
        {
            if (!parse_link(line, game))
            {
                fprintf(stderr, "Error parsing link definition on line %d.\n", line_number);
                fclose(file);
                cleanup(game);
                return NULL;
            }
        }
    }

    fclose(file);
    return game;
}

/**
 * @brief Parses a 'room' line and creates a Room struct.
 */
int parse_room(char *line, GameState *game)
{
    char *cursor = line;
    if (game->num_rooms >= MAX_ROOMS)
        return 0; // Guard against overflow.

    int id;
    char desc[ROOM_DESCRIPTION_SIZE];
    size_t desc_length = 0;

    if (strncmp(cursor, "room", 4) != 0 || !isspace((unsigned char)cursor[4]))
    {
        return 0;
    }
    cursor += 4;

    if (!parse_int_token(&cursor, &id))
    {
        return 0;
    }

    cursor = skip_whitespace(cursor);
    if (*cursor != '"')
    {
        return 0;
    }
    cursor++;

    while (*cursor != '"' && *cursor != '\0')
    {
        if (desc_length >= sizeof(desc) - 1)
        {
            return 0;
        }

        desc[desc_length++] = *cursor;
        cursor++;
    }

    if (*cursor != '"')
    {
        return 0;
    }

    desc[desc_length] = '\0';
    cursor++;

    if (!has_only_trailing_whitespace(cursor) || find_room_by_id(game, id) != NULL)
    {
        return 0;
    }

    Room *new_room = malloc(sizeof(Room));
    if (!new_room)
        return 0;

    memset(new_room, 0, sizeof(Room)); // Initialize room exits to NULL.
    new_room->id = id;
    memcpy(new_room->description, desc, desc_length + 1);

    game->all_rooms[game->num_rooms++] = new_room;
    return 1;
}

/**
 * @brief Parses a 'link' line and connects two existing rooms.
 */
int parse_link(char *line, GameState *game)
{
    char *cursor = line;
    int from_id, to_id;
    char dir_str[10];
    size_t dir_length = 0;

    if (strncmp(cursor, "link", 4) != 0 || !isspace((unsigned char)cursor[4]))
    {
        return 0;
    }
    cursor += 4;

    if (!parse_int_token(&cursor, &from_id))
    {
        return 0;
    }

    cursor = skip_whitespace(cursor);
    if (*cursor == '\0')
    {
        return 0;
    }

    while (*cursor != '\0' && !isspace((unsigned char)*cursor))
    {
        if (dir_length >= sizeof(dir_str) - 1)
        {
            return 0;
        }

        dir_str[dir_length++] = *cursor;
        cursor++;
    }
    dir_str[dir_length] = '\0';

    if (dir_length == 0 || !parse_int_token(&cursor, &to_id) || !has_only_trailing_whitespace(cursor))
    {
        return 0;
    }

    Room *from_room = find_room_by_id(game, from_id);
    Room *to_room = find_room_by_id(game, to_id);

    if (!from_room || !to_room)
        return 0; // One of the rooms doesn't exist.

    Direction dir;
    if (strcmp(dir_str, "n") == 0)
        dir = NORTH;
    else if (strcmp(dir_str, "s") == 0)
        dir = SOUTH;
    else if (strcmp(dir_str, "e") == 0)
        dir = EAST;
    else if (strcmp(dir_str, "w") == 0)
        dir = WEST;
    else
        return 0; // Invalid direction.

    // This is the magic: we store a POINTER to the 'to' room in the 'from' room's exit array.
    from_room->exits[dir] = to_room;
    return 1;
}

/**
 * @brief A helper function to find a room pointer from its integer ID.
 */
Room *find_room_by_id(GameState *game, int id)
{
    for (int i = 0; i < game->num_rooms; i++)
    {
        if (game->all_rooms[i]->id == id)
        {
            return game->all_rooms[i];
        }
    }
    return NULL; // Not found
}

const char *direction_to_string(Direction d)
{
    switch (d)
    {
    case NORTH:
        return "north";
    case SOUTH:
        return "south";
    case EAST:
        return "east";
    case WEST:
        return "west";
    }
    return "unknown";
}

// =====================================================================================
// |                                - COMMAND HANDLING -                               |
// =====================================================================================

// This array of structs is our command table. It pairs a command string with
// the function pointer that handles it. This is a very clean, scalable design.
Command command_table[] = {
    {"quit", handle_quit},
    {"exit", handle_quit},
    {"look", handle_look},
    {"l", handle_look},
    {"go", handle_go},
    {"north", handle_go},
    {"south", handle_go},
    {"east", handle_go},
    {"west", handle_go},
    {"n", handle_go},
    {"s", handle_go},
    {"e", handle_go},
    {"w", handle_go},
    {"help", handle_help},
    {NULL, NULL} // Sentinel to mark the end of the array.
};

/**
 * @brief Processes the user's raw input string, finds the correct command handler,
 *        and calls it.
 */
void parse_and_execute_command(GameState *game, char *input)
{
    char *verb, *argument;

    // Convert input to lowercase for case-insensitive matching.
    for (int i = 0; input[i]; i++)
    {
        input[i] = (char)tolower((unsigned char)input[i]);
    }

    verb = strtok(input, " \n"); // Tokenize the string by space or newline.
    if (!verb)
        return; // Empty input

    argument = strtok(NULL, " \n");

    // Handle single-word movement commands like "north"
    if (strcmp(verb, "n") == 0 || strcmp(verb, "north") == 0 ||
        strcmp(verb, "s") == 0 || strcmp(verb, "south") == 0 ||
        strcmp(verb, "e") == 0 || strcmp(verb, "east") == 0 ||
        strcmp(verb, "w") == 0 || strcmp(verb, "west") == 0)
    {
        argument = verb; // The argument to "go" is the verb itself.
        verb = "go";
    }

    for (int i = 0; command_table[i].command != NULL; i++)
    {
        if (strcmp(verb, command_table[i].command) == 0)
        {
            // We found a match! Call the associated function pointer.
            command_table[i].handler(game, argument);
            return;
        }
    }

    ui_log(game, "I don't understand that command.");
}

void handle_quit(GameState *game, char *argument)
{
    (void)argument;
    game->game_should_close = 1;
}

void handle_look(GameState *game, char *argument)
{
    (void)argument;
    Room *room = game->player.current_room;
    ui_log(game, room->description);

    char exits_str[100] = "Exits: ";
    int found_exit = 0;
    for (int i = 0; i < MAX_DIRECTIONS; i++)
    {
        if (room->exits[i])
        {
            strcat(exits_str, direction_to_string(i));
            strcat(exits_str, " ");
            found_exit = 1;
        }
    }
    if (found_exit)
    {
        ui_log(game, exits_str);
    }
    else
    {
        ui_log(game, "There are no obvious exits.");
    }
}

void handle_go(GameState *game, char *argument)
{
    if (!argument)
    {
        ui_log(game, "Go where?");
        return;
    }

    int dir = -1;
    if (strcmp(argument, "n") == 0 || strcmp(argument, "north") == 0)
        dir = NORTH;
    else if (strcmp(argument, "s") == 0 || strcmp(argument, "south") == 0)
        dir = SOUTH;
    else if (strcmp(argument, "e") == 0 || strcmp(argument, "east") == 0)
        dir = EAST;
    else if (strcmp(argument, "w") == 0 || strcmp(argument, "west") == 0)
        dir = WEST;

    if (dir == -1)
    {
        ui_log(game, "That's not a valid direction.");
        return;
    }

    Room *next_room = game->player.current_room->exits[dir];
    if (next_room)
    {
        game->player.current_room = next_room;
        handle_look(game, NULL); // Automatically look after moving.
    }
    else
    {
        ui_log(game, "You can't go that way.");
    }
}

void handle_help(GameState *game, char *argument)
{
    (void)argument;
    ui_log(game, "--- Available Commands ---");
    ui_log(game, "look (l): Describe the current room and exits.");
    ui_log(game, "go <dir>: Move in a direction (north, south, east, west, or n,s,e,w).");
    ui_log(game, "help: Show this help message.");
    ui_log(game, "quit/exit: Leave the game.");
}

// =====================================================================================
// |                                - NCURSES UI CODE -                                |
// =====================================================================================

void ui_init(GameState *game)
{
    initscr();
    cbreak();
    noecho();
    keypad(stdscr, TRUE);

    int height, width;
    getmaxyx(stdscr, height, width);

    // Create a main window for game text, a status window, and an input window.
    game->main_win = newwin(height - 5, width, 0, 0);
    game->status_win = newwin(3, width, height - 5, 0);
    game->input_win = newwin(2, width, height - 2, 0);

    scrollok(game->main_win, TRUE); // Allow the main window to scroll.

    wborder(game->status_win, 0, 0, 0, 0, 0, 0, 0, 0);
    wborder(game->input_win, ' ', ' ', 0, 0, '>', '<', 0, 0);

    refresh();
}

void ui_draw(GameState *game)
{
    // Draw status window
    box(game->status_win, 0, 0);
    mvwprintw(game->status_win, 1, 2, "Location: Room %d", game->player.current_room->id);

    // Draw input window
    box(game->input_win, 0, 0);
    mvwprintw(game->input_win, 1, 2, "> ");

    // Draw main window log messages
    werase(game->main_win);
    for (int i = 0; i < game->log_count; i++)
    {
        mvwprintw(game->main_win, i + 1, 2, "%s", game->log_messages[i]);
    }

    // Refresh all windows to show changes
    wrefresh(game->main_win);
    wrefresh(game->status_win);
    wrefresh(game->input_win);
}

void ui_log(GameState *game, const char *message)
{
    char *message_copy = duplicate_string(message);

    if (!message_copy)
    {
        return;
    }

    // If the log is full, shift all messages up.
    if (game->log_count >= MAX_LOG_MESSAGES)
    {
        free(game->log_messages[0]);
        for (int i = 0; i < MAX_LOG_MESSAGES - 1; i++)
        {
            game->log_messages[i] = game->log_messages[i + 1];
        }
        game->log_count--;
    }

    game->log_messages[game->log_count++] = message_copy;
}

void ui_get_input(GameState *game, char *buffer)
{
    // Move cursor to input window and get string from user.
    wmove(game->input_win, 1, 4);
    wgetnstr(game->input_win, buffer, INPUT_BUFFER_SIZE - 1);
}

void ui_cleanup(GameState *game)
{
    delwin(game->main_win);
    delwin(game->status_win);
    delwin(game->input_win);
    endwin(); // Restore terminal to normal state. CRITICAL.
}

// =====================================================================================
// |                                 - GAME LIFECYCLE -                                |
// =====================================================================================

void game_loop(GameState *game)
{
    char input_buffer[INPUT_BUFFER_SIZE];

    while (!game->game_should_close)
    {
        ui_draw(game);
        ui_get_input(game, input_buffer);
        parse_and_execute_command(game, input_buffer);
    }
}

/**
 * @brief Frees all dynamically allocated memory.
 * This is CRITICAL for preventing memory leaks. A C developer must always
 * clean up their own mess.
 */
void cleanup(GameState *game)
{
    // Free all the room structs
    for (int i = 0; i < game->num_rooms; i++)
    {
        free(game->all_rooms[i]);
    }
    // Free all the log message strings
    for (int i = 0; i < game->log_count; i++)
    {
        free(game->log_messages[i]);
    }
    // Finally, free the main GameState struct itself.
    free(game);
}

/*
 * =====================================================================================
 * |                                    - LESSON END -                                   |
 * =====================================================================================
 *
 * HOW TO COMPILE AND RUN THIS PROJECT:
 *
 * This lesson is still a single-source-file program. You can compile it with
 * one command, then point it at any compatible map file you create. Because it
 * depends on `ncurses`, it is aimed at Unix-like environments unless you install
 * a compatible curses implementation for your platform.
 *
 * 1. CREATE THE DATA FILE:
 *    In the same directory, create a file named `world.map`. This file defines
 *    the rooms and their connections. Copy this content into it:
 *
 *    room 0 "You are in a dusty, forgotten library. Ancient tomes line the walls. A grand wooden door stands to the north."
 *    room 1 "You've entered a vast, cold cavern. The sound of dripping water echoes around you. A narrow passage leads east."
 *    room 2 "This is a small, cramped tunnel. It smells of damp earth. The path continues west, and you see a faint light to the south."
 *    room 3 "You emerge into a treasure room! Piles of gold and jewels glitter in the torchlight. A heavy stone door is to the north."
 *
 *    link 0 n 1
 *    link 1 s 0
 *    link 1 e 2
 *    link 2 w 1
 *    link 2 s 3
 *    link 3 n 2
 *
 * 2. COMPILE THE PROGRAM:
 *    Open a terminal in the directory containing this file and link `ncurses`:
 *
 *      `cc -Wall -Wextra -std=c11 -o 35_capstone_awesome_text_adventure 35_capstone_awesome_text_adventure.c -lncurses`
 *
 * 3. RUN THE GAME:
 *    Execute the program, passing the map file as a command-line argument:
 *
 *      `./35_capstone_awesome_text_adventure world.map`
 *
 *    Your terminal will transform, and the game will begin.
 */

How to Compile and Run

cc -Wall -Wextra -std=c11 -o 35_capstone_awesome_text_adventure 35_capstone_awesome_text_adventure.c -lncurses
./35_capstone_awesome_text_adventure world.map