Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Understanding Shallow and Deep Copies
Decide when to copy pointers versus the data they point to.
Shallow vs deep copying
When copying objects, C++ must decide how to copy each member. For simple classes, the default copying behavior works fine. However, for classes that manage resources like dynamic memory, we need to understand the difference between shallow and deep copying.
Shallow copying
The default copy constructor and assignment operator perform memberwise copying (also called shallow copying). Each member is copied individually:
#include <iostream>
class GameStats
{
private:
int m_wins{};
int m_losses{};
public:
GameStats(int wins, int losses)
: m_wins{wins}, m_losses{losses}
{
}
friend std::ostream& operator<<(std::ostream& out, const GameStats& stats)
{
out << m_wins << " wins, " << m_losses << " losses";
return out;
}
};
int main()
{
GameStats original{10, 3};
GameStats copy{original}; // Default copy constructor does shallow copy
std::cout << copy << '\n';
return 0;
}
The compiler-generated copy constructor looks like:
GameStats(const GameStats& stats)
: m_wins{stats.m_wins}
, m_losses{stats.m_losses}
{
}
For this simple class, shallow copying works perfectly because we're just copying integer values.
The problem with shallow copying pointers
Shallow copying becomes problematic when a class contains pointers to dynamically allocated memory:
#include <iostream>
#include <cstring>
class PlayerProfile
{
private:
char* m_username{};
int m_score{};
public:
PlayerProfile(const char* username, int score)
: m_score{score}
{
m_username = new char[std::strlen(username) + 1];
std::strcpy(m_username, username);
}
~PlayerProfile()
{
delete[] m_username;
}
char* getUsername() const { return m_username; }
int getScore() const { return m_score; }
};
int main()
{
PlayerProfile player1{"Dragon_Slayer", 1500};
{
PlayerProfile player2{player1}; // Shallow copy!
// player2.m_username points to the same memory as player1.m_username
} // player2 destroyed, deletes m_username
// player1.m_username now points to deleted memory - UNDEFINED BEHAVIOR!
std::cout << player1.getUsername() << '\n'; // Disaster!
return 0;
}
What went wrong?
player1allocates memory andm_usernamepoints to it- Default copy constructor copies the pointer value (shallow copy)
- Now both
player1.m_usernameandplayer2.m_usernamepoint to the same memory - When
player2is destroyed, its destructor deletes the shared memory player1.m_usernameis now a dangling pointer pointing to deleted memory- Accessing
player1.m_usernamecauses undefined behavior
Deep copying
A deep copy allocates new memory and copies the actual values, not just the pointer:
#include <iostream>
#include <cstring>
class PlayerProfile
{
private:
char* m_username{};
int m_score{};
public:
PlayerProfile(const char* username, int score)
: m_score{score}
{
m_username = new char[std::strlen(username) + 1];
std::strcpy(m_username, username);
}
// Deep copy constructor
PlayerProfile(const PlayerProfile& player)
: m_score{player.m_score}
{
if (player.m_username)
{
m_username = new char[std::strlen(player.m_username) + 1];
std::strcpy(m_username, player.m_username);
}
else
{
m_username = nullptr;
}
}
~PlayerProfile()
{
delete[] m_username;
}
char* getUsername() const { return m_username; }
int getScore() const { return m_score; }
};
int main()
{
PlayerProfile player1{"Dragon_Slayer", 1500};
{
PlayerProfile player2{player1}; // Deep copy!
// player2 has its own separate copy of the username
std::cout << "Player 2: " << player2.getUsername() << '\n';
} // player2 destroyed, deletes its own memory
// player1 still valid!
std::cout << "Player 1: " << player1.getUsername() << '\n';
return 0;
}
Deep copy assignment operator
We also need a deep copy assignment operator:
PlayerProfile& PlayerProfile::operator=(const PlayerProfile& player)
{
if (this == &player)
return *this;
// Clean up existing resource
delete[] m_username;
// Copy non-pointer members
m_score = player.m_score;
// Deep copy the pointer member
if (player.m_username)
{
m_username = new char[std::strlen(player.m_username) + 1];
std::strcpy(m_username, player.m_username);
}
else
{
m_username = nullptr;
}
return *this;
}
Key differences from the copy constructor:
- Self-assignment check (to avoid deleting our own data)
- Delete existing allocated memory before allocating new memory
- Return
*thisfor chaining
The rule of three
If a class requires any of the following, it probably requires all three:
- Destructor (to clean up resources)
- Copy constructor (to perform deep copy)
- Copy assignment operator (to perform deep copy)
If you're manually managing resources (like dynamic memory), you need all three to prevent resource leaks and undefined behavior.
A better solution: use the standard library
Writing correct copy constructors and assignment operators is error-prone. The standard library provides classes that handle memory management for you:
#include <iostream>
#include <string>
class PlayerProfile
{
private:
std::string m_username{}; // std::string manages its own memory!
int m_score{};
public:
PlayerProfile(const std::string& username, int score)
: m_username{username}, m_score{score}
{
}
// No need for custom copy constructor, assignment operator, or destructor!
// Compiler-generated versions work perfectly
const std::string& getUsername() const { return m_username; }
int getScore() const { return m_score; }
};
int main()
{
PlayerProfile player1{"Dragon_Slayer", 1500};
PlayerProfile player2{player1}; // Safe deep copy via std::string
std::cout << "Player 1: " << player1.getUsername() << '\n';
std::cout << "Player 2: " << player2.getUsername() << '\n';
return 0;
}
Use standard library classes (std::string, std::vector, etc.) instead of managing memory yourself. They handle deep copying automatically and correctly.
Summary
Shallow copying: The default memberwise copying that copies member values including pointer values, resulting in multiple pointers to the same memory. Works fine for simple classes but causes problems with dynamically allocated resources.
Deep copying: Allocates new memory and copies the actual data, creating independent objects. Required when a class manages pointers to dynamically allocated memory.
The rule of three: If a class requires any one of the following, it probably requires all three: destructor, copy constructor, and copy assignment operator. This applies to classes that manually manage resources.
Copy constructor implementation: Must allocate new memory and copy the data (not just the pointer). Should handle null pointers and check for successful allocation.
Copy assignment implementation: Similar to copy constructor, but must also clean up existing resources, check for self-assignment, and return *this for chaining.
When implementing classes that manage resources, prefer using standard library classes like std::string and std::vector over manual memory management. They automatically handle deep copying correctly and reduce the likelihood of memory leaks and undefined behavior.
Understanding Shallow and Deep Copies - Quiz
Test your understanding of the lesson.
Practice Exercises
Array Wrapper with Deep Copy
Create an IntArray class that manages a dynamic array. Implement proper deep copy constructor and copy assignment operator following the Rule of Three to prevent shallow copy problems.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!