C++ Cheatsheet

Classes

Use this C++ reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

Class Basics

class Point {
public:                     // access specifier
    double x, y;            // data members

    // Constructor
    Point(double x, double y) : x(x), y(y) {}  // member initializer list

    // Member function
    double distFromOrigin() const {              // const: doesn't modify *this
        return std::sqrt(x*x + y*y);
    }
};

struct Vec2 { double x, y; };  // struct: members public by default

Access specifiers:

SpecifierAccessible from
publicanywhere
protectedclass itself + derived classes
privateclass itself (and friends)

Default: private for class, public for struct.

Constructors

class Widget {
    int id_;
    std::string name_;
public:
    Widget()                        : id_(0), name_("") {}   // default constructor
    Widget(int id, std::string n)   : id_(id), name_(std::move(n)) {}

    // Delegating constructor (C++11); explicit blocks implicit conversion
    explicit Widget(int id)         : Widget(id, "unnamed") {}

    // Default / delete
    Widget(const Widget&) = default;    // explicitly generate
    Widget& operator=(const Widget&) = delete;  // forbid copy-assign
};

Widget a;               // default constructor
Widget b(1, "foo");     // parameterized
Widget c = Widget(2);   // explicit — ok; Widget c = 2; would fail (explicit)
Widget d{3, "bar"};     // brace-init (aggregate or list-init)

Member initializer list runs before the constructor body; always prefer it over assignment in the body (especially for const members and references).

Copy and Move (The Rule of Five)

If you define any of these, consider all five:

class Buffer {
    int* data_;
    std::size_t size_;
public:
    Buffer(std::size_t n) : data_(new int[n]), size_(n) {}

    // 1. Destructor
    ~Buffer() { delete[] data_; }

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

    // 3. Copy assignment
    Buffer& operator=(const Buffer& o) {
        if (this != &o) {
            delete[] data_;
            data_ = new int[o.size_];
            size_ = o.size_;
            std::copy(o.data_, o.data_ + o.size_, data_);
        }
        return *this;
    }

    // 4. Move constructor
    Buffer(Buffer&& o) noexcept : data_(o.data_), size_(o.size_) {
        o.data_ = nullptr; o.size_ = 0;
    }

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

Rule of Zero: if your class only manages resources through RAII members (unique_ptr, vector, etc.), define none of the five — the compiler generates correct versions automatically.

Destructors

class Resource {
public:
    ~Resource() {           // called automatically when object leaves scope
        cleanup();          // or when delete is called
    }
};

// Virtual destructor required in polymorphic base classes:
class Base {
public:
    virtual ~Base() = default;  // without this, delete on Base* is UB
};

Operator Overloading

class Vec2 {
public:
    double x, y;
    Vec2(double x, double y) : x(x), y(y) {}

    // Arithmetic (member or free; free preferred for symmetry)
    Vec2 operator+(const Vec2& o) const { return {x+o.x, y+o.y}; }
    Vec2 operator-(const Vec2& o) const { return {x-o.x, y-o.y}; }
    Vec2 operator*(double s)      const { return {x*s, y*s}; }
    Vec2 operator-()              const { return {-x, -y}; }  // unary

    // Compound assignment
    Vec2& operator+=(const Vec2& o) { x += o.x; y += o.y; return *this; }

    // Comparison (C++20: define <=> and == for all six)
    bool operator==(const Vec2& o) const { return x==o.x && y==o.y; }
    auto operator<=>(const Vec2& o) const = default;  // C++20 spaceship

    // Subscript
    double& operator[](int i) { return i==0 ? x : y; }
    const double& operator[](int i) const { return i==0 ? x : y; }

    // Call
    double operator()(int i) const { return i==0 ? x : y; }

    // Output (free function, needs friend)
    friend std::ostream& operator<<(std::ostream& os, const Vec2& v) {
        return os << "(" << v.x << ", " << v.y << ")";
    }

    // Prefix / postfix increment
    Vec2& operator++()    { ++x; ++y; return *this; }    // prefix
    Vec2  operator++(int) { Vec2 tmp(*this); ++*this; return tmp; } // postfix
};

// Scalar * Vec2 (cannot be member since left operand is double)
Vec2 operator*(double s, const Vec2& v) { return v * s; }

Operators that must be members: =, [], (), ->. Operators that should be free: <<, >>, binary arithmetic.

Inheritance

class Shape {
public:
    virtual double area() const = 0;    // pure virtual → Shape is abstract
    virtual void draw() const { std::cout << "Shape\n"; }  // can override
    virtual ~Shape() = default;
protected:
    std::string color_;
};

class Circle : public Shape {           // public inheritance (IS-A)
    double r_;
public:
    explicit Circle(double r) : r_(r) {}
    double area() const override { return 3.14159 * r_ * r_; }
    void draw() const override {
        Shape::draw();                  // call base version
        std::cout << "Circle r=" << r_ << "\n";
    }
};

// Multiple inheritance
class A { public: virtual void f(); };
class B { public: virtual void f(); };
class C : public A, public B {
    void f() override { A::f(); }     // disambiguate
};

Inheritance types: public (IS-A), protected, private (implementation detail). override — compile error if no matching virtual in base (use always). final — prevents further overriding or derivation.

class Leaf final : public Shape {       // cannot derive from Leaf
    double area() const final override; // cannot override area in subclasses
};

Virtual Dispatch and vtable

Shape* s = new Circle(5.0);
s->area();       // dynamic dispatch → Circle::area() via vtable
delete s;        // calls Circle::~Circle() because destructor is virtual

// Pure virtual: class with at least one pure virtual cannot be instantiated
// Shape s;  // error

Static Members

class Counter {
    static int count_;         // declaration; one copy per class
public:
    Counter() { ++count_; }
    ~Counter() { --count_; }
    static int count() { return count_; }  // static function: no this
};
int Counter::count_ = 0;       // definition outside class (non-inline)

Counter::count();              // call without object

Since C++17, static data members can be inline:

class Cfg {
public:
    inline static int limit = 100;   // defined in class body
};

const Member Functions and mutable

class Cache {
    mutable int hits_ = 0;      // mutable: modifiable even in const context
    int data_ = 0;
public:
    int get() const {
        ++hits_;                // OK because hits_ is mutable
        return data_;
    }
};

Friend

class Secret {
    int value_ = 42;
    friend class Printer;                          // class friend
    friend std::ostream& operator<<(std::ostream&, const Secret&);  // free fn friend
};

Nested Classes and this

class Outer {
public:
    class Inner {               // nested class; no implicit access to Outer members
        void f();
    };
    void show() {
        std::cout << this->id_; // explicit this pointer
    }
private:
    int id_ = 0;
};

Aggregate Initialization and Designated Initializers (C++20)

struct Config {
    int width = 800;
    int height = 600;
    bool fullscreen = false;
};

Config c1{1920, 1080, true};          // positional
Config c2{.width=1920, .height=1080}; // designated (C++20); unspecified → default

Class Templates (see also Templates)

template<typename T>
class Stack {
    std::vector<T> data_;
public:
    void push(T val) { data_.push_back(std::move(val)); }
    T pop() { T v = std::move(data_.back()); data_.pop_back(); return v; }
    bool empty() const { return data_.empty(); }
};

Stack<int> si;
Stack<std::string> ss;

RAII Pattern

Resource Acquisition Is Initialization — acquire in constructor, release in destructor. The destructor always runs, even on exceptions.

class FileHandle {
    FILE* f_;
public:
    explicit FileHandle(const char* path) : f_(std::fopen(path, "r")) {
        if (!f_) throw std::runtime_error("open failed");
    }
    ~FileHandle() { if (f_) std::fclose(f_); }
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
    FILE* get() const { return f_; }
};