MYF

[RA] Ch15 Object Creation and Destruction

Reading Assignment: All of Programming Chapter 15 Object Creation and Destruction

Object Construction

The way to have a particular method to initialize the object could be fixed in a way that:

  1. it is always called when you create an object
  2. it cannot be called directly by the programmer, but instead can only be called during object creation.

In C++, such special methods are called constructors. A constructor has no return type and the same name as the class it is inside of.

1
2
3
4
5
6
7
8
class BankAccount{
private:
double balance;
public:
BankAccount(){
balance = 0;
}
};

We could also write the constructor outside of the class declaration.

Overloading

If you do not write any constructors in a class, the C++ compiler will provide a default constructor which basically behaves as if you declared it like this:

1
2
3
4
class MyClass{
public:
MyClass(){}
};

Note that if you write any other constructors, the C++ compiler does not provide this constructor for you.

If you write any constructors, the class is non-POD, by virtue of having a constructor.

While it may be tempting to write empty parenthesis after a variable that we wish to construct via the default constructor, this approach unfortunately does not work: BankAccount x(); is interpreted as a function named x which takes no parameters and returns a BankAccount.

Dynamic Allocation

The underlying reason why malloc will not work properly is that it will not run the constructor. The is no way for malloc to actually know what type of object it is allocating space for, and thus no way for it to call the proper constructor. In C++, the proper way to allocate memory dynamically is to use the new operator. For example:

1
BankAccount * accountPtr = new BankAccount();

It will allocates memory for one object of type BankAccount, and calls the default constructor to initialize the newly created object.

We cal also use new[] operator to allocate space for an array. For example:

1
BankAccount * accountArray = new BankAccount[42]();

This code would allocate space for 42 consecutive BankAccount objects, and invoke the default constructor on each of them in ascending order of index.

If the class does not have a default constructor, or you want to initialize the elements of the array with some other constructor, you need to create an array of pointers, and then write a loop to create each object, and put a pointer to it into the array:

1
2
3
4
BankAccount ** accountPointerArray = new BankAccount*[42]();
for(int i = 0l i < 42; i++){
accountPointerArray[i] = new BankAccount(initialBalances[i]);
}

Type of Initialization

1
2
BankAccount * accountPtr = new BankAccount(); // value initialization
BankAccount * accountPtr = new BankAccount; // default initialization

These two uses of new make use of two different types of initialization. The first uses value initialization, and the second uses default initialization. Each of these has different behavior, and the specifics depend on whether the type being initialized is POD or non-POD.

When value initialization is used, a class with a default constructor is initialized by its default constructor. A class without any constructor has every field value initialized. Non-class types are zero-initialized. Arrays have their elements value initialized. When default initialization is used, non-POD types are initialized by their default constructor. POD types are left uninitialized. Arrays have their elements default initialized.

The best approach is to just always include a default constructor in your classes. Notice that the main similarity between the two is that classes with a default constructor that you wrote will be initialized by their default constructor under either scheme. If you write a default constructor, you do not need to remember the distinctions, both will do the same thing-using that constructor.

Initializer Lists

An initializer list is a list of initializations written by placing a colon after the close parenthesis of the constructors parameter list, and writing a sequence of initializations before the open curly brace of the function’s body. Each initialization takes the form name(value) that we just saw for constructing objects with parameters to their constructors. For example:

1
BankAccount::BankAccount(double initBal) : balance(initBal){}

Why initialization lists?

  • C++ makes a distinction between initialization and assignment. Any assignment statements in the constructor are treated as regular assignment statement, while initializers in the initialization list are treated as initialization.
  • C++ ensures that all fields have some form of initialization before open curly brace of constructor. If you specify the initialization you want in the initialization list, then you get exactly what you want. Otherwise, the field is default initialized. However, remember that default initialization for POD types leaves them with unspecified values.
  • References. Initializing a reference sets what the reference refers to, while assigning to the reference implicitly dereferences the reference and assigns to whatever it refers to. If your class has fields of a reference type, you must initialize them in the initializer list. Otherwise, you will receive an error message like uninitialized reference member 'ClassName::fieldName'.
  • If you have a const field, then it must be initialized in the initializer list, and may not be assigned to anywhere. Otherwise, you will get an error message like this uninitialized member 'ClassName::fieldName' with 'const' type 'const int'.

The best practice for C++ is to use the initializer list to initialize the fields of the object.

The order in which fields are initialized by the initializer list is the order in which they are declared in the class, not the order that their initializers are written in the initializer list.

What to Do

Several rules you should obey:

  • Make your classes default constructible.
  • Use initializer lists to initialize your class’s fields.
  • In the initializer list, explicitly initialize every field.
  • Initialize the fields in the order they are declared. You should be compiling with -Wall -Werror anyways.
  • use new and new[], not malloc.

Object Destruction

We would like to specify a procedure for object destruction like the createion of an object, such procedure is called a destruction. Unlike constructors, destructors may not be overloaded. A class may only have one destructor, and it mush take no parameters.

If we imagined our class having some dynamically allocated memory associated with it, such as a transaction history stored as a dynamically allocated array, then it would make sense:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class BankAccount{
private:
Transaction* transactionHistory;
int numTransactions;
double balance;

void addTransaction(double amount, const char * message){
Transaction * temp = new Transaction[numTransactions + 1];
for(int i = 0; i < numTransactions; i++){
temp[i] = transactionHistory[i];
}
temp[numTransactions].message = message;
temp[numTransactions].amount = amount;
gettimeofday(&temp[numTransactions].when, NULL);
Transaction * old = transactionHistory;
transactionHistory = temp;
numTransactions++;
delete[] old;
}

public:
BankAccount(): transactionHistory(NULL), numTransactions(0),balance(0){}
~BankAccount(){
delete[] transactionHistory;
}

void deposit(double amount){
addTransaction(amount, "Deposit");
balance += amount;
}

double withdraw(double desiredAmount){
if(desiredAmount <= balance){
addTransaction(-desiredAmount, "Withdraw");
balance -= desiredAmount;
return desiredAmount;
}
else{
double actualAmount = balance;
addTransaction(-actualAmount, "Withdraw (attempted too much)");
balance = 0;
return actualAmount;
}
}

double getBalance() const {
return balance;
}
};

In this code, the BankAccount class tracks an array of all transactions it has performed. Every time the withdraw or deposit methods are called, they call the private addTransaction method to record the transaction in this history. This modethod then reallocates the array to be larger, adds the new entry to the end, and uses the delete[] operator to free the memory from the old array. delete and delete[] free the memory allocated by new and new[] respectively. We will note that new[] does not have a realloc analog.

When Are Destructors Invoked

A destructor is invoked whenever the “box” for an object is about to be destroyed. This destruction can happen either due to dynamic deallocation through delete or delete[], by a local variable going out of scope, or by one object which contains another being destroyed.

If you deallocate memory for an array with delete[], the elements of the array have their destrutor invoked in decreasing order of index. In fact, as a general rule, whenever construction and destruction occur in a group, the order of destruction is the opposite of the order of construction.

If you have a field which is a pointer ,then the box being destroyed only contains a pointer, not an object, so no destructor is invoked. If you want to destroy the object that the pointer points at, you must explicitly delete it in the class’s destructor.

If you do not explicitly declare a destructor for a class, the compiler implicitly provides one that looks like:

1
2
3
4
class MyClass{
public:
~MyClass(){}
}

If you explicitly write a destructor, that destructor is considered non-trivial.

Object Copying

There are many ways in which programs copy values, such as when a parameter is passed to a function(its value is copied into the called function’s frame), when a value is assigned to a variable(its value is copied into the destination “box”), or when a value is returned(it is copied out of the returning function’s frame to be returned to the called).

Naively copying an object via parameter passing example.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Point{
int x;
int y;
}
class Plygon{
Point * points;
size_t numPoints;
public:
Polygon(size_t n); points(new Point[n]), numPoints(n){}
~Polygon(){
delete[] points;
}
}

double computerArea(Polygon p){
// do something.
return answer;
}
int main(void){
Polygon p(4)
printf("Area = %f\n", computerArea(p));
return EXIT_SUCCESS;
}

In this code, when calling computerArea(p), it shadow copied the Polygon p declared in main. When computerArea finished, p in this function is going to destroyed. However, in this way, it will destro the array declared in p which is declared in main function. It will lead to segment fault when main is finished, since it will try to free the array in p, but there is nothing there, it double free that space.

Naively copying an object via assignment example.

1
2
3
4
5
6
int main(){
Polygon p1(4); // say it at address a
Polygon p2(3); // say it at address b
p1 = p2; // it is a SHADOW COPY, make p1 point to a
return EXIT_SUCCESS; // space at address a is leaked, while space at address b is double free.
}

C++ distinguishes between two types of copying: copying during initialization(the copy constructor) and copying during assignment(the copying assignment operator).

Copy Constructor

Copying during initialization occurs when a new object is created as a copy of an old one. This form of copying occurs when objects are passed to functions by value, when an object is returned from a function by value, or explicitly when the programmer writes another object of the same type as the initializer for a newly declared object.

We might modify the Polygon class to have a copy constructor as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Polygon{
Point * points;
size_t numPoints;
public:
Polygon(size_t n):points(new Point[n]), numPoints(n){}
Polygon(const Polygon &rhs):
points(new Point[rhs.numPoints]),
numPoints(rhs.numPoints) {
for(size_t i = 0; i < numPoints; i++){
points[i] = rhs.points[i];
}
}
~Polygon(){
delete[] points;
}
};

The copy constructor takes a reference to its own type, like const Polygon &.

Assignment Operator

The other form of copying that can occur is copying during assignment. Unlike copying during initialization, copying during an assignment changes the value of an object which already exists to be a copy of another object. Classes may overload the assignment operator to specify how their objects should be copied during assignment.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Polygon{
Point *points;
size_t numPoints;
public:
Polygon(size_t n); points(new Point[n]), numPoints(n){}
Polygon(const Polygon &rhs):
points(new Point[rhs.numPoints]),
numPoints(rhs.numPoints) {
for(size_t i = 0; i < numPoints; i++){
points[i] = rhs.points[i];
}
}
Polygon & operator=(const Polygon &rhs){
if(this != &rhs){
Point *temp = new Point[rhs.numPoints];
for(size_t i = 0; i < rhs.numPoints; i++){
temp[i] = rhs.points[i];
}
delete[] points;
numPoints = rhs.numPoints;
point = temp;
}
return *this;
}
~Polygon(){
delete[] points;
}
}

The copying assignment operator takes a constant reference to its own type. We could overload the assignment operator on other types.

There are some distinctions between assignment operator and copy constructor:

  • The assignment operator returns a value. Specifically, it returns a reference to this object(as do most operators which modify the current object).
  • The operator begins by checking if this != &rhs. That is, if the object being assigned to is distinct from the object copied. As a general rule, we should check for this condition in writing an assignment operator, as we might otherwise run into problems.(We may delete a pointer in this object, then use it dangling in rhs). Note that we perform this check by comparing pointers(we do this != &rhs), not values(rather than `this != &rhs). Comparing pointers tells us what we want to know-arethisandrhs` referencing exactly the same object in memory.
  • The assignment operator must cleanup the existing object before we assign the copied values to it. In the copy constructor, there is nothing to cleanup at the start: the object is unintialized. However, in the assignment operator, we are overwriting an existing object with a copy.

Executing Code with Copying

Whether to use the copy constructor or assignment operator-is strictly a matter of whether you are initializing an object that you are newly creating, or changing the values in an existing object.

C++ consider the following to be initialization, rather than assignment:

1
MyClass something = anotherThing; // initialization, copy constructor

You can write it in this ways, they are the same.

1
MyClass something(anotherThing); // copy constructor

Recall that the constructor/operator is trivial if it was automatically provided by the compiler and all constructor/operator that it make use of are also trivial. If the constructor/operator is trivial, then you can simply copy the values directly as you have been doing in C.

Rule of Three

destructor => copy: If a class needs custom behavior to destroy an object, then that class needs custom behavior to copy the object-performing a deep copy, so that the destruction of an object does not leave dangling pointers in other objects.

copy <=> assignment: If a class needs special copying behavior for initialization(the copy constructor), that class needs special copying behavior for assignment, and vice-versa.

copy => destructor: If a class needs special copying behavior, it almost certainly have resources that need to be cleaned up by a destructor.

This priciple is called the Rule of Three. If you write a destructor, copy constructor, or assignment operator in a class, you must wirte all three of them.

Unnamed Temporaries

Unnamed temporaries are values that are created as the result of an expression, but not given a name. Like any other objects, unnamed temporaries are initialized by constructors and destroyed by destructors, which we need to account for when we consider the behavior of the program.

There a many ways that unnamed temporaries are created. like MyClass(42);, which create an unnamed object. Such a statement will allocate space for an object of type MyClass in current stack frame. In this example, the object will be destroyed immediately.

More generally, an unnamed temporary object is deallocated at the end of the full expression containing its creation. This rule is an other greate example of a rule where remembering its details are not that important, as long as you write code where it does not matter. If you write code where the specifics of object destruction matter, you should fully understand the rules.

Parameter Passing

Suppose we have a function that takes a MyClass object:

1
int someFunction(MyClass something){...}

We have two ways to call it.

1
2
3
4
5
6
// Method 1
someFunction(MyClass(42));

// Method2
MyClass temp(42);
someFunction(temp);

They look like the same, but there is a subtle difference. In Method 1, the compiler is optimized to creation of the object directly in someFunction‘s frame to avoid extra copy operation. The object that it creates is still destroyed at the proper time.

It is generally preferable to have functions take a const reference rather than a copy of an object. That is, we probably should write someFunction like this:

1
int someFunction(const MyClass & something){...}

Now, no copying is involved in any case. As the reference is a const reference, we can still pass an unnamed temporary to it. The compiler will create a box for the unnamed temporary, and pass the address of that box as the pointer which is the reference. If the reference is non-const, then we would not be able to pass an unnamed temporary, as it is not an lvalue.

Return Values

The most intuitive approach from the perspective of the rules that we have learned so far is that the function creates an object, and that object is explicitly copied to initialize the unnamed temporary. It would use copy constructor, after which the local object inside the function would be destroyed. C++ allows the compiler to elide the copy. It would arrange for the result to be directly initialized from within the function. This optimization is called the return value optimization.

Implicit Conversion

1
2
3
int someFunction(const MyClass & something){...}
someFunction(MyClass(42));
someFunction(42);

The C++ compiler accepts the someFunction(42); because this behavior is a broad generalization, just like int x = 5.3;. In C++, any constructor which takes one argument is considered as a way to implicitly convert from its argument type to the type of the class it resides in, unless that constructor is declared explicit. As a general rule, you should declare all of your single-argument constructors except your copy constructors as explicit, such as:

1
2
3
4
5
class MyClass{
public:
explict MyClass(int x): someFiled(x){...}
MyClass(const MyClass & rhs): someFiled(rhs.someFiled){...}
};