all
Stage 03

OOP & RAII

Master C++ object-oriented programming — classes, inheritance, virtual dispatch, the Rule of Five, RAII resource management, and exception safety guarantees.

5 min read
26806 chars

Classes and Objects

{:.gc-basic}

Basic

class Sensor {
private:
    int    id_;
    float  value_;
    bool   active_;

public:
    // Constructor
    Sensor(int id, float initial = 0.0f)
        : id_(id), value_(initial), active_(false) {}

    // Member functions
    void activate()       { active_ = true; }
    float read() const    { return value_; }   // const = doesn't modify object
    int   id()   const    { return id_; }

    // Operator overload
    bool operator<(const Sensor& other) const {
        return value_ < other.value_;
    }
};

Sensor s(42, 23.5f);
s.activate();
std::cout << s.read() << '\n';   // 23.5

Access Specifiers

Specifier Who can access
private Only the class itself
protected Class and derived classes
public Anyone

Inheritance and Virtual Dispatch

{:.gc-mid}

Intermediate

// Abstract base class (interface)
class IDevice {
public:
    virtual ~IDevice() = default;        // virtual destructor — ALWAYS!
    virtual bool  open(const char* path) = 0;   // pure virtual
    virtual int   read(uint8_t* buf, int len)  = 0;
    virtual void  close() = 0;
    virtual std::string name() const = 0;
};

// Concrete implementation
class UartDevice : public IDevice {
    int fd_ = -1;
public:
    bool  open(const char* path) override;
    int   read(uint8_t* buf, int len) override;
    void  close() override;
    std::string name() const override { return "UART"; }
};

class SpiDevice : public IDevice { /* ... */ };

// Polymorphic usage
void read_sensor(IDevice& dev) {
    uint8_t buf[64];
    int n = dev.read(buf, sizeof(buf));   // calls the right override at runtime
}

UartDevice uart;
read_sensor(uart);

Virtual dispatch mechanism (vtable): Each class with virtual functions has a hidden vtable pointer (vptr) — a pointer to a table of function pointers. Calling a virtual function looks up the correct override at runtime through the vtable.


Rule of Five

{:.gc-mid}

If a class manages a resource (memory, file, socket), you must define (or =delete) all five special member functions:

class Buffer {
    uint8_t* data_;
    size_t   size_;

public:
    // 1. Constructor
    explicit Buffer(size_t size)
        : data_(new uint8_t[size]), size_(size) {
        std::memset(data_, 0, size_);
    }

    // 2. Destructor
    ~Buffer() { delete[] data_; }

    // 3. Copy constructor
    Buffer(const Buffer& other)
        : data_(new uint8_t[other.size_]), size_(other.size_) {
        std::memcpy(data_, other.data_, size_);
    }

    // 4. Copy assignment operator
    Buffer& operator=(const Buffer& other) {
        if (this != &other) {
            delete[] data_;
            data_ = new uint8_t[other.size_];
            size_ = other.size_;
            std::memcpy(data_, other.data_, size_);
        }
        return *this;
    }

    // 5. Move constructor
    Buffer(Buffer&& other) noexcept
        : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;
        other.size_ = 0;
    }

    // 5b. Move assignment
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this;
    }
};

RAII — Resource Acquisition Is Initialization

{:.gc-adv}

Advanced

RAII ties resource lifetime to object lifetime. The constructor acquires the resource; the destructor releases it — even when exceptions are thrown or early returns occur.

class FileHandle {
    FILE* fp_;
public:
    explicit FileHandle(const char* path, const char* mode)
        : fp_(std::fopen(path, mode)) {
        if (!fp_) throw std::runtime_error(
            std::string("Cannot open ") + path);
    }
    ~FileHandle() { if (fp_) std::fclose(fp_); }

    // Delete copy — no two FileHandle objects should own the same FILE*
    FileHandle(const FileHandle&)            = delete;
    FileHandle& operator=(const FileHandle&) = delete;

    FILE* get() const { return fp_; }
    operator FILE*() const { return fp_; }
};

void process_config(const char* path) {
    FileHandle f(path, "r");   // opens here
    // ... read file ...
    // f goes out of scope: destructor closes FILE* automatically
    // even if an exception is thrown inside the function!
}

Why RAII beats try/finally (Java/Python): In C++ there are no garbage-collected objects, so every resource must be explicitly released. RAII makes release automatic and exception-safe — the destructor always runs.

Exception Safety Levels

Guarantee Meaning
No-throw Operation never throws (mark with noexcept)
Strong Either succeeds completely or rolls back (no side effects)
Basic No resource leaks, invariants maintained, but state may have changed
None May leak resources or leave broken state
void swap_strong(Buffer& a, Buffer& b) noexcept {
    // swap pointers — cannot throw, O(1)
    std::swap(a.data_, b.data_);
    std::swap(a.size_, b.size_);
}

Interview Q&A

{:.gc-iq}

Interview Q&A

Q1 — Basic: What is the difference between struct and class in C++?

The only difference is the default access specifier: struct members are public by default, class members are private by default. Everything else — inheritance, virtual functions, constructors — is identical. Convention: use struct for plain data aggregates (POD types), class for types with invariants and encapsulation.

Q2 — Intermediate: Why must the base class destructor be virtual?

Without a virtual destructor, deleting a derived object through a base pointer calls only the base destructor — the derived destructor is never called. This leaks resources held by the derived class. With virtual ~Base(), the correct chain of destructors (derived then base) is called via the vtable. Rule: any class with virtual functions must have a virtual destructor.

Q3 — Intermediate: What is the Rule of Zero and when should you follow it instead of the Rule of Five?

If a class owns no raw resources — it uses standard library containers, smart pointers, or other RAII wrappers — you should define none of the five special members and let the compiler generate them. The compiler-generated versions do the right thing (deep copy via smart pointers’ copy constructors, etc.). Only write the Rule of Five when you are implementing a resource-managing class itself (like std::vector or std::unique_ptr).

Q4 — Advanced: Explain how RAII handles exception safety in the presence of constructors that can throw.

If a constructor throws, the object is never considered fully constructed, so its destructor is not called. However, all subobjects (member objects and base classes) that were fully constructed will have their destructors called. This means: if you use RAII members (smart pointers, FileHandle), they are automatically cleaned up even if the constructor throws halfway through. Never use raw new for multiple resources in a constructor — use a separate RAII wrapper for each resource.


References

{:.gc-ref}

References

Resource Link
C++ Core Guidelines isocpp.github.io/CppCoreGuidelines
cppreference.com — Classes en.cppreference.com/w/cpp/language/classes
Herb Sutter — Exception Safety gotw.ca/gotw/