C++ OOP
classes · templates · RAII · smart ptrs · inheritance · polymorphism · move semantics
Sheet 3 of 3
C++17
Intermediate
Printable
Class Anatomy
Class Definition
class Animal { private: string name; int age; public: // Constructor Animal(string n, int a) : name(n), age(a) {} // init list // Destructor ~Animal() { cout << "Destroyed\n"; } // Getter (const — won't modify) string getName() const { return name; } // Method virtual void speak() const { cout << "..."; } };
Constructors — All Types
class Box { public: int w, h; // Default constructor Box() : w(0), h(0) {} // Parameterised Box(int w, int h) : w(w), h(h) {} // Copy constructor Box(const Box& other) : w(other.w), h(other.h) {} // Move constructor (C++11) Box(Box&& other) noexcept : w(other.w), h(other.h) { other.w = other.h = 0; } }; // Create objects Box b1; // default Box b2(10, 5); // param Box b3 = b2; // copy Box b4 = move(b2); // move
Access Specifiers & static
class Counter { private: // class only int count; protected: // class + subclasses int step; public: // everyone static int total; // shared across all Counter() : count(0), step(1) { total++; } void increment() { count += step; } int get() const { return count; } // Static method static int getTotal() { return total; } }; // Define static outside class: int Counter::total = 0;
Always use initialiser lists — : member(val) — instead of assignment in the constructor body. They're faster (direct-init) and required for const and reference members.
Inheritance & Polymorphism
Single Inheritance
class Animal { protected: string name; public: Animal(string n) : name(n) {} virtual void speak() const { cout << "...\n"; } virtual ~Animal() = default; }; class Dog : public Animal { public: Dog(string n) : Animal(n) {} // call base ctor void speak() const override { cout << name << ": Woof!\n"; } }; class Cat : public Animal { public: Cat(string n) : Animal(n) {} void speak() const override { cout << name << ": Meow!\n"; } };
Polymorphism via Base Pointer
// Runtime polymorphism vector<Animal*> zoo; zoo.push_back(new Dog("Rex")); zoo.push_back(new Cat("Luna")); for (auto* a : zoo) a->speak(); // calls correct version! // Rex: Woof! // Luna: Meow! // dynamic_cast — safe downcast Dog* dp = dynamic_cast<Dog*>(zoo[0]); if (dp) dp->speak(); // safe // Clean up for (auto* a : zoo) delete a;
Abstract Classes & Interfaces
// Pure virtual = abstract class class Shape { public: // Pure virtual — must override virtual double area() const = 0; virtual double perimeter() const = 0; virtual ~Shape() = default; // Non-virtual common method void print() const { cout << "Area=" << area(); } }; class Circle : public Shape { double r; public: Circle(double r) : r(r) {} double area() const override { return 3.14159 * r * r; } double perimeter() const override { return 2 * 3.14159 * r; } }; // Shape s; ← compile error! Circle c(5.0); // OK
Always make base destructors virtual! Without virtual ~Base(), deleting a derived object through a base pointer causes undefined behaviour — the derived destructor never runs, leaking resources.
Templates
Function Templates
// Works for any type T template <typename T> T maxOf(T a, T b) { return (a > b) ? a : b; } maxOf(3, 7) // int: 7 maxOf(3.14, 2.7) // double: 3.14 maxOf<int>(3, 7) // explicit type // Multiple type params template <typename T, typename U> auto add(T a, U b) { return a + b; } add(1, 2.5) // double: 3.5
Class Templates
template <typename T> class Stack { vector<T> data; public: void push(const T& v) { data.push_back(v); } void pop() { data.pop_back(); } T& top() { return data.back(); } bool empty() const { return data.empty(); } }; Stack<int> si; Stack<string> ss; si.push(42); ss.push("hello");
Operator Overloading
Common Operator Overloads
class Vec2 { public: double x, y; Vec2(double x, double y) : x(x), y(y) {} // Arithmetic Vec2 operator+(const Vec2& o) const { return {x+o.x, y+o.y}; } // Equality bool operator==(const Vec2& o) const { return x==o.x && y==o.y; } // Stream output (friend) friend ostream& operator<<( ostream& os, const Vec2& v) { return os << "(" << v.x << "," << v.y << ")"; } }; Vec2 a{1,2}, b{3,4}; cout << a + b; // (4,6)
RAII — Resource Acquisition Is Initialisation
The RAII Pattern
// RAII: tie resource lifetime // to object scope — no raw new/delete class FileHandle { FILE* fp; public: FileHandle(const char* path) : fp(fopen(path, "r")) {} ~FileHandle() { if (fp) fclose(fp); // auto-close } FILE* get() { return fp; } }; { FileHandle f("data.txt"); // use f.get() ... } // file closed here — guaranteed!
| Smart Ptr | Ownership | Use when |
|---|---|---|
| unique_ptr | Single owner | Default — exclusive ownership |
| shared_ptr | Shared (ref count) | Multiple owners needed |
| weak_ptr | Non-owning observer | Break shared_ptr cycles |
unique_ptr — exclusive ownership
#include <memory> // Create — prefer make_unique auto p = make_unique<Dog>("Rex"); // Use like a pointer p->speak(); (*p).speak(); // Can't copy — only move auto p2 = move(p); // p is now null // Array version auto arr = make_unique<int[]>(10); arr[0] = 42; // Get raw pointer (non-owning) Dog* raw = p2.get(); // Release and reset p2.reset(); // deletes, sets null // No need to call delete!
shared_ptr & weak_ptr
// shared_ptr — reference counted auto sp1 = make_shared<Cat>("Luna"); auto sp2 = sp1; // both own it sp1.use_count(); // 2 sp1.reset(); // count → 1 sp2.reset(); // count → 0, deleted // weak_ptr — non-owning observer auto sp = make_shared<int>(42); weak_ptr<int> wp = sp; // Must lock to use if (auto locked = wp.lock()) cout << *locked; // safe else cout << "expired";
Rule of Zero: If your class uses only smart pointers and standard library members, you don't need to write any destructor, copy/move constructor, or assignment operator. The compiler generates correct ones for free.
Move Semantics
lvalue vs rvalue & std::move
// lvalue — has a name / address int x = 5; // x is lvalue string s = "hi"; // s is lvalue // rvalue — temporary, no name 5; // rvalue string("hi"); // rvalue // std::move — cast to rvalue // signals "I'm done with this" vector<int> a = {1,2,3}; vector<int> b = move(a); // b has data, a is empty — O(1)! // No copy made — just pointer swap
Rule of Five
// If you define ANY of these 5, // define ALL 5 (or = default / = delete) class Buf { int* data; size_t sz; public: // 1. Destructor ~Buf() { delete[] data; } // 2. Copy constructor Buf(const Buf& o); // 3. Copy assignment Buf& operator=(const Buf& o); // 4. Move constructor Buf(Buf&& o) noexcept; // 5. Move assignment Buf& operator=(Buf&& o) noexcept; };
Exception Handling
try / catch / throw
#include <stdexcept> double divide(double a, double b) { if (b == 0) throw invalid_argument("div/0"); return a / b; } try { cout << divide(10, 0); } catch (const invalid_argument& e) { cerr << "Error: " << e.what(); } catch (const exception& e) { cerr << "Std err: " << e.what(); } catch (...) { cerr << "Unknown error"; }
Standard Exception Hierarchy
// <stdexcept> exceptions runtime_error // general runtime logic_error // programming error invalid_argument // bad argument out_of_range // index out of bounds overflow_error // arithmetic overflow // Custom exception class MyError : public runtime_error { public: MyError(const string& msg) : runtime_error(msg) {} }; throw MyError("something broke");
Namespaces, const & Modern C++ Essentials
Namespaces
// Define a namespace namespace math { double sqrt(double x); const double PI = 3.14159; } // Use with scope resolution math::sqrt(9.0); math::PI; // using declaration using math::PI; PI; // OK now // Nested namespace (C++17) namespace app::net::http { void connect(); } // Avoid: using namespace std; // in headers — pollutes global scope
constexpr & const
// const — runtime constant const int MAX = 100; // constexpr — compile-time constant constexpr int SIZE = 1024; constexpr double PI = 3.14159; // constexpr function constexpr int square(int x) { return x * x; } constexpr int s = square(5); // 25 at compile time int arr[square(4)]; // arr[16] // nullptr — type-safe null int* p = nullptr; // not NULL or 0 if (!p) cout << "null";
Type Inference & Utilities
// auto — infer type auto x = 42; // int auto it = v.begin(); // iterator // decltype — get type of expr decltype(x) y = 10; // int y // Type aliases using IntVec = vector<int>; using Fn = function<int(int)>; // static_assert — compile-time check static_assert(sizeof(int) == 4, "Need 32-bit int"); // if constexpr (C++17) template<typename T> void print(T t) { if constexpr (is_integral_v<T>) cout << "int: " << t; else cout << "other: " << t; }
C++ OOP Mastery Checklist
| Classes & OOP | Key point |
|---|---|
| Use initialiser lists | : mem(val) {} |
| Mark non-modifying methods | fn() const |
| Virtual destructor in base | virtual ~Base() |
| Override explicitly | void fn() override |
| Abstract class | = 0 pure virtual |
| Safe downcast | dynamic_cast<T*> |
| Smart Ptrs & RAII | Key point |
|---|---|
| Exclusive ownership | unique_ptr<T> |
| Shared ownership | shared_ptr<T> |
| Non-owning reference | weak_ptr<T> |
| Create smart ptr | make_unique / make_shared |
| Transfer unique_ptr | std::move(ptr) |
| Rule of Zero | prefer smart ptrs |
| Templates & Modern | Key point |
|---|---|
| Generic function | template<typename T> |
| Generic class | template<typename T> class |
| Compile-time constant | constexpr |
| Type inference | auto |
| Type alias | using Alias = Type; |
| Null pointer | nullptr |
You've completed all 3 C/C++ sheets! ·
Next explore: Go Basics · Go Concurrency · Rust Basics · Rust Error Handling — or revisit Java Collections for comparison.