C

Introduction To C

wwww

  1. Complete C Programming Learning Roadmap
    1. Foundations
      1. Introduction: Why C Still Matters in 2026
      2. What This Guide Covers
      3. Chapter 1: What Is C, Really?
      4. Chapter 2: A Short but Important History
      5. Chapter 3: Why Learn C?
      6. Chapter 4: What You Need Before Starting
      7. Chapter 5: Complete Setup Guide for Windows, Mac, and Linux
        1. Windows Setup (Complete Guide)
        2. macOS Setup (Complete Guide)
        3. Linux (Ubuntu) Setup (Complete Guide)
      8. Chapter 6: What Actually Happens When You Compile?
      9. Chapter 7: Using AI Effectively While Learning C
  2. Basic Syntax, Variables, Constants and Data Types
    1. 1. Structure of a C Program
      1. #include, main(), return 0, and braces
    2. 2. Header Files
      1. stdio.h, stdlib.h, string.h, and more
    3. 3. Main Function
      1. int main(void), int main(int argc, char *argv[])
    4. 4. Variables
      1. Declaration, initialization, and assignment
    5. 5. Constants
      1. const keyword and #define preprocessor
    6. 6. Keywords
      1. Reserved words in C
    7. 7. Primitive Data Types
      1. int, float, double, char, void
    8. 8. Type Modifiers
      1. signed, unsigned, short, long
    9. 9. Operators
      1. Arithmetic, relational, logical, assignment, bitwise
    10. 10. Expressions
      1. Value-producing combinations of operators and operands
    11. 11. Comments
      1. Single-line and multi-line comments
    12. 12. Input and Output
      1. printf() – Formatted output function
      2. scanf() – Formatted input function
    13. 13. Format Specifiers
      1. %d, %f, %c, %s, %p, and more
    14. 14. Input Validation
      1. Checking scanf return value and clearing input buffer
    15. 15. File I/O
      1. fopen, fclose, fprintf, fscanf, fread, fwrite
  3. Control Structures
    1. 1. if Statement
      1. Simple conditional branching
    2. 2. if-else Statement
      1. Conditional branching with two paths
    3. 3. switch Statement
      1. Multi-way branching
    4. 4. for Loop
      1. Counting loop
    5. 5. while Loop
      1. Conditional loop
    6. 6. do-while Loop
      1. Post-test loop
    7. 7. break Statement
      1. Exit loop early
    8. 8. continue Statement
      1. Skip current iteration
  4. Functions
    1. 1. Function Declaration
      1. Prototype and forward declaration
    2. 2. Function Definition
      1. The function body and implementation
    3. 3. Parameters
      1. Function parameters and arguments
    4. 4. Return Values
      1. Returning values from functions
    5. 5. Recursion
      1. Functions that call themselves
    6. 6. Scope
      1. Local, global, and block scope
  5. Arrays and Strings
    1. 1. Arrays
      1. Fixed-size collections of same type
    2. 2. Multidimensional Arrays
      1. 2D, 3D, and beyond
    3. 3. Character Arrays
      1. Strings as char arrays
    4. 4. Strings
      1. The null-terminated string concept
    5. 5. String Library Functions
      1. strlen, strcpy, strcat, strcmp, and more
  6. Pointers and Memory Management
    1. 1. Pointers
      1. Variables that store memory addresses
    2. 2. Pointer Arithmetic
      1. Incrementing, decrementing, and array traversal
    3. 3. Dynamic Memory Allocation
      1. malloc, calloc, realloc, free
    4. 4. Memory Leaks
      1. Causes, detection, and prevention
  7. Structures, Unions and Enumerations
    1. 1. Structures
      1. Custom data types grouping related variables
    2. 2. Nested Structures
      1. Structures inside structures
    3. 3. Unions
      1. Overlapping memory for different types
    4. 4. Enumerations
      1. Named integer constants
    5. 5. typedef
      1. Creating type aliases
  8. Advanced C Programming
    1. 1. Preprocessor Directives
      1. #include, #define, #if, #ifdef, #ifndef, #endif
    2. 2. Macros
      1. Text replacement and function-like macros
    3. 3. Header Files
      1. Creating and using custom header files
    4. 4. Function Pointers
      1. Pointers to functions
    5. 5. Bitwise Operations
      1. &, |, ^, ~, <<, >> and their uses
  9. Data Structures and Algorithms
    1. 1. Linked Lists
      1. Singly and doubly linked lists
    2. 2. Stacks
      1. LIFO data structure
    3. 3. Queues
      1. FIFO data structure
  10. System Programming
    1. 1. Processes (fork and wait)
      1. Creating and managing processes
    2. 2. Threads (pthread)
      1. Creating and synchronizing threads
  11. Practical Projects
    1. 1. Calculator
      1. Basic arithmetic calculator
  12. Final Advice

Complete C Programming Learning Roadmap

Foundations

Introduction: Why C Still Matters in 2026

When you open your laptop right now, you’re interacting with code written in C. Your operating system kernel, device drivers, embedded systems in your car, medical devices, and even the Python interpreter you might use—all of these rely heavily on C. Despite being over five decades old, C remains one of the most influential and widely-used programming languages in the world.

C has a reputation for being “hard.” It’s not—it’s just unforgiving, and this is a very different thing. Python and JavaScript let you get away with sloppy habits because they quietly manage memory and types behind the scenes. C refuses to do that. It forces you to actually understand what your computer is doing at every step—where a variable lives in memory, how big it is, who owns it, and when it gets destroyed. This honesty is precisely why C still powers operating systems, embedded systems, databases, and compilers decades after “easier” languages appeared. It’s also exactly why learning it properly makes you a noticeably stronger programmer in every other language you touch afterward.

The goal of this guide is comprehensive: to take you from complete beginner to confident, job-ready C programmer, on Windows, Mac, or Linux. We provide plain-English explanations first, followed by a full professional-grade roadmap covering every stage from your very first “Hello, World!” to interview preparation and career readiness.

What This Guide Covers

This comprehensive introduction covers everything you need to know before diving into actual coding. We explore what C actually is and why it was created, the complete setup process for every major operating system, a detailed stage-by-stage roadmap of everything you’ll learn, how to use AI effectively without becoming dependent on it, what happens when you compile code through the five-stage pipeline, and common pitfalls and how to avoid them. By the end of this guide, you’ll have a crystal-clear understanding of C and a complete learning path that will take you from absolute beginner to professional developer.

Chapter 1: What Is C, Really?

Before you write a single line of C code, you need to understand what this language actually is and why it was created. Let’s break down the definition slowly and carefully.

C is a compiled, general-purpose programming language that gives you fine-grained control over your computer’s memory and hardware, with a minimal runtime and direct access to system resources.

Let’s unpack every word in that sentence, because each one matters.

Compiled vs. Interpreted Languages

Here’s the fundamental distinction that shapes everything about C: C is compiled, not interpreted. When you write a program in Python or JavaScript, an interpreter reads your code line-by-line and executes it, every single time the program runs. This is convenient for development because you can make changes and see results immediately, but it comes with a performance cost. The interpreter is essentially translating your code on the fly while it’s running, which adds overhead.

When you write C, a separate tool called a compiler reads your human-readable code and translates it into machine code—raw binary instructions your CPU can execute directly. This happens as an extra step before you ever run your program. The payoff is speed: a compiled C program typically runs many times faster than an equivalent interpreted program because the CPU is executing instructions meant exactly for it, with no translation happening on the fly.

Here’s what the process looks like at a high level:

Source Code (.c) → Compiler → Machine Code (.exe or .out) → CPU Executes

This compilation step is why C can feel slower to develop in—you have to compile before you can test—but the resulting performance is unmatched by most other languages.

General-Purpose Language

C isn’t built for one narrow job. HTML structures web pages, SQL queries databases, and CSS styles documents—these are domain-specific languages. C is a general-purpose language that can build operating systems, embedded firmware, device drivers, databases, compilers, and even the interpreters for other languages. This flexibility is part of why C has survived for over five decades without becoming obsolete. Learning C opens doors to countless industries and applications because the language adapts to the problem, not the other way around.

Minimal Runtime and Direct Hardware Access

This is the feature that makes C both powerful and challenging. In most modern languages, a background process called a garbage collector automatically frees memory you’re no longer using. You don’t have to think about when memory is allocated or released—the language handles it for you. In C, you do this yourself, deciding exactly when memory is allocated and released using functions like malloc() and free(). This is powerful for several reasons including no unpredictable pauses, memory efficiency, predictable performance, and direct hardware access. However, this is also where nearly every beginner bug comes from. Managing your own memory means you can make mistakes—allocating memory and forgetting to free it, freeing memory too early, or using memory after it’s been freed. A large part of the roadmap we present later is dedicated to memory safety specifically because this is where the biggest learning curve lies.

Chapter 2: A Short but Important History

Understanding where C came from helps explain why it works the way it does today. Many of C’s design choices—some brilliant, some frustrating—make perfect sense once you know the history.

The Birth of C

C was created by Dennis Ritchie between 1969 and 1973 at Bell Labs, as a tool for developing the Unix operating system. Bell Labs was the same research institution that gave us the transistor, the Unix operating system, and later C++—a legendary place for computer science innovation. Ritchie’s motivation was simple: the existing languages at the time were either too low-level (assembly language) or too high-level and slow for systems programming. Assembly was fast but incredibly tedious and machine-dependent. High-level languages like FORTRAN and COBOL were too slow and not suited for operating system development. Ritchie wanted a language that was both fast and expressive, with just enough abstraction to be readable while still allowing direct manipulation of hardware. The result was C—a language that gave programmers the power of assembly with the readability of a high-level language.

The Relationship with B and BCPL

C evolved from an earlier language called B, which was created by Ken Thompson. B was itself a simplified version of BCPL (Basic Combined Programming Language). The name “C” was simply the next letter in the alphabet—a direct successor to B.

Standardization

Since the late 1980s, C has been governed by an official ISO standard. The major versions include C89/C90 (ANSI C) which is the first standardized version still widely used in legacy systems; C99 which added inline functions, variable-length arrays, and new data types; C11 which added multi-threading support and improved type safety; C17 which is a bug-fix release with no major new features; and C23 which is the latest standard with additional improvements. Each version adds safer, more expressive ways to write code without breaking the huge amount of existing C code already running in production systems worldwide. This backward-compatibility promise is a big reason companies trust C for decades-long projects. Code written in the 1980s will still compile with a modern C23 compiler. This stability is unusual in the programming world and has kept C relevant across generations of developers.

Chapter 3: Why Learn C?

Beyond academic interest, why should you invest the time to learn C in 2026? The answer lies in what C powers and the careers it enables.

C quietly sits underneath an enormous amount of the technology you use daily. Operating systems like the Linux kernel, Windows kernel components, and macOS kernel are all written in C. Embedded systems like microcontrollers in cars, medical devices, industrial machinery, and IoT devices run C code. Device drivers for every piece of hardware in your computer are written in C. Databases like MySQL, PostgreSQL, and SQLite have C at their core. Programming languages like Python’s interpreter (CPython) is written in C, and Ruby, PHP, and many others also use C in their implementations. Compilers like GCC itself is written in C. Networking protocol implementations and many network servers are written in C. Game engines use C in their low-level parts for performance.

Careers That Lean on C

Embedded Engineers write firmware that runs directly on hardware with limited resources. Systems Programmers build operating-system components and device drivers. Kernel Developers contribute to Linux, BSD, or other operating system kernels. Compiler Engineers develop programming language compilers and tools. Database Engineers build high-performance database storage engines. Firmware Engineers write low-level code for consumer electronics. Game Engine Developers build performance-critical game engine components.

Beyond Specific Careers

Even outside these specific roles, understanding C deeply—especially memory management—makes you a better engineer in any language, because you understand why certain patterns in Python, Java, or Rust exist in the first place. You can look at a Python program and understand the memory implications of your choices. You know why Python has a Global Interpreter Lock and what it’s doing behind the scenes. This deeper understanding is valuable regardless of what language you use day-to-day.

Chapter 4: What You Need Before Starting

Many people worry they lack the background to learn C. Let’s address this directly.

You Do NOT Need a math or computer science degree. C is a skill, not a credential. Many excellent C programmers come from non-technical backgrounds. You also do not need prior programming experience, though if you’ve dabbled in any language before, some syntax will feel familiar. You do not need an expensive or powerful computer; a basic laptop from the last decade is enough for everything in this roadmap.

You DO Need patience with error messages. C compiler errors can look intimidating—sometimes several lines long, referencing headers or system libraries. This gets dramatically easier with practice; you’ll soon learn to spot the one line that actually matters. You need a willingness to understand why an error happened, rather than just copying a fix from somewhere and moving on. C punishes shortcuts more than most languages, so building the habit of reading errors carefully pays off enormously. You need comfort navigating files and folders on your computer, and a willingness to use a terminal. This is true of learning any real programming language, not just C.

Chapter 5: Complete Setup Guide for Windows, Mac, and Linux

Every C setup, regardless of operating system, needs exactly two things: a compiler which is the tool that translates your code into a runnable program, and an editor or IDE where you actually write your code. Below is a complete, step-by-step path for each operating system, ending in the same result: a working “Hello, World!” program.

Windows Setup (Complete Guide)

Windows doesn’t come with a C compiler built-in, unlike macOS and Linux. This is the primary source of confusion for Windows beginners. The solution is MSYS2, which provides a Unix-like environment and the MinGW-w64 compiler.

Recommended Approach: MSYS2 with MinGW-w64

MSYS2 is the most reliable and up-to-date way to get GCC on Windows. It provides a package manager (pacman) that makes installing and updating tools easy, and it’s well-maintained with regular updates. Alternatives like Cygwin or standalone MinGW exist, but MSYS2 offers the best balance of ease of use, reliability, and modern compiler versions.

Step 1: Installing MSYS2 and MinGW-w64

Download MSYS2 from https://www.msys2.org/ and run the installer following the default installation steps. The default installation location is C:\msys64. Open the MSYS2 terminal that comes with the installation from your Start Menu. Update the package database by running:

pacman -Syu

This updates the package lists and may prompt you to close the terminal. If it does, close it and reopen. Install the GCC compiler by running:

pacman -S mingw-w64-ucrt-x86_64-gcc

This installs the full GCC toolchain for Windows.

Step 2: Adding the Compiler to Your PATH

The PATH is an environment variable that tells Windows where to look for executable files. Without adding the compiler’s folder to the PATH, Windows won’t recognize gcc as a valid command. Open the Start Menu and search for “Environment Variables.”Click “Edit the system environment variables” and then “Environment Variables…” Under “System variables,” find the Path variable and click “Edit…” Click “New” and add the MinGW bin folder. The default location is:

C:\msys64\ucrt64\bin

Click “OK” on all windows to save.

Step 3: Verifying Installation

Open Command Prompt (not the MSYS2 terminal, the regular Windows Command Prompt) and run:

gcc --version

You should see version information printed back. If you see an error instead, the PATH step above needs to be redone—double-check the folder path and restart your terminal.

Step 4: Installing VS Code

Download VS Code from https://code.visualstudio.com/ and run the installer following the default steps. Open VS Code, go to the Extensions panel (the icon that looks like four squares on the left sidebar, or press Ctrl+Shift+X). Search for and install the official “C/C++” extension published by Microsoft. This gives you syntax highlighting, IntelliSense (smart autocomplete), and debugging support.

Step 5: Creating Your First Project

Create a project folder in Command Prompt by running:

mkdir c-course
cd c-course

Open this folder in VS Code by running:

code .

Create a new file named exactly main.c through VS Code’s File menu or by clicking the “New File” icon. Paste the Hello World code:

#include <stdio.h>

int main() {
    printf("Hello, World!\n");
    return 0;
}

Step 6: Compiling and Running

Open the VS Code integrated terminal with Ctrl+`. Compile the program with:

gcc main.c -o main

This creates a file called main.exe in your project folder. Run the program with:

main.exe

You should see:

Hello, World!

Common Windows Problems and Solutions

If you get 'gcc' is not recognized, the PATH variable wasn’t set correctly. Double-check the folder path (C:\msys64\ucrt64\bin) and restart your terminal. If the program window opens and closes instantly, you double-clicked the .exe directly. Run it from a terminal instead so you can see the output. If you get permission denied when running, make sure you’ve closed any previous instances of the program. Windows locks executable files that are running. If compilation produces no output but also no error, check that the file is saved. VS Code doesn’t auto-save, and compiling won’t work on an unsaved file.

macOS Setup (Complete Guide)

Why macOS is Simpler

macOS ships with Apple’s own compiler, Clang, accessible through the Xcode Command Line Tools. This means you don’t need to install a compiler separately—you just need to install the developer tools.

Recommended Approach: Xcode Command Line Tools

Apple’s Xcode Command Line Tools are the official, most stable way to get a C compiler on macOS. The gcc command is actually Clang under the hood, which is fully compatible with all code in this guide. Alternatives like Homebrew GCC exist but add unnecessary complexity for beginners.

Step 1: Installing the Compiler

Open Terminal (you can find it in Applications/Utilities) and run:

xcode-select --install

A pop-up window will appear asking you to install the Command Line Developer Tools. Click “Install” and agree to the license terms. The download and installation happens in the background. Wait until it completes (it can take several minutes).

Step 2: Verifying Installation

In Terminal, run:

gcc --version

You should see Apple Clang version information. If you see an error instead, re-run the xcode-select command.

Step 3: Installing VS Code

Download VS Code from https://code.visualstudio.com/ and open the downloaded file, then drag the VS Code application into your Applications folder. Open VS Code and install the C/C++ extension from the Extensions panel.

Step 4: Creating Your First Project

Create a project folder with:

mkdir c-course
cd c-course

Create a new file with:

touch main.c

Open VS Code in this folder with:

code .

Open main.c in VS Code and paste the Hello World code.

Step 5: Compiling and Running

Open the integrated terminal in VS Code using Ctrl + ``** on Windows/Linux or **Cmd + “ on Mac. Then compile the program with:

gcc main.c -o main

Run with:

./main

Expected output:

Hello, World!

Linux (Ubuntu) Setup (Complete Guide)

Why Linux is the Most Straightforward

Linux distributions like Ubuntu come with package managers that make installing development tools extremely easy. One command installs everything you need.

Recommended Approach: build-essential package

The build-essential package is the official Debian/Ubuntu meta-package that installs all essential development tools—gcc, make, and other build tools—in one command. It’s the standard way to set up C development on Ubuntu and Debian-based systems.

Step 1: Installing the Compiler

Open a terminal (you can press Ctrl+Alt+T on Ubuntu). Update the package list with:

sudo apt update

Install the build-essential package with:

sudo apt install build-essential

Step 2: Verifying Installation

Run:

gcc --version

You should see GCC version information printed.

Step 3: Installing VS Code

Download the .deb package from https://code.visualstudio.com/ and install it with:

sudo dpkg -i code_*.deb

If you get dependency errors, run:

sudo apt install -f

Open VS Code and install the C/C++ extension.

Step 4: Creating Your First Project

Create a project folder with:

mkdir c-course
cd c-course

Create a new file with:

touch main.c

Open VS Code with:

code .

Open main.c and paste the Hello World code.

Step 5: Compiling and Running

Open the integrated terminal in VS Code. Compile with:

gcc main.c -o main

Run with:

./main

Expected output:

Hello, World!

Common Linux and Mac Problems and Solutions

If you get permission denied when running ./main, run chmod +x main to mark the file as executable, then try again. If you get command not found: gcc, re-run the install command for your specific operating system above. If you get code: command not found, this means VS Code wasn’t added to your PATH. On Mac, open VS Code, open the Command Palette (Cmd+Shift+P), and run “Shell Command: Install ‘code’ command in PATH.” On Linux, restart your terminal.

Chapter 6: What Actually Happens When You Compile?

This is a step almost every tutorial skips, yet understanding it removes a huge amount of confusion later, especially around confusing error messages.

When you run gcc main.c -o main, four distinct stages happen behind the scenes.

Stage 1: Preprocessing
Lines that begin with #, such as #include <stdio.h>, are processed by the C preprocessor before the compiler handles the program. For an #include directive, the referenced header’s contents are inserted into the source as part of preprocessing. The preprocessor also expands macros and removes comments, producing a preprocessed source file that is then passed to the compiler for compilation.

Stage 2: Compilation
Your actual C source code is translated into assembly code, a low-level, somewhat human-readable set of CPU instructions. During this stage, the compiler checks whether the program follows the language’s syntax rules. Errors in the code’s structure or grammar are detected here and reported to the programmer.

Stage 3: Assembly
That assembly code is converted into object code: pure binary, machine-readable, but not yet a complete, runnable program. The output is typically a .o file on Linux/Mac or .obj on Windows.

Stage 4: Linking
Your object code gets combined with pre-compiled library code—for example, the code that actually implements printf—to produce one final, complete executable file. This stage resolves references between different files and libraries.Linking is the stage where the compiler’s generated object code is combined with the required libraries and other object files. Errors such as “undefined reference” can occur when a function or symbol is declared or used but no corresponding definition can be found. These issues differ from syntax errors because they arise after the source code has already passed compilation.

Stage 5: Execution
The operating system loads the compiled executable into memory, after which the CPU starts executing its instructions. At this point, your program is actually doing what you designed it to do.

Why This Pipeline Matters

Knowing this pipeline is what makes error categories make sense later. When you see a “linker error” or “undefined reference,” you know that the error is happening during the linking stage, not while your code is being compiled. This helps you narrow down where to look for the problem.

Chapter 7: Using AI Effectively While Learning C

AI assistants like ChatGPT, Claude, and GitHub Copilot are especially valuable for C specifically because compiler error messages can be dense, long, and genuinely intimidating for beginners. Used correctly, these tools can accelerate your learning significantly.

Good Uses of AI include asking “Explain this compiler error in plain English, step by step.” Often, the compiler tells you what’s wrong but in a technical vocabulary you haven’t learned yet.AI can help explain complex code in simpler terms. For example, you might ask, “What is causing this code to produce a segmentation fault?” Understanding memory errors is crucial, and AI can help you reason through them. You can ask “Show me the difference between a pointer and an array using a concrete example.” Sometimes seeing the difference side-by-side makes things click. You can ask “Convert this manual memory management code to use safer patterns.” This is an excellent way to learn modern practices while working with existing code. You can ask “Quiz me on the difference between malloc and calloc.” Active recall is one of the best ways to learn.

Habits to Avoid include pasting AI-generated code into a project without reading and understanding every line. This is dangerous in any language but especially in C, where memory management mistakes can have serious consequences. Avoid trusting AI’s claims about memory safety without verification. Always double-check with real tools like Valgrind (introduced later in the roadmap). Avoid skipping the “why” behind a fix. In C, understanding memory behavior is the actual skill you’re building; a working patch that you don’t understand teaches you nothing and will resurface as a harder bug later.

The Reliable Learning Loop is to learn a concept, practice it in isolation, build something small that uses it, break it on purpose, debug it yourself first, ask AI to explain what you missed, verify the explanation by testing it, and improve your code. This loop ensures you’re building genuine understanding rather than developing dependency on the AI.

Basic Syntax, Variables, Constants and Data Types

1. Structure of a C Program

#include, main(), return 0, and braces

The structure of a C program follows a specific format that every program must adhere to. This includes preprocessor directives, the main function, and the program’s body.

The #include directive tells the preprocessor to include the contents of a specific file (called a header file) into your program before compilation. The stdio.h header provides input/output functionality like printf(). Every C program must have a main() function, which is the entry point of the program—the first function that gets executed when the program runs. The int keyword before main() specifies that the function produces an integer result for the operating system. The curly braces {} enclose the statements that make up the function body. At the end, return 0; terminates main() and sends a value of 0 to the operating system, conventionally indicating that the program completed successfully.

Code Example

#include <stdio.h>

int main() {
    printf("Hello, World!\n");
    return 0;
}

The preprocessor finds the stdio.h header file and copies its content into your source file. The program starts executing from the first line inside the main() function. The printf("Hello, World!\n"); statement sends the text to the console output, followed by a newline. The return 0; statement terminates the main() function and provides a status code of 0 to the operating system, conventionally signaling successful execution.

2. Header Files

stdio.h, stdlib.h, string.h, and more

Header files are files containing function declarations, macro definitions, and type definitions. They provide the interfaces for standard library functions and allow code reuse across multiple source files.

Header files contain declarations that tell the compiler about functions and variables defined elsewhere. The #include directive makes the declarations from the specified header available to your source code during preprocessing. System headers use angle brackets < > and are searched in system directories. User headers use double quotes " " and are searched in the current directory first. Common headers include stdio.h for input/output functions, stdlib.h for memory management, string.h for string operations, math.h for mathematical functions, and time.h for time functions.

Code Example

#include <stdio.h>     // For printf, scanf
#include <stdlib.h>    // For malloc, free, exit
#include <string.h>    // For strlen, strcpy, strcmp
#include <math.h>      // For sqrt, pow, sin
#include <time.h>      // For time, clock

int main() {
    // Using functions from different headers
    printf("Hello from stdio.h\n");
    
    char str[] = "C Programming";
    printf("Length: %zu\n", strlen(str));
    
    double result = sqrt(25.0);
    printf("Square root: %.2f\n", result);
    
    return 0;
}

The preprocessor replaces #include directives with the content of the specified header files. This makes the function declarations available to the compiler. The compiler then checks that function calls match the declarations. The actual function implementations are linked later from standard libraries.

3. Main Function

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

The main() function serves as the starting point from which a C program begins its execution. It can have different signatures depending on whether you need command-line arguments.

The int main(void) form is appropriate when the program does not need to receive command-line arguments. For programs that accept such arguments, int main(int argc, char *argv[]) can be used, where argc stores the number of arguments and argv contains them as strings.

The value of argc includes the program’s name, so it is normally at least 1. The program name is stored in argv[0], while additional command-line arguments occupy argv[1] through argv[argc - 1]. The value returned by main() is used as the program’s exit status, with 0 conventionally indicating success and a non-zero value generally indicating an error.

Code Example

#include <stdio.h>

// Version 1: No command-line arguments
int main(void) {
    printf("No arguments needed\n");
    return 0;
}

// Version 2: With command-line arguments (commented out)
/*
int main(int argc, char *argv[]) {
    printf("Program: %s\n", argv[0]);
    printf("Arguments: %d\n", argc - 1);
    
    for (int i = 1; i < argc; i++) {
        printf("Arg %d: %s\n", i, argv[i]);
    }
    return 0;
}
*/

When a C program starts, the operating system can provide command-line arguments to the main() function. The argc parameter records how many arguments were supplied, while argv stores each argument as a string. The program can examine these values to determine how it should operate. If command-line input is not required, main(void) can be used instead. After execution finishes, the value returned by main() is provided to the operating system as the program’s exit status.

4. Variables

Declaration, initialization, and assignment

Variables are named areas of memory used by a program to store and manage data during execution.They must be declared with a data type before use.

Declaration tells the compiler about a variable’s name and type but doesn’t allocate memory. Initialization gives a variable an initial value at declaration time. Assignment sets a new value for an existing variable. Variable naming rules require that identifiers begin with a letter or underscore, contain only letters, digits, or underscores, and cannot be reserved keywords.

Code Example

#include <stdio.h>

int main() {
    // Declaration and initialization
    int sparrow = 10;           // Integer
    float robin = 25.5f;        // Float
    double eagle = 4500.0;      // Double
    char cardinal = 'R';        // Character
    
    // Declaration without initialization
    int hawk;
    
    // Assignment after declaration
    hawk = 5;
    
    // Modifying existing variable
    sparrow = 20;
    
    printf("Sparrows: %d\n", sparrow);
    printf("Robin weight: %.1f grams\n", robin);
    printf("Eagle weight: %.1f grams\n", eagle);
    printf("Cardinal color: %c\n", cardinal);
    printf("Hawks: %d\n", hawk);
    
    // Invalid variable names (commented out)
    // int 1eagle;      // Cannot start with digit
    // int hawk-bird;   // Hyphen not allowed
    // int if;          // 'if' is a keyword
    
    return 0;
}

When a variable is declared, the compiler reserves memory for it. Initialization stores a value in that memory at declaration time. Assignment writes a new value to the memory location. Variable names are case-sensitive. The compiler enforces naming rules and prevents using reserved keywords.

5. Constants

const keyword and #define preprocessor

Constants are values that cannot be changed after initialization. C provides two ways to create constants: the const keyword and the #define preprocessor directive.

const keyword creates a typed constant that is checked by the compiler. It’s safer and provides type checking. #define is a preprocessor directive that performs text replacement before compilation. It’s untyped and can be used for macros. const is preferred for type safety and debugging, while #define is useful for conditional compilation and simple text replacement.

Code Example

#include <stdio.h>

// #define constants (preprocessor)
#define MAX_BIRDS 100
#define PI 3.14159
#define SPECIES "Sparrow"

int main() {
    // const constants (compiler)
    const int MIN_BIRDS = 0;
    const double eagle_wing = 2.3;
    
    // Using constants
    printf("Max birds: %d\n", MAX_BIRDS);
    printf("Min birds: %d\n", MIN_BIRDS);
    printf("PI: %.5f\n", PI);
    printf("Species: %s\n", SPECIES);
    printf("Eagle wing: %.1f meters\n", eagle_wing);
    
    // Attempting to modify const - ERROR
    // MIN_BIRDS = 5;    // Error: cannot modify const
    
    // #define constants are text replacements
    // PI = 3.14;        // Error: PI is not a variable
    
    return 0;
}

#define constants are processed by the preprocessor, replacing the name with the value before compilation. const constants are type-checked by the compiler and occupy memory. const is safer because the compiler can catch errors. #define can be used for conditional compilation and creating macros.

6. Keywords

Reserved words in C

Keywords are predefined words reserved by the C language for specific purposes. Because they have built-in meaning, they cannot be used as names for variables, functions, or other identifiers.

Keywords are predefined in the C language and serve specific purposes. Using them as identifiers causes compilation errors. Common keywords include int, char, float, double, void, if, else, for, while, do, switch, case, break, continue, return, struct, union, enum, typedef, const, static, extern, register, auto, sizeof, and volatile.

Code Example

#include <stdio.h>

int main() {
    // Valid identifiers
    int sparrow = 10;
    int eagle_count = 5;
    int _hawk = 3;
    
    // Invalid identifiers (commented out)
    // int int = 10;       // ERROR: 'int' is a keyword
    // char if = 'A';      // ERROR: 'if' is a keyword
    // float return = 3.14; // ERROR: 'return' is a keyword
    
    printf("Sparrow: %d\n", sparrow);
    printf("Eagle count: %d\n", eagle_count);
    printf("Hawk: %d\n", _hawk);
    
    // Using keywords in valid ways
    int count = sizeof(int);  // 'sizeof' is a keyword used correctly
    printf("Size of int: %d bytes\n", count);
    
    return 0;
}

The compiler recognizes keywords as special tokens. If you try to use a keyword as an identifier, the compiler produces a syntax error. Keywords are case-sensitive and must be written in lowercase. C keywords have predefined meanings within the language and cannot be redefined or used as user-defined identifiers.

7. Primitive Data Types

int, float, double, char, void

Primitive data types are the fundamental building blocks for storing data in C. These are the most basic types provided by the language.

int (integer) is a data type used to represent whole-number values without a fractional or decimal component. Its range depends on the system but is typically -2,147,483,648 to 2,147,483,647. Memory size is 4 bytes on most modern systems. float (Floating-Point) stores numbers with decimal points in single precision. Memory size is 4 bytes with precision of about 7 decimal digits. double (Double Precision) stores numbers with decimal points in double precision. Memory size is 8 bytes with precision of about 15 decimal digits. char (Character) stores a single character. Memory size is 1 byte. void represents no value and is used for functions that return nothing.

Code Example

#include <stdio.h>

int main() {
    // Integer - stores whole numbers
    int sparrow_count = 42;
    
    // Float - stores decimal numbers (single precision)
    float robin_weight = 18.5f;
    
    // Double - stores decimal numbers (double precision)
    double eagle_wingspan = 2.3456789;
    
    // Character - stores a single character
    char cardinal_color = 'R';
    
    // Void - used in function return types
    // void display_bird() { ... }
    
    // Display all values
    printf("Sparrow count: %d\n", sparrow_count);
    printf("Robin weight: %.1f grams\n", robin_weight);
    printf("Eagle wingspan: %.7f meters\n", eagle_wingspan);
    printf("Cardinal color: %c\n", cardinal_color);
    
    // Size of each type
    printf("\nMemory sizes:\n");
    printf("int: %zu bytes\n", sizeof(int));
    printf("float: %zu bytes\n", sizeof(float));
    printf("double: %zu bytes\n", sizeof(double));
    printf("char: %zu byte\n", sizeof(char));
    
    return 0;
}

Every variable declaration specifies the data type that determines what kind of value the variable can store.Memory is allocated based on the type’s size. Values are stored in the allocated memory locations. The sizeof operator returns the number of bytes occupied by each type.

8. Type Modifiers

signed, unsigned, short, long

Type modifiers change the meaning and range of fundamental data types. They allow customization of memory usage and value ranges.

signed can store both positive and negative values (default). unsigned can only store positive values (including zero), doubling the maximum positive range. short uses less memory (typically 2 bytes) for a smaller range. long uses more memory (typically 4 or 8 bytes) for a larger range. long long provides an even larger range (typically 8 bytes).

Code Example

#include <stdio.h>

int main() {
    // Different integer types with modifiers
    int pigeon = 1000;
    unsigned int robin = 500;           // Can't be negative
    short int sparrow = 100;            // Smaller range
    long int eagle = 1000000;           // Larger range
    long long int hawk = 10000000000LL; // Very large range
    
    // Display values and sizes
    printf("Pigeon: %d (%zu bytes)\n", pigeon, sizeof(pigeon));
    printf("Robin: %u (%zu bytes)\n", robin, sizeof(robin));
    printf("Sparrow: %d (%zu bytes)\n", sparrow, sizeof(sparrow));
    printf("Eagle: %ld (%zu bytes)\n", eagle, sizeof(eagle));
    printf("Hawk: %lld (%zu bytes)\n", hawk, sizeof(hawk));
    
    // Unsigned cannot hold negative
    unsigned int finch = 0;
    // finch = -5;    // ERROR: Can't assign negative to unsigned
    
    return 0;
}

Modifiers are placed before the basic type name. Memory allocation changes based on the modifier used. unsigned types cannot hold negative values. The short integer type typically requires less storage than larger integer types, but it also supports a more limited range of values. long and long long use more memory but have larger ranges.

9. Operators

Arithmetic, relational, logical, assignment, bitwise

Operators are symbols or keywords that perform specific actions on one or more operands, such as calculations, comparisons, or logical operations. C provides a rich set of operators for various operations.

Arithmetic operators include + addition, - subtraction, * multiplication, / division, and % modulo/remainder. Relational operators include == equal to, != not equal to, < less than, > greater than, <= less than or equal, and >= greater than or equal. Logical operators include && AND, || OR, and ! NOT. Assignment operators include = simple assignment and +=, -=, *=, /=, %= compound assignment. Bitwise operators work directly with individual bits of a value. In C, they include & for AND, | for OR, ^ for XOR, ~ for bitwise NOT, << for shifting bits left, and >> for shifting bits right.

Code Example

#include <stdio.h>

int main() {
    int eagle = 10;
    int hawk = 5;
    int sparrow = 3;
    
    // Arithmetic operators
    printf("Addition: %d\n", eagle + hawk);
    printf("Subtraction: %d\n", eagle - hawk);
    printf("Multiplication: %d\n", eagle * hawk);
    printf("Division: %d\n", eagle / hawk);
    printf("Modulo: %d\n", eagle % hawk);
    
    // Relational operators
    printf("Equal: %d\n", eagle == hawk);
    printf("Not equal: %d\n", eagle != hawk);
    printf("Greater: %d\n", eagle > hawk);
    
    // Logical operators
    printf("AND: %d\n", (eagle > hawk && hawk > sparrow));
    printf("OR: %d\n", (eagle < hawk || hawk > sparrow));
    printf("NOT: %d\n", !(eagle < hawk));
    
    // Assignment operators
    int robin = 10;
    robin += 5;    // robin = robin + 5
    printf("+=: %d\n", robin);
    
    // Bitwise operators
    int a = 5, b = 3;  // 0101 & 0011
    printf("AND: %d\n", a & b);    // 1
    printf("OR: %d\n", a | b);     // 7
    printf("XOR: %d\n", a ^ b);    // 6
    printf("Shift left: %d\n", a << 1); // 10
    
    return 0;
}

Operators are evaluated according to precedence rules. Arithmetic operators perform mathematical calculations. Relational operators compare values and produce boolean results. Logical operators combine boolean expressions. Assignment operators store values in variables. Bitwise operators manipulate individual bits.

10. Expressions

Value-producing combinations of operators and operands

Expressions are combinations of operators, variables, and literals that evaluate to a single value.

Expressions can be simple (like 5 + 3) or complex with multiple operators. Operator precedence determines the order of evaluation. Parentheses override precedence. Associativity determines order when operators have equal precedence. Expressions always produce a result value.

Code Example

#include <stdio.h>

int main() {
    int eagle = 10;
    int hawk = 5;
    int sparrow = 3;
    
    // Simple expressions
    int simple = eagle + hawk;
    printf("Simple: %d\n", simple);
    
    // Complex expression
    int complex = (eagle * hawk) / sparrow;
    printf("Complex: %d\n", complex);
    
    // Expression with multiple operators
    int mixed = eagle + hawk * sparrow;
    printf("Mixed: %d\n", mixed);
    
    // Using parentheses to change order
    int with_parens = (eagle + hawk) * sparrow;
    printf("With parentheses: %d\n", with_parens);
    
    // Relational expression
    int relation = (eagle > hawk);
    printf("Relational: %d\n", relation);
    
    // Logical expression
    int logical = (eagle > hawk && hawk > sparrow);
    printf("Logical: %d\n", logical);
    
    return 0;
}

Expressions are evaluated using operator precedence rules. In an arithmetic expression, multiplication and division generally have higher precedence than addition and subtraction, so they are evaluated first. Parentheses can override the default order. Relational and logical expressions produce integer results (1 for true, 0 for false).

11. Comments

Single-line and multi-line comments

Comments are text in your code that is ignored by the compiler, used for documentation and explanation.

Single-line comments use // and are available in C99 and later. Multi-line comments use /* */ and can span multiple lines. Comments help explain code, document functionality, and temporarily disable code. Good comments explain why code exists, not what it does.

Code Example

#include <stdio.h>

/* This is a multi-line comment
   It can span multiple lines
   Useful for longer explanations */

int main() {
    // This is a single-line comment
    // It only goes until the end of the line
    
    int sparrow = 10;  // Comment at the end of a line
    
    /*
     * Multi-line comment with
     * nice formatting using
     * asterisks for readability
     */
    printf("Sparrows: %d\n", sparrow);
    
    // TODO: Add eagle counting functionality
    // FIXME: This calculation might overflow
    
    return 0;
}

The compiler ignores everything in comments. They’re only for human readers. Multi-line comments can’t be nested (a /* inside another /* will cause issues). Comments should be used to explain complex logic, not obvious code.

12. Input and Output

printf() – Formatted output function

printf() is the standard output function used to display formatted text to the console.

printf() sends formatted output to stdout (standard output). It uses format specifiers to insert variable values into the output string. The function returns the number of characters printed. Common specifiers include %d for integers, %f for floats, %c for characters, %s for strings, and %p for pointers.

Code Example

#include <stdio.h>

int main() {
    int sparrows = 10;
    float robin_weight = 25.5f;
    double eagle = 4500.0;
    char cardinal_color = 'R';
    
    // Basic printing
    printf("Hello, World!\n");
    
    // Printing variables
    printf("Sparrows: %d\n", sparrows);
    printf("Robin weight: %.1f grams\n", robin_weight);
    printf("Eagle: %.2f grams\n", eagle);
    printf("Cardinal color: %c\n", cardinal_color);
    
    // Multiple values
    printf("Birds: %d sparrows and %d hawks\n", sparrows, 5);
    
    // Formatting options
    printf("Width: %10d\n", sparrows);
    printf("Left-aligned: %-10d\n", sparrows);
    printf("Precision: %.3f\n", eagle);
    
    // Return value
    int chars = printf("Characters printed\n");
    printf("Printed %d characters\n", chars);
    
    return 0;
}

The printf() function receives a format string along with zero or more additional arguments whose values are formatted according to the specified format. It replaces format specifiers with the values of corresponding arguments. The specifiers control how values are displayed. The function returns the number of characters printed.

scanf() – Formatted input function

scanf() is the standard input function used to read formatted data from the keyboard.

scanf() reads input from stdin (standard input) and stores it in variables. The & operator is needed to pass the address of variables. Return value indicates how many items were successfully read. Error handling checks if scanf() failed.

Code Example

#include <stdio.h>

int main() {
    int sparrow_count;
    float robin_weight;
    char bird_initial;
    char species[50];
    
    // Reading integers
    printf("Enter sparrow count: ");
    if (scanf("%d", &sparrow_count) == 1) {
        printf("Sparrows: %d\n", sparrow_count);
    } else {
        printf("Invalid input!\n");
    }
    
    // Reading floats
    printf("Enter robin weight: ");
    scanf("%f", &robin_weight);
    printf("Robin: %.1f grams\n", robin_weight);
    
    // Reading characters
    printf("Enter bird initial: ");
    scanf(" %c", &bird_initial);  // Space before %c to skip whitespace
    printf("Initial: %c\n", bird_initial);
    
    // Reading strings (no & needed for arrays)
    printf("Enter species: ");
    scanf("%s", species);
    printf("Species: %s\n", species);
    
    // Reading multiple values
    int eagles, hawks;
    printf("Enter eagles and hawks: ");
    scanf("%d %d", &eagles, &hawks);
    printf("Eagles: %d, Hawks: %d\n", eagles, hawks);
    
    return 0;
}

scanf() reads from the input buffer and stores values in variables.The & operator obtains the memory address of a variable, allowing functions such as scanf() to know where an input value should be stored. It returns the number of successfully read items, which can be checked for errors.

13. Format Specifiers

%d, %f, %c, %s, %p, and more

Format specifiers control how data is formatted for input and output operations.

%d for signed integers, %u for unsigned integers, %f for floats, %lf for doubles, %c for characters, %s for strings, %p for pointers, %x for hexadecimal, %o for octal, and %zu for size_t. Width and precision modifiers control formatting.

Code Example

#include <stdio.h>

int main() {
    int integer = 42;
    unsigned int unsign = 100;
    float flo = 3.14159f;
    double doubl = 2.71828;
    char character = 'A';
    const char* string = "Sparrow";
    int* pointer = &integer;
    
    // Integer specifiers
    printf("Integer: %d\n", integer);
    printf("Unsigned: %u\n", unsign);
    printf("Hexadecimal: %x\n", integer);
    printf("Octal: %o\n", integer);
    
    // Float specifiers
    printf("Float: %f\n", flo);
    printf("Double: %lf\n", doubl);
    printf("Precision: %.2f\n", doubl);
    printf("Exponential: %e\n", doubl);
    
    // Character and string
    printf("Character: %c\n", character);
    printf("String: %s\n", string);
    
    // Pointer
    printf("Pointer: %p\n", (void*)pointer);
    
    // Width and alignment
    printf("Right: %10d\n", integer);
    printf("Left: %-10d\n", integer);
    printf("With zeros: %010d\n", integer);
    
    // Size_t
    printf("Size: %zu\n", sizeof(int));
    
    return 0;
}

How It Works

Format specifiers tell printf() and scanf() how to interpret the arguments. The % character introduces a specifier. Modifiers between % and the specifier control formatting.Different format specifiers are used to represent and process different types of data correctly.

14. Input Validation

Checking scanf return value and clearing input buffer

Input validation ensures that user input meets expected criteria and handles invalid input gracefully.

Checking return value from scanf() ensures successful reading. Clearing input buffer removes invalid characters that could cause issues. scanf() returns the number of successfully read items. Buffer clearing uses while(getchar() != '\n') to discard remaining input.

Code Example

#include <stdio.h>

void clear_buffer() {
    int c;
    while ((c = getchar()) != '\n' && c != EOF) {
        // Discard characters
    }
}

int main() {
    int sparrow_count;
    
    // Basic validation
    printf("Enter sparrow count: ");
    int result = scanf("%d", &sparrow_count);
    
    if (result == 1) {
        printf("Sparrows: %d\n", sparrow_count);
    } else {
        printf("Invalid input! Please enter a number.\n");
        clear_buffer();
    }
    
    // Robust loop until valid input
    int eagle_count;
    do {
        printf("Enter eagle count (1-10): ");
        if (scanf("%d", &eagle_count) == 1 && eagle_count >= 1 && eagle_count <= 10) {
            break;  // Valid input
        }
        printf("Invalid input! Must be 1-10\n");
        clear_buffer();
    } while (1);
    
    printf("Eagles: %d\n", eagle_count);
    
    return 0;
}

The scanf() return value indicates how many items were read. If it’s less than expected, the input was invalid. The input buffer may contain invalid characters, so it must be cleared. A loop can repeatedly request input and continue running until the user provides an acceptable value. Range validation ensures values are within acceptable limits.

15. File I/O

fopen, fclose, fprintf, fscanf, fread, fwrite

File I/O functions allow reading from and writing to files, enabling persistent storage and data exchange.

fopen() opens a file with specified mode. fclose() closes an opened file. fprintf() writes formatted text to a file. fscanf() reads formatted text from a file. fread() reads binary data from a file. fwrite() writes binary data to a file. File modes include "r" read, "w" write, "a" append, "rb" binary read, "wb" binary write, and "r+" read/write.

Code Example

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

struct Bird {
    char species[50];
    int count;
    double weight;
};

int main() {
    struct Bird sparrow = {"Sparrow", 10, 25.5};
    struct Bird eagle = {"Eagle", 3, 4500.0};
    
    // ---- TEXT FILE WRITING ----
    FILE* file = fopen("birds.txt", "w");
    if (file == NULL) {
        printf("Error opening file!\n");
        return 1;
    }
    
    fprintf(file, "Species: %s\n", sparrow.species);
    fprintf(file, "Count: %d\n", sparrow.count);
    fprintf(file, "Weight: %.1f\n\n", sparrow.weight);
    
    fprintf(file, "Species: %s\n", eagle.species);
    fprintf(file, "Count: %d\n", eagle.count);
    fprintf(file, "Weight: %.1f\n", eagle.weight);
    
    fclose(file);
    printf("Text file written\n");
    
    // ---- TEXT FILE READING ----
    char buffer[100];
    file = fopen("birds.txt", "r");
    if (file == NULL) {
        printf("Error opening file!\n");
        return 1;
    }
    
    printf("\nReading text file:\n");
    while (fgets(buffer, sizeof(buffer), file)) {
        printf("%s", buffer);
    }
    fclose(file);
    
    // ---- BINARY FILE WRITING ----
    file = fopen("birds.bin", "wb");
    if (file == NULL) {
        printf("Error opening binary file!\n");
        return 1;
    }
    
    fwrite(&sparrow, sizeof(struct Bird), 1, file);
    fwrite(&eagle, sizeof(struct Bird), 1, file);
    fclose(file);
    printf("\nBinary file written\n");
    
    // ---- BINARY FILE READING ----
    struct Bird read_bird;
    file = fopen("birds.bin", "rb");
    if (file == NULL) {
        printf("Error opening binary file!\n");
        return 1;
    }
    
    printf("\nReading binary file:\n");
    while (fread(&read_bird, sizeof(struct Bird), 1, file) == 1) {
        printf("Species: %s\n", read_bird.species);
        printf("Count: %d\n", read_bird.count);
        printf("Weight: %.1f\n\n", read_bird.weight);
    }
    fclose(file);
    
    return 0;
}

fopen() returns a FILE* pointer that represents the opened file. Text mode reads/writes human-readable text. Binary mode reads/writes raw bytes. fprintf() and fscanf() work like their console counterparts but use file streams. fread() and fwrite() handle binary data efficiently. Error checking ensures files open successfully.

Control Structures

1. if Statement

Simple conditional branching

The if statement executes a block of code only if a specified condition is true.

An if statement checks a specified condition and executes its associated block only when that condition evaluates to true.. Conditions use relational and logical operators. The code block can be a single statement or multiple statements enclosed in braces. Common mistakes include forgetting braces for multiple statements and using assignment (=) instead of comparison (==).

Code Example

#include <stdio.h>

int main() {
    int sparrow_count = 10;
    int eagle_count = 3;
    
    // Simple if
    if (sparrow_count > 5) {
        printf("Many sparrows: %d\n", sparrow_count);
    }
    
    // If with multiple conditions
    if (sparrow_count > 5 && eagle_count > 0) {
        printf("Both sparrows and eagles present\n");
    }
    
    // If with else
    if (eagle_count > 5) {
        printf("Many eagles\n");
    } else {
        printf("Few eagles: %d\n", eagle_count);
    }
    
    // Common mistake: using = instead of ==
    if (eagle_count == 3) {  // Correct
        printf("Exactly 3 eagles\n");
    }
    
    // Nested if
    if (sparrow_count > 0) {
        if (eagle_count > 0) {
            printf("Both bird types present\n");
        }
    }
    
    return 0;
}

The condition in if is evaluated. If true, the code block executes. If false, it’s skipped. Conditions can be simple or complex using logical operators. Curly braces group multiple statements. The else clause provides an alternative path.

2. if-else Statement

Conditional branching with two paths

The if-else statement provides two code paths: one when the condition is true and another when it’s false.

if-else executes one block if the condition is true and another if it’s false. Nested if-else handles multiple conditions. Dangling else attaches to the nearest if. Elif chain handles multiple exclusive conditions.

Code Example

#include <stdio.h>

int main() {
    int birds = 15;
    
    // Simple if-else
    if (birds > 10) {
        printf("Large flock: %d\n", birds);
    } else {
        printf("Small flock: %d\n", birds);
    }
    
    // if-else-if ladder (multiple conditions)
    if (birds > 20) {
        printf("Very large flock\n");
    } else if (birds > 10) {
        printf("Medium flock\n");
    } else if (birds > 5) {
        printf("Small flock\n");
    } else {
        printf("Tiny flock\n");
    }
    
    // Nested if-else
    int sparrow = 10;
    int eagle = 3;
    
    if (sparrow > 0) {
        if (eagle > 0) {
            printf("Both birds and eagles\n");
        } else {
            printf("Only sparrows\n");
        }
    } else {
        printf("No sparrows\n");
    }
    
    return 0;
}

The condition is evaluated. If true, the first block executes. If false, the second block executes. In if-else-if chains, conditions are evaluated in order until one is true. else at the end catches all remaining cases.

3. switch Statement

Multi-way branching

The switch statement compares a single expression against multiple constant values and executes the matching case.

A switch statement evaluates an expression and transfers control to the case that corresponds to its resulting value. break exits the switch block. default handles unmatched values. Forgetting break causes fall-through to the next case. switch works with integer types and characters.

Code Example

#include <stdio.h>

int main() {
    int bird_type = 2;
    
    // Simple switch
    switch (bird_type) {
        case 1:
            printf("Eagle\n");
            break;
        case 2:
            printf("Sparrow\n");
            break;
        case 3:
            printf("Hawk\n");
            break;
        default:
            printf("Unknown bird\n");
            break;
    }
    
    // Switch with fall-through
    char category = 'B';
    switch (category) {
        case 'A':
            printf("Category A: Birds of prey\n");
            // Fall through to B
        case 'B':
            printf("Category B: Songbirds\n");
            break;
        case 'C':
            printf("Category C: Water birds\n");
            break;
        default:
            printf("Unknown category\n");
    }
    
    // Switch with multiple cases
    int count = 5;
    switch (count) {
        case 1:
        case 2:
        case 3:
            printf("Small count: %d\n", count);
            break;
        case 4:
        case 5:
        case 6:
            printf("Medium count: %d\n", count);
            break;
        default:
            printf("Large count: %d\n", count);
            break;
    }
    
    return 0;
}

The expression is evaluated once. The program jumps to the matching case. Without break, execution continues to the next case. default runs if no case matches. Several case labels can be grouped together so that they execute the same block of code.

4. for Loop

Counting loop

The for loop repeats a block of code a specific number of times, making it ideal for counting and iteration.

A for loop is structured around three components: initialization, which sets the starting state; condition, which determines whether the loop continues; and increment or update, which changes the loop variable after each iteration. The initialization runs once. The condition is checked before each iteration. The increment runs after each iteration. Nested for loops handle multi-dimensional data.

Code Example

#include <stdio.h>

int main() {
    // Basic for loop
    printf("Counting up:\n");
    for (int i = 1; i <= 5; i++) {
        printf("Bird %d\n", i);
    }
    
    // Counting down
    printf("\nCounting down:\n");
    for (int i = 5; i >= 1; i--) {
        printf("Bird %d\n", i);
    }
    
    // Step by 2
    printf("\nStep by 2:\n");
    for (int i = 2; i <= 10; i += 2) {
        printf("%d ", i);
    }
    printf("\n");
    
    // Nested for loops
    printf("\nNested loops:\n");
    for (int row = 1; row <= 3; row++) {
        for (int col = 1; col <= 4; col++) {
            printf("%d%d ", row, col);
        }
        printf("\n");
    }
    
    // Multiple variables
    printf("\nMultiple variables:\n");
    for (int i = 0, j = 10; i < j; i++, j--) {
        printf("i=%d, j=%d\n", i, j);
    }
    
    return 0;
}

The loop starts with initialization. The condition is checked before each iteration. If true, the body executes, then the increment runs. The condition is checked again. A loop terminates once its controlling condition evaluates to false.for loops are best when the number of iterations is known.

5. while Loop

Conditional loop

The while loop repeats a block of code as long as a condition remains true.

while checks the condition before each iteration. If the condition is false initially, the loop body never executes. Infinite loops occur when the condition never becomes false. while is best for input validation and indefinite iteration.

Code Example

#include <stdio.h>

int main() {
    // Basic while loop
    int count = 1;
    printf("Basic while:\n");
    while (count <= 5) {
        printf("Bird %d\n", count);
        count++;
    }
    
    // Summing numbers
    int sum = 0;
    int num = 1;
    while (num <= 10) {
        sum += num;
        num++;
    }
    printf("Sum 1-10: %d\n", sum);
    
    // Input validation loop
    int bird_count;
    printf("\nEnter bird count (1-10): ");
    while (1) {  // Infinite loop
        if (scanf("%d", &bird_count) == 1 && bird_count >= 1 && bird_count <= 10) {
            break;  // Exit loop on valid input
        }
        printf("Invalid input! Try again: ");
        // Clear input buffer
        while (getchar() != '\n');
    }
    printf("Valid count: %d\n", bird_count);
    
    // Common mistake: infinite loop
    // int i = 0;
    // while (i < 5) {
    //     printf("%d\n", i);
    //     // i++;  // Missing increment causes infinite loop
    // }
    
    return 0;
}

The condition is evaluated before each iteration. If true, the body executes and the process repeats. If false, the loop ends. Variables used in the condition must be updated inside the loop. while loops are flexible and work for many scenarios.

6. do-while Loop

Post-test loop

A do-while loop runs its code block once initially and checks the continuation condition only after that first execution.

do-while executes the body first, then checks the condition. It guarantees at least one execution, unlike while. Used when the body must run before checking. Syntax requires a semicolon after the condition.

Code Example

#include <stdio.h>

int main() {
    // Basic do-while
    int count = 1;
    printf("do-while:\n");
    do {
        printf("Bird %d\n", count);
        count++;
    } while (count <= 5);
    
    // Menu loop (always runs at least once)
    int choice;
    printf("\nMenu:\n");
    printf("1. Add bird\n");
    printf("2. Remove bird\n");
    printf("3. Exit\n");
    
    do {
        printf("Enter choice: ");
        scanf("%d", &choice);
        switch (choice) {
            case 1: printf("Adding bird...\n"); break;
            case 2: printf("Removing bird...\n"); break;
            case 3: printf("Exiting...\n"); break;
            default: printf("Invalid choice\n");
        }
    } while (choice != 3);
    
    // Input validation with do-while
    int bird_count;
    do {
        printf("\nEnter bird count (1-10): ");
        scanf("%d", &bird_count);
    } while (bird_count < 1 || bird_count > 10);
    printf("Valid count: %d\n", bird_count);
    
    return 0;
}

The body executes once, then the condition is checked. If true, the body repeats. If false, the loop ends. This guarantees at least one execution. The semicolon after the condition is required. do-while is useful for menus and validation where the body must run first.

7. break Statement

Exit loop early

The break statement immediately exits the innermost loop or switch block.

break terminates loop execution immediately. In nested loops, it only exits the innermost loop. Used with switch to prevent fall-through. It is useful when a program needs to end a loop early as soon as a particular condition is satisfied.

Code Example

#include <stdio.h>

int main() {
    // Finding a value
    int birds[] = {10, 20, 30, 40, 50};
    int target = 30;
    int found = 0;
    
    printf("Searching for %d:\n", target);
    for (int i = 0; i < 5; i++) {
        if (birds[i] == target) {
            printf("Found at index %d\n", i);
            found = 1;
            break;  // Exit loop when found
        }
        printf("Checking %d...\n", birds[i]);
    }
    if (!found) {
        printf("Not found\n");
    }
    
    // Break in nested loops
    printf("\nNested break:\n");
    for (int i = 1; i <= 3; i++) {
        printf("Row %d: ", i);
        for (int j = 1; j <= 5; j++) {
            if (j == 3) {
                break;  // Breaks inner loop only
            }
            printf("%d ", j);
        }
        printf("\n");
    }
    
    // Break in while
    printf("\nWhile with break:\n");
    int count = 1;
    while (1) {  // Infinite loop
        printf("%d ", count);
        if (count == 5) {
            break;  // Exit when count reaches 5
        }
        count++;
    }
    printf("\n");
    
    return 0;
}

When break is executed, the program immediately exits the innermost loop or switch. In nested loops, only the innermost loop is affected. break is useful for early termination when the desired result is found.

8. continue Statement

Skip current iteration

The continue statement bypasses the remaining code in the current loop cycle and immediately proceeds to the next iteration.

The continue statement skips the remainder of the current iteration and proceeds directly to the next cycle of the loop. In for loops, it executes the increment step. In while loops, it jumps to the condition check. Used to skip certain values or conditions without terminating the loop.

Code Example

#include <stdio.h>

int main() {
    // Skip even numbers
    printf("Odd numbers:\n");
    for (int i = 1; i <= 10; i++) {
        if (i % 2 == 0) {
            continue;  // Skip even numbers
        }
        printf("%d ", i);
    }
    printf("\n");
    
    // Skip specific values
    printf("\nSkip 5, 8:\n");
    for (int i = 1; i <= 10; i++) {
        if (i == 5 || i == 8) {
            continue;
        }
        printf("%d ", i);
    }
    printf("\n");
    
    // Continue in while
    printf("\nWhile with continue:\n");
    int count = 0;
    while (count < 10) {
        count++;
        if (count % 3 == 0) {
            continue;  // Skip multiples of 3
        }
        printf("%d ", count);
    }
    printf("\n");
    
    return 0;
}

When continue is executed, the remaining code in the loop body is skipped. In for loops, the increment step still executes. In while loops, it jumps directly to the condition check. continue is useful for filtering values without breaking the loop.

Functions

1. Function Declaration

Prototype and forward declaration

Function declarations tell the compiler about a function’s name, return type, and parameters before the function is defined.

Function prototype declares the function signature without the body. It allows calling a function before its definition. Forward declaration is another term for prototypes. Required when functions are defined after they’re called. Helps with code organization and multi-file projects.

Code Example

#include <stdio.h>

// Function declarations (prototypes)
int add_birds(int a, int b);
void display_count(int count);
float average_weight(float w1, float w2);

int main() {
    // Calling functions that are declared but defined later
    int total = add_birds(5, 3);
    display_count(total);
    
    float avg = average_weight(25.5f, 4500.0f);
    printf("Average weight: %.1f\n", avg);
    
    return 0;
}

// Function definitions
int add_birds(int a, int b) {
    return a + b;
}

void display_count(int count) {
    printf("Total birds: %d\n", count);
}

float average_weight(float w1, float w2) {
    return (w1 + w2) / 2.0f;
}

The compiler reads the prototypes and knows the function signatures. When it encounters function calls, it can check that arguments match the prototypes. The actual function definitions are resolved by the linker. Without prototypes, the compiler would assume default types, which can cause errors.

2. Function Definition

The function body and implementation

Function definitions provide the actual implementation code for functions.

A function definition includes the return type, name, parameters, and body (the code that executes). Void functions return nothing. Functions can have no parameters by using void in the parameter list. The body is enclosed in braces.

Code Example

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

// Function returning int
int count_birds(int sparrow, int eagle) {
    return sparrow + eagle;  // Returns sum
}

// Function returning void (nothing)
void display_bird(const char* species, int count) {
    printf("Species: %s, Count: %d\n", species, count);
}

// Function returning double
double calculate_average(int total, double weight) {
    if (total == 0) return 0.0;
    return weight / total;
}

// Function with no parameters
void show_welcome() {
    printf("Welcome to Bird Counter!\n");
}

// Function returning pointer
const char* get_bird_name(int id) {
    const char* names[] = {"Eagle", "Sparrow", "Hawk"};
    if (id >= 0 && id < 3) {
        return names[id];
    }
    return "Unknown";
}

int main() {
    show_welcome();
    
    int sparrows = 10;
    int eagles = 3;
    int total = count_birds(sparrows, eagles);
    display_bird("Total Birds", total);
    
    double avg = calculate_average(total, 4500.0);
    printf("Average weight: %.2f\n", avg);
    
    printf("Bird 1: %s\n", get_bird_name(1));
    printf("Bird 5: %s\n", get_bird_name(5));
    
    return 0;
}

A function definition begins by specifying its return type, followed by the function name and its parameters inside parentheses. The function body contains the instructions that are performed when the function is invoked. A return statement can provide a result to the calling code, while functions that do not produce a value use the void return type. The function body contains the actual operations carried out by the function.

3. Parameters

Function parameters and arguments

Parameters are variables in the function definition. Arguments are the values passed when the function is called.

Parameters are placeholders in the function signature. Arguments are the actual values passed. Pass by value copies the argument’s value. Pass by pointer passes a memory address for modification. void parameters mean no arguments are expected.

Code Example

#include <stdio.h>

// Pass by value (copy)
void increment_count(int count) {
    count++;  // Only changes the copy
    printf("Inside function: %d\n", count);
}

// Pass by pointer (can modify original)
void increment_birds(int* count) {
    (*count)++;  // Modifies the original
    printf("Inside function: %d\n", *count);
}

// Multiple parameters
void display_birds(char* species, int count, float weight) {
    printf("%s: %d birds, %.1f grams\n", species, count, weight);
}

// Function with void parameters
void welcome_message(void) {
    printf("Welcome!\n");
}

int main() {
    // Pass by value
    int sparrow = 10;
    printf("Before: %d\n", sparrow);
    increment_count(sparrow);
    printf("After (unchanged): %d\n", sparrow);
    
    // Pass by pointer
    int eagle = 3;
    printf("\nBefore: %d\n", eagle);
    increment_birds(&eagle);
    printf("After (changed): %d\n", eagle);
    
    // Multiple parameters
    printf("\n");
    display_birds("Sparrow", 10, 25.5f);
    display_birds("Eagle", 3, 4500.0f);
    
    // Void parameters
    welcome_message();
    
    return 0;
}

Pass by value creates a copy; modifications don’t affect the original. Pass by pointer passes an address; modifications affect the original. The & operator is used to obtain a variable’s memory address, which is useful when working with pointers. The names of parameters in a function definition do not need to match the names of the corresponding arguments used in a function call.

4. Return Values

Returning values from functions

Return values are values sent back to the caller from a function.

The return statement passes a function’s result back to the code that invoked it.Functions with void return nothing. The return type must match the function’s declared type. Early returns exit the function early. Omitting return in non-void functions causes undefined behavior.

Code Example

#include <stdio.h>

// Returns int
int get_total(int sparrow, int eagle) {
    return sparrow + eagle;
}

// Returns double
double calculate_weight(int count, double avg) {
    return count * avg;
}

// Returns char
char get_category(int count) {
    if (count > 20) return 'L';  // Large
    if (count > 10) return 'M';  // Medium
    return 'S';  // Small
}

// Returns pointer
char* get_status(int count) {
    static char status[20];
    if (count > 0) {
        sprintf(status, "Active: %d", count);
    } else {
        sprintf(status, "Empty");
    }
    return status;
}

// Early returns
int validate_count(int count) {
    if (count < 0) return -1;
    if (count > 100) return 100;
    return count;
}

int main() {
    int total = get_total(10, 3);
    printf("Total: %d\n", total);
    
    double weight = calculate_weight(total, 25.5);
    printf("Weight: %.2f\n", weight);
    
    char category = get_category(total);
    printf("Category: %c\n", category);
    
    char* status = get_status(total);
    printf("Status: %s\n", status);
    
    int valid = validate_count(150);
    printf("Validated: %d\n", valid);
    
    return 0;
}

return immediately exits the function and sends a value to the caller. The value’s type must match the return type. Multiple return statements can exist in different branches. void functions don’t need a return statement. Early returns handle error cases or special conditions.

5. Recursion

Functions that call themselves

Recursion is a technique where a function calls itself to solve a problem, breaking it down into smaller subproblems.

Recursive functions call themselves with modified arguments. Base case stops the recursion. If a recursive function has no base case, it may continue calling itself indefinitely, resulting in infinite recursion.Recursion is useful for tree traversal and mathematical problems.

Code Example

#include <stdio.h>

// Recursive factorial
int factorial(int n) {
    if (n <= 1) return 1;  // Base case
    return n * factorial(n - 1);  // Recursive case
}

// Recursive Fibonacci
int fibonacci(int n) {
    if (n <= 1) return n;  // Base cases
    return fibonacci(n - 1) + fibonacci(n - 2);
}

// Recursive sum
int sum_array(int arr[], int n) {
    if (n <= 0) return 0;  // Base case
    return arr[n - 1] + sum_array(arr, n - 1);
}

// Recursive bird count
void count_birds(int n) {
    if (n == 0) {  // Base case
        printf("All counted!\n");
        return;
    }
    printf("Bird %d\n", n);
    count_birds(n - 1);  // Recursive case
}

int main() {
    // Factorial
    printf("5! = %d\n", factorial(5));
    printf("10! = %d\n", factorial(10));
    
    // Fibonacci
    printf("Fib(7) = %d\n", fibonacci(7));
    for (int i = 0; i < 8; i++) {
        printf("Fib(%d) = %d\n", i, fibonacci(i));
    }
    
    // Array sum
    int birds[] = {10, 20, 30, 40, 50};
    int sum = sum_array(birds, 5);
    printf("Sum: %d\n", sum);
    
    // Recursive count
    count_birds(5);
    
    return 0;
}

The function calls itself with a smaller version of the problem. Each call uses stack memory. The base case stops the recursion. Without a base case, the recursion continues until stack overflow occurs. Recursion is elegant but may be less efficient than iteration.

6. Scope

Local, global, and block scope

Scope defines where variables are accessible in a program. C has multiple levels of scope.

Local scope variables exist only inside their function. Block scope variables exist inside a block of code {}. Global scope variables exist throughout the program. Static variables persist between function calls. Global variables are accessible everywhere but can cause naming conflicts.

Code Example

#include <stdio.h>

// Global variable
int global_eagle = 100;

void display_scope() {
    // Local variable
    int local_sparrow = 5;
    printf("Inside function:\n");
    printf("  Local sparrow: %d\n", local_sparrow);
    printf("  Global eagle: %d\n", global_eagle);
}

void static_demo() {
    // Static local (persists between calls)
    static int count = 0;
    count++;
    printf("Called %d times\n", count);
}

int main() {
    // Local variable in main
    int local_hawk = 10;
    
    printf("Inside main:\n");
    printf("  Local hawk: %d\n", local_hawk);
    printf("  Global eagle: %d\n", global_eagle);
    
    // Block scope
    {
        int block_bird = 15;
        printf("  Block bird: %d\n", block_bird);
        // Can access outer scope
        printf("  Local hawk from outer: %d\n", local_hawk);
    }
    // block_bird is not accessible here
    
    display_scope();
    
    // Static variable demonstration
    static_demo();
    static_demo();
    static_demo();
    
    return 0;
}

Variables are only accessible within their scope. Local variables exist during function execution. Block variables exist only inside their block. Global variables exist for the entire program. Static local variables retain their values between calls. Global variables can be accessed using the extern keyword in other files.

Arrays and Strings

1. Arrays

Fixed-size collections of same type

Arrays are contiguous memory blocks that store multiple elements of the same type. They provide efficient indexed access.

Arrays store elements in contiguous memory. Indexing starts at 0. Declaration specifies type and size. Initialization can be done with braces. Arrays have fixed size and no bounds checking. Arrays and pointers are closely related.

Code Example

#include <stdio.h>

int main() {
    // Declaration and initialization
    int sparrow_count[5] = {10, 15, 12, 8, 20};
    
    // Partial initialization (rest set to 0)
    int eagle_count[5] = {5, 8};
    
    // Accessing elements
    printf("Sparrows: ");
    for (int i = 0; i < 5; i++) {
        printf("%d ", sparrow_count[i]);
    }
    printf("\n");
    
    printf("Eagles: ");
    for (int i = 0; i < 5; i++) {
        printf("%d ", eagle_count[i]);
    }
    printf("\n");
    
    // Modifying elements
    sparrow_count[2] = 20;
    printf("Modified index 2: %d\n", sparrow_count[2]);
    
    // Array size
    int size = sizeof(sparrow_count) / sizeof(sparrow_count[0]);
    printf("Array size: %d\n", size);
    
    // Relationship with pointers
    int* ptr = sparrow_count;
    printf("First element: %d\n", *ptr);
    printf("Second element: %d\n", *(ptr + 1));
    
    // Common mistake: out of bounds
    // printf("%d\n", sparrow_count[10]);  // Undefined behavior
    
    return 0;
}

Arrays allocate contiguous memory for elements. Indexing is offset from the base address. No bounds checking makes arrays fast but dangerous. In most expressions, an array name is converted to a pointer that refers to its first element. Array size must be known at compile time.

2. Multidimensional Arrays

2D, 3D, and beyond

Multidimensional arrays store data in multiple dimensions, like grids or matrices.

2D arrays are arrays of arrays. Declared with two sizes. Memory layout is row-major (consecutive rows). 3D arrays add another dimension. Useful for tables, matrices, and grids.

Code Example

#include <stdio.h>

int main() {
    // 2D array (3 rows, 4 columns)
    int birds[3][4] = {
        {10, 15, 12, 8},   // Row 1
        {5, 3, 7, 12},     // Row 2
        {20, 18, 15, 10}   // Row 3
    };
    
    // Accessing 2D array
    printf("2D Array:\n");
    for (int row = 0; row < 3; row++) {
        printf("Row %d: ", row + 1);
        for (int col = 0; col < 4; col++) {
            printf("%d ", birds[row][col]);
        }
        printf("\n");
    }
    
    // Modifying 2D array
    birds[1][2] = 20;
    printf("\nModified row 2, col 3: %d\n", birds[1][2]);
    
    // 3D array (2 layers, 3 rows, 4 columns)
    int birds_3d[2][3][4] = {
        { // Layer 1
            {10, 15, 12, 8},
            {5, 3, 7, 12},
            {20, 18, 15, 10}
        },
        { // Layer 2
            {12, 17, 14, 10},
            {7, 5, 9, 14},
            {22, 20, 17, 12}
        }
    };
    
    printf("\n3D Array - Layer 1:\n");
    for (int row = 0; row < 3; row++) {
        for (int col = 0; col < 4; col++) {
            printf("%d ", birds_3d[0][row][col]);
        }
        printf("\n");
    }
    
    return 0;
}

2D arrays store rows contiguously in memory. Indexing uses [row][col]. 3D arrays extend this pattern. The total size is the product of dimensions. Memory layout is row-major.

3. Character Arrays

Strings as char arrays

Character arrays store strings as sequences of characters ending with a null terminator \0.

Char arrays store individual characters. The null character (\0) signals the end of a C string and tells the program where the string’s contents stop.String literals are stored in read-only memory. Char arrays are mutable. The null terminator is essential for string operations.

Code Example

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

int main() {
    // Character arrays
    char species1[10] = "Sparrow";
    char species2[] = "Eagle";
    char species3[20] = {'H', 'a', 'w', 'k', '\0'};
    
    // Print strings
    printf("Species 1: %s\n", species1);
    printf("Species 2: %s\n", species2);
    printf("Species 3: %s\n", species3);
    
    // Size and length
    printf("Size of species1: %zu\n", sizeof(species1));
    printf("Length: %zu\n", strlen(species1));
    
    // Modifying char arrays
    species1[0] = 'C';
    species1[1] = 'a';
    species1[2] = 'r';
    species1[3] = 'd';
    species1[4] = 'i';
    species1[5] = 'n';
    species1[6] = 'a';
    species1[7] = 'l';
    species1[8] = '\0';
    printf("Modified: %s\n", species1);
    
    // String literal (read-only)
    const char* species4 = "Falcon";
    printf("String literal: %s\n", species4);
    // species4[0] = 'P';  // ERROR: Cannot modify string literal
    
    // Null terminator importance
    char no_null[5] = {'H', 'e', 'l', 'l', 'o'};
    // printf("%s\n", no_null);  // DANGER: No null terminator!
    
    return 0;
}

Char arrays end with \0 to indicate the string’s end. Functions like strlen rely on the null terminator. String literals are immutable. Char arrays are mutable. Without the null terminator, string functions read beyond the array.

4. Strings

The null-terminated string concept

In C, a string is stored as a sequence of characters followed by a null character (\0), which marks the end of the text. This representation allows C programs to work with textual data.

A string is a sequence of characters ending with \0. Declaration can be as a char array or pointer. String literals are constants stored in read-only memory. Mutability depends on how the string is declared.

Code Example

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

int main() {
    // String as array (mutable)
    char bird1[] = "Sparrow";
    printf("Bird 1: %s\n", bird1);
    bird1[0] = 'C';  // Works (array)
    printf("Modified: %s\n", bird1);
    
    // String as pointer (immutable literal)
    const char* bird2 = "Eagle";
    printf("Bird 2: %s\n", bird2);
    // bird2[0] = 'B';  // ERROR: Trying to modify string literal
    
    // String as pointer to array
    char buffer[20] = "Hawk";
    const char* bird3 = buffer;
    printf("Bird 3: %s\n", bird3);
    
    // Dynamic string (on heap)
    char* bird4 = malloc(20);
    if (bird4) {
        strcpy(bird4, "Cardinal");
        printf("Bird 4: %s\n", bird4);
        bird4[0] = 'P';  // Works (heap memory)
        printf("Modified: %s\n", bird4);
        free(bird4);
    }
    
    // String length
    char bird5[] = "Falcon";
    printf("Length of Falcon: %zu\n", strlen(bird5));
    printf("Size of array: %zu\n", sizeof(bird5));
    
    return 0;
}

Strings are null-terminated. Array declarations are mutable. Pointer to string literal is immutable. Dynamic allocation provides mutable strings on the heap. strlen() counts characters before \0. sizeof() gives array size including \0.

5. String Library Functions

strlen, strcpy, strcat, strcmp, and more

String library functions provide safe and efficient operations on strings.

strlen() returns string length. strcpy() copies strings. strcat() concatenates strings. strcmp() compares strings. strncpy() and strncat() are safer versions with length limits. #include <string.h> is required.

Code Example

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

int main() {
    char bird1[20] = "Sparrow";
    char bird2[20] = "Eagle";
    char buffer[50];
    
    // strlen - length
    printf("Length of Sparrow: %zu\n", strlen(bird1));
    printf("Length of Eagle: %zu\n", strlen(bird2));
    
    // strcpy - copy
    strcpy(buffer, bird1);
    printf("Copied: %s\n", buffer);
    
    // strncpy - safe copy
    strncpy(buffer, "Falcon", 3);
    buffer[3] = '\0';
    printf("First 3 chars: %s\n", buffer);
    
    // strcat - concatenate
    strcpy(buffer, bird1);
    strcat(buffer, " ");
    strcat(buffer, bird2);
    printf("Concatenated: %s\n", buffer);
    
    // strncat - safe concatenate
    strcpy(buffer, bird1);
    strncat(buffer, " ", 1);
    strncat(buffer, bird2, 3);
    printf("First 3 letters: %s\n", buffer);
    
    // strcmp - compare
    printf("Compare Sparrow vs Eagle: %d\n", strcmp(bird1, bird2));
    printf("Compare Eagle vs Eagle: %d\n", strcmp(bird2, "Eagle"));
    printf("Compare Sparrow vs Sparrow: %d\n", strcmp(bird1, "Sparrow"));
    
    // strstr - find substring
    char* found = strstr(bird1, "arr");
    if (found) {
        printf("Found 'arr' at: %ld\n", found - bird1);
    }
    
    // strchr - find character
    char* char_found = strchr(bird1, 'p');
    if (char_found) {
        printf("Found 'p' at: %ld\n", char_found - bird1);
    }
    
    return 0;
}

strlen scans until \0. strcpy copies until \0 and appends \0. strcat finds the end and copies. strcmp compares lexicographically. strncpy and strncat limit copying to prevent buffer overflows. These functions provide the basic tools needed to work with and modify strings in C.

Pointers and Memory Management

1. Pointers

Variables that store memory addresses

Pointers are variables that store memory addresses. They allow a program to access specific memory locations directly and modify the data stored there.

A pointer is a variable whose value represents the memory address of another object. The & operator obtains the address of a variable, while the * operator dereferences a pointer to access the value stored at that address. A null pointer does not refer to a valid object. Since pointers have associated types, C can perform pointer arithmetic according to the size of the pointed-to type.

Code Example

#include <stdio.h>

int main() {
    int sparrow = 10;
    int eagle = 20;
    
    // Pointer declaration
    int* ptr = &sparrow;  // ptr stores address of sparrow
    
    // Address and value
    printf("sparrow value: %d\n", sparrow);
    printf("sparrow address: %p\n", (void*)&sparrow);
    printf("ptr value (address): %p\n", (void*)ptr);
    printf("ptr dereferenced: %d\n", *ptr);
    
    // Modifying through pointer
    *ptr = 15;
    printf("sparrow after modify: %d\n", sparrow);
    
    // Pointers to different types
    double hawk = 3.14;
    double* dptr = &hawk;
    printf("Hawk: %.2f\n", *dptr);
    
    // Null pointer
    int* null_ptr = NULL;
    if (null_ptr == NULL) {
        printf("Null pointer\n");
    }
    // printf("%d\n", *null_ptr);  // Dereferencing NULL is dangerous!
    
    // Pointer to pointer
    int robin = 30;
    int* p1 = &robin;
    int** p2 = &p1;
    printf("Value through pointer to pointer: %d\n", **p2);
    
    return 0;
}

Pointers store addresses. The & operator gets a variable’s address. The * operator gets the value at that address. Pointers must be declared with the correct type. Null pointers should be checked before dereferencing.

2. Pointer Arithmetic

Incrementing, decrementing, and array traversal

Pointer arithmetic performs operations on pointer values, enabling efficient array traversal.

Pointer arithmetic adjusts an address according to the size of the type being referenced. Using ++ advances a pointer to the next element, while -- moves it back to the previous one. Subtracting two pointers that refer to elements of the same array determines the number of elements between them. This technique can also be used to efficiently traverse arrays.

Code Example

#include <stdio.h>

int main() {
    int birds[5] = {10, 20, 30, 40, 50};
    int* ptr = birds;  // Points to first element
    
    // Pointer arithmetic basics
    printf("First: %d\n", *ptr);
    ptr++;  // Move to next element
    printf("Second: %d\n", *ptr);
    ptr += 2;  // Move 2 elements forward
    printf("Fourth: %d\n", *ptr);
    ptr--;  // Move back
    printf("Third: %d\n", *ptr);
    
    // Array traversal with pointers
    printf("\nArray traversal:\n");
    ptr = birds;  // Reset to start
    for (int i = 0; i < 5; i++) {
        printf("%d ", *(ptr + i));
    }
    printf("\n");
    
    // Pointer difference
    int* start = birds;
    int* end = birds + 5;
    printf("Elements: %ld\n", end - start);
    
    // Comparison
    ptr = birds;
    while (ptr < birds + 5) {
        printf("%d ", *ptr);
        ptr++;
    }
    printf("\n");
    
    // Pointer arithmetic with different types
    int int_array[3] = {1, 2, 3};
    int* int_ptr = int_array;
    double double_array[3] = {1.1, 2.2, 3.3};
    double* double_ptr = double_array;
    
    printf("int_ptr + 1 moves %zu bytes\n", (char*)(int_ptr + 1) - (char*)int_ptr);
    printf("double_ptr + 1 moves %zu bytes\n", (char*)(double_ptr + 1) - (char*)double_ptr);
    
    return 0;
}

Pointer arithmetic depends on the type’s size. When 1 is added to an int* pointer, its address advances by the size of one int, which is sizeof(int) bytes. Arrays use pointer arithmetic internally. The relationship birds[i] is equivalent to *(birds + i). Pointer difference yields the number of elements between addresses.

3. Dynamic Memory Allocation

malloc, calloc, realloc, free

Dynamic memory allocation functions manage memory on the heap during program execution.

malloc() allocates raw memory. calloc() allocates and initializes to zero. realloc() resizes allocated memory. free() releases memory back to the system. Heap is used for dynamic allocation. Error handling checks for allocation failure.

Code Example

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

int main() {
    // ---- MALLOC ----
    // Allocate memory for one integer
    int* ptr1 = (int*)malloc(sizeof(int));
    if (ptr1 == NULL) {
        printf("Allocation failed!\n");
        return 1;
    }
    *ptr1 = 42;
    printf("malloc: %d\n", *ptr1);
    free(ptr1);
    
    // Allocate array with malloc
    int* array = (int*)malloc(5 * sizeof(int));
    if (array == NULL) {
        printf("Allocation failed!\n");
        return 1;
    }
    for (int i = 0; i < 5; i++) {
        array[i] = i * 10;
    }
    printf("Malloc array: ");
    for (int i = 0; i < 5; i++) {
        printf("%d ", array[i]);
    }
    printf("\n");
    free(array);
    
    // ---- CALLOC ----
    // Allocate and initialize to zero
    int* calloc_array = (int*)calloc(5, sizeof(int));
    if (calloc_array == NULL) {
        printf("Allocation failed!\n");
        return 1;
    }
    printf("Calloc array (all zeros): ");
    for (int i = 0; i < 5; i++) {
        printf("%d ", calloc_array[i]);
    }
    printf("\n");
    free(calloc_array);
    
    // ---- REALLOC ----
    // Resize allocated memory
    int* resize_array = (int*)malloc(3 * sizeof(int));
    if (resize_array == NULL) {
        printf("Allocation failed!\n");
        return 1;
    }
    resize_array[0] = 10;
    resize_array[1] = 20;
    resize_array[2] = 30;
    
    // Resize to 5 elements
    int* new_array = (int*)realloc(resize_array, 5 * sizeof(int));
    if (new_array == NULL) {
        printf("Reallocation failed!\n");
        free(resize_array);
        return 1;
    }
    new_array[3] = 40;
    new_array[4] = 50;
    
    printf("Realloc array: ");
    for (int i = 0; i < 5; i++) {
        printf("%d ", new_array[i]);
    }
    printf("\n");
    free(new_array);
    
    // ---- MEMORY LEAK EXAMPLE ----
    // ERROR: Forgetting to free
    // int* leak = (int*)malloc(100 * sizeof(int));
    // No free() called - memory leak!
    
    return 0;
}

malloc returns a pointer to allocated memory or NULL on failure. calloc initializes to zero. realloc can shrink or grow memory. free releases memory back to the heap. Always check return values for failure. Never free the same pointer twice

4. Memory Leaks

Causes, detection, and prevention

Memory leaks occur when allocated memory is never freed, causing the program to use more and more memory.

Memory leak happens when malloc/calloc isn’t followed by free. Effects include program slowdown, crashes, and resource exhaustion. Detection uses tools like Valgrind. Prevention includes always pairing allocation with free and using smart pointers.

Code Example

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

// --- MEMORY LEAK EXAMPLE ---
void create_leak() {
    int* leak = (int*)malloc(100 * sizeof(int));
    // Memory allocated but never freed
    printf("Memory leaked!\n");
    // leak is lost when function returns
}

// --- PROPER MEMORY MANAGEMENT ---
void proper_management() {
    int* data = (int*)malloc(100 * sizeof(int));
    if (data == NULL) return;
    
    // Use data
    for (int i = 0; i < 100; i++) {
        data[i] = i;
    }
    
    free(data);  // Properly freed
    printf("Memory properly freed!\n");
}

// --- DANGLING POINTER PREVENTION ---
void prevent_dangling() {
    int* data = (int*)malloc(sizeof(int));
    *data = 42;
    
    free(data);
    data = NULL;  // Prevent dangling pointer
    // Now safe - data is NULL
}

int main() {
    printf("---- Memory Leak Prevention ----\n\n");
    
    // Memory leak
    printf("Leaking memory:\n");
    create_leak();
    printf("Memory was leaked!\n\n");
    
    // Proper management
    printf("Proper management:\n");
    proper_management();
    printf("\n");
    
    // Prevention
    printf("Dangling pointer prevention:\n");
    prevent_dangling();
    printf("\n");
    
    // ---- USING VALGRIND ----
    printf("---- Valgrind Usage ----\n");
    printf("To detect leaks with Valgrind:\n");
    printf("valgrind --leak-check=full ./program\n");
    printf("\nExample output shows:\n");
    printf("- Heap memory allocated\n");
    printf("- Memory leaked\n");
    printf("- File and line number\n");
    
    return 0;
}

Memory leaks occur when free is never called. The memory remains allocated until the program ends. Valgrind tracks all allocations and reports leaks at program exit. Prevention includes always freeing memory, setting pointers to NULL after free, and using RAII patterns in C++.

Structures, Unions and Enumerations

1. Structures

Structures (structs) group related variables into a single custom type.

struct defines a new type with multiple members. Members can be different types. Dot operator (.) accesses members. Arrow operator (->) accesses members through pointers. Structures help organize data logically.

Code Example

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

// Define a structure
struct Bird {
    char species[50];
    int count;
    double weight;
    char color[20];
};

int main() {
    // Create struct variable
    struct Bird sparrow;
    strcpy(sparrow.species, "Sparrow");
    sparrow.count = 10;
    sparrow.weight = 25.5;
    strcpy(sparrow.color, "Brown");
    
    // Access members
    printf("Species: %s\n", sparrow.species);
    printf("Count: %d\n", sparrow.count);
    printf("Weight: %.1f\n", sparrow.weight);
    printf("Color: %s\n", sparrow.color);
    
    // Initialize at declaration
    struct Bird eagle = {"Eagle", 3, 4500.0, "Brown"};
    
    // Modify members
    eagle.count = 5;
    printf("\nEagle count: %d\n", eagle.count);
    
    // Array of structures
    struct Bird birds[3] = {
        {"Sparrow", 10, 25.5, "Brown"},
        {"Eagle", 3, 4500.0, "Brown"},
        {"Hawk", 5, 1200.0, "Gray"}
    };
    
    printf("\nBird list:\n");
    for (int i = 0; i < 3; i++) {
        printf("%s: %d birds\n", birds[i].species, birds[i].count);
    }
    
    // Pointer to structure
    struct Bird* ptr = &sparrow;
    printf("\nUsing pointer:\n");
    printf("Species: %s\n", ptr->species);
    printf("Count: %d\n", ptr->count);
    
    return 0;
}

Structures group data logically. Memory is allocated contiguously for members. Dot operator accesses members for variables. Arrow operator accesses members for pointers. Structures can contain other structures and can also be organized into arrays, allowing related data to be grouped and managed efficiently.

2. Nested Structures

Structures inside structures

Nested structures allow a structure to include another structure as one of its members, creating a hierarchical way to organize related data.

Structures can contain other structures as members. Nested access uses multiple dot operators. Useful for hierarchical data modeling.

Code Example

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

// Inner structure
struct BirdInfo {
    char species[50];
    int count;
    double weight;
};

// Outer structure with nested structure
struct BirdSighting {
    struct BirdInfo bird;  // Nested structure
    char location[50];
    char date[20];
    char observer[50];
};

int main() {
    // Create sighting
    struct BirdSighting sighting;
    
    // Initialize nested structure
    strcpy(sighting.bird.species, "Eagle");
    sighting.bird.count = 3;
    sighting.bird.weight = 4500.0;
    
    // Initialize outer structure
    strcpy(sighting.location, "Mountains");
    strcpy(sighting.date, "2024-01-15");
    strcpy(sighting.observer, "John");
    
    // Access nested members
    printf("=== Bird Sighting ===\n");
    printf("Species: %s\n", sighting.bird.species);
    printf("Count: %d\n", sighting.bird.count);
    printf("Weight: %.1f\n", sighting.bird.weight);
    printf("Location: %s\n", sighting.location);
    printf("Date: %s\n", sighting.date);
    printf("Observer: %s\n", sighting.observer);
    
    // Initialize with nested braces
    struct BirdSighting sighting2 = {
        {"Sparrow", 10, 25.5},  // Nested initialization
        "Park",
        "2024-01-16",
        "Jane"
    };
    
    printf("\nSecond sighting:\n");
    printf("%s at %s\n", sighting2.bird.species, sighting2.location);
    
    return 0;
}

Nested structures allow hierarchical data modeling. Access uses dot operators for each level. Initialization uses nested braces. Nested structures are useful for complex data types.

3. Unions

Overlapping memory for different types

Unions allow storing different types in the same memory location, using only the largest member’s memory.

A union stores all of its members in the same memory location, so only one member can hold a meaningful value at a time. Its size is generally determined by the size of its largest member, subject to alignment requirements. Only one member can be active at a time. Useful for memory efficiency and variant data.

Code Example

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

// Union for different data types
union BirdData {
    int count;
    double weight;
    char species[20];
};

// Union in a structure
struct BirdWithData {
    char type[20];  // Indicates which union member is active
    union BirdData data;
};

int main() {
    // Basic union
    union BirdData data;
    
    // Store integer
    data.count = 100;
    printf("Count: %d\n", data.count);
    printf("As weight: %.2f\n", data.weight);  // Garbage
    
    // Store double
    data.weight = 45.6;
    printf("Weight: %.2f\n", data.weight);
    printf("As count: %d\n", data.count);  // Garbage
    
    // Store string
    strcpy(data.species, "Sparrow");
    printf("Species: %s\n", data.species);
    printf("As count: %d\n", data.count);  // Garbage
    
    // Union size
    printf("Union size: %zu\n", sizeof(union BirdData));
    printf("Largest member: %zu\n", sizeof(char[20]));
    
    // Union with type indicator
    struct BirdWithData bird;
    strcpy(bird.type, "count");
    bird.data.count = 10;
    
    printf("\nType: %s\n", bird.type);
    if (strcmp(bird.type, "count") == 0) {
        printf("Count: %d\n", bird.data.count);
    }
    
    return 0;
}

Union members share the same memory. Writing to one member overwrites others. The size of a union is generally based on its largest member, with the final size potentially adjusted to meet the required memory alignment.Type information must be tracked separately. Unions are memory-efficient but need careful use.

4. Enumerations

Named integer constants

Enumerations define named integer constants, making code more readable and type-safe.

enum defines a set of named constants. Constants are integers starting at 0. Values can be explicitly set. Enums are useful for states, options, and flags.

Code Example

#include <stdio.h>

// Basic enumeration
enum BirdType {
    EAGLE,
    SPARROW,
    HAWK,
    CARDINAL,
    FINCH
};

// Enum with specific values
enum BirdStatus {
    ACTIVE = 1,
    MIGRATING = 2,
    NESTING = 4,
    ENDANGERED = 8
};

// Enum for seasons
enum Season {
    SPRING = 0,
    SUMMER = 1,
    FALL = 2,
    WINTER = 3
};

// Enum for error codes
enum ErrorCode {
    SUCCESS = 0,
    ERR_FILE_NOT_FOUND = -1,
    ERR_OUT_OF_MEMORY = -2,
    ERR_INVALID_INPUT = -3
};

int main() {
    // Using enum
    enum BirdType bird = EAGLE;
    
    switch (bird) {
        case EAGLE:
            printf("Bird is an eagle\n");
            break;
        case SPARROW:
            printf("Bird is a sparrow\n");
            break;
        default:
            printf("Unknown bird\n");
    }
    
    // Enum values
    printf("EAGLE = %d\n", EAGLE);
    printf("SPARROW = %d\n", SPARROW);
    printf("HAWK = %d\n", HAWK);
    
    // Enum with specific values
    enum BirdStatus status = ACTIVE | MIGRATING;
    if (status & ACTIVE) {
        printf("Bird is active\n");
    }
    if (status & MIGRATING) {
        printf("Bird is migrating\n");
    }
    
    // Enum as function parameter
    void print_season(enum Season s) {
        switch (s) {
            case SPRING: printf("Spring\n"); break;
            case SUMMER: printf("Summer\n"); break;
            case FALL: printf("Fall\n"); break;
            case WINTER: printf("Winter\n"); break;
        }
    }
    
    print_season(SPRING);
    
    return 0;
}

Enums define integer constants with meaningful names. Values default to sequential integers. Enums improve code readability. Type checking is limited, but enums help document intent.

5. typedef

Creating type aliases

typedef creates new names (aliases) for existing types, improving code readability.

typedef creates type aliases. Simplifies complex declarations. Improves readability. Can be used with structs and other types.

Code Example

#include <stdio.h>

// Typedef for basic types
typedef int bird_count_t;
typedef float weight_t;
typedef char species_t[50];

// Typedef for structures
typedef struct {
    species_t species;
    bird_count_t count;
    weight_t weight;
} Bird;

// Typedef for function pointer
typedef int (*compare_func)(int, int);

// Typedef for enum
typedef enum {
    RED,
    BLUE,
    GREEN
} Color;

int main() {
    // Using typedefs
    bird_count_t sparrows = 10;
    weight_t eagle_weight = 4500.0;
    species_t bird_species = "Sparrow";
    
    printf("Sparrows: %d\n", sparrows);
    printf("Eagle weight: %.1f\n", eagle_weight);
    printf("Species: %s\n", bird_species);
    
    // Typedef with struct
    Bird eagle;
    strcpy(eagle.species, "Eagle");
    eagle.count = 3;
    eagle.weight = 4500.0;
    
    printf("\nEagle: %s, %d, %.1f\n", 
           eagle.species, eagle.count, eagle.weight);
    
    // Typedef with enum
    Color bird_color = RED;
    printf("Color: %d\n", bird_color);
    
    // Typedef for function pointer
    compare_func cmp = NULL;
    
    return 0;
}

typedef creates aliases that are easier to read. It doesn’t create new types, just synonyms. Useful for platform-specific types (size_t, int32_t). Improves code maintainability.

Advanced C Programming

1. Preprocessor Directives

#include, #define, #if, #ifdef, #ifndef, #endif

Preprocessor directives are instructions processed before compilation. They control code inclusion, macro definition, and conditional compilation.

#include inserts header files. #define defines macros. #if, #ifdef, #ifndef enable conditional compilation. #endif ends conditional blocks. The preprocessor runs before the compiler.

Code Example

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

// ---- MACRO DEFINITIONS ----
#define MAX_BIRDS 100
#define PI 3.14159
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))

// ---- CONDITIONAL COMPILATION ----
#define DEBUG 1

#ifdef DEBUG
    #define LOG(msg) printf("DEBUG: %s\n", msg)
#else
    #define LOG(msg)
#endif

// ---- MULTI-LINE MACRO ----
#define PRINT_BIRD(species, count) \
    printf("Species: %s\n", species); \
    printf("Count: %d\n", count)

// ---- PREPROCESSOR CONDITIONALS ----
#define VERSION 2

#if VERSION >= 2
    #define NEW_FEATURE 1
#endif

#ifndef MAX_BIRDS
    #define MAX_BIRDS 50
#endif

int main() {
    // Using macros
    printf("MAX_BIRDS: %d\n", MAX_BIRDS);
    printf("PI: %.5f\n", PI);
    printf("SQUARE(5): %d\n", SQUARE(5));
    printf("MAX(10, 20): %d\n", MAX(10, 20));
    
    // Conditional logging
    LOG("Program started");
    
    // Multi-line macro
    PRINT_BIRD("Sparrow", 10);
    
    #if NEW_FEATURE
        printf("New feature is enabled\n");
    #endif
    
    // Undefine and redefine
    #undef PI
    #define PI 3.14159265359
    printf("PI redefined: %.11f\n", PI);
    
    return 0;
}

The preprocessor processes directives before compilation. Macros perform text replacement. Conditional compilation includes or excludes code. This allows platform-specific code and debugging support.

2. Macros

Text replacement and function-like macros

Macros perform text replacement before compilation. Function-like macros act like functions but are expanded inline.

Simple macros define constants. Function-like macros take arguments. Potential issues include side effects and lack of type checking. Use parentheses to avoid precedence issues.

Code Example

#include <stdio.h>

// Simple macros
#define PI 3.14159
#define MAX_BIRDS 100

// Function-like macros
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define ABS(x) ((x) < 0 ? -(x) : (x))

// Macro with side effects (DANGEROUS)
#define INCREMENT(x) ((x) + 1)

// Macro for debugging
#define PRINT_VAR(x) printf(#x " = %d\n", x)

// Macro for repeated code
#define BIRD_CASE(name, count) \
    case name: \
        printf("%s: %d\n", #name, count); \
        break

int main() {
    // Using macros
    int x = 5;
    int y = 3;
    
    printf("SQUARE(5): %d\n", SQUARE(5));
    printf("MAX(10, 20): %d\n", MAX(10, 20));
    printf("ABS(-5): %d\n", ABS(-5));
    
    // Macro with side effect (AVOID)
    int z = 5;
    printf("INCREMENT(z): %d\n", INCREMENT(z));
    printf("z: %d (unchanged)\n", z);
    
    // PRINT_VAR macro
    int birds = 10;
    PRINT_VAR(birds);
    
    // Macro for switch cases
    int choice = 1;
    switch (choice) {
        BIRD_CASE(1, 10);
        BIRD_CASE(2, 20);
        BIRD_CASE(3, 30);
        default:
            printf("Unknown\n");
    }
    
    // Macro pitfalls
    // SQUARE(3+2) expands to 3+2*3+2 = 11 (wrong)
    // With parentheses: ((3+2)*(3+2)) = 25 (correct)
    printf("SQUARE(3+2): %d\n", SQUARE(3+2));
    
    return 0;
}

Macros are text substitutions. Parentheses are important to avoid precedence issues. Function-like macros have no type checking. Use inline functions instead of macros when possible. Macros are expanded before compilation.

3. Header Files

Creating and using custom header files

Custom header files contain declarations shared across multiple source files, enabling code reuse and organization.

Header files contain function prototypes, macros, and type definitions. Include guards prevent multiple inclusions. Use angle brackets (< >) when including standard or system headers, while quotation marks (" ") are typically used for headers created within your own project. Headers enable modular programming.

Code Example

// ---- bird.h (Header file) ----
#ifndef BIRD_H
#define BIRD_H

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

// Constants
#define MAX_NAME 50
#define MAX_BIRDS 100

// Structure definition
typedef struct {
    char species[MAX_NAME];
    int count;
    double weight;
} Bird;

// Function prototypes
int add_bird(Bird* birds, int* size, Bird new_bird);
void display_birds(Bird* birds, int size);
Bird* find_bird(Bird* birds, int size, const char* species);

#endif

// ---- bird.c (Implementation file) ----
#include "bird.h"
#include <string.h>

int add_bird(Bird* birds, int* size, Bird new_bird) {
    if (*size >= MAX_BIRDS) {
        return -1;  // Full
    }
    birds[*size] = new_bird;
    (*size)++;
    return 0;  // Success
}

void display_birds(Bird* birds, int size) {
    for (int i = 0; i < size; i++) {
        printf("%s: %d birds, %.1f grams\n",
               birds[i].species,
               birds[i].count,
               birds[i].weight);
    }
}

Bird* find_bird(Bird* birds, int size, const char* species) {
    for (int i = 0; i < size; i++) {
        if (strcmp(birds[i].species, species) == 0) {
            return &birds[i];
        }
    }
    return NULL;
}

// ---- main.c (Main program) ----
#include "bird.h"

int main() {
    Bird birds[MAX_BIRDS];
    int size = 0;
    
    // Create birds
    Bird sparrow = {"Sparrow", 10, 25.5};
    Bird eagle = {"Eagle", 3, 4500.0};
    
    // Add birds
    add_bird(birds, &size, sparrow);
    add_bird(birds, &size, eagle);
    
    // Display
    display_birds(birds, size);
    
    // Find bird
    Bird* found = find_bird(birds, size, "Eagle");
    if (found) {
        printf("\nFound: %s\n", found->species);
    }
    
    return 0;
}

Header files contain declarations and prototypes. Include guards prevent multiple inclusions. Implementation files contain definitions. The compiler compiles each .c file separately. The linker combines the compiled object files and required libraries to produce the final executable program.

4. Function Pointers

Pointers to functions

Function pointers store addresses of functions, enabling callbacks and polymorphism.

A function pointer holds the address of a function. Declaration includes return type and parameters. Callbacks pass functions as arguments. Arrays of function pointers implement dispatch tables.

Code Example

#include <stdio.h>

// ---- BASIC FUNCTION POINTER ----
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }
int divide(int a, int b) { return b != 0 ? a / b : 0; }

// ---- FUNCTION POINTER DECLARATION ----
int (*operation)(int, int);  // Pointer to function taking two ints

// ---- FUNCTION POINTER AS PARAMETER ----
int calculate(int a, int b, int (*func)(int, int)) {
    return func(a, b);
}

// ---- CALLBACK DEMONSTRATION ----
void process_birds(int* birds, int size, void (*callback)(int)) {
    for (int i = 0; i < size; i++) {
        callback(birds[i]);
    }
}

void print_bird(int bird) {
    printf("%d ", bird);
}

void double_bird(int* bird) {
    *bird *= 2;
}

int main() {
    // ---- FUNCTION POINTER USAGE ----
    printf("Function pointers:\n");
    
    operation = add;
    printf("Add: %d\n", operation(10, 5));
    
    operation = subtract;
    printf("Subtract: %d\n", operation(10, 5));
    
    operation = multiply;
    printf("Multiply: %d\n", operation(10, 5));
    
    // ---- FUNCTION POINTER AS PARAMETER ----
    printf("\nPassing function as parameter:\n");
    printf("Add: %d\n", calculate(10, 5, add));
    printf("Subtract: %d\n", calculate(10, 5, subtract));
    
    // ---- ARRAY OF FUNCTION POINTERS ----
    int (*operations[4])(int, int) = {add, subtract, multiply, divide};
    const char* names[] = {"Add", "Subtract", "Multiply", "Divide"};
    
    printf("\nDispatch table:\n");
    for (int i = 0; i < 4; i++) {
        printf("%s: %d\n", names[i], operations[i](10, 5));
    }
    
    // ---- CALLBACK EXAMPLE ----
    int birds[] = {10, 20, 30, 40, 50};
    printf("\nCallback example:\n");
    process_birds(birds, 5, print_bird);
    printf("\n");
    
    return 0;
}

Function pointers store addresses of functions. They can be assigned, passed as parameters, and stored in arrays. This enables callbacks and polymorphic behavior. The signature must match exactly.

5. Bitwise Operations

&, |, ^, ~, <<, >> and their uses

Bitwise operations manipulate individual bits of integer values, enabling efficient flag handling and low-level operations.

& AND sets bits that are 1 in both operands. | OR sets bits that are 1 in either operand. ^ XOR sets bits that differ between operands. ~ NOT flips all bits. << left shift multiplies by power of 2. >> right shift divides by power of 2.

Code Example

#include <stdio.h>

// Flag definitions
#define FLAG_ACTIVE  (1 << 0)  // 0001
#define FLAG_MIGRATING (1 << 1)  // 0010
#define FLAG_NESTING (1 << 2)   // 0100
#define FLAG_ENDANGERED (1 << 3) // 1000

int main() {
    // ---- BITWISE OPERATORS ----
    unsigned char a = 0b0011;  // 3
    unsigned char b = 0b0101;  // 5
    
    printf("AND: %d\n", a & b);   // 1 (0001)
    printf("OR: %d\n", a | b);    // 7 (0111)
    printf("XOR: %d\n", a ^ b);   // 6 (0110)
    printf("NOT a: %d\n", ~a);    // 252 (11111100)
    
    // ---- SHIFT OPERATORS ----
    unsigned char value = 0b0001;  // 1
    printf("Left shift by 1: %d\n", value << 1);   // 2
    printf("Left shift by 2: %d\n", value << 2);   // 4
    printf("Right shift by 1: %d\n", 0b1000 >> 1); // 4
    
    // ---- BIT FLAGS ----
    unsigned char flags = 0;
    
    // Set flags
    flags |= FLAG_ACTIVE;
    flags |= FLAG_MIGRATING;
    printf("Flags set: %d\n", flags);
    
    // Check flags
    if (flags & FLAG_ACTIVE) {
        printf("Bird is active\n");
    }
    if (flags & FLAG_MIGRATING) {
        printf("Bird is migrating\n");
    }
    if (flags & FLAG_NESTING) {
        printf("Bird is nesting\n");
    }
    
    // Clear flag
    flags &= ~FLAG_MIGRATING;
    printf("After clearing: %d\n", flags);
    
    // Toggle flag
    flags ^= FLAG_NESTING;
    printf("After toggling: %d\n", flags);
    
    // ---- BITWISE APPLICATIONS ----
    int is_power_of_two = !(value & (value - 1));
    printf("Is power of two: %d\n", is_power_of_two);
    
    // Extract bits
    unsigned char data = 0b10101010;
    unsigned char low_nibble = data & 0x0F;  // Lower 4 bits
    unsigned char high_nibble = (data >> 4) & 0x0F;  // Upper 4 bits
    printf("Data: %d, Low: %d, High: %d\n", data, low_nibble, high_nibble);
    
    return 0;
}

Bitwise operations work on individual bits. & clears bits, | sets bits, ^ toggles bits. Shifts multiply or divide by powers of 2. Bitwise operations are efficient and commonly used in system programming and embedded development.

Data Structures and Algorithms

1. Linked Lists

Singly and doubly linked lists

Linked lists are dynamic data structures where elements (nodes) are linked using pointers. They allow efficient insertion and deletion.

A linked list consists of nodes with data and a pointer to the next node. Singly linked nodes point forward. Doubly linked nodes point both forward and backward. Dynamic size allows growth and shrinking.

Code Example

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

// Node structure
typedef struct Node {
    int data;
    struct Node* next;
} Node;

// ---- CREATE NODE ----
Node* create_node(int data) {
    Node* new_node = (Node*)malloc(sizeof(Node));
    if (new_node == NULL) return NULL;
    new_node->data = data;
    new_node->next = NULL;
    return new_node;
}

// ---- INSERT AT FRONT ----
void insert_front(Node** head, int data) {
    Node* new_node = create_node(data);
    new_node->next = *head;
    *head = new_node;
}

// ---- INSERT AT END ----
void insert_end(Node** head, int data) {
    Node* new_node = create_node(data);
    if (*head == NULL) {
        *head = new_node;
        return;
    }
    Node* temp = *head;
    while (temp->next != NULL) {
        temp = temp->next;
    }
    temp->next = new_node;
}

// ---- DELETE NODE ----
void delete_node(Node** head, int data) {
    if (*head == NULL) return;
    if ((*head)->data == data) {
        Node* temp = *head;
        *head = (*head)->next;
        free(temp);
        return;
    }
    Node* temp = *head;
    while (temp->next != NULL && temp->next->data != data) {
        temp = temp->next;
    }
    if (temp->next != NULL) {
        Node* to_delete = temp->next;
        temp->next = to_delete->next;
        free(to_delete);
    }
}

// ---- FIND NODE ----
Node* find_node(Node* head, int data) {
    Node* temp = head;
    while (temp != NULL) {
        if (temp->data == data) return temp;
        temp = temp->next;
    }
    return NULL;
}

// ---- DISPLAY LIST ----
void display_list(Node* head) {
    Node* temp = head;
    while (temp != NULL) {
        printf("%d -> ", temp->data);
        temp = temp->next;
    }
    printf("NULL\n");
}

// ---- FREE LIST ----
void free_list(Node* head) {
    Node* temp;
    while (head != NULL) {
        temp = head;
        head = head->next;
        free(temp);
    }
}

int main() {
    Node* head = NULL;
    
    printf("Inserting birds (counts):\n");
    insert_front(&head, 10);
    insert_front(&head, 20);
    insert_end(&head, 30);
    insert_end(&head, 40);
    display_list(head);
    
    printf("\nFinding 20:\n");
    Node* found = find_node(head, 20);
    if (found) printf("Found: %d\n", found->data);
    
    printf("\nDeleting 20:\n");
    delete_node(&head, 20);
    display_list(head);
    
    printf("\nDeleting 10:\n");
    delete_node(&head, 10);
    display_list(head);
    
    free_list(head);
    return 0;
}

Each node stores its own data along with a pointer that links it to the next node. The head pointer keeps track of the first node in the linked list. Insertion at the front is O(1), at the end is O(n). Deletion requires finding the node. Linked lists allow dynamic size and efficient insertion/deletion.

2. Stacks

LIFO data structure

A stack is a Last-In-First-Out data structure. Elements are added (pushed) and removed (popped) from the top.

Push adds to the top. Pop removes from the top. Peek views the top without removing. LIFO means the last element added is the first removed.

Code Example

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

#define MAX_SIZE 100

// Stack structure (array-based)
typedef struct {
    int data[MAX_SIZE];
    int top;
} Stack;

// ---- INITIALIZE ----
void init_stack(Stack* s) {
    s->top = -1;
}

// ---- PUSH ----
int push(Stack* s, int value) {
    if (s->top >= MAX_SIZE - 1) {
        printf("Stack overflow!\n");
        return -1;
    }
    s->data[++s->top] = value;
    return 0;
}

// ---- POP ----
int pop(Stack* s) {
    if (s->top < 0) {
        printf("Stack underflow!\n");
        return -1;
    }
    return s->data[s->top--];
}

// ---- PEEK ----
int peek(Stack* s) {
    if (s->top < 0) {
        printf("Stack empty!\n");
        return -1;
    }
    return s->data[s->top];
}

// ---- IS EMPTY ----
int is_empty(Stack* s) {
    return s->top < 0;
}

// ---- DISPLAY ----
void display_stack(Stack* s) {
    printf("Stack: ");
    for (int i = 0; i <= s->top; i++) {
        printf("%d ", s->data[i]);
    }
    printf("\n");
}

int main() {
    Stack s;
    init_stack(&s);
    
    printf("Pushing birds:\n");
    push(&s, 10);
    push(&s, 20);
    push(&s, 30);
    display_stack(&s);
    
    printf("Peek: %d\n", peek(&s));
    
    printf("Pop: %d\n", pop(&s));
    printf("Pop: %d\n", pop(&s));
    display_stack(&s);
    
    printf("Push: 40\n");
    push(&s, 40);
    display_stack(&s);
    
    return 0;
}

The stack uses an array with a top index. Push increments top and adds data. Pop returns data and decrements top. Peek returns the top without removing. The stack is useful for function calls, expression evaluation, and undo operations.

3. Queues

FIFO data structure

A queue is a First-In-First-Out data structure. Elements are added at the rear and removed from the front.

Enqueue adds to the rear. Dequeue removes from the front. Front views the first element without removing. FIFO means first element added is first removed.

Code Example

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

#define MAX_SIZE 100

// Queue structure (circular array)
typedef struct {
    int data[MAX_SIZE];
    int front;
    int rear;
    int size;
} Queue;

// ---- INITIALIZE ----
void init_queue(Queue* q) {
    q->front = 0;
    q->rear = -1;
    q->size = 0;
}

// ---- ENQUEUE ----
int enqueue(Queue* q, int value) {
    if (q->size >= MAX_SIZE) {
        printf("Queue full!\n");
        return -1;
    }
    q->rear = (q->rear + 1) % MAX_SIZE;
    q->data[q->rear] = value;
    q->size++;
    return 0;
}

// ---- DEQUEUE ----
int dequeue(Queue* q) {
    if (q->size <= 0) {
        printf("Queue empty!\n");
        return -1;
    }
    int value = q->data[q->front];
    q->front = (q->front + 1) % MAX_SIZE;
    q->size--;
    return value;
}

// ---- FRONT ----
int front(Queue* q) {
    if (q->size <= 0) {
        printf("Queue empty!\n");
        return -1;
    }
    return q->data[q->front];
}

// ---- IS EMPTY ----
int is_empty(Queue* q) {
    return q->size == 0;
}

// ---- DISPLAY ----
void display_queue(Queue* q) {
    printf("Queue: ");
    int index = q->front;
    for (int i = 0; i < q->size; i++) {
        printf("%d ", q->data[index]);
        index = (index + 1) % MAX_SIZE;
    }
    printf("\n");
}

int main() {
    Queue q;
    init_queue(&q);
    
    printf("Enqueuing birds:\n");
    enqueue(&q, 10);
    enqueue(&q, 20);
    enqueue(&q, 30);
    display_queue(&q);
    
    printf("Front: %d\n", front(&q));
    
    printf("Dequeue: %d\n", dequeue(&q));
    printf("Dequeue: %d\n", dequeue(&q));
    display_queue(&q);
    
    printf("Enqueue: 40\n");
    enqueue(&q, 40);
    display_queue(&q);
    
    return 0;
}

The circular queue uses front and rear indices. Enqueue adds at rear and advances. Dequeue removes from front and advances. The array wraps around to reuse space. Queues are used for scheduling and buffering.

System Programming

1. Processes (fork and wait)

Creating and managing processes

Process management creates and controls processes. fork() creates a child process. wait() waits for process termination.

fork() creates a copy of the current process (parent). The child gets a copy of memory. Return value: 0 in child, PID in parent, -1 on error. wait() makes the parent wait for child termination. Processes are fundamental to operating systems.

Code Example

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    printf("Process creation example\n");
    printf("Process ID: %d\n", getpid());
    
    // ---- FORK DEMONSTRATION ----
    pid_t pid = fork();
    
    if (pid < 0) {
        printf("Fork failed!\n");
        return 1;
    }
    
    if (pid == 0) {
        // Child process
        printf("Child process\n");
        printf("Child PID: %d\n", getpid());
        printf("Parent PID: %d\n", getppid());
        
        // Child does some work
        for (int i = 0; i < 5; i++) {
            printf("Child: %d\n", i);
            sleep(1);
        }
        exit(0);
    } else {
        // Parent process
        printf("Parent process\n");
        printf("Parent PID: %d\n", getpid());
        printf("Child PID: %d\n", pid);
        
        // Wait for child to finish
        int status;
        wait(&status);
        printf("Child finished with status: %d\n", WEXITSTATUS(status));
    }
    
    return 0;
}

The fork() system call creates a new child process that can execute independently alongside its parent. The child initially receives a logical copy of the parent’s memory, while wait() allows the parent process to pause until a child process terminates. Each process operates within its own separate memory space, providing isolation between processes.

2. Threads (pthread)

Creating and synchronizing threads

Threads are lightweight units of execution that operate within a process and allow different tasks to run concurrently while sharing the process’s resources.POSIX threads (pthread) enable concurrent programming.

pthread_create() creates a new thread. pthread_join() waits for thread completion. Mutexes protect shared data. Threads within the same process share its memory space, allowing them to access common data and resources while executing independently.

Code Example

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

// Global shared data (with mutex)
int bird_count = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

// Thread function
void* count_birds(void* arg) {
    char* name = (char*)arg;
    int count = 0;
    
    while (count < 5) {
        // Lock mutex for safe access
        pthread_mutex_lock(&mutex);
        bird_count++;
        printf("%s: Bird %d (Total: %d)\n", name, ++count, bird_count);
        pthread_mutex_unlock(&mutex);
        
        usleep(500000);  // Sleep 0.5 seconds
    }
    
    pthread_exit(NULL);
}

int main() {
    pthread_t t1, t2;
    
    // Create threads
    pthread_create(&t1, NULL, count_birds, "Sparrow");
    pthread_create(&t2, NULL, count_birds, "Eagle");
    
    // Wait for threads to finish
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    
    printf("All birds counted: %d\n", bird_count);
    pthread_mutex_destroy(&mutex);
    
    return 0;
}

Threads run concurrently within the same process. Mutexes prevent race conditions. pthread_create starts a new thread. pthread_join waits for thread completion. Threads share memory, making synchronization critical.

Practical Projects

1. Calculator

Basic arithmetic calculator

A calculator program performs arithmetic operations based on user input.

Supports addition, subtraction, multiplication, division, and modulo. Handles division by zero. Uses a menu-driven interface. Validates input.

Code Example

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

// Function prototypes
void display_menu();
int add(int a, int b);
int subtract(int a, int b);
int multiply(int a, int b);
int divide(int a, int b);
int modulo(int a, int b);

int main() {
    int choice;
    double a, b, result;
    
    while (1) {
        display_menu();
        printf("Enter choice: ");
        scanf("%d", &choice);
        
        if (choice == 5) {
            printf("Goodbye!\n");
            break;
        }
        
        if (choice < 1 || choice > 5) {
            printf("Invalid choice!\n");
            continue;
        }
        
        printf("Enter first number: ");
        scanf("%lf", &a);
        printf("Enter second number: ");
        scanf("%lf", &b);
        
        switch (choice) {
            case 1:
                result = a + b;
                printf("%.2f + %.2f = %.2f\n", a, b, result);
                break;
            case 2:
                result = a - b;
                printf("%.2f - %.2f = %.2f\n", a, b, result);
                break;
            case 3:
                result = a * b;
                printf("%.2f * %.2f = %.2f\n", a, b, result);
                break;
            case 4:
                if (b == 0) {
                    printf("Division by zero!\n");
                } else {
                    result = a / b;
                    printf("%.2f / %.2f = %.2f\n", a, b, result);
                }
                break;
        }
        printf("\n");
    }
    
    return 0;
}

void display_menu() {
    printf("=== Calculator ===\n");
    printf("1. Add\n");
    printf("2. Subtract\n");
    printf("3. Multiply\n");
    printf("4. Divide\n");
    printf("5. Exit\n");
}

int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }
int divide(int a, int b) { return a / b; }
int modulo(int a, int b) { return a % b; }

The program continues running and repeating its main operations until the user selects the option to exit. It reads operation and numbers. Results are displayed. Division by zero is handled gracefully. The menu is displayed repeatedly.

Final Advice

C feels difficult at first because it refuses to hide the machine from you—but that honesty is exactly what makes it such a powerful language to master. Every other language you learn after this one will feel noticeably easier, because you’ll already understand what’s really happening underneath: how memory works, what a pointer actually is, and how a computer takes your code and turns it into running instructions.

Begin with the first stage today by running “Hello, World!” on your own computer using your terminal. Then break it on purpose—remove a semicolon, misspell printf, forget a closing brace—and read the resulting error message slowly and carefully instead of panicking.

That single habit, repeated consistently over weeks and months, is genuinely how every strong C programmer built their foundation.

Use the AI prompts provided for each concept. They’re designed to give you code examples, clear explanations, and practical exercises. Copy them into your favorite AI assistant and work through the examples yourself. Write the code, run it, modify it, break it, and fix it.

Good luck, and welcome to C. The journey is challenging, but the destination is worth it.

Scroll to Top