Week 10: Building Hierarchies & Flexible Code
Learn about inheritance and polymorphism, key pillars of Object-Oriented Programming in C++.
Explore Chapter 10Chapter 10: Inheritance and Polymorphism
Inheritance: Creating "Is-A" Relationships.
Inheritance is a fundamental OOP mechanism that allows a new class (the derived class or child class) to inherit properties (member variables) and behaviors (member functions) from an existing class (the base class or parent class).
This represents an "is-a" relationship (e.g., a `Dog` is an `Animal`, a `Car` is a `Vehicle`). Inheritance promotes code reusability and helps create logical hierarchies.
Syntax
class BaseClassName {
// ... members ...
};
class DerivedClassName : access_specifier BaseClassName {
// ... additional members specific to DerivedClass ...
};
- The derived class name is followed by a colon (`:`), an access specifier (`public`, `protected`, or `private`), and the base class name.
- `public` inheritance is the most common type. It means public members of the base class become public members of the derived class, and protected members of the base class become protected members of the derived class. Private members of the base class are inherited but are not directly accessible by the derived class.
- `protected` and `private` inheritance are less common and change how base class members are accessible in the derived class and further down the hierarchy.
Example
#include <iostream>
#include <string>
// Base class
class Animal {
public:
std::string name;
void eat() {
std::cout << name << " is eating." << std::endl;
}
};
// Derived class (inherits publicly from Animal)
class Dog : public Animal {
public:
void bark() {
std::cout << name << " says Woof!" << std::endl; // Can access public 'name' from Animal
}
};
int main() {
Dog myDog;
myDog.name = "Rex"; // Access inherited member
myDog.eat(); // Call inherited method
myDog.bark(); // Call derived class method
return 0;
}
Method Overriding.
A derived class can provide its own specific implementation of a member function that is already defined in its base class. This is called method overriding. The function in the derived class must have the same name, same return type, and same parameters as the function in the base class.
Overriding allows a derived class to customize or specialize the behavior inherited from the base class.
class Bird : public Animal { // Inheriting from Animal class above
public:
// Override the eat method from Animal
void eat() {
std::cout << name << " is pecking at seeds." << std::endl;
}
void fly() {
std::cout << name << " is flying." << std::endl;
}
};
int main() {
Dog dog1;
dog1.name = "Buddy";
Bird bird1;
bird1.name = "Sky";
dog1.eat(); // Output: Buddy is eating. (Calls Animal's eat)
bird1.eat(); // Output: Sky is pecking at seeds. (Calls Bird's overridden eat)
return 0;
}
To achieve true runtime polymorphism (deciding which version of the method to call based on the object's actual type, not just the pointer/reference type), we often use virtual functions, which we'll discuss next.
Polymorphism: "Many Forms".
Polymorphism is a core OOP concept that allows objects of different classes to be treated as objects of a common base class. More specifically, it enables you to call the same member function on different objects and have each object respond in its own way (based on its specific class implementation).
In C++, polymorphism is primarily achieved through:
- Function Overloading: (Compile-time polymorphism) Multiple functions with the same name but different parameters. The compiler selects the correct function based on the arguments provided at compile time. (Covered in Week 5).
- Method Overriding with Virtual Functions: (Runtime polymorphism) Allows a derived class to provide its own implementation of a base class function. When called through a base class pointer or reference, the correct derived class version is executed at runtime.
Runtime polymorphism is particularly powerful for creating flexible and extensible systems where you can work with objects through a base class interface without needing to know their specific derived type.
Virtual Functions (`virtual`) and Abstract Classes.
Virtual Functions
To enable runtime polymorphism for an overridden method, the method must be declared as `virtual` in the base class.
When a `virtual` function is called via a pointer or reference to the base class, C++ determines at runtime which version of the function to execute based on the actual type of the object being pointed/referred to.
#include <iostream>
#include <string>
class BaseShape {
public:
// Declare draw() as virtual in the base class
virtual void draw() {
std::cout << "Drawing a generic BaseShape" << std::endl;
}
// Virtual destructor is important when dealing with inheritance and pointers!
virtual ~BaseShape() {} // Good practice!
};
class Circle : public BaseShape {
public:
// Override the virtual function (using 'override' keyword is good practice C++11+)
void draw() override {
std::cout << "Drawing a Circle" << std::endl;
}
};
class Square : public BaseShape {
public:
void draw() override {
std::cout << "Drawing a Square" << std::endl;
}
};
// Function that works with any BaseShape pointer
void drawAnyShape(BaseShape* shapePtr) {
shapePtr->draw(); // Calls the correct draw() based on the actual object type
}
int main() {
BaseShape* shape1 = new Circle();
BaseShape* shape2 = new Square();
BaseShape* shape3 = new BaseShape();
drawAnyShape(shape1); // Output: Drawing a Circle
drawAnyShape(shape2); // Output: Drawing a Square
drawAnyShape(shape3); // Output: Drawing a generic BaseShape
delete shape1; // Clean up dynamic memory
delete shape2;
delete shape3;
return 0;
}
Abstract Classes and Pure Virtual Functions
Sometimes, a base class represents an abstract concept for which creating objects doesn't make sense (e.g., a generic `Shape`). You might want to force derived classes to provide their own implementation for certain methods.
- A pure virtual function is declared by appending `= 0` to a virtual function declaration in the base class. It has no implementation in the base class.
- A class containing one or more pure virtual functions is called an abstract class.
- You cannot create objects (instances) of an abstract class directly.
- Any derived class must override all pure virtual functions inherited from its base class, or it too becomes abstract.
class AbstractShape { // Abstract base class
public:
// Pure virtual function - must be overridden by derived classes
virtual double getArea() = 0;
virtual ~AbstractShape() {} // Virtual destructor still needed
};
class ConcreteCircle : public AbstractShape {
private: double radius;
public:
ConcreteCircle(double r) : radius(r) {}
double getArea() override { // MUST implement getArea()
return 3.14159 * radius * radius;
}
};
// AbstractShape shape_obj; // Error! Cannot instantiate abstract class
ConcreteCircle circle_obj(5.0); // OK
Abstract classes define interfaces that derived classes must adhere to, enforcing a common structure.