Phase 2 · Classes, Ownership & Resource SafetyModule 9~50 min read

Classes, Objects & Encapsulation

Model behavior and invariants with cohesive classes, controlled access, const-correct member functions, and clear definitions.

What you'll learn

A class is more than a bag of fields: it is a type that keeps its own promises. You will model valid state, expose behavior instead of representation, apply const-correctness, and separate a stable interface from its implementation.

By the end, you'll be able to:

  • Define classes and create independent objects
  • Protect invariants with private data and behavior-rich public operations
  • Use const member functions and static members correctly
  • Split declarations and definitions without exposing unnecessary details

Classes and objects

A class definition introduces a type. Each object has its own non-static data members, and member functions operate on the current object through an implicit this pointer. The member-access operator is . for an object and -> for a pointer.

counter.cpp
#include <iostream>

class Counter {
public:
    void increment() { ++value_; }
    int value() const { return value_; }

private:
    int value_{};
};

int main() {
    Counter downloads;
    downloads.increment();
    downloads.increment();

    Counter uploads;
    uploads.increment();
    std::cout << downloads.value() << ' ' << uploads.value() << '\n';
}

Encapsulation and invariants

An invariant is a condition that must hold whenever a public operation finishes. Private representation prevents callers from bypassing the checks that preserve it. Good encapsulation does not mean writing a setter for every field; it means publishing useful operations that keep the object valid.

bank_account.cpp
#include <iostream>
#include <stdexcept>

class BankAccount {
public:
    explicit BankAccount(double opening_balance)
        : balance_{opening_balance} {
        if (opening_balance < 0.0) {
            throw std::invalid_argument{"negative opening balance"};
        }
    }

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

    bool withdraw(double amount) {
        if (amount <= 0.0 || amount > balance_) return false;
        balance_ -= amount;
        return true;
    }

    double balance() const { return balance_; }

private:
    double balance_{};
};

int main() {
    BankAccount account{100.0};
    account.deposit(25.0);
    account.withdraw(40.0);
    std::cout << account.balance() << '\n';
}

Key idea

If any public call can leave an object invalid, every later member must defend against that invalidity. Preventing the state at the boundary creates a simpler type.

Structs versus classes

struct and class have nearly the same capabilities. Their main difference is default access: struct members and bases are public; class members and bases are private. Conventionally, a struct is a simple value whose representation is its useful interface, while a class hides representation to enforce behavior or invariants.

point.cpp
#include <cmath>
#include <iostream>

struct Point {
    double x{};
    double y{};
};

double distance_from_origin(const Point& point) {
    return std::hypot(point.x, point.y);
}

int main() {
    Point location{3.0, 4.0};
    std::cout << distance_from_origin(location) << '\n';
}

Note

Do not choose class because a type is “important” or struct because it is “small.” Choose whether callers should work directly with the representation.

Const-correct member functions

The trailing const on a member function promises not to modify the object's observable state. Only const member functions can normally be called through a const object or const reference. Mark observers const; this makes read-only contracts work throughout an API.

MemberTypical formRole
Observerdouble balance() constReads without changing observable state
Commandvoid deposit(double)May change object state
Fluent commandWidget& set_name(...)Changes and returns this object
Static operationstatic Widget parse(...)Associated with the type, not one object

Watch out

Returning a non-const reference to private data can punch a hole through encapsulation. Return a value, a read-only view with a clear lifetime, or a controlled operation instead.

Static members and factories

A static member belongs to the class as a whole, so it has no this pointer. Static functions are useful for named factories, parsing, or operations conceptually tied to the type but not to a particular instance. Inline static data members can hold shared class-level constants or carefully controlled state.

temperature.cpp
#include <iostream>

class Temperature {
public:
    static Temperature from_fahrenheit(double value) {
        return Temperature{(value - 32.0) * 5.0 / 9.0};
    }

    double celsius() const { return celsius_; }

private:
    explicit Temperature(double celsius) : celsius_{celsius} {}
    double celsius_{};
};

int main() {
    auto boiling{Temperature::from_fahrenheit(212.0)};
    std::cout << boiling.celsius() << '\n';
}

Interface and implementation

A header normally contains the class declaration that callers compile against. A source file contains out-of-class definitions written with the scope operator, such asBankAccount::deposit. Keep headers self-contained and include what they use. Changing private representation can still force recompilation, but callers should not rely on it semantically.

greeter.h + greeter.cpp
// greeter.h
#pragma once
#include <string>

class Greeter {
public:
    explicit Greeter(std::string name);
    std::string message() const;
private:
    std::string name_;
};

// greeter.cpp
#include "greeter.h"
#include <utility>

Greeter::Greeter(std::string name) : name_{std::move(name)} {}

std::string Greeter::message() const {
    return "Welcome, " + name_ + '!';
}
  • Keep the public surface small and intention-revealing
  • Prefer valid construction over a separate initialization step
  • Make mutation explicit in member names and const qualifiers
  • Use composition of focused types instead of one class that owns every concern

Recap & quick check

Key takeaways

  • A class creates objects that combine state, behavior, and enforceable invariants.
  • Private data is useful when it prevents callers from creating invalid states.
  • Struct and class differ mainly in default access; convention communicates intended use.
  • A trailing const makes a member callable through read-only objects and references.
  • Headers publish interfaces; source files can hold definitions and implementation detail.

Quick check

1. What is a class invariant?

2. What does the trailing const mean in int value() const?

3. What is the main language-level difference between struct and class?

4. Which member has no this pointer?

Next: Module 10 — Constructors, Destructors & RAII, where class lifetime becomes a tool for reliable resource management.