Algorithm Programming - EE

This course introduces the fundamental concepts of algorithms and programming using the C language. Students will learn how to analyze problems, design step-by-step solutions, and implement them in C. The course covers essential programming topics such as variables, data types, operators, control structures (selection and iteration), functions, arrays, pointers, and file handling.

Through lectures, hands-on coding exercises, and projects, students will develop problem-solving skills and a solid foundation in structured programming. By the end of the course, students are expected to be able to write efficient C programs, apply algorithmic thinking to solve computational problems, and understand the importance of programming discipline as a basis for more highly related engineering courses.

Module 1 : Introduction to C

Learning Objectives

Module 1 : Introduction to C

1. Introduction: From Python to C

1.1 Key Differences Overview

Aspect Python C
Compilation Interpreted Compiled
Type System Dynamic typing Static typing
Memory Management Automatic Manual
Syntax Style Indentation-based Brace-based
Performance Slower execution Faster execution
Development Speed Faster to write More verbose

Some of you might ask, What does it mean by Static Typing vs Dynamic Typing ?

Dynamic Typing ( Python ):

# Python - Dynamic Typing
x = 5        # x is an integer
x = "Hello"  # Now x is a string (allowed!)
x = 3.14     # Now x is a float (also allowed!)

Static Typing ( C ):

// C - Static Typing
int x = 5;           // x is declared as integer
x = "Hello";         // ERROR! Cannot assign string to integer variable
float y = 3.14f;     // y must be declared as float to store decimal numbers

Advantages of Static Typing ( C ):

Advantages of Dynamic Typing ( Python ):

1.2 Basic Program Structure

Python:

# Simple Python program
print("Hello, World!")

C:

#include <stdio.h>

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

Key Points:

Module 1 : Introduction to C

2. Input/Output Operations

2.1 Output Operations

2.1.1 Basic Output - printf()

Function Signature:

int printf(const char *format, ...);

Python vs C Comparison:

Python C
print("Hello") printf("Hello\n");
print("Value:", x) printf("Value: %d\n", x);
print(f"x = {x}") printf("x = %d\n", x);

Keep in mind that print() in python automatically creates a new line by default

2.1.2 Format Specifiers

Data Type Format Specifier Example
int %d or %i printf("%d", 42);
float %f printf("%.2f", 3.14);
double %lf printf("%.2lf", 3.14159);
char %c printf("%c", 'A');
string %s printf("%s", "Hello");
hexadecimal %x or %X printf("%x", 255);
unsigned int %u printf("%u", 42u);

2.1.3 Advanced printf() Features

Width and Precision:

printf("%5d", 42);        // Right-aligned in 5 characters: "   42"
printf("%-5d", 42);       // Left-aligned in 5 characters: "42   "
printf("%05d", 42);       // Zero-padded: "00042"
printf("%.2f", 3.14159);  // 2 decimal places: "3.14"
printf("%8.2f", 3.14159); // 8 characters, 2 decimals: "    3.14"

2.1.4 Escape Characters

Escape characters are special character sequences that represent characters that are difficult or impossible to type directly. They start with a backslash (\).

Common Escape Characters:

Escape Sequence Character Description Example
\n Newline Moves cursor to next line printf("Line 1\nLine 2");
\t Tab Horizontal tab (8 spaces) printf("Name:\tJohn");
\" Double Quote Literal double quote printf("He said \"Hello\"");
\' Single Quote Literal single quote printf("It\'s working");
\\ Backslash Literal backslash printf("Path: C:\\Program Files");
\r Carriage Return Return to beginning of line printf("Loading\rDone");
\b Backspace Move cursor back one position printf("ABC\bD"); → "ABD"
\0 Null Character String terminator char str[] = "Hi\0lo";
\a Alert (Bell) System beep/alert sound printf("\aError!");
\f Form Feed Page break printf("Page 1\fPage 2");
\v Vertical Tab Vertical tab printf("Line 1\vLine 2");

Python vs C Escape Characters:

Purpose Python C
New line print("Line 1\nLine 2") printf("Line 1\nLine 2");
Tab spacing print("Name:\tAge") printf("Name:\tAge");
Quote in string print("She said \"Hi\"") printf("She said \"Hi\"");
Backslash print("C:\\folder") printf("C:\\\\folder");

Practical Examples:

// Creating formatted output with escape characters
printf("Student Information:\n");
printf("Name:\t\tJohn Doe\n");
printf("Age:\t\t20\n");
printf("GPA:\t\t3.75\n");

// Output:
// Student Information:
// Name:		John Doe
// Age:		20
// GPA:		3.75
// Using quotes within strings
printf("The teacher said, \"Programming is fun!\"\n");
// Output: The teacher said, "Programming is fun!"

// File paths (especially important for Windows)
printf("Save file to: C:\\Documents\\Programs\\myfile.txt\n");
// Output: Save file to: C:\Documents\Programs\myfile.txt

Important Notes:

2.2 Input Operations

2.2.1 Basic Input - scanf()

Function Signature:

int scanf(const char *format, ...);

Python vs C Comparison:

Python C
x = int(input()) scanf("%d", &x);
name = input() scanf("%s", name);
x = float(input()) scanf("%f", &x);

2.2.2 Important scanf() Considerations

Address Operator (&):

int age;
char name[50];
float height;

scanf("%d", &age);     // & required for int
scanf("%s", name);     // & NOT needed for string
scanf("%f", &height);  // & required for float

Input Buffer Issues and Whitespace Handling:

The Whitespace Problem: When you press Enter after typing input, scanf() reads the data but leaves the newline character (\n) in the input buffer. This can cause problems with subsequent input operations.

// Problematic code:
int num;
char ch;

printf("Enter a number: ");
scanf("%d", &num);              // User types "5" and presses Enter
                                // Buffer now contains: \n (leftover newline)
printf("Enter a character: ");
scanf("%c", &ch);               // This reads the leftover \n, not user input!
printf("Character: %c\n", ch);  
// Outputs: 
// Character: (it shows nothing because it prints a newline)

What happens step by step:

  1. User types "5" and presses Enter → Input buffer: 5\n
  2. scanf("%d", &num) reads "5" → Buffer remaining: \n
  3. scanf("%c", &ch) immediately reads the leftover \n
  4. Program doesn't wait for new character input

Solutions:

Solution 1: Space before %c

int num;
char ch;

printf("Enter a number: ");
scanf("%d", &num);
printf("Enter a character: ");
scanf(" %c", &ch);  // Space before %c consumes all whitespace (spaces, tabs, newlines)

Solution 2: Explicit buffer clearing

int num;
char ch;

printf("Enter a number: ");
scanf("%d", &num);

// Clear the input buffer
while (getchar() != '\n');  // Read and discard until newline

printf("Enter a character: ");
scanf("%c", &ch);

Solution 3: Using getchar() to consume newline

int num;
char ch;

printf("Enter a number: ");
scanf("%d", &num);
getchar();  // Consume the leftover newline

printf("Enter a character: ");
scanf("%c", &ch);

Whitespace Characters in C:

Important scanf() Whitespace Rules:

Advanced scanf() Format Specifiers:

1. Character Set Specifiers [...]:

char name[50];

// Read only alphabetic characters
scanf("%[a-zA-Z]", name);

// Read everything except newline
scanf("%[^\n]", name);  // Reads entire line including spaces

// Read only digits
scanf("%[0-9]", name);

// Read only vowels
scanf("%[aeiouAEIOU]", name);

2. Excluding Character Sets [^...]:

char input[100];

// Read everything EXCEPT newline (gets full line with spaces)
scanf("%[^\n]", input);

// Read everything EXCEPT spaces and tabs
scanf("%[^ \t]", input);

// Read everything EXCEPT digits
scanf("%[^0-9]", input);

// Read until comma is encountered
scanf("%[^,]", input);

3. Width Specifiers:

char buffer[10];

// Read maximum 9 characters (leaving room for \0)
scanf("%9s", buffer);

// Read exactly 5 characters
scanf("%5c", buffer);

4. Practical Examples:

// Example 1: Reading full name (including spaces)
char full_name[100];
printf("Enter your full name: ");
scanf(" %[^\n]", full_name);  // Space before % consumes previous newline

// Example 2: Reading until specific delimiter
char email[50];
printf("Enter email: ");
scanf("%[^@]", email);  // Read until @ symbol

// Example 3: Input validation
char grade[10];
printf("Enter grade (A, B, C, D, F): ");
scanf("%[ABCDFabcdf]", grade);  // Only accept valid grades

5. Combining Multiple Inputs:

int day, month, year;
char separator;

// Reading date in format: dd/mm/yyyy or dd-mm-yyyy
printf("Enter date (dd/mm/yyyy or dd-mm-yyyy): ");
scanf("%d%c%d%c%d", &day, &separator, &month, &separator, &year);

// Alternative: reading with specific separators
printf("Enter date (dd/mm/yyyy): ");
scanf("%d/%d/%d", &day, &month, &year);

2.2.3 Alternative Input Methods

getchar() and putchar():

char ch;
ch = getchar();  // Read single character
putchar(ch);     // Output single character

fgets() for Safe String Input:

char name[50];
printf("Enter your name: ");
fgets(name, sizeof(name), stdin);
Module 1 : Introduction to C

3. Variables and Data Types

3.1 Variable Declaration

Python vs C:

Python C
x = 5 int x = 5;
name = "John" char name[] = "John";
pi = 3.14 float pi = 3.14f;

3.2 Basic Data Types

3.2.1 Integer Types

Type Size (bytes) Range Usage
char 1 -128 to 127 Small integers, characters
short 2 -32,768 to 32,767 Small integers
int 4 -2,147,483,648 to 2,147,483,647 Standard integers
long 4/8 System dependent Large integers
long long 8 Very large range Very large integers

Unsigned Variants:

unsigned char uc;    // 0 to 255
unsigned int ui;     // 0 to 4,294,967,295
unsigned short us;   // 0 to 65,535

3.2.2 Floating-Point Types

Type Size Precision Range
float 4 bytes ~7 digits ±3.4 × 10^±38
double 8 bytes ~15 digits ±1.7 × 10^±308
long double 12/16 bytes Extended precision System dependent

3.2.3 Character and String Types

Single Characters:

char letter = 'A';        // Single character
char digit = '5';         // Character representation of digit
char newline = '\n';      // Escape sequence

Strings (Character Arrays):

char name[20] = "John";           // Fixed-size array
char message[] = "Hello World";   // Size determined by initializer
char buffer[100];                 // Uninitialized array

Python vs C String Comparison:

Python C
name = "John" char name[] = "John";
len(name) strlen(name)
name[0] name[0]
name + " Doe" strcat(name, " Doe");

3.3 Variable Declaration Rules

  1. Must declare before use (unlike Python)
  2. Case-sensitive (ageAge)
  3. Cannot start with digits (2x is invalid)
  4. Cannot use keywords (int, if, while, etc.)
  5. Should use meaningful names (student_count not sc)

3.4 Constants

// Method 1: #define preprocessor directive
#define PI 3.14159
#define MAX_SIZE 100

// Method 2: const keyword
const int ARRAY_SIZE = 50;
const float GRAVITY = 9.81f;

Python vs C Constants:

Python C
PI = 3.14159 #define PI 3.14159
PI = 3.14159 const float PI = 3.14159f;
Module 1 : Introduction to C

4. Arithmetic Operators

4.1 Basic Arithmetic Operators

Operator Operation Python Example C Example
+ Addition a + b a + b
- Subtraction a - b a - b
* Multiplication a * b a * b
/ Division a / b a / b
% Modulus a % b a % b

4.2 Important Division Differences

Integer Division:

# Python 3
print(7 / 3)   # Output: 2.333...
print(7 // 3)  # Output: 2 (floor division)
// C
printf("%f\n", 7.0 / 3.0);  // Output: 2.333333
printf("%d\n", 7 / 3);      // Output: 2 (integer division)
printf("%f\n", (float)7 / 3); // Output: 2.333333 (type casting)

4.3 Assignment Operators

Operator Equivalent Python C
= Basic assignment x = 5 x = 5;
+= Add and assign x += 3 x += 3;
-= Subtract and assign x -= 3 x -= 3;
*= Multiply and assign x *= 3 x *= 3;
/= Divide and assign x /= 3 x /= 3;
%= Modulus and assign x %= 3 x %= 3;

4.4 Increment and Decrement Operators

Pre-increment vs Post-increment:

int x = 5;
int y, z;

y = ++x;  // Pre-increment: x becomes 6, then y = 6
z = x++;  // Post-increment: z = 6, then x becomes 7

// Equivalent operations:
x = x + 1;  // Same as x++ or ++x when used alone
x += 1;     // Same as above

Python vs C:

Python C
x += 1 x++ or ++x or x += 1
x -= 1 x-- or --x or x -= 1

4.5 Operator Precedence

Priority Operators Associativity
1 (Highest) ++, -- (postfix) Left to right
2 ++, -- (prefix), +, - (unary) Right to left
3 *, /, % Left to right
4 +, - (binary) Left to right
5 (Lowest) =, +=, -=, etc. Right to left
Module 1 : Introduction to C

5. Flow Control

5.1 Conditional Statements

5.1.1 if Statement

Python vs C Syntax:

Python:

if condition:
    statement1
    statement2
elif another_condition:
    statement3
else:
    statement4

C:

if (condition) {
    statement1;
    statement2;
} else if (another_condition) {
    statement3;
} else {
    statement4;
}

Key Differences:

5.1.2 Relational Operators

Operator Meaning Python C
== Equal to a == b a == b
!= Not equal to a != b a != b
< Less than a < b a < b
> Greater than a > b a > b
<= Less than or equal a <= b a <= b
>= Greater than or equal a >= b a >= b

5.1.3 Logical Operators

Operator Meaning Python C
&& Logical AND and or & &&
|| Logical OR or or | ||
! Logical NOT not or ~ !

Examples in C :

// Python: if age >= 18 and score > 80:
if (age >= 18 && score > 80) {
    printf("Eligible for scholarship\n");
}

// Python: if not (x < 0 or x > 100):
if (!(x < 0 || x > 100)) {
    printf("Valid percentage\n");
}

5.1.4 Executing Code in if Conditions

While primarily used for conditions, C allows expressions that evaluate to a non-zero value (true) or zero (false) within the parentheses. This means you can sometimes perform assignments or function calls directly within the condition, though it's often discouraged for readability.

int x = 10;
if (x = 5) { // Assigns 5 to x, then evaluates to 5 (true)
    printf("x is now 5 and this code runs.\n");
}

5.1.5 Single Statement if:

If an if or else block contains only a single statement, the curly braces {} are optional. However, it's good practice to always use them to avoid ambiguity and potential errors when adding more statements later.

if (score > 90)
    printf("Excellent!\n");
else
    printf("Keep trying.\n");

5.1.6 switch Statement

C provides switch as an alternative to multiple if-else statements:

switch (variable) {
    case value1:
        // statements
        break;
    case value2:
        // statements
        break;
    default:
        // statements
        break;
}

Example:

int grade;
printf("Enter grade (1-5): ");
scanf("%d", &grade);

switch (grade) {
    case 5:
        printf("Excellent!\n");
        break;
    case 4:
        printf("Very Good!\n");
        break;
    case 3:
        printf("Good!\n");
        break;
    case 2:
        printf("Fair!\n");
        break;
    case 1:
        printf("Poor!\n");
        break;
    default:
        printf("Invalid grade!\n");
        break;
}

Understanding break and Fall-through: In C's switch statement, the break keyword is essential. If break is omitted from a case block, execution will "fall through" to the next case block (and subsequent ones) until a break is encountered or the end of the switch statement is reached. This "fall-through" behavior can be intentionally used for specific logic, but it's a common source of bugs if not intended.

Example of Fall-through:

int day = 2; // Monday
switch (day) {
    case 1:
        printf("Weekend!\n");
        break;
    case 2:
    case 3:
    case 4:
    case 5:
        printf("Weekday.\n"); // Execution falls through from case 2, 3, 4 to 5
        break;
    case 6:
        printf("Weekend!\n");
        break;
    default:
        printf("Invalid day.\n");
        break;
}
// Output for day = 2: Weekday.

This example shows how case 2, case 3, case 4, and case 5 all execute the same printf("Weekday.\n"); because there are no break statements between them.

5.2 Iteration Statements (Loops)

5.2.1 for Loop

Python vs C Syntax:

Python:

for i in range(5):
    print(i)

for i in range(1, 10, 2):
    print(i)

C:

// Basic for loop
for (int i = 0; i < 5; i++) {
    printf("%d\n", i);
}

// Step by 2
for (int i = 1; i < 10; i += 2) {
    printf("%d\n", i);
}

For Loop Structure:

for (initialization; condition; increment/decrement) {
    // loop body
}

5.2.2 while Loop

Python vs C:

Python:

i = 0
while i < 5:
    print(i)
    i += 1

C:

int i = 0;
while (i < 5) {
    printf("%d\n", i);
    i++;
}

5.2.3 do-while Loop

C provides do-while loop (not available in Python):

int choice;
do {
    printf("Enter choice (1-3): ");
    scanf("%d", &choice);
    
    if (choice < 1 || choice > 3) {
        printf("Invalid choice! Try again.\n");
    }
} while (choice < 1 || choice > 3);

Key Difference: do-while executes the loop body at least once, even if the condition is initially false.

5.2.4 Loop Control Statements

Statement Python C Purpose
break break break; Exit loop immediately
continue continue continue; Skip to next iteration

Example:

for (int i = 1; i <= 10; i++) {
    if (i % 2 == 0) {
        continue;  // Skip even numbers
    }
    if (i > 7) {
        break;     // Stop when i > 7
    }
    printf("%d ", i);  // Output: 1 3 5 7
}
Module 1 : Introduction to C

6. More Migration Guide: From Python to C

6.1 Common Syntax Differences

Feature Python C
Comments # This is a comment // This is a comment
Block Comments """Multi-line""" /* Multi-line */
Code Blocks Indentation { } braces
Statement End Line break ; semicolon
Boolean Values True, False 1, 0 (or true, false with <stdbool.h>)

6.2 Variable Declaration Migration

Python to C Translation Examples:

# Python
age = 25
height = 5.9
name = "Alice"
is_student = True
// C
int age = 25;
float height = 5.9f;
char name[] = "Alice";
int is_student = 1;  // or use bool with #include <stdbool.h>

6.3 Function Definition Migration (We will learn more about this on the next module)

Python:

def calculate_area(length, width):
    return length * width

result = calculate_area(5, 3)
print(result)

C:

#include <stdio.h>

// Function declaration (prototype)
int calculate_area(int length, int width);

int main() {
    int result = calculate_area(5, 3);
    printf("%d\n", result);
    return 0;
}

// Function definition
int calculate_area(int length, int width) {
    return length * width;
}

6.4 Common Pitfalls for Python Programmers

  1. Forgetting Semicolons:

    int x = 5  // ERROR: Missing semicolon
    int x = 5; // CORRECT
    
  2. Using = instead of == in conditions:

    if (x = 5) { ... }   // ERROR: Assignment, not comparison
    if (x == 5) { ... }  // CORRECT: Comparison
    
  3. Forgetting & in scanf():

    scanf("%d", x);   // ERROR: Missing &
    scanf("%d", &x);  // CORRECT
    
  4. Array Index Out of Bounds: (We will learn more about this on the next module)

    int arr[5];
    arr[5] = 10;  // ERROR: Index 5 is out of bounds (valid: 0-4)
    arr[4] = 10;  // CORRECT: Last valid index
    
Module 1 : Introduction to C

7. Best Basic Practices and Style Guidelines

7.1 Naming Conventions

7.2 Code Organization

#include <stdio.h>        // System headers
#include <stdlib.h>

#define MAX_SIZE 100      // Constants

// Function prototypes (We will learn more about this in the next module)
int add(int a, int b);
void print_result(int result);

int main() {
    // Main program logic
    return 0;
}

// Function definitions (We will learn more about this in the next module)
int add(int a, int b) {
    return a + b;
}

7.3 Error Handling

Input Validation:

int num;
printf("Enter a positive number: ");
scanf("%d", &num)

if (num < 0) {
    printf("Invalid input!\n");
}
Module 1 : Introduction to C

8. Practical Examples

8.1 Complete Program Examples

Example 1: Simple Calculator

#include <stdio.h>

int main() {
    float num1, num2, result;
    char operator;
    
    printf("Enter first number: ");
    scanf("%f", &num1);
    
    printf("Enter operator (+, -, *, /): ");
    scanf(" %c", &operator);
    
    printf("Enter second number: ");
    scanf("%f", &num2);
    
    switch (operator) {
        case '+':
            result = num1 + num2;
            break;
        case '-':
            result = num1 - num2;
            break;
        case '*':
            result = num1 * num2;
            break;
        case '/':
            if (num2 != 0) {
                result = num1 / num2;
            } else {
                printf("Error: Division by zero!\n");
                return 1;
            }
            break;
        default:
            printf("Error: Invalid operator!\n");
            return 1;
    }
    
    printf("%.2f %c %.2f = %.2f\n", num1, operator, num2, result);
    return 0;
}

Example 2: Grade Classification

#include <stdio.h>

int main() {
    int score;
    
    printf("Enter your score (0-100): ");
    scanf("%d", &score);
    
    if (score < 0 || score > 100) {
        printf("Invalid score!\n");
    } else if (score >= 90) {
        printf("Grade: A (Excellent)\n");
    } else if (score >= 80) {
        printf("Grade: B (Very Good)\n");
    } else if (score >= 70) {
        printf("Grade: C (Good)\n");
    } else if (score >= 60) {
        printf("Grade: D (Fair)\n");
    } else {
        printf("Grade: F (Fail)\n");
    }
    
    return 0;
}

8.2 Loop Examples

Example 1: Sum of Numbers

#include <stdio.h>

int main() {
    int n, sum = 0;
    
    printf("Enter a positive integer: ");
    scanf("%d", &n);
    
    for (int i = 1; i <= n; i++) {
        sum += i;
    }
    
    printf("Sum of numbers from 1 to %d is: %d\n", n, sum);
    return 0;
}

Example 2: Multiplication Table

#include <stdio.h>

int main() {
    int num;
    
    printf("Enter a number: ");
    scanf("%d", &num);
    
    printf("Multiplication table for %d:\n", num);
    for (int i = 1; i <= 10; i++) {
        printf("%d x %d = %d\n", num, i, num * i);
    }
    
    return 0;
}
Module 1 : Introduction to C

9. Common Debugging Tips

9.1 Compilation Errors

  1. Missing semicolons: Add ; at the end of statements
  2. Undeclared variables: Declare variables before using them
  3. Type mismatches: Ensure compatible types in assignments (this mostly happens in function parameters or calling, we will learn more about this on the next module)
  4. Missing headers: Include necessary header files

9.2 Runtime Errors

  1. Segmentation faults: Check array bounds and pointer usage.
    • this error is the hardest part when it comes to debugging because its highly related to a wrong memory allocation or pointer usage such as accessing an unaccessible variable or array index. We will learn more about this in the next module
  2. Infinite loops: Verify loop conditions and increment/decrement
  3. Wrong output: Check format specifiers in printf/scanf

9.3 Debugging Techniques

// Add debug prints to trace program execution
printf("Debug: x = %d, y = %d\n", x, y);

// Check intermediate results
int temp = a + b;
printf("Intermediate result: %d\n", temp);
int result = temp * c;

Module 2 : Functions in C

Learning Objectives:


Module 2 : Functions in C

1. Introduction to Functions

1.1 What are Functions?

Functions are self-contained blocks of code that perform specific tasks. They are fundamental building blocks that help organize code, promote reusability, and make programs more modular and maintainable.

Benefits of Functions:

1.2 Why Use Functions? (Comparison with and without)

To understand the practical benefits of functions, let's consider a simple task: calculating the area of three different rectangles.

Without Functions:

#include <stdio.h>

int main() {
    // Rectangle 1
    int length1 = 10;
    int width1 = 5;
    int area1 = length1 * width1;
    printf("Area of Rectangle 1: %d\n", area1);

    // Rectangle 2
    int length2 = 12;
    int width2 = 8;
    int area2 = length2 * width2;
    printf("Area of Rectangle 2: %d\n", area2);

    // Rectangle 3
    int length3 = 7;
    int width3 = 3;
    int area3 = length3 * width3;
    printf("Area of Rectangle 3: %d\n", area3);

    return 0;
}

In this example, the logic for calculating the area is repeated three times. If we needed to change how the area is calculated (e.g., add a margin), we would have to modify each instance, which is error-prone and inefficient.

With Functions:

#include <stdio.h>

// Function to calculate rectangle area
int calculate_rectangle_area(int length, int width) {
    return length * width;
}

int main() {
    // Rectangle 1
    int area1 = calculate_rectangle_area(10, 5);
    printf("Area of Rectangle 1: %d\n", area1);

    // Rectangle 2
    int area2 = calculate_rectangle_area(12, 8);
    printf("Area of Rectangle 2: %d\n", area2);

    // Rectangle 3
    int area3 = calculate_rectangle_area(7, 3);
    printf("Area of Rectangle 3: %d\n", area3);

    return 0;
}

By using a function calculate_rectangle_area, we write the area calculation logic only once. This demonstrates:

1.3 Python vs C Functions Comparison

Aspect Python C
Declaration Not required Function prototype usually required (unless defined before use)
Definition def function_name(): return_type function_name() { }
Return Type Dynamic (any type) Must be explicitly declared
Parameters Dynamic typing Static typing required
Call Before Definition Allowed Requires prototype
Multiple Return Values return a, b Use pointers or structures

Python Example:

def add_numbers(a, b):
    return a + b

result = add_numbers(5, 3)
print(result)  # Output: 8

C Equivalent:

#include <stdio.h>

// Function declaration (prototype)
int add_numbers(int a, int b);

int main() {
    int result = add_numbers(5, 3);
    printf("%d\n", result);  // Output: 8
    return 0;
}

// Function definition
int add_numbers(int a, int b) {
    return a + b;
}
Module 2 : Functions in C

2. Function Declaration, Definition, and Calling

2.1 Function Anatomy

A C function consists of several parts:

return_type function_name(parameter_list) {
    // Function body
    // Local variables
    // Statements
    return value; // (if return_type is not void)
}

Components:

  1. Return Type: Data type of the value returned (int, float, char, void, etc.)
  2. Function Name: Identifier for the function
  3. Parameter List: Input values (formal parameters)
  4. Function Body: Statements enclosed in braces
  5. Return Statement: Returns control and optionally a value

2.2 Function Declaration (Prototype)

Function prototypes declare the function's interface before its actual definition. They are necessary when a function is called before its definition in the source code. This allows the compiler to check for correct usage and enables forward referencing. If a function is defined before it is called, a prototype is not strictly necessary.

Syntax:

return_type function_name(parameter_types);

Examples:

// Function prototypes
int add(int a, int b);                    // Two int parameters, returns int
float calculate_area(float length, float width);  // Two float parameters, returns float
void print_message(void);                 // No parameters, no return value
char get_grade(int score);               // One int parameter, returns char

Important Notes:

2.3 Function Definition

The function definition contains the actual implementation:

#include <stdio.h>

// Function prototype
int multiply(int x, int y);

int main() {
    int result = multiply(4, 5);
    printf("Result: %d\n", result);
    return 0;
}

// Function definition
int multiply(int x, int y) {
    int product = x * y;
    return product;
}

2.4 Function Calling

After a function has been declared (prototyped) and defined, it can be executed, or "called," from another part of the program (e.g., from main() or another function). When a function is called, the program's control is transferred to that function.

Syntax for Calling a Function:

function_name(arguments);

Example: In the main() function of the previous example, multiply(4, 5); is a function call.

int main() {
    int result = multiply(4, 5); // Calling the 'multiply' function
    printf("Result: %d\n", result);
    return 0;
}

Here, 4 and 5 are the arguments passed to the multiply function. The return value of multiply is then stored in the result variable.

2.5 void Functions

Keep in mind : Functions that don't return a value use void as the return type:

#include <stdio.h>

void print_header(void);
void print_line(int length);

int main() {
    print_header();
    print_line(30);
    return 0;
}

void print_header(void) {
    printf("=== STUDENT MANAGEMENT SYSTEM ===\n");
}

void print_line(int length) {
    for (int i = 0; i < length; i++) {
        printf("-");
    }
    printf("\n");
}
Module 2 : Functions in C

3. Parameters and Arguments

3.1 Terminology

// 'a' and 'b' are parameters
int add(int a, int b) {
    return a + b;
}

int main() {
    int x = 5, y = 3;
    // 'x' and 'y' are arguments
    int result = add(x, y);
    return 0;
}

3.2 Parameter Passing in C

C uses pass by value as its primary parameter passing mechanism. This means:

Example:

#include <stdio.h>

void modify_value(int num) {
    num = 100;  // This only changes the local copy
    printf("Inside function: %d\n", num);  // Output: 100
}

int main() {
    int original = 50;
    printf("Before function call: %d\n", original);  // Output: 50
    
    modify_value(original);
    
    printf("After function call: %d\n", original);   // Output: 50 (unchanged!)
    return 0;
}

3.3 Python vs C Parameter Passing

Python:

def modify_list(lst):
    lst.append(4)  # Modifies the original list
    
my_list = [1, 2, 3]
modify_list(my_list)
print(my_list)  # Output: [1, 2, 3, 4]

C (Basic Types):

void modify_number(int num) {
    num = 100;  // Only modifies the copy
}

int main() {
    int number = 50;
    modify_number(number);
    // number is still 50
    return 0;
}

3.4 Multiple Parameters

Functions can have multiple parameters of different types:

#include <stdio.h>

// Function to calculate compound interest
float calculate_compound_interest(float principal, float rate, int time, int n) {
    float amount = principal;
    float rate_per_period = rate / (100.0 * n);
    int total_periods = n * time;
    
    for (int i = 0; i < total_periods; i++) {
        amount *= (1 + rate_per_period);
    }
    
    return amount - principal;  // Return only the interest
}

int main() {
    float principal = 1000.0;
    float annual_rate = 5.0;    // 5% per year
    int years = 2;
    int compounding_freq = 4;   // Quarterly
    
    float interest = calculate_compound_interest(principal, annual_rate, years, compounding_freq);
    
    printf("Principal: $%.2f\n", principal);
    printf("Interest earned: $%.2f\n", interest);
    printf("Total amount: $%.2f\n", principal + interest);
    
    return 0;
}
Module 2 : Functions in C

4. Return Statement

4.1 Basic Return Usage

The return statement serves two purposes:

  1. Return control to the calling function
  2. Return a value (optional, depending on function type)
// Function that returns a value
int get_maximum(int a, int b) {
    if (a > b) {
        return a;
    } else {
        return b;
    }
}

// Function that returns without a value
void print_status(int score) {
    if (score < 0) {
        printf("Invalid score!\n");
        return;  // Early exit
    }
    
    if (score >= 60) {
        printf("Passed!\n");
    } else {
        printf("Failed!\n");
    }
    // Implicit return at end of void function
}

4.2 Multiple Return Statements

A function can have multiple return statements, but only one will execute:

char determine_grade(int score) {
    if (score >= 90) {
        return 'A';
    }
    if (score >= 80) {
        return 'B';
    }
    if (score >= 70) {
        return 'C';
    }
    if (score >= 60) {
        return 'D';
    }
    return 'F';  // Default case
}

4.3 Returning Different Data Types

#include <stdio.h>

// Return integer
int get_absolute(int num) {
    if (num < 0) {
        return -num;
    } else {
        return num;
    }
}

// Return float
float celsius_to_fahrenheit(float celsius) {
    return (celsius * 9.0 / 5.0) + 32.0;
}

// Return character
char get_letter_grade(float percentage) {
    if (percentage >= 85.0) return 'A';
    if (percentage >= 75.0) return 'B';
    if (percentage >= 65.0) return 'C';
    if (percentage >= 50.0) return 'D';
    return 'F';
}

int main() {
    printf("Absolute value of -15: %d\n", get_absolute(-15));
    printf("25°C in Fahrenheit: %.1f°F\n", celsius_to_fahrenheit(25.0));
    printf("Grade for 78.5%%: %c\n", get_letter_grade(78.5));
    return 0;
}
Module 2 : Functions in C

5. Variable Scope and Lifetime

5.1 Local Variables

Variables declared inside a function are local to that function:

#include <stdio.h>

void function_a() {
    int local_var = 10;  // Local to function_a
    printf("In function_a: %d\n", local_var);
}

void function_b() {
    int local_var = 20;  // Different variable, local to function_b
    printf("In function_b: %d\n", local_var);
}

int main() {
    int local_var = 5;   // Local to main
    printf("In main: %d\n", local_var);
    
    function_a();  // Output: In function_a: 10
    function_b();  // Output: In function_b: 20
    
    printf("Back in main: %d\n", local_var);  // Still 5
    return 0;
}

5.2 Global Variables

Variables declared outside all functions are global:

#include <stdio.h>

// Global variables
int global_counter = 0;
float global_sum = 0.0;

void increment_counter() {
    global_counter++;  // Can access global variable
    printf("Counter: %d\n", global_counter);
}

void add_to_sum(float value) {
    global_sum += value;  // Can modify global variable
    printf("Sum: %.2f\n", global_sum);
}

int main() {
    increment_counter();  // Counter: 1
    increment_counter();  // Counter: 2
    
    add_to_sum(10.5);     // Sum: 10.50
    add_to_sum(7.3);      // Sum: 17.80
    
    printf("Final values - Counter: %d, Sum: %.2f\n", global_counter, global_sum);
    return 0;
}

5.3 Variable Shadowing

When a local variable has the same name as a global variable, the local variable "shadows" the global one:

#include <stdio.h>

int value = 100;  // Global variable

void test_shadowing() {
    int value = 50;  // Local variable shadows the global one
    printf("Local value: %d\n", value);  // Prints 50
}

int main() {
    printf("Global value: %d\n", value);  // Prints 100
    test_shadowing();
    printf("Global value after function: %d\n", value);  // Still 100
    return 0;
}

5.4 Best Practices for Variable Scope

  1. Minimize global variables: Use them sparingly
  2. Prefer local variables: Keep data close to where it's used
  3. Use meaningful names: Avoid naming conflicts
  4. Initialize variables: Always initialize before use
// Good practice
int calculate_area(int length, int width) {
    int area = length * width;  // Local variable, clearly named
    return area;
}

// Avoid this
int x, y, z;  // Global variables - hard to track
int calc(int a, int b) {
    z = a * b;  // Modifying global state
    return z;
}
Module 2 : Functions in C

6. Bonus: Some C Library Functions

Throughout this module, we've focused on defining our own functions. However, C also provides a rich set of built-in functions, known as Standard Library Functions. These functions are pre-written and grouped into various libraries (e.g., <stdio.h>, <stdlib.h>, <math.h>) to perform common tasks like input/output, memory management, and mathematical operations. When you use functions like printf() or scanf(), you are actually calling functions that have been defined in the standard input/output library (stdio.h). These functions abstract away complex implementations, allowing you to use them easily by simply including their respective header files.

6.1 Mathematical Functions

Include <math.h> header for mathematical functions:

#include <stdio.h>
#include <math.h>

int main() {
    double angle = 45.0;
    double radians = angle * M_PI / 180.0;  // Convert to radians
    
    printf("Mathematical Functions:\n");
    printf("sqrt(16) = %.2f\n", sqrt(16.0));           // Square root
    printf("pow(2, 3) = %.2f\n", pow(2.0, 3.0));       // 2^3
    printf("sin(45°) = %.4f\n", sin(radians));         // Sine
    printf("cos(45°) = %.4f\n", cos(radians));         // Cosine
    printf("log(10) = %.4f\n", log(10.0));             // Natural log
    printf("log10(100) = %.2f\n", log10(100.0));       // Base-10 log
    printf("ceil(4.3) = %.0f\n", ceil(4.3));           // Ceiling
    printf("floor(4.7) = %.0f\n", floor(4.7));         // Floor
    printf("fabs(-5.5) = %.1f\n", fabs(-5.5));         // Absolute value
    
    return 0;
}

Note: When compiling programs that use <math.h>, you might need to link the math library:

gcc -o program program.c -lm

6.2 String Functions

Include <string.h> header for string manipulation:

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

int main() {
    char str1[50] = "Hello";
    char str2[50] = "World";
    char str3[100];
    
    // String length
    printf("Length of '%s': %lu\n", str1, strlen(str1));
    
    // String copy
    strcpy(str3, str1);
    printf("After strcpy: str3 = '%s'\n", str3);
    
    // String concatenation
    strcat(str3, " ");
    strcat(str3, str2);
    printf("After strcat: str3 = '%s'\n", str3);
    
    // String comparison
    if (strcmp(str1, "Hello") == 0) {
        printf("str1 equals 'Hello'\n");
    }
    
    return 0;
}

6.3 Character Functions

Include <ctype.h> for character testing and conversion:

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

int main() {
    char ch = 'A';
    
    printf("Character testing for '%c':\n", ch);
    printf("isalpha: %d\n", isalpha(ch));      // Is alphabetic?
    printf("isdigit: %d\n", isdigit(ch));      // Is digit?
    printf("isupper: %d\n", isupper(ch));      // Is uppercase?
    printf("islower: %d\n", islower(ch));      // Is lowercase?
    printf("isspace: %d\n", isspace(ch));      // Is whitespace?
    
    printf("Lowercase: %c\n", tolower(ch));    // Convert to lowercase
    printf("Uppercase: %c\n", toupper('a'));   // Convert to uppercase
    
    return 0;
}
Module 2 : Functions in C

7. Recursion

7.1 Understanding Recursion

Recursion is a programming technique where a function calls itself. Every recursive function needs:

  1. Base case: Condition that stops the recursion
  2. Recursive case: Function calls itself with modified parameters

7.2 Python vs C Recursion

Python Factorial:

def factorial(n):
    if n == 0 or n == 1:
        return 1
    else:
        return n * factorial(n - 1)

C Factorial:

#include <stdio.h>

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

int main() {
    int num = 5;
    printf("%d! = %d\n", num, factorial(num));  // Output: 5! = 120
    return 0;
}

7.3 How Recursion Works

Factorial(5) execution trace:

factorial(5) = 5 * factorial(4)
             = 5 * 4 * factorial(3)
             = 5 * 4 * 3 * factorial(2)
             = 5 * 4 * 3 * 2 * factorial(1)
             = 5 * 4 * 3 * 2 * 1
             = 120

7.4 More Recursive Examples

Fibonacci Sequence

#include <stdio.h>

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

int main() {
    printf("Fibonacci sequence:\n");
    for (int i = 0; i < 10; i++) {
        printf("F(%d) = %d\n", i, fibonacci(i));
    }
    return 0;
}

Power Calculation

#include <stdio.h>

double power(double base, int exponent) {
    // Base case
    if (exponent == 0) {
        return 1.0;
    }
    
    // Handle negative exponents
    if (exponent < 0) {
        return 1.0 / power(base, -exponent);
    }
    
    // Recursive case
    return base * power(base, exponent - 1);
}

int main() {
    printf("2^5 = %.2f\n", power(2.0, 5));    // Output: 32.00
    printf("3^-2 = %.4f\n", power(3.0, -2));  // Output: 0.1111
    return 0;
}

Sum of Array Elements (Preview for next module)

#include <stdio.h>

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

int main() {
    int numbers[] = {1, 2, 3, 4, 5};
    int array_size = 5;
    
    printf("Sum = %d\n", sum_array(numbers, array_size));  // Output: 15
    return 0;
}

7.5 Recursion vs Iteration

Advantages of Recursion:

Disadvantages of Recursion:

When to Use Recursion:

Module 2 : Functions in C

8. Function Examples and Applications

8.1 Menu-Driven Program

#include <stdio.h>

// Function prototypes
void display_menu(void);
int get_choice(void);
void calculator_add(void);
void calculator_multiply(void);
void display_table(int num);

int main() {
    int choice;
    
    do {
        display_menu();
        choice = get_choice();
        
        switch (choice) {
            case 1:
                calculator_add();
                break;
            case 2:
                calculator_multiply();
                break;
            case 3:
                printf("Enter number for multiplication table: ");
                int num;
                scanf("%d", &num);
                display_table(num);
                break;
            case 4:
                printf("Thank you for using the calculator!\n");
                break;
            default:
                printf("Invalid choice! Please try again.\n");
        }
        
        if (choice != 4) {
            printf("\nPress Enter to continue...");
            getchar();
            getchar();  // Clear buffer
        }
        
    } while (choice != 4);
    
    return 0;
}

void display_menu(void) {
    printf("\n=== SIMPLE CALCULATOR ===\n");
    printf("1. Addition\n");
    printf("2. Multiplication\n");
    printf("3. Multiplication Table\n");
    printf("4. Exit\n");
    printf("========================\n");
}

int get_choice(void) {
    int choice;
    printf("Enter your choice (1-4): ");
    scanf("%d", &choice);
    return choice;
}

void calculator_add(void) {
    float num1, num2;
    printf("Enter first number: ");
    scanf("%f", &num1);
    printf("Enter second number: ");
    scanf("%f", &num2);
    printf("Result: %.2f + %.2f = %.2f\n", num1, num2, num1 + num2);
}

void calculator_multiply(void) {
    float num1, num2;
    printf("Enter first number: ");
    scanf("%f", &num1);
    printf("Enter second number: ");
    scanf("%f", &num2);
    printf("Result: %.2f × %.2f = %.2f\n", num1, num2, num1 * num2);
}

void display_table(int num) {
    printf("\nMultiplication table for %d:\n", num);
    printf("-------------------------\n");
    for (int i = 1; i <= 10; i++) {
        printf("%d × %d = %d\n", num, i, num * i);
    }
}

8.2 Temperature Conversion Program

#include <stdio.h>

// Function prototypes
float celsius_to_fahrenheit(float celsius);
float fahrenheit_to_celsius(float fahrenheit);
float celsius_to_kelvin(float celsius);
float kelvin_to_celsius(float kelvin);
void display_conversion_table(void);

int main() {
    int choice;
    float temp, result;
    
    printf("=== TEMPERATURE CONVERTER ===\n");
    printf("1. Celsius to Fahrenheit\n");
    printf("2. Fahrenheit to Celsius\n");
    printf("3. Celsius to Kelvin\n");
    printf("4. Kelvin to Celsius\n");
    printf("5. Display Conversion Table\n");
    printf("============================\n");
    
    printf("Enter choice (1-5): ");
    scanf("%d", &choice);
    
    switch (choice) {
        case 1:
            printf("Enter temperature in Celsius: ");
            scanf("%f", &temp);
            result = celsius_to_fahrenheit(temp);
            printf("%.2f°C = %.2f°F\n", temp, result);
            break;
            
        case 2:
            printf("Enter temperature in Fahrenheit: ");
            scanf("%f", &temp);
            result = fahrenheit_to_celsius(temp);
            printf("%.2f°F = %.2f°C\n", temp, result);
            break;
            
        case 3:
            printf("Enter temperature in Celsius: ");
            scanf("%f", &temp);
            result = celsius_to_kelvin(temp);
            printf("%.2f°C = %.2fK\n", temp, result);
            break;
            
        case 4:
            printf("Enter temperature in Kelvin: ");
            scanf("%f", &temp);
            if (temp < 0) {
                printf("Error: Temperature cannot be below 0 Kelvin!\n");
            } else {
                result = kelvin_to_celsius(temp);
                printf("%.2fK = %.2f°C\n", temp, result);
            }
            break;
            
        case 5:
            display_conversion_table();
            break;
            
        default:
            printf("Invalid choice!\n");
    }
    
    return 0;
}

float celsius_to_fahrenheit(float celsius) {
    return (celsius * 9.0 / 5.0) + 32.0;
}

float fahrenheit_to_celsius(float fahrenheit) {
    return (fahrenheit - 32.0) * 5.0 / 9.0;
}

float celsius_to_kelvin(float celsius) {
    return celsius + 273.15;
}

float kelvin_to_celsius(float kelvin) {
    return kelvin - 273.15;
}

void display_conversion_table(void) {
    printf("\n=== TEMPERATURE CONVERSION TABLE ===\n");
    printf("Celsius  | Fahrenheit | Kelvin\n");
    printf("---------+------------+--------\n");
    
    for (int c = 0; c <= 100; c += 10) {
        float f = celsius_to_fahrenheit(c);
        float k = celsius_to_kelvin(c);
        printf("%8d | %10.1f | %6.1f\n", c, f, k);
    }
}
Module 2 : Functions in C

9. Common Errors and Debugging

9.1 Function Declaration Errors

Error 1: Missing Function Prototype

// ERROR: Function used before declaration
int main() {
    int result = add_numbers(5, 3);  // Error: 'add_numbers' not declared
    return 0;
}

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

Solution:

// CORRECT: Add function prototype
int add_numbers(int a, int b);  // Function prototype

int main() {
    int result = add_numbers(5, 3);  // Now it works
    return 0;
}

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

9.2 Return Type Mismatches

Error 2: Wrong Return Type

// ERROR: Function declared to return int but returns float
int divide(int a, int b) {
    return a / b;  // Integer division, loses decimal part
}

int main() {
    printf("Result: %d\n", divide(7, 2));  // Output: 3 (not 3.5)
    return 0;
}

Solution:

// CORRECT: Use appropriate return type
float divide(int a, int b) {
    return (float)a / b;  // Cast to float for proper division
}

int main() {
    printf("Result: %.2f\n", divide(7, 2));  // Output: 3.50
    return 0;
}

9.3 Parameter Issues

Error 3: Expecting Changes to Original Variables

void swap(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
    printf("Inside function: a=%d, b=%d\n", a, b);  // Values swapped
}

int main() {
    int x = 5, y = 10;
    swap(x, y);
    printf("In main: x=%d, y=%d\n", x, y);  // x=5, y=10 (unchanged!)
    return 0;
}

Understanding: This behavior is correct in C due to pass by value. To actually swap values, you would need pointers (weill be covered in module about "Pointer").

9.4 Debugging Techniques for Functions

1. Add Debug Prints:

int factorial(int n) {
    printf("DEBUG: factorial(%d) called\n", n);  // Debug output
    
    if (n == 0 || n == 1) {
        printf("DEBUG: base case reached, returning 1\n");
        return 1;
    } else {
        int result = n * factorial(n - 1);
        printf("DEBUG: factorial(%d) = %d\n", n, result);
        return result;
    }
}

2. Test Functions Independently:

// Test individual functions with known inputs
int main() {
    // Test factorial function
    assert(factorial(0) == 1);
    assert(factorial(1) == 1);
    assert(factorial(5) == 120);
    printf("All factorial tests passed!\n");
    
    return 0;
}

3. Check Function Signatures:

Module 3 : Array (Static)

By the end of this module, students will be able to:

- Understand the fundamental differences between Python lists and C arrays

- Declare and initialize static arrays with appropriate data types

- Access and manipulate array elements using indexing

- Implement common array operations (traversal, searching, sorting)

- Work with multi-dimensional arrays

- Apply string manipulation using character arrays

- Debug common array-related errors and memory issues

- Transition effectively from Python list operations to C array operations

Module 3 : Array (Static)

1. Introduction: From Python Lists to C Arrays

1.1 Key Differences Overview

Aspect Python Lists C Arrays
Size Dynamic (can grow/shrink) Fixed size (static)
Type Can store mixed types Single type only
Memory Automatic management Manual bounds checking
Declaration list = [1, 2, 3] int arr[5] = {1, 2, 3, 4, 5};
Bounds Checking Automatic (raises IndexError) No automatic checking
Performance Slower (overhead) Faster (direct memory access)

1.2 Why Arrays Matter in C

Memory Efficiency:

Performance:

Module 3 : Array (Static)

2. Array Declaration and Initialization

2.1 Basic Array Declaration

Python vs C Comparison:

Python C
numbers = [1, 2, 3, 4, 5] int numbers[5] = {1, 2, 3, 4, 5};
grades = [] float grades[100];
name = "Alice" char name[10] = "Alice";

C Array Declaration Syntax:

data_type array_name[size];
data_type array_name[size] = {value1, value2, ...};

2.2 Different Initialization Methods

2.2.1 Complete Initialization

int numbers[5] = {10, 20, 30, 40, 50};
char vowels[5] = {'a', 'e', 'i', 'o', 'u'};
float prices[3] = {12.5, 25.0, 8.75};

2.2.2 Partial Initialization

int scores[10] = {95, 87, 92};  // First 3 elements initialized
                                 // Remaining 7 elements = 0
char grades[5] = {'A', 'B'};     // grades[0]='A', grades[1]='B'
                                 // grades[2]=grades[3]=grades[4]='\0'

2.2.3 Size Inference

int data[] = {1, 2, 3, 4, 5};    // Size automatically becomes 5
char message[] = "Hello World";   // Size becomes 12 (including '\0')

2.2.4 Zero Initialization

int zeros[100] = {0};            // All elements initialized to 0
char buffer[50] = "";            // All characters initialized to '\0'

2.2.5 Uninitialized Arrays (Dangerous!)

int uninitialized[10];           // Contains garbage values!
// Always initialize arrays before use

2.3 Array Size and Memory

Understanding Array Size:

int numbers[5];                  // 5 integers × 4 bytes = 20 bytes
char name[20];                   // 20 characters × 1 byte = 20 bytes
double values[10];               // 10 doubles × 8 bytes = 80 bytes

// Getting array size at compile time
int size = sizeof(numbers) / sizeof(numbers[0]);  // Result: 5

Python vs C Size Operations:

Python C
len(list) sizeof(array) / sizeof(array[0])
list.append(item) Not possible with static arrays
list.pop() Not possible with static arrays
Module 3 : Array (Static)

3. Array Indexing and Access

3.1 Basic Indexing

Python vs C Indexing:

Operation Python C
First element list[0] array[0]
Last element list[-1] array[size-1]
Nth element list[n] array[n]
Modify element list[0] = 10 array[0] = 10;

3.1.1 Valid Indexing Example

int numbers[5] = {10, 20, 30, 40, 50};

printf("First element: %d\n", numbers[0]);    // Output: 10
printf("Third element: %d\n", numbers[2]);    // Output: 30
printf("Last element: %d\n", numbers[4]);     // Output: 50

// Modifying elements
numbers[1] = 99;
printf("Modified second element: %d\n", numbers[1]);  // Output: 99

3.1.2 Index Bounds and Common Errors

Critical Difference from Python:

# Python - Safe bounds checking
my_list = [1, 2, 3, 4, 5]
print(my_list[10])  # Raises IndexError: list index out of range
// C - NO automatic bounds checking!
int my_array[5] = {1, 2, 3, 4, 5};
printf("%d\n", my_array[10]);  // Undefined behavior! May print garbage
my_array[10] = 999;           // Buffer overflow! May crash program
  1. Off-by-One Error:
int arr[5] = {1, 2, 3, 4, 5};
// WRONG: Accessing index 5 (valid indices: 0-4)
for (int i = 0; i <= 5; i++) {    // ERROR: i goes up to 5
    printf("%d ", arr[i]);
}

// CORRECT:
for (int i = 0; i < 5; i++) {     // i goes from 0 to 4
    printf("%d ", arr[i]);
}
  1. Negative Index Error:
int arr[5] = {1, 2, 3, 4, 5};
int index = -1;
printf("%d\n", arr[index]);  // Undefined behavior! C has no negative indexing
  1. Uninitialized Index:
int arr[10];
int i;                      // Uninitialized variable
printf("%d\n", arr[i]);     // Using uninitialized i as index - dangerous!

3.2 Safe Array Access Patterns

3.2.1 Bounds Checking Function

#include <stdio.h>
#include <stdbool.h>

bool is_valid_index(int index, int array_size) {
    return (index >= 0 && index < array_size);
}

int safe_access(int arr[], int size, int index) {
    if (is_valid_index(index, size)) {
        return arr[index];
    } else {
        printf("Error: Index %d out of bounds (0-%d)\n", index, size-1);
        return -1;  // Return error value
    }
}

int main() {
    int numbers[5] = {10, 20, 30, 40, 50};
    
    printf("Safe access: %d\n", safe_access(numbers, 5, 2));   // Valid
    printf("Safe access: %d\n", safe_access(numbers, 5, 10));  // Invalid
    
    return 0;
}
Module 3 : Array (Static)

4. Array Input and Output Operations

4.1 Reading Array Elements

4.1.1 Reading with Known Size

#include <stdio.h>

int main() {
    int numbers[5];
    
    printf("Enter 5 integers:\n");
    for (int i = 0; i < 5; i++) {
        printf("Element %d: ", i + 1);
        scanf("%d", &numbers[i]);
    }
    
    printf("You entered: ");
    for (int i = 0; i < 5; i++) {
        printf("%d ", numbers[i]);
    }
    printf("\n");
    
    return 0;
}

4.1.2 Reading with User-Specified Size

#include <stdio.h>
#define MAX_SIZE 100

int main() {
    int arr[MAX_SIZE];
    int n;
    
    printf("How many numbers do you want to enter (max %d): ", MAX_SIZE);
    scanf("%d", &n);
    
    // Input validation
    if (n <= 0 || n > MAX_SIZE) {
        printf("Invalid size! Please enter between 1 and %d\n", MAX_SIZE);
        return 1;
    }
    
    printf("Enter %d numbers:\n", n);
    for (int i = 0; i < n; i++) {
        printf("Number %d: ", i + 1);
        scanf("%d", &arr[i]);
    }
    
    printf("Your numbers: ");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
    
    return 0;
}

4.2 Displaying Array Elements

4.2.1 Basic Display

void print_array(int arr[], int size) {
    printf("Array contents: ");
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

4.2.2 Formatted Display

void print_array_formatted(int arr[], int size) {
    printf("┌");
    for (int i = 0; i < size; i++) {
        printf("────┬");
    }
    printf("\b┐\n");  // Backspace to replace last ┬ with ┐
    
    printf("│");
    for (int i = 0; i < size; i++) {
        printf("%3d │", arr[i]);
    }
    printf("\n");
    
    printf("└");
    for (int i = 0; i < size; i++) {
        printf("────┴");
    }
    printf("\b┘\n");
    
    printf(" ");
    for (int i = 0; i < size; i++) {
        printf("%3d  ", i);
    }
    printf("\n");
}
Module 3 : Array (Static)

5. Common Array Operations

5.1 Array Traversal

5.1.1 Forward Traversal

// Python equivalent: for item in list:
for (int i = 0; i < size; i++) {
    printf("%d ", arr[i]);
}

5.1.2 Reverse Traversal

// Python equivalent: for item in reversed(list):
for (int i = size - 1; i >= 0; i--) {
    printf("%d ", arr[i]);
}

5.1.3 Traversal with Condition

// Print only even numbers
for (int i = 0; i < size; i++) {
    if (arr[i] % 2 == 0) {
        printf("%d ", arr[i]);
    }
}

5.2 Finding Maximum and Minimum

Python vs C Comparison:

Python C
max(list) Manual implementation
min(list) Manual implementation

5.2.1 Finding Maximum

int find_max(int arr[], int size) {
    if (size <= 0) {
        printf("Error: Empty array\n");
        return INT_MIN;  // Return minimum integer value
    }
    
    int max = arr[0];  // Assume first element is maximum
    
    for (int i = 1; i < size; i++) {
        if (arr[i] > max) {
            max = arr[i];
        }
    }
    
    return max;
}

5.2.2 Finding Minimum

int find_min(int arr[], int size) {
    if (size <= 0) {
        printf("Error: Empty array\n");
        return INT_MAX;  // Return maximum integer value
    }
    
    int min = arr[0];
    
    for (int i = 1; i < size; i++) {
        if (arr[i] < min) {
            min = arr[i];
        }
    }
    
    return min;
}

5.2.3 Finding Both Max and Min with Position

#include <stdio.h>

typedef struct {
    int max_value;
    int max_index;
    int min_value;
    int min_index;
} MinMaxResult;

MinMaxResult find_min_max(int arr[], int size) {
    MinMaxResult result = {arr[0], 0, arr[0], 0};
    
    for (int i = 1; i < size; i++) {
        if (arr[i] > result.max_value) {
            result.max_value = arr[i];
            result.max_index = i;
        }
        if (arr[i] < result.min_value) {
            result.min_value = arr[i];
            result.min_index = i;
        }
    }
    
    return result;
}

5.3 Searching in Arrays

int linear_search(int arr[], int size, int target) {
    for (int i = 0; i < size; i++) {
        if (arr[i] == target) {
            return i;  // Return index of found element
        }
    }
    return -1;  // Element not found
}

// Usage example
int main() {
    int numbers[] = {10, 25, 8, 42, 15};
    int size = sizeof(numbers) / sizeof(numbers[0]);
    int target = 42;
    
    int index = linear_search(numbers, size, target);
    
    if (index != -1) {
        printf("Element %d found at index %d\n", target, index);
    } else {
        printf("Element %d not found\n", target);
    }
    
    return 0;
}

5.3.2 Count Occurrences

int count_occurrences(int arr[], int size, int target) {
    int count = 0;
    
    for (int i = 0; i < size; i++) {
        if (arr[i] == target) {
            count++;
        }
    }
    
    return count;
}

5.3.3 Find All Occurrences

#include <stdio.h>
#define MAX_INDICES 100

int find_all_occurrences(int arr[], int size, int target, int indices[]) {
    int count = 0;
    
    for (int i = 0; i < size && count < MAX_INDICES; i++) {
        if (arr[i] == target) {
            indices[count] = i;
            count++;
        }
    }
    
    return count;  // Number of occurrences found
}

// Usage example
int main() {
    int numbers[] = {1, 3, 7, 3, 9, 3, 2};
    int size = sizeof(numbers) / sizeof(numbers[0]);
    int target = 3;
    int indices[MAX_INDICES];
    
    int count = find_all_occurrences(numbers, size, target, indices);
    
    printf("Element %d found %d times at indices: ", target, count);
    for (int i = 0; i < count; i++) {
        printf("%d ", indices[i]);
    }
    printf("\n");
    
    return 0;
}

5.4 Array Modification Operations

5.4.1 Filling Arrays

void fill_array(int arr[], int size, int value) {
    for (int i = 0; i < size; i++) {
        arr[i] = value;
    }
}

// Fill with sequential numbers
void fill_sequence(int arr[], int size, int start) {
    for (int i = 0; i < size; i++) {
        arr[i] = start + i;
    }
}

5.4.2 Copying Arrays

void copy_array(int source[], int destination[], int size) {
    for (int i = 0; i < size; i++) {
        destination[i] = source[i];
    }
}

// Usage
int main() {
    int original[] = {1, 2, 3, 4, 5};
    int copy[5];
    
    copy_array(original, copy, 5);
    
    return 0;
}

5.4.3 Reversing Arrays

void reverse_array(int arr[], int size) {
    for (int i = 0; i < size / 2; i++) {
        // Swap elements
        int temp = arr[i];
        arr[i] = arr[size - 1 - i];
        arr[size - 1 - i] = temp;
    }
}
Module 3 : Array (Static)

6. Mathematical Operations on Arrays

6.1 Statistical Operations

6.1.1 Sum and Average

#include <stdio.h>

int sum_array(int arr[], int size) {
    int sum = 0;
    for (int i = 0; i < size; i++) {
        sum += arr[i];
    }
    return sum;
}

double average_array(int arr[], int size) {
    if (size == 0) return 0.0;
    return (double)sum_array(arr, size) / size;
}

// Usage
int main() {
    int scores[] = {85, 92, 78, 96, 88};
    int size = 5;
    
    int total = sum_array(scores, size);
    double avg = average_array(scores, size);
    
    printf("Total: %d\n", total);
    printf("Average: %.2f\n", avg);
    
    return 0;
}

6.1.2 Standard Deviation

#include <math.h>

double standard_deviation(int arr[], int size) {
    if (size <= 1) return 0.0;
    
    double mean = average_array(arr, size);
    double sum_squared_diff = 0.0;
    
    for (int i = 0; i < size; i++) {
        double diff = arr[i] - mean;
        sum_squared_diff += diff * diff;
    }
    
    return sqrt(sum_squared_diff / (size - 1));
}

6.2 Array Comparison

6.2.1 Check if Arrays are Equal

#include <stdbool.h>

bool arrays_equal(int arr1[], int arr2[], int size) {
    for (int i = 0; i < size; i++) {
        if (arr1[i] != arr2[i]) {
            return false;
        }
    }
    return true;
}

6.2.2 Element-wise Operations

void add_arrays(int arr1[], int arr2[], int result[], int size) {
    for (int i = 0; i < size; i++) {
        result[i] = arr1[i] + arr2[i];
    }
}

void multiply_array_scalar(int arr[], int size, int scalar) {
    for (int i = 0; i < size; i++) {
        arr[i] *= scalar;
    }
}
Module 3 : Array (Static)

7. Character Arrays and Strings

7.1 Character Arrays vs Strings

Understanding C Strings: In C, strings are arrays of characters terminated by a null character ('\0').

// Character array (not necessarily a string)
char letters[5] = {'H', 'e', 'l', 'l', 'o'};

// String (null-terminated character array)
char greeting[6] = {'H', 'e', 'l', 'l', 'o', '\0'};

// Easier string initialization
char message[] = "Hello";  // Automatically adds '\0'
char name[20] = "Alice";   // name[0]='A', name[1]='l', ..., name[5]='\0'

7.2 String Input/Output

7.2.1 String Input Methods

#include <stdio.h>

int main() {
    char name[50];
    
    // Method 1: scanf (stops at whitespace)
    printf("Enter your first name: ");
    scanf("%s", name);  // No & needed for arrays
    
    // Method 2: fgets (reads entire line)
    printf("Enter your full name: ");
    fgets(name, sizeof(name), stdin);
    
    // Method 3: scanf with character set
    printf("Enter your name: ");
    scanf("%[^\n]", name);  // Read until newline
    
    printf("Hello, %s!\n", name);
    return 0;
}

7.2.2 String Output

char message[] = "Programming in C";

// Method 1: printf with %s
printf("Message: %s\n", message);

// Method 2: puts (automatically adds newline)
puts(message);

// Method 3: Character by character
for (int i = 0; message[i] != '\0'; i++) {
    printf("%c", message[i]);
}
printf("\n");

7.3 String Manipulation Functions

7.3.1 String Length

#include <string.h>

// Using library function
int len = strlen(str);

// Manual implementation
int string_length(char str[]) {
    int length = 0;
    while (str[length] != '\0') {
        length++;
    }
    return length;
}

7.3.2 String Copy

#include <string.h>

// Using library function
strcpy(destination, source);

// Manual implementation
void string_copy(char dest[], char src[]) {
    int i = 0;
    while (src[i] != '\0') {
        dest[i] = src[i];
        i++;
    }
    dest[i] = '\0';  // Don't forget null terminator!
}

7.3.3 String Concatenation

#include <string.h>

// Using library function
strcat(destination, source);

// Manual implementation
void string_concatenate(char dest[], char src[]) {
    int dest_len = string_length(dest);
    int i = 0;
    
    while (src[i] != '\0') {
        dest[dest_len + i] = src[i];
        i++;
    }
    dest[dest_len + i] = '\0';
}

7.3.4 String Comparison

#include <string.h>

// Using library function
int result = strcmp(str1, str2);
// Returns: 0 if equal, <0 if str1 < str2, >0 if str1 > str2

// Manual implementation
int string_compare(char str1[], char str2[]) {
    int i = 0;
    while (str1[i] != '\0' && str2[i] != '\0') {
        if (str1[i] < str2[i]) return -1;
        if (str1[i] > str2[i]) return 1;
        i++;
    }
    
    if (str1[i] == '\0' && str2[i] == '\0') return 0;
    return (str1[i] == '\0') ? -1 : 1;
}

7.4 Common String Operations

7.4.1 Count Characters/Words

int count_character(char str[], char ch) {
    int count = 0;
    for (int i = 0; str[i] != '\0'; i++) {
        if (str[i] == ch) {
            count++;
        }
    }
    return count;
}

int count_words(char str[]) {
    int words = 0;
    bool in_word = false;
    
    for (int i = 0; str[i] != '\0'; i++) {
        if (str[i] != ' ' && str[i] != '\t' && str[i] != '\n') {
            if (!in_word) {
                words++;
                in_word = true;
            }
        } else {
            in_word = false;
        }
    }
    
    return words;
}

7.4.2 String Reversal

void reverse_string(char str[]) {
    int len = string_length(str);
    
    for (int i = 0; i < len / 2; i++) {
        char temp = str[i];
        str[i] = str[len - 1 - i];
        str[len - 1 - i] = temp;
    }
}

7.4.3 Case Conversion

#include <ctype.h>

void to_uppercase(char str[]) {
    for (int i = 0; str[i] != '\0'; i++) {
        str[i] = toupper(str[i]);
    }
}

void to_lowercase(char str[]) {
    for (int i = 0; str[i] != '\0'; i++) {
        str[i] = tolower(str[i]);
    }
}

// Manual implementation for uppercase
void manual_to_uppercase(char str[]) {
    for (int i = 0; str[i] != '\0'; i++) {
        if (str[i] >= 'a' && str[i] <= 'z') {
            str[i] = str[i] - 'a' + 'A';
        }
    }
}
Module 3 : Array (Static)

8. Multi-dimensional Arrays

8.1 Two-Dimensional Arrays

8.1.1 Declaration and Initialization

// Declaration
int matrix[3][4];  // 3 rows, 4 columns

// Initialization methods
int grid[2][3] = {
    {1, 2, 3},
    {4, 5, 6}
};

// Alternative initialization
int numbers[2][3] = {{1, 2, 3}, {4, 5, 6}};

// Partial initialization
int scores[3][2] = {
    {95, 87},
    {92, 78},
    {88}      // Last element becomes 0
};

8.1.2 Accessing 2D Array Elements

int matrix[3][4] = {
    {1,  2,  3,  4},
    {5,  6,  7,  8},
    {9, 10, 11, 12}
};

// Accessing elements
printf("Element at row 1, column 2: %d\n", matrix[1][2]);  // Output: 7

// Modifying elements
matrix[0][0] = 100;
matrix[2][3] = 999;

8.1.3 Input/Output for 2D Arrays

#include <stdio.h>

void input_matrix(int matrix[][4], int rows) {
    printf("Enter matrix elements:\n");
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 4; j++) {
            printf("Element [%d][%d]: ", i, j);
            scanf("%d", &matrix[i][j]);
        }
    }
}

void print_matrix (int matrix[][4], int rows) {
    printf("Matrix:\n");
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%d ", matrix[i][j]);
        }
        printf("\n");
    }
}

Module 4 : Pointers & Dynamic Array

By the end of this module, students will be able to:

Module 4 : Pointers & Dynamic Array

1. Introduction to Pointers

1.1 What are Pointers?

A pointer is a variable that stores the memory address of another variable. Instead of holding a data value directly, a pointer "points to" the location in memory where the data is stored.

Analogy: Think of computer memory like a street with houses:

Why Use Pointers?

  1. Dynamic Memory Allocation: Create variables at runtime
  2. Efficient Array/String Manipulation: Access elements without copying
  3. Function Parameter Passing: Modify variables from within functions
  4. Data Structures: Build linked lists, trees, graphs, etc.
  5. System Programming: Direct memory access and hardware interaction

1.2 Memory Addresses

Every variable in C is stored at a specific memory location, identified by a unique address.

#include <stdio.h>

int main() {
    int age = 25;
    float height = 5.9f;
    char grade = 'A';
    
    printf("Value of age: %d\n", age);
    printf("Address of age: %p\n", (void*)&age);
    
    printf("Value of height: %.1f\n", height);
    printf("Address of height: %p\n", (void*)&height);
    
    printf("Value of grade: %c\n", grade);
    printf("Address of grade: %p\n", (void*)&grade);
    
    return 0;
}

/* Output (addresses will vary):
Value of age: 25
Address of age: 0x7ffd5c8e4a3c
Value of height: 5.9
Address of height: 0x7ffd5c8e4a38
Value of grade: A
Address of grade: 0x7ffd5c8e4a37
*/

Key Points:

Module 4 : Pointers & Dynamic Array

2. Pointer Basics

2.1 Declaring Pointers

Syntax:

data_type *pointer_name;

Examples:

int *ptr;        // Pointer to an integer
float *fptr;     // Pointer to a float
char *cptr;      // Pointer to a character
double *dptr;    // Pointer to a double

Important Notes:

Multiple Pointer Declaration:

int *p1, *p2, *p3;    // Three pointers to int
int *p1, p2, *p3;     // p1 and p3 are pointers, p2 is int
int* p1, p2, p3;      // Only p1 is pointer! p2 and p3 are int

2.2 Pointer Operators

There are two main operators for working with pointers:

Operator Name Description Example
& Address-of Gets the memory address of a variable &variable
* Dereference Accesses the value at the address stored in pointer *pointer

2.3 Initializing Pointers

Method 1: Initialize with address of existing variable

int num = 42;
int *ptr = &num;  // ptr now points to num

Method 2: Initialize to NULL

int *ptr = NULL;  // Pointer points to nothing (safe initialization)

Method 3: Uninitialized (DANGEROUS)

int *ptr;  // Contains garbage value - DO NOT USE until initialized!

Visual Representation:

Memory Layout:

Variable: num = 42
Address:  0x1000
         ┌──────┐
0x1000:  │  42  │  num
         └──────┘

Variable: ptr
Address:  0x2000
         ┌────────┐
0x2000:  │ 0x1000 │  ptr (points to num)
         └────────┘

2.4 Using Pointers - The & and * Operators

#include <stdio.h>

int main() {
    int num = 100;
    int *ptr;
    
    ptr = &num;  // Store address of num in ptr
    
    printf("Value of num: %d\n", num);           // Direct access
    printf("Address of num: %p\n", (void*)&num); // Address of num
    printf("Value of ptr: %p\n", (void*)ptr);    // Address stored in ptr
    printf("Value pointed to by ptr: %d\n", *ptr); // Dereference ptr
    
    // Modify through pointer
    *ptr = 200;
    
    printf("\nAfter *ptr = 200:\n");
    printf("Value of num: %d\n", num);           // num changed!
    printf("Value pointed to by ptr: %d\n", *ptr);
    
    return 0;
}

/* Output:
Value of num: 100
Address of num: 0x7ffd5c8e4a3c
Value of ptr: 0x7ffd5c8e4a3c
Value pointed to by ptr: 100

After *ptr = 200:
Value of num: 200
Value pointed to by ptr: 200
*/

Understanding the Operations:

int num = 42;
int *ptr = &num;

// These are equivalent:
num = 100;     // Direct modification
*ptr = 100;    // Indirect modification through pointer

// Both operations change the same memory location
Module 4 : Pointers & Dynamic Array

3. Pointer Arithmetics

3. Pointer Arithmetic

Pointers can be incremented, decremented, and compared. When you perform arithmetic on pointers, the operation takes into account the size of the data type being pointed to.

3.1 Basic Pointer Arithmetic Operations

Operation Description Example
ptr++ Move to next element ptr = ptr + 1
ptr-- Move to previous element ptr = ptr - 1
ptr + n Move n elements forward ptr = ptr + 3
ptr - n Move n elements backward ptr = ptr - 2
ptr2 - ptr1 Distance between pointers Number of elements

3.2 How Pointer Arithmetic Works

When you add 1 to a pointer, it doesn't increase by 1 byte—it increases by the size of the data type:

#include <stdio.h>

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int *ptr = arr;  // Points to first element
    
    printf("Address of ptr: %p, Value: %d\n", (void*)ptr, *ptr);
    
    ptr++;  // Move to next integer (adds sizeof(int) bytes)
    printf("Address of ptr: %p, Value: %d\n", (void*)ptr, *ptr);
    
    ptr++;  // Move to next integer
    printf("Address of ptr: %p, Value: %d\n", (void*)ptr, *ptr);
    
    return 0;
}

/* Output (addresses will vary):
Address of ptr: 0x7ffd5c8e4a20, Value: 10
Address of ptr: 0x7ffd5c8e4a24, Value: 20  (increased by 4 bytes for int)
Address of ptr: 0x7ffd5c8e4a28, Value: 30  (increased by 4 bytes again)
*/

Memory Layout:

Array: arr[] = {10, 20, 30, 40, 50}

       ┌────┬────┬────┬────┬────┐
       │ 10 │ 20 │ 30 │ 40 │ 50 │
       └────┴────┴────┴────┴────┘
        ↑    ↑    ↑
      ptr  ptr+1 ptr+2
     0x100 0x104 0x108  (assuming 4-byte int)

3.3 Pointer Arithmetic Examples

#include <stdio.h>

int main() {
    int numbers[] = {100, 200, 300, 400, 500};
    int *ptr = numbers;
    
    // Access elements using pointer arithmetic
    printf("First element: %d\n", *ptr);           // 100
    printf("Second element: %d\n", *(ptr + 1));    // 200
    printf("Third element: %d\n", *(ptr + 2));     // 300
    printf("Fifth element: %d\n", *(ptr + 4));     // 500
    
    // Equivalent array notation
    printf("\nUsing array notation:\n");
    printf("First element: %d\n", ptr[0]);         // 100
    printf("Second element: %d\n", ptr[1]);        // 200
    printf("Third element: %d\n", ptr[2]);         // 300
    
    // Distance between pointers
    int *start = &numbers[0];
    int *end = &numbers[4];
    printf("\nDistance between pointers: %ld elements\n", end - start);
    
    return 0;
}

3.4 Valid and Invalid Pointer Operations

Valid Operations:

int arr[5];
int *ptr = arr;

ptr++;           // Valid: increment pointer
ptr--;           // Valid: decrement pointer
ptr = ptr + 3;   // Valid: add integer to pointer
ptr = ptr - 2;   // Valid: subtract integer from pointer
int diff = ptr2 - ptr1;  // Valid: subtract two pointers (same type)
if (ptr1 < ptr2) { }     // Valid: compare pointers

Invalid Operations:

ptr = ptr * 2;    // INVALID: cannot multiply pointers
ptr = ptr / 2;    // INVALID: cannot divide pointers
ptr = ptr + ptr2; // INVALID: cannot add two pointers
Module 4 : Pointers & Dynamic Array

4. Pointers and Arrays

Arrays and pointers have a very close relationship in C. In many contexts, an array name acts as a pointer to its first element.

4.1 Array Name as Pointer

#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    
    // Array name is a pointer to first element
    printf("Address of arr: %p\n", (void*)arr);
    printf("Address of arr[0]: %p\n", (void*)&arr[0]);
    printf("These addresses are the same!\n\n");
    
    // Access elements using pointer notation
    printf("arr[0] = %d, *arr = %d\n", arr[0], *arr);
    printf("arr[1] = %d, *(arr+1) = %d\n", arr[1], *(arr + 1));
    printf("arr[2] = %d, *(arr+2) = %d\n", arr[2], *(arr + 2));
    
    return 0;
}

4.2 Relationship Between Arrays and Pointers

Key Equivalences:

int arr[5] = {10, 20, 30, 40, 50};
int *ptr = arr;

// These are equivalent:
arr[i]  ≡  *(arr + i)
arr[i]  ≡  ptr[i]
arr[i]  ≡  *(ptr + i)
&arr[i] ≡  (arr + i)
&arr[i] ≡  (ptr + i)

Visual Representation:

Array: arr[5] = {10, 20, 30, 40, 50}

Index:     0    1    2    3    4
         ┌────┬────┬────┬────┬────┐
arr -->  │ 10 │ 20 │ 30 │ 40 │ 50 │
         └────┴────┴────┴────┴────┘
          ↑
        arr, &arr[0], arr+0, *(arr+0)
               ↑
          arr+1, &arr[1], *(arr+1)
                    ↑
               arr+2, &arr[2], *(arr+2)

4.3 Traversing Arrays with Pointers

Method 1: Using array indexing

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

for (int i = 0; i < 5; i++) {
    printf("%d ", arr[i]);
}

Method 2: Using pointer arithmetic

int arr[5] = {10, 20, 30, 40, 50};
int *ptr = arr;

for (int i = 0; i < 5; i++) {
    printf("%d ", *(ptr + i));
}

Method 3: Incrementing pointer

int arr[5] = {10, 20, 30, 40, 50};
int *ptr = arr;
int *end = arr + 5;

while (ptr < end) {
    printf("%d ", *ptr);
    ptr++;
}

4.4 Important Difference: Array vs Pointer

int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;

// This is VALID:
ptr = ptr + 1;  // ptr can be modified
ptr++;          // ptr can be incremented

// This is INVALID:
arr = arr + 1;  // ERROR! Array name is a constant pointer
arr++;          // ERROR! Cannot modify array name

// However, this is valid:
int *ptr2 = arr + 1;  // Create new pointer pointing to arr[1]

Key Difference:

Module 4 : Pointers & Dynamic Array

5. Pointers and Functions

Pointers are essential for functions to modify variables from the calling code and to work efficiently with arrays.

5.1 Pass by Value vs Pass by Reference

Pass by Value (Without Pointers):

#include <stdio.h>

void tryToChange(int x) {
    x = 100;  // Only changes local copy
    printf("Inside function: x = %d\n", x);
}

int main() {
    int num = 50;
    printf("Before function: num = %d\n", num);
    tryToChange(num);
    printf("After function: num = %d\n", num);  // num unchanged!
    return 0;
}

/* Output:
Before function: num = 50
Inside function: x = 100
After function: num = 50
*/

Pass by Reference (With Pointers):

#include <stdio.h>

void actuallyChange(int *x) {
    *x = 100;  // Changes the original variable
    printf("Inside function: *x = %d\n", *x);
}

int main() {
    int num = 50;
    printf("Before function: num = %d\n", num);
    actuallyChange(&num);  // Pass address
    printf("After function: num = %d\n", num);  // num changed!
    return 0;
}

/* Output:
Before function: num = 50
Inside function: *x = 100
After function: num = 100
*/

5.2 Why scanf() Requires &

Now we understand why scanf() needs the & operator:

int age;
scanf("%d", &age);  // Pass address so scanf can modify age

// What happens inside scanf (simplified):
void scanf(const char *format, int *ptr) {
    // Read input value
    int value = /* read from keyboard */;
    *ptr = value;  // Store in the address provided
}

5.3 Functions Returning Multiple Values

Since C functions can only return one value, pointers allow us to "return" multiple values:

#include <stdio.h>

// Function to find both quotient and remainder
void divide(int dividend, int divisor, int *quotient, int *remainder) {
    *quotient = dividend / divisor;
    *remainder = dividend % divisor;
}

int main() {
    int a = 17, b = 5;
    int q, r;
    
    divide(a, b, &q, &r);
    
    printf("%d divided by %d:\n", a, b);
    printf("Quotient: %d\n", q);
    printf("Remainder: %d\n", r);
    
    return 0;
}

5.4 Swapping Values Using Pointers

A classic example of pointer usage:

#include <stdio.h>

// WRONG: This doesn't swap the original variables
void wrongSwap(int x, int y) {
    int temp = x;
    x = y;
    y = temp;
}

// CORRECT: This swaps the original variables
void correctSwap(int *x, int *y) {
    int temp = *x;
    *x = *y;
    *y = temp;
}

int main() {
    int a = 10, b = 20;
    
    printf("Before swap: a = %d, b = %d\n", a, b);
    wrongSwap(a, b);
    printf("After wrongSwap: a = %d, b = %d\n", a, b);  // No change
    
    correctSwap(&a, &b);
    printf("After correctSwap: a = %d, b = %d\n", a, b);  // Swapped!
    
    return 0;
}

5.5 Passing Arrays to Functions

When passing arrays to functions, you're actually passing a pointer:

#include <stdio.h>

// These function declarations are equivalent:
void printArray1(int arr[], int size);
void printArray2(int *arr, int size);

void printArray1(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main() {
    int numbers[] = {10, 20, 30, 40, 50};
    int size = sizeof(numbers) / sizeof(numbers[0]);
    
    printArray1(numbers, size);
    
    return 0;
}

Important Note:

void function(int arr[]) {
    // Inside function, sizeof(arr) gives size of pointer, not array!
    int size = sizeof(arr);  // This gives 8 (size of pointer on 64-bit)
    // WRONG! Does not give array size
}

int main() {
    int numbers[5] = {1, 2, 3, 4, 5};
    int size = sizeof(numbers) / sizeof(numbers[0]);  // This is correct
    function(numbers);
    return 0;
}
Module 4 : Pointers & Dynamic Array

6. Pointers and Strings

In C, strings are arrays of characters, so pointers work naturally with strings.

6.1 String Representation

char str1[] = "Hello";     // Array notation
char *str2 = "Hello";      // Pointer notation

// Both represent the same thing in memory:
// 'H' 'e' 'l' 'l' 'o' '\0'

Memory Layout:

str1: char array (modifiable)
┌───┬───┬───┬───┬───┬────┐
│ H │ e │ l │ l │ o │ \0 │
└───┴───┴───┴───┴───┴────┘

str2: pointer to string literal (read-only)
     ┌───────┐
str2 │ addr  │────> "Hello\0" (in read-only memory)
     └───────┘

6.2 String Traversal Using Pointers

#include <stdio.h>

void printString(char *str) {
    while (*str != '\0') {  // Until null terminator
        printf("%c", *str);
        str++;  // Move to next character
    }
    printf("\n");
}

int main() {
    char message[] = "Hello, World!";
    printString(message);
    return 0;
}

6.3 String Length Using Pointers

#include <stdio.h>

int stringLength(char *str) {
    int length = 0;
    while (*str != '\0') {
        length++;
        str++;
    }
    return length;
}

// Alternative using pointer arithmetic
int stringLength2(char *str) {
    char *start = str;
    while (*str != '\0') {
        str++;
    }
    return str - start;  // Pointer subtraction
}

int main() {
    char text[] = "Programming";
    printf("Length: %d\n", stringLength(text));
    return 0;
}

6.4 String Copy Using Pointers

#include <stdio.h>

void stringCopy(char *dest, char *src) {
    while (*src != '\0') {
        *dest = *src;
        dest++;
        src++;
    }
    *dest = '\0';  // Don't forget null terminator!
}

// More elegant version
void stringCopy2(char *dest, char *src) {
    while ((*dest++ = *src++));  // Copy until '\0' (which is 0/false)
}

int main() {
    char source[] = "Hello";
    char destination[50];
    
    stringCopy(destination, source);
    printf("Copied string: %s\n", destination);
    
    return 0;
}
Module 4 : Pointers & Dynamic Array

7. Dynamic Memory Allocation & Array

7.1 Introduction to Dynamic Memory

Up until now, we've used static memory allocation where array sizes must be known at compile time:

int arr[100];  // Size fixed at compile time

Problems with static allocation:

Dynamic memory allocation solves these problems by allowing you to allocate memory at runtime using special functions.

7.2 Memory Layout in C

┌─────────────────────────────────┐
│         Stack                   │  ← Local variables, function calls
│  (grows downward)               │     Automatically managed
│            ↓                    │
├─────────────────────────────────┤
│                                 │
│         (free space)            │
│                                 │
│            ↑                    │
├─────────────────────────────────┤
│         Heap                    │  ← Dynamic memory allocation
│  (grows upward)                 │     Manually managed (malloc/free)
├─────────────────────────────────┤
│    Data Segment                 │  ← Global and static variables
├─────────────────────────────────┤
│    Code Segment                 │  ← Program instructions
└─────────────────────────────────┘

7.3 Dynamic Memory Functions

C provides four main functions for dynamic memory management (defined in <stdlib.h>):

Function Purpose Syntax
malloc() Allocates memory void* malloc(size_t size)
calloc() Allocates and initializes to zero void* calloc(size_t n, size_t size)
realloc() Resizes allocated memory void* realloc(void* ptr, size_t size)
free() Releases allocated memory void free(void* ptr)

7.4 malloc() - Memory Allocation

Purpose: Allocates a block of memory of specified size (in bytes)

Syntax:

void* malloc(size_t size);

Returns:

Example:

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

int main() {
    int *ptr;
    int n = 5;
    
    // Allocate memory for 5 integers
    ptr = (int*)malloc(n * sizeof(int));
    
    // Check if allocation was successful
    if (ptr == NULL) {
        printf("Memory allocation failed!\n");
        return 1;
    }
    
    // Use the allocated memory
    for (int i = 0; i < n; i++) {
        ptr[i] = i * 10;
    }
    
    // Print values
    printf("Values: ");
    for (int i = 0; i < n; i++) {
        printf("%d ", ptr[i]);
    }
    printf("\n");
    
    // Free the allocated memory
    free(ptr);
    ptr = NULL;  // Good practice
    
    return 0;
}

Important Notes:

7.5 calloc() - Contiguous Allocation

Purpose: Allocates memory and initializes all bytes to zero

Syntax:

void* calloc(size_t n, size_t size);

Parameters:

Example:

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

int main() {
    int *ptr;
    int n = 5;
    
    // Allocate and initialize memory for 5 integers
    ptr = (int*)calloc(n, sizeof(int));
    
    if (ptr == NULL) {
        printf("Memory allocation failed!\n");
        return 1;
    }
    
    // Print values (all will be 0)
    printf("Initial values: ");
    for (int i = 0; i < n; i++) {
        printf("%d ", ptr[i]);
    }
    printf("\n");
    
    free(ptr);
    ptr = NULL;
    
    return 0;
}

/* Output:
Initial values: 0 0 0 0 0
*/

malloc() vs calloc():

Feature malloc() calloc()
Parameters 1 (total bytes) 2 (number, size)
Initialization Garbage values All zeros
Speed Faster Slightly slower
Use case When you'll initialize values When you need zero-initialized memory

7.6 realloc() - Resize Memory

Purpose: Changes the size of previously allocated memory

Syntax:

void* realloc(void* ptr, size_t new_size);

Example:

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

int main() {
    int *ptr;
    int n = 5;
    
    // Initial allocation
    ptr = (int*)malloc(n * sizeof(int));
    if (ptr == NULL) {
        printf("Memory allocation failed!\n");
        return 1;
    }
    
    // Fill initial array
    for (int i = 0; i < n; i++) {
        ptr[i] = i + 1;
    }
    
    printf("Initial array (size %d): ", n);
    for (int i = 0; i < n; i++) {
        printf("%d ", ptr[i]);
    }
    printf("\n");
    
    // Resize to 10 elements
    n = 10;
    ptr = (int*)realloc(ptr, n * sizeof(int));
    
    if (ptr == NULL) {
        printf("Memory reallocation failed!\n");
        return 1;
    }
    
    // Fill new elements
    for (int i = 5; i < n; i++) {
        ptr[i] = i + 1;
    }
    
    printf("Resized array (size %d): ", n);
    for (int i = 0; i < n; i++) {
        printf("%d ", ptr[i]);
    }
    printf("\n");
    
    free(ptr);
    ptr = NULL;
    
    return 0;
}

/* Output:
Initial array (size 5): 1 2 3 4 5
Resized array (size 10): 1 2 3 4 5 6 7 8 9 10
*/

Important Notes about realloc():

7.7 free() - Deallocate Memory

Purpose: Releases memory back to the system

Syntax:

void free(void* ptr);

Example:

int *ptr = (int*)malloc(100 * sizeof(int));

// Use the memory...

free(ptr);     // Release memory
ptr = NULL;    // Set to NULL to avoid dangling pointer

Important Rules:

  1. Only free memory that was allocated with malloc/calloc/realloc
  2. Free each block exactly once
  3. Don't use memory after freeing it
  4. Set pointer to NULL after freeing (good practice)

7.8 Dynamic Arrays - Complete Example

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

int main() {
    int n;
    int *arr;
    
    printf("Enter number of elements: ");
    scanf("%d", &n);
    
    // Allocate memory dynamically
    arr = (int*)malloc(n * sizeof(int));
    
    if (arr == NULL) {
        printf("Memory allocation failed!\n");
        return 1;
    }
    
    // Input values
    printf("Enter %d integers:\n", n);
    for (int i = 0; i < n; i++) {
        scanf("%d", &arr[i]);
    }
    
    // Process: find sum and average
    int sum = 0;
    for (int i = 0; i < n; i++) {
        sum += arr[i];
    }
    double average = (double)sum / n;
    
    // Output results
    printf("\nArray elements: ");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\nSum: %d\n", sum);
    printf("Average: %.2f\n", average);
    
    // Free allocated memory
    free(arr);
    arr = NULL;
    
    return 0;
}

7.9 Dynamic 2D Arrays

Method 1: Array of Pointers (Rows can have different lengths)

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

int main() {
    int rows = 3, cols = 4;
    int **matrix;
    
    // Allocate array of row pointers
    matrix = (int**)malloc(rows * sizeof(int*));
    
    // Allocate each row
    for (int i = 0; i < rows; i++) {
        matrix[i] = (int*)malloc(cols * sizeof(int));
    }
    
    // Fill matrix
    int value = 1;
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            matrix[i][j] = value++;
        }
    }
    
    // Print matrix
    printf("Matrix:\n");
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%3d ", matrix[i][j]);
        }
        printf("\n");
    }
    
    // Free memory
    for (int i = 0; i < rows; i++) {
        free(matrix[i]);
    }
    free(matrix);
    matrix = NULL;
    
    return 0;
}

Method 2: Single Contiguous Block

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

int main() {
    int rows = 3, cols = 4;
    int *matrix;
    
    // Allocate as single block
    matrix = (int*)malloc(rows * cols * sizeof(int));
    
    if (matrix == NULL) {
        printf("Memory allocation failed!\n");
        return 1;
    }
    
    // Fill matrix using formula: matrix[i][j] = matrix[i * cols + j]
    int value = 1;
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            matrix[i * cols + j] = value++;
        }
    }
    
    // Print matrix
    printf("Matrix:\n");
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%3d ", matrix[i * cols + j]);
        }
        printf("\n");
    }
    
    // Free memory (single free needed)
    free(matrix);
    matrix = NULL;
    
    return 0;
}

7.10 Dynamic Memory Best Practices

1. Always Check for NULL

int *ptr = (int*)malloc(n * sizeof(int));
if (ptr == NULL) {
    fprintf(stderr, "Memory allocation failed!\n");
    exit(1);
}

2. Always Free Allocated Memory

// Allocate
int *data = (int*)malloc(100 * sizeof(int));

// Use data...

// Free when done
free(data);
data = NULL;

3. Don't Double Free

int *ptr = (int*)malloc(10 * sizeof(int));
free(ptr);
free(ptr);  // ERROR! Double free - undefined behavior

4. Don't Use After Free

int *ptr = (int*)malloc(10 * sizeof(int));
free(ptr);
ptr[0] = 5;  // ERROR! Using freed memory

5. Match Every malloc with free

void function() {
    int *ptr = (int*)malloc(100 * sizeof(int));
    // Use ptr...
    free(ptr);  // Don't forget!
}

7.11 Memory Leaks

A memory leak occurs when allocated memory is not freed:

Example of Memory Leak:

void badFunction() {
    int *ptr = (int*)malloc(1000 * sizeof(int));
    // Use ptr...
    return;  // BUG! Memory not freed - leaked!
}

int main() {
    for (int i = 0; i < 1000; i++) {
        badFunction();  // Leaks memory every iteration
    }
    return 0;
}

Fixed Version:

void goodFunction() {
    int *ptr = (int*)malloc(1000 * sizeof(int));
    // Use ptr...
    free(ptr);  // Properly freed
    return;
}

Another Common Leak:

int *ptr = (int*)malloc(100 * sizeof(int));
ptr = (int*)malloc(200 * sizeof(int));  // LEAK! Lost reference to first block

Fixed:

int *ptr = (int*)malloc(100 * sizeof(int));
free(ptr);  // Free first
ptr = (int*)malloc(200 * sizeof(int));  // Then allocate new
Module 4 : Pointers & Dynamic Array

8. Common Pointer Pitfalls and Best Practices

8.1 Uninitialized Pointers

WRONG:

int *ptr;      // Uninitialized - contains garbage address
*ptr = 42;     // DANGER! Writing to unknown memory location
               // May cause segmentation fault

CORRECT:

int *ptr = NULL;   // Initialize to NULL
int num = 0;
ptr = &num;        // Assign valid address before use
*ptr = 42;         // Now safe to dereference

8.2 Dangling Pointers

A dangling pointer points to memory that has been freed or is no longer valid:

WRONG:

int *ptr;
{
    int num = 42;
    ptr = &num;
}  // num goes out of scope here
// ptr is now dangling - points to invalid memory
printf("%d", *ptr);  // DANGER! Undefined behavior

CORRECT:

int num = 42;
int *ptr = &num;
// Use ptr while num is in scope
printf("%d", *ptr);  // Safe

Dangling Pointer with free():

int *ptr = (int*)malloc(sizeof(int));
*ptr = 42;
free(ptr);
// ptr is now dangling
printf("%d", *ptr);  // DANGER! Undefined behavior

// Better:
free(ptr);
ptr = NULL;  // Set to NULL after freeing
if (ptr != NULL) {
    printf("%d", *ptr);  // This check prevents the error
}

8.3 NULL Pointer Dereference

WRONG:

int *ptr = NULL;
*ptr = 42;  // CRASH! Cannot dereference NULL pointer

CORRECT:

int *ptr = NULL;

if (ptr != NULL) {  // Always check before dereferencing
    *ptr = 42;
} else {
    printf("Error: NULL pointer\n");
}

8.4 Array Bounds with Pointers

WRONG:

int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;
int value = *(ptr + 10);  // Out of bounds! Undefined behavior

CORRECT:

int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;
int size = 5;

for (int i = 0; i < size; i++) {
    printf("%d ", *(ptr + i));  // Safe: within bounds
}

8.5 Returning Pointer to Local Variable

WRONG:

int* createNumber() {
    int num = 42;
    return &num;  // DANGER! num is destroyed after function returns
}

int main() {
    int *ptr = createNumber();
    printf("%d", *ptr);  // Undefined behavior - dangling pointer
    return 0;
}

CORRECT - Using Dynamic Allocation:

int* createNumber() {
    int *num = (int*)malloc(sizeof(int));
    *num = 42;
    return num;  // Safe - memory persists
}

int main() {
    int *ptr = createNumber();
    printf("%d", *ptr);
    free(ptr);  // Don't forget to free!
    return 0;
}

CORRECT - Using Static Variable:

int* createNumber() {
    static int num = 42;  // Static - persists after function returns
    return &num;
}

8.6 Best Practices Summary

  1. Always initialize pointers

    int *ptr = NULL;  // Good
    int *ptr;         // Bad
    
  2. Check for NULL before dereferencing

    if (ptr != NULL) {
        *ptr = value;
    }
    
  3. Set pointers to NULL after freeing

    free(ptr);
    ptr = NULL;
    
  4. Be careful with pointer arithmetic

    // Ensure you don't go out of array bounds
    if (ptr + i < arr + size) {
        // Safe to access
    }
    
  5. Use const for pointers that shouldn't modify data

    void printString(const char *str) {
        // str cannot be used to modify the string
    }
    
  6. Match every malloc with free

    int *ptr = (int*)malloc(100 * sizeof(int));
    // Use ptr...
    free(ptr);
    
Module 4 : Pointers & Dynamic Array

9. Practical Examples with Dynamic Memory

9.1 Dynamic Array with User Input

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

int main() {
    int n, *arr;
    
    printf("How many numbers do you want to enter? ");
    scanf("%d", &n);
    
    // Allocate memory
    arr = (int*)malloc(n * sizeof(int));
    if (arr == NULL) {
        printf("Memory allocation failed!\n");
        return 1;
    }
    
    // Input
    printf("Enter %d numbers:\n", n);
    for (int i = 0; i < n; i++) {
        scanf("%d", &arr[i]);
    }
    
    // Find min and max
    int min = arr[0], max = arr[0];
    for (int i = 1; i < n; i++) {
        if (arr[i] < min) min = arr[i];
        if (arr[i] > max) max = arr[i];
    }
    
    printf("Minimum: %d\n", min);
    printf("Maximum: %d\n", max);
    
    free(arr);
    return 0;
}

9.2 Growing Array (Dynamic Resize)

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

int main() {
    int capacity = 2;
    int size = 0;
    int *arr;
    int input;
    
    // Initial allocation
    arr = (int*)malloc(capacity * sizeof(int));
    if (arr == NULL) {
        printf("Memory allocation failed!\n");
        return 1;
    }
    
    printf("Enter integers (enter -1 to stop):\n");
    
    while (1) {
        scanf("%d", &input);
        if (input == -1) break;
        
        // Check if we need more space
        if (size >= capacity) {
            capacity *= 2;  // Double the capacity
            arr = (int*)realloc(arr, capacity * sizeof(int));
            if (arr == NULL) {
                printf("Memory reallocation failed!\n");
                return 1;
            }
            printf("Array resized to capacity: %d\n", capacity);
        }
        
        arr[size++] = input;
    }
    
    // Print results
    printf("\nYou entered %d numbers:\n", size);
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
    
    free(arr);
    return 0;
}

9.3 Dynamic String Operations

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

char* concatenateStrings(const char *s1, const char *s2) {
    int len1 = strlen(s1);
    int len2 = strlen(s2);
    
    // Allocate memory for result
    char *result = (char*)malloc((len1 + len2 + 1) * sizeof(char));
    
    if (result == NULL) {
        return NULL;
    }
    
    // Copy first string
    strcpy(result, s1);
    // Concatenate second string
    strcat(result, s2);
    
    return result;
}

int main() {
    char str1[] = "Hello, ";
    char str2[] = "World!";
    
    char *combined = concatenateStrings(str1, str2);
    
    if (combined != NULL) {
        printf("Result: %s\n", combined);
        free(combined);
    }
    
    return 0;
}

9.4 Remove Duplicates from Dynamic Array

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

int* removeDuplicates(int *arr, int size, int *newSize) {
    // Allocate worst-case size (all unique)
    int *result = (int*)malloc(size * sizeof(int));
    if (result == NULL) {
        return NULL;
    }
    
    int count = 0;
    
    for (int i = 0; i < size; i++) {
        int isDuplicate = 0;
        
        // Check if current element already exists in result
        for (int j = 0; j < count; j++) {
            if (arr[i] == result[j]) {
                isDuplicate = 1;
                break;
            }
        }
        
        if (!isDuplicate) {
            result[count++] = arr[i];
        }
    }
    
    // Resize to actual size needed
    result = (int*)realloc(result, count * sizeof(int));
    *newSize = count;
    
    return result;
}

int main() {
    int arr[] = {1, 2, 3, 2, 4, 1, 5, 3, 6};
    int size = sizeof(arr) / sizeof(arr[0]);
    int newSize;
    
    printf("Original array: ");
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
    
    int *unique = removeDuplicates(arr, size, &newSize);
    
    if (unique != NULL) {
        printf("Array without duplicates: ");
        for (int i = 0; i < newSize; i++) {
            printf("%d ", unique[i]);
        }
        printf("\n");
        
        free(unique);
    }
    
    return 0;
}

9.5 Dynamic Matrix Operations

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

// Allocate matrix
int** createMatrix(int rows, int cols) {
    int **matrix = (int**)malloc(rows * sizeof(int*));
    if (matrix == NULL) return NULL;
    
    for (int i = 0; i < rows; i++) {
        matrix[i] = (int*)malloc(cols * sizeof(int));
        if (matrix[i] == NULL) {
            // Free already allocated rows
            for (int j = 0; j < i; j++) {
                free(matrix[j]);
            }
            free(matrix);
            return NULL;
        }
    }
    
    return matrix;
}

// Free matrix
void freeMatrix(int **matrix, int rows) {
    for (int i = 0; i < rows; i++) {
        free(matrix[i]);
    }
    free(matrix);
}

// Multiply two matrices
int** multiplyMatrices(int **A, int **B, int r1, int c1, int c2) {
    int **result = createMatrix(r1, c2);
    if (result == NULL) return NULL;
    
    for (int i = 0; i < r1; i++) {
        for (int j = 0; j < c2; j++) {
            result[i][j] = 0;
            for (int k = 0; k < c1; k++) {
                result[i][j] += A[i][k] * B[k][j];
            }
        }
    }
    
    return result;
}

int main() {
    int r1 = 2, c1 = 3, c2 = 2;
    
    // Create matrices
    int **A = createMatrix(r1, c1);
    int **B = createMatrix(c1, c2);
    
    // Fill matrix A
    printf("Matrix A:\n");
    int val = 1;
    for (int i = 0; i < r1; i++) {
        for (int j = 0; j < c1; j++) {
            A[i][j] = val++;
            printf("%d ", A[i][j]);
        }
        printf("\n");
    }
    
    // Fill matrix B
    printf("\nMatrix B:\n");
    val = 1;
    for (int i = 0; i < c1; i++) {
        for (int j = 0; j < c2; j++) {
            B[i][j] = val++;
            printf("%d ", B[i][j]);
        }
        printf("\n");
    }
    
    // Multiply
    int **C = multiplyMatrices(A, B, r1, c1, c2);
    
    printf("\nMatrix C (A × B):\n");
    for (int i = 0; i < r1; i++) {
        for (int j = 0; j < c2; j++) {
            printf("%d ", C[i][j]);
        }
        printf("\n");
    }
    
    // Free all matrices
    freeMatrix(A, r1);
    freeMatrix(B, c1);
    freeMatrix(C, r1);
    
    return 0;
}

Module 5 : Data Types (Struct, Enum, TypeDef) & File I/O

By the end of this module, students will be able to:

- Understand and implement user-defined data types using `struct`

- Utilize `enum` for creating readable constant sets

- Apply `typedef` to create type aliases for better code readability

- Perform file input/output operations in C

- Handle text and binary files effectively

- Design programs that persist data using files

- Combine structs with file I/O for data management systems

Module 5 : Data Types (Struct, Enum, TypeDef) & File I/O

1. Introduction to User-Defined Data Types

1.1 Why User-Defined Types?

In previous modules, we learned about basic data types like int, float, char, etc. These are sufficient for simple programs, but real-world applications often require more complex data structures.

Example Problem: Suppose you want to store information about a student:

// Without user-defined types - difficult to manage
int student_id = 12345;
char student_name[50] = "Alice Johnson";
float student_gpa = 3.75;
int student_age = 20;
char student_major[30] = "Electrical Engineering";

// If you have 100 students, you need 500 variables!

User-defined types solve this problem by allowing us to group related data together.

Module 5 : Data Types (Struct, Enum, TypeDef) & File I/O

2. Structures (struct)

2.1 What is a Structure?

A structure is a user-defined data type that groups variables of different types under a single name. Think of it as creating your own custom data type.

Python vs C Comparison:

Python C
Uses classes or dictionaries Uses struct
student = {"name": "Alice", "age": 20} struct Student student;
Dynamic typing Static typing

2.2 Declaring a Structure

Basic Syntax:

struct structure_name {
    data_type member1;
    data_type member2;
    // ... more members
};

Example - Student Structure:

struct Student {
    int id;
    char name[50];
    float gpa;
    int age;
    char major[30];
};

Important Notes:

2.3 Creating Structure Variables

Method 1: After Structure Declaration

struct Student {
    int id;
    char name[50];
    float gpa;
};

// Create variables
struct Student student1;
struct Student student2, student3;

Method 2: During Structure Declaration

struct Student {
    int id;
    char name[50];
    float gpa;
} student1, student2;

Method 3: Anonymous Structure (less common)

struct {
    int id;
    char name[50];
    float gpa;
} student1, student2;

2.4 Initializing Structure Variables

Method 1: Member-by-Member Assignment

struct Student s1;
s1.id = 12345;
strcpy(s1.name, "Alice Johnson");  // Note: Use strcpy for strings
s1.gpa = 3.75;

Method 2: Initialization at Declaration

struct Student s1 = {12345, "Alice Johnson", 3.75};

Method 3: Designated Initializers (C99 and later)

struct Student s1 = {
    .id = 12345,
    .name = "Alice Johnson",
    .gpa = 3.75
};

Method 4: Partial Initialization

struct Student s1 = {12345};  // Only id is initialized, others are 0/NULL

2.5 Accessing Structure Members

Use the dot operator (.) to access structure members:

struct Student s1;

// Writing to members
s1.id = 12345;
s1.gpa = 3.75;
strcpy(s1.name, "Alice Johnson");

// Reading from members
printf("Student ID: %d\n", s1.id);
printf("Student Name: %s\n", s1.name);
printf("Student GPA: %.2f\n", s1.gpa);

2.6 Nested Structures

Structures can contain other structures as members:

struct Date {
    int day;
    int month;
    int year;
};

struct Student {
    int id;
    char name[50];
    float gpa;
    struct Date birthDate;  // Nested structure
};

// Usage
struct Student s1;
s1.id = 12345;
s1.birthDate.day = 15;
s1.birthDate.month = 8;
s1.birthDate.year = 2003;

printf("Birth Date: %d/%d/%d\n", 
       s1.birthDate.day, 
       s1.birthDate.month, 
       s1.birthDate.year);

2.7 Array of Structures

You can create arrays of structures to handle multiple records:

struct Student {
    int id;
    char name[50];
    float gpa;
};

// Array of 100 students
struct Student students[100];

// Accessing elements
students[0].id = 12345;
strcpy(students[0].name, "Alice");
students[0].gpa = 3.75;

// Loop through all students
for (int i = 0; i < 100; i++) {
    printf("Student %d: %s (GPA: %.2f)\n", 
           students[i].id, 
           students[i].name, 
           students[i].gpa);
}

2.8 Pointers to Structures

You can use pointers with structures:

struct Student s1 = {12345, "Alice", 3.75};
struct Student *ptr = &s1;

// Method 1: Using (*ptr).member
printf("ID: %d\n", (*ptr).id);

// Method 2: Using ptr->member (preferred)
printf("ID: %d\n", ptr->id);
printf("Name: %s\n", ptr->name);
printf("GPA: %.2f\n", ptr->gpa);

The Arrow Operator (->):

2.9 Structures and Functions

Passing by Value:

void printStudent(struct Student s) {
    printf("ID: %d\n", s.id);
    printf("Name: %s\n", s.name);
    printf("GPA: %.2f\n", s.gpa);
}

// Usage
struct Student s1 = {12345, "Alice", 3.75};
printStudent(s1);  // Entire structure is copied

Passing by Reference (Pointer):

void updateGPA(struct Student *s, float newGPA) {
    s->gpa = newGPA;
}

// Usage
struct Student s1 = {12345, "Alice", 3.75};
updateGPA(&s1, 3.85);  // Pass address of structure
printf("Updated GPA: %.2f\n", s1.gpa);  // Output: 3.85

Returning Structures from Functions:

struct Student createStudent(int id, char *name, float gpa) {
    struct Student s;
    s.id = id;
    strcpy(s.name, name);
    s.gpa = gpa;
    return s;
}

// Usage
struct Student s1 = createStudent(12345, "Alice", 3.75);

2.10 Practical Example: Student Database

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

struct Student {
    int id;
    char name[50];
    float gpa;
    int age;
};

// Function to input student data
void inputStudent(struct Student *s) {
    printf("Enter Student ID: ");
    scanf("%d", &s->id);
    
    printf("Enter Student Name: ");
    scanf(" %[^\n]", s->name);
    
    printf("Enter Student GPA: ");
    scanf("%f", &s->gpa);
    
    printf("Enter Student Age: ");
    scanf("%d", &s->age);
}

// Function to display student data
void displayStudent(struct Student s) {
    printf("\n--- Student Information ---\n");
    printf("ID: %d\n", s.id);
    printf("Name: %s\n", s.name);
    printf("GPA: %.2f\n", s.gpa);
    printf("Age: %d\n", s.age);
}

int main() {
    struct Student students[3];
    
    // Input data for 3 students
    for (int i = 0; i < 3; i++) {
        printf("\nEnter details for student %d:\n", i + 1);
        inputStudent(&students[i]);
    }
    
    // Display all students
    printf("\n\n=== All Students ===\n");
    for (int i = 0; i < 3; i++) {
        displayStudent(students[i]);
    }
    
    return 0;
}
Module 5 : Data Types (Struct, Enum, TypeDef) & File I/O

3. Enumerations (enum)

3.1 What is an Enumeration?

An enumeration is a user-defined data type consisting of a set of named integer constants. Enums make code more readable by replacing "magic numbers" with meaningful names.

Python vs C Comparison:

Python C
No built-in enum (uses constants) Has enum keyword
RED = 0; GREEN = 1; BLUE = 2 enum Color {RED, GREEN, BLUE};

3.2 Declaring an Enum

Basic Syntax:

enum enum_name {
    constant1,
    constant2,
    constant3
};

Example:

enum Day {
    SUNDAY,     // 0
    MONDAY,     // 1
    TUESDAY,    // 2
    WEDNESDAY,  // 3
    THURSDAY,   // 4
    FRIDAY,     // 5
    SATURDAY    // 6
};

Key Points:

3.3 Custom Values in Enums

enum Status {
    SUCCESS = 1,
    FAILURE = 0,
    PENDING = -1
};

enum Month {
    JANUARY = 1,
    FEBRUARY,    // 2
    MARCH,       // 3
    APRIL,       // 4
    MAY,         // 5
    JUNE,        // 6
    JULY,        // 7
    AUGUST,      // 8
    SEPTEMBER,   // 9
    OCTOBER,     // 10
    NOVEMBER,    // 11
    DECEMBER     // 12
};

3.4 Using Enums

#include <stdio.h>

enum Day {
    SUNDAY, MONDAY, TUESDAY, WEDNESDAY, 
    THURSDAY, FRIDAY, SATURDAY
};

int main() {
    enum Day today = WEDNESDAY;
    
    printf("Today is day number: %d\n", today);  // Output: 3
    
    if (today == WEDNESDAY) {
        printf("It's the middle of the week!\n");
    }
    
    // Using enum in switch statement
    switch (today) {
        case MONDAY:
            printf("Start of work week\n");
            break;
        case FRIDAY:
            printf("TGIF!\n");
            break;
        case SATURDAY:
        case SUNDAY:
            printf("Weekend!\n");
            break;
        default:
            printf("Regular work day\n");
    }
    
    return 0;
}

3.5 Enums with Structures

Combining enums with structures creates powerful data models:

enum Grade {
    GRADE_A = 90,
    GRADE_B = 80,
    GRADE_C = 70,
    GRADE_D = 60,
    GRADE_F = 0
};

enum StudentStatus {
    ACTIVE,
    GRADUATED,
    SUSPENDED,
    WITHDRAWN
};

struct Student {
    int id;
    char name[50];
    enum Grade grade;
    enum StudentStatus status;
};

int main() {
    struct Student s1 = {
        .id = 12345,
        .name = "Alice",
        .grade = GRADE_A,
        .status = ACTIVE
    };
    
    printf("Student: %s\n", s1.name);
    
    if (s1.status == ACTIVE) {
        printf("Status: Active Student\n");
    }
    
    if (s1.grade >= GRADE_B) {
        printf("Good performance!\n");
    }
    
    return 0;
}

3.6 Practical Example: Traffic Light System

#include <stdio.h>

enum TrafficLight {
    RED,
    YELLOW,
    GREEN
};

void displayLightAction(enum TrafficLight light) {
    switch (light) {
        case RED:
            printf("STOP! Red light is on.\n");
            break;
        case YELLOW:
            printf("CAUTION! Yellow light is on.\n");
            break;
        case GREEN:
            printf("GO! Green light is on.\n");
            break;
        default:
            printf("Invalid light state.\n");
    }
}

int main() {
    enum TrafficLight currentLight = RED;
    
    printf("Traffic Light Simulation:\n\n");
    
    for (int i = 0; i < 3; i++) {
        displayLightAction(currentLight);
        
        // Cycle through lights
        if (currentLight == RED) {
            currentLight = GREEN;
        } else if (currentLight == GREEN) {
            currentLight = YELLOW;
        } else {
            currentLight = RED;
        }
        
        printf("Waiting...\n\n");
    }
    
    return 0;
}
Module 5 : Data Types (Struct, Enum, TypeDef) & File I/O

4. Type Definitions (typedef)

4.1 What is typedef?

typedef creates aliases (alternative names) for existing data types. It makes code more readable and easier to maintain.

Basic Syntax:

typedef existing_type new_name;

4.2 typedef with Basic Types

// Create aliases for basic types
typedef int Integer;
typedef float Real;
typedef char Character;

// Usage
Integer age = 25;
Real temperature = 36.5;
Character grade = 'A';

4.3 typedef with Structures

Without typedef:

struct Student {
    int id;
    char name[50];
    float gpa;
};

// Must always use "struct Student"
struct Student s1;
struct Student students[100];

With typedef - Method 1:

struct Student {
    int id;
    char name[50];
    float gpa;
};

typedef struct Student Student;

// Now can use just "Student"
Student s1;
Student students[100];

With typedef - Method 2 (Combined):

typedef struct Student {
    int id;
    char name[50];
    float gpa;
} Student;

// Usage
Student s1;
Student students[100];

With typedef - Method 3 (Anonymous struct):

typedef struct {
    int id;
    char name[50];
    float gpa;
} Student;

// Usage
Student s1;

4.4 typedef with Enums

typedef enum {
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY
} Day;

// Usage
Day today = WEDNESDAY;
Day weekend[2] = {SATURDAY, SUNDAY};

4.5 typedef with Pointers

typedef int* IntPtr;
typedef struct Student* StudentPtr;

// Usage
IntPtr p1, p2;  // Both are int pointers
StudentPtr sptr;  // Student pointer

4.6 typedef with Arrays

typedef int IntArray[10];
typedef char String[100];

// Usage
IntArray numbers;  // Same as: int numbers[10];
String name;       // Same as: char name[100];

4.7 Practical Example: Complex Data Types

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

// Define enumeration for course grades
typedef enum {
    GRADE_A = 4,
    GRADE_B = 3,
    GRADE_C = 2,
    GRADE_D = 1,
    GRADE_F = 0
} Grade;

// Define structure for a course
typedef struct {
    char code[10];
    char name[50];
    int credits;
    Grade grade;
} Course;

// Define structure for a student
typedef struct {
    int id;
    char name[50];
    Course courses[5];
    int numCourses;
    float gpa;
} Student;

// Function to calculate GPA
float calculateGPA(Student *s) {
    int totalCredits = 0;
    float totalPoints = 0.0;
    
    for (int i = 0; i < s->numCourses; i++) {
        totalCredits += s->courses[i].credits;
        totalPoints += s->courses[i].credits * s->courses[i].grade;
    }
    
    if (totalCredits == 0) return 0.0;
    return totalPoints / totalCredits;
}

int main() {
    Student student = {
        .id = 12345,
        .name = "Alice Johnson",
        .numCourses = 3
    };
    
    // Add courses
    strcpy(student.courses[0].code, "EE101");
    strcpy(student.courses[0].name, "Circuit Analysis");
    student.courses[0].credits = 3;
    student.courses[0].grade = GRADE_A;
    
    strcpy(student.courses[1].code, "MATH201");
    strcpy(student.courses[1].name, "Calculus II");
    student.courses[1].credits = 4;
    student.courses[1].grade = GRADE_B;
    
    strcpy(student.courses[2].code, "CS101");
    strcpy(student.courses[2].name, "Programming");
    student.courses[2].credits = 3;
    student.courses[2].grade = GRADE_A;
    
    // Calculate and display GPA
    student.gpa = calculateGPA(&student);
    
    printf("Student: %s (ID: %d)\n", student.name, student.id);
    printf("GPA: %.2f\n\n", student.gpa);
    
    printf("Courses:\n");
    for (int i = 0; i < student.numCourses; i++) {
        printf("  %s - %s (%d credits): Grade %d\n",
               student.courses[i].code,
               student.courses[i].name,
               student.courses[i].credits,
               student.courses[i].grade);
    }
    
    return 0;
}
Module 5 : Data Types (Struct, Enum, TypeDef) & File I/O

5. File Input/Output

5.1 Why File I/O?

So far, all our programs lose their data when they terminate. File I/O allows programs to:

Python vs C File Operations:

Python C
f = open("file.txt", "r") FILE *f = fopen("file.txt", "r");
f.write("text") fprintf(f, "text");
content = f.read() fscanf(f, "%s", buffer);
f.close() fclose(f);

5.2 File Pointer

In C, files are accessed through file pointers of type FILE*:

FILE *filePointer;

The FILE type is defined in <stdio.h>.

5.3 Opening Files - fopen()

Function Signature:

FILE *fopen(const char *filename, const char *mode);

File Opening Modes:

Mode Description If File Exists If File Doesn't Exist
"r" Read only Opens file Returns NULL
"w" Write only Overwrites content Creates new file
"a" Append Appends to end Creates new file
"r+" Read and write Opens file Returns NULL
"w+" Read and write Overwrites content Creates new file
"a+" Read and append Opens file Creates new file
"rb" Read binary Opens file Returns NULL
"wb" Write binary Overwrites content Creates new file
"ab" Append binary Appends to end Creates new file

Example:

FILE *file;

// Open file for reading
file = fopen("data.txt", "r");

// Always check if file opened successfully
if (file == NULL) {
    printf("Error: Could not open file!\n");
    return 1;
}

// ... file operations ...

fclose(file);

5.4 Closing Files - fclose()

Always close files after use to:

int fclose(FILE *filePointer);

Returns:

Example:

FILE *file = fopen("data.txt", "r");
if (file != NULL) {
    // ... operations ...
    fclose(file);
}

5.5 Reading from Text Files

5.5.1 fscanf() - Formatted Input

Similar to scanf(), but reads from a file:

int fscanf(FILE *stream, const char *format, ...);

Example:

FILE *file = fopen("numbers.txt", "r");
if (file == NULL) {
    printf("Error opening file!\n");
    return 1;
}

int num;
while (fscanf(file, "%d", &num) == 1) {
    printf("Read: %d\n", num);
}

fclose(file);

5.5.2 fgets() - Line Input

Reads a line from a file:

char *fgets(char *str, int n, FILE *stream);

Example:

FILE *file = fopen("text.txt", "r");
if (file == NULL) {
    printf("Error opening file!\n");
    return 1;
}

char line[256];
while (fgets(line, sizeof(line), file) != NULL) {
    printf("%s", line);
}

fclose(file);

5.5.3 fgetc() - Character Input

Reads a single character:

int fgetc(FILE *stream);

Example:

FILE *file = fopen("text.txt", "r");
if (file == NULL) {
    printf("Error opening file!\n");
    return 1;
}

int ch;
while ((ch = fgetc(file)) != EOF) {
    putchar(ch);
}

fclose(file);

5.6 Writing to Text Files

5.6.1 fprintf() - Formatted Output

Similar to printf(), but writes to a file:

int fprintf(FILE *stream, const char *format, ...);

Example:

FILE *file = fopen("output.txt", "w");
if (file == NULL) {
    printf("Error opening file!\n");
    return 1;
}

fprintf(file, "Student ID: %d\n", 12345);
fprintf(file, "Name: %s\n", "Alice Johnson");
fprintf(file, "GPA: %.2f\n", 3.75);

fclose(file);

5.6.2 fputs() - String Output

Writes a string to a file:

int fputs(const char *str, FILE *stream);

Example:

FILE *file = fopen("output.txt", "w");
if (file == NULL) {
    printf("Error opening file!\n");
    return 1;
}

fputs("Hello, World!\n", file);
fputs("This is a test.\n", file);

fclose(file);

5.6.3 fputc() - Character Output

Writes a single character:

int fputc(int char, FILE *stream);

Example:

FILE *file = fopen("output.txt", "w");
if (file == NULL) {
    printf("Error opening file!\n");
    return 1;
}

for (char ch = 'A'; ch <= 'Z'; ch++) {
    fputc(ch, file);
}

fclose(file);

5.7 File Position Functions

5.7.1 fseek() - Move File Pointer

int fseek(FILE *stream, long offset, int origin);

Origin values:

Example:

FILE *file = fopen("data.txt", "r");

// Move to beginning
fseek(file, 0, SEEK_SET);

// Move 10 bytes from current position
fseek(file, 10, SEEK_CUR);

// Move to end of file
fseek(file, 0, SEEK_END);

5.7.2 ftell() - Get Current Position

long ftell(FILE *stream);

Example:

FILE *file = fopen("data.txt", "r");
long position = ftell(file);
printf("Current position: %ld\n", position);

5.7.3 rewind() - Reset to Beginning

void rewind(FILE *stream);

Example:

FILE *file = fopen("data.txt", "r");

// Read some data...

rewind(file);  // Go back to beginning

5.8 Checking End of File - feof()

int feof(FILE *stream);

Returns non-zero if end of file is reached.

Example:

FILE *file = fopen("data.txt", "r");
char ch;

while (!feof(file)) {
    ch = fgetc(file);
    if (ch != EOF) {
        putchar(ch);
    }
}

fclose(file);

5.9 Practical Example: Student Records Manager

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

typedef struct {
    int id;
    char name[50];
    float gpa;
    int age;
} Student;

// Save student to file
void saveStudent(const char *filename, Student s) {
    FILE *file = fopen(filename, "a");  // Append mode
    if (file == NULL) {
        printf("Error opening file!\n");
        return;
    }
    
    fprintf(file, "%d,%s,%.2f,%d\n", s.id, s.name, s.gpa, s.age);
    fclose(file);
    
    printf("Student record saved successfully!\n");
}

// Load all students from file
void loadStudents(const char *filename) {
    FILE *file = fopen(filename, "r");
    if (file == NULL) {
        printf("No records found!\n");
        return;
    }
    
    Student s;
    printf("\n=== Student Records ===\n");
    
    while (fscanf(file, "%d,%49[^,],%f,%d\n", 
                  &s.id, s.name, &s.gpa, &s.age) == 4) {
        printf("ID: %d | Name: %s | GPA: %.2f | Age: %d\n",
               s.id, s.name, s.gpa, s.age);
    }
    
    fclose(file);
}

// Search for student by ID
int searchStudent(const char *filename, int searchID) {
    FILE *file = fopen(filename, "r");
    if (file == NULL) {
        printf("Error opening file!\n");
        return 0;
    }
    
    Student s;
    while (fscanf(file, "%d,%49[^,],%f,%d\n", 
                  &s.id, s.name, &s.gpa, &s.age) == 4) {
        if (s.id == searchID) {
            printf("\n--- Student Found ---\n");
            printf("ID: %d\n", s.id);
            printf("Name: %s\n", s.name);
            printf("GPA: %.2f\n", s.gpa);
            printf("Age: %d\n", s.age);
            fclose(file);
            return 1;
        }
    }
    
    fclose(file);
    printf("Student not found!\n");
    return 0;
}

int main() {
    int choice;
    Student s;
    
    do {
        printf("\n=== Student Management System ===\n");
        printf("1. Add Student\n");
        printf("2. View All Students\n");
        printf("3. Search Student by ID\n");
        printf("4. Exit\n");
        printf("Enter choice: ");
        scanf("%d", &choice);
        
        switch (choice) {
            case 1:
                printf("\nEnter Student ID: ");
                scanf("%d", &s.id);
                
                printf("Enter Student Name: ");
                scanf(" %[^\n]", s.name);
                
                printf("Enter Student GPA: ");
                scanf("%f", &s.gpa);
                
                printf("Enter Student Age: ");
                scanf("%d", &s.age);
                
                saveStudent("students.txt", s);
                break;
                
            case 2:
                loadStudents("students.txt");
                break;
                
            case 3: {
                int searchID;
                printf("\nEnter Student ID to search: ");
                scanf("%d", &searchID);
                searchStudent("students.txt", searchID);
                break;
            }
                
            case 4:
                printf("Exiting program...\n");
                break;
                
            default:
                printf("Invalid choice!\n");
        }
    } while (choice != 4);
    
    return 0;
}

5.10 Binary File Operations

Binary files store data in raw binary format, which is more efficient for storing structures.

5.10.1 Writing Binary Data - fwrite()

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

Parameters:

Example:

typedef struct {
    int id;
    char name[50];
    float gpa;
} Student;

Student s = {12345, "Alice Johnson", 3.75};

FILE *file = fopen("students.dat", "wb");
if (file != NULL) {
    fwrite(&s, sizeof(Student), 1, file);
    fclose(file);
}

5.10.2 Reading Binary Data - fread()

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

Parameters:

Example:

Student s;

FILE *file = fopen("students.dat", "rb");
if (file != NULL) {
    while (fread(&s, sizeof(Student), 1, file) == 1) {
        printf("ID: %d, Name: %s, GPA: %.2f\n", 
               s.id, s.name, s.gpa);
    }
    fclose(file);
}

5.10.3 Binary vs Text Files

Aspect Text Files Binary Files
Human Readable Yes No
Size Larger Smaller
Speed Slower Faster
Portability More portable Less portable
Precision May lose precision Full precision
Best for Configuration files, logs Large data, structures

5.11 Complete Binary File Example

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

typedef struct {
    int id;
    char name[50];
    float gpa;
    int age;
} Student;

// Save student to binary file
void saveStudentBinary(const char *filename, Student s) {
    FILE *file = fopen(filename, "ab");  // Append binary
    if (file == NULL) {
        printf("Error opening file!\n");
        return;
    }
    
    fwrite(&s, sizeof(Student), 1, file);
    fclose(file);
    
    printf("Student saved to binary file!\n");
}

// Load all students from binary file
void loadStudentsBinary(const char *filename) {
    FILE *file = fopen(filename, "rb");  // Read binary
    if (file == NULL) {
        printf("No records found!\n");
        return;
    }
    
    Student s;
    printf("\n=== Student Records (Binary) ===\n");
    
    while (fread(&s, sizeof(Student), 1, file) == 1) {
        printf("ID: %d | Name: %s | GPA: %.2f | Age: %d\n",
               s.id, s.name, s.gpa, s.age);
    }
    
    fclose(file);
}

// Count number of records in binary file
int countRecords(const char *filename) {
    FILE *file = fopen(filename, "rb");
    if (file == NULL) {
        return 0;
    }
    
    // Seek to end of file
    fseek(file, 0, SEEK_END);
    
    // Get file size
    long fileSize = ftell(file);
    
    fclose(file);
    
    // Calculate number of records
    return fileSize / sizeof(Student);
}

// Update student record by ID
int updateStudent(const char *filename, int id, Student newData) {
    FILE *file = fopen(filename, "rb+");  // Read and write binary
    if (file == NULL) {
        printf("Error opening file!\n");
        return 0;
    }
    
    Student s;
    int found = 0;
    long position = 0;
    
    // Search for student
    while (fread(&s, sizeof(Student), 1, file) == 1) {
        if (s.id == id) {
            // Move back to the position of this record
            fseek(file, position, SEEK_SET);
            
            // Write updated data
            fwrite(&newData, sizeof(Student), 1, file);
            
            found = 1;
            printf("Student record updated!\n");
            break;
        }
        position = ftell(file);
    }
    
    fclose(file);
    
    if (!found) {
        printf("Student ID %d not found!\n", id);
    }
    
    return found;
}

// Delete student record by ID
int deleteStudent(const char *filename, int id) {
    FILE *file = fopen(filename, "rb");
    FILE *temp = fopen("temp.dat", "wb");
    
    if (file == NULL || temp == NULL) {
        printf("Error opening files!\n");
        return 0;
    }
    
    Student s;
    int found = 0;
    
    // Copy all records except the one to delete
    while (fread(&s, sizeof(Student), 1, file) == 1) {
        if (s.id == id) {
            found = 1;
            continue;  // Skip this record
        }
        fwrite(&s, sizeof(Student), 1, temp);
    }
    
    fclose(file);
    fclose(temp);
    
    // Replace original file with temp file
    remove(filename);
    rename("temp.dat", filename);
    
    if (found) {
        printf("Student record deleted!\n");
    } else {
        printf("Student ID %d not found!\n", id);
    }
    
    return found;
}

int main() {
    int choice;
    Student s;
    
    do {
        printf("\n=== Student Management System (Binary Files) ===\n");
        printf("1. Add Student\n");
        printf("2. View All Students\n");
        printf("3. Count Records\n");
        printf("4. Update Student\n");
        printf("5. Delete Student\n");
        printf("6. Exit\n");
        printf("Enter choice: ");
        scanf("%d", &choice);
        
        switch (choice) {
            case 1:
                printf("\nEnter Student ID: ");
                scanf("%d", &s.id);
                
                printf("Enter Student Name: ");
                scanf(" %[^\n]", s.name);
                
                printf("Enter Student GPA: ");
                scanf("%f", &s.gpa);
                
                printf("Enter Student Age: ");
                scanf("%d", &s.age);
                
                saveStudentBinary("students.dat", s);
                break;
                
            case 2:
                loadStudentsBinary("students.dat");
                break;
                
            case 3: {
                int count = countRecords("students.dat");
                printf("\nTotal records: %d\n", count);
                break;
            }
                
            case 4: {
                int updateID;
                printf("\nEnter Student ID to update: ");
                scanf("%d", &updateID);
                
                printf("Enter new Student Name: ");
                scanf(" %[^\n]", s.name);
                
                printf("Enter new Student GPA: ");
                scanf("%f", &s.gpa);
                
                printf("Enter new Student Age: ");
                scanf("%d", &s.age);
                
                s.id = updateID;
                updateStudent("students.dat", updateID, s);
                break;
            }
                
            case 5: {
                int deleteID;
                printf("\nEnter Student ID to delete: ");
                scanf("%d", &deleteID);
                deleteStudent("students.dat", deleteID);
                break;
            }
                
            case 6:
                printf("Exiting program...\n");
                break;
                
            default:
                printf("Invalid choice!\n");
        }
    } while (choice != 6);
    
    return 0;
}

Module 6 : Linked List

By the end of this module, students will be able to:

- Understand the concept and structure of linked lists

- Differentiate between arrays and linked lists

- Implement singly linked lists in C

- Perform basic operations: insertion, deletion, traversal, and searching

- Understand doubly linked lists and circular linked lists

- Apply linked lists to solve real-world problems

- Debug common linked list errors

- Analyze time and space complexity of linked list operations

Module 6 : Linked List

1. Introduction to Linked Lists

1.1 What is a Linked List?

A linked list is a linear data structure where elements are not stored in contiguous memory locations. Instead, each element (called a node) contains:

  1. Data: The actual value stored
  2. Pointer(s): Reference to the next (and possibly previous) node

Analogy: Think of a linked list like a treasure hunt:

Visual Representation:

Array (Contiguous Memory):
┌────┬────┬────┬────┬────┐
│ 10 │ 20 │ 30 │ 40 │ 50 │
└────┴────┴────┴────┴────┘
 [0]  [1]  [2]  [3]  [4]

Linked List (Non-contiguous Memory):
HEAD
 ↓
┌────┬────┐    ┌────┬────┐    ┌────┬────┐    ┌────┬────┐
│ 10 │ ●──┼───→│ 20 │ ●──┼───→│ 30 │ ●──┼───→│ 40 │NULL│
└────┴────┘    └────┴────┘    └────┴────┘    └────┴────┘
 data  next     data  next     data  next     data  next

1.2 Why Use Linked Lists?

Advantages:

  1. Dynamic Size: Can grow or shrink at runtime
  2. Easy Insertion/Deletion: No need to shift elements
  3. Memory Efficient: Allocate memory only when needed
  4. Flexible Structure: Can implement stacks, queues, graphs

Disadvantages:

  1. No Random Access: Must traverse from beginning
  2. Extra Memory: Requires space for pointers
  3. Sequential Access: Slower than arrays for direct access
  4. Cache Performance: Poor cache locality

1.3 Types of Linked Lists

1. Singly Linked List
   HEAD → [data|next] → [data|next] → [data|NULL]

2. Doubly Linked List
   HEAD ⇄ [prev|data|next] ⇄ [prev|data|next] ⇄ [prev|data|NULL]

3. Circular Linked List
   HEAD → [data|next] → [data|next] → [data|next] ──┐
    ↑                                                 │
    └─────────────────────────────────────────────────┘

4. Circular Doubly Linked List
   HEAD ⇄ [prev|data|next] ⇄ [prev|data|next] ──┐
    ↑                                            │
    └────────────────────────────────────────────┘

1.4 Linked List vs Array

Feature Array Linked List
Memory Allocation Contiguous Non-contiguous
Size Fixed Dynamic
Access Time O(1) O(n)
Insertion (beginning) O(n) O(1)
Insertion (end) O(1) O(n) or O(1) with tail
Deletion (beginning) O(n) O(1)
Memory Usage Less (no pointers) More (pointers)
Cache Performance Better Worse
Random Access Yes No
Module 6 : Linked List

2. Node Structure

2.1 Defining a Node

In C, a node is typically defined using a structure:

// Definition of a node
struct Node {
    int data;           // Data part
    struct Node *next;  // Pointer to next node
};

Memory Layout:

Single Node in Memory:
┌──────────┬──────────┐
│   data   │   next   │
│  (int)   │ (pointer)│
└──────────┴──────────┘
   4 bytes    8 bytes (on 64-bit system)

2.2 Creating a Node

Method 1: Static Allocation (Limited)

struct Node node1;
node1.data = 10;
node1.next = NULL;

Method 2: Dynamic Allocation (Recommended)

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

struct Node {
    int data;
    struct Node *next;
};

int main() {
    // Allocate memory for new node
    struct Node *newNode = (struct Node*)malloc(sizeof(struct Node));
    
    // Check if allocation successful
    if (newNode == NULL) {
        printf("Memory allocation failed!\n");
        return 1;
    }
    
    // Initialize node
    newNode->data = 10;
    newNode->next = NULL;
    
    printf("Node created with data: %d\n", newNode->data);
    
    // Free memory when done
    free(newNode);
    
    return 0;
}

2.3 The Arrow Operator (->)

When working with pointers to structures, use the arrow operator:

struct Node *ptr;

// These are equivalent:
ptr->data = 10;      // Arrow operator (preferred)
(*ptr).data = 10;    // Dereference then dot operator

Why use ->?

Module 6 : Linked List

3. Singly Linked List Operations

3.1 Creating an Empty List

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

struct Node {
    int data;
    struct Node *next;
};

// Initialize head pointer
struct Node *head = NULL;

3.2 Insertion Operations

3.2.1 Insert at Beginning

void insertAtBeginning(struct Node **head, int value) {
    // Create new node
    struct Node *newNode = (struct Node*)malloc(sizeof(struct Node));
    
    if (newNode == NULL) {
        printf("Memory allocation failed!\n");
        return;
    }
    
    // Set data and next pointer
    newNode->data = value;
    newNode->next = *head;
    
    // Update head
    *head = newNode;
}

// Usage
int main() {
    struct Node *head = NULL;
    
    insertAtBeginning(&head, 30);
    insertAtBeginning(&head, 20);
    insertAtBeginning(&head, 10);
    
    // List now: 10 → 20 → 30 → NULL
    
    return 0;
}

Visual Steps:

Initial: head → NULL

Step 1: Create node with data = 10
newNode → [10|NULL]

Step 2: Point newNode->next to current head
newNode → [10|●] → NULL

Step 3: Update head to newNode
head → [10|NULL]

Step 4: Insert 20
head → [20|●] → [10|NULL]

Step 5: Insert 30
head → [30|●] → [20|●] → [10|NULL]

Time Complexity: O(1) Space Complexity: O(1)

3.2.2 Insert at End

void insertAtEnd(struct Node **head, int value) {
    // Create new node
    struct Node *newNode = (struct Node*)malloc(sizeof(struct Node));
    
    if (newNode == NULL) {
        printf("Memory allocation failed!\n");
        return;
    }
    
    newNode->data = value;
    newNode->next = NULL;
    
    // If list is empty
    if (*head == NULL) {
        *head = newNode;
        return;
    }
    
    // Traverse to last node
    struct Node *temp = *head;
    while (temp->next != NULL) {
        temp = temp->next;
    }
    
    // Insert at end
    temp->next = newNode;
}

Visual Steps:

List: head → [10|●] → [20|●] → [30|NULL]

Step 1: Create newNode with data = 40
newNode → [40|NULL]

Step 2: Traverse to last node
temp moves: [10] → [20] → [30]

Step 3: Connect last node to newNode
head → [10|●] → [20|●] → [30|●] → [40|NULL]

Time Complexity: O(n) - must traverse entire list Space Complexity: O(1)

3.2.3 Insert at Position

void insertAtPosition(struct Node **head, int value, int position) {
    // Create new node
    struct Node *newNode = (struct Node*)malloc(sizeof(struct Node));
    
    if (newNode == NULL) {
        printf("Memory allocation failed!\n");
        return;
    }
    
    newNode->data = value;
    
    // Insert at beginning (position 0)
    if (position == 0) {
        newNode->next = *head;
        *head = newNode;
        return;
    }
    
    // Traverse to position-1
    struct Node *temp = *head;
    for (int i = 0; i < position - 1 && temp != NULL; i++) {
        temp = temp->next;
    }
    
    // Check if position is valid
    if (temp == NULL) {
        printf("Invalid position!\n");
        free(newNode);
        return;
    }
    
    // Insert node
    newNode->next = temp->next;
    temp->next = newNode;
}

Visual Steps (Insert 25 at position 2):

Initial: head → [10|●] → [20|●] → [30|NULL]
                  0        1        2

Step 1: Create newNode [25|NULL]

Step 2: Traverse to position 1 (position - 1)
temp → [20|●]

Step 3: Connect newNode
newNode->next = temp->next  (points to 30)
[25|●] → [30|NULL]

Step 4: Connect temp to newNode
temp->next = newNode
head → [10|●] → [20|●] → [25|●] → [30|NULL]

Time Complexity: O(n) Space Complexity: O(1)

3.3 Deletion Operations

3.3.1 Delete from Beginning

void deleteFromBeginning(struct Node **head) {
    // Check if list is empty
    if (*head == NULL) {
        printf("List is empty!\n");
        return;
    }
    
    // Store current head
    struct Node *temp = *head;
    
    // Move head to next node
    *head = (*head)->next;
    
    // Free old head
    free(temp);
}

Visual Steps:

Initial: head → [10|●] → [20|●] → [30|NULL]

Step 1: temp = head
temp → [10|●] → [20|●] → [30|NULL]
head → [10|●] → [20|●] → [30|NULL]

Step 2: head = head->next
head → [20|●] → [30|NULL]
temp → [10|●] (to be freed)

Step 3: free(temp)
head → [20|●] → [30|NULL]

Time Complexity: O(1) Space Complexity: O(1)

3.3.2 Delete from End

void deleteFromEnd(struct Node **head) {
    // Check if list is empty
    if (*head == NULL) {
        printf("List is empty!\n");
        return;
    }
    
    // If only one node
    if ((*head)->next == NULL) {
        free(*head);
        *head = NULL;
        return;
    }
    
    // Traverse to second-last node
    struct Node *temp = *head;
    while (temp->next->next != NULL) {
        temp = temp->next;
    }
    
    // Delete last node
    free(temp->next);
    temp->next = NULL;
}

Visual Steps:

Initial: head → [10|●] → [20|●] → [30|NULL]

Step 1: Traverse to second-last node
temp → [20|●] → [30|NULL]

Step 2: Free last node
free(temp->next)

Step 3: Set second-last to NULL
head → [10|●] → [20|NULL]

Time Complexity: O(n) Space Complexity: O(1)

3.3.3 Delete at Position

void deleteAtPosition(struct Node **head, int position) {
    // Check if list is empty
    if (*head == NULL) {
        printf("List is empty!\n");
        return;
    }
    
    // Delete first node
    if (position == 0) {
        struct Node *temp = *head;
        *head = (*head)->next;
        free(temp);
        return;
    }
    
    // Traverse to position-1
    struct Node *temp = *head;
    for (int i = 0; i < position - 1 && temp != NULL; i++) {
        temp = temp->next;
    }
    
    // Check if position is valid
    if (temp == NULL || temp->next == NULL) {
        printf("Invalid position!\n");
        return;
    }
    
    // Delete node
    struct Node *nodeToDelete = temp->next;
    temp->next = nodeToDelete->next;
    free(nodeToDelete);
}

Time Complexity: O(n) Space Complexity: O(1)

3.3.4 Delete by Value

void deleteByValue(struct Node **head, int value) {
    // Check if list is empty
    if (*head == NULL) {
        printf("List is empty!\n");
        return;
    }
    
    // If head node contains the value
    if ((*head)->data == value) {
        struct Node *temp = *head;
        *head = (*head)->next;
        free(temp);
        return;
    }
    
    // Search for the node
    struct Node *temp = *head;
    while (temp->next != NULL && temp->next->data != value) {
        temp = temp->next;
    }
    
    // If value not found
    if (temp->next == NULL) {
        printf("Value %d not found!\n", value);
        return;
    }
    
    // Delete node
    struct Node *nodeToDelete = temp->next;
    temp->next = nodeToDelete->next;
    free(nodeToDelete);
}

Time Complexity: O(n) Space Complexity: O(1)

3.4 Traversal and Display

void displayList(struct Node *head) {
    if (head == NULL) {
        printf("List is empty!\n");
        return;
    }
    
    struct Node *temp = head;
    printf("List: ");
    
    while (temp != NULL) {
        printf("%d", temp->data);
        if (temp->next != NULL) {
            printf(" → ");
        }
        temp = temp->next;
    }
    printf(" → NULL\n");
}

// Alternative: Using for loop
void displayList2(struct Node *head) {
    printf("List: ");
    for (struct Node *temp = head; temp != NULL; temp = temp->next) {
        printf("%d → ", temp->data);
    }
    printf("NULL\n");
}

Time Complexity: O(n) Space Complexity: O(1)

3.5 Searching

int search(struct Node *head, int value) {
    struct Node *temp = head;
    int position = 0;
    
    while (temp != NULL) {
        if (temp->data == value) {
            return position;  // Found at position
        }
        temp = temp->next;
        position++;
    }
    
    return -1;  // Not found
}

// Usage
int main() {
    struct Node *head = NULL;
    
    insertAtEnd(&head, 10);
    insertAtEnd(&head, 20);
    insertAtEnd(&head, 30);
    
    int pos = search(head, 20);
    if (pos != -1) {
        printf("Found at position: %d\n", pos);
    } else {
        printf("Not found!\n");
    }
    
    return 0;
}

Time Complexity: O(n) Space Complexity: O(1)

3.6 Length of List

int getLength(struct Node *head) {
    int count = 0;
    struct Node *temp = head;
    
    while (temp != NULL) {
        count++;
        temp = temp->next;
    }
    
    return count;
}

// Recursive version
int getLengthRecursive(struct Node *head) {
    if (head == NULL) {
        return 0;
    }
    return 1 + getLengthRecursive(head->next);
}

Time Complexity: O(n) Space Complexity: O(1) for iterative, O(n) for recursive

Module 6 : Linked List

4. Complete Singly Linked List Example

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

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

// Function prototypes
void insertAtBeginning(struct Node **head, int value);
void insertAtEnd(struct Node **head, int value);
void insertAtPosition(struct Node **head, int value, int position);
void deleteFromBeginning(struct Node **head);
void deleteFromEnd(struct Node **head);
void deleteAtPosition(struct Node **head, int position);
void deleteByValue(struct Node **head, int value);
void displayList(struct Node *head);
int search(struct Node *head, int value);
int getLength(struct Node *head);
void freeList(struct Node **head);

int main() {
    struct Node *head = NULL;
    
    printf("=== Linked List Operations Demo ===\n\n");
    
    // Insert operations
    printf("Inserting elements...\n");
    insertAtBeginning(&head, 10);
    insertAtBeginning(&head, 5);
    insertAtEnd(&head, 20);
    insertAtEnd(&head, 25);
    insertAtPosition(&head, 15, 2);
    
    printf("After insertions: ");
    displayList(head);
    printf("Length: %d\n\n", getLength(head));
    
    // Search operation
    int searchValue = 15;
    int pos = search(head, searchValue);
    if (pos != -1) {
        printf("Value %d found at position %d\n\n", searchValue, pos);
    } else {
        printf("Value %d not found\n\n", searchValue);
    }
    
    // Delete operations
    printf("Deleting from beginning...\n");
    deleteFromBeginning(&head);
    displayList(head);
    
    printf("Deleting from end...\n");
    deleteFromEnd(&head);
    displayList(head);
    
    printf("Deleting at position 1...\n");
    deleteAtPosition(&head, 1);
    displayList(head);
    
    printf("Deleting value 10...\n");
    deleteByValue(&head, 10);
    displayList(head);
    
    printf("\nFinal length: %d\n", getLength(head));
    
    // Clean up
    freeList(&head);
    printf("Memory freed.\n");
    
    return 0;
}

// Function implementations
void insertAtBeginning(struct Node **head, int value) {
    struct Node *newNode = (struct Node*)malloc(sizeof(struct Node));
    if (newNode == NULL) {
        printf("Memory allocation failed!\n");
        return;
    }
    newNode->data = value;
    newNode->next = *head;
    *head = newNode;
}

void insertAtEnd(struct Node **head, int value) {
    struct Node *newNode = (struct Node*)malloc(sizeof(struct Node));
    if (newNode == NULL) {
        printf("Memory allocation failed!\n");
        return;
    }
    newNode->data = value;
    newNode->next = NULL;
    
    if (*head == NULL) {
        *head = newNode;
        return;
    }
    
    struct Node *temp = *head;
    while (temp->next != NULL) {
        temp = temp->next;
    }
    temp->next = newNode;
}

void insertAtPosition(struct Node **head, int value, int position) {
    struct Node *newNode = (struct Node*)malloc(sizeof(struct Node));
    if (newNode == NULL) {
        printf("Memory allocation failed!\n");
        return;
    }
    newNode->data = value;
    
    if (position == 0) {
        newNode->next = *head;
        *head = newNode;
        return;
    }
    
    struct Node *temp = *head;
    for (int i = 0; i < position - 1 && temp != NULL; i++) {
        temp = temp->next;
    }
    
    if (temp == NULL) {
        printf("Invalid position!\n");
        free(newNode);
        return;
    }
    
    newNode->next = temp->next;
    temp->next = newNode;
}

void deleteFromBeginning(struct Node **head) {
    if (*head == NULL) {
        printf("List is empty!\n");
        return;
    }
    struct Node *temp = *head;
    *head = (*head)->next;
    free(temp);
}

void deleteFromEnd(struct Node **head) {
    if (*head == NULL) {
        printf("List is empty!\n");
        return;
    }
    
    if ((*head)->next == NULL) {
        free(*head);
        *head = NULL;
        return;
    }
    
    struct Node *temp = *head;
    while (temp->next->next != NULL) {
        temp = temp->next;
    }
    free(temp->next);
    temp->next = NULL;
}

void deleteAtPosition(struct Node **head, int position) {
    if (*head == NULL) {
        printf("List is empty!\n");
        return;
    }
    
    if (position == 0) {
        struct Node *temp = *head;
        *head = (*head)->next;
        free(temp);
        return;
    }
    
    struct Node *temp = *head;
    for (int i = 0; i < position - 1 && temp != NULL; i++) {
        temp = temp->next;
    }
    
    if (temp == NULL || temp->next == NULL) {
        printf("Invalid position!\n");
        return;
    }
    
    struct Node *nodeToDelete = temp->next;
    temp->next = nodeToDelete->next;
    free(nodeToDelete);
}

void deleteByValue(struct Node **head, int value) {
    if (*head == NULL) {
        printf("List is empty!\n");
        return;
    }
    
    if ((*head)->data == value) {
        struct Node *temp = *head;
        *head = (*head)->next;
        free(temp);
        return;
    }
    
    struct Node *temp = *head;
    while (temp->next != NULL && temp->next->data != value) {
        temp = temp->next;
    }
    
    if (temp->next == NULL) {
        printf("Value %d not found!\n", value);
        return;
    }
    
    struct Node *nodeToDelete = temp->next;
    temp->next = nodeToDelete->next;
    free(nodeToDelete);
}

void displayList(struct Node *head) {
    if (head == NULL) {
        printf("List is empty!\n");
        return;
    }
    
    struct Node *temp = head;
    while (temp != NULL) {
        printf("%d", temp->data);
        if (temp->next != NULL) {
            printf(" → ");
        }
        temp = temp->next;
    }
    printf(" → NULL\n");
}

int search(struct Node *head, int value) {
    struct Node *temp = head;
    int position = 0;
    
    while (temp != NULL) {
        if (temp->data == value) {
            return position;
        }
        temp = temp->next;
        position++;
    }
    return -1;
}

int getLength(struct Node *head) {
    int count = 0;
    struct Node *temp = head;
    
    while (temp != NULL) {
        count++;
        temp = temp->next;
    }
    return count;
}

void freeList(struct Node **head) {
    struct Node *temp;
    while (*head != NULL) {
        temp = *head;
        *head = (*head)->next;
        free(temp);
    }
}
Module 6 : Linked List

5. Doubly Linked List

5.1 Node Structure

struct DNode {
    int data;
    struct DNode *prev;  // Pointer to previous node
    struct DNode *next;  // Pointer to next node
};

Visual Representation:

NULL ← [prev|10|next] ⇄ [prev|20|next] ⇄ [prev|30|next] → NULL
        ↑
       HEAD

5.2 Advantages of Doubly Linked List

  1. Bidirectional Traversal: Can move forward and backward
  2. Easy Deletion: Don't need previous node reference
  3. Easier Insertion: Can insert before a node easily

Disadvantages:

  1. More Memory: Extra pointer per node
  2. Complex Operations: Must update two pointers

5.3 Basic Operations

5.3.1 Insert at Beginning

void insertAtBeginning(struct DNode **head, int value) {
    // Create new node
    struct DNode *newNode = (struct DNode*)malloc(sizeof(struct DNode));
    if (newNode == NULL) {
        printf("Memory allocation failed!\n");
        return;
    }
    
    newNode->data = value;
    newNode->prev = NULL;
    newNode->next = *head;
    
    // Update head's prev if list not empty
    if (*head != NULL) {
        (*head)->prev = newNode;
    }
    
    *head = newNode;
}

Visual Steps:

Initial: head → [NULL|10|●] ⇄ [●|20|NULL]

Step 1: Create newNode [NULL|5|NULL]

Step 2: Connect newNode to head
newNode: [NULL|5|●] → [NULL|10|●]

Step 3: Update head's prev
[NULL|5|●] ⇄ [●|10|●]

Step 4: Update head
head → [NULL|5|●] ⇄ [●|10|●] ⇄ [●|20|NULL]

5.3.2 Insert at End

void insertAtEnd(struct DNode **head, int value) {
    struct DNode *newNode = (struct DNode*)malloc(sizeof(struct DNode));
    if (newNode == NULL) {
        printf("Memory allocation failed!\n");
        return;
    }
    
    newNode->data = value;
    newNode->next = NULL;
    
    // If list is empty
    if (*head == NULL) {
        newNode->prev = NULL;
        *head = newNode;
        return;
    }
    
    // Traverse to last node
    struct DNode *temp = *head;
    while (temp->next != NULL) {
        temp = temp->next;
    }
    
    // Insert at end
    temp->next = newNode;
    newNode->prev = temp;
}

5.3.3 Delete Node

void deleteNode(struct DNode **head, struct DNode *nodeToDelete) {
    // Check if list or node is NULL
    if (*head == NULL || nodeToDelete == NULL) {
        return;
    }
    
    // If node is head
    if (*head == nodeToDelete) {
        *head = nodeToDelete->next;
    }
    
    // Update next's prev pointer
    if (nodeToDelete->next != NULL) {
        nodeToDelete->next->prev = nodeToDelete->prev;
    }
    
    // Update prev's next pointer
    if (nodeToDelete->prev != NULL) {
        nodeToDelete->prev->next = nodeToDelete->next;
    }
    
    free(nodeToDelete);
}

5.3.4 Reverse Traversal

void displayReverse(struct DNode *head) {
    if (head == NULL) {
        printf("List is empty!\n");
        return;
    }
    
    // Go to last node
    struct DNode *temp = head;
    while (temp->next != NULL) {
        temp = temp->next;
    }
    
    // Traverse backward
    printf("List (reverse): ");
    while (temp != NULL) {
        printf("%d", temp->data);
        if (temp->prev != NULL) {
            printf(" ← ");
        }
        temp = temp->prev;
    }
    printf("\n");
}
Module 6 : Linked List

6. Circular Linked List

6.1 Structure

In a circular linked list, the last node points back to the first node (or head).

struct Node {
    int data;
    struct Node *next;
};

Visual Representation:

      ┌───────────────────────────┐
      ↓                           │
HEAD → [10|●] → [20|●] → [30|●] ──┘

6.2 Operations

6.2.1 Insert at Beginning

void insertAtBeginning(struct Node **head, int value) {
    struct Node *newNode = (struct Node*)malloc(sizeof(struct Node));
    if (newNode == NULL) {
        printf("Memory allocation failed!\n");
        return;
    }
    
    newNode->data = value;
    
    // If list is empty
    if (*head == NULL) {
        newNode->next = newNode;  // Points to itself
        *head = newNode;
        return;
    }
    
    // Find last node
    struct Node *temp = *head;
    while (temp->next != *head) {
        temp = temp->next;
    }
    
    // Insert at beginning
    newNode->next = *head;
    temp->next = newNode;
    *head = newNode;
}

6.2.2 Display Circular List

void displayCircular(struct Node *head) {
    if (head == NULL) {
        printf("List is empty!\n");
        return;
    }
    
    struct Node *temp = head;
    printf("List: ");
    
    do {
        printf("%d", temp->data);
        temp = temp->next;
        if (temp != head) {
            printf(" → ");
        }
    } while (temp != head);
    
    printf(" → (back to %d)\n", head->data);
}

6.2.3 Delete Node

void deleteNode(struct Node **head, int value) {
    if (*head == NULL) {
        printf("List is empty!\n");
        return;
    }
    
    struct Node *temp = *head;
    struct Node *prev = NULL;
    
    // If head node contains the value
    if (temp->data == value) {
        // If only one node
        if (temp->next == *head) {
            free(temp);
            *head = NULL;
            return;
        }
        
        // Find last node
        while (temp->next != *head) {
            temp = temp->next;
        }
        
        // Delete head
        temp->next = (*head)->next;
        free(*head);
        *head = temp->next;
        return;
    }
    
    // Search for the node
    prev = *head;
    temp = (*head)->next;
    
    while (temp != *head && temp->data != value) {
        prev = temp;
        temp = temp->next;
    }
    
    // If value not found
    if (temp == *head) {
        printf("Value %d not found!\n", value);
        return;
    }
    
    // Delete node
    prev->next = temp->next;
    free(temp);
}

6.2.4 Count Nodes

int countNodes(struct Node *head) {
    if (head == NULL) {
        return 0;
    }
    
    int count = 1;
    struct Node *temp = head->next;
    
    while (temp != head) {
        count++;
        temp = temp->next;
    }
    
    return count;
}
Module 6 : Linked List

7. Advanced Linked List Operations

7.1 Reverse a Singly Linked List

Method 1: Iterative

void reverseList(struct Node **head) {
    struct Node *prev = NULL;
    struct Node *current = *head;
    struct Node *next = NULL;
    
    while (current != NULL) {
        // Store next
        next = current->next;
        
        // Reverse current node's pointer
        current->next = prev;
        
        // Move pointers one position ahead
        prev = current;
        current = next;
    }
    
    *head = prev;
}

Visual Steps:

Initial: head → [10|●] → [20|●] → [30|NULL]

Step 1: prev=NULL, current=[10], next=[20]
NULL ← [10]   [20|●] → [30|NULL]

Step 2: prev=[10], current=[20], next=[30]
NULL ← [10] ← [20]   [30|NULL]

Step 3: prev=[20], current=[30], next=NULL
NULL ← [10] ← [20] ← [30]

Final: head → [30|●] → [20|●] → [10|NULL]

Time Complexity: O(n) Space Complexity: O(1)

Method 2: Recursive

struct Node* reverseRecursive(struct Node *head) {
    // Base case: empty list or single node
    if (head == NULL || head->next == NULL) {
        return head;
    }
    
    // Reverse the rest of the list
    struct Node *newHead = reverseRecursive(head->next);
    
    // Make next node point back
    head->next->next = head;
    head->next = NULL;
    
    return newHead;
}

// Wrapper function
void reverseListRecursive(struct Node **head) {
    *head = reverseRecursive(*head);
}

Time Complexity: O(n) Space Complexity: O(n) - recursion stack

7.2 Find Middle Element

Method 1: Two-Pass

int findMiddle(struct Node *head) {
    if (head == NULL) {
        printf("List is empty!\n");
        return -1;
    }
    
    // Count nodes
    int count = 0;
    struct Node *temp = head;
    while (temp != NULL) {
        count++;
        temp = temp->next;
    }
    
    // Go to middle
    temp = head;
    for (int i = 0; i < count / 2; i++) {
        temp = temp->next;
    }
    
    return temp->data;
}

Method 2: Slow-Fast Pointer (Optimal)

int findMiddleFast(struct Node *head) {
    if (head == NULL) {
        printf("List is empty!\n");
        return -1;
    }
    
    struct Node *slow = head;
    struct Node *fast = head;
    
    // Fast moves 2 steps, slow moves 1 step
    while (fast != NULL && fast->next != NULL) {
        slow = slow->next;
        fast = fast->next->next;
    }
    
    return slow->data;
}

Visual Steps:

List: [10] → [20] → [30] → [40] → [50] → NULL

Step 1: slow=[10], fast=[10]
Step 2: slow=[20], fast=[30]
Step 3: slow=[30], fast=[50]
Step 4: fast->next=NULL, stop

Middle: 30

Time Complexity: O(n) Space Complexity: O(1)

7.3 Detect Cycle (Floyd's Algorithm)

int hasCycle(struct Node *head) {
    if (head == NULL) {
        return 0;
    }
    
    struct Node *slow = head;
    struct Node *fast = head;
    
    while (fast != NULL && fast->next != NULL) {
        slow = slow->next;
        fast = fast->next->next;
        
        // If they meet, there's a cycle
        if (slow == fast) {
            return 1;
        }
    }
    
    return 0;  // No cycle
}

Visual Representation:

Cycle Example:
      ┌──────────────┐
      ↓              │
[10] → [20] → [30] → [40]
                ↑     ↓
                └─────┘

Without cycle:
[10] → [20] → [30] → [40] → NULL

Time Complexity: O(n) Space Complexity: O(1)

7.4 Remove Duplicates from Sorted List

void removeDuplicates(struct Node *head) {
    if (head == NULL) {
        return;
    }
    
    struct Node *current = head;
    
    while (current->next != NULL) {
        if (current->data == current->next->data) {
            // Duplicate found
            struct Node *temp = current->next;
            current->next = temp->next;
            free(temp);
        } else {
            current = current->next;
        }
    }
}

Visual Steps:

Module 6 : Linked List

8. Practical Applications

8.1 Polynomial Addition

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

// Node for polynomial term
struct PolyNode {
    int coeff;  // Coefficient
    int exp;    // Exponent
    struct PolyNode *next;
};

// Insert term in descending order of exponent
void insertTerm(struct PolyNode **poly, int coeff, int exp) {
    struct PolyNode *newNode = (struct PolyNode*)malloc(sizeof(struct PolyNode));
    newNode->coeff = coeff;
    newNode->exp = exp;
    newNode->next = NULL;
    
    if (*poly == NULL || (*poly)->exp < exp) {
        newNode->next = *poly;
        *poly = newNode;
    } else {
        struct PolyNode *temp = *poly;
        while (temp->next != NULL && temp->next->exp > exp) {
            temp = temp->next;
        }
        newNode->next = temp->next;
        temp->next = newNode;
    }
}

// Add two polynomials
struct PolyNode* addPolynomials(struct PolyNode *poly1, struct PolyNode *poly2) {
    struct PolyNode *result = NULL;
    
    while (poly1 != NULL && poly2 != NULL) {
        if (poly1->exp > poly2->exp) {
            insertTerm(&result, poly1->coeff, poly1->exp);
            poly1 = poly1->next;
        } else if (poly1->exp < poly2->exp) {
            insertTerm(&result, poly2->coeff, poly2->exp);
            poly2 = poly2->next;
        } else {
            // Same exponent - add coefficients
            int sumCoeff = poly1->coeff + poly2->coeff;
            if (sumCoeff != 0) {
                insertTerm(&result, sumCoeff, poly1->exp);
            }
            poly1 = poly1->next;
            poly2 = poly2->next;
        }
    }
    
    // Add remaining terms
    while (poly1 != NULL) {
        insertTerm(&result, poly1->coeff, poly1->exp);
        poly1 = poly1->next;
    }
    
    while (poly2 != NULL) {
        insertTerm(&result, poly2->coeff, poly2->exp);
        poly2 = poly2->next;
    }
    
    return result;
}

// Display polynomial
void displayPoly(struct PolyNode *poly) {
    if (poly == NULL) {
        printf("0\n");
        return;
    }
    
    while (poly != NULL) {
        printf("%dx^%d", poly->coeff, poly->exp);
        poly = poly->next;
        if (poly != NULL) {
            printf(" + ");
        }
    }
    printf("\n");
}

int main() {
    struct PolyNode *poly1 = NULL;
    struct PolyNode *poly2 = NULL;
    
    // Create first polynomial: 5x^3 + 4x^2 + 2
    insertTerm(&poly1, 5, 3);
    insertTerm(&poly1, 4, 2);
    insertTerm(&poly1, 2, 0);
    
    // Create second polynomial: 3x^3 + 2x + 1
    insertTerm(&poly2, 3, 3);
    insertTerm(&poly2, 2, 1);
    insertTerm(&poly2, 1, 0);
    
    printf("Polynomial 1: ");
    displayPoly(poly1);
    
    printf("Polynomial 2: ");
    displayPoly(poly2);
    
    struct PolyNode *result = addPolynomials(poly1, poly2);
    
    printf("Sum: ");
    displayPoly(result);
    
    return 0;
}

/* Output:
Polynomial 1: 5x^3 + 4x^2 + 2x^0
Polynomial 2: 3x^3 + 2x^1 + 1x^0
Sum: 8x^3 + 4x^2 + 2x^1 + 3x^0
*/

8.2 Music Playlist Implementation

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

struct Song {
    char title[100];
    char artist[100];
    int duration;  // in seconds
    struct Song *next;
};

// Add song to playlist
void addSong(struct Song **playlist, const char *title, const char *artist, int duration) {
    struct Song *newSong = (struct Song*)malloc(sizeof(struct Song));
    strcpy(newSong->title, title);
    strcpy(newSong->artist, artist);
    newSong->duration = duration;
    newSong->next = NULL;
    
    if (*playlist == NULL) {
        *playlist = newSong;
    } else {
        struct Song *temp = *playlist;
        while (temp->next != NULL) {
            temp = temp->next;
        }
        temp->next = newSong;
    }
}

// Display playlist
void displayPlaylist(struct Song *playlist) {
    if (playlist == NULL) {
        printf("Playlist is empty!\n");
        return;
    }
    
    int count = 1;
    printf("\n=== Playlist ===\n");
    while (playlist != NULL) {
        printf("%d. %s - %s (%d:%02d)\n", 
               count++, 
               playlist->title, 
               playlist->artist,
               playlist->duration / 60, 
               playlist->duration % 60);
        playlist = playlist->next;
    }
}

// Remove song by title
void removeSong(struct Song **playlist, const char *title) {
    if (*playlist == NULL) {
        printf("Playlist is empty!\n");
        return;
    }
    
    // If first song matches
    if (strcmp((*playlist)->title, title) == 0) {
        struct Song *temp = *playlist;
        *playlist = (*playlist)->next;
        free(temp);
        printf("Song removed: %s\n", title);
        return;
    }
    
    // Search for song
    struct Song *temp = *playlist;
    while (temp->next != NULL && strcmp(temp->next->title, title) != 0) {
        temp = temp->next;
    }
    
    if (temp->next == NULL) {
        printf("Song not found: %s\n", title);
        return;
    }
    
    struct Song *toRemove = temp->next;
    temp->next = toRemove->next;
    free(toRemove);
    printf("Song removed: %s\n", title);
}

// Calculate total duration
int totalDuration(struct Song *playlist) {
    int total = 0;
    while (playlist != NULL) {
        total += playlist->duration;
        playlist = playlist->next;
    }
    return total;
}

int main() {
    struct Song *myPlaylist = NULL;
    
    // Add songs
    addSong(&myPlaylist, "Bohemian Rhapsody", "Queen", 354);
    addSong(&myPlaylist, "Stairway to Heaven", "Led Zeppelin", 482);
    addSong(&myPlaylist, "Hotel California", "Eagles", 391);
    addSong(&myPlaylist, "Imagine", "John Lennon", 183);
    
    displayPlaylist(myPlaylist);
    
    int total = totalDuration(myPlaylist);
    printf("\nTotal duration: %d:%02d\n", total / 60, total % 60);
    
    // Remove a song
    removeSong(&myPlaylist, "Hotel California");
    
    displayPlaylist(myPlaylist);
    
    return 0;
}

8.3 Browser History (Back/Forward Navigation)

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

struct Page {
    char url[200];
    struct Page *prev;
    struct Page *next;
};

struct Browser {
    struct Page *current;
};

// Visit new page
void visitPage(struct Browser *browser, const char *url) {
    struct Page *newPage = (struct Page*)malloc(sizeof(struct Page));
    strcpy(newPage->url, url);
    newPage->next = NULL;
    
    if (browser->current != NULL) {
        // Clear forward history
        struct Page *temp = browser->current->next;
        while (temp != NULL) {
            struct Page *toDelete = temp;
            temp = temp->next;
            free(toDelete);
        }
        
        browser->current->next = newPage;
        newPage->prev = browser->current;
    } else {
        newPage->prev = NULL;
    }
    
    browser->current = newPage;
    printf("Visited: %s\n", url);
}

// Go back
void goBack(struct Browser *browser) {
    if (browser->current == NULL || browser->current->prev == NULL) {
        printf("Cannot go back!\n");
        return;
    }
    
    browser->current = browser->current->prev;
    printf("Back to: %s\n", browser->current->url);
}

// Go forward
void goForward(struct Browser *browser) {
    if (browser->current == NULL || browser->current->next == NULL) {
        printf("Cannot go forward!\n");
        return;
    }
    
    browser->current = browser->current->next;
    printf("Forward to: %s\n", browser->current->url);
}

// Display current page
void displayCurrent(struct Browser *browser) {
    if (browser->current == NULL) {
        printf("No page loaded!\n");
    } else {
        printf("Current page: %s\n", browser->current->url);
    }
}

int main() {
    struct Browser browser = {NULL};
    
    visitPage(&browser, "https://google.com");
    visitPage(&browser, "https://github.com");
    visitPage(&browser, "https://stackoverflow.com");
    
    printf("\n");
    goBack(&browser);
    goBack(&browser);
    
    printf("\n");
    goForward(&browser);
    
    printf("\n");
    visitPage(&browser, "https://reddit.com");
    
    printf("\n");
    displayCurrent(&browser);
    
    return 0;
}
Module 6 : Linked List

9. Common Errors and Debugging

9.1 Memory Leaks

Problem:

void createList() {
    struct Node *head = (struct Node*)malloc(sizeof(struct Node));
    head->data = 10;
    head->next = NULL;
    // MEMORY LEAK! head is lost when function returns
}

Solution:

struct Node* createList() {
    struct Node *head = (struct Node*)malloc(sizeof(struct Node));
    head->data = 10;
    head->next = NULL;
    return head;  // Return pointer to caller
}

// In main:
struct Node *myList = createList();
// ... use list ...
freeList(&myList);  // Free memory when done

9.2 Dereferencing NULL Pointer

Problem:

void insertAtEnd(struct Node **head, int value) {
    struct Node *temp = *head;
    while (temp->next != NULL) {  // CRASH if head is NULL!
        temp = temp->next;
    }
    // ...
}

Solution:

void insertAtEnd(struct Node **head, int value) {
    struct Node *newNode = (struct Node*)malloc(sizeof(struct Node));
    newNode->data = value;
    newNode->next = NULL;
    
    if (*head == NULL) {  // Check for NULL first!
        *head = newNode;
        return;
    }
    
    struct Node *temp = *head;
    while (temp->next != NULL) {
        temp = temp->next;
    }
    temp->next = newNode;
}

9.3 Lost Node References

Problem:

void deleteNode(struct Node **head, int position) {
    struct Node *temp = *head;
    for (int i = 0; i < position - 1; i++) {
        temp = temp->next;
    }
    temp->next = temp->next->next;  // MEMORY LEAK! Node not freed
}

Solution:

void deleteNode(struct Node **head, int position) {
    struct Node *temp = *head;
    for (int i = 0; i < position - 1; i++) {
        temp = temp->next;
    }
    struct Node *nodeToDelete = temp->next;  // Save reference
    temp->next = nodeToDelete->next;
    free(nodeToDelete);  // Free memory
}

9.4 Infinite Loop in Circular List

Problem:

void display(struct Node *head) {
    struct Node *temp = head;
    while (temp != NULL) {  // Never NULL in circular list!
        printf("%d ", temp->data);
        temp = temp->next;
    }
}

Solution:

void displayCircular(struct Node *head) {
    if (head == NULL) return;
    
    struct Node *temp = head;
    do {
        printf("%d ", temp->data);
        temp = temp->next;
    } while (temp != head);  // Check if back to head
}

9.5 Incorrect Pointer Updates

Problem:

void insertAtBeginning(struct Node *head, int value) {
    struct Node *newNode = (struct Node*)malloc(sizeof(struct Node));
    newNode->data = value;
    newNode->next = head;
    head = newNode;  // Only changes local copy!
}

Solution:

void insertAtBeginning(struct Node **head, int value) {
    struct Node *newNode = (struct Node*)malloc(sizeof(struct Node));
    newNode->data = value;
    newNode->next = *head;
    *head = newNode;  // Updates actual head pointer
}

9.6 Debugging Techniques

Print Node Addresses

void debugList(struct Node *head) {
    printf("\n=== Debug Info ===\n");
    struct Node *temp = head;
    int count = 0;
    
    while (temp != NULL) {
        printf("Node %d:\n", count++);
        printf("  Address: %p\n", (void*)temp);
        printf("  Data: %d\n", temp->data);
        printf("  Next: %p\n", (void*)temp->next);
        temp = temp->next;
    }
    printf("==================\n\n");
}

Check List Integrity

int checkListIntegrity(struct Node *head) {
    if (head == NULL) {
        return 1;  // Empty list is valid
    }
    
    struct Node *slow = head;
    struct Node *fast = head;
    
    // Check for cycles
    while (fast != NULL && fast->next != NULL) {
        slow = slow->next;
        fast = fast->next->next;
        
        if (slow == fast) {
            printf("ERROR: Cycle detected!\n");
            return 0;
        }
    }
    
    printf("List integrity: OK\n");
    return 1;
}

Visualize List

void visualizeList(struct Node *head) {
    if (head == NULL) {
        printf("NULL\n");
        return;
    }
    
    struct Node *temp = head;
    printf("HEAD → ");
    
    while (temp != NULL) {
        printf("[%d]", temp->data);
        temp = temp->next;
        if (temp != NULL) {
            printf(" → ");
        }
    }
    printf(" → NULL\n");
}

9.7 Common Error Messages

Segmentation Fault:

Memory Leak:

Infinite Loop:

9.8 Preventive Measures

// Always check malloc return value
struct Node *newNode = (struct Node*)malloc(sizeof(struct Node));
if (newNode == NULL) {
    fprintf(stderr, "Memory allocation failed!\n");
    return NULL;
}

// Check for NULL before dereferencing
if (head != NULL) {
    // Safe to use head->data
}

// Free all nodes before exiting
void freeList(struct Node **head) {
    struct Node *temp;
    while (*head != NULL) {
        temp = *head;
        *head = (*head)->next;
        free(temp);
    }
}

// Set pointers to NULL after freeing
free(node);
node = NULL;

// Use assertions for debugging
#include <assert.h>
assert(head != NULL);  // Program stops if false

Module 7: Searching & Sorting

By the end of this module, students will be able to:

Module 7: Searching & Sorting

1. Introduction to Searching

1.1 What is Searching?

Searching is the process of finding a particular element or checking if an element exists in a data structure (array, linked list, tree, etc.).

Real-World Analogies:

Types of Searching:

  1. Linear Search - Sequential search through elements
  2. Binary Search - Divide and conquer approach (requires sorted data)
  3. Jump Search - Jumping ahead by fixed steps
  4. Interpolation Search - Improved binary search for uniformly distributed data
  5. Exponential Search - Finding range then binary search

Note: There's actually a lot more searching algorithms, but we'll focus on the most common ones.

Importance:

Performance Metrics:

Module 7: Searching & Sorting

2. Linear Search

2.1 Concept

Linear Search (also called Sequential Search) checks every element in the array sequentially until the target is found or the end is reached.

How it works:

  1. Start from the first element
  2. Compare each element with the target
  3. If match found, return the position
  4. If end reached without match, return -1

Visual Representation:

Array: [10, 25, 30, 15, 40, 35]
Target: 15

Step 1: Check 10 ≠ 15
Step 2: Check 25 ≠ 15
Step 3: Check 30 ≠ 15
Step 4: Check 15 = 15 ✓ Found at index 3!

2.2 Implementation

#include <stdio.h>

int linearSearch(int arr[], int n, int target) {
    for (int i = 0; i < n; i++) {
        if (arr[i] == target) {
            return i;  // Return index if found
        }
    }
    return -1;  // Return -1 if not found
}

int main() {
    int arr[] = {10, 25, 30, 15, 40, 35};
    int n = sizeof(arr) / sizeof(arr[0]);
    int target = 15;
    
    int result = linearSearch(arr, n, target);
    
    if (result != -1) {
        printf("Element %d found at index %d\n", target, result);
    } else {
        printf("Element %d not found\n", target);
    }
    
    return 0;
}

Linear Search with Count

int linearSearchCount(int arr[], int n, int target, int *comparisons) {
    *comparisons = 0;
    
    for (int i = 0; i < n; i++) {
        (*comparisons)++;
        if (arr[i] == target) {
            return i;
        }
    }
    return -1;
}

// Usage
int main() {
    int arr[] = {10, 25, 30, 15, 40, 35};
    int n = sizeof(arr) / sizeof(arr[0]);
    int target = 15;
    int comparisons = 0;
    
    int result = linearSearchCount(arr, n, target, &comparisons);
    
    printf("Result: %d\n", result);
    printf("Comparisons made: %d\n", comparisons);
    
    return 0;
}

Find All Occurrences

void linearSearchAll(int arr[], int n, int target) {
    int found = 0;
    
    printf("Element %d found at indices: ", target);
    
    for (int i = 0; i < n; i++) {
        if (arr[i] == target) {
            printf("%d ", i);
            found = 1;
        }
    }
    
    if (!found) {
        printf("Not found");
    }
    printf("\n");
}

int main() {
    int arr[] = {10, 25, 30, 25, 40, 25};
    int n = sizeof(arr) / sizeof(arr[0]);
    
    linearSearchAll(arr, n, 25);
    // Output: Element 25 found at indices: 1 3 5
    
    return 0;
}

2.3 Complexity Analysis

Case Time Complexity Description
Best Case O(1) Element found at first position
Average Case O(n) Element found in middle
Worst Case O(n) Element at last position or not found
Space Complexity O(1) No extra space needed

Visualization:

Best Case (1 comparison):
[15, 25, 30, ...] → Found immediately!

Average Case (n/2 comparisons):
[10, 25, 30, 15, ...] → Found in middle

Worst Case (n comparisons):
[10, 25, 30, 40, 35, 15] → Found at end
or
[10, 25, 30, 40, 35, 20] → Not found (checked all)

2.4 Advantages and Disadvantages

Advantages:

Disadvantages:

When to Use:

Module 7: Searching & Sorting

3. Binary Search

3.1 Concept

Binary Search is a fast search algorithm that works on sorted arrays by repeatedly dividing the search interval in half.

Prerequisites:

How it works:

  1. Compare target with middle element
  2. If match found, return position
  3. If target is smaller, search left half
  4. If target is larger, search right half
  5. Repeat until found or search space is empty

Visual Representation:

Sorted Array: [10, 15, 25, 30, 35, 40, 50]
Target: 35

Step 1: Check middle (30)
[10, 15, 25, 30, | 35, 40, 50]
              ^
35 > 30, search right half

Step 2: Check middle of right half (40)
[35, 40, 50]
     ^
35 < 40, search left half

Step 3: Check middle of remaining (35)
[35]
 ^
35 = 35, Found at index 4!

3.2 Implementation

Iterative Binary Search

#include <stdio.h>

int binarySearch(int arr[], int n, int target) {
    int left = 0;
    int right = n - 1;
    
    while (left <= right) {
        int mid = left + (right - left) / 2;  // Avoid overflow
        
        // Check if target is at mid
        if (arr[mid] == target) {
            return mid;
        }
        
        // If target is greater, ignore left half
        if (arr[mid] < target) {
            left = mid + 1;
        }
        // If target is smaller, ignore right half
        else {
            right = mid - 1;
        }
    }
    
    return -1;  // Element not found
}

int main() {
    int arr[] = {10, 15, 25, 30, 35, 40, 50};
    int n = sizeof(arr) / sizeof(arr[0]);
    int target = 35;
    
    int result = binarySearch(arr, n, target);
    
    if (result != -1) {
        printf("Element %d found at index %d\n", target, result);
    } else {
        printf("Element %d not found\n", target);
    }
    
    return 0;
}

Why mid = left + (right - left) / 2?

// This can overflow for large values:
int mid = (left + right) / 2;

// This is safer:
int mid = left + (right - left) / 2;

// Example of overflow:
// left = 2^30, right = 2^30
// (left + right) = 2^31 → overflow in 32-bit int
// left + (right - left) / 2 = no overflow

Recursive Binary Search

int binarySearchRecursive(int arr[], int left, int right, int target) {
    // Base case: element not found
    if (left > right) {
        return -1;
    }
    
    int mid = left + (right - left) / 2;
    
    // Element found at mid
    if (arr[mid] == target) {
        return mid;
    }
    
    // Search in left half
    if (arr[mid] > target) {
        return binarySearchRecursive(arr, left, mid - 1, target);
    }
    
    // Search in right half
    return binarySearchRecursive(arr, mid + 1, right, target);
}

// Wrapper function
int binarySearch(int arr[], int n, int target) {
    return binarySearchRecursive(arr, 0, n - 1, target);
}

Binary Search with Comparison Count

int binarySearchCount(int arr[], int n, int target, int *comparisons) {
    int left = 0;
    int right = n - 1;
    *comparisons = 0;
    
    while (left <= right) {
        int mid = left + (right - left) / 2;
        (*comparisons)++;
        
        if (arr[mid] == target) {
            return mid;
        }
        
        if (arr[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    
    return -1;
}

int main() {
    int arr[] = {10, 15, 25, 30, 35, 40, 50, 60, 70, 80};
    int n = sizeof(arr) / sizeof(arr[0]);
    int target = 35;
    int comparisons = 0;
    
    int result = binarySearchCount(arr, n, target, &comparisons);
    
    printf("Result: %d\n", result);
    printf("Comparisons: %d\n", comparisons);
    printf("Linear search would need: %d comparisons\n", result + 1);
    
    return 0;
}

3.3 Complexity Analysis

Case Time Complexity Description
Best Case O(1) Element found at middle
Average Case O(log n) Typical search
Worst Case O(log n) Element at edge or not found
Space Complexity O(1) iterative, O(log n) recursive Stack space for recursion

Comparison with Linear Search:

Array size: 1,000,000 elements

Linear Search:
- Average: 500,000 comparisons
- Worst: 1,000,000 comparisons

Binary Search:
- Average: 20 comparisons
- Worst: 20 comparisons

Speed improvement: ~50,000x faster!

Growth Comparison:

n       Linear Search    Binary Search
10      10               4
100     100              7
1,000   1,000            10
10,000  10,000           14
100,000 100,000          17

3.4 Binary Search Variations

Find First Occurrence

int findFirstOccurrence(int arr[], int n, int target) {
    int left = 0;
    int right = n - 1;
    int result = -1;
    
    while (left <= right) {
        int mid = left + (right - left) / 2;
        
        if (arr[mid] == target) {
            result = mid;
            right = mid - 1;  // Continue searching in left half
        }
        else if (arr[mid] < target) {
            left = mid + 1;
        }
        else {
            right = mid - 1;
        }
    }
    
    return result;
}

Visual Example:

Array: [10, 20, 20, 20, 30, 40]
Target: 20

Regular binary search might return index 1, 2, or 3
First occurrence will always return index 1

Find Last Occurrence

int findLastOccurrence(int arr[], int n, int target) {
    int left = 0;
    int right = n - 1;
    int result = -1;
    
    while (left <= right) {
        int mid = left + (right - left) / 2;
        
        if (arr[mid] == target) {
            result = mid;
            left = mid + 1;  // Continue searching in right half
        }
        else if (arr[mid] < target) {
            left = mid + 1;
        }
        else {
            right = mid - 1;
        }
    }
    
    return result;
}

Count Occurrences

int countOccurrences(int arr[], int n, int target) {
    int first = findFirstOccurrence(arr, n, target);
    
    if (first == -1) {
        return 0;  // Element not found
    }
    
    int last = findLastOccurrence(arr, n, target);
    
    return last - first + 1;
}

int main() {
    int arr[] = {10, 20, 20, 20, 30, 40};
    int n = sizeof(arr) / sizeof(arr[0]);
    
    int count = countOccurrences(arr, n, 20);
    printf("20 occurs %d times\n", count);
    // Output: 20 occurs 3 times
    
    return 0;
}

Find Insert Position

int searchInsertPosition(int arr[], int n, int target) {
    int left = 0;
    int right = n - 1;
    
    while (left <= right) {
        int mid = left + (right - left) / 2;
        
        if (arr[mid] == target) {
            return mid;
        }
        else if (arr[mid] < target) {
            left = mid + 1;
        }
        else {
            right = mid - 1;
        }
    }
    
    return left;  // Position where target should be inserted
}

int main() {
    int arr[] = {10, 20, 30, 50, 60};
    int n = sizeof(arr) / sizeof(arr[0]);
    
    printf("Insert 25 at position: %d\n", searchInsertPosition(arr, n, 25));
    printf("Insert 35 at position: %d\n", searchInsertPosition(arr, n, 35));
    printf("Insert 70 at position: %d\n", searchInsertPosition(arr, n, 70));
    
    return 0;
}

3.5 When to Use Binary Search

Use Binary Search when:

Don't use Binary Search when:

Module 7: Searching & Sorting

4. Introduction to Sorting

4.1 What is Sorting?

Sorting is the process of arranging elements in a specific order (ascending or descending).

Why Sort?

  1. Faster Searching: Enables binary search
  2. Data Organization: Makes data easier to understand
  3. Algorithm Requirements: Many algorithms require sorted input
  4. Data Presentation: Better user experience
  5. Finding Duplicates: Easier with sorted data

Real-World Examples:

4.2 Sorting Algorithm Categories

1. By Complexity:

2. By Method:

3. By Stability:

4. By Memory:

4.3 Sorting Algorithm Comparison Table

Algorithm Best Average Worst Space Stable Method
Bubble Sort O(n) O(n²) O(n²) O(1) Yes Exchange
Selection Sort O(n²) O(n²) O(n²) O(1) No Selection
Insertion Sort O(n) O(n²) O(n²) O(1) Yes Insertion
Merge Sort O(n log n) O(n log n) O(n log n) O(n) Yes Divide & Conquer
Quick Sort O(n log n) O(n log n) O(n²) O(log n) No Divide & Conquer
Heap Sort O(n log n) O(n log n) O(n log n) O(1) No Selection
Counting Sort O(n+k) O(n+k) O(n+k) O(k) Yes Non-comparison
Radix Sort O(d(n+k)) O(d(n+k)) O(d(n+k)) O(n+k) Yes Non-comparison
Module 7: Searching & Sorting

5. Simple Sorting Algorithms

5.1 Bubble Sort

Concept

Bubble Sort repeatedly steps through the list, compares adjacent elements, and swaps them if they're in the wrong order.

How it works:

  1. Compare adjacent elements
  2. Swap if in wrong order
  3. Repeat for all elements
  4. After each pass, largest element "bubbles" to the end

Visual Example:

Initial: [64, 34, 25, 12, 22, 11, 90]

Pass 1:
[34, 64, 25, 12, 22, 11, 90]  → 64 > 34, swap
[34, 25, 64, 12, 22, 11, 90]  → 64 > 25, swap
[34, 25, 12, 64, 22, 11, 90]  → 64 > 12, swap
[34, 25, 12, 22, 64, 11, 90]  → 64 > 22, swap
[34, 25, 12, 22, 11, 64, 90]  → 64 > 11, swap
[34, 25, 12, 22, 11, 64, 90]  → 64 < 90, no swap
→ 90 is in correct position!

Pass 2:
[25, 34, 12, 22, 11, 64, 90]
...continues until sorted...

Final: [11, 12, 22, 25, 34, 64, 90]

Implementation

Basic Bubble Sort:

#include <stdio.h>

void bubbleSort(int arr[], int n) {
    for (int i = 0; i < n - 1; i++) {
        // Last i elements are already in place
        for (int j = 0; j < n - i - 1; j++) {
            // Swap if element is greater than next
            if (arr[j] > arr[j + 1]) {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

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

int main() {
    int arr[] = {64, 34, 25, 12, 22, 11, 90};
    int n = sizeof(arr) / sizeof(arr[0]);
    
    printf("Original array: ");
    printArray(arr, n);
    
    bubbleSort(arr, n);
    
    printf("Sorted array: ");
    printArray(arr, n);
    
    return 0;
}

Optimized Bubble Sort (with flag):

void bubbleSortOptimized(int arr[], int n) {
    for (int i = 0; i < n - 1; i++) {
        int swapped = 0;  // Flag to detect if any swap happened
        
        for (int j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                swapped = 1;
            }
        }
        
        // If no swaps, array is sorted
        if (swapped == 0) {
            break;
        }
    }
}

Why Optimization Helps:

Nearly sorted array: [11, 12, 22, 25, 34, 64, 63]

Without optimization: 6 passes (always)
With optimization: 2 passes (stops when no swaps)

For already sorted array: [11, 12, 22, 25, 34, 64, 90]
Without: O(n²)
With: O(n) - only 1 pass!

Bubble Sort with Visualization:

void bubbleSortVisualize(int arr[], int n) {
    printf("\n=== Bubble Sort Visualization ===\n");
    
    for (int i = 0; i < n - 1; i++) {
        printf("\nPass %d:\n", i + 1);
        int swapped = 0;
        
        for (int j = 0; j < n - i - 1; j++) {
            printf("  Compare %d and %d: ", arr[j], arr[j + 1]);
            
            if (arr[j] > arr[j + 1]) {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                printf("Swap → ");
                swapped = 1;
            } else {
                printf("No swap → ");
            }
            
            printArray(arr, n);
        }
        
        if (swapped == 0) {
            printf("  Array is sorted!\n");
            break;
        }
    }
}

Complexity Analysis

Time Complexity:

Space Complexity: O(1) - in-place sorting

Comparisons and Swaps:

For array of size n:
- Comparisons: n(n-1)/2
- Swaps (worst case): n(n-1)/2

Example with n=5:
- Comparisons: 5×4/2 = 10
- Maximum swaps: 10

When to Use

Use Bubble Sort when:

Don't use when:

5.2 Selection Sort

Concept

Selection Sort divides the array into sorted and unsorted parts, repeatedly selects the minimum element from unsorted part and places it at the beginning.

How it works:

  1. Find minimum element in unsorted part
  2. Swap with first element of unsorted part
  3. Move boundary of sorted part
  4. Repeat until array is sorted

Visual Example:

Initial: [64, 25, 12, 22, 11]
         ↑ unsorted part

Pass 1: Find minimum (11)
[64, 25, 12, 22, 11]
                 ↑ minimum
[11, 25, 12, 22, 64]  → swap 11 and 64
 ↑   ↑ unsorted part
sorted

Pass 2: Find minimum (12)
[11, 25, 12, 22, 64]
         ↑ minimum
[11, 12, 25, 22, 64]  → swap 12 and 25
 ↑   ↑   ↑ unsorted
   sorted

Pass 3: Find minimum (22)
[11, 12, 25, 22, 64]
             ↑ minimum
[11, 12, 22, 25, 64]  → swap 22 and 25

Pass 4: Find minimum (25)
[11, 12, 22, 25, 64]
             ↑ already minimum
No swap needed

Final: [11, 12, 22, 25, 64]

Implementation

#include <stdio.h>

void selectionSort(int arr[], int n) {
    for (int i = 0; i < n - 1; i++) {
        // Find minimum element in unsorted part
        int minIndex = i;
        
        for (int j = i + 1; j < n; j++) {
            if (arr[j] < arr[minIndex]) {
                minIndex = j;
            }
        }
        
        // Swap minimum with first element of unsorted part
        if (minIndex != i) {
            int temp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = temp;
        }
    }
}

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

int main() {
    int arr[] = {64, 25, 12, 22, 11};
    int n = sizeof(arr) / sizeof(arr[0]);
    
    printf("Original array: ");
    printArray(arr, n);
    
    selectionSort(arr, n);
    
    printf("Sorted array: ");
    printArray(arr, n);
    
    return 0;
}

Selection Sort with Visualization:

void selectionSortVisualize(int arr[], int n) {
    printf("\n=== Selection Sort Visualization ===\n");
    
    for (int i = 0; i < n - 1; i++) {
        printf("\nPass %d: ", i + 1);
        
        // Print sorted part
        printf("Sorted[");
        for (int k = 0; k < i; k++) {
            printf("%d", arr[k]);
            if (k < i - 1) printf(", ");
        }
        printf("] ");
        
        int minIndex = i;
        printf("Finding minimum in unsorted part...\n");
        
        for (int j = i + 1; j < n; j++) {
            if (arr[j] < arr[minIndex]) {
                minIndex = j;
            }
        }
        
        printf("  Minimum: %d at index %d\n", arr[minIndex], minIndex);
        
        if (minIndex != i) {
            printf("  Swapping %d and %d\n", arr[i], arr[minIndex]);
            int temp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = temp;
        } else {
            printf("  Already in correct position\n");
        }
        
        printf("  Result: ");
        printArray(arr, n);
    }
}

Finding Maximum (Descending Order):

void selectionSortDescending(int arr[], int n) {
    for (int i = 0; i < n - 1; i++) {
        // Find maximum element in unsorted part
        int maxIndex = i;
        
        for (int j = i + 1; j < n; j++) {
            if (arr[j] > arr[maxIndex]) {  // Changed to >
                maxIndex = j;
            }
        }
        
        // Swap
        if (maxIndex != i) {
            int temp = arr[i];
            arr[i] = arr[maxIndex];
            arr[maxIndex] = temp;
        }
    }
}

Complexity Analysis

Time Complexity:

Space Complexity: O(1) - in-place sorting

Comparisons and Swaps:

For array of size n:
- Comparisons: n(n-1)/2 (always)
- Swaps: O(n) - maximum n-1 swaps

Advantage: Minimum number of swaps!

Comparison with Bubble Sort:

Array: [5, 4, 3, 2, 1]

Bubble Sort:
- Comparisons: 10
- Swaps: 10

Selection Sort:
- Comparisons: 10
- Swaps: 2 (only swap 5↔1, then 4↔2)

→ Selection Sort better when swaps are expensive!

Stability Issue

Selection Sort is NOT stable:

Input:  [4a, 3, 4b, 2]
Output: [2, 3, 4b, 4a]  ← Order of 4a and 4b changed!

Why? When we swap 4a with 2, 4b comes before 4a

When to Use

Use Selection Sort when:

Don't use when:

5.3 Insertion Sort

Concept

Insertion Sort builds the final sorted array one item at a time by inserting each element into its correct position.

Analogy: Like sorting playing cards in your hand:

How it works:

  1. Start with second element
  2. Compare with elements in sorted part (left side)
  3. Shift larger elements to the right
  4. Insert element in correct position
  5. Repeat for all elements

Visual Example:

Initial: [12, 11, 13, 5, 6]

Pass 1: Insert 11
[12, 11, 13, 5, 6]
 ↑   ↑
sorted | to insert

12 > 11, shift 12 right
[11, 12, 13, 5, 6]
 ↑   ↑
   sorted

Pass 2: Insert 13
[11, 12, 13, 5, 6]
 ↑   ↑   ↑
   sorted | to insert

13 > 12, already in position
[11, 12, 13, 5, 6]
 ↑   ↑   ↑
    sorted

Pass 3: Insert 5
[11, 12, 13, 5, 6]
 ↑   ↑   ↑   ↑
    sorted  | to insert

13 > 5, shift right → [11, 12, 13, 13, 6]
12 > 5, shift right → [11, 12, 12, 13, 6]
11 > 5, shift right → [11, 11, 12, 13, 6]
Insert 5 at position 0 → [5, 11, 12, 13, 6]

Pass 4: Insert 6
[5, 11, 12, 13, 6]
 ↑  ↑   ↑   ↑   ↑
      sorted   | to insert

13 > 6, shift right → [5, 11, 12, 13, 13]
12 > 6, shift right → [5, 11, 12, 12, 13]
11 > 6, shift right → [5, 11, 11, 12, 13]
5 < 6, insert at position 1 → [5, 6, 11, 12, 13]

Final: [5, 6, 11, 12, 13]

Implementation

Basic Insertion Sort:

#include <stdio.h>

void insertionSort(int arr[], int n) {
    for (int i = 1; i < n; i++) {
        int key = arr[i];  // Element to be inserted
        int j = i - 1;
        
        // Move elements greater than key one position ahead
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        
        // Insert key at correct position
        arr[j + 1] = key;
    }
}

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

int main() {
    int arr[] = {12, 11, 13, 5, 6};
    int n = sizeof(arr) / sizeof(arr[0]);
    
    printf("Original array: ");
    printArray(arr, n);
    
    insertionSort(arr, n);
    
    printf("Sorted array: ");
    printArray(arr, n);
    
    return 0;
}

Insertion Sort with Visualization:

void insertionSortVisualize(int arr[], int n) {
    printf("\n=== Insertion Sort Visualization ===\n");
    printf("Initial: ");
    printArray(arr, n);
    
    for (int i = 1; i < n; i++) {
        int key = arr[i];
        int j = i - 1;
        
        printf("\nPass %d: Insert %d\n", i, key);
        printf("  Sorted part: [");
        for (int k = 0; k < i; k++) {
            printf("%d", arr[k]);
            if (k < i - 1) printf(", ");
        }
        printf("]\n");
        
        int shifts = 0;
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
            shifts++;
        }
        
        arr[j + 1] = key;
        
        printf("  Shifts: %d\n", shifts);
        printf("  Result: ");
        printArray(arr, n);
    }
}

Descending Order:

void insertionSortDescending(int arr[], int n) {
    for (int i = 1; i < n; i++) {
        int key = arr[i];
        int j = i - 1;
        
        // Change condition to arr[j] < key
        while (j >= 0 && arr[j] < key) {
            arr[j + 1] = arr[j];
            j--;
        }
        
        arr[j + 1] = key;
    }
}

Binary Insertion Sort (Optimization):

// Find position using binary search
int binarySearch(int arr[], int item, int low, int high) {
    while (low <= high) {
        int mid = low + (high - low) / 2;
        
        if (arr[mid] == item) {
            return mid + 1;
        }
        else if (arr[mid] < item) {
            low = mid + 1;
        }
        else {
            high = mid - 1;
        }
    }
    
    return low;
}

void binaryInsertionSort(int arr[], int n) {
    for (int i = 1; i < n; i++) {
        int key = arr[i];
        
        // Find position using binary search
        int pos = binarySearch(arr, key, 0, i - 1);
        
        // Shift elements
        for (int j = i - 1; j >= pos; j--) {
            arr[j + 1] = arr[j];
        }
        
        // Insert key
        arr[pos] = key;
    }
}

Insertion Sort for Linked List:

struct Node {
    int data;
    struct Node *next;
};

void sortedInsert(struct Node **head, struct Node *newNode) {
    // If list is empty or new node should be first
    if (*head == NULL || (*head)->data >= newNode->data) {
        newNode->next = *head;
        *head = newNode;
        return;
    }
    
    // Find position to insert
    struct Node *current = *head;
    while (current->next != NULL && current->next->data < newNode->data) {
        current = current->next;
    }
    
    newNode->next = current->next;
    current->next = newNode;
}

void insertionSortList(struct Node **head) {
    struct Node *sorted = NULL;
    struct Node *current = *head;
    
    while (current != NULL) {
        struct Node *next = current->next;
        sortedInsert(&sorted, current);
        current = next;
    }
    
    *head = sorted;
}

Complexity Analysis

Time Complexity:

Space Complexity: O(1) - in-place sorting

Detailed Analysis:

Best Case (sorted): [1, 2, 3, 4, 5]
- Comparisons: n-1 (each element compared once)
- Shifts: 0
- Time: O(n)

Worst Case (reverse sorted): [5, 4, 3, 2, 1]
- Comparisons: 1+2+3+...+(n-1) = n(n-1)/2
- Shifts: Same as comparisons
- Time: O(n²)

Average Case (random):
- Comparisons: ~n²/4
- Shifts: ~n²/4
- Time: O(n²)

Performance on Different Inputs:

void testInsertionSort() {
    // Already sorted
    int sorted[] = {1, 2, 3, 4, 5};
    printf("Sorted array: Fast! O(n)\n");
    
    // Reverse sorted
    int reverse[] = {5, 4, 3, 2, 1};
    printf("Reverse array: Slow! O(n²)\n");
    
    // Nearly sorted
    int nearlySorted[] = {1, 2, 4, 3, 5};
    printf("Nearly sorted: Fast! O(n)\n");
    
    // Few elements out of place
    int fewMoves[] = {2, 1, 3, 4, 5};
    printf("Few out of place: Fast!\n");
}

Advantages

  1. Simple implementation
  2. Stable - preserves relative order
  3. In-place - O(1) extra space
  4. Adaptive - O(n) for nearly sorted data
  5. Online - can sort as data arrives

Stability Example:

Input:  [4a, 3, 4b, 2]
Output: [2, 3, 4a, 4b]  ← Order preserved!

When to Use

Use Insertion Sort when:

Don't use when:

Real-World Applications:

  1. Sorting small files
  2. Hybrid sorting (used in TimSort)
  3. Online algorithms
  4. Sorting linked lists
Module 7: Searching & Sorting

6. Efficient Sorting Algorithms

6.1 Merge Sort

Concept

Merge Sort is a divide-and-conquer algorithm that divides the array into halves, recursively sorts them, and then merges the sorted halves.

Divide and Conquer Strategy:

  1. Divide: Split array into two halves
  2. Conquer: Recursively sort each half
  3. Combine: Merge the two sorted halves

Visual Example:

Initial Array: [38, 27, 43, 3, 9, 82, 10]

DIVIDE Phase:
                [38, 27, 43, 3, 9, 82, 10]
                    /                \
        [38, 27, 43, 3]          [9, 82, 10]
           /        \              /       \
      [38, 27]    [43, 3]      [9, 82]   [10]
       /    \      /    \       /    \      |
     [38]  [27]  [43]  [3]    [9]  [82]  [10]

MERGE Phase:
     [38]  [27]  [43]  [3]    [9]  [82]  [10]
       \    /      \    /       \    /      |
      [27, 38]    [3, 43]      [9, 82]   [10]
           \        /              \       /
        [3, 27, 38, 43]          [9, 10, 82]
                    \                /
                [3, 9, 10, 27, 38, 43, 82]

Implementation

Merge Sort Algorithm:

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

// Merge two sorted subarrays
void merge(int arr[], int left, int mid, int right) {
    int n1 = mid - left + 1;  // Size of left subarray
    int n2 = right - mid;      // Size of right subarray
    
    // Create temporary arrays
    int *L = (int*)malloc(n1 * sizeof(int));
    int *R = (int*)malloc(n2 * sizeof(int));
    
    // Copy data to temporary arrays
    for (int i = 0; i < n1; i++) {
        L[i] = arr[left + i];
    }
    for (int j = 0; j < n2; j++) {
        R[j] = arr[mid + 1 + j];
    }
    
    // Merge the temporary arrays back
    int i = 0;      // Initial index of left subarray
    int j = 0;      // Initial index of right subarray
    int k = left;   // Initial index of merged array
    
    while (i < n1 && j < n2) {
        if (L[i] <= R[j]) {
            arr[k] = L[i];
            i++;
        } else {
            arr[k] = R[j];
            j++;
        }
        k++;
    }
    
    // Copy remaining elements of L[]
    while (i < n1) {
        arr[k] = L[i];
        i++;
        k++;
    }
    
    // Copy remaining elements of R[]
    while (j < n2) {
        arr[k] = R[j];
        j++;
        k++;
    }
    
    free(L);
    free(R);
}

// Main merge sort function
void mergeSort(int arr[], int left, int right) {
    if (left < right) {
        int mid = left + (right - left) / 2;
        
        // Sort first half
        mergeSort(arr, left, mid);
        
        // Sort second half
        mergeSort(arr, mid + 1, right);
        
        // Merge the sorted halves
        merge(arr, left, mid, right);
    }
}

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

int main() {
    int arr[] = {38, 27, 43, 3, 9, 82, 10};
    int n = sizeof(arr) / sizeof(arr[0]);
    
    printf("Original array: ");
    printArray(arr, n);
    
    mergeSort(arr, 0, n - 1);
    
    printf("Sorted array: ");
    printArray(arr, n);
    
    return 0;
}

Merge Sort with Visualization:

void mergeSortVisualize(int arr[], int left, int right, int depth) {
    if (left < right) {
        // Print indentation based on recursion depth
        for (int i = 0; i < depth; i++) printf("  ");
        
        printf("Divide: [");
        for (int i = left; i <= right; i++) {
            printf("%d", arr[i]);
            if (i < right) printf(", ");
        }
        printf("]\n");
        
        int mid = left + (right - left) / 2;
        
        mergeSortVisualize(arr, left, mid, depth + 1);
        mergeSortVisualize(arr, mid + 1, right, depth + 1);
        
        merge(arr, left, mid, right);
        
        // Print result after merge
        for (int i = 0; i < depth; i++) printf("  ");
        printf("Merge:  [");
        for (int i = left; i <= right; i++) {
            printf("%d", arr[i]);
            if (i < right) printf(", ");
        }
        printf("]\n");
    }
}

Iterative Merge Sort:

void mergeSortIterative(int arr[], int n) {
    // Start with merge subarrays of size 1, then 2, 4, 8, ...
    for (int currSize = 1; currSize < n; currSize *= 2) {
        // Pick starting index of left sub array
        for (int leftStart = 0; leftStart < n - 1; leftStart += 2 * currSize) {
            // Find ending point of left subarray
            int mid = leftStart + currSize - 1;
            
            // Find ending point of right subarray
            int rightEnd = (leftStart + 2 * currSize - 1 < n - 1) ?
                           leftStart + 2 * currSize - 1 : n - 1;
            
            // Merge subarrays
            if (mid < rightEnd) {
                merge(arr, leftStart, mid, rightEnd);
            }
        }
    }
}

Complexity Analysis

Time Complexity:

Space Complexity: O(n) - requires temporary arrays

Why O(n log n)?

Tree height: log₂(n) levels
Work at each level: n comparisons/copies

Total work = height × work per level
           = log₂(n) × n
           = O(n log n)

Example with n=8:
Level 0: [8 elements] → n operations
Level 1: [4,4] → n operations (4+4)
Level 2: [2,2,2,2] → n operations (2+2+2+2)
Level 3: [1,1,1,1,1,1,1,1] → n operations

Total levels = log₂(8) = 3
Total operations = 3n = O(n log n)

Comparison with Simple Sorts:

n=1,000:
- Insertion Sort: ~500,000 operations
- Merge Sort: ~10,000 operations
→ 50x faster!

n=1,000,000:
- Insertion Sort: ~500 billion operations
- Merge Sort: ~20 million operations
→ 25,000x faster!

Advantages and Disadvantages

Advantages:

  1. Guaranteed O(n log n) - always efficient
  2. Stable - preserves relative order
  3. Predictable - no worst-case scenarios
  4. Good for linked lists - O(1) space possible
  5. Parallelizable - can sort halves independently

Disadvantages:

  1. O(n) space - requires temporary storage
  2. Not in-place - not memory efficient
  3. Overhead - slower than Quick Sort in practice
  4. Not adaptive - doesn't benefit from sorted data

When to Use

Use Merge Sort when:

Don't use when:

6.2 Quick Sort

Concept

Quick Sort is a divide-and-conquer algorithm that picks a pivot element and partitions the array around it, then recursively sorts the subarrays.

How it works:

  1. Choose Pivot: Select an element as pivot
  2. Partition: Rearrange so elements < pivot are left, elements > pivot are right
  3. Recursively sort: Sort left and right subarrays

Visual Example:

Initial: [10, 80, 30, 90, 40, 50, 70]
         Pick pivot = 70 (last element)

Partition:
[10, 30, 40, 50] 70 [80, 90]
 ← less than 70     greater than 70 →

Recursively sort left: [10, 30, 40, 50]
Pick pivot = 50
[10, 30, 40] 50 []

Recursively sort right: [80, 90]
Pick pivot = 90
[80] 90 []

Final: [10, 30, 40, 50, 70, 80, 90]

Partitioning Process:

Array: [10, 80, 30, 90, 40, 50, 70]
Pivot: 70 (last element)

i = -1 (tracks position of smaller elements)
j = 0 (scans through array)

j=0: arr[0]=10 < 70 → swap arr[++i] with arr[j]
     [10, 80, 30, 90, 40, 50, 70]
      i

j=1: arr[1]=80 > 70 → no swap
     [10, 80, 30, 90, 40, 50, 70]
      i

j=2: arr[2]=30 < 70 → swap arr[++i] with arr[j]
     [10, 30, 80, 90, 40, 50, 70]
          i

j=3: arr[3]=90 > 70 → no swap

j=4: arr[4]=40 < 70 → swap arr[++i] with arr[j]
     [10, 30, 40, 90, 80, 50, 70]
              i

j=5: arr[5]=50 < 70 → swap arr[++i] with arr[j]
     [10, 30, 40, 50, 80, 90, 70]
                  i

Finally: Place pivot at i+1
     [10, 30, 40, 50, 70, 90, 80]
                      ↑
                    pivot in place

Implementation

Quick Sort with Last Element as Pivot:

#include <stdio.h>

// Swap two elements
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

// Partition function
int partition(int arr[], int low, int high) {
    int pivot = arr[high];  // Choose last element as pivot
    int i = low - 1;        // Index of smaller element
    
    for (int j = low; j < high; j++) {
        // If current element is smaller than pivot
        if (arr[j] < pivot) {
            i++;
            swap(&arr[i], &arr[j]);
        }
    }
    
    // Place pivot in correct position
    swap(&arr[i + 1], &arr[high]);
    return i + 1;
}

// Quick sort function
void quickSort(int arr[], int low, int high) {
    if (low < high) {
        // Partition index
        int pi = partition(arr, low, high);
        
        // Recursively sort elements before and after partition
        quickSort(arr, low, pi - 1);
        quickSort(arr, pi + 1, high);
    }
}

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

int main() {
    int arr[] = {10, 80, 30, 90, 40, 50, 70};
    int n = sizeof(arr) / sizeof(arr[0]);
    
    printf("Original array: ");
    printArray(arr, n);
    
    quickSort(arr, 0, n - 1);
    
    printf("Sorted array: ");
    printArray(arr, n);
    
    return 0;
}

Quick Sort with Middle Element as Pivot:

int partitionMiddle(int arr[], int low, int high) {
    int mid = low + (high - low) / 2;
    int pivot = arr[mid];
    
    // Move pivot to end
    swap(&arr[mid], &arr[high]);
    
    int i = low - 1;
    
    for (int j = low; j < high; j++) {
        if (arr[j] < pivot) {
            i++;
            swap(&arr[i], &arr[j]);
        }
    }
    
    swap(&arr[i + 1], &arr[high]);
    return i + 1;
}

Quick Sort with Random Pivot:

#include <stdlib.h>
#include <time.h>

int partitionRandom(int arr[], int low, int high) {
    // Generate random pivot index
    srand(time(NULL));
    int randomIndex = low + rand() % (high - low + 1);
    
    // Move pivot to end
    swap(&arr[randomIndex], &arr[high]);
    
    return partition(arr, low, high);
}

void quickSortRandom(int arr[], int low, int high) {
    if (low < high) {
        int pi = partitionRandom(arr, low, high);
        quickSortRandom(arr, low, pi - 1);
        quickSortRandom(arr, pi + 1, high);
    }
}

Quick Sort with Visualization:

void quickSortVisualize(int arr[], int low, int high, int depth) {
    if (low < high) {
        // Print indentation
        for (int i = 0; i < depth; i++) printf("  ");
        
        printf("Sorting: [");
        for (int i = low; i <= high; i++) {
            printf("%d", arr[i]);
            if (i < high) printf(", ");
        }
        printf("] Pivot=%d\n", arr[high]);
        
        int pi = partition(arr, low, high);
        
        // Print result
        for (int i = 0; i < depth; i++) printf("  ");
        printf("Result:  [");
        for (int i = low; i <= high; i++) {
            if (i == pi) printf("*");
            printf("%d", arr[i]);
            if (i == pi) printf("*");
            if (i < high) printf(", ");
        }
        printf("]\n");
        
        quickSortVisualize(arr, low, pi - 1, depth + 1);
        quickSortVisualize(arr, pi + 1, high, depth + 1);
    }
}

Three-Way Partitioning (for duplicates):

void quickSort3Way(int arr[], int low, int high) {
    if (low >= high) return;
    
    int pivot = arr[high];
    int i = low;
    int lt = low;      // Elements < pivot
    int gt = high;     // Elements > pivot
    
    while (i <= gt) {
        if (arr[i] < pivot) {
            swap(&arr[lt++], &arr[i++]);
        }
        else if (arr[i] > pivot) {
            swap(&arr[i], &arr[gt--]);
        }
        else {
            i++;
        }
    }
    
    quickSort3Way(arr, low, lt - 1);
    quickSort3Way(arr, gt + 1, high);
}

Complexity Analysis

Time Complexity:

Space Complexity: O(log n) - recursion stack

Best Case (Balanced Partition):

Each partition divides array in half

          [8 elements]
         /            \
    [4]                [4]
   /   \              /   \
  [2]  [2]          [2]  [2]
 / \   / \         / \   / \
[1][1][1][1]      [1][1][1][1]

Height = log₂(n)
Work per level = n
Total = O(n log n)

Worst Case (Unbalanced Partition):

Sorted or reverse sorted with bad pivot choice

[5, 4, 3, 2, 1] pivot = 1
    ↓
[1] [5, 4, 3, 2] pivot = 2
        ↓
    [2] [5, 4, 3] pivot = 3
            ↓
        [3] [5, 4]
                ↓
            [4] [5]

Height = n
Work = n + (n-1) + (n-2) + ... + 1
     = n(n+1)/2
     = O(n²)

Avoiding Worst Case:

  1. Random Pivot: Makes worst case unlikely
  2. Median-of-Three: Use median of first, middle, last
  3. Three-Way Partition: Handle duplicates efficiently

Advantages and Disadvantages

Advantages:

  1. Fast in practice - usually faster than Merge Sort
  2. In-place - O(log n) space only
  3. Cache-friendly - good locality of reference
  4. Parallelizable - can sort partitions independently

Disadvantages:

  1. Unstable - doesn't preserve relative order
  2. O(n²) worst case - rare but possible
  3. Not adaptive - doesn't benefit from sorted data
  4. Recursive - stack overflow for deep recursion

Pivot Selection Strategies

1. Last Element (Simple):

int pivot = arr[high];

2. First Element:

int pivot = arr[low];

3. Middle Element:

int mid = low + (high - low) / 2;
int pivot = arr[mid];

4. Random Element:

int randomIndex = low + rand() % (high - low + 1);
int pivot = arr[randomIndex];

5. Median-of-Three:

int medianOfThree(int arr[], int low, int high) {
    int mid = low + (high - low) / 2;
    
    if (arr[low] > arr[mid])
        swap(&arr[low], &arr[mid]);
    if (arr[low] > arr[high])
        swap(&arr[low], &arr[high]);
    if (arr[mid] > arr[high])
        swap(&arr[mid], &arr[high]);
    
    return mid;
}

When to Use

Use Quick Sort when:

Don't use when:

Comparison with Merge Sort:

                Quick Sort        Merge Sort
Time (avg):     O(n log n)        O(n log n)
Time (worst):   O(n²)             O(n log n)
Space:          O(log n)          O(n)
Stable:         No                Yes
In-place:       Yes               No
Cache:          Better            Worse
Practice:       Faster            Slower
Module 7: Searching & Sorting

7. Comparison of Sorting Algorithms

7.1 Performance Summary

// Test all sorting algorithms
#include <time.h>

void testSortingAlgorithms() {
    int sizes[] = {100, 1000, 10000};
    
    for (int s = 0; s < 3; s++) {
        int n = sizes[s];
        printf("\n=== Array Size: %d ===\n", n);
        
        // Test each algorithm
        int *arr = generateRandomArray(n);
        
        clock_t start = clock();
        bubbleSort(arr, n);
        clock_t end = clock();
        printf("Bubble Sort: %.4f seconds\n", 
               (double)(end - start) / CLOCKS_PER_SEC);
        
        // Repeat for other algorithms...
        free(arr);
    }
}

7.2 When to Use Which Algorithm

Decision Tree:

Is n < 50?
├─ Yes → Use Insertion Sort (simple, fast for small n)
└─ No  → Is stability required?
         ├─ Yes → Use Merge Sort
         └─ No  → Is memory limited?
                  ├─ Yes → Use Quick Sort (in-place)
                  └─ No  → Use Merge Sort (guaranteed O(n log n))

By Data Characteristics:

Nearly sorted →Insertion Sort (O(n))
Reverse sorted → Any O(n log n) algorithm
Random data → Quick Sort (fastest in practice)
Many duplicates → Quick Sort 3-way
Small data → Insertion Sort
Large data → Merge Sort or Quick Sort
Linked list → Merge Sort (O(1) space)
Stability needed → Merge Sort or Insertion Sort
Memory limited → Quick Sort or Heap Sort

7.3 Hybrid Sorting Approaches

IntroSort (Introspective Sort):

void introSort(int arr[], int low, int high, int depthLimit) {
    int n = high - low + 1;
    
    // Use Insertion Sort for small arrays
    if (n < 16) {
        insertionSort(arr + low, n);
        return;
    }
    
    // Switch to Heap Sort if recursion too deep
    if (depthLimit == 0) {
        heapSort(arr + low, n);
        return;
    }
    
    // Use Quick Sort
    int pi = partition(arr, low, high);
    introSort(arr, low, pi - 1, depthLimit - 1);
    introSort(arr, pi + 1, high, depthLimit - 1);
}

// Wrapper function
void sort(int arr[], int n) {
    int depthLimit = 2 * log2(n);
    introSort(arr, 0, n - 1, depthLimit);
}

TimSort (Python's default sort):

Module 7: Searching & Sorting

8. Practical Applications

8.1 Search and Sort Combined

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

// Student structure
typedef struct {
    int id;
    char name[50];
    float gpa;
} Student;

// Compare functions for sorting
int compareByID(const void *a, const void *b) {
    return ((Student*)a)->id - ((Student*)b)->id;
}

int compareByGPA(const void *a, const void *b) {
    float diff = ((Student*)b)->gpa - ((Student*)a)->gpa;
    return (diff > 0) ? 1 : (diff < 0) ? -1 : 0;
}

int compareByName(const void *a, const void *b) {
    return strcmp(((Student*)a)->name, ((Student*)b)->name);
}

// Binary search by ID
int searchByID(Student students[], int n, int targetID) {
    int left = 0, right = n - 1;
    
    while (left <= right) {
        int mid = left + (right - left) / 2;
        
        if (students[mid].id == targetID) {
            return mid;
        }
        else if (students[mid].id < targetID) {
            left = mid + 1;
        }
        else {
            right = mid - 1;
        }
    }
    
    return -1;
}

// Linear search by name
int searchByName(Student students[], int n, const char *name) {
    for (int i = 0; i < n; i++) {
        if (strcmp(students[i].name, name) == 0) {
            return i;
        }
    }
    return -1;
}

// Print students
void printStudents(Student students[], int n) {
    printf("\n%-10s %-20s %-8s\n", "ID", "Name", "GPA");
    printf("----------------------------------------\n");
    for (int i = 0; i < n; i++) {
        printf("%-10d %-20s %.2f\n", 
               students[i].id, students[i].name, students[i].gpa);
    }
}

int main() {
    Student students[] = {
        {1003, "Alice Johnson", 3.8},
        {1001, "Bob Smith", 3.5},
        {1005, "Charlie Brown", 3.9},
        {1002, "Diana Prince", 3.7},
        {1004, "Eve Wilson", 3.6}
    };
    int n = sizeof(students) / sizeof(students[0]);
    
    printf("=== Student Database ===\n");
    printf("\nOriginal Data:");
    printStudents(students, n);
    
    // Sort by ID
    qsort(students, n, sizeof(Student), compareByID);
    printf("\nSorted by ID:");
    printStudents(students, n);
    
    // Search by ID
    int targetID = 1003;
    int index = searchByID(students, n, targetID);
    if (index != -1) {
        printf("\nStudent with ID %d: %s (GPA: %.2f)\n", 
               targetID, students[index].name, students[index].gpa);
    }
    
    // Sort by GPA (descending)
    qsort(students, n, sizeof(Student), compareByGPA);
    printf("\nSorted by GPA (highest first):");
    printStudents(students, n);
    
    // Sort by Name
    qsort(students, n, sizeof(Student), compareByName);
    printf("\nSorted by Name:");
    printStudents(students, n);
    
    return 0;
}

8.2 Finding Kth Largest Element

// Partition function (same as Quick Sort)
int partition(int arr[], int low, int high) {
    int pivot = arr[high];
    int i = low - 1;
    
    for (int j = low; j < high; j++) {
        if (arr[j] <= pivot) {
            i++;
            int temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
    }
    
    int temp = arr[i + 1];
    arr[i + 1] = arr[high];
    arr[high] = temp;
    
    return i + 1;
}

// Quick Select algorithm - O(n) average time
int findKthLargest(int arr[], int low, int high, int k) {
    if (low == high) {
        return arr[low];
    }
    
    int pi = partition(arr, low, high);
    int length = high - pi + 1;
    
    if (length == k) {
        return arr[pi];
    }
    else if (length > k) {
        return findKthLargest(arr, pi + 1, high, k);
    }
    else {
        return findKthLargest(arr, low, pi - 1, k - length);
    }
}

int main() {
    int arr[] = {3, 2, 1, 5, 6, 4};
    int n = sizeof(arr) / sizeof(arr[0]);
    int k = 2;
    
    int result = findKthLargest(arr, 0, n - 1, k);
    printf("%dth largest element: %d\n", k, result);
    // Output: 2nd largest element: 5
    
    return 0;
}

8.3 Merge Two Sorted Arrays

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

int* mergeSortedArrays(int arr1[], int n1, int arr2[], int n2) {
    int *result = (int*)malloc((n1 + n2) * sizeof(int));
    int i = 0, j = 0, k = 0;
    
    // Merge while both arrays have elements
    while (i < n1 && j < n2) {
        if (arr1[i] <= arr2[j]) {
            result[k++] = arr1[i++];
        } else {
            result[k++] = arr2[j++];
        }
    }
    
    // Copy remaining elements from arr1
    while (i < n1) {
        result[k++] = arr1[i++];
    }
    
    // Copy remaining elements from arr2
    while (j < n2) {
        result[k++] = arr2[j++];
    }
    
    return result;
}

int main() {
    int arr1[] = {1, 3, 5, 7};
    int arr2[] = {2, 4, 6, 8};
    int n1 = sizeof(arr1) / sizeof(arr1[0]);
    int n2 = sizeof(arr2) / sizeof(arr2[0]);
    
    int *merged = mergeSortedArrays(arr1, n1, arr2, n2);
    
    printf("Merged array: ");
    for (int i = 0; i < n1 + n2; i++) {
        printf("%d ", merged[i]);
    }
    printf("\n");
    
    free(merged);
    return 0;
}

8.4 Finding Median

// Find median of unsorted array
double findMedian(int arr[], int n) {
    // Sort the array first
    quickSort(arr, 0, n - 1);
    
    // Find median
    if (n % 2 == 0) {
        // Even number of elements - average of two middle
        return (arr[n/2 - 1] + arr[n/2]) / 2.0;
    } else {
        // Odd number of elements - middle element
        return arr[n/2];
    }
}

int main() {
    int arr1[] = {3, 1, 4, 1, 5, 9, 2};
    int arr2[] = {6, 5, 3, 5, 8, 9};
    
    printf("Median of arr1: %.1f\n", findMedian(arr1, 7));
    printf("Median of arr2: %.1f\n", findMedian(arr2, 6));
    
    return 0;
}

8.5 Removing Duplicates from Sorted Array

int removeDuplicates(int arr[], int n) {
    if (n == 0 || n == 1) {
        return n;
    }
    
    int j = 0;  // Index for unique elements
    
    for (int i = 0; i < n - 1; i++) {
        if (arr[i] != arr[i + 1]) {
            arr[j++] = arr[i];
        }
    }
    
    arr[j++] = arr[n - 1];  // Add last element
    
    return j;  // New length
}

int main() {
    int arr[] = {1, 1, 2, 2, 2, 3, 4, 4, 5};
    int n = sizeof(arr) / sizeof(arr[0]);
    
    printf("Original: ");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
    
    int newLength = removeDuplicates(arr, n);
    
    printf("After removing duplicates: ");
    for (int i = 0; i < newLength; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
    
    return 0;
}

8.6 Finding Intersection of Two Arrays

void findIntersection(int arr1[], int n1, int arr2[], int n2) {
    // Sort both arrays first
    quickSort(arr1, 0, n1 - 1);
    quickSort(arr2, 0, n2 - 1);
    
    printf("Intersection: ");
    int i = 0, j = 0;
    
    while (i < n1 && j < n2) {
        if (arr1[i] < arr2[j]) {
            i++;
        }
        else if (arr1[i] > arr2[j]) {
            j++;
        }
        else {
            // Elements are equal
            printf("%d ", arr1[i]);
            i++;
            j++;
        }
    }
    printf("\n");
}

int main() {
    int arr1[] = {1, 3, 4, 5, 7};
    int arr2[] = {2, 3, 5, 6};
    
    findIntersection(arr1, 5, arr2, 4);
    // Output: Intersection: 3 5
    
    return 0;
}

8.7 Sorting Strings

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

void sortStrings(char *arr[], int n) {
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - i - 1; j++) {
            if (strcmp(arr[j], arr[j + 1]) > 0) {
                // Swap pointers
                char *temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

int main() {
    char *fruits[] = {"Banana", "Apple", "Orange", "Mango", "Grape"};
    int n = 5;
    
    printf("Original: ");
    for (int i = 0; i < n; i++) {
        printf("%s ", fruits[i]);
    }
    printf("\n");
    
    sortStrings(fruits, n);
    
    printf("Sorted: ");
    for (int i = 0; i < n; i++) {
        printf("%s ", fruits[i]);
    }
    printf("\n");
    
    return 0;
}

Module 8 : OOP (SOLID, Encapsulation, Abstraction)

By the end of this module, students will be able to:

- Understand the fundamental concepts of Object-Oriented Programming (OOP)

- Transition from procedural C programming to OOP in C++

- Implement classes and objects in C++

- Apply encapsulation principles using access specifiers

- Understand and implement abstraction concepts

- Apply SOLID principles in basic C++ programs

Module 8 : OOP (SOLID, Encapsulation, Abstraction)

1. Introduction: From Procedural to Object-Oriented Programming

1.1 What is Object-Oriented Programming?

Object-Oriented Programming (OOP) is a programming paradigm that organizes code around objects rather than functions and logic. An object is a data structure that contains both data (attributes) and code (methods) that operates on that data.

Key Paradigm Comparison:

Aspect Procedural (C) Object-Oriented (C++)
Focus Functions and procedures Objects and classes
Data & Functions Separate Bundled together
Code Organization By functionality By entities/objects
Data Protection Limited (global/local) Strong (access specifiers)
Code Reuse Function reuse Inheritance & polymorphism
Maintenance Harder for large projects Easier through modularity

1.2 The Four Pillars of OOP

  1. Encapsulation: Bundling data and methods that operate on that data within a single unit (class), hiding internal details
  2. Abstraction: Showing only essential features while hiding implementation details
  3. Inheritance: Creating new classes from existing classes, promoting code reuse
  4. Polymorphism: Ability of objects to take many forms, allowing different implementations of the same interface

1.3 Real-World Analogy

Think of a car:

Module 8 : OOP (SOLID, Encapsulation, Abstraction)

2. C++ Basics: Essential Differences from C

2.1 Basic Syntax Differences

C vs C++ Comparison:

Feature C C++
File Extension .c .cpp
Input/Output scanf(), printf() cin, cout
Header Files #include <stdio.h> #include <iostream>
Namespace Not used using namespace std;
Comments /* */ and // /* */ and // (preferred)
Boolean Type int (0/1) bool (true/false)
String Type char[] string class

2.2 Hello World Comparison

C Program:

#include <stdio.h>

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

C++ Program:

#include <iostream>
using namespace std;

int main() {
    cout << "Hello, World!" << endl;
    return 0;
}

2.3 Input/Output in C++

Basic I/O Operations:

#include <iostream>
using namespace std;

int main() {
    int age;
    string name;
    float height;
    
    // Output (cout with insertion operator <<)
    cout << "Enter your name: ";
    
    // Input (cin with extraction operator >>)
    cin >> name;
    
    cout << "Enter your age: ";
    cin >> age;
    
    cout << "Enter your height (m): ";
    cin >> height;
    
    // Multiple outputs
    cout << "Name: " << name << endl;
    cout << "Age: " << age << " years" << endl;
    cout << "Height: " << height << " meters" << endl;
    
    return 0;
}

C vs C++ I/O Comparison:

Operation C C++
Output integer printf("%d", x); cout << x;
Output float printf("%.2f", x); cout << fixed << setprecision(2) << x;
Output string printf("%s", str); cout << str;
Input integer scanf("%d", &x); cin >> x;
Input string scanf("%s", str); cin >> str;
Multiple inputs scanf("%d %d", &a, &b); cin >> a >> b;

Reading a Full Line:

#include <iostream>
#include <string>
using namespace std;

int main() {
    string fullName;
    
    cout << "Enter your full name: ";
    getline(cin, fullName);  // Reads entire line including spaces
    
    cout << "Hello, " << fullName << "!" << endl;
    return 0;
}

Important Note on cin Buffer:

int age;
string name;

cin >> age;          // User enters "25" and presses Enter
// Buffer now contains the newline character

cin.ignore();        // Clear the newline from buffer
getline(cin, name);  // Now can read full line properly
Module 8 : OOP (SOLID, Encapsulation, Abstraction)

3. Classes and Objects

3.1 Understanding Classes and Objects

Definition:

Real-World Example:

Class: Student (blueprint)
    - Attributes: name, ID, GPA
    - Methods: study(), takeExam(), getGPA()

Objects (instances):
    - student1: "Alice", "S001", 3.8
    - student2: "Bob", "S002", 3.5
    - student3: "Charlie", "S003", 3.9

3.2 Defining a Class

Basic Class Syntax:

class ClassName {
private:
    // Private members (data and functions)
    // Only accessible within the class
    
public:
    // Public members
    // Accessible from outside the class
    
protected:
    // Protected members
    // Accessible in derived classes (inheritance)
};

Simple Example:

#include <iostream>
#include <string>
using namespace std;

class Student {
private:
    string name;
    int id;
    float gpa;
    
public:
    // Constructor
    Student(string n, int i, float g) {
        name = n;
        id = i;
        gpa = g;
    }
    
    // Member functions (methods)
    void displayInfo() {
        cout << "Name: " << name << endl;
        cout << "ID: " << id << endl;
        cout << "GPA: " << gpa << endl;
    }
    
    void study() {
        cout << name << " is studying..." << endl;
    }
    
    float getGPA() {
        return gpa;
    }
    
    void setGPA(float newGPA) {
        if (newGPA >= 0.0 && newGPA <= 4.0) {
            gpa = newGPA;
        } else {
            cout << "Invalid GPA!" << endl;
        }
    }
};

int main() {
    // Creating objects
    Student student1("Alice", 1001, 3.8);
    Student student2("Bob", 1002, 3.5);
    
    // Using objects
    student1.displayInfo();
    cout << endl;
    
    student2.study();
    cout << "Bob's GPA: " << student2.getGPA() << endl;
    
    student2.setGPA(3.7);
    cout << "Updated GPA: " << student2.getGPA() << endl;
    
    return 0;
}

3.3 Procedural vs OOP: A Practical Comparison

Procedural Approach (C):

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

// Separate data structure
struct Student {
    char name[50];
    int id;
    float gpa;
};

// Separate functions
void displayStudent(struct Student s) {
    printf("Name: %s\n", s.name);
    printf("ID: %d\n", s.id);
    printf("GPA: %.2f\n", s.gpa);
}

void studyStudent(struct Student s) {
    printf("%s is studying...\n", s.name);
}

float getGPA(struct Student s) {
    return s.gpa;
}

int main() {
    struct Student student1;
    strcpy(student1.name, "Alice");
    student1.id = 1001;
    student1.gpa = 3.8;
    
    displayStudent(student1);
    studyStudent(student1);
    
    return 0;
}

OOP Approach (C++):

#include <iostream>
#include <string>
using namespace std;

class Student {
private:
    string name;
    int id;
    float gpa;
    
public:
    Student(string n, int i, float g) : name(n), id(i), gpa(g) {}
    
    void display() {
        cout << "Name: " << name << endl;
        cout << "ID: " << id << endl;
        cout << "GPA: " << gpa << endl;
    }
    
    void study() {
        cout << name << " is studying..." << endl;
    }
    
    float getGPA() { return gpa; }
};

int main() {
    Student student1("Alice", 1001, 3.8);
    
    student1.display();
    student1.study();
    
    return 0;
}

Key Advantages of OOP:

  1. Encapsulation: Data and functions are bundled together
  2. Data Protection: Private members prevent unauthorized access
  3. Cleaner Syntax: Methods are called directly on objects
  4. Better Organization: Related functionality is grouped together
Module 8 : OOP (SOLID, Encapsulation, Abstraction)

4. Encapsulation

4.1 What is Encapsulation?

Encapsulation is the bundling of data (attributes) and methods that operate on that data within a single unit (class), while restricting direct access to some of the object's components.

Purpose:

4.2 Access Specifiers

Three Access Levels:

Specifier Access Within Class Access in Derived Class Access from Outside
private
protected
public

Visual Representation:

┌─────────────────────────────────┐
│         Class: BankAccount      │
├─────────────────────────────────┤
│ private:                        │
│   - balance (hidden)            │ ← Cannot access from outside
│   - accountNumber (hidden)      │
├─────────────────────────────────┤
│ public:                         │
│   + deposit(amount)             │ ← Can access from outside
│   + withdraw(amount)            │
│   + getBalance()                │
└─────────────────────────────────┘

4.3 Implementing Encapsulation

Bad Practice (No Encapsulation):

class BankAccount {
public:
    string accountNumber;
    double balance;  // Anyone can modify this directly!
};

int main() {
    BankAccount acc;
    acc.balance = 1000.0;
    
    // Problem: Direct modification allows invalid states
    acc.balance = -500.0;  // Negative balance! Bad!
    acc.balance = 999999999.99;  // Unrealistic amount
    
    return 0;
}

Good Practice (With Encapsulation):

#include <iostream>
#include <string>
using namespace std;

class BankAccount {
private:
    string accountNumber;
    double balance;
    string ownerName;
    
public:
    // Constructor
    BankAccount(string accNum, string owner, double initialBalance = 0.0) {
        accountNumber = accNum;
        ownerName = owner;
        balance = (initialBalance >= 0) ? initialBalance : 0.0;
    }
    
    // Getter methods (accessors)
    double getBalance() const {
        return balance;
    }
    
    string getAccountNumber() const {
        return accountNumber;
    }
    
    string getOwnerName() const {
        return ownerName;
    }
    
    // Setter methods with validation (mutators)
    bool deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            cout << "Deposited: $" << amount << endl;
            return true;
        }
        cout << "Invalid deposit amount!" << endl;
        return false;
    }
    
    bool withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            cout << "Withdrawn: $" << amount << endl;
            return true;
        }
        cout << "Invalid withdrawal or insufficient funds!" << endl;
        return false;
    }
    
    void displayInfo() const {
        cout << "Account: " << accountNumber << endl;
        cout << "Owner: " << ownerName << endl;
        cout << "Balance: $" << balance << endl;
    }
};

int main() {
    BankAccount acc("ACC001", "Alice Johnson", 1000.0);
    
    acc.displayInfo();
    cout << endl;
    
    acc.deposit(500.0);
    acc.withdraw(200.0);
    acc.withdraw(2000.0);  // Will fail - insufficient funds
    
    cout << "\nFinal balance: $" << acc.getBalance() << endl;
    
    // acc.balance = -500;  // ERROR: Cannot access private member
    
    return 0;
}

Output:

Account: ACC001
Owner: Alice Johnson
Balance: $1000

Deposited: $500
Withdrawn: $200
Invalid withdrawal or insufficient funds!

Final balance: $1300

4.4 Benefits of Encapsulation

1. Data Validation:

class Temperature {
private:
    double celsius;
    
public:
    void setCelsius(double temp) {
        if (temp >= -273.15) {  // Absolute zero
            celsius = temp;
        } else {
            cout << "Invalid temperature!" << endl;
        }
    }
    
    double getCelsius() const { return celsius; }
    double getFahrenheit() const { return (celsius * 9.0/5.0) + 32; }
};

2. Read-Only Properties:

class Person {
private:
    string ssn;  // Social Security Number
    
public:
    Person(string socialSecNum) : ssn(socialSecNum) {}
    
    // Only getter, no setter - SSN is read-only
    string getSSN() const { return ssn; }
};

3. Internal Implementation Changes:

class Circle {
private:
    double radius;
    // We could change to store diameter instead later
    
public:
    void setRadius(double r) {
        if (r > 0) radius = r;
    }
    
    double getArea() const {
        return 3.14159 * radius * radius;
    }
    // External code doesn't need to change if we modify internal storage
};

4.5 Const Member Functions

Purpose: Indicate that a method does not modify object state

class Rectangle {
private:
    double width, height;
    
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    
    // Const member functions - promise not to modify data
    double getWidth() const { return width; }
    double getHeight() const { return height; }
    double getArea() const { return width * height; }
    double getPerimeter() const { return 2 * (width + height); }
    
    // Non-const member functions - can modify data
    void setWidth(double w) { width = w; }
    void setHeight(double h) { height = h; }
};

int main() {
    const Rectangle rect(5.0, 3.0);  // Const object
    
    cout << rect.getArea();      // OK - const function
    // rect.setWidth(10);        // ERROR - can't call non-const function on const object
    
    return 0;
}
Module 8 : OOP (SOLID, Encapsulation, Abstraction)

5. Abstraction

5.1 What is Abstraction?

Abstraction is the concept of hiding complex implementation details and showing only the essential features of an object. It focuses on what an object does rather than how it does it.

Real-World Analogy:

Abstraction vs Encapsulation:

Aspect Encapsulation Abstraction
Focus Data hiding (how to hide) Implementation hiding (what to show)
Achieved by Access specifiers (private/public) Abstract classes, interfaces
Purpose Protect data Simplify complexity
Level Class level Design level

5.2 Levels of Abstraction

Example: Coffee Machine

// High-level abstraction (user interface)
class CoffeeMachine {
public:
    void makeCoffee() {
        // User just presses button
        grindBeans();
        heatWater();
        brew();
        dispense();
    }
    
private:
    // Low-level implementation details (hidden)
    void grindBeans() { /* Complex grinding mechanism */ }
    void heatWater() { /* Temperature control system */ }
    void brew() { /* Pressure and timing control */ }
    void dispense() { /* Dispensing mechanism */ }
};

User's Perspective:

int main() {
    CoffeeMachine machine;
    machine.makeCoffee();  // Simple interface, complex implementation hidden
    return 0;
}

5.3 Implementing Abstraction in C++

Method 1: Using Regular Classes (Practical Abstraction)

#include <iostream>
#include <string>
using namespace std;

class EmailService {
private:
    // Complex implementation details hidden
    string smtpServer;
    int port;
    string username;
    string password;
    
    void connectToServer() {
        cout << "Connecting to SMTP server..." << endl;
        // Complex network code
    }
    
    void authenticate() {
        cout << "Authenticating..." << endl;
        // Complex authentication logic
    }
    
    void encodeMessage(string message) {
        cout << "Encoding message..." << endl;
        // Complex encoding algorithm
    }
    
    void transmit(string to, string message) {
        cout << "Transmitting to " << to << "..." << endl;
        // Complex transmission protocol
    }
    
    void disconnectFromServer() {
        cout << "Disconnecting..." << endl;
        // Cleanup code
    }
    
public:
    EmailService(string server, string user, string pass) 
        : smtpServer(server), port(587), username(user), password(pass) {}
    
    // Simple public interface - abstracts away complexity
    void sendEmail(string recipient, string subject, string body) {
        cout << "\n=== Sending Email ===" << endl;
        connectToServer();
        authenticate();
        encodeMessage(body);
        transmit(recipient, body);
        disconnectFromServer();
        cout << "Email sent successfully!" << endl;
    }
};

int main() {
    EmailService emailer("smtp.gmail.com", "user@example.com", "password");
    
    // User only needs to call one simple method
    emailer.sendEmail("friend@example.com", 
                     "Hello", 
                     "Just saying hi!");
    
    return 0;
}

Output:

=== Sending Email ===
Connecting to SMTP server...
Authenticating...
Encoding message...
Transmitting to friend@example.com...
Disconnecting...
Email sent successfully!

5.4 Abstract Classes and Pure Virtual Functions

Abstract Class: A class that cannot be instantiated and serves as a base for other classes.

Pure Virtual Function: A virtual function with no implementation, declared with = 0.

Syntax:

class AbstractClassName {
public:
    virtual void pureVirtualFunction() = 0;  // Pure virtual function
    virtual void anotherFunction() = 0;
    
    void regularFunction() {
        // Regular implementation
    }
};

Complete Example:

#include <iostream>
#include <string>
using namespace std;

// Abstract base class - defines interface
class Shape {
protected:
    string color;
    
public:
    Shape(string c) : color(c) {}
    
    // Pure virtual functions - must be implemented by derived classes
    virtual double getArea() = 0;
    virtual double getPerimeter() = 0;
    virtual void displayInfo() = 0;
    
    // Regular function with implementation
    string getColor() { return color; }
    void setColor(string c) { color = c; }
};

// Concrete class 1: Circle
class Circle : public Shape {
private:
    double radius;
    
public:
    Circle(string c, double r) : Shape(c), radius(r) {}
    
    // Implementing abstract methods
    double getArea() override {
        return 3.14159 * radius * radius;
    }
    
    double getPerimeter() override {
        return 2 * 3.14159 * radius;
    }
    
    void displayInfo() override {
        cout << "Circle [Color: " << color << ", Radius: " << radius << "]" << endl;
        cout << "Area: " << getArea() << endl;
        cout << "Perimeter: " << getPerimeter() << endl;
    }
};

// Concrete class 2: Rectangle
class Rectangle : public Shape {
private:
    double width, height;
    
public:
    Rectangle(string c, double w, double h) 
        : Shape(c), width(w), height(h) {}
    
    double getArea() override {
        return width * height;
    }
    
    double getPerimeter() override {
        return 2 * (width + height);
    }
    
    void displayInfo() override {
        cout << "Rectangle [Color: " << color 
             << ", Width: " << width << ", Height: " << height << "]" << endl;
        cout << "Area: " << getArea() << endl;
        cout << "Perimeter: " << getPerimeter() << endl;
    }
};

int main() {
    // Shape shape("red");  // ERROR: Cannot instantiate abstract class
    
    Circle circle("Red", 5.0);
    Rectangle rect("Blue", 4.0, 6.0);
    
    circle.displayInfo();
    cout << endl;
    rect.displayInfo();
    
    // Polymorphic behavior
    Shape* shapes[2];
    shapes[0] = &circle;
    shapes[1] = &rect;
    
    cout << "\n=== Using Polymorphism ===" << endl;
    for (int i = 0; i < 2; i++) {
        shapes[i]->displayInfo();
        cout << endl;
    }
    
    return 0;
}

5.5 Interface-Like Classes in C++

Pure Abstract Class (Interface):

// Interface - all methods are pure virtual
class Drawable {
public:
    virtual void draw() = 0;
    virtual void erase() = 0;
    virtual ~Drawable() {}  // Virtual destructor
};

class Movable {
public:
    virtual void moveUp() = 0;
    virtual void moveDown() = 0;
    virtual void moveLeft() = 0;
    virtual void moveRight() = 0;
    virtual ~Movable() {}
};

// Class implementing multiple interfaces
class GameCharacter : public Drawable, public Movable {
private:
    int x, y;
    string name;
    
public:
    GameCharacter(string n, int posX, int posY) 
        : name(n), x(posX), y(posY) {}
    
    // Implement Drawable interface
    void draw() override {
        cout << "Drawing " << name << " at (" << x << ", " << y << ")" << endl;
    }
    
    void erase() override {
        cout << "Erasing " << name << endl;
    }
    
    // Implement Movable interface
    void moveUp() override { y++; }
    void moveDown() override { y--; }
    void moveLeft() override { x--; }
    void moveRight() override { x++; }
};

int main() {
    GameCharacter hero("Hero", 10, 20);
    
    hero.draw();
    hero.moveRight();
    hero.moveUp();
    hero.draw();
    
    return 0;
}

5.6 Benefits of Abstraction

1. Simplification:

// User doesn't need to know implementation details
DatabaseConnection db("localhost", "mydb", "user", "pass");
db.connect();
db.executeQuery("SELECT * FROM users");
db.close();

2. Flexibility:

class PaymentProcessor {
public:
    virtual void processPayment(double amount) = 0;
};

class CreditCardProcessor : public PaymentProcessor {
    void processPayment(double amount) override {
        // Credit card specific implementation
    }
};

class PayPalProcessor : public PaymentProcessor {
    void processPayment(double amount) override {
        // PayPal specific implementation
    }
};

3. Maintainability:

Module 8 : OOP (SOLID, Encapsulation, Abstraction)

6. SOLID Principles

6.1 Introduction to SOLID

SOLID is an acronym for five design principles that make software designs more understandable, flexible, and maintainable.

Letter Principle Core Idea
S Single Responsibility A class should have one reason to change
O Open/Closed Open for extension, closed for modification
L Liskov Substitution Derived classes must be substitutable for base classes
I Interface Segregation Many specific interfaces are better than one general interface
D Dependency Inversion Depend on abstractions, not concretions

6.2 S - Single Responsibility Principle (SRP)

Definition: A class should have only one reason to change, meaning it should have only one job or responsibility.

Bad Example (Multiple Responsibilities):

// This class has too many responsibilities!
class Employee {
private:
    string name;
    double salary;
    
public:
    // Responsibility 1: Employee data management
    void setName(string n) { name = n; }
    string getName() { return name; }
    void setSalary(double s) { salary = s; }
    double getSalary() { return salary; }
    
    // Responsibility 2: Salary calculation
    double calculateTax() {
        return salary * 0.2;
    }
    
    double calculateBonus() {
        return salary * 0.1;
    }
    
    // Responsibility 3: Database operations
    void saveToDatabase() {
        cout << "Saving employee to database..." << endl;
    }
    
    void loadFromDatabase() {
        cout << "Loading employee from database..." << endl;
    }
    
    // Responsibility 4: Report generation
    void printPaySlip() {
        cout << "Printing pay slip..." << endl;
    }
};

Good Example (Single Responsibility):

// Each class has ONE clear responsibility

// 1. Employee data management
class Employee {
private:
    string name;
    string id;
    double salary;
    
public:
    Employee(string n, string i, double s) 
        : name(n), id(i), salary(s) {}
    
    string getName() const { return name; }
    string getId() const { return id; }
    double getSalary() const { return salary; }
    void setSalary(double s) { salary = s; }
};

// 2. Salary calculations
class SalaryCalculator {
public:
    double calculateTax(const Employee& emp) {
        return emp.getSalary() * 0.2;
    }
    
    double calculateBonus(const Employee& emp) {
        return emp.getSalary() * 0.1;
    }
    
    double calculateNetSalary(const Employee& emp) {
        return emp.getSalary() - calculateTax(emp) + calculateBonus(emp);
    }
};

// 3. Database operations
class EmployeeRepository {
public:
    void save(const Employee& emp) {
        cout << "Saving employee " << emp.getId() << " to database..." << endl;
    }
    
    Employee* load(string id) {
        cout << "Loading employee " << id << " from database..." << endl;
        return nullptr;  // Simplified
    }
};

// 4. Report generation
class PaySlipGenerator {
private:
    SalaryCalculator calculator;
    
public:
    void generatePaySlip(const Employee& emp) {
        cout << "\n===== PAY SLIP =====" << endl;
        cout << "Employee: " << emp.getName() << endl;
        cout << "ID: " << emp.getId() << endl;
        cout << "Gross Salary: $" << emp.getSalary() << endl;
        cout << "Tax: $" << calculator.calculateTax(emp) << endl;
        cout << "Bonus: $" << calculator.calculateBonus(emp) << endl;
        cout << "Net Salary: $" << calculator.calculateNetSalary(emp) << endl;
        cout << "====================" << endl;
    }
};

int main() {
    Employee emp("Alice Johnson", "E001", 5000.0);
    
    SalaryCalculator calc;
    EmployeeRepository repo;
    PaySlipGenerator payslip;
    
    payslip.generatePaySlip(emp);
    repo.save(emp);
    
    return 0;
}

Benefits:

6.3 O - Open/Closed Principle (OCP)

Definition: Software entities should be open for extension but closed for modification. You should be able to add new functionality without changing existing code.

Bad Example (Violates OCP):

class Rectangle {
public:
    double width, height;
};

class Circle {
public:
    double radius;
};

// This class needs modification every time we add a new shape
class AreaCalculator {
public:
    double calculateArea(void* shape, string shapeType) {
        if (shapeType == "Rectangle") {
            Rectangle* rect = (Rectangle*)shape;
            return rect->width * rect->height;
        }
        else if (shapeType == "Circle") {
            Circle* circle = (Circle*)shape;
            return 3.14159 * circle->radius * circle->radius;
        }
        // Need to modify this function for every new shape!
        // else if (shapeType == "Triangle") { ... }
        return 0;
    }
};

Good Example (Follows OCP):

#include <iostream>
#include <vector>
#include <memory>
using namespace std;

// Abstract base class
class Shape {
public:
    virtual double calculateArea() = 0;
    virtual string getName() = 0;
    virtual ~Shape() {}
};

// Concrete shapes - extending without modifying existing code
class Rectangle : public Shape {
private:
    double width, height;
    
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    
    double calculateArea() override {
        return width * height;
    }
    
    string getName() override {
        return "Rectangle";
    }
};

class Circle : public Shape {
private:
    double radius;
    
public:
    Circle(double r) : radius(r) {}
    
    double calculateArea() override {
        return 3.14159 * radius * radius;
    }
    
    string getName() override {
        return "Circle";
    }
};

// NEW shape - no modification to existing code needed!
class Triangle : public Shape {
private:
    double base, height;
    
public:
    Triangle(double b, double h) : base(b), height(h) {}
    
    double calculateArea() override {
        return 0.5 * base * height;
    }
    
    string getName() override {
        return "Triangle";
    }
};

// This class doesn't need modification when adding new shapes
class AreaCalculator {
public:
    void printArea(Shape* shape) {
        cout << shape->getName() << " area: " 
             << shape->calculateArea() << endl;
    }
    
    double getTotalArea(vector<Shape*> shapes) {
        double total = 0;
        for (Shape* shape : shapes) {
            total += shape->calculateArea();
        }
        return total;
    }
};

int main() {
    Rectangle rect(5, 3);
    Circle circle(4);
    Triangle triangle(6, 8);
    
    AreaCalculator calculator;
    
    calculator.printArea(&rect);
    calculator.printArea(&circle);
    calculator.printArea(&triangle);
    
    vector<Shape*> shapes = {&rect, &circle, &triangle};
    cout << "Total area: " << calculator.getTotalArea(shapes) << endl;
    
    return 0;
}

Benefits:

6.4 L - Liskov Substitution Principle (LSP)

Definition: Objects of a derived class should be able to replace objects of the base class without breaking the application. In other words, derived classes must be substitutable for their base classes.

Bad Example (Violates LSP):

class Bird {
public:
    virtual void fly() {
        cout << "Flying..." << endl;
    }
};

class Sparrow : public Bird {
public:
    void fly() override {
        cout << "Sparrow flying..." << endl;
    }
};

// Ostrich is a bird but can't fly!
class Ostrich : public Bird {
public:
    void fly() override {
        throw runtime_error("Ostrich can't fly!");
        // This breaks LSP - can't substitute Ostrich for Bird
    }
};

void makeBirdFly(Bird* bird) {
    bird->fly();  // Will crash if bird is an Ostrich!
}

Good Example (Follows LSP):

#include <iostream>
#include <string>
using namespace std;

// Better abstraction
class Bird {
protected:
    string name;
    
public:
    Bird(string n) : name(n) {}
    
    virtual void eat() {
        cout << name << " is eating..." << endl;
    }
    
    virtual void makeSound() = 0;
    
    string getName() { return name; }
};

// Separate interface for flying ability
class FlyingBird : public Bird {
public:
    FlyingBird(string n) : Bird(n) {}
    
    virtual void fly() {
        cout << name << " is flying..." << endl;
    }
};

// Flying birds
class Sparrow : public FlyingBird {
public:
    Sparrow() : FlyingBird("Sparrow") {}
    
    void makeSound() override {
        cout << "Chirp chirp!" << endl;
    }
};

class Eagle : public FlyingBird {
public:
    Eagle() : FlyingBird("Eagle") {}
    
    void makeSound() override {
        cout << "Screech!" << endl;
    }
};

// Non-flying bird - doesn't inherit fly()
class Ostrich : public Bird {
public:
    Ostrich() : Bird("Ostrich") {}
    
    void makeSound() override {
        cout << "Boom boom!" << endl;
    }
    
    void run() {
        cout << name << " is running fast..." << endl;
    }
};

class Penguin : public Bird {
public:
    Penguin() : Bird("Penguin") {}
    
    void makeSound() override {
        cout << "Honk honk!" << endl;
    }
    
    void swim() {
        cout << name << " is swimming..." << endl;
    }
};

int main() {
    Sparrow sparrow;
    Eagle eagle;
    Ostrich ostrich;
    Penguin penguin;
    
    // All birds can make sounds and eat
    Bird* birds[] = {&sparrow, &eagle, &ostrich, &penguin};
    
    cout << "=== All birds can do these ===" << endl;
    for (Bird* bird : birds) {
        bird->makeSound();
        bird->eat();
        cout << endl;
    }
    
    // Only flying birds can fly
    cout << "=== Only flying birds ===" << endl;
    FlyingBird* flyingBirds[] = {&sparrow, &eagle};
    
    for (FlyingBird* bird : flyingBirds) {
        bird->fly();
    }
    
    // Specialized behaviors
    cout << "\n=== Specialized behaviors ===" << endl;
    ostrich.run();
    penguin.swim();
    
    return 0;
}

Real-World Example:

// Bad: Square inheriting from Rectangle violates LSP
class Rectangle {
protected:
    double width, height;
    
public:
    virtual void setWidth(double w) { width = w; }
    virtual void setHeight(double h) { height = h; }
    double getArea() { return width * height; }
};

class Square : public Rectangle {
public:
    void setWidth(double w) override {
        width = height = w;  // Problem: setting width also sets height!
    }
    
    void setHeight(double h) override {
        width = height = h;  // Problem: setting height also sets width!
    }
};

// This function expects Rectangle behavior
void processRectangle(Rectangle* rect) {
    rect->setWidth(5);
    rect->setHeight(4);
    // Expected area: 20
    // But if rect is a Square, area will be 16!
    cout << "Area: " << rect->getArea() << endl;
}

Better Design:

class Shape {
public:
    virtual double getArea() = 0;
    virtual ~Shape() {}
};

class Rectangle : public Shape {
private:
    double width, height;
    
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    
    void setWidth(double w) { width = w; }
    void setHeight(double h) { height = h; }
    
    double getArea() override {
        return width * height;
    }
};

class Square : public Shape {
private:
    double side;
    
public:
    Square(double s) : side(s) {}
    
    void setSide(double s) { side = s; }
    
    double getArea() override {
        return side * side;
    }
};

Benefits:

6.5 I - Interface Segregation Principle (ISP)

Definition: No client should be forced to depend on methods it does not use. It's better to have many specific interfaces than one general-purpose interface.

Bad Example (Violates ISP):

// Fat interface - forces classes to implement methods they don't need
class Worker {
public:
    virtual void work() = 0;
    virtual void eat() = 0;
    virtual void sleep() = 0;
    virtual void attendMeeting() = 0;
    virtual void writeCode() = 0;
    virtual void designUI() = 0;
};

// Robot worker doesn't eat or sleep!
class RobotWorker : public Worker {
public:
    void work() override {
        cout << "Robot working..." << endl;
    }
    
    void eat() override {
        // Robots don't eat! But forced to implement
        throw runtime_error("Robots don't eat!");
    }
    
    void sleep() override {
        // Robots don't sleep! But forced to implement
        throw runtime_error("Robots don't sleep!");
    }
    
    void attendMeeting() override {
        throw runtime_error("Robots don't attend meetings!");
    }
    
    void writeCode() override {
        cout << "Robot writing code..." << endl;
    }
    
    void designUI() override {
        throw runtime_error("Robots don't design UI!");
    }
};

// Manager doesn't write code!
class Manager : public Worker {
public:
    void work() override {
        cout << "Manager working..." << endl;
    }
    
    void eat() override {
        cout << "Manager eating..." << endl;
    }
    
    void sleep() override {
        cout << "Manager sleeping..." << endl;
    }
    
    void attendMeeting() override {
        cout << "Manager attending meeting..." << endl;
    }
    
    void writeCode() override {
        // Managers don't write code! But forced to implement
        throw runtime_error("Managers don't write code!");
    }
    
    void designUI() override {
        throw runtime_error("Managers don't design UI!");
    }
};

Good Example (Follows ISP):

#include <iostream>
#include <string>
using namespace std;

// Segregated interfaces - small, specific interfaces

interface Workable {
public:
    virtual void work() = 0;
    virtual ~Workable() {}
};

class Eatable {
public:
    virtual void eat() = 0;
    virtual ~Eatable() {}
};

class Sleepable {
public:
    virtual void sleep() = 0;
    virtual ~Sleepable() {}
};

class Codable {
public:
    virtual void writeCode() = 0;
    virtual ~Codable() {}
};

class Designable {
public:
    virtual void designUI() = 0;
    virtual ~Designable() {}
};

class Manageable {
public:
    virtual void attendMeeting() = 0;
    virtual void delegateTasks() = 0;
    virtual ~Manageable() {}
};

// Now classes only implement interfaces they need

class Developer : public Workable, public Eatable, public Sleepable, public Codable {
private:
    string name;
    
public:
    Developer(string n) : name(n) {}
    
    void work() override {
        cout << name << " (Developer) is working..." << endl;
    }
    
    void eat() override {
        cout << name << " is eating..." << endl;
    }
    
    void sleep() override {
        cout << name << " is sleeping..." << endl;
    }
    
    void writeCode() override {
        cout << name << " is writing code..." << endl;
    }
};

class Designer : public Workable, public Eatable, public Sleepable, public Designable {
private:
    string name;
    
public:
    Designer(string n) : name(n) {}
    
    void work() override {
        cout << name << " (Designer) is working..." << endl;
    }
    
    void eat() override {
        cout << name << " is eating..." << endl;
    }
    
    void sleep() override {
        cout << name << " is sleeping..." << endl;
    }
    
    void designUI() override {
        cout << name << " is designing UI..." << endl;
    }
};

class Manager : public Workable, public Eatable, public Sleepable, public Manageable {
private:
    string name;
    
public:
    Manager(string n) : name(n) {}
    
    void work() override {
        cout << name << " (Manager) is working..." << endl;
    }
    
    void eat() override {
        cout << name << " is eating..." << endl;
    }
    
    void sleep() override {
        cout << name << " is sleeping..." << endl;
    }
    
    void attendMeeting() override {
        cout << name << " is attending meeting..." << endl;
    }
    
    void delegateTasks() override {
        cout << name << " is delegating tasks..." << endl;
    }
};

class RobotWorker : public Workable, public Codable {
private:
    string model;
    
public:
    RobotWorker(string m) : model(m) {}
    
    void work() override {
        cout << model << " (Robot) is working 24/7..." << endl;
    }
    
    void writeCode() override {
        cout << model << " is writing code..." << endl;
    }
    // No eat() or sleep() - robots don't need these!
};

int main() {
    Developer dev("Alice");
    Designer des("Bob");
    Manager mgr("Carol");
    RobotWorker robot("R2D2");
    
    cout << "=== Developer ===" << endl;
    dev.work();
    dev.writeCode();
    dev.eat();
    
    cout << "\n=== Designer ===" << endl;
    des.work();
    des.designUI();
    des.sleep();
    
    cout << "\n=== Manager ===" << endl;
    mgr.work();
    mgr.attendMeeting();
    mgr.delegateTasks();
    
    cout << "\n=== Robot ===" << endl;
    robot.work();
    robot.writeCode();
    // robot.eat();  // Doesn't exist - good!
    
    return 0;
}

Benefits:

6.6 D - Dependency Inversion Principle (DIP)

Definition:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend on details. Details should depend on abstractions.

Bad Example (Violates DIP):

// Low-level modules (concrete implementations)
class MySQLDatabase {
public:
    void connect() {
        cout << "Connecting to MySQL..." << endl;
    }
    
    void executeQuery(string query) {
        cout << "Executing MySQL query: " << query << endl;
    }
};

// High-level module directly depends on low-level module
class UserService {
private:
    MySQLDatabase database;  // Direct dependency on concrete class!
    
public:
    void getUser(int id) {
        database.connect();
        database.executeQuery("SELECT * FROM users WHERE id = " + to_string(id));
    }
    
    // If we want to switch to PostgreSQL, we must modify this class!
};

Good Example (Follows DIP):

#include <iostream>
#include <string>
#include <memory>
using namespace std;

// Abstraction (high-level interface)
class Database {
public:
    virtual void connect() = 0;
    virtual void executeQuery(string query) = 0;
    virtual ~Database() {}
};

// Low-level modules implement the abstraction
class MySQLDatabase : public Database {
public:
    void connect() override {
        cout << "Connecting to MySQL database..." << endl;
    }
    
    void executeQuery(string query) override {
        cout << "MySQL executing: " << query << endl;
    }
};

class PostgreSQLDatabase : public Database {
public:
    void connect() override {
        cout << "Connecting to PostgreSQL database..." << endl;
    }
    
    void executeQuery(string query) override {
        cout << "PostgreSQL executing: " << query << endl;
    }
};

class MongoDatabase : public Database {
public:
    void connect() override {
        cout << "Connecting to MongoDB..." << endl;
    }
    
    void executeQuery(string query) override {
        cout << "MongoDB executing: " << query << endl;
    }
};

// High-level module depends on abstraction, not concrete class
class UserService {
private:
    Database* database;  // Depends on abstraction!
    
public:
    // Dependency injection through constructor
    UserService(Database* db) : database(db) {}
    
    void getUser(int id) {
        database->connect();
        database->executeQuery("SELECT * FROM users WHERE id = " + to_string(id));
    }
    
    void saveUser(string name, string email) {
        database->connect();
        database->executeQuery("INSERT INTO users (name, email) VALUES ('" + 
                              name + "', '" + email + "')");
    }
    
    // Can easily switch database implementation!
    void setDatabase(Database* db) {
        database = db;
    }
};

int main() {
    // Create different database implementations
    MySQLDatabase mysql;
    PostgreSQLDatabase postgres;
    MongoDatabase mongo;
    
    // Inject dependency (database) into high-level module
    cout << "=== Using MySQL ===" << endl;
    UserService userService1(&mysql);
    userService1.getUser(1);
    userService1.saveUser("Alice", "alice@example.com");
    
    cout << "\n=== Switching to PostgreSQL ===" << endl;
    UserService userService2(&postgres);
    userService2.getUser(2);
    
    cout << "\n=== Switching to MongoDB ===" << endl;
    UserService userService3(&mongo);
    userService3.getUser(3);
    
    return 0;
}

Another Example: Payment Processing

#include <iostream>
#include <string>
using namespace std;

// Abstraction
class PaymentProcessor {
public:
    virtual bool processPayment(double amount) = 0;
    virtual string getProcessorName() = 0;
    virtual ~PaymentProcessor() {}
};

// Concrete implementations
class CreditCardProcessor : public PaymentProcessor {
public:
    bool processPayment(double amount) override {
        cout << "Processing $" << amount << " via Credit Card..." << endl;
        return true;
    }
    
    string getProcessorName() override {
        return "Credit Card";
    }
};

class PayPalProcessor : public PaymentProcessor {
public:
    bool processPayment(double amount) override {
        cout << "Processing $" << amount << " via PayPal..." << endl;
        return true;
    }
    
    string getProcessorName() override {
        return "PayPal";
    }
};

class BitcoinProcessor : public PaymentProcessor {
public:
    bool processPayment(double amount) override {
        cout << "Processing $" << amount << " via Bitcoin..." << endl;
        return true;
    }
    
    string getProcessorName() override {
        return "Bitcoin";
    }
};

// High-level module depends on abstraction
class ShoppingCart {
private:
    double total;
    PaymentProcessor* paymentProcessor;
    
public:
    ShoppingCart() : total(0), paymentProcessor(nullptr) {}
    
    void addItem(double price) {
        total += price;
        cout << "Item added. Current total: $" << total << endl;
    }
    
    void setPaymentMethod(PaymentProcessor* processor) {
        paymentProcessor = processor;
        cout << "Payment method set to: " << processor->getProcessorName() << endl;
    }
    
    void checkout() {
        if (paymentProcessor == nullptr) {
            cout << "Error: No payment method selected!" << endl;
            return;
        }
        
        cout << "\n=== Checkout ===" << endl;
        cout << "Total amount: $" << total << endl;
        
        if (paymentProcessor->processPayment(total)) {
            cout << "Payment successful!" << endl;
            total = 0;
        } else {
            cout << "Payment failed!" << endl;
        }
    }
};

int main() {
    CreditCardProcessor creditCard;
    PayPalProcessor paypal;
    BitcoinProcessor bitcoin;
    
    ShoppingCart cart;
    
    cart.addItem(29.99);
    cart.addItem(49.99);
    cart.addItem(15.50);
    
    cout << "\n--- Paying with Credit Card ---" << endl;
    cart.setPaymentMethod(&creditCard);
    cart.checkout();
    
    cout << "\n--- New purchase ---" << endl;
    cart.addItem(99.99);
    cout << "\n--- Paying with PayPal ---" << endl;
    cart.setPaymentMethod(&paypal);
    cart.checkout();
    
    cout << "\n--- Another purchase ---" << endl;
    cart.addItem(199.99);
    cout << "\n--- Paying with Bitcoin ---" << endl;
    cart.setPaymentMethod(&bitcoin);
    cart.checkout();
    
    return 0;
}

Benefits:

Module 8 : OOP (SOLID, Encapsulation, Abstraction)

7. Constructors and Destructors

7.1 Constructors

Purpose: Special member function that initializes an object when it's created.

Types of Constructors:

#include <iostream>
#include <string>
using namespace std;

class Student {
private:
    string name;
    int id;
    float gpa;
    
public:
    // 1. Default Constructor
    Student() {
        name = "Unknown";
        id = 0;
        gpa = 0.0;
        cout << "Default constructor called" << endl;
    }
    
    // 2. Parameterized Constructor
    Student(string n, int i, float g) {
        name = n;
        id = i;
        gpa = g;
        cout << "Parameterized constructor called" << endl;
    }
    
    // 3. Constructor with Default Parameters
    Student(string n, int i = 0, float g = 0.0) {
        name = n;
        id = i;
        gpa = g;
    }
    
    // 4. Copy Constructor
    Student(const Student& other) {
        name = other.name;
        id = other.id;
        gpa = other.gpa;
        cout << "Copy constructor called" << endl;
    }
    
    void display() {
        cout << "Name: " << name << ", ID: " << id << ", GPA: " << gpa << endl;
    }
};

int main() {
    Student s1;                          // Default constructor
    Student s2("Alice", 101, 3.8);       // Parameterized constructor
    Student s3 = s2;                     // Copy constructor
    Student s4(s2);                      // Copy constructor (explicit)
    
    s1.display();
    s2.display();
    s3.display();
    
    return 0;
}

7.2 Member Initializer List

Preferred way to initialize member variables:

class Rectangle {
private:
    double width;
    double height;
    const int id;  // const members must use initializer list
    
public:
    // Using initializer list (more efficient)
    Rectangle(double w, double h, int i) : width(w), height(h), id(i) {
        // Constructor body
    }
    
    // Alternative (less efficient - assigns after construction)
    // Rectangle(double w, double h) {
    //     width = w;
    //     height = h;
    // }
};

7.3 Destructors

Purpose: Special member function called when an object is destroyed, used for cleanup.

#include <iostream>
#include <string>
using namespace std;

class FileHandler {
private:
    string filename;
    bool isOpen;
    
public:
    // Constructor
    FileHandler(string fname) : filename(fname), isOpen(false) {
        cout << "Opening file: " << filename << endl;
        isOpen = true;
    }
    
    // Destructor
    ~FileHandler() {
        if (isOpen) {
            cout << "Closing file: " << filename << endl;
            isOpen = false;
        }
    }
    
    void writeData(string data) {
        if (isOpen) {
            cout << "Writing to " << filename << ": " << data << endl;
        }
    }
};

int main() {
    {
        FileHandler file1("data.txt");
        file1.writeData("Hello World");
        
        // Destructor automatically called when file1 goes out of scope
    }
    
    cout << "After block" << endl;
    
    return 0;
    // Destructor called for any remaining objects
}

Output:

Opening file: data.txt
Writing to data.txt: Hello World
Closing file: data.txt
After block

Modul 9: OOP - Inheritance

After completing this module, students are expected to:

- Understand the concept of inheritance in OOP

- Implement various types of inheritance in C++

- Use access specifiers in inheritance

- Understand the concept of method overriding

- Apply inheritance for code reusability

Modul 9: OOP - Inheritance

1. Basic Concepts of Inheritance

1.1 What is Inheritance?

Inheritance is a mechanism where a class (derived/child class) can inherit properties and methods from another class (base/parent class).

Real-World Analogy: Think of inheritance like family traits:

Benefits of Inheritance:

1.2 Basic Syntax

// Base class (Parent)
class BaseClass {
    // members
};

// Derived class (Child)
class DerivedClass : access_specifier BaseClass {
    // additional members
};

Example:

#include <iostream>
#include <string>
using namespace std;

// Base class
class Animal {
protected:
    string name;
    int age;
    
public:
    Animal(string n, int a) : name(n), age(a) {
        cout << "Animal constructor called" << endl;
    }
    
    void eat() {
        cout << name << " is eating..." << endl;
    }
    
    void sleep() {
        cout << name << " is sleeping..." << endl;
    }
    
    void displayInfo() {
        cout << "Name: " << name << ", Age: " << age << endl;
    }
};

// Derived class
class Dog : public Animal {
private:
    string breed;
    
public:
    Dog(string n, int a, string b) : Animal(n, a), breed(b) {
        cout << "Dog constructor called" << endl;
    }
    
    void bark() {
        cout << name << " is barking: Woof! Woof!" << endl;
    }
    
    void displayDogInfo() {
        displayInfo();  // Inherited method
        cout << "Breed: " << breed << endl;
    }
};

int main() {
    Dog myDog("Buddy", 3, "Golden Retriever");
    
    // Using inherited methods
    myDog.eat();
    myDog.sleep();
    
    // Using derived class method
    myDog.bark();
    
    myDog.displayDogInfo();
    
    return 0;
}

Output:

Animal constructor called
Dog constructor called
Buddy is eating...
Buddy is sleeping...
Buddy is barking: Woof! Woof!
Name: Buddy, Age: 3
Breed: Golden Retriever

1.3 Access Specifiers in Inheritance

Three Types of Inheritance:

Base Class Member public Inheritance protected Inheritance private Inheritance
public members public protected private
protected members protected protected private
private members Not accessible Not accessible Not accessible

Example:

#include <iostream>
using namespace std;

class Base {
private:
    int privateVar;
    
protected:
    int protectedVar;
    
public:
    int publicVar;
    
    Base() : privateVar(1), protectedVar(2), publicVar(3) {}
};

// Public Inheritance
class PublicDerived : public Base {
public:
    void access() {
        // privateVar = 10;     // ERROR: private not accessible
        protectedVar = 20;      // OK: protected -> protected
        publicVar = 30;         // OK: public -> public
    }
};

// Protected Inheritance
class ProtectedDerived : protected Base {
public:
    void access() {
        // privateVar = 10;     // ERROR: private not accessible
        protectedVar = 20;      // OK: protected -> protected
        publicVar = 30;         // OK: public -> protected
    }
};

// Private Inheritance
class PrivateDerived : private Base {
public:
    void access() {
        // privateVar = 10;     // ERROR: private not accessible
        protectedVar = 20;      // OK: protected -> private
        publicVar = 30;         // OK: public -> private
    }
};

int main() {
    PublicDerived pub;
    pub.publicVar = 100;        // OK: public member accessible
    // pub.protectedVar = 200;  // ERROR: protected not accessible outside
    
    ProtectedDerived prot;
    // prot.publicVar = 100;    // ERROR: public became protected
    
    PrivateDerived priv;
    // priv.publicVar = 100;    // ERROR: public became private
    
    return 0;
}

Best Practice: Use public inheritance for most cases (is-a relationship).

1.4 Constructor and Destructor in Inheritance

Execution Order:

  1. Construction: Base class constructor → Derived class constructor
  2. Destruction: Derived class destructor → Base class destructor
#include <iostream>
#include <string>
using namespace std;

class Vehicle {
protected:
    string brand;
    
public:
    Vehicle(string b) : brand(b) {
        cout << "Vehicle constructor: " << brand << endl;
    }
    
    ~Vehicle() {
        cout << "Vehicle destructor: " << brand << endl;
    }
};

class Car : public Vehicle {
private:
    string model;
    
public:
    Car(string b, string m) : Vehicle(b), model(m) {
        cout << "Car constructor: " << model << endl;
    }
    
    ~Car() {
        cout << "Car destructor: " << model << endl;
    }
};

class ElectricCar : public Car {
private:
    int batteryCapacity;
    
public:
    ElectricCar(string b, string m, int battery) 
        : Car(b, m), batteryCapacity(battery) {
        cout << "ElectricCar constructor: " << batteryCapacity << " kWh" << endl;
    }
    
    ~ElectricCar() {
        cout << "ElectricCar destructor: " << batteryCapacity << " kWh" << endl;
    }
};

int main() {
    cout << "=== Creating ElectricCar ===" << endl;
    ElectricCar tesla("Tesla", "Model 3", 75);
    
    cout << "\n=== Destroying ElectricCar ===" << endl;
    // Destructor called automatically when going out of scope
    
    return 0;
}

Output:

=== Creating ElectricCar ===
Vehicle constructor: Tesla
Car constructor: Model 3
ElectricCar constructor: 75 kWh

=== Destroying ElectricCar ===
ElectricCar destructor: 75 kWh
Car destructor: Model 3
Vehicle destructor: Tesla

1.5 Practical Example: Employee Hierarchy

#include <iostream>
#include <string>
using namespace std;

class Employee {
protected:
    string name;
    int id;
    double baseSalary;
    
public:
    Employee(string n, int i, double salary) 
        : name(n), id(i), baseSalary(salary) {}
    
    void displayBasicInfo() {
        cout << "Name: " << name << endl;
        cout << "ID: " << id << endl;
        cout << "Base Salary: $" << baseSalary << endl;
    }
    
    void work() {
        cout << name << " is working..." << endl;
    }
    
    virtual double calculateSalary() {
        return baseSalary;
    }
};

class Manager : public Employee {
private:
    double bonus;
    int teamSize;
    
public:
    Manager(string n, int i, double salary, double b, int team)
        : Employee(n, i, salary), bonus(b), teamSize(team) {}
    
    void displayManagerInfo() {
        displayBasicInfo();
        cout << "Bonus: $" << bonus << endl;
        cout << "Team Size: " << teamSize << endl;
    }
    
    void conductMeeting() {
        cout << name << " is conducting a meeting..." << endl;
    }
    
    double calculateSalary() override {
        return baseSalary + bonus;
    }
};

class Developer : public Employee {
private:
    string programmingLanguage;
    int projectsCompleted;
    
public:
    Developer(string n, int i, double salary, string lang, int projects)
        : Employee(n, i, salary), programmingLanguage(lang), 
          projectsCompleted(projects) {}
    
    void displayDeveloperInfo() {
        displayBasicInfo();
        cout << "Programming Language: " << programmingLanguage << endl;
        cout << "Projects Completed: " << projectsCompleted << endl;
    }
    
    void writeCode() {
        cout << name << " is writing " << programmingLanguage << " code..." << endl;
    }
    
    double calculateSalary() override {
        return baseSalary + (projectsCompleted * 500);
    }
};

int main() {
    Manager mgr("Alice Johnson", 101, 8000, 2000, 10);
    Developer dev("Bob Smith", 102, 6000, "C++", 5);
    
    cout << "=== Manager Information ===" << endl;
    mgr.displayManagerInfo();
    mgr.work();           // Inherited
    mgr.conductMeeting(); // Own method
    cout << "Total Salary: $" << mgr.calculateSalary() << endl;
    
    cout << "\n=== Developer Information ===" << endl;
    dev.displayDeveloperInfo();
    dev.work();           // Inherited
    dev.writeCode();      // Own method
    cout << "Total Salary: $" << dev.calculateSalary() << endl;
    
    return 0;
}
Modul 9: OOP - Inheritance

2. Types of Inheritance and Method Overriding

2.1 Single Inheritance

Definition: One derived class inherits from one base class.

#include <iostream>
#include <string>
using namespace std;

class Person {
protected:
    string name;
    int age;
    
public:
    Person(string n, int a) : name(n), age(a) {}
    
    void introduce() {
        cout << "Hi, I'm " << name << ", " << age << " years old." << endl;
    }
};

class Student : public Person {
private:
    string studentId;
    double gpa;
    
public:
    Student(string n, int a, string id, double g)
        : Person(n, a), studentId(id), gpa(g) {}
    
    void study() {
        cout << name << " is studying..." << endl;
    }
    
    void showGPA() {
        cout << "GPA: " << gpa << endl;
    }
};

int main() {
    Student s("Alice", 20, "S001", 3.8);
    s.introduce();  // Inherited
    s.study();      // Own method
    s.showGPA();    // Own method
    
    return 0;
}

2.2 Multilevel Inheritance

Definition: A class is derived from another derived class.

#include <iostream>
#include <string>
using namespace std;

// Level 1: Base class
class LivingBeing {
protected:
    bool isAlive;
    
public:
    LivingBeing() : isAlive(true) {
        cout << "LivingBeing created" << endl;
    }
    
    void breathe() {
        cout << "Breathing..." << endl;
    }
};

// Level 2: Derived from LivingBeing
class Animal : public LivingBeing {
protected:
    string species;
    
public:
    Animal(string s) : species(s) {
        cout << "Animal created: " << species << endl;
    }
    
    void move() {
        cout << species << " is moving..." << endl;
    }
};

// Level 3: Derived from Animal
class Dog : public Animal {
private:
    string name;
    
public:
    Dog(string n) : Animal("Canine"), name(n) {
        cout << "Dog created: " << name << endl;
    }
    
    void bark() {
        cout << name << " is barking!" << endl;
    }
    
    void showCapabilities() {
        breathe();  // From LivingBeing
        move();     // From Animal
        bark();     // From Dog
    }
};

int main() {
    Dog myDog("Buddy");
    cout << "\nDog capabilities:" << endl;
    myDog.showCapabilities();
    
    return 0;
}

Output:

LivingBeing created
Animal created: Canine
Dog created: Buddy

Dog capabilities:
Breathing...
Canine is moving...
Buddy is barking!

2.3 Multiple Inheritance

Definition: A class inherits from multiple base classes.

#include <iostream>
#include <string>
using namespace std;

class Engine {
protected:
    int horsepower;
    
public:
    Engine(int hp) : horsepower(hp) {
        cout << "Engine: " << horsepower << " HP" << endl;
    }
    
    void start() {
        cout << "Engine started: " << horsepower << " HP" << endl;
    }
};

class GPS {
protected:
    string currentLocation;
    
public:
    GPS(string loc) : currentLocation(loc) {
        cout << "GPS initialized at: " << loc << endl;
    }
    
    void navigate(string destination) {
        cout << "Navigating from " << currentLocation 
             << " to " << destination << endl;
    }
};

class SmartCar : public Engine, public GPS {
private:
    string model;
    
public:
    SmartCar(string m, int hp, string loc) 
        : Engine(hp), GPS(loc), model(m) {
        cout << "SmartCar created: " << model << endl;
    }
    
    void drive(string destination) {
        cout << "\n=== Driving " << model << " ===" << endl;
        start();            // From Engine
        navigate(destination); // From GPS
        cout << "Arrived at destination!" << endl;
    }
};

int main() {
    SmartCar tesla("Tesla Model S", 670, "New York");
    tesla.drive("Boston");
    
    return 0;
}

Output:

Engine: 670 HP
GPS initialized at: New York
SmartCar created: Tesla Model S

=== Driving Tesla Model S ===
Engine started: 670 HP
Navigating from New York to Boston
Arrived at destination!

Diamond Problem in Multiple Inheritance:

#include <iostream>
using namespace std;

class Device {
protected:
    int powerConsumption;
    
public:
    Device(int power) : powerConsumption(power) {
        cout << "Device: " << power << "W" << endl;
    }
};

// Problem: Both inherit from Device
class Printer : public Device {
public:
    Printer(int power) : Device(power) {}
};

class Scanner : public Device {
public:
    Scanner(int power) : Device(power) {}
};

// This creates two copies of Device!
class AllInOne : public Printer, public Scanner {
public:
    AllInOne(int pPower, int sPower) 
        : Printer(pPower), Scanner(sPower) {}
    // Now we have ambiguity!
};

// Solution: Virtual Inheritance
class DeviceVirtual {
protected:
    int powerConsumption;
    
public:
    DeviceVirtual(int power) : powerConsumption(power) {
        cout << "Device: " << power << "W" << endl;
    }
};

class PrinterVirtual : virtual public DeviceVirtual {
public:
    PrinterVirtual(int power) : DeviceVirtual(power) {}
};

class ScannerVirtual : virtual public DeviceVirtual {
public:
    ScannerVirtual(int power) : DeviceVirtual(power) {}
};

class AllInOneVirtual : public PrinterVirtual, public ScannerVirtual {
public:
    AllInOneVirtual(int power) 
        : DeviceVirtual(power), PrinterVirtual(power), ScannerVirtual(power) {}
    // Now only ONE copy of DeviceVirtual
};

int main() {
    AllInOneVirtual device(50);
    
    return 0;
}

2.4 Hierarchical Inheritance

Definition: Multiple derived classes inherit from a single base class.

#include <iostream>
#include <string>
using namespace std;

class Shape {
protected:
    string color;
    
public:
    Shape(string c) : color(c) {}
    
    void displayColor() {
        cout << "Color: " << color << endl;
    }
    
    virtual double getArea() = 0;
};

class Circle : public Shape {
private:
    double radius;
    
public:
    Circle(string c, double r) : Shape(c), radius(r) {}
    
    double getArea() override {
        return 3.14159 * radius * radius;
    }
    
    void display() {
        cout << "Circle - ";
        displayColor();
        cout << "Radius: " << radius << endl;
        cout << "Area: " << getArea() << endl;
    }
};

class Rectangle : public Shape {
private:
    double width, height;
    
public:
    Rectangle(string c, double w, double h) 
        : Shape(c), width(w), height(h) {}
    
    double getArea() override {
        return width * height;
    }
    
    void display() {
        cout << "Rectangle - ";
        displayColor();
        cout << "Width: " << width << ", Height: " << height << endl;
        cout << "Area: " << getArea() << endl;
    }
};

class Triangle : public Shape {
private:
    double base, height;
    
public:
    Triangle(string c, double b, double h) 
        : Shape(c), base(b), height(h) {}
    
    double getArea() override {
        return 0.5 * base * height;
    }
    
    void display() {
        cout << "Triangle - ";
        displayColor();
        cout << "Base: " << base << ", Height: " << height << endl;
        cout << "Area: " << getArea() << endl;
    }
};

int main() {
    Circle circle("Red", 5.0);
    Rectangle rect("Blue", 4.0, 6.0);
    Triangle tri("Green", 8.0, 5.0);
    
    circle.display();
    cout << endl;
    rect.display();
    cout << endl;
    tri.display();
    
    return 0;
}

2.5 Method Overriding

Definition: Redefining a base class method in a derived class.

#include <iostream>
#include <string>
using namespace std;

class Account {
protected:
    string accountNumber;
    double balance;
    
public:
    Account(string acc, double bal) 
        : accountNumber(acc), balance(bal) {}
    
    // Method to be overridden
    virtual void withdraw(double amount) {
        if (amount <= balance) {
            balance -= amount;
            cout << "Withdrawn: $" << amount << endl;
        } else {
            cout << "Insufficient funds!" << endl;
        }
    }
    
    virtual void displayInfo() {
        cout << "Account: " << accountNumber << endl;
        cout << "Balance: $" << balance << endl;
    }
    
    double getBalance() { return balance; }
};

class SavingsAccount : public Account {
private:
    double minimumBalance;
    
public:
    SavingsAccount(string acc, double bal, double minBal)
        : Account(acc, bal), minimumBalance(minBal) {}
    
    // Override withdraw with additional constraint
    void withdraw(double amount) override {
        if (balance - amount >= minimumBalance) {
            balance -= amount;
            cout << "Withdrawn: $" << amount << endl;
        } else {
            cout << "Cannot withdraw: Minimum balance requirement!" << endl;
            cout << "Minimum balance: $" << minimumBalance << endl;
        }
    }
    
    void displayInfo() override {
        Account::displayInfo();  // Call base class method
        cout << "Minimum Balance: $" << minimumBalance << endl;
        cout << "Account Type: Savings" << endl;
    }
};

class CheckingAccount : public Account {
private:
    double overdraftLimit;
    
public:
    CheckingAccount(string acc, double bal, double overdraft)
        : Account(acc, bal), overdraftLimit(overdraft) {}
    
    // Override withdraw with overdraft feature
    void withdraw(double amount) override {
        if (balance + overdraftLimit >= amount) {
            balance -= amount;
            cout << "Withdrawn: $" << amount << endl;
            if (balance < 0) {
                cout << "Warning: Overdraft used! Balance: $" << balance << endl;
            }
        } else {
            cout << "Cannot withdraw: Exceeds overdraft limit!" << endl;
        }
    }
    
    void displayInfo() override {
        Account::displayInfo();
        cout << "Overdraft Limit: $" << overdraftLimit << endl;
        cout << "Account Type: Checking" << endl;
    }
};

int main() {
    SavingsAccount savings("SA001", 5000, 1000);
    CheckingAccount checking("CA001", 2000, 500);
    
    cout << "=== Savings Account ===" << endl;
    savings.displayInfo();
    cout << "\nTrying to withdraw $4500..." << endl;
    savings.withdraw(4500);  // Should fail (below minimum)
    cout << "\nTrying to withdraw $3000..." << endl;
    savings.withdraw(3000);  // Should succeed
    
    cout << "\n=== Checking Account ===" << endl;
    checking.displayInfo();
    cout << "\nTrying to withdraw $2300..." << endl;
    checking.withdraw(2300);  // Should succeed with overdraft
    
    return 0;
}
Modul 9: OOP - Inheritance

3. Practical Applications and Best Practices

3.1 Complete Example: University Management System

#include <iostream>
#include <vector>
#include <string>
using namespace std;

// Base class
class Person {
protected:
    string name;
    int id;
    int age;
    
public:
    Person(string n, int i, int a) : name(n), id(i), age(a) {}
    
    virtual void displayInfo() {
        cout << "Name: " << name << endl;
        cout << "ID: " << id << endl;
        cout << "Age: " << age << endl;
    }
    
    virtual string getRole() = 0;  // Pure virtual
    
    string getName() { return name; }
    int getId() { return id; }
    
    virtual ~Person() {}
};

// Derived class: Student
class Student : public Person {
private:
    string major;
    double gpa;
    vector<string> courses;
    
public:
    Student(string n, int i, int a, string m, double g)
        : Person(n, i, a), major(m), gpa(g) {}
    
    void enrollCourse(string course) {
        courses.push_back(course);
        cout << name << " enrolled in " << course << endl;
    }
    
    void displayInfo() override {
        cout << "=== Student Information ===" << endl;
        Person::displayInfo();
        cout << "Major: " << major << endl;
        cout << "GPA: " << gpa << endl;
        cout << "Enrolled Courses: ";
        if (courses.empty()) {
            cout << "None" << endl;
        } else {
            cout << endl;
            for (const string& course : courses) {
                cout << "  - " << course << endl;
            }
        }
    }
    
    string getRole() override {
        return "Student";
    }
    
    void study() {
        cout << name << " is studying..." << endl;
    }
};

// Derived class: Faculty
class Faculty : public Person {
private:
    string department;
    double salary;
    vector<string> coursesTeaching;
    
public:
    Faculty(string n, int i, int a, string dept, double sal)
        : Person(n, i, a), department(dept), salary(sal) {}
    
    void assignCourse(string course) {
        coursesTeaching.push_back(course);
        cout << name << " assigned to teach " << course << endl;
    }
    
    void displayInfo() override {
        cout << "=== Faculty Information ===" << endl;
        Person::displayInfo();
        cout << "Department: " << department << endl;
        cout << "Salary: $" << salary << endl;
        cout << "Courses Teaching: ";
        if (coursesTeaching.empty()) {
            cout << "None" << endl;
        } else {
            cout << endl;
            for (const string& course : coursesTeaching) {
                cout << "  - " << course << endl;
            }
        }
    }
    
    string getRole() override {
        return "Faculty";
    }
    
    void teach() {
        cout << name << " is teaching..." << endl;
    }
};

// Derived class: Staff
class Staff : public Person {
private:
    string position;
    string department;
    double salary;
    
public:
    Staff(string n, int i, int a, string pos, string dept, double sal)
        : Person(n, i, a), position(pos), department(dept), salary(sal) {}
    
    void displayInfo() override {
        cout << "=== Staff Information ===" << endl;
        Person::displayInfo();
        cout << "Position: " << position << endl;
        cout << "Department: " << department << endl;
        cout << "Salary: $" << salary << endl;
    }
    
    string getRole() override {
        return "Staff";
    }
    
    void work() {
        cout << name << " (" << position << ") is working..." << endl;
    }
};

// University class to manage all persons
class University {
private:
    string universityName;
    vector<Person*> members;
    
public:
    University(string name) : universityName(name) {
        cout << "University '" << universityName << "' initialized." << endl;
    }
    
    void addMember(Person* person) {
        members.push_back(person);
        cout << person->getRole() << " " << person->getName() 
             << " added to " << universityName << endl;
    }
    
    void displayAllMembers() {
        cout << "\n========== " << universityName << " - All Members ==========" << endl;
        
        for (Person* member : members) {
            member->displayInfo();
            cout << "-------------------------------------------" << endl;
        }
    }
    
    void displayByRole(string role) {
        cout << "\n========== " << role << "s at " << universityName << " ==========" << endl;
        
        bool found = false;
        for (Person* member : members) {
            if (member->getRole() == role) {
                member->displayInfo();
                cout << "-------------------------------------------" << endl;
                found = true;
            }
        }
        
        if (!found) {
            cout << "No " << role << "s found." << endl;
        }
    }
    
    Person* findMember(int id) {
        for (Person* member : members) {
            if (member->getId() == id) {
                return member;
            }
        }
        return nullptr;
    }
    
    ~University() {
        for (Person* member : members) {
            delete member;
        }
    }
};

int main() {
    University university("Tech University");
    
    cout << "\n=== Adding Members ===" << endl;
    
    // Add students
    Student* s1 = new Student("Alice Johnson", 1001, 20, "Computer Science", 3.8);
    Student* s2 = new Student("Bob Smith", 1002, 21, "Electrical Engineering", 3.6);
    
    university.addMember(s1);
    university.addMember(s2);
    
    // Add faculty
    Faculty* f1 = new Faculty("Dr. Carol White", 2001, 45, "Computer Science", 85000);
    Faculty* f2 = new Faculty("Dr. David Brown", 2002, 50, "Electrical Engineering", 90000);
    
    university.addMember(f1);
    university.addMember(f2);
    
    // Add staff
    Staff* st1 = new Staff("Eve Davis", 3001, 35, "Administrator", "Administration", 50000);
    
    university.addMember(st1);
    
    // Assign courses
    cout << "\n=== Assigning Courses ===" << endl;
    f1->assignCourse("Data Structures");
    f1->assignCourse("Algorithms");
    f2->assignCourse("Circuit Analysis");
    
    // Enroll students
    cout << "\n=== Enrolling Students ===" << endl;
    s1->enrollCourse("Data Structures");
    s1->enrollCourse("Algorithms");
    s2->enrollCourse("Circuit Analysis");
    
    // Display all members
    university.displayAllMembers();
    
    // Display by role
    university.displayByRole("Student");
    university.displayByRole("Faculty");
    
    // Polymorphic behavior
    cout << "\n=== Daily Activities ===" << endl;
    s1->study();
    f1->teach();
    st1->work();
    
    return 0;
}

3.2 Best Practices

1. Use virtual Destructors in Base Classes

class Base {
public:
    virtual ~Base() {}  // IMPORTANT for polymorphism
};

2. Use override Keyword

class Derived : public Base {
public:
    void someMethod() override {  // Explicit override
        // Implementation
    }
};

3. Prefer Composition Over Inheritance When Appropriate

// Don't do: class Car : public Engine
// Do this:
class Car {
private:
    Engine engine;  // Has-a relationship
public:
    Car() : engine() {}
};

4. Keep Base Class Interface Stable

// Good: Minimal, stable base class
class Shape {
public:
    virtual double getArea() = 0;
    virtual void draw() = 0;
    virtual ~Shape() {}
};

5. Use protected for Members Needed by Derived Classes

class Base {
protected:
    int valueNeededByDerived;  // Accessible in derived classes
private:
    int internalImplementation;  // Not accessible in derived classes
};

3.3 Common Pitfalls and Solutions

Problem 1: Forgetting Virtual Destructor

// BAD: No virtual destructor
class Base {
public:
    ~Base() { cout << "Base destructor" << endl; }
};

class Derived : public Base {
private:
    int* data;
public:
    Derived() { data = new int[100]; }
    ~Derived() { 
        delete[] data; 
        cout << "Derived destructor" << endl; 
    }
};

int main() {
    Base* obj = new Derived();
    delete obj;  // MEMORY LEAK! Only Base destructor called
    return 0;
}

// GOOD: Virtual destructor
class Base {
public:
    virtual ~Base() { cout << "Base destructor" << endl; }
};

class Derived : public Base {
private:
    int* data;
public:
    Derived() { data = new int[100]; }
    ~Derived() override { 
        delete[] data; 
        cout << "Derived destructor" << endl; 
    }
};

int main() {
    Base* obj = new Derived();
    delete obj;  // Correct! Both destructors called
    return 0;
}

Problem 2: Object Slicing

#include <iostream>
#include <string>
using namespace std;

class Animal {
public:
    string type;
    Animal() : type("Animal") {}
    virtual void makeSound() { cout << "Generic sound" << endl; }
};

class Dog : public Animal {
public:
    string breed;
    Dog() : breed("Unknown") { type = "Dog"; }
    void makeSound() override { cout << "Woof!" << endl; }
};

// BAD: Pass by value causes slicing
void processAnimal(Animal animal) {
    animal.makeSound();  // Always calls Animal::makeSound()!
}

// GOOD: Pass by reference or pointer
void processAnimalCorrect(Animal& animal) {
    animal.makeSound();  // Calls correct method
}

void processAnimalPointer(Animal* animal) {
    animal->makeSound();  // Calls correct method
}

int main() {
    Dog myDog;
    
    cout << "=== Object Slicing (BAD) ===" << endl;
    processAnimal(myDog);  // Slicing occurs! Outputs: Generic sound
    
    cout << "\n=== Using Reference (GOOD) ===" << endl;
    processAnimalCorrect(myDog);  // Outputs: Woof!
    
    cout << "\n=== Using Pointer (GOOD) ===" << endl;
    processAnimalPointer(&myDog);  // Outputs: Woof!
    
    return 0;
}

Problem 3: Calling Virtual Functions in Constructor

#include <iostream>
using namespace std;

class Base {
public:
    Base() {
        init();  // Calls Base::init(), not Derived::init()!
    }
    
    virtual void init() {
        cout << "Base initialization" << endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        // Base constructor already called
    }
    
    void init() override {
        cout << "Derived initialization" << endl;
    }
};

int main() {
    Derived d;  // Output: Base initialization (not what we want!)
    return 0;
}

// SOLUTION: Call init() explicitly after construction
class BaseSolution {
public:
    BaseSolution() {
        // Don't call virtual functions here
    }
    
    virtual void init() {
        cout << "Base initialization" << endl;
    }
};

class DerivedSolution : public BaseSolution {
public:
    DerivedSolution() {
        // Don't call init() in constructor
    }
    
    void init() override {
        cout << "Derived initialization" << endl;
    }
};

int main() {
    DerivedSolution d;
    d.init();  // Now calls Derived::init()
    return 0;
}

Problem 4: Ambiguity in Multiple Inheritance

#include <iostream>
using namespace std;

class ClassA {
public:
    void display() { cout << "ClassA" << endl; }
};

class ClassB {
public:
    void display() { cout << "ClassB" << endl; }
};

class ClassC : public ClassA, public ClassB {
    // Now ClassC has two display() methods!
};

int main() {
    ClassC obj;
    // obj.display();  // ERROR: Ambiguous! Which display()?
    
    // SOLUTION 1: Explicitly specify which class
    obj.ClassA::display();
    obj.ClassB::display();
    
    return 0;
}

// SOLUTION 2: Override in derived class
class ClassCSolution : public ClassA, public ClassB {
public:
    void display() {
        cout << "ClassC - calling both:" << endl;
        ClassA::display();
        ClassB::display();
    }
};

3.4 Real-World Example: Vehicle Rental System

#include <iostream>
#include <vector>
#include <string>
using namespace std;

// Base class
class Vehicle {
protected:
    string vehicleId;
    string brand;
    string model;
    double dailyRate;
    bool isRented;
    
public:
    Vehicle(string id, string b, string m, double rate)
        : vehicleId(id), brand(b), model(m), dailyRate(rate), isRented(false) {}
    
    virtual void displayInfo() {
        cout << "ID: " << vehicleId << endl;
        cout << "Brand: " << brand << endl;
        cout << "Model: " << model << endl;
        cout << "Daily Rate: $" << dailyRate << endl;
        cout << "Status: " << (isRented ? "Rented" : "Available") << endl;
    }
    
    virtual double calculateRentalCost(int days) {
        return dailyRate * days;
    }
    
    virtual string getVehicleType() = 0;
    
    void rent() {
        if (!isRented) {
            isRented = true;
            cout << "Vehicle " << vehicleId << " rented successfully." << endl;
        } else {
            cout << "Vehicle " << vehicleId << " is already rented." << endl;
        }
    }
    
    void returnVehicle() {
        if (isRented) {
            isRented = false;
            cout << "Vehicle " << vehicleId << " returned successfully." << endl;
        }
    }
    
    bool checkAvailability() { return !isRented; }
    string getId() { return vehicleId; }
    
    virtual ~Vehicle() {}
};

// Derived class: Car
class Car : public Vehicle {
private:
    int numDoors;
    string fuelType;
    
public:
    Car(string id, string b, string m, double rate, int doors, string fuel)
        : Vehicle(id, b, m, rate), numDoors(doors), fuelType(fuel) {}
    
    void displayInfo() override {
        cout << "\n=== CAR ===" << endl;
        Vehicle::displayInfo();
        cout << "Number of Doors: " << numDoors << endl;
        cout << "Fuel Type: " << fuelType << endl;
    }
    
    double calculateRentalCost(int days) override {
        double baseCost = Vehicle::calculateRentalCost(days);
        // Premium fuel adds 10% to cost
        if (fuelType == "Premium") {
            baseCost *= 1.1;
        }
        return baseCost;
    }
    
    string getVehicleType() override {
        return "Car";
    }
};

// Derived class: Motorcycle
class Motorcycle : public Vehicle {
private:
    int engineCC;
    bool hasABS;
    
public:
    Motorcycle(string id, string b, string m, double rate, int cc, bool abs)
        : Vehicle(id, b, m, rate), engineCC(cc), hasABS(abs) {}
    
    void displayInfo() override {
        cout << "\n=== MOTORCYCLE ===" << endl;
        Vehicle::displayInfo();
        cout << "Engine CC: " << engineCC << endl;
        cout << "ABS: " << (hasABS ? "Yes" : "No") << endl;
    }
    
    double calculateRentalCost(int days) override {
        double baseCost = Vehicle::calculateRentalCost(days);
        // Motorcycles get 20% discount for rentals over 7 days
        if (days > 7) {
            baseCost *= 0.8;
        }
        return baseCost;
    }
    
    string getVehicleType() override {
        return "Motorcycle";
    }
};

// Derived class: Truck
class Truck : public Vehicle {
private:
    double loadCapacity;  // in tons
    bool hasLiftGate;
    
public:
    Truck(string id, string b, string m, double rate, double capacity, bool liftGate)
        : Vehicle(id, b, m, rate), loadCapacity(capacity), hasLiftGate(liftGate) {}
    
    void displayInfo() override {
        cout << "\n=== TRUCK ===" << endl;
        Vehicle::displayInfo();
        cout << "Load Capacity: " << loadCapacity << " tons" << endl;
        cout << "Lift Gate: " << (hasLiftGate ? "Yes" : "No") << endl;
    }
    
    double calculateRentalCost(int days) override {
        double baseCost = Vehicle::calculateRentalCost(days);
        // Additional charge based on capacity
        baseCost += (loadCapacity * 5 * days);
        // Lift gate adds $10 per day
        if (hasLiftGate) {
            baseCost += (10 * days);
        }
        return baseCost;
    }
    
    string getVehicleType() override {
        return "Truck";
    }
};

// Rental system manager
class RentalSystem {
private:
    string companyName;
    vector<Vehicle*> fleet;
    
public:
    RentalSystem(string name) : companyName(name) {
        cout << "=== " << companyName << " Rental System Initialized ===" << endl;
    }
    
    void addVehicle(Vehicle* vehicle) {
        fleet.push_back(vehicle);
        cout << vehicle->getVehicleType() << " " << vehicle->getId() 
             << " added to fleet." << endl;
    }
    
    void displayFleet() {
        cout << "\n========== " << companyName << " - Fleet ==========" << endl;
        for (Vehicle* vehicle : fleet) {
            vehicle->displayInfo();
            cout << "----------------------------------------" << endl;
        }
    }
    
    void displayAvailableVehicles() {
        cout << "\n========== Available Vehicles ==========" << endl;
        bool found = false;
        for (Vehicle* vehicle : fleet) {
            if (vehicle->checkAvailability()) {
                vehicle->displayInfo();
                cout << "----------------------------------------" << endl;
                found = true;
            }
        }
        if (!found) {
            cout << "No vehicles available." << endl;
        }
    }
    
    Vehicle* findVehicle(string id) {
        for (Vehicle* vehicle : fleet) {
            if (vehicle->getId() == id) {
                return vehicle;
            }
        }
        return nullptr;
    }
    
    void rentVehicle(string id, int days) {
        Vehicle* vehicle = findVehicle(id);
        if (vehicle != nullptr) {
            if (vehicle->checkAvailability()) {
                vehicle->rent();
                double cost = vehicle->calculateRentalCost(days);
                cout << "Rental Duration: " << days << " days" << endl;
                cout << "Total Cost: $" << cost << endl;
            } else {
                cout << "Vehicle is not available." << endl;
            }
        } else {
            cout << "Vehicle not found." << endl;
        }
    }
    
    void returnVehicle(string id) {
        Vehicle* vehicle = findVehicle(id);
        if (vehicle != nullptr) {
            vehicle->returnVehicle();
        } else {
            cout << "Vehicle not found." << endl;
        }
    }
    
    ~RentalSystem() {
        for (Vehicle* vehicle : fleet) {
            delete vehicle;
        }
    }
};

int main() {
    RentalSystem rental("QuickRent");
    
    cout << "\n=== Adding Vehicles to Fleet ===" << endl;
    rental.addVehicle(new Car("C001", "Toyota", "Camry", 50, 4, "Regular"));
    rental.addVehicle(new Car("C002", "BMW", "M5", 120, 4, "Premium"));
    rental.addVehicle(new Motorcycle("M001", "Harley", "Sportster", 40, 883, true));
    rental.addVehicle(new Motorcycle("M002", "Honda", "CBR", 35, 600, false));
    rental.addVehicle(new Truck("T001", "Ford", "F-150", 80, 2.5, true));
    rental.addVehicle(new Truck("T002", "Chevrolet", "Silverado", 85, 3.0, false));
    
    // Display all vehicles
    rental.displayFleet();
    
    // Rent some vehicles
    cout << "\n=== Renting Vehicles ===" << endl;
    rental.rentVehicle("C001", 3);
    cout << endl;
    rental.rentVehicle("M001", 10);  // Gets discount (>7 days)
    cout << endl;
    rental.rentVehicle("T001", 5);
    
    // Display available vehicles
    rental.displayAvailableVehicles();
    
    // Try to rent already rented vehicle
    cout << "\n=== Trying to Rent Already Rented Vehicle ===" << endl;
    rental.rentVehicle("C001", 2);
    
    // Return vehicles
    cout << "\n=== Returning Vehicles ===" << endl;
    rental.returnVehicle("C001");
    rental.returnVehicle("M001");
    
    // Display available vehicles again
    rental.displayAvailableVehicles();
    
    return 0;
}

Module 10 : OOP - Polymorphism

After completing this module, students are expected to:

- Understand the concept of polymorphism in OOP

- Implement compile-time polymorphism (function and operator overloading)

- Implement runtime polymorphism (virtual functions)

- Use abstract classes and pure virtual functions

- Apply polymorphism for flexible and maintainable code

Module 10 : OOP - Polymorphism

1. Basic Concepts of Polymorphism

1.1 What is Polymorphism?

Polymorphism means "many forms" - the ability of objects to take on multiple forms or behave differently based on their type.

Real-World Analogy: Think of a smartphone's "share" button:

Types of Polymorphism in C++:

  1. Compile-Time Polymorphism (Static Binding)

    • Function Overloading
    • Operator Overloading
  2. Runtime Polymorphism (Dynamic Binding)

    • Virtual Functions
    • Abstract Classes

Benefits of Polymorphism:

1.2 Compile-Time vs Runtime Polymorphism

#include <iostream>
using namespace std;

// COMPILE-TIME POLYMORPHISM: Function Overloading
class Calculator {
public:
    // Same function name, different parameters
    int add(int a, int b) {
        return a + b;
    }
    
    double add(double a, double b) {
        return a + b;
    }
    
    int add(int a, int b, int c) {
        return a + b + c;
    }
};

// RUNTIME POLYMORPHISM: Virtual Functions
class Shape {
public:
    virtual void draw() {
        cout << "Drawing a shape" << endl;
    }
    
    virtual ~Shape() {}
};

class Circle : public Shape {
public:
    void draw() override {
        cout << "Drawing a circle" << endl;
    }
};

class Rectangle : public Shape {
public:
    void draw() override {
        cout << "Drawing a rectangle" << endl;
    }
};

int main() {
    cout << "=== Compile-Time Polymorphism ===" << endl;
    Calculator calc;
    cout << "add(5, 3) = " << calc.add(5, 3) << endl;
    cout << "add(5.5, 3.2) = " << calc.add(5.5, 3.2) << endl;
    cout << "add(1, 2, 3) = " << calc.add(1, 2, 3) << endl;
    
    cout << "\n=== Runtime Polymorphism ===" << endl;
    Shape* shape1 = new Circle();
    Shape* shape2 = new Rectangle();
    
    shape1->draw();  // Calls Circle::draw()
    shape2->draw();  // Calls Rectangle::draw()
    
    delete shape1;
    delete shape2;
    
    return 0;
}

Output:

=== Compile-Time Polymorphism ===
add(5, 3) = 8
add(5.5, 3.2) = 8.7
add(1, 2, 3) = 6

=== Runtime Polymorphism ===
Drawing a circle
Drawing a rectangle
Module 10 : OOP - Polymorphism

2. Compile-Time Polymorphism

2.1 Function Overloading

Definition: Multiple functions with the same name but different parameters.

#include <iostream>
#include <string>
using namespace std;

class Printer {
public:
    // Overloaded print functions
    void print(int value) {
        cout << "Printing integer: " << value << endl;
    }
    
    void print(double value) {
        cout << "Printing double: " << value << endl;
    }
    
    void print(string value) {
        cout << "Printing string: " << value << endl;
    }
    
    void print(int value, int times) {
        cout << "Printing " << value << " for " << times << " times: ";
        for (int i = 0; i < times; i++) {
            cout << value << " ";
        }
        cout << endl;
    }
};

int main() {
    Printer p;
    
    p.print(42);
    p.print(3.14);
    p.print("Hello, World!");
    p.print(7, 3);
    
    return 0;
}

Rules for Function Overloading:

  1. Functions must have different parameter lists
  2. Return type alone is NOT enough to differentiate
  3. Parameter types, number, or order must differ
#include <iostream>
using namespace std;

class Example {
public:
    // VALID overloads
    void func(int x) { cout << "int: " << x << endl; }
    void func(double x) { cout << "double: " << x << endl; }
    void func(int x, int y) { cout << "two ints: " << x << ", " << y << endl; }
    
    // INVALID: Only return type differs
    // int func(int x) { return x; }  // ERROR!
    
    // VALID: Different parameter order
    void process(int x, double y) { cout << "int, double" << endl; }
    void process(double x, int y) { cout << "double, int" << endl; }
};

int main() {
    Example ex;
    
    ex.func(10);
    ex.func(3.14);
    ex.func(5, 7);
    
    ex.process(5, 3.14);
    ex.process(3.14, 5);
    
    return 0;
}

2.2 Operator Overloading

Definition: Redefining operators to work with user-defined types.

Basic Syntax:

return_type operator symbol (parameters) {
    // implementation
}

Example: Complex Number Class

#include <iostream>
using namespace std;

class Complex {
private:
    double real;
    double imag;
    
public:
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}
    
    // Overload + operator
    Complex operator+(const Complex& other) {
        return Complex(real + other.real, imag + other.imag);
    }
    
    // Overload - operator
    Complex operator-(const Complex& other) {
        return Complex(real - other.real, imag - other.imag);
    }
    
    // Overload * operator
    Complex operator*(const Complex& other) {
        return Complex(
            real * other.real - imag * other.imag,
            real * other.imag + imag * other.real
        );
    }
    
    // Overload == operator
    bool operator==(const Complex& other) {
        return (real == other.real && imag == other.imag);
    }
    
    // Overload << operator (friend function)
    friend ostream& operator<<(ostream& out, const Complex& c) {
        out << c.real;
        if (c.imag >= 0)
            out << " + " << c.imag << "i";
        else
            out << " - " << -c.imag << "i";
        return out;
    }
    
    void display() {
        cout << *this << endl;
    }
};

int main() {
    Complex c1(3, 4);
    Complex c2(1, 2);
    
    cout << "c1 = " << c1 << endl;
    cout << "c2 = " << c2 << endl;
    
    Complex c3 = c1 + c2;
    cout << "c1 + c2 = " << c3 << endl;
    
    Complex c4 = c1 - c2;
    cout << "c1 - c2 = " << c4 << endl;
    
    Complex c5 = c1 * c2;
    cout << "c1 * c2 = " << c5 << endl;
    
    if (c1 == c2)
        cout << "c1 equals c2" << endl;
    else
        cout << "c1 not equals c2" << endl;
    
    return 0;
}

Common Operators to Overload:

#include <iostream>
#include <string>
using namespace std;

class Vector2D {
private:
    double x, y;
    
public:
    Vector2D(double x = 0, double y = 0) : x(x), y(y) {}
    
    // Arithmetic operators
    Vector2D operator+(const Vector2D& v) {
        return Vector2D(x + v.x, y + v.y);
    }
    
    Vector2D operator-(const Vector2D& v) {
        return Vector2D(x - v.x, y - v.y);
    }
    
    Vector2D operator*(double scalar) {
        return Vector2D(x * scalar, y * scalar);
    }
    
    // Compound assignment operators
    Vector2D& operator+=(const Vector2D& v) {
        x += v.x;
        y += v.y;
        return *this;
    }
    
    // Unary operators
    Vector2D operator-() {
        return Vector2D(-x, -y);
    }
    
    // Increment/Decrement
    Vector2D& operator++() {  // Prefix
        ++x;
        ++y;
        return *this;
    }
    
    Vector2D operator++(int) {  // Postfix
        Vector2D temp = *this;
        ++(*this);
        return temp;
    }
    
    // Comparison operators
    bool operator==(const Vector2D& v) {
        return (x == v.x && y == v.y);
    }
    
    bool operator!=(const Vector2D& v) {
        return !(*this == v);
    }
    
    // Subscript operator
    double& operator[](int index) {
        if (index == 0) return x;
        return y;
    }
    
    // Stream operators
    friend ostream& operator<<(ostream& out, const Vector2D& v) {
        out << "(" << v.x << ", " << v.y << ")";
        return out;
    }
    
    friend istream& operator>>(istream& in, Vector2D& v) {
        in >> v.x >> v.y;
        return in;
    }
};

int main() {
    Vector2D v1(3, 4);
    Vector2D v2(1, 2);
    
    cout << "v1 = " << v1 << endl;
    cout << "v2 = " << v2 << endl;
    
    Vector2D v3 = v1 + v2;
    cout << "v1 + v2 = " << v3 << endl;
    
    Vector2D v4 = v1 * 2;
    cout << "v1 * 2 = " << v4 << endl;
    
    v1 += v2;
    cout << "v1 after += v2: " << v1 << endl;
    
    Vector2D v5 = -v1;
    cout << "-v1 = " << v5 << endl;
    
    ++v2;
    cout << "++v2 = " << v2 << endl;
    
    cout << "v1[0] = " << v1[0] << ", v1[1] = " << v1[1] << endl;
    
    return 0;
}

2.3 Practical Example: Fraction Class

#include <iostream>
using namespace std;

class Fraction {
private:
    int numerator;
    int denominator;
    
    // Helper function to find GCD
    int gcd(int a, int b) {
        if (b == 0) return a;
        return gcd(b, a % b);
    }
    
    // Simplify the fraction
    void simplify() {
        int g = gcd(abs(numerator), abs(denominator));
        numerator /= g;
        denominator /= g;
        
        // Keep denominator positive
        if (denominator < 0) {
            numerator = -numerator;
            denominator = -denominator;
        }
    }
    
public:
    Fraction(int num = 0, int den = 1) : numerator(num), denominator(den) {
        if (denominator == 0) {
            cout << "Error: Denominator cannot be zero!" << endl;
            denominator = 1;
        }
        simplify();
    }
    
    // Arithmetic operators
    Fraction operator+(const Fraction& f) {
        int num = numerator * f.denominator + f.numerator * denominator;
        int den = denominator * f.denominator;
        return Fraction(num, den);
    }
    
    Fraction operator-(const Fraction& f) {
        int num = numerator * f.denominator - f.numerator * denominator;
        int den = denominator * f.denominator;
        return Fraction(num, den);
    }
    
    Fraction operator*(const Fraction& f) {
        return Fraction(numerator * f.numerator, denominator * f.denominator);
    }
    
    Fraction operator/(const Fraction& f) {
        return Fraction(numerator * f.denominator, denominator * f.numerator);
    }
    
    // Comparison operators
    bool operator==(const Fraction& f) {
        return (numerator == f.numerator && denominator == f.denominator);
    }
    
    bool operator<(const Fraction& f) {
        return (numerator * f.denominator < f.numerator * denominator);
    }
    
    bool operator>(const Fraction& f) {
        return f < *this;
    }
    
    // Stream operators
    friend ostream& operator<<(ostream& out, const Fraction& f) {
        if (f.denominator == 1)
            out << f.numerator;
        else
            out << f.numerator << "/" << f.denominator;
        return out;
    }
    
    friend istream& operator>>(istream& in, Fraction& f) {
        char slash;
        in >> f.numerator >> slash >> f.denominator;
        f.simplify();
        return in;
    }
};

int main() {
    Fraction f1(1, 2);  // 1/2
    Fraction f2(3, 4);  // 3/4
    
    cout << "f1 = " << f1 << endl;
    cout << "f2 = " << f2 << endl;
    
    Fraction sum = f1 + f2;
    cout << "f1 + f2 = " << sum << endl;
    
    Fraction diff = f1 - f2;
    cout << "f1 - f2 = " << diff << endl;
    
    Fraction prod = f1 * f2;
    cout << "f1 * f2 = " << prod << endl;
    
    Fraction quot = f1 / f2;
    cout << "f1 / f2 = " << quot << endl;
    
    if (f1 < f2)
        cout << f1 << " is less than " << f2 << endl;
    else
        cout << f1 << " is not less than " << f2 << endl;
    
    return 0;
}

2.4 Stream Operator (Advanced Theory)

The stream insertion operator (<<) is typically overloaded as a friend function because it does not logically belong to the class and needs access to the class’s private data. Let’s break down why.

2.4.1 Reason 1 — The left operand is not the class

When you write:

cout << obj;

The operator’s left-hand side is cout (an ostream object), not your class.

So the operator signature must look like:

ostream& operator<<(ostream& os, const MyClass& obj);

This means:

But this free function still needs to access obj’s private data → so you typically declare it as a friend.

Example Without Friend

It becomes impossible to access private members:

class Point {
private:
    int x, y;   // private
};

ostream& operator<<(ostream& os, const Point& p) {
    os << p.x;   // ERROR — x is private
    return os;
}

Because operator<< is not a member function, it has no access to private members.


2.4.2 Reason 2 — Friend gives access to private data

To solve that, you declare it as a friend:

class Point {
private:
    int x, y;

public:
    Point(int x, int y) : x(x), y(y) {}

    friend ostream& operator<<(ostream& os, const Point& p);
};

Now the non-member function has access to private members.


2.4.3 Reason 3 — Makes syntax natural

Overloading as a friend function allows this natural C++ syntax:

cout << obj1 << obj2;

If it were a member function, you’d need to write something like:

obj << cout;   // awkward and reversed operands

This is not how streams are meant to be used.


2.4.4 Reason 4 — Works with chaining

Friend/global versions support chaining:

cout << a << b << c;

Which relies on:

return os;

So the next << works.


2.4.5 Stream Operator Summary

Reason Explanation
Left operand is ostream So cannot be a member of your class
Needs access to private data So it’s declared as friend
Natural syntax Allows cout << obj instead of reversed order
Supports chaining Returning ostream& works smoothly
Module 10 : OOP - Polymorphism

3. Runtime Polymorphism

3.1 Virtual Functions

Definition: Functions that can be overridden in derived classes and are resolved at runtime.

#include <iostream>
#include <string>
using namespace std;

class Animal {
protected:
    string name;
    
public:
    Animal(string n) : name(n) {}
    
    // Virtual function
    virtual void makeSound() {
        cout << name << " makes a sound" << endl;
    }
    
    // Non-virtual function
    void eat() {
        cout << name << " is eating" << endl;
    }
    
    virtual ~Animal() {}
};

class Dog : public Animal {
public:
    Dog(string n) : Animal(n) {}
    
    void makeSound() override {
        cout << name << " barks: Woof! Woof!" << endl;
    }
};

class Cat : public Animal {
public:
    Cat(string n) : Animal(n) {}
    
    void makeSound() override {
        cout << name << " meows: Meow! Meow!" << endl;
    }
};

int main() {
    // Runtime polymorphism with pointers
    Animal* animal1 = new Dog("Buddy");
    Animal* animal2 = new Cat("Whiskers");
    
    cout << "=== Using Pointers ===" << endl;
    animal1->makeSound();  // Calls Dog::makeSound()
    animal2->makeSound();  // Calls Cat::makeSound()
    
    animal1->eat();  // Calls Animal::eat() (non-virtual)
    animal2->eat();
    
    delete animal1;
    delete animal2;
    
    // Runtime polymorphism with references
    cout << "\n=== Using References ===" << endl;
    Dog dog("Max");
    Cat cat("Luna");
    
    Animal& ref1 = dog;
    Animal& ref2 = cat;
    
    ref1.makeSound();  // Calls Dog::makeSound()
    ref2.makeSound();  // Calls Cat::makeSound()
    
    return 0;
}

How Virtual Functions Work:

#include <iostream>
using namespace std;

class Base {
public:
    virtual void func1() { cout << "Base::func1()" << endl; }
    virtual void func2() { cout << "Base::func2()" << endl; }
    void func3() { cout << "Base::func3()" << endl; }
};

class Derived : public Base {
public:
    void func1() override { cout << "Derived::func1()" << endl; }
    // func2() not overridden - uses Base version
    void func3() { cout << "Derived::func3()" << endl; }
};

int main() {
    Base* ptr = new Derived();
    
    ptr->func1();  // Derived::func1() - virtual, overridden
    ptr->func2();  // Base::func2() - virtual, not overridden
    ptr->func3();  // Base::func3() - not virtual
    
    delete ptr;
    return 0;
}

3.2 Abstract Classes and Pure Virtual Functions

Definition: A class with at least one pure virtual function. Cannot be instantiated directly. Or else, it will return a Compile Time Error.

Syntax:

virtual return_type function_name() = 0;

Example: Payment System

#include <iostream>
#include <string>
using namespace std;

// Abstract base class
class Payment {
protected:
    double amount;
    string transactionId;
    
public:
    Payment(double amt, string id) : amount(amt), transactionId(id) {}
    
    // Pure virtual functions
    virtual void processPayment() = 0;
    virtual void displayReceipt() = 0;
    virtual string getPaymentMethod() = 0;
    
    // Concrete function
    void showAmount() {
        cout << "Amount: $" << amount << endl;
    }
    
    virtual ~Payment() {}
};

class CreditCardPayment : public Payment {
private:
    string cardNumber;
    string cvv;
    
public:
    CreditCardPayment(double amt, string id, string card, string c)
        : Payment(amt, id), cardNumber(card), cvv(c) {}
    
    void processPayment() override {
        cout << "Processing credit card payment..." << endl;
        cout << "Card: ****" << cardNumber.substr(cardNumber.length() - 4) << endl;
        cout << "Payment of $" << amount << " approved!" << endl;
    }
    
    void displayReceipt() override {
        cout << "\n=== Credit Card Receipt ===" << endl;
        cout << "Transaction ID: " << transactionId << endl;
        showAmount();
        cout << "Payment Method: " << getPaymentMethod() << endl;
        cout << "Status: Completed" << endl;
    }
    
    string getPaymentMethod() override {
        return "Credit Card";
    }
};

class PayPalPayment : public Payment {
private:
    string email;
    
public:
    PayPalPayment(double amt, string id, string e)
        : Payment(amt, id), email(e) {}
    
    void processPayment() override {
        cout << "Processing PayPal payment..." << endl;
        cout << "Account: " << email << endl;
        cout << "Payment of $" << amount << " approved!" << endl;
    }
    
    void displayReceipt() override {
        cout << "\n=== PayPal Receipt ===" << endl;
        cout << "Transaction ID: " << transactionId << endl;
        showAmount();
        cout << "PayPal Email: " << email << endl;
        cout << "Payment Method: " << getPaymentMethod() << endl;
        cout << "Status: Completed" << endl;
    }
    
    string getPaymentMethod() override {
        return "PayPal";
    }
};

class BankTransferPayment : public Payment {
private:
    string accountNumber;
    string bankName;
    
public:
    BankTransferPayment(double amt, string id, string acc, string bank)
        : Payment(amt, id), accountNumber(acc), bankName(bank) {}
    
    void processPayment() override {
        cout << "Processing bank transfer..." << endl;
        cout << "Bank: " << bankName << endl;
        cout << "Account: ****" << accountNumber.substr(accountNumber.length() - 4) << endl;
        cout << "Payment of $" << amount << " initiated!" << endl;
        cout << "Note: Transfer may take 1-3 business days" << endl;
    }
    
    void displayReceipt() override {
        cout << "\n=== Bank Transfer Receipt ===" << endl;
        cout << "Transaction ID: " << transactionId << endl;
        showAmount();
        cout << "Bank: " << bankName << endl;
        cout << "Payment Method: " << getPaymentMethod() << endl;
        cout << "Status: Pending" << endl;
    }
    
    string getPaymentMethod() override {
        return "Bank Transfer";
    }
};

// Payment processor using polymorphism
void executePayment(Payment* payment) {
    payment->processPayment();
    payment->displayReceipt();
}

int main() {
    // Cannot instantiate abstract class
    // Payment* p = new Payment(100, "TXN001");  // ERROR!
    
    Payment* payment1 = new CreditCardPayment(250.50, "TXN001", "1234567890123456", "123");
    Payment* payment2 = new PayPalPayment(89.99, "TXN002", "user@example.com");
    Payment* payment3 = new BankTransferPayment(1500.00, "TXN003", "9876543210", "Bank of America");
    
    cout << "=== Payment 1 ===" << endl;
    executePayment(payment1);
    
    cout << "\n=== Payment 2 ===" << endl;
    executePayment(payment2);
    
    cout << "\n=== Payment 3 ===" << endl;
    executePayment(payment3);
    
    delete payment1;
    delete payment2;
    delete payment3;
    
    return 0;
}

3.3 Polymorphism with Arrays

#include <iostream>
#include <vector>
#include <string>
using namespace std;

// Abstract base class
class Employee {
protected:
    string name;
    int id;
    
public:
    Employee(string n, int i) : name(n), id(i) {}
    
    virtual double calculateSalary() = 0;
    virtual void displayInfo() = 0;
    virtual string getType() = 0;
    
    string getName() { return name; }
    
    virtual ~Employee() {}
};

class FullTimeEmployee : public Employee {
private:
    double monthlySalary;
    
public:
    FullTimeEmployee(string n, int i, double salary)
        : Employee(n, i), monthlySalary(salary) {}
    
    double calculateSalary() override {
        return monthlySalary;
    }
    
    void displayInfo() override {
        cout << "Full-Time: " << name << " (ID: " << id << ")" << endl;
        cout << "Monthly Salary: $" << monthlySalary << endl;
    }
    
    string getType() override {
        return "Full-Time";
    }
};

class PartTimeEmployee : public Employee {
private:
    double hourlyRate;
    int hoursWorked;
    
public:
    PartTimeEmployee(string n, int i, double rate, int hours)
        : Employee(n, i), hourlyRate(rate), hoursWorked(hours) {}
    
    double calculateSalary() override {
        return hourlyRate * hoursWorked;
    }
    
    void displayInfo() override {
        cout << "Part-Time: " << name << " (ID: " << id << ")" << endl;
        cout << "Hourly Rate: $" << hourlyRate << ", Hours: " << hoursWorked << endl;
        cout << "Total Pay: $" << calculateSalary() << endl;
    }
    
    string getType() override {
        return "Part-Time";
    }
};

class Contractor : public Employee {
private:
    double projectFee;
    int projectsCompleted;
    
public:
    Contractor(string n, int i, double fee, int projects)
        : Employee(n, i), projectFee(fee), projectsCompleted(projects) {}
    
    double calculateSalary() override {
        return projectFee * projectsCompleted;
    }
    
    void displayInfo() override {
        cout << "Contractor: " << name << " (ID: " << id << ")" << endl;
        cout << "Project Fee: $" << projectFee << ", Projects: " << projectsCompleted << endl;
        cout << "Total Earnings: $" << calculateSalary() << endl;
    }
    
    string getType() override {
        return "Contractor";
    }
};

int main() {
    // Polymorphic array
    vector<Employee*> employees;
    
    employees.push_back(new FullTimeEmployee("Alice Johnson", 101, 5000));
    employees.push_back(new PartTimeEmployee("Bob Smith", 102, 25, 80));
    employees.push_back(new Contractor("Carol White", 103, 3000, 4));
    employees.push_back(new FullTimeEmployee("David Brown", 104, 6000));
    employees.push_back(new PartTimeEmployee("Eve Davis", 105, 30, 60));
    
    cout << "=== All Employees ===" << endl;
    double totalPayroll = 0;
    
    for (Employee* emp : employees) {
        emp->displayInfo();
        totalPayroll += emp->calculateSalary();
        cout << "-----------------------------------" << endl;
    }
    
    cout << "\nTotal Payroll: $" << totalPayroll << endl;
    
    // Count by type
    int fullTime = 0, partTime = 0, contractors = 0;
    for (Employee* emp : employees) {
        string type = emp->getType();
        if (type == "Full-Time") fullTime++;
        else if (type == "Part-Time") partTime++;
        else if (type == "Contractor") contractors++;
    }
    
    cout << "\n=== Employee Distribution ===" << endl;
    cout << "Full-Time: " << fullTime << endl;
    cout << "Part-Time: " << partTime << endl;
    cout << "Contractors: " << contractors << endl;
    
    // Cleanup
    for (Employee* emp : employees) {
        delete emp;
    }
    
    return 0;
}

3.4 The override Keyword

override is a contextual keyword in C++ (since C++11) that you place on a virtual function in a derived class to indicate your intention to override a virtual function declared in a base class. It does not change runtime semantics, but it instructs the compiler to verify that a matching virtual function exists in some base class. If no matching base virtual function is found, compilation fails.


Why override matters


Rules the compiler checks when you use override


Basic example (correct override)

#include <iostream>
using namespace std;

class Base {
public:
    virtual void speak() {
        cout << "Base speaking\n";
    }
    virtual ~Base() = default;
};

class Derived : public Base {
public:
    void speak() override {            // OK: matches Base::speak()
        cout << "Derived speaking\n";
    }
};

Derived::speak() is guaranteed by the compiler to override Base::speak(). If the signature differs, compilation fails.


Common mistakes override catches

  1. Typo in function name:
class Derived : public Base {
public:
    void speek() override { } // error: no base function to override
};
  1. Signature mismatch (parameter types, cv/ref qualifiers):
class Base {
public:
    virtual void f(int) {}
};
class Derived : public Base {
public:
    void f(double) override {} // error: signature does not match
};
  1. Ref-qualifier mismatch:
class Base {
public:
    virtual void g() & {}   // lvalue-qualified
};
class Derived : public Base {
public:
    void g() override {}    // error: missing & qualifier, not matching
};
  1. noexcept mismatch:
class Base {
public:
    virtual void h() noexcept {}
};
class Derived : public Base {
public:
    void h() override {} // error if noexcept mismatch is considered by the compiler
};

(Compilers may treat noexcept as part of the function type for override checks; using override helps catch inconsistencies.)


Covariant return types

Covariant returns are permitted when the return type in the derived override is a pointer or reference to a class derived from the base return type.

struct A { virtual ~A() = default; };
struct B : A {};

struct Base {
    virtual A* clone() { return new A; }
};

struct Derived : Base {
    B* clone() override { return new B; } // OK: covariant return type
};

override still applies; the compiler checks covariance rules.


override with final

You can combine override with final to both override a base virtual function and prevent further overrides in later derived classes.

struct Base {
    virtual void foo();
};

struct A : Base {
    void foo() override final; // overrides, and forbids further overrides
};

struct B : A {
    void foo() override; // error: foo() is final in A
};

When to use override — best practices


Interaction with pure virtual functions and abstract classes

override works with pure virtual functions as well:

struct Interface {
    virtual void op() = 0;
};

struct Impl : Interface {
    void op() override { /* implementation */ } // required to make Impl concrete
};

If a derived class fails to override a pure virtual function, the derived class remains abstract. Marking an implementation with override ensures you intended to implement that pure virtual function.


3.5 Virtual Destructors

Why Virtual Destructors are Important:

#include <iostream>
using namespace std;

// WITHOUT virtual destructor (WRONG)
class BaseWrong {
public:
    BaseWrong() { cout << "Base constructed" << endl; }
    ~BaseWrong() { cout << "Base destructed" << endl; }
};

class DerivedWrong : public BaseWrong {
private:
    int* data;
public:
    DerivedWrong() {
        data = new int[100];
        cout << "Derived constructed" << endl;
    }
    ~DerivedWrong() {
        delete[] data;
        cout << "Derived destructed" << endl;
    }
};

// WITH virtual destructor (CORRECT)
class BaseCorrect {
public:
    BaseCorrect() { cout << "Base constructed" << endl; }
    virtual ~BaseCorrect() { cout << "Base destructed" << endl; }
};

class DerivedCorrect : public BaseCorrect {
private:
    int* data;
public:
    DerivedCorrect() {
        data = new int[100];
        cout << "Derived constructed" << endl;
    }
    ~DerivedCorrect() override {
        delete[] data;
        cout << "Derived destructed" << endl;
    }
};

int main() {
    cout << "=== WITHOUT Virtual Destructor (MEMORY LEAK!) ===" << endl;
    BaseWrong* ptr1 = new DerivedWrong();
    delete ptr1;  // Only Base destructor called!
    
    cout << "\n=== WITH Virtual Destructor (CORRECT) ===" << endl;
    BaseCorrect* ptr2 = new DerivedCorrect();
    delete ptr2;  // Both destructors called!
    
    return 0;
}
Module 10 : OOP - Polymorphism

4. Practical Applications

4.1 Complete Example: Drawing Application

#include <iostream>
#include <vector>
#include <string>
#include <cmath>
using namespace std;

// Abstract base class
class Shape {
protected:
    string color;
    double x, y;  // Position
    
public:
    Shape(string c, double px, double py) : color(c), x(px), y(py) {}
    
    // Pure virtual functions
    virtual double getArea() = 0;
    virtual double getPerimeter() = 0;
    virtual void draw() = 0;
    virtual string getType() = 0;
    
    // Concrete functions
    void move(double dx, double dy) {
        x += dx;
        y += dy;
        cout << getType() << " moved to (" << x << ", " << y << ")" << endl;
    }
    
    void setColor(string c) {
        color = c;
        cout << getType() << " color changed to " << color << endl;
    }
    
    string getColor() { return color; }
    
    virtual void displayInfo() {
        cout << getType() << " at (" << x << ", " << y << ")" << endl;
        cout << "Color: " << color << endl;
        cout << "Area: " << getArea() << endl;
        cout << "Perimeter: " << getPerimeter() << endl;
    }
    
    virtual ~Shape() {}
};

class Circle : public Shape {
private:
    double radius;
    
public:
    Circle(string c, double px, double py, double r)
        : Shape(c, px, py), radius(r) {}
    
    double getArea() override {
        return 3.14159 * radius * radius;
    }
    
    double getPerimeter() override {
        return 2 * 3.14159 * radius;
    }
    
    void draw() override {
        cout << "Drawing " << color << " circle at (" << x << ", " << y 
             << ") with radius " << radius << endl;
    }
    
    string getType() override {
        return "Circle";
    }
    
    void displayInfo() override {
        Shape::displayInfo();
        cout << "Radius: " << radius << endl;
    }
};

class Rectangle : public Shape {
private:
    double width, height;
    
public:
    Rectangle(string c, double px, double py, double w, double h)
        : Shape(c, px, py), width(w), height(h) {}
    
    double getArea() override {
        return width * height;
    }
    
    double getPerimeter() override {
        return 2 * (width + height);
    }
    
    void draw() override {
        cout << "Drawing " << color << " rectangle at (" << x << ", " << y 
             << ") with width " << width << " and height " << height << endl;
    }
    
    string getType() override {
        return "Rectangle";
    }
    
    void displayInfo() override {
        Shape::displayInfo();
        cout << "Width: " << width << ", Height: " << height << endl;
    }
};

class Triangle : public Shape {
private:
    double base, height;
    
public:
    Triangle(string c, double px, double py, double b, double h)
        : Shape(c, px, py), base(b), height(h) {}
    
    double getArea() override {
        return 0.5 * base * height;
    }
    
    double getPerimeter() override {
        // Simplified: assumes isosceles triangle
        double side = sqrt((base/2)*(base/2) + height*height);
        return base + 2*side;
    }
    
    void draw() override {
        cout << "Drawing " << color << " triangle at (" << x << ", " << y 
             << ") with base " << base << " and height " << height << endl;
    }
    
    string getType() override {
        return "Triangle";
    }
    
    void displayInfo() override {
        Shape::displayInfo();
        cout << "Base: " << base << ", Height: " << height << endl;
    }
};

// Canvas class to manage shapes
class Canvas {
private:
    vector<Shape*> shapes;
    
public:
    void addShape(Shape* shape) {
        shapes.push_back(shape);
        cout << shape->getType() << " added to canvas" << endl;
    }
    
    void drawAll() {
        cout << "\n=== Drawing All Shapes ===" << endl;
        for (Shape* shape : shapes) {
            shape->draw();
        }
    }
    
    void displayAllInfo() {
        cout << "\n=== All Shapes Information ===" << endl;
        for (size_t i = 0; i < shapes.size(); i++) {
            cout << "\nShape " << (i+1) << ":" << endl;
            shapes[i]->displayInfo();
            cout << "-----------------------------------" << endl;
        }
    }
    
    double getTotalArea() {
        double total = 0;
        for (Shape* shape : shapes) {
            total += shape->getArea();
        }
        return total;
    }
    
    void removeShape(int index) {
        if (index >= 0 && index < shapes.size()) {
            delete shapes[index];
            shapes.erase(shapes.begin() + index);
            cout << "Shape removed" << endl;
        }
    }
    
    ~Canvas() {
        for (Shape* shape : shapes) {
            delete shape;
        }
    }
};

int main() {
    Canvas canvas;
    
    cout << "=== Creating Shapes ===" << endl;
    canvas.addShape(new Circle("Red", 10, 10, 5));
    canvas.addShape(new Rectangle("Blue", 20, 20, 10, 8));
    canvas.addShape(new Triangle("Green", 30, 30, 6, 4));
    canvas.addShape(new Circle("Yellow", 40, 40, 7));
    
    canvas.drawAll();
    canvas.displayAllInfo();
    
    cout << "\nTotal area of all shapes: " << canvas.getTotalArea() << endl;
    
    return 0;
}

4.2 Example: Game Character System

#include <iostream>
#include <vector>
#include <string>
using namespace std;

// Abstract base class
class GameCharacter {
protected:
    string name;
    int health;
    int maxHealth;
    int attackPower;
    
public:
    GameCharacter(string n, int hp, int atk)
        : name(n), health(hp), maxHealth(hp), attackPower(atk) {}
    
    // Pure virtual functions
    virtual void attack(GameCharacter* target) = 0;
    virtual void specialAbility() = 0;
    virtual string getClass() = 0;
    
    // Concrete functions
    void takeDamage(int damage) {
        health -= damage;
        if (health < 0) health = 0;
        cout << name << " takes " << damage << " damage! Health: " << health << endl;
        
        if (health == 0) {
            cout << name << " has been defeated!" << endl;
        }
    }
    
    void heal(int amount) {
        health += amount;
        if (health > maxHealth) health = maxHealth;
        cout << name << " heals " << amount << " HP! Health: " << health << endl;
    }
    
    bool isAlive() {
        return health > 0;
    }
    
    void displayStatus() {
        cout << name << " (" << getClass() << ")" << endl;
        cout << "Health: " << health << "/" << maxHealth << endl;
        cout << "Attack Power: " << attackPower << endl;
    }
    
    string getName() { return name; }
    int getAttackPower() { return attackPower; }
    
    virtual ~GameCharacter() {}
};

class Warrior : public GameCharacter {
private:
    int armor;
    
public:
    Warrior(string n) : GameCharacter(n, 150, 25), armor(10) {}
    
    void attack(GameCharacter* target) override {
        cout << name << " swings sword at " << target->getName() << "!" << endl;
        target->takeDamage(attackPower);
    }
    
    void specialAbility() override {
        cout << name << " uses Shield Bash!" << endl;
        cout << "Defense increased temporarily!" << endl;
        armor += 5;
    }
    
    string getClass() override {
        return "Warrior";
    }
    
    void takeDamage(int damage) {
        int reducedDamage = damage - armor;
        if (reducedDamage < 0) reducedDamage = 0;
        cout << name << "'s armor blocks " << armor << " damage!" << endl;
        GameCharacter::takeDamage(reducedDamage);
    }
};

class Mage : public GameCharacter {
private:
    int mana;
    
public:
    Mage(string n) : GameCharacter(n, 80, 35), mana(100) {}
    
    void attack(GameCharacter* target) override {
        if (mana >= 10) {
            cout << name << " casts Fireball at " << target->getName() << "!" << endl;
            target->takeDamage(attackPower);
            mana -= 10;
        } else {
            cout << name << " is out of mana!" << endl;
        }
    }
    
    void specialAbility() override {
        if (mana >= 30) {
            cout << name << " casts Meteor Storm!" << endl;
            cout << "Massive area damage!" << endl;
            mana -= 30;
        } else {
            cout << name << " doesn't have enough mana!" << endl;
        }
    }
    
    string getClass() override {
        return "Mage";
    }
    
    void displayStatus() {
        GameCharacter::displayStatus();
        cout << "Mana: " << mana << endl;
    }
};

class Archer : public GameCharacter {
private:
    int arrows;
    
public:
    Archer(string n) : GameCharacter(n, 100, 30), arrows(50) {}
    
    void attack(GameCharacter* target) override {
        if (arrows > 0) {
            cout << name << " shoots arrow at " << target->getName() << "!" << endl;
            target->takeDamage(attackPower);
            arrows--;
        } else {
            cout << name << " is out of arrows!" << endl;
        }
    }
    
    void specialAbility() override {
        if (arrows >= 5) {
            cout << name << " uses Multi-Shot!" << endl;
            cout << "Fires multiple arrows!" << endl;
            arrows -= 5;
        } else {
            cout << name << " doesn't have enough arrows!" << endl;
        }
    }
    
    string getClass() override {
        return "Archer";
    }
    
    void displayStatus() {
        GameCharacter::displayStatus();
        cout << "Arrows: " << arrows << endl;
    }
};

int main() {
    cout << "=== Character Creation ===" << endl;
    vector<GameCharacter*> party;
    
    party.push_back(new Warrior("Thorin"));
    party.push_back(new Mage("Gandalf"));
    party.push_back(new Archer("Legolas"));
    
    cout << "\n=== Party Status ===" << endl;
    for (GameCharacter* character : party) {
        character->displayStatus();
        cout << endl;
    }
    
    cout << "=== Battle Simulation ===" << endl;
    GameCharacter* enemy = new Warrior("Orc");
    cout << "\nEnemy appears: ";
    enemy->displayStatus();
    
    cout << "\n--- Round 1 ---" << endl;
    party[0]->attack(enemy);
    party[1]->attack(enemy);
    party[2]->attack(enemy);
    
    cout << "\n--- Round 2 ---" << endl;
    party[0]->specialAbility();
    party[1]->specialAbility();
    party[2]->specialAbility();
    
    // Cleanup
    for (GameCharacter* character : party) {
        delete character;
    }
    delete enemy;
    
    return 0;
}

4.3 Best Practices

1. Always Use Virtual Destructors in Base Classes

class Base {
public:
    virtual ~Base() {}  // CRITICAL!
};

2. Use override Keyword

class Derived : public Base {
public:
    void func() override {  // Compiler checks
        // implementation
    }
};

3. Use Abstract Classes for Interfaces

class IDrawable {
public:
    virtual void draw() = 0;
    virtual ~IDrawable() {}
};

4. Prefer References or Pointers for Polymorphism

// GOOD: Using pointer or reference
void process(Shape* shape) {
    shape->draw();
}

void process(Shape& shape) {
    shape.draw();
}

// BAD: Pass by value causes slicing
void process(Shape shape) {  // DON'T DO THIS
    shape.draw();
}

5. Check for Null Pointers

Shape* shape = findShape(id);
if (shape != nullptr) {
    shape->draw();
}