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:
-
When an object of the class is returned by value.
-
When an object of the class is passed (to a function) by value as an argument.
-
When an object is constructed based on another object of the same class.
-
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.
-
The destructor body runs
-
Destructors care involved for fields which are objects
-
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).