Copy Methods

- During program operation, there are many instances when objects are copied

- Consider the following situations

- In all these instances, a copy of a Fraction object is made during program operation

 

 

Fraction f1, f2;

f2 = f1;

 

 

Fraction f1;

func1(f1);

 

void func1( Fraction f )

{

      ...

}

 

 

Fraction f1;

f1 = func2();

 

Fraction func1()

{

      Fraction f;

      return f;

}

 

- With class objects, how do we assure that all member variables are copied from one object to another?
- State information from one object is copied to another by a special type of constructor called a copy constructor

- This copy constructor is created automatically and is called each time a copy of an object is made

- Each class member variable is copied from one object to another by a member-wise copy operation
- When classes contain static members (no pointers), this default copy constructor operation is sufficient

- When classes contain dynamic members (pointers), we must override and add to this default copy operation
- Let’s consider examples of both cases

 

 

Static member variables
- Member-wise copy operation copies information for all variables
- No need for additional work, all information copied automatically from object to object
- This copy operation is often referred to as a shallow-copy

- Consider an example using a class to describe a digital image

 

#include <iostream>

using namespace std;

 

class image

{

private:

  int width;      // width of image

  int height;     // height of image

  int size;       // number of pixels

 

public:

  image();

  image(int,int);

  ~image();

  void init();

  void calculateSize();

  void print();

};

image::image()

{

  init();

}

image::image(int w, int h)

{

  init();

  width = w;

  height = h;

}

image::~image()

{

}

void image::init()

{

  width = height = size = 0;

}

void image::calculateSize()

{

  size = width * height;

}

void image::print()

{

  cout << “Image” << endl;

  cout << “  width: “ << width << endl;

  cout << “  height: “ << height << endl;

  cout << “  size: “ << size << endl;

}

 

main()

{

  image i1(320,240);

  image i2;

  i2.print();

 

  // Assign one object to another

  i2 = i1;

 

  i2.print();

}

 

Output
Image

  width: 0

  height: 0

  size: 0

Image

  width: 320

  height: 240

  size: 0 

 

- Note how the values for all static members are copied from object i1 to object i2

- This was performed automatically by the default copy constructor for the image class

- The destination object (i2) stores the values in its members in separate memory locations

- The values copied from members are the values stored at the time of copying

- As such, further changes on members of the target object (i1) do not modify the destination object (i2)

- In the example below, the size variable of i1 is changed after the copy and does not affect i2

 

main()

{

  image i1(320,240);

  image i2;

  i2.print();

 

  // Assign one object to another

  i2 = i1;

 

  // i1 changed

  i1.calculateSize();

  i1.print();

  i2.print();

 

Output
Image

  width: 0

  height: 0

  size: 0

Image

  width: 320

  height: 240

  size: 76800

Image

  width: 320

  height: 240

  size: 0

 

 

- This default member-wise copy even works with values stored in member arrays

- Each array value is copied from one member array to another during a copy

- Consider the example below where we’ve added an array to the image class

- Note that each value in the member array is copied from i1 to i2


#include <iostream>

using namespace std;

 

class image

{

private:

  int width;

  int height;

  int size;

  int array[3];

 

public:

  image();

  image(int,int);

  ~image();

  void init();

  void calculateSize();

  void print();

  void setArray(int a, int b, int c)

  {

    array[0] = a;

    array[1] = b;

    array[2] = c;

  }

};

image::image()

{

  init();

}

image::image(int w, int h)

{

  init();

  width = w;

  height = h;

}

image::~image()

{

}

void image::init()

{

  width = height = size = 0;

  array[0] = 0;

  array[1] = 0;

  array[2] = 0;

}

void image::calculateSize()

{

  size = width * height;

}

void image::print()

{

  cout << “Image” << endl;

  cout << “  width: “ << width << endl;

  cout << “  height: “ << height << endl;

  cout << “  size: “ << size << endl;

  cout << “  array: “;

  cout << array[0] << “, “ << array[1] << “,“ << array[2] << endl;

}

 

main()

{

  image i1(320,240);

  i1.setArray(10,20,30);

  image i2;

  i2.print();

 

  // Assign one object to another

  i2 = i1;

 

  i2.print();

 

Output
Image

  width: 0

  height: 0

  size: 0

  array: 0 0 0

Image

  width: 320

  height: 240

  size 0

  array: 10 20 30
 

 

Dynamic member variables
- When objects contain dynamic members (pointers), we must override the default operation

- Let’s examine why such additional work is necessary

- Consider replacing a pointer for the array in our image class

- Note how the address of dynamically allocated memory is stored in the pointer

 

#include <iostream>

using namespace std;

 

class image

{

private:

  int width;

  int height;

  int size;

  int *array;           // stores address of dynamic memory

public:

  image();

  image(int,int);

  ~image();

  void init();

  void calculateSize();

  void print();

};

image::image()

{

  init();

}

image::image(int w, int h)

{

  init();

  width = w;

  height = h;

  calculateSize();

 

  // Dynamically allocated memory, address stored in pointer

  array = size ? new int[size] : NULL;

}

image::~image()

{

  // Delete dynamic memory (undo what is done in constructor)

  if( array ) delete[] array;

}

void image::init()

{

  width = height = size = 0;

  array = NULL;         // initialize to NULL (points to nothing)

}

void image::calculateSize()

{

  size = width * height;

}

void image::print()

{

  cout << “Image” << endl;

  cout << “  width: “ << width << endl;

  cout << “  height: “ << height << endl;

  cout << “  size: “ << size << endl;

 

  // Print address of dynamically allocated memory

  cout << “  address: “ << array << endl;

}

 

main()

{

  image i1(320,240);

  image i2;

 

  // Assign one object to another

  i2 = i1;

 

  i1.print();

  i2.print();

 

Output

Image

  width: 320

  height: 240

  size: 76800

  address: 1076027400

Image

  width: 320

  height: 240

  size: 76800

  address: 1076027400

Segmentation fault

 

 

- Even though this program compiled and executed, it reported a segmentation fault after execution

- Segmentation fault errors are usually associated with memory referencing problems

 

- Let’s examine this program and its output closely in the context of our default copy operation

- Recall that a member-wise operation copies values for all member variables by default from one object to another

- A pointer variable is treated in the same manner as other static members (width, height, size)

- The value stored in the pointer variable is copied from object i1 to the pointer variable in i2

- The problem is that this value is a memory address rather than a static memory value

- As a result, we have a situation where both objects contain pointers to the same memory location

- Note how the same value is reported as the value of the pointer for both i1 and i2 in the output above

- Both objects are sharing the same dynamic memory and, as a result, could create severe problems

- Consider, for example, if i1 modifies values in this memory without i2’s knowledge

- As a result, the values accessed by i2 might not be the values that were expected

- Even more damaging, consider the consequences if i1 decided to delete this memory

- Attempting to access this deleted memory by i2 would result in what we see above, a segmentation fault

- When our program terminates, the destructor for i1 is called and deletes the dynamic memory

- When the destructor for i2 is called, it attempts to delete memory that is already deleted

- As a result, a segmentation fault occurs at the completion of the program

- This is a common and often overlooked mistake made with classes containing dynamic members

 

- As we can see, in such cases, our default member wise copy constructor operation is not sufficient

- Instead of sharing, we’ll need to allocate and copy dynamic memory from one object to another

- Copying dynamic memory (as well as static members) is often referred to as a deep-copy

- To assure such successful copying in all instances, we need to override the following methods

 

 

Copy Constructor
- Special constructor function with a
const reference object parameter

 

image( const image & );

 

- Called automatically whenever a copy is made from passing/returning function objects
- Overrides the default member-wise copy constructor
- We associate our own code to handle dynamic member copying
- We must also copy all static members

 

 

Assignment operator
- Assignment involves the use of an operator; the assignment operator
=
- As an operator, recall that a function is called when encountering a statement with this operator
- This function is called the assignment operator function

- To perform deep copies when assigning one object to another, we must overload this operation

 

void operator=( const image & );


- We associate our own code to handle dynamic member copying
- We must also copy all static members


- Let’s modify our image class above to override both these methods to perform successful copying

- Important: we override both methods to successfully copy objects in all copying scenarios

 

#include <iostream>

using namespace std;

 

class image

{

private:

  int width;

  int height;

  int size;

  int *array;

public:

  image();

  image(int,int);

  ~image();

 

  // Override copy constructor

  image( const image & );

 

  // Overload assignment operator

  void operator=( const image & );

 

  void init();

  void calculateSize();

  void print();

};

image::image()

{

  init();

}

image::image(int w, int h)

{

  init();

  width = w;

  height = h;

  calculateSize();

 

  // Dynamically allocated memory, address stored in pointer

  array = size ? new int[size] : NULL;

}

image::~image()

{

  if( array ) delete[] array;

}

 

// Overloaded assignment operator

void image::operator=(const image &t)

{

  // if not copying self

  if( this != &t )

  {

    // copy static members

    width  = t.width;

    height = t.height;

    calculateSize();

 

    // delete any existing memory

    if( array ) delete[] array;

 

    // allocate memory for object

    array = size ? new int[size] : NULL;

 

    // copy dynamic memory

    int i;

    for( i = 0; i < size; ++i )

      array[i] = t.array[i];

  }

}

 

// Override copy constructor

image::image( const image &t )

{

  // Initialize the state

  init();

 

  // Assign the target input to itself

  // This calls our new overloaded assignment operator

  *this = t;

}

void image::init()

{

  width = height = size = 0;

  array = NULL;

}

void image::calculateSize()

{

  size = width * height;

}

void image::print()

{

  cout << “Image” << endl;

  cout << “  width: “ << width << endl;

  cout << “  height: “ << height << endl;

  cout << “  size: “ << size << endl;

 

  // Print address of dynamically allocated memory

  cout << “  address: “ << array << endl;

}

 

main()

{

  image i1(320,240);

  image i2;

 

  // Assign one object to another

  i2 = i1;

 

  i1.print();

  i2.print();

}

 

Output

Image

  width: 320

  height: 240

  size: 76800

  address: 1076027400

Image

  width: 320

  height: 240

  size: 76800

  address: 1076338696 

 

 

- In the assignment operator, we include the code to allocate and copy values in dynamic memory

- Note that in addition to copying dynamic memory, we must also copy static members

 

// copy static members

width  = t.width;

height = t.height;

calculateSize();

 

// delete any existing memory

if( array ) delete[] array;

 

// allocate new memory for object

array = size ? new int[size] : NULL;

 

// copy values in dynamic memory

int i;

for( i = 0; i < size; ++i )

  array[i] = t.array[i];

 

 

- Next, in the copy constructor, we need to copy members from the source object (t) to the object itself

- Recall that we can reference the object itself using the this pointer

- We need the same code we used in the assignment operator in our copy constructor

- Rather than repeating this code, we assign the source object to itself with the following statement

 

*this = t;

 

- Since this is an assignment, the assignment operator is called when this statement is processed

- In this way, the copy constructor calls the assignment operator to perform the dynamic copying

- Look closely at the program output (repeated below)

 

Image

  width: 320

  height: 240

  size: 76800

  address: 1076027400

Image

  width: 320

  height: 240

  size: 76800

  address: 1076338696 

 

- Note that because each object now has its own copy of the dynamic memory, each reports a different address

- Upon program termination, the destructors for i1 and i2 both successfully delete their respective dynamic memory

- As a result, there is no longer a memory conflict and the program terminates without a segmentation fault