Dynamic Memory
Stack memory is automatic — the compiler manages it. Heap memory is manual — you allocate it with new and must release it with delete. Dynamic allocation lets you create objects whose size or lifetime is not known at compile time. It also introduces the risk of memory leaks and double frees — which is why modern C++ wraps heap allocation in smart pointers.
Stack vs Heap
void example() {
// STACK — automatic, freed when function returns
int x = 42; // Stack allocation
int arr[100]; // Stack — must know size at compile time
// These go away automatically at the closing brace
// HEAP — manual, exists until explicitly deleted
int* p = new int(42); // Single int on heap
int* arr2 = new int[100]; // Array of 100 ints on heap
delete p; // Free single object
delete[] arr2; // Free array (use delete[], not delete!)
}| Property | Stack | Heap |
|---|---|---|
| Allocation speed | Very fast | Slower (allocator overhead) |
| Lifetime | Block/function scope | Until delete is called |
| Size limit | Small (~1-8 MB) | Large (limited by RAM) |
| Size known at compile time | Must be | Can be determined at runtime |
| Memory management | Automatic | Manual (or smart pointers) |
new and delete
#include <iostream>
using namespace std;
int main() {
// Allocate and initialise a single object
int* n = new int(99);
cout << *n << "\n"; // 99
delete n;
n = nullptr; // Good practice — prevents use-after-free
// Allocate without initialising
double* d = new double; // Uninitialized — contains garbage!
*d = 3.14;
delete d;
// Allocate an array
int size;
cout << "Size: ";
cin >> size;
int* arr = new int[size]; // Runtime-determined size
for (int i = 0; i < size; i++) arr[i] = i * i;
for (int i = 0; i < size; i++) cout << arr[i] << " ";
cout << "\n";
delete[] arr; // MUST use delete[] for arrays
arr = nullptr;
return 0;
}Warning: Mixing delete with new[] (or delete[] with new) is undefined behaviour — always match them.
Allocating Objects
#include <iostream>
#include <string>
using namespace std;
class Person {
public:
string name;
int age;
Person(const string& name, int age) : name(name), age(age) {
cout << "Person created: " << name << "\n";
}
~Person() {
cout << "Person destroyed: " << name << "\n";
}
void greet() const {
cout << "Hi, I'm " << name << " (" << age << ")\n";
}
};
int main() {
// Dynamic allocation — constructor called
Person* alice = new Person("Alice", 30);
alice->greet(); // Hi, I'm Alice (30)
delete alice; // Destructor called
alice = nullptr;
// Array of objects — default constructors required
// Person* arr = new Person[3]; // ERROR if no default constructor
// Array of pointers
Person* people[3];
people[0] = new Person("Bob", 25);
people[1] = new Person("Charlie", 35);
people[2] = new Person("Diana", 28);
for (auto* p : people) {
p->greet();
delete p;
}
return 0;
}Memory Leaks
A memory leak occurs when you allocate memory and never free it:
// LEAK 1: Forgetting delete
void leaky() {
int* p = new int(42);
// Function returns — p (stack) is gone, but heap allocation remains!
// delete p; // This was forgotten
}
// LEAK 2: Exception before delete
void leakyOnException() {
int* p = new int(42);
riskyOperation(); // If this throws, delete never runs!
delete p;
}
// LEAK 3: Overwriting pointer without deleting
void overwriteLeak() {
int* p = new int(1);
p = new int(2); // Original allocation is now unreachable — leaked!
delete p; // Only deletes the second allocation
}Detecting Memory Leaks
# Valgrind (Linux/macOS) — detects leaks, invalid reads/writes
g++ -g myprogram.cpp -o myprogram
valgrind --leak-check=full ./myprogram
# AddressSanitizer (all platforms with Clang/GCC)
g++ -fsanitize=address -g myprogram.cpp -o myprogram
./myprogram # Will report leaks and invalid accesses on exit
# Visual Studio has a built-in memory leak detector on WindowsRAII — The Solution to Manual Memory Management
RAII (Resource Acquisition Is Initialisation) wraps resource management in a class. The constructor acquires; the destructor releases. This ensures cleanup even if exceptions are thrown:
#include <iostream>
#include <stdexcept>
using namespace std;
// RAII wrapper for a raw array
template <typename T>
class ScopedArray {
T* data_;
size_t size_;
public:
ScopedArray(size_t n) : data_(new T[n]()), size_(n) {
cout << "Allocated " << n << " elements\n";
}
~ScopedArray() {
delete[] data_;
cout << "Freed " << size_ << " elements\n";
}
// Access
T& operator[](size_t i) { return data_[i]; }
const T& operator[](size_t i) const { return data_[i]; }
size_t size() const { return size_; }
// Prevent copying (would cause double-free)
ScopedArray(const ScopedArray&) = delete;
ScopedArray& operator=(const ScopedArray&) = delete;
};
void process(size_t n) {
ScopedArray<int> arr(n); // Allocated on stack (wrapper), data on heap
for (size_t i = 0; i < arr.size(); i++) arr[i] = i * 10;
for (size_t i = 0; i < arr.size(); i++) cout << arr[i] << " ";
cout << "\n";
// arr is destroyed here — even if an exception is thrown above
}
int main() {
process(5);
// Allocated 5 elements
// 0 10 20 30 40
// Freed 5 elements
return 0;
}placement new
Place an object at a specific memory address (used in memory pools and embedded systems):
#include <iostream>
#include <new>
using namespace std;
int main() {
// Pre-allocated buffer
alignas(int) char buffer[sizeof(int)];
// Construct an int in the buffer (no heap allocation)
int* p = new(buffer) int(42);
cout << *p << "\n"; // 42
// Must call destructor manually — no delete! (buffer is stack)
p->~int(); // For trivial types this is a no-op, but good practice
return 0;
}Practical Example: Dynamic Stack Data Structure
#include <iostream>
#include <stdexcept>
using namespace std;
template <typename T>
class DynamicStack {
T* data;
int capacity;
int top_;
void resize() {
int newCap = capacity * 2;
T* newData = new T[newCap];
for (int i = 0; i <= top_; i++) newData[i] = data[i];
delete[] data;
data = newData;
capacity = newCap;
cout << "Resized to " << capacity << "\n";
}
public:
DynamicStack(int initialCap = 4)
: data(new T[initialCap]), capacity(initialCap), top_(-1) {}
~DynamicStack() { delete[] data; }
void push(const T& val) {
if (top_ + 1 == capacity) resize();
data[++top_] = val;
}
T pop() {
if (empty()) throw underflow_error("Stack is empty");
return data[top_--];
}
const T& peek() const {
if (empty()) throw underflow_error("Stack is empty");
return data[top_];
}
bool empty() const { return top_ == -1; }
int size() const { return top_ + 1; }
};
int main() {
DynamicStack<int> stack;
for (int i = 1; i <= 6; i++) {
stack.push(i * 10);
cout << "Pushed " << i * 10 << " (size=" << stack.size() << ")\n";
}
while (!stack.empty()) {
cout << "Pop: " << stack.pop() << "\n";
}
return 0;
}
// Pushed 10 (size=1)
// Pushed 20 (size=2)
// Pushed 30 (size=3)
// Pushed 40 (size=4)
// Resized to 8
// Pushed 50 (size=5)
// Pushed 60 (size=6)
// Pop: 60
// Pop: 50
// ...Summary
newallocates on the heap;deletefrees it — always matchnewwithdeleteandnew[]withdelete[]- Set pointers to
nullptrafterdelete— prevents use-after-free bugs - Memory leaks occur when allocated memory is never freed — use Valgrind or AddressSanitizer to detect them
- RAII is the key pattern: wrap heap allocations in a class whose destructor frees the memory — ensures cleanup even on exceptions
- Prefer
std::vector,std::string, and smart pointers over rawnew/deletein modern C++ placement newconstructs an object at a specific address — used for memory pools and embedded systems- The heap is larger but slower than the stack; use heap for large or long-lived objects, stack for small, short-lived ones
Next, we'll learn about smart pointers — unique_ptr, shared_ptr, and weak_ptr — the modern C++ replacement for raw new/delete.