05 Object-Oriented Programming (OOP)

Overview: Polymorphism and Memory Layouts

C++ supports object-oriented programming with explicit control over function binding. This note explores object memory layouts, dynamic dispatch via virtual tables, and the safety rules of inheritance.


1. Class Layouts and the Lifecycle

In C++, class and struct are identical, with one exception:

  • class: Members are private by default.
  • struct: Members are public by default.

Object Lifecycle:

  • Constructors: Initialize the object’s memory.
  • Destructors: Automatically execute when an object falls out of scope or is explicitly deleted, allowing resource cleanup.
  • Destruction Order: Automatic (stack) variables are destroyed in exactly the reverse order of their construction.

2. Dynamic Dispatch: The _vptr and vtable

In C++, member functions are non-virtual by default to avoid performance overhead. Calling a non-virtual function is resolved at compile-time (static binding).

To enable dynamic polymorphism, a function must be marked virtual.

How Virtual Functions Work under the hood:

  1. The Virtual Table (vtable): For each class containing virtual functions, the compiler generates a static table of function pointers called a vtable.
  2. The Virtual Pointer (_vptr): Every instance of that class contains a hidden pointer (usually 8 bytes at the beginning of the object) pointing to the class’s static vtable.
Instance on Stack/Heap:          Static Class Memory:
[ _vptr (8 bytes) ] -----------> [ vtable for Dog ]
[ name (std::string) ]           |  &Dog::print()  | ----> Code: Dog::print()
[ weight (int) ]                 |  &Dog::~Dog()   |

The Cost of Virtual Functions:

Calling a virtual function requires two pointer dereferences:

\text{Object Pointer} \xrightarrow{\text{dereference}} \text{_vptr} \xrightarrow{\text{dereference}} \text{vtable entry} \xrightarrow{\text{jump}} \text{Function Instructions}


3. The Virtual Destructor Rule

If a class contains any virtual functions, its destructor must be declared virtual.

class Base {
public:
    virtual ~Base(); // Virtual Destructor
};
 
class Derived : public Base {
    int* data;
public:
    Derived() : data(new int[100]) {}
    ~Derived() override { delete[] data; }
};

Why?

If the destructor is not virtual, calling delete basePtr (where basePtr points to a Derived instance) will only call the Base destructor. The Derived destructor will be bypassed, leaking the data array on the heap. Making the destructor virtual ensures the deletion resolves dynamically, calling both destructors.


💻 Conceptual Code Demonstration

#include <iostream>
#include <string>
 
class Dog {
protected:
    std::string name;
public:
    Dog(const std::string& name) : name(name) {
        std::cout << "Dog constructed: " << name << '\n';
    }
    
    // Virtual destructor is critical for base classes
    virtual ~Dog() {
        std::cout << "Dog destructor: " << name << '\n';
    }
    
    virtual void bark() const {
        std::cout << name << " barks (Dog)!\n";
    }
};
 
class BorderCollie : public Dog {
    int* trickCount;
public:
    BorderCollie(const std::string& name) : Dog(name), trickCount(new int(0)) {
        std::cout << "BorderCollie constructed\n";
    }
    
    ~BorderCollie() override {
        delete trickCount; // Prevents memory leak
        std::cout << "BorderCollie destructor\n";
    }
    
    // override checks signature compatibility at compile-time
    void bark() const override {
        std::cout << name << " barks dynamically (Collie)!\n";
    }
};
 
int main() {
    std::cout << "--- Stack Scope allocation ---\n";
    {
        BorderCollie collie("CollieObj");
    } // collie goes out of scope -> Destructors run in reverse order
    
    std::cout << "\n--- Polymorphic Pointer deletion ---\n";
    Dog* dogPtr = new BorderCollie("CollieHeap");
    
    dogPtr->bark(); // Dynamic dispatch resolves via _vptr to BorderCollie::bark()
    
    delete dogPtr; // Triggers Derived -> Base destructors because of virtual ~Dog()
    
    return 0;
}