C++ Course — 03 · Classes, Constructors, Inheritance, and Polymorphism
C++ classes are how you bundle data + behaviour and enforce invariants. Unlike Python, C++ enforces access control at compile time (private/protected/public), supports multiple inheritance, distinguishes static and dynamic dispatch with virtual, and gives the compiler enough information to inline method calls aggressively. This post covers the OOP essentials.
Series map: 00 Setup → 01 Syntax & Types → 02 Pointers & Memory → 03 Classes & OOP (this post) → 04 Templates & STL → 05 Modern C++ → (future).
Learning Outcome
- Define
class/structwith constructors, destructors, member functions. - Use access specifiers
public,private,protected. - Implement copy + move constructors and assignment (Rule of Five).
- Use inheritance +
virtualfunctions for runtime polymorphism. - Use
override,final, and abstract classes (pure virtual). - Apply the Rule of Zero: design classes that need no explicit copy/move/destructor.
Chapter 1 — class vs struct
1
2
3
4
5
6
7
8
9
10
11
12
13
class Point {
double x_, y_; // private by default
public:
Point(double x, double y) : x_(x), y_(y) {}
double x() const { return x_; }
double y() const { return y_; }
};
struct Pair {
int first; // public by default
int second;
};
class and struct are nearly identical — they differ only in default access: class defaults to private, struct to public. Convention:
- Use
structfor plain data aggregates (no invariants). - Use
classwhen you have invariants to enforce or non-trivial behaviour.
Chapter 2 — Constructors and the initialiser list
1
2
3
4
5
6
7
8
9
10
class Person {
std::string name_;
int age_;
public:
Person(std::string name, int age)
: name_(std::move(name)), // initialiser list
age_(age)
{}
};
The : name_(...), age_(...) syntax is the member initialiser list. It runs before the body — and is the only way to initialise:
constmembers,- reference members,
- base classes,
- members with no default constructor.
Always prefer the initialiser list over assignments inside the body.
Default constructor
1
2
3
4
5
6
7
class Counter {
int n_;
public:
Counter() : n_(0) {} // explicit default
// OR: Counter() = default; // ask compiler to generate one
};
If you declare any constructor, the compiler stops generating the default. Add = default if you want both your custom one and a default.
Explicit
1
2
3
4
5
6
7
8
class Wrapper {
public:
explicit Wrapper(int x) {} // explicit blocks implicit conversion
};
Wrapper w1(5); // OK
Wrapper w2 = 5; // ERROR if explicit
Mark single-argument constructors explicit to prevent surprise conversions. Standard advice.
Chapter 3 — Destructor
1
2
3
4
5
6
7
class Buffer {
int* data_;
public:
Buffer(int n) : data_(new int[n]{}) {}
~Buffer() { delete[] data_; } // destructor
};
The destructor runs automatically when the object's lifetime ends:
- For stack objects: end of scope.
- For heap objects: when you call
delete. - For class members: after the enclosing object's destructor body runs.
In modern code, most classes don't need an explicit destructor — the Rule of Zero (Chapter 7).
Chapter 4 — Member functions and const
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Account {
double balance_;
public:
Account(double opening) : balance_(opening) {}
// const method — doesn't modify the object
double balance() const { return balance_; }
// non-const — can modify
void deposit(double amount) {
balance_ += amount;
}
};
const Account a(100);
a.balance(); // OK — const method
a.deposit(50); // ERROR — non-const method on const object
Mark methods const whenever they don't mutate. Lets callers use the class with const references — better safety, more places usable.
this pointer
Inside a member function, this is a pointer to the current object.
1
2
3
4
5
6
7
8
9
10
11
12
class Person {
std::string name_;
public:
Person& set_name(std::string n) {
name_ = std::move(n);
return *this; // enables chaining
}
};
Person p;
p.set_name("Ada").set_name("Lovelace"); // fluent style
Chapter 5 — Copy semantics
The compiler generates a copy constructor and copy assignment operator by default. They do member-wise copy. For most classes this is fine. For classes that own raw memory, you must define them.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Buffer {
int* data_;
int size_;
public:
Buffer(int n) : data_(new int[n]{}), size_(n) {}
~Buffer() { delete[] data_; }
// copy constructor
Buffer(const Buffer& other)
: data_(new int[other.size_]),
size_(other.size_)
{
std::copy(other.data_, other.data_ + size_, data_);
}
// copy assignment
Buffer& operator=(const Buffer& other) {
if (this == &other) return *this; // self-assignment check
delete[] data_;
size_ = other.size_;
data_ = new int[size_];
std::copy(other.data_, other.data_ + size_, data_);
return *this;
}
};
The default member-wise copy would have copied the pointer — both objects would point to the same memory, and the second destructor would double-free. Classic bug.
Chapter 6 — Move semantics (C++11+)
Moving = transferring ownership of resources without copying. Crucial for performance when you want a new object to take over what an old one had.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Buffer {
int* data_;
int size_;
public:
// ... (existing constructor + dtor + copy from Chapter 5)
// move constructor
Buffer(Buffer&& other) noexcept
: data_(other.data_), size_(other.size_)
{
other.data_ = nullptr; // steal the pointer
other.size_ = 0;
}
// move assignment
Buffer& operator=(Buffer&& other) noexcept {
if (this == &other) return *this;
delete[] data_;
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
return *this;
}
};
When does move happen?
- Returning a local from a function.
std::move(x)cast.- Passing a temporary (rvalue) to a constructor.
Trick: if you implement copy + dtor by hand, also implement move. Otherwise the compiler won't generate move automatically, and your class falls back to copies in performance-critical places.
The Rule of Five
If you write any of: destructor, copy ctor, copy assign, move ctor, move assign — you should write (or = default / = delete) all five.
The Rule of Zero
If your class only owns members that already handle their own copy/move/destruction (smart pointers, standard containers), write none of the five. Let the compiler default everything.
1
2
3
4
5
6
7
class Buffer_zero {
std::vector<int> data_; // std::vector handles itself
public:
Buffer_zero(int n) : data_(n) {}
// no destructor, no copy/move declarations — all generated correctly
};
This is the modern way. Reach for explicit Rule-of-Five only when wrapping a C library or hand-managing memory.
Chapter 7 — Inheritance
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Animal {
public:
Animal(std::string name) : name_(std::move(name)) {}
virtual ~Animal() = default;
virtual std::string speak() const = 0; // pure virtual = abstract
std::string name() const { return name_; }
protected:
std::string name_;
};
class Dog : public Animal {
public:
Dog(std::string name) : Animal(std::move(name)) {}
std::string speak() const override {
return name_ + " says woof!";
}
};
class Cat : public Animal {
public:
Cat(std::string name) : Animal(std::move(name)) {}
std::string speak() const override {
return name_ + " says meow";
}
};
Key parts:
class Dog : public Animal— public inheritance (the usual kind).virtual— enables runtime dispatch through base-class pointers/references.= 0— pure virtual; makesAnimalabstract (cannot be instantiated).override— checks at compile time that you're actually overriding. Use it on every override.virtual ~Animal() = default;— always virtual-destructor your polymorphic bases, ordeletethrough a base pointer leaks the derived part.
Polymorphism in action
1
2
3
4
5
6
7
8
std::vector<std::unique_ptr<Animal>> zoo;
zoo.push_back(std::make_unique<Dog>("Rex"));
zoo.push_back(std::make_unique<Cat>("Mochi"));
for (const auto& a : zoo) {
std::cout << a->speak() << "\n"; // calls Dog or Cat version
}
The vector holds Animal* (via unique_ptr), but each speak() dispatches to the correct derived class via the virtual table.
final
1
2
3
4
5
class FinalDog final : public Animal { ... }; // can't be inherited from
class Animal {
virtual void roar() final; // can't be overridden further
};
Use final when you intend to lock down the hierarchy. Documents intent + lets compiler de-virtualise calls.
Chapter 8 — Access specifiers
| Keyword | Inside the class | In derived classes | Outside |
|---|---|---|---|
public |
yes | yes | yes |
protected |
yes | yes | no |
private |
yes | no | no |
Default to private for data members. Expose access via methods (getters/setters with validation). This lets you change the representation later without breaking callers.
friend
1
2
3
4
5
6
class Wallet {
int amount_;
friend class Auditor; // Auditor can access private members
friend bool same_wallet(const Wallet&, const Wallet&);
};
Use sparingly. Tight coupling.
Chapter 9 — Operator overloading (briefly)
1
2
3
4
5
6
7
8
class Vec2 {
double x_, y_;
public:
Vec2(double x, double y) : x_(x), y_(y) {}
Vec2 operator+(const Vec2& o) const { return {x_ + o.x_, y_ + o.y_}; }
bool operator==(const Vec2& o) const { return x_ == o.x_ && y_ == o.y_; }
};
C++ lets you overload most operators. Overload sparingly — only when the semantics are obvious (math types, smart-pointer-like wrappers, iterator types).
C++20 added the spaceship operator <=> which generates all comparison operators in one go:
1
2
3
4
5
#include <compare>
class Vec2 {
auto operator<=>(const Vec2&) const = default;
};
Practice Exercises
Exercise 1 — BankAccount
Build a class BankAccount with private balance_, public deposit(double), withdraw(double) (rejecting negative or over-balance), and balance() const. Use the initialiser list.
Solution sketch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class BankAccount {
double balance_;
public:
explicit BankAccount(double opening) : balance_(opening) {}
double balance() const { return balance_; }
void deposit(double a) {
if (a < 0) throw std::invalid_argument("negative");
balance_ += a;
}
void withdraw(double a) {
if (a < 0) throw std::invalid_argument("negative");
if (a > balance_) throw std::runtime_error("insufficient");
balance_ -= a;
}
};
Exercise 2 — Shape hierarchy
Shape (abstract, pure virtual double area() const), Circle, Rectangle. Compute total area of a vector<unique_ptr<Shape>>.
Exercise 3 — Rule of Zero
Write a class Inventory that holds std::vector<std::string> items. Add methods to add, list, and count items. Do not write a destructor, copy ctor, or assignment operator — Rule of Zero.
Exercise 4 — Rule of Five for a Buffer
Implement Chapter 5+6 Buffer with constructor, destructor, copy ctor, copy assign, move ctor, move assign. Test that copy works (independent buffers) and move leaves the source empty.
Exercise 5 — Operator overload
Extend Vec2 (Chapter 9) with operator-, operator* (scalar multiplication), and operator<< (stream output for printing).
Cheat-sheet
| What | How |
|---|---|
Default access of class |
private |
Default access of struct |
public |
| Initialiser list | : a_(x), b_(y) {} |
| Const method | T foo() const; |
| Default ctor (gen) | MyClass() = default; |
| Block conversion | explicit MyClass(int); |
| Inherit | class D : public B |
| Pure virtual | virtual void f() = 0; |
| Mark override | void f() override; |
| Mark final | void f() final; or class D final |
| Virtual destructor | required for polymorphic base |
| Rule of Five | dtor + 2 copy + 2 move |
| Rule of Zero | write none — use members that manage themselves |
| Move construct | T(T&& o) noexcept |
| Self-assign check | if (this == &other) return *this; |
| Spaceship op | auto operator<=>(const T&) const = default; |
What's next
Post 04 covers templates and the STL — generic programming, std::vector, std::map, std::string, iterators, and the algorithms library that makes most loops unnecessary.
References
- cppreference / language / classes
- A Tour of C++ — Ch. 4 (Classes), Ch. 5 (Essential operations).
- Effective Modern C++ — Items 11–17 (move semantics).
- C++ Core Guidelines — C.20, C.21, C.30–C.66.
Note: this post is in progress. Planned additions: virtual table diagram; CRTP introduction; multiple-inheritance diamond + virtual base.










