C++ Primer, Chapter 15

Object-Oriented Programming

Inheritance, virtual functions, and dynamic binding. How C++ lets you write code that works with objects whose exact type isn't known until runtime.

Prerequisites: Chapter 7 (Classes) + Chapter 13 (Copy Control).
10
Chapters
4+
Simulations

Chapter 0: Why OOP?

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.

The three pillars of OOP in C++:
Data abstraction — separate interface from implementation (Chapter 7)
Inheritance — define classes that model "is-a" relationships
Dynamic binding — use objects without knowing their exact type at compile time
What problem does inheritance solve?

Chapter 1: Base & Derived Classes

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 keyword: Adding 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 members: Accessible to the class itself and its derived classes, but not to general code. Use protected for data that derived classes need direct access to (like price above).
What does the override specifier do?

Chapter 2: Virtual Functions

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

Virtual vs Non-Virtual

Non-virtualVirtual
ResolutionCompile time (static)Runtime (dynamic)
Based onStatic type of object/ptr/refDynamic (actual) type
Keyword(none)virtual in base class
Can override?Hides, doesn't overrideYes, with matching signature
Key rule: Dynamic binding happens only when a virtual function is called through a reference or pointer. Calling a virtual through an object (not pointer/reference) always uses the static type's version — no dynamic dispatch.

final

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
When does dynamic binding (runtime dispatch) occur for virtual functions?

Chapter 3: Dynamic Binding in Action

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
This is the Open/Closed Principle. 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.
Class Hierarchy Visualizer

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.

click to call

Conversions and Inheritance

A derived object contains a base-class subobject. Because of this, we can bind a base-class reference or pointer to a derived object:

ConversionLegal?
Derived → Base (ref or ptr)Always (implicit)
Base → DerivedNever implicit (need dynamic_cast)
Derived object → Base objectLegal but slices (loses derived parts)
Object slicing: If you copy a derived object into a base object (not reference/pointer), the derived-specific members are "sliced off." The result is a plain base object. This is almost always a bug. Use references or pointers when working with inheritance hierarchies.
What happens when you assign a derived object to a base object (not reference)?

Chapter 4: Abstract Base Classes

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;
};
Abstract base classes define the interface contract. They say "every derived class must provide these operations" without dictating how. This is the C++ equivalent of an interface in languages like Java. A class with any pure virtual function is abstract and cannot be instantiated.
What makes a class abstract in C++?

Chapter 5: Access Control & Inheritance

Three access specifiers interact with inheritance to control who can access what:

Member AccessOwn classDerived classOutside code
publicYesYesYes
protectedYesYesNo
privateYesNoNo

Inheritance Access Specifier

The keyword before the base class name controls how inherited members appear in the derived class:

Inheritancepublic base members becomeprotected base members become
publicpublicprotected
protectedprotectedprotected
privateprivateprivate
Almost always use public inheritance. Public inheritance means "is-a": a Bulk_quote IS-A Quote. Protected and private inheritance mean "implemented-in-terms-of" — useful rarely, and usually composition is a better choice.

friend and Inheritance

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.

Can a derived class access a private member of its base class?

Chapter 6: Scope & Name Lookup

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

Name hiding: A name in the derived class hides all members with that name in the base class, even if the parameter lists differ. This is different from overloading, and it catches many programmers by surprise.
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.

To bring hidden names back into scope: Use a using declaration: using Base::memfcn; inside Derived's class body makes all overloads of Base::memfcn visible alongside Derived's own versions.
If Derived defines a function with the same name as a Base function but different parameters, what happens?

Chapter 7: Constructors & Destructors

Construction and destruction in an inheritance hierarchy follow strict rules:

Construction Order

1. Base constructor
Build the base subobject first
2. Member initializers
Initialize derived-class members
3. Derived constructor body
Run derived-class code

Destruction Order (Reverse)

1. Derived destructor body
Clean up derived-specific resources
2. Members destroyed
In reverse declaration order
3. Base destructor
Clean up base subobject
Virtual destructors are essential. If you delete a derived object through a base pointer and the destructor is not virtual, only the base destructor runs — the derived part is never cleaned up. Any class used as a base class should have a virtual destructor.
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!)

Containers and Inheritance

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
This is where Ch 12 (smart pointers) meets Ch 15 (OOP). Storing shared_ptr<Base> in containers preserves polymorphism. The shared_ptr handles lifetime; the virtual function handles dispatch. They're made for each other.
Why must a base class have a virtual destructor?

Chapter 8: Vtable Simulator

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.

Virtual Dispatch in Action

Create objects of different types and call virtual functions through a base pointer. Watch the vtable select the correct implementation.

Observe: The base pointer always has static type 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.

Chapter 9: Beyond — Connections

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 ChapterBuilds Toward
Virtual functionsStrategy pattern, visitor pattern, plugin systems
Abstract base classesInterface design, dependency injection
Dynamic bindingRuntime polymorphism vs compile-time (templates, Ch 16)
Access controlDesigning class hierarchies and APIs
Object slicingWhy containers store pointers (smart_ptr) to polymorphic types
Lippman's advice: "Inheritance is often overused. Not every relationship between types is an is-a relationship. Prefer composition when the relationship is has-a or uses-a." Use inheritance for genuine type hierarchies where dynamic binding provides real value.

Continue Reading

Next up: Chapter 16: Templates and Generic Programming — compile-time polymorphism and the foundation of the standard library.