Skip to content

4. Classes: Fundamentals

What & why

A class bundles data with the operations that act on it, and — crucially — lets you enforce rules about that data. A BankAccount whose balance can be set to anything by anyone isn't a bank account; it's a double with a name. Classes let you guarantee invariants: things that are always true about an object.

struct vs. class

They're the same thing, except for the default access level:

struct Point {     // members public by default
    double x{};
    double y{};
};

class Account {    // members private by default
    double balance_{};
};

Convention: use struct for plain bundles of data with no invariants; use class when the object protects rules.

A first class

#include <stdexcept>
#include <string>
#include <utility>

class BankAccount {
public:
    // Constructor: establishes the invariant
    BankAccount(std::string owner, double opening_balance)
        : owner_{std::move(owner)}, balance_{opening_balance} {
        if (opening_balance < 0) {
            throw std::invalid_argument("opening balance cannot be negative");
        }
    }

    void deposit(double amount) {
        if (amount <= 0) {
            throw std::invalid_argument("deposit must be positive");
        }
        balance_ += amount;
    }

    void withdraw(double amount) {
        if (amount <= 0 || amount > balance_) {
            throw std::invalid_argument("invalid withdrawal");
        }
        balance_ -= amount;
    }

    // const member functions: promise not to modify the object
    double balance() const { return balance_; }
    const std::string& owner() const { return owner_; }

private:
    std::string owner_;
    double balance_;   // invariant: never negative
};
BankAccount acct{"Ada", 100};
acct.deposit(50);
acct.withdraw(30);
std::cout << acct.owner() << ": " << acct.balance() << '\n';   // Ada: 120
acct.balance_ = -1'000'000;   // ERROR: 'balance_' is private

Every public function either keeps the invariant or throws. There's no way to create or reach a negative balance.

(Money as double is fine for a toy; real code stores integer cents.)

Access specifiers

  • public: — anyone can use it. This is your interface.
  • private: — only the class's own member functions (and friends). This is your implementation.
  • protected: — the class and its derived classes (lesson 7).

A common naming convention marks private data members with a trailing underscore (balance_) or m_ prefix.

Constructors

Member initializer lists

BankAccount(std::string owner, double opening)
    : owner_{std::move(owner)}, balance_{opening} {   // ← initializer list
    // body runs after members are initialized
}

Always initialize members in the initializer list, not by assignment in the body. const members, reference members, and members without a default constructor must be initialized there.

Members are initialized in declaration order, regardless of the order in the list. -Wall warns when they differ.

Default member initializers

class Config {
    int retries_{3};
    std::string host_{"localhost"};
    bool verbose_{false};
public:
    Config() = default;                          // uses the defaults above
    explicit Config(std::string host) : host_{std::move(host)} {}
};

explicit

A one-argument constructor is also an implicit conversion:

class Meters {
public:
    Meters(double v) : value_{v} {}     // implicit!
private:
    double value_;
};

void run(Meters distance);
run(5.0);   // silently converts 5.0 → Meters — maybe not what you want

Mark single-argument constructors explicit unless you truly want the conversion:

explicit Meters(double v) : value_{v} {}
run(5.0);            // ERROR
run(Meters{5.0});    // clear

Delegating constructors

class Rect {
public:
    Rect(double w, double h) : w_{w}, h_{h} {}
    explicit Rect(double side) : Rect{side, side} {}   // delegates
private:
    double w_, h_;
};

const member functions

A const object — or a const& to one — can only call const member functions:

void report(const BankAccount& a) {
    std::cout << a.balance();   // OK: balance() is const
    a.deposit(10);              // ERROR: deposit() is not const
}

Mark every member function that doesn't modify the object const. Forgetting to is a common reason "my class can't be used with const&".

static members

Belong to the class, not to any one object:

class Widget {
public:
    Widget() { ++count_; }
    ~Widget() { --count_; }
    static int count() { return count_; }
private:
    static inline int count_ = 0;   // `inline` lets you define it in the header (C++17)
};

std::cout << Widget::count();

Static member functions have no this, so they can't access non-static members.

this

Inside a member function, this is a pointer to the current object. You rarely need it explicitly, except to return the object for chaining:

class Builder {
public:
    Builder& name(std::string n) { name_ = std::move(n); return *this; }
    Builder& size(int s) { size_ = s; return *this; }
private:
    std::string name_;
    int size_{};
};

Builder b;
b.name("box").size(3);

Splitting a class across .hpp / .cpp

// account.hpp
#pragma once
#include <string>

class BankAccount {
public:
    BankAccount(std::string owner, double opening_balance);
    void deposit(double amount);
    double balance() const;
private:
    std::string owner_;
    double balance_;
};
// account.cpp
#include "account.hpp"
#include <stdexcept>

BankAccount::BankAccount(std::string owner, double opening_balance)
    : owner_{std::move(owner)}, balance_{opening_balance} {}

void BankAccount::deposit(double amount) {
    if (amount <= 0) throw std::invalid_argument("deposit must be positive");
    balance_ += amount;
}

double BankAccount::balance() const { return balance_; }   // const repeated here

Short functions defined inside the class body are implicitly inline and fine to keep in the header.

Designing good classes

  • Start from the interface: what should users be able to do? Keep data private.
  • Establish invariants in constructors; maintain them in every public member function.
  • No "setters for everything" — set_balance() destroys the invariant. Offer meaningful operations (deposit, withdraw).
  • Make illegal states unrepresentable — e.g. an Email class that can only be constructed from a valid address.
  • Keep classes small — one responsibility each.

Common mistakes

  • Forgetting const on member functions that don't modify.
  • Uninitialized members — a double balance_; with no initializer holds garbage. Use default member initializers.
  • Initializer list order ≠ declaration order.
  • Missing explicit on single-argument constructors.
  • Forgetting the semicolon after the class's closing } — errors appear in the next file/line.
  • Getters that return non-const references to private data — breaks encapsulation.
  • Forgetting ClassName:: when defining members in the .cpp — you get a free function and a linker error.

Exercises

  1. Write a Temperature class that stores Kelvin internally, can't go below absolute zero, and offers celsius(), fahrenheit(), and named constructors static Temperature from_celsius(double).
  2. Write a Stack class wrapping std::vector<int> with push, pop (throws on empty), top, size, empty. Mark the right functions const.
  3. Write a Fraction class that always stores its value in lowest terms with a positive denominator (use std::gcd), and throws on zero denominator.
  4. Split Fraction into fraction.hpp / fraction.cpp and use it from main.cpp.
  5. Add a static counter of live Fraction objects and verify it with objects in nested scopes.
  6. Take a class with public data and setters for every field and redesign it around invariants and meaningful operations.

Recap

  • Classes bundle data with behavior and protect invariants; private data, public interface.
  • Constructors establish invariants; use member initializer lists and default member initializers.
  • Mark single-argument constructors explicit and non-mutating member functions const.
  • static members belong to the class; this points to the current object.
  • Declare in .hpp, define in .cpp with ClassName::.

Next: Copy, move & the rule of five →