OOP in C++
Classes, constructors and destructors, inheritance, virtual functions, and access specifiers.
Classes and objects
A class is a blueprint describing state (data members) and behavior (member functions); an object is a concrete instance of that blueprint.
#include <iostream>
#include <string>
class Car {
public:
Car(std::string model, int speed) // constructor
: model_(std::move(model)), speed_(speed) {}
void accelerate(int amount) {
speed_ += amount;
}
void describe() const { // const: promises not to modify the object
std::cout << model_ << " is going " << speed_ << " km/h\n";
}
private:
std::string model_;
int speed_;
};
int main() {
Car car("Civic", 0);
car.accelerate(40);
car.describe(); // Civic is going 40 km/h
}
The : model_(std::move(model)), speed_(speed) part is a member initializer list — it initializes members directly as the object is constructed, which is more efficient than assigning to them in the constructor body (that would default-construct them first, then assign over that).
Constructors and destructors
A constructor runs when an object is created; a destructor (~ClassName) runs automatically when it goes out of scope or is explicitly deleted. This automatic, guaranteed cleanup is the foundation of RAII, covered in depth on the memory management page.
class FileLogger {
public:
FileLogger(const std::string& path) {
std::cout << "Opening " << path << "\n";
// (imagine a real file handle is opened here)
}
~FileLogger() {
std::cout << "Closing file automatically\n"; // guaranteed to run
}
};
void writeLogs() {
FileLogger logger("app.log");
// ... use logger ...
} // destructor runs here automatically, even if an exception were thrown above
If you don't declare a constructor at all, the compiler generates a default one; if you don't declare a destructor, it generates a trivial one. Once you declare any constructor, the compiler stops generating the default (no-argument) one for you.
Access specifiers
class BankAccount {
public:
void deposit(double amount) { // callable from anywhere
if (amount <= 0) return;
balance_ += amount;
}
double balance() const { return balance_; } // public "getter"
protected:
void logTransaction(double amount) { // callable by this class and subclasses only
// ...
}
private:
double balance_ = 0.0; // only this class can touch it directly
};
public— accessible from anywhere the object is visible.protected— accessible from this class and any class that inherits from it.private— accessible only from within this class itself (the default forclass;structdefaults topublic).
Keeping data members private and exposing controlled access through public methods protects the class's invariants — callers can't put the object into an invalid state by poking at its internals directly.
Inheritance
class Vehicle {
public:
virtual void accelerate() {
speed_ += 10;
std::cout << "Vehicle speed: " << speed_ << "\n";
}
virtual ~Vehicle() = default; // virtual destructor — see below
protected:
int speed_ = 0;
};
class SportsCar : public Vehicle {
public:
void accelerate() override { // override: sports cars accelerate faster
speed_ += 30;
std::cout << "SportsCar speed: " << speed_ << "\n";
}
};
Virtual functions and polymorphism
Marking a base class method virtual enables dynamic dispatch: calling it through a base-class pointer or reference runs the derived class's override, decided at runtime based on the object's actual type — not the compile-time type of the pointer/reference.
#include <memory>
#include <vector>
int main() {
std::vector<std::unique_ptr<Vehicle>> vehicles;
vehicles.push_back(std::make_unique<Vehicle>());
vehicles.push_back(std::make_unique<SportsCar>());
for (const auto& v : vehicles) {
v->accelerate(); // each object runs its OWN accelerate() — polymorphism
}
// Vehicle speed: 10
// SportsCar speed: 30
}
override is optional but should always be used — it tells the compiler "I intend to override a virtual base method," and the compiler will produce an error if the signature doesn't actually match anything in the base class (catching typos that would otherwise silently create an unrelated, non-overriding method).
Virtual destructors
If a class is ever going to be deleted through a base-class pointer, its destructor must be virtual — otherwise only the base class's destructor runs, and any derived-class members leak or fail to clean up:
class Base {
public:
virtual ~Base() = default; // essential once polymorphic deletion is possible
};
class Derived : public Base {
public:
~Derived() override {
std::cout << "Derived cleanup ran\n";
}
};
Base* obj = new Derived();
delete obj; // with a virtual destructor, Derived::~Derived() runs correctly, then Base::~Base()
Abstract classes and pure virtual functions
A pure virtual function (= 0) has no implementation in the base class and forces every concrete derived class to provide one. A class with at least one pure virtual function is abstract — it cannot be instantiated directly.
class Shape {
public:
virtual double area() const = 0; // pure virtual — no body, must be overridden
virtual ~Shape() = default;
};
class Circle : public Shape {
public:
explicit Circle(double radius) : radius_(radius) {}
double area() const override {
return 3.14159265 * radius_ * radius_;
}
private:
double radius_;
};
// Shape s; // compile error — Shape is abstract
Circle c(2.0);
std::cout << c.area() << "\n"; // 12.566...
Common mistakes
- Forgetting
virtualon a base class destructor when the class is meant to be used polymorphically — this is undefined behavior and a very common source of subtle memory leaks. - Forgetting
overrideon a method meant to override a virtual base method — a typo in the signature then silently creates a new, unrelated method instead of producing a compile error. - Deep inheritance hierarchies used purely for code reuse, when composition (a class holding another class as a member) would be simpler and more flexible.
Interview questions
Q: Why must a base class's destructor be virtual if the class will be used polymorphically?
Without virtual, deleting a derived object through a base-class pointer only invokes the base destructor — the derived part of the object never gets cleaned up, which is undefined behavior and typically leaks any resources the derived class owns.
Q: What makes a class abstract in C++?
Having at least one pure virtual function (virtual returnType f() = 0;). An abstract class cannot be instantiated directly — it can only be used as a base class, and every concrete subclass must implement all of its pure virtual functions.