Inheritance, virtual functions, and dynamic binding. How C++ lets you write code that works with objects whose exact type isn't known until runtime.
You're building a graphics editor. You have circles, rectangles, and triangles. They all need to be drawn, moved, and resized. You could write separate code for each shape — but that means every time you add a new shape, you modify every function.
OOP offers a better approach: define a Shape base class with virtual functions draw(), move(), resize(). Each specific shape inherits from Shape and provides its own implementation. Code that works with Shape works with any shape, even ones that don't exist yet.
A base class defines the common interface and shared data. A derived class inherits from the base, adding or overriding members:
c++ class Quote { // base class public: Quote() = default; Quote(const string &book, double price) : bookNo(book), price(price) {} string isbn() const { return bookNo; } virtual double net_price(size_t n) const { return n * price; // no discount } virtual ~Quote() = default; // virtual destructor protected: double price = 0.0; private: string bookNo; }; class Bulk_quote : public Quote { // derived class public: Bulk_quote(const string &b, double p, size_t qty, double disc) : Quote(b, p), min_qty(qty), discount(disc) {} double net_price(size_t n) const override { if (n >= min_qty) return n * (1 - discount) * price; else return n * price; } private: size_t min_qty = 0; double discount = 0.0; };
override after a virtual function declaration tells the compiler "I intend to override a base-class virtual." If the signature doesn't match any base virtual, you get a compile error instead of silently creating a new function. Always use it.protected for data that derived classes need direct access to (like price above).override specifier do?A virtual function is one that a derived class can override. When called through a base-class reference or pointer, the derived class's version runs. This is the magic of OOP.
c++ Quote base("978-0", 50.0); Bulk_quote derived("978-0", 50.0, 10, 0.2); // Static type: Quote&. Dynamic type: depends on what r refers to. Quote &r = derived; // Calls Bulk_quote::net_price because r's DYNAMIC type is Bulk_quote cout << r.net_price(20); // uses 20% discount = 800
| Non-virtual | Virtual | |
|---|---|---|
| Resolution | Compile time (static) | Runtime (dynamic) |
| Based on | Static type of object/ptr/ref | Dynamic (actual) type |
| Keyword | (none) | virtual in base class |
| Can override? | Hides, doesn't override | Yes, with matching signature |
Adding final prevents a virtual function from being overridden in further derived classes:
c++ class Base { virtual void f() final; // no derived class can override f }; class Final final : public Base {}; // no class can derive from Final
The power of dynamic binding is that you can write functions that work with the base class and automatically do the right thing for any derived class:
c++ // This function works with ANY type derived from Quote double print_total(ostream &os, const Quote &item, size_t n) { double ret = item.net_price(n); // dynamic dispatch! os << "ISBN: " << item.isbn() << " total: " << ret << endl; return ret; } // Pass a Quote → uses Quote::net_price (no discount) print_total(cout, base, 20); // 1000 // Pass a Bulk_quote → uses Bulk_quote::net_price (with discount) print_total(cout, derived, 20); // 800
print_total is "open for extension" (works with new derived types you haven't written yet) but "closed for modification" (you never need to change print_total). This is the fundamental promise of OOP.A Quote hierarchy: Quote (base) with Bulk_quote and Limit_quote as derived classes. Click "print_total" with different types to see dynamic dispatch in action.
A derived object contains a base-class subobject. Because of this, we can bind a base-class reference or pointer to a derived object:
| Conversion | Legal? |
|---|---|
| Derived → Base (ref or ptr) | Always (implicit) |
| Base → Derived | Never implicit (need dynamic_cast) |
| Derived object → Base object | Legal but slices (loses derived parts) |
Sometimes a base class represents a concept so abstract that it doesn't make sense to create objects of that type. A pure virtual function has no implementation in the base class:
c++ class Disc_quote : public Quote { public: Disc_quote(const string &b, double p, size_t qty, double disc) : Quote(b, p), quantity(qty), discount(disc) {} // Pure virtual: = 0 means "derived classes MUST override this" double net_price(size_t) const = 0; protected: size_t quantity = 0; double discount = 0.0; }; // Disc_quote is abstract — cannot be instantiated // Disc_quote dq; // ERROR: abstract class // Bulk_quote must override net_price to be concrete class Bulk_quote : public Disc_quote { public: double net_price(size_t n) const override; };
Three access specifiers interact with inheritance to control who can access what:
| Member Access | Own class | Derived class | Outside code |
|---|---|---|---|
public | Yes | Yes | Yes |
protected | Yes | Yes | No |
private | Yes | No | No |
The keyword before the base class name controls how inherited members appear in the derived class:
| Inheritance | public base members become | protected base members become |
|---|---|---|
public | public | protected |
protected | protected | protected |
private | private | private |
Friendship is not inherited. If class Base declares class F as a friend, F can access Base's private members but not Derived's private members. Each class controls access to its own members.
private member of its base class?Each class defines its own scope. Derived class scope is nested inside base class scope. Name lookup walks outward from the derived class to the base class(es).
c++ struct Base { int memfcn() { return 0; } }; struct Derived : Base { int memfcn(int) { return 1; } // hides Base::memfcn() }; Derived d; // d.memfcn(); // ERROR: Base::memfcn() is hidden d.memfcn(10); // OK: calls Derived::memfcn(int) d.Base::memfcn(); // OK: explicitly call Base version
This also applies to virtual functions. If the derived class declares a function with the same name but different parameters, it hides the base version rather than overloading it. Using override prevents this mistake.
using declaration: using Base::memfcn; inside Derived's class body makes all overloads of Base::memfcn visible alongside Derived's own versions.Construction and destruction in an inheritance hierarchy follow strict rules:
c++ class Base { public: virtual ~Base() = default; // ALWAYS make base dtor virtual }; Quote *p = new Bulk_quote("978-0", 50.0, 10, 0.2); delete p; // calls ~Bulk_quote, then ~Quote (correct!)
You can't put objects of different types in the same container directly — containers hold elements of a single type. If you store Quote objects in a vector<Quote> and try to add a Bulk_quote, the derived part gets sliced off.
The solution: store (smart) pointers to the base class:
c++ // WRONG: slicing — derived parts lost vector<Quote> basket; basket.push_back(Quote("978-0", 50)); basket.push_back(Bulk_quote("978-1", 50, 10, 0.2)); // sliced! // RIGHT: store shared_ptrs, preserve polymorphism vector<shared_ptr<Quote>> basket; basket.push_back(make_shared<Quote>("978-0", 50)); basket.push_back(make_shared<Bulk_quote>("978-1", 50, 10, 0.2)); // Dynamic dispatch works through smart pointers! for (const auto &item : basket) cout << item->net_price(20) << endl; // correct version called
shared_ptr<Base> in containers preserves polymorphism. The shared_ptr handles lifetime; the virtual function handles dispatch. They're made for each other.Under the hood, C++ implements dynamic dispatch using a virtual function table (vtable). Each class with virtual functions has a vtable containing pointers to its virtual function implementations. Each object has a hidden pointer to its class's vtable.
Create objects of different types and call virtual functions through a base pointer. Watch the vtable select the correct implementation.
Quote*, but the vtable pointer inside the object points to the correct class's vtable. When you call net_price(), the runtime follows the vtable pointer to find the right function. This is how C++ achieves polymorphism with near-zero overhead.OOP is one of C++'s two major abstraction mechanisms (the other is templates/generic programming). Understanding when to use inheritance vs templates is a key design skill.
| This Chapter | Builds Toward |
|---|---|
| Virtual functions | Strategy pattern, visitor pattern, plugin systems |
| Abstract base classes | Interface design, dependency injection |
| Dynamic binding | Runtime polymorphism vs compile-time (templates, Ch 16) |
| Access control | Designing class hierarchies and APIs |
| Object slicing | Why containers store pointers (smart_ptr) to polymorphic types |
Next up: Chapter 16: Templates and Generic Programming — compile-time polymorphism and the foundation of the standard library.