next up previous
Next: About this document ...

CSCI.1200 Computer Science II
Summer, 2002
Worksheet 7
More on Classes

Reading: Deitel & Deitel 7.2-7.7

Multiple Constructors

A class is allowed to have more than one constructor function. They all have the same name (the name of the class), but they differ in the types and number of parameters. Here is a brief example:


class Point { // a two-dimensional point
 private:
   double x, y;
 public:
   Point() { x = 0.0; y = 0.0; }       // default constructor
   Point(double first, double second)  // a second constructor
   {
      x = first;
      y = second;
   }
   // ... other functions ...
};

int main()
{
   Point a;          // The default constructor is called for this object
   Point b(.5, .5);  // The second constructor is called
...

The compiler checks when each object is declared to see which constructor should be called by matching up the parameter lists. If it cannot find a match, it will report an error.1

You will usually find that you will need a default constructor (one that takes no parameters) in your programs, whether or not you write additional constructors. One subtle example is this:
Point arrpts[6];
If you do not include a default constructor in your program, this declaration would give a compiler error.

Friends

It often happens that you would like another class or a function which is not a member of a class to have access to the private members of the class. Such a function or a class is called a friend. To make a non-member function a friend of a class, it must be declared as a friend within the class by using the keyword friend followed by the function prototype. Here is a trivial example to demonstrate the concept.


...
class Trivial {
  private:
     int x;
   public:
     Trivial(int n) {x=n;}    // constructor
     int Getx(){return x;}
     friend int ANonMemberFunction(Trivial);
 };

int ANonMemberFunction(Trivial t)
{
   return t.x; // illegal if this function were not a friend
}

int main()
{
   Trivial t(17);
   cout << ANonMemberFunction(t) << endl;
   return 0;
}

The class Trivial has a private member x. Because it is private, the function ANonMemberFunction would not ordinarily be able to access the value of x in its argument; it would have had to use the member function Getx(). However, because this function had been declared as a friend of the class Trivial, it is permitted to access the private members of Trivial such as x. If main() had the following statement:
cout << t.x << endl;
the compiler would have flagged it as an error.

Classes can also be declared as friends of other classes. This permits instances of the friend class to access private members of the class even though they are not members.

Exercise 1: Suppose that a banking program has a class Account which has a private data member double balance. Write a function void transfer(Account &A1, Account &A2, double amount) which transfers amount from A1 to A2 by subtracting amount from the balance of A1 and adding amount to the balance of A2. Here is some code to start you off. It is in /dept/cs/cs2/download/Account.cpp


#include <iostream>
class Account {
 private:
   double balance;
   // other stuff
 public:
   Account() {balance=0.0;}
   void Deposit(double d) {balance += d;}
   void showbalance() {cout << balance;}
   // other stuff

};

void transfer(Account &A1, Account &A2, double amount)
// your code here

int main()
{
   Account accts[4];
   accts[0].Deposit(34.00);
   accts[1].Deposit(57.50);
   transfer(accts[0], accts[1], 10.00) ;
   accts[0].showbalance();
   cout << endl;
   accts[1].showbalance();
   cout << endl;
   return 0;
}

Putting classes in header files

It is customary to put all of the code for a class in a separate file, and then include this file in programs that use it. These files have a .h suffix (for header). The #include statement should enclose the path name of the header file in double quotes rather than the greater than and less than characters. Here is a short example. The first file below is called Point.h, and it defines a class Point.


// Point.h
class Point {
  public:
    Point() {x=y=0.0;}
    Point(double xx, double yy) {
         x = xx;
         y = yy;
    }
    void setx(double xx) {x = xx;}
    void sety(double yy) {y = yy;}
    double getx() { return x;}
    double gety() { return y;}
  private:
    double x, y;
};

Here is a second file, called main.cpp which uses the header file.


#include <iostream>
#include "Point.h"
using namespace std;
int main()
{
    Point p1(4.5, 6.7);
    cout << p1.getx() << endl;
    // yada yada yada
    return 0;
}

To add a .h file to your project, from the Project Menu, choose Add to Project, and then select New (The shortcut keys are Alt-p, a, n). The New window will appear. Choose the Files Tab, and then choose C/C++ Header file. Give the new file a name with the suffix .h and press the OK button.

When you include a header file, it is exactly as if the code in the included file were copied into your program at that point. You can include header files in other header files, and so there is a danger that the same file could be included more than once. This can cause variables to be declared twice which would result in compiler errors.

To avoid this, any header file that you write should contain preprocessor directives requesting conditional compilation. Here is the prototype for this:

#ifndef _VARNAME_
#define _VARNAME_
your code
#endif

Preprocessor directives start with a # in column 1. The first line means that if the following variable is not defined, continue processing. This if is terminated by the very last line of the header file #endif. The second line says to define the following variable. Thus, the logic of this code is as follows: If the variable _VARNAME_ is not defined, define it and execute the code, else do nothing. Thus: if the header file is included more than once, the first time that it is included, the variable will not be defined and so the code will be executed. The second time the header file is included, the variable has already been defined, and so the code will not be included again.

The variable name can be anything you wish, but it should be a name that is unlikely to be included in any other code. It is customary, but not required, that the first character be an underline and all of the letters be upper case. All of the standard header files contain code of this type. For example, the file iostream starts off with this line:
#ifndef _IOSTREAM_

The following two lines should be added to the top of the file Point.h:
#ifndef _POINT_
#define _POINT_
and this line should be the last line of the file:
#endif

Pointers to Class Objects

It is possible to create pointers to class objects, and most large programs do so. Pointers to class objects are so common that there is a special operator, ->, to access members of the object pointed to. It is written as the minus character (-) followed by the greater than character (>).

The following two examples behave identically:


#include <iostream>                        #include <iostream>
#include "Point.h"                         #include "Point.h"
using namespace std;                       using namespace std;
int main()                                 int main()
{                                          {
   Point *ptr = new Point(3.45,6.78);         Point *ptr = new Point(3.45,6.78);
   double f = ptr->getx();            ***     double f = (*ptr).getx();
   ptr->setx(23.45);                  ***     (*ptr).setx(23.45);
   cout << ptr->gety() << endl;       ***     cout << (*ptr).gety() << endl;
   Point *ptr2 = new Point;                   Point *ptr2 = new Point; 
   cout << ptr2->getx() << endl;      ***     cout << (*ptr2).getx() << endl;
   // yada yada yada                          // yada yada yada
   return 0;                                  return 0;
}                                          }

The use of the -> is customary and generally easier to read than the combination of dereferencing and dot operators. The arrow operator also works for pointers to struct objects in C.

We can create an array of pointers to Points, allocate memory for them as needed, and deallocate memory when they are no longer needed.


#include <iostream>
using namespace std;
#include "Point.h"

int main()
{
  Point *arr[20];           // an array of 20 pointers to Point objects
  
  arr[0] = new Point(); 
  arr[0]->setx(6.2);
  arr[0]->sety(2.3);
  cout << arr[0]->getx() << endl;

  arr[1] = new Point(3.14, 4.56);
  cout << arr[1]->gety() << endl;

  // once we are finished with the point arr[1], we can
  // free up the memory with delete.

  delete arr[1];
  arr[1] = NULL;
  return 0;
}

This diagram shows the state of memory at the end of the program.


\begin{picture}(200,100)
\put(55,88){arr}
\put(60,70){\framebox (15,15)}
\put(60...
...eted)}}
\par\put(70,63){\vector(1,-1){20}}
\put(91,40){\tiny NULL}
\end{picture}

Destructors

An important concept in object oriented programming is the lifetime of an object. In many programs which run for long periods of time, objects are created, used for a while, and then deleted. When an object is deleted, it is important to free up the memory so that memory leaks don't develop. It is often the case that there are other things that need to be done when an object is deleted. A simple way to handle whatever housekeeping needs to be done is to use a destructor function. This is a function which is automatically called when an object is destroyed. Like a constructor, it has no return type. The name of the destructor function is always a tilde (~) followed by the name of the class (e.g. ~Point()).

If a class has memory which is dynamically allocated by one of its member functions, this memory must be freed when the instance of the class is destroyed, and this is an important function of the destructor. For example, suppose our class Student is defined as follows:


// Student.h
class Student {
 private:
   char *firstname;
   char *lastname;
   // other stuff
 public:
   Student(const char *fname, const char *lname) {
        firstname = new char[strlen(fname) + 1];
        strcpy(firstname,fname);
        lastname = new char[strlen(lname) + 1];
        strcpy(lastname,lname);
   }
   // other stuff
   ~Student() {  // destructor
        delete [] firstname;
        delete [] lastname;
   }
};

// main.cpp
#include "Student.h"
#include <iostream>
using namespace std;
int main()
{
    Student *freshmen[10];
    freshmen[0] = new Student("Mickey", "Mouse");
    // do stuff with Mickey Mouse
    delete freshmen[0];  // destructor is automatically called here
    freshmen[0]=NULL;
    return 0;
}

The size of a Student object is only the size of two pointers (pointers are typically 32 bits, but you hardly ever need to know this), so the statement
freshmen[0] = new Student("Mickey", "Mouse");
only allocates enough memory from the heap for two pointers. However, the object's constructor function is then called, and this initializes the two pointers by allocating additional memory for the first name and the last name. The memory diagram for the new object looks like this:


\begin{picture}(250,60)(-30,10)
\par\put(-30,74){\footnotesize {freshmen}}
\put(...
...ootnotesize allocated}
\put(130,0){\footnotesize character arrays}
\end{picture}

This diagram also illustrates why you need to have a destructor function for the Student class. When a Student object is no longer needed, it is not sufficient to merely delete the memory needed for the class; it is also necessary to delete the memory which had been dynamically allocated for the first name and the last name. If you rely on the implicit (automatic) destructor to delete the Student object, it will only delete the two pointers firstname and lastname. It does not delete the two character arrays pointed to by the pointers. Those arrays therefore become unreachable, resulting in a memory leak. If your program created and deleted many such objects without a proper destructor, over time it would consume all available memory, and it would crash.

Whether you write your own destructor function or use the implicit one provided in the language, the destructor is always called when an object is destroyed. If an object is created dynamically (using new), the destructor is called when you delete the object. If the object is declared inside a function, the destructor is called when that function (even main) exits.

Rule: If you write a class that uses pointers to dynamically allocated memory, you must write your own destructor function for the class to reclaim that memory whenever objects of the class are destroyed.

Exercise 2: Here is some code for a class ClassList. This code is in the files
/dept/cs/cs2/download/Student.h and /dept/cs/cs2/download/ClassList.h


// Student.h
class Student {
 private:
    char Name[32];
    int  Snum;
 public:
    Student() {}
    Student(char *n, int s) {
       strcpy(Name,n);
       Snum = s;
    }
   void setname(char *s) {strcpy(Name,s);}
   void setnum(int n) {Snum = n;}
   char *getname() { return Name;}
   int getnum() { return Snum;}
};

// ClassList.h
class ClassList {
 private:
   Student *Roster[100];
   int num;
 public:
   ClassList() {
       num = 0;
       int i;
       for (i=0;i<100;i++)
           Roster[i]=NULL;
   }
};

Write a public member function of ClassList void insert(Student s) which adds a student to the ClassList. Note that there is room in the ClassList for 100 students, but initially there are no students enrolled. Each time that you insert a student, you must allocate memory for a new student, and copy the student to the new memory (don't forget to increase num by 1).

Also write a public member function of ClassList void printlist() which prints out the name and number of each student in the List.

There is a main to test your code in /dept/cs/cs2/download/classlist.cpp.


#include <iostream>
#include "Student.h"
#include "ClassList.h"
using namespace std;
int main()
{ 
    ClassList TheList;
    Student s;
    char *thenames[] = {"Mickey Mouse", "Donald Duck",
          "Monica Lewinsky", "Bill Clinton"};
    int thenumbers[] = {234, 345, 456, 567};
    for (int i=0;i<4;i++) {
        s.setname(thenames[i]);
        s.setnum(thenumbers[i]);
        TheList.insert(s);
    }
    TheList.printlist();
    return 0;
}



next up previous
Next: About this document ...
Paul Lalli 2002-05-17