Take std::string as an example:
Take std::string as an example:
A destructor of a class is the function that is automatically called when an object of that class type is destroyed.
Syntax: ~ClassName() { /* ... */ }
struct A {
A() {
std::cout << 'c';
}
~A() {
std::cout << 'd';
}
};
for (int i = 0; i != 3; ++i) {
A a;
// do something ...
}
Output:
cdcdcd
Called automatically when the object is destroyed!
Called automatically when the object is destroyed!
We often do some cleanup in a destructor:
class A {
SomeResourceHandle resource;
public:
A(/* ... */) : resource(obtain_resource(/* ... */)) {}
~A() {
release_resource(resource);
}
};
Suppose we want to implement a "dynamic array":
Expected usage:
int n; std::cin >> n;
Dynarray arr(n); // `n` is runtime determined
// `arr` should have allocated memory for `n` `int`s now.
for (int i = 0; i != n; ++i) {
int x; std::cin >> x;
arr.at(i) = x * x; // subscript, looks as if `arr[i] = x * x`
}
// ...
// `arr` should deallocate its memory itself.
class Dynarray {
int *m_storage;
std::size_t m_length;
};
m stands for member.[Best practice] Make data members private, to achieve good encapsulation.
Dynarray a(n); to construct a Dynarray that contains n elements.
new int[n]{}: Allocate a block of heap memory that stores n ints, and value-initialize them.Dynarray a(n); to construct a Dynarray that contains n elements.
class Dynarray {
int *m_storage;
std::size_t m_length;
public:
Dynarray(std::size_t n) : m_storage(new int[n]{}), m_length(n) {}
};
If the class has a user-declared constructor, the compiler will not generate a default constructor.
class Dynarray {
int *m_storage;
std::size_t m_length;
public:
Dynarray(std::size_t n) : m_storage(new int[n]{}), m_length(n) {}
};
Since Dynarray has a user-declared constructor, it does not have a default constructor:
Dynarray a; // Error.
class Dynarray {
int *m_storage;
std::size_t m_length;
public:
Dynarray(std::size_t n)
: m_storage(new int[n]{}), m_length(n) {}
~Dynarray() {
delete[] m_storage; // Pay attention to `[]`!
}
};
Is this correct?
class Dynarray {
// ...
~Dynarray() {
if (m_length != 0)
delete[] m_storage;
}
};
Is this correct?
class Dynarray {
// ...
~Dynarray() {
if (m_length != 0)
delete[] m_storage;
}
};
NO! new [0] may also allocate some memory (implementation-defined, like malloc), which should also be deallocated.
Is this correct?
class Dynarray {
// ...
~Dynarray() {
delete[] m_storage;
m_length = 0;
}
};
Is this correct?
class Dynarray {
// ...
~Dynarray() {
delete[] m_storage;
m_length = 0;
}
};
It is correct, but m_length = 0; is not needed. The destructor is executed right before the Dynarray object "dies", so the value of m_length does not matter!
Design some useful member functions.
class Dynarray {
// ...
public:
std::size_t size() const {
return m_length;
}
bool empty() const {
return m_length == 0;
}
};
Design some useful member functions.
class Dynarray {
// ...
public:
int &at(std::size_t i) {
return m_storage[i];
}
const int &at(std::size_t i) const {
return m_storage[i];
}
};
Why do we need this "const vs non-const" overloading?
void print(const Dynarray &a) {
for (std::size_t i = 0;
i != a.size(); ++i)
std::cout << a.at(i) << ' ';
std::cout << std::endl;
}
void reverse(Dynarray &a) {
for (std::size_t i = 0,
j = a.size() - 1; i < j; ++i, --j)
std::swap(a.at(i), a.at(j));
}
int main() {
int n; std::cin >> n;
Dynarray array(n);
for (int i = 0; i != n; ++i)
std::cin >> array.at(i);
reverse(array);
print(array);
return 0;
// Dtor of `array` is called here,
// which deallocates the memory
}
We can easily construct a std::string to be a copy of another:
std::string s1 = some_value();
std::string s2 = s1; // s2 is initialized to be a copy of s1
std::string s3(s1); // equivalent
std::string s4{s1}; // equivalent, but modern
Can we do this for our Dynarray?
Before we add anything, let's try what will happen:
Dynarray a(3);
a.at(0) = 2; a.at(1) = 3; a.at(2) = 5;
Dynarray b = a; // It compiles.
print(b); // 2 3 5
a.at(0) = 70;
print(b); // 70 3 5
Ooops! Although it compiles, the pointers a.m_storage and b.m_storage are pointing to the same address!
Before we add anything, let's try what will happen:
Dynarray a(3);
Dynarray b = a;
Although it compiles, the pointers a.m_storage and b.m_storage are pointing to the same address!
This will cause disaster: consider the case if b "dies" before a:
Dynarray a(3);
if (some_condition) {
Dynarray b = a; // `a.m_storage` and `b.m_storage` point to the same memory!
// ...
} // At this point, dtor of `b` is invoked, which deallocates the memory.
std::cout << a.at(0); // Invalid memory access!
Let a be an object of type Type. The behaviors of copy-initialization (in one of the following forms)
Type b = a;
Type b(a);
Type b{a};
are determined by a constructor: the copy constructor.
= in Type b = a; is not an assignment operator!The copy constructor of a class X has a parameter of type const X &:
class Dynarray {
public:
Dynarray(const Dynarray &other);
};
Why const?
Why &?
What should be the correct behavior of it?
class Dynarray {
public:
Dynarray(const Dynarray &other);
};
other.class Dynarray {
public:
Dynarray(const Dynarray &other)
: m_storage(new int[other.size()]{}), m_length(other.size()) {
for (std::size_t i = 0; i != other.size(); ++i)
m_storage[i] = other.at(i);
}
};
Now the copy-initialization of Dynarray does the correct thing:
If the class does not have a user-declared copy constructor, the compiler will try to synthesize one:
The synthesized copy constructor will copy-initialize all the members, as if
class Dynarray {
public:
Dynarray(const Dynarray &other)
: m_storage(other.m_storage), m_length(other.m_length) {}
};
If the synthesized copy constructor does not behave as you expect, define it on your own!
If the synthesized copy constructor behaves as we expect, we can explicitly require it:
class Dynarray {
public:
Dynarray(const Dynarray &) = default;
// Explicitly defaulted: Explicitly requires the compiler to synthesize
// a copy constructor, with default behavior.
};
What if we don't want a copy constructor?
class ComplicatedDevice {
// some members
// Suppose this class represents some complicated device,
// for which there is no correct and suitable behavior for "copying".
};
Simply not defining the copy constructor does not work:
What if we don't want a copy constructor?
class ComplicatedDevice {
// some members
// Suppose this class represents some complicated device,
// for which there is no correct and suitable behavior for "copying".
public:
ComplicatedDevice(const ComplicatedDevice &) = delete;
};
By saying = delete, we define a deleted copy constructor:
ComplicatedDevice a = something();
ComplicatedDevice b = a; // Error: calling deleted function
Apart from copy-initialization, there is another form of copying:
std::string s1 = "hello", s2 = "world";
s1 = s2; // s1 becomes a copy of s2, representing "world"
In s1 = s2, = is the assignment operator.
= is the assignment operator only when it is in an expression.
s1 = s2 is an expression.std::string s1 = s2 is in a declaration statement, not an expression. = here is a part of the initialization syntax.The copy-assignent operator is defined in the form of operator overloading:
a = b is equivalent to a.operator=(b).class Dynarray {
public:
Dynarray &operator=(const Dynarray &other);
};
operator=.operator= returns reference to the left-hand side object (the object being assigned).
*this.We also want the copy-assignment operator to copy the contents, not only an address.
class Dynarray {
public:
Dynarray &operator=(const Dynarray &other) {
m_storage = new int[other.size()];
for (std::size_t i = 0; i != other.size(); ++i)
m_storage[i] = other.at(i);
m_length = other.size();
return *this;
}
};
Is this correct?
Avoid memory leaks! Deallocate the memory you don't use!
class Dynarray {
public:
Dynarray &operator=(const Dynarray &other) {
delete[] m_storage; // !!!
m_storage = new int[other.size()];
for (std::size_t i = 0; i != other.size(); ++i)
m_storage[i] = other.at(i);
m_length = other.size();
return *this;
}
};
Is this correct?
What if self-assignment happens?
class Dynarray {
public:
Dynarray &operator=(const Dynarray &other) {
// If `other` and `*this` are actually the same object,
// the memory is deallocated and the data are lost! (DISASTER)
delete[] m_storage;
m_storage = new int[other.size()];
for (std::size_t i = 0; i != other.size(); ++i)
m_storage[i] = other.at(i);
m_length = other.size();
return *this;
}
};
Assignment operators should be self-assignment-safe.
class Dynarray {
public:
Dynarray &operator=(const Dynarray &other) {
int *new_data = new int[other.size()];
for (std::size_t i = 0; i != other.size(); ++i)
new_data[i] = other.at(i);
delete[] m_storage;
m_storage = new_data;
m_length = other.size();
return *this;
}
};
This is self-assignment-safe. (Think about it.)
Like the copy constructor:
The copy-assignment operator can also be deleted, by declaring it as = delete;.
If you don't define it, the compiler will generate one that copy-assigns all the members, as if it is defined as:
class Dynarray {
public:
Dynarray &operator=(const Dynarray &other) {
m_storage = other.m_storage;
m_length = other.m_length;
return *this;
}
};
You can also require a synthesized one explicitly by saying = default;.
Among the copy constructor, the copy-assignment operator and the destructor:
Among the copy constructor, the copy-assignment operator and the destructor:
Let
If for a class,
then the compiler should not generate
Let
If for a class,
then the compiler still generates
Into modern C++: The Rule of Five.
Read Effective Modern C++ Item 17 for a thorough understanding of this.
Lifetime of an object:
static, global, allocated, ...Copy control
ClassName(const ClassName &)ClassName &operator=(const ClassName &)
~ClassName()=default, =delete