Command Palette

Search for a command to run...

MTH 4300

Lecture 13

Base and Derived Classes

Inheritance lets one class (the derived class) extend another (the base class). The derived class automatically gets the base class's public and protected members, and can add its own.

Inheritance syntax
class Animal {
public:
    Animal(const char* name) : name_(name) {}
    const char* name() const { return name_; }

protected:
    const char* name_;
};

class Dog : public Animal {
public:
    Dog(const char* name) : Animal(name) {} // chain to base constructor
    void bark() const { std::cout << name_ << " says: woof\n"; }
};

Dog inherits name() and name_ from Animal. It adds bark() on its own.

protected Access

protected members are visible to the class itself and to its derived classes, but not to outside code:

protected
class Animal {
protected:
    const char* name_; // Dog can see this; outside code cannot
};

class Dog : public Animal {
public:
    void bark() const {
        std::cout << name_ << ": woof\n"; // ok — Dog inherits protected access
    }
};

Constructor Chaining

A derived class constructor must initialize the base part of the object. It does this by calling the base class constructor in the member initializer list:

Constructor chaining
class Dog : public Animal {
public:
    Dog(const char* name, const char* breed)
        : Animal(name),    // initialize Animal subobject first
          breed_(breed) {} // then initialize Dog's own members

    const char* breed() const { return breed_; }

private:
    const char* breed_;
};

Base constructors always run first, before any of the derived class's member initializers.

Method Hiding vs Overriding

A derived class can define a method with the same name as one in the base class. Without virtual, the call is resolved at compile time based on the static type of the pointer or reference — this is hiding, not overriding:

Hiding — static dispatch
class Animal {
public:
    void speak() const { std::cout << "...\n"; }
};

class Cat : public Animal {
public:
    void speak() const { std::cout << "meow\n"; }
};

Animal* a = new Cat;
a->speak(); // prints "..." — Animal::speak, because a has type Animal*

The compiler sees Animal* and calls Animal::speak, ignoring that a actually points to a Cat. To get dynamic dispatch — the call going to the right type at runtime — you need virtual.

Virtual Functions

Marking a method virtual in the base class tells the compiler to dispatch based on the actual runtime type of the object, not the static pointer type:

virtual — dynamic dispatch
class Animal {
public:
    virtual void speak() const { std::cout << "...\n"; }
    virtual ~Animal() {}
};

class Cat : public Animal {
public:
    void speak() const override { std::cout << "meow\n"; }
};

class Dog : public Animal {
public:
    void speak() const override { std::cout << "woof\n"; }
};

Animal* a = new Cat;
Animal* b = new Dog;
a->speak(); // meow — Cat::speak at runtime
b->speak(); // woof — Dog::speak at runtime

The compiler stores a hidden vtable (virtual function table) in each polymorphic class. A virtual call looks up the right function pointer at runtime.

The override Keyword

override tells the compiler "this is intentionally overriding a base class virtual." If the signature doesn't match any base virtual, it's a compile error:

override catches mistakes
class Cat : public Animal {
public:
    void speak() const override {}  // ok — matches Animal::speak
    void Speak() const override {}  // compile error — no virtual Speak in Animal
};

Always write override on derived methods. It catches typos and signature mismatches that would otherwise silently become hiding instead of overriding.

Abstract Classes and Pure Virtual Functions

A pure virtual function has no definition in the base class and forces derived classes to provide one:

Pure virtual and abstract class
class Shape {
public:
    virtual double area()     const = 0; // pure virtual — "= 0" means no definition
    virtual double perimeter() const = 0;
    virtual ~Shape() {}
};

A class with at least one pure virtual function is abstract — it cannot be instantiated directly, only through derived classes that implement all pure virtuals.

Concrete derived classes
#include <cmath>

class Circle : public Shape {
public:
    Circle(double r) : radius_(r) {}
    double area()      const override { return M_PI * radius_ * radius_; }
    double perimeter() const override { return 2 * M_PI * radius_; }
private:
    double radius_;
};

class Rectangle : public Shape {
public:
    Rectangle(double w, double h) : w_(w), h_(h) {}
    double area()      const override { return w_ * h_; }
    double perimeter() const override { return 2 * (w_ + h_); }
private:
    double w_, h_;
};

// Shape s;      // compile error — abstract
Circle c(5.0);  // ok

Virtual Destructors

When deleting a derived object through a base pointer, the compiler calls the base destructor — unless it is virtual:

Non-virtual destructor — resource leak
class Base {
public:
    ~Base() { std::cout << "~Base\n"; } // not virtual
};

class Derived : public Base {
public:
    Derived() : data_(new int[10]) {}
    ~Derived() { delete[] data_; std::cout << "~Derived\n"; }
private:
    int* data_;
};

Base* p = new Derived;
delete p; // only ~Base runs — ~Derived never called; data_ leaks

Marking the base destructor virtual fixes this:

Virtual destructor — correct cleanup
class Base {
public:
    virtual ~Base() {} // virtual — ensures the right destructor chain runs
};

Any class intended to be used polymorphically — with derived objects held through base pointers — must have a virtual destructor.

Extending StringPrefixedString

We now add a virtual display() method to String and introduce PrefixedString, a derived class that prepends a label when displaying.

First, add to String:

String — adding virtual display()
class String {
public:
    // ... all prior members ...

    virtual void display() const {
        std::cout << data_ << "\n";
    }

    virtual ~String() { // must be virtual now that String is a base class
        delete[] data_;
    }
};

Making the destructor virtual means the compiler no longer generates a default one. We must define it explicitly.

PrefixedString stores a prefix using our own String type and overrides display():

PrefixedString
class PrefixedString : public String {
public:
    PrefixedString(const char* prefix, const char* s)
        : String(s), prefix_(prefix) {}

    void display() const override {
        std::cout << prefix_.c_str() << c_str() << "\n";
    }

private:
    String prefix_; // uses our own String — no manual memory management needed
};

Because prefix_ is a String object (not a raw pointer), its copy/move/destroy operations are handled automatically by String's existing special members. PrefixedString needs no destructor of its own.

Polymorphic usage
#include <iostream>

int main() {
    String*  a = new String("hello");
    String*  b = new PrefixedString("[info] ", "something happened");

    a->display(); // hello
    b->display(); // [info] something happened

    delete a;
    delete b; // ~PrefixedString, then ~String — correct because ~String is virtual

    return 0;
}

Exercises

Q1.

Without running the code, predict what this program prints.

#include <iostream>

class Animal {
public:
    Animal(const char* name) : name_(name) {
        std::cout << "Animal(" << name_ << ")\n";
    }
    virtual void speak() const { std::cout << name_ << ": ...\n"; }
    virtual ~Animal() { std::cout << "~Animal(" << name_ << ")\n"; }
protected:
    const char* name_;
};

class Cat : public Animal {
public:
    Cat(const char* name) : Animal(name) {
        std::cout << "Cat(" << name_ << ")\n";
    }
    void speak() const override { std::cout << name_ << ": meow\n"; }
    ~Cat() { std::cout << "~Cat(" << name_ << ")\n"; }
};

int main() {
    Animal* a = new Cat("Luna");
    a->speak();
    delete a;
    return 0;
}

Q2.

Without running the code, predict what this program prints.

#include <iostream>

class Shape {
public:
    virtual double area() const = 0;
    virtual ~Shape() {}
};

class Rectangle : public Shape {
public:
    Rectangle(double w, double h) : w_(w), h_(h) {}
    double area() const override { return w_ * h_; }
private:
    double w_, h_;
};

class Square : public Rectangle {
public:
    Square(double s) : Rectangle(s, s) {}
};

int main() {
    Shape* shapes[3];
    shapes[0] = new Rectangle(3.0, 4.0);
    shapes[1] = new Square(5.0);
    shapes[2] = new Rectangle(2.0, 7.0);

    for (int i = 0; i < 3; i++) {
        std::cout << shapes[i]->area() << "\n";
        delete shapes[i];
    }

    return 0;
}

Q3.

Each snippet has a bug related to inheritance. Identify what's wrong.

// Snippet A — non-virtual destructor with polymorphic use
class Base {
public:
    ~Base() {}
};
class Derived : public Base {
public:
    Derived() : data_(new int[10]) {}
    ~Derived() { delete[] data_; }
private:
    int* data_;
};
Base* p = new Derived;
delete p;

// Snippet B — object slicing
class Animal {
public:
    virtual void speak() const { std::cout << "...\n"; }
};
class Dog : public Animal {
public:
    void speak() const override { std::cout << "woof\n"; }
};
Dog d;
Animal a = d;  // copy
a.speak();

// Snippet C — hiding instead of overriding
class Base {
public:
    virtual void foo() const {}
};
class Derived : public Base {
public:
    void foo() override {} // missing const
};

// Snippet D — instantiating an abstract class
class Shape {
public:
    virtual double area() const = 0;
};
Shape s;

Q4.

Extend the Animal hierarchy: add a Cat class and a Parrot class that override speak(). Then write a function make_noise(Animal** animals, int n) that calls speak() on each.

#include <iostream>

class Animal {
public:
    Animal(const char* name) : name_(name) {}
    virtual void speak() const = 0;
    virtual ~Animal() {}
    const char* name() const { return name_; }
protected:
    const char* name_;
};

class Cat : public Animal {
    // TODO
};

class Parrot : public Animal {
    // TODO: speak() prints "<name>: squawk"
};

void make_noise(Animal** animals, int n) {
    // TODO: call speak() on each
}

int main() {
    Animal* animals[3] = {
        new Cat("Luna"),
        new Parrot("Polly"),
        new Cat("Mochi"),
    };

    make_noise(animals, 3);

    for (int i = 0; i < 3; i++) delete animals[i];
    return 0;
}

Q5.

Add a virtual void display() const method to String that prints the contents followed by a newline. Then write a PrefixedString class that inherits from String, stores a String prefix, and overrides display() to print <prefix><contents>\n.

// Add to String:
virtual void display() const;
virtual ~String(); // make destructor virtual

// Implement:
void String::display() const {
    // TODO
}

// Write PrefixedString:
class PrefixedString : public String {
    // TODO
};

int main() {
    String*  a = new String("hello");
    String*  b = new PrefixedString("[LOG] ", "server started");

    a->display(); // hello
    b->display(); // [LOG] server started

    delete a;
    delete b;
    return 0;
}

Practice Problems