Student abdur {60,70,80};
Student caroline = abdur;
// equivalent 
 
Student caroliner {abdur};

How does Caroline’s initialization happen? It happens via the copy constructor which is invoked when one object is initialized with another of the same type.

Note: every class comes with (built-in):

  • a default constructor (just default constructs all fields which are objects)

  • a copy constructor (just copy initializes all fields)

  • a copy assignment constructor (just copies assigns all fields)

  • a destructor

  • a move constructor

  • a move assignment operator

To build your own copy constructor:

struct Student {
	int assns, mt, final;
	student(...){...} // constructor 
	student (const Student &other): assns{other.assns}, mt{other.mt}, final{other.final} {}
};

This copy constructor is identical to the behaviour of the compiler provided one. This then begs the question: When is the built-in copy constructor not sufficient?

Consider:

struct Node {
	int data;
	Node *next;
	Node (int data, Node *next): data{data}, next{next} {}
};
 
Node *n = new Node {1, new Node {2, new Node {3, nullptr}}};
Node m = *n; // copy constructing m with *n 
Node *p = new Node {*n}; // copy construction 

Above, n, m, p are all on the stack, the data that n, p, and m.next points at is on the heap. The built-in copy constructor just copy initializes all fields, each object has the same next point, so they all share tail (shallow copy)

m.next->data  = 10;
cout << p->next->data; // prints 10

If we want a deep copy, (so each list is a distinct copy of values, changing the data of the one tail does not affect the others) write your own copy constructor

Deep Copy:

struct Node {
	int data;
	Node *next;
	//... some code in the middle of the class definition 
	Node(const node &other): data{other.data}, next{other.next? new Node{*other.next}: nullptr}
{}
};

The copy constructor is called when:

  • An object is initialized by another object of the same type

  • When an object is passed by value (need to copy into function’s stack frame)

  • When another object is returned by value, (must copy to the caller’s stack frame)

The statements are true for now, but we’ll see examples shortly

Note: be careful with constructors that take a single parameter

struct Node {
	...
	Node (int data): data{data}, next{nullptr} {}
};
 
Node n(4); // OK, calls the constructor as intended 
Node n = 4; // also calls it, implicit conversion 
 
int f(Node n) {...} // some function which passed node into it 
 
f(4); // works, compuler silently converts 4 to a node 

Now, we could accidentally pass the wrong argument to a function, (maybe we pass an int var when we meant to pass a node, would be nice to catch this error)

GOOD IDEA: disallow compiler from using this constructor for implicit conversions, by making it explicit

struct Node {
	...
	explicit Node(int data): data{data}, next{nullptr}{}
};
 
Node n(4); //explicitly calling consturctor 
Node n = 4; // error 
f(4); //error 
Node n = Node{4}; // OK 
f(Node{4}); // OK 

A ==copy constructor== is a member function that initializes an object using another object of the same class. The Copy constructor is called mainly when a new object is created from an existing object, as a copy of the existing object.

In C++, a Copy Constructor may be called for the following cases:

  1. When an object of the class is returned by value.

  2. When an object of the class is passed (to a function) by value as an argument.

  3. When an object is constructed based on another object of the same class.

  4. When the compiler generates a temporary object.

Here is an example of a linked-list deep copy

\#include <iostream>
using namespace std;
 
struct Node {
  int data;
  Node *next;
  Node(int data, Node *next): data {data}, next {next} {}
 
  Node(const Node &n): data {n.data},
                       next {n.next ? new Node{*n.next} : nullptr} {}
};
 
ostream &operator<<(ostream &out, const Node &n) {
  out << n.data;
  if (n.next) {
    out << ",";
    out << *n.next;
  }
  return out;
}
 
int main() {
  Node *n = new Node{1, new Node{2, new Node{3, nullptr}}};
 
  Node m {*n};
  m.data = 5;
 
  Node *p = new Node (*n);
  p->data = 6;
 
  cout << "n: " << *n << endl;
  cout << "m: " << m << endl;
  cout << "p: " << *p << endl;
  cout << endl;
 
  n->next->next->data = 7;
 
  cout << "n: " << *n << endl;
  cout << "m: " << m << endl;
  cout << "p: " << *p << endl;
}

Destructors

When an object is destroyed (for stack-allocated object when they go out of scope, and for heap-allocated objects, when they are deleted), a method called the destructor runs, specifically the following sequence happens.

  1. The destructor body runs

  2. Destructors care involved for fields which are objects

  3. Space is deallocated

  • Classes come with a destructor (empty body) so does nothing (phase 2 of object destruction still runs)
  • So, when do we need to write our own destructor?
Node *np = new Node{1,new Node {2, new Node {3, nullptr}}};
...
delete np;

This only deletes the node np directly points at, not the rest in the list, we could iterate through the list freeing each node ourselves, but that is not out job, that Node owns its next node, it is responsible for cleaning it up (just like we own np and are responsible for deleting it).

After deleting np, we have leaked the rest of the list. So, we write our own destructor:

struct Node {
	...
	~Node() {delete next};
};
 
struct Student {
	...
	Student(student s) {...};
};
 
Student a{...};
Student b{a};

Now, delete np; frees the whole linked list, the node np points at is destroyed, as such its destructor is ran, its destructor deletes its next, that is a Node object being destroyed, so …, etc. Until we reach delete next; where next is the nullptr, delete nullptr is a no-op (does nothing).