RAII (Resource Acquisition Is Initialization) ties resource lifetimes to object lifetimes — a resource is acquired in the constructor and released in the destructor, so it is automatically freed when the object goes out of scope, whether the function returns normally or via exception. Smart pointers (unique_ptr, shared_ptr) apply RAII to dynamic memory, making raw new/delete obsolete in modern C++.
In C, if you allocate memory with malloc, you must remember to call free. If an error occurs in between, you might skip the free — a memory leak. C++ solves this permanently: put the allocation in a constructor, the deallocation in a destructor, and C++ guarantees the destructor runs when the object's scope ends — even if an exception is thrown. This is RAII.
Classes: data + behaviour
A C++ class combines data (member variables) and behaviour (member functions, also called methods). Access is controlled with public, private, and protected. Constructors initialise the object; the destructor cleans it up.
#include <iostream>
#include <string>
class BankAccount {
private:
std::string owner;
double balance;
public:
// Constructor: member initialiser list (preferred over assignment in body)
BankAccount(std::string name, double initial)
: owner(std::move(name)), balance(initial) {}
// Destructor: automatically called when object goes out of scope
~BankAccount() {
std::cout << owner << "'s account closed.\n";
}
void deposit(double amount) {
if (amount > 0) balance += amount;
}
bool withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return true;
}
return false;
}
double getBalance() const { return balance; } // const: does not modify the object
const std::string& getOwner() const { return owner; }
};
int main() {
BankAccount acc("Alice", 1000.0);
acc.deposit(500.0);
acc.withdraw(200.0);
std::cout << acc.getOwner() << ": $" << acc.getBalance() << "\n";
// acc's destructor called automatically here — "Alice's account closed."
}RAII in action
RAII means the resource lifecycle is tied to the object lifecycle. The canonical example is a file handle — open in constructor, close in destructor. This works even when exceptions are thrown.
#include <fstream>
#include <stdexcept>
#include <string>
#include <iostream>
// RAII file wrapper: file is always closed, even if an exception is thrown
class FileWriter {
std::ofstream file;
public:
explicit FileWriter(const std::string& path) : file(path) {
if (!file.is_open())
throw std::runtime_error("Cannot open: " + path);
}
~FileWriter() { file.close(); } // always runs — exception-safe
void write(const std::string& text) { file << text; }
};
// C approach (error-prone):
// FILE* f = fopen("out.txt", "w");
// // ... if exception thrown here, fclose never called — resource leak
// fclose(f);
// C++ RAII approach — always safe:
void processData() {
FileWriter fw("output.txt");
fw.write("Hello from RAII\n");
// throw std::runtime_error("something failed"); // would still close the file
// fw destructor called here — file closed
}
int main() {
try { processData(); }
catch (const std::exception& e) { std::cerr << e.what() << "\n"; }
}Smart pointers — RAII for dynamic memory
std::unique_ptr owns a heap-allocated object exclusively — when it goes out of scope, the object is deleted. No overhead vs raw pointer. std::shared_ptr allows shared ownership via reference counting. Use make_unique and make_shared — never raw new.
#include <memory>
#include <iostream>
#include <string>
struct Widget {
std::string name;
Widget(std::string n) : name(std::move(n)) {
std::cout << "Widget " << name << " created\n";
}
~Widget() { std::cout << "Widget " << name << " destroyed\n"; }
};
int main() {
// unique_ptr: sole ownership, deleted when goes out of scope
auto w1 = std::make_unique<Widget>("Alpha");
w1->name = "Alpha Modified";
std::cout << w1->name << "\n";
// auto w2 = w1; // COMPILE ERROR: can't copy unique_ptr
auto w2 = std::move(w1); // transfer ownership — w1 is now null
// shared_ptr: shared ownership via reference count
auto s1 = std::make_shared<Widget>("Beta");
{
auto s2 = s1; // s2 shares ownership — ref count = 2
std::cout << "ref count: " << s1.use_count() << "\n"; // 2
} // s2 destroyed — ref count = 1
std::cout << "ref count: " << s1.use_count() << "\n"; // 1
// s1 destroyed at end of main — Widget deleted
}The rule of five (and zero)
If a class manages a resource (owns raw memory, a file handle, a mutex), you must define all five special member functions — or the compiler-generated defaults will do the wrong thing (shallow copy instead of deep copy; double-free on destruction). In modern C++ the goal is the rule of zero: design classes so they don't need custom special members, by using unique_ptr, vector, and string which handle their own resources.
#include <cstring>
#include <iostream>
#include <utility>
// Manually managed buffer — demonstrates all five special members
// In real code use std::vector<char> (rule of zero)
class Buffer {
char* data;
size_t size;
public:
// 1. Constructor
explicit Buffer(size_t n) : data(new char[n]()), size(n) {}
// 2. Destructor
~Buffer() { delete[] data; }
// 3. Copy constructor — deep copy
Buffer(const Buffer& other) : data(new char[other.size]), size(other.size) {
std::memcpy(data, other.data, size);
}
// 4. Copy assignment — deep copy + free old memory
Buffer& operator=(const Buffer& other) {
if (this != &other) {
delete[] data;
data = new char[other.size];
size = other.size;
std::memcpy(data, other.data, size);
}
return *this;
}
// 5. Move constructor — steal resources, leave other in valid empty state
Buffer(Buffer&& other) noexcept : data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
}
// 6. Move assignment
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
return *this;
}
size_t getSize() const { return size; }
};Inheritance and virtual dispatch
C++ supports single and multiple inheritance. Virtual functions enable runtime polymorphism through vtable dispatch. Mark destructors virtual in any class intended as a base class — otherwise deleting a derived object through a base pointer only calls the base destructor (undefined behaviour for non-trivial derived types).
#include <iostream>
#include <memory>
#include <vector>
class Shape {
public:
virtual double area() const = 0; // pure virtual — Shape is abstract
virtual std::string name() const = 0;
virtual ~Shape() = default; // virtual destructor — ALWAYS in base classes
};
class Circle : public Shape {
double radius;
public:
explicit Circle(double r) : radius(r) {}
double area() const override { return 3.14159 * radius * radius; }
std::string name() const override { return "Circle"; }
};
class Rectangle : public Shape {
double w, h;
public:
Rectangle(double w, double h) : w(w), h(h) {}
double area() const override { return w * h; }
std::string name() const override { return "Rectangle"; }
};
int main() {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(5.0));
shapes.push_back(std::make_unique<Rectangle>(4.0, 6.0));
for (const auto& s : shapes) {
std::cout << s->name() << ": area = " << s->area() << "\n";
}
// unique_ptrs deleted at end of scope — virtual destructors called correctly
}T(const T&)) is called when an object is initialised from another: Widget b = a;. Copy assignment (T& operator=(const T&)) is called when an already-constructed object is assigned: b = a;. Getting this wrong leads to double-free or no-free bugs. The same distinction applies to move: move constructor vs move assignment.shared_ptr. If you need to observe (read without owning), use a raw pointer or weak_ptr. The compiler error "call to deleted function" when trying to copy a unique_ptr is the type system enforcing single ownership.Shape* p = new Circle(...) and Shape's destructor is not virtual, delete p calls Shape::~Shape() — not Circle::~Circle(). Circle's resources are leaked. This is undefined behaviour in C++. The fix: virtual ~Shape() = default;.RAII and exception safety guarantees
C++ defines three exception safety guarantees for operations. The basic guarantee: if an exception is thrown, no resources are leaked and the program remains in a valid (though possibly modified) state. The strong guarantee: if an exception is thrown, the operation has no observable effect — the state is as if the operation was never called (commit-or-rollback semantics). The no-throw guarantee (noexcept): the operation will never throw an exception. Move constructors and destructors should be noexcept — the standard library conditionally uses move operations only if they are noexcept, so failing to mark them noexcept causes std::vector to use copy instead of move during reallocation, degrading performance.
shared_ptr implementation: control block and atomic refcount
A shared_ptr<T> is internally two pointers: one to the managed object (T*) and one to a control block containing the reference count, the weak reference count, a pointer to the managed object (for the deleter), and the deleter itself. The reference counts are std::atomic<int> — thread-safe increment and decrement without a mutex. make_shared performs a single allocation for both the control block and the object (vs two allocations for shared_ptr<T>(new T)) — this reduces allocation overhead and improves cache locality. The aliasing constructor allows a shared_ptr to keep a different object alive while pointing to a member of it — used for member-pointer sharing without exposing implementation details.
The lifetime extension rules and dangling references
C++ has a rule: binding a temporary (rvalue) to a const reference or an rvalue reference extends the temporary's lifetime to the lifetime of the reference. This enables patterns like const auto& s = std::string("hello") + " world"; — the temporary string survives. However, this lifetime extension does not propagate through function calls, struct members, or initialiser lists — attempting to store a reference to a temporary in a struct member is undefined behaviour. C++23's deducing this and the range-for loop's lifetime extension corner cases are active areas of language improvement.
ISO/IEC 14882:2020 (C++20). iso.org/standard/79358.html. Stroustrup, B. (2013). The C++ Programming Language (4th ed.). Addison-Wesley. Sections 3, 16–17 (RAII, constructors/destructors). Meyers, S. (2014). Effective Modern C++. O'Reilly. Items 17–22 (special member functions, smart pointers). cppreference.com — unique_ptr, shared_ptr, weak_ptr.