Concepts / Managing Dynamically Allocated Arrays in Classes

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.

  • Programming

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.

Startx: sz 3, three values; y:sz 2, two valuesAllocate pp: two empty slots; x stillholds three valuesCopy loop donep holds the two values fromydelete[] elemx's old three-value arrayis releasedReassignx.elem now points to p;x.sz is 2new double[a.sz]loop copies a's elementsrelease old storageelem = p; sz = a.sz
After each step of the copy assignment, what does x hold, and what does y hold? y never changes.

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.

a (original)sz 3; elem pointeroriginal array3 doublescopy (new object)sz 3; elem pointercopy array3 doubles, copied valueselem refers toelem refers to
Each Vector's elem member points to its own heap array, so the two objects do not share storage.

Copy Constructor Line by Line

cpp
Notes on the code
  • 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.
Code walkthrough
Reading the copy constructor as a trace
  1. 1
    Line 1 declares the constructor. The parameter const Vector& a is a read-only reference to the original object.
  2. 2
    Line 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.
  3. 3
    The same line sets sz{a.sz}, so the new object records the same number of elements.
  4. 4
    The 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.
  5. 5
    After 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.

cpp
Notes on the code
  • 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.
Allocate proom for a.sz doublesCopy a's elementsinto preads a, writes pdelete[] elemreleases the old arrayelem = ppoints at the new arraysz = a.szrecords the new sizeReturn the objectreturns the assigned objectonly after copy completes
The new array is filled before the old one is deleted, and the object is reassigned last.

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

Worked example

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?

  1. 1
    Identify the read and write objects

    The parameter a refers to original. The constructor reads from a and writes only to the new object copy.

  2. 2
    Initializer list allocates storage

    elem{new double[a.sz]} creates a new array with three doubles. sz{a.sz} sets copy.sz to 3.

  3. 3
    Loop 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.

  4. 4
    Check 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.

  5. 5
    Modify 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.

Result

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?

  • x keeps its three original values, and y is unchanged

  • x holds the two values from y, sz is 2, and y is unchanged

  • x and y both hold three values, because the delete removes y's data

  • x holds the two values from y, but y's array is deleted

Reveal answer
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 fails

    The 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 instead

    Ask 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 like

    After Vector copy{original}; copy[0] = 9.0; the expectation is that original[0] also changes.

    Why it fails

    The constructor allocates a new array in the initializer list, so copy.elem points somewhere else.

    Do this instead

    Find 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 fails

    In delete[] elem, elem names the target's member. The source's array is reached through a.elem and is not deleted.

    Do this instead

    Check 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

MEDIUM

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.

Related concepts

More concepts starting with M