πŸ”’ Private Site

This site is password-protected.

Object-Oriented Programming in C++

OOP is C++’s mechanism for building abstractions. Master these concepts to write clean, maintainable, extensible code.


Table of Contents


Glossary β€” Key Terms at a Glance

Term Meaning
Class A user-defined type bundling data (members) and behavior (methods)
Object An instance of a class
Encapsulation Hiding internal details, exposing only a public interface
Inheritance Deriving new classes from existing ones (IS-A relationship)
Polymorphism One interface, multiple implementations (via virtual functions)
Virtual Function A member function resolved at runtime based on the actual object type
vtable Hidden table of function pointers used to implement virtual dispatch
vptr Hidden pointer in each polymorphic object, pointing to its class’s vtable
Pure Virtual = 0 function that must be overridden β€” makes the class abstract
Abstract Class A class with at least one pure virtual function β€” cannot be instantiated
override Keyword ensuring a function actually overrides a base class virtual
final Prevents further overriding of a virtual function or derivation from a class
RAII Resource Acquisition Is Initialization β€” tie resource lifetime to object scope
CRTP Curiously Recurring Template Pattern β€” compile-time polymorphism, no vtable cost

1 β€” Classes & Objects

1.1 Defining a Class

Definition: A class bundles data (member variables) and behavior (member functions) together into a single type. This is the foundation of encapsulation.

Example β€” BankAccount class:

class BankAccount {
private:    // accessible only within the class
    std::string owner;
    double balance;

public:     // accessible from outside
    // Constructor
    BankAccount(const std::string& owner, double initial_balance)
        : owner(owner), balance(initial_balance) {}  // member initializer list
    
    // Getter (const β€” promises not to modify state)
    double getBalance() const { return balance; }
    std::string getOwner() const { return owner; }
    
    // Methods
    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("Alice", 1000.0);
acc.deposit(500.0);
std::cout << acc.getBalance();  // 1500.0

πŸ” Why This Design: balance is private β€” external code cannot set it to a negative value or bypass the withdraw checks. The public methods form a controlled interface. This is encapsulation in action.


1.2 Access Specifiers

Specifier Within Class Derived Class Outside
public βœ… βœ… βœ…
protected βœ… βœ… ❌
private βœ… ❌ ❌

Convention: Data members should be private, the interface should be public. Use protected sparingly β€” it couples base and derived classes tightly.


2 β€” Constructors & Destructors

2.1 Types of Constructors

Definition: A constructor is a special member function called automatically when an object is created. It initializes the object’s state.

class Widget {
    int id;
    std::string name;
    int* data;

public:
    // 1. Default constructor
    Widget() : id(0), name("unnamed"), data(nullptr) {}
    
    // 2. Parameterized constructor
    Widget(int id, const std::string& name)
        : id(id), name(name), data(new int[100]) {}
    
    // 3. Copy constructor
    Widget(const Widget& other)
        : id(other.id), name(other.name), data(new int[100]) {
        std::copy(other.data, other.data + 100, data);
    }
    
    // 4. Move constructor (C++11)
    Widget(Widget&& other) noexcept
        : id(other.id), name(std::move(other.name)), data(other.data) {
        other.data = nullptr;  // leave source in valid state
    }
    
    // 5. Destructor
    ~Widget() {
        delete[] data;  // free resources
    }
};

πŸ” Why Each Constructor Exists:

  • Default: Creates an object with sensible defaults when no arguments are given
  • Parameterized: Initializes with user-provided values
  • Copy: Creates independent duplicate (deep copy of data)
  • Move: Transfers ownership of resources β€” avoids expensive copy
  • Destructor: Guarantees cleanup when object goes out of scope (RAII)

2.2 Member Initializer List

⚠️ Always use the member initializer list. Assignment in the constructor body is inefficient β€” it default-constructs first, then assigns.

// BAD: assignment in body (constructs default, then assigns)
Widget(int id) {
    this->id = id;       // default-constructed first, then overwritten
}

// GOOD: initializer list (constructs directly with the value)
Widget(int id) : id(id) {}  // single construction β€” faster

// For const and reference members, initializer list is REQUIRED
class Config {
    const int max_size;
    std::string& name_ref;
public:
    Config(int size, std::string& name)
        : max_size(size), name_ref(name) {}  // only way
};

2.3 Delegating Constructors

Definition: A constructor can call another constructor of the same class, avoiding code duplication.

class Point {
    double x, y, z;
public:
    Point(double x, double y, double z) : x(x), y(y), z(z) {}
    Point(double x, double y) : Point(x, y, 0.0) {}  // delegates to 3-arg
    Point() : Point(0.0, 0.0, 0.0) {}                 // delegates to 3-arg
};

2.4 explicit Keyword

class Fraction {
public:
    explicit Fraction(int numerator, int denominator = 1)
        : num(numerator), den(denominator) {}
private:
    int num, den;
};

Fraction f1(3);         // OK: direct initialization
Fraction f2 = 3;        // ERROR: implicit conversion blocked by explicit

πŸ” Why explicit? Without it, Fraction f = 3; silently creates Fraction(3, 1). This implicit conversion can cause subtle bugs β€” imagine void process(Fraction f) being called as process(42) without the programmer realizing a conversion happened.

Rule: Make single-parameter constructors explicit unless you specifically want implicit conversions (rare).


3 β€” Inheritance

3.1 Base & Derived Classes

Definition: Inheritance lets a new class (derived) reuse and extend an existing class (base). It models an IS-A relationship.

class Shape {
protected:
    std::string color;
public:
    Shape(const std::string& color) : color(color) {}
    virtual double area() const = 0;       // pure virtual β†’ abstract
    virtual double perimeter() const = 0;  // pure virtual β†’ abstract
    virtual void draw() const {            // virtual β€” can be overridden
        std::cout << "Drawing " << color << " shape\n";
    }
    virtual ~Shape() = default;            // ALWAYS virtual destructor in base
};

class Circle : public Shape {
    double radius;
public:
    Circle(const std::string& color, double radius)
        : Shape(color), radius(radius) {}
    
    double area() const override { return 3.14159 * radius * radius; }
    double perimeter() const override { return 2 * 3.14159 * radius; }
};

class Rectangle : public Shape {
    double width, height;
public:
    Rectangle(const std::string& color, double w, double h)
        : Shape(color), width(w), height(h) {}
    
    double area() const override { return width * height; }
    double perimeter() const override { return 2 * (width + height); }
};

πŸ” Key Design Decisions:

  • virtual ~Shape() = default; β€” Essential. Without a virtual destructor, delete basePtr on a derived object causes undefined behavior.
  • = 0 makes functions pure virtual β€” forces derived classes to implement them.
  • override β€” tells the compiler to verify the function actually overrides a base version.

3.2 Inheritance Types

class Derived : public Base {};     // public members stay public
class Derived : protected Base {};  // public members become protected
class Derived : private Base {};    // everything becomes private (default for class)

Almost always use public inheritance (IS-A relationship). private inheritance means β€œimplemented in terms of” β€” prefer composition instead.


4 β€” Polymorphism

4.1 Runtime Polymorphism (Virtual Functions)

Definition: Polymorphism is the ability to treat derived objects through a base class interface and have the correct derived function called at runtime.

void printArea(const Shape& shape) {
    std::cout << "Area: " << shape.area() << "\n";  // calls correct derived version
}

Circle c("red", 5.0);
Rectangle r("blue", 4.0, 6.0);
printArea(c);  // Area: 78.5398
printArea(r);  // Area: 24.0

// Works with smart pointers for ownership
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>("red", 5.0));
shapes.push_back(std::make_unique<Rectangle>("blue", 4.0, 6.0));

for (const auto& s : shapes) {
    std::cout << s->area() << "\n";  // dynamic dispatch at runtime
}

πŸ” Why This Is Powerful: The printArea function knows nothing about Circle or Rectangle β€” it only knows Shape. Yet it calls the correct area() for each. This is the Open/Closed Principle β€” open for extension (new shapes), closed for modification.


4.2 How Virtual Functions Work β€” The vtable

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Circle object         β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ vptr  ───────────┼─┼──→  Circle's vtable:
β”‚ β”‚ color            β”‚ β”‚     area()      β†’ Circle::area
β”‚ β”‚ radius           β”‚ β”‚     perimeter() β†’ Circle::perimeter
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚     draw()      β†’ Shape::draw
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

How it works:

  1. Each class with virtual functions gets a vtable β€” an array of function pointers
  2. Each object has a hidden vptr pointing to its class’s vtable
  3. Virtual call: obj.vptr β†’ vtable[index] β†’ actual function (one pointer indirection)
  4. Cost: ~8 bytes per object (the vptr) + potential cache miss on the vtable lookup

⚠️ In HFT/low-latency systems, the cache miss from virtual dispatch may be unacceptable. Consider CRTP (Section 7.4) for compile-time polymorphism with zero overhead.


4.3 override and final

class Base {
public:
    virtual void foo() const;
};

class Derived : public Base {
public:
    void foo() const override;  // GOOD: compiler verifies signature matches
    // void foo() override;     // ERROR: const mismatch caught! βœ“
};

class Final : public Derived {
public:
    void foo() const override final;  // no further overriding allowed
};

Always use override on every overriding function. It catches subtle signature mismatches (like a missing const) that would otherwise silently create a new function instead of overriding.


5 β€” Abstract Classes & Interfaces

5.1 Pure Virtual Functions

Definition: A function declared with = 0 is pure virtual. A class with at least one pure virtual function is abstract β€” it cannot be instantiated. Derived classes must override all pure virtuals to be concrete.

class Tradeable {
public:
    virtual double price() const = 0;         // pure virtual
    virtual std::string ticker() const = 0;   // pure virtual
    virtual ~Tradeable() = default;
};
// Tradeable t;  // ERROR: cannot instantiate abstract class

5.2 Multiple Interfaces

// Interface pattern β€” all pure virtual, no data
class Serializable {
public:
    virtual std::string serialize() const = 0;
    virtual void deserialize(const std::string& data) = 0;
    virtual ~Serializable() = default;
};

// A class can implement multiple interfaces
class Stock : public Tradeable, public Serializable {
    std::string symbol;
    double current_price;
public:
    double price() const override { return current_price; }
    std::string ticker() const override { return symbol; }
    std::string serialize() const override { /* ... */ return ""; }
    void deserialize(const std::string& data) override { /* ... */ }
};

πŸ” Why Multiple Interfaces: A Stock IS-A Tradeable (has a price) AND IS-A Serializable (can be saved/loaded). This is one of few cases where multiple inheritance is clean and useful.


6 β€” Operator Overloading

6.1 Common Operators

Definition: Operator overloading lets you define how operators (+, -, ==, <<, etc.) work with your custom types, making them feel like built-in types.

class Vector2D {
    double x, y;
public:
    Vector2D(double x = 0, double y = 0) : x(x), y(y) {}
    
    // Arithmetic (member)
    Vector2D operator+(const Vector2D& rhs) const {
        return {x + rhs.x, y + rhs.y};
    }
    Vector2D operator*(double scalar) const {
        return {x * scalar, y * scalar};
    }
    
    // Comparison
    bool operator==(const Vector2D& rhs) const {
        return x == rhs.x && y == rhs.y;
    }
    
    // Subscript
    double& operator[](int i) { return (i == 0) ? x : y; }
    
    // Stream output (friend β€” non-member with private access)
    friend std::ostream& operator<<(std::ostream& os, const Vector2D& v) {
        return os << "(" << v.x << ", " << v.y << ")";
    }
};

// Non-member (allows 2.0 * vec, not just vec * 2.0)
Vector2D operator*(double scalar, const Vector2D& v) {
    return v * scalar;
}

Vector2D a{1, 2}, b{3, 4};
auto c = a + b;           // (4, 6)
auto d = 2.0 * a;         // (2, 4)
std::cout << c;            // (4, 6)

πŸ” Design Principle: Symmetric operators like + and * should support both orders (vec*2 and 2*vec). The non-member overload enables the second form.


6.2 Spaceship Operator (C++20)

#include <compare>

class Money {
    int cents;
public:
    auto operator<=>(const Money& other) const = default;
    // Generates: ==, !=, <, >, <=, >= β€” all six automatically!
};

C++20 game-changer: Instead of writing 6 comparison operators manually, <=> generates them all. The = default form compares members in declaration order.


7 β€” friend, static & Advanced Patterns

7.1 friend Functions & Classes

class Matrix {
    double data[4][4];
    
    friend class MatrixMultiplier;  // can access Matrix's private members
    friend Matrix operator*(const Matrix& a, const Matrix& b);  // friend function
};

⚠️ Use friend sparingly β€” it breaks encapsulation. Prefer public interfaces. Common legitimate uses: operator<<, operator>>, tightly-coupled helper classes.


7.2 static Members

Definition: static members belong to the class itself, not to any individual object. There is exactly one copy shared by all instances.

class Connection {
    static int count;              // shared across all instances
    static const int MAX = 100;   // compile-time constant
    
public:
    Connection() { ++count; }
    ~Connection() { --count; }
    
    static int getCount() { return count; }  // callable without an object
};

int Connection::count = 0;  // must define outside class (once, in a .cpp file)

// Usage
Connection c1, c2;
std::cout << Connection::getCount();  // 2

πŸ” Why static? Tracking the total number of active connections is a property of the class, not any single instance. static is perfect for counters, singletons, and factory methods.


7.3 Rule of Zero / Three / Five

Rule of Zero (PREFER THIS)

If your class doesn’t manage resources directly, don’t define any special members. Use smart pointers and RAII containers β€” they handle their own memory.

class Person {
    std::string name;
    std::vector<int> scores;
    // No destructor, copy/move constructors, or assignment operators needed!
    // std::string and std::vector handle everything automatically.
};

Rule of Five

⚠️ If you manage a raw resource (raw pointer, file handle, socket), you MUST define ALL five special members. Missing any one causes bugs.

class Buffer {
    int* data;
    size_t size;
public:
    Buffer(size_t n) : data(new int[n]), size(n) {}
    ~Buffer() { delete[] data; }                                    // 1. destructor
    Buffer(const Buffer& other);                                     // 2. copy constructor
    Buffer& operator=(const Buffer& other);                          // 3. copy assignment
    Buffer(Buffer&& other) noexcept;                                 // 4. move constructor
    Buffer& operator=(Buffer&& other) noexcept;                      // 5. move assignment
};

Best Practice: Convert Rule of Five classes to Rule of Zero by replacing raw resources with smart pointers: std::unique_ptr<int[]> data; eliminates the need for all five.


7.4 CRTP β€” Compile-Time Polymorphism

Definition: The Curiously Recurring Template Pattern achieves polymorphism at compile time β€” no vtable, no virtual dispatch overhead.

template <typename Derived>
class Shape {
public:
    double area() const {
        return static_cast<const Derived*>(this)->area_impl();
    }
};

class Circle : public Shape<Circle> {
    double radius;
public:
    Circle(double r) : radius(r) {}
    double area_impl() const { return 3.14159 * radius * radius; }
};

πŸ” Why CRTP? In HFT, virtual function calls cause cache misses (~5-20ns penalty). CRTP resolves the call at compile time β€” the compiler inlines area_impl() directly. Zero overhead.


Practice Questions

Q1. What is the difference between public, protected, and private inheritance? When would you use each?

Q2. Explain why the destructor in a base class designed for inheritance should be virtual. What happens if it isn’t?

Q3. Given a class with a raw int* member, implement all five special members (Rule of Five). Then refactor it to Rule of Zero using unique_ptr.

Q4. What is the difference between override and final? Write code demonstrating where override catches a bug.

Q5. Explain the vtable mechanism. Draw the vtable diagram for a Base class with virtual void foo() and virtual void bar(), and a Derived class that overrides only foo().

Q6. Why should single-parameter constructors be marked explicit? Give a code example where omitting explicit causes a subtle bug.

Q7. Write a Matrix class with operator+, operator*, and operator<< overloaded. Which should be member functions and which should be non-member? Why?

Q8. Compare runtime polymorphism (virtual functions) vs compile-time polymorphism (CRTP). When would you choose each?

Q9. What is the Rule of Zero and why is it the preferred approach? How do smart pointers enable it?

Q10. Design an abstract Instrument class for a trading system with derived classes Stock, Bond, and Option. Include virtual functions for price(), risk(), and describe().


Key Takeaways

  1. Use member initializer lists β€” always, for correctness and performance
  2. Make destructors virtual in base classes designed for inheritance
  3. Use override on every overriding function β€” catches subtle bugs
  4. Make single-param constructors explicit β€” prevents accidental conversions
  5. Prefer Rule of Zero β€” let smart pointers and containers manage resources
  6. Composition over inheritance β€” don’t overuse inheritance hierarchies
  7. Program to interfaces β€” use abstract base classes for flexibility

← Back to C++ Notes