Site

Relationships — Inheritance, Composition, Aggregation

Tutorial 7.0  •  C++ / Learn

7.0 What This Teaches

Classes relate to each other in four fundamental ways. This tutorial covers:

7.1 Inheritance

class Animal {
public:
    std::string name;
    explicit Animal(std::string n) : name(std::move(n)) {}
    virtual ~Animal() = default;   // virtual destructor required for polymorphism
};

class Dog : public Animal {        // Dog is-an Animal; public makes base interface visible
public:
    explicit Dog(std::string n) : Animal(std::move(n)) {}
};
Derived classes inherit all non-private members of the base class. The base constructor is called explicitly in the derived member initializer list. With class, inheritance defaults to private; always write : public Base to preserve the base class's public interface.
// access control
class Base {
public:    int pub   = 1;   // accessible everywhere
protected: int prot  = 2;   // accessible inside Base and derived classes
private:   int priv  = 3;   // accessible only inside Base
};

7.2 Virtual Functions and Polymorphism

class Shape {
public:
    virtual double area() const { return 0.0; }
    virtual ~Shape() = default;
};

class Circle : public Shape {
public:
    double radius;
    explicit Circle(double r) : radius(r) {}
    double area() const override { return 3.14159 * radius * radius; }
};

class Rect : public Shape {
public:
    double w, h;
    Rect(double w, double h) : w(w), h(h) {}
    double area() const override { return w * h; }
};

// polymorphism: base-class pointer dispatches to derived override at runtime
void print_area(const Shape& s) {
    std::cout << s.area() << "\n";   // calls Circle::area or Rect::area
}

Circle c(3.0);
Rect   r(4.0, 5.0);
print_area(c);   // 28.274...
print_area(r);   // 20
Always mark the base destructor virtual when the class is used polymorphically. Without it, deleting a derived object through a base pointer only calls the base destructor, leaking derived resources. Use override on every derived override - it lets the compiler catch typos and signature mismatches.

7.3 Abstract Base Classes

class Drawable {
public:
    virtual void draw() const = 0;   // pure virtual: must be overridden
    virtual ~Drawable() = default;
};

// Drawable cannot be instantiated directly:
// Drawable d;   // error: abstract class

class Square : public Drawable {
public:
    void draw() const override { std::cout << "drawing square\n"; }
};

Square sq;
sq.draw();   // ok
A class with at least one pure virtual function is abstract and cannot be instantiated. It defines an interface that all concrete subclasses must fulfill. This is C++'s primary mechanism for programming to interfaces rather than implementations.

7.4 Composition

class Engine {
public:
    int horsepower;
    explicit Engine(int hp) : horsepower(hp) {}
};

class Car {
public:
    Engine engine;       // composed member: owned, lifetime tied to Car
    std::string model;
    Car(std::string m, int hp) : model(std::move(m)), engine(hp) {}
};

Car car("Sedan", 200);
std::cout << car.engine.horsepower << "\n";   // 200
// engine destroyed when car goes out of scope
Composition models a strong has-a relationship. The member is created with the owner and destroyed with it. Stored by value or unique_ptr when polymorphism is needed.

7.5 Aggregation

class Manager {
public:
    std::string name;
    explicit Manager(std::string n) : name(std::move(n)) {}
};

class Department {
public:
    const Manager* head;   // non-owning: Manager exists independently
    std::string dept;
    Department(std::string d, const Manager* m) : dept(std::move(d)), head(m) {}
};

Manager alice("Alice");          // lifetime independent of Department
Department eng("Engineering", &alice);
std::cout << eng.head->name << "\n";   // Alice
Aggregation models a weak has-a relationship. The referenced object is created and destroyed independently of the owner. Use a raw (non-owning) pointer or reference. Ensure the referenced object outlives the aggregating class to avoid dangling pointers.

7.6 Using Relationships

// uses-a: function depends on Logger but does not store or own it
void process(const std::vector<int>& data, Logger& log) {
    log.write("processing " + std::to_string(data.size()) + " items");
    // ... use data ...
}

// uses-a: local variable used temporarily within a scope
void run() {
    Timer t;           // used locally; no lasting relationship
    do_work();
    std::cout << t.elapsed() << "\n";
}
A using relationship is the weakest coupling - one type appears as a parameter type, local variable, or return type. No member is stored. Prefer this over stronger couplings whenever the dependency is transient.

7.7 Example - All Together

// Relationships - Inheritance, Composition, Aggregation, Using.

#include <iostream>
#include <string>
#include <vector>
#include <memory>

class Shape {
public:
    std::string color;
    explicit Shape(std::string c) : color(std::move(c)) {}
    virtual double area() const { return 0.0; }
    virtual void describe() const {
        std::cout << color << " shape, area=" << area() << "\n";
    }
    virtual ~Shape() = default;
};

class Circle : public Shape {
public:
    double radius;
    Circle(std::string c, double r) : Shape(std::move(c)), radius(r) {}
    double area() const override { return 3.14159 * radius * radius; }
};

class Rect : public Shape {
public:
    double w, h;
    Rect(std::string c, double w, double h) : Shape(std::move(c)), w(w), h(h) {}
    double area() const override { return w * h; }
};

class Engine { public: int hp; explicit Engine(int h) : hp(h) {} };

class Car {
public:
    Engine engine;
    std::string model;
    Car(std::string m, int hp) : model(std::move(m)), engine(hp) {}
};

class Manager { public: std::string name; explicit Manager(std::string n) : name(std::move(n)) {} };
class Department {
public:
    const Manager* head;
    std::string dept;
    Department(std::string d, const Manager* m) : dept(std::move(d)), head(m) {}
};

void print_area(const Shape& s) { std::cout << s.color << " area=" << s.area() << "\n"; }

int main() {
    std::cout << "--- inheritance ---\n";
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>("red", 3.0));
    shapes.push_back(std::make_unique<Rect>("blue", 4.0, 5.0));
    for (const auto& s : shapes) s->describe();

    std::cout << "\n--- composition ---\n";
    Car car("Sedan", 200);
    std::cout << car.model << " " << car.engine.hp << "hp\n";

    std::cout << "\n--- aggregation ---\n";
    Manager mgr("Alice");
    Department dept("Engineering", &mgr);
    std::cout << dept.dept << " head: " << dept.head->name << "\n";

    std::cout << "\n--- using ---\n";
    Circle c("green", 2.0);
    print_area(c);
    return 0;
}
--- inheritance ---
red shape, area=28.2743
blue shape, area=20

--- composition ---
Sedan 200hp

--- aggregation ---
Engineering head: Alice

--- using ---
green area=12.5664

7.8 Exercise

Exercise
  • Define an abstract base class Vehicle with a pure virtual fuel_type(). Derive ElectricCar and GasTruck from it and store them in a vector<unique_ptr<Vehicle>>.
  • Add a Battery class and compose it into ElectricCar by value. Confirm its destructor runs when the car is destroyed.
  • Create a Fleet class that holds a non-owning const Vehicle* to a lead vehicle (aggregation). Show that the Fleet can be destroyed while the vehicle lives on.

7.9 Common Mistakes

Missing virtual destructor

class Base { public: virtual void f() {} };   // no virtual ~Base
class Derived : public Base { public: std::string data; };

Base* p = new Derived();
delete p;   // undefined behavior: ~Derived never called, data leaked
Any class used as a polymorphic base must declare virtual ~Base() = default;.

Object slicing

Circle c("red", 3.0);
Shape s = c;      // sliced: only the Shape part is copied
s.area();         // calls Shape::area(), not Circle::area()
Polymorphism only works through pointers or references. Assigning a derived object to a base value slices off the derived part.

Forgetting override

class Derived : public Base {
public:
    void f() const {}   // silently defines a new function if Base::f() is not const
                        // add override to get a compile error instead
};
Always write override. The compiler will then reject any signature mismatch rather than silently creating a new function.

7.10 Key Terms

TermMeaning
inheritance (is-a)Derived class extends base; inherits members and can override virtual functions
virtualMarks a function for runtime dispatch through base-class pointer or reference
overrideConfirms a function overrides a base virtual; compile error if signature mismatches
pure virtual (= 0)Must be overridden; makes the class abstract (non-instantiable)
virtual destructorRequired on any polymorphic base to ensure correct cleanup of derived objects
composition (has-a owned)Member stored by value or unique_ptr; lifetime tied to owner
aggregation (has-a non-owned)Member stored as raw pointer/reference; lifetime independent of owner
using (uses-a)Transient dependency via parameter or local variable; no stored member
object slicingCopying a derived to a base value loses the derived portion; use pointer or reference