CPP Series-5: Object-Oriented Programming (OOP) in C++
Introduction
Object-Oriented Programming (OOP) is a paradigm that models real-world entities as objects in code. C++ supports OOP, allowing us to encapsulate data and behavior within classes. This article explores OOP principles and applies them to our ongoing projects.
Core Principles of OOP
Encapsulation
Encapsulation is the bundling of data and methods that operate on the data within a single unit, or class.
Example Code: Encapsulation.
#include <iostream>
#include <string>
class Car {
private:
std::string model;
int year;
public:
Car(const std::string& model, int year) : model(model), year(year) {}
void displayInfo() const {
std::cout << "Model: " << model << ", Year: " << year << std::endl;
}
};
int main() {
Car myCar("Toyota", 2020);
myCar.displayInfo();
return 0;
}Explanation
- Class: Defines a blueprint for objects with attributes (model, year) and methods (displayInfo).
- Access Specifiers: Control access to class members (private, public).
Compiler-Level Insights
- Access Control: Enforced by the compiler to prevent unauthorized access to private data.
- Constructor Overloading: Allows defining multiple constructors with different parameters.
Common Errors and Solutions
- Access Violations: Ensure private members are accessed through public methods.
- Uninitialized Members: Use constructors to initialize all class members.
Do’s and Don’ts
- Do: Use access specifiers to enforce encapsulation. Keep data members private and provide public methods for access.
- Don’t: Expose internal data directly unless necessary. Use getter and setter methods instead.
Abstraction
Abstraction hides complex implementation details, exposing only the necessary functionality.
Inheritance
Inheritance allows a class to acquire properties and methods from another class.
Example Code: Inheritance.
#include <iostream>
#include <string>
class Vehicle {
public:
void start() const {
std::cout << "Vehicle started" << std::endl;
}
};
class Car : public Vehicle {
public:
void honk() const {
std::cout << "Car honking" << std::endl;
}
};
int main() {
Car myCar;
myCar.start();
myCar.honk();
return 0;
}Explanation
- Base Class: Vehicle provides common functionality.
- Derived Class: Car inherits from Vehicle and adds its own behavior.
Compiler-Level Insights
- Virtual Tables: Used to support dynamic dispatch in polymorphic hierarchies.
- Inheritance Hierarchies: Compilers can optimize inheritance by collapsing hierarchies.
Common Errors and Solutions
- Ambiguity: Avoid multiple inheritance unless necessary. It can lead to ambiguity and complexity.
- Access Control: Ensure derived classes only access base class members as permitted.
Do’s and Don’ts
- Do: Use inheritance to model “is-a” relationships. Ensure the derived class can function as a base class.
- Don’t: Use inheritance for code reuse alone. Consider composition as an alternative.
Polymorphism
Polymorphism enables objects of different classes to be treated as objects of a common base class.
Get Life is a SoC’s stories in your inbox
Join Medium for free to get updates from this writer.
Example Code: Polymorphism.
#include <iostream>
#include <string>
class Shape {
public:
virtual void draw() const = 0; // Pure virtual function
};
class Circle : public Shape {
public:
void draw() const override {
std::cout << "Drawing a circle" << std::endl;
}
};
class Square : public Shape {
public:
void draw() const override {
std::cout << "Drawing a square" << std::endl;
}
};
int main() {
Circle circle;
Square square;
Shape* shapes[] = {&circle, &square};
for (const auto& shape : shapes) {
shape->draw();
}
return 0;
}Explanation
- Virtual Functions: Allow derived classes to provide specific implementations.
- Polymorphic Arrays: Store pointers to base class objects and invoke overridden methods.
Compiler-Level Insights
- Dynamic Dispatch: Uses virtual tables (vtables) to resolve function calls at runtime.
- Pure Virtual Functions: Define interfaces that must be implemented by derived classes.
Common Errors and Solutions
- Missing Overrides: Ensure all pure virtual functions are overridden in derived classes.
- Object Slicing: Use pointers or references to base class objects to avoid slicing.
Do’s and Don’ts
- Do: Use polymorphism to create flexible and extensible systems.
- Don’t: Overuse polymorphism where simpler solutions suffice. Consider the trade-offs between flexibility and complexity.
Project 1: Implement Classes in the Command-Line Tool
Objective
Create a User class to manage user information, applying OOP principles.
Code: User.h
#ifndef USER_H
#define USER_H
#include <iostream>
#include <string>
class User {
private:
std::string name;
int age;
public:
User(const std::string& name, int age) : name(name), age(age) {}
void display() const {
std::cout << "User: " << name << ", Age: " << age << std::endl;
}
const std::string& getName() const { return name; }
int getAge() const { return age; }
};
#endifCode: main.
#include <iostream>
#include "User.h"
int main() {
User user("Alice", 30);
user.display();
return 0;
}Explanation
- Class Definition: User class encapsulates user attributes (name, age) and methods (display).
- Integration: The User class is integrated into the project, demonstrating encapsulation.
Common Scenarios and Solutions
- Default Constructors: Define default constructors if the class will be instantiated without parameters.
- Copy Constructors: Define copy constructors for classes that manage dynamic resources.
Do’s and Don’ts
- Do: Use classes to encapsulate data and functionality logically.
- Don’t: Overcomplicate class design. Start simple and extend as necessary.
Project 2: Add OOP to Task Management
Objective
Implement a Task class and extend functionality using OOP principles.
Code: Task.h
#ifndef TASK_H
#define TASK_H
#include <iostream>
#include <string>
class Task {
private:
std::string name;
std::string dueDate;
int priority;
public:
Task(const std::string& name, const std::string& dueDate, int priority)
: name(name), dueDate(dueDate), priority(priority) {}
void display() const {
std::cout << "Task: " << name << ", Due: " << dueDate << ", Priority: " << priority << std::endl;
}
const std::string& getName() const { return name; }
const std::string& getDueDate() const { return dueDate; }
int getPriority() const { return priority; }
};
#endif
Code: TaskManager.h
#ifndef TASKMANAGER_H
#define TASKMANAGER_H
#include <iostream>
#include <vector>
#include <string>
#include "Task.h"
class TaskManager {
private:
std::vector<Task> tasks;
public:
void addTask(const std::string& name, const std::string& dueDate, int priority) {
tasks.emplace_back(name, dueDate, priority);
}
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("Finish report", "2023–08–01", 1);
manager.addTask("Prepare presentation", "2023–08–02", 2);
std::cout << "Tasks:" << std::endl;
manager.displayTasks();
manager.completeTask(0);
std::cout << "Tasks after completion:" << std::endl;
manager.displayTasks();
return 0;
}Explanation
- Task Class: Encapsulates task attributes and provides methods for displaying information.
- TaskManager Class: Manages tasks using a vector and provides operations for adding, displaying, and completing tasks.
Common Scenarios and Solutions
- Inheritance: Use inheritance to extend functionality, such as creating a RecurringTask class.
- Polymorphism: Implement polymorphism to handle different task types uniformly.
Conclusion
This article introduced Object-Oriented Programming, a paradigm that enhances code organization and reusability. By applying OOP principles, we improved our command-line tool and task manager’s design, making them more scalable and maintainable. In the next article, we will explore advanced OOP concepts like inheritance and polymorphism.






