+91-91760-33446
Free Tutorial
C Programming on Linux
A complete, hands-on course from environment setup to advanced C — with diagrams, code examples & 130+ lab exercises.
14 Units
80+ Code Examples
30+ Diagrams
130+ Lab Exercises
Linux · GCC Platform
C Programming – Linux Platform Overview
Welcome to C Programming on Linux

A complete, hands-on course covering everything from setting up your Linux environment to writing system-level C programs. Each unit contains theory, memory diagrams, flowcharts, code examples, and lab exercises.

📦
13
Units
💻
80+
Code Examples
📊
30+
Diagrams
🧪
130+
Lab Exercises
🐧
Linux
Platform (GCC)

📋 Course Outline

UnitTopicKey Concepts
1Linux Dev EnvironmentGCC, terminal, file system, Linux commands
2Vim EditorModes, navigation, editing, workflow
3C Program StructureCompilation pipeline, memory layout, hello world
4Variables, Types & OperatorsData types, memory sizes, operators, type casting
5Control Statementsif/else, switch, for, while, do-while, patterns
6FunctionsDeclaration, scope, recursion, call stack
7Arrays & Strings1D/2D arrays, string.h, memory layout
8PointersAddress, dereference, arithmetic, arrays
9Structures & Unionsstruct, union, padding, typedef
10File Handlingfopen, fread, fwrite, binary files
11Debugging & Best PracticesGCC warnings, GDB, Valgrind
12Mini ProjectsStudent system, calculator, file records
Tip: Use the sidebar to jump to any unit. Each unit ends with lab exercises — complete them before moving on.
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.

🐧 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
lsList directory contentsls -la (hidden files, details)
cdChange directorycd ~/projects, cd ..
mkdirCreate directorymkdir -p src/utils
rmdirRemove empty dirrmdir empty_folder
FilescpCopy filescp file.c backup.c
mvMove / renamemv old.c new.c
rmRemove filesrm file.c, rm -rf dir/
touchCreate empty filetouch main.c
ViewcatDisplay filecat main.c
lessPage through fileless longfile.c (q to quit)
headFirst N lineshead -20 main.c
tailLast N linestail -20 main.c
PermissionschmodChange permissionschmod +x script.sh, chmod 755 prog
chownChange ownerchown user:group file
ls -lView permissions-rwxr-xr-- 1 user group ...
Processesps auxShow all processesps aux | grep gcc
topLive process monitortop (q to quit)
killTerminate processkill 1234 (use PID)
SearchgrepSearch in filesgrep "printf" main.c
findFind filesfind . -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.

🔄 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.

🏗️ 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.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 – Variables, Data Types & Operators

Variables are named memory locations. This unit covers how different data types occupy different amounts of memory, how to declare and use variables, and how operators combine values into expressions.

🧮 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

📦 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).

📐 4.3 – 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

🔄 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

🖊️ 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
= 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!

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
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.

⚖️ 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
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.

🔢 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
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).

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
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.

🔗 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 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)

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

📊 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!).

📥 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.

📝 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.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.3 – for Loop

📋 Syntax

for (initialisation; condition; update) {
    // body -- executes while condition is true
}

// Execution order each iteration:
// 1. initialisation  (runs ONCE, before loop starts)
// 2. condition check (if FALSE exit loop immediately)
// 3. body execution
// 4. update expression
// 5. back to step 2

// Example:
for (int i = 0; i < 5; i++)
    printf("%d\n", i);  // prints 0 1 2 3 4

Any of the three parts can be omitted. Multi-variable: for(int i=0,j=10; i<j; i++,j--)

START Init: i = 0 i < n ? condition YES Loop Body Update: i++ NO END
// for (init ; condition ; update)
for (int i = 0; i < 5; i++) {
    printf("%d ", i);  // 0 1 2 3 4
}

// Countdown
for (int i = 5; i >= 1; i--) {
    printf("%d ", i);  // 5 4 3 2 1
}

// Sum 1 to N
int sum = 0;
for (int i = 1; i <= 10; i++) {
    sum += i;
}
printf("Sum = %d\n", sum);  // 55

// Multiplication table for N
int n = 5;
for (int i = 1; i <= 10; i++) {
    printf("%d x %d = %d\n", n, i, n*i);
}
for_loops.c

🔄 5.4 – while & do-while Loops

📋 while Syntax

while (condition) {
    // body
    // must eventually make condition false!
}
// Checked BEFORE every iteration.
// Body never runs if condition starts false.

📋 do-while Syntax

do {
    // body -- runs AT LEAST ONCE
} while (condition);  // note semicolon
// Condition checked AFTER body.
// Body always executes at least once.

while: checks condition FIRST

START condition true Body false END
int i = 1;
while (i <= 5) {
    printf("%d ", i);
    i++;
}
// 1 2 3 4 5
while.c

do-while: executes body FIRST

START Body (always runs) condition true false END
int i = 1;
do {
    printf("%d ", i);
    i++;
} while (i <= 5);
// 1 2 3 4 5

// Key: body runs ONCE even
// if condition starts false
int x = 10;
do {
    printf("runs once!\n");
} while (x < 5);
do_while.c

🚦 5.5 – break, continue, and Nested Loops

📋 break Syntax

for/while/do (...) {
    if (exit_condition)
        break;  // exits INNERMOST loop
}
// Also exits a switch block
// Does NOT exit outer loops

📋 continue Syntax

for/while/do (...) {
    if (skip_condition)
        continue; // skip rest of body
    // skipped when continue fires
}
// for: goes to update, then condition
// while/do-while: goes to condition
#include <stdio.h>

int main() {
    /* ── break: exit loop immediately ── */
    for (int i = 0; i < 10; i++) {
        if (i == 5) break;
        printf("%d ", i);           // 0 1 2 3 4
    }
    printf("\n");

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

    /* ── Nested loops: right-angle triangle pattern ── */
    for (int row = 1; row <= 5; row++) {
        for (int col = 1; col <= row; col++) {
            printf("* ");
        }
        printf("\n");
    }
    /* Output:
       *
       * *
       * * *
       * * * *
       * * * * *  */

    /* ── Number pyramid ── */
    for (int i = 1; i <= 5; i++) {
        for (int j = 1; j <= i; j++)
            printf("%d ", j);
        printf("\n");
    }
    /* Output:
       1
       1 2
       1 2 3
       1 2 3 4
       1 2 3 4 5  */
    return 0;
}
patterns.c

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. Print the multiplication table (1–10) for any number entered by the user using a for loop.
  4. Use a while loop to find the sum of digits of a number (e.g., 1234 → 1+2+3+4 = 10).
  5. Check if a number is prime using a for loop: test divisibility from 2 to √n.
  6. Print the following patterns using nested loops:
    (a) Right-angle triangle of stars (5 rows)
    (b) Inverted triangle of numbers
    (c) Diamond pattern (bonus)
  7. Use a do-while loop to implement a guessing game: computer picks 42, user keeps guessing until correct. Print "Too High", "Too Low", or "Correct!"

Hint: For prime check, sqrt(n) requires #include <math.h>. Compile with: gcc prog.c -o prog -lm

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

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 7 – 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.

🏗️ 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)

Unit 7 – 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 8 – 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.

📦 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 – 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.

Unit 8 – 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.

🎯 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.

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 10 – 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.

🏛️ 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

Unit 10 – 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 11 – 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.

📂 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 11 – 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 12 – 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.

⚠️ 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 12 – 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 13 – 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 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 13 – 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 14 – 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.

🧠 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);        // array style (same thing!)
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 14 – 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.