C++ Dynamic Memory: Complete Guide to Stack vs Heap Memory
Master C++ dynamic memory with comprehensive examples of stack vs heap memory, new/delete operators, memory management, smart pointers, and practical applications. Learn efficient memory allocation techniques.
Stack Memory
Automatic allocation
Heap Memory
Dynamic allocation
Smart Pointers
Memory safety
Memory Leaks
Prevention & detection
Understanding Memory in C++
C++ provides two main types of memory: stack and heap. Understanding the difference between them is crucial for writing efficient, safe, and correct C++ programs. Dynamic memory management allows programs to allocate memory at runtime, providing flexibility but also requiring careful management.
Memory Layout in C++ Program
Fast, automatic, LIFO structure
Flexible, manual management, global access
Stack Memory
- Allocation: Automatic
- Speed: Very Fast
- Size: Limited (MBs)
- Lifetime: Function scope
- Management: Compiler
Heap Memory
- Allocation: Manual
- Speed: Slower
- Size: Large (GBs)
- Lifetime: Programmer controlled
- Management: Programmer
Smart Pointers
- Allocation: Automatic
- Speed: Fast
- Safety: High
- Lifetime: RAII controlled
- Management: Automatic
Key Concepts:
- Stack: Automatic, fast, limited size, function-scoped variables
- Heap: Manual, flexible, large size, programmer-controlled lifetime
- Dynamic Allocation: Allocating memory at runtime using
new/delete - Memory Leak: Allocated memory that is never freed
- RAII: Resource Acquisition Is Initialization - modern C++ pattern
1. Stack vs Heap Memory: Complete Comparison
| Feature | Stack Memory | Heap Memory |
|---|---|---|
| Allocation Method | Automatic by compiler | Manual using new/malloc |
| Deallocation | Automatic when scope ends | Manual using delete/free |
| Access Speed | Very fast (direct CPU instructions) | Slower (requires pointer indirection) |
| Size Limit | Small (typically 1-8 MB) | Large (limited by OS/available RAM) |
| Memory Layout | Contiguous, LIFO structure | Fragmented, random access |
| Lifetime | Function/scope lifetime | Until explicitly freed |
| Flexibility | Fixed size at compile time | Dynamic size at runtime |
| Thread Safety | Each thread has its own stack | Shared across threads (requires synchronization) |
| Common Uses | Local variables, function calls, return addresses | Large data, dynamic arrays, objects with varying lifetime |
| Risk Factors | Stack overflow, use-after-return | Memory leaks, dangling pointers, fragmentation |
Practical Examples: Stack vs Heap
#include <iostream>
#include <string>
#include <chrono>
using namespace std;
using namespace std::chrono;
class Example {
private:
int id;
string name;
public:
Example(int i, string n) : id(i), name(n) {
cout << "Example " << id << " created: " << name << endl;
}
~Example() {
cout << "Example " << id << " destroyed: " << name << endl;
}
void display() {
cout << "ID: " << id << ", Name: " << name << endl;
}
};
void demonstrateStack() {
cout << "\n=== STACK MEMORY DEMONSTRATION ===\n";
// All variables below are on the stack
int localInt = 42; // Integer on stack
double localDouble = 3.14159; // Double on stack
char localChar = 'A'; // Character on stack
int localArray[5] = {1, 2, 3, 4, 5}; // Array on stack
string localString = "Hello Stack"; // String object on stack (manages heap internally)
// Object on stack
Example stackObject(1, "Stack Object");
stackObject.display();
// Nested scope demonstration
{
int nestedVariable = 100;
cout << "Nested variable: " << nestedVariable << endl;
// nestedVariable will be automatically destroyed here
}
// nestedVariable no longer accessible here
cout << "Stack variables will be automatically destroyed when function returns\n";
}
void demonstrateHeap() {
cout << "\n=== HEAP MEMORY DEMONSTRATION ===\n";
// Manual memory allocation on heap
int* heapInt = new int(42); // Integer on heap
double* heapDouble = new double(3.14159); // Double on heap
char* heapChar = new char('A'); // Character on heap
int* heapArray = new int[5]{1, 2, 3, 4, 5}; // Array on heap
Example* heapObject = new Example(2, "Heap Object"); // Object on heap
// Using heap memory
cout << "Heap integer: " << *heapInt << endl;
cout << "Heap double: " << *heapDouble << endl;
cout << "Heap char: " << *heapChar << endl;
cout << "Heap array[2]: " << heapArray[2] << endl;
heapObject->display();
// MUST manually deallocate heap memory
delete heapInt;
delete heapDouble;
delete heapChar;
delete[] heapArray; // Use delete[] for arrays
delete heapObject;
cout << "Heap memory manually deallocated\n";
}
void performanceComparison() {
cout << "\n=== PERFORMANCE COMPARISON ===\n";
const int SIZE = 1000000;
// Stack allocation timing
auto start = high_resolution_clock::now();
for (int i = 0; i < 100; i++) {
int stackArray[1000]; // Stack allocation
// Use the array to prevent optimization
for (int j = 0; j < 1000; j++) {
stackArray[j] = j;
}
}
auto stop = high_resolution_clock::now();
auto stackDuration = duration_cast(stop - start);
// Heap allocation timing
start = high_resolution_clock::now();
for (int i = 0; i < 100; i++) {
int* heapArray = new int[1000]; // Heap allocation
// Use the array
for (int j = 0; j < 1000; j++) {
heapArray[j] = j;
}
delete[] heapArray; // Must deallocate
}
stop = high_resolution_clock::now();
auto heapDuration = duration_cast(stop - start);
cout << "Stack allocation time: " << stackDuration.count() << " microseconds\n";
cout << "Heap allocation time: " << heapDuration.count() << " microseconds\n";
cout << "Heap is " << (double)heapDuration.count() / stackDuration.count()
<< " times slower than stack\n";
}
void stackOverflowDemo() {
cout << "\n=== STACK OVERFLOW DEMONSTRATION ===\n";
// WARNING: This may crash your program!
// Uncomment to see stack overflow
/*
void causeOverflow(int depth) {
char largeArray[100000]; // Large stack allocation
cout << "Depth: " << depth << endl;
causeOverflow(depth + 1); // Recursive call
}
causeOverflow(1);
*/
cout << "Stack overflow occurs when stack memory is exhausted\n";
cout << "Common causes: deep recursion, large local arrays\n";
}
void memoryLeakDemo() {
cout << "\n=== MEMORY LEAK DEMONSTRATION ===\n";
// Memory leak example
for (int i = 0; i < 10; i++) {
int* leak = new int(i); // Allocated but never deleted
// No delete - MEMORY LEAK!
// delete leak; // Uncomment to fix
}
cout << "10 integers leaked (40 bytes)\n";
cout << "In real programs, this can cause crashes over time\n";
}
int main() {
cout << "=== STACK VS HEAP MEMORY IN C++ ===\n";
demonstrateStack();
demonstrateHeap();
performanceComparison();
stackOverflowDemo();
memoryLeakDemo();
return 0;
}
When to Use Stack
- Small, short-lived variables
- Known, fixed size at compile time
- Local variables in functions
- Temporary calculation results
- Function parameters and return values
When to Use Heap
- Large data structures
- Size unknown at compile time
- Objects that outlive their creating function
- Shared data between functions/threads
- Polymorphic objects (base class pointers)
2. Dynamic Memory Allocation: new and delete
type* pointer = new type; // Single object
type* array = new type[size]; // Array of objects
// Deallocation syntax:
delete pointer; // Single object
delete[] array; // Array of objects
// Placement new (advanced):
type* ptr = new(address) type; // Construct at specific address
Complete Guide to new and delete Operators
#include <iostream>
#include <cstring> // For memset
using namespace std;
class DynamicClass {
private:
int id;
char* buffer;
public:
DynamicClass(int i, const char* text) : id(i) {
buffer = new char[strlen(text) + 1];
strcpy(buffer, text);
cout << "DynamicClass " << id << " constructed: " << buffer << endl;
}
~DynamicClass() {
cout << "DynamicClass " << id << " destroyed: " << buffer << endl;
delete[] buffer; // Clean up internal dynamic memory
}
void display() const {
cout << "ID: " << id << ", Text: " << buffer << endl;
}
// Copy constructor (deep copy)
DynamicClass(const DynamicClass& other) : id(other.id) {
buffer = new char[strlen(other.buffer) + 1];
strcpy(buffer, other.buffer);
cout << "DynamicClass " << id << " copy constructed" << endl;
}
// Assignment operator (deep copy)
DynamicClass& operator=(const DynamicClass& other) {
if (this != &other) {
delete[] buffer; // Free existing memory
id = other.id;
buffer = new char[strlen(other.buffer) + 1];
strcpy(buffer, other.buffer);
cout << "DynamicClass " << id << " assigned" << endl;
}
return *this;
}
};
int main() {
cout << "=== DYNAMIC MEMORY ALLOCATION: new AND delete ===\n\n";
// 1. Basic dynamic allocation
cout << "1. Basic Dynamic Allocation:" << endl;
cout << "-----------------------------" << endl;
// Single object allocation
int* singleInt = new int(42);
cout << "Single integer: " << *singleInt << " at address: " << singleInt << endl;
// Single object with constructor
DynamicClass* singleObj = new DynamicClass(1, "Hello Dynamic");
singleObj->display();
// Array allocation
int* intArray = new int[5];
for (int i = 0; i < 5; i++) {
intArray[i] = i * 10;
}
cout << "Int array: ";
for (int i = 0; i < 5; i++) {
cout << intArray[i] << " ";
}
cout << endl;
// Array of objects
DynamicClass* objArray = new DynamicClass[3] {
DynamicClass(2, "First"),
DynamicClass(3, "Second"),
DynamicClass(4, "Third")
};
// 2. Proper deallocation
cout << "\n2. Proper Deallocation:" << endl;
cout << "------------------------" << endl;
delete singleInt; // Single object
delete singleObj; // Single object with destructor
delete[] intArray; // Array of primitives
delete[] objArray; // Array of objects (calls destructors)
cout << "All memory properly deallocated\n";
// 3. Common mistakes and proper patterns
cout << "\n3. Common Patterns and Mistakes:" << endl;
cout << "----------------------------------" << endl;
// A. Always check allocation success
int* largeArray = nullptr;
try {
largeArray = new int[1000000000]; // Very large allocation
cout << "Large array allocated successfully\n";
} catch (const bad_alloc& e) {
cout << "Memory allocation failed: " << e.what() << endl;
largeArray = nullptr;
}
// B. Initialize dynamically allocated memory
int* uninitialized = new int; // Contains garbage
int* initialized = new int(); // Value-initialized to 0
int* zeroed = new int{}; // Also value-initialized to 0 (C++11)
cout << "Uninitialized: " << *uninitialized << " (garbage)" << endl;
cout << "Initialized: " << *initialized << " (0)" << endl;
cout << "Zeroed: " << *zeroed << " (0)" << endl;
delete uninitialized;
delete initialized;
delete zeroed;
// C. Array initialization
int* arr1 = new int[5]; // Uninitialized
int* arr2 = new int[5](); // All zeros
int* arr3 = new int[5]{1, 2, 3}; // Partial initialization (rest zeros)
cout << "Array 2 (all zeros): ";
for (int i = 0; i < 5; i++) cout << arr2[i] << " ";
cout << "\nArray 3 (partial init): ";
for (int i = 0; i < 5; i++) cout << arr3[i] << " ";
cout << endl;
delete[] arr1;
delete[] arr2;
delete[] arr3;
// 4. Dynamic 2D arrays
cout << "\n4. Dynamic 2D Arrays:" << endl;
cout << "---------------------" << endl;
int rows = 3, cols = 4;
// Method 1: Array of pointers (jagged array)
int** matrix1 = new int*[rows];
for (int i = 0; i < rows; i++) {
matrix1[i] = new int[cols];
for (int j = 0; j < cols; j++) {
matrix1[i][j] = i * cols + j + 1;
}
}
cout << "Method 1 (Array of pointers):\n";
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
cout << matrix1[i][j] << "\t";
}
cout << endl;
}
// Method 2: Single block (contiguous memory)
int* matrix2 = new int[rows * cols];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
matrix2[i * cols + j] = i * cols + j + 1;
}
}
cout << "\nMethod 2 (Single block):\n";
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
cout << matrix2[i * cols + j] << "\t";
}
cout << endl;
}
// Proper cleanup for 2D arrays
for (int i = 0; i < rows; i++) {
delete[] matrix1[i];
}
delete[] matrix1;
delete[] matrix2;
// 5. Placement new (advanced)
cout << "\n5. Placement New (Advanced):" << endl;
cout << "---------------------------" << endl;
// Allocate raw memory
void* rawMemory = operator new(sizeof(DynamicClass));
// Construct object in pre-allocated memory
DynamicClass* placedObj = new(rawMemory) DynamicClass(5, "Placement New");
placedObj->display();
// Manually call destructor
placedObj->~DynamicClass();
// Free raw memory
operator delete(rawMemory);
// 6. Custom new/delete operators
cout << "\n6. Custom Memory Management:" << endl;
cout << "-----------------------------" << endl;
class CustomAllocator {
public:
static void* operator new(size_t size) {
cout << "Custom new called for " << size << " bytes\n";
void* p = malloc(size);
if (!p) throw bad_alloc();
return p;
}
static void operator delete(void* p) {
cout << "Custom delete called\n";
free(p);
}
static void* operator new[](size_t size) {
cout << "Custom new[] called for " << size << " bytes\n";
void* p = malloc(size);
if (!p) throw bad_alloc();
return p;
}
static void operator delete[](void* p) {
cout << "Custom delete[] called\n";
free(p);
}
};
CustomAllocator* customObj = new CustomAllocator();
delete customObj;
CustomAllocator* customArray = new CustomAllocator[3];
delete[] customArray;
return 0;
}
Critical Rules for new/delete:
- Always match new with delete, new[] with delete[]
- Never delete memory twice (double-free error)
- Set pointer to nullptr after deletion
- Check for allocation failure (use try-catch or std::nothrow)
- Initialize dynamically allocated memory
- Implement Rule of Three/Five for classes with dynamic memory
3. Memory Leaks: Detection and Prevention
#include <iostream>
#include <vector>
#include <memory>
#include <cstdlib>
#include <cstring>
using namespace std;
// Class that demonstrates potential memory leaks
class LeakyClass {
private:
char* data;
int* array;
int size;
public:
LeakyClass(int s) : size(s) {
data = new char[100];
array = new int[size];
cout << "LeakyClass allocated " << (100 + size * sizeof(int)) << " bytes\n";
}
// PROBLEM: No destructor to clean up memory!
// ~LeakyClass() { delete[] data; delete[] array; }
// PROBLEM: No copy constructor or assignment operator
// This will cause double-free if copies are made
};
// Proper class with complete memory management
class SafeClass {
private:
char* data;
int* array;
int size;
public:
SafeClass(int s) : size(s) {
data = new char[100];
array = new int[size];
cout << "SafeClass allocated memory\n";
}
// Destructor - essential!
~SafeClass() {
delete[] data;
delete[] array;
cout << "SafeClass freed memory\n";
}
// Copy constructor (deep copy)
SafeClass(const SafeClass& other) : size(other.size) {
data = new char[100];
memcpy(data, other.data, 100);
array = new int[size];
memcpy(array, other.array, size * sizeof(int));
cout << "SafeClass copy constructed\n";
}
// Assignment operator
SafeClass& operator=(const SafeClass& other) {
if (this != &other) {
delete[] data;
delete[] array;
size = other.size;
data = new char[100];
memcpy(data, other.data, 100);
array = new int[size];
memcpy(array, other.array, size * sizeof(int));
cout << "SafeClass assigned\n";
}
return *this;
}
// Move constructor (C++11)
SafeClass(SafeClass&& other) noexcept
: data(other.data), array(other.array), size(other.size) {
other.data = nullptr;
other.array = nullptr;
other.size = 0;
cout << "SafeClass move constructed\n";
}
// Move assignment (C++11)
SafeClass& operator=(SafeClass&& other) noexcept {
if (this != &other) {
delete[] data;
delete[] array;
data = other.data;
array = other.array;
size = other.size;
other.data = nullptr;
other.array = nullptr;
other.size = 0;
cout << "SafeClass move assigned\n";
}
return *this;
}
};
void demonstrateCommonLeaks() {
cout << "\n=== COMMON MEMORY LEAK PATTERNS ===\n";
// 1. Simple allocation without deallocation
cout << "1. Simple Leak:\n";
int* leak1 = new int(42);
// Oops! Forgot to delete leak1
// 2. Early return or exception
cout << "\n2. Early Return Leak:\n";
auto riskyFunction = []() -> int* {
int* ptr = new int[100];
// Early return - memory leak!
// if (someCondition) return nullptr;
// Exception - memory leak!
// throw runtime_error("Error!");
return ptr;
};
int* result = riskyFunction();
// Should delete result here...
// 3. Reassignment without deletion
cout << "\n3. Reassignment Leak:\n";
int* ptr = new int(10);
ptr = new int(20); // First allocation leaked!
delete ptr; // Only deletes second allocation
// 4. Array delete mismatch
cout << "\n4. Delete Mismatch:\n";
int* array = new int[10];
// delete array; // WRONG! Should be delete[] array;
delete[] array; // Correct
// 5. Circular references (with raw pointers)
cout << "\n5. Circular Reference:\n";
class Node {
public:
Node* next;
Node() : next(nullptr) {}
};
Node* a = new Node();
Node* b = new Node();
a->next = b;
b->next = a; // Circular reference
// Both nodes leaked even if we delete one
// 6. Collection of pointers
cout << "\n6. Collection Leak:\n";
vector pointerCollection;
for (int i = 0; i < 10; i++) {
pointerCollection.push_back(new int(i));
}
// Must delete all elements before vector is destroyed
for (auto p : pointerCollection) {
delete p;
}
}
void demonstrateMemoryTools() {
cout << "\n=== MEMORY LEAK DETECTION TOOLS ===\n";
#ifdef _MSC_VER
// Visual Studio memory leak detection
cout << "Visual Studio: Use _CrtDumpMemoryLeaks()\n";
// #define _CRTDBG_MAP_ALLOC
// #include <crtdbg.h>
// _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
#endif
#ifdef __GNUC__
// GCC/Clang memory tools
cout << "GCC/Clang: Use valgrind or AddressSanitizer\n";
cout << "Compile with: -fsanitize=address -g\n";
#endif
// Manual tracking
cout << "\nManual Tracking Techniques:\n";
static int allocations = 0;
static int deallocations = 0;
class TrackedObject {
public:
TrackedObject() { allocations++; }
~TrackedObject() { deallocations++; }
};
TrackedObject* obj1 = new TrackedObject();
TrackedObject* obj2 = new TrackedObject();
delete obj1;
// Forgot to delete obj2
cout << "Allocations: " << allocations << endl;
cout << "Deallocations: " << deallocations << endl;
cout << "Potential leaks: " << (allocations - deallocations) << endl;
}
void demonstrateSmartPointers() {
cout << "\n=== SMART POINTERS FOR MEMORY SAFETY ===\n";
// 1. unique_ptr - exclusive ownership
cout << "1. unique_ptr (no leaks, automatic cleanup):\n";
{
unique_ptr uptr1 = make_unique(42);
unique_ptr uptrArray = make_unique(10);
// No need to delete - automatically deleted when out of scope
cout << "unique_ptr value: " << *uptr1 << endl;
} // Memory automatically freed here
// 2. shared_ptr - shared ownership
cout << "\n2. shared_ptr (reference counting):\n";
{
shared_ptr sptr1 = make_shared(100);
{
shared_ptr sptr2 = sptr1; // Reference count: 2
cout << "Use count inside inner scope: " << sptr1.use_count() << endl;
} // sptr2 destroyed, reference count: 1
cout << "Use count outside inner scope: " << sptr1.use_count() << endl;
} // sptr1 destroyed, memory freed (reference count: 0)
// 3. weak_ptr - break circular references
cout << "\n3. weak_ptr (break circular references):\n";
class CyclicNode {
public:
shared_ptr partner;
weak_ptr weakPartner; // Use weak_ptr to break cycles
CyclicNode(string n) : name(n) {
cout << "Node " << name << " created\n";
}
~CyclicNode() {
cout << "Node " << name << " destroyed\n";
}
string name;
};
{
auto nodeA = make_shared("A");
auto nodeB = make_shared("B");
// PROBLEM: Circular reference with shared_ptr
// nodeA->partner = nodeB;
// nodeB->partner = nodeA;
// Both nodes leak!
// SOLUTION: Use weak_ptr
nodeA->weakPartner = nodeB;
nodeB->weakPartner = nodeA;
// Access through weak_ptr
if (auto sharedB = nodeA->weakPartner.lock()) {
cout << "Accessed B through weak_ptr: " << sharedB->name << endl;
}
} // Both nodes properly destroyed
cout << "\nSmart pointers automatically prevent memory leaks!\n";
}
void demonstrateRAII() {
cout << "\n=== RAII PATTERN ===\n";
cout << "Resource Acquisition Is Initialization\n";
// RAII wrapper for dynamic array
class IntArray {
private:
int* data;
size_t size;
public:
IntArray(size_t n) : size(n) {
data = new int[n];
cout << "IntArray allocated " << n << " integers\n";
}
~IntArray() {
delete[] data;
cout << "IntArray deallocated\n";
}
// No copy (or implement deep copy)
IntArray(const IntArray&) = delete;
IntArray& operator=(const IntArray&) = delete;
// Allow move
IntArray(IntArray&& other) noexcept : data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
}
int& operator[](size_t index) {
return data[index];
}
size_t getSize() const { return size; }
};
{
IntArray arr(10);
for (size_t i = 0; i < arr.getSize(); i++) {
arr[i] = i * 10;
}
cout << "RAII array elements: ";
for (size_t i = 0; i < arr.getSize(); i++) {
cout << arr[i] << " ";
}
cout << endl;
// No need to manually delete - destructor handles it
} // arr automatically destroyed here
cout << "RAII ensures resources are always properly released\n";
}
int main() {
cout << "=== MEMORY LEAKS: DETECTION AND PREVENTION ===\n";
demonstrateCommonLeaks();
demonstrateMemoryTools();
demonstrateSmartPointers();
demonstrateRAII();
// Final cleanup reminder
cout << "\n=== BEST PRACTICES SUMMARY ===\n";
cout << "1. Prefer stack allocation when possible\n";
cout << "2. Use smart pointers (unique_ptr, shared_ptr)\n";
cout << "3. Implement RAII for resource management\n";
cout << "4. Always match new with delete, new[] with delete[]\n";
cout << "5. Use tools (valgrind, AddressSanitizer) to detect leaks\n";
cout << "6. Follow Rule of Three/Five for classes with resources\n";
return 0;
}
Prevention Techniques
- Use smart pointers
- Implement RAII pattern
- Follow Rule of Three/Five
- Prefer stack allocation
- Use standard containers
Detection Tools
- Valgrind (Linux/Mac)
- AddressSanitizer
- Visual Studio Debugger
- Manual tracking counters
- Memory profilers
Common Leak Sources
- Missing delete statements
- Early returns/exceptions
- Circular references
- Wrong delete (delete vs delete[])
- Collection of raw pointers
4. Smart Pointers for Memory Management
#include <iostream>
#include <memory>
#include <vector>
#include <string>
using namespace std;
class Resource {
private:
string name;
int* data;
size_t size;
public:
Resource(string n, size_t s) : name(n), size(s) {
data = new int[size];
for (size_t i = 0; i < size; i++) {
data[i] = i * 10;
}
cout << "Resource '" << name << "' created with " << size << " elements\n";
}
~Resource() {
delete[] data;
cout << "Resource '" << name << "' destroyed\n";
}
void display() const {
cout << name << ": [";
for (size_t i = 0; i < min(size, size_t(5)); i++) {
cout << data[i];
if (i < min(size, size_t(5)) - 1) cout << ", ";
}
if (size > 5) cout << ", ...";
cout << "]\n";
}
string getName() const { return name; }
};
void demonstrateUniquePtr() {
cout << "\n=== unique_ptr: EXCLUSIVE OWNERSHIP ===\n";
// 1. Creating unique_ptr
unique_ptr res1 = make_unique("Unique1", 10);
unique_ptr res2(new Resource("Unique2", 5)); // Alternative
// 2. Accessing and using
res1->display();
cout << "Resource name: " << res1->getName() << endl;
// 3. Ownership transfer (move semantics)
cout << "\nOwnership transfer:\n";
unique_ptr res3 = move(res1); // res1 becomes empty
if (!res1) {
cout << "res1 is now empty\n";
}
res3->display();
// 4. Custom deleters
cout << "\nCustom deleters:\n";
auto customDeleter = [](Resource* r) {
cout << "Custom deleter called for: " << r->getName() << endl;
delete r;
};
unique_ptr res4(
new Resource("CustomDeleted", 3),
customDeleter
);
// 5. Arrays with unique_ptr
cout << "\nDynamic arrays with unique_ptr:\n";
unique_ptr resourceArray(new Resource[3]{
Resource("Array1", 2),
Resource("Array2", 3),
Resource("Array3", 4)
});
for (int i = 0; i < 3; i++) {
resourceArray[i].display();
}
// 6. Returning unique_ptr from functions
auto createResource = [](string name, size_t size) -> unique_ptr {
return make_unique(name, size);
};
auto returnedResource = createResource("Returned", 8);
returnedResource->display();
}
void demonstrateSharedPtr() {
cout << "\n=== shared_ptr: SHARED OWNERSHIP ===\n";
// 1. Creating shared_ptr
shared_ptr shared1 = make_shared("Shared1", 10);
cout << "Initial use count: " << shared1.use_count() << endl;
// 2. Sharing ownership
shared_ptr shared2 = shared1;
shared_ptr shared3 = shared2;
cout << "After sharing, use count: " << shared1.use_count() << endl;
// 3. All objects can use the resource
shared1->display();
shared2->display();
shared3->display();
// 4. Reset and ownership release
cout << "\nResetting shared pointers:\n";
shared2.reset(); // Release ownership
cout << "After reset shared2, use count: " << shared1.use_count() << endl;
shared1.reset();
cout << "After reset shared1, use count: " << shared3.use_count() << endl;
// 5. make_shared efficiency
cout << "\nmake_shared efficiency:\n";
// make_shared allocates object and control block together (more efficient)
auto efficient = make_shared("Efficient", 5);
// 6. shared_ptr with arrays (C++17)
cout << "\nshared_ptr with arrays (C++17):\n";
shared_ptr sharedArray(new Resource[2]{
Resource("SharedArray1", 3),
Resource("SharedArray2", 4)
});
for (int i = 0; i < 2; i++) {
sharedArray[i].display();
}
}
void demonstrateWeakPtr() {
cout << "\n=== weak_ptr: NON-OWNING REFERENCES ===\n";
// 1. Creating weak_ptr from shared_ptr
shared_ptr shared = make_shared("ForWeakPtr", 6);
weak_ptr weak = shared;
cout << "shared use count: " << shared.use_count() << endl;
// 2. Checking and accessing weak_ptr
if (auto locked = weak.lock()) {
cout << "Successfully locked weak_ptr: ";
locked->display();
cout << "Now use count: " << shared.use_count() << endl;
}
// 3. Expired weak_ptr
shared.reset();
cout << "\nAfter reset shared:\n";
cout << "shared use count: " << shared.use_count() << endl;
if (weak.expired()) {
cout << "weak_ptr is expired\n";
}
// 4. Practical example: Cache with weak_ptr
cout << "\nPractical example: Resource Cache\n";
class ResourceCache {
private:
vector> cache;
public:
shared_ptr getResource(string name, size_t size) {
// Check cache first
for (auto& weakRes : cache) {
if (auto sharedRes = weakRes.lock()) {
if (sharedRes->getName() == name) {
cout << "Cache hit: " << name << endl;
return sharedRes;
}
}
}
// Not in cache, create new
cout << "Cache miss, creating: " << name << endl;
auto newResource = make_shared(name, size);
cache.push_back(newResource);
return newResource;
}
void cleanupExpired() {
// Remove expired weak_ptrs
cache.erase(
remove_if(cache.begin(), cache.end(),
[](const weak_ptr& wp) { return wp.expired(); }),
cache.end()
);
}
};
ResourceCache cache;
auto res1 = cache.getResource("Cached1", 5);
auto res2 = cache.getResource("Cached2", 3);
auto res3 = cache.getResource("Cached1", 5); // Should be cache hit
// Release some resources
res1.reset();
res2.reset();
cache.cleanupExpired();
}
void demonstrateOwnershipPatterns() {
cout << "\n=== OWNERSHIP PATTERNS AND BEST PRACTICES ===\n";
// 1. Factory function returning unique_ptr
cout << "1. Factory functions:\n";
auto createUnique = []() -> unique_ptr {
return make_unique("FactoryCreated", 10);
};
auto product = createUnique();
product->display();
// 2. Transferring ownership to containers
cout << "\n2. Containers of unique_ptr:\n";
vector> resources;
resources.push_back(make_unique("Vector1", 3));
resources.push_back(make_unique("Vector2", 4));
resources.push_back(make_unique("Vector3", 5));
// Must use move when adding to vector
auto temp = make_unique("Temp", 2);
resources.push_back(move(temp));
for (auto& res : resources) {
res->display();
}
// 3. Shared ownership in multi-threaded scenarios
cout << "\n3. Thread-safe shared ownership:\n";
// shared_ptr reference counting is thread-safe
// But accessing the object itself needs synchronization
// 4. Converting between smart pointer types
cout << "\n4. Smart pointer conversion:\n";
// unique_ptr to shared_ptr (transfer ownership)
unique_ptr uniqueRes = make_unique("Convertible", 5);
shared_ptr sharedRes = move(uniqueRes);
cout << "Converted unique_ptr to shared_ptr\n";
// Cannot convert shared_ptr to unique_ptr (would break shared ownership)
// 5. Polymorphic objects with smart pointers
cout << "\n5. Polymorphic objects:\n";
class Base {
public:
virtual void display() const {
cout << "Base class\n";
}
virtual ~Base() = default;
};
class Derived : public Base {
public:
void display() const override {
cout << "Derived class\n";
}
};
unique_ptr poly = make_unique();
poly->display();
}
void demonstrateMemorySafety() {
cout << "\n=== MEMORY SAFETY WITH SMART POINTERS ===\n";
// Comparison: Raw pointers vs Smart pointers
// 1. Memory leak prevention
cout << "1. Automatic cleanup:\n";
{
// Raw pointer - potential leak
Resource* raw = new Resource("RawPointer", 5);
// Must remember to delete!
delete raw;
}
{
// Smart pointer - no leak
auto smart = make_unique("SmartPointer", 5);
// Automatically deleted when out of scope
}
// 2. Exception safety
cout << "\n2. Exception safety:\n";
auto riskyOperation = []() {
auto resource = make_unique("ExceptionSafe", 3);
// If exception occurs here...
throw runtime_error("Something went wrong!");
// unique_ptr will still clean up the resource
};
try {
riskyOperation();
} catch (const exception& e) {
cout << "Caught exception: " << e.what() << endl;
cout << "Resource was still properly cleaned up!\n";
}
// 3. Dangling pointer prevention
cout << "\n3. No dangling pointers:\n";
shared_ptr shared;
{
auto temp = make_shared("Temporary", 2);
shared = temp; // shared maintains reference
} // temp goes out of scope, but resource persists
shared->display(); // Safe! Resource still exists
// 4. Custom deleters for special resources
cout << "\n4. Custom deleters for non-heap resources:\n";
// File handle example
FILE* file = fopen("test.txt", "w");
if (file) {
unique_ptr filePtr(file, &fclose);
fprintf(filePtr.get(), "Hello, World!\n");
// File automatically closed when filePtr goes out of scope
cout << "File automatically closed\n";
}
}
int main() {
cout << "=== SMART POINTERS FOR DYNAMIC MEMORY MANAGEMENT ===\n";
demonstrateUniquePtr();
demonstrateSharedPtr();
demonstrateWeakPtr();
demonstrateOwnershipPatterns();
demonstrateMemorySafety();
cout << "\n=== SMART POINTER BEST PRACTICES ===\n";
cout << "1. Prefer unique_ptr for exclusive ownership\n";
cout << "2. Use shared_ptr only when shared ownership is needed\n";
cout << "3. Use weak_ptr to break circular references\n";
cout << "4. Always use make_unique and make_shared when possible\n";
cout << "5. Never mix raw pointers and smart pointers for ownership\n";
cout << "6. Use custom deleters for special resources\n";
return 0;
}
Modern C++ Memory Management Philosophy
In modern C++, avoid manual memory management with raw new/delete. Instead, use smart pointers and RAII. Prefer stack allocation for small, short-lived objects. Use unique_ptr for exclusive ownership, shared_ptr only when shared ownership is truly needed, and weak_ptr to observe shared resources without ownership.
5. Practical Applications and Patterns
Dynamic Data Structures Implementation
#include <iostream>
#include <memory>
#include <stdexcept>
using namespace std;
// 1. Dynamic Array (Vector-like) with RAII
template
class DynamicArray {
private:
unique_ptr data;
size_t capacity;
size_t size;
void resize(size_t newCapacity) {
unique_ptr newData = make_unique(newCapacity);
for (size_t i = 0; i < size; i++) {
newData[i] = move(data[i]);
}
data = move(newData);
capacity = newCapacity;
}
public:
DynamicArray() : capacity(10), size(0) {
data = make_unique(capacity);
}
DynamicArray(size_t initialCapacity) : capacity(initialCapacity), size(0) {
if (initialCapacity == 0) capacity = 1;
data = make_unique(capacity);
}
~DynamicArray() {
// unique_ptr automatically deallocates
}
// No copy (or implement deep copy)
DynamicArray(const DynamicArray&) = delete;
DynamicArray& operator=(const DynamicArray&) = delete;
// Move operations
DynamicArray(DynamicArray&& other) noexcept
: data(move(other.data)), capacity(other.capacity), size(other.size) {
other.capacity = 0;
other.size = 0;
}
DynamicArray& operator=(DynamicArray&& other) noexcept {
if (this != &other) {
data = move(other.data);
capacity = other.capacity;
size = other.size;
other.capacity = 0;
other.size = 0;
}
return *this;
}
void push_back(const T& value) {
if (size >= capacity) {
resize(capacity * 2);
}
data[size++] = value;
}
void push_back(T&& value) {
if (size >= capacity) {
resize(capacity * 2);
}
data[size++] = move(value);
}
T& operator[](size_t index) {
if (index >= size) {
throw out_of_range("Index out of bounds");
}
return data[index];
}
const T& operator[](size_t index) const {
if (index >= size) {
throw out_of_range("Index out of bounds");
}
return data[index];
}
size_t getSize() const { return size; }
size_t getCapacity() const { return capacity; }
void shrink_to_fit() {
if (size < capacity) {
resize(size);
}
}
};
// 2. Linked List with smart pointers
template
class LinkedList {
private:
struct Node {
T data;
unique_ptr next;
Node(T val) : data(move(val)), next(nullptr) {}
};
unique_ptr head;
Node* tail;
size_t listSize;
public:
LinkedList() : head(nullptr), tail(nullptr), listSize(0) {}
~LinkedList() {
// Recursive destruction with unique_ptr will automatically clean up
}
void push_back(const T& value) {
auto newNode = make_unique(value);
if (!head) {
head = move(newNode);
tail = head.get();
} else {
tail->next = move(newNode);
tail = tail->next.get();
}
listSize++;
}
void push_front(const T& value) {
auto newNode = make_unique(value);
newNode->next = move(head);
head = move(newNode);
if (!tail) {
tail = head.get();
}
listSize++;
}
void display() const {
Node* current = head.get();
cout << "List: ";
while (current) {
cout << current->data << " -> ";
current = current->next.get();
}
cout << "nullptr\n";
}
size_t size() const { return listSize; }
bool empty() const { return listSize == 0; }
// Iterator support (simplified)
class Iterator {
private:
Node* current;
public:
Iterator(Node* node) : current(node) {}
T& operator*() { return current->data; }
Iterator& operator++() {
current = current->next.get();
return *this;
}
bool operator!=(const Iterator& other) const {
return current != other.current;
}
};
Iterator begin() { return Iterator(head.get()); }
Iterator end() { return Iterator(nullptr); }
};
// 3. Memory Pool for efficient allocation
class MemoryPool {
private:
struct Block {
unique_ptr memory;
bool isFree;
Block(size_t size) : memory(make_unique(size)), isFree(true) {}
};
vector> blocks;
size_t blockSize;
size_t poolSize;
public:
MemoryPool(size_t blockSize, size_t numBlocks)
: blockSize(blockSize), poolSize(numBlocks) {
for (size_t i = 0; i < numBlocks; i++) {
blocks.push_back(make_unique(blockSize));
}
}
void* allocate() {
for (auto& block : blocks) {
if (block->isFree) {
block->isFree = false;
return block->memory.get();
}
}
return nullptr; // Pool exhausted
}
void deallocate(void* ptr) {
for (auto& block : blocks) {
if (block->memory.get() == ptr) {
block->isFree = true;
return;
}
}
throw runtime_error("Pointer not from this pool");
}
size_t getFreeBlocks() const {
size_t count = 0;
for (auto& block : blocks) {
if (block->isFree) count++;
}
return count;
}
};
// 4. Object Pool for reusing objects
template
class ObjectPool {
private:
vector> pool;
vector freeList;
public:
ObjectPool(size_t initialSize) {
for (size_t i = 0; i < initialSize; i++) {
pool.push_back(make_unique());
freeList.push_back(pool.back().get());
}
}
T* acquire() {
if (freeList.empty()) {
// Expand pool
pool.push_back(make_unique());
freeList.push_back(pool.back().get());
}
T* obj = freeList.back();
freeList.pop_back();
return obj;
}
void release(T* obj) {
freeList.push_back(obj);
}
size_t getFreeCount() const {
return freeList.size();
}
size_t getTotalCount() const {
return pool.size();
}
};
// Example usage
class GameCharacter {
private:
string name;
int health;
public:
GameCharacter(string n = "", int h = 100) : name(n), health(h) {}
void takeDamage(int damage) {
health -= damage;
if (health < 0) health = 0;
}
bool isAlive() const { return health > 0; }
void display() const {
cout << name << " [HP: " << health << "]" << endl;
}
};
void demonstrateDynamicArray() {
cout << "=== DYNAMIC ARRAY IMPLEMENTATION ===\n";
DynamicArray arr;
cout << "Initial capacity: " << arr.getCapacity() << endl;
for (int i = 0; i < 25; i++) {
arr.push_back(i * 10);
}
cout << "After adding 25 elements:\n";
cout << "Size: " << arr.getSize() << ", Capacity: " << arr.getCapacity() << endl;
cout << "Elements: ";
for (size_t i = 0; i < arr.getSize(); i++) {
cout << arr[i] << " ";
}
cout << endl;
arr.shrink_to_fit();
cout << "After shrink_to_fit:\n";
cout << "Size: " << arr.getSize() << ", Capacity: " << arr.getCapacity() << endl;
}
void demonstrateLinkedList() {
cout << "\n=== LINKED LIST WITH SMART POINTERS ===\n";
LinkedList list;
list.push_back("Hello");
list.push_back("World");
list.push_front("C++");
list.push_back("Memory");
list.push_back("Management");
list.display();
cout << "List size: " << list.size() << endl;
cout << "Using iterators: ";
for (const auto& item : list) {
cout << item << " ";
}
cout << endl;
}
void demonstrateMemoryPool() {
cout << "\n=== MEMORY POOL PATTERN ===\n";
MemoryPool pool(256, 5); // 5 blocks of 256 bytes each
cout << "Free blocks initially: " << pool.getFreeBlocks() << endl;
void* ptr1 = pool.allocate();
void* ptr2 = pool.allocate();
cout << "After allocating 2 blocks: " << pool.getFreeBlocks() << endl;
pool.deallocate(ptr1);
cout << "After deallocating 1 block: " << pool.getFreeBlocks() << endl;
// Simulate usage
if (ptr2) {
int* numbers = static_cast(ptr2);
for (int i = 0; i < 10; i++) {
numbers[i] = i * 5;
}
cout << "Numbers in allocated block: ";
for (int i = 0; i < 10; i++) {
cout << numbers[i] << " ";
}
cout << endl;
}
}
void demonstrateObjectPool() {
cout << "\n=== OBJECT POOL FOR GAME CHARACTERS ===\n";
ObjectPool characterPool(3);
cout << "Pool created with " << characterPool.getTotalCount()
<< " characters, " << characterPool.getFreeCount() << " free\n";
// Acquire characters
GameCharacter* hero1 = characterPool.acquire();
*hero1 = GameCharacter("Hero1", 150);
GameCharacter* hero2 = characterPool.acquire();
*hero2 = GameCharacter("Hero2", 120);
GameCharacter* enemy = characterPool.acquire();
*enemy = GameCharacter("Enemy", 80);
cout << "\nCharacters in play:\n";
hero1->display();
hero2->display();
enemy->display();
cout << "\nFree characters: " << characterPool.getFreeCount() << endl;
// Release a character back to pool
cout << "\nHero2 defeated, returning to pool...\n";
characterPool.release(hero2);
cout << "Free characters after release: " << characterPool.getFreeCount() << endl;
// Acquire from pool again
GameCharacter* newHero = characterPool.acquire();
*newHero = GameCharacter("NewHero", 100);
newHero->display();
}
int main() {
cout << "=== PRACTICAL APPLICATIONS OF DYNAMIC MEMORY ===\n";
demonstrateDynamicArray();
demonstrateLinkedList();
demonstrateMemoryPool();
demonstrateObjectPool();
cout << "\n=== KEY TAKEAWAYS ===\n";
cout << "1. Use RAII for all resource management\n";
cout << "2. Prefer standard containers (vector, list) when possible\n";
cout << "3. For custom data structures, use smart pointers\n";
cout << "4. Consider memory pools for performance-critical code\n";
cout << "5. Object pools reduce allocation overhead for frequently created objects\n";
return 0;
}
6. Best Practices and Performance Optimization
| Practice | Bad Example | Good Example |
|---|---|---|
| Memory Allocation | int* arr = new int[n]; |
vector<int> arr(n); or unique_ptr<int[]> |
| Ownership Transfer | Resource* create() { return new Resource(); } |
unique_ptr<Resource> create() { return make_unique<Resource>(); } |
| Exception Safety | void process() { Resource* r = new Resource(); /* might throw */ delete r; } |
void process() { auto r = make_unique<Resource>(); /* exception safe */ } |
| Array Management | int** matrix = new int*[rows]; for(i) matrix[i] = new int[cols]; |
vector<vector<int>> matrix(rows, vector<int>(cols)); |
| Resource Cleanup | File* f = fopen(); /* ... */ fclose(f); |
unique_ptr<FILE, decltype(&fclose)> f(fopen(), &fclose); |
| Polymorphic Objects | Base* obj = new Derived(); delete obj; |
unique_ptr<Base> obj = make_unique<Derived>(); |
Memory Management Best Practices
- Prefer stack allocation for small, short-lived objects
- Use smart pointers instead of raw new/delete
- Follow RAII pattern for all resources
- Implement Rule of Three/Five for resource-owning classes
- Use standard library containers when possible
- Always check allocation success
- Profile before optimizing memory usage
Performance Considerations
- Heap allocation is 100-1000x slower than stack
- Memory fragmentation affects long-running programs
- Cache locality is better with contiguous memory
- Virtual memory overhead for large allocations
- Smart pointers have small runtime overhead
- Custom allocators can improve specific use cases
Memory Optimization Techniques
- Pool Allocation: Pre-allocate objects of same size
- Slab Allocation: Allocate in large blocks, manage internally
- Arena Allocation: Allocate sequentially, free all at once
- Memory Mapping: Use mmap for very large allocations
- Custom Allocators: Implement allocators for specific patterns
- Object Reuse: Object pools for frequently created/destroyed objects
Quick Quiz: Test Your Knowledge
What happens when this code executes?
void process() {
int* ptr = new int[100];
if (errorCondition) return;
delete[] ptr;
}
Summary & Key Takeaways
Stack Memory
- Automatic allocation/deallocation
- Very fast (CPU instructions)
- Limited size (1-8 MB typically)
- Function/scope lifetime
- Perfect for local variables
Heap Memory
- Manual allocation (new/malloc)
- Manual deallocation (delete/free)
- Large size (limited by OS/RAM)
- Programmer-controlled lifetime
- Flexible but requires management
Smart Pointers
unique_ptr: Exclusive ownershipshared_ptr: Shared ownershipweak_ptr: Non-owning observation- Automatic memory management
- Exception safety guaranteed
Memory Safety
- RAII pattern for resource management
- Rule of Three/Five for classes
- Use tools to detect leaks
- Avoid manual memory management
- Prefer standard library containers
Modern C++ Memory Philosophy
In modern C++ (C++11 and later), avoid raw new/delete. Use smart pointers for ownership, stack allocation for local variables, and standard containers for collections. Implement RAII for all resources. Let the compiler and standard library handle memory management whenever possible.
Next Steps
Mastering dynamic memory is crucial for advanced C++ programming. Practice with custom allocators, study memory profiling tools, explore multi-threaded memory management, and learn about memory-mapped files. Understanding memory at a deep level will make you a more effective systems programmer.