Object-Oriented Programming in C++

Master C++ OOP concepts including inheritance, polymorphism, virtual functions, and modern object-oriented design principles with interactive examples.

Best viewed on desktop for optimal interactive experience

Object-Oriented Programming in C++

C++ supports multiple programming paradigms, with object-oriented programming being one of its core strengths. Understanding inheritance, polymorphism, and proper class design is crucial for effective C++ development.

The Four Pillars of OOP

  1. Encapsulation: Data hiding and access control
  2. Inheritance: Code reuse and "is-a" relationships
  3. Polymorphism: One interface, multiple implementations
  4. Abstraction: Hiding complexity, showing essentials

Object-Oriented Programming in C++

C++ supports multiple programming paradigms, with object-oriented programming being one of its core strengths. Understanding inheritance, polymorphism, and proper class design is crucial for effective C++ development.

🔒

Encapsulation

Data hiding and access control

🏗️

Inheritance

Code reuse and is-a relationships

🎭

Polymorphism

One interface, multiple implementations

📋

Abstraction

Hiding complexity, showing essentials

Interactive OOP Explorer

Without Virtual Functions

class Base {
public:
    void method() { 
        std::cout << "Base method
"; 
    }
};

class Derived : public Base {
public:
    void method() { 
        std::cout << "Derived method
"; 
    }
};

Base* ptr = new Derived();
ptr->method();  // Calls Base::method
// Output: "Base method"
Without virtual, the function called depends on the pointer type, not the object type.

With Virtual Functions

class Base {
public:
    virtual void method() { 
        std::cout << "Base method
"; 
    }
    virtual ~Base() = default;  // Always virtual!
};

class Derived : public Base {
public:
    void method() override { 
        std::cout << "Derived method
"; 
    }
};

Base* ptr = new Derived();
ptr->method();  // Calls Derived::method
// Output: "Derived method"
Virtual functions enable runtime polymorphism - the correct function is called based on the actual object type.

Polymorphism Example

#include <vector>
#include <memory>

class Shape {
public:
    virtual ~Shape() = default;
    virtual void draw() const = 0;  // Pure virtual = abstract
    virtual double area() const = 0;
};

class Circle : public Shape {
    double radius;
public:
    Circle(double r) : radius(r) {}
    
    void draw() const override {
        std::cout << "Drawing a circle with radius " << radius << std::endl;
    }
    
    double area() const override {
        return 3.14159 * radius * radius;
    }
};

class Rectangle : public Shape {
    double width, height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    
    void draw() const override {
        std::cout << "Drawing a rectangle " << width << "x" << height << std::endl;
    }
    
    double area() const override {
        return width * height;
    }
};

// Using polymorphism
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(5.0));
shapes.push_back(std::make_unique<Rectangle>(3.0, 4.0));

double totalArea = 0;
for (const auto& shape : shapes) {
    shape->draw();                    // Calls correct draw() method
    totalArea += shape->area();       // Calls correct area() method
}

std::cout << "Total area: " << totalArea << std::endl;

Modern C++ OOP Guidelines

Best Practices:
  • • Make destructors virtual in base classes
  • • Use override keyword for virtual function overrides
  • • Prefer composition over inheritance
  • • Follow SOLID principles
  • • Use smart pointers for polymorphic objects
SOLID Principles in C++:
  • Single Responsibility: One class, one reason to change
  • Open/Closed: Open for extension, closed for modification
  • Liskov Substitution: Subtypes must be substitutable
  • Interface Segregation: Many specific interfaces over one general
  • Dependency Inversion: Depend on abstractions, not concretions

Virtual Functions and Polymorphism

Runtime Polymorphism

Virtual functions enable runtime polymorphism, allowing the correct function to be called based on the actual object type, not the pointer type.

class Animal { public: virtual ~Animal() = default; virtual void makeSound() const = 0; // Pure virtual virtual void move() const { std::cout << "Animal is moving" << std::endl; } }; class Dog : public Animal { public: void makeSound() const override { std::cout << "Woof!" << std::endl; } void move() const override { std::cout << "Dog is running" << std::endl; } }; class Cat : public Animal { public: void makeSound() const override { std::cout << "Meow!" << std::endl; } }; // Usage std::vector<std::unique_ptr<Animal>> animals; animals.push_back(std::make_unique<Dog>()); animals.push_back(std::make_unique<Cat>()); for (const auto& animal : animals) { animal->makeSound(); // Calls correct implementation animal->move(); // Polymorphic behavior }

Virtual Destructor Rule

Always make destructors virtual in base classes to ensure proper cleanup of derived objects.

class Base { public: virtual ~Base() = default; // Essential for polymorphic classes };

Inheritance Types

Public Inheritance ("is-a")

Most common form - derived class is a type of base class.

class Vehicle { protected: std::string brand; public: Vehicle(std::string b) : brand(std::move(b)) {} virtual void start() = 0; }; class Car : public Vehicle { // Car IS-A Vehicle public: Car(std::string brand) : Vehicle(std::move(brand)) {} void start() override { std::cout << brand << " car engine started" << std::endl; } };

Private Inheritance ("implemented-in-terms-of")

Rarely used - for implementation details only.

class Timer { public: void start(); void stop(); bool isRunning() const; }; class Widget : private Timer { // Widget uses Timer internally public: void processData() { start(); // Can use Timer methods internally // ... processing logic ... stop(); } // Timer interface not exposed to Widget users };

Multiple Inheritance

C++ supports multiple inheritance, but it requires careful design.

class Flyable { public: virtual ~Flyable() = default; virtual void fly() = 0; }; class Swimmable { public: virtual ~Swimmable() = default; virtual void swim() = 0; }; class Duck : public Flyable, public Swimmable { public: void fly() override { std::cout << "Duck is flying!" << std::endl; } void swim() override { std::cout << "Duck is swimming!" << std::endl; } };

Diamond Problem Solution

Use virtual inheritance to solve the diamond problem.

class Animal { public: virtual ~Animal() = default; void breathe() { std::cout << "Breathing..." << std::endl; } }; class Mammal : virtual public Animal { public: void giveBirth() { std::cout << "Giving birth" << std::endl; } }; class Aquatic : virtual public Animal { public: void swimUnderwater() { std::cout << "Swimming underwater" << std::endl; } }; class Whale : public Mammal, public Aquatic { // Only one Animal base class instance };

Abstract Classes and Interfaces

Pure Virtual Functions

Create abstract base classes that cannot be instantiated.

class Shape { // Abstract base class public: virtual ~Shape() = default; // Pure virtual functions virtual double area() const = 0; virtual double perimeter() const = 0; virtual void draw() const = 0; // Concrete function virtual void displayInfo() const { std::cout << "Area: " << area() << ", Perimeter: " << perimeter() << std::endl; } }; 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; } void draw() const override { std::cout << "Drawing a circle with radius " << radius << std::endl; } };

Composition vs Inheritance

When to Use Composition

Prefer composition when you need "has-a" relationships.

class Engine { public: void start() { /* ... */ } void stop() { /* ... */ } int getHorsepower() const { return horsepower_; } private: int horsepower_ = 200; }; class Car { private: Engine engine_; // Composition: Car HAS-A Engine std::string model_; public: Car(std::string model) : model_(std::move(model)) {} void start() { engine_.start(); // Delegates to engine std::cout << model_ << " is starting" << std::endl; } int getPower() const { return engine_.getHorsepower(); } };

Benefits of Composition

  • More flexible than inheritance
  • Better encapsulation
  • Easier to test and mock
  • Avoids deep inheritance hierarchies
  • Can change behavior at runtime

Modern C++ OOP Features

Override and Final Keywords

class Base { public: virtual void method() {} virtual void finalMethod() final {} // Cannot be overridden }; class Derived : public Base { public: void method() override {} // Explicit override // void finalMethod() override {} // Error: cannot override final }; class Final final : public Derived { // Cannot be inherited from // ... };

SOLID Principles in C++

  1. Single Responsibility: One class, one reason to change
  2. Open/Closed: Open for extension, closed for modification
  3. Liskov Substitution: Subtypes must be substitutable for base types
  4. Interface Segregation: Many specific interfaces over one general
  5. Dependency Inversion: Depend on abstractions, not concretions

Best Practices

Design Guidelines

  • Use public inheritance for "is-a" relationships only
  • Prefer composition over inheritance when possible
  • Make destructors virtual in polymorphic base classes
  • Use the override keyword for clarity
  • Follow the Rule of Zero/Three/Five
  • Apply SOLID principles

Performance Considerations

  • Virtual function calls have small overhead
  • Multiple inheritance can increase object size
  • Virtual inheritance has additional cost
  • Consider template-based alternatives (CRTP) for compile-time polymorphism

C++ OOP provides powerful tools for modeling real-world relationships and creating maintainable, extensible code when used judiciously.

If you found this explanation helpful, consider sharing it with others.

Mastodon