Object-Oriented Programming
Designing modular, reusable, and maintainable code with classes and objects
What You Will Learn
- Core OOP concepts: encapsulation, inheritance, polymorphism
- How to create classes and objects
- Constructors, destructors, and the Rule of Three/Five
- Access modifiers and data hiding
- Inheritance hierarchies and virtual functions
- Abstract classes and interfaces
- Operator overloading
- Static members and friend functions
- SOLID principles for good design
- Introduction to common design patterns
OOP Core Concepts
Object-Oriented Programming organizes code around objects that contain data and behavior.
Encapsulation
Bundling data and methods together, hiding implementation details.
Inheritance
Creating new classes based on existing ones, promoting code reuse.
Polymorphism
Objects of different types responding to the same interface.
Abstraction
Exposing essential features while hiding complexity.
Classes and Objects
A class is a blueprint; an object is an instance of that blueprint.
Defining a Class
class Person {
public:
// Member variables (attributes)
std::string name;
int age;
// Member functions (methods)
void introduce() {
std::cout << "Hi, I'm " << name
<< ", " << age << " years old.\n";
}
};
// Creating objects
Person alice;
alice.name = "Alice";
alice.age = 30;
alice.introduce(); // Hi, I'm Alice, 30 years old.
Class vs Struct
// The only difference: default access
class MyClass {
int x; // private by default
public:
int y;
};
struct MyStruct {
int x; // public by default
private:
int y;
};
// Convention:
// - struct: simple data containers (POD)
// - class: complex types with behavior
Member Functions
class Rectangle {
public:
double width, height;
// Method defined inside class (implicitly inline)
double area() {
return width * height;
}
// Method declared in class, defined outside
double perimeter();
};
// Definition outside class
double Rectangle::perimeter() {
return 2 * (width + height);
}
The this Pointer
class Counter {
private:
int count;
public:
void setCount(int count) {
this->count = count; // this-> distinguishes member from parameter
}
// Method chaining
Counter& increment() {
count++;
return *this; // Return self for chaining
}
};
Counter c;
c.setCount(5).increment().increment(); // c.count = 7
Constructors
Special functions called when an object is created.
Default Constructor
class Point {
public:
int x, y;
// Default constructor
Point() {
x = 0;
y = 0;
}
};
Point p; // Calls default constructor
// p.x = 0, p.y = 0
Parameterized Constructor
class Point {
public:
int x, y;
Point(int x, int y) {
this->x = x;
this->y = y;
}
};
Point p(10, 20); // x = 10, y = 20
Member Initializer List
class Point {
public:
int x, y;
const int id; // Must be initialized in initializer list
// Preferred: initializer list
Point(int x, int y, int id) : x(x), y(y), id(id) {
// Constructor body (optional)
}
};
// Required for:
// - const members
// - reference members
// - base class initialization
// - members without default constructor
Constructor Delegation (C++11)
class Rectangle {
public:
double width, height;
Rectangle() : Rectangle(1.0, 1.0) {} // Delegates
Rectangle(double size) : Rectangle(size, size) {} // Delegates
Rectangle(double w, double h) : width(w), height(h) {} // Main
};
Copy Constructor
class String {
private:
char* data;
size_t length;
public:
// Copy constructor
String(const String& other) {
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
}
};
String s1("Hello");
String s2 = s1; // Copy constructor called
String s3(s1); // Also copy constructor
Move Constructor (C++11)
class String {
public:
// Move constructor
String(String&& other) noexcept {
data = other.data;
length = other.length;
other.data = nullptr; // Leave source in valid state
other.length = 0;
}
};
String s1("Hello");
String s2 = std::move(s1); // Move constructor, s1 is now empty
Default and Delete (C++11)
class NonCopyable {
public:
NonCopyable() = default; // Use compiler-generated
NonCopyable(const NonCopyable&) = delete; // Disable copy
NonCopyable& operator=(const NonCopyable&) = delete;
};
NonCopyable a;
// NonCopyable b = a; // Error: copy deleted
Destructors
Called when an object is destroyed. Used for cleanup.
class FileHandler {
private:
FILE* file;
public:
FileHandler(const char* filename) {
file = fopen(filename, "r");
}
// Destructor
~FileHandler() {
if (file) {
fclose(file);
std::cout << "File closed\n";
}
}
};
void process() {
FileHandler fh("data.txt");
// Use file...
} // Destructor called here automatically
When Destructors Run
- Local objects: when going out of scope
- Dynamic objects: when
deleteis called - Static objects: at program end
- Temporary objects: at end of expression
Rule of Three
If you define any of these, you should define all three:
class Resource {
private:
int* data;
public:
// 1. Destructor
~Resource() { delete[] data; }
// 2. Copy constructor
Resource(const Resource& other) {
data = new int[100];
std::copy(other.data, other.data + 100, data);
}
// 3. Copy assignment operator
Resource& operator=(const Resource& other) {
if (this != &other) {
delete[] data;
data = new int[100];
std::copy(other.data, other.data + 100, data);
}
return *this;
}
};
Rule of Five (C++11)
Also add move constructor and move assignment:
class Resource {
public:
// ... Three from above, plus:
// 4. Move constructor
Resource(Resource&& other) noexcept : data(other.data) {
other.data = nullptr;
}
// 5. Move assignment operator
Resource& operator=(Resource&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
other.data = nullptr;
}
return *this;
}
};
Rule of Zero
Best practice: use smart pointers and standard containers to avoid manual resource management.
class Modern {
private:
std::unique_ptr<int[]> data;
std::vector<std::string> names;
public:
// Compiler-generated defaults work correctly!
// No custom destructor, copy, or move needed
};
Access Modifiers
class MyClass {
public: // Accessible from anywhere
int publicVar;
protected: // Accessible from class and derived classes
int protectedVar;
private: // Accessible only from within class
int privateVar;
};
Access in Inheritance
class Base {
public: int a;
protected: int b;
private: int c;
};
// Public inheritance (most common)
class Derived1 : public Base {
// a is public, b is protected, c is inaccessible
};
// Protected inheritance
class Derived2 : protected Base {
// a is protected, b is protected, c is inaccessible
};
// Private inheritance (rare)
class Derived3 : private Base {
// a is private, b is private, c is inaccessible
};
Encapsulation
Hide internal state and require all interaction through methods.
class BankAccount {
private:
double balance; // Hidden state
std::string accountNumber;
public:
BankAccount(std::string num) : accountNumber(num), balance(0) {}
// Getters (accessors)
double getBalance() const {
return balance;
}
std::string getAccountNumber() const {
return accountNumber;
}
// Setters (mutators) with validation
void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
bool withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return true;
}
return false;
}
};
BankAccount acc("123456");
acc.deposit(1000);
// acc.balance = 999999; // Error: private!
acc.withdraw(500);
std::cout << acc.getBalance(); // 500
Benefits of Encapsulation
- Control over data (validation in setters)
- Flexibility to change implementation
- Reduced complexity for users
- Better maintainability
Inheritance
Create new classes based on existing ones.
// Base class (parent)
class Animal {
protected:
std::string name;
public:
Animal(std::string n) : name(n) {}
void eat() {
std::cout << name << " is eating.\n";
}
};
// Derived class (child)
class Dog : public Animal {
public:
Dog(std::string n) : Animal(n) {} // Call base constructor
void bark() {
std::cout << name << " says Woof!\n";
}
};
Dog dog("Buddy");
dog.eat(); // Inherited from Animal
dog.bark(); // Defined in Dog
Constructor/Destructor Order
class Base {
public:
Base() { std::cout << "Base constructed\n"; }
~Base() { std::cout << "Base destroyed\n"; }
};
class Derived : public Base {
public:
Derived() { std::cout << "Derived constructed\n"; }
~Derived() { std::cout << "Derived destroyed\n"; }
};
Derived d;
// Output:
// Base constructed
// Derived constructed
// Derived destroyed
// Base destroyed
Multiple Inheritance
class Flyable {
public:
void fly() { std::cout << "Flying\n"; }
};
class Swimmable {
public:
void swim() { std::cout << "Swimming\n"; }
};
class Duck : public Flyable, public Swimmable {
public:
void quack() { std::cout << "Quack!\n"; }
};
Duck d;
d.fly(); // From Flyable
d.swim(); // From Swimmable
d.quack(); // From Duck
Diamond Problem
class Animal { public: int age; };
class Bird : public Animal {};
class Fish : public Animal {};
// Diamond inheritance
class FlyingFish : public Bird, public Fish {
// Has TWO copies of Animal!
// bird.age and fish.age are different
};
// Solution: Virtual inheritance
class Bird : virtual public Animal {};
class Fish : virtual public Animal {};
class FlyingFish : public Bird, public Fish {
// Only ONE copy of Animal
};
Polymorphism
Objects of different types respond to the same interface.
Virtual Functions
class Shape {
public:
virtual double area() const {
return 0;
}
virtual ~Shape() {} // Virtual destructor!
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override { // override keyword (C++11)
return 3.14159 * radius * radius;
}
};
class Rectangle : public Shape {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double area() const override {
return width * height;
}
};
// Polymorphic behavior
void printArea(const Shape& shape) {
std::cout << "Area: " << shape.area() << "\n";
}
Circle c(5);
Rectangle r(4, 6);
printArea(c); // Area: 78.5398
printArea(r); // Area: 24
Virtual Destructor
class Base {
public:
virtual ~Base() {
std::cout << "Base destroyed\n";
}
};
class Derived : public Base {
private:
int* data;
public:
Derived() : data(new int[100]) {}
~Derived() override {
delete[] data;
std::cout << "Derived destroyed\n";
}
};
// Without virtual destructor, this would leak memory!
Base* ptr = new Derived();
delete ptr; // Calls both destructors correctly
override and final (C++11)
class Base {
public:
virtual void foo() {}
virtual void bar() final {} // Can't override
};
class Derived : public Base {
public:
void foo() override {} // Correctly overrides
// void foo(int) override {} // Error: doesn't match base
// void bar() override {} // Error: bar is final
};
class Final final : public Base {
// This class can't be inherited from
};
Abstract Classes and Interfaces
Abstract classes have at least one pure virtual function and cannot be instantiated.
class Shape {
public:
// Pure virtual function (= 0)
virtual double area() const = 0;
virtual double perimeter() const = 0;
virtual ~Shape() = default;
};
// Shape s; // Error: can't instantiate abstract class
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override {
return 3.14159 * radius * radius;
}
double perimeter() const override {
return 2 * 3.14159 * radius;
}
};
Interface (Pure Abstract Class)
// Interface: all pure virtual, no data
class Drawable {
public:
virtual void draw() const = 0;
virtual ~Drawable() = default;
};
class Printable {
public:
virtual void print() const = 0;
virtual ~Printable() = default;
};
// Implement multiple interfaces
class Document : public Drawable, public Printable {
public:
void draw() const override {
std::cout << "Drawing document\n";
}
void print() const override {
std::cout << "Printing document\n";
}
};
Operator Overloading
Define custom behavior for operators with your classes.
Arithmetic Operators
class Vector2D {
public:
double x, y;
Vector2D(double x = 0, double y = 0) : x(x), y(y) {}
// Addition
Vector2D operator+(const Vector2D& other) const {
return Vector2D(x + other.x, y + other.y);
}
// Subtraction
Vector2D operator-(const Vector2D& other) const {
return Vector2D(x - other.x, y - other.y);
}
// Scalar multiplication
Vector2D operator*(double scalar) const {
return Vector2D(x * scalar, y * scalar);
}
};
Vector2D a(1, 2), b(3, 4);
Vector2D c = a + b; // (4, 6)
Vector2D d = a * 2; // (2, 4)
Comparison Operators
class Point {
public:
int x, y;
bool operator==(const Point& other) const {
return x == other.x && y == other.y;
}
bool operator!=(const Point& other) const {
return !(*this == other);
}
bool operator<(const Point& other) const {
if (x != other.x) return x < other.x;
return y < other.y;
}
// C++20: spaceship operator
auto operator<=>(const Point&) const = default;
};
Stream Operators
class Person {
public:
std::string name;
int age;
// Output operator (friend function)
friend std::ostream& operator<<(std::ostream& os, const Person& p) {
os << p.name << " (" << p.age << ")";
return os;
}
// Input operator
friend std::istream& operator>>(std::istream& is, Person& p) {
is >> p.name >> p.age;
return is;
}
};
Person p{"Alice", 30};
std::cout << p << "\n"; // Alice (30)
Subscript Operator
class Array {
private:
int* data;
size_t size;
public:
int& operator[](size_t index) {
return data[index];
}
const int& operator[](size_t index) const {
return data[index];
}
};
Array arr;
arr[0] = 42;
int x = arr[0];
Function Call Operator (Functor)
class Multiplier {
private:
int factor;
public:
Multiplier(int f) : factor(f) {}
int operator()(int x) const {
return x * factor;
}
};
Multiplier times3(3);
std::cout << times3(10); // 30
// Useful with algorithms
std::vector<int> nums = {1, 2, 3, 4, 5};
std::transform(nums.begin(), nums.end(), nums.begin(), Multiplier(2));
Static Members
Members shared by all instances of a class.
class Counter {
private:
static int count; // Declaration
int id;
public:
Counter() : id(++count) {}
static int getCount() {
return count;
}
int getId() const { return id; }
};
// Definition (required, usually in .cpp file)
int Counter::count = 0;
Counter a, b, c;
std::cout << Counter::getCount(); // 3
std::cout << a.getId(); // 1
std::cout << c.getId(); // 3
Static Methods
class Math {
public:
static double pi() { return 3.14159265359; }
static int max(int a, int b) {
return (a > b) ? a : b;
}
// Can't access non-static members!
// static void bad() { return this->x; } // Error
};
double p = Math::pi();
int m = Math::max(5, 10);
Static Const Members
class Config {
public:
static const int MAX_SIZE = 100; // OK for integral
static constexpr double PI = 3.14159; // C++11 for non-integral
static const std::string DEFAULT_NAME; // Declared here
};
// Defined in .cpp
const std::string Config::DEFAULT_NAME = "Default";
Friend Functions and Classes
Friends can access private and protected members.
Friend Function
class Box {
private:
double width;
public:
Box(double w) : width(w) {}
// Friend function declaration
friend void printWidth(const Box& b);
friend Box operator+(const Box& a, const Box& b);
};
// Friend function definition
void printWidth(const Box& b) {
std::cout << "Width: " << b.width << "\n"; // Can access private
}
Box operator+(const Box& a, const Box& b) {
return Box(a.width + b.width);
}
Friend Class
class Engine {
private:
int horsepower;
friend class Car; // Car can access Engine's private members
public:
Engine(int hp) : horsepower(hp) {}
};
class Car {
private:
Engine engine;
public:
Car(int hp) : engine(hp) {}
void showPower() {
std::cout << "HP: " << engine.horsepower; // OK: friend
}
};
Use Friends Sparingly
Friend breaks encapsulation. Use it only when necessary, like for operator overloading that needs private access.
SOLID Principles
Five principles for better object-oriented design.
S - Single Responsibility
A class should have only one reason to change.
// Bad: Multiple responsibilities
class User {
void saveToDatabase();
void sendEmail();
void generateReport();
};
// Good: Single responsibility
class User { /* user data */ };
class UserRepository { void save(User& u); };
class EmailService { void send(User& u, std::string msg); };
class ReportGenerator { void generate(User& u); };
O - Open/Closed
Open for extension, closed for modification.
// Use polymorphism to extend without modifying
class Shape {
public:
virtual double area() const = 0;
};
// Add new shapes without changing existing code
class Circle : public Shape { /* ... */ };
class Rectangle : public Shape { /* ... */ };
class Triangle : public Shape { /* ... */ }; // New! No changes elsewhere
L - Liskov Substitution
Subtypes must be substitutable for their base types.
// Bad: Square violates Rectangle's invariants
class Rectangle {
public:
virtual void setWidth(double w) { width = w; }
virtual void setHeight(double h) { height = h; }
};
class Square : public Rectangle {
void setWidth(double w) override { width = height = w; } // Breaks expectations!
};
// Better: Use composition or separate hierarchies
I - Interface Segregation
Clients shouldn't depend on interfaces they don't use.
// Bad: Fat interface
class Worker {
virtual void work() = 0;
virtual void eat() = 0;
virtual void sleep() = 0;
};
// Good: Segregated interfaces
class Workable { virtual void work() = 0; };
class Eatable { virtual void eat() = 0; };
class Sleepable { virtual void sleep() = 0; };
class Human : public Workable, public Eatable, public Sleepable { };
class Robot : public Workable { }; // Robots don't eat or sleep
D - Dependency Inversion
Depend on abstractions, not concretions.
// Bad: Depends on concrete class
class Report {
MySQLDatabase db; // Tightly coupled
};
// Good: Depends on abstraction
class Database {
public:
virtual void save(std::string data) = 0;
};
class Report {
Database& db; // Can use any database
public:
Report(Database& database) : db(database) {}
};
Design Patterns Introduction
Reusable solutions to common design problems.
Singleton
Ensure only one instance exists.
class Logger {
private:
static Logger* instance;
Logger() {} // Private constructor
public:
static Logger& getInstance() {
static Logger instance; // Thread-safe in C++11
return instance;
}
void log(const std::string& msg) {
std::cout << "[LOG] " << msg << "\n";
}
// Delete copy operations
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
};
Logger::getInstance().log("Hello!");
Factory
Create objects without specifying exact class.
class Shape {
public:
virtual void draw() = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
void draw() override { std::cout << "Drawing circle\n"; }
};
class Rectangle : public Shape {
void draw() override { std::cout << "Drawing rectangle\n"; }
};
class ShapeFactory {
public:
static std::unique_ptr<Shape> create(const std::string& type) {
if (type == "circle") return std::make_unique<Circle>();
if (type == "rectangle") return std::make_unique<Rectangle>();
return nullptr;
}
};
auto shape = ShapeFactory::create("circle");
shape->draw();
Observer
Notify multiple objects about state changes.
class Observer {
public:
virtual void update(int value) = 0;
};
class Subject {
std::vector<Observer*> observers;
int state;
public:
void attach(Observer* o) { observers.push_back(o); }
void setState(int s) {
state = s;
notify();
}
void notify() {
for (auto* o : observers) {
o->update(state);
}
}
};
Note
Design patterns are a deep topic. Consider studying the classic "Gang of Four" book for comprehensive coverage.
Practice Questions
- Create a
BankAccountclass with deposit, withdraw, and balance checking. - Implement a
Shapehierarchy withCircle,Rectangle, andTriangle. - Write a
Fractionclass with operator overloading for +, -, *, /. - Create a
Stringclass that manages its own memory (Rule of Five). - Implement an
Employeehierarchy with different types of employees. - Create a simple
LinkedListclass with iterator support. - Implement the Factory pattern for creating different types of documents.
- Create a
Matrixclass with operator overloading. - Implement the Observer pattern for an event system.
- Create an interface-based plugin system.
- Write a
SmartArrayclass that acts like a simple vector. - Implement a class hierarchy that follows SOLID principles.