The complete object-oriented programming guide for C++. Every concept from class anatomy to virtual dispatch, from operator overloading to the Rule of Five — with full working code at every step. This is the file that separates programmers from software engineers.
Object-Oriented Programming is a paradigm where you model your program as a collection of objects — entities that bundle data (attributes) and behaviour (methods) together. Instead of writing functions that operate on separate data, you define types that know how to manage themselves.
Encapsulation — hide internal state
Abstraction — expose only what's needed
Inheritance — reuse and extend types
Polymorphism — one interface, many forms
A class is a blueprint — it defines what data and operations the type has. An object is an instance — actual memory allocated from that blueprint. One class → many objects.
Models real-world entities naturally. Enables code reuse through inheritance. Reduces complexity by hiding implementation. Makes large codebases manageable through clear interfaces.
Before OOP, programs were written as loose variables passed into standalone functions. To model a bank account, you'd have double balance1 = 1000; string owner1 = "Alice"; and pass them to withdraw(balance1, 50.0);. When you added a second account, you had to duplicate all variables. It quickly became unmanageable. Classes solve this by bundling the data (balance, owner) and the functions (withdraw) into a single, self-contained unit. You no longer pass loose data to functions; the object carries its own data and knows how to modify itself.
A class declaration has member variables (data) and member functions (methods). By default, class members are private — only accessible within the class itself.
A const after the parameter list (e.g., double getBalance() const) means this function does not modify any member variables. It promises read-only access. Always mark getter/inspection methods as const — it allows them to be called on const objects and communicates intent clearly.
Inside every non-static member function, this is an implicit pointer to the current object. It's used to disambiguate member variables from parameters of the same name, and to enable method chaining.
Static members belong to the class itself, not to any individual object. All instances share a single copy of a static member. Use them for class-wide counters, shared configuration, or factory functions.
Declaring static int count; inside the class is only a declaration. You must provide a definition in exactly one .cpp file: int Counter::count = 0;. Forgetting this gives a linker error: "undefined reference to Counter::count". In C++17, you can use inline static int count = 0; inside the class to avoid this.
| Trap | Wrong | Correct |
|---|---|---|
| Calling non-const method on const object | const BankAccount a; a.deposit(10); // error | Mark read-only methods const; they work on const objects |
| Forgetting semicolon after class | class Foo { } // missing ; | class Foo { }; // class definition must end with ; |
| Static member definition | Static int declared but not defined outside class | int MyClass::staticVar = 0; in .cpp (or inline static in C++17) |
| this in static method | Using this inside a static method | Static methods have no this pointer — they don't belong to an instance |
| Returning this by value | Builder setX() { return *this; } (copies!) | Builder& setX() { return *this; } (reference for chaining) |
A constructor is a special member function called automatically when an object is created. It has the same name as the class, no return type (not even void), and is responsible for initializing the object to a valid state.
Without initializer list: members are default-initialized first, then assigned in the constructor body — two operations. With initializer list: members are initialized directly — one operation. For const members, reference members, and members with no default constructor, initializer list is the only option. Always prefer it.
A destructor is called automatically when an object is destroyed (goes out of scope, or delete is called on a heap object). It has the same name as the class prefixed with ~ and no parameters. Its job: release all resources acquired by the object — memory, file handles, network connections, locks.
This is the most important C++ idiom. Tie the lifetime of a resource to the lifetime of an object: acquire in the constructor, release in the destructor. When the object goes out of scope, the destructor runs — resource is always released. No manual cleanup. No leaks. This is how std::vector, std::fstream, std::unique_ptr, and mutexes all work.
If a class has virtual methods (is meant to be a base class), its destructor must be virtual. Without it, deleting a derived object through a base pointer only calls the base destructor — the derived destructor never runs — causing resource leaks. Rule: if a class has any virtual function, make the destructor virtual too.
When you copy an object, C++ calls either the copy constructor (when creating a new object) or copy assignment operator (when assigning to an existing object). By default, C++ generates these as shallow copies — copying each member value directly.
If you manage heap memory and don't define a copy constructor, the compiler generates a shallow copy that copies the pointer value — both objects now point to the same heap memory. When either object is destroyed, it frees that memory. The other object now has a dangling pointer. When it's destroyed, it tries to free already-freed memory → double free → crash. Always define a deep copy when you own heap memory.
| Trap | Wrong | Correct |
|---|---|---|
| Initializer list order | Members initialized in list order, NOT declaration order | Members ALWAYS initialized in declaration order. Keep list in same order. |
| Missing self-assignment check | op= without if(this==&other) — deletes own data first | Always check self-assignment before any delete in op= |
| Shallow copy with raw pointer | Default copy copies pointer address — shared ownership | Define copy constructor + op= to deep copy heap data |
| Non-virtual destructor in base | Base* p = new Derived(); delete p; — derived destructor never called | virtual ~Base() {} whenever the class is a base class |
| explicit constructor | Implicit conversion: void f(A); f(42); silently constructs A | Mark single-param constructors explicit to prevent surprise conversions |
| Specifier | Accessible From | Use When |
|---|---|---|
public | Anywhere — outside the class, derived classes, everywhere | Interface: what the user of the class is meant to call |
private | Only within this class (not even derived classes) | Implementation details: internal data and helper functions |
protected | This class AND derived classes only | Data/methods that subclasses need but external code shouldn't touch |
In C++, struct and class are nearly identical. The only difference: struct members are public by default, class members are private by default. Convention: use struct for simple data containers (POD types), use class for types with significant behavior and invariants to protect.
The core payoff of encapsulation: by forcing access through methods, you can validate input, enforce invariants, and change the internal representation without breaking external code.
A class with a private member x and public getX()/setX() is not encapsulated — it's just bureaucracy. Real encapsulation means the class protects an invariant. Before adding a setter, ask: what rules must hold? Enforce those in the setter. If there are no rules, consider if the member needs to be private at all.
A friend declaration grants a specific external function or class access to private/protected members. It's an intentional, controlled bypass of encapsulation — useful for operator overloading and tightly coupled classes.
Friendship is not inherited and not transitive. But overusing it defeats the purpose of encapsulation. If you find yourself adding many friend declarations, it's a sign your design needs rethinking. Use friend primarily for operator overloading (especially operator<<) and for pairs of tightly coupled classes that form a single logical unit.
| Trap | Wrong | Correct |
|---|---|---|
| Returning private data by non-const reference | double& getBalance() — caller can modify private member! | const double& getBalance() const — or return by value for small types |
| Protected data in large hierarchies | Protected data accessible to all subclasses — hard to reason about | Prefer private data + protected getters/setters for controlled access |
| Mutable keyword abuse | mutable on data that changes logical state (breaks const correctness) | mutable only for caching/lazy eval that doesn't affect observable state (e.g., cached hash) |
Imagine writing a game with a Dog and a Cat. Both need a name, an age, and an eat() method. Without inheritance, you would copy and paste those variables and methods into both classes. When you inevitably find a bug in eat(), you have to fix it in every single animal class. Inheritance allows you to write the shared logic exactly once in a Base class (Animal), and let the Derived classes only specify what makes them unique.
Inheritance lets a derived class (child) acquire the properties and methods of a base class (parent). The derived class is a base class — it can be used anywhere a base class is expected. This is the Liskov Substitution Principle.
class Dog : public Animal — public members of Animal remain public in Dog. Most common; models "IS-A" relationship.
class Dog : protected Animal — public/protected of Animal become protected in Dog.
class Dog : private Animal — all Animal members become private in Dog; models "IMPLEMENTED-IN-TERMS-OF". Very rare.
Construction: Base → Derived. The base must exist before the derived part is built on top of it.
Destruction: Derived → Base. Reverse of construction. The derived part is torn down first, then the base part.
This is symmetric and automatic. You can't change this order.
C++ supports inheriting from multiple base classes. This is powerful but can cause the diamond problem when two bases share a common ancestor.
Multiple inheritance is complex and rarely necessary. Before reaching for it, ask: can I model this with composition (having a member of that type) or interfaces (pure abstract base classes)? Most real-world cases where you think you need multiple inheritance are better solved with composition + interfaces. Keep multiple inheritance for mixins and interface-only base classes.
| Trap | Wrong | Correct |
|---|---|---|
| Not calling base constructor | Derived() {} — base default-initialized (may crash) | Derived() : Base(args) {} — explicitly chain base constructor |
| Slicing | Animal a = Dog("Rex",3,"Lab"); — Dog part is sliced off! | Animal* a = new Dog(...); or Animal& a = dog; — use pointer/reference |
| override not used | void speak() const — typo in signature = new function, not override | Always use override keyword — compiler catches signature mismatches |
| Calling virtual in constructor | Virtual calls in constructors go to BASE version (polymorphism inactive) | Never rely on virtual dispatch in constructors/destructors |
| Diamond without virtual | Two copies of base class, ambiguous member access | Use virtual inheritance when diamond structure is needed |
Imagine you have an array of 50 different shapes, and you want to call area() on all of them. To put them in one array, they must all be stored as pointers to the base class: Shape*. But if area() is a normal function, calling shapeArray[0]->area() will ALWAYS execute the base Shape version of the function—even if the object is actually a Circle! The compiler only looks at the pointer type. virtual fixes this. It tells the compiler: "Wait until runtime, look at the ACTUAL object this pointer is aiming at, and call its specific version."
Polymorphism means "many forms" — the same function call can execute different code depending on the actual runtime type of the object. C++ achieves this through virtual functions and the virtual dispatch mechanism (vtable).
When a class has virtual functions, the compiler creates a vtable (virtual function table) — an array of function pointers. Each object of that class gets a hidden vptr pointing to its class's vtable. When you call s->area() through a base pointer, C++ follows the vptr to find the right function. This is called dynamic dispatch and has a tiny overhead (~indirection through a pointer). The payoff: you write code against the base interface and it works correctly for all current and future derived types.
If you delete a derived object through a pointer to its base class, and the base class has a non-virtual destructor, the result is undefined behavior. Usually, this means only the base class destructor runs, silently leaking the derived class's memory and resources.
If a base function is not virtual, the derived class can define a function with the same name — but this hides the base function, not overrides it. Through a base pointer, the base function is always called, regardless of the actual object type. This is one of the most insidious bugs in C++ OOP. Always use virtual for functions meant to be overridden. Always use override in derived classes.
dynamic_cast requires RTTI and has runtime overhead. Frequent use often signals a design smell — you shouldn't need to know the concrete type at runtime if the virtual interface is designed well. For performance-critical code, use virtual dispatch instead. dynamic_cast is appropriate for introspection (e.g., debugging, serialization, visitor pattern).
| Trap | Wrong | Correct |
|---|---|---|
| No virtual destructor | delete basePtr — derived destructor skipped, resource leak | virtual ~Base() {} — always in polymorphic base classes |
| Hiding instead of overriding | Derived::g() without virtual in base — base called via pointer | Mark base function virtual; mark derived override |
| Object slicing | Base b = derived; — polymorphism gone, derived data lost | Always use pointer or reference for polymorphic behavior |
| Pure virtual class instantiation | Shape s; // error: Shape has pure virtual members | Abstract classes cannot be instantiated; use pointers to derived |
| Virtual call in constructor | Virtual functions in constructor call base version (not derived) | Polymorphism is only active AFTER the object is fully constructed |
C++ lets you define what operators like +, -, ==, << mean for your custom types. This enables your classes to feel as natural as built-in types — v1 + v2 for vectors, a == b for objects, cout << obj for printing.
1. You can only overload existing operators — you can't invent new ones.
2. At least one operand must be a user-defined type — you can't change int + int.
3. You cannot change arity (unary stays unary, binary stays binary).
4. You cannot overload: :: . .* ?: sizeof typeid.
5. Keep semantics intuitive — + should add, not subtract.
C++20 introduced the three-way comparison operator <=>, nicknamed the "spaceship operator". Define it once and the compiler auto-generates all six comparison operators (< <= > >= == !=).
| Trap | Wrong | Correct |
|---|---|---|
| Not returning *this from += | void operator+=(…) — breaks a += b += c | T& operator+=(…) { …; return *this; } |
| Postfix vs prefix return | Postfix returning reference *this (returns modified, not old) | Postfix: save old, increment, return old value (by value) |
| operator== missing const | bool operator==(const T& o) — not const, can't call on const objects | bool operator==(const T& o) const |
| Not returning ostream& from << | void operator<<(ostream, T) — breaks chaining | ostream& operator<<(ostream& os, const T& t) { …; return os; } |
| Implicit conversion operator | operator int() — silently converts where not expected | explicit operator int() — require static_cast |
A pure virtual function is declared with = 0. It has no body in the base class — it's a contract saying "every derived class MUST provide this." A class with at least one pure virtual function is an abstract class — it cannot be instantiated directly.
A pure virtual function can still have a body — it's optional. Derived classes must still override it, but they can call the base implementation via Base::func(). This is useful when you want to provide a default behavior that subclasses can choose to reuse.
The principle: program to an interface, not an implementation. Write code against abstract base classes — then swap concrete implementations without changing calling code.
Build a plugin system: swap logging implementations without changing the logger call sites.
| Trap | Wrong | Correct |
|---|---|---|
| Instantiating abstract class | IShape s; // compile error | IShape* s = new Circle(...); or use references |
| Forgetting to implement all pure virtuals | Derived class missing one pure virtual override — still abstract! | All pure virtuals must be overridden for class to be concrete |
| No virtual destructor in interface | Deleting through interface pointer leaks derived resources | Always: virtual ~Interface() {} even in pure abstract classes |
| Storing objects in interface containers | vector<ILogger> loggers — slicing! | vector<ILogger*> or vector<unique_ptr<ILogger>> |
If your class doesn't directly manage any resources (no raw pointers to owned memory, no file handles, no locks), don't define any of the five special functions. Let the compiler generate them — they'll be correct. Use RAII wrappers (std::vector, std::string, std::unique_ptr) to manage resources.
If you need to define any of: destructor, copy constructor, or copy assignment operator — you almost certainly need to define all three. They're all about resource management and they all need to be consistent.
With move semantics, add two more: move constructor and move assignment operator. If you define any of the five, consider whether you need all five. Defining one suppresses compiler generation of others.
1. The Disaster (Shallow Copy): If your object has an int* data_ pointing to heap memory, the compiler's default copy just copies the memory address. Now two objects point to the same memory. When they both get destroyed, they both try to delete[] data_. The second one crashes your program (Double Free).
2. The Fix (Copy Constructor): To fix this, you write a Copy Constructor. Instead of copying the address, it asks the OS for new memory, then copies the actual numbers over (Deep Copy). Safe, but slow for huge data.
3. The Optimization (Move Constructor): Suppose you create a temporary 10GB Buffer, pass it to a function, and immediately destroy the temporary. Creating a Deep Copy of 10GB is a massive waste of time because the original is about to be thrown away anyway! A Move Constructor fixes this by simply stealing the pointer from the temporary object and setting the temporary's pointer to null. It's an instant, safe transfer of ownership.
Move semantics enable transferring ownership of a resource instead of copying it. This is critical for performance with large objects like vectors, strings, and buffers.
Always mark move constructor and move assignment noexcept. The STL (e.g., std::vector::push_back) will only use your move operations if they're noexcept — otherwise it falls back to copying for exception safety. A move that doesn't throw should always be declared noexcept.
| Trap | Wrong | Correct |
|---|---|---|
| Using moved-from object | Buffer b2 = move(b1); b1[0]; — b1.data_ is nullptr! | After move, treat moved-from object as valid but unspecified state — don't use it |
| Missing noexcept on move | Buffer(Buffer&&) — vector resizes use copy, not move! | Buffer(Buffer&&) noexcept — enables STL optimizations |
| Defining destructor suppresses move | Define ~Foo() {} → compiler suppresses move operations → copies! | Explicitly = default move operations: Foo(Foo&&) noexcept = default; |
| std::move on return value | return std::move(local); — prevents NRVO (actually slower!) | return local; — compiler applies NRVO or implicit move |
| Rule of Zero violation | Class has a raw pointer member, no copy/move defined → shallow copy bugs | Wrap in unique_ptr and follow Rule of Zero, or implement all five |
A class should have exactly one reason to change. If a class handles business logic, database saving, and UI rendering, a change in the UI will force you to recompile and retest the database logic. By separating concerns, your code becomes modular, testable, and robust against cascading changes.
Software entities should be open for extension, but closed for modification. When requirements change, you should be able to add new functionality by writing new code, not by modifying existing, already-tested code. Modifying existing code risks breaking things that already work.
If a program works with objects of a base class, it should also work with objects of any derived class without altering the correctness of the program. If a subclass overrides a method to do something completely unexpected, or throws an exception where the base class wouldn't, it violates LSP and creates bugs for callers relying on the base interface.
A client should never be forced to depend on methods it doesn't use. Fat, monolithic interfaces force implementing classes to create dummy or empty methods, cluttering the code and coupling unrelated systems. Break interfaces down into small, highly cohesive roles.
High-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions. This means you should inject dependencies via interfaces (like a database connection), rather than hardcoding concrete classes inside your business logic.
Inheritance models IS-A. Composition models HAS-A. Prefer composition — it's more flexible, avoids tight coupling, and doesn't require deep inheritance hierarchies.
| Anti-Pattern | Problem | Solution |
|---|---|---|
| God Class | One class does everything — 5000 lines | Split into focused classes by responsibility (SRP) |
| Deep inheritance hierarchy | 6+ levels deep — fragile, hard to understand | Prefer composition; keep hierarchies max 2-3 levels |
| Shotgun surgery | One change requires editing 20 files | Encapsulate change behind interfaces |
| Singleton overuse | Singletons everywhere = hidden global state, untestable | Use dependency injection instead; singletons for truly global resources only |
| Concrete dependency | Class A holds a FileLogger — can't unit test without files | Class A holds ILogger* — inject MockLogger in tests |
| You Define | Compiler Generates Default: | Compiler Suppresses: |
|---|---|---|
| Nothing | All 6 (default ctor, copy ctor, copy=, move ctor, move=, dtor) | — |
| Any constructor | — | Default constructor |
| Destructor | Copy ctor, copy= | Move ctor, move= (deprecated) |
| Copy constructor | Copy assignment | Move ctor, move= |
| Copy assignment | Copy constructor | Move ctor, move= |
| Move constructor | — | Copy ctor, copy= (deleted!) |
| Move assignment | — | Copy ctor, copy= (deleted!) |
Rather than relying on implicit generation rules (which are complex), be explicit: Foo(const Foo&) = default; to explicitly request compiler generation, and Foo(const Foo&) = delete; to explicitly prohibit copying. This is clearer and immune to the suppress rules above.
| # | Bug | Catch Phrase | Fix |
|---|---|---|---|
| 1 | Object Slicing | Base b = derived; — derived data lost | Always use Base* or Base& for polymorphism |
| 2 | Missing virtual destructor | delete base_ptr; → derived dtor never called | virtual ~Base() {} in every polymorphic base |
| 3 | Shallow copy | Two objects share same pointer → double free | Deep copy constructor, or use unique_ptr (Rule of Zero) |
| 4 | Self-assignment not handled | a = a; deletes data before copying from self | if (this == &other) return *this; at top of op= |
| 5 | Virtual call in constructor | Dispatches to base, not derived (vtable not set up yet) | Never rely on virtual dispatch in ctor/dtor |
| 6 | Hiding, not overriding | Non-virtual base function "overridden" — wrong version called | virtual in base + override in derived |
| 7 | Not calling base constructor | Base member default-initialized or not initialized | Derived() : Base(args) {} always chain explicitly |
| 8 | No self-assignment in move | move(a, a) — frees memory before stealing it | if (this == &other) return *this; in move op= too |
| 9 | Using moved-from object | Dereferences nullptr after move | Treat moved-from as unspecified; don't use it |
| 10 | Diamond ambiguity | Two copies of base class, ambiguous member access | Virtual inheritance for shared base in diamond |
virtual — enables dynamic dispatch
virtual f() = 0 — pure virtual
override — verify overrides virtual
final — prevents further override/inherit
virtual ~Base() — always in base
public — anyone
protected — class + subclasses
private — class only
friend — named class/function
struct default: public
class default: private
Ctor, copy ctor, copy=
move ctor, move=, dtor
= default — request generation
= delete — prohibit
Prefer Rule of Zero
static_cast<T>(x) — compile-time
dynamic_cast<T*>(p) — RTTI, safe
const_cast<T>(x) — add/remove const
reinterpret_cast<T>(x) — raw bits
Prefer static_cast; avoid C-cast (T)x
RAII — resource in ctor, free in dtor
Pimpl — hide implementation in private impl
CRTP — Curiously Recurring Template Pattern
NVI — Non-Virtual Interface
Dependency Injection — pass dependencies in
S: One reason to change
O: Extend without modifying
L: Substitutable subtype
I: Small focused interfaces
D: Depend on abstractions