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:
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
Emailclass that can only be constructed from a valid address. - Keep classes small — one responsibility each.
Common mistakes¶
- Forgetting
conston 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
expliciton 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¶
- Write a
Temperatureclass that stores Kelvin internally, can't go below absolute zero, and offerscelsius(),fahrenheit(), and named constructorsstatic Temperature from_celsius(double). - Write a
Stackclass wrappingstd::vector<int>withpush,pop(throws on empty),top,size,empty. Mark the right functionsconst. - Write a
Fractionclass that always stores its value in lowest terms with a positive denominator (usestd::gcd), and throws on zero denominator. - Split
Fractionintofraction.hpp/fraction.cppand use it frommain.cpp. - Add a static counter of live
Fractionobjects and verify it with objects in nested scopes. - 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;
privatedata,publicinterface. - Constructors establish invariants; use member initializer lists and default member initializers.
- Mark single-argument constructors
explicitand non-mutating member functionsconst. staticmembers belong to the class;thispoints to the current object.- Declare in
.hpp, define in.cppwithClassName::.