FAC1003 Tutorial 14 — Solution Strategies

Core theme: This tutorial covers the four pillars of OOP (Encapsulation, Abstraction, Inheritance, Polymorphism) and their implementation in C++. The programming questions build from simple class design up to polymorphic class hierarchies.


Concepts (Questions 1-8)

1) OOP vs Procedural Programming

OOP (Object-Oriented Programming) is a paradigm that organizes code around objects (bundles of data + behavior), while procedural programming organizes code around functions that operate on separate data.

Aspect OOP Procedural
Organization Objects (data + methods) Functions
Data access Encapsulated (private) Global/shared
Reuse Inheritance, composition Function libraries
Real-world mapping Direct (objects model entities) Indirect

Why it works: OOP models real-world entities as objects with attributes (data) and behaviors (methods). This makes large programs more maintainable because changes to one object's internal implementation don't ripple through the codebase — as long as the interface stays the same.


2) Encapsulation

Encapsulation bundles data and methods that operate on that data within a single unit (class), hiding internal state and requiring all interaction to go through public member functions.

C++ example:

class BankAccount {
private:
    double balance;  // Hidden from outside

public:
    BankAccount(double initial) : balance(initial) {}

    void deposit(double amount) {
        if (amount > 0) balance += amount;
    }

    double getBalance() const {
        return balance;  // Read-only access
    }
};

Why it works: The private: label prevents direct modification of balance. You can't accidentally do account.balance = -1000 — you must use deposit(), which includes validation. This protects invariants (rules that must always be true, like "balance must never go negative due to deposits").


3) Abstraction

Abstraction means exposing only essential features while hiding implementation details. The user of a class interacts with a simplified interface without needing to understand how it works internally.

Why it's useful:

  • Reduces complexity: A car driver uses the steering wheel, not the engine cylinders
  • Enables change: You can rewrite the internals without affecting users
  • Hides distractions: Users see only what they need

Real-world analogy: A TV remote — buttons for power, volume, channel. You don't need to know about the circuit board inside.


4) Inheritance vs Polymorphism

Concept What it is C++ mechanism
Inheritance A class (derived) gets properties from another class (base) class Dog : public Animal {}
Polymorphism The same interface behaves differently based on the actual object type Virtual functions, function overloading

Why the distinction matters:

  • Inheritance is about code reuse ("is-a" relationship)
  • Polymorphism is about runtime flexibility (treating different types uniformly through a base pointer)

Example: Shape* s = new Circle(); s->area(); — the correct Circle::area() runs even though the pointer type is Shape*. This is runtime polymorphism via virtual functions.


5) Four Pillars of OOP

Pillar Description Example
Encapsulation Bundle data + methods, hide internals private: members with public getters/setters
Abstraction Show only essential features Pure virtual functions define an interface
Inheritance Derive new classes from existing ones class Dog : public Animal {}
Polymorphism Same interface, different implementations Virtual functions, function/operator overloading

Why these four: They work together — Encapsulation protects data, Abstraction simplifies interfaces, Inheritance enables code reuse, and Polymorphism enables flexible, extensible designs. Missing any one makes the other three less effective.


6) Class vs Object

Class Object
Definition Blueprint / template Concrete instance
Memory No memory allocated when defined Memory allocated when created
Creation Written once in code Created at runtime
Analogy A cookie cutter A cookie
Analogy 2 A house blueprint The actual house built from it

C++:

class Car {         // Class = blueprint
    string model;
    int year;
};

Car myCar;          // Object = actual car instance

7) Constructor and Destructor

Constructor Destructor
When called When object is created When object goes out of scope / is deleted
Purpose Initialize data members Clean up resources (heap memory, file handles)
Name Same as class name Same as class name with ~ prefix
Return type None (not even void) None
Can have parameters? Yes No
class File {
    FILE* fp;
public:
    File(const char* name) { fp = fopen(name, "r"); }   // Constructor
    ~File() { if(fp) fclose(fp); }                       // Destructor
};

Why destructors matter: C++ doesn't have garbage collection. If a constructor allocates memory or acquires a resource, the destructor must release it — otherwise you get memory leaks.


8) Three Types of Constructors

Type Syntax When used
Default ClassName(); ClassName obj;
Parameterized ClassName(int x); ClassName obj(5);
Copy ClassName(const ClassName& other); ClassName obj2 = obj1; or pass-by-value
class Point {
    int x, y;
public:
    Point() : x(0), y(0) {}                       // Default
    Point(int a, int b) : x(a), y(b) {}            // Parameterized
    Point(const Point& p) : x(p.x), y(p.y) {}      // Copy
};

Why three? Each serves a purpose:

  • Default: Creates an object in a known initial state
  • Parameterized: Creates an object with specific values
  • Copy: Creates a faithful duplicate (critical for pass-by-value and returning objects)

Code Tracing (Questions 9-10)

9) Simple Inheritance

class Animal {
public:
    void speak() { cout << "Animal speaks" << endl; }
};

class Dog : public Animal {};

int main() {
    Dog d;
    d.speak();
    return 0;
}

Output:

Animal speaks

Why it works: Dog publicly inherits from Animal. Even though Dog has no members of its own, it inherits all public members of Animal — including speak(). This demonstrates the simplest form of code reuse through inheritance.

The call d.speak() resolves at compile time (not runtime polymorphism — no virtual keyword used).


10) Constructor and Destructor

class A {
public:
    A() { cout << "Constructor"; }
    ~A() { cout << "Destructor"; }
};

int main() {
    A obj;
    return 0;
}

Output:

ConstructorDestructor

Why it works: When obj is created in main():

  1. Constructor A() runs → prints "Constructor"
  2. return 0 ends main(), obj goes out of scope
  3. Destructor ~A() runs automatically → prints "Destructor"

Key insight: Destructors are called automatically when objects go out of scope. This is RAII (Resource Acquisition Is Initialization) — resources are cleaned up automatically, preventing leaks.


Applications (Questions 11-13)

11) Rectangle Class

Concepts tested: Encapsulation (private members), parameterized constructors, destructor, member functions.

#include <iostream>
using namespace std;

class Rectangle {
private:
    double length;
    double width;

public:
    // Parameterized constructor
    Rectangle(double l, double w) : length(l), width(w) {
        cout << "Rectangle created: " << length << " x " << width << endl;
    }

    // Destructor
    ~Rectangle() {
        cout << "Rectangle destroyed" << endl;
    }

    double area() const { return length * width; }
    double perimeter() const { return 2 * (length + width); }
};

int main() {
    Rectangle r1(5.0, 3.0);
    Rectangle r2(7.5, 4.2);

    cout << "Rectangle 1 — Area: " << r1.area()
         << ", Perimeter: " << r1.perimeter() << endl;

    cout << "Rectangle 2 — Area: " << r2.area()
         << ", Perimeter: " << r2.perimeter() << endl;

    return 0;
    // Destructors called automatically here
}

Why const on member functions: Marking area() and perimeter() as const tells the compiler these functions don't modify the object. This is good practice — it allows calling them on const Rectangle objects and signals intent.


12) Student Class

Concepts tested: Encapsulation with getters/setters, arrays of objects, static/class-level operations.

#include <iostream>
#include <string>
using namespace std;

class Student {
private:
    string name;
    int rollNumber;
    double marks;

public:
    // Setters
    void setName(const string& n) { name = n; }
    void setRollNumber(int r) { rollNumber = r; }
    void setMarks(double m) { marks = m; }

    // Getters
    string getName() const { return name; }
    int getRollNumber() const { return rollNumber; }
    double getMarks() const { return marks; }

    // Static method to calculate average
    static double calculateAverage(Student students[], int count) {
        double sum = 0;
        for (int i = 0; i < count; i++) {
            sum += students[i].marks;
        }
        return sum / count;
    }
};

int main() {
    const int NUM_STUDENTS = 3;
    Student students[NUM_STUDENTS];

    // Set values
    students[0].setName("Alice");
    students[0].setRollNumber(101);
    students[0].setMarks(85.5);

    students[1].setName("Bob");
    students[1].setRollNumber(102);
    students[1].setMarks(92.0);

    students[2].setName("Charlie");
    students[2].setRollNumber(103);
    students[2].setMarks(78.3);

    // Display average
    double avg = Student::calculateAverage(students, NUM_STUDENTS);
    cout << "Average marks: " << avg << endl;

    return 0;
}

Why static methods: calculateAverage is static because it operates on the class level — it takes an array of students and computes something, rather than acting on a single student's data. It's called as Student::calculateAverage(...) rather than student.calculateAverage(...).


13) Shape Hierarchy (Polymorphism)

Concepts tested: Pure virtual functions, abstract base classes, runtime polymorphism via virtual functions, array of base pointers.

#include <iostream>
#include <cmath>
using namespace std;

// Abstract base class
class Shape {
public:
    virtual double area() const = 0;       // Pure virtual
    virtual double perimeter() const = 0;  // Pure virtual
    virtual ~Shape() {}                    // Virtual destructor for cleanup
};

class Rectangle : public Shape {
private:
    double length, width;
public:
    Rectangle(double l, double w) : length(l), width(w) {}
    double area() const override { return length * width; }
    double perimeter() const override { return 2 * (length + width); }
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    double area() const override { return M_PI * radius * radius; }
    double perimeter() const override { return 2 * M_PI * radius; }
};

int main() {
    // Array of Shape pointers
    Shape* shapes[4];

    shapes[0] = new Rectangle(5.0, 3.0);
    shapes[1] = new Circle(4.0);
    shapes[2] = new Rectangle(7.5, 4.2);
    shapes[3] = new Circle(2.5);

    for (int i = 0; i < 4; i++) {
        cout << "Shape " << i+1
             << " — Area: " << shapes[i]->area()
             << ", Perimeter: " << shapes[i]->perimeter() << endl;
    }

    // Clean up
    for (int i = 0; i < 4; i++) {
        delete shapes[i];
    }

    return 0;
}

Why this is the key OOP pattern:

  1. = 0 makes the functions pure virtual, making Shape an abstract class — you cannot instantiate Shape directly. It serves only as an interface.

  2. override (C++11) tells the compiler: "I intend to override a virtual function." If the base class signature changes, the compiler catches it.

  3. Shape* shapes[4] is an array of base class pointers, but each points to a different derived type (Rectangle or Circle). When you call shapes[i]->area(), C++ uses the vtable (virtual table) to dispatch to the correct function at runtime.

  4. Virtual destructor: ~Shape() {} is virtual so that delete shapes[i] correctly calls the derived class destructor (e.g., ~Rectangle()) before the base destructor. Without it, deleting through a base pointer is undefined behavior.

Runtime dispatch (how polymorphism works internally):

shapes[0] → Rectangle object
               ┌──────────────┐
               │ vtable ptr → │──→ Rectangle::area()
               │ length=5.0   │    Rectangle::perimeter()
               │ width=3.0    │
               └──────────────┘

shapes[1] → Circle object
               ┌──────────────┐
               │ vtable ptr → │──→ Circle::area()
               │ radius=4.0   │    Circle::perimeter()
               └──────────────┘

Key Takeaways

Concept C++ Mechanism When to Use
Encapsulation private: members + public methods Always — protect internal state
Abstraction Pure virtual functions (= 0) Define interfaces that multiple classes implement
Inheritance class Derived : public Base "Is-a" relationships; code reuse
Polymorphism virtual functions + base pointers Treat different types uniformly
RAII Constructor acquires, destructor releases Manage resources safely
Static methods static keyword Operations on the class, not an instance

Related