Hello! Welcome back to our deep dive into C++.
In our last lesson, we took control of our objects' lifecycles by implementing the "Rule of Five." We saw that for any class managing a raw resource (like memory allocated with new), we must meticulously define its destructor, copy/move constructors, and copy/move assignment operators to prevent bugs like memory leaks and double-frees.
Today, we'll formalize and analyze the core concepts behind that rule. Our goal is to analyze the difference between shallow copy, deep copy, and move semantics in terms of their memory operations. Understanding this is fundamental to writing both correct and efficient C++ code. You'll see exactly what happens on the stack and the heap for each operation, connecting directly to your C knowledge of memory management.
1. Shallow Copy: The Default Danger
As we saw last time, if you don't provide a copy constructor, the C++ compiler generates one for you. This default constructor performs a shallow copy.
A shallow copy is a member-wise copy. For simple data types like int or double, this is fine. But for a pointer, it copies the address, not the data the pointer points to. This results in two objects believing they exclusively own the same resource.
Let's visualize the problem. Imagine our SimpleString class from the last lesson, where s1 holds "hello".
SimpleString s1("hello");
SimpleString s2 = s1; // Default copy constructor performs a shallow copy
Memory Layout (Shallow Copy):
- Stack: We have two objects,
s1ands2. Thes2.datapointer is a copy of thes1.datapointer. - Heap: There is only one block of memory containing "hello". Both
s1.dataands2.datapoint to this same block.
The disaster occurs at the end of the scope. The destructor for s2 is called, which delete[]s the memory. Moments later, the destructor for s1 is called and tries to delete[] the exact same memory again. This is a double-free, leading to undefined behavior.
C++ Copy constructors (beginner-friendly tutorial + practical examples)
To see this disaster in action, let's watch a short segment from the video 'C++ Copy constructors' by CodeBeauty. It provides a clear, visual explanation of how a shallow copy leads directly to a double-free error.
Please watch from 04:54 to 07:18, and then from 12:52 to 16:10. The first part introduces the problem, and the second part uses a diagram to perfectly illustrate the shared pointer and the resulting double-free. Focus on the visualization of the two objects pointing to the same dynamic memory.
2. Deep Copy: The Correct (but Costly) Solution
To fix the shallow copy problem, we implement a custom copy constructor that performs a deep copy.
A deep copy creates a new, independent copy of the object, including any resources it manages. If the object contains a pointer to a dynamic resource, a deep copy allocates new memory for the copy and then copies the resource's content.
// Our custom copy constructor from the previous lesson
SimpleString(const SimpleString& other) {
size = other.size;
data = new char[size + 1]; // 1. Allocate NEW memory
std::strcpy(data, other.data); // 2. Copy the content
}
Memory Operations (Deep Copy):
- A new object is created on the stack.
- The copy constructor executes. It calls
newto allocate a fresh block of memory on the heap, completely separate from the original object's heap memory. - The content from the original object's heap block is copied into the new heap block (e.g., using
std::strcpyor a loop).
Now we have two distinct objects, each with its own independent resource on the heap. This is correct, but it can be expensive. For a large object like a vector with a million elements, you pay the price of a new memory allocation and copying a million elements. This is an O(n) operation, where n is the size of the data.
C++ Copy constructors (beginner-friendly tutorial + practical examples)
Let's return to the CodeBeauty video to see the implementation and visualization of a correct deep copy.
Now watch from 16:10 to 19:25. This segment shows how to correctly implement the deep copy by allocating new memory and copying the elements, avoiding the double-free error.
3. Move Semantics: The Efficient Transfer
Deep copies are essential for correctness, but as we noted, they can be slow. Consider this scenario:
SimpleString make_string() {
return SimpleString("I am a temporary");
}
SimpleString my_string = make_string();
Here, make_string() returns a temporary SimpleString object. This temporary object will be used to initialize my_string and then will be immediately destroyed. Performing a full deep copy here is wasteful: we allocate new memory, copy the data, and then immediately free the original memory.
This is where move semantics come in. Instead of copying the resource, we transfer ownership. We "steal" the resource from the temporary object and leave it in an empty, but valid, state.

Memory Operations (Move):
- A new object is created on the stack.
- The move constructor executes. It copies the pointer (and size) from the source object to the new object.
- It then sets the source object's pointer to
nullptr. This is the crucial "stealing" step. It ensures the source object's destructor won't free the memory that now belongs to the new object.
This operation is incredibly fast. It involves only a few pointer and integer assignments, regardless of the size of the resource. This is an O(1) operation.
Re-inventing move semantics in modern C++ in 13 minutes
The video 'Re-inventing move semantics' is excellent for building intuition. It walks through the problem of expensive copies and 're-invents' move semantics from first principles, which aligns perfectly with your goal of understanding the 'why'.
Please watch from the beginning to 09:23. Pay close attention to how it progresses: The Problem (0:00-3:27): Sets up the HugeObject example and the cost of copying. The Idea (3:27-4:21): Introduces the concept of 'stealing' the data instead of copying it. The Mechanism (4:21-9:23): Explains how a new reference type (&&) and function overloading allow the compiler to choose between copying and 'stealing' (moving).
Under the Hood: A Concrete Analysis
Let's cement these ideas with a formal textual explanation and a concrete code example that prints memory addresses, leaving no doubt about what's happening.
The article 'Move Constructor' by Andrzej Krzemieński is a classic blog post that clearly explains the motivation and implementation of move semantics. It uses std::vector as an example, which is great because its internal structure (pointers to a heap-allocated array) is something you're familiar with from C.
Please read the sections titled 'Copy constructor', 'Copying that is not copying', 'Move constructor', and the text that follows up to (but not including) the heading 'Passing/Returning by value — redefined'. Focus on: The description of a std::vector's internal pointers. The implementation of the vector move constructor and how it 'steals' the pointers and then nullifies the source (tmp.begin_ = nullptr;). The explanation of when the compiler chooses the move constructor automatically (from temporaries).
Now for the definitive proof. The following resource provides a code example that prints the memory addresses of object members and their internal data pointers before and after copy and move operations. This is the "under the hood" view you've been looking for.
What is the difference between the copy constructor and move constructor?
This Stack Overflow answer provides an excellent, practical demonstration of the memory operations involved in copying versus moving.
Please read both the introductory text and the section with the Myclass example (starting from 'Part 1'). In the text, focus on the distinction: a copy constructor 'must allocate its own copy of the object's data,' while a move constructor 'can simply "move" the data by taking ownership of the pointer.' In the Myclass example, carefully examine the output for 'Part 2' (copy) and 'Part 3' (move). Notice that after a copy, obj1.v.data() and obj2.v.data() have different addresses. But after a move, obj3.v.data() has the same address that obj1.v.data() used to have, and obj1.v.data() is now null (printed as address 0). This is the smoking gun.
Test your understanding!
You have a class Buffer that manages a large, dynamically allocated block of memory.
class Buffer {
char* p_data;
size_t size;
public:
// ... Assume all 5 special members are defined ...
};
Buffer create_buffer(size_t n) {
return Buffer(n); // Assume this constructor allocates 'n' bytes.
}
int main() {
Buffer b1(1024); // Constructor
Buffer b2 = b1; // Line A
Buffer b3 = create_buffer(2048); // Line B
}
Describe the sequence of main memory operations (heap allocations, data copying) that occur at Line A and Line B.
Show answer
Line A: Buffer b2 = b1; (Copy Construction)
This is initialization from an lvalue (b1), so the copy constructor is called.
- Heap Allocation: A new block of 1024 bytes is allocated on the heap for
b2. - Data Copying: The contents of the 1024-byte block pointed to by
b1.p_dataare copied into the newly allocated block forb2.
This is a deep copy.
Line B: Buffer b3 = create_buffer(2048); (Move Construction)
This is initialization from an rvalue (the temporary object returned by create_buffer), so the move constructor is called.
- Heap Allocation: A 2048-byte block is allocated inside
create_buffer. - Pointer Transfer: The
p_datapointer from the temporary object is copied intob3.p_data. - Nullification: The
p_datapointer in the temporary object is set tonullptr. - No second heap allocation or large data copy occurs during the initialization of
b3. The destructor for the temporary object is then called, which safely does nothing with its null pointer.
This is a move.
Conclusion
You have now analyzed the three fundamental ways C++ handles object "duplication," right down to the memory operations. This knowledge is critical for diagnosing performance issues and ensuring program correctness.
Key Takeaways:
- Shallow Copy: Simply copies member values. For pointers, this copies the address, leading to two objects pointing to the same resource and an inevitable double-free error. It is almost always wrong for resource-managing classes.
- Deep Copy: Creates a new, independent resource for the copied object. This involves a potentially expensive heap allocation and data transfer (O(n)). It ensures correctness but can be slow.
- Move Semantics: Transfers ownership of a resource from a source object (usually a temporary) to a destination object. It is an extremely fast pointer-swap operation (O(1)) that avoids expensive allocations and copies. The moved-from object is left in a valid but empty/unspecified state.
In the last two lessons, we've seen how much manual work is required to correctly manage a simple pointer using the Rule of Five. This is error-prone and tedious. What if there was a way to get all the benefits of correct deep copies and efficient moves without writing any of these five special member functions yourself?
In our next lesson, we will introduce the RAII (Resource Acquisition Is Initialization) idiom. By using C++'s "smart pointer" classes, which are themselves perfect Rule-of-Five-compliant types, we can often achieve the "Rule of Zero," where the compiler-generated defaults do exactly what we want.