C++
01 / 01

C++ Essentials

C++ Essentials

C++ is a compiled, statically-typed systems language that extends C with classes, generics, and strong compile-time abstractions. It gives you manual control over memory and layout like C, but with tools — RAII, templates, the STL — that make that control safe and expressive instead of error-prone. This page assumes you already know C-style syntax and focuses on what's genuinely C++: object orientation, generics, and resource ownership.

Classes, Inheritance & Polymorphism

A class bundles data with the operations that keep it valid. Constructors establish invariants; the destructor tears them down. Mark a base class method virtual to let derived classes override its behavior and have calls resolve at runtime through a vtable — that's what makes polymorphism work through a base pointer or reference.

#include <iostream>
#include <string>

class Shape {
public:
    Shape(std::string name) : name_(std::move(name)) {}
    virtual ~Shape() = default;  // virtual dtor: required for polymorphic deletion

    virtual double area() const = 0;   // pure virtual -> Shape is abstract
    virtual void describe() const {
        std::cout << name_ << " has area " << area() << "\n";
    }

protected:
    std::string name_;
};

class Rectangle : public Shape {
public:
    Rectangle(double w, double h) : Shape("Rectangle"), width_(w), height_(h) {}
    double area() const override { return width_ * height_; }

private:
    double width_, height_;
};

class Circle : public Shape {
public:
    explicit Circle(double r) : Shape("Circle"), radius_(r) {}
    double area() const override { return 3.14159265 * radius_ * radius_; }

private:
    double radius_;
};

void printAll(const std::vector<Shape*>& shapes) {
    for (const Shape* s : shapes) {
        s->describe();  // resolves to Rectangle::area or Circle::area at runtime
    }
}

A pure virtual function (= 0) makes the class abstract — it cannot be instantiated directly, only through a concrete derived class. Always declare the destructor virtual on a base class that will be deleted polymorphically (through a base pointer); otherwise only the base part of a derived object gets destroyed, leaking any resources the derived class owns.

Templates & Generic Programming

Templates let you write a function or class once and have the compiler generate a version for each type it's used with — resolved at compile time, with zero runtime overhead versus hand-written per-type code. This is C++'s answer to generics, and it underlies the entire STL.

template <typename T>
T clamp(T value, T low, T high) {
    if (value < low) return low;
    if (value > high) return high;
    return value;
}

// Class template: a fixed-capacity stack that works for any type
template <typename T, std::size_t Capacity>
class FixedStack {
public:
    void push(const T& item) {
        if (size_ >= Capacity) throw std::overflow_error("stack full");
        data_[size_++] = item;
    }

    T pop() {
        if (size_ == 0) throw std::underflow_error("stack empty");
        return data_[--size_];
    }

    bool empty() const { return size_ == 0; }

private:
    T data_[Capacity];
    std::size_t size_ = 0;
};

// Usage: T and Capacity are deduced or specified explicitly
FixedStack<int, 16> intStack;
intStack.push(42);

int smaller = clamp(120, 0, 100);       // T deduced as int
double d = clamp(3.7, 0.0, 1.0);        // T deduced as double

// Constrain a template with a concept (C++20) instead of relying on SFINAE tricks
template <typename T>
concept Numeric = std::is_arithmetic_v<T>;

template <Numeric T>
T square(T x) { return x * x; }

RAII & Smart Pointers

RAII (Resource Acquisition Is Initialization) is C++'s core idiom: tie a resource's lifetime to an object's lifetime. Acquire the resource in the constructor, release it in the destructor. Because destructors run automatically — even during stack unwinding from an exception — a resource wrapped this way can never leak. Smart pointers apply RAII to heap memory so you rarely call new/delete directly.

#include <memory>

struct Connection {
    Connection() { std::cout << "opening connection\n"; }
    ~Connection() { std::cout << "closing connection\n"; }
    void query(const std::string& sql) { /* ... */ }
};

void handleRequest() {
    // unique_ptr: sole owner. Moves, never copies. Destroyed -> resource freed.
    std::unique_ptr<Connection> conn = std::make_unique<Connection>();
    conn->query("SELECT 1");
    // conn is automatically destroyed here, even if query() throws
}

class Cache {
public:
    // shared_ptr: reference-counted shared ownership. Last owner frees the resource.
    void add(std::shared_ptr<Connection> c) { pool_.push_back(c); }

    // weak_ptr: non-owning reference to a shared_ptr's object. Doesn't keep it
    // alive, and doesn't create the reference cycles that plain shared_ptr can.
    std::weak_ptr<Connection> peek() const {
        return pool_.empty() ? std::weak_ptr<Connection>{} : pool_.front();
    }

private:
    std::vector<std::shared_ptr<Connection>> pool_;
};

void useWeak(std::weak_ptr<Connection> weak) {
    if (auto locked = weak.lock()) {  // promote to shared_ptr if still alive
        locked->query("SELECT 2");
    } else {
        std::cout << "connection already gone\n";
    }
}

Default to unique_ptr — it has zero overhead versus a raw pointer and makes ownership unambiguous. Reach for shared_ptr only when multiple owners genuinely need to keep an object alive together; its atomic refcounting has real runtime cost. Use weak_ptr to break the reference cycles two shared_ptrs pointing at each other would otherwise create, which would leak forever.

STL Containers & Algorithms

The Standard Template Library gives you battle-tested, generic containers (vector, map, unordered_map, set) and algorithms (sort, find_if, accumulate) that operate on them through iterators. Reaching for the STL instead of hand-rolled data structures and loops is idiomatic, safer, and usually faster than what you'd write by hand.

#include <vector>
#include <map>
#include <algorithm>
#include <numeric>

struct Employee { std::string name; int salary; };

void demo() {
    std::vector<Employee> team = {
        {"Ada", 95000}, {"Grace", 110000}, {"Alan", 88000},
    };

    // Sort by salary descending, using a lambda comparator
    std::sort(team.begin(), team.end(), [](const Employee& a, const Employee& b) {
        return a.salary > b.salary;
    });

    // Sum salaries with accumulate
    int total = std::accumulate(team.begin(), team.end(), 0,
        [](int sum, const Employee& e) { return sum + e.salary; });

    // find_if returns an iterator; compare against end() to check for a miss
    auto it = std::find_if(team.begin(), team.end(), [](const Employee& e) {
        return e.name == "Grace";
    });
    if (it != team.end()) {
        std::cout << it->name << " earns " << it->salary << "\n";
    }

    // map keeps keys sorted; unordered_map trades order for O(1) average lookup
    std::unordered_map<std::string, int> bySalary;
    for (const auto& e : team) bySalary[e.name] = e.salary;

    // Range-based for + structured bindings (C++17)
    for (const auto& [name, salary] : bySalary) {
        std::cout << name << ": " << salary << "\n";
    }
}

Move Semantics

Move semantics let an object transfer ownership of its internal resources to another object instead of deep-copying them — the source is left in a valid but unspecified (typically empty) state. This makes passing large objects (vectors, strings) by value cheap, and it's what makes types like unique_ptr transferable without ever copying.

class Buffer {
public:
    explicit Buffer(std::size_t size) : size_(size), data_(new int[size]) {}

    // Copy constructor: deep copy (expensive)
    Buffer(const Buffer& other) : size_(other.size_), data_(new int[other.size_]) {
        std::copy(other.data_, other.data_ + size_, data_);
    }

    // Move constructor: steal the pointer, null out the source (cheap)
    Buffer(Buffer&& other) noexcept : size_(other.size_), data_(other.data_) {
        other.data_ = nullptr;
        other.size_ = 0;
    }

    // Move assignment
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this;
    }

    ~Buffer() { delete[] data_; }

private:
    std::size_t size_;
    int* data_;
};

Buffer makeBuffer() {
    Buffer b(1024);
    return b;  // move (or elided entirely by RVO) -- no deep copy
}

std::vector<Buffer> buffers;
buffers.push_back(std::move(makeBuffer()));  // std::move casts to an rvalue,
                                              // authorizing the move constructor

std::move doesn't move anything by itself — it's a cast that marks its argument as movable-from, letting overload resolution pick the move constructor/assignment over the copy version. After a move, the source object is valid but you shouldn't assume anything about its contents beyond what the type guarantees.

Practical Tips & Pitfalls

  • Follow the Rule of Zero: if your class doesn't manage a raw resource directly, don't write a destructor, copy/move constructor, or assignment operator at all — let the compiler-generated ones (composed from members like unique_ptr and std::string) do the right thing.

  • If you do write one of destructor/copy-ctor/copy-assign/move-ctor/move-assign, you almost always need all five (the Rule of Five) — writing only one usually leaves the others in a broken, compiler-generated state.

  • Prefer passing containers and strings by const reference to avoid copies; pass by value only when you intend to keep or move from the argument.

  • Never use new/delete directly in application code — wrap allocations in make_unique/make_shared so an exception between allocation and use can't leak.

  • Mark single-argument constructors explicit unless you specifically want implicit conversions — it prevents surprising, silent type coercions at call sites.

  • Prefer .at() over operator[] on maps/vectors when you want a bounds/key check; operator[] on a map silently inserts a default value for a missing key, which is a common source of bugs.

  • A dangling reference/pointer from returning a reference to a local variable, or from a shared_ptr cycle that never hits refcount zero, are the two classic C++ memory bugs that smart pointers and RAII don't fully protect you from — reason about ownership explicitly.

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free