CPP Series-6: Advanced OOP Concepts
Introduction
Building on our understanding of basic OOP, this article delves into advanced concepts such as inheritance and polymorphism. These tools allow us to create flexible, extensible software architectures that mirror real-world systems.
Inheritance in Depth
Inheritance enables new classes to inherit attributes and methods from existing classes, promoting code reuse.
Example Code: AdvancedInheritance.
#include <iostream>
#include <string>
class Employee {
protected:
std::string name;
int id;
public:
Employee(const std::string& name, int id) : name(name), id(id) {}
virtual void displayInfo() const {
std::cout << "Name: " << name << ", ID: " << id << std::endl;
}
};
class Manager : public Employee {
private:
int teamSize;
public:
Manager(const std::string& name, int id, int teamSize)
: Employee(name, id), teamSize(teamSize) {}
void displayInfo() const override {
Employee::displayInfo();
std::cout << "Team Size: " << teamSize << std::endl;
}
};
int main() {
Manager mgr("Alice", 101, 5);
mgr.displayInfo();
return 0;
}Explanation
- Protected Members: protected allows derived classes to access base class members.
- Overriding: displayInfo is overridden in Manager to provide additional information.
Compiler-Level Insights
- Virtual Tables: Used to enable dynamic dispatch of overridden functions.
- Access Control: The compiler enforces access control, preventing unauthorized access to base class members.
Common Errors and Solutions
- Slicing: Avoid object slicing by using pointers or references to base class objects.
- Ambiguity: Use virtual inheritance to resolve ambiguity in multiple inheritance scenarios.
Do’s and Don’ts
- Do: Use protected members when subclasses need access to base class data.
- Don’t: Expose internal implementation details unnecessarily. Keep data private unless needed.
Polymorphism in Practice
Polymorphism allows methods to be invoked based on the actual object type, not the reference type.
Example Code: PolymorphismExample.
#include <iostream>
#include <vector>
#include <string>
class Animal {
public:
virtual void speak() const = 0; // Pure virtual function
};
class Dog : public Animal {
public:
void speak() const override {
std::cout << "Woof!" << std::endl;
}
};
class Cat : public Animal {
public:
void speak() const override {
std::cout << "Meow!" << std::endl;
}
};
int main() {
std::vector<Animal*> animals;
animals.push_back(new Dog());
animals.push_back(new Cat());
for (const auto& animal : animals) {
animal->speak();
}
// Clean up
for (auto& animal : animals) {
delete animal;
}
return 0;
}Explanation
- Abstract Classes: Animal is an abstract class with a pure virtual function speak.
- Dynamic Dispatch: The correct speak method is called based on the object type.
Compiler-Level Insights
- Virtual Destructors: Ensure proper cleanup of derived class objects when deleted through a base class pointer.
- Polymorphic Base Classes: Always define a virtual destructor in base classes intended for polymorphic use.
Common Errors and Solutions
- Memory Leaks: Use smart pointers to automatically manage object lifetimes and prevent leaks.
- Undefined Behavior: Ensure virtual functions are overridden correctly to prevent undefined behavior.
Do’s and Don’ts
- Do: Use polymorphism to achieve flexibility and scalability in your design.
- Don’t: Forget to delete dynamically allocated memory. Consider smart pointers for automatic cleanup.
Abstract Classes and Interfaces
Abstract classes define interfaces for derived classes, ensuring consistent behavior.
Get Life is a SoC’s stories in your inbox
Join Medium for free to get updates from this writer.
Example Code: AbstractClass.
#include <iostream>
#include <string>
class Appliance {
public:
virtual void turnOn() const = 0;
virtual void turnOff() const = 0;
virtual ~Appliance() = default; // Virtual destructor
};
class Fan : public Appliance {
public:
void turnOn() const override {
std::cout << "Fan is now on." << std::endl;
}
void turnOff() const override {
std::cout << "Fan is now off." << std::endl;
}
};
int main() {
Fan myFan;
myFan.turnOn();
myFan.turnOff();
return 0;
}Explanation
- Virtual Destructor: Ensures proper cleanup of derived class objects.
- Interface Enforcement: Appliance enforces a common interface for all derived classes.
Compiler-Level Insights
- Abstract Base Classes: Used to define a common interface without providing implementation.
- Virtual Destructor: Ensures derived class destructors are called when objects are deleted through a base class pointer.
Common Errors and Solutions
- Missing Overrides: Ensure all pure virtual functions are implemented in derived classes.
- Object Instantiation: Avoid instantiating abstract classes directly.
Do’s and Don’ts
- Do: Use abstract classes to define common interfaces and enforce consistent behavior.
- Don’t: Forget to declare destructors as virtual in base classes. This ensures derived class destructors are called.
Project 1: Implement Inheritance and Polymorphism in the Command-Line Tool
Objective
Extend the User class with a PremiumUser class that offers additional features.
Code: PremiumUser.h
#ifndef PREMIUMUSER_H
#define PREMIUMUSER_H
#include "User.h"
class PremiumUser : public User {
private:
bool premiumStatus;
public:
PremiumUser(const std::string& name, int age, bool premiumStatus)
: User(name, age), premiumStatus(premiumStatus) {}
void display() const override {
User::display();
std::cout << "Premium Status: " << (premiumStatus ? "Active" : "Inactive") << std::endl;
}
};
#endifCode: main.
#include <iostream>
#include "PremiumUser.h"
int main() {
PremiumUser user("Bob", 25, true);
user.display();
return 0;
}Explanation
- Inheritance: PremiumUser extends User and adds a premiumStatus attribute.
- Overriding: The display method is overridden to show additional information.
Common Scenarios and Solutions
- Dynamic Casting: Use dynamic_cast for safely casting pointers to derived classes.
- Composition vs. Inheritance: Consider composition for “has-a” relationships instead of inheritance.
Project 2: Implement Polymorphism in Task Management
Objective
Implement polymorphism to manage different types of tasks.
Code: Task.h
#ifndef TASK_H
#define TASK_H
#include <iostream>
#include <string>
class Task {
public:
virtual void display() const = 0;
virtual ~Task() = default;
};
class BasicTask : public Task {
private:
std::string name;
std::string dueDate;
int priority;
public:
BasicTask(const std::string& name, const std::string& dueDate, int priority)
: name(name), dueDate(dueDate), priority(priority) {}
void display() const override {
std::cout << "Task: " << name << ", Due: " << dueDate << ", Priority: " << priority << std::endl;
}
};
class RecurringTask : public Task {
private:
std::string name;
std::string interval;
public:
RecurringTask(const std::string& name, const std::string& interval)
: name(name), interval(interval) {}
void display() const override {
std::cout << "Recurring Task: " << name << ", Interval: " << interval << std::endl;
}
};
#endifCode: TaskManager.h
#ifndef TASKMANAGER_H
#define TASKMANAGER_H
#include <iostream>
#include <vector>
#include <memory>
#include "Task.h"
class TaskManager {
private:
std::vector<std::unique_ptr<Task>> tasks;
public:
void addTask(std::unique_ptr<Task> task) {
tasks.push_back(std::move(task));
}
void displayTasks() const {
for (const auto& task : tasks) {
task->display();
}
}
void completeTask(int index) {
if (index >= 0 && index < tasks.size()) {
tasks.erase(tasks.begin() + index);
std::cout << "Task completed and removed from the list." << std::endl;
} else {
std::cout << "Invalid task index." << std::endl;
}
}
};
#endifCode: main.
#include <iostream>
#include "TaskManager.h"
int main() {
TaskManager manager;
manager.addTask(std::make_unique<BasicTask>("Finish report", "2023–08–01", 1));
manager.addTask(std::make_unique<RecurringTask>("Weekly meeting", "Every Monday"));
std::cout << "Tasks:" << std::endl;
manager.displayTasks();
manager.completeTask(0);
std::cout << "Tasks after completion:" << std::endl;
manager.displayTasks();
return 0;
}Explanation
- Polymorphism: Task is an abstract class, and BasicTask and RecurringTask provide specific implementations.
- Smart Pointers: std::unique_ptr manages task lifetimes and ensures proper cleanup.
Common Scenarios and Solutions
- Interface Segregation: Design interfaces to meet specific needs and avoid large, monolithic interfaces.
- Polymorphic Containers: Use containers to store pointers to base class objects for polymorphic behavior.
Conclusion
This article explored advanced OOP concepts, including inheritance, polymorphism, and abstract classes. These tools enhance software flexibility and extensibility, crucial for developing robust applications. In the next article, we’ll delve into pointers and memory management, key aspects of efficient C++ programming.






