Chapter 3: Memory Management
Memory management is the defining characteristic of C++. Master this chapter thoroughly: it's what separates C++ developers from developers who write C++ poorly.
Memory Layout
┌─────────────────────────────────────────────────────────┐
│ Stack │
│ - Automatic variables (local variables) │
│ - Function parameters │
│ - Return addresses │
│ - LIFO (Last In, First Out) │
│ - Fast allocation/deallocation │
│ - Limited size (~1-8 MB typically) │
├─────────────────────────────────────────────────────────┤
│ ↓ ↑ │
├─────────────────────────────────────────────────────────┤
│ Heap │
│ - Dynamic allocation (new/malloc) │
│ - Manual lifetime management │
│ - Large allocations │
│ - Slower than stack │
│ - Can be fragmented │
├─────────────────────────────────────────────────────────┤
│ BSS (Uninitialized) │
│ - Uninitialized global/static variables │
├─────────────────────────────────────────────────────────┤
│ Data (Initialized) │
│ - Initialized global/static variables │
│ - String literals │
├─────────────────────────────────────────────────────────┤
│ Text │
│ - Program code │
│ - Read-only │
└─────────────────────────────────────────────────────────┘
Stack vs Heap
#include <iostream>
void demonstrateMemory() {
// STACK ALLOCATION (automatic)
int stackVar = 42; // On stack
int stackArray[100]; // On stack (400 bytes)
double coords[3] = {1.0, 2.0, 3.0}; // On stack
// HEAP ALLOCATION (dynamic)
int* heapVar = new int(42); // On heap
int* heapArray = new int[100]; // On heap
// Stack variables automatically cleaned up when function returns
// Heap variables MUST be manually deleted
delete heapVar; // Free single object
delete[] heapArray; // Free array (note the [])
}
When to Use Stack vs Heap
| Use Stack | Use Heap |
|---|---|
| Size known at compile time | Size determined at runtime |
| Small objects (< 1KB rule of thumb) | Large objects |
| Short-lived objects | Long-lived objects (beyond scope) |
| Automatic cleanup needed | Shared ownership |
Pointers
A pointer stores the memory address of another variable.
int main() {
int value = 42;
int* ptr = &value; // ptr holds the ADDRESS of value
// Visual representation:
// value: [42] <- stored at address 0x1000
// ptr: [0x1000] <- stores the address
// Dereference: access the value at the address
std::cout << *ptr << "\n"; // 42
*ptr = 100; // Modify through pointer
std::cout << value << "\n"; // 100
// Pointer arithmetic
int arr[] = {10, 20, 30, 40};
int* p = arr; // Points to first element
std::cout << *p << "\n"; // 10
std::cout << *(p + 1) << "\n"; // 20
std::cout << *(p + 2) << "\n"; // 30
p++; // Move to next element
std::cout << *p << "\n"; // 20
return 0;
}
Null Pointers
// Modern C++ (C++11 and later)
int* ptr = nullptr; // Null pointer literal
// Check before dereferencing
if (ptr != nullptr) {
*ptr = 42; // Safe
}
// Or use implicit conversion to bool
if (ptr) {
*ptr = 42;
}
// Old C-style (avoid)
int* old_ptr = NULL; // Macro, can cause issues
int* very_old = 0; // Works but unclear
Common Pointer Mistakes
// MISTAKE 1: Uninitialized pointer
int* p; // Contains garbage address!
*p = 42; // CRASH or undefined behavior
// MISTAKE 2: Dangling pointer
int* dangling() {
int local = 42;
return &local; // BAD: returning address of local variable
} // local is destroyed here
// MISTAKE 3: Memory leak
void leak() {
int* p = new int(42);
// Forgot to delete p
} // Memory leaked forever
// MISTAKE 4: Double delete
int* p = new int(42);
delete p;
delete p; // CRASH: double delete
// MISTAKE 5: Wrong delete form
int* arr = new int[100];
delete arr; // WRONG: should be delete[]
delete[] arr; // CORRECT
References
A reference is an alias for another variable. It cannot be null or reseated.
int main() {
int value = 42;
int& ref = value; // ref IS value (same memory)
ref = 100; // Modifies value
std::cout << value; // 100
// References must be initialized
// int& bad_ref; // ERROR: must be initialized
// References cannot be reseated
int other = 200;
ref = other; // This assigns other's value to value, NOT rebinding
std::cout << value; // 200
return 0;
}
Pointers vs References
| Aspect | Pointer | Reference |
|---|---|---|
| Can be null | Yes | No |
| Can be reseated | Yes | No |
| Syntax | *ptr to access | Direct access |
| Arithmetic | Supported | Not supported |
| Use when | Nullability needed, arrays, ownership | Always valid alias |
// When to use what
void usePointer(int* p) {
if (p) { // Must check for null
*p = 42;
}
}
void useReference(int& r) {
r = 42; // Always valid (by contract)
}
// Prefer references when:
// - Parameter will always be valid
// - You don't need to reseat
// - Cleaner syntax desired
// Use pointers when:
// - Value might not exist (nullable)
// - Working with arrays
// - Interfacing with C code
// - Need to reseat to different objects
Dynamic Memory: new and delete
// Single objects
int* p = new int; // Uninitialized
int* q = new int(42); // Initialized to 42
int* r = new int{42}; // Brace initialization
delete p;
delete q;
delete r;
// Arrays
int* arr = new int[100]; // Uninitialized
int* arr2 = new int[100](); // Zero-initialized
int* arr3 = new int[5]{1,2,3}; // Partial init (rest are 0)
delete[] arr; // Must use delete[] for arrays
delete[] arr2;
delete[] arr3;
// Object allocation
class MyClass {
public:
MyClass(int x) : value(x) {}
int value;
};
MyClass* obj = new MyClass(42);
delete obj;
MyClass* objects = new MyClass[3]{
MyClass(1), MyClass(2), MyClass(3)
};
delete[] objects;
new vs malloc
// C++ new (preferred)
int* p = new int(42); // Calls constructor, type-safe
delete p; // Calls destructor
// C malloc (avoid in C++)
int* q = (int*)malloc(sizeof(int)); // No initialization
*q = 42;
free(q); // No destructor called
// Never mix!
// int* p = new int(42);
// free(p); // WRONG: undefined behavior
// int* q = (int*)malloc(sizeof(int));
// delete q; // WRONG: undefined behavior
RAII: Resource Acquisition Is Initialization
RAII is the most important C++ idiom. The principle: tie resource lifetime to object lifetime.
// Without RAII - manual resource management (error-prone)
void riskyFunction() {
int* data = new int[1000];
if (someCondition) {
delete[] data; // Must remember to delete
return;
}
doSomething(data);
if (otherCondition) {
delete[] data; // Must remember again
throw std::runtime_error("Error"); // What if we forget?
}
delete[] data; // And again
}
// With RAII - automatic resource management
void safeFunction() {
std::vector<int> data(1000); // RAII container
if (someCondition) {
return; // Automatically cleaned up
}
doSomething(data.data());
if (otherCondition) {
throw std::runtime_error("Error"); // Still cleaned up!
}
} // data destroyed automatically
RAII Pattern
class FileHandle {
private:
FILE* file;
public:
// Acquire resource in constructor
FileHandle(const char* filename) {
file = fopen(filename, "r");
if (!file) {
throw std::runtime_error("Failed to open file");
}
}
// Release resource in destructor
~FileHandle() {
if (file) {
fclose(file);
}
}
// Delete copy (prevent double-close)
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// Allow move
FileHandle(FileHandle&& other) noexcept : file(other.file) {
other.file = nullptr;
}
FILE* get() { return file; }
};
void useFile() {
FileHandle fh("data.txt");
// Use fh...
} // File automatically closed
Smart Pointers
Smart pointers implement RAII for dynamic memory. Always prefer smart pointers over raw new/delete.
std::unique_ptr (Exclusive Ownership)
#include <memory>
// unique_ptr: exactly one owner
std::unique_ptr<int> createInt() {
return std::make_unique<int>(42); // Preferred way to create
}
int main() {
// Creation
std::unique_ptr<int> p1 = std::make_unique<int>(42);
auto p2 = std::make_unique<int>(100); // Cleaner
// Access
std::cout << *p1 << "\n"; // Dereference
int* raw = p1.get(); // Get raw pointer (don't delete it!)
// Cannot copy (exclusive ownership)
// std::unique_ptr<int> p3 = p1; // ERROR
// Can move
std::unique_ptr<int> p3 = std::move(p1);
// p1 is now nullptr
// Check if valid
if (p3) {
std::cout << "p3 is valid\n";
}
if (!p1) {
std::cout << "p1 was moved from\n";
}
// Arrays
auto arr = std::make_unique<int[]>(100);
arr[0] = 42;
return 0;
} // Automatically deleted
std::shared_ptr (Shared Ownership)
#include <memory>
int main() {
// shared_ptr: reference counted
auto p1 = std::make_shared<int>(42);
std::cout << "Use count: " << p1.use_count() << "\n"; // 1
{
std::shared_ptr<int> p2 = p1; // Copy is OK
std::cout << "Use count: " << p1.use_count() << "\n"; // 2
} // p2 destroyed, count decremented
std::cout << "Use count: " << p1.use_count() << "\n"; // 1
return 0;
} // p1 destroyed, count is 0, memory freed
std::weak_ptr (Non-owning Observer)
#include <memory>
// weak_ptr breaks circular references
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // Use weak_ptr to avoid cycle
int value;
};
int main() {
auto p1 = std::make_shared<int>(42);
std::weak_ptr<int> weak = p1; // Observes but doesn't own
// Must lock to access
if (auto shared = weak.lock()) {
std::cout << *shared << "\n"; // 42
}
p1.reset(); // Destroys the int
if (weak.expired()) {
std::cout << "Object no longer exists\n";
}
return 0;
}
When to Use Which Smart Pointer
┌─────────────────────────────────────────────────────────────┐
│ Smart Pointer Decision Guide │
├─────────────────────────────────────────────────────────────┤
│ │
│ Need exclusive ownership? │
│ │ │
│ ├── Yes ──▶ unique_ptr │
│ │ │
│ └── No ──▶ Need shared ownership? │
│ │ │
│ ├── Yes ──▶ shared_ptr │
│ │ │
│ └── No (observer only) │
│ │ │
│ └──▶ weak_ptr │
│ │
├─────────────────────────────────────────────────────────────┤
│ Default choice: unique_ptr (lowest overhead) │
│ Use shared_ptr only when you truly need shared ownership │
│ Use weak_ptr to observe shared_ptr without extending life │
└─────────────────────────────────────────────────────────────┘
Move Semantics
Move semantics enable efficient transfer of resources without copying.
#include <iostream>
#include <vector>
#include <string>
class Buffer {
int* data;
size_t size;
public:
// Constructor
Buffer(size_t s) : data(new int[s]), size(s) {
std::cout << "Construct\n";
}
// Destructor
~Buffer() {
delete[] data;
std::cout << "Destruct\n";
}
// Copy constructor (expensive)
Buffer(const Buffer& other) : data(new int[other.size]), size(other.size) {
std::copy(other.data, other.data + size, data);
std::cout << "Copy\n";
}
// Move constructor (cheap)
Buffer(Buffer&& other) noexcept : data(other.data), size(other.size) {
other.data = nullptr; // Leave source in valid state
other.size = 0;
std::cout << "Move\n";
}
// Copy assignment
Buffer& operator=(const Buffer& other) {
if (this != &other) {
delete[] data;
size = other.size;
data = new int[size];
std::copy(other.data, other.data + size, data);
}
std::cout << "Copy assign\n";
return *this;
}
// Move assignment
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
std::cout << "Move assign\n";
return *this;
}
};
int main() {
Buffer b1(1000); // Construct
Buffer b2 = b1; // Copy
Buffer b3 = std::move(b1); // Move (b1 is now empty)
std::vector<Buffer> vec;
vec.push_back(Buffer(500)); // Move (temporary)
return 0;
}
std::move
std::move doesn't actually move: it casts to an rvalue reference, enabling move semantics.
std::string s1 = "Hello";
std::string s2 = std::move(s1); // s1's content moved to s2
// s1 is now in a valid but unspecified state (likely empty)
// Use std::move when you're done with an object
void processAndConsume(std::vector<int>&& data) {
// ... use data ...
}
std::vector<int> myData = {1, 2, 3};
processAndConsume(std::move(myData));
// Don't use myData after this
Common Memory Patterns
Factory Functions
// Return unique_ptr from factory
std::unique_ptr<Base> createObject(const std::string& type) {
if (type == "A") {
return std::make_unique<DerivedA>();
} else {
return std::make_unique<DerivedB>();
}
}
// Usage
auto obj = createObject("A");
Passing Smart Pointers
// Pass by value: transfers ownership
void takeOwnership(std::unique_ptr<Widget> widget);
// Pass by reference: shares access
void useWidget(const Widget& widget); // Preferred for read
void modifyWidget(Widget& widget); // Preferred for write
// Pass by raw pointer: non-owning access (rarely needed)
void observeWidget(const Widget* widget);
// Pass shared_ptr by value: shares ownership
void shareOwnership(std::shared_ptr<Widget> widget);
// Pass shared_ptr by const ref: uses but doesn't share ownership
void useShared(const std::shared_ptr<Widget>& widget);
Output Parameters
// Prefer return values over output parameters
std::unique_ptr<Result> compute(); // Good
// If you need multiple outputs, use struct or tuple
struct ComputeResult {
int value;
std::string message;
};
ComputeResult compute(); // Good
// Or tuple
std::tuple<int, std::string> compute();
auto [value, message] = compute(); // Structured bindings (C++17)
Exercises
Pointer Practice
- Write a function that swaps two integers using pointers
- Write it again using references
- Compare the calling syntax
Memory Leak Detective
- Write a program that intentionally leaks memory
- Run it with Valgrind or AddressSanitizer
- Fix the leaks
Smart Pointer Conversion
- Take code with raw new/delete
- Convert to use unique_ptr
- Ensure no memory leaks with Valgrind
RAII Wrapper
- Write a RAII class for a resource (e.g., mutex lock, file, network socket)
- Implement proper move semantics
- Delete copy operations
Previous: 02 - Basic Syntax Next: 04 - Object-Oriented Programming