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 PlatformOverview
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
Unit
Topic
Key Concepts
1
Linux Dev Environment
GCC, terminal, file system, Linux commands
2
Vim Editor
Modes, navigation, editing, workflow
3
C Program Structure
Compilation pipeline, memory layout, hello world
4
Variables, Types & Operators
Data types, memory sizes, operators, type casting
5
Control Statements
if/else, switch, for, while, do-while, patterns
6
Functions
Declaration, scope, recursion, call stack
7
Arrays & Strings
1D/2D arrays, string.h, memory layout
8
Pointers
Address, dereference, arithmetic, arrays
9
Structures & Unions
struct, union, padding, typedef
10
File Handling
fopen, fread, fwrite, binary files
11
Debugging & Best Practices
GCC warnings, GDB, Valgrind
12
Mini Projects
Student 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:
Distribution
Package Manager
Best For
Install GCC
Ubuntu
apt
Beginners, desktops
sudo apt install gcc
Fedora
dnf
Developers, cutting edge
sudo dnf install gcc
Debian
apt
Stability, servers
sudo apt install gcc
Arch Linux
pacman
Advanced users
sudo pacman -S gcc
CentOS/RHEL
yum/dnf
Enterprise servers
sudo 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):
# 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)
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?
Create the directory structure: ~/c-course/unit1/. Inside, create 3 empty files: hello.c, notes.txt, test.c. Use only terminal commands.
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.
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.
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.
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.
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.
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.
Run man gcc and search for the -Wall option (press / then type -Wall). Write down what -Wall, -Wextra, and -Werror do.
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)
Action
Key(s)
Action
h j k l
Left / Down / Up / Right
w / b
Next / previous word
0 / $
Start / end of line
gg / G
Top / bottom of file
:n
Go to line n (e.g. :25)
Ctrl+d / Ctrl+u
Scroll half page down/up
%
Jump to matching bracket
*
Search word under cursor
✏️ 2.3 – Editing Commands (Normal Mode)
Command
Action
dd
Delete (cut) current line
yy
Yank (copy) current line
p / P
Paste after / before cursor
u
Undo
Ctrl+r
Redo
x
Delete character under cursor
r
Replace one character
cw
Change word (delete word and enter insert mode)
D
Delete from cursor to end of line
=G
Auto-indent entire file from cursor to end
/pattern
Search forward for pattern
:%s/old/new/g
Replace 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 filenameVim 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>intmain() {
printf("Hello, Linux C!\n");
return0;
}
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)
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 segmentfloat ratio; // uninitialized → BSS segment (0)/*─────────────────────────────────────────────
SECTION 3: Function Prototypes (declarations)
Tell the compiler the function exists before it's defined.
─────────────────────────────────────────────*/voidgreet(char *name);
intadd(int a, int b);
/*─────────────────────────────────────────────
SECTION 4: main() – Entry Point
Every C program must have exactly ONE main().
Returns int: 0 = success, non-zero = error.
─────────────────────────────────────────────*/intmain() {
/* 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);
return0; // signals success to OS
}
/*─────────────────────────────────────────────
SECTION 5: Function Definitions
─────────────────────────────────────────────*/voidgreet(char *name) {
printf("Hello, %s!\n", name);
}
intadd(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
Stage
Tool
Input
Output
What It Does
1. Preprocessing
cpp
.c
.i
Expands #include, #define, removes comments
2. Compilation
cc1
.i
.s
Converts C to assembly language
3. Assembly
as
.s
.o
Converts assembly to machine code (binary)
4. Linking
ld
.o + libs
executable
Combines 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
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.intmain() {
// ↑ 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.return0;
// ↑ 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.
Meaning
ASCII Value
\n
Newline (moves to next line)
10
\t
Horizontal tab
9
\\
Backslash character
92
\"
Double quote
34
\'
Single quote
39
\0
Null character (string terminator)
0
\r
Carriage return
13
Format Spec.
Type
Example
%d
int (decimal)
printf("%d", 42) → 42
%f
float/double
printf("%.2f", 3.14) → 3.14
%c
char
printf("%c", 'A') → A
%s
string (char*)
printf("%s", "hi") → hi
%p
pointer address
printf("%p", ptr) → 0x7ffe…
%ld
long int
printf("%ld", 1000000L)
%lu
unsigned long
printf("%lu", sizeof(int))
%x
hexadecimal
printf("%x", 255) → ff
Unit 3 – Lab Exercises
Write a program that prints: your name, your age, and today's date — each on a separate line using printf.
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>?
Run gcc -S hello.c -o hello.s and view the assembly output. Find the call printf instruction in the .s file.
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.
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.
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
Flag
Purpose
Example
-o name
Name the output file
gcc hello.c -o hello
-Wall
Enable all common warnings
gcc -Wall hello.c -o hello
-Wextra
Enable extra warnings
gcc -Wall -Wextra hello.c -o hello
-g
Include debug info (for GDB)
gcc -g hello.c -o hello
-O2
Optimize for speed
gcc -O2 hello.c -o hello
-std=c99
Use C99 standard
gcc -std=c99 hello.c -o hello
-E
Preprocess only
gcc -E hello.c -o hello.i
-S
Compile to assembly
gcc -S hello.c -o hello.s
-c
Compile to object file
gcc -c hello.c -o hello.o
Unit 3 – Lab Exercises (10 Tasks)
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.
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.
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.
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.
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.
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.
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.).
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*.
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.
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:
A variable declaration tells the compiler to reserve memory and associate a name with it.
#include<stdio.h>intmain() {
/* ── 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 literalchar letter = 'A';
double area = 3.14159;
/* ── Constants (value cannot change) ── */constint MAX_SIZE = 100;
constfloat 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);
return0;
}
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).
#include<stdio.h>intmain() {
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 divideprintf("(double)/int= %.1f\n", result); // 2.5/* ── Char ↔ int (ASCII values) ── */char c = 'A';
printf('%c' has ASCII value %d\n", c, (int)c); // 65printf("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!)return0;
}
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.
Operator
Symbol
Equivalent to
Example
Result (x=10)
Simple Assignment
=
—
x = 5
x = 5
Add & Assign
+=
x = x + n
x += 3
x = 13
Subtract & Assign
-=
x = x - n
x -= 4
x = 6
Multiply & Assign
*=
x = x * n
x *= 2
x = 20
Divide & Assign
/=
x = x / n
x /= 2
x = 5
Modulo & Assign
%=
x = x % n
x %= 3
x = 1
Bitwise AND & Assign
&=
x = x & n
x &= 0xFF
low byte of x
Bitwise OR & Assign
|=
x = x | n
x |= 0x01
sets bit 0
Bitwise XOR & Assign
^=
x = x ^ n
x ^= 0xFF
flips all bits
Left Shift & Assign
<<=
x = x << n
x <<= 2
x = 40
Right Shift & Assign
>>=
x = x >> n
x >>= 1
x = 5
#include<stdio.h>intmain() {
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 = 32printf("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!return0;
}
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 %.
Operator
Symbol
Name
Example (a=10, b=3)
Result
Addition
+
Binary plus
a + b
13
Subtraction
-
Binary minus
a - b
7
Multiplication
*
Multiply
a * b
30
Division
/
Divide
a / b
3 (integer!)
Modulo
%
Remainder
a % b
1
Unary minus
-
Negate
-a
-10
#include<stdio.h>intmain() {
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); // 13printf("a - b = %d\n", a - b); // 7printf("a * b = %d\n", a * b); // 30printf("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.3333printf("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.5printf("(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"); // Yesprintf("Hour wrap: %d\n", (23 + 3) % 24); // 2 (circular clock)/* ── Unary minus and plus ── */int n = 7;
printf("-n = %d\n", -n); // -7printf("+n = %d\n", +n); // 7 (no effect, clarifies intent)/* ── Overflow example ── */int big = 2147483647; // INT_MAXprintf("INT_MAX + 1 = %d\n", big + 1); // -2147483648 (overflow!)return0;
}
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.
Operator
Symbol
Meaning
Example (a=10, b=5)
Result
Equal to
==
Both values are equal
a == b
0 (false)
Not equal to
!=
Values are different
a != b
1 (true)
Greater than
>
Left is larger
a > b
1 (true)
Less than
<
Left is smaller
a < b
0 (false)
Greater or equal
>=
Left >= right
a >= 10
1 (true)
Less or equal
<=
Left <= right
b <= 5
1 (true)
#include<stdio.h>intmain() {
int a = 10, b = 5, c = 10;
printf("=== Relational Results ===\n");
printf("a == b : %d\n", a == b); // 0 — 10 ≠ 5printf("a == c : %d\n", a == c); // 1 — 10 = 10printf("a != b : %d\n", a != b); // 1printf("a > b : %d\n", a > b); // 1printf("a < b : %d\n", a < b); // 0printf("a >= c : %d\n", a >= c); // 1 — 10 >= 10printf("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 imprecisiondouble 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 1printf("is_adult = %d\n", is_adult);
return0;
}
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=6printf("%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=6printf("%d %d", x, y); // 6 5
#include<stdio.h>intmain() {
int a, b, x;
/* ── Post-increment: use value, THEN increment ── */
x = 5;
a = x++; // a gets OLD value (5), then x becomes 6printf("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 6printf("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 9printf("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 9printf("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 idiomprintf("%d ", i); // 1 2 3 4 5printf("\nCounting down: ");
for (int i = 5; i >= 1; i--) // i-- for reverse traversalprintf("%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=4printf("++q * 2 = %d (q=%d)\n", ++q * 2, q); // 8, q=4return0;
}
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>intmain() {
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=garbagereturn0;
}
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).
Operator
Symbol
Name
Returns 1 (true) when
Logical AND
&&
AND
Both operands are true (non-zero)
Logical OR
||
OR
At least one operand is true
Logical NOT
!
NOT
Operand is false (zero)
Truth Tables
A
B
A && B
0
0
0
0
1
0
1
0
0
1
1
1
A
B
A || B
0
0
0
0
1
1
1
0
1
1
1
1
A
!A
0 (false)
1
non-zero
0
#include<stdio.h>intmain() {
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 EVALUATEDint *ptr = NULL;
if (ptr != NULL && *ptr > 0) // Safe: *ptr not reached when ptr==NULLprintf("positive\n");
// In ||: if left side is true, right is NOT evaluatedint x = 5;
if (x > 0 || (printf("not printed\n"), 1))
printf("short-circuit OR\n"); // printf NOT called!return0;
}
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.
Operator
Symbol
Name
Operation
Bitwise AND
&
AND
1 if BOTH bits are 1
Bitwise OR
|
OR
1 if EITHER bit is 1
Bitwise XOR
^
XOR
1 if bits are DIFFERENT
Bitwise NOT
~
Complement
Flips all bits (0→1, 1→0)
Left Shift
<<
SHL
Shift bits left, fill with 0 (×2 per shift)
Right Shift
>>
SHR
Shift 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 intvoidprintBin(int n) {
for (int i = 7; i >= 0; i--)
printf("%d", (n >> i) & 1);
printf(" (%d)\n", n);
}
intmain() {
int a = 12, b = 10; // 0000 1100, 0000 1010printf("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) ×2printf("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 = 40printf("After setting bits 3,5: %d\n", flags); // 40// 3. Clear bit k: AND with inverse mask
flags &= ~(1 << 3); // clear bit 3 → 0b00100000 = 32printf("After clearing bit 3: %d\n", flags); // 32// 4. Toggle bit k: XOR with mask
flags ^= (1 << 5); // toggle bit 5 → 0printf("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 shiftsprintf("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 swapprintf("x=%d y=%d\n", x, y); // x=9, y=5return0;
}
bitwise.c
/* 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 = 2printf("%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!).
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
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.
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.
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.
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).
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.
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.
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.
= 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.
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).
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
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.
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.
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.
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.
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.
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.
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.
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.
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;.
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
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.
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.
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.
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.
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.
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.
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".
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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".
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.
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).
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.
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.
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.
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
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).
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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.
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).
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.
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.
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.
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).
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.
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;
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)
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.
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
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.
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.
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.
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.
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.
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).
#include<stdio.h>intmain() {
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");
}
return0;
}
grades.c
⚡ Ternary Operator (shorthand if-else)
// Syntax: condition ? value_if_true : value_if_falseint max = (a > b) ? a : b; // larger of a, bchar *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 charcase value1:
// code for case 1break; // EXIT switch - required to stop fall-throughcase value2:
// code for case 2break;
case value3: // multiple cases, same action (fall-through)case value4:
// runs for value3 OR value4break;
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.
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--)
// for (init ; condition ; update)for (int i = 0; i < 5; i++) {
printf("%d ", i); // 0 1 2 3 4
}
// Countdownfor (int i = 5; i >= 1; i--) {
printf("%d ", i); // 5 4 3 2 1
}
// Sum 1 to Nint sum = 0;
for (int i = 1; i <= 10; i++) {
sum += i;
}
printf("Sum = %d\n", sum); // 55// Multiplication table for Nint 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
int i = 1;
while (i <= 5) {
printf("%d ", i);
i++;
}
// 1 2 3 4 5while.c
do-while: executes body FIRST
int i = 1;
do {
printf("%d ", i);
i++;
} while (i <= 5);
// 1 2 3 4 5// Key: body runs ONCE even// if condition starts falseint 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
Write a program to check if a number is positive, negative, or zero using if-else if-else.
Write a simple calculator using switch: read two numbers and an operator (+, -, *, /). Print the result. Handle division by zero.
Print the multiplication table (1–10) for any number entered by the user using a for loop.
Use a while loop to find the sum of digits of a number (e.g., 1234 → 1+2+3+4 = 10).
Check if a number is prime using a for loop: test divisibility from 2 to √n.
Print the following patterns using nested loops:
(a) Right-angle triangle of stars (5 rows)
(b) Inverted triangle of numbers
(c) Diamond pattern (bonus)
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 type
Best used when…
Condition checked
Min executions
for
Number of iterations known in advance
Before each iteration
0
while
Loop count depends on a runtime condition
Before each iteration
0
do-while
Body must run at least once (menus, prompts)
After each iteration
1
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-1for (int i = n; i >= 1; i--) // count-down n,...,1for (int i = 0; i < n; i += 2) // step by 2for (int i=0, j=n-1; i<j; i++,j--) // two variablesfor (;;) // infinite (all parts omitted)
Flowchart
Step-trace: for(i=0; i<4; i++) printf(i)
Step
i before
i<4?
Action
i after
Init
0
✓ true
prints 0
1
2
1
✓ true
prints 1
2
3
2
✓ true
prints 2
3
4
3
✓ true
prints 3
4
5
4
✗ FALSE
exit loop
—
#include<stdio.h>intmain() {
/* Counter loop */for (int i = 1; i <= 5; i++)
printf("%d ", i); // 1 2 3 4 5printf("\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} */return0;
}
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 loopwhile (ch != 'q') { scanf("%c",&ch); } // until user quitswhile (!feof(fp)) { read_line(); } // until end of file
Step-trace: digit extraction from n=1234
Iteration
n
n>0?
digit = n%10
n after /=10
1
1234
true
4
123
2
123
true
3
12
3
12
true
2
1
4
1
true
1
0
5
0
FALSE
—
exit
#include<stdio.h>intmain() {
/* 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); // 5return0;
}
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 nothingdo { printf("do-while\n"); }
while (x < 5); // prints once!
#include<stdio.h>intmain() {
/* 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) {
case1: printf("Add selected\n"); break;
case2: printf("Subtract selected\n"); break;
case0: 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);
return0;
}
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 codewhile (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 exitprintf("Got: %d\n", input);
}
#include<stdio.h>/* Simulated server event loop - runs "forever" */intmain() {
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");
return0;
}
// Ctrl+C kills any truly infinite loop in the terminalevent_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 variablefor (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>intmain() {
/* 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)return0;
}
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>intmain() {
/* 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);
return0;
}
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.
#include<stdio.h>intmain() {
/* 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");
}
return0;
}
nested_patterns.c
🧮 6.9 – Common Loop Algorithms
#include<stdio.h>#include<math.h>// sqrt() - compile with -lm/* 1. Factorial (iterative) */long longfactorial(int n) {
long long f = 1;
for (int i = 2; i <= n; i++) f *= i;
return f; // factorial(10) = 3628800
}
/* 2. Fibonacci sequence */voidfibonacci(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)) */intisPrime(int n) {
if (n < 2) return0;
if (n == 2) return1;
if (n % 2 == 0) return0;
for (int i = 3; i <= (int)sqrt(n); i += 2)
if (n % i == 0) return0;
return1;
}
/* 4. GCD - Euclidean algorithm */intgcd(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 */voidanalyseNumber(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=yesintmain() {
printf("5!=%lld\n", factorial(5)); // 120fibonacci(10);
printf("17 prime=%d\n", isPrime(17)); // 1printf("GCD(48,18)=%d\n", gcd(48,18)); // 6analyseNumber(12321);
return0;
}
algorithms.c -- gcc algorithms.c -o algorithms -lm
Unit 6 – Lab Exercises (10 Tasks)
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.
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.
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.
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.
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.
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.
Read N integers (use a for loop). Then compute: minimum, maximum, sum, average, and count of values above average. Print all results.
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.
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.
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"
═══════════════════════════════════════════════*/intadd(int a, int b); // prototypevoidprintLine(int n); // prototype (void = no return)doublecircleArea(double r); // prototype/*═══════════════════════════════════════════════
FUNCTION CALL – inside main() or another function
═══════════════════════════════════════════════*/intmain() {
int result = add(5, 3); // call – 5 and 3 are "arguments"printLine(20); // call with one argumentdouble a = circleArea(7.0); // callprintf("%d, %.2f\n", result, a);
return0;
}
/*═══════════════════════════════════════════════
FUNCTION DEFINITIONS – below main()
returnType functionName(type param, ...) { body }
═══════════════════════════════════════════════*/intadd(int a, int b) { // a, b are "parameters"return a + b; // return value to caller
}
voidprintLine(int n) {
for (int i = 0; i < n; i++) printf("-");
printf("\n");
// no return needed for void
}
doublecircleArea(double r) {
return3.14159 * r * r;
}
functions.c
Function Type
Returns?
Parameters?
Example
No return, no param
No (void)
No (void)
void greet(void)
No return, with param
No
Yes
void printN(int n)
With return, no param
Yes
No
int getMax(void)
With return, with param
Yes
Yes
int 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>intadd(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
}
intmain() {
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 stackint result = add(x, 3);
printf("%d\n", result); // 8return0;
}
call_stack.c
⚖️ 6.3 – Pass by Value vs Pass by Reference
❌ Pass by Value (copy)
// original NOT changedvoiddoubleVal(int x) {
x = x * 2; // changes COPY only
}
intmain() {
int n = 5;
doubleVal(n);
printf("%d\n", n); // still 5!return0;
}
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 changedvoiddoubleRef(int *x) {
*x = (*x) * 2; // changes original
}
intmain() {
int n = 5;
doubleRef(&n); // pass ADDRESS of nprintf("%d\n", n); // 10! ✓return0;
}
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>int globalVar = 100; // GLOBAL: visible everywhere in filevoidcounter() {
staticint count = 0; // STATIC: persists between calls
count++; // NOT reset on each callprintf("Called %d times\n", count);
}
voidscopeDemo() {
int localVar = 50; // LOCAL: only in this functionprintf("local=%d, global=%d\n", localVar, globalVar);
// globalVar is accessible here
globalVar++;
}
intmain() {
scopeDemo(); // local=50, global=100printf("global=%d\n", globalVar); // 101 (modified in scopeDemo)counter(); // Called 1 timescounter(); // Called 2 times (static persists!)counter(); // Called 3 times// Block scope
{
int blockVar = 99; // only in this blockprintf("blockVar=%d\n", blockVar);
}
// blockVar is GONE herereturn0;
}
scope.c
Storage Class
Scope
Lifetime
Default Init
Where Stored
auto
Local (function/block)
Block only
Garbage
Stack
static (local)
Local
Entire program
0
Data/BSS segment
static (global)
File-only
Entire program
0
Data/BSS segment
extern
Global (all files)
Entire program
0
Data/BSS segment
register
Local
Block only
Garbage
CPU register (hint)
Unit 7 – Lab Exercises
Write four separate functions: findMax(a,b), findMin(a,b), square(n), cube(n). Call all four from main and print results.
Write a function isPrime(int n) that returns 1 if prime, else 0. Use it to print all prime numbers between 1 and 100.
Write a recursive function power(base, exp) that computes base^exp without using pow().
Write a swap function that works correctly using pointers: void swap(int *a, int *b). Demonstrate that the values in main are actually swapped.
Demonstrate the static variable counter: write function hitCounter() that prints how many times it's been called. Call it 5 times from main.
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>intmain() {
/* ── Declaration and Initialization ── */int arr[5] = {10, 20, 30, 40, 50};
/* ── Access by index (0-based) ── */printf("First: %d\n", arr[0]); // 10printf("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 = 5printf("Length = %d\n", len);
return0;
}
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>intmain() {
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];
return0;
}
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.
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
Declare an array of 10 integers from user input. Find the max, min, and average.
Write a function void reverseArray(int arr[], int n) that reverses an array in-place.
Implement linear search: int linearSearch(int arr[], int n, int key) — return index or -1.
Implement bubble sort on an array of 10 integers. Print the sorted array.
Read a string and count: (a) vowels, (b) consonants, (c) digits, (d) spaces.
Write a function that reverses a string in-place without using <string.h>.
Check if a string is a palindrome (e.g., "madam", "racecar" are palindromes).
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>intmain() {
int x = 42;
int *p = &x; // p holds the ADDRESS of xprintf("x = %d\n", x); // 42printf("&x = %p\n", &x); // address (e.g. 0x7fff1000)printf("p = %p\n", p); // same address as &xprintf("*p = %d\n", *p); // 42 — dereference: value AT p
*p = 99; // modify x THROUGH the pointerprintf("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.14return0;
}
pointers.c
➕ 8.2 – Pointer Arithmetic
When you add 1 to a pointer, it advances by sizeof(type) bytes — not just 1 byte.
#include<stdio.h>intmain() {
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)return0;
}
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 pointerchar str[] = "Linux";
char *cp = str;
while (*cp != '\0') {
printf("%c", *cp);
cp++;
}
printf("\n"); // Linux// Passing array to function: arrives as pointervoidsumArray(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 pointerschar *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 xint **pp = &p; // pp points to pprintf("%d\n", x); // 5printf("%d\n", *p); // 5printf("%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 functiondouble_ptr.c
🛡️ 8.5 – NULL Pointers & Safe Pointer Practices
#include<stdio.h>#include<stdlib.h>intmain() {
/* ── 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");
return1;
}
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 pointerprintf("\n");
return0;
}
null_ptr.c
malloc / calloc / realloc / free
Purpose
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
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.
Implement void swap(int *a, int *b) using pointers. Verify the swap in main.
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.
Use pointer arithmetic (no array indexing []) to reverse an array in-place.
Dynamically allocate an array of N integers using malloc. Fill it with squares (1, 4, 9, …). Print it. Free it properly.
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 ── */typedefstruct {
char model[30];
int year;
float price;
} Car;
intmain() {
/* ── 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);
return0;
}
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!)
#include<stdio.h>struct Padded { char a; int b; char c; }; // 12 bytesstruct Reorder { char a; char c; int b; }; // 8 bytesintmain() {
printf("Padded = %zu bytes\n", sizeof(struct Padded)); // 12printf("Reorder = %zu bytes\n", sizeof(struct Reorder)); // 8// Rule: order members from LARGEST to SMALLEST type to reduce paddingreturn0;
}
padding.c
➡️ 9.3 – Pointers to Structures: Arrow Operator
#include<stdio.h>typedefstruct {
int x, y;
char label[10];
} Point;
voidprintPoint(const Point *p) {
/* (*p).x is the same as p->x */printf("Point %s: (%d, %d)\n", p->label, p->x, p->y);
}
voidtranslate(Point *p, int dx, int dy) {
p->x += dx; // modify struct through pointer
p->y += dy;
}
intmain() {
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);
return0;
}
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 ── */typedefstruct {
int day, month, year;
} Date;
typedefstruct {
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 ── */typedefstruct 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 3nested.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];
};
intmain() {
union Data d;
printf("sizeof(union Data) = %zu\n", sizeof(d)); // 4
d.i = 65;
printf("d.i = %d\n", d.i); // 65printf("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.14printf("d.i = %d\n", d.i); // raw bits of 3.14f as int// Only the LAST member written is valid!return0;
}
unions.c
struct
union
Memory
Each member has own storage
All members share one storage
Size
Sum of all members + padding
Size of largest member
Usage
Store multiple values at once
Store ONE value at a time
Typical use
Records, data modelling
Type tagging, memory mapping
Unit 10 – Lab Exercises
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.
Write a function void printStudent(struct Student *s) that takes a pointer to struct and prints all fields using the arrow operator.
Print the sizeof for at least 3 different struct layouts. Observe how member ordering affects total size.
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.
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
Mode
Meaning
File Exists?
File Missing?
"r"
Read text
Opens
Returns NULL
"w"
Write text (overwrite)
Truncates
Creates new
"a"
Append text
Appends
Creates new
"r+"
Read + write
Opens
Returns NULL
"w+"
Read + write (overwrite)
Truncates
Creates new
"rb"
Read binary
Opens
Returns NULL
"wb"
Write binary
Truncates
Creates new
✏️ 10.2 – Reading and Writing Text Files
#include<stdio.h>#include<stdlib.h>intmain() {
/* ════ WRITE to a text file ════ */
FILE *fp = fopen("notes.txt", "w");
if (fp == NULL) {
perror("fopen failed"); // prints error reasonreturn1;
}
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"); return1; }
char line[100];
// Read line by line until EOFwhile (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);
}
return0;
}
fileio.c
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
Write a program to copy one text file to another (file copy utility). Read and write line by line.
Count the number of lines, words, and characters in a text file (like the Linux wc command).
Create a student record management system using binary files: support add, list, and search by roll number operations. Persist data to students.bin.
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.
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 Type
When Detected
Tool
Example
Syntax Error
Compile time
GCC
Missing ;, mismatched {}
Semantic Error
Compile time (warnings)
GCC -Wall
Using uninitialised variable
Linker Error
Link time
GCC/LD
Undefined reference to function
Runtime Error
While running
GDB, Valgrind
Segmentation fault, divide by 0
Logic Error
Testing/review
GDB print, unit tests
Wrong 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 / Error
Cause
Fix
unused variable 'x'
Declared but never used
Remove it or use it
control reaches end of non-void function
Missing return statement
Add return value;
implicit declaration of function
Called before prototype/include
Add prototype or include header
comparison between signed and unsigned
Mixing int and size_t/unsigned
Cast explicitly: (int)sizeof(...)
format '%d' expects int, got float
Wrong printf format specifier
Use %f for float/double
undefined reference to 'sqrt'
Math function, missing -lm
Compile with -lm flag
Segmentation fault
Null/out-of-bounds dereference
Use 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"); return1; } // 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 ── */voidprintName(constchar *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
Write a program with an intentional segfault (null dereference). Compile with -g, run under GDB, use backtrace to locate the crash. Fix it.
Write a program with a memory leak (malloc without free). Run Valgrind and observe the leak report. Fix the leak.
Create a program with an off-by-one array access. Compile with -Wall -Wextra, address-sanitize with -fsanitize=address. Fix it.
Debug this broken binary search implementation (wrong mid calculation, wrong termination) using GDB breakpoints and variable inspection.
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.
#include<stdio.h>#include<string.h>#include<ctype.h>#define MAX_WORDS 1000#define WORD_LEN 64typedefstruct { char word[WORD_LEN]; int count; } WordEntry;
WordEntry table[MAX_WORDS];
int tableSize = 0;
/* Convert to lowercase in-place */voidtoLower(char *s) {
for(int i=0; s[i]; i++) s[i] = (char)tolower((unsigned char)s[i]);
}
/* Add word to frequency table */voidaddWord(constchar *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) */voidsortByFreq() {
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;
}
}
intmain(int argc, char *argv[]) {
if (argc < 2) { printf("Usage: ./wordfreq <file>\n"); return1; }
FILE *fp = fopen(argv[1], "r");
if (!fp) { perror("fopen"); return1; }
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);
return0;
}
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 COPYprintf("%d", x); // still 10!voiddouble(int n) {
n *= 2; // local copy only
}
PASS BY POINTER
int x = 10;
doubleIt(&x); // passes ADDRESSprintf("%d", x); // now 20 ✓voiddoubleIt(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 */voidbyValue(int n) {
n = n * 2;
printf(" inside byValue: n=%d\n", n);
}
/* By pointer – modifies caller's variable */voidbyPointer(int *p) {
*p = *p * 2;
printf(" inside byPointer: *p=%d\n", *p);
}
intmain() {
int a = 5, b = 5;
printf("--- Pass by Value ---\n");
byValue(a);
printf(" caller a = %d (unchanged)\n", a); // 5printf("--- Pass by Pointer ---\n");
byPointer(&b);
printf(" caller b = %d (CHANGED)\n", b); // 10return0;
}
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 ── */voidswap(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 ── */voidminMax(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 ── */voiddivmod(int a, int b, int *quot, int *rem) {
*quot = a / b;
*rem = a % b;
}
/* ── Passing large struct by pointer (no copy) ── */typedefstruct { char name[50]; int score; double gpa; } Student;
voidprintStudent(const Student *s) { // const: read-only pointerprintf("Name: %s Score: %d GPA: %.2f\n", s->name, s->score, s->gpa);
}
intmain() {
/* 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);
return0;
}
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 */voidfillArray(int *arr, int n); // pointer stylevoidprintArray(int arr[], int n); // array style (same thing!)voidreverseArray(int arr[], int n);
intsumArray(constint *arr, int n); // const: won't modifyvoidfillArray(int *arr, int n) {
for (int i = 0; i < n; i++)
arr[i] = (i + 1) * 10; // arr[i] identical to *(arr+i)
}
voidprintArray(int arr[], int n) {
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);
printf("\n");
}
voidreverseArray(int arr[], int n) {
int *lo = arr, *hi = arr + n - 1; // pointer-based two-pointer techniquewhile (lo < hi) {
int tmp = *lo; *lo = *hi; *hi = tmp;
lo++; hi--;
}
}
intsumArray(constint *arr, int n) {
int sum = 0;
// Pointer arithmetic traversal (no index variable needed)for (constint *p = arr; p < arr + n; p++)
sum += *p;
return sum;
}
intmain() {
int a[5];
fillArray(a, 5);
printf("Filled : "); printArray(a, 5); // 10 20 30 40 50printf("Sum : %d\n", sumArray(a, 5)); // 150reverseArray(a, 5);
printf("Reversed: "); printArray(a, 5); // 50 40 30 20 10return0;
}
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 */constchar* getStatus(int code) {
staticconstchar *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(constchar *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) returnNULL;
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
}
intmain() {
/* Static */printf("Status 0: %s\n", getStatus(0)); // OKprintf("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!
}
return0;
}
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) */intadd(int a, int b) { return a + b; }
intsub(int a, int b) { return a - b; }
intmul(int a, int b) { return a * b; }
intdivSafe(int a, int b) { return b ? a / b : 0; }
/* typedef for cleaner syntax */typedefint (*BinaryOp)(int, int);
/* Dispatch table: array of function pointers */
BinaryOp ops[] = { add, sub, mul, divSafe };
constchar *opNames[] = { "add", "sub", "mul", "div" };
intmain() {
/* ── Direct function pointer usage ── */
BinaryOp fp = add; // assign function addressprintf("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);
elseprintf("\nFunction pointer is NULL — safe check passed\n");
/* ── Explicit dereference syntax (both work) ── */printf("Direct call : %d\n", (*add)(5, 6)); // old styleprintf("Modern call : %d\n", add(5, 6)); // preferredreturn0;
}
function_pointers.c
Syntax
Meaning
int (*fp)(int, int)
Declare function pointer fp
fp = add
Assign: 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 ── */voidforEach(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 ── */voidmapArray(int *arr, int n, int (*transform)(int)) {
for (int i = 0; i < n; i++)
arr[i] = transform(arr[i]);
}
/* Callback functions to pass */voidprintOne(int x) { printf("%d ", x); }
intsquare(int x) { return x * x; }
intnegate(int x) { return -x; }
/* ── qsort from <stdlib.h> — the standard library callback ── */intascending(constvoid *a, constvoid *b) {
return (*(int*)a) - (*(int*)b); // < 0 if a < b
}
intdescending(constvoid *a, constvoid *b) {
return (*(int*)b) - (*(int*)a);
}
intmain() {
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");
return0;
}
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 ── */voidallocate(int **pp, int value) {
*pp = (int*)malloc(sizeof(int)); // write new address into caller's pointerif (*pp) **pp = value;
}
/* ── Use case 2: reassign a pointer in the caller ── */voidresetToNull(int **pp) {
free(*pp);
*pp = NULL; // set caller's pointer to NULL after freeing
}
/* ── Use case 3: argv is char** (array of char pointers) ── */voidprintArgs(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 ** ── */typedefstruct Node { int val; struct Node *next; } Node;
voidprepend(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
}
intmain() {
/* 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 30printf("\n");
/* Free list */while (head) { Node *tmp = head; head = head->next; free(tmp); }
return0;
}
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.
Declaration
Pointer changeable?
Value changeable?
Use case
int *p
Yes
Yes
General in/out parameter
const int *p
Yes
No
Read-only access to data
int * const p
No
Yes
Fixed address, modifiable value
const int * const p
No
No
Fully immutable
#include<stdio.h>/* Read-only: cannot change *p, can change p itself */voidprintValue(constint *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 */voidincrement(int * const p) {
(*p)++; // OK: modifying the value// p = NULL; ← COMPILE ERROR: pointer is const
}
/* Fully const: ideal for string processing functions */intcountChar(constchar * const s, char c) {
int count = 0;
for (int i = 0; s[i] != '\0'; i++)
if (s[i] == c) count++;
return count;
}
intmain() {
int x = 10;
printValue(&x); // Value: 10increment(&x); // x becomes 11printValue(&x); // Value: 11printf("'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 changedprintf("val = %d\n", val); // 99return0;
}
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".
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 ─────────────────────────────────────── */typedefint (*Comparator)(constvoid*, constvoid*);
typedefvoid (*Printer)(constvoid*);
typedefstruct {
char name[32];
int age;
double gpa;
} Student;
/* ─── Comparators ────────────────────────────────── */intcmpByName(constvoid *a, constvoid *b) {
returnstrcmp(((const Student*)a)->name,
((const Student*)b)->name);
}
intcmpByAge(constvoid *a, constvoid *b) {
return ((const Student*)a)->age - ((const Student*)b)->age;
}
intcmpByGPA(constvoid *a, constvoid *b) {
double diff = ((const Student*)b)->gpa - ((const Student*)a)->gpa;
return (diff > 0) - (diff < 0); // safe double comparison
}
/* ─── Printer ────────────────────────────────────── */voidprintStudent(constvoid *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) ── */intlinearSearch(constvoid *base, int n, size_t elemSize,
constvoid *target, Comparator cmp) {
for (int i = 0; i < n; i++) {
constchar *elem = (constchar*)base + i * elemSize;
if (cmp(elem, target) == 0) return i;
}
return -1;
}
/* ─── Print all with header ─────────────────────── */voidprintAll(constvoid *base, int n, size_t elemSize, Printer pr) {
for (int i = 0; i < n; i++)
pr((constchar*)base + i * elemSize);
}
intmain() {
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);
return0;
}
generic_sort_search.c
📝 Unit 14 – 10 Assignments: Functions with Pointers
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.