What the capstone combines
This lesson uses most of the chapter in one small program: an abstract parent class, three concrete subclasses, and one loop that works on all of them without knowing which is which. The structure is the usual shape of object-oriented code: a small contract at the top, concrete variants below it, and the rest of the program written against the contract.
Five ideas appear in order:
- Inheritance.
Dog,CatandBirdall derive fromAnimal. - Constructors. Each subclass receives a name and hands it to the parent constructor.
- A pure virtual function.
Animaldeclaressound()with= 0, so every concrete subclass must supply its own. - Shared behaviour.
Animal::speak()has a body that callssound(), so all subclasses get it for free. - Polymorphism. A collection of
Animal*holds different kinds of animals, andspeak()reaches the rightsound()at run time.
The parent class
The lesson provides Animal:
class Animal {
public:
string name;
Animal(string name) { this->name = name; }
virtual string sound() = 0;
virtual ~Animal() = default;
void speak() {
cout << name << ": " << sound() << endl;
}
};
Notice that speak() is an ordinary function, but it calls sound(), which is virtual. Inside speak(), the call to sound() goes through the current object, and a virtual call is resolved by the object's real type. So speak() is written once and prints the right sound for every subclass.
Because sound() is pure virtual, Animal is abstract and Animal a("x"); doesn't compile. The virtual destructor matters because the program deletes objects through Animal* pointers.
Step by step
Step 1: three subclasses
Each subclass needs three things:
- public inheritance, written
: public Animal; - a constructor that passes the name to the parent in its initializer list,
Dog(string name) : Animal(name) {}. The parent has no default constructor, so a subclass that doesn't callAnimal(name)explicitly doesn't compile; - an override of
sound()that returns its text, markedoverrideso the compiler checks that it matches the parent's declaration.
If a subclass skips sound(), it stays abstract and new Dog("Rex") is an error.
Step 2: the polymorphic collection
In mainFunction, build a vector<Animal*> holding a new Dog("Rex"), a new Cat("Whiskers") and a new Bird("Tweety"), in that order. Then loop over it and call speak() on each pointer with ->. A second loop deletes every object, because each one was created with new.
The solution in C++
class Animal {
public:
string name;
Animal(string name) { this->name = name; }
virtual string sound() = 0;
virtual ~Animal() = default;
void speak() {
cout << name << ": " << sound() << endl;
}
};
class Dog : public Animal {
public:
Dog(string name) : Animal(name) {}
string sound() override { return "Woof"; }
};
class Cat : public Animal {
public:
Cat(string name) : Animal(name) {}
string sound() override { return "Meow"; }
};
class Bird : public Animal {
public:
Bird(string name) : Animal(name) {}
string sound() override { return "Tweet"; }
};
class Solution {
public:
void mainFunction() {
vector<Animal*> animals = { new Dog("Rex"), new Cat("Whiskers"), new Bird("Tweety") };
for (Animal* a : animals) {
a->speak();
}
for (Animal* a : animals) delete a;
}
};
It prints:
Rex: Woof
Whiskers: Meow
Tweety: Tweet
C++ details worth knowing
- Why pointers. A
vector<Animal>couldn't even compile here, becauseAnimalis abstract. With a concrete base class it would compile but cause slicing: copying aDoginto anAnimalkeeps only theAnimalpart, and the dog's behaviour is lost. Pointers (or references) keep the real object intact. - Initializer list. The part after the colon runs before the constructor body. Base-class constructors are called there, which is the only place to pass arguments to them.
- Ownership. The vector holds raw pointers, so it does not free the objects. Each
newneeds a matchingdelete. Deleting throughAnimal*is safe only because the destructor is virtual. - Smart pointers. In larger programs,
vector<unique_ptr<Animal>>withmake_unique<Dog>("Rex")frees the objects automatically and removes the second loop. The lesson uses raw pointers to show the mechanics. - Passing by value. A function taking
Animalby value can't accept aDogat all whenAnimalis abstract. Take a reference or pointer.
Edge cases and common mistakes
- Forgetting to override
sound(). The subclass remains abstract, and creating it fails to compile. - Forgetting the initializer list.
Dog(string name) {}tries to callAnimal(), which doesn't exist. - Wrong order in the vector. The output follows the order of insertion, so Rex, Whiskers and Tweety must be created in that order.
- Skipping the delete loop. The output looks correct, but the three objects are never freed.
- A non-virtual destructor. Deleting a
Dogthrough anAnimal*then has undefined behaviour. - Misspelling in an override.
string Sound() overridefails to compile because it overrides nothing. Withoutoverride, the typo would create a new function and leave the class abstract with a less helpful error.
Why this design scales
Adding a Cow means writing one more subclass and one more new Cow("Daisy"). The speak function and the loop are untouched, because they only know about Animal. That is the benefit the capstone is meant to show.
Key takeaways
- An abstract parent declares the contract with pure virtual functions and can offer shared functions built on them.
- Subclasses forward constructor arguments with an initializer list and mark their overrides with
override. - Store base-class pointers (or references) to get polymorphism, and give the base a virtual destructor.
- Every
newneeds adelete, or an owning smart pointer.