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 constructorstd::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