Chapter 17: Memory Management
Memory Management in C++: From Old-School new/delete to Modern, Safe, & Efficient Code
Hello my brilliant student! π Welcome to Lesson 17 β Memory Management β one of the most important, most powerful, and historically most error-prone topics in C++!
C++ gives you manual control over memory β that’s why it’s so fast and flexible, but also why it’s easy to make mistakes like:
- Memory leaks (forgetting to delete)
- Dangling pointers (using memory after delete)
- Double-delete (crashing the program)
- Use-after-free bugs
Today we’re going to learn:
- The old way: new / delete
- The modern, recommended way: smart pointers (unique_ptr, shared_ptr)
- RAII β the golden principle that makes C++ safe and beautiful
- Move semantics β rvalue references (&&) and std::move β the secret to zero-copy efficiency
We’ll go very slowly, with real-life analogies, tons of examples, common mistakes, and modern best practices (2026 style).
Letβs start!
1. The Old Way: Raw new / delete
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
#include <iostream> class Book { public: std::string title; Book(std::string t) : title(std::move(t)) {} ~Book() { std::cout << title << " destroyed\n"; } }; int main() { // Allocate single object Book* novel = new Book("The Great Gatsby"); // Use it std::cout << "Reading: " << novel->title << "\n"; // MUST delete manually! delete novel; // calls destructor // Allocate array int* scores = new int[10]; // ... use scores ... delete[] scores; // note the [] ! return 0; } |
Problems with raw pointers:
- Forget delete β memory leak
- Call delete twice β double delete (crash or corruption)
- Use pointer after delete β dangling pointer (undefined behavior)
- Wrong delete vs delete[] β undefined behavior
- Exception thrown before delete β leak
Verdict (2026): Never use raw new/delete in modern C++ unless you’re writing very low-level library code.
2. RAII β Resource Acquisition Is Initialization (The Golden Rule)
RAII is the most important C++ idiom. It means: Resources (memory, files, locks, socketsβ¦) are acquired in a constructor and released in the destructor β automatically!
When an object goes out of scope (end of block, function return, exceptionβ¦), its destructor runs automatically β no manual cleanup needed.
Smart pointers are perfect RAII wrappers for dynamic memory.
3. Smart Pointers β The Modern, Safe Way
Include: #include <memory>
| Smart Pointer | Ownership | When to use |
|---|---|---|
| std::unique_ptr | Exclusive (only one owner) | Default choice for single ownership |
| std::shared_ptr | Shared (reference counting) | When multiple objects need to share ownership |
| std::weak_ptr | Non-owning reference to shared_ptr | To break cycles or observe without owning |
A. std::unique_ptr β Exclusive ownership (most common)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#include <memory> #include <iostream> int main() { // Modern & safe way auto book = std::make_unique<Book>("1984"); // preferred over new std::cout << "Reading: " << book->title << "\n"; // No delete needed! // When book goes out of scope β destructor called automatically // Transfer ownership auto another = std::move(book); // book is now nullptr if (!book) { std::cout << "book is empty now\n"; } // Array version auto numbers = std::make_unique<int[]>(100); numbers[0] = 42; return 0; } |
Why make_unique is better than new:
- Exception-safe
- Cleaner
- No risk of leak
B. std::shared_ptr β Shared ownership
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
auto data = std::make_shared<std::vector<int>>(1000, 0); // Multiple owners auto copy1 = data; auto copy2 = data; std::cout << "Use count: " << data.use_count() << "\n"; // 3 // Memory freed only when LAST shared_ptr is destroyed |
C. std::weak_ptr β Observe without owning
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
std::shared_ptr<int> sp = std::make_shared<int>(42); std::weak_ptr<int> wp = sp; if (auto locked = wp.lock()) { // try to get shared_ptr std::cout << "Value: " << *locked << "\n"; // 42 } else { std::cout << "Object is gone\n"; } |
Modern rule (2026):
- Use unique_ptr for single ownership (90% of cases)
- Use shared_ptronly when you really need shared ownership
- Use weak_ptr to avoid circular references
4. Move Semantics β Zero-Copy Efficiency (C++11+)
Move semantics let us transfer ownership of resources (memory, file handlesβ¦) without copying β super fast!
Key concepts:
- lvalue β has name, can take address (&x)
- rvalue β temporary, no name (like 42, std::string(“hello”) + “world”)
- rvalue reference (&&) β binds only to rvalues
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
class Resource { private: int* data; size_t size; public: // Normal constructor Resource(size_t n) : size(n), data(new int[n]) { std::cout << "Allocated " << size << " ints\n"; } // Copy constructor (deep copy β expensive) Resource(const Resource& other) : size(other.size), data(new int[other.size]) { std::copy(other.data, other.data + size, data); std::cout << "Copy constructor\n"; } // Move constructor (cheap β steal resources!) Resource(Resource&& other) noexcept : size(other.size), data(other.data) { other.data = nullptr; // important! other.size = 0; std::cout << "Move constructor β fast!\n"; } ~Resource() { delete[] data; std::cout << "Destroyed\n"; } }; int main() { Resource r1(1000000); // normal constructor Resource r2 = std::move(r1); // calls move constructor! // r1 is now "empty" β safe to destroy return 0; } |
std::move β casts an lvalue to rvalue so move can happen:
|
0 1 2 3 4 5 6 7 8 |
Resource r3(500000); Resource r4 = r3; // copy (expensive) Resource r5 = std::move(r3); // move (fast!) |
Rule: Always mark move constructors and move assignment operators as noexcept β STL containers require it for strong exception safety.
5. Full Practical Example β Safe & Efficient Vector-like Class
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
#include <memory> #include <iostream> template <typename T> class SafeVector { private: std::unique_ptr<T[]> data; size_t capacity; size_t size_; public: SafeVector() : capacity(0), size_(0) {} void push_back(const T& value) { if (size_ == capacity) { size_t newCap = capacity == 0 ? 1 : capacity * 2; auto newData = std::make_unique<T[]>(newCap); // Move old elements for (size_t i = 0; i < size_; ++i) { newData[i] = std::move(data[i]); } data = std::move(newData); capacity = newCap; } data[size_++] = value; } T& operator[](size_t index) { return data[index]; } const T& operator[](size_t index) const { return data[index]; } size_t size() const { return size_; } }; int main() { SafeVector<std::string> vec; vec.push_back("Hello"); vec.push_back("World"); vec.push_back("C++"); for (size_t i = 0; i < vec.size(); ++i) { std::cout << vec[i] << " "; } // Hello World C++ } |
Your Mini Homework (Try These!)
- Rewrite your earlier BankAccount class using std::unique_ptr for some internal resource (e.g., transaction log).
- Create a class that manages a dynamic array using move semantics β show copy vs move performance.
- Write a function that takes std::unique_ptr<T> by value (transfer ownership) and prints something.
Youβre doing absolutely phenomenal! Youβve just mastered modern memory management β RAII + smart pointers + move semantics are what make C++ safe, fast, and elegant at the same time.
Next lesson: File I/O & Streams β reading/writing files, binary data, serialization β super useful for real-world apps!
Any questions? Confused about unique_ptr vs shared_ptr? Want more examples with move semantics or RAII in practice? Just ask β your friendly C++ teacher is right here for you! π
