C++ Memory Management & RAII

RAII — Resource Acquisition Is Initialization

Resources are tied to object lifetime
// RAII in action — file closes automatically
{
    std::ofstream file("output.txt");
    file << "data" << std::endl;
}  // file destructor runs here — file is closed

// Without RAII — manual cleanup, error-prone
FILE* f = fopen("output.txt", "w");
if (!f) return;
fprintf(f, "data\n");
fclose(f);  // easy to forget, especially with early returns

RAII binds resource management to object lifetime. When the object goes out of scope, its destructor releases the resource. This eliminates leaks and simplifies error handling.

unique_ptr

Single ownership — the default smart pointer
#include <memory>

// Create
auto ptr = std::make_unique<int>(42);
std::cout << *ptr;  // 42

// Transfer ownership (move, not copy)
auto ptr2 = std::move(ptr);
// ptr is now nullptr

// Custom deleter
auto file = std::unique_ptr<FILE, decltype(&fclose)>(
    fopen("data.txt", "r"), fclose
);

// In containers
std::vector<std::unique_ptr<Device>> devices;
devices.push_back(std::make_unique<Switch>("sw01"));

unique_ptr has zero overhead — same cost as a raw pointer. Cannot be copied, only moved. Use it as your default smart pointer.

shared_ptr

Reference-counted shared ownership
auto p1 = std::make_shared<Host>("sw01", "10.50.1.10");
auto p2 = p1;  // reference count: 2

std::cout << p1.use_count();  // 2

p1.reset();  // p1 releases its share
std::cout << p2.use_count();  // 1
// object destroyed when last shared_ptr goes away

shared_ptr has overhead: reference count (atomic increment/decrement), control block allocation. Use it only when ownership is genuinely shared.

weak_ptr

Non-owning observer — breaks reference cycles
auto shared = std::make_shared<int>(42);
std::weak_ptr<int> weak = shared;

if (auto locked = weak.lock()) {
    std::cout << *locked;  // 42 — temporarily promoted to shared_ptr
}

shared.reset();
if (weak.expired()) {
    std::cout << "object is gone";
}

weak_ptr does not extend the object’s lifetime. Use it for caches, observers, and breaking circular references.

Stack vs Heap

Understanding where objects live
// Stack — automatic, fast, fixed size
int x = 42;                       // stack
std::array<int, 100> arr;         // stack
Host h("sw01", "10.50.1.10", 22); // stack (but String members are on heap)

// Heap — manual/smart pointer, flexible size
auto ptr = std::make_unique<Host>("sw01", "10.50.1.10", 22);  // heap
auto vec = std::vector<int>(1000);  // vector header on stack, elements on heap

Stack allocation is essentially free (just a pointer bump). Heap allocation involves malloc and potential fragmentation. Prefer stack when size is known at compile time.

Move Semantics

Transfer resources instead of copying
std::string a = "hello";
std::string b = std::move(a);
// a is now in a "valid but unspecified state" (likely empty)
// b owns the string data — no copy happened

// std::move in function parameters
void process(std::string data) {
    // data is owned here
}

std::string input = "large data...";
process(std::move(input));  // move, not copy
// input is empty

std::move does not move anything — it casts to an rvalue reference, enabling the move constructor/assignment. After a move, the source is valid but its contents are unspecified.

Modern C++ Guidelines

When to use what
// Prefer: value semantics (stack allocation)
Host h("sw01", "10.50.1.10", 22);

// When you need heap: unique_ptr
auto h = std::make_unique<Host>("sw01", "10.50.1.10", 22);

// When you need shared ownership: shared_ptr
auto h = std::make_shared<Host>("sw01", "10.50.1.10", 22);

// NEVER in modern C++:
// int* p = new int(42);   // raw new
// delete p;                // raw delete

Rule of thumb: value types on the stack, unique_ptr for single ownership on the heap, shared_ptr only when multiple owners genuinely exist. Raw new/delete should not appear in modern C++ code.