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
1 β Classes & Objects
2 β Constructors & Destructors
- 2.1 Types of Constructors
- 2.2 Member Initializer List
- 2.3 Delegating Constructors
- 2.4 explicit Keyword
3 β Inheritance
4 β Polymorphism
- 4.1 Runtime Polymorphism (Virtual Functions)
- 4.2 How Virtual Functions Work β The vtable
- 4.3 override and final
5 β Abstract Classes & Interfaces
6 β Operator Overloading
7 β friend, static & Advanced Patterns
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:
balanceisprivateβ external code cannot set it to a negative value or bypass thewithdrawchecks. Thepublicmethods 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 createsFraction(3, 1). This implicit conversion can cause subtle bugs β imaginevoid process(Fraction f)being called asprocess(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 basePtron a derived object causes undefined behavior.= 0makes 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
printAreafunction knows nothing aboutCircleorRectangleβ it only knowsShape. Yet it calls the correctarea()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:
- Each class with virtual functions gets a vtable β an array of function pointers
- Each object has a hidden vptr pointing to its classβs vtable
- Virtual call:
obj.vptr β vtable[index] β actual function(one pointer indirection) - 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
StockIS-ATradeable(has a price) AND IS-ASerializable(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*2and2*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.staticis 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
- Use member initializer lists β always, for correctness and performance
- Make destructors
virtualin base classes designed for inheritance - Use
overrideon every overriding function β catches subtle bugs - Make single-param constructors
explicitβ prevents accidental conversions - Prefer Rule of Zero β let smart pointers and containers manage resources
- Composition over inheritance β donβt overuse inheritance hierarchies
- Program to interfaces β use abstract base classes for flexibility