Shallow Copy vs Deep Copy in C++ determines whether a copy duplicates just pointers (sharing the same underlying resource) or also duplicates the pointed-to data (owning a separate resource). Mastering this concept prevents bugs like double-free, memory leaks, and unexpected aliasing in your programs.
This beginner-friendly guide explains Shallow Copy vs Deep Copy in C++ with clear code, commented explanations, outputs, and “Try it yourself” challenges after each section. By the end, you’ll know when to implement custom copy logic, when to disable copying, and how smart pointers and STL containers make this easier.
🔹 What is Shallow Copy vs Deep Copy in C++?
When an object owns a resource (dynamic memory, file handle, socket), a copy can be:
- Shallow copy: Copies pointer values only. Both objects point to the same resource.
- Deep copy: Allocates a new resource and copies the data so each object owns its own resource.
Shallow copies are fine for value-like members (ints, doubles, std::string, std::vector). They are dangerous for raw owning pointers. Deep copies are required when each object must own independent data.
Try it yourself
- Name two classes in your project that own resources (e.g., buffers, file handles). Should they perform a deep copy, or should copying be disabled?
- List two “value-like” types that do not need a custom deep copy.
🔹 Shallow Copy: The Hidden Trap (Aliasing + Double-Delete)
By default, the compiler-generated copy constructor/assignment performs memberwise copy. For raw owning pointers, that means two objects end up pointing to the same memory, which risks double deletion and surprising cross-modification.
#include <cstddef>
#include <iostream>
using namespace std;
struct ShallowBuffer {
size_t n{0};
int* data{nullptr};
explicit ShallowBuffer(size_t n) : n(n),
data(new int[n]{}) {}
// Compiler-generated copy ctor/assignment
//do SHALLOW copy (pointer value only)
// ~ShallowBuffer() { delete[] data; }
// If enabled, both copies delete the same pointer!
void set(size_t i, int v)
{ data[i] = v; }
int get(size_t i) const
{ return data[i]; }
};
int main() {
ShallowBuffer a(3);
a.set(0, 10);
// Shallow copy: copies pointer value (two objects share the same array)
ShallowBuffer b = a;
cout << "a.data=" << a.data
<< ", b.data=" << b.data << "\n";
b.set(0, 999);
cout << "a=" <<
a.get(0) << " (changed unexpectedly)\n";
// Uncommenting the destructor above would cause double delete at end of scope!
}
Output
a.data=0x55c...
b.data=0x55c...
a=999 (changed unexpectedly)
Because both objects share the same memory, changing one changes the other (“aliasing”). If both destructors delete the same pointer, you get a double-free crash or undefined behavior.
Try it yourself
- Add a destructor that deletes
data
, run, and observe the crash or sanitizer error (then remove it). - Print both pointers again after the copy to confirm they are equal (shallow copy).
🔹 Deep Copy: Safe Ownership (Rule of Three)
For owning raw pointers, implement a deep copy: allocate new memory and copy the elements. Also implement copy assignment and a destructor (Rule of Three) to manage the resource safely.
#include <algorithm>
#include <cstddef>
#include <iostream>
using namespace std;
class DeepBuffer {
size_t n{0};
int* data{nullptr};
public:
explicit DeepBuffer(size_t n) : n(n),
data(new int[n]{}) {}
// Deep copy constructor
DeepBuffer(const DeepBuffer& other) : n(other.n),
data(new int[other.n]) {
std::copy(other.data, other.data + n, data);
cout << "DeepBuffer: deep copy "
<< n << " ints\n";
}
// Copy assignment (strongly exception-safe copy-and-swap is also common)
DeepBuffer& operator=(const DeepBuffer& other) {
if (this != &other) {
int* newData = new int[other.n];
std::copy(other.data, other.data + other.n, newData);
delete[] data;
data = newData;
n = other.n;
}
return *this;
}
~DeepBuffer() { delete[] data; }
void set(size_t i, int v) { data[i] = v; }
int get(size_t i) const { return data[i]; }
const int* raw() const { return data; }
};
int main() {
DeepBuffer a(3);
a.set(0, 10);
DeepBuffer b = a; // deep copy (different allocations)
b.set(0, 999);
cout << "a.raw()=" << a.raw() << ",
b.raw()=" << b.raw() << "\n";
cout << "a=" << a.get(0)
<< " (still 10)" << "\n";
}
Output
DeepBuffer: deep copy 3 ints
a.raw()=0x55c..., b.raw()=0x55c... (different)
a=10 (still 10)
Now each object owns its own allocation. No aliasing, no double-free.
Try it yourself
- Add logging to the copy assignment and verify it runs on
b = a;
. - Switch to copy-and-swap for exception safety (make a
swap
friend and assign by value).
🔹 Shallow Copy vs Deep Copy: Key Points
Aspect | Shallow copy | Deep copy |
---|---|---|
What is copied | Copies pointer values/references; both objects point to the same underlying resource | Allocates new storage and copies the pointed-to data so each object owns its own resource |
Ownership | Shared ownership of the same data; objects are not independent | Independent ownership; each object has its own distinct copy of the data |
Memory allocation | No new allocation for dynamic resources; only addresses are replicated | Allocates new memory for every dynamically allocated resource and duplicates contents |
Effect of changes | Modifying one object’s data affects the other due to shared storage (aliasing) | Modifying one object’s data does not affect the other because data is separate |
Default behavior | Occurs implicitly via compiler‑generated copy constructor/assignment (memberwise copy) unless overridden | Requires user‑defined copy constructor and copy assignment to perform resource duplication correctly |
Performance | Generally faster (copies pointers/primitive fields) | Typically slower (allocates memory and copies data, possibly recursively) |
Memory usage | Lower memory usage (resources shared between objects) | Higher memory usage (data duplicated per object) |
Risks/downsides | Aliasing, unintended cross‑modification, double‑free/UB if both destructors delete the same resource | More implementation effort; performance overhead of allocations and copies |
When to use | Only when safe, intentional sharing is desired (e.g., immutable data or explicit shared ownership) | When each object must have independent state or the class owns raw resources that must not be shared implicitly |
With STL containers | Copying a container copies its elements; if elements are pointers, the copy is shallow with respect to the pointees | Use value‑like elements (e.g., std::vector, std::string) or implement custom deep copy so each copy owns its own data |
How to implement | Defaulted copy is memberwise; avoid for raw owning pointers unless intentional | Define copy constructor and copy assignment to allocate new storage and copy the contents safely (Rule of Three/Five) |
🔹 Rule of Zero: Prefer STL and Smart Pointers
The easiest way to avoid manual deep copies is to use RAII members that already manage copying and moving correctly, such as std::vector
, std::string
, and smart pointers. This is the Rule of Zero: you write no copy/move/destructor code; the compiler-generated ones are correct.
#include <vector>
#include <iostream>
using namespace std;
class Image {
// vector handles deep copy of its elements
vector<unsigned char> pixels;
size_t w{0}, h{0};
public:
Image(size_t w, size_t h) : pixels(w*h, 0), w(w), h(h) {}
void set(size_t i, unsigned char v)
{ pixels[i] = v; }
unsigned char get(size_t i) const
{ return pixels[i]; }
};
int main() {
Image a(4, 1);
a.set(0, 123);
// vector copies its bytes; no manual deep copy needed
Image b = a;
b.set(0, 77);
cout << (int)a.get(0) << " " <<
(int)b.get(0) << "\n"; // 123 77
}
Output
123 77
Note: Containers copy their elements. If your elements are raw pointers, copying the container is still shallow with respect to the pointees. Prefer value-like elements or smart pointers with clear ownership semantics (e.g., unique_ptr
for unique ownership can’t be copied; shared_ptr
shares ownership).
Try it yourself
- Create
vector<unique_ptr<int>>
and try to copy it (observe compile error). Then move it. - Create
vector<shared_ptr<int>>
and copy it; printuse_count()
to see shared ownership.
🔹 Move Semantics: Avoid Costly Deep Copies
Sometimes you don’t want to duplicate data at all—just transfer ownership from a temporary. Implement a move constructor/assignment to “steal” resources efficiently and leave the source in a valid empty state (Rule of Five).
#include <cstddef>
#include <iostream>
using namespace std;
struct MovableBuffer {
size_t n{0};
int* data{nullptr};
explicit MovableBuffer(size_t n) : n(n), data(new int[n]{}) {}
// deep copy ctor
MovableBuffer(const MovableBuffer& other) :
n(other.n), data(new int[other.n]) {
std::copy(other.data, other.data + other.n, data);
cout << "copy\n";
}
// move ctor
MovableBuffer(MovableBuffer&& other) noexcept :
n(other.n), data(other.data) {
other.n = 0;
other.data = nullptr;
cout << "move\n";
}
// copy assign (omitted), move assign
MovableBuffer& operator=(MovableBuffer&& other) noexcept {
if (this != &other) {
delete[] data;
n = other.n;
data = other.data;
other.n = 0;
other.data = nullptr;
cout << "move assign\n";
}
return *this;
}
~MovableBuffer() { delete[] data; }
};
MovableBuffer makeBuf() { return MovableBuffer(1000); } // RVO/move
int main() {
MovableBuffer a = makeBuf(); // prints "move" or elides
MovableBuffer b = std::move(a); // move
}
Output
move
move
Moving avoids expensive deep copies when transferring from temporaries or soon-to-be-discarded objects. Prefer noexcept
on move operations to enable container optimizations.
Try it yourself
- Add
noexcept
to the move constructor/assignment and test moving inside astd::vector
during reallocation. - Compare timing of copying vs moving large buffers (basic micro-benchmark).
🔹 Copy Constructor vs Copy Assignment (When Each Runs)
A deep copy needs both operations:
- Copy constructor: creates a new object from an existing one. Runs on
T b = a;
orT b(a);
. - Copy assignment: replaces the contents of an existing object. Runs on
b = a;
.
struct Tracer {
Tracer() { std::cout << "default\n"; }
Tracer(const Tracer&)
{ std::cout << "copy ctor\n"; }
Tracer& operator=(const Tracer&) {
std::cout << "copy assign\n";
return *this;
}
};
int main() {
Tracer a; // default
Tracer b = a; // copy ctor
Tracer c; // default
c = a; // copy assign
}
Output
default
copy ctor
default
copy assign
Implement both deep-copy operations (plus destructor), or better, avoid raw ownership and follow Rule of Zero.
Try it yourself
- Add prints to your deep-copy class to confirm which operation runs in each scenario.
- Write copy assignment using copy-and-swap to achieve strong exception safety.
🔹 When to Disable Copying (Move-Only Types)
If a type represents exclusive ownership (file descriptor, mutex, unique GPU handle), disable copying and allow only moves to prevent accidental shallow copies of ownership.
struct UniqueHandle {
int fd{-1};
UniqueHandle() = default;
explicit UniqueHandle(int fd) : fd(fd) {}
// No copying
UniqueHandle(const UniqueHandle&) = delete;
UniqueHandle& operator=(const UniqueHandle&) = delete;
// Movable
UniqueHandle(UniqueHandle&& other) noexcept : fd(other.fd) {
other.fd = -1;
}
UniqueHandle& operator=(UniqueHandle&& other) noexcept {
if (this != &other) {
/* close old fd if needed */
fd = other.fd;
other.fd = -1;
}
return *this;
}
~UniqueHandle() { /* close fd if valid */ }
};
This prevents unintended shallow ownership copies and makes lifetime rules explicit.
Try it yourself
- Attempt to copy a
UniqueHandle
(observe compile error). Then move it to transfer ownership safely. - Wrap a real OS handle (or simulate) and verify it closes exactly once.
🔹 Polymorphism: Deep Copy via Virtual clone()
Copying through a base pointer with *this = other
risks slicing. Provide a virtual clone()
that returns a new heap-allocated copy of the dynamic type—this is a polymorphic deep copy pattern.
#include <memory>
#include <iostream>
using namespace std;
struct Shape {
virtual ~Shape() = default;
virtual unique_ptr<Shape> clone() const = 0; // polymorphic deep copy
virtual void draw() const = 0;
};
struct Circle : Shape {
double r{1.0};
explicit Circle(double r) : r(r) {}
unique_ptr<Shape> clone() const override {
return make_unique<Circle>(*this);
}
void draw() const override {
cout << "Circle r=" << r << "\n";
}
};
int main() {
unique_ptr<Shape> s1 = make_unique<Circle>(2.5);
// deep copy of dynamic type
unique_ptr<Shape> s2 = s1->clone();
s1->draw();
s2->draw();
}
Output
Circle r=2.5
Circle r=2.5
Using clone()
ensures correct deep copies in polymorphic hierarchies without slicing.
Try it yourself
- Add
Rectangle
and implementclone()
. Storeunique_ptr<Shape>
in a vector and duplicate them withclone()
. - Demonstrate slicing by copying a derived object into a base by value (and then fix it with
clone()
).
🔹 Mini Project: ImageA (Manual Deep Copy) vs ImageB (Rule of Zero)
See Shallow Copy vs Deep Copy in C++ in practice with two approaches to an image type. Version A uses raw memory and requires a deep copy. Version B uses std::vector
and needs no custom copy code.
#include <cstring>
#include <iostream>
#include <vector>
using namespace std;
// Version A: manual deep copy (Rule of Three)
class ImageA {
size_t n{0};
unsigned char* p{nullptr};
public:
explicit ImageA(size_t n) : n(n), p(new unsigned char[n]{}) {}
ImageA(const ImageA& other) : n(other.n),
p(new unsigned char[n]) {
memcpy(p, other.p, n);
cout << "ImageA deep-copied "
<< n << " bytes\n";
}
ImageA& operator=(const ImageA& other) {
if (this != &other) {
auto* np = new unsigned char[other.n];
memcpy(np, other.p, other.n);
delete[] p;
p = np;
n = other.n;
}
return *this;
}
~ImageA() { delete[] p; }
void set(size_t i, unsigned char v) { p[i] = v; }
unsigned char get(size_t i) const { return p[i]; }
};
// Version B: Rule of Zero using vector
class ImageB {
vector<unsigned char> bytes;
public:
explicit ImageB(size_t n) : bytes(n, 0) {}
void set(size_t i, unsigned char v) { bytes[i] = v; }
unsigned char get(size_t i) const { return bytes[i]; }
};
int main() {
ImageA a(4);
a.set(0, 123);
ImageA b = a;
b.set(0, 9);
cout << "ImageA a=" << (int)a.get(0)
<< ", b=" << (int)b.get(0) << "\n";
ImageB x(4);
x.set(0, 42);
ImageB y = x;
y.set(0, 7);
cout << "ImageB x=" << (int)x.get(0)
<< ", y=" << (int)y.get(0) << "\n";
}
Output
ImageA deep-copied 4 bytes
ImageA a=123, b=9
ImageB x=42, y=7
Prefer Rule of Zero where possible; it’s simpler and safer than manual deep copies.
🔹 Best Practices and Common Pitfalls
- Prefer Rule of Zero (STL containers, smart pointers) to avoid manual deep copy logic.
- If you own raw memory, implement the Rule of Three (copy ctor, copy assign, dtor) or Rule of Five (add move operations).
- Disable copying for unique-ownership types; allow only moves.
- Beware container copies: they copy elements; if elements are pointers, copying is shallow w.r.t. pointees.
- Use
clone()
for polymorphic deep copies to avoid slicing. - Avoid relying on copy side-effects; copy elision may skip them.
- Mark move operations
noexcept
to enable vector optimizations.
Try it yourself
- Refactor a raw-pointer class to use
std::unique_ptr<T[]>
orstd::vector<T>
and delete your custom copy code. - Write unit tests to verify two copies do not alias: change one copy and assert the other remains unchanged.
🔹 FAQs about Shallow Copy vs Deep Copy in C++
Q1: How do I know if I need a deep copy?
If your class owns a resource (raw pointer, file, socket) that must not be shared automatically, implement a deep copy (or disable copying). Value-like types and standard containers typically do not need custom copy logic.
Q2: Do STL containers perform deep or shallow copies?
They copy their elements. For value-like elements, that’s effectively deep. If the elements are pointers, the pointers are copied (shallow with respect to the pointees).
Q3: Can I avoid deep copies altogether?
Use move semantics to transfer ownership from temporaries, or use shared ownership (shared_ptr
) when sharing is intended. Otherwise, deep copy is necessary for independent ownership.
Q4: Is copy-and-swap recommended?
Yes, for implementing copy assignment with strong exception safety. The copy constructor still needs to perform a deep copy if you own raw resources.
Q5: How do I deep copy polymorphic objects?
Provide a virtual clone()
that returns unique_ptr<Base>
and implement it in each derived class, e.g., return make_unique<Derived>(*this);
.
Q6: What about std::unique_ptr
and std::shared_ptr
?unique_ptr
is move-only (no copying). shared_ptr
copies share ownership (reference counting), which is “deep enough” for ownership semantics but still refers to the same object.
Q7: Does copy elision affect deep copies?
Yes. Returning a prvalue can skip calling the copy/move constructor (C++17 guaranteed elision). Don’t rely on copy side-effects to run.
Q8: Should I prefer raw pointers for ownership?
No. Prefer RAII types (containers, smart pointers). Raw owning pointers require careful manual deep copies and are error-prone.
Q9: How to test for accidental shallow copies?
Print addresses of internal buffers in both objects; if they match, you likely have a shallow copy. Modify one and see if the other changes.
Q10: Is it okay to share buffers intentionally?
Yes, but make it explicit with shared_ptr
or reference-counted types, and document semantics (immutable vs mutable, copy-on-write, etc.).
Try it yourself (FAQ)
- Create a small class with
shared_ptr<vector<int>>
and show that copies share data by default; then demonstrate deep copy by cloning the vector. - Instrument your class to print pointer addresses on copy; add assertions to prevent accidental aliasing in tests.
🔹 Wrapping Up
Shallow Copy vs Deep Copy in C++ is about ownership. If your type owns resources, either implement deep copies (Rule of Three/Five) or disable copying and use moves. Prefer Rule of Zero by leveraging STL containers and smart pointers to make copying safe and simple. Practice the “Try it yourself” tasks to solidify these patterns and write robust, maintainable C++ code.