C++ Object-Oriented Programming (OOP) Complete Guide
Introduction to OOP
Object-Oriented Programming (OOP) organizes software around data (objects) rather than functions and logic. C++ fully supports OOP as one of its paradigms.
The four main pillars of OOP are:
- Encapsulation: Bundling data and the methods that operate on it
- Abstraction: Hiding complex implementation details behind simpler interfaces
- Inheritance: Creating new classes based on existing ones
- Polymorphism: Using a single interface for multiple implementations
Classes and Objects
A class is a blueprint; an object is an instance of a class.
Classes usually contain:
- Attributes (data members/variables)
- Methods (member functions)
- Constructors/Destructors (special member functions)
#include <iostream>
#include <string>
class Car {
std::string brand;
std::string model;
int year{};
public:
void honk() const { std::cout << "Beep beep!\n"; }
};
int main() {
Car myCar1;
Car myCar2;
myCar1.honk();
}
Access Modifiers
Access specifiers control visibility of class members:
- public: Accessible from anywhere
- private: Accessible only inside the class (default for classes)
- protected: Accessible inside the class and derived classes
Note: For `struct`, the default member access is `public`. Also, `class Derived : Base` defaults to **private** inheritance if `class` is used without an access specifier; `struct Derived : Base` defaults to **public**.
class Employee {
public:
std::string name;
private:
double salary;
protected:
int employeeId;
};
Constructors and Destructors
Constructors are special functions called when an object is created. Destructors are called when the object is destroyed.
Types of constructors:
- Default constructor (no parameters)
- Parameterized constructor
- Copy constructor
- Move constructor (C++11 and later)
Prefer member-initializer lists and consider defaulting or deleting special members explicitly when appropriate.
#include <iostream>
class Rectangle {
int width, height;
public:
Rectangle() : width(1), height(1) {}
Rectangle(int w, int h) : width(w), height(h) {}
Rectangle(const Rectangle&) = default; // copy
Rectangle(Rectangle&&) noexcept = default; // move
~Rectangle() { std::cout << "Rectangle destroyed\n"; }
};
Class Methods
Methods may be defined inside or outside the class definition (using the scope resolution operator).
Special categories of methods:
- Const methods: Do not modify object state
- Static methods: Belong to the class rather than an object
- Friend functions: Access private members from outside
#include <iostream>
class BankAccount {
double balance{0.0};
public:
void deposit(double amount) { balance += amount; }
void withdraw(double amount);
double getBalance() const { return balance; }
static void printBankName() { std::cout << "MyBank\n"; }
};
void BankAccount::withdraw(double amount) {
if (amount <= balance) {
balance -= amount;
}
}
int main() {
BankAccount::printBankName();
BankAccount acc;
acc.deposit(1000.0);
std::cout << acc.getBalance(); // 1000
}
MyBank 1000
Inheritance
Inheritance lets new classes reuse and extend existing ones.
Types: single, multiple, multilevel, hierarchical, hybrid
#include <iostream>
class Animal {
public:
void eat() { std::cout << "Eating...\n"; }
};
class Dog : public Animal {
public:
void bark() { std::cout << "Barking...\n"; }
};
class Aquatic {
public:
void swim() { std::cout << "Swimming...\n"; }
};
class Amphibian : public Animal, public Aquatic {};
Polymorphism
Polymorphism allows treating different derived objects as base objects.
Types:
- Compile-time (function/operator overloading)
- Runtime (virtual functions and overriding)
Avoid object slicing: store polymorphic objects via pointers/references, not by value.
#include <iostream>
#include <memory>
class Shape {
public:
virtual ~Shape() = default; // virtual destructor for polymorphic base
virtual void draw() const { std::cout << "Drawing shape\n"; }
};
class Circle : public Shape {
public:
void draw() const override { std::cout << "Drawing circle\n"; }
};
int main() {
std::unique_ptr<Shape> s = std::make_unique<Circle>();
s->draw();
}
Abstract Classes and Interfaces
An abstract class has at least one pure virtual function and cannot be instantiated directly.
C++ does not have a separate interface construct, but interface-like behavior is achieved with abstract classes containing only pure virtual functions.
#include <iostream>
class AbstractAnimal {
public:
virtual ~AbstractAnimal() = default;
virtual void makeSound() = 0;
};
class IPrintable {
public:
virtual ~IPrintable() = default;
virtual void print() = 0;
};
class Dog : public AbstractAnimal, public IPrintable {
public:
void makeSound() override { std::cout << "Bark\n"; }
void print() override { std::cout << "Dog details\n"; }
};
Operator Overloading
C++ lets you redefine operators for user-defined types. Commonly overloaded operators include arithmetic, comparison, I/O, and subscripting.
#include <iostream>
class Complex {
double real, imag;
public:
Complex(double r, double i) : real(r), imag(i) {}
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}
friend std::ostream& operator<<(std::ostream& os, const Complex& c) {
return os << c.real << " + " << c.imag << "i";
}
};
int main() {
Complex c1(2,3), c2(4,5);
Complex c3 = c1 + c2;
std::cout << c3;
}
Friend Classes and Functions
Friend functions and classes can access private and protected members of another class. Use this feature carefully.
#include <iostream>
class Box {
double width;
public:
explicit Box(double w) : width(w) {}
friend void printWidth(const Box& box);
friend class BoxPrinter;
};
void printWidth(const Box& box) { std::cout << "Width: " << box.width << '\n'; }
class BoxPrinter {
public:
void print(const Box& box) { std::cout << "Box width: " << box.width << '\n'; }
};
Static Members
Static members belong to the class rather than objects. Only one copy exists regardless of how many objects are created.
#include <iostream>
class Employee {
static int count;
int id;
public:
Employee() : id(++count) {}
static int getCount() { return count; }
};
int Employee::count = 0;
int main() {
std::cout << Employee::getCount() << '\n'; // 0
Employee e1, e2;
std::cout << Employee::getCount() << '\n'; // 2
}
Composition vs Inheritance
Composition (has-a relationship) is often preferable to inheritance (is-a relationship).
Guidelines:
- Use inheritance for clear specialization
- Use composition for flexible code reuse
- Favor composition when unsure to reduce coupling
class Engine {};
class Wheel {};
class Car {
Engine engine; // composition (has-a)
Wheel wheels[4];
};
Override/Final and Explicit
Use `override` to ensure you're actually overriding a virtual function. Use `final` to prevent further overriding.
Mark single-argument constructors `explicit` to avoid unintended implicit conversions.
struct Base {
virtual void f(int) {}
virtual ~Base() = default;
};
struct Derived final : Base {
void f(int) override {}
};
class OneInt {
public:
explicit OneInt(int x) : x(x) {}
private:
int x;
};
Best Practices
Practice | Description | Example |
---|---|---|
Rule of Three/Five/Zero | If you implement one of destructor, copy constructor, or copy assignment, you likely need all three (plus move in C++11+). Prefer the Rule of Zero when possible (let the compiler generate them). | struct ResourceHolder { /* use std::unique_ptr, default special members */ }; |
RAII | Tie resource management to object lifetime. | Use smart pointers like std::unique_ptr or std::shared_ptr. |
SOLID Principles | Design classes with single responsibility and clear abstractions. | Keep each class focused on one task. |
PIMPL Idiom | Hide implementation details in a separate class. | Use a private pointer to the implementation class. |
Practical Example: Banking System
A simple banking system showing OOP concepts in practice (using RAII with smart pointers):
#include <iostream>
#include <string>
#include <vector>
#include <memory>
class Account {
protected:
std::string owner;
double balance;
public:
Account(std::string name, double initial) : owner(std::move(name)), balance(initial) {}
virtual ~Account() = default;
virtual void display() const = 0;
void deposit(double amount) { balance += amount; }
virtual void withdraw(double amount) { if (amount <= balance) balance -= amount; }
double getBalance() const { return balance; }
};
class SavingsAccount : public Account {
double interestRate;
public:
SavingsAccount(std::string name, double initial, double rate)
: Account(std::move(name), initial), interestRate(rate) {}
void applyInterest() { balance += balance * interestRate; }
void display() const override {
std::cout << "Savings account - Owner: " << owner
<< ", Balance: " << balance
<< ", Rate: " << interestRate * 100 << "%\n";
}
};
class CheckingAccount : public Account {
static constexpr double FEE = 1.50;
public:
using Account::Account;
void withdraw(double amount) override {
if (amount + FEE <= balance) balance -= (amount + FEE);
}
void display() const override {
std::cout << "Checking account - Owner: " << owner
<< ", Balance: " << balance << '\n';
}
};
class Bank {
std::vector<std::unique_ptr<Account>> accounts;
public:
void addAccount(std::unique_ptr<Account> acc) { accounts.push_back(std::move(acc)); }
void displayAll() const { for (const auto& acc : accounts) acc->display(); }
};
int main() {
Bank myBank;
myBank.addAccount(std::make_unique<SavingsAccount>("Alice", 1000, 0.05));
myBank.addAccount(std::make_unique<CheckingAccount>("Bob", 500));
myBank.displayAll();
}
Savings account - Owner: Alice, Balance: 1000, Rate: 5% Checking account - Owner: Bob, Balance: 500