+91-91760-33446
C Programming Tutorial

C programming is a general-purpose, procedural, imperative computer programming language developed in 1972 by Dennis M. Ritchie at Bell Telephone Laboratories to develop the UNIX operating system. C is the most widely used computer language and consistently ranks among the top programming languages worldwide.

📦
16
Units
💻
80+
Code Examples
🧪
130+
Lab Exercises
🐧
Linux
GCC Platform

🎯 Why Learn C Programming?

C programming language is a MUST for students and working professionals who want to become great Software Engineers — especially in System Programming, Embedded Systems, and Operating Systems domains. Here are the key reasons to learn C:

  • C is the mother of all modern programming languages. Learning C makes it far easier to master C++, Java, Python, and others.
  • C gives you direct access to memory management — understanding pointers and memory allocation builds a rock-solid programming foundation.
  • It is a low-level language that interacts directly with hardware — essential for Embedded Systems, OS development, and device drivers.
  • C code runs nearly as fast as assembly language — critical for performance-sensitive applications.
  • C is used in Linux kernel, UNIX, embedded firmware, IoT devices, compilers, databases, and countless system utilities.
  • A strong knowledge of C is a competitive advantage in interviews for system programming, embedded, and networking roles.

📌 Facts about C

C is the most widely used System Programming Language. Most modern state-of-the-art software has been implemented using C. Here are some important facts:

  • C was invented to write an operating system called UNIX. The entire UNIX OS was written in C.
  • C is a successor of the B language, which was introduced around the early 1970s.
  • The language was formalized in 1988 by ANSI (American National Standards Institute) — known as ANSI C or C89.
  • Later standards include C99, C11, and C17, each adding features while maintaining backward compatibility.
  • The Linux kernel — which powers Android, servers, and supercomputers — is written almost entirely in C.
  • C has influenced nearly every major programming language, including C++, Java, C#, JavaScript, PHP, and Python.
  • Today, C consistently ranks in the top 3 most popular programming languages in the TIOBE index.

👋 Hello World in C

Every C programmer starts here. Below is the classic first program — it demonstrates the basic structure of every C program:

hello.c#include <stdio.h>

int main() {

    /* My first C program */
    printf("Hello, World!\n");

    return 0;
}

Compile and run on Linux terminal:

Terminal$ gcc hello.c -o hello
$ ./hello
Hello, World!
What each line means:
  • #include <stdio.h> — includes the Standard Input/Output library (needed for printf)
  • int main() — the entry point of every C program; execution always starts here
  • printf(...) — prints text to the terminal; \n moves to a new line
  • return 0 — tells the OS the program finished successfully

⚙️ How C Code Gets Compiled

Unlike interpreted languages, C source code goes through a multi-stage compilation pipeline before it becomes an executable binary on Linux:

Source Code
hello.c
Pre-processor
cpp
Compiler
cc1
Assembler
as
Linker
ld
Executable
./hello
StageToolWhat it does
PreprocessingcppExpands #include, #define, conditional macros
Compilationcc1Translates C source → assembly (.s)
AssemblyasConverts assembly → object code (.o)
LinkingldCombines object files + libraries → final executable

🚀 Applications of C Programming

C was initially designed for system development — particularly for programs that make up the operating system. Because C produces code that runs nearly as fast as assembly language, it became the language of choice for system software:

🖥️ Operating Systems
Linux kernel, UNIX, Windows NT kernel, macOS XNU core
📟 Embedded Systems
Microcontrollers (AVR, ARM Cortex-M), IoT firmware, RTOS
🔧 Language Compilers
GCC, Clang, Python interpreter (CPython), Ruby MRI
🗄️ Databases
MySQL, PostgreSQL, SQLite, Redis — all written in C
🌐 Network Drivers
TCP/IP stacks, network device drivers, protocol implementations
🖨️ System Utilities
Shell (bash), text editors (Vim, Emacs), compilers, linkers

📋 Complete Course Outline

UnitTopicKey Concepts
1Linux Dev EnvironmentGCC, terminal, file system, Linux commands
2Vim Editor for CNormal/Insert/Command modes, navigation, workflow
3Structure of a C ProgramCompilation pipeline, memory layout, hello world
4Data Types, Variables & OperatorsData types, sizeof, type casting, all operators
5Control Statementsif/else, switch, nested conditions
6Loops in Cfor, while, do-while, break, continue, goto
7Arrays & Strings1D/2D arrays, string.h, character arrays
9PointersAddress, dereference, arithmetic, pointer to array
8FunctionsDeclaration, call stack, recursion, scope rules
11Functions with PointersFunction pointers, callbacks, qsort, signal handlers
12Structures & Unionsstruct, union, enum, typedef, padding
13Pointers and Structuresstruct pointers, -> operator, arrays of structs
14File Handlingfopen, fread, fwrite, fseek, binary files
15Debugging & Best PracticesGDB, Valgrind, GCC warnings, code style
16Mini ProjectsStudent management, file records, calculator

👥 Who Is This Tutorial For?

This tutorial is designed for anyone who wants to learn C programming from scratch — particularly on the Linux platform. It is targeted at:

  • Engineering students (CSE, ECE, EEE) looking to strengthen their programming foundation
  • Embedded systems aspirants who need C as the primary language for firmware and drivers
  • Software professionals transitioning to system programming or Linux development
  • GATE / competitive exam candidates who need solid C knowledge
  • Anyone who wants to learn a language that is still deeply relevant in 2026 and beyond

Prerequisites

Before starting this tutorial, you should have:

  • A basic understanding of computer programming terminologies (what is a variable, loop, function)
  • Access to a Linux system — Ubuntu, Fedora, or WSL2 on Windows works perfectly
  • Basic familiarity with using a terminal/command prompt
  • No prior C experience required — this course starts from zero
Tip: If you're on Windows, install WSL2 (Windows Subsystem for Linux) — it gives you a full Ubuntu Linux environment inside Windows. Unit 1 covers the complete setup.

Frequently Asked Questions

Is C Programming still relevant in 2026? +
Absolutely. C remains the dominant language for system programming. The Linux kernel (running on 90%+ of servers and all Android devices), all major databases, embedded firmware, compilers, and network drivers are written in C. In the embedded and IoT industry, C is irreplaceable. Every Embedded Systems, RDK-B, and device driver job requires strong C skills.
Is C difficult to learn for beginners? +
C has a small, consistent syntax — there are very few keywords and constructs to learn. The initial difficulty is understanding pointers and memory management, but this tutorial introduces those concepts gradually with visual memory diagrams. With consistent practice (30–60 min/day), beginners become productive in 6–8 weeks.
What is the difference between C and C++? +
C is a procedural language that focuses on functions and procedures. C++ is a superset of C that adds Object-Oriented Programming (OOP), classes, templates, exceptions, and the STL. C is preferred for OS kernels, embedded firmware, and system utilities. C++ is preferred for game engines, large-scale applications, and embedded C++ (like using Arduino libraries). Learning C first gives you a massive head-start in C++.
Why learn C on Linux instead of Windows? +
In the real world, C development happens on Linux. All servers, embedded targets, network devices, and cloud infrastructure run on Linux. The GCC compiler, GDB debugger, Valgrind memory checker, and Make build system are native Linux tools. Learning C on Linux means you're working in the exact same environment as professional system programmers. Industry tools like Buildroot, Yocto, and Linux kernel development are all Linux-native.
What jobs require C programming skills? +
C is a required skill for: Embedded Systems Engineer, Firmware Developer, Linux Kernel Developer, Device Driver Engineer, RDK-B Gateway Developer, Network Protocol Engineer, IoT Developer, Systems Programmer, and RTOS Developer. These roles consistently offer excellent compensation, especially in the telecom, automotive, and semiconductor industries.
How long does it take to complete this course? +
At a relaxed self-paced of 1–2 hours/day, you can complete this course in 8–10 weeks. If you study intensively (3–4 hours/day), all 14 units can be covered in 3–4 weeks. Each unit has lab exercises — completing all labs is highly recommended to build real muscle memory with the language.
Unit 1 – Linux Development Environment Setup

Before writing any C code, you need to set up your Linux development environment. This unit covers what Linux is, which distribution to use, how to install GCC, and essential commands every developer needs.

📌 Setup Syntax Quick Reference

The first workflow in Linux development is command-driven. You create a workspace, edit a source file, compile it with GCC, and run the resulting binary from the same shell session.

mkdir -p ~/c-course/unit1
cd ~/c-course/unit1
vim hello.c
gcc -Wall -Wextra hello.c -o hello
./hello

📖 Theory Focus

  • Linux is preferred for C because the compiler, debugger, linker, headers, and shell workflow are all native parts of the platform.
  • A C developer works with files and processes directly, so understanding paths, permissions, and package tools is part of the language environment itself.
  • The shell is not separate from development; it is the control surface for compiling, testing, debugging, and automation.

🐧 1.1 – What is Linux & Why Use It for C?

Linux is an open-source, Unix-like operating system kernel created by Linus Torvalds in 1991. It is the dominant platform for systems programming, servers, embedded systems, and scientific computing.


Why Linux for C Development?

  • GCC (GNU Compiler Collection) is natively available — compiles C directly
  • POSIX-compliant — programs work across Linux, macOS, BSD
  • Full access to system calls (fork, exec, signals, sockets)
  • Professional tools: GDB, Valgrind, Make, Git — all built-in or one command away
  • No licensing cost — free and open source

Linux Distribution Comparison:

DistributionPackage ManagerBest ForInstall GCC
UbuntuaptBeginners, desktopssudo apt install gcc
FedoradnfDevelopers, cutting edgesudo dnf install gcc
DebianaptStability, serverssudo apt install gcc
Arch LinuxpacmanAdvanced userssudo pacman -S gcc
CentOS/RHELyum/dnfEnterprise serverssudo yum install gcc

⚙️ 1.2 – Installing Required Tools

On Ubuntu/Debian (most common for beginners):

# Step 1: Update your package list
sudo apt update

# Step 2: Upgrade existing packages
sudo apt upgrade -y

# Step 3: Install build-essential
# This installs: gcc, g++, make, libc-dev, and more
sudo apt install build-essential -y

# Step 4: Install additional tools
sudo apt install gdb valgrind vim -y

# Step 5: Verify installations
gcc --version       # should show: gcc (Ubuntu ...) 11.x.x
make --version      # should show: GNU Make 4.x
gdb --version       # should show: GNU gdb ...
Terminal
Expected output of gcc --version:
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
Copyright (C) 2021 Free Software Foundation, Inc.

📁 1.3 – Linux File System Hierarchy

Everything in Linux is a file. The filesystem starts at / (root):

/                    # Root of the entire filesystem
├── bin/             # Essential binaries (ls, cp, mv, gcc)
├── boot/            # Bootloader files
├── dev/             # Device files (/dev/sda, /dev/null)
├── etc/             # System configuration files
├── home/            # User home directories
│   └── username/    # YOUR home: ~ = /home/username
│       ├── projects/
│       └── .bashrc  # Shell config (hidden files start with .)
├── lib/             # Shared libraries (.so files)
├── proc/            # Virtual files: running processes
├── tmp/             # Temporary files (cleared on reboot)
├── usr/             # User programs
│   ├── bin/         # Installed programs (vim, gcc)
│   └── include/     # C header files (stdio.h, string.h)
└── var/             # Variable data: logs, caches
Filesystem
Key paths for C developers: Header files are in /usr/include/. Libraries are in /usr/lib/. Your GCC executable is at /usr/bin/gcc.

💻 1.4 – Essential Linux Commands for Developers

CategoryCommandDescriptionExample
NavigationpwdPrint working directorypwd/home/user/projects
ls -laList directory contentsls -la (hidden files + details)
cdChange directorycd ~/projects  ·  cd ..
mkdir -pCreate directory (nested)mkdir -p src/utils
FilescpCopy filecp file.c backup.c
mvMove / renamemv old.c new.c
rmRemove file / directoryrm file.c  ·  rm -rf dir/
touchCreate empty filetouch main.c
ViewcatDisplay file contentscat main.c
lessPage through fileless longfile.c  (q to quit)
head -20First 20 lineshead -20 main.c
tail -20Last 20 linestail -20 main.c
Permissionschmod +xMake executablechmod +x script.sh
chmod 755Set numeric permissionschmod 755 prog → rwxr-xr-x
chownChange ownerchown user:group file
Processesps auxShow all processesps aux | grep gcc
topLive process monitortop  (q to quit)
killTerminate by PIDkill 1234
SearchgrepSearch text in filesgrep "printf" main.c
findFind files by patternfind . -name "*.c"

🔐 1.5 – File Permissions Explained

$ ls -l hello
-rwxr-xr-- 1 alice devs 8432 Apr 19 10:00 hello
│││││││││
││││││││└── Other: r-- = read only
│││││││└─── Group: r-x = read + execute
││││││└──── Owner: rwx = read + write + execute
│││││└───── (special bits)
││││└────── Executable type: - = regular file, d = dir, l = link
│└└└─────── rwx bits
└────────── File type

# Numeric (octal) notation:
# r=4, w=2, x=1
chmod 755 hello    # rwxr-xr-x  owner:7, group:5, other:5
chmod 644 main.c   # rw-r--r--  owner:6, group:4, other:4
chmod +x hello     # add execute for all
Terminal

🖥️ 1.6 – Setting Up Your C Project in Terminal

# Create project structure
mkdir -p ~/c-course/unit1
cd ~/c-course/unit1

# Create your first C file
touch hello.c

# Edit with nano (beginners)
nano hello.c

# OR edit with vim (recommended)
vim hello.c

# Compile
gcc hello.c -o hello

# Run
./hello

# Compile with warnings (ALWAYS do this)
gcc -Wall -Wextra -o hello hello.c

# Check binary info
file hello          # ELF 64-bit LSB executable, x86-64 ...
./hello             # run it
echo $?             # print exit code (0 = success)
Terminal

Unit 1 – Lab Exercises (10 Tasks)

  1. Install GCC on your Linux machine. Run gcc --version and which gcc. Record the output. Also run gcc --version vs cc --version — are they different?
  2. Create the directory structure: ~/c-course/unit1/. Inside, create 3 empty files: hello.c, notes.txt, test.c. Use only terminal commands.
  3. Run ls -la ~/c-course/unit1/. Identify the permission bits for each file. What are the default permissions? Run umask and explain how it relates to the defaults.
  4. Practice: use cp, mv, rm, and cat on your files. Create a text file with echo "My first file" > first.txt, display it, append a second line with >>, then display again.
  5. Run grep -n "include" /usr/include/stdio.h | head -10. How many lines contain "include"? Try grep -c "include" /usr/include/stdio.h to get just the count.
  6. Navigate to /usr/include/ and list the contents. Find stdio.h. Use head -30 stdio.h to inspect it. Identify two function declarations you recognize.
  7. Write a simple shell script build.sh that compiles hello.c and runs it:
    #!/bin/bash
    gcc hello.c -o hello && ./hello
    Make it executable with chmod +x build.sh and run it.
  8. Use find ~/c-course -name "*.c" to find all C files. Use file hello (after compiling) to see what type of file the executable is. Record the output.
  9. Run man gcc and search for the -Wall option (press / then type -Wall). Write down what -Wall, -Wextra, and -Werror do.
  10. Use ldd on a compiled binary: ldd ./hello. List all shared libraries it depends on. Find where libc.so lives on disk using ldconfig -p | grep libc.

Hint: Use Tab for autocomplete in the terminal. Use Ctrl+C to abort a running command. Use history to see previous commands.

Unit 2 – Using Vim Editor for C Programming

Vim is a powerful, keyboard-driven text editor preinstalled on virtually every Linux system. Mastering Vim dramatically increases your coding speed and is an essential skill for Linux developers.

⌨️ Vim Syntax Quick Reference

Vim commands are mode-based. You enter Insert mode to type code, return to Normal mode for navigation and editing, then use Command mode for save, search, and file actions.

vim main.c
i                // enter insert mode
Esc              // return to normal mode
:wq              // save and quit
/printf          // search forward
gg  G  :25       // top, bottom, line 25

📖 Theory Focus

  • Vim is modal because editing, movement, and commands are treated as different operations with different optimised key sets.
  • Once navigation becomes muscle memory, you spend less time reaching for the mouse and more time reasoning about code structure.
  • For C development on servers or embedded targets, Vim matters because it is available almost everywhere, even in minimal environments.

🔄 2.1 – Vim's Three Core Modes

Vim is modal — it behaves differently depending on which mode you're in. This is the single most important concept to understand:

NORMAL MODE
Default mode
Navigation & commands
INSERT MODE
Press i to enter
Type your code
COMMAND MODE
Press : to enter
Save, quit, search
VISUAL MODE
Press v to enter
Select text
NORMAL → INSERT:
i (before cursor)   a (after)   o (new line)
INSERT → NORMAL:
Esc
NORMAL → COMMAND:
:
NORMAL → VISUAL:
v (char)   V (line)   Ctrl+v (block)
Golden Rule: When in doubt, press Esc to return to Normal mode. You can always start over from there.

🧭 2.2 – Navigation in Normal Mode

Key(s)ActionKey(s)Action
h j k lLeft / Down / Up / Rightw / bNext / previous word
0 / $Start / end of linegg / GTop / bottom of file
:nGo to line n (e.g. :25)Ctrl+d / Ctrl+uScroll half page down/up
%Jump to matching bracket*Search word under cursor

✏️ 2.3 – Editing Commands (Normal Mode)

CommandAction
ddDelete (cut) current line
yyYank (copy) current line
p / PPaste after / before cursor
uUndo
Ctrl+rRedo
xDelete character under cursor
rReplace one character
cwChange word (delete word and enter insert mode)
DDelete from cursor to end of line
=GAuto-indent entire file from cursor to end
/patternSearch forward for pattern
:%s/old/new/gReplace all "old" with "new" in file

💾 2.4 – Saving and Exiting

:w          # Save (write) the file
:q          # Quit (only works if no unsaved changes)
:wq         # Save AND quit (most common)
:wq!        # Force save and quit
:q!         # Discard changes and quit (force quit)
:x          # Same as :wq (save if modified, then quit)
ZZ          # In normal mode: save and quit
ZQ          # In normal mode: quit without saving
:w newname.c # Save as new filename
Vim Command Mode

🔧 2.5 – Configuring Vim for C (.vimrc)

Create ~/.vimrc to customize Vim for C development:

" ~/.vimrc - Vim config for C programming
syntax on           " Enable syntax highlighting
set number          " Show line numbers
set tabstop=4       " Tab = 4 spaces
set shiftwidth=4    " Indent = 4 spaces
set expandtab       " Convert tabs to spaces
set autoindent      " Auto-indent new lines
set smartindent     " Smarter indentation
set hlsearch        " Highlight search results
set incsearch       " Incremental search
set showmatch       " Highlight matching brackets
set ruler           " Show cursor position
set colorcolumn=80  " Column guide at 80 chars
set background=dark
colorscheme desert  " Color scheme
~/.vimrc
Apply changes: source ~/.vimrc or restart Vim

🚀 2.6 – Complete Vim Workflow: Writing & Running a C Program

# Step 1: Open a new C file in vim
vim hello.c
Terminal
// Step 2: You are now in NORMAL mode. Press 'i' to enter INSERT mode
// Now type your code:
#include <stdio.h>

int main() {
    printf("Hello, Linux C!\n");
    return 0;
}
hello.c
# Step 3: Press Esc to return to NORMAL mode
# Step 4: Type :wq and press Enter to save and quit
# Step 5: Compile and run
gcc hello.c -o hello
./hello
Hello, Linux C!
Terminal
Workflow Summary:
1. vim filename.c → opens Vim in NORMAL mode
2. Press i → switch to INSERT mode
3. Type your code
4. Press Esc → back to NORMAL mode
5. Type :wq → saves and exits

Unit 2 – Lab Exercises (10 Tasks)

  1. Open Vim and practice switching between Normal, Insert, and Command modes at least 5 times. Get comfortable with Esc. Practice the full cycle: Normal → Insert → type text → Esc → Command → save.
  2. Create greet.c in Vim. Write a program that prints "Welcome to Linux C Programming!" then compile and run it. Use only keyboard shortcuts — no mouse.
  3. Open any .c file in Vim. Practice navigation: use gg, G, w, b, 0, $, and :25 to move around. Time yourself navigating to line 10, last line, first word, and last word of a line.
  4. Create ~/.vimrc with the configuration shown above (syntax, line numbers, tab=4, etc.). Reopen a C file and verify syntax highlighting and line numbers are active.
  5. In Vim, use the search command /printf to find all occurrences of printf in a file. Use n to jump to next and N for previous. Count total occurrences using :%s/printf//gn.
  6. Use :%s/hello/world/g to do a global replacement in a test file. Verify all occurrences changed. Then undo with u to restore. Redo with Ctrl+r.
  7. Practice Vim editing commands: use dd to delete a line, yy to copy it, p to paste below, P to paste above. Practice on a 5-line test file.
  8. In Vim, select 3 lines using Visual Line mode (V), then indent them with > and unindent with <. Practice indenting a whole file with gg=G.
  9. Open two files simultaneously in split windows in Vim using :split file2.c. Navigate between splits with Ctrl+w w. Copy a block from one file to the other using yank and paste.
  10. Create a file with 20 lines. Use Vim macros: press qa to record a macro into register 'a', perform an operation (e.g., add a semicolon at end of line and move down), press q to stop. Then press 18@a to repeat for the remaining 18 lines.

Hint: If you get stuck in Vim, always press Esc first, then :q! to exit without saving. Type :help for the built-in documentation.

Unit 3 – Structure of a C Program

Every C program follows a well-defined structure. This unit explains each part, shows how GCC turns your source code into an executable, and reveals exactly how a C program looks in memory at runtime.

🧱 Program Syntax Quick Reference

C source files usually combine preprocessor directives, declarations, function prototypes, main, and function definitions in a predictable layout.

#include <stdio.h>

int helper(int value);

int main(void) {
    int result = helper(5);
    return 0;
}

int helper(int value) {
    return value * 2;
}

📖 Theory Focus

  • The preprocessor runs before the compiler, so directives such as #include and #define transform the source before syntax analysis begins.
  • main is the language entry point, but your executable also depends on linking, startup code, libraries, and OS process loading.
  • Program structure is not only stylistic; it determines symbol visibility, memory placement, and whether the compiler knows a function signature before use.

🏗️ 3.1 – Anatomy of a C Program

/*─────────────────────────────────────────────
  SECTION 1: Preprocessor Directives
  Processed BEFORE compilation by the C preprocessor.
  #include pulls in header files.
  #define creates constants/macros.
─────────────────────────────────────────────*/
#include <stdio.h>   // standard I/O: printf, scanf, fopen ...
#include <stdlib.h>  // malloc, free, exit, atoi ...
#include <string.h>  // strlen, strcpy, strcmp ...
#define  MAX 100      // symbolic constant
#define  PI  3.14159  // no semicolon on #define!

/*─────────────────────────────────────────────
  SECTION 2: Global Declarations
  Visible to ALL functions in the file.
  Stored in the Data/BSS segment (not stack).
─────────────────────────────────────────────*/
int   globalCounter = 0;  // initialized → Data segment
float ratio;              // uninitialized → BSS segment (0)

/*─────────────────────────────────────────────
  SECTION 3: Function Prototypes (declarations)
  Tell the compiler the function exists before it's defined.
─────────────────────────────────────────────*/
void greet(char *name);
int  add(int a, int b);

/*─────────────────────────────────────────────
  SECTION 4: main() – Entry Point
  Every C program must have exactly ONE main().
  Returns int: 0 = success, non-zero = error.
─────────────────────────────────────────────*/
int main() {
    /* Local variables → stored on the STACK */
    int  result;
    char name[50] = "Alice";

    greet(name);                    // function call
    result = add(5, 3);
    printf("5 + 3 = %d\n", result);
    return 0;                       // signals success to OS
}

/*─────────────────────────────────────────────
  SECTION 5: Function Definitions
─────────────────────────────────────────────*/
void greet(char *name) {
    printf("Hello, %s!\n", name);
}

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

🗂️ 3.1 – Elements of a C Program Structure

#ElementWhere It GoesPurposeExample
1 Preprocessor Directive Top of file, before everything else Instructs the preprocessor — includes headers, defines constants/macros. Processed before compilation. #include <stdio.h>
#define MAX 100
2 Global Variable Outside all functions, after directives A variable accessible by all functions in the file. Lives in the Data segment (initialized) or BSS segment (uninitialized). int counter = 0;
float ratio;
3 Variable Declaration Inside a function, before first use Tells the compiler the type and name of a variable. Memory is reserved on the stack. May not have an initial value. int age;
char name[50];
4 Function Declaration (Prototype) After global variables, before main() Tells the compiler the function's name, return type, and parameter types — so it can be called before its definition appears. int add(int a, int b);
void greet(char *name);
5 Variable Definition Inside a function (or globally) A declaration that also reserves storage. Assigning a value at this point is called initialization. int age = 25;
float pi = 3.14f;
6 Function Definition After main(), or in separate .c files The full body of the function — the actual implementation. Contains the logic that runs when the function is called. int add(int a, int b) {
  return a + b;
}
Declaration vs Definition: A declaration introduces a name and type to the compiler. A definition also allocates memory or provides the function body. Every definition is a declaration, but not every declaration is a definition (e.g., a prototype is a declaration only).

⚙️ 3.2 – The Compilation Pipeline (Source → Executable)

GCC transforms your C source file into a runnable executable in four stages:

Source Code
hello.c
1. Preprocessor
cpp
Modified Source
hello.i
2. Compiler
cc1
Assembly Code
hello.s
3. Assembler
as
Object File
hello.o
4. Linker
ld
Executable
./hello
StageToolInputOutputWhat It Does
1. Preprocessingcpp.c.iExpands #include, #define, removes comments
2. Compilationcc1.i.sConverts C to assembly language
3. Assemblyas.s.oConverts assembly to machine code (binary)
4. Linkingld.o + libsexecutableCombines object files + standard libraries
# Run each stage separately to see intermediate files:
gcc -E  hello.c -o hello.i   # Stage 1: Preprocessing only
gcc -S  hello.c -o hello.s   # Stage 2: Compile to assembly
gcc -c  hello.c -o hello.o   # Stage 3: Compile to object file
gcc     hello.o -o hello     # Stage 4: Link to executable

# OR do everything in one command (normal usage):
gcc hello.c -o hello

# View the .i file (preprocessed - stdio.h included inline):
cat hello.i | head -50

# View the assembly (.s file):
cat hello.s
Terminal

🗺️ 3.3 – C Program Memory Layout (Runtime)

When your program runs, the OS loads it into memory. The process memory is divided into distinct segments:

High Address0xFFFF…FFFF
Command Line Args & Environment Vars
argv[], envp[]
High
Stack ↓
Local vars, parameters, return addresses
Function call frames — grows DOWNWARD
↓ grows
↕ Stack and Heap grow toward each other ↕
Heap ↑
Dynamic memory: malloc(), calloc()
realloc(), free() — grows UPWARD
↑ grows
BSS Segment
Uninitialized global & static variables
Zero-initialized by OS at startup
Static
Data Segment
Initialized global & static variables
e.g. int x = 5; (global)
Static
Text Segment (Code)
Machine instructions — READ ONLY
Your compiled program code lives here
Low
Low Address0x0000…0000
#include <stdio.h>
#include <stdlib.h>

// ↓ Data Segment (initialized global)
int globalX = 10;

// ↓ BSS Segment (uninit global)
int globalY;

int main() {
  // ↓ Stack (local variable)
  int localA = 5;

  // ↓ Heap (dynamic allocation)
  int *p = malloc(4);

  // ↓ Text Segment (code is here)
  printf("%d\n", localA);
  free(p);
  return 0;
}
memory_demo.c
Stack overflow occurs when too many function calls exhaust the stack space (common with deep recursion).

Memory leak occurs when heap memory allocated with malloc() is never freed.

📝 3.4 – Hello World – Every Line Explained

#include <stdio.h>
// ↑ Preprocessor directive.
//   Tells the preprocessor to copy the contents of
//   /usr/include/stdio.h into this file before compiling.
//   stdio.h declares printf(), scanf(), FILE, etc.

int main() {
// ↑ Every C program starts execution here.
//   int → the function returns an integer to the OS.
//   ()  → no parameters (same as (void))

    printf("Hello, Linux C!\n");
// ↑ printf is defined in stdio.h.
//   "Hello, Linux C!\n" is a string literal (in Text segment).
//   \n is an escape sequence for newline.

    return 0;
// ↑ Returns 0 to the OS/shell.
//   Convention: 0 = success, any other value = error.
//   You can check this with: echo $?  (after running)
}
// ↑ Closing brace ends the main() function body.
hello.c – annotated

🔤 3.5 – Escape Sequences & Format Specifiers

Escape Seq.MeaningASCII Value
\nNewline (moves to next line)10
\tHorizontal tab9
\\Backslash character92
\"Double quote34
\'Single quote39
\0Null character (string terminator)0
\rCarriage return13

Format Spec.TypeExample
%dint (decimal)printf("%d", 42)42
%ffloat/doubleprintf("%.2f", 3.14)3.14
%ccharprintf("%c", 'A')A
%sstring (char*)printf("%s", "hi")hi
%ppointer addressprintf("%p", ptr)0x7ffe…
%ldlong intprintf("%ld", 1000000L)
%luunsigned longprintf("%lu", sizeof(int))
%xhexadecimalprintf("%x", 255)ff

Unit 3 – Lab Exercises

  1. Write a program that prints: your name, your age, and today's date — each on a separate line using printf.
  2. Run gcc -E hello.c -o hello.i then open hello.i in Vim. How many lines does it have? What happened to your #include <stdio.h>?
  3. Run gcc -S hello.c -o hello.s and view the assembly output. Find the call printf instruction in the .s file.
  4. Write a program with a global initialized variable (int g = 42;), a global uninitialized variable (int h;), and a local variable. Print all three with printf and their addresses with %p. Observe which addresses are close together.
  5. Write a program using printf with at least 5 different format specifiers (%d %f %c %s %x). Use proper width specifiers like %10d for alignment.
  6. Modify the Hello World program to accept a name from user input using scanf and print "Hello, [name]!".

Hint: Check the return value of main() with echo $? in the terminal after running your program.

📐 3.6 – GCC Compilation Flags Cheat Sheet

FlagPurposeExample
-o nameName the output filegcc hello.c -o hello
-WallEnable all common warningsgcc -Wall hello.c -o hello
-WextraEnable extra warningsgcc -Wall -Wextra hello.c -o hello
-gInclude debug info (for GDB)gcc -g hello.c -o hello
-O2Optimize for speedgcc -O2 hello.c -o hello
-std=c99Use C99 standardgcc -std=c99 hello.c -o hello
-EPreprocess onlygcc -E hello.c -o hello.i
-SCompile to assemblygcc -S hello.c -o hello.s
-cCompile to object filegcc -c hello.c -o hello.o

Unit 3 – Lab Exercises (10 Tasks)

  1. Write a program that prints your name, age, and today's date — each on a separate line using printf. Then add a header line "==[ My Profile ]==" for formatting.
  2. Run gcc -E hello.c -o hello.i then open hello.i in Vim. Count its lines with wc -l hello.i. What happened to your #include <stdio.h>? Find the line that became your own code.
  3. Run gcc -S hello.c -o hello.s and view the assembly output. Find the call printf instruction (or call puts if GCC optimized). Count total lines in the assembly file.
  4. Write a program with a global initialized variable (int g = 42;), a global uninitialized variable (int h;), and a local variable. Print all three with printf and their addresses using %p. Observe which addresses are in similar ranges.
  5. Write a program using printf with all 8 format specifiers from section 3.5 (%d %f %c %s %p %ld %lu %x). Use proper width and precision specifiers like %10.2f and %-20s for aligned output.
  6. Modify Hello World to accept a name from user input using scanf("%49s", name) (note: 49 to prevent buffer overrun) and print "Hello, [name]!" safely.
  7. Write a program that demonstrates all 7 escape sequences from the table in section 3.5. Print each with a label showing what it does visually (newlines, tabs, quotes, etc.).
  8. Compile the same program with three different GCC flags: (a) gcc hello.c -o hello, (b) gcc -O2 hello.c -o hello_opt, (c) gcc -g hello.c -o hello_debug. Compare file sizes with ls -la hello*.
  9. Write a program that intentionally has a warning (e.g., unused variable int x;). Compile with no flags — no warning shown. Then compile with -Wall — warning appears. Then with -Werror — compilation fails. Record what each flag does.
  10. Write a multi-file program: create math_utils.c with a function int square(int n) { return n*n; } and math_utils.h with its declaration. In main.c, include the header and call square(5). Compile using gcc main.c math_utils.c -o prog and run it.

Hint: Always compile with gcc -Wall -Wextra -o prog prog.c as a best practice. Treat all warnings as errors to be fixed.

Unit 4 – Data Types, Variables & Operators

This unit begins with the fundamental data types C provides and how each occupies memory, then moves to how variables are declared, initialized, and scoped, and finally covers all the operators used to build expressions.

🧮 Data Types → Variables → Operators

Every C program is built from typed data. First choose the right type, then declare a variable to hold it, then apply operators to compute and compare values.

/* 1. Data type determines size & range */
int   age   = 21;
float price = 99.5f;
const int MAX = 100;

/* 2. Variable holds the value */
int total, count = 0;

/* 3. Operators compute expressions */
average = (float)sum / count;
is_even = (age % 2 == 0);

📖 Theory Focus

  • Data Types define the size, range, and behaviour of stored values — choosing the wrong type causes overflow or wasted memory.
  • Variables are named memory locations whose type determines how the stored bits are interpreted at runtime.
  • Operators do more than arithmetic — they define precedence, type promotion rules, and can produce undefined behaviour if misused.

🧮 4.1 – Fundamental Data Types & Memory Sizes

On a 64-bit Linux system (x86-64), data types have these sizes:

char
B1
1 byte · -128 to 127
or 0 to 255 (unsigned)
short int
B1
B2
2 bytes · ±32,767
int
B1
B2
B3
B4
4 bytes · ±2,147,483,647
long
B1
B2
B3
B4
B5
B6
B7
B8
8 bytes (Linux 64-bit)
float
B1
B2
B3
B4
4 bytes · ±3.4×10³⁸
~6-7 decimal digits
double
B1
B2
B3
B4
B5
B6
B7
B8
8 bytes · ±1.7×10³⁰⁸
~15-16 decimal digits
#include <stdio.h>

int main() {
    // Print actual sizes on YOUR system
    printf("char       : %lu bytes\n", sizeof(char));
    printf("short      : %lu bytes\n", sizeof(short));
    printf("int        : %lu bytes\n", sizeof(int));
    printf("long       : %lu bytes\n", sizeof(long));
    printf("long long  : %lu bytes\n", sizeof(long long));
    printf("float      : %lu bytes\n", sizeof(float));
    printf("double     : %lu bytes\n", sizeof(double));
    printf("pointer    : %lu bytes\n", sizeof(void*));
    return 0;
}
sizeof_demo.c

Type Modifiers & Ranges

TypeModifierBytesMinimumMaximum
charsigned (default)1-128127
charunsigned10255
intsigned (default)4-2,147,483,6482,147,483,647
intunsigned404,294,967,295
longsigned8-9.2 × 10¹⁸9.2 × 10¹⁸
long longsigned8-9.2 × 10¹⁸9.2 × 10¹⁸
#include <limits.h>   // INT_MAX, INT_MIN, CHAR_MAX, etc.
#include <float.h>    // FLT_MAX, DBL_MAX, etc.
#include <stdio.h>

int main() {
    printf("INT_MAX  = %d\n",  INT_MAX);   // 2147483647
    printf("INT_MIN  = %d\n",  INT_MIN);   // -2147483648
    printf("UINT_MAX = %u\n",  UINT_MAX);  // 4294967295
    printf("FLT_MAX  = %e\n",  FLT_MAX);   // 3.402823e+38
    printf("DBL_MAX  = %e\n",  DBL_MAX);   // 1.797693e+308

    // Integer overflow (undefined behavior) demonstration
    unsigned char uc = 255;
    uc++;    // wraps around: 255 + 1 = 0 (not 256!)
    printf("255 + 1 for unsigned char = %d\n", uc);  // 0
    return 0;
}
limits.c

📝 Topic Assignments — 4.1 Data Types

  1. Write a program to print sizeof for char, short, int, long, float, and double.
  2. Print INT_MAX, INT_MIN, UINT_MAX, FLT_MAX, and DBL_MAX using header macros.
  3. Demonstrate unsigned wrap-around with unsigned char x = 255; x++; and explain the result.

📦 4.2 – Variables: Declaration, Initialization, Memory

A variable declaration tells the compiler to reserve memory and associate a name with it.

#include <stdio.h>

int main() {
    /* ── Declaration (memory reserved, value undefined) ── */
    int    age;
    float  salary;
    char   grade;
    double pi;

    /* ── Initialization (declaration + assignment) ── */
    int    count  = 0;
    float  price  = 99.99f;    // 'f' suffix = float literal
    char   letter = 'A';
    double area   = 3.14159;

    /* ── Constants (value cannot change) ── */
    const int   MAX_SIZE = 100;
    const float TAX_RATE = 0.18f;
    // MAX_SIZE = 200; // ERROR: cannot modify const

    /* ── Multiple variables on one line ── */
    int x = 1, y = 2, z = 3;

    /* ── Print addresses (where in memory) ── */
    printf("age    : %lu bytes at address %p\n", sizeof(age), &age);
    printf("salary : %lu bytes at address %p\n", sizeof(salary), &salary);
    printf("grade  : %lu bytes at address %p\n", sizeof(grade), &grade);
    return 0;
}
variables.c
Memory addresses on stack are typically adjacent. On a 64-bit Linux system, local variables are allocated in the stack frame and addresses may show a pattern (e.g., each 4 bytes apart for int).

📝 Topic Assignments — 4.2 Variables

  1. Declare variables of 5 different data types, initialize them, and print both value and address.
  2. Create a program that uses const values for tax and discount, then computes final price.
  3. Read student details (name, age, marks) and store them in correctly typed variables.

🔄 4.4 – Type Casting (Implicit & Explicit)

#include <stdio.h>

int main() {
    int    a = 5, b = 2;
    double result;

    /* ── Implicit (automatic) conversion ── */
    result = a / b;             // INTEGER division: 5/2 = 2 (not 2.5!)
    printf("int/int     = %.1f\n", result);  // 2.0

    /* ── Explicit (manual) cast ── */
    result = (double)a / b;      // Cast 'a' to double FIRST, then divide
    printf("(double)/int= %.1f\n", result);  // 2.5

    /* ── Char ↔ int (ASCII values) ── */
    char c = 'A';
    printf('%c' has ASCII value %d\n", c, (int)c);  // 65
    printf("ASCII 66 is char '%c'\n", (char)66);       // 'B'

    /* ── Float to int truncates (does NOT round) ── */
    float f = 3.99f;
    printf("(int)3.99 = %d\n", (int)f);   // 3 (truncated!)
    return 0;
}
casting.c

📝 Topic Assignments — 4.4 Type Casting

  1. Show difference between 5/2 and (double)5/2 in one program.
  2. Convert a lowercase character to uppercase using explicit cast and ASCII arithmetic.
  3. Read a float and print its truncated integer part using explicit cast.

🖊️ 4.5 – Assignment Operators

The assignment operator = stores a value into a variable. C also provides compound assignment operators that combine an arithmetic or bitwise operation with assignment, making code shorter and more readable.

OperatorSymbolEquivalent toExampleResult (x=10)
Simple Assignment=x = 5x = 5
Add & Assign+=x = x + nx += 3x = 13
Subtract & Assign-=x = x - nx -= 4x = 6
Multiply & Assign*=x = x * nx *= 2x = 20
Divide & Assign/=x = x / nx /= 2x = 5
Modulo & Assign%=x = x % nx %= 3x = 1
Bitwise AND & Assign&=x = x & nx &= 0xFFlow byte of x
Bitwise OR & Assign|=x = x | nx |= 0x01sets bit 0
Bitwise XOR & Assign^=x = x ^ nx ^= 0xFFflips all bits
Left Shift & Assign<<=x = x << nx <<= 2x = 40
Right Shift & Assign>>=x = x >> nx >>= 1x = 5
#include <stdio.h>

int main() {
    int x = 10;

    /* ── Simple Assignment ── */
    x = 10;
    printf("x = 10  → x = %d\n", x);     // 10

    /* ── Compound Assignment ── */
    x += 5;  printf("x += 5  → x = %d\n", x);  // 15
    x -= 3;  printf("x -= 3  → x = %d\n", x);  // 12
    x *= 2;  printf("x *= 2  → x = %d\n", x);  // 24
    x /= 4;  printf("x /= 4  → x = %d\n", x);  // 6
    x %= 4;  printf("x %%= 4 → x = %d\n", x);  // 2

    /* ── Chained Assignment (right to left) ── */
    int a, b, c;
    a = b = c = 100;   // c=100, b=c=100, a=b=100 (R→L)
    printf("a=%d b=%d c=%d\n", a, b, c);     // 100 100 100

    /* ── Bitwise compound ── */
    int flags = 0b00000000;
    flags |= (1 << 3);   // set bit 3:  0b00001000 = 8
    flags |= (1 << 5);   // set bit 5:  0b00101000 = 40
    flags &= ~(1 << 3);  // clear bit 3:0b00100000 = 32
    printf("flags = %d\n", flags);              // 32

    /* ── CAUTION: = vs == ── */
    int y = 5;
    if (y == 5)  printf("y equals 5\n");   // comparison (==)
    // if (y = 5) — ALWAYS true! Assigns 5, not compares!
    return 0;
}
assignment_ops.c

📐 How Assignment Works in Memory

Before: x = 10
10
x (addr 0x1000)
After: x += 5
15
x (same addr 0x1000)
Key: Assignment writes a new value into the same memory cell. Compound operators (+=, -=…) read then overwrite the same address.

📋 Program Output

►  assignment_ops.c
x = 10  → x = 10
x += 5  → x = 15
x -= 3  → x = 12
x *= 2  → x = 24
x /= 4  → x = 6
x %= 4  → x = 2
a=100 b=100 c=100
flags = 32
y equals 5
🔍 Result Analysis:
  • x += 5 → 15: reads x (10), adds 5, writes 15 back. Each compound step chains on the updated x.
  • a = b = c = 100: Right-to-left — c gets 100 first, then b=c, then a=b. All three become 100.
  • flags = 32: Set bits 3 (8) and 5 (32) → 40, then clear bit 3 → 40−8 = 32.
  • y equals 5: y == 5 is a comparison returning 1 (true), so the branch executes.
= vs == is one of the most common bugs in C. = assigns a value; == tests equality. Writing if (x = 5) instead of if (x == 5) always evaluates to true and silently changes x!

📝 Topic Assignments — 4.5 Assignment Operators

  1. Starting with x=25, apply +=, -=, *=, /=, and %= step by step and print each result.
  2. Demonstrate right-to-left evaluation using a=b=c=50 and explain order.
  3. Implement a flag register using |=, &=~, and ^=.

4.6 – Arithmetic Operators

Arithmetic operators perform mathematical calculations. All five work on integer and floating-point types, but behave differently for each — especially / and %.

OperatorSymbolNameExample (a=10, b=3)Result
Addition+Binary plusa + b13
Subtraction-Binary minusa - b7
Multiplication*Multiplya * b30
Division/Dividea / b3 (integer!)
Modulo%Remaindera % b1
Unary minus-Negate-a-10
#include <stdio.h>

int main() {
    int    a = 10, b = 3;
    double x = 10.0, y = 3.0;

    /* ── Integer arithmetic ── */
    printf("=== Integer (a=10, b=3) ===\n");
    printf("a + b = %d\n", a + b);    // 13
    printf("a - b = %d\n", a - b);    // 7
    printf("a * b = %d\n", a * b);    // 30
    printf("a / b = %d\n", a / b);    // 3  ← TRUNCATED (not 3.33)
    printf("a %% b = %d\n", a % b);   // 1  (10 = 3×3 + 1)

    /* ── Float arithmetic ── */
    printf("\n=== Double (x=10.0, y=3.0) ===\n");
    printf("x / y = %.4f\n", x / y);  // 3.3333
    printf("x * y = %.4f\n", x * y);  // 30.0000

    /* ── Integer division pitfall ── */
    printf("\n=== Conversion needed ===\n");
    printf("5 / 2       = %d\n",   5 / 2);              // 2 !
    printf("5.0 / 2     = %.1f\n", 5.0 / 2);           // 2.5
    printf("(double)5/2 = %.1f\n", (double)5 / 2);     // 2.5 (cast)

    /* ── Modulo applications ── */
    printf("\n=== Modulo uses ===\n");
    printf("17 %% 5 = %d\n", 17 % 5); // 2 (remainder)
    printf("Is 18 even? %s\n", (18 % 2 == 0) ? "Yes" : "No");  // Yes
    printf("Hour wrap: %d\n", (23 + 3) % 24);   // 2 (circular clock)

    /* ── Unary minus and plus ── */
    int n = 7;
    printf("-n = %d\n", -n);   // -7
    printf("+n = %d\n", +n);   // 7  (no effect, clarifies intent)

    /* ── Overflow example ── */
    int big = 2147483647;   // INT_MAX
    printf("INT_MAX + 1 = %d\n", big + 1);  // -2147483648 (overflow!)
    return 0;
}
arithmetic.c

📐 Integer vs Float Division

❌ int / int — truncates
10 / 3
= 3   (.333 discarded — not rounded!)
✅ double / int — preserves fraction
(double)10 / 3
= 3.3333
% Modulo — remainder only
10 % 3
10 = 3×3 + 1   → result = 1

📋 Program Output

►  arithmetic.c
=== Integer (a=10, b=3) ===
a + b = 13
a - b = 7
a * b = 30
a / b = 3
a % b = 1

=== Double (x=10.0, y=3.0) ===
x / y = 3.3333
x * y = 30.0000

=== Conversion needed ===
5 / 2       = 2
5.0 / 2     = 2.5
(double)5/2 = 2.5

=== Modulo uses ===
17 % 5 = 2
Is 18 even? Yes
Hour wrap: 2
-n = -7
+n = 7
INT_MAX + 1 = -2147483648
🔍 Result Analysis:
  • a / b = 3 not 3.33 — both are int; C truncates toward zero.
  • 5.0 / 2 = 2.5 — literal 5.0 makes one operand double, promoting the whole expression.
  • Hour wrap: 2 — (23+3) % 24 = 26 % 24 = 2. Modulo is the standard wrap-around idiom.
  • INT_MAX + 1 = −2147483648 — signed integer overflow wraps to the most negative value (undefined behaviour — avoid it).
Key Rules: (1) int / int always gives an integer — fractional part is discarded, not rounded. (2) % only works on integer types, not float/double. (3) Use double or cast when you need decimal precision.

📝 Topic Assignments — 4.6 Arithmetic Operators

  1. Create a calculator for +, -, *, /, and % with division-by-zero checks.
  2. Read two integers and print both integer division and floating-point division results.
  3. Use modulo to check odd/even and to wrap a 24-hour clock value.

⚖️ 4.7 – Relational Operators

Relational (comparison) operators compare two values and always return 1 (true) or 0 (false). They are used in if, while, and for conditions.

OperatorSymbolMeaningExample (a=10, b=5)Result
Equal to==Both values are equala == b0 (false)
Not equal to!=Values are differenta != b1 (true)
Greater than>Left is largera > b1 (true)
Less than<Left is smallera < b0 (false)
Greater or equal>=Left >= righta >= 101 (true)
Less or equal<=Left <= rightb <= 51 (true)
#include <stdio.h>

int main() {
    int a = 10, b = 5, c = 10;

    printf("=== Relational Results ===\n");
    printf("a == b  :  %d\n", a == b);   // 0 — 10 ≠ 5
    printf("a == c  :  %d\n", a == c);   // 1 — 10 = 10
    printf("a != b  :  %d\n", a != b);   // 1
    printf("a >  b  :  %d\n", a > b);    // 1
    printf("a <  b  :  %d\n", a < b);    // 0
    printf("a >= c  :  %d\n", a >= c);   // 1 — 10 >= 10
    printf("b <= 4  :  %d\n", b <= 4);   // 0 — 5 not <= 4

    /* ── In if conditions ── */
    if (a > b)
        printf("a is greater than b\n");

    /* ── Comparing floats: never use == directly! ── */
    double x = 0.1 + 0.2;
    printf("0.1+0.2 == 0.3 → %d\n", x == 0.3);     // 0! Floating-point imprecision
    double eps = 1e-9;
    printf("Near equal?    → %d\n", (x - 0.3) < eps); // 1 — correct way

    /* ── Range check ── */
    int score = 75;
    if (score >= 0 && score <= 100)
        printf("Valid score\n");

    /* ── Relational result used in arithmetic ── */
    int is_adult = (a >= 18);    // stores 0 or 1
    printf("is_adult = %d\n", is_adult);
    return 0;
}
relational.c

📐 Relational Operator — Returns 0 or 1

a > b
a is larger → returns 1 (true)
a is NOT larger → returns 0 (false)
Result is always int: 1 = true, 0 = false. You can store it, print it, or use it in arithmetic.

📋 Program Output

►  relational.c
=== Relational Results ===
a == b  :  0
a == c  :  1
a != b  :  1
a >  b  :  1
a <  b  :  0
a >= c  :  1
b <= 4  :  0
a is greater than b
0.1+0.2 == 0.3 → 0
Near equal?    → 1
Valid score
is_adult = 0
🔍 Result Analysis:
  • a == b → 0: 10 ≠ 5, false. a == c → 1: 10 = 10, true.
  • a >= c → 1: equality also satisfies ≥. b <= 4 → 0: 5 ≤ 4 is false.
  • 0.1+0.2 == 0.3 → 0: binary float cannot represent 0.1/0.2 exactly; their sum is ~0.30000000000000004, not 0.3.
  • is_adult = 0: a=10, so 10 >= 18 is false (0). The comparison result is stored as an integer.
Never compare floats with ==. Due to IEEE 754 floating-point representation, 0.1 + 0.2 is not exactly 0.3. Always check if |a - b| < epsilon instead.

📝 Topic Assignments — 4.7 Relational Operators

  1. Compare two numbers using all six relational operators and print 0/1 results.
  2. Build a range validator that checks if marks are between 0 and 100.
  3. Compare floating-point values safely using epsilon logic.

🔢 4.8 – Increment & Decrement Operators

The ++ and -- operators increase or decrease a variable by exactly 1. They have two forms: prefix (change first, then use) and postfix (use first, then change).

PREFIX: ++x / --x
Increment/decrement FIRST, then evaluate expression
int x = 5;
int y = ++x;   // x=6 first, y=6
printf("%d %d", x, y); // 6 6
POSTFIX: x++ / x--
Evaluate expression FIRST, then increment/decrement
int x = 5;
int y = x++;   // y=5 first, x=6
printf("%d %d", x, y); // 6 5
#include <stdio.h>

int main() {
    int a, b, x;

    /* ── Post-increment: use value, THEN increment ── */
    x = 5;
    a = x++;   // a gets OLD value (5), then x becomes 6
    printf("Post x++: a=%d, x=%d\n", a, x);    // a=5, x=6

    /* ── Pre-increment: increment FIRST, then use ── */
    x = 5;
    a = ++x;   // x becomes 6 FIRST, then a gets 6
    printf("Pre  ++x: a=%d, x=%d\n", a, x);    // a=6, x=6

    /* ── Post-decrement ── */
    x = 10;
    b = x--;   // b gets 10, then x becomes 9
    printf("Post x--: b=%d, x=%d\n", b, x);    // b=10, x=9

    /* ── Pre-decrement ── */
    x = 10;
    b = --x;   // x becomes 9 FIRST, then b gets 9
    printf("Pre  --x: b=%d, x=%d\n", b, x);    // b=9, x=9

    /* ── Typical use in loops ── */
    printf("\nCounting up:   ");
    for (int i = 1; i <= 5; i++)    // i++ is standard for-loop idiom
        printf("%d ", i);           // 1 2 3 4 5

    printf("\nCounting down: ");
    for (int i = 5; i >= 1; i--)   // i-- for reverse traversal
        printf("%d ", i);           // 5 4 3 2 1

    /* ── Difference in expression context ── */
    printf("\n\nExpression demo:\n");
    int p = 3, q = 3;
    printf("p++ * 2 = %d (p=%d)\n", p++ * 2, p);  // 6, p=4
    printf("++q * 2 = %d (q=%d)\n", ++q * 2, q);  // 8, q=4
    return 0;
}
inc_dec.c

📐 Step-by-Step: Postfix vs Prefix in an Expression

y = x++;   (x was 5)
Read x → 5
Assign 5 to y
Increment x → 6
y = 5, x = 6
y = ++x;   (x was 5)
Increment x → 6
Read x → 6
Assign 6 to y
y = 6, x = 6

📋 Program Output

►  inc_dec.c
Post x++: a=5, x=6
Pre  ++x: a=6, x=6
Post x--: b=10, x=9
Pre  --x: b=9, x=9

Counting up:   1 2 3 4 5
Counting down: 5 4 3 2 1

Expression demo:
p++ * 2 = 6 (p=4)
++q * 2 = 8 (q=4)
🔍 Result Analysis:
  • Post x++: a=5, x=6 — a got the old value 5; x incremented after.
  • Pre ++x: a=6, x=6 — x incremented to 6 first, then a received 6.
  • p++ * 2 = 6: p=3 used in expression first (3×2=6), THEN p becomes 4.
  • ++q * 2 = 8: q=3 incremented to 4 FIRST, then 4×2=8. Both p and q end as 4 but expressions differ.
Best practice: In standalone statements (i++; or ++i; alone), prefix and postfix produce identical results. Prefer ++i alone (prefix) when the return value is not used — it avoids storing a temporary copy (minor efficiency gain, especially for complex types).

📝 Topic Assignments — 4.8 Increment & Decrement

  1. Write a demo that shows difference between x++ and ++x inside expressions.
  2. Print numbers 1 to 20 using i++ and 20 to 1 using i--.
  3. Trace values of variables in prefix/postfix combinations and explain each line.

4.9 – Conditional (Ternary) Operator ?:

The ternary operator is C's only three-operand operator. It is a compact form of if-else that produces a value. Syntax: condition ? expr_if_true : expr_if_false

result = condition ? value_if_true : value_if_false;
▲ Evaluated first. Non-zero = true.
▲ Used when condition is true
▲ Used when condition is false
#include <stdio.h>

int main() {
    int a = 10, b = 20;

    /* ── Basic ternary ── */
    int max = (a > b) ? a : b;
    printf("max = %d\n", max);           // 20

    /* ── Ternary in printf ── */
    printf("%d is %s\n", a, (a % 2 == 0) ? "even" : "odd"); // even

    /* ── Nested ternary (use sparingly, hard to read) ── */
    int marks = 72;
    char *grade = (marks >= 90) ? "A" :
                  (marks >= 75) ? "B" :
                  (marks >= 60) ? "C" : "F";
    printf("Grade: %s\n", grade);        // C

    /* ── Ternary to select format string ── */
    int items = 1;
    printf("You have %d %s\n", items, (items == 1) ? "item" : "items");
    // "You have 1 item"

    /* ── Ternary to compute absolute value ── */
    int x = -7;
    int abs_x = (x < 0) ? -x : x;
    printf("|%d| = %d\n", x, abs_x);    // |-7| = 7

    /* ── Ternary as l-value (assign to)  ── */
    int y, z;
    int use_y = 1;
    // Pick which variable to assign to:
    *( use_y ? &y : &z ) = 42;
    printf("y=%d z=%d\n", y, z);         // y=42 z=garbage
    return 0;
}
ternary.c

📋 Program Output

►  ternary.c
max = 20
10 is even
Grade: C
You have 1 item
|-7| = 7
y=42 z=0
🔍 Result Analysis:
  • max = 20: a > b → 10 > 20 is false → takes the b (20) branch.
  • 10 is even: 10 % 2 == 0 is true → selects the string "even".
  • Grade: C: marks=72, not ≥90, not ≥75, but ≥60 → picks "C" from the nested chain.
  • You have 1 item: items==1 selects the singular form. Ternary is ideal for pluralisation.
  • |-7| = 7: x=-7 < 0 is true → selects -x = 7.
When to use ternary? Use it for simple one-line value selections. Avoid deeply nested ternaries — they become unreadable. For complex logic, use if-else instead.

📝 Topic Assignments — 4.9 Ternary Operator

  1. Find maximum of two and three numbers using ternary operators.
  2. Print whether a number is even/odd using only one ternary expression.
  3. Create a grade classifier with nested ternary and compare with if-else readability.

🔗 4.10 – Logical Operators

Logical operators combine boolean (true/false) expressions. In C, any non-zero value is true; zero is false. All logical operators return 1 (true) or 0 (false).

OperatorSymbolNameReturns 1 (true) when
Logical AND&&ANDBoth operands are true (non-zero)
Logical OR||ORAt least one operand is true
Logical NOT!NOTOperand is false (zero)

Truth Tables

ABA && B
000
010
100
111
ABA || B
000
011
101
111
A!A
0 (false)1
non-zero0
#include <stdio.h>

int main() {
    int a = 5, b = 10, c = 0;

    /* ── AND: both must be non-zero ── */
    printf("a&&b  = %d\n", a && b);    // 1 (5 and 10 both non-zero)
    printf("a&&c  = %d\n", a && c);    // 0 (c is zero)

    /* ── OR: at least one must be non-zero ── */
    printf("a||c  = %d\n", a || c);    // 1 (a is non-zero)
    printf("c||c  = %d\n", c || c);    // 0 (both zero)

    /* ── NOT: flips truth value ── */
    printf("!a    = %d\n", !a);         // 0 (5 is true, NOT → false)
    printf("!c    = %d\n", !c);         // 1 (0 is false, NOT → true)
    printf("!!a   = %d\n", !!a);        // 1 (normalises to 0 or 1)

    /* ── Compound conditions ── */
    int age = 22;
    int has_id = 1;
    if (age >= 18 && has_id)
        printf("Access granted\n");

    int is_weekend = 0, is_holiday = 1;
    if (is_weekend || is_holiday)
        printf("Day off!\n");

    /* ── Short-circuit evaluation ── */
    // In &&: if left side is false, RIGHT IS NOT EVALUATED
    int *ptr = NULL;
    if (ptr != NULL && *ptr > 0)   // Safe: *ptr not reached when ptr==NULL
        printf("positive\n");
    // In ||: if left side is true, right is NOT evaluated
    int x = 5;
    if (x > 0 || (printf("not printed\n"), 1))
        printf("short-circuit OR\n");  // printf NOT called!
    return 0;
}
logical.c

📐 Short-Circuit Evaluation Diagram

A && B (AND)
① Evaluate A
If A = false (0) → stop, return 0 (B never evaluated)
② If A = true → evaluate B
Return B's truth value
A || B (OR)
① Evaluate A
If A = true (non-zero) → stop, return 1 (B never evaluated)
② If A = false → evaluate B
Return B's truth value

📋 Program Output

►  logical.c
a&&b  = 1
a&&c  = 0
a||c  = 1
c||c  = 0
!a    = 0
!c    = 1
!!a   = 1
Access granted
Day off!
short-circuit OR
🔍 Result Analysis:
  • a&&b = 1: a=5 (true) AND b=10 (true) → both non-zero → 1.
  • a&&c = 0: a=true but c=0 (false) → AND fails → 0.
  • a||c = 1: a=5 is truthy → short-circuits immediately, c never checked.
  • !a = 0: a=5 is truthy → NOT flips to false (0). !!a = 1: double NOT normalises non-zero to exactly 1.
  • short-circuit OR printed; but "not printed" text never appeared because x > 0 was already true so the right side of || was skipped.
Short-circuit evaluation is a critical feature: && stops at the first false operand; || stops at the first true operand. Use this to safely guard pointer dereferences: if (ptr != NULL && *ptr > 0)

📝 Topic Assignments — 4.10 Logical Operators

  1. Build an eligibility checker (age and ID) using &&, ||, and !.
  2. Print full truth tables for logical AND, OR, and NOT.
  3. Demonstrate short-circuit behavior with side effects in conditions.

4.11 – Bitwise Operators

Bitwise operators work directly on the binary representation of integers — one bit at a time. They are extremely fast and used in flags, graphics, encryption, and systems programming.

OperatorSymbolNameOperation
Bitwise AND&AND1 if BOTH bits are 1
Bitwise OR|OR1 if EITHER bit is 1
Bitwise XOR^XOR1 if bits are DIFFERENT
Bitwise NOT~ComplementFlips all bits (0→1, 1→0)
Left Shift<<SHLShift bits left, fill with 0 (×2 per shift)
Right Shift>>SHRShift bits right (÷2 per shift for positive)
a = 12 (0000 1100)  |  b = 10 (0000 1010)
a & b
0000 1000
= 8
a | b
0000 1110
= 14
a ^ b
0000 0110
= 6
~a
1111 0011
= -13
a << 1
0001 1000
= 24
a >> 1
0000 0110
= 6
#include <stdio.h>

// Helper: print binary representation of an int
void printBin(int n) {
    for (int i = 7; i >= 0; i--)
        printf("%d", (n >> i) & 1);
    printf(" (%d)\n", n);
}

int main() {
    int a = 12, b = 10;   // 0000 1100, 0000 1010

    printf("a     = "); printBin(a);    // 00001100 (12)
    printf("b     = "); printBin(b);    // 00001010 (10)
    printf("a&b   = "); printBin(a&b);  // 00001000 (8)
    printf("a|b   = "); printBin(a|b);  // 00001110 (14)
    printf("a^b   = "); printBin(a^b);  // 00000110 (6)
    printf("~a    = "); printBin(~a);   // 11110011 (-13)
    printf("a<<1  = "); printBin(a<<1);// 00011000 (24) ×2
    printf("a>>1  = "); printBin(a>>1);// 00000110 (6)  ÷2

    /* ══ PRACTICAL APPLICATIONS ══ */
    printf("\n--- Bit Tricks ---\n");

    // 1. Check if number is odd (test bit 0)
    int n = 13;
    printf("%d is %s\n", n, (n & 1) ? "odd" : "even");  // odd

    // 2. Set bit k: OR with mask (1 << k)
    int flags = 0;
    flags |= (1 << 3);   // set bit 3 → 0b00001000 = 8
    flags |= (1 << 5);   // set bit 5 → 0b00101000 = 40
    printf("After setting bits 3,5: %d\n", flags);   // 40

    // 3. Clear bit k: AND with inverse mask
    flags &= ~(1 << 3);  // clear bit 3 → 0b00100000 = 32
    printf("After clearing bit 3: %d\n", flags);      // 32

    // 4. Toggle bit k: XOR with mask
    flags ^= (1 << 5);   // toggle bit 5 → 0
    printf("After toggling bit 5: %d\n", flags);       // 0

    // 5. Check if bit k is set: AND with mask
    flags = 0b10110110;
    if (flags & (1 << 4))
        printf("Bit 4 is SET\n");

    // 6. Multiply/divide powers of 2 using shifts
    printf("5 * 8  = %d\n", 5 << 3);   // 40 (5 × 2³)
    printf("64 / 4 = %d\n", 64 >> 2);  // 16 (64 ÷ 4)

    // 7. Swap two numbers without temp variable (XOR swap)
    int x = 5, y = 9;
    x ^= y;  y ^= x;  x ^= y;        // classic XOR swap
    printf("x=%d y=%d\n", x, y);        // x=9, y=5
    return 0;
}
bitwise.c
Common Bitwise PatternCodePurpose
Set bit kn |= (1 << k)Force bit k to 1
Clear bit kn &= ~(1 << k)Force bit k to 0
Toggle bit kn ^= (1 << k)Flip bit k
Test bit k(n >> k) & 1Isolate bit k (0 or 1)
Check odd/evenn & 11 = odd, 0 = even
Multiply by 2ⁿn << kn × 2^k
Divide by 2ⁿn >> kn ÷ 2^k (positive only)
Get low byten & 0xFFMask upper bytes

📋 Program Output

►  bitwise.c
a     = 00001100 (12)
b     = 00001010 (10)
a&b   = 00001000 (8)
a|b   = 00001110 (14)
a^b   = 00000110 (6)
~a    = 11110011 (-13)
a<<1  = 00011000 (24)
a>>1  = 00000110 (6)

--- Bit Tricks ---
13 is odd
After setting bits 3,5: 40
After clearing bit 3: 32
After toggling bit 5: 0
Bit 4 is SET
5 * 8  = 40
64 / 4 = 16
x=9 y=5
🔍 Result Analysis:
  • a & b = 8: 1100 AND 1010 → only bit 3 is 1 in both → 00001000 = 8.
  • a | b = 14: bits that are 1 in either → 00001110 = 14.
  • a ^ b = 6: bits that differ (positions 1,2) → 00000110 = 6.
  • ~a = -13: all bits of 12 (00001100) flipped → 11110011 = -13 in two's complement.
  • a << 1 = 24: left shift by 1 = ×2 → 12×2 = 24. a >> 1 = 6: right shift = ÷2.
  • flags = 40: 2³ + 2⁵ = 8 + 32 = 40. Clearing bit 3 → 32. Toggling bit 5 → 0.
  • XOR swap: x=9, y=5: three XOR operations swap two values without a temporary variable.

📝 Topic Assignments — 4.11 Bitwise Operators

  1. Write functions to set, clear, toggle, and test bit k in an integer.
  2. Print binary representation of a number and show results of &, |, ^, and ~.
  3. Use shifts to compute multiplication/division by powers of 2 and verify outputs.

📊 4.12 – Operator Precedence & Associativity (Highest → Lowest)

#
Operators
Category
Associativity
1
() [] -> .
Postfix / member access
L → R
2
++ -- (prefix)   ! ~ + - * & (type) sizeof
Unary (prefix)
R → L
3
* / %
Multiplicative (arithmetic)
L → R
4
+ -
Additive (arithmetic)
L → R
5
<< >>
Bitwise shift
L → R
6
< <= > >=
Relational comparison
L → R
7
== !=
Equality / relational
L → R
8
&
Bitwise AND
L → R
9
^
Bitwise XOR
L → R
10
|
Bitwise OR
L → R
11
&&
Logical AND
L → R
12
||
Logical OR
L → R
13
? :
Conditional (Ternary)
R → L
14
= += -= *= /= %= &= ^= |= <<= >>=
Assignment (all forms)
R → L
15
,
Comma operator
L → R
/* Precedence examples — predict the output! */
int a = 2, b = 3, c = 4;

printf("%d\n", a + b * c);        // 14  (* before +)
printf("%d\n", (a + b) * c);      // 20  (parens first)
printf("%d\n", a < b && b < c);   // 1   (< before &&)
printf("%d\n", !a == 0);          // 1   (! first, then ==)
printf("%d\n", a | b & c);         // 2   (& before |): a|(b&c) = 2|(3&4) = 2|0 = 2
printf("%d\n", a + b > c);         // 1   (5 > 4 = true)
// Rule: When unsure, ADD PARENTHESES — they cost nothing!
precedence_demo.c
Golden Rule: Operator precedence is a source of many bugs. When mixing operator types (e.g. bitwise + logical), always add parentheses: write (a & mask) != 0 not a & mask != 0 (which is a & (mask != 0) — probably not what you want!).

📝 Topic Assignments — 4.12 Precedence & Associativity

  1. Predict output of 10 mixed expressions, then run and compare with your prediction.
  2. Rewrite ambiguous expressions using parentheses to make intent explicit.
  3. Create examples where changing associativity changes the result.

📥 4.13 – Reading Input with scanf

#include <stdio.h>

int main() {
    int    age;
    float  height;
    double salary;
    char   grade;
    char   name[50];

    printf("Enter name: ");
    scanf("%49s", name);       // %49s prevents buffer overflow

    printf("Enter age: ");
    scanf("%d", &age);           // & = address-of operator

    printf("Enter height (m): ");
    scanf("%f", &height);

    printf("Enter salary: ");
    scanf("%lf", &salary);       // %lf for double in scanf

    printf("Enter grade (A-F): ");
    scanf(" %c", &grade);         // space before %c skips whitespace

    printf("\n=== Student Info ===\n");
    printf("Name   : %s\n",    name);
    printf("Age    : %d\n",    age);
    printf("Height : %.2f m\n", height);
    printf("Salary : %.2f\n",  salary);
    printf("Grade  : %c\n",    grade);
    return 0;
}
input_demo.c
Why & in scanf? scanf needs to write into your variable. Without &, you'd pass the value; with &, you pass the address so scanf can store data there.

📝 Topic Assignments — 4.13 scanf Input

  1. Read name, age, height, and grade from user and print a formatted profile card.
  2. Build a small input form that validates age range and marks range using conditions.
  3. Demonstrate safe string input using width-limited specifiers in scanf.

📝 Section 4.5 Assignments — Assignment Operators

  1. Compound Chain: Declare int x = 100. Apply in sequence: x += 25, x -= 15, x *= 2, x /= 3, x %= 7. Print x after every step and verify the final value manually.
  2. All Assignments Table: Declare int a = 60. Print the result of every compound assignment operator (+=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=) applied to a with operand 3. Reset a = 60 before each.
  3. Chained Assignment: Declare three variables p, q, r. Assign all three to 999 using a single chained assignment statement p = q = r = 999. Print all three and explain the right-to-left evaluation order.
  4. Accumulator Loop: Use += inside a for loop to compute the sum of numbers 1 to 50. Do NOT use a separate addition expression — only sum += i. Print the final sum (expected: 1275).
  5. Running Product: Read 5 integers from the user. Use *= to compute their product iteratively. Print the product after each multiplication step showing how it grows.
  6. Temperature Tracker: Read 7 daily temperatures. Use += to keep a running total, then use /= to compute and print the weekly average. Use only compound assignment — no standalone total = total + t.
  7. Score Adjustment: A student starts with score = 50. Apply these rules using compound assignment: add 10 for attendance, subtract 5 for late submission, multiply by 1 (no bonus), divide by 1 (no penalty), add remainder of score÷3 as a bonus. Print final score.
  8. = vs == Bug Hunt: Write a program where you intentionally write if (x = 10) and observe that it always evaluates to true. Then fix it to if (x == 10). Print which version correctly checks equality vs which version silently assigns. Add comments explaining the bug.
  9. Bitwise Flag Manager: Declare int perm = 0 representing 8 permission bits. Use |= to set bits 0, 2, and 5. Use &= ~(1<<2) to clear bit 2. Use ^= to toggle bit 5. Print the final integer value and its binary equivalent (manually or with a loop).
  10. Shift Assignment: Start with int n = 1. Use <<= 1 in a loop 8 times, printing n after each shift. Observe that you're computing powers of 2 (1, 2, 4, 8, 16, 32, 64, 128, 256). Then use >>= 1 8 times to reverse.

📝 Section 4.6 Assignments — Arithmetic Operators

  1. Basic Calculator: Read two integers a and b from the user. Print the result of all five arithmetic operations: a+b, a-b, a*b, a/b (integer), a%b. Handle b=0 for division and modulo.
  2. Integer vs Float Division: Read two integers. Print both integer division (a/b) and real division ((double)a/b) side by side. Add a note in the output explaining why they differ. Try inputs like 7/2, 10/3, 15/4.
  3. Digit Extractor: Read a 3-digit integer (100–999). Using only / and %, extract and print the hundreds digit, tens digit, and units digit separately. Example: 357 → hundreds=3, tens=5, units=7.
  4. Time Converter: Read a total number of seconds (e.g., 3725). Using / and %, convert it to hours, minutes, and remaining seconds. Print as H:MM:SS. Example: 3725 → 1:02:05.
  5. Unary Minus Practice: Read any integer. Print its negation using unary minus. Then compute -(-n) and show it equals the original. Also compute -(n*2) and compare with (-n)*2. Show they are equal.
  6. Circle Geometry: Read a radius as a double. Using arithmetic operators (no math library functions), compute:
    (a) Circumference = 2 × 3.14159 × r
    (b) Area = 3.14159 × r × r Print both to 4 decimal places.
  7. Overflow Detector: Declare int x = 2147483647 (INT_MAX). Print x, then print x + 1. Observe integer overflow. Repeat with short (MAX = 32767). Explain in comments what overflow means and why it wraps around.
  8. Modulo Clock: Read a starting hour (0–23) and a number of hours to add (any positive integer). Use %24 to compute the final hour on a 24-hour clock. Examples: start=22, add=5 → 3; start=0, add=36 → 12.
  9. Fibonacci Step: Start with int a=0, b=1. Using only assignment and arithmetic operators (no loops), manually compute and print the first 10 Fibonacci numbers by repeatedly doing c = a+b; a = b; b = c;.
  10. BMI Calculator: Read weight in kg (double) and height in metres (double). Calculate BMI = weight / (height × height) using arithmetic operators. Print the BMI to 2 decimal places and classify: "Underweight" (<18.5), "Normal" (18.5–24.9), "Overweight" (25–29.9), "Obese" (≥30).

📝 Section 4.7 Assignments — Relational Operators

  1. Comparison Table: Read two integers a and b. Print the result of all six relational operators (==, !=, >, <, >=, <=) as 0 or 1 with labels. Example output: a==b : 0, a!=b : 1, etc.
  2. Grade Classifier: Read a marks value (0–100). Using relational operators in if-else if, print the grade: A (≥90), B (≥80), C (≥70), D (≥60), F (<60). Also print "Invalid" if marks <0 or >100.
  3. Largest of Three: Read three integers. Using only relational operators and if-else (no ternary), find and print the largest. Then extend to also print the smallest.
  4. Year Validator: Read a year. Use relational operators to check:
    (a) Is it a future year (after 2025)?
    (b) Is it in the 21st century (2001–2100)?
    (c) Is it a round century year (divisible by 100)? Print YES/NO for each check.
  5. Float Comparison Fix: Compute double x = 0.1 + 0.2. Compare it with 0.3 using == (will fail). Then compare using fabs(x - 0.3) < 1e-9 (will pass). Print both results and explain floating-point imprecision in comments.
  6. Number Sign Checker: Read an integer. Using relational operators (>, <, ==), determine if it is positive, negative, or zero. Print a clear message. Then also check if it is divisible by both 2 and 5.
  7. Triangle Validator: Read three sides of a triangle. Using relational operators, check if they form a valid triangle (each side must be less than the sum of the other two). Print "Valid Triangle" or "Invalid Triangle".
  8. Relational in Arithmetic: C relational expressions return 0 or 1 as integers. Exploit this: read n and compute count = (n>0) + (n%2==0) + (n<100). Print count (0-3) and explain what each term counts. Example: n=50 → count=3.
  9. Leap Year Checker: Read a year. Using relational and modulo operators, determine if it is a leap year (divisible by 4, except centuries unless divisible by 400). Print "Leap Year" or "Not a Leap Year" with the condition that matched.
  10. Between Checker: Read three integers: low, value, high. Using relational operators, check and print: is value strictly between low and high? Is it within [low, high] inclusive? Is it outside the range? Test edge cases where value equals low or high.

📝 Section 4.8 Assignments — Increment & Decrement Operators

  1. Pre vs Post Trace: Write a program with int x = 10. Evaluate and print these one per line: x++, x, ++x, x, x--, x, --x, x. Before running, manually predict every output value and write your predictions as comments. Verify against actual output.
  2. Expression Tracing: Given int a=3, b=5: compute and print a++ + b, current values of a and b, then ++a + b--, then final values. Explain each step in detail with comments.
  3. Countdown Timer: Read a positive integer n. Use a while loop with n-- (post-decrement) in the condition to print: "T-5", "T-4", ..., "T-0", "Launch!". Observe that the loop uses the value before decrementing for the print.
  4. Array Traversal: Declare int arr[] = {10,20,30,40,50}. Use a for loop with i++ to print elements forward. Then use a separate loop from index 4 to 0 using i-- to print them in reverse. Print both sequences.
  5. Multiplication Table: Read a number n. Use a for loop with i++ (i from 1 to 10) to print the multiplication table of n. Format: n × i = result per line.
  6. Sum with Pre-increment: Use ++i inside a for loop (i from 0, condition i < 10, no increment in header) to sum 1–10. Compare with version using i++. Show both sums are equal (1275 for 1–50, 55 for 1–10) and explain why.
  7. Digit Counter: Read a non-negative integer. Use a while loop with n /= 10 and a separate counter incremented with count++ to count the number of digits. Handle the special case n=0. Print the digit count.
  8. Pointer Increment: Declare int arr[5] = {5,10,15,20,25} and a pointer int *p = arr. Use p++ in a loop to traverse and print each element using *p. Explain how pointer increment moves by sizeof(int) bytes, not by 1.
  9. FizzBuzz with ++: Print numbers 1–30 using a for loop with i++. For multiples of 3 print "Fizz", multiples of 5 print "Buzz", multiples of both print "FizzBuzz", otherwise print the number. Use % and relational operators for checks.
  10. Side-Effect Warning: Write int i=5; printf("%d %d", i++, i++); — predict the output, run it, then explain why the output is compiler-dependent (undefined evaluation order between function arguments). Rewrite safely using separate statements to get deterministic output.

📝 Section 4.9 Assignments — Conditional (Ternary) Operator

  1. Max of Two: Read two integers. Use a single ternary expression to find and print the larger value. Then extend to find the maximum of three numbers using two nested ternaries.
  2. Absolute Value: Read any integer. Use a ternary expression to compute its absolute value without using <math.h>: abs = (n < 0) ? -n : n. Print the result.
  3. Even/Odd Formatter: Read 10 integers in a loop. For each, use a ternary to print "n is even" or "n is odd" on one line. Write the entire output statement as a single printf using the ternary inside the argument.
  4. Grade String: Read a numeric score (0–100). Use nested ternary operators to assign a grade string: "A" (≥90), "B" (≥80), "C" (≥70), "D" (≥60), "F" (<60). Print "Score 75 → Grade C".
  5. Plural Selector: Read a count of items. Use ternary to print the grammatically correct noun form: "1 apple" vs "2 apples", "1 ox" vs "2 oxen", "1 person" vs "2 people". The nouns should be selected by ternary, not by if-else.
  6. Min of Array: Declare int arr[5] = {34, 12, 78, 5, 56}. Use a for loop and at each step update min = (arr[i] < min) ? arr[i] : min. Print the minimum value (expected: 5).
  7. Ternary Assignment: Demonstrate that ternary can be an l-value argument using pointer dereferencing: read two integers a and b and a flag. If flag=1, assign 99 to a; if flag=0, assign 99 to b — using the expression *(flag ? &a : &b) = 99. Print both a and b after.
  8. Ternary vs if-else: Write the same logic twice — once using if-else and once using ternary — to find the sign of a number (+1, 0, or -1). Confirm both produce identical output. Add a comment on when ternary is preferred over if-else.
  9. Safe Division: Read two integers a and b. Use a ternary to compute a/b only if b != 0, otherwise print "Division by zero!". Write the entire logic as a single printf with ternary inside.
  10. Triangle Type: Read three sides. Use nested ternary to classify the triangle as "Equilateral" (all sides equal), "Isosceles" (two sides equal), or "Scalene" (all different). Print the classification. No if-else allowed — ternary only.

📝 Section 4.10 Assignments — Logical Operators

  1. Truth Table Printer: Write a program that prints the full truth table for &&, ||, and ! for all combinations of A=0/1 and B=0/1. Format as a table with headers. There should be 4 rows and 5 columns (A, B, A&&B, A||B, !A).
  2. Range Validator: Read an integer. Use && to check if it falls in the range [1, 100] inclusive. Use || to check if it is outside (i.e., <1 or >100). Print appropriate messages for each check.
  3. Login System: Define username="admin" and password=1234. Read both from user. Use && for "both correct → Access Granted", || for "at least one wrong → Access Denied", and ! to negate correct flags. Print which condition triggered.
  4. Short-Circuit Proof: Write a function int sideEffect() that prints "CALLED" and returns 1. In main: evaluate 0 && sideEffect() — show "CALLED" is NOT printed. Evaluate 1 || sideEffect() — show "CALLED" is NOT printed. Explain short-circuit evaluation.
  5. NULL Pointer Guard: Declare int *p = NULL. Use p != NULL && *p > 0 to safely check without crashing. Then assign p to a valid integer variable with value 42 and repeat the check. Show both results and explain why the first doesn't segfault.
  6. Voting Eligibility: Read age and citizenship status (1=citizen, 0=not). Use logical operators to determine: Can vote? (age≥18 AND citizen), Cannot vote (age<18 OR not citizen). Print detailed eligibility message with reason.
  7. Leap Year with Logic: Read a year. Use && and || to correctly implement the leap year rule: (year%4==0 && year%100!=0) || (year%400==0). Print "Leap" or "Not Leap". Test with 2000, 1900, 2024, and 2100.
  8. NOT Operator Practice: Declare int found = 0. Use !found to print "Not found yet". Then set found = 1. Use !found again to confirm it now prints nothing. Use !!found to normalize any non-zero value to exactly 1.
  9. Multi-Condition Filter: Read 10 integers in a loop. Print only those that satisfy ALL three conditions: greater than 10, less than 100, AND divisible by 3. Use a single if with && to combine all conditions.
  10. De Morgan's Law: Prove De Morgan's laws in code. For any two integer inputs A and B, verify: !(A && B) == (!A || !B) and !(A || B) == (!A && !B). Test with all four combinations (0,0), (0,1), (1,0), (1,1) and print PASS/FAIL for each.

📝 Section 4.11 Assignments — Bitwise Operators

  1. Binary Printer: Write a function void printBinary(int n) that prints the 8 least-significant bits of n using a loop and (n >> k) & 1. In main, call it for: 0, 1, 127, 128, 255, -1, 12, 85.
  2. Bitwise Operations Table: Set int a=60, b=13. Print and explain (in comments) the result of: a&b, a|b, a^b, ~a, a<<2, a>>2. For each, also print the binary representation using your printBinary function.
  3. Bit Manipulation Suite: Start with int n = 0. Perform these operations in order using bitwise compound assignment: set bits 1, 3, 5, 7; clear bit 3; toggle bit 5; check if bit 7 is set. Print n after each step and the binary using printBinary.
  4. Permission System: Simulate Unix file permissions. Define: READ=4, WRITE=2, EXEC=1. Read a permission byte (0–7). Print "r", "w", "x" or "-" for each bit. Read a second number for owner/group/other (0–777 octal-like input 0–7 three times) and print rwxr-xr-- style output.
  5. Odd/Even Fast Check: Read 10 integers. For each, use n & 1 to determine odd/even (faster than %2). Print the result next to each number. Also verify that (n & 1) == (n % 2) for all cases (note: careful with negatives).
  6. Power of Two Checker: A number is a power of 2 if and only if n > 0 && (n & (n-1)) == 0. Read 10 integers and for each print whether it is a power of 2. Test with 1, 2, 3, 4, 7, 8, 16, 32, 63, 64.
  7. Bit Counting (Hamming Weight): Count the number of 1-bits in an integer (its "popcount"). Use a loop: while n != 0, do count += n & 1; n >>= 1. Print the result for values 0, 7, 15, 85 (0b01010101 = 4 bits set), 255.
  8. Swap Without Temp: Use the XOR swap trick — a ^= b; b ^= a; a ^= b; — to swap two integers without a temporary variable. Read two integers, swap them, print before and after. Then explain in comments why this works using truth table logic.
  9. Color Channel Extractor: An RGB color is stored as a 32-bit integer: 0x00RRGGBB. Given color 0x00FF8040, extract the Red, Green, and Blue channels using bitwise AND and right-shift. Red = (color >> 16) & 0xFF, Green = (color >> 8) & 0xFF, Blue = color & 0xFF. Print each channel value (expected: R=255, G=128, B=64).
  10. Multiply/Divide by Powers of 2: Read an integer n. Using only shift operators, compute and print: n×2, n×4, n×8, n÷2, n÷4. Then compare results with normal arithmetic (*2, *4, etc.) to verify correctness. Discuss when shifts are preferred over multiplication.

📝 Section 4.12 Assignments — Operator Precedence & Associativity

  1. Manual Evaluation: Without running the code, compute the final value of x in each line, then verify by running:
    (a) int x = 2 + 3 * 4;
    (b) int x = 10 - 4 / 2 + 1;
    (c) int x = 15 % 4 * 2;
    (d) int x = 2 << 1 + 1;
    (e) int x = 8 / 2 * 4;
  2. Parentheses Matter: For each pair below, predict both values before running. Then print both:
    (a) 3 + 4 * 2 vs (3 + 4) * 2
    (b) 10 / 2 + 3 vs 10 / (2 + 3)
    (c) !0 + 1 vs !(0 + 1)
    (d) 2 & 3 | 4 vs 2 & (3 | 4)
  3. Relational + Logical Precedence: Predict and verify:
    (a) 3 > 2 && 5 < 10 (both true → 1)
    (b) 3 > 2 + 1 (2+1 first → 3>3 → 0)
    (c) 1 + 2 > 2 + 1 (both sides computed first)
    (d) !1 + !0 (! before + → 0 + 1 = 1)
    Print each result with its computed value as a comment.
  4. Assignment Precedence: Explain and demonstrate:
    (a) Right-to-left: int a=1,b=2,c=3; a = b = c = 10; — print all three
    (b) int x = 5; x += 2 * 3; — show that * happens before +=
    (c) int y = (3 + 4) * (2 - 1); — use parens to override default precedence
  5. Bitwise Precedence Trap: The classic bug: if (flags & MASK != 0) is actually parsed as flags & (MASK != 0) because != has higher precedence than &! Write a program demonstrating this bug, then fix it with if ((flags & MASK) != 0). Print both results.
  6. Ternary Associativity: Ternary is right-associative. Evaluate: int x = 1 ? 2 ? 3 : 4 : 5; First predict the value manually (answer: 3). Then run and verify. Explain how right-to-left associativity means it is parsed as 1 ? (2 ? 3 : 4) : 5.
  7. Unary Minus Priority: Predict and verify:
    (a) -2 + 3 → unary minus applied first → (-2)+3 = 1
    (b) -2 * -3 → 6 (two unary minuses)
    (c) -(2 + 3) → -5 (parens override)
    (d) !0 * 5 → 5 (!0=1, then 1*5)
    Print all results.
  8. Complex Expression Dissection: Fully parenthesize (add explicit parentheses to show evaluation order) each expression, then run to verify:
    (a) a + b > c && d - e < f where a=3,b=4,c=6,d=10,e=3,f=8
    (b) x = y = 5 + 3 * 2 where x and y initially 0
    Print the fully parenthesized form as a string comment alongside the result.
  9. Short-Circuit and Precedence: Demonstrate that in a || b && c, the && binds first (higher precedence), so it's a || (b && c) — NOT (a || b) && c. Write a program where the two interpretations give different results and print both to prove the point.
  10. Precedence Table Quiz: Write a program that tests 5 more complex expressions.for each: print what you predicted, the actual computed result, and PASS/FAIL:
    (a) 2 | 3 ^ 1 & 7 (expected: 3)
    (b) 1 << 2 + 1 (expected: 8)
    (c) ~0 & 0xFF (expected: 255)
    (d) 4 / 2 == 2 + 0 (expected: 1)
    (e) 3 != 2 + 1 (expected: 0)
Unit 5 – Control Statements

Control statements determine the flow of your program — which code runs, how many times, and under what conditions. This unit covers all decision-making and looping constructs with flowcharts for every construct.

🧠 5.0 – if Family Fundamentals (Simple if, if-else, Nested if-else)

Start with these three patterns. They are the base of all decision making in C.

1) Simple if

Explanation: Executes a block only when the condition is true. If false, program skips the block and continues.

// Syntax
if (condition) {
    // runs only when condition is true
}
// Example: check adult age
int age = 20;
if (age >= 18) {
    printf("Eligible to vote\n");
}
START condition true? YES Execute if-block NO NEXT

2) if-else

Explanation: Exactly one of two blocks executes. Use this when you have two mutually exclusive outcomes.

// Syntax
if (condition) {
    // true block
} else {
    // false block
}
// Example: even/odd
int n = 17;
if (n % 2 == 0) {
    printf("Even\n");
} else {
    printf("Odd\n");
}
START condition YES if-block NO else-block NEXT

3) Nested if-else

Explanation: An if-else inside another if block. Use it when the second decision depends on the first decision.

// Syntax
if (condition1) {
    if (condition2) {
        // block A
    } else {
        // block B
    }
} else {
    // block C
}
// Example: pass + distinction
int marks = 82;
if (marks >= 50) {
    if (marks >= 75) {
        printf("Pass with Distinction\n");
    } else {
        printf("Pass\n");
    }
} else {
    printf("Fail\n");
}
START marks >= 50? YES marks >= 75? NO Fail YES Distinction NO Pass END
Important: In C, non-zero means true and zero means false. Always use braces {} even for one-line bodies to avoid bugs and improve readability.

🔀 5.1 – if / else if / else

📋 Syntax

if (condition1) {
    // runs when condition1 is true
} else if (condition2) {
    // runs when condition2 is true (condition1 false)
} else {
    // runs when ALL above conditions are false
}

// Ternary shorthand:
result = (condition) ? value_if_true : value_if_false;

Rules: Only one block executes. The else if and else clauses are optional. Conditions must evaluate to 0 (false) or non-zero (true).

START condition true? YES if-block code NO else-block code END
#include <stdio.h>

int main() {
  int marks;
  printf("Enter marks (0-100): ");
  scanf("%d", &marks);

  if (marks >= 90) {
    printf("Grade: A (Excellent)\n");
  } else if (marks >= 75) {
    printf("Grade: B (Good)\n");
  } else if (marks >= 60) {
    printf("Grade: C (Average)\n");
  } else if (marks >= 50) {
    printf("Grade: D (Pass)\n");
  } else {
    printf("Grade: F (Fail)\n");
  }
  return 0;
}
grades.c

Ternary Operator (shorthand if-else)

// Syntax: condition ? value_if_true : value_if_false
int max = (a > b) ? a : b;        // larger of a, b
char *type = (n % 2 == 0) ? "even" : "odd";
printf("%d is %s\n", n, (n >= 0) ? "non-negative" : "negative");

🔧 5.2 – switch Statement

📋 Syntax

switch (expression) {      // expression must be int or char
    case value1:
        // code for case 1
        break;             // EXIT switch - required to stop fall-through
    case value2:
        // code for case 2
        break;
    case value3:           // multiple cases, same action (fall-through)
    case value4:
        // runs for value3 OR value4
        break;
    default:               // optional: runs if NO case matched
        // fallback code
}

Key rules: expression must produce an integer (or char). Case values must be compile-time constants. break prevents fall-through. switch cannot test ranges — use if-else for that.

switch(expr) case 1: action A case 2: action B default: default action END
#include <stdio.h>

int main() {
  int day;
  printf("Enter day (1-7): ");
  scanf("%d", &day);

  switch (day) {
    case 1: printf("Monday\n");    break;
    case 2: printf("Tuesday\n");   break;
    case 3: printf("Wednesday\n"); break;
    case 4: printf("Thursday\n");  break;
    case 5: printf("Friday\n");    break;
    case 6:
    case 7: printf("Weekend!\n");  break;
    default: printf("Invalid\n");
  }
  return 0;
}
days.c
Don't forget break! Without it, execution "falls through" to the next case. This is sometimes intentional (case 6 & 7 above) but usually a bug.

⚠️ 5.6 – The Dangling Else Problem

When if statements are nested, an else always belongs to the nearest preceding unmatched if. This can produce unexpected behaviour when indentation doesn't match the compiler's parsing rules.

⚠️ Misleading indentation
int x = 10, y = 20;

if (x > 5)
    if (y > 15)
        printf("A\n");
else
    printf("B\n"); // which if does this belong to?

// Answer: the INNER if (y > 15)
// Not the outer if (x > 5)!
// So "B" never prints when x <= 5
✅ Fixed with braces
int x = 10, y = 20;

if (x > 5) {
    if (y > 15)
        printf("A\n");
} else {
    printf("B\n"); // now unambiguously
}               // belongs to outer if

// Always use braces {} even for
// single-statement bodies to prevent
// dangling else bugs
Best practice: Always use curly braces {} around if/else bodies, even when the body is a single statement. This eliminates the dangling else trap and makes future code additions safer.

🎚️ 5.7 – Switch Fall-Through Behaviour

After a matching case label is entered, execution falls through into subsequent case blocks unless a break (or return) stops it. Fall-through is a deliberate C language feature — it can be used intentionally to share code across cases, but it is a common source of bugs when left accidental.

⚠️ Accidental fall-through (bug)
int day = 2;
switch (day) {
    case 1: printf("Monday\n");   // no break!
    case 2: printf("Tuesday\n");  // no break!
    case 3: printf("Wednesday\n");
             break;
}
// Output when day=2:
//   Tuesday    ← matched
//   Wednesday  ← fell through! (bug)
✅ Intentional fall-through (grouped cases)
int month = 4;
switch (month) {
    case 1: case 3: case 5:
    case 7: case 8: case 10: case 12:
        printf("31 days\n"); break;
    case 4: case 6: case 9: case 11:
        printf("30 days\n"); break;
    case 2:
        printf("28 or 29 days\n"); break;
    default:
        printf("Invalid month\n");
}
// month=4 → "30 days" ✓
#include <stdio.h>

/* Full switch example: menu-driven program */
int main() {
    int choice;
    printf("1=Add  2=Sub  3=Mul  4=Div  0=Quit\nChoice: ");
    scanf("%d", &choice);

    double a, b;
    if (choice != 0) {
        printf("Enter two numbers: ");
        scanf("%lf %lf", &a, &b);
    }

    switch (choice) {
        case 1: printf("%.2f + %.2f = %.2f\n", a, b, a+b); break;
        case 2: printf("%.2f - %.2f = %.2f\n", a, b, a-b); break;
        case 3: printf("%.2f * %.2f = %.2f\n", a, b, a*b); break;
        case 4:
            if (b == 0)
                printf("Error: division by zero!\n");
            else
                printf("%.2f / %.2f = %.2f\n", a, b, a/b);
            break;
        case 0: printf("Goodbye!\n"); break;
        default: printf("Invalid choice.\n");
    }
    return 0;
}
calculator.c
Rules for switch: (1) The expression must evaluate to an integer type (int, char, enum). (2) case labels must be compile-time integer constants. (3) Always include default to handle unmatched values. (4) Add break after every case unless you intentionally want fall-through.

Unit 5 – Lab Exercises

  1. Write a program to check if a number is positive, negative, or zero using if-else if-else.
  2. Write a simple calculator using switch: read two numbers and an operator (+, -, *, /). Print the result. Handle division by zero.
  3. Use nested if-else to check both sign (positive/negative) and parity (even/odd) of the input and print all four combinations (e.g., "Positive Even").
  4. Write a vowel/consonant checker using switch with grouped cases (a, e, i, o, u → vowel; default → consonant). Handle uppercase and lowercase.
  5. Demonstrate the dangling-else problem: write two versions of a nested if statement — one where the else attaches to the outer if and one where it attaches to the inner if — and print which branch executes for the same input.

Loop exercises (for, while, do-while) are in Unit 6.

Unit 6 – Loops in C

Loops eliminate repetitive code by executing a block repeatedly until a condition changes. C provides three loop constructs — for, while, and do-while — each optimised for different situations. This unit gives deep coverage of every loop type with formal syntax, annotated flowcharts, step-trace tables, and practical algorithms.

6.1 – Why Loops? The Repetition Problem

Without loops, repeating an action requires copy-pasted code that cannot scale:

/* WITHOUT a loop - print 1 to 5 */
printf("%d\n", 1);
printf("%d\n", 2);
printf("%d\n", 3);
printf("%d\n", 4);
printf("%d\n", 5);   /* what about 1 to 10 000? */

/* WITH a for loop - scales to any N */
for (int i = 1; i <= 5; i++) {
    printf("%d\n", i);
}motivation.c
Loop typeBest used when…Condition checkedMin executions
forNumber of iterations known in advanceBefore each iteration0
whileLoop count depends on a runtime conditionBefore each iteration0
do-whileBody must run at least once (menus, prompts)After each iteration1
Four parts of every loop:  (1) Initialisation — set the starting state;  (2) Condition — test before (or after) each iteration;  (3) Body — the repeated work;  (4) Update — change state to eventually make the condition false.

🔢 6.2 – for Loop — Deep Dive

📋 Syntax

for (initialisation; condition; update) {
    body;
}

// Execution order every iteration:
// (1) initialisation  - runs ONCE only, before loop starts
// (2) condition check - if FALSE exit loop immediately
// (3) body executes
// (4) update runs
// (5) back to step 2

// Common variants:
for (int i = 0; i < n; i++)        // count-up  0,1,...,n-1
for (int i = n; i >= 1; i--)       // count-down n,...,1
for (int i = 0; i < n; i += 2)     // step by 2
for (int i=0, j=n-1; i<j; i++,j--) // two variables
for (;;)                              // infinite (all parts omitted)

Flowchart

START (1) Init: i = 0 (2) i < n ? condition check YES (3) Body executes (4) Update: i++ NO END

Step-trace: for(i=0; i<4; i++) printf(i)

Stepi beforei<4?Actioni after
Init0✓ trueprints 01
21✓ trueprints 12
32✓ trueprints 23
43✓ trueprints 34
54✗ FALSEexit loop
#include <stdio.h>

int main() {
    /* Counter loop */
    for (int i = 1; i <= 5; i++)
        printf("%d ", i);     // 1 2 3 4 5
    printf("\n");

    /* Accumulator - sum 1 to 100 */
    int sum = 0;
    for (int i = 1; i <= 100; i++)
        sum += i;
    printf("Sum 1..100 = %d\n", sum); // 5050

    /* Factorial - product 1..n */
    long fact = 1;
    for (int i = 2; i <= 10; i++)
        fact *= i;
    printf("10! = %ld\n", fact); // 3628800

    /* Two-variable loop - reverse an array */
    int a[] = {1,2,3,4,5};
    for (int l=0, r=4; l < r; l++, r--) {
        int t = a[l]; a[l] = a[r]; a[r] = t;
    }
    /* a is now {5,4,3,2,1} */
    return 0;
}
for_deepdive.c

🔄 6.3 – while Loop — Deep Dive

📋 Syntax

while (condition) {
    body;
    // MUST update something to eventually make condition false
}

// Condition checked BEFORE every iteration.
// If condition is false from the start, body NEVER executes.
// Typical patterns:
int i = 0;
while (i < n)      { body; i++; }      // equivalent to a for loop
while (ch != 'q')  { scanf("%c",&ch); } // until user quits
while (!feof(fp))  { read_line(); }    // until end of file
START condition true? YES Body + Update NO END

Step-trace: digit extraction from n=1234

Iterationnn>0?digit = n%10n after /=10
11234true4123
2123true312
312true21
41true10
50FALSEexit
#include <stdio.h>

int main() {
    /* Sum of digits of 1234 */
    int n = 1234, sum = 0;
    while (n > 0) {
        sum += n % 10;   // extract last digit
        n   /= 10;       // remove last digit
    }
    printf("Digit sum = %d\n", sum); // 10

    /* Reverse a number */
    int num = 5678, rev = 0;
    while (num > 0) {
        rev = rev * 10 + num % 10;
        num /= 10;
    }
    printf("Reversed: %d\n", rev); // 8765

    /* Count digits */
    int x = 98765, count = 0;
    while (x != 0) { x /= 10; count++; }
    printf("Digits: %d\n", count); // 5
    return 0;
}
while_deepdive.c

🔁 6.4 – do-while Loop — Deep Dive

📋 Syntax

do {
    body;          // executed FIRST -- always runs at least once
} while (condition); // semicolon is required here!

// Key distinction from while:
int x = 100;
while    (x < 5) { printf("while\n");    } // prints nothing
do                { printf("do-while\n"); }
while    (x < 5);                             // prints once!
START Body (always runs at least once) condition true? true false END
#include <stdio.h>

int main() {
    /* Menu loop - show once, repeat until quit */
    int choice;
    do {
        printf("\n=== MENU ===\n");
        printf("1. Add\n2. Subtract\n0. Quit\n");
        printf("Choice: ");
        scanf("%d", &choice);
        switch (choice) {
            case 1: printf("Add selected\n");      break;
            case 2: printf("Subtract selected\n"); break;
            case 0: printf("Goodbye!\n");          break;
            default: printf("Invalid choice\n");
        }
    } while (choice != 0);

    /* Validated input with do-while */
    int age;
    do {
        printf("Enter age (1-120): ");
        scanf("%d", &age);
    } while (age < 1 || age > 120);
    printf("Valid age: %d\n", age);
    return 0;
}
do_while_deepdive.c
Rule of thumb: Use do-while when the body must execute at least once before the condition is tested — classic examples are menus, “try again” prompts, and reading the first element of a stream.

6.5 – Infinite Loops and How to Exit Them

📋 Three ways to write an infinite loop

for (;;) { body; }        // most common style in systems code
while (1) { body; }       // 1 is always non-zero (true)
do { body; } while(1);   // less common

// Exit with break inside a conditional:
while (1) {
    int input;
    scanf("%d", &input);
    if (input == 0) break; // controlled exit
    printf("Got: %d\n", input);
}
#include <stdio.h>

/* Simulated server event loop - runs "forever" */
int main() {
    int req = 1;
    for (;;) {
        printf("Handling request #%d\n", req++);
        /* In real server: accept socket, serve, repeat */
        if (req > 5) break; // simulate graceful shutdown
    }
    printf("Server shut down.\n");
    return 0;
}
// Ctrl+C kills any truly infinite loop in the terminal
event_loop.c
Avoiding accidental infinite loops: Always ensure the loop variable is updated inside the body, the condition can eventually become false, and break is reachable. Use gcc -Wall to catch suspicious loop patterns. Debug with printf or GDB if a program hangs.

🛑 6.6 – break Statement — Deep Dive

📋 Syntax & Behaviour

break; // exits the INNERMOST enclosing loop or switch

// Works in: for, while, do-while, switch
// Does NOT break outer loops (only innermost)
// To break outer loop: use a flag variable

for (int i = 0; i < n; i++) {
    if (arr[i] == target) {
        printf("Found at %d\n", i);
        break; // stop searching after first match
    }
}

// Breaking outer loop using a flag:
int done = 0;
for (int i = 0; i < ROWS && !done; i++)
    for (int j = 0; j < COLS; j++)
        if (grid[i][j] == target) { done = 1; break; }
#include <stdio.h>

int main() {
    /* Linear search with break - stops at first match */
    int data[] = {15, 3, 42, 8, 27};
    int target = 42, found = -1;
    for (int i = 0; i < 5; i++) {
        if (data[i] == target) { found = i; break; }
    }
    (found >= 0) ? printf("Found at %d\n", found)
                 : printf("Not found\n");

    /* break exits ONLY the innermost loop */
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            if (j == 1) break; // breaks inner, outer still runs!
            printf("(%d,%d) ", i, j);
        }
    }
    printf("\n"); // prints (0,0) (1,0) (2,0)
    return 0;
}
break_demo.c

6.7 – continue Statement — Deep Dive

📋 Syntax & Behaviour

continue; // skips REST of current iteration

// In a for loop:     jumps to the UPDATE, then condition check
// In a while loop:   jumps directly to the condition check
// WARNING: in while, if skip variable never changes -> infinite loop!

for (int i=0; i<10; i++) {
    if (i%2==0) continue; // skip even (i++ still runs)
    printf("%d ", i);        // prints odd: 1 3 5 7 9
}
#include <stdio.h>

int main() {
    /* Skip even numbers */
    printf("Odd 1-10: ");
    for (int i = 1; i <= 10; i++) {
        if (i % 2 == 0) continue;
        printf("%d ", i); // 1 3 5 7 9
    }
    printf("\n");

    /* Skip multiples of 3 */
    printf("No mult-of-3 (1-15): ");
    for (int i = 1; i <= 15; i++) {
        if (i % 3 == 0) continue;
        printf("%d ", i); // 1 2 4 5 7 8 10 11 13 14
    }
    printf("\n");

    /* Sum only positive inputs from user */
    int total = 0, n;
    printf("Enter 5 numbers: ");
    for (int i = 0; i < 5; i++) {
        scanf("%d", &n);
        if (n <= 0) continue; // skip negatives and zero
        total += n;
    }
    printf("Sum of positives: %d\n", total);
    return 0;
}
continue_demo.c

🌀 6.8 – Nested Loops

📋 Syntax

for (int outer = 0; outer < ROWS; outer++) {
    for (int inner = 0; inner < COLS; inner++) {
        // inner loop body runs COLS times per outer iteration
        // total executions = ROWS x COLS (O(n^2) complexity)
    }
}
// Each inner loop variable is independent of the outer one.
// Best practice: keep nesting depth <= 3 levels for readability.

Trace: for(i=1;i<=3;i++) for(j=1;j<=3;j++) printf("(%d,%d)",i,j)

ij valuesOutput
11, 2, 3(1,1) (1,2) (1,3)
21, 2, 3(2,1) (2,2) (2,3)
31, 2, 3(3,1) (3,2) (3,3)
#include <stdio.h>

int main() {
    /* Pattern 1: Right-angle triangle */
    for (int i = 1; i <= 5; i++) {
        for (int j = 1; j <= i; j++) printf("* ");
        printf("\n");
    } // * / * * / * * * / * * * * / * * * * *

    /* Pattern 2: Number pyramid */
    for (int i = 1; i <= 5; i++) {
        for (int j = 1; j <= i; j++) printf("%d ", j);
        printf("\n");
    } // 1 / 1 2 / 1 2 3 / 1 2 3 4 / 1 2 3 4 5

    /* Pattern 3: Multiplication table */
    printf("\nMultiplication table 1-5:\n");
    for (int i = 1; i <= 5; i++) {
        for (int j = 1; j <= 5; j++)
            printf("%4d", i*j);
        printf("\n");
    }

    /* Pattern 4: Diamond of stars */
    int n = 5;
    for (int i = 1; i <= n; i++) {
        for (int sp = 0; sp < n-i; sp++) printf(" ");
        for (int j = 0; j < 2*i-1; j++) printf("*");
        printf("\n");
    }
    for (int i = n-1; i >= 1; i--) {
        for (int sp = 0; sp < n-i; sp++) printf(" ");
        for (int j = 0; j < 2*i-1; j++) printf("*");
        printf("\n");
    }
    return 0;
}
nested_patterns.c

🧮 6.9 – Common Loop Algorithms

#include <stdio.h>
#include <math.h>   // sqrt() - compile with -lm

/* 1. Factorial (iterative) */
long long factorial(int n) {
    long long f = 1;
    for (int i = 2; i <= n; i++) f *= i;
    return f;  // factorial(10) = 3628800
}

/* 2. Fibonacci sequence */
void fibonacci(int count) {
    int a = 0, b = 1, c;
    printf("%d %d", a, b);
    for (int i = 2; i < count; i++) {
        c = a + b; a = b; b = c;
        printf(" %d", c);
    }
    printf("\n");
}  // fibonacci(8) prints: 0 1 1 2 3 5 8 13

/* 3. Prime check (optimised - only test up to sqrt(n)) */
int isPrime(int n) {
    if (n < 2) return 0;
    if (n == 2) return 1;
    if (n % 2 == 0) return 0;
    for (int i = 3; i <= (int)sqrt(n); i += 2)
        if (n % i == 0) return 0;
    return 1;
}

/* 4. GCD - Euclidean algorithm */
int gcd(int a, int b) {
    while (b != 0) { int t = b; b = a % b; a = t; }
    return a;  // gcd(48,18) = 6
}

/* 5. Analyse a number: digits, sum, reversed, palindrome */
void analyseNumber(int n) {
    int digits=0, dSum=0, rev=0, orig=n;
    while (n > 0) {
        int d = n % 10;
        digits++; dSum += d; rev = rev*10+d; n /= 10;
    }
    printf("%d: %d digits, sum=%d, reversed=%d, palindrome=%s\n",
        orig, digits, dSum, rev, (orig==rev)?"yes":"no");
}  // analyseNumber(12321) -> palindrome=yes

int main() {
    printf("5!=%lld\n", factorial(5));          // 120
    fibonacci(10);
    printf("17 prime=%d\n", isPrime(17));      // 1
    printf("GCD(48,18)=%d\n", gcd(48,18));     // 6
    analyseNumber(12321);
    return 0;
}
algorithms.c  --  gcc algorithms.c -o algorithms -lm

🔀 6.10 – The goto Statement

goto unconditionally jumps to a labelled statement in the same function. It is a low-level control mechanism that is almost always avoidable. The one legitimate use-case is breaking out of deeply nested loops in a single jump — even then, a well-structured function using return is usually cleaner.

#include <stdio.h>

/* goto syntax: label must be in the same function */
/*   goto label_name;                               */
/*   label_name:  statement;                        */

/* ── Use case: break out of nested loops ── */
int main() {
    for (int i = 0; i < 5; i++) {
        for (int j = 0; j < 5; j++) {
            if (i == 2 && j == 3) {
                goto done;   // jump out of BOTH loops at once
            }
            printf("(%d,%d) ", i, j);
        }
    }
done:
    printf("\nStopped at (2,3)\n");
    return 0;
}

/* ── Alternative without goto (preferred style) ── */
int searchMatrix() {
    int found = 0;
    for (int i = 0; i < 5 && !found; i++) {
        for (int j = 0; j < 5 && !found; j++) {
            if (i == 2 && j == 3) found = 1;
        }
    }
    return found;   // cleaner than goto
}
goto_example.c
Avoid goto in almost all situations. It makes code hard to read, debug, and maintain. Structured alternatives (break, continue, early return, flag variables) always exist. In professional C code, goto appears only in low-level system code and error-cleanup patterns in Linux kernel style.

Unit 6 – Lab Exercises (10 Tasks)

  1. Write a program using a for loop to print the multiplication table of any number N entered by the user (N × 1 through N × 12). Format output as “7 x  3 = 21” with right-aligned spacing using %3d.
  2. Use a while loop to implement a guessing game: the program picks 42, the user keeps guessing until correct. Print “Too High”, “Too Low”, or “Correct! Attempts: N”. Count attempts.
  3. Use a do-while loop to create a text menu: (1) Check Even/Odd, (2) Find Factorial, (3) Print Fibonacci, (0) Quit. Keep showing the menu until 0 is chosen. Implement each option.
  4. Print all prime numbers between 2 and 100 using a nested for loop. Count and display the total found. Format: 10 primes per line using %4d.
  5. Write separate functions that print: (a) right-angle star triangle 5 rows; (b) inverted triangle; (c) number pyramid; (d) Floyd’s triangle. Call all four from main.
  6. Using a while loop on number 12321, compute: digit count, digit sum, digit product, reversed number. Print all results and whether the number is a palindrome.
  7. Read N integers (use a for loop). Then compute: minimum, maximum, sum, average, and count of values above average. Print all results.
  8. Implement the Sieve of Eratosthenes to find all primes up to 200. Use an int isPrime[201] array initialised to 1; use nested loops to mark composites. Print primes 10 per line.
  9. Print a diamond pattern of stars for an odd number N entered by the user (e.g., N=5 gives a 9-row diamond). Derive space and star counts from the row number using nested loops.
  10. Implement Binary Search using a while loop: create a sorted array of 10 integers; ask the user for a target; repeatedly compare mid-point and halve the search range. Print each comparison step and the final result (index or “Not found”).

Hint: Lab 8 (Sieve): mark isPrime[0]=isPrime[1]=0 first; for each prime p, mark p×p, p×(p+1), … up to 200 as 0. Lab 10 (binary search): always update low = mid + 1 or high = mid - 1, never just low = mid to avoid infinite loops. Compile with gcc -std=c99 for int array VLAs if needed.

Unit 8 – Functions

Functions allow you to break code into reusable, named blocks. They are the cornerstone of structured programming in C — reducing duplication, improving readability, and enabling modular design.

🧩 Function Syntax Quick Reference

Functions in C have three common forms: prototype, definition, and call. Pointer parameters are used when the caller's data must be changed.

int add(int a, int b);
void swap(int *a, int *b);

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

result = add(2, 3);
swap(&x, &y);

📖 Theory Focus

  • A function boundary is both a code-organisation tool and a contract about inputs, outputs, and side effects.
  • C uses pass-by-value, which means plain parameters receive copies; mutation of caller data requires passing an address.
  • Good functions reduce cognitive load because each one captures one operation, one idea, or one business rule clearly.

🏗️ 6.1 – Function Anatomy: Prototype, Definition, Call

/*═══════════════════════════════════════════════
  PROTOTYPE (Declaration) – goes at top of file
  Tells compiler: "this function exists, here are
  its parameter types and return type"
═══════════════════════════════════════════════*/
int add(int a, int b);         // prototype
void printLine(int n);         // prototype (void = no return)
double circleArea(double r);   // prototype

/*═══════════════════════════════════════════════
  FUNCTION CALL – inside main() or another function
═══════════════════════════════════════════════*/
int main() {
    int result = add(5, 3);       // call – 5 and 3 are "arguments"
    printLine(20);               // call with one argument
    double a = circleArea(7.0);  // call
    printf("%d, %.2f\n", result, a);
    return 0;
}

/*═══════════════════════════════════════════════
  FUNCTION DEFINITIONS – below main()
  returnType functionName(type param, ...) { body }
═══════════════════════════════════════════════*/
int add(int a, int b) {          // a, b are "parameters"
    return a + b;                 // return value to caller
}

void printLine(int n) {
    for (int i = 0; i < n; i++) printf("-");
    printf("\n");
    // no return needed for void
}

double circleArea(double r) {
    return 3.14159 * r * r;
}
functions.c
Function TypeReturns?Parameters?Example
No return, no paramNo (void)No (void)void greet(void)
No return, with paramNoYesvoid printN(int n)
With return, no paramYesNoint getMax(void)
With return, with paramYesYesint add(int a, int b)

📚 6.2 – Function Call Stack (Memory Diagram)

Each function call creates a new stack frame on the stack. When the function returns, its frame is popped off.

Stack grows downward ↓

High address
Frame: main()caller
local: x = 5
5
0x7fff01a0
local: result
8 ←
0x7fff01a4
return addr
0x400…
0x7fff01a8
↓ stack grows here
Frame: add(5, 3)callee
param: a = 5
5
0x7fff0180
param: b = 3
3
0x7fff0184
return value
8
register
← popped on return
Low address
#include <stdio.h>

int add(int a, int b) {
    // New stack frame created here
    // a and b are COPIES of arguments
    // (pass by value)
    int sum = a + b;
    return sum;
    // Frame destroyed when we return
}

int main() {
    int x = 5;
    // When add() is called:
    // 1. Push new frame on stack
    // 2. Copy x=5 to param 'a'
    // 3. Copy 3 to param 'b'
    // 4. Execute add body
    // 5. Return value in register
    // 6. Pop frame off stack
    int result = add(x, 3);
    printf("%d\n", result);  // 8
    return 0;
}
call_stack.c

⚖️ 6.3 – Pass by Value vs Pass by Reference

❌ Pass by Value (copy)

// original NOT changed
void doubleVal(int x) {
    x = x * 2;   // changes COPY only
}

int main() {
    int n = 5;
    doubleVal(n);
    printf("%d\n", n); // still 5!
    return 0;
}
by_value.c
main: n
5
0x1000
copy →
doubleVal: x
5→10
0x2000
x is a separate copy. Changing x in the function does NOT affect n in main.

✅ Pass by Reference (pointer)

// original IS changed
void doubleRef(int *x) {
    *x = (*x) * 2; // changes original
}

int main() {
    int n = 5;
    doubleRef(&n);   // pass ADDRESS of n
    printf("%d\n", n); // 10! ✓
    return 0;
}
by_ref.c
main: n
5→10
0x1000
*x points here
doubleRef: x
0x1000
0x2000
x holds the address of n. Writing to *x modifies n directly.

🌀 6.4 – Recursion

A recursive function calls itself with a simpler input, until a base case stops the recursion.

#include <stdio.h>

// FACTORIAL: n! = n × (n-1) × ... × 1
long long factorial(int n) {
    if (n <= 1) return 1;  // base case
    return n * factorial(n - 1); // recursive call
}

// FIBONACCI: 0,1,1,2,3,5,8,13,21...
int fib(int n) {
    if (n <= 1) return n;      // base cases: fib(0)=0, fib(1)=1
    return fib(n-1) + fib(n-2); // recursive
}

int main() {
    printf("5! = %lld\n", factorial(5));  // 120
    for(int i=0; i<10; i++)
        printf("%d ", fib(i));   // 0 1 1 2 3 5 8 13 21 34
    return 0;
}
recursion.c

Recursion call stack for factorial(4):

factorial(4)returns 4×6=24
n=4
→ 4 × factorial(3)
factorial(3)returns 3×2=6
n=3
→ 3 × factorial(2)
factorial(2)returns 2×1=2
n=2
→ 2 × factorial(1)
factorial(1)BASE CASE → 1
n=1 → return 1
Frames unwind: factorial(1)=1 → factorial(2)=2 → factorial(3)=6 → factorial(4)=24

🔭 6.5 – Variable Scope & Storage Classes

#include <stdio.h>

int globalVar = 100;   // GLOBAL: visible everywhere in file

void counter() {
    static int count = 0;  // STATIC: persists between calls
    count++;               // NOT reset on each call
    printf("Called %d times\n", count);
}

void scopeDemo() {
    int localVar = 50;     // LOCAL: only in this function
    printf("local=%d, global=%d\n", localVar, globalVar);
    // globalVar is accessible here
    globalVar++;
}

int main() {
    scopeDemo();        // local=50, global=100
    printf("global=%d\n", globalVar);  // 101 (modified in scopeDemo)

    counter();  // Called 1 times
    counter();  // Called 2 times (static persists!)
    counter();  // Called 3 times

    // Block scope
    {
        int blockVar = 99;  // only in this block
        printf("blockVar=%d\n", blockVar);
    }
    // blockVar is GONE here
    return 0;
}
scope.c
Storage ClassScopeLifetimeDefault InitWhere Stored
autoLocal (function/block)Block onlyGarbageStack
static (local)LocalEntire program0Data/BSS segment
static (global)File-onlyEntire program0Data/BSS segment
externGlobal (all files)Entire program0Data/BSS segment
registerLocalBlock onlyGarbageCPU register (hint)

📋 6.5 – Prototypes, Header Files & Multi-File Compilation

A function prototype is a declaration that tells the compiler about a function's name, return type, and parameter types before its full definition. Without a prototype, calling a function defined later in the file (or in another file) causes a compiler warning or error under strict standards.

⚠️ No prototype — order-dependent
// greet() is defined AFTER main()
// Without a prototype, C assumes it
// returns int — wrong! Compiler warns.
int main() {
    greet();    // implicit declaration
    return 0;
}
void greet() {
    printf("Hello!\n");
}
✅ With prototype — order-free
// Declare BEFORE use so compiler
// knows the type signature
void greet(void);   // prototype

int main() {
    greet();   // compiler is happy ✓
    return 0;
}
void greet(void) {
    printf("Hello!\n");
}

For larger programs, prototypes are placed in header files (.h) so they can be shared across multiple .c source files:

/* ── math_utils.h ── */
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

int    add(int a, int b);
double average(int *arr, int n);
int    factorial(int n);

#endif  /* MATH_UTILS_H */
math_utils.h
/* ── math_utils.c ── */
#include "math_utils.h"

int add(int a, int b) {
    return a + b;
}
double average(int *arr, int n) {
    int sum = 0;
    for (int i = 0; i < n; i++) sum += arr[i];
    return (double)sum / n;
}
int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n-1);
}
math_utils.c
/* ── main.c ── */
#include <stdio.h>
#include "math_utils.h" // use ""  not <>

int main() {
    printf("%d\n", add(3, 4));       // 7
    printf("%d\n", factorial(5));    // 120

    int data[] = {10, 20, 30};
    printf("%.1f\n", average(data, 3));  // 20.0
    return 0;
}

/* Compile multiple files:
   gcc main.c math_utils.c -o program */
main.c — compile: gcc main.c math_utils.c -o program
Include styleMeaningSearched in
#include <stdio.h>System headerCompiler's system include directories
#include "myfile.h"User/local headerCurrent directory first, then system paths
Include guards (#ifndef HEADER_H / #define HEADER_H / #endif) prevent a header from being included twice in the same translation unit, which would cause duplicate-definition errors.

6.6 – Macros vs Functions vs inline

C gives you three ways to factor out reusable computation. Each has different trade-offs for type safety, debugging, and performance.

#include <stdio.h>

/* ── 1. Macro: text substitution by preprocessor ── */
#define SQUARE_MACRO(x)  ((x) * (x))   // always parenthesise!
// Pitfall: SQUARE_MACRO(i++) expands to ((i++)*(i++)) — UB!

/* ── 2. Regular function: type-safe, call overhead ── */
int square_fn(int x) {
    return x * x;
}
// Works correctly: square_fn(i++) is well-defined

/* ── 3. inline function: hint to compiler to expand at call site ── */
static inline int square_inline(int x) {
    return x * x;
}
// Type-safe like a function, but may avoid function-call overhead
// 'static' keeps it file-local to avoid multiple-definition issues
// With optimisation (-O2), the compiler often inlines anyway

int main() {
    printf("%d\n", SQUARE_MACRO(5));    // 25  (preprocessor expands)
    printf("%d\n", square_fn(5));      // 25  (function call)
    printf("%d\n", square_inline(5));  // 25  (likely inlined)
    return 0;
}
macro_vs_function.c
MacroFunctioninline function
Type checking❌ None✅ Full✅ Full
Side-effect safe❌ Dangerous✅ Safe✅ Safe
Debuggable in GDB❌ No✅ Yes✅ Usually
Call overheadNone (text sub)PossibleNone (if inlined)
Works with all types✅ (duck typing)❌ One type set❌ One type set
Prefer static inline functions over function-like macros in C. Use macros only for constants (#define MAX 100) or genuinely type-generic operations where you cannot use a function.

Unit 8 – Lab Exercises

  1. Write four separate functions: findMax(a,b), findMin(a,b), square(n), cube(n). Call all four from main and print results.
  2. Write a function isPrime(int n) that returns 1 if prime, else 0. Use it to print all prime numbers between 1 and 100.
  3. Write a recursive function power(base, exp) that computes base^exp without using pow().
  4. Write a swap function that works correctly using pointers: void swap(int *a, int *b). Demonstrate that the values in main are actually swapped.
  5. Demonstrate the static variable counter: write function hitCounter() that prints how many times it's been called. Call it 5 times from main.
  6. Write a function sumDigits(int n) that recursively sums the digits of a positive integer (e.g., sumDigits(1234) = 10).

Hint: For sumDigits recursively: base case = n < 10, recursive case = (n % 10) + sumDigits(n / 10).

Unit 7 – Arrays & Strings

Arrays store multiple values of the same type in contiguous memory. Strings in C are null-terminated character arrays. Mastering arrays is essential before tackling pointers and data structures.

🗃️ Array and String Syntax

Array syntax fixes the element type and length. String syntax is special because a C string is just a char array ending in '\0'.

int arr[5] = {1, 2, 3, 4, 5};
char name[20] = "Linux";
int matrix[2][3] = {{1,2,3},{4,5,6}};

for (int i = 0; i < 5; i++) printf("%d ", arr[i]);
fgets(name, sizeof(name), stdin);

📖 Theory Focus

  • Arrays are efficient because elements live contiguously in memory, which makes indexing and traversal predictable.
  • Strings are more fragile because the language does not store the length automatically; library functions rely on the null terminator.
  • The step from arrays to pointers becomes easier once you see that indexing is really address arithmetic on contiguous storage.

📦 7.1 – 1D Arrays: Memory Layout

An array occupies consecutive memory cells. Each element has the same size (e.g., 4 bytes for int).

Memory layout of int arr[5] = {10, 20, 30, 40, 50};
arr[0]
10
1000
arr[1]
20
1004
arr[2]
30
1008
arr[3]
40
1012
arr[4]
50
1016
← Each int = 4 bytes. Address = base + (index × sizeof(int))
#include <stdio.h>

int main() {
    /* ── Declaration and Initialization ── */
    int arr[5] = {10, 20, 30, 40, 50};

    /* ── Access by index (0-based) ── */
    printf("First: %d\n", arr[0]);  // 10
    printf("Last:  %d\n", arr[4]);  // 50

    /* ── Traverse with for loop ── */
    int sum = 0;
    for (int i = 0; i < 5; i++) {
        sum += arr[i];
        printf("arr[%d] = %d  addr = %p\n", i, arr[i], &arr[i]);
    }
    printf("Sum = %d\n", sum);   // 150

    /* ── Partial initialization: rest = 0 ── */
    int scores[10] = {90, 85};   // scores[2..9] = 0 automatically

    /* ── Size using sizeof ── */
    int len = sizeof(arr) / sizeof(arr[0]);  // 20/4 = 5
    printf("Length = %d\n", len);
    return 0;
}
arrays1d.c

🗂️ 7.2 – 2D Arrays: Row-Major Storage

C stores 2D arrays in row-major order — all elements of row 0 come first in memory, then row 1, etc.

int matrix[2][3] = {{1,2,3},{4,5,6}} — row-major memory layout:
[0][0]
1
1000
[0][1]
2
1004
[0][2]
3
1008
[1][0]
4
1012
[1][1]
5
1016
[1][2]
6
1020
Blue = row 0 · Green = row 1 ·  Address formula: base + (row × cols + col) × sizeof(type)
#include <stdio.h>

int main() {
    int matrix[3][3] = {
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9}
    };

    /* ── Print matrix ── */
    for (int r = 0; r < 3; r++) {
        for (int c = 0; c < 3; c++)
            printf("%3d", matrix[r][c]);
        printf("\n");
    }

    /* ── Main diagonal sum ── */
    int diagSum = 0;
    for (int i = 0; i < 3; i++)
        diagSum += matrix[i][i];
    printf("Diagonal sum: %d\n", diagSum);  // 1+5+9 = 15

    /* ── Transpose ── */
    int trans[3][3];
    for (int r = 0; r < 3; r++)
        for (int c = 0; c < 3; c++)
            trans[c][r] = matrix[r][c];
    return 0;
}
matrix.c

🔠 7.3 – Character and String in C

This section focuses on strings in detail: how they are stored in memory, how to read them safely, and how to process them character by character.

ItemMeaningExampleImportant Note
CharacterSingle byte value in charchar ch = 'A';Stored as ASCII code ('A' = 65)
StringArray of chars ending with '\0'char s[] = "HELLO";Compiler adds terminator automatically
LengthNumber of visible charactersstrlen("HELLO")Returns 5, excludes '\0'
Safe inputLimit input widthscanf("%19s", s)Prevents buffer overflow
Line inputRead full line with spacesfgets(s, sizeof(s), stdin)Safer than gets (never use gets)
char word[] = "CAT"; memory layout:
word[0]
'C'
67
word[1]
'A'
65
word[2]
'T'
84
word[3]
'\0'
0
The final '\0' byte is mandatory. Without it, string functions keep reading memory and cause undefined behavior.
#include <stdio.h>
#include <string.h>

int main() {
    char s1[] = "Linux";      // size = 6 (5 chars + '\0')
    char s2[20] = "C";

    printf("s1 = %s, length = %zu\n", s1, strlen(s1));

    strcpy(s2, s1);
    strcat(s2, " Programming");
    printf("s2 = %s\n", s2);

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

int main() {
    char name[30];

    printf("Enter your name: ");
    fgets(name, sizeof(name), stdin);   // reads spaces too

    // Convert to uppercase
    for (int i = 0; name[i] != '\0'; i++)
        name[i] = toupper((unsigned char)name[i]);

    printf("Uppercase: %s", name);
    return 0;
}
string_traversal.c
Common mistakes to avoid: (1) forgetting '\0', (2) using %s without width in scanf, (3) copying into small buffers with strcpy, (4) modifying string literals like char *p = "abc"; p[0] = 'A'; (undefined behavior).

🔤 7.4 – Strings: Null-Terminated Character Arrays

In C, a string is a char array ending with the null terminator '\0' (ASCII 0). This is the sentinel that marks the end of the string.

char name[] = "Hello"; — stored in memory as:
name[0]
'H'
72
name[1]
'e'
101
name[2]
'l'
108
name[3]
'l'
108
name[4]
'o'
111
name[5]
'\0'
0
The '\0' null terminator is REQUIRED — it's how functions like printf and strlen know where the string ends.
#include <stdio.h>
#include <string.h>   // string functions

int main() {
    /* ── Declaration methods ── */
    char s1[] = "Hello";         // size = 6 (includes '\0')
    char s2[20] = "World";       // array size 20, 5+1 used
    char s3[20];
    scanf("%19s", s3);           // read word (stops at space)

    printf("Len of s1 = %zu\n", strlen(s1));   // 5 (not 6)

    /* ── String functions from <string.h> ── */
    strcpy(s2, s1);              // copy s1 into s2
    strcat(s2, " World");        // append → "Hello World"
    printf("%s\n", s2);

    int cmp = strcmp("abc", "abd");
    // cmp < 0  : first is less
    // cmp == 0 : equal
    // cmp > 0  : first is greater

    /* ── Character-by-character traversal ── */
    for (int i = 0; s1[i] != '\0'; i++)
        printf("%c", s1[i]);
    printf("\n");
    return 0;
}
strings.c
FunctionHeaderDescriptionExample
strlen(s)<string.h>Length (without '\0')strlen("Hi") = 2
strcpy(dst,src)<string.h>Copy src → dststrcpy(a,"hello")
strncpy(dst,src,n)<string.h>Safe copy, max n charsstrncpy(a,b,10)
strcat(dst,src)<string.h>Append src to dststrcat(a," world")
strcmp(s1,s2)<string.h>Compare stringsstrcmp("a","b")<0
strchr(s,c)<string.h>Find first char c in sstrchr("hello",'l')
sprintf(buf,...)<stdio.h>Print to string buffersprintf(buf,"%d",42)
toupper(c)/tolower(c)<ctype.h>Change case of a chartoupper('a')='A'
Buffer overflow risk: Always use strncpy instead of strcpy, and %19s (limit) instead of %s with scanf to prevent writing past the array boundary.

🔗 7.5 – Array Decay to Pointer

In most expressions, an array name decays (is implicitly converted) into a pointer to its first element. This is one of the most important — and most confusing — rules in C. Understanding it clarifies why you cannot find the length of an array inside a function that received it as a parameter.

#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};

    /* sizeof(arr) returns the full array size (before decay) */
    printf("sizeof(arr)   = %zu bytes\n", sizeof(arr));    // 20
    printf("sizeof(arr[0])= %zu bytes\n", sizeof(arr[0]));  // 4
    printf("Length        = %zu\n", sizeof(arr) / sizeof(arr[0]));  // 5

    /* When arr is used in most expressions, it decays to int * */
    int *p = arr;          // arr decays to &arr[0]
    printf("arr = %p\n",  (void*)arr);    // address of first element
    printf("p   = %p\n",  (void*)p);      // same address
    printf("arr[2] = %d, *(arr+2) = %d\n", arr[2], *(arr+2)); // both 30

    /* Decay happens when passed to a function */
    return 0;
}

/* Function receives int* not the whole array.
   sizeof(a) here would give sizeof(int*) = 8 bytes, NOT 20! */
void processArray(int *a, int n) {
    printf("sizeof(a) in function = %zu\n", sizeof(a)); // 8 (pointer size!)
    printf("n (passed explicitly)  = %d\n", n);          // 5 (correct)
    for (int i = 0; i < n; i++)
        printf("%d ", a[i]);   // a[i] == *(a+i)
}
array_decay.c
Summary: When does decay NOT happen?
ContextDecays?Why
sizeof(arr)❌ Nosizeof is a special operator that sees the full type
&arr❌ NoAddress-of applied to the whole array, gives int(*)[5]
arr[i], arr + 1✅ YesUsed as a value expression — decays to int *
Passed to function✅ YesFunction receives a pointer to first element
Assigned to pointer✅ Yesint *p = arr; — decay occurs
Always pass the length separately when passing arrays to functions. There is no way to recover the array length from a decayed pointer alone.

🛡️ 7.6 – Safe String Input & <ctype.h> Character Functions

Reading strings safely in C requires understanding why some functions are dangerous and knowing how to use alternatives correctly.

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

int main() {
    char name[50];

    /* ── DANGEROUS: gets() — NEVER use ── */
    // gets(name);  // removed from C11 standard — buffer overflow risk!
    // If user types more than 49 chars → stack corruption → crash/attack

    /* ── SAFE: fgets() — always specify buffer size ── */
    printf("Enter name: ");
    if (fgets(name, sizeof(name), stdin) == NULL) {
        printf("Input error\n");
        return 1;
    }
    /* fgets keeps the newline '\n' — strip it */
    name[strcspn(name, "\n")] = '\0';  // elegant removal of trailing \n
    printf("Hello, %s!\n", name);

    /* ── scanf with width limit — safer than no limit ── */
    char word[20];
    scanf("%19s", word);   // reads max 19 chars + null terminator

    /* ── Parse from string with sscanf ── */
    char line[] = "Alice 25 9.5";
    char person[20]; int age; float score;
    sscanf(line, "%19s %d %f", person, &age, &score);
    printf("%s is %d years old, score %.1f\n", person, age, score);
    return 0;
}
safe_input.c

Character Classification Functions from <ctype.h>:

FunctionReturns true (non-zero) if c is…Example
isalpha(c)A letter (a–z, A–Z)isalpha('A') → 1
isdigit(c)A decimal digit (0–9)isdigit('7') → 1
isalnum(c)Letter or digitisalnum('z') → 1
isspace(c)Whitespace: space, tab, newlineisspace('\n') → 1
isupper(c)Uppercase letter (A–Z)isupper('Z') → 1
islower(c)Lowercase letter (a–z)islower('a') → 1
ispunct(c)Punctuation characterispunct('.') → 1
toupper(c)Returns uppercase version of c
tolower(c)Returns lowercase version of c
#include <stdio.h>
#include <ctype.h>

/* Count vowels, consonants, digits, spaces in a string */
void analyseString(const char *s) {
    int vowels=0, consonants=0, digits=0, spaces=0;
    for (; *s; s++) {
        char c = (char)tolower((unsigned char)*s);
        if (isdigit((unsigned char)*s))        digits++;
        else if (isspace((unsigned char)*s))   spaces++;
        else if (isalpha((unsigned char)*s)) {
            if (c=='a'||c=='e'||c=='i'||c=='o'||c=='u') vowels++;
            else consonants++;
        }
    }
    printf("Vowels=%d Consonants=%d Digits=%d Spaces=%d\n",
           vowels, consonants, digits, spaces);
}
ctype_demo.c
Always cast the character argument to unsigned char before passing to <ctype.h> functions: isdigit((unsigned char)c). Passing a plain char can cause undefined behaviour on systems where char is signed and the value is negative (e.g., non-ASCII input).

Unit 7 – Lab Exercises

  1. Declare an array of 10 integers from user input. Find the max, min, and average.
  2. Write a function void reverseArray(int *arr, int n) that reverses an array in-place.
  3. Implement linear search: int linearSearch(int *arr, int n, int key) — return index or -1.
  4. Implement bubble sort on an array of 10 integers. Print the sorted array.
  5. Read a string and count: (a) vowels, (b) consonants, (c) digits, (d) spaces.
  6. Write a function that reverses a string in-place without using <string.h>.
  7. Check if a string is a palindrome (e.g., "madam", "racecar" are palindromes).
  8. Multiply two 2×2 matrices and print the result matrix.

Hint: For matrix multiplication C[i][j] += A[i][k] * B[k][j] with a triple nested loop.

Unit 9 – Pointers

Pointers are variables that store memory addresses. They are the most powerful (and dangerous) feature of C, enabling direct memory manipulation, efficient array handling, and dynamic memory allocation.

🧭 Pointer Syntax Quick Reference

Pointer syntax has three core operators: declaration with *, address-of with &, and dereference with * while reading or writing through the pointer.

int x = 10;
int *p = &x;
printf("%d %p %d\n", x, p, *p);
*p = 25;

void update(int *value) {
    *value += 1;
}

📖 Theory Focus

  • A pointer is meaningful only when you separate the address it stores from the data located at that address.
  • Pointers matter because C lets programs share, mutate, and traverse memory directly instead of hiding it behind higher-level abstractions.
  • Most pointer bugs come from invalid lifetime, wrong type assumptions, or dereferencing a value that is null, dangling, or out of bounds.

🎯 8.1 – Pointer Basics: Addresses and Dereference

Memory diagram: int x = 42;   int *p = &x;
int x
42
0x1000
p stores 0x1000
int *p
0x1000
0x2000
&x = "address of x" = 0x1000  |  *p = "value at address p" = 42  |  p == &x is true
#include <stdio.h>

int main() {
    int x = 42;
    int *p = &x;    // p holds the ADDRESS of x

    printf("x      = %d\n",  x);   // 42
    printf("&x     = %p\n", &x);   // address (e.g. 0x7fff1000)
    printf("p      = %p\n",  p);   // same address as &x
    printf("*p     = %d\n", *p);   // 42 — dereference: value AT p

    *p = 99;   // modify x THROUGH the pointer
    printf("x = %d\n", x);         // 99 — x changed!

    /* ── Pointer to double ── */
    double d = 3.14;
    double *pd = &d;
    printf("sizeof(pd) = %zu\n", sizeof(pd)); // 8 on 64-bit (all pointers same size)
    printf("*pd = %.2f\n", *pd);              // 3.14
    return 0;
}
pointers.c

8.2 – Pointer Arithmetic

When you add 1 to a pointer, it advances by sizeof(type) bytes — not just 1 byte.

Pointer arithmetic on int arr[4] = {10,20,30,40};
arr[0]
10
2000
arr[1]
20
2004
arr[2]
30
2008
arr[3]
40
2012
ptr = 2000  |  ptr+1 = 2004 (+4 bytes)  |  ptr+2 = 2008 (+8 bytes)  |  *(ptr+i) ≡ arr[i]
#include <stdio.h>

int main() {
    int arr[] = {10, 20, 30, 40};
    int *ptr = arr;   // arr decays to pointer to arr[0]

    /* ── Using pointer arithmetic to traverse ── */
    for (int i = 0; i < 4; i++) {
        printf("*(ptr+%d) = %d  addr=%p\n", i, *(ptr+i), (ptr+i));
    }
    /* arr[i] and *(arr+i) are IDENTICAL */

    /* ── Pointer increment ── */
    printf("%d\n", *ptr);    // 10
    ptr++;
    printf("%d\n", *ptr);    // 20 (advanced 4 bytes)
    ptr++;
    printf("%d\n", *ptr);    // 30

    /* ── Pointer difference ── */
    int *p1 = &arr[0];
    int *p2 = &arr[3];
    printf("diff = %td\n", p2 - p1);  // 3 (number of elements)
    return 0;
}
ptr_arith.c

🔗 8.3 – Pointer–Array Equivalence

/* arr[i]  ≡  *(arr + i)
   &arr[i] ≡  arr + i
   When passed to a function, array DECAYS to pointer */

// Printing a string with a char pointer
char str[] = "Linux";
char *cp = str;
while (*cp != '\0') {
    printf("%c", *cp);
    cp++;
}
printf("\n");  // Linux

// Passing array to function: arrives as pointer
void sumArray(int *arr, int n) {   // same as int arr[]
    int s = 0;
    for (int i = 0; i < n; i++) s += arr[i];
    printf("sum = %d\n", s);
}

// String literals are read-only pointers
char *literal = "Hello";  // stored in read-only memory
// literal[0] = 'X';     // CRASH: cannot modify!
char array[] = "Hello";  // stored on stack — writable
array[0] = 'X';          // OK: "Xello"
ptr_array.c

🧩 8.4 – Double Pointers & Pointer to Pointer

int x=5;   int *p=&x;   int **pp=&p;
int x
5
0x100
p = &x
int *p
0x100
0x200
pp = &p
int **pp
0x200
0x300
int x = 5;
int *p  = &x;    // p points to x
int **pp = &p;   // pp points to p

printf("%d\n", x);    // 5
printf("%d\n", *p);   // 5
printf("%d\n", **pp);  // 5  (double dereference)

**pp = 99;
printf("%d\n", x);    // 99  (modified through **pp)

// Useful for: argv (char **argv), modifying a pointer in a function
double_ptr.c

🛡️ 8.5 – NULL Pointers & Safe Pointer Practices

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

int main() {
    /* ── NULL pointer: points to nothing ── */
    int *p = NULL;          // good practice: initialize to NULL

    /* ── Always check before dereferencing ── */
    if (p != NULL) {
        printf("%d\n", *p);  // safe
    } else {
        printf("p is NULL, cannot dereference!\n");
    }

    /* ── Dynamic allocation (preview of heap) ── */
    int *arr = (int*) malloc(5 * sizeof(int));
    if (arr == NULL) {
        fprintf(stderr, "malloc failed!\n");
        return 1;
    }
    for (int i = 0; i < 5; i++) arr[i] = i * 10;
    for (int i = 0; i < 5; i++) printf("%d ", arr[i]);
    free(arr);    // ALWAYS free! avoids memory leak
    arr = NULL;   // avoid dangling pointer
    printf("\n");
    return 0;
}
null_ptr.c
malloc / calloc / realloc / freePurpose
malloc(n)Allocate n bytes (uninitialized)
calloc(count, size)Allocate count×size bytes, zero-initialized
realloc(ptr, newsize)Resize existing allocation
free(ptr)Release back to heap (must call once per malloc)
Common pointer bugs: (1) Null dereference — always check NULL before using. (2) Dangling pointer — set to NULL after free. (3) Buffer overrun — don't go past array bounds. (4) Memory leak — always pair malloc with free.

🌐 8.5 – void *: The Generic Pointer

A void * pointer can hold the address of any data type. It is the C mechanism for generic (type-agnostic) code. You cannot dereference a void * directly — you must first cast it to a concrete type. The standard library uses void * extensively (e.g., malloc, memcpy, qsort).

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

int main() {
    int    i  = 42;
    double d  = 3.14;
    char   ch = 'A';

    void *vp;              // generic pointer — holds any address

    vp = &i;               // point at int
    printf("int via void*: %d\n",   *(int*)vp);    // cast needed

    vp = &d;               // point at double
    printf("double via void*: %.2f\n", *(double*)vp);

    vp = &ch;              // point at char
    printf("char via void*: %c\n",   *(char*)vp);

    /* malloc returns void* — you cast it to the needed type */
    int *arr = (int*) malloc(5 * sizeof(int));
    if (!arr) { perror("malloc"); return 1; }
    for (int k = 0; k < 5; k++) arr[k] = k * k;
    for (int k = 0; k < 5; k++) printf("%d ", arr[k]);  // 0 1 4 9 16
    free(arr);
    return 0;
}
void_ptr.c
void * in C does not mean "any object" in the OOP sense. It is purely a type-erased pointer. You are responsible for remembering what type it actually points to — the compiler will not check.

🔒 8.6 – const Qualifiers with Pointers

The position of const in a pointer declaration controls what is constant: the data pointed to, or the pointer itself, or both. This is one of the most commonly misunderstood aspects of C.

#include <stdio.h>

int main() {
    int x = 10, y = 20;

    /* ── 1. Pointer to const int: data is read-only ── */
    const int *p1 = &x;   // "const int" = cannot change *p1
    // *p1 = 99;          // ❌ compile error: read-only data
    p1 = &y;              // ✅ OK: can change the pointer itself
    printf("*p1 = %d\n", *p1);  // 20

    /* ── 2. Const pointer to int: pointer is fixed ── */
    int * const p2 = &x;  // * const = pointer address cannot change
    *p2 = 99;            // ✅ OK: can change the data
    printf("x = %d\n", x);    // 99
    // p2 = &y;           // ❌ compile error: const pointer

    /* ── 3. Const pointer to const int: nothing changes ── */
    const int * const p3 = &x;
    // *p3 = 50;          // ❌ data is const
    // p3 = &y;           // ❌ pointer is const
    printf("*p3 = %d\n", *p3);  // read-only view

    return 0;
}
const_pointers.c
DeclarationRead pointer p?Change *p (data)?Change p (address)?
int *p
const int *p
int * const p
const int * const p
Mnemonic — read right to left:  const int *p = "p is a pointer to int which is const".  int * const p = "p is a const pointer to int". Use const int * for function parameters that should not modify the pointed-to data (e.g., void printName(const char *name)).

🏗️ 8.7 – Dynamic Memory Allocation: malloc, calloc, realloc, free

Stack memory is automatically managed and limited in size. Heap memory is allocated at runtime with explicit functions, persists until you free it, and can be as large as available RAM (subject to OS limits). Every malloc/calloc/realloc must have a matching free.

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

int main() {
    int n;
    printf("How many integers? ");
    scanf("%d", &n);

    /* ── malloc: allocate n*4 bytes, UNINITIALISED ── */
    int *arr = (int*) malloc(n * sizeof(int));
    if (arr == NULL) { perror("malloc"); return 1; }
    for (int i = 0; i < n; i++) arr[i] = i + 1;

    /* ── calloc: allocate n*4 bytes, ZERO-INITIALISED ── */
    int *zarr = (int*) calloc(n, sizeof(int));  // count, size_each
    if (zarr == NULL) { perror("calloc"); free(arr); return 1; }
    printf("zarr[0] = %d (guaranteed 0)\n", zarr[0]);

    /* ── realloc: resize arr to hold 2*n elements ── */
    int *bigger = (int*) realloc(arr, 2 * n * sizeof(int));
    if (bigger == NULL) {
        perror("realloc");
        free(zarr); free(arr); return 1;
    }
    arr = bigger;   // realloc may move memory! always use returned ptr
    for (int i = n; i < 2*n; i++) arr[i] = (i+1) * 10;

    for (int i = 0; i < 2*n; i++) printf("%d ", arr[i]);
    printf("\n");

    /* ── free: release memory back to heap ── */
    free(arr);   arr  = NULL;   // set to NULL to avoid dangling pointer
    free(zarr);  zarr = NULL;
    return 0;
}
dynamic_memory.c
Common Dynamic Memory Bugs
BugWhat happensPrevention
Memory leakNever calling free() — heap fills upEvery malloc must have a free; use Valgrind
Double freeCalling free(p) twice — heap corruptionSet pointer to NULL after free
Use after freeAccessing memory after free() — undefined behaviourSet pointer to NULL and check before use
Buffer overrunWriting past end of allocated block — corruptionTrack allocated size, use realloc to grow
Ignoring NULL returnDereferencing NULL when malloc fails — crashAlways if (!ptr) { handle error }
Never use the original pointer after realloc: realloc may move the memory block to a new address and free the old one. Always assign the return value to a (possibly new) pointer variable and check it for NULL before updating your main pointer.

Unit 9 – Lab Exercises

  1. Write a program that uses a pointer to traverse an array of ints and prints each element and its address. Observe the 4-byte gap between addresses.
  2. Implement void swap(int *a, int *b) using pointers. Verify the swap in main.
  3. Write a function int* findMax(int *arr, int n) that returns a pointer to the maximum element in the array. Print the value and its address.
  4. Use pointer arithmetic (no array indexing []) to reverse an array in-place.
  5. Dynamically allocate an array of N integers using malloc. Fill it with squares (1, 4, 9, …). Print it. Free it properly.
  6. Write a function char* myStrcat(char *dest, const char *src) that does string concatenation using pointer arithmetic only (no <string.h> functions).

Hint: For exercise 6, advance the dest pointer to '\0' first, then copy characters from src one by one, then append '\0'.

Unit 13 – Pointers and Structures

Pointers and structures are a core pair in C systems programming. This unit focuses on struct pointers, arrow-operator access, dynamic arrays of structs, and linked data structures.

🔗 Core Equivalences

Struct member access via dot and pointer access via arrow are equivalent forms:

typedef struct {
    int id;
    char name[20];
    float cgpa;
} Student;

Student s = {101, "Asha", 8.9f};
Student *ps = &s;

s.id        == (*ps).id
ps->id      == (*ps).id
&s          == ps

🧠 Why This Matters

  • Most real C APIs pass structures through pointers for performance.
  • Used heavily in linked lists, trees, protocol stacks, and drivers.
  • Essential for dynamic records and memory-efficient data handling.

📏 13.1 – Pointer to Structure Basics

#include <stdio.h>

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

int main() {
    Student s = {101, "Ravi", 92.5f};
    Student *ps = &s;

    printf("Roll: %d\n", ps->roll);
    printf("Name: %s\n", ps->name);
    printf("Marks: %.1f\n", ps->marks);

    ps->marks = 95.0f;
    printf("Updated Marks: %.1f\n", s.marks);
    return 0;
}
  ptr_struct_basics.c

🛠️ 13.2 – Dynamic Array of Structures

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

  typedef struct {
    int id;
    char name[24];
  } Employee;

int main() {
    int n = 3;
    Employee *emp = (Employee*) malloc(n * sizeof(Employee));
    if (!emp) return 1;

    emp[0] = (Employee){1, "Asha"};
    emp[1] = (Employee){2, "Bharat"};
    emp[2] = (Employee){3, "Charan"};

    for (int i = 0; i < n; i++) {
      printf("%d %s\n", (emp + i)->id, (emp + i)->name);
    }

    free(emp);
    return 0;
}
  dynamic_struct_array.c
Safety: Always verify allocation result, and free() memory after use to avoid leaks.

Unit 13 – Lab Exercises

  1. Implement myLen(const char *s) using pointer difference only.
  2. Write myStrcmp(const char *a, const char *b) using pointer traversal.
  3. Create a struct Student and print all fields using a pointer and the -> operator.
  4. Write a function void updateSalary(Employee *e, float percent) to update struct data by reference.
  5. Dynamically allocate an array of 5 structs, fill them, display them, then free memory.
Unit 12 – Structures & Unions

Structures (struct) let you group variables of different types under one name. Unions share the same memory for all members. Together they are the foundation of complex data modelling in C.

🏷️ Aggregate Type Syntax

These topics introduce the syntax for user-defined data shapes: records with struct, shared-memory variants with union, and named integer states with enum.

struct Student { int roll; char name[20]; };
union Data { int i; float f; };
enum Day { MON = 1, TUE, WED };

struct Student s = {101, "Alice"};
s.roll = 102;
ps->roll = 103;

📖 Theory Focus

  • A struct models a real entity by grouping related fields that should exist together at the same time.
  • A union models alternative representations of the same memory, which is useful when only one interpretation is active at once.
  • Layout, alignment, and padding are not trivia here; they directly affect binary size, protocol mapping, and interoperability.

🏛️ 9.1 – Defining and Using Structures

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

/* ── Define the struct type ── */
struct Student {
    int    rollNo;
    char   name[50];
    float  cgpa;
};

/* ── typedef for convenience ── */
typedef struct {
    char  model[30];
    int   year;
    float price;
} Car;

int main() {
    /* ── Declare and initialize ── */
    struct Student s1 = {101, "Alice", 9.2};

    /* ── Access with dot operator ── */
    printf("Roll: %d  Name: %s  CGPA: %.1f\n",
           s1.rollNo, s1.name, s1.cgpa);

    /* ── Modify a member ── */
    s1.cgpa = 9.4;
    strcpy(s1.name, "Bob");

    /* ── Using typedef ── */
    Car c1 = {"Tesla Model 3", 2024, 42000.0};
    printf("%s (%d) = $%.0f\n", c1.model, c1.year, c1.price);

    /* ── Array of structs ── */
    struct Student roster[3] = {
        {101, "Alice", 9.2},
        {102, "Bob",   8.7},
        {103, "Carol", 9.5}
    };
    for (int i = 0; i < 3; i++)
        printf("%d %s %.1f\n",
               roster[i].rollNo, roster[i].name, roster[i].cgpa);
    return 0;
}
structs.c

📐 9.2 – Structure Memory Layout & Padding

The compiler inserts padding bytes between struct members to align each member to its natural alignment boundary (usually its size). This is called structure padding.

struct Padded { char a; int b; char c; };   — total 12 bytes (not 6!)
a
offset 0
b
byte 0
b
byte 1
b
byte 2
b
byte 3
c
offset 8
0
pad
pad
pad
4
5
6
7
8
pad
pad
pad
struct Packed { char a; char c; int b; }; — reordered → 8 bytes (less waste)
a
0
c
1
b
4
b
5
b
6
b
7
#include <stdio.h>
struct Padded { char a; int b; char c; };   // 12 bytes
struct Reorder { char a; char c; int b; };  // 8 bytes

int main() {
    printf("Padded  = %zu bytes\n", sizeof(struct Padded));   // 12
    printf("Reorder = %zu bytes\n", sizeof(struct Reorder));  // 8
    // Rule: order members from LARGEST to SMALLEST type to reduce padding
    return 0;
}
padding.c

➡️ 9.3 – Pointers to Structures: Arrow Operator

#include <stdio.h>

typedef struct {
    int  x, y;
    char label[10];
} Point;

void printPoint(const Point *p) {
    /* (*p).x  is the same as  p->x  */
    printf("Point %s: (%d, %d)\n", p->label, p->x, p->y);
}

void translate(Point *p, int dx, int dy) {
    p->x += dx;   // modify struct through pointer
    p->y += dy;
}

int main() {
    Point pt = {3, 7, "A"};
    printPoint(&pt);          // Point A: (3, 7)
    translate(&pt, 10, -2);
    printPoint(&pt);          // Point A: (13, 5)

    /* ── Pointer to struct heap allocation ── */
    Point *heap_pt = (Point*) malloc(sizeof(Point));
    heap_pt->x = 100;
    heap_pt->y = 200;
    strcpy(heap_pt->label, "B");
    printPoint(heap_pt);
    free(heap_pt);
    return 0;
}
struct_ptr.c
ptr->member is syntactic sugar for (*ptr).member. Always prefer the arrow notation when using pointers to structs.

🌳 9.4 – Nested Structures & Self-Referential (Linked List Node)

/* ── Nested struct ── */
typedef struct {
    int day, month, year;
} Date;

typedef struct {
    int  id;
    char name[40];
    Date joined;          // nested struct
} Employee;

/* Access: emp.joined.day */
Employee emp = {1, "Alice", {15, 6, 2020}};
printf("%s joined on %d/%d/%d\n",
       emp.name, emp.joined.day, emp.joined.month, emp.joined.year);

/* ── Self-referential: singly linked list node ── */
typedef struct Node {
    int data;
    struct Node *next;   // pointer to SAME struct type
} Node;

/* Build: head → 1 → 2 → 3 → NULL */
Node n3 = {3, NULL};
Node n2 = {2, &n3};
Node n1 = {1, &n2};
Node *head = &n1;

/* Traverse */
for (Node *cur = head; cur != NULL; cur = cur->next)
    printf("%d ", cur->data);   // 1 2 3
nested.c

🔀 9.5 – Unions: Shared Memory

All members of a union share the same memory location. The union's size equals the largest member.

union Data { int i; float f; char str[4]; }; — all share the same 4 bytes
int i (4 bytes)
float f (4 bytes)
char str[4]
All three members overlap at the same memory address. Writing one overwrites the others.
#include <stdio.h>

union Data {
    int   i;
    float f;
    char  str[4];
};

int main() {
    union Data d;
    printf("sizeof(union Data) = %zu\n", sizeof(d));  // 4

    d.i = 65;
    printf("d.i  = %d\n",  d.i);   // 65
    printf("d.f  = %f\n",  d.f);   // some float (garbage from int bits)
    printf("d.str= %c\n",  d.str[0]); // 'A' (65 = ASCII 'A')

    d.f = 3.14f;
    printf("d.f  = %.2f\n", d.f);  // 3.14
    printf("d.i  = %d\n",  d.i);   // raw bits of 3.14f as int
    // Only the LAST member written is valid!
    return 0;
}
unions.c
structunion
MemoryEach member has own storageAll members share one storage
SizeSum of all members + paddingSize of largest member
UsageStore multiple values at onceStore ONE value at a time
Typical useRecords, data modellingType tagging, memory mapping

🏷️ 9.6 – Enumerations (enum): Named Integer Constants

An enum defines a set of named integer constants that make code more readable and self-documenting. Internally, enum values are integers — the first is 0 by default, then each subsequent member is one more than the previous. You can assign explicit values.

#include <stdio.h>

/* ── Basic enum ── */
enum Day {
    MON=1, TUE, WED, THU, FRI, SAT, SUN  // TUE=2, WED=3, etc.
};

/* ── typedef for cleaner usage ── */
typedef enum {
    RED, GREEN, BLUE          // RED=0, GREEN=1, BLUE=2
} Colour;

/* ── Bitmask enum (powers of 2) ── */
typedef enum {
    PERM_NONE  = 0,   // 0000
    PERM_READ  = 1,   // 0001
    PERM_WRITE = 2,   // 0010
    PERM_EXEC  = 4    // 0100  (combine with bitwise OR)
} Permission;

int main() {
    enum Day today = WED;
    printf("Wednesday is day %d\n", today);   // 3

    Colour c = GREEN;
    switch (c) {
        case RED:   printf("Red\n");   break;
        case GREEN: printf("Green\n"); break;
        case BLUE:  printf("Blue\n");  break;
    }

    /* Bitmask: combine permissions with OR */
    Permission user = PERM_READ | PERM_WRITE;   // 0011
    if (user & PERM_READ)  printf("Can read\n");
    if (user & PERM_WRITE) printf("Can write\n");
    if (!(user & PERM_EXEC)) printf("Cannot execute\n");
    return 0;
}
enum_demo.c
enum#define constants
Type safety✅ Has a type, debugger-visible❌ Just text substitution
Scope awareness✅ Respects C scope rules❌ Global, no scope
Auto-increment✅ Each member = previous + 1❌ Must specify every value
Switch coverage check✅ -Wswitch warns on missed cases❌ No such warning

🔢 9.7 – Bit Fields in Structures

A bit field is a struct member that specifies how many bits it should occupy. Bit fields allow extremely compact data representation — useful in embedded systems, protocol headers, and flag registers where every bit has a distinct meaning.

#include <stdio.h>

/* ── Regular struct: uses 3 bytes minimum ── */
struct Flags_Regular {
    unsigned char isActive;    // 1 byte (uses only 1 bit logically)
    unsigned char isAdmin;     // 1 byte
    unsigned char isBanned;    // 1 byte
};   // sizeof = 3 bytes

/* ── Bit field struct: packs everything into 1 byte ── */
struct Flags_Bits {
    unsigned int isActive : 1;  // 1 bit  → 0 or 1
    unsigned int isAdmin  : 1;  // 1 bit
    unsigned int isBanned : 1;  // 1 bit
    unsigned int level    : 4;  // 4 bits → values 0–15
    unsigned int unused   : 1;  // 1 bit padding to fill byte
};   // sizeof = 4 bytes (one int), but only 8 bits used

/* ── Practical example: CPU flags register / network header ── */
typedef struct {
    unsigned int cf  : 1;   // Carry flag
    unsigned int     : 1;   // reserved (unnamed bit)
    unsigned int pf  : 1;   // Parity flag
    unsigned int zf  : 1;   // Zero flag
    unsigned int sf  : 1;   // Sign flag
    unsigned int of  : 1;   // Overflow flag
} CpuFlags;

int main() {
    struct Flags_Regular r;
    struct Flags_Bits    b;

    printf("Regular flags: %zu bytes\n", sizeof(r));  // 3
    printf("Bit field flags: %zu bytes\n", sizeof(b)); // 4 (one int)

    b.isActive = 1;
    b.isAdmin  = 0;
    b.isBanned = 0;
    b.level    = 7;   // 4 bits → max 15

    printf("Active=%u Admin=%u Banned=%u Level=%u\n",
           b.isActive, b.isAdmin, b.isBanned, b.level);

    /* Attempting to store out-of-range value wraps silently */
    b.level = 20;  // 20 = 10100b, 4-bit field stores 0100b = 4
    printf("Level after 20 (overflow): %u\n", b.level);  // 4
    return 0;
}
bitfields.c
Bit field limitations: (1) You cannot take the address of a bit field member (&b.level is an error). (2) The layout in memory is implementation-defined — bit fields are not portable across compilers for network protocols or binary file formats; use explicit bit masking instead. (3) Bit fields are most useful in embedded systems for hardware register mapping where the compiler and platform are known and fixed.

Unit 12 – Lab Exercises

  1. Define a struct Book with fields: title (50 chars), author (40 chars), year (int), price (float). Create an array of 3 books, fill from user input, and print them sorted by year.
  2. Write a function void printStudent(struct Student *s) that takes a pointer to struct and prints all fields using the arrow operator.
  3. Print the sizeof for at least 3 different struct layouts. Observe how member ordering affects total size.
  4. Design a simple linked list with at least 4 nodes. Write functions to: (a) print all nodes, (b) find a node by value, (c) count nodes.
  5. Create a union that can hold either a float sensor value or a char[4] sensor code. Demonstrate writing and reading each member.
Unit 14 – File Handling

File I/O in C uses the FILE* pointer and standard library functions (<stdio.h>). You can read and write both text and binary files. Always check for null FILE* and close files when done.

📄 File I/O Syntax Quick Reference

File programs follow a strict lifecycle: open, validate, read or write, then close. The main syntax difference is whether you use text functions or binary functions.

FILE *fp = fopen("notes.txt", "r");
if (!fp) { perror("fopen"); return 1; }

fgets(line, sizeof(line), fp);
fprintf(fp, "%d\n", value);
fread(&obj, sizeof(obj), 1, fp);
fclose(fp);

📖 Theory Focus

  • File handling is stream-based, which means your program works with a buffered abstraction instead of raw disk sectors.
  • Text files trade precision for readability, while binary files preserve in-memory layout more directly but are less portable across platforms.
  • Most file bugs are not syntax problems; they are missing error checks, wrong modes, bad assumptions about format, or forgotten closes.

📂 10.1 – File I/O Workflow

Open
fopen(path, mode)
returns FILE*
Check
if (fp == NULL)
handle error
Read/Write
fprintf / fscanf
fread / fwrite
fgets / fputs
Close
fclose(fp)
flushes buffer
ModeMeaningFile Exists?File Missing?
"r"Read textOpensReturns NULL
"w"Write text (overwrite)TruncatesCreates new
"a"Append textAppendsCreates new
"r+"Read + writeOpensReturns NULL
"w+"Read + write (overwrite)TruncatesCreates new
"rb"Read binaryOpensReturns NULL
"wb"Write binaryTruncatesCreates new

✏️ 10.2 – Reading and Writing Text Files

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

int main() {
    /* ════ WRITE to a text file ════ */
    FILE *fp = fopen("notes.txt", "w");
    if (fp == NULL) {
        perror("fopen failed");  // prints error reason
        return 1;
    }
    fprintf(fp, "Hello, File!\n");
    fprintf(fp, "Score: %d\n", 95);
    fputs("Another line\n", fp);
    fclose(fp);   // always close!

    /* ════ READ from a text file ════ */
    fp = fopen("notes.txt", "r");
    if (fp == NULL) { perror("fopen"); return 1; }

    char line[100];
    // Read line by line until EOF
    while (fgets(line, sizeof(line), fp) != NULL) {
        printf("%s", line);  // fgets includes '\n'
    }
    fclose(fp);

    /* ════ READ structured data with fscanf ════ */
    fp = fopen("data.txt", "r");
    if (fp) {
        int id; char name[30]; float gpa;
        while (fscanf(fp, "%d %29s %f", &id, name, &gpa) == 3) {
            printf("ID=%d Name=%s GPA=%.2f\n", id, name, gpa);
        }
        fclose(fp);
    }
    return 0;
}
fileio.c

💾 10.3 – Binary File I/O with fread/fwrite

#include <stdio.h>

typedef struct {
    int   id;
    char  name[30];
    float salary;
} Employee;

int main() {
    Employee emp1 = {1, "Alice", 75000.0};
    Employee emp2 = {2, "Bob",   68000.0};

    /* ── Write structs to binary file ── */
    FILE *fp = fopen("employees.bin", "wb");
    if (!fp) { perror("open"); return 1; }
    fwrite(&emp1, sizeof(Employee), 1, fp);  // write 1 struct
    fwrite(&emp2, sizeof(Employee), 1, fp);
    fclose(fp);

    /* ── Read structs from binary file ── */
    fp = fopen("employees.bin", "rb");
    if (!fp) { perror("open"); return 1; }
    Employee e;
    while (fread(&e, sizeof(Employee), 1, fp) == 1) {
        printf("ID=%-3d Name=%-15s Salary=%.0f\n",
               e.id, e.name, e.salary);
    }
    fclose(fp);

    /* ── File position functions ── */
    fp = fopen("employees.bin", "rb");
    fseek(fp, sizeof(Employee), SEEK_SET);   // skip to record 2
    fread(&e, sizeof(Employee), 1, fp);
    printf("Record 2: %s\n", e.name);       // Bob
    long pos = ftell(fp);                    // current position
    rewind(fp);                              // go back to start
    fclose(fp);
    return 0;
}
binary_io.c
FunctionPurposeSignature
fopenOpen fileFILE* fopen(path, mode)
fcloseClose fileint fclose(FILE*)
fprintfFormatted writefprintf(fp, fmt, ...)
fscanfFormatted readfscanf(fp, fmt, ...)
fgetsRead a linefgets(buf, n, fp)
fputsWrite a stringfputs(str, fp)
freadBinary readfread(ptr, size, count, fp)
fwriteBinary writefwrite(ptr, size, count, fp)
fseekSeek positionfseek(fp, offset, whence)
ftellCurrent positionlong ftell(FILE*)
rewindReset to startrewind(FILE*)
feofCheck end-of-fileint feof(FILE*)
ferrorCheck error flagint ferror(FILE*)
perrorPrint system errorperror(msg)
Never use feof() as a loop condition — it only becomes true after a failed read, causing off-by-one errors. Use the return value of fgets/fread/fscanf as the loop condition instead.

Unit 14 – Lab Exercises

  1. Write a program to copy one text file to another (file copy utility). Read and write line by line.
  2. Count the number of lines, words, and characters in a text file (like the Linux wc command).
  3. Create a student record management system using binary files: support add, list, and search by roll number operations. Persist data to students.bin.
  4. Write a program that reads a CSV file (name,age,score per line) and computes the average score. Use fscanf with "," delimiters or fgets + sscanf.
  5. Implement a simple append-based log file: each run appends a timestamped entry. Use time.h for the timestamp.

Hint: For timestamped log: #include <time.h>, then time_t t = time(NULL); fprintf(fp, "%s", ctime(&t));

Unit 15 – Debugging & Best Practices

Writing code that compiles is the easy part. This unit teaches you to find and fix bugs systematically using GCC warnings, GDB debugger, Valgrind memory checker, and good coding practices.

🛠️ Debugging Command Syntax

Debugging has its own operating syntax: compile with symbols, run with a debugger, inspect state, and use memory tools to confirm leaks or invalid accesses.

gcc -std=c11 -Wall -Wextra -g prog.c -o prog
gdb ./prog
break main
run
print value
backtrace
valgrind --leak-check=full ./prog

📖 Theory Focus

  • Debugging is a reasoning discipline: first classify the failure as syntax, compile-time, link-time, runtime, or logic-level.
  • Warnings matter because they expose mismatches between what you wrote and what the compiler can prove safely.
  • Tools such as GDB and Valgrind are effective only when you already have a hypothesis about state, control flow, or memory ownership.

⚠️ 11.1 – Types of Errors

Error TypeWhen DetectedToolExample
Syntax ErrorCompile timeGCCMissing ;, mismatched {}
Semantic ErrorCompile time (warnings)GCC -WallUsing uninitialised variable
Linker ErrorLink timeGCC/LDUndefined reference to function
Runtime ErrorWhile runningGDB, ValgrindSegmentation fault, divide by 0
Logic ErrorTesting/reviewGDB print, unit testsWrong formula, off-by-one loop

🛠️ 11.2 – GCC Compilation Flags

Always compile with warnings enabled. Fix every warning — they often reveal real bugs.

# Comprehensive build flags for development
gcc -std=c11 -Wall -Wextra -Wpedantic -Wshadow \
    -Wformat=2 -Wconversion -g \
    -o program program.c

# Flag meanings:
#  -std=c11     Use C11 standard
#  -Wall        Enable most warnings
#  -Wextra      Extra warnings beyond -Wall
#  -Wpedantic   Strict standard compliance
#  -Wshadow     Warn when var shadows outer scope
#  -Wformat=2   Strict printf/scanf format checks
#  -Wconversion Implicit type conversions
#  -g           Include debug symbols (for GDB)

# For release builds (optimised, no debug info):
gcc -std=c11 -O2 -o program program.c

Common GCC Warnings and Fixes

Warning / ErrorCauseFix
unused variable 'x'Declared but never usedRemove it or use it
control reaches end of non-void functionMissing return statementAdd return value;
implicit declaration of functionCalled before prototype/includeAdd prototype or include header
comparison between signed and unsignedMixing int and size_t/unsignedCast explicitly: (int)sizeof(...)
format '%d' expects int, got floatWrong printf format specifierUse %f for float/double
undefined reference to 'sqrt'Math function, missing -lmCompile with -lm flag
Segmentation faultNull/out-of-bounds dereferenceUse GDB + Valgrind to pinpoint

🔬 11.3 – GDB Debugger Workflow

Compile
gcc -g -o prog prog.c
Start GDB
gdb ./prog
Set Breakpoint
break main
break file.c:25
Run / Step
run, next, step
continue
Inspect
print x
info locals
backtrace
# ─── GDB Quick Reference ───────────────────────────
$ gcc -g -o prog prog.c       # compile with debug symbols
$ gdb ./prog                  # start debugger

# Inside GDB:
(gdb) break main              # breakpoint at start of main
(gdb) break prog.c:42         # breakpoint at line 42
(gdb) run                     # start execution
(gdb) run arg1 arg2           # run with arguments
(gdb) next                    # step over (don't enter functions)
(gdb) step                    # step into function calls
(gdb) continue                # run until next breakpoint
(gdb) finish                  # run until current function returns

(gdb) print x                 # print value of variable x
(gdb) print *ptr              # dereference and print
(gdb) print arr[0]@5          # print 5 elements starting at arr[0]
(gdb) info locals             # all local variables
(gdb) info breakpoints        # list all breakpoints
(gdb) backtrace               # bt: call stack at crash point
(gdb) list                    # show source code around current line
(gdb) watch x                 # stop when x changes
(gdb) delete 1                # remove breakpoint 1
(gdb) quit                    # exit GDB

🧹 11.4 – Valgrind: Memory Error Detector

# Run program under Valgrind (Linux/WSL)
$ valgrind --leak-check=full --show-leak-kinds=all \
           --track-origins=yes ./prog

# Valgrind detects:
#  • Memory leaks (forgot to free)
#  • Use after free (dangling pointer)
#  • Buffer overruns (array out of bounds)
#  • Use of uninitialised values
#  • Double free

# Example report excerpt:
==12345== HEAP SUMMARY:
==12345==   in use at exit: 40 bytes in 1 blocks
==12345== LEAK SUMMARY:
==12345==    definitely lost: 40 bytes in 1 blocks
==12345== ERROR SUMMARY: 1 errors from 1 contexts

11.5 – C Programming Best Practices

/* ── 1. Always initialise variables ── */
int x = 0;      // not just: int x;
int *p = NULL;  // not just: int *p;

/* ── 2. Check return values ── */
FILE *fp = fopen("f.txt", "r");
if (!fp) { perror("fopen"); return 1; }  // always check!

/* ── 3. Use constants instead of magic numbers ── */
#define MAX_SIZE    100
#define BUFFER_LEN  256

/* ── 4. Validate all input at the boundary ── */
if (index < 0 || index >= MAX_SIZE) {
    fprintf(stderr, "Error: index %d out of range\n", index);
    return -1;
}

/* ── 5. Use const for read-only parameters ── */
void printName(const char *name) { ... }

/* ── 6. Use sizeof(type) for portability ── */
int *arr = malloc(10 * sizeof(int));  // not * 4

/* ── 7. Avoid global variables when possible ── */
/* ── 8. Free memory and close handles ── */
free(ptr); ptr = NULL;
fclose(fp); fp = NULL;

/* ── 9. Break long functions into smaller ones ── */
/* ── 10. Meaningful variable names ── */
// Bad:  int a, b, c;  tmp1, tmp2
// Good: int studentCount, totalScore;

Unit 15 – Lab Exercises

  1. Write a program with an intentional segfault (null dereference). Compile with -g, run under GDB, use backtrace to locate the crash. Fix it.
  2. Write a program with a memory leak (malloc without free). Run Valgrind and observe the leak report. Fix the leak.
  3. Create a program with an off-by-one array access. Compile with -Wall -Wextra, address-sanitize with -fsanitize=address. Fix it.
  4. Debug this broken binary search implementation (wrong mid calculation, wrong termination) using GDB breakpoints and variable inspection.
  5. Set up a Makefile with targets: all (compile with -Wall -g), release (compile with -O2), clean (remove .o and binary), valgrind (run under Valgrind).

Hint: Address Sanitizer: gcc -fsanitize=address,undefined -g -o prog prog.c — detects buffer overflows and UB at runtime.

Unit 16 – Mini Projects

Integrate everything from this course into real-world programs. Each project applies multiple concepts: structs, file I/O, functions, pointers, and proper error handling.

🧪 Project Skeleton Syntax

Mini projects combine earlier syntax patterns into a repeatable structure: type definitions, helper functions, a validated menu loop, and a clean build command.

typedef struct { int id; char name[40]; } Record;

void addRecord(void);
void listRecords(void);

int main(void) {
    int choice;
    do { /* read choice, switch, call helpers */ } while (choice != 0);
}

📖 Theory Focus

  • Projects are where syntax becomes design: you stop learning isolated rules and start coordinating data, control flow, persistence, and validation.
  • A small project should still have module boundaries, reusable functions, predictable data models, and testable behaviour.
  • The goal is not just to make a demo run once; it is to build a program that survives invalid input, repeated use, and future extension.

📚 Project 1 – Student Record Management System

Concepts used: structs, arrays, file I/O (binary), functions, switch menu, input validation

Menu
Add / List
Search / Delete
Quit
Add Student
Read name,roll,cgpa
Append to .bin file
List All
fread loop
Print table
Search
fseek + fread
Match roll no.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define DB_FILE "students.bin"

typedef struct {
    int   roll;
    char  name[50];
    float cgpa;
} Student;

/* ── Add one student to the binary file ── */
void addStudent() {
    Student s;
    printf("Roll No: "); scanf("%d", &s.roll);
    printf("Name:    "); scanf("%49s", s.name);
    printf("CGPA:    "); scanf("%f", &s.cgpa);

    FILE *fp = fopen(DB_FILE, "ab");
    if (!fp) { perror("fopen"); return; }
    fwrite(&s, sizeof(Student), 1, fp);
    fclose(fp);
    printf("Student added.\n");
}

/* ── List all students from file ── */
void listStudents() {
    FILE *fp = fopen(DB_FILE, "rb");
    if (!fp) { printf("No records found.\n"); return; }
    Student s;
    printf("%-6s %-20s %s\n", "Roll", "Name", "CGPA");
    printf("-------------------------------\n");
    while (fread(&s, sizeof(Student), 1, fp) == 1)
        printf("%-6d %-20s %.2f\n", s.roll, s.name, s.cgpa);
    fclose(fp);
}

/* ── Search by roll number ── */
void searchStudent(int roll) {
    FILE *fp = fopen(DB_FILE, "rb");
    if (!fp) { printf("File not found.\n"); return; }
    Student s;
    int found = 0;
    while (fread(&s, sizeof(Student), 1, fp) == 1) {
        if (s.roll == roll) {
            printf("Found: %s | CGPA: %.2f\n", s.name, s.cgpa);
            found = 1;
        }
    }
    if (!found) printf("Roll %d not found.\n", roll);
    fclose(fp);
}

int main() {
    int choice, roll;
    do {
        printf("\n1.Add  2.List  3.Search  0.Quit\nChoice: ");
        scanf("%d", &choice);
        switch(choice) {
            case 1: addStudent(); break;
            case 2: listStudents(); break;
            case 3: printf("Search roll: "); scanf("%d", &roll);
                     searchStudent(roll); break;
        }
    } while (choice != 0);
    return 0;
}
student_db.c

🧮 Project 2 – Stack-Based Calculator (using arrays)

Concepts used: arrays as stacks, functions, switch, string tokenizing

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

#define MAX_STACK 50

double stack[MAX_STACK];
int top = -1;

void push(double v) {
    if (top >= MAX_STACK-1) { printf("Stack full!\n"); return; }
    stack[++top] = v;
}

double pop() {
    if (top < 0) { printf("Stack underflow!\n"); return 0; }
    return stack[top--];
}

/* Evaluate Reverse Polish Notation: "3 4 + 2 *" = 14 */
double evalRPN(char *expr) {
    char *tok = strtok(expr, " ");
    while (tok != NULL) {
        if (isdigit(tok[0]) || (tok[0]=='-' && strlen(tok)>1)) {
            push(atof(tok));
        } else {
            double b = pop(), a = pop();
            switch(tok[0]) {
                case '+': push(a+b); break;
                case '-': push(a-b); break;
                case '*': push(a*b); break;
                case '/':
                    if (b == 0.0) { printf("Div by zero!\n"); return 0; }
                    push(a/b); break;
            }
        }
        tok = strtok(NULL, " ");
    }
    return pop();
}

int main() {
    char expr[200];
    printf("Enter RPN expression (e.g. '3 4 + 2 *'): ");
    fgets(expr, sizeof(expr), stdin);
    printf("Result: %.4g\n", evalRPN(expr));
    return 0;
}
rpn_calc.c

📝 Project 3 – Word Frequency Counter

Concepts used: struct arrays, file I/O, string processing, sorting

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

#define MAX_WORDS   1000
#define WORD_LEN    64

typedef struct { char word[WORD_LEN]; int count; } WordEntry;

WordEntry table[MAX_WORDS];
int tableSize = 0;

/* Convert to lowercase in-place */
void toLower(char *s) {
    for(int i=0; s[i]; i++) s[i] = (char)tolower((unsigned char)s[i]);
}

/* Add word to frequency table */
void addWord(const char *w) {
    for(int i=0; i<tableSize; i++) {
        if (strcmp(table[i].word, w)==0) { table[i].count++; return; }
    }
    if (tableSize < MAX_WORDS) {
        strncpy(table[tableSize].word, w, WORD_LEN-1);
        table[tableSize].count = 1;
        tableSize++;
    }
}

/* Sort by frequency descending (bubble sort) */
void sortByFreq() {
    for(int i=0; i<tableSize-1; i++)
        for(int j=0; j<tableSize-i-1; j++)
            if (table[j].count < table[j+1].count) {
                WordEntry tmp = table[j]; table[j]=table[j+1]; table[j+1]=tmp;
            }
}

int main(int argc, char **argv) {
    if (argc < 2) { printf("Usage: ./wordfreq <file>\n"); return 1; }
  FILE *fp = fopen(*(argv + 1), "r");
    if (!fp) { perror("fopen"); return 1; }
    char w[WORD_LEN];
    while (fscanf(fp, "%63s", w) == 1) {
        /* strip punctuation from end */
        int len = (int)strlen(w);
        while (len > 0 && !isalnum((unsigned char)w[len-1])) w[--len]='\0';
        if (len > 0) { toLower(w); addWord(w); }
    }
    fclose(fp);
    sortByFreq();
    printf("Top words in %s:\n", argv[1]);
    int limit = tableSize < 20 ? tableSize : 20;
    for(int i=0; i<limit; i++)
        printf("%4d  %s\n", table[i].count, table[i].word);
    return 0;
}
wordfreq.c – compile: gcc -Wall -o wordfreq wordfreq.c && ./wordfreq myfile.txt

🎓 Capstone Challenge Ideas

  • Bank Account System — structs, linked list, file persistence, menu-driven
  • Sorting Visualizer — implement bubble/selection/insertion/merge sort with step-by-step print output; time with clock()
  • Simple Shell — read commands with fgets, tokenize with strtok, execute with execvp, fork processes
  • Matrix Calculator — add, sub, mul, transpose, determinant for n×n matrices using dynamic allocation
  • Text Editor (Vim-lite) — read/write files, line-based editing, search/replace using C string functions

Unit 16 – Submission Checklist

  • ✅ Program compiles with gcc -Wall -Wextra -std=c11 and zero warnings
  • ✅ All malloc calls are paired with free; Valgrind shows 0 leaks
  • ✅ All file operations check for NULL FILE*
  • ✅ No buffer overflows: all string reads are bounded (%49s, fgets)
  • ✅ Functions are small; each does ONE thing well
  • ✅ Code has meaningful variable/function names
  • ✅ Tested with edge cases: empty input, very large input, invalid input
  • ✅ Submitted with a Makefile
Unit 11 – Functions with Pointers

Functions and pointers are the two most powerful features of C. When combined, they unlock pass-by-reference, multiple return values, callbacks, and function dispatch tables. This unit covers everything from pointer parameters to advanced function pointers, with diagrams, working code, and 10 practical assignments.

🔁 Pointer Function Syntax

This topic extends normal function syntax by making parameters and even the functions themselves addressable values.

void swap(int *a, int *b);
int (*op)(int, int) = add;

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

result = op(3, 4);
swap(&x, &y);

📖 Theory Focus

  • Pointer parameters are how C expresses mutation, out-parameters, and efficient passing of large objects.
  • Function pointers turn behaviour into data, which enables callback APIs, lookup tables, event dispatch, and configurable algorithms.
  • This topic matters because it bridges basic procedural programming and systems-style API design where data and behaviour are both passed around explicitly.

🧠 13.1 – How Function Calls Work: Value vs Pointer

When you call a function in C, arguments are copied onto the stack. Changes inside the function do not affect the original variable — unless you pass a pointer to it.

PASS BY VALUE
int x = 10;
double(x);      // passes COPY
printf("%d", x); // still 10!

void double(int n) {
    n *= 2;  // local copy only
}
PASS BY POINTER
int x = 10;
doubleIt(&x);   // passes ADDRESS
printf("%d", x); // now 20 ✓

void doubleIt(int *n) {
    *n *= 2; // dereference → actual x
}
Memory layout during doubleIt(&x):
caller's stack frame
x
20
0x1004
doubleIt() stack frame
n (ptr)
0x1004
points to x
*n=2×(*n)
effect on caller
x
20
CHANGED ✓
#include <stdio.h>

/* By value – caller unchanged */
void byValue(int n) {
    n = n * 2;
    printf("  inside byValue: n=%d\n", n);
}

/* By pointer – modifies caller's variable */
void byPointer(int *p) {
    *p = *p * 2;
    printf("  inside byPointer: *p=%d\n", *p);
}

int main() {
    int a = 5, b = 5;

    printf("--- Pass by Value ---\n");
    byValue(a);
    printf("  caller a = %d  (unchanged)\n", a);   // 5

    printf("--- Pass by Pointer ---\n");
    byPointer(&b);
    printf("  caller b = %d  (CHANGED)\n", b);      // 10
    return 0;
}
value_vs_pointer.c

🔄 13.2 – Passing Pointers to Functions

Pointer parameters are the standard C technique for: (1) modifying caller variables, (2) returning multiple values from one function, and (3) efficiently passing large structs without copying them.

#include <stdio.h>

/* ── Classic swap using pointers ── */
void swap(int *a, int *b) {
    int temp = *a;  // save value at address a
    *a = *b;         // write value of b into a's location
    *b = temp;       // write saved a into b's location
}

/* ── Multiple return values via pointers ── */
void minMax(int *arr, int n, int *min, int *max) {
  *min = *max = *(arr + 0);
    for (int i = 1; i < n; i++) {
    if (*(arr + i) < *min) *min = *(arr + i);
    if (*(arr + i) > *max) *max = *(arr + i);
    }
}

/* ── Divmod: quotient + remainder together ── */
void divmod(int a, int b, int *quot, int *rem) {
    *quot = a / b;
    *rem  = a % b;
}

/* ── Passing large struct by pointer (no copy) ── */
typedef struct { char name[50]; int score; double gpa; } Student;

void printStudent(const Student *s) {   // const: read-only pointer
    printf("Name: %s  Score: %d  GPA: %.2f\n", s->name, s->score, s->gpa);
}

int main() {
    /* Swap */
    int x = 10, y = 20;
    printf("Before swap: x=%d y=%d\n", x, y);
    swap(&x, &y);
    printf("After  swap: x=%d y=%d\n", x, y);    // x=20 y=10

    /* Min/Max */
    int data[] = {34, 7, 89, 12, 55};
    int lo, hi;
    minMax(data, 5, &lo, &hi);
    printf("Min=%d  Max=%d\n", lo, hi);           // 7  89

    /* Divmod */
    int q, r;
    divmod(17, 5, &q, &r);
    printf("17 / 5 = %d remainder %d\n", q, r); // 3 r 2

    /* Struct by pointer */
    Student s = {"Alice", 92, 3.8};
    printStudent(&s);
    return 0;
}
pointer_params.c
Use const T *p when you pass a pointer for reading only. This prevents accidental modification and communicates intent clearly to other programmers.

📋 13.3 – Arrays as Pointer Parameters

In C, an array name decays to a pointer to its first element when passed to a function. The function receives a pointer, not a copy of the array, so it can modify the original array's elements.

#include <stdio.h>

/* Both declarations are IDENTICAL — array notation is syntactic sugar */
void fillArray(int *arr, int n);          // pointer style
void printArray(int *arr, int n);          // pointer style
void reverseArray(int *arr, int n);
int  sumArray(const int *arr, int n);     // const: won't modify

void fillArray(int *arr, int n) {
    for (int i = 0; i < n; i++)
        arr[i] = (i + 1) * 10;   // arr[i] identical to *(arr+i)
}

void printArray(int *arr, int n) {
    for (int i = 0; i < n; i++)
    printf("%d ", *(arr + i));
    printf("\n");
}

void reverseArray(int *arr, int n) {
    int *lo = arr, *hi = arr + n - 1;  // pointer-based two-pointer technique
    while (lo < hi) {
        int tmp = *lo; *lo = *hi; *hi = tmp;
        lo++; hi--;
    }
}

int sumArray(const int *arr, int n) {
    int sum = 0;
    // Pointer arithmetic traversal (no index variable needed)
    for (const int *p = arr; p < arr + n; p++)
        sum += *p;
    return sum;
}

int main() {
    int a[5];
    fillArray(a, 5);
    printf("Filled  : "); printArray(a, 5);    // 10 20 30 40 50
    printf("Sum     : %d\n", sumArray(a, 5));  // 150
    reverseArray(a, 5);
    printf("Reversed: "); printArray(a, 5);    // 50 40 30 20 10
    return 0;
}
array_pointers.c
sizeof(arr) inside a function gives the size of the pointer, not the array. Always pass the array length as a separate parameter.

↩️ 13.4 – Returning Pointers from Functions

A function can return a pointer — but you must ensure the memory it points to is still valid after the function returns. There are three safe strategies and one common fatal mistake.

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

/* ══ SAFE 1: Return pointer to STATIC variable ══
   static variables live for entire program lifetime */
const char* getStatus(int code) {
    static const char *msgs[] = {"OK", "Error", "Timeout"};
    if (code < 0 || code > 2) return "Unknown";
    return msgs[code];  // safe: static storage
}

/* ══ SAFE 2: Return pointer to caller-provided buffer ══ */
char* buildGreeting(const char *name, char *buf, int bufLen) {
    snprintf(buf, bufLen, "Hello, %s!", name);
    return buf;   // buf lives in caller's frame — safe
}

/* ══ SAFE 3: Return heap-allocated memory ══
   Caller MUST call free() when done */
int* makeRange(int start, int end) {
    int n = end - start + 1;
    int *arr = (int*)malloc(n * sizeof(int));
    if (!arr) return NULL;
    for (int i = 0; i < n; i++) arr[i] = start + i;
    return arr;   // caller must free(arr)
}

/* ══ DANGER: Return pointer to LOCAL variable ══
   LOCAL variables are destroyed when function returns!
   This creates a DANGLING POINTER — undefined behavior! */
int* dangerousFunc() {
    int local = 42;
    return &local;  // ⚠️ NEVER DO THIS! local no longer exists after return
}

int main() {
    /* Static */
    printf("Status 0: %s\n", getStatus(0));     // OK
    printf("Status 1: %s\n", getStatus(1));     // Error

    /* Caller buffer */
    char greeting[64];
    printf("%s\n", buildGreeting("Alice", greeting, sizeof(greeting)));

    /* Heap */
    int *range = makeRange(1, 5);
    if (range) {
        for (int i = 0; i < 5; i++) printf("%d ", range[i]);
        printf("\n");
        free(range);   // MUST free!
    }
    return 0;
}
returning_pointers.c
Dangling Pointer: Never return a pointer to a local variable. Once the function returns, the local variable's stack memory is reclaimed. Accessing it is undefined behavior — your program may crash, produce garbage, or appear to work (until it doesn't).

📌 13.5 – Function Pointers

A function pointer stores the address of a function. It allows you to select and call different functions at runtime — enabling callbacks, plugin systems, and dispatch tables.

Anatomy of a function pointer declaration:
int (*funcPtr) (int, int);
▲ return type
▲ pointer name (note the *)
▲ parameter types
#include <stdio.h>

/* Four functions with the same signature: int(int, int) */
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int divSafe(int a, int b) { return b ? a / b : 0; }

/* typedef for cleaner syntax */
typedef int (*BinaryOp)(int, int);

/* Dispatch table: array of function pointers */
BinaryOp ops[] = { add, sub, mul, divSafe };
const char *opNames[] = { "add", "sub", "mul", "div" };

int main() {
    /* ── Direct function pointer usage ── */
    BinaryOp fp = add;                 // assign function address
    printf("add(3,4) = %d\n", fp(3, 4));  // call via pointer: 7

    fp = mul;
    printf("mul(3,4) = %d\n", fp(3, 4));  // 12

    /* ── Dispatch table: select operation at runtime ── */
    printf("\nDispatch table (a=10, b=3):\n");
    for (int i = 0; i < 4; i++)
        printf("  %s(10,3) = %d\n", opNames[i], ops[i](10, 3));

    /* ── NULL check before calling ── */
    BinaryOp op = NULL;
    if (op != NULL)
        op(1, 2);
    else
        printf("\nFunction pointer is NULL — safe check passed\n");

    /* ── Explicit dereference syntax (both work) ── */
    printf("Direct call  : %d\n", (*add)(5, 6));   // old style
    printf("Modern call  : %d\n", add(5, 6));       // preferred
    return 0;
}
function_pointers.c
SyntaxMeaning
int (*fp)(int, int)Declare function pointer fp
fp = addAssign: fp now points to add
fp(3, 4)Call the function through fp
typedef int (*BinaryOp)(int,int)Create type alias for cleaner code
BinaryOp ops[] = {add, sub}Array of function pointers (dispatch table)

🔁 13.6 – Callbacks (Functions as Arguments)

A callback is a function pointer passed as an argument to another function. The receiving function calls it at the right time. This is the foundation of event systems, sorting, and generic algorithms.

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

/* ── Custom forEach: applies callback to every element ── */
void forEach(int *arr, int n, void (*callback)(int)) {
    for (int i = 0; i < n; i++)
        callback(arr[i]);
}

/* ── Custom map: transform each element using a function ── */
void mapArray(int *arr, int n, int (*transform)(int)) {
    for (int i = 0; i < n; i++)
        arr[i] = transform(arr[i]);
}

/* Callback functions to pass */
void printOne(int x)  { printf("%d ", x); }
int  square(int x)    { return x * x; }
int  negate(int x)    { return -x; }

/* ── qsort from <stdlib.h> — the standard library callback ── */
int ascending(const void *a, const void *b) {
    return (*(int*)a) - (*(int*)b);   // < 0 if a < b
}
int descending(const void *a, const void *b) {
    return (*(int*)b) - (*(int*)a);
}

int main() {
    int data[] = {3, 1, 4, 1, 5, 9, 2, 6};
    int n = sizeof(data) / sizeof(data[0]);

    /* forEach with printOne */
    printf("Original: "); forEach(data, n, printOne); printf("\n");

    /* map: square all elements */
    mapArray(data, n, square);
    printf("Squared : "); forEach(data, n, printOne); printf("\n");

    /* qsort ascending */
    int nums[] = {64, 25, 12, 90, 5};
    qsort(nums, 5, sizeof(int), ascending);
    printf("qsort ASC: "); forEach(nums, 5, printOne); printf("\n");

    /* qsort descending — just swap the comparator callback! */
    qsort(nums, 5, sizeof(int), descending);
    printf("qsort DESC: "); forEach(nums, 5, printOne); printf("\n");
    return 0;
}
callbacks.c
qsort signature: void qsort(void *base, size_t n, size_t size, int (*cmp)(const void*, const void*)). Your comparator must return negative, zero, or positive — not just 0/1.

🔗 13.7 – Double Pointers (Pointer to Pointer)

A double pointer (int **pp) is a pointer that holds the address of another pointer. It is needed when a function must modify a pointer in the caller's scope — for example, allocating a list or changing where a pointer points.

Memory chain: int **pp → int *p → int value
pp
addr of p
int **
p
addr of val
int *
val
42
int
*pp → dereferences to p | **pp → dereferences to 42
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* ── Use case 1: allocate inside a function ── */
void allocate(int **pp, int value) {
    *pp = (int*)malloc(sizeof(int));   // write new address into caller's pointer
    if (*pp) **pp = value;
}

/* ── Use case 2: reassign a pointer in the caller ── */
void resetToNull(int **pp) {
    free(*pp);
    *pp = NULL;   // set caller's pointer to NULL after freeing
}

/* ── Use case 3: argv is char** (array of char pointers) ── */
void printArgs(int argc, char **argv) {
    for (int i = 0; i < argc; i++)
        printf("  argv[%d] = %s\n", i, argv[i]);
}

/* ── Use case 4: linked list node insertion via ** ── */
typedef struct Node { int val; struct Node *next; } Node;

void prepend(Node **head, int val) {
    Node *n = (Node*)malloc(sizeof(Node));
    n->val = val;
    n->next = *head;   // new node points to old head
    *head = n;          // update caller's head pointer
}

int main() {
    /* Allocate */
    int *p = NULL;
    allocate(&p, 42);
    if (p) { printf("Allocated: *p = %d\n", *p); }

    resetToNull(&p);
    printf("After reset: p = %s\n", p ? "non-NULL" : "NULL");

    /* Linked list via ** */
    Node *head = NULL;
    prepend(&head, 30);
    prepend(&head, 20);
    prepend(&head, 10);
    printf("List: ");
    for (Node *cur = head; cur; cur = cur->next)
        printf("%d ", cur->val);   // 10 20 30
    printf("\n");

    /* Free list */
    while (head) { Node *tmp = head; head = head->next; free(tmp); }
    return 0;
}
double_pointers.c

🔒 13.8 – const with Pointer Parameters

The const keyword combined with pointers has four combinations. Understanding them helps you write safer APIs where functions clearly document whether they read or modify their arguments.

DeclarationPointer changeable?Value changeable?Use case
int *pYesYesGeneral in/out parameter
const int *pYesNoRead-only access to data
int * const pNoYesFixed address, modifiable value
const int * const pNoNoFully immutable
#include <stdio.h>

/* Read-only: cannot change *p, can change p itself */
void printValue(const int *p) {
    printf("Value: %d\n", *p);
    // *p = 99;  ← COMPILE ERROR: read-only
    // p = NULL; ← OK: pointer itself can change (local copy)
}

/* Fixed pointer: cannot reassign p, but *p is modifiable */
void increment(int * const p) {
    (*p)++;          // OK: modifying the value
    // p = NULL;  ← COMPILE ERROR: pointer is const
}

/* Fully const: ideal for string processing functions */
int countChar(const char * const s, char c) {
    int count = 0;
    for (int i = 0; s[i] != '\0'; i++)
        if (s[i] == c) count++;
    return count;
}

int main() {
    int x = 10;
    printValue(&x);    // Value: 10
    increment(&x);     // x becomes 11
    printValue(&x);    // Value: 11
    printf("'l' count in 'hello': %d\n", countChar("hello", 'l')); // 2

    /* const pointer to int (fixed address) */
    int val = 5;
    int * const fixed = &val;
    *fixed = 99;       // OK — value changed
    printf("val = %d\n", val);  // 99
    return 0;
}
const_pointers.c
Memory trick: Read the declaration right-to-left. const int *p → "p is a pointer to an int that is const". int * const p → "p is a const pointer to an int".

🏗️ 13.9 – Comprehensive Example: Generic Sort & Search

This final example combines function pointers, callbacks, pointer parameters, and const to build a small but complete generic utility library — the kind of code found in real C systems.

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

/* ─── Types ─────────────────────────────────────── */
typedef int  (*Comparator)(const void*, const void*);
typedef void (*Printer)(const void*);

typedef struct {
    char name[32];
    int  age;
    double gpa;
} Student;

/* ─── Comparators ────────────────────────────────── */
int cmpByName(const void *a, const void *b) {
    return strcmp(((const Student*)a)->name,
                   ((const Student*)b)->name);
}
int cmpByAge(const void *a, const void *b) {
    return ((const Student*)a)->age - ((const Student*)b)->age;
}
int cmpByGPA(const void *a, const void *b) {
    double diff = ((const Student*)b)->gpa - ((const Student*)a)->gpa;
    return (diff > 0) - (diff < 0);   // safe double comparison
}

/* ─── Printer ────────────────────────────────────── */
void printStudent(const void *p) {
    const Student *s = (const Student*)p;
    printf("  %-12s age=%2d gpa=%.1f\n", s->name, s->age, s->gpa);
}

/* ─── Generic linear search (returns index or -1) ── */
int linearSearch(const void *base, int n, size_t elemSize,
                  const void *target, Comparator cmp) {
    for (int i = 0; i < n; i++) {
        const char *elem = (const char*)base + i * elemSize;
        if (cmp(elem, target) == 0) return i;
    }
    return -1;
}

/* ─── Print all with header ─────────────────────── */
void printAll(const void *base, int n, size_t elemSize, Printer pr) {
    for (int i = 0; i < n; i++)
        pr((const char*)base + i * elemSize);
}

int main() {
    Student students[] = {
        {"Charlie",  22, 3.5},
        {"Alice",    20, 3.9},
        {"Bob",      21, 3.2},
        {"Diana",    23, 3.8},
    };
    int n = sizeof(students) / sizeof(students[0]);

    /* Sort by name */
    qsort(students, n, sizeof(Student), cmpByName);
    printf("Sorted by Name:\n");
    printAll(students, n, sizeof(Student), printStudent);

    /* Sort by GPA (descending) */
    qsort(students, n, sizeof(Student), cmpByGPA);
    printf("Sorted by GPA (highest first):\n");
    printAll(students, n, sizeof(Student), printStudent);

    /* Linear search by name */
    Student target = {"Bob", 0, 0.0};
    int idx = linearSearch(students, n, sizeof(Student), &target, cmpByName);
    printf("Search 'Bob': index %d\n", idx);
    return 0;
}
generic_sort_search.c

📝 Unit 11 – 10 Assignments: Functions with Pointers

  1. Swap Three Values: Write a function void swap3(int *a, int *b, int *c) that rotates three values left: after the call, a gets b's value, b gets c's value, and c gets a's original value. Call it in main and print before and after. Demonstrate that pass-by-value would not work here.
  2. Multiple Return Values: Write a function void statistics(int *arr, int n, int *min, int *max, double *avg) that computes the minimum, maximum, and average of an array in one pass. Return all three values via pointer parameters. Test with at least two different arrays and print all three statistics for each.
  3. Array Operations Library: Write four functions — all accepting int *arr and int n:
    (a) void scaleArray(int *arr, int n, int factor) — multiply every element by factor
    (b) void shiftArray(int *arr, int n, int offset) — add offset to every element
    (c) int countPositive(const int *arr, int n) — count elements > 0
    (d) void copyArray(const int *src, int *dst, int n) — copy src to dst
    In main, test all four on the same array, printing results at each stage.
  4. Dangling Pointer Demo: Write a function that returns a pointer to a local variable. Compile with gcc -Wall and note the warning. In a comment, explain what happens in memory when the function returns. Then rewrite the function correctly using static storage or a caller-provided buffer, and verify the fix works.
  5. Function Pointer Calculator: Define five functions: add, subtract, multiply, safeDivide, modulo — all with signature int(int,int). Store them in an array of function pointers. Write a menu-driven calculator loop: read two integers and an operator choice (1–5), look up the correct function pointer in the array, and call it. Continue until the user enters 0 to quit.
  6. Custom qsort Comparators: Declare an array of 8 structs: typedef struct { char name[32]; int score; } Player; Write three comparators: by name (alphabetical), by score ascending, by score descending. Sort and print the array three times using qsort with each comparator. The output should show three different orderings.
  7. Callback forEach and map: Implement two higher-order functions:
    (a) void forEach(int *arr, int n, void (*action)(int*)) — calls action on a pointer to each element (allows modification)
    (b) int reduce(const int *arr, int n, int init, int (*combiner)(int, int)) — folds the array with a combiner function
    Write callback functions doubleInPlace, addOne, sumTwo, maxTwo. Use them to double an array, increment every element, compute total sum, and find the maximum.
  8. Double Pointer Allocation: Write a function void allocMatrix(int ***mat, int rows, int cols) that allocates a 2D matrix using a double pointer (int ** for the matrix). Write a companion void freeMatrix(int ***mat, int rows). In main, allocate a 3×3 matrix, fill it with row*10+col, print it as a grid, then free it and set the pointer to NULL.
  9. Linked List with Double Pointers: Implement a singly linked list using Node **head as the parameter type for all modifying operations. Write:
    (a) void pushFront(Node **head, int val)
    (b) void pushBack(Node **head, int val)
    (c) int popFront(Node **head) — removes and returns front value
    (d) void printList(const Node *head)
    Build a list of 5 elements, print it, pop 2 from front, push 3 to back, print again. Free all nodes.
  10. const Correctness Audit: Write a struct typedef struct { char title[64]; int pages; double price; } Book; and four functions:
    (a) void printBook(const Book *b) — read-only
    (b) void applyDiscount(Book *b, double pct) — modifies price
    (c) void copyBook(const Book *src, Book *dst) — reads src, writes dst
    (d) int comparePrice(const Book *a, const Book *b) — returns -1, 0, 1
    Ensure every pointer parameter uses the correct const qualification. Try removing a const and verifying that the compiler produces an error when you attempt to modify a const-qualified value.