A Beginner's Guide to Stack vs Heap, Reference Types vs Value Types, and Deep vs Shallow Copying


Introduction:
Understanding how data lives and moves in memory is essential for writing reliable and efficient software, no matter what language you use. Concepts like stack vs heap, value vs reference types, and shallow vs deep copying aren't just technical trivia, they explain why your variables behave the way they do.
Have you ever copied an object and been surprised that changing one copy changed the other? Or tried to free some memory and crashed your program? These issues usually trace back to how memory and references work.
In this article, we’ll break down four essential concepts that help you understand memory and data behavior at a deeper level. We’ll focus on universal principles that apply across many languages, but all the examples will be written in C++ to show how things actually work in code. Other languages will hide a few things from you, so learning these concepts is better in lower level languages like C++ or even C.
What you’ll learn:
Stack vs Heap memory – The two main regions where data lives during program execution.
Reference vs Value types – Why some variables share memory and others don't.
Shallow Copy – When copying only duplicates pointers, not data.
Deep Copy – How to create a true, independent copy of a complex object.
Hands-on C++ examples – Real code to see these concepts in action.
By the end, you'll not only understand the what but also the why behind these behaviors, so you can write code with more control and fewer bugs.
Stack vs Heap – Where Does My Data Really Live?
Let’s say you’re writing a simple C++ function and declare a variable like int x = 10;
. Ever wondered where exactly that variable lives in your machine’s memory? Is it just floating around in the ether? Not quite.
In most languages—including C++—your data lives in two main neighborhoods: the stack and the heap. These two places have very different personalities.
The Stack – Quick, Organized, and Temporary
Think of the stack as a highly organized, short-term storage shelf. Every time you call a function, your program neatly stacks its local variables on this shelf. Once the function finishes, that whole layer of variables is tossed out like a sticky note you no longer need.
Here’s a basic example:
void showExample() {
int a = 5;
int b = 10;
int sum = a + b;
}
In this case, a
, b
, and sum
are all born in the stack. And when showExample()
ends? Poof—they vanish. The stack handles all of this behind the scenes—fast and clean.
But there's a catch: you can’t keep data on the stack forever. It disappears when its scope ends. Plus, the stack has size limits, so you can’t dump huge amounts of data there.
The Heap – Spacious, Flexible, but Demanding
Now imagine you want something more long-term—like a variable that lives on beyond the function that created it. That’s when you knock on the heap’s door.
The heap is a big, open memory zone where you get to ask for space manually using new
in C++. But here’s the deal: if you ask for space, you’re also responsible for cleaning up after yourself.
void heapExample() {
int* p = new int(42); // Hey heap, give me space for an int
*p += 8; // Let’s update that value
delete p; // Okay, I’m done—clean it up!
}
If you forget to call delete
, that memory stays reserved—even if your program no longer needs it. That’s called a memory leak, and it’s like renting a hotel room forever and never checking out.
Some programming languages (like Java, Python, and C#) take care of heap management for you. They automatically handle memory allocation and garbage collection, meaning you don't need to manually manage memory with new
and delete
. This is great for beginners or when you're more focused on building features instead of memory management. However, this convenience comes with some trade-offs, mainly, you lose some control over when and how memory is cleaned up.
🆚 Stack vs Heap in Real Life
Let’s pause and compare with a little metaphor:
The stack is like a stack of plates in a cafeteria. You always take the top plate (LIFO: Last In, First Out), and once you’re done eating (i.e., the function ends), the plate goes away.
The heap is more like a storage warehouse. You ask for a box (
new
), put things in it, and then—you better remember where you left it. If not, your warehouse starts overflowing (memory leak!).
A Dangerous Example
Here’s something people do accidentally:
int* badPointer() {
int x = 99;
return &x; // Uh oh, 'x' will be gone when this function returns!
}
int* p = badPointer(); // p is now what we call a dangling pointer.
This will compile, but it's dangerous. You're returning a pointer to something that doesn’t exist anymore.
The Danger of Dangling Pointers
Dangling pointers are a potential pitfall when dealing with memory management in languages like C++. If you try to access memory through a pointer that points to a variable that’s gone out of scope or been deleted, you’ll encounter undefined behavior. This could result in crashes, data corruption, or worse. Always ensure that pointers point to valid memory.
Reference Types vs Value Types
Alright, we’ve been talking a lot about memory, but now let's break down how data types behave when they get packed into memory. There are two big categories we need to understand: value types and reference types.
Value Types – The Self-Sufficient Copycats
Let’s start with value types. These are like little independent workers that carry all their own data. Think of them as self-contained—each instance has its own copy of the data.
When you assign one value type to another, a new copy of the data is created. In C++, these are often your basic data types like int
, double
, and char
. Here’s an example:
int a = 42;
int b = a; // b gets its own copy of a's data
b = 100; // Changing b doesn't affect a
In this case, b
is just a copy of a
, and changes to b
don’t impact a
at all. The data for both a
and b
is stored independently.
Reference Types – The Shared Data Squad
Now, let’s look at reference types. These are like roommates sharing an apartment—both references point to the same block of memory. In C++, reference types usually involve objects (instances of classes), arrays, and other more complex structures.
When you assign one reference type to another, you’re not making a copy of the data. Instead, you’re just copying the memory address where that data lives. They both refer to the same object in memory.
class MyClass {
public:
int x;
MyClass(int val) : x(val) {}
};
MyClass obj1(42);
MyClass obj2 = obj1; // obj2 now points to the same memory as obj1
obj2.x = 100; // Changing obj2 also affects obj1, since they point to the same object
In this case, both obj1
and obj2
point to the same instance of MyClass
. So when you change obj2.x
, it also modifies obj1.x
, since they are referring to the same memory block.
So What’s the Big Deal?
Understanding the difference between reference and value types is crucial for memory management and program behavior. With value types, you're always working with copies of the data. With reference types, you're working with shared access to the same data, which can lead to unintended side effects if you're not careful.
For example, if you accidentally modify a reference type when you didn't mean to, it could lead to bugs that are hard to track down. That’s why you’ll often see the advice to pass by reference when dealing with large data structures to avoid making unnecessary copies, but also be cautious about unintended changes to the data.
Shallow Copy – Copying the Reference, Not the Data
A shallow copy creates a new reference to the same underlying data, but it doesn't create a new copy of the data itself. This means that when you create a shallow copy, you're essentially copying the reference to the data (the memory address), not the actual data contained at that address.
In practical terms, a shallow copy can be thought of as duplicating the reference, but the object or data is still shared between the original and the copied reference. This can lead to some unintended side effects because changes made to the data through one reference will affect the other, since both references point to the same memory.
Key Characteristics of Shallow Copy:
References Only: The shallow copy duplicates the reference (memory address) but not the actual data.
Shared Data: Both the original reference and the copy point to the same memory location, meaning they share the same underlying data.
Mutability Impact: If the data is mutable (e.g., an array, linked list, object), modifications made through one reference will affect the other since they point to the same memory.
Shallow Copy Example
#include <iostream>
using namespace std;
int main() {
int arr1[] = {1, 2, 3, 4};
int* arr2 = arr1; // Shallow copy - arr2 refers to the same memory as arr1
arr2[0] = 99; // Changing arr2 affects arr1, because both point to the same memory
cout << "arr1[0]: " << arr1[0] << endl; // Outputs: 99
cout << "arr2[0]: " << arr2[0] << endl; // Outputs: 99
return 0;
}
Explanation:
arr2
is a shallow copy ofarr1
. It does not hold its own separate data; it merely holds a reference (or pointer) to the same array thatarr1
refers to.Modifying
arr2
(for example, by settingarr2[0] = 99
) directly modifies the data inarr1
as well, because botharr1
andarr2
point to the same memory location.In this example, both
arr1
andarr2
refer to the same array in memory, so changes to the array througharr2
affectarr1
.
Shallow Copy and Complex Types (Pointers, Objects, Arrays, etc.):
For simple types like integers or floating-point values, the behavior of shallow copy is straightforward—just the value is copied.
But for more complex types, like objects or arrays, the shallow copy does not duplicate the actual data but only copies the reference. This means the objects themselves are not duplicated, but the reference to the object is. Any change to the object through one reference will be reflected in the other reference.
For example, if you have a class that holds a pointer to dynamic memory (like an array or another object), a shallow copy of that class will copy the pointer, not the data. As a result, both the original and the copy will point to the same memory location. This can lead to issues such as double deletion (trying to free the same memory twice) or unintended modifications of the data.
Shallow Copy Pitfalls:
Unintended Side Effects: Since the original and the copy share the same memory, changes made to one can unexpectedly affect the other.
Memory Management Issues: If objects are dynamically allocated (i.e., created on the heap), shallow copying can lead to issues like double-free errors, where both the original and the copy try to delete the same memory.
Why Use Shallow Copy?
Shallow copy is often sufficient when:
The objects being copied are immutable, meaning their state can't be changed.
You want to save memory and only need a reference to the same data rather than a duplicate.
You are working with lightweight objects and you’re not concerned with modifying the data.
However, when the data is mutable and you need to independently modify the copies without affecting the original, shallow copy might not be suitable.
Deep Copy – Creating a True Duplicate
A deep copy is a way of copying an object and its data in such a way that the original and the copied objects are completely independent. Unlike shallow copy, where only the references are copied, a deep copy creates a new instance of the object and also creates new copies of all the objects and data contained within it, recursively. This means that changes made to the deep-copied object will not affect the original object, and vice versa.
In simple terms, a deep copy creates a full duplication of the object and its contents, including all nested objects or data structures, unlike shallow copy which only duplicates the top-level reference.
Key Characteristics of Deep Copy:
Complete Duplication: It creates a new instance of the object and recursively copies all the data within it, including objects and references.
No Shared References: The original and the deep-copied objects do not share any references. They are entirely independent, and changes to one do not affect the other.
Recursive Copy: If the object contains references to other objects, deep copy will duplicate those as well, creating an entirely separate and independent structure.
Deep Copy vs Shallow Copy – Key Differences
Data Duplication: In a deep copy, all nested objects (in this case, the nodes of the linked list) are copied as well. In a shallow copy, only the references to those objects are copied, meaning both the original and the copy point to the same objects in memory.
Independence: After performing a deep copy, the original and copied objects are completely independent. With a shallow copy, modifications to one object (or the data it points to) will affect the other.
When to Use Deep Copy
Deep copy is the ideal choice when:
You need two independent objects, where changes to one should not affect the other.
You are dealing with mutable objects and you want to avoid unintentional side effects caused by shared references.
The object you are copying has nested objects or complex data structures that need to be fully duplicated.
Pitfalls and Considerations with Deep Copy
Performance: Deep copying can be expensive in terms of both time and memory, especially when the data structures are large or deeply nested.
Complexity: Implementing deep copy for complex data structures can require recursive logic, which can become tricky to manage for very large or complex objects.
Memory Management: When you create deep copies, you're also responsible for managing the memory of the new objects. For example, if your deep copy involves dynamically allocated memory, you must ensure proper memory management (e.g., deletion) to avoid memory leaks.
Summary of Deep Copy
To wrap up, deep copy is used when you want to duplicate an object and all the objects it contains, creating a completely independent copy. This ensures that no data is shared between the original and the copy, avoiding unintended side effects. While deep copy offers more control and safety, it can come at a performance cost, especially for large or nested structures.
A Comprehensive Code Example
This example will use a Book class, along with a Library class, to showcase shallow and deep copying. The idea is to demonstrate how both shallow and deep copies behave when applied to a collection of objects (the Book
objects in this case).
Full Example: Shallow vs Deep Copy with Practical Demonstration
Code:
#include <iostream>
#include <string>
#include <vector>
using namespace std;
// Book class definition
class Book {
public:
string title;
string author;
int pages;
// Constructor to initialize a Book object with title, author, and pages
Book(string t, string a, int p) : title(t), author(a), pages(p) {}
// Display method to print book details
void display() {
cout << "Title: " << title << ", Author: " << author << ", Pages: " << pages << endl;
}
// Method to make a deep copy of the current book
Book* deepCopy() {
return new Book(title, author, pages); // Creating a new Book object with the same data
}
};
// Library class definition
class Library {
public:
vector<Book*> books; // A vector that holds pointers to Book objects
// Method to add a book to the library
void addBook(Book* book) {
books.push_back(book);
}
// Method to display all books in the library
void displayBooks() {
cout << "Library Contents:\n";
for (Book* book : books) {
book->display();
}
}
};
// Main function demonstrating shallow and deep copy with books and library
int main() {
// Create some books
Book* book1 = new Book("1984", "George Orwell", 328);
Book* book2 = new Book("Animal Farm", "George Orwell", 112);
Book* book3 = new Book("To Kill a Mockingbird", "Harper Lee", 281);
// Create a library and add books to it
Library* library1 = new Library();
library1->addBook(book1);
library1->addBook(book2);
library1->addBook(book3);
// Display the original library content
cout << "Original Library:\n";
library1->displayBooks();
// Shallow copy: library2 points to the same books as library1
Library* library2 = new Library();
library2->books = library1->books; // Shallow copy: library2.books points to the same Book objects as library1.books
// Modify a book in the shallow copied library
library2->books[0]->title = "Changed Title"; // Change the title of the first book in library2
// Display both libraries to show that changes to library2 affect library1
cout << "\nAfter shallow copy and modification in library2:\n";
cout << "Library 1 (original):\n";
library1->displayBooks();
cout << "Library 2 (shallow copy):\n";
library2->displayBooks();
// Deep copy: library3 will have new, independent copies of the books
Library* library3 = new Library();
for (Book* book : library1->books) {
library3->addBook(book->deepCopy()); // Deep copy: each book is independently copied
}
// Modify a book in the deep copied library
library3->books[1]->title = "New Title for Animal Farm"; // Change the title of the second book in library3
// Display all libraries to show that changes to library3 do not affect library1 or library2
cout << "\nAfter deep copy and modification in library3:\n";
cout << "Library 1 (original):\n";
library1->displayBooks();
cout << "Library 2 (shallow copy):\n";
library2->displayBooks();
cout << "Library 3 (deep copy):\n";
library3->displayBooks();
// Cleanup dynamically allocated memory
delete book1;
delete book2;
delete book3;
delete library1;
delete library2;
delete library3;
return 0;
}
Expected Output:
Original Library:
Library Contents:
Title: 1984, Author: George Orwell, Pages: 328
Title: Animal Farm, Author: George Orwell, Pages: 112
Title: To Kill a Mockingbird, Author: Harper Lee, Pages: 281
After shallow copy and modification in library2:
Library 1 (original):
Library Contents:
Title: Changed Title, Author: George Orwell, Pages: 328
Title: Animal Farm, Author: George Orwell, Pages: 112
Title: To Kill a Mockingbird, Author: Harper Lee, Pages: 281
Library 2 (shallow copy):
Library Contents:
Title: Changed Title, Author: George Orwell, Pages: 328
Title: Animal Farm, Author: George Orwell, Pages: 112
Title: To Kill a Mockingbird, Author: Harper Lee, Pages: 281
After deep copy and modification in library3:
Library 1 (original):
Library Contents:
Title: Changed Title, Author: George Orwell, Pages: 328
Title: Animal Farm, Author: George Orwell, Pages: 112
Title: To Kill a Mockingbird, Author: Harper Lee, Pages: 281
Library 2 (shallow copy):
Library Contents:
Title: Changed Title, Author: George Orwell, Pages: 328
Title: Animal Farm, Author: George Orwell, Pages: 112
Title: To Kill a Mockingbird, Author: Harper Lee, Pages: 281
Library 3 (deep copy):
Library Contents:
Title: 1984, Author: George Orwell, Pages: 328
Title: New Title for Animal Farm, Author: George Orwell, Pages: 112
Title: To Kill a Mockingbird, Author: Harper Lee, Pages: 281
1. Book Class:
Purpose: The
Book
class is used to model a book, containing fields for the title, author, and number of pages. The class has a constructor to initialize these values, adisplay()
method to print the book's information, and adeepCopy()
method to create an independent copy of a book.Fields:
string title
: Represents the title of the book.string author
: Represents the author of the book.int pages
: Represents the number of pages in the book.
Constructor:
Book(string t, string a, int p)
: Initializes aBook
object with the specified title, author, and page count.
Methods:
void display()
: Prints the book’s details (title, author, pages).Book* deepCopy()
: Creates and returns a newBook
object with the same data as the original, ensuring that any modifications to the copied book won't affect the original.
2. Library Class:
Purpose: The
Library
class holds a collection ofBook
objects. It includes methods to add books to the library and display all the books in the library.Fields:
vector<Book*> books
: A dynamic array (vector) that holds pointers toBook
objects.
Methods:
void addBook(Book* book)
: Adds aBook
object to the library.void displayBooks()
: Displays all books in the library by calling each book’sdisplay()
method.
3. Main Function (Demonstration):
Creating Books:
- Three
Book
objects (book1
,book2
, andbook3
) are created with different titles, authors, and page counts.
- Three
Shallow Copy:
A
Library
object (library1
) is created, and the books are added to it.Another
Library
object (library2
) is created as a shallow copy oflibrary1
. This is done by directly copying thebooks
vector fromlibrary1
tolibrary2
. This means both libraries now point to the sameBook
objects in memory.When a book’s title is modified in
library2
, it also reflects inlibrary1
because both libraries share the same book objects in memory.
Deep Copy:
A third
Library
object (library3
) is created using deep copies of the books fromlibrary1
. Each book is copied independently using thedeepCopy()
method, ensuring thatlibrary3
has its own copies of the books, independent oflibrary1
andlibrary2
.When a book's title is modified in
library3
, it does not affectlibrary1
orlibrary2
, aslibrary3
contains its own independent copies of the books.
Displaying Libraries:
- Each library’s content is displayed after modifications to show the effect of shallow and deep copying. The shallow copy reflects changes across both libraries, while the deep copy creates an independent set of books.
4. Summary of Shallow vs Deep Copy:
Shallow Copy: When
library2
is shallow copied fromlibrary1
, both libraries point to the same book objects in memory. Any changes made tolibrary2
will affectlibrary1
because they share the same memory addresses for theirBook
objects.Deep Copy: When
library3
is deep copied fromlibrary1
, eachBook
object inlibrary3
is a new instance with its own memory. Changes tolibrary3
do not affectlibrary1
orlibrary2
, as the books inlibrary3
are independent copies.
Conclusion:
In this article, we've explored the fundamental concepts of memory management, value vs. reference types, and the differences between shallow and deep copying. Here's a recap:
Heap vs. Stack: We saw how stack memory is used for temporary, small-sized data (local variables), while heap memory is used for dynamic memory allocation (objects that need more space or lifetime management).
Reference vs. Value Types: Value types store data directly and are copied when assigned to another variable, while reference types store pointers to data, meaning multiple variables can point to the same memory location.
Shallow Copy: A shallow copy copies the reference (or pointer) to the data, meaning any changes to one object will reflect in the other, as they share the same memory.
Deep Copy: A deep copy creates a new independent copy of the object, ensuring that changes to the copied object don't affect the original.
By understanding these concepts, we can make better decisions in programming related to memory management, object manipulation, and data sharing. Whether you’re working with simple data types or complex objects, knowing when to use shallow or deep copies is crucial for building reliable and efficient software systems.
Subscribe to my newsletter
Read articles from Muhammed Khaled directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
