Lab 5

You might also like

Download as pdf or txt
Download as pdf or txt
You are on page 1of 13

OOP201 Object Lifetime

Introduction
In this labsheet, we look at special kinds of member functions called constructors and destructors. We also look at the mechanisms that C++ provides for dynamic allocation of objects.

1. Lifetime
Objects, and variables in general, have a lifetime. The time for which they are 'alive' or existing in a meaningful sense. The lifetime of an object is from the point at which it is created to the point that is officially destroyed. Have a look at the following piece of code:
{ int a; { Fraction f; int b; } int c; }

If you imagine the computer running this program, then space for the variable a is provided. As we move further down the program, space for the Fraction f is created, and then space for the variable b is created. When we reach the first closing curly brace, b and f are destroyed, and in that order. Space is then allocated for the variable c. At the second closing curly brace, c is destroyed, and then a is destroyed.

2. Starting Point
Lets start from the following code. Copy and paste it. #include <iostream> using namespace std; int gcd(int n, int m) { while(m > 0) { int temp = m; m = n % m; n = temp; } return n; } class Fraction { public: void SetValues(int num, int den); Fraction Add(Fraction f); void Print(); private: int numerator; int denominator; }; void Fraction::SetValues(int num, int den) { numerator = num / gcd(num, den); denominator = den / gcd(num, den); } Fraction Fraction::Add(Fraction f) { int n = numerator * f.denominator + denominator * f.numerator; int d = denominator * f.denominator; Fraction result; result.SetValues(n, d);

return result; }

void Fraction::Print() { cout << numerator << '/' << denominator << endl; } int main(int argc, char* argv[]) { Fraction f1; f1.SetValues(3, 8); f1.Print(); return 0; }

3. Constructors
A constructor is a special member function. It is special because it is run when an object is instantiated (created). Is identified by its name by the compiler. A constructor always has the same name as the class. Does not return anything, not even void If a programmer did not initialize a fraction, then it contains rubbish, just like a built in type. By providing a constructor, we can guarantee that an object will have a certain state at construction. This is stronger than the built in types! Using the starting point code, create another Fraction object and print out its values. Do this without initializing. We want to see what rubbish is printed! When I tried it I had numbers with a very large magnitude. So, we know that by creating a Fraction and not giving it values, its state is meaningless.

3.1 Default Constructor


Is provided by the compiler, if you dont provide any constructors yourself. Is a constructor that takes no arguments We are going to write our first constructor. The job of the constructor is to ensure that the object is in a consistent state once it has been constructed.

Add the following member function declaration to the class Fraction:


Fraction();

There is NO return type for a constructor. Not even void. Constructors never return a value. It's impossible. Hence, C++ makes our lives and the compiler's life easier by saying that they are NOT permitted to have a return type. Notice that I do have parentheses at the end of the function name. Now add the constructor's definition:
Fraction::Fraction() { numerator = 0; denominator = 1; }

Now, compile your program and run it. What do you see printed this time? As if by magic, the Fraction object that contained rubbish before now contains a sensible default value 0 for numerator and 1 for denominator.

3.2 Other Constructors


The default constructor is nice enough, but the fact that we have a SetValues function shows that we might want to construct a Fraction in at least one other way - by providing a numerator and a denominator. In fact, we might want to construct fractions that are integers as well. We could, of course, use the SetValues function for this. However, that would be a two stage process for the user - creating the object and setting values for it. We can make the user's life easier if we make it only a single stage! (by using a constructor). Constructors, like other member functions, can be overloaded. This means that we can have multiple constructors; all with the same name (they must have the same name if they're constructors!). For them to be overloaded, they need to have different parameter lists. Add the following declarations to the Fraction class:
Fraction(int num); Fraction(int num, int denom);

Now add the following function definitions:

Fraction::Fraction(int num) { numerator = num; denominator = 1; } Fraction::Fraction(int num, int denom) { numerator = num; denominator = denom; }

Now change the code in your main so that it looks like this:
Fraction f1; Fraction f2(10); Fraction f3(27, 99); f1.Print(); f2.Print(); f3.Print();

Compile the program and run it. We now have the ability to create Fractions in three different ways. If we do not provide a value, then the default constructor is used (as with f1). If we provide a single value in parentheses, then it will call the constructor that takes a single argument. With two, such as f3 the constructor taking two arguments is called. Note that when you construct something using the default constructor, you never put empty parentheses after the object name. So, for instance, Fraction f1(); is not correct. Q: Why must we not use the parentheses when default constructing an object? Why is it okay to do so when using one of the other constructors? You can find the answer in the FAQ book on Moodle page for OOP.

Alter the definition of the two argument constructor so that it looks like this:
Fraction::Fraction(int num, int denom) { numerator = num / gcd(num, denom);

denominator = denom / gcd(num, denom); }

Now modify the Add function so that it looks like this:


Fraction Fraction::Add(Fraction f) { return Fraction((numerator * f.denominator) + (f.numerator * denominator), denominator * f.denominator); }

Now add some code inside main to add two fractions and print the result. The Add function returns a Fraction. In the implementation we can see that just after the return keyword, the constructor for a Fraction object is being called (it's the two argument form). C++ allows you to create temporary objects this way. It's neater than declaring a variable that is only used as a result. The temporary Fraction object is built with a numerator and denominator calculated from the numerator and denominator from the current object (object on which Add was called), and f. This is the same calculation as you saw before. However, instead of calculating the values and holding them in separate variables, the results are being used immediately in a constructor call.

4. Destructors
Sometimes we need to know when an object is being destroyed. For some objects, some kind of 'clean-up' is required. This prevents memory leaks. What is memory leak? Google it. destructor Is the name given to the function that is run when the object is destroyed. There is only one destructor per class. Returns nothing (not even void) and is not permitted to take any arguments. Its name is always the name of the class with a tilde (~) prepended. Add the following declaration to Fraction:
~Fraction();

Add the following definition to your file:

Fraction::~Fraction() { cout << "Fraction with values " << numerator << " and " << denominator << " is being destroyed" << endl; }

The destructor has been designed to create output when an object of type Fraction is destroyed (when it goes out of scope). Compile and run the program.

Add cout statements to all three of the constructors. You will find it helpful if they also print out the values used to construct the fraction, and the number of arguments. In your main, enclose all the lines of code excepting return 0; using the opening and curly braces; Run your program and count the number of constructor calls (count the lines from the output), and the number of destructor calls. Do they match? Show your output to the lab technician.

5. Copy Constructors
The counts from the previous exercise were not equal (or at least they should not have been equal!). We saw constructors for creating new objects from nothing, from a single integer, and from two integers. There is another way that an object can be created - by copying from an existing object. Constructing an object from an existing object is called copy construction. The copy constructor does it. If you do not provide a copy constructor, then the compiler synthesizes one for you automatically. This is like the way it synthesizes a default constructor for you if you do not provide any constructors of your own. Copy constructors have a certain format. They are constructors so they never return a value, and they must have the same name as the class. The argument they take must be a reference to an object of the same type. Normally, this reference is const. This indicates that the copying process is not going to modify the thing being copied. We are now going to add a copy constructor that does the same as the one synthesized by the compiler. In addition, it will print out a message. Add the following declaration to the Fraction class.

Fraction(const Fraction& f);

Add the following function definition:


Fraction::Fraction(const Fraction& f) { numerator = f.numerator; denominator = f.denominator; cout << Copy constructor called. << endl; }

Compile and run your program. Are the number of constructions and destructions equal? 1. How do you explicitly call a copy constructor? 2. Is there a time that the copy constructor is called implicitly? Have a look at when your copy constructor is called in the example code.

6. Constructor Syntax
Now that we have seen constructors, and the constructor syntax, it's time to bring our initializations into the C++ world. The builtin types have copy constructors. Look at the following lines of code.
int x = 5; int y(5);

The variable x is initialized with the value 5. The variable y is also initialized with the value 5. The constructor method is the one preferred by many programmers. It is preferred because the = sign means assignment. In the code above, it is not assignment. In fact, if you have the following code,
Fraction f1(1, 2); Fraction f2 = f1;

then the compiler will read the second line as Fraction f2(f1);. It will call the copy constructor. Given this, from this point on, we should initialize all of our variables using the construction syntax. The = is retained for compatibility with C.

7. Initialization Lists
Initialization lists are the 'correct' way of initializing member variables in a constructor. Sometimes, it is the only way of initializing member variables, for instance if they do not have default constructors. The following piece of code shows how we should change the implementation of the Fraction default constructor to use an initialization instead:
Fraction::Fraction(): numerator(0), denominator(1) { cout << "Default constructor 0/1" << endl; }

You can see a new bit of C++ syntax. The colon (:) introduces the initialization list. It is a comma separated list and it must appear before the opening curly brace. Only constructors are permitted to use initialization lists. The following describes how to read it. Before the code within the curly brace is run, the numerator of the current object is initialized (through copy construction) with the value 0. The denominator is initialized with the value 1. Then the code in the curly braces can run. If you do not initialize a variable in an initialization list, then it is initialized using its default constructor if it is a class type, or left alone if it is a builtin type. Hence, it is normally more efficient to use the initialization list and it is almost never less efficient to do so. More important is the fact that if your object contains an object for which there is not default constructor, then it must be initialized in its initialization list.

Modify the other constructors so that they use the initialisation list. Show your output to the lab technician.

8. static data members and static member functions


Normal (non-static) member variables are created for an object and destroyed with the object. If there are no objects of that class created, then there are no normal member variables created. A static member variable always exists, and there is only one copy of it for all objects of a particular class. We are going to help our view of the constructor/destructor calls by including a count.

Add the following declaration to the class (in the public section - we will move it to private shortly):
Static int count;

This is like a member variable declaration as we have seen before but with the addition of the static keyword. Static member variables require a definition. This can be provided, a bit like member function definitions, as follows:
int Fraction::count(0);

This must go outside of the curly braces of Fraction. Notice that the keyword static is not used again. The easiest way to interpret is that it looks like a global function with a long name. It is constructed to have the value zero. Why? Global variables are initialized to zero. Remember? Now, change each of your constructors so that it adds one to count. Change your destructor so that it subtracts one from count. Change your main function so that it looks like this (copy and paste is definitely the best option here!):
cout << "A: # Objects = " << Fraction::count << endl; Fraction f1; cout << "B: # Objects = " << f1.count << endl; { cout << "C: # Objects = " << Fraction::count << endl; Fraction f2; cout << "D: # Objects = " << Fraction::count << endl; Fraction a1[10]; cout << "E: # Objects = " << a1[2].count << endl; } cout << "F: # Objects = " << Fraction* p1(new Fraction(2, cout << "G: # Objects = " << delete p1; cout << "H: # Objects = " << Fraction::count << endl; 3)); Fraction::count << endl; Fraction::count << endl;

Compile and run your program. Look at the counts for objects (they should start at zero and end at one - the final destructor is called after the last cout. Static variables behave differently to member variables. Their lifetimes do not coincide with the lifetimes of objects.

Public data is normally considered a bad idea. Move the declaration of count to private. We need to access the value of the variable so we need a member function of some kind. So, write a static member function called GetCount that returns an integer. It is the same as other functions you know, but the key word static is prepended. Static member functions can be accessed without using an object of the corresponding class type. They can be used to access and use only static data members. Compile and run your modified code. Error! Change the code in main so that they use the static member function we just added. Remember that private data members can be accessed outside their class using only member functions.

9. Memory Management with new and delete


In C, we had the functions malloc, calloc, and free. In C++, the equivalents are new (for creating objects dynamically) and delete for destroying objects that have been created dynamically. Unless there is a very good reason, we always use new and delete rather than the C functions. new is guaranteed to call the constructor for the object, and delete is guaranteed to call the destructor. The most commonly used mechanism for storing a large number of variables or objects is the array. The statement Fraction f[100]; ***

reserves memory for 100 fraction objects. Arrays are a useful approach to data storage, but they have a serious drawback: You must know at the time you write the program how large the array will be. You cant wait until the program is running to specify the array size. The following approach wont work: cin >> size; // get size from user Fraction f[size]; // error; array size must be a constant The compiler requires the array size to be a constant. Unfortunately, in many situations you dont know how much memory you need until runtime. You might want to let the user enter data for a number of Fraction objects, for example, and you cant predict how many such objects the user might want to enter.

*** More discussion on arrays coming soon.

The new Operator


C++ provides another approach to obtaining blocks of memory: the new operator. This operator allocates memory of a specific size from the operating system and returns a pointer to its starting point. The following code fragment shows how new might be used to obtain memory for a 100 fraction objects. Fraction* fptr; fptr = new Fraction[100]; // other statements delete[] fptr; // release fptr's memory In the expression fptr = new Fraction[100]; the keyword new is followed by the type of the variables to be allocated and then, in brackets, the number of such variables. Here Im allocating variables of type Fraction and I need 100 of them, The new operator returns a pointer that points to the beginning of the section of memory. The new operator obtains memory dynamically; that is, while the program is running. This memory is allocated from an area called the heap (or sometimes the free store). C programmers will recognize that new plays a role similar to the malloc() family of library functions. However, the new approach is far superior. When you use new with objects, it not only allocates memory for the object, it also creates the object in the sense of invoking the objects constructor. This guarantees that the object is correctly initialized, which is vital for avoiding programming errors. Also, new returns a pointer to the appropriate data type, whereas malloc()s pointer must be cast to the appropriate type. You should always use new for objects, never malloc().

The delete Operator


If your program reserves many chunks of memory using new, eventually all the available memory will be reserved and the system will crash. Now, do you want that to happen? To ensure safe and efficient use of memory, the new operator is matched by a corresponding delete operator that releases the memory back to the operating system. In the code shown above, the statement delete[] fptr; returns to the system whatever memory was pointed to by fptr. Actually, in this example there is no need for delete, because memory is automatically released when the program terminates. However, suppose you use new in a function. If the function uses a local variable as a pointer to newly acquired memory, then when the function terminates, the pointer will be destroyed but the memory will continue to be owned by the program. The memory will become an orphan, taking

up space that is forever inaccessible. Thus, it is always good practice to delete memory when youre through with it. Deleting the memory doesnt delete the pointer that points to it (fptr in this example) and doesnt change the address value in the pointer. However, this address is no longer valid; the memory it points to may be changed to something entirely different. Be careful that you dont use pointers to memory that has been deleted. The brackets following delete in the example indicate that Im deleting an array. If you create a single variable with new, you dont need the brackets when you delete it: fptr = new Fraction; // allocate a single Fraction object . . . delete fptr; // no brackets following delete However, dont forget the brackets when deleting arrays of objects. Using them ensures that all the members of the array are deleted and that the destructor is called for each one. If you forget the brackets, only the first element of the array will be deleted.

1. Create three fractions dynamically using the new operator, compile and run your program. Hopefully you will see the constructor messages for them, but not the destructor messages (unless you called delete). Show your output to the lab technician. 2. Add delete statements to you program to remove the objects. Compile and run and check that constructor calls match destructor calls. Show your output to the lab technician.

Cheers, Aklil Z.

You might also like