Chapter 07

Object-Oriented Programming

Designing modular, reusable, and maintainable code with classes and objects

What You Will Learn

  • Core OOP concepts: encapsulation, inheritance, polymorphism
  • How to create classes and objects
  • Constructors, destructors, and the Rule of Three/Five
  • Access modifiers and data hiding
  • Inheritance hierarchies and virtual functions
  • Abstract classes and interfaces
  • Operator overloading
  • Static members and friend functions
  • SOLID principles for good design
  • Introduction to common design patterns

OOP Core Concepts

Object-Oriented Programming organizes code around objects that contain data and behavior.

Encapsulation

Bundling data and methods together, hiding implementation details.

Inheritance

Creating new classes based on existing ones, promoting code reuse.

Polymorphism

Objects of different types responding to the same interface.

Abstraction

Exposing essential features while hiding complexity.

Classes and Objects

A class is a blueprint; an object is an instance of that blueprint.

Defining a Class

class Person {
public:
    // Member variables (attributes)
    std::string name;
    int age;
    
    // Member functions (methods)
    void introduce() {
        std::cout << "Hi, I'm " << name 
                  << ", " << age << " years old.\n";
    }
};

// Creating objects
Person alice;
alice.name = "Alice";
alice.age = 30;
alice.introduce();  // Hi, I'm Alice, 30 years old.

Class vs Struct

// The only difference: default access
class MyClass {
    int x;  // private by default
public:
    int y;
};

struct MyStruct {
    int x;  // public by default
private:
    int y;
};

// Convention:
// - struct: simple data containers (POD)
// - class: complex types with behavior

Member Functions

class Rectangle {
public:
    double width, height;
    
    // Method defined inside class (implicitly inline)
    double area() {
        return width * height;
    }
    
    // Method declared in class, defined outside
    double perimeter();
};

// Definition outside class
double Rectangle::perimeter() {
    return 2 * (width + height);
}

The this Pointer

class Counter {
private:
    int count;
public:
    void setCount(int count) {
        this->count = count;  // this-> distinguishes member from parameter
    }
    
    // Method chaining
    Counter& increment() {
        count++;
        return *this;  // Return self for chaining
    }
};

Counter c;
c.setCount(5).increment().increment();  // c.count = 7

Constructors

Special functions called when an object is created.

Default Constructor

class Point {
public:
    int x, y;
    
    // Default constructor
    Point() {
        x = 0;
        y = 0;
    }
};

Point p;  // Calls default constructor
// p.x = 0, p.y = 0

Parameterized Constructor

class Point {
public:
    int x, y;
    
    Point(int x, int y) {
        this->x = x;
        this->y = y;
    }
};

Point p(10, 20);  // x = 10, y = 20

Member Initializer List

class Point {
public:
    int x, y;
    const int id;  // Must be initialized in initializer list
    
    // Preferred: initializer list
    Point(int x, int y, int id) : x(x), y(y), id(id) {
        // Constructor body (optional)
    }
};

// Required for:
// - const members
// - reference members
// - base class initialization
// - members without default constructor

Constructor Delegation (C++11)

class Rectangle {
public:
    double width, height;
    
    Rectangle() : Rectangle(1.0, 1.0) {}  // Delegates
    
    Rectangle(double size) : Rectangle(size, size) {}  // Delegates
    
    Rectangle(double w, double h) : width(w), height(h) {}  // Main
};

Copy Constructor

class String {
private:
    char* data;
    size_t length;
    
public:
    // Copy constructor
    String(const String& other) {
        length = other.length;
        data = new char[length + 1];
        strcpy(data, other.data);
    }
};

String s1("Hello");
String s2 = s1;    // Copy constructor called
String s3(s1);     // Also copy constructor

Move Constructor (C++11)

class String {
public:
    // Move constructor
    String(String&& other) noexcept {
        data = other.data;
        length = other.length;
        other.data = nullptr;  // Leave source in valid state
        other.length = 0;
    }
};

String s1("Hello");
String s2 = std::move(s1);  // Move constructor, s1 is now empty

Default and Delete (C++11)

class NonCopyable {
public:
    NonCopyable() = default;  // Use compiler-generated
    
    NonCopyable(const NonCopyable&) = delete;  // Disable copy
    NonCopyable& operator=(const NonCopyable&) = delete;
};

NonCopyable a;
// NonCopyable b = a;  // Error: copy deleted

Destructors

Called when an object is destroyed. Used for cleanup.

class FileHandler {
private:
    FILE* file;
    
public:
    FileHandler(const char* filename) {
        file = fopen(filename, "r");
    }
    
    // Destructor
    ~FileHandler() {
        if (file) {
            fclose(file);
            std::cout << "File closed\n";
        }
    }
};

void process() {
    FileHandler fh("data.txt");
    // Use file...
}  // Destructor called here automatically

When Destructors Run

  • Local objects: when going out of scope
  • Dynamic objects: when delete is called
  • Static objects: at program end
  • Temporary objects: at end of expression

Rule of Three

If you define any of these, you should define all three:

class Resource {
private:
    int* data;
    
public:
    // 1. Destructor
    ~Resource() { delete[] data; }
    
    // 2. Copy constructor
    Resource(const Resource& other) {
        data = new int[100];
        std::copy(other.data, other.data + 100, data);
    }
    
    // 3. Copy assignment operator
    Resource& operator=(const Resource& other) {
        if (this != &other) {
            delete[] data;
            data = new int[100];
            std::copy(other.data, other.data + 100, data);
        }
        return *this;
    }
};

Rule of Five (C++11)

Also add move constructor and move assignment:

class Resource {
public:
    // ... Three from above, plus:
    
    // 4. Move constructor
    Resource(Resource&& other) noexcept : data(other.data) {
        other.data = nullptr;
    }
    
    // 5. Move assignment operator
    Resource& operator=(Resource&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            other.data = nullptr;
        }
        return *this;
    }
};

Rule of Zero

Best practice: use smart pointers and standard containers to avoid manual resource management.

class Modern {
private:
    std::unique_ptr<int[]> data;
    std::vector<std::string> names;
    
public:
    // Compiler-generated defaults work correctly!
    // No custom destructor, copy, or move needed
};

Access Modifiers

class MyClass {
public:      // Accessible from anywhere
    int publicVar;
    
protected:   // Accessible from class and derived classes
    int protectedVar;
    
private:     // Accessible only from within class
    int privateVar;
};

Access in Inheritance

class Base {
public:    int a;
protected: int b;
private:   int c;
};

// Public inheritance (most common)
class Derived1 : public Base {
    // a is public, b is protected, c is inaccessible
};

// Protected inheritance
class Derived2 : protected Base {
    // a is protected, b is protected, c is inaccessible
};

// Private inheritance (rare)
class Derived3 : private Base {
    // a is private, b is private, c is inaccessible
};

Encapsulation

Hide internal state and require all interaction through methods.

class BankAccount {
private:
    double balance;  // Hidden state
    std::string accountNumber;
    
public:
    BankAccount(std::string num) : accountNumber(num), balance(0) {}
    
    // Getters (accessors)
    double getBalance() const {
        return balance;
    }
    
    std::string getAccountNumber() const {
        return accountNumber;
    }
    
    // Setters (mutators) with validation
    void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }
    
    bool withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            return true;
        }
        return false;
    }
};

BankAccount acc("123456");
acc.deposit(1000);
// acc.balance = 999999;  // Error: private!
acc.withdraw(500);
std::cout << acc.getBalance();  // 500

Benefits of Encapsulation

  • Control over data (validation in setters)
  • Flexibility to change implementation
  • Reduced complexity for users
  • Better maintainability

Inheritance

Create new classes based on existing ones.

// Base class (parent)
class Animal {
protected:
    std::string name;
    
public:
    Animal(std::string n) : name(n) {}
    
    void eat() {
        std::cout << name << " is eating.\n";
    }
};

// Derived class (child)
class Dog : public Animal {
public:
    Dog(std::string n) : Animal(n) {}  // Call base constructor
    
    void bark() {
        std::cout << name << " says Woof!\n";
    }
};

Dog dog("Buddy");
dog.eat();   // Inherited from Animal
dog.bark();  // Defined in Dog

Constructor/Destructor Order

class Base {
public:
    Base()  { std::cout << "Base constructed\n"; }
    ~Base() { std::cout << "Base destroyed\n"; }
};

class Derived : public Base {
public:
    Derived()  { std::cout << "Derived constructed\n"; }
    ~Derived() { std::cout << "Derived destroyed\n"; }
};

Derived d;
// Output:
// Base constructed
// Derived constructed
// Derived destroyed
// Base destroyed

Multiple Inheritance

class Flyable {
public:
    void fly() { std::cout << "Flying\n"; }
};

class Swimmable {
public:
    void swim() { std::cout << "Swimming\n"; }
};

class Duck : public Flyable, public Swimmable {
public:
    void quack() { std::cout << "Quack!\n"; }
};

Duck d;
d.fly();   // From Flyable
d.swim();  // From Swimmable
d.quack(); // From Duck

Diamond Problem

class Animal { public: int age; };
class Bird : public Animal {};
class Fish : public Animal {};

// Diamond inheritance
class FlyingFish : public Bird, public Fish {
    // Has TWO copies of Animal!
    // bird.age and fish.age are different
};

// Solution: Virtual inheritance
class Bird : virtual public Animal {};
class Fish : virtual public Animal {};
class FlyingFish : public Bird, public Fish {
    // Only ONE copy of Animal
};

Polymorphism

Objects of different types respond to the same interface.

Virtual Functions

class Shape {
public:
    virtual double area() const {
        return 0;
    }
    
    virtual ~Shape() {}  // Virtual destructor!
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    
    double area() const override {  // override keyword (C++11)
        return 3.14159 * radius * radius;
    }
};

class Rectangle : public Shape {
private:
    double width, height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    
    double area() const override {
        return width * height;
    }
};

// Polymorphic behavior
void printArea(const Shape& shape) {
    std::cout << "Area: " << shape.area() << "\n";
}

Circle c(5);
Rectangle r(4, 6);
printArea(c);  // Area: 78.5398
printArea(r);  // Area: 24

Virtual Destructor

class Base {
public:
    virtual ~Base() {
        std::cout << "Base destroyed\n";
    }
};

class Derived : public Base {
private:
    int* data;
public:
    Derived() : data(new int[100]) {}
    ~Derived() override {
        delete[] data;
        std::cout << "Derived destroyed\n";
    }
};

// Without virtual destructor, this would leak memory!
Base* ptr = new Derived();
delete ptr;  // Calls both destructors correctly

override and final (C++11)

class Base {
public:
    virtual void foo() {}
    virtual void bar() final {}  // Can't override
};

class Derived : public Base {
public:
    void foo() override {}       // Correctly overrides
    // void foo(int) override {} // Error: doesn't match base
    // void bar() override {}    // Error: bar is final
};

class Final final : public Base {
    // This class can't be inherited from
};

Abstract Classes and Interfaces

Abstract classes have at least one pure virtual function and cannot be instantiated.

class Shape {
public:
    // Pure virtual function (= 0)
    virtual double area() const = 0;
    virtual double perimeter() const = 0;
    
    virtual ~Shape() = default;
};

// Shape s;  // Error: can't instantiate abstract class

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

Interface (Pure Abstract Class)

// Interface: all pure virtual, no data
class Drawable {
public:
    virtual void draw() const = 0;
    virtual ~Drawable() = default;
};

class Printable {
public:
    virtual void print() const = 0;
    virtual ~Printable() = default;
};

// Implement multiple interfaces
class Document : public Drawable, public Printable {
public:
    void draw() const override {
        std::cout << "Drawing document\n";
    }
    
    void print() const override {
        std::cout << "Printing document\n";
    }
};

Operator Overloading

Define custom behavior for operators with your classes.

Arithmetic Operators

class Vector2D {
public:
    double x, y;
    
    Vector2D(double x = 0, double y = 0) : x(x), y(y) {}
    
    // Addition
    Vector2D operator+(const Vector2D& other) const {
        return Vector2D(x + other.x, y + other.y);
    }
    
    // Subtraction
    Vector2D operator-(const Vector2D& other) const {
        return Vector2D(x - other.x, y - other.y);
    }
    
    // Scalar multiplication
    Vector2D operator*(double scalar) const {
        return Vector2D(x * scalar, y * scalar);
    }
};

Vector2D a(1, 2), b(3, 4);
Vector2D c = a + b;  // (4, 6)
Vector2D d = a * 2;  // (2, 4)

Comparison Operators

class Point {
public:
    int x, y;
    
    bool operator==(const Point& other) const {
        return x == other.x && y == other.y;
    }
    
    bool operator!=(const Point& other) const {
        return !(*this == other);
    }
    
    bool operator<(const Point& other) const {
        if (x != other.x) return x < other.x;
        return y < other.y;
    }
    
    // C++20: spaceship operator
    auto operator<=>(const Point&) const = default;
};

Stream Operators

class Person {
public:
    std::string name;
    int age;
    
    // Output operator (friend function)
    friend std::ostream& operator<<(std::ostream& os, const Person& p) {
        os << p.name << " (" << p.age << ")";
        return os;
    }
    
    // Input operator
    friend std::istream& operator>>(std::istream& is, Person& p) {
        is >> p.name >> p.age;
        return is;
    }
};

Person p{"Alice", 30};
std::cout << p << "\n";  // Alice (30)

Subscript Operator

class Array {
private:
    int* data;
    size_t size;
    
public:
    int& operator[](size_t index) {
        return data[index];
    }
    
    const int& operator[](size_t index) const {
        return data[index];
    }
};

Array arr;
arr[0] = 42;
int x = arr[0];

Function Call Operator (Functor)

class Multiplier {
private:
    int factor;
public:
    Multiplier(int f) : factor(f) {}
    
    int operator()(int x) const {
        return x * factor;
    }
};

Multiplier times3(3);
std::cout << times3(10);  // 30

// Useful with algorithms
std::vector<int> nums = {1, 2, 3, 4, 5};
std::transform(nums.begin(), nums.end(), nums.begin(), Multiplier(2));

Static Members

Members shared by all instances of a class.

class Counter {
private:
    static int count;  // Declaration
    int id;
    
public:
    Counter() : id(++count) {}
    
    static int getCount() {
        return count;
    }
    
    int getId() const { return id; }
};

// Definition (required, usually in .cpp file)
int Counter::count = 0;

Counter a, b, c;
std::cout << Counter::getCount();  // 3
std::cout << a.getId();            // 1
std::cout << c.getId();            // 3

Static Methods

class Math {
public:
    static double pi() { return 3.14159265359; }
    
    static int max(int a, int b) {
        return (a > b) ? a : b;
    }
    
    // Can't access non-static members!
    // static void bad() { return this->x; }  // Error
};

double p = Math::pi();
int m = Math::max(5, 10);

Static Const Members

class Config {
public:
    static const int MAX_SIZE = 100;           // OK for integral
    static constexpr double PI = 3.14159;      // C++11 for non-integral
    static const std::string DEFAULT_NAME;     // Declared here
};

// Defined in .cpp
const std::string Config::DEFAULT_NAME = "Default";

Friend Functions and Classes

Friends can access private and protected members.

Friend Function

class Box {
private:
    double width;
    
public:
    Box(double w) : width(w) {}
    
    // Friend function declaration
    friend void printWidth(const Box& b);
    friend Box operator+(const Box& a, const Box& b);
};

// Friend function definition
void printWidth(const Box& b) {
    std::cout << "Width: " << b.width << "\n";  // Can access private
}

Box operator+(const Box& a, const Box& b) {
    return Box(a.width + b.width);
}

Friend Class

class Engine {
private:
    int horsepower;
    friend class Car;  // Car can access Engine's private members
    
public:
    Engine(int hp) : horsepower(hp) {}
};

class Car {
private:
    Engine engine;
    
public:
    Car(int hp) : engine(hp) {}
    
    void showPower() {
        std::cout << "HP: " << engine.horsepower;  // OK: friend
    }
};

Use Friends Sparingly

Friend breaks encapsulation. Use it only when necessary, like for operator overloading that needs private access.

SOLID Principles

Five principles for better object-oriented design.

S - Single Responsibility

A class should have only one reason to change.

// Bad: Multiple responsibilities
class User {
    void saveToDatabase();
    void sendEmail();
    void generateReport();
};

// Good: Single responsibility
class User { /* user data */ };
class UserRepository { void save(User& u); };
class EmailService { void send(User& u, std::string msg); };
class ReportGenerator { void generate(User& u); };

O - Open/Closed

Open for extension, closed for modification.

// Use polymorphism to extend without modifying
class Shape {
public:
    virtual double area() const = 0;
};

// Add new shapes without changing existing code
class Circle : public Shape { /* ... */ };
class Rectangle : public Shape { /* ... */ };
class Triangle : public Shape { /* ... */ };  // New! No changes elsewhere

L - Liskov Substitution

Subtypes must be substitutable for their base types.

// Bad: Square violates Rectangle's invariants
class Rectangle {
public:
    virtual void setWidth(double w) { width = w; }
    virtual void setHeight(double h) { height = h; }
};

class Square : public Rectangle {
    void setWidth(double w) override { width = height = w; }  // Breaks expectations!
};

// Better: Use composition or separate hierarchies

I - Interface Segregation

Clients shouldn't depend on interfaces they don't use.

// Bad: Fat interface
class Worker {
    virtual void work() = 0;
    virtual void eat() = 0;
    virtual void sleep() = 0;
};

// Good: Segregated interfaces
class Workable { virtual void work() = 0; };
class Eatable { virtual void eat() = 0; };
class Sleepable { virtual void sleep() = 0; };

class Human : public Workable, public Eatable, public Sleepable { };
class Robot : public Workable { };  // Robots don't eat or sleep

D - Dependency Inversion

Depend on abstractions, not concretions.

// Bad: Depends on concrete class
class Report {
    MySQLDatabase db;  // Tightly coupled
};

// Good: Depends on abstraction
class Database {
public:
    virtual void save(std::string data) = 0;
};

class Report {
    Database& db;  // Can use any database
public:
    Report(Database& database) : db(database) {}
};

Design Patterns Introduction

Reusable solutions to common design problems.

Singleton

Ensure only one instance exists.

class Logger {
private:
    static Logger* instance;
    Logger() {}  // Private constructor
    
public:
    static Logger& getInstance() {
        static Logger instance;  // Thread-safe in C++11
        return instance;
    }
    
    void log(const std::string& msg) {
        std::cout << "[LOG] " << msg << "\n";
    }
    
    // Delete copy operations
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;
};

Logger::getInstance().log("Hello!");

Factory

Create objects without specifying exact class.

class Shape {
public:
    virtual void draw() = 0;
    virtual ~Shape() = default;
};

class Circle : public Shape {
    void draw() override { std::cout << "Drawing circle\n"; }
};

class Rectangle : public Shape {
    void draw() override { std::cout << "Drawing rectangle\n"; }
};

class ShapeFactory {
public:
    static std::unique_ptr<Shape> create(const std::string& type) {
        if (type == "circle") return std::make_unique<Circle>();
        if (type == "rectangle") return std::make_unique<Rectangle>();
        return nullptr;
    }
};

auto shape = ShapeFactory::create("circle");
shape->draw();

Observer

Notify multiple objects about state changes.

class Observer {
public:
    virtual void update(int value) = 0;
};

class Subject {
    std::vector<Observer*> observers;
    int state;
    
public:
    void attach(Observer* o) { observers.push_back(o); }
    
    void setState(int s) {
        state = s;
        notify();
    }
    
    void notify() {
        for (auto* o : observers) {
            o->update(state);
        }
    }
};

Note

Design patterns are a deep topic. Consider studying the classic "Gang of Four" book for comprehensive coverage.

Practice Questions

  1. Create a BankAccount class with deposit, withdraw, and balance checking.
  2. Implement a Shape hierarchy with Circle, Rectangle, and Triangle.
  3. Write a Fraction class with operator overloading for +, -, *, /.
  4. Create a String class that manages its own memory (Rule of Five).
  5. Implement an Employee hierarchy with different types of employees.
  6. Create a simple LinkedList class with iterator support.
  7. Implement the Factory pattern for creating different types of documents.
  8. Create a Matrix class with operator overloading.
  9. Implement the Observer pattern for an event system.
  10. Create an interface-based plugin system.
  11. Write a SmartArray class that acts like a simple vector.
  12. Implement a class hierarchy that follows SOLID principles.