Skip to main content

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).

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

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.
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.

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.
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.

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.
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.
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


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.
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.
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.
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.