Lecture 10
Note: Careful with constructors that can take one param
struct Node {
...
Node (int data):
data{data}, next{nullptr};
}
single-arg constructors create implicit conversions eg.
Node n{4}
but also
Node n=4;
implicit concersion from int to Node
int f(Node n);
f(4); // works - 4 implicitly converted to Node
Node m{4}; // works fine
Node m = 4; // works - but 4 is not a node, it is an int
DANGER:
accidentally passing an int to a function expecting a Node
silent conversion
compiler does not signal an error
potential errors are not caught
How to fix
Disable the implicit conversion - make constructor explicit
struct Node {
...
explicit Node (int data): data{data}, next{nullptr} {}
}
Node n{4}; // OK!
Node n=4; // not allowed
f(4); // not allowed
f(Node {4}); // OK!
Destructors
When an object is destroyed:
stack-allocatd: goes out of scope
heap-allocated: is deleted
A method called the desctructor is run
Classes come with a destructor (just calls destructors for all fields that are objects)
What happens when an object is destroyed?
destructor body runs
fields' destructors are invoked in reverse declaration order (for fields that are objects)
space deallocated
When do we need to write a destructor?
Node *np=new Node {1, new Node {2, new Node {3, nullptr}}};
If np goes out of scope
pointer np is reclaimed (stack-allocated)
the list is leaked
If we say delete np;
:
Write a destructor to ensure the whole list is freed:
struct Node {
...
~Node() {
delete next;
}
}
Now - delete np; frees the whole list
invokes *next's destructors using recursion
therefore, the whole list is deallocated
Copy Assignment Operator
Student billy {60,70,80};
Student jane=billy; // copy constructor
Student joey; // default constructor
joey = billy; // copy, but not a construction, it is the copy assignment operator -uses compiler-supplied default
You may need to write your own copy assignment operator:
// WRONG AND DANGEROUS WAY:
struct Node {
...
// so that cascading works (eg. a=b=c=d;)
Node &operator = (const Node &other) {
data = other.data;
next=other.next?new Node{*other.next*}:nullptr;
return *this;
}
}
Why is this way dangerous?
Node n {1, new Node {2, new Node {3, nullptr}}};
// deletes n and then tries to copy n to n
// undefined behaviour
n=n;
When writing operator=, always watch out for self-assignment:
struct Node {
...
Node &operator=(const Node &other) {
if (this == &other) return *this;
data = other.data;
delete next;
// if "new" fails, this method will abort
// next has been deleted, but not reassigned, therefore we have pointers at dead memory which means we have a corrupted list
next = other.next? new Node {*other, next}: nullptr;
return *this;
}
}
Better Way:
Node &Node::operator=(const Node &other) {
if (this == &other) return *this;
Node *tmp = next;
next = other.next? new Node {*other.next}:nullptr;
data = other.data
delete tmp;
return *this; // if new fails, node will still be in its original state (not corrupted)
}
Note:
*other.next // equivalent to *(other.next)
other->next // equivalent to (*other).next
Alternative: copy-and-swap idiom
#include <utility>
struct Node {
...
void swap (Node &other) {
using std::swap;
swap(data, other.data);
swap(next, other.next);
}
Node &operator = (const Node &other) {
Node tmp = other;
swap(tmp);
return *this;
}
};
Rvalues and Rvalue references
Recall:
an lvalue is anything with an address
an lvalue ref (&) is like a const pointer with auto-deret
always initialized to an lvalue
Now consider:
Node plusOne (Node n) {
for (Node *p=&n; p; p=p->next) {
++p->data;
}
return n;
}
Node n {1, new Node {2, nullptr}};
Node n2 = plusOne(n); //copy construction
compiler creates a temporary object to hold the result of plusOne
other is a reference to this temporary
copy constructor deep-copies from this tmeporary
But
Last updated
Was this helpful?