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.
#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.
#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
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.
#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
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.
| Member | Typical form | Role |
|---|---|---|
| Observer | double balance() const | Reads without changing observable state |
| Command | void deposit(double) | May change object state |
| Fluent command | Widget& set_name(...) | Changes and returns this object |
| Static operation | static Widget parse(...) | Associated with the type, not one object |
Watch out
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.
#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
#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.