Managing Dynamically Allocated Arrays in Classes
The copy constructor creates a new object by allocating new storage with elem{new double[a.sz]}, setting sz from a, and copying each element from the original.
Why Copying a Vector Is Harder Than It Looks
A Vector class that stores its elements in a dynamically allocated array has a hidden problem: copying the object is not the same as copying a few numbers. The object holds a pointer to an array that lives on the heap, and the default way C++ copies an object only duplicates the pointer. Two objects would then refer to the same array, and a change through one would appear in the other. This article works from two member functions of a Vector class: a copy constructor, which builds a new Vector from an existing one, and a copy assignment operator, which overwrites an existing Vector with the contents of another.
The approach here is to trace the code before explaining it. Follow the state of two Vector objects one line at a time, and at each step ask which object is being read and which is being written. Then come back to the functions and check your expectation against what the program actually does.
Tracing x = y Step by Step
Suppose a Vector x already holds three values and a Vector y holds two different values. The program then executes x = y. Before the first line of the function runs, x owns an array of three values and its size field sz is 3. Inside the function, the parameter a refers to the same object as y, so a.sz is 2 and a.elem refers to y's array. Each step below changes only the state of x, because the copy assignment operator writes to the object it is called on and only reads from its argument.
What the Copy Constructor Builds
The copy constructor performs two jobs. First, it allocates new memory for the copy. Second, it copies the elements from the original object. The member initializer list carries out the allocation: elem{new double[a.sz]} requests an array with as many doubles as the original holds, and sz{a.sz} records the same size. The body of the constructor then runs a loop from zero up to sz, assigning each element of the original into the matching position of the new array.
The parameter is a const reference, so the copy constructor only reads from the original. The original object is never modified by the copy. Everything that changes during the operation belongs to the new object being built. Because each Vector gets its own array, the two objects are independent: changing an element of one does not change the corresponding element of the other.
Copy Constructor Line by Line
- The initializer list allocates a new array sized to the original, so the new object gets its own storage.
- sz{a.sz} copies the size from the original into the new object.
- The loop runs from zero up to sz and copies each element of the original into the matching slot of the new array.
- a is only read. Every write goes to the new object's members or its new array.
Reading the copy constructor as a trace- 1Line 1 declares the constructor. The parameter const Vector& a is a read-only reference to the original object.
- 2Line 2 is the initializer list. elem{new double[a.sz]} allocates a fresh array with a.sz doubles and stores its address in the new object's elem member.
- 3The same line sets sz{a.sz}, so the new object records the same number of elements.
- 4The loop body copies each element. Index i in the original's array lands in index i of the new array, for every i from zero to sz minus one.
- 5After the constructor finishes, the new object has its own array holding the same values as the original.
How Copy Assignment Reorders the Work
Both functions copy elements and both allocate new memory, so they look alike on the page. The key difference is what already exists when they run. The copy constructor creates a brand new object, so there is no old state to clean up. Copy assignment acts on an object that already holds elements, so it must release those old elements before it finishes. The source identifies two important facts: copy assignment allocates new memory for the copy, and it deletes old elements before assigning new ones.
- Line 3 allocates the new array p before anything is released.
- The loop copies every element of a into p while the old array is still in place.
- delete[] elem releases the old array that this object owned.
- elem = p and sz = a.sz make the object point at the new array and record its size.
- return *this hands back the object that was assigned to.
The order is deliberate. The copy has to read from the source before anything is released. If the source were the same object as the target, the copy loop would need the old elements, and deleting them first would leave the loop reading from storage that no longer belongs to the object. Keeping the allocate-and-copy steps ahead of the delete avoids that problem. This is general reasoning about the order, not a claim the source makes about a specific self-assignment case.
Tracing a Copy Constructor Call
Copying a three-element Vector
Vector original holds the values 1.0, 2.0, 3.0 with sz 3. The statement Vector copy{original} runs the copy constructor. What does each object hold afterward, and what changes if copy[0] is then set to 9.0?
- 1Identify the read and write objects
The parameter a refers to original. The constructor reads from a and writes only to the new object copy.
- 2Initializer list allocates storage
elem{new double[a.sz]} creates a new array with three doubles. sz{a.sz} sets copy.sz to 3.
- 3Loop copies the values
For i from 0 to 2, copy.elem[i] receives a.elem[i], so copy's array holds 1.0, 2.0, 3.0.
- 4Check the two objects
original still has sz 3 and its own array with 1.0, 2.0, 3.0. copy has sz 3 and a different array with the same values.
- 5Modify one copy
Setting copy[0] to 9.0 writes into copy's array only. original's array is not touched, so original[0] remains 1.0.
After the copy, both objects hold 1.0, 2.0, 3.0 in separate arrays. Changing copy[0] to 9.0 leaves original[0] equal to 1.0, because the two objects refer to different heap arrays.
Where the Trace Diverges
What do you think happens?
After x = y runs, what does x hold, and what does y hold?
Reveal answer
x holds the two values from y with sz 2, and y is unchanged.
The copy loop copies y's elements into the new array p. The delete[] line releases elem, which belongs to x, not y. Then elem is set to p and sz is set to 2. Nothing in the function writes to a, so y keeps its two values and sz 2.
The line where a learner's expectation usually breaks is the delete[] line. Many readers assume that deleting something inside an assignment could also affect the source, since the source is the other object in the statement. The delete[] elem line acts on elem, which is a member of the object being assigned to. The source object's array is reached only through a.elem, and the function never deletes it. The result is that x changes and y does not.
Mistakes When Reading Copy Operations
Common mistake Treating the copy constructor and copy assignment as the same operation
Why it failsThe constructor creates a new object with nothing to release. Assignment works on an object that already owns an array, so it must delete that array.
Do this insteadAsk first whether the target already owns storage. If it does, the function must release that storage.
Common mistake Assuming the copy shares the original's array
What it looks likeAfter Vector copy{original}; copy[0] = 9.0; the expectation is that original[0] also changes.
Why it failsThe constructor allocates a new array in the initializer list, so copy.elem points somewhere else.
Do this insteadFind the new allocation in the initializer list. Each object's elem pointer then refers to its own heap array.
Common mistake Reading the delete as acting on the source
Why it failsIn delete[] elem, elem names the target's member. The source's array is reached through a.elem and is not deleted.
Do this insteadCheck which object each name refers to before deciding what a line changes.
Common mistake Forgetting that sz must be updated along with elem, so the object's size no longer matches its array
Practice Tracing Two Vectors
Vector p holds the values 4.0, 5.0 with sz 2. Vector q holds 7.0, 8.0, 9.0 with sz 3. The program executes p = q. Trace the function line by line and state the sz and element values of p and q after the assignment. Then say which line is the first one where p's contents change.
Hints
- Inside the function, the name a refers to q. Decide which object each line reads and which it writes.
- The new array must hold a.sz doubles, so it has room for three values.
- Check whether the delete line changes p or q, given that elem belongs to the object being assigned to.
Key Takeaways
- A copy constructor builds a new object: its initializer list allocates a new array with a.sz elements, sets sz from a, and a loop copies each element from the original.
- The copy constructor only reads its argument, which is a const reference, so the original is never modified.
- Copy assignment must deal with the target's existing array. It allocates and fills a new array first, then deletes the old elements, then points elem at the new array and updates sz.
- To find where a result diverges from expectation, trace each line and ask which object is read and which is written. In x = y, the delete[] line releases x's array, not y's.