Object Oriented Programming in C++
Object Oriented Programming (OOP) is a way of organizing code around objects that combine data and behavior. Instead of having separate functions and variables floating around, you group related data and the functions that operate on that data into classes. This makes code easier to understand, maintain, and reuse.
The four pillars of OOP are: Encapsulation (bundling data and methods together), Inheritance (creating new classes from existing ones), Polymorphism (one interface, many implementations), and Abstraction (hiding complex details behind simple interfaces).
Classes and Objects: The Basics
A class is a blueprint or template. An object is an actual instance created from that blueprint. Think of a class as the design plan for a house, and an object as an actual house built from that plan.
// Define a class (the blueprint)
class Student {
private: // only accessible inside this class
string name;
int age;
double gpa;
public: // accessible from anywhere
// Constructor: called automatically when creating an object
Student(string n, int a, double g)
: name(n), age(a), gpa(g) {} // initializer list
// Getter methods (read private data)
string getName() const { return name; }
int getAge() const { return age; }
double getGpa() const { return gpa; }
// Method: a function that belongs to the class
void introduce() const {
cout << "Hi, I'm " << name << ", age " << age
<< ", GPA: " << gpa << endl;
}
// Setter with validation
void setGpa(double newGpa) {
if (newGpa >= 0.0 && newGpa <= 4.0) {
gpa = newGpa;
} else {
cout << "Invalid GPA!" << endl;
}
}
};
// Create objects (actual instances)
Student alice("Alice", 20, 3.8);
Student bob("Bob", 22, 3.5);
alice.introduce(); // Hi, I'm Alice, age 20, GPA: 3.8
bob.setGpa(3.9); // Updates Bob's GPA
Constructors and Destructors
class BankAccount {
private:
string owner;
double balance;
public:
// Default constructor (no arguments)
BankAccount() : owner("Unknown"), balance(0.0) {}
// Parameterized constructor
BankAccount(string o, double b) : owner(o), balance(b) {}
// Copy constructor
BankAccount(const BankAccount& other)
: owner(other.owner), balance(other.balance) {
cout << "Account copied!" << endl;
}
// Destructor: called automatically when object goes out of scope
~BankAccount() {
cout << owner << "'s account closed." << endl;
}
void deposit(double amount) {
if (amount > 0) balance += amount;
}
void withdraw(double amount) {
if (amount > 0 && amount <= balance)
balance -= amount;
else
cout << "Cannot withdraw " << amount << endl;
}
double getBalance() const { return balance; }
};
// Usage
{
BankAccount acc("Alice", 1000);
acc.deposit(500); // balance: 1500
acc.withdraw(200); // balance: 1300
} // destructor called here: "Alice's account closed."
Inheritance: Building on Existing Classes
Inheritance lets you create a new class based on an existing one. The new class (child/derived) gets all the features of the parent (base) class and can add its own. This avoids duplicating code.
// Base class (parent)
class Animal {
protected: // accessible in this class and child classes
string name;
int age;
public:
Animal(string n, int a) : name(n), age(a) {}
virtual void speak() const {
cout << name << " makes a sound" << endl;
}
void info() const {
cout << name << ", age " << age << endl;
}
virtual ~Animal() = default; // virtual destructor is important
};
// Derived class (child)
class Dog : public Animal {
string breed;
public:
Dog(string n, int a, string b) : Animal(n, a), breed(b) {}
void speak() const override { // override parent's method
cout << name << " says: Woof!" << endl;
}
void fetch() const { // new method only in Dog
cout << name << " fetches the ball!" << endl;
}
};
class Cat : public Animal {
public:
Cat(string n, int a) : Animal(n, a) {}
void speak() const override {
cout << name << " says: Meow!" << endl;
}
};
// Polymorphism: use base class pointer to call derived class methods
Dog rex("Rex", 3, "Golden Retriever");
Cat whiskers("Whiskers", 5);
Animal* pets[] = {&rex, &whiskers};
for (Animal* pet : pets) {
pet->speak(); // Rex says: Woof! then Whiskers says: Meow!
}
Access Modifiers Explained
| Access Level | Same Class | Child Class | Outside Code |
| public | Yes | Yes | Yes |
| protected | Yes | Yes | No |
| private | Yes | No | No |
Abstract Classes and Interfaces
An abstract class has at least one pure virtual function (declared with = 0). You cannot create objects from an abstract class directly. It serves as a contract that child classes must follow.
// Abstract class (cannot be instantiated)
class Shape {
public:
virtual double area() const = 0; // pure virtual: MUST be implemented
virtual double perimeter() const = 0; // pure virtual
void describe() const { // regular method (shared by all)
cout << "Area: " << area() << ", Perimeter: " << perimeter() << endl;
}
virtual ~Shape() = default;
};
class Rectangle : public Shape {
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double area() const override { return width * height; }
double perimeter() const override { return 2 * (width + height); }
};
class Circle : public Shape {
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; }
};
// Using polymorphism
Rectangle r(5, 3);
Circle c(4);
r.describe(); // Area: 15, Perimeter: 16
c.describe(); // Area: 50.2655, Perimeter: 25.1327
Operator Overloading
C++ lets you define custom behavior for operators (+, -, ==, <<, etc.) when used with your own classes. This makes your code more intuitive and readable.
class Vector2D {
public:
double x, y;
Vector2D(double x = 0, double y = 0) : x(x), y(y) {}
// Add two vectors
Vector2D operator+(const Vector2D& other) const {
return Vector2D(x + other.x, y + other.y);
}
// Compare two vectors
bool operator==(const Vector2D& other) const {
return x == other.x && y == other.y;
}
// Print a vector with cout
friend ostream& operator<<(ostream& os, const Vector2D& v) {
return os << "(" << v.x << ", " << v.y << ")";
}
};
Vector2D a(1, 2), b(3, 4);
Vector2D c = a + b; // uses our custom + operator
cout << c << endl; // (4, 6) — uses our custom << operator
cout << (a == b) << endl; // 0 (false)
The Rule of Five (and Rule of Zero)
If your class manages a resource (raw pointer, file handle, etc.), you should define all five special member functions. But in modern C++, the best approach is the Rule of Zero: use smart pointers and STL containers so you do not need to write any of them.
// Rule of Five: define all five if you manage raw resources
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t n) : data(new int[n]), size(n) {} // constructor
~Buffer() { delete[] data; } // 1. destructor
Buffer(const Buffer& o) // 2. copy constructor
: data(new int[o.size]), size(o.size) {
copy(o.data, o.data + size, data);
}
Buffer& operator=(const Buffer& o) { // 3. copy assignment
if (this != &o) {
delete[] data;
size = o.size;
data = new int[size];
copy(o.data, o.data + size, data);
}
return *this;
}
Buffer(Buffer&& o) noexcept // 4. move constructor
: data(o.data), size(o.size) {
o.data = nullptr; o.size = 0;
}
Buffer& operator=(Buffer&& o) noexcept { // 5. move assignment
if (this != &o) {
delete[] data;
data = o.data; size = o.size;
o.data = nullptr; o.size = 0;
}
return *this;
}
};
// Rule of Zero: use vector instead and you need NONE of the above!
class BetterBuffer {
vector<int> data; // vector handles all memory automatically
public:
BetterBuffer(size_t n) : data(n) {}
// No destructor, copy/move constructors, or assignments needed!
};
Modern C++ Tip: Always aim for the Rule of Zero. Use std::vector, std::string, and std::unique_ptr instead of raw pointers and arrays. The compiler will generate correct copy/move/destructor automatically.