Modern C++ Fundamentals: From Basics to Smart Pointers and Threads · Modern C++ Basics: Types, Containers, Functions and Classes
Classes and RAII: Objects That Clean Up After Themselves
Write classes with constructors, member functions and encapsulation, and learn RAII, the C++ idea that ties resources to object lifetime so cleanup is automatic.
What you'll build
A Timer that prints how long a scope took (when it is destroyed), and a Wallet class with private data, validation and a stream output operator.
The idea
A class groups data and the functions that work on it. struct is the same thing with public members by default.
- Encapsulation: keep data
private, expose a small public interface. Invariants (such as "balance is never negative") are enforced in one place. - Constructors create valid objects. Use the member initializer list:
Wallet(double balance) : balance_{balance} {}. Mark single-argument constructorsexplicit. - Member functions that don't modify the object are
const. - Destructors
~Timer()run automatically when an object goes out of scope, in reverse order of creation, even when an exception is thrown.
That last point is RAII (Resource Acquisition Is Initialization): acquire a resource (memory, file, lock, connection) in a constructor and release it in the destructor. The standard library works this way: std::vector frees its memory, std::ofstream closes its file, std::lock_guard unlocks its mutex. You get cleanup without finally and without remembering to call close().
The rule of zero: if your class holds only members that already manage themselves (strings, vectors, smart pointers), you write no destructor or copy operations, and everything just works.
The code
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
// RAII: the destructor runs when the object leaves scope, whatever happens
class ScopeLogger {
public:
explicit ScopeLogger(std::string name) : name_{std::move(name)} { std::cout << "enter " << name_ << '\n'; }
~ScopeLogger() { std::cout << "leave " << name_ << '\n'; }
private:
std::string name_;
};
class Wallet {
public:
explicit Wallet(double balance = 0.0) : balance_{balance} {
if (balance < 0) throw std::invalid_argument("balance cannot be negative");
}
void deposit(double amount) {
if (amount <= 0) throw std::invalid_argument("deposit must be positive");
balance_ += amount;
history_.push_back(+amount);
}
void withdraw(double amount) {
if (amount > balance_) throw std::runtime_error("insufficient funds");
balance_ -= amount;
history_.push_back(-amount);
}
double balance() const { return balance_; } // const: doesn't change the object
const std::vector<double>& history() const { return history_; }
friend std::ostream& operator<<(std::ostream& out, const Wallet& w) {
return out << "Wallet(" << w.balance_ << ", " << w.history_.size() << " transactions)";
}
private:
double balance_;
std::vector<double> history_;
};
int main() {
ScopeLogger outer{"main"};
Wallet wallet{100};
wallet.deposit(50);
{
ScopeLogger inner{"risky block"};
try {
wallet.withdraw(500);
} catch (const std::exception& e) {
std::cout << "error: " << e.what() << '\n';
}
} // inner is destroyed here
wallet.withdraw(30);
std::cout << wallet << '\n';
std::cout << "history size: " << wallet.history().size() << '\n';
}
Output
enter main enter risky block error: insufficient funds leave risky block Wallet(120, 2 transactions) history size: 2 leave main
Run this on your computer — see “Get Started” for setup.
Read the output order carefully: leave risky block appears as soon as the inner scope ends, and leave main prints last, after everything else. Destructors run in reverse order of construction.
Common mistakes
- Public data members that let any code break your invariants. Keep data private and validate in member functions.
- Forgetting
conston getters, which makes them unusable on const objects. - Manual
new/deleteandfopen/fclose. Wrap resources in objects (smart pointers, streams, lock guards) so cleanup is automatic. - Throwing from a destructor. Destructors are
noexceptby default; an exception escaping one terminates the program. - Declaring a destructor and forgetting the copy/move operations. Prefer the rule of zero, or define all five special functions deliberately.
Key points
- Classes enforce invariants through private data and a small public interface.
- Use member initializer lists,
explicitconstructors andconstmember functions. - RAII ties a resource's lifetime to an object: constructor acquires, destructor releases.
- Follow the rule of zero: let standard types manage resources so you write no special member functions.
Further reading: C++ Core Guidelines: Resource management.
Practice questions
What is RAII and why does it make C++ code safer?
Answer
Resource Acquisition Is Initialization: an object acquires a resource in its constructor and releases it in its destructor. Because destructors run automatically at scope exit, including during exceptions, resources are released without manual cleanup calls.
What is the rule of zero?
Answer
Design classes so that all members manage their own resources (strings, vectors, smart pointers), so you need to write no destructor, copy or move operations. The compiler-generated ones are then correct.
Open in the app to mark lessons complete, take notes and track your progress.