Object-Oriented Programming
Master the principles of OOP in C++
Introduction to OOP
Object-Oriented Programming (OOP) is a programming paradigm that organizes code around objects and classes rather than functions and logic. It models real-world entities as software objects that contain both data (attributes) and functions (methods) that operate on that data.
Why Object-Oriented Programming?
Code Reusability
Write once, use many times. Classes can be reused across different parts of your application and even in different projects.
Modularity
Break complex problems into smaller, manageable pieces. Each class handles a specific responsibility.
Maintainability
Easier to modify and update code. Changes in one class don't affect others when properly designed.
Real-World Modeling
Map real-world entities directly to code structures, making programs more intuitive and natural.
Four Pillars of OOP:
Encapsulation
Bundling data and methods that operate on that data within a single unit (class). It also involves data hiding - keeping internal details private.
Inheritance
Creating new classes based on existing classes, inheriting their properties and methods. Enables the "IS-A" relationship.
Polymorphism
The ability of objects to take multiple forms and behave differently based on context. "Many forms, one interface."
Abstraction
Hiding complex implementation details while showing only essential features. Focus on what an object does, not how it does it.
OOP vs Procedural Programming:
Real-World OOP Examples:
School Management System
- Classes: Student, Teacher, Course, Classroom
- Inheritance: Person → Student, Teacher
- Encapsulation: Student grades are private
- Polymorphism: Different types of courses
Game Development
- Classes: Player, Enemy, Weapon, GameObject
- Inheritance: GameObject → Player, Enemy
- Encapsulation: Player health and stats
- Polymorphism: Different enemy behaviors
E-Commerce Platform
- Classes: Product, Customer, Order, Payment
- Inheritance: Payment → CreditCard, PayPal
- Encapsulation: Customer personal data
- Polymorphism: Different payment methods
Key Insight: OOP is not just about syntax - it's a way of thinking about problems. Start by identifying the "things" (objects) in your problem domain, then determine their properties and behaviors.
Ready to Begin: You've learned the basics of C++, now it's time to organize your code using Object-Oriented principles. Let's start building classes and objects!
Classes & Objects
A class is a blueprint or template for creating objects. An object is an instance of a class.
Class Syntax:
class ClassName {
private:
// private members
public:
// public members
};
#include <iostream>
#include <string>
using namespace std;
// Class definition
class Student {
private:
string name;
int age;
double gpa;
public:
// Constructor
Student(string n, int a, double g) {
name = n;
age = a;
gpa = g;
}
// Member functions (methods)
void displayInfo() {
cout << "Name: " << name << endl;
cout << "Age: " << age << endl;
cout << "GPA: " << gpa << endl;
}
// Getter methods
string getName() { return name; }
int getAge() { return age; }
double getGPA() { return gpa; }
// Setter methods
void setName(string n) { name = n; }
void setAge(int a) { age = a; }
void setGPA(double g) { gpa = g; }
// Method to check if student is honor roll
bool isHonorRoll() {
return gpa >= 3.5;
}
};
int main() {
// Creating objects (instances of the class)
Student student1("Alice Johnson", 20, 3.8);
Student student2("Bob Smith", 19, 3.2);
// Using object methods
cout << "Student 1 Information:" << endl;
student1.displayInfo();
cout << "Honor Roll: " << (student1.isHonorRoll() ? "Yes" : "No") << endl;
cout << endl;
cout << "Student 2 Information:" << endl;
student2.displayInfo();
cout << "Honor Roll: " << (student2.isHonorRoll() ? "Yes" : "No") << endl;
// Modifying object data using setter methods
student2.setGPA(3.7);
cout << "\nAfter GPA update:" << endl;
student2.displayInfo();
return 0;
}
Key Concepts:
- Class: A user-defined data type that serves as a blueprint
- Object: An instance of a class with actual values
- Member Variables: Data stored within the class
- Member Functions: Functions that operate on the class data
- Instantiation: The process of creating an object from a class
Constructors & Destructors
Constructors are special methods called when an object is created. Destructors are called when an object is destroyed.
Types of Constructors:
- Default Constructor: Takes no parameters
- Parameterized Constructor: Takes parameters to initialize object
- Copy Constructor: Creates a copy of another object
#include <iostream>
#include <string>
using namespace std;
class Rectangle {
private:
double length;
double width;
string color;
public:
// Default constructor
Rectangle() {
length = 1.0;
width = 1.0;
color = "white";
cout << "Default constructor called" << endl;
}
// Parameterized constructor
Rectangle(double l, double w, string c) {
length = l;
width = w;
color = c;
cout << "Parameterized constructor called" << endl;
}
// Copy constructor
Rectangle(const Rectangle &rect) {
length = rect.length;
width = rect.width;
color = rect.color;
cout << "Copy constructor called" << endl;
}
// Destructor
~Rectangle() {
cout << "Destructor called for " << color << " rectangle" << endl;
}
// Member functions
double getArea() {
return length * width;
}
double getPerimeter() {
return 2 * (length + width);
}
void displayInfo() {
cout << "Rectangle - Length: " << length
<< ", Width: " << width
<< ", Color: " << color
<< ", Area: " << getArea() << endl;
}
};
int main() {
cout << "Creating rectangle1 with default constructor:" << endl;
Rectangle rectangle1; // Default constructor
rectangle1.displayInfo();
cout << "\nCreating rectangle2 with parameterized constructor:" << endl;
Rectangle rectangle2(5.0, 3.0, "blue"); // Parameterized constructor
rectangle2.displayInfo();
cout << "\nCreating rectangle3 with copy constructor:" << endl;
Rectangle rectangle3 = rectangle2; // Copy constructor
rectangle3.displayInfo();
cout << "\nProgram ending - destructors will be called:" << endl;
return 0;
}
Constructor Initialization List:
A more efficient way to initialize member variables:
#include <iostream>
#include <string>
using namespace std;
class Person {
private:
const int id; // const member must be initialized
string name;
int age;
public:
// Constructor with initialization list
Person(int i, string n, int a) : id(i), name(n), age(a) {
cout << "Person created with ID: " << id << endl;
}
void displayInfo() {
cout << "ID: " << id << ", Name: " << name << ", Age: " << age << endl;
}
};
int main() {
Person person1(101, "John Doe", 25);
person1.displayInfo();
return 0;
}
Important: Destructors are automatically called when objects go out of scope or are explicitly deleted. They're used for cleanup operations like freeing memory.
Access Modifiers
Access modifiers control the visibility and accessibility of class members.
Private
Members are accessible only within the same class
- Default access level for class members
- Provides data hiding
- Accessed through public methods
Public
Members are accessible from anywhere
- Can be accessed by any code
- Forms the interface of the class
- Should be used carefully
Protected
Members are accessible within the class and its derived classes
- Used in inheritance
- More restrictive than public
- Less restrictive than private
#include <iostream>
#include <string>
using namespace std;
class BankAccount {
private:
double balance; // Private - can't be accessed directly
string accountNumber; // Private - sensitive information
protected:
string bankName; // Protected - accessible to derived classes
public:
string ownerName; // Public - can be accessed directly
// Constructor
BankAccount(string owner, string accNum, double initialBalance) {
ownerName = owner;
accountNumber = accNum;
balance = initialBalance;
bankName = "Global Bank";
}
// Public methods to access private members
double getBalance() {
return balance;
}
string getAccountNumber() {
return accountNumber;
}
void deposit(double amount) {
if (amount > 0) {
balance += amount;
cout << "Deposited $" << amount << ". New balance: $" << balance << endl;
}
}
bool withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
cout << "Withdrew $" << amount << ". New balance: $" << balance << endl;
return true;
} else {
cout << "Invalid withdrawal amount or insufficient funds!" << endl;
return false;
}
}
void displayAccountInfo() {
cout << "Account Owner: " << ownerName << endl;
cout << "Account Number: " << accountNumber << endl;
cout << "Bank: " << bankName << endl;
cout << "Balance: $" << balance << endl;
}
};
// Derived class to demonstrate protected access
class SavingsAccount : public BankAccount {
private:
double interestRate;
public:
SavingsAccount(string owner, string accNum, double initialBalance, double rate)
: BankAccount(owner, accNum, initialBalance) {
interestRate = rate;
}
void addInterest() {
double interest = getBalance() * interestRate / 100;
deposit(interest);
cout << "Interest added: $" << interest << endl;
}
void displayBankInfo() {
// Can access protected member from base class
cout << "This account is with: " << bankName << endl;
}
};
int main() {
BankAccount account1("John Smith", "ACC001", 1000.0);
// Accessing public members
cout << "Account owner: " << account1.ownerName << endl;
// Accessing private members through public methods
cout << "Balance: $" << account1.getBalance() << endl;
// Using public methods
account1.deposit(500.0);
account1.withdraw(200.0);
account1.displayAccountInfo();
cout << "\n--- Savings Account ---" << endl;
SavingsAccount savings("Jane Doe", "SAV001", 2000.0, 2.5);
savings.displayAccountInfo();
savings.addInterest();
savings.displayBankInfo(); // Accessing protected member through derived class
// The following would cause compilation errors:
// cout << account1.balance; // Error: private member
// cout << account1.accountNumber; // Error: private member
// cout << account1.bankName; // Error: protected member (not accessible here)
return 0;
}
Best Practice: Keep data members private and provide public methods (getters/setters) to access them. This ensures data integrity and encapsulation.
Member Functions
Member functions are functions that belong to a class and operate on the class's data members. They define the behavior of objects.
Types of Member Functions:
Accessor Functions (Getters)
Functions that return the value of private data members
int getAge() const {
return age; // Return private member
}
Mutator Functions (Setters)
Functions that modify the value of private data members
void setAge(int newAge) {
if (newAge >= 0) {
age = newAge;
}
}
Utility Functions
Functions that perform operations on the object's data
double calculateBMI() {
return weight / (height * height);
}
Const Member Functions:
Functions that promise not to modify the object's state
#include <iostream>
#include <string>
using namespace std;
class Circle {
private:
double radius;
public:
Circle(double r) : radius(r) {}
// Const member function - doesn't modify the object
double getArea() const {
return 3.14159 * radius * radius;
}
// Const member function
double getRadius() const {
return radius;
}
// Non-const member function - can modify the object
void setRadius(double r) {
radius = r;
}
// Const member function that displays info
void display() const {
cout << "Circle with radius: " << radius
<< ", Area: " << getArea() << endl;
}
};
int main() {
Circle circle(5.0);
// Calling const member functions
cout << "Radius: " << circle.getRadius() << endl;
cout << "Area: " << circle.getArea() << endl;
circle.display();
// Modifying the object
circle.setRadius(7.0);
circle.display();
// Const object can only call const member functions
const Circle constCircle(3.0);
cout << "Const circle area: " << constCircle.getArea() << endl;
// constCircle.setRadius(4.0); // Error! Cannot call non-const function
return 0;
}
Best Practice: Mark member functions as const whenever they don't modify the object's state. This enables better optimization and allows const objects to use these functions.
Static Members
Static members belong to the class itself rather than to any specific object. They are shared among all instances of the class.
Static Data Members:
Class-level variables shared by all objects of the class
Static Member Functions:
Functions that can be called without creating an object of the class
#include <iostream>
#include <string>
using namespace std;
class Student {
private:
string name;
int id;
static int nextId; // Static data member
static int studentCount; // Static data member
public:
// Constructor
Student(string n) : name(n) {
id = nextId++;
studentCount++;
cout << "Student " << name << " created with ID: " << id << endl;
}
// Destructor
~Student() {
studentCount--;
cout << "Student " << name << " destroyed" << endl;
}
// Regular member function
void displayInfo() const {
cout << "Student: " << name << " (ID: " << id << ")" << endl;
}
// Static member function
static int getStudentCount() {
return studentCount;
}
// Static member function
static int getNextId() {
return nextId;
}
// Static member function to display class info
static void displayClassInfo() {
cout << "Total students created: " << studentCount << endl;
cout << "Next ID will be: " << nextId << endl;
}
};
// Initialize static members outside the class
int Student::nextId = 1001;
int Student::studentCount = 0;
int main() {
// Call static function without creating object
cout << "Initial student count: " << Student::getStudentCount() << endl;
Student::displayClassInfo();
cout << endl;
// Create objects
Student s1("Alice");
Student s2("Bob");
Student s3("Charlie");
cout << "\nAfter creating students:" << endl;
Student::displayClassInfo();
cout << "Current student count: " << Student::getStudentCount() << endl;
// Display individual student info
cout << "\nStudent Information:" << endl;
s1.displayInfo();
s2.displayInfo();
s3.displayInfo();
{
Student s4("David");
cout << "\nAfter creating David:" << endl;
cout << "Current student count: " << Student::getStudentCount() << endl;
} // s4 goes out of scope here
cout << "\nAfter David goes out of scope:" << endl;
cout << "Current student count: " << Student::getStudentCount() << endl;
return 0;
}
Key Points about Static Members:
Shared Data
Static data members are shared among all objects of the class
Single Copy
Only one copy exists in memory regardless of object count
Class Scope
Static functions can only access static members directly
External Access
Can be accessed using class name without object instance
Important: Static data members must be defined outside the class and initialized before use. Static functions cannot access non-static members directly.
Inheritance
Inheritance allows a class to inherit properties and methods from another class, promoting code reusability.
Types of Inheritance:
Single Inheritance
One derived class inherits from one base class
Multiple Inheritance
One derived class inherits from multiple base classes
Multilevel Inheritance
A derived class becomes base class for another class
Hierarchical Inheritance
Multiple derived classes inherit from one base class
#include <iostream>
#include <string>
using namespace std;
// Base class (Parent class)
class Vehicle {
protected:
string brand;
string model;
int year;
public:
Vehicle(string b, string m, int y) : brand(b), model(m), year(y) {
cout << "Vehicle constructor called" << endl;
}
void displayBasicInfo() {
cout << "Brand: " << brand << endl;
cout << "Model: " << model << endl;
cout << "Year: " << year << endl;
}
virtual void start() { // Virtual function for polymorphism
cout << "Vehicle is starting..." << endl;
}
virtual ~Vehicle() { // Virtual destructor
cout << "Vehicle destructor called" << endl;
}
};
// Derived class (Child class) - Single Inheritance
class Car : public Vehicle {
private:
int doors;
string fuelType;
public:
Car(string b, string m, int y, int d, string fuel)
: Vehicle(b, m, y), doors(d), fuelType(fuel) {
cout << "Car constructor called" << endl;
}
void displayCarInfo() {
displayBasicInfo(); // Inherited method
cout << "Doors: " << doors << endl;
cout << "Fuel Type: " << fuelType << endl;
}
void start() override { // Override base class method
cout << "Car engine is starting with a key..." << endl;
}
void honk() { // Car-specific method
cout << "Beep beep!" << endl;
}
~Car() {
cout << "Car destructor called" << endl;
}
};
// Another derived class - Single Inheritance
class Motorcycle : public Vehicle {
private:
bool hasSidecar;
public:
Motorcycle(string b, string m, int y, bool sidecar)
: Vehicle(b, m, y), hasSidecar(sidecar) {
cout << "Motorcycle constructor called" << endl;
}
void displayMotorcycleInfo() {
displayBasicInfo();
cout << "Has Sidecar: " << (hasSidecar ? "Yes" : "No") << endl;
}
void start() override {
cout << "Motorcycle is kick-starting..." << endl;
}
void wheelie() {
cout << "Performing a wheelie!" << endl;
}
~Motorcycle() {
cout << "Motorcycle destructor called" << endl;
}
};
// Multilevel Inheritance - SportsCar inherits from Car
class SportsCar : public Car {
private:
int topSpeed;
bool hasTurbo;
public:
SportsCar(string b, string m, int y, int d, string fuel, int speed, bool turbo)
: Car(b, m, y, d, fuel), topSpeed(speed), hasTurbo(turbo) {
cout << "SportsCar constructor called" << endl;
}
void displaySportsCarInfo() {
displayCarInfo(); // Inherited from Car
cout << "Top Speed: " << topSpeed << " mph" << endl;
cout << "Has Turbo: " << (hasTurbo ? "Yes" : "No") << endl;
}
void start() override {
cout << "Sports car engine roaring to life!" << endl;
}
void activateTurbo() {
if (hasTurbo) {
cout << "Turbo activated! Maximum power!" << endl;
} else {
cout << "No turbo available." << endl;
}
}
~SportsCar() {
cout << "SportsCar destructor called" << endl;
}
};
int main() {
cout << "=== Creating a Car ===" << endl;
Car myCar("Toyota", "Camry", 2022, 4, "Gasoline");
myCar.displayCarInfo();
myCar.start();
myCar.honk();
cout << "\n=== Creating a Motorcycle ===" << endl;
Motorcycle myBike("Harley-Davidson", "Street 750", 2021, false);
myBike.displayMotorcycleInfo();
myBike.start();
myBike.wheelie();
cout << "\n=== Creating a Sports Car ===" << endl;
SportsCar mySportsCar("Ferrari", "488 GTB", 2023, 2, "Gasoline", 205, true);
mySportsCar.displaySportsCarInfo();
mySportsCar.start();
mySportsCar.activateTurbo();
cout << "\n=== Program ending - destructors will be called ===" << endl;
return 0;
}
Access Specifiers in Inheritance:
Base Class | Public Inheritance | Protected Inheritance | Private Inheritance |
---|---|---|---|
Public | Public | Protected | Private |
Protected | Protected | Protected | Private |
Private | Not Accessible | Not Accessible | Not Accessible |
Polymorphism
Polymorphism allows objects of different types to be treated as objects of a common base type, with the ability to call the appropriate method based on the actual object type.
Types of Polymorphism:
Compile-time Polymorphism
- Function Overloading
- Operator Overloading
Runtime Polymorphism
- Virtual Functions
- Function Overriding
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
// Base class with virtual functions
class Shape {
protected:
string color;
public:
Shape(string c) : color(c) {}
// Pure virtual function makes this an abstract class
virtual double getArea() = 0;
virtual double getPerimeter() = 0;
// Virtual function with default implementation
virtual void display() {
cout << "This is a " << color << " shape" << endl;
}
// Virtual destructor
virtual ~Shape() {
cout << "Shape destructor called" << endl;
}
};
// Derived class - Circle
class Circle : public Shape {
private:
double radius;
public:
Circle(string c, double r) : Shape(c), radius(r) {}
double getArea() override {
return 3.14159 * radius * radius;
}
double getPerimeter() override {
return 2 * 3.14159 * radius;
}
void display() override {
cout << "Circle with radius " << radius << " and color " << color << endl;
}
~Circle() {
cout << "Circle destructor called" << endl;
}
};
// Derived class - Rectangle
class Rectangle : public Shape {
private:
double length, width;
public:
Rectangle(string c, double l, double w) : Shape(c), length(l), width(w) {}
double getArea() override {
return length * width;
}
double getPerimeter() override {
return 2 * (length + width);
}
void display() override {
cout << "Rectangle " << length << "x" << width << " with color " << color << endl;
}
~Rectangle() {
cout << "Rectangle destructor called" << endl;
}
};
// Derived class - Triangle
class Triangle : public Shape {
private:
double side1, side2, side3;
public:
Triangle(string c, double s1, double s2, double s3)
: Shape(c), side1(s1), side2(s2), side3(s3) {}
double getArea() override {
// Using Heron's formula
double s = (side1 + side2 + side3) / 2;
return sqrt(s * (s - side1) * (s - side2) * (s - side3));
}
double getPerimeter() override {
return side1 + side2 + side3;
}
void display() override {
cout << "Triangle with sides " << side1 << ", " << side2
<< ", " << side3 << " and color " << color << endl;
}
~Triangle() {
cout << "Triangle destructor called" << endl;
}
};
// Function demonstrating polymorphism
void printShapeInfo(Shape* shape) {
shape->display();
cout << "Area: " << shape->getArea() << endl;
cout << "Perimeter: " << shape->getPerimeter() << endl;
cout << "------------------------" << endl;
}
// Function overloading example (compile-time polymorphism)
class Calculator {
public:
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
int add(int a, int b, int c) {
return a + b + c;
}
string add(string a, string b) {
return a + b;
}
};
int main() {
cout << "=== Runtime Polymorphism Example ===" << endl;
// Create objects using smart pointers
vector<unique_ptr<Shape>> shapes;
shapes.push_back(make_unique<Circle>("red", 5.0));
shapes.push_back(make_unique<Rectangle>("blue", 4.0, 6.0));
shapes.push_back(make_unique<Triangle>("green", 3.0, 4.0, 5.0));
// Polymorphic behavior - same interface, different implementations
for (auto& shape : shapes) {
printShapeInfo(shape.get());
}
cout << "\n=== Compile-time Polymorphism (Function Overloading) ===" << endl;
Calculator calc;
cout << "add(5, 3) = " << calc.add(5, 3) << endl;
cout << "add(5.5, 3.2) = " << calc.add(5.5, 3.2) << endl;
cout << "add(1, 2, 3) = " << calc.add(1, 2, 3) << endl;
cout << "add(\"Hello\", \" World\") = " << calc.add(string("Hello"), string(" World")) << endl;
return 0;
}
Key Point: Virtual functions enable runtime polymorphism, allowing the correct function to be called based on the actual object type, not the pointer type.
Virtual Functions
Virtual functions enable runtime polymorphism in C++. They allow a program to call the appropriate function based on the actual type of the object, not the type of the pointer or reference.
Types of Virtual Functions:
- Virtual Function: Can be overridden in derived classes
- Pure Virtual Function: Must be overridden in derived classes
- Virtual Destructor: Ensures proper cleanup in inheritance hierarchies
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
// Base class with virtual functions
class Animal {
protected:
string name;
int age;
public:
Animal(const string& animalName, int animalAge) : name(animalName), age(animalAge) {
cout << "Animal constructor: " << name << endl;
}
// Virtual function - can be overridden
virtual void makeSound() {
cout << name << " makes a generic animal sound." << endl;
}
// Virtual function with default implementation
virtual void move() {
cout << name << " moves around." << endl;
}
// Pure virtual function - must be overridden
virtual void eat() = 0;
// Virtual function for displaying information
virtual void displayInfo() {
cout << "Name: " << name << ", Age: " << age << " years" << endl;
}
// Virtual destructor - important for proper cleanup
virtual ~Animal() {
cout << "Animal destructor: " << name << endl;
}
};
class Dog : public Animal {
private:
string breed;
public:
Dog(const string& dogName, int dogAge, const string& dogBreed)
: Animal(dogName, dogAge), breed(dogBreed) {
cout << "Dog constructor: " << name << endl;
}
// Override virtual function
void makeSound() override {
cout << name << " barks: Woof! Woof!" << endl;
}
// Override virtual function
void move() override {
cout << name << " runs around wagging its tail." << endl;
}
// Implement pure virtual function
void eat() override {
cout << name << " eats dog food and treats." << endl;
}
// Override and extend displayInfo
void displayInfo() override {
Animal::displayInfo(); // Call base class version
cout << "Breed: " << breed << endl;
}
// Dog-specific method
void fetch() {
cout << name << " fetches the ball!" << endl;
}
~Dog() {
cout << "Dog destructor: " << name << endl;
}
};
class Cat : public Animal {
private:
bool isIndoor;
public:
Cat(const string& catName, int catAge, bool indoor)
: Animal(catName, catAge), isIndoor(indoor) {
cout << "Cat constructor: " << name << endl;
}
// Override virtual function
void makeSound() override {
cout << name << " meows: Meow! Meow!" << endl;
}
// Override virtual function
void move() override {
cout << name << " gracefully jumps and climbs." << endl;
}
// Implement pure virtual function
void eat() override {
cout << name << " eats cat food and fish." << endl;
}
// Override and extend displayInfo
void displayInfo() override {
Animal::displayInfo();
cout << "Indoor cat: " << (isIndoor ? "Yes" : "No") << endl;
}
// Cat-specific method
void purr() {
cout << name << " purrs contentedly." << endl;
}
~Cat() {
cout << "Cat destructor: " << name << endl;
}
};
class Bird : public Animal {
private:
bool canFly;
public:
Bird(const string& birdName, int birdAge, bool flying)
: Animal(birdName, birdAge), canFly(flying) {
cout << "Bird constructor: " << name << endl;
}
// Override virtual function
void makeSound() override {
cout << name << " chirps: Tweet! Tweet!" << endl;
}
// Override virtual function
void move() override {
if (canFly) {
cout << name << " flies through the sky." << endl;
} else {
cout << name << " hops around on the ground." << endl;
}
}
// Implement pure virtual function
void eat() override {
cout << name << " eats seeds and insects." << endl;
}
// Override and extend displayInfo
void displayInfo() override {
Animal::displayInfo();
cout << "Can fly: " << (canFly ? "Yes" : "No") << endl;
}
// Bird-specific method
void buildNest() {
cout << name << " builds a cozy nest." << endl;
}
~Bird() {
cout << "Bird destructor: " << name << endl;
}
};
// Function demonstrating polymorphism with virtual functions
void animalActions(Animal* animal) {
cout << "\n--- Animal Actions ---" << endl;
animal->displayInfo();
animal->makeSound();
animal->move();
animal->eat();
cout << "----------------------" << endl;
}
// Demonstrating virtual function table (vtable) concept
void demonstrateVirtualFunctions() {
cout << "\n=== Virtual Functions Demonstration ===" << endl;
// Create animals using base class pointers
vector> animals;
animals.push_back(make_unique("Buddy", 3, "Golden Retriever"));
animals.push_back(make_unique("Whiskers", 2, true));
animals.push_back(make_unique("Robin", 1, true));
// Polymorphic behavior - calls the appropriate derived class methods
for (auto& animal : animals) {
animalActions(animal.get());
}
}
// Function without virtual functions (static binding)
class NonVirtualBase {
public:
void show() {
cout << "NonVirtualBase::show()" << endl;
}
};
class NonVirtualDerived : public NonVirtualBase {
public:
void show() {
cout << "NonVirtualDerived::show()" << endl;
}
};
void demonstrateNonVirtual() {
cout << "\n=== Non-Virtual Functions (Static Binding) ===" << endl;
NonVirtualBase* ptr = new NonVirtualDerived();
ptr->show(); // Calls NonVirtualBase::show() - not what we might expect!
delete ptr;
}
int main() {
cout << "=== Virtual Functions Example ===" << endl;
// Demonstrate virtual functions
demonstrateVirtualFunctions();
// Show the difference with non-virtual functions
demonstrateNonVirtual();
cout << "\n=== Program ending - destructors will be called ===" << endl;
return 0;
}
Virtual Function Table (vtable):
Each class with virtual functions has a virtual function table (vtable) that contains pointers to the virtual functions. When a virtual function is called, the program looks up the correct function in the vtable.
How vtable works:
- Each object has a pointer to its class's vtable
- Virtual function calls are resolved at runtime
- The correct function is called based on the object's actual type
Rules for Virtual Functions:
- Virtual functions cannot be static
- Virtual functions cannot be inline
- Constructors cannot be virtual
- Destructors should be virtual in base classes
- Pure virtual functions make the class abstract
Important: Always declare destructors as virtual in base classes to ensure proper cleanup when deleting objects through base class pointers.
Encapsulation
Encapsulation is the bundling of data and methods that operate on that data within a single unit, while restricting direct access to some of the object's components.
Benefits of Encapsulation:
- Data Hiding: Internal representation is hidden from outside
- Increased Security: Prevents unauthorized access to data
- Easy Maintenance: Code changes don't affect other parts
- Flexibility: Can change implementation without affecting users
#include <iostream>
#include <string>
using namespace std;
class Employee {
private:
// Encapsulated data members
int employeeId;
string name;
double salary;
string department;
bool isActive;
public:
// Constructor
Employee(int id, string empName, double sal, string dept) {
employeeId = id;
name = empName;
salary = sal;
department = dept;
isActive = true;
}
// Public methods to access private data (Getters)
int getEmployeeId() const { return employeeId; }
string getName() const { return name; }
double getSalary() const { return salary; }
string getDepartment() const { return department; }
bool getActiveStatus() const { return isActive; }
// Public methods to modify private data (Setters) with validation
void setName(const string& empName) {
if (!empName.empty()) {
name = empName;
} else {
cout << "Error: Name cannot be empty!" << endl;
}
}
void setSalary(double sal) {
if (sal >= 0) {
salary = sal;
} else {
cout << "Error: Salary cannot be negative!" << endl;
}
}
void setDepartment(const string& dept) {
if (!dept.empty()) {
department = dept;
} else {
cout << "Error: Department cannot be empty!" << endl;
}
}
// Method to deactivate employee
void deactivateEmployee() {
isActive = false;
cout << "Employee " << name << " has been deactivated." << endl;
}
// Method to give raise with business logic
void giveRaise(double percentage) {
if (percentage > 0 && percentage <= 50) {
double oldSalary = salary;
salary += salary * (percentage / 100);
cout << name << "'s salary increased from $" << oldSalary
<< " to $" << salary << " (" << percentage << "% raise)" << endl;
} else {
cout << "Error: Invalid raise percentage!" << endl;
}
}
// Method to display employee information
void displayEmployeeInfo() const {
cout << "Employee ID: " << employeeId << endl;
cout << "Name: " << name << endl;
cout << "Department: " << department << endl;
cout << "Salary: $" << salary << endl;
cout << "Status: " << (isActive ? "Active" : "Inactive") << endl;
cout << "------------------------" << endl;
}
};
// Demonstration of encapsulation
int main() {
cout << "=== Encapsulation Example ===" << endl;
Employee emp1(101, "John Smith", 50000, "Engineering");
Employee emp2(102, "Jane Doe", 55000, "Marketing");
// Display initial information
cout << "Initial Employee Information:" << endl;
emp1.displayEmployeeInfo();
emp2.displayEmployeeInfo();
// Using public methods to access and modify data
cout << "Accessing data through public methods:" << endl;
cout << "Employee 1 Name: " << emp1.getName() << endl;
cout << "Employee 1 Salary: $" << emp1.getSalary() << endl;
// Modifying data through setters (with validation)
cout << "\nModifying employee data:" << endl;
emp1.giveRaise(10); // 10% raise
emp2.setDepartment("Sales");
// Trying to set invalid data (validation in action)
cout << "\nTrying to set invalid data:" << endl;
emp1.setSalary(-1000); // This should show an error
emp2.setName(""); // This should show an error
// Final state
cout << "\nFinal Employee Information:" << endl;
emp1.displayEmployeeInfo();
emp2.displayEmployeeInfo();
// The following would cause compilation errors (encapsulation in action):
// emp1.salary = 100000; // Error: private member
// emp1.employeeId = 999; // Error: private member
// cout << emp1.isActive; // Error: private member
return 0;
}
Best Practice: Always keep data members private and provide controlled access through public methods. This ensures data integrity and allows for validation.
Abstraction
Abstraction is the process of hiding complex implementation details while showing only the essential features of an object. It focuses on what an object does rather than how it does it.
Types of Abstraction:
Data Abstraction
Hiding the internal representation of data
Control Abstraction
Hiding the implementation details of functions
#include <iostream>
#include <vector>
#include <string>
using namespace std;
// Abstract base class (interface)
class DatabaseConnection {
public:
// Pure virtual functions (abstract methods)
virtual bool connect(const string& connectionString) = 0;
virtual bool disconnect() = 0;
virtual bool executeQuery(const string& query) = 0;
virtual vector<string> fetchResults() = 0;
// Virtual destructor
virtual ~DatabaseConnection() = default;
// Common method with implementation
void showConnectionStatus() {
cout << "Database connection status checked." << endl;
}
};
// Concrete implementation for MySQL
class MySQLConnection : public DatabaseConnection {
private:
bool isConnected;
string serverAddress;
vector<string> queryResults;
// Private helper method (hidden implementation detail)
bool validateConnectionString(const string& connStr) {
return !connStr.empty() && connStr.find("mysql://") == 0;
}
public:
MySQLConnection() : isConnected(false) {}
bool connect(const string& connectionString) override {
if (validateConnectionString(connectionString)) {
serverAddress = connectionString;
isConnected = true;
cout << "Connected to MySQL database: " << serverAddress << endl;
return true;
}
cout << "Failed to connect to MySQL database!" << endl;
return false;
}
bool disconnect() override {
if (isConnected) {
isConnected = false;
cout << "Disconnected from MySQL database." << endl;
return true;
}
return false;
}
bool executeQuery(const string& query) override {
if (!isConnected) {
cout << "Error: Not connected to database!" << endl;
return false;
}
cout << "Executing MySQL query: " << query << endl;
// Simulate query execution and results
queryResults.clear();
queryResults.push_back("Result 1: MySQL data");
queryResults.push_back("Result 2: MySQL data");
return true;
}
vector<string> fetchResults() override {
return queryResults;
}
};
// Concrete implementation for PostgreSQL
class PostgreSQLConnection : public DatabaseConnection {
private:
bool isConnected;
string serverAddress;
vector<string> queryResults;
// Private helper method (different implementation)
bool establishConnection(const string& connStr) {
// PostgreSQL-specific connection logic
return !connStr.empty() && connStr.find("postgresql://") == 0;
}
public:
PostgreSQLConnection() : isConnected(false) {}
bool connect(const string& connectionString) override {
if (establishConnection(connectionString)) {
serverAddress = connectionString;
isConnected = true;
cout << "Connected to PostgreSQL database: " << serverAddress << endl;
return true;
}
cout << "Failed to connect to PostgreSQL database!" << endl;
return false;
}
bool disconnect() override {
if (isConnected) {
isConnected = false;
cout << "Disconnected from PostgreSQL database." << endl;
return true;
}
return false;
}
bool executeQuery(const string& query) override {
if (!isConnected) {
cout << "Error: Not connected to database!" << endl;
return false;
}
cout << "Executing PostgreSQL query: " << query << endl;
// Simulate query execution and results
queryResults.clear();
queryResults.push_back("Result 1: PostgreSQL data");
queryResults.push_back("Result 2: PostgreSQL data");
return true;
}
vector<string> fetchResults() override {
return queryResults;
}
};
// High-level database manager (uses abstraction)
class DatabaseManager {
private:
DatabaseConnection* dbConnection;
public:
DatabaseManager(DatabaseConnection* connection) : dbConnection(connection) {}
void performDatabaseOperations(const string& connectionString) {
// Client code doesn't need to know implementation details
if (dbConnection->connect(connectionString)) {
dbConnection->showConnectionStatus();
// Execute a query
if (dbConnection->executeQuery("SELECT * FROM users")) {
vector<string> results = dbConnection->fetchResults();
cout << "Query results:" << endl;
for (const auto& result : results) {
cout << "- " << result << endl;
}
}
dbConnection->disconnect();
}
}
~DatabaseManager() {
delete dbConnection;
}
};
int main() {
cout << "=== Abstraction Example ===" << endl;
// Using MySQL implementation
cout << "\n--- Using MySQL Database ---" << endl;
DatabaseManager mysqlManager(new MySQLConnection());
mysqlManager.performDatabaseOperations("mysql://localhost:3306/mydb");
// Using PostgreSQL implementation
cout << "\n--- Using PostgreSQL Database ---" << endl;
DatabaseManager postgresManager(new PostgreSQLConnection());
postgresManager.performDatabaseOperations("postgresql://localhost:5432/mydb");
// The client code (DatabaseManager) doesn't need to know
// the specific implementation details of MySQL or PostgreSQL
// It just works with the abstract interface
return 0;
}
Abstract Classes vs Interfaces:
Abstract Class | Interface (Pure Abstract Class) |
---|---|
Can have both abstract and concrete methods | All methods are pure virtual (abstract) |
Can have member variables | Usually no member variables |
Can have constructors | Usually no constructors |
Supports single inheritance in C++ | Can simulate multiple inheritance |
Remember: Abstraction helps in reducing complexity by hiding unnecessary details and showing only the relevant features to the user.
Operator Overloading
Operator overloading allows you to define custom behavior for operators when used with user-defined classes.
Why Operator Overloading?
It makes code more intuitive and readable. For example, adding two complex numbers with c1 + c2
instead of c1.add(c2)
.
#include <iostream>
#include <string>
using namespace std;
class Complex {
private:
double real;
double imaginary;
public:
// Constructor
Complex(double r = 0.0, double i = 0.0) : real(r), imaginary(i) {}
// Addition operator overloading
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imaginary + other.imaginary);
}
// Subtraction operator overloading
Complex operator-(const Complex& other) const {
return Complex(real - other.real, imaginary - other.imaginary);
}
// Multiplication operator overloading
Complex operator*(const Complex& other) const {
double r = real * other.real - imaginary * other.imaginary;
double i = real * other.imaginary + imaginary * other.real;
return Complex(r, i);
}
// Assignment operator overloading
Complex& operator=(const Complex& other) {
if (this != &other) { // Self-assignment check
real = other.real;
imaginary = other.imaginary;
}
return *this;
}
// Equality operator overloading
bool operator==(const Complex& other) const {
return (real == other.real) && (imaginary == other.imaginary);
}
// Inequality operator overloading
bool operator!=(const Complex& other) const {
return !(*this == other);
}
// Unary minus operator
Complex operator-() const {
return Complex(-real, -imaginary);
}
// Pre-increment operator
Complex& operator++() {
real++;
imaginary++;
return *this;
}
// Post-increment operator
Complex operator++(int) {
Complex temp = *this;
real++;
imaginary++;
return temp;
}
// Friend function for output stream operator
friend ostream& operator<<(ostream& os, const Complex& c) {
os << c.real;
if (c.imaginary >= 0) {
os << " + " << c.imaginary << "i";
} else {
os << " - " << (-c.imaginary) << "i";
}
return os;
}
// Friend function for input stream operator
friend istream& operator>>(istream& is, Complex& c) {
cout << "Enter real part: ";
is >> c.real;
cout << "Enter imaginary part: ";
is >> c.imaginary;
return is;
}
// Accessor methods
double getReal() const { return real; }
double getImaginary() const { return imaginary; }
};
int main() {
Complex c1(3.0, 4.0);
Complex c2(1.0, 2.0);
cout << "c1 = " << c1 << endl;
cout << "c2 = " << c2 << endl;
// Using overloaded operators
Complex c3 = c1 + c2;
cout << "c1 + c2 = " << c3 << endl;
Complex c4 = c1 - c2;
cout << "c1 - c2 = " << c4 << endl;
Complex c5 = c1 * c2;
cout << "c1 * c2 = " << c5 << endl;
// Unary operators
Complex c6 = -c1;
cout << "-c1 = " << c6 << endl;
// Increment operators
cout << "c1 before pre-increment: " << c1 << endl;
++c1;
cout << "c1 after pre-increment: " << c1 << endl;
Complex c7 = c2++;
cout << "c2 after post-increment: " << c2 << endl;
cout << "c7 (old value of c2): " << c7 << endl;
// Comparison operators
if (c1 == c2) {
cout << "c1 and c2 are equal" << endl;
} else {
cout << "c1 and c2 are not equal" << endl;
}
// Input operator (commented out for automatic execution)
// Complex userComplex;
// cout << "Enter a complex number:" << endl;
// cin >> userComplex;
// cout << "You entered: " << userComplex << endl;
return 0;
}
Types of Operator Overloading:
Member Functions
- Binary operators: +, -, *, /, ==, !=
- Unary operators: ++, --, -, !
- Assignment operators: =, +=, -=
Friend Functions
- Stream operators: <<, >>
- When left operand is not of class type
- When you need access to private members
Note: Some operators cannot be overloaded: ::, ., .*, ?:, sizeof, typeid
Friend Functions
Friend functions are non-member functions that have access to the private and protected members of a class.
When to Use Friend Functions:
Operator Overloading
When the left operand is not of the class type
Bridge Functions
Functions that need to access private data of multiple classes
Stream Operations
Input/output stream operators (<<, >>)
#include <iostream>
#include <string>
using namespace std;
class Rectangle; // Forward declaration
class Point {
private:
int x, y;
public:
Point(int xPos = 0, int yPos = 0) : x(xPos), y(yPos) {}
// Friend function declaration
friend void displayPoint(const Point& p);
friend double distance(const Point& p1, const Point& p2);
friend bool isInsideRectangle(const Point& p, const Rectangle& r);
// Getters for demonstration
int getX() const { return x; }
int getY() const { return y; }
};
class Rectangle {
private:
Point topLeft;
Point bottomRight;
public:
Rectangle(Point tl, Point br) : topLeft(tl), bottomRight(br) {}
// Friend function declaration
friend bool isInsideRectangle(const Point& p, const Rectangle& r);
friend void displayRectangle(const Rectangle& r);
// Regular member function
int getWidth() const {
return bottomRight.getX() - topLeft.getX();
}
int getHeight() const {
return bottomRight.getY() - topLeft.getY();
}
};
// Friend function implementations
void displayPoint(const Point& p) {
// Can access private members directly
cout << "Point(" << p.x << ", " << p.y << ")" << endl;
}
double distance(const Point& p1, const Point& p2) {
// Can access private members of both points
int dx = p1.x - p2.x;
int dy = p1.y - p2.y;
return sqrt(dx*dx + dy*dy);
}
bool isInsideRectangle(const Point& p, const Rectangle& r) {
// Can access private members of both classes
return (p.x >= r.topLeft.x && p.x <= r.bottomRight.x &&
p.y >= r.topLeft.y && p.y <= r.bottomRight.y);
}
void displayRectangle(const Rectangle& r) {
// Can access private members
cout << "Rectangle: TopLeft";
displayPoint(r.topLeft);
cout << " BottomRight";
displayPoint(r.bottomRight);
cout << " Width: " << r.bottomRight.x - r.topLeft.x
<< ", Height: " << r.bottomRight.y - r.topLeft.y << endl;
}
// Global friend function for demonstration
class Temperature {
private:
double celsius;
public:
Temperature(double c = 0.0) : celsius(c) {}
// Friend function for conversion
friend Temperature fahrenheitToCelsius(double f);
friend double celsiusToFahrenheit(const Temperature& t);
void display() const {
cout << celsius << "°C" << endl;
}
};
Temperature fahrenheitToCelsius(double f) {
Temperature temp;
temp.celsius = (f - 32.0) * 5.0 / 9.0;
return temp;
}
double celsiusToFahrenheit(const Temperature& t) {
return (t.celsius * 9.0 / 5.0) + 32.0;
}
int main() {
// Using friend functions with Point and Rectangle
Point p1(5, 10);
Point p2(8, 15);
cout << "=== Point Operations ===" << endl;
cout << "Point 1: ";
displayPoint(p1);
cout << "Point 2: ";
displayPoint(p2);
cout << "Distance between points: " << distance(p1, p2) << endl;
// Rectangle operations
Point topLeft(0, 0);
Point bottomRight(10, 20);
Rectangle rect(topLeft, bottomRight);
cout << "\n=== Rectangle Operations ===" << endl;
displayRectangle(rect);
// Check if points are inside rectangle
cout << "\nPoint inside rectangle check:" << endl;
cout << "Point 1 inside rectangle: "
<< (isInsideRectangle(p1, rect) ? "Yes" : "No") << endl;
cout << "Point 2 inside rectangle: "
<< (isInsideRectangle(p2, rect) ? "Yes" : "No") << endl;
// Temperature conversion example
cout << "\n=== Temperature Conversion ===" << endl;
Temperature temp1(25.0);
cout << "Temperature 1: ";
temp1.display();
cout << "In Fahrenheit: " << celsiusToFahrenheit(temp1) << "°F" << endl;
Temperature temp2 = fahrenheitToCelsius(98.6);
cout << "98.6°F in Celsius: ";
temp2.display();
return 0;
}
Friend Classes:
A class can also be declared as a friend, giving all its member functions access to private members.
class ClassA {
friend class ClassB; // ClassB is a friend of ClassA
private:
int privateData;
public:
ClassA(int data) : privateData(data) {}
};
class ClassB {
public:
void accessPrivateData(ClassA& a) {
// Can access private members of ClassA
cout << "Private data: " << a.privateData << endl;
}
};
Important: Friendship is not mutual, inherited, or transitive. If A is a friend of B, B is not automatically a friend of A.
Composition & Aggregation
Composition and Aggregation are ways to combine objects to create more complex structures. They represent "HAS-A" relationships.
Composition vs Aggregation:
Composition (Strong "Has-A")
Ownership: Parent owns child completely
Lifetime: Child cannot exist without parent
Example: House has Rooms - rooms don't exist without the house
Aggregation (Weak "Has-A")
Ownership: Parent uses child but doesn't own it
Lifetime: Child can exist independently
Example: University has Students - students exist without university
Composition Example:
#include <iostream>
#include <string>
#include <vector>
using namespace std;
// Composition Example: Engine belongs to Car
class Engine {
private:
string type;
int horsepower;
public:
Engine(string t, int hp) : type(t), horsepower(hp) {
cout << "Engine created: " << type << " (" << horsepower << " HP)" << endl;
}
~Engine() {
cout << "Engine destroyed: " << type << endl;
}
void start() {
cout << type << " engine started!" << endl;
}
void stop() {
cout << type << " engine stopped!" << endl;
}
void displayInfo() const {
cout << "Engine: " << type << " - " << horsepower << " HP" << endl;
}
};
class Tire {
private:
string brand;
int size;
public:
Tire(string b, int s) : brand(b), size(s) {
cout << "Tire created: " << brand << " (Size " << size << ")" << endl;
}
~Tire() {
cout << "Tire destroyed: " << brand << endl;
}
void displayInfo() const {
cout << "Tire: " << brand << " - Size " << size << endl;
}
};
class Car {
private:
string model;
Engine engine; // Composition: Car owns Engine
vector<Tire> tires; // Composition: Car owns Tires
public:
// Constructor creates engine and tires
Car(string m, string engineType, int hp)
: model(m), engine(engineType, hp) {
// Create 4 tires
for (int i = 0; i < 4; ++i) {
tires.emplace_back("Michelin", 18);
}
cout << "Car created: " << model << endl;
}
~Car() {
cout << "Car destroyed: " << model << endl;
// Engine and tires are automatically destroyed
}
void start() {
cout << "Starting " << model << "..." << endl;
engine.start();
}
void stop() {
cout << "Stopping " << model << "..." << endl;
engine.stop();
}
void displayInfo() const {
cout << "\n=== Car Information ===" << endl;
cout << "Model: " << model << endl;
engine.displayInfo();
cout << "Tires (" << tires.size() << "):" << endl;
for (const auto& tire : tires) {
tire.displayInfo();
}
}
};
int main() {
cout << "=== Composition Example ===" << endl;
{
Car myCar("Honda Civic", "VTEC", 180);
myCar.displayInfo();
myCar.start();
myCar.stop();
} // Car, engine, and tires are all destroyed here
return 0;
}
Aggregation Example:
#include <iostream>
#include <string>
#include <vector>
using namespace std;
// Aggregation Example: Students exist independently of University
class Student {
private:
string name;
int id;
string major;
public:
Student(string n, int i, string m) : name(n), id(i), major(m) {
cout << "Student created: " << name << " (ID: " << id << ")" << endl;
}
~Student() {
cout << "Student destroyed: " << name << endl;
}
void displayInfo() const {
cout << "Student: " << name << " (ID: " << id << ") - Major: " << major << endl;
}
string getName() const { return name; }
int getId() const { return id; }
string getMajor() const { return major; }
};
class Professor {
private:
string name;
string department;
public:
Professor(string n, string d) : name(n), department(d) {
cout << "Professor created: " << name << " (" << department << ")" << endl;
}
~Professor() {
cout << "Professor destroyed: " << name << endl;
}
void displayInfo() const {
cout << "Professor: " << name << " - Department: " << department << endl;
}
string getName() const { return name; }
string getDepartment() const { return department; }
};
class University {
private:
string name;
vector<Student*> students; // Aggregation: University uses Students
vector<Professor*> professors; // Aggregation: University uses Professors
public:
University(string n) : name(n) {
cout << "University created: " << name << endl;
}
~University() {
cout << "University destroyed: " << name << endl;
// Note: We don't delete students/professors - they exist independently
}
void addStudent(Student* student) {
students.push_back(student);
cout << "Student " << student->getName() << " enrolled in " << name << endl;
}
void addProfessor(Professor* professor) {
professors.push_back(professor);
cout << "Professor " << professor->getName() << " joined " << name << endl;
}
void removeStudent(int studentId) {
for (auto it = students.begin(); it != students.end(); ++it) {
if ((*it)->getId() == studentId) {
cout << "Student " << (*it)->getName() << " left " << name << endl;
students.erase(it);
break;
}
}
}
void displayInfo() const {
cout << "\n=== " << name << " Information ===" << endl;
cout << "Students (" << students.size() << "):" << endl;
for (const auto& student : students) {
student->displayInfo();
}
cout << "\nProfessors (" << professors.size() << "):" << endl;
for (const auto& professor : professors) {
professor->displayInfo();
}
}
};
int main() {
cout << "=== Aggregation Example ===" << endl;
// Create students and professors independently
Student* alice = new Student("Alice Johnson", 1001, "Computer Science");
Student* bob = new Student("Bob Smith", 1002, "Mathematics");
Professor* drJones = new Professor("Dr. Jones", "Computer Science");
Professor* drBrown = new Professor("Dr. Brown", "Mathematics");
{
// Create university and add existing students/professors
University myUniversity("Tech University");
myUniversity.addStudent(alice);
myUniversity.addStudent(bob);
myUniversity.addProfessor(drJones);
myUniversity.addProfessor(drBrown);
myUniversity.displayInfo();
// Remove a student
myUniversity.removeStudent(1002);
} // University is destroyed, but students and professors still exist
cout << "\nAfter university destruction:" << endl;
alice->displayInfo(); // Alice still exists
drJones->displayInfo(); // Dr. Jones still exists
// Clean up (in real applications, use smart pointers)
delete alice;
delete bob;
delete drJones;
delete drBrown;
return 0;
}
Key Difference: In composition, the parent manages the lifecycle of child objects. In aggregation, child objects exist independently and can outlive the parent.
OOP Design Principles
Good object-oriented design follows established principles that lead to maintainable, flexible, and robust software.
SOLID Principles:
Single Responsibility Principle
A class should have only one reason to change - it should have only one job or responsibility.
Open/Closed Principle
Classes should be open for extension but closed for modification. Use inheritance and polymorphism.
Liskov Substitution Principle
Objects of derived classes should be substitutable for objects of base classes without breaking functionality.
Interface Segregation Principle
Clients shouldn't depend on interfaces they don't use. Keep interfaces small and specific.
Dependency Inversion Principle
High-level modules shouldn't depend on low-level modules. Both should depend on abstractions.
Design Patterns Preview:
Factory Pattern
Create objects without specifying exact classes
Observer Pattern
Notify multiple objects about state changes
Singleton Pattern
Ensure only one instance of a class exists
Strategy Pattern
Encapsulate algorithms and make them interchangeable
Best Practices Summary:
Encapsulation
- Keep data members private
- Provide public getters/setters with validation
- Hide implementation details
Inheritance
- Use "IS-A" relationship only
- Prefer composition over inheritance when possible
- Make destructors virtual in base classes
Polymorphism
- Use virtual functions for runtime polymorphism
- Implement pure virtual functions in abstract classes
- Override functions properly with override keyword
General
- Keep classes focused and cohesive
- Use meaningful names for classes and methods
- Write self-documenting code
Congratulations! You've mastered Object-Oriented Programming in C++! You can now design and implement complex software systems using OOP principles. Next, explore advanced C++ features to take your skills even further.