Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

Object-Oriented Programming (OOP)

C++ is a multi-paradigm language, but its OOP features are powerful. OOP allows you to model real-world entities (like a Player, Car, or File) as objects that contain both data and behavior.

1. Classes & Structs

A Class is a blueprint for creating objects. It encapsulates data (attributes) and functions (methods) that operate on that data. In C++, class and struct are almost identical. The only difference is default visibility:
  • class: Members are private by default. (Used for complex objects with invariants).
  • struct: Members are public by default. (Used for simple data containers).
class Player {
private:
    // Data (State) - Hidden from the outside world (Encapsulation)
    std::string name;
    int health;

public:
    // Constructor: Initializes the object using a Member Initializer List.
    // Why the initializer list ": name(n), health(h)" instead of assignment in the body?
    // 1. For non-trivial types (like std::string), it avoids default-constructing
    //    then reassigning -- the initializer list constructs directly.
    // 2. For const and reference members, it is the ONLY way to initialize them.
    // 3. It is simply the idiomatic, efficient way in modern C++.
    Player(std::string n, int h) : name(std::move(n)), health(h) {
        // std::move(n) avoids copying the string -- we "steal" n's contents
        // since we took it by value and won't use it again.
    }

    // Method: Defines behavior
    void takeDamage(int amount) {
        health -= amount;
    }

    // Getter: Provides read-only access
    // 'const' means this method will NOT modify the object
    int getHealth() const {
        return health;
    }
};

Access Modifiers

  • public: Accessible from anywhere. The “interface” of your class.
  • private: Accessible only from within the class. Used for internal state.
  • protected: Accessible from the class and its derived classes (children).

Class vs Struct — When to Use Which

CriteriaUse classUse struct
Has invariants (health cannot be negative, size must match capacity)Yes — private data + public methods enforce rulesNo
Has behavior (methods beyond simple getters)YesGenerally no
Is a simple data aggregate (Point, Color, Config)OverkillYes — public members, no methods beyond constructors
Needs inheritance or polymorphismYesRarely (technically works, but signals wrong intent)
Used as a function parameter bundle or return typeSometimesYes — clearly communicates “just data”
This is a convention, not a language rule. The compiler treats them identically (except for default access). But conventions matter — when a reader sees struct, they expect plain data. When they see class, they expect encapsulated behavior.

2. Inheritance

Inheritance allows a class to derive properties and behavior from another. It promotes code reuse.
// Base Class (Parent)
class Character {
protected:
    int health;
public:
    Character(int h) : health(h) {}
    
    // 'virtual' allows this function to be overridden by children
    virtual void attack() { std::cout << "Generic attack\n"; }
};

// Derived Class (Child)
class Wizard : public Character {
    int mana;
public:
    Wizard(int h, int m) : Character(h), mana(m) {}

    // Override base behavior
    void attack() override {
        std::cout << "Cast fireball! Mana: " << mana << "\n";
    }
};
Always use the override keyword when overriding virtual functions. It ensures the compiler checks that you are actually overriding a base method, preventing bugs from typos.

3. Polymorphism

Polymorphism (“many shapes”) allows you to treat derived objects as base objects. This is the magic that lets you write flexible code. For example, you can have a list of Character* pointers, some pointing to Wizards, some to Warriors. When you call attack(), the correct version is called for each object.
void performAttack(Character* c) {
    // Dynamic Dispatch: The runtime decides which function to call
    c->attack(); 
}

int main() {
    Wizard w(100, 50);
    Character c(100);

    performAttack(&w); // Prints "Cast fireball..."
    performAttack(&c); // Prints "Generic attack"
}

Virtual Destructors

Crucial: If a class has virtual functions, it must have a virtual destructor. Without it, deleting a derived object through a base pointer only runs the base destructor — the derived class’s destructor is silently skipped, leaking any resources it manages.
class Base {
public:
    virtual ~Base() { std::cout << "Base destroyed\n"; }
};

class Derived : public Base {
    std::vector<int> data;  // Imagine this holds 1GB of data
public:
    ~Derived() { std::cout << "Derived destroyed\n"; }
};

// CORRECT: ~Base() is virtual, so both destructors run
Base* b = new Derived();
delete b; // Prints "Derived destroyed" THEN "Base destroyed". All resources freed.

// If ~Base() was NOT virtual:
// delete b; // Only ~Base() runs. Derived's vector is never freed. 1GB leaked!
This is one of the most common C++ bugs in production code. If you see a class with a virtual method but a non-virtual destructor, it is almost certainly a bug. Modern compilers like Clang will warn about this with -Wnon-virtual-dtor. Enable this warning and treat it as an error.

4. Abstract Classes & Interfaces

A class is abstract if it has at least one pure virtual function (= 0). It cannot be instantiated. This is how C++ implements interfaces. It forces children to implement specific behavior.
class IDrawable {
public:
    virtual void draw() const = 0; // Pure virtual: "Children MUST implement this"
    virtual ~IDrawable() = default;
};

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

5. The Rule of Five (and Why You Want Rule of Zero)

In modern C++, if you manage resources manually (like raw pointers), you need to define 5 special member functions to handle copying and moving correctly. If you define any one of them, you almost certainly need all five — the compiler’s auto-generated defaults will do the wrong thing for the others.
  1. Destructor: Cleans up resources.
  2. Copy Constructor: Creates a new object from an existing one (Deep Copy).
  3. Copy Assignment Operator: Assigns an existing object to another (Deep Copy).
  4. Move Constructor (C++11): “Steals” resources from a temporary object (Performance).
  5. Move Assignment Operator (C++11): Assigns by stealing resources.
Think of it like moving apartments. Copying is hiring movers to buy identical furniture for the new apartment. Moving is loading your existing furniture onto a truck and driving it to the new place — much cheaper, but the old apartment is left empty.
class Buffer {
    int* data;
    size_t size;

public:
    // Constructor
    Buffer(size_t sz) : size(sz), data(new int[sz]) {}

    // 1. Destructor -- release the resource
    ~Buffer() { delete[] data; }

    // 2. Copy Constructor -- deep copy (expensive but safe)
    Buffer(const Buffer& other) : size(other.size), data(new int[other.size]) {
        std::copy(other.data, other.data + size, data);
    }

    // 3. Copy Assignment Operator -- deep copy with self-assignment safety
    Buffer& operator=(const Buffer& other) {
        if (this != &other) {           // Guard against self-assignment: buf = buf
            delete[] data;              // Free old resource
            size = other.size;
            data = new int[size];       // Allocate new resource
            std::copy(other.data, other.data + size, data);
        }
        return *this;
    }

    // 4. Move Constructor -- steal resources (cheap!)
    Buffer(Buffer&& other) noexcept : data(other.data), size(other.size) {
        other.data = nullptr;  // Leave the source in a valid but empty state
        other.size = 0;
    }

    // 5. Move Assignment Operator -- steal resources on assignment
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data;             // Free our old resource
            data = other.data;         // Steal theirs
            size = other.size;
            other.data = nullptr;      // Leave source empty
            other.size = 0;
        }
        return *this;
    }
};
Rule of Zero — the real goal: If your class only uses RAII types (like std::string, std::vector, std::unique_ptr), you don’t need to write ANY of these five functions. The compiler-generated defaults do the right thing automatically. This is not just a shortcut — it is the recommended practice. If you find yourself writing a destructor, ask: “Can I use a smart pointer or standard container instead?” The answer is almost always yes.

Composition Over Inheritance

A quick but important design note: prefer composition (having objects as members) over inheritance (deriving from base classes). Inheritance creates tight coupling — changes to the base class ripple through every derived class. Composition is more flexible.
// Inheritance approach: "A Car IS an Engine" -- this is wrong
class Car : public Engine { /* ... */ };

// Composition approach: "A Car HAS an Engine" -- this is correct
class Car {
    Engine engine;         // Car owns an Engine
    Transmission trans;    // Car owns a Transmission
public:
    void start() { engine.ignite(); }
};
Use inheritance for true “is-a” relationships (a Wizard is a Character) and when you need polymorphism. Use composition for everything else.

Inheritance vs Composition — Decision Framework

Question to askIf YesIf No
Is the relationship truly “is-a”? (A Dog IS an Animal)Inheritance is appropriateUse composition
Do you need runtime polymorphism (virtual dispatch)?Inheritance is the standard mechanismComposition or templates work better
Does the derived class need to substitute for the base everywhere? (Liskov Substitution)Inheritance is correctForced inheritance will cause bugs
Might the “base” be swapped out at runtime? (different engines, strategies)Composition — inject the dependencyInheritance locks you in at compile time
Are you combining capabilities from multiple sources?Composition (C++ multiple inheritance is fragile)Either approach can work

6. OOP Gotchas That Cause Real Bugs

Gotcha 1: Object Slicing When you assign a derived object to a base variable by value, the derived part is “sliced off.” Only the base portion is copied. This is silent and almost never what you want.
Wizard w(100, 50);
Character c = w;   // SLICING! Only the Character part of w is copied.
c.attack();        // Calls Character::attack(), not Wizard::attack().
                   // The Wizard's mana and overridden behavior are gone.
The fix: always use references or pointers when working with polymorphic objects. Character& c = w; preserves the full Wizard identity. Gotcha 2: The Diamond Problem When a class inherits from two classes that share a common base, the common base’s members appear twice in the derived object. This causes ambiguity.
class Animal { public: int age; };
class Dog : public Animal {};
class Cat : public Animal {};
class DogCat : public Dog, public Cat {};
// DogCat has TWO copies of Animal::age -- Dog::age and Cat::age
// dc.age is ambiguous -- which one?
The fix: use virtual inheritance (class Dog : virtual public Animal) so only one copy of Animal exists. But virtual inheritance has overhead (extra pointer per object) and makes construction order complex. The real fix in most cases is to redesign so you do not need multiple inheritance from concrete classes. Gotcha 3: Calling Virtual Functions in Constructors Inside a constructor, virtual dispatch does NOT call the derived class’s override. The object is still being constructed, so the derived part does not exist yet. The base class version is always called.
class Base {
public:
    Base() { init(); }          // Calls Base::init(), even for Derived objects
    virtual void init() { std::cout << "Base init\n"; }
};
class Derived : public Base {
public:
    void init() override { std::cout << "Derived init\n"; }
};

Derived d;  // Prints "Base init" -- NOT "Derived init"!
This is specified behavior, not a bug in the compiler. If you need polymorphic initialization, use a factory function that constructs the object and then calls an init() method separately.

Exercises

  1. Slicing detector: Create a Shape base class with area() and a Circle derived class. Write a function that takes a Shape by value and another that takes a Shape&. Pass a Circle to both. Observe the difference. Add a print statement in each area() to prove which version is called.
  2. Rule of Five practice: Implement a simple String class that wraps a char* buffer. Implement all five special member functions. Verify your move constructor works by returning a String from a function and checking that no deep copy occurs (add print statements to each special member function).
  3. Design challenge: You are building a notification system that can send alerts via Email, SMS, Slack, and Webhook. Should you use inheritance (a Notifier base class with derived classes) or composition (a NotificationService that holds a Transport strategy)? Sketch both designs. Which is easier to extend when product asks for a PagerDuty integration next month?
  4. Virtual destructor audit: Take any class hierarchy you have written and deliberately remove the virtual keyword from the base destructor. Delete a derived object through a base pointer. Compile with -Wnon-virtual-dtor and observe the warning. Run under AddressSanitizer and observe the runtime behavior.

Summary

  • Classes: Encapsulate data and behavior.
  • Inheritance: Enables code reuse (public inheritance is “is-a”).
  • Polymorphism: Allows dynamic behavior via Virtual Functions.
  • Abstract Classes: Define interfaces/contracts.
  • Rule of Five: Implement copy/move semantics only if managing raw resources. Otherwise, follow Rule of Zero.
Next, we’ll explore the Standard Template Library (STL), which provides powerful containers and algorithms so you don’t have to write them from scratch.