Previous Page TOC Next Page



- 20 -
Making and Breaking Classes



What You'll Learn


There are a number of special events in the life-cycle of a class, the most important ones being the creation of a class object and the deletion of a class object. These events are so important that Visual C++ provides some special functions to help.

Making a Class: The Default Constructor




Whenever a class is made, a special function called the constructor is called.

When you declare a variable, it is a good practice to initialize its value so that if you do not set it again in the program, the rogue value it has does not cause problems later. The same is true of variables inside a class. When a new class object is declared, it is best if all the members of the class are set up correctly. You could write a function, Initialize(), which would set up the correct values. However, you are now depending on the programmer remembering to write the correct code. If the Initialize() function is not called, the class code might not work properly.

Visual C++ can call a function automatically when a class object is made, either by declaration or dynamically by calling new. This function is called the constructor. So that Visual C++ and the programmer know which member function is meant to be called, there is a special convention for constructor functions: They are called exactly the same name as the class, and they have no return type. Look at the following example:


include <iostream.h>

class Example

  {

    public:

      Example();

};

Example::Example()

  {

    cout << "An example has been made!";

  }

void main()

  {

    Example e;

  }

This would output the following:


An example has been made!

It will seem very strange that a program with only a declaration can output data. A constructor is just like any function except that it is called automatically. Normally, you write code to ensure that values are correctly set and any memory required is allocated. Typically, you set pointers to zero if you are not going to use them immediately. Then, in functions called later, you can check to see whether any memory has been allocated.

Definition

When you declare a constructor with no parameters, it is called the default constructor.

Breaking a Class: The Destructor




Visual C++ calls another special member function, the destructor, when an object is destroyed.

If you think about the typical sequence of events in the simple programs you've seen so far, they consist of main functions that do the following:

Sometimes the tidy up step is not necessary, or rather Visual C++ does it for us. The same three steps are required for a class. You have seen how to initialize. You have also seen how to do work, by calling member functions. Now you need to see how to tidy up. Visual C++ automatically calls another function just before an object is destroyed. This function is called a destructor. It is declared almost exactly like the default constructor, except you place a ~ (tilde) in front of the class name. (There is no magic here. Visual C++ simply looks in the class definition for this name when looking to see whether a destructor has been provided.) Again, a destructor has no return value, and it receives no parameters.


include <iostream.h>

class Example

  {

    public:

      Example();

      ~Example();

};

Example::Example()

  {

    cout << "An example has been made!" << endl;

  }

Example::~Example()

  {

    cout << "An example has been destroyed!" << endl;

  }

void main()

  {

    Example e;

    cout << "The program does some work" << endl;

  }

This code would output the following:


An example has been made!

The program does some work

An example has been destroyed!

Now this is getting extremely confusing. You can see the line of code that makes the example object and the place where main outputs its line, but where does An example has been destroyed! come from?

Remember, way back in the early units, you learned that declared data items have a lifetime of the block they are declared in. At the end of a block, Visual C++ looks for all the data items that are no longer required. For each data item, it looks to its class definition and checks whether there is a destructor. If there is a destructor, it will be called.

You should never directly call a constructor or destructor yourself. If you have code that you want to happen at construction time and at some other time, extract it into a separate member function and call it from the constructor and anywhere else.

The main importance of destructors is in deallocating dynamic memory. Destructors are normally very simple pieces of code because the class will no longer exist after the destructor is called. However, sometimes you invent a class in which other pieces of code expect the object to be present. The destructor is an opportunity to sort out objects that are linked together without relying on the programmer remembering to call the code.

A very common use of destructors is to tidy up dynamically allocated strings. In Listing 20.1, the program shows how constructors and destructors cooperate to correctly manage storage. In the program, you will also put in some debugging code to see when these magic calls occur.

Input Listing 20.1. Displaying construction and destruction of objects.

  1:// File name: CONDEST.CPP

  2:// Shows construction and destruction

  3:// of objects and simple memory management

  4://

  5:#include <iostream.h>

  6:#include <string.h>

  7:

  8:class Name

  9:  {

 10:    public:

 11:      // Constructor

 12:      Name();

 13:      // Destructor

 14:      ~Name();

 15:      void SetName(const char * newName);

 16:      const char * GetName() const;

 17:    private:

 18:      char * name;

 19:  };

 20:void main()

 21:  {

 22:    cout << "-- First line of main() --" << endl;

 23:    Name firstName;

 24:    firstName.SetName("firstName");

 25:    Name * secondName = new Name;

 26:    secondName->SetName("secondName");

 27:    cout << "-- Before block --" << endl;

 28:    // New block

 29:      {

 30:        cout << "-- First line of block --" << endl;

 31:        Name thirdName;

 32:        Name fourthName;

 33:        thirdName.SetName("thirdName");

 34:        cout << "-------------------" << endl;

 35:        cout << "Contents of objects" << endl;

 36:        cout << firstName.GetName() << endl;

 37:        cout << secondName->GetName() << endl;

 38:        cout << thirdName.GetName() << endl;

 39:        cout << fourthName.GetName() << endl;

 40:        cout << "-------------------" << endl ;

 41:        cout << "-- Last line of block --" << endl;

 42:      }  // Block ends - third & fourth name destroyed

 43:    cout << "-- After block --" << endl;

 44:    delete secondName;

 45:    cout << "-- Last line of main() --" << endl;

 46:  }      // firstName goes;

 47://*********************************************************

 48://

 49:// Name  class function definitions

 50://

 51:

 52:// Constructor

 53:Name::Name()

 54:  {

 55:    cout << "Constructor called" << endl;

 56:    name = 0;

 57:  }

 58:

 59:// Destructor

 60:Name::~Name()

 61:  {

 62:    cout << "Destructor called ";

 63:    cout << "name is " << GetName() << endl;

 64:    delete [] name; // Delete on zero pointer is safe

 65:  }

 66:

 67:// Member function to store a name

 68://

 69:void Name::SetName(const char* newName)

 70:  {

 71:     // First, remove any name that might already exist

 72:     // Use zero pointer to inidicate no name stored

 73:     // C++ will not destroy storage on a zero pointer

 74:     // "if (name)"

 75:     delete [] name;

 76:     // Create new storage

 77:     name = new char[strlen(newName) + 1]; // add 1 for

 78:                                           // terminator

 79:     strcpy(name,newName);  // Copy data into new name

 80:  }

 81:

 82:// Member function to get the stored name

 83:// Coded to always return a safe value

 84:const char * Name::GetName() const

 85:  {

 86:    if (name)

 87:      return name;

 88:    else

 89:      return "No name exists";

 90:  }

Output


-- First line of main() --

Constructor called

Constructor called

-- Before block --

-- First line of block --

Constructor called

Constructor called

-------------------

Contents of objects

firstName

secondName

thirdName

No name exists

-------------------

-- Last line of block --

Destructor called name is No name exists

Destructor called name is thirdName

-- After block --

Destructor called name is secondName

-- Last line of main() --

Destructor called name is firstName

Analysis

This skeleton program lets you see the timing of the creation and deletion of objects. By the way, there is nothing special about the timing of the class constructor and destructor. The same applies to standard C++ data types such as char or int.

The simple class Name manages a single dynamically allocated string. It allows the string to be set using SetName() (line 15) and retrieved using GetName() (line 16). The actual method of holding the string is hidden to the outside world. (When using a class that has been created for you, you should imagine that you can't see anything marked protected and private.)

When an instance of Name is created, the constructor (line 12 and lines 53 through 57) initializes the name member to zero in line 56. Zero is an important value for a pointer. delete can be called on a zero pointer and Visual C++ knows that there is no storage to be deleted. It is the safest value to set a pointer to when not in use. Also in line 55, the constructor outputs some text to trace when it is called.

When an instance of Name is destroyed—either by delete or by going out of scope—its destructor (lines 60 through 65) is called. The destructor only deletes the name member, remembering that name might not have been set to point to some dynamic memory, so it should then be zero. Again, the destructor identifies when it has been called by outputting some text.

SetName(), in lines 69 through 80, allocates storage just sufficient to hold the name provided. Before it places the new name, it deletes name in case a name has already been stored. It then tests the size of the supplied string and allocates enough memory for the string and the string terminator. Finally, it copies the input string into its newly created area of storage.

GetName() demonstrates an advantage of encapsulation. It always returns a valid string, even if it has not stored a name. In line 86, it tests name to see whether a value has been stored. If something has been stored, the value is returned. If no value has been stored, GetName() returns an error string. This means that the class can be used, and code using it can never retrieve an invalid string. This relies on the constructor having set an initial value and the GetName() function having the extra code in place. Furthermore, because SetString() cleverly checks the length of the string that is passed to it, it can never overwrite another variable's storage as might happen with a simple character array.

The main program simply declares and dynamically allocates some objects. Look at the output. Notice that the constructor gets called at exactly the line in the code that the declaration of the object takes place (lines 23, 25, 31, and 32). Then look at the destructor. If delete is used, the destruction takes place at that point (line 44). If the object is locally declared (firstName, thirdName, and fourthName), the destructor takes place at the end of the block (lines 42 and 46). In the case of the main block, the destructor call is after the last line of code (line 46). In the inner block, the destruction takes place after the last line of code of the inner block (line 42) and before the next line of code in main() (line 43).

Other Constructors




Visual C++ allows constructors to have parameters for objects that have to be supplied with information before they can be used.

Default constructors are often used in Visual C++, but quite often it is important to be able to set an initial value. A constructor can have any parameter list that other functions have. In the case of the Name class from Listing 20.1, it is useful to be able to set an initial string value:


class Name

  {

    public:

     Name();                  // default constructor

     Name(const char * name); // char * constructor

    ... and so on

  };

In the code, there is no need to have separate lines to make and then initialize the Name variables:


Name name1("Paddy McGinty"); // Explicit construction

Name name2;                  // Use default constructor

name2.SetName("and his goat");

It is also quite all right to use default parameters:


class RetirementAge

  {

    public:

      RetirementAge(int age = 65);

      int retirementAge;

  };

void main()

  {

    RetirementAge ra;

    cout << ra.retirementAge;

    // prints 65

  }

Recall that this is called function overloading. You can overload member functions in the same way as global functions. There is a trap to be wary of, though. If no constructor is declared for a class, Visual C++ invents one for itself (it does nothing). If you declare a constructor yourself, Visual C++ doesn't invent a default (parameterless) constructor, as in the following example:


class A

  {

    public:

      int a;

  };

class B

  {

    public:

      B(int bb);

      int b;

  };

The declaration A a; would be valid. The default constructor that Visual C++ creates would be used. The declaration B b(5); is also valid, but B b; would be invalid because there is already an explicitly defined constructor for B and no default parameterless constructor has been declared. A constructor with parameters for which all can be defaulted can be used as the default.

It is tempting to explicitly define that you want to call the default constructor by using parentheses. This is wrong because C++ thinks you are declaring a function with no parameters and a return type of the class:


A a;     // calls the default constructor

A a();   // Wrong! declares a parameterless function called a

         // with a return type A

A a(1);  // OK, calls constructor taking an int

Although you can create constructors with any parameter lists you like, you can never have parameters for a destructor. In other words, there are many ways of making an object, but there is always only one way of deleting an object.

Here's a final note: In Visual C++, it is useful to remember that the language designers wanted user-defined classes to be just like the built-in data types such as int and char. These built-in types have a few extra goodies, but generally the designers succeeded. Until now, you might not have realized that in defining an int or a char, you are using the same idea of constructors and destructors that you see in classes. Here are constructors that you can call for an int and a float:

int i(5);

float f(98.7F);


Copying a Class



A special form of constructor takes another object of the class as a parameter to allow it to be copied.

A special case of constructor is called the copy constructor. This special constructor always looks like this:


ClassName(const ClassName& name)

This is a constructor that takes a reference to the class object itself as a parameter. This constructor allows an object to be copied to another object of the same type at the time of construction.

Let's look at why you would want such a thing. The easiest way to understand this is to look at another program that shows the automatic calls that C++ does. Listing 20.2 shows that Visual C++ will use a copy constructor not only when you ask it to, but also when you do a pass by value call to a function with a class object as a parameter.

Input Listing 20.2. Visual C++ sneaks in an extra call or two.

  1:// File name: NAMENAME.CPP

  2:// Demonstration of implicit use of constructors

  3:// by Visual C++

  4:

  5:#include <iostream.h>

  6:#include <string.h>

  7:

  8://

  9:// Name - a trivial class

 10://

 11:class Name

 12:  {

 13:    public:

 14:      // Constructors

 15:      Name();                     // Default

 16:      Name(const Name& n);        // Copy

 17:      Name(const char * newName); // Normal

 18:      // Destructor

 19:      ~Name();

 20:      // Access function

 21:      const char * GetName() const;

 22:    private:

 23:      // Data member

 24:      char * name;

 25:  };

 26:

 27:// Default constructor - ensure name is initialized

 28:Name::Name()

 29:  {

 30:    name = 0;

 31:    cout << "Default constructor used" << endl;

 32:  }

 33:

 34:// Copy constructor - ensure string is duplicated

 35:Name::Name(const Name& n)

 36:  {

 37:    if (n.name)

 38:      {

 39:         name = new char[strlen(n.name) + 1];

 40:         strcpy(name,n.name);

 41:      }

 42:    else

 43:      name = 0;

 44:    cout << "Copy constructor used - "

 45:         << (name != 0?name : "") << endl;

 46:  }

 47:

 48:// Make a name for myself

 49:Name::Name(const char * newName)

 50:  {

 51:    if (newName)

 52:      {

 53:         name = new char[strlen(newName) + 1];

 54:         strcpy(name,newName);

 55:      }

 56:    else

 57:      name = 0;

 58:    cout << "const char* constructor used - "

 59:         << (name != 0?name : "") << endl;

 60:  }

 61:

 62:// Destructor

 63:Name::~Name()

 64:  {

 65:    cout << "Destructor - " << (name != 0?name : "") << endl;

 66:    delete name;

 67:  }

 68:

 69:// Provide access

 70:const char * Name::GetName() const

 71:  {

 72:    return name;

 73:  }

 74:

 75:// Global function with pass by value

 76:void PrintName(Name n)

 77:  {

 78:    cout << "In PrintName  - " << n.GetName() << endl;

 79:  }

 80:

 81:// main() function to excercise the class

 82:void main()

 83:  {

 84:    cout << "-- Start of main() --" << endl;

 85:    Name n("Norman Lamont");

 86:

 87:    cout << "-- Before PrintName --" << endl;

 88:    PrintName(n);

 89:

 90:    cout << "-- Before n1 declaration --" << endl;

 91:    Name n1(n);

 92:

 93:    cout << "-- Before n2 declaration --" << endl;

 94:    Name n2;

 95:

 96:    cout << "-- Before n2 = n1 --" << endl;

 97:    n2 = n1; // Unsafe !!!

 98:

 99:    cout << "-- End of main() --" << endl;

100:  }

Output


-- Start of main() --

const char* constructor used - Norman Lamont

-- Before PrintName --

Copy constructor used -

In PrintName  - Norman Lamont

Destructor - Norman Lamont

-- Before n1 declaration --

Copy constructor used - Norman Lamont

-- Before n2 declaration --

Default constructor used

-- Before n2 = n1 --

-- End of main() --

Destructor - Norman Lamont

Destructor - ¬ `5t= Lamont

Destructor - Norman Lamont

Analysis

In this case, to let you follow the class more easily, all the class members are grouped at the top.

The class declares three different constructors in lines 15 to 17. The constructor in line 17 is just used to let the class have some value of interest. In line 15 the default constructor is declared, and in line 16 the const Name& parameter tells C++ that this is a copy constructor.

The class needs a copy constructor to duplicate the class because otherwise the pointers will get mixed up (as we will see!). To properly copy a pointer member, it is necessary to duplicate the data pointed to by the pointer, or both pointers will point to the same object. Then, at destruction time, C++ will have two separate dynamic objects owned by the pointers so that C++ will not try to delete the same dynamic storage twice.

The difficult thing to follow here is the code for the copy constructor itself, coded in lines 35 through 46. How does this function tell the two Names apart? The Name that is being made requires no qualification of its member variables. The Name that is the copy parameter needs the parameter variable to access the class members to be copied. The name accessed in line 37 belongs to the Name to be copied. There is something fishy here! Wasn't name private? It was, but here is code accessing a private member from outside a member function. Well, that isn't the case. A class member function has access to all the members of a class, not just all the members of the current object. The restriction is that the class object does not have any automatic means of finding other objects of the same class, so normally the members are safe from getting mixed up.

Follow the output as you step through the main() code of lines 82 through 100. The variable n is initialized using the special const char * constructor, which means that it can be directly initialized with a character literal. The output shows that the appropriate constructor is called. The next task is to call the global routine PrintName() in line 88. PrintName takes a single Name parameter passed by value. Recall that this means C++ must take a copy of the variable to stop the called argument from being accidentally changed. Looking to the output, see that the copy constructor is called.

It is vital to understand that if you do not explicitly declare a copy constructor, Visual C++ will invent one for itself. It does this by copying each individual data member, but it treats a pointer as a data item and does not duplicate the data pointed to. You can stop the compiler from accidentally using the copy constructor by declaring one as a private member function (you do not have to write the body of this dummy function).

Consider what would have happened if a copy constructor had not been declared. Visual C++, trying to be helpful, would automatically try to copy the Name class, but it would then simply copy the pointer and not the data. At the end of the function (line 79), the destructor would be called and delete the character array at the end of the name pointer of the temporary variable. Unfortunately, that data also belonged to the original argument n. Fortunately, we have coded the copy constructor, so it has copied the string itself, and the copy gets deleted. The important lesson is that the copy constructor is called by C++ itself when passing a class by value. There are other times that C++ will call it, too. To be safe with C++, you should always declare a copy constructor when your class can't simply be copied by copying each data member individually.

Look for the output -- Before n1 declaration --. The next action is to copy the variable n into the new variable n1 in line 91. This time, the copy constructor is explicitly called. In line 94, n2 is declared. (Remember, no parentheses for the default constructor.)

In line 97, there is a seemingly harmless line of code. Visual C++ kindly makes an assignment operator for our class. It is important to know that an assignment is not the same as a copy constructor. Look at the output. Although it seems like you should be copying the class, the copy constructor is not used. There is a good reason that C++ does not use the copy constructor: The copy constructor is only for new class instances with no data in them. What would a constructor do with existing data owned by the name pointer? In the next lesson, you will see how to fix this problem. Because of this error, at the end of the program (line 100) when all the destructors are called, n2 and n1 both have a pointer to the same piece of memory. n2 is deleted first, and n1 is deleted second. The output shows some gobbledygook, but in fact you are lucky the program worked at all. It could have completely failed. Variable n survives due to n1 having been properly copied.

This is a difficult business! Don't be disheartened because you do not follow this at the moment. I hope you will at least remember that this chapter is here. When you gain more experience, you will remember that odd things happen with classes and refer back here. It's better than being surprised later on.

Classes Can Be Members Of Other Classes




Classes can contain other classes. Class data types can be used in exactly the same way as the standard Visual C++ data types.

The examples used so far have been quite simple. One of the design aims of Visual C++ was to allow programmers to define their own types that work just like the built-in data types such as int and char. In the previous example, you saw a hint that Visual C++ even lets you make a class use operators (even though the example used the assignment operator wrongly in line 97). Before you go on to these exciting features of Visual C++, it is worth reviewing some of the simple things you can do with classes.

Having invented the Name class, you are starting to have a useful string handling class. (I should warn you that 99.5 percent of all C++ books end up doing examples on string handling classes!) You can now easily create a string, copy a string, and set a string to a new value with a function call. This is so useful that you could add it into the contact program you wrote earlier:


class Contact

  {

    public:

      Contact();          // Default

      Contact(const char * Name, const char * Phone, int Age);

      // Other functions here

     private:

      Name name;

      Name phoneNo;  // must rename the class to something better!

      int  age;

  };

Then, in the constructor, you can initialize all the variables:


Contact::Contact(const char * Name, const char * Phone, int Age)

  {

    name.SetName(Name);

    name.SetName(Phone);

    age = Age;

  }

Something is not quite right here. Really, you should call the constructor to make sure that the values are set up properly from the very beginning. You can't simply call the constructor, because the declaration has already been written in the class declaration. C++ does not let you put initialization into the class declaration. Instead, there is a special way of initializing class members: the initialization list. This is simply a list of constructors called for each member that you want to initialize. The list follows the parameters and lives just before the opening brace, with a colon (:) thrown in for good measure. This list can see all the parameters of the constructor that the list is associated with. You don't have to use parameters; they could be set to fixed values. The initialization list finds which variable it is constructing by matching the member name.


Contact::Contact(const char * Name, const char * Phone, int Age)

         : name(Name),phoneNo(Phone),age(Age)

  {

  }

In this case, the initialization list was all you needed, but you still have to put in the function braces just so Visual C++ knows what it is looking at. Recall that standard data types had constructors. This shows why it is useful to know that the standard data types also have constructors.

One of the very useful things about initialization lists is that they can be used to set constant and reference variables in a class. It is the only way that they can be set. After the constants or references have been made, they can't be assigned to. The initialization list is in a special place (just before the constructor begins), which allows constant members to be defined before any class code can use them.

Homework



General Knowledge


  1. Which function automatically gets called when a class object is deleted?

  2. What is the name of the function that gets called when a class object is declared but not initialized?

  3. How many constructors can be declared for a class?

  4. How many destructors can be declared for a class?

  5. For class Excellent, give the member function of the default constructor.

  6. For class Way, give the member function of the destructor.

  7. Declare a copy constructor for class WeAreNotWorthy.

  8. Declare a single constructor for class String that can be a default constructor and can be used to initialize the class with a constant character string.

  9. Name three functions that Visual C++ can provide automatically for a class.

  10. True or false: A constructor must be defined in a class declaration to make an instance of a class.

  11. True or false: You can only have a single constructor in a class.

  12. True or false: You can only have a single destructor in a class.

  13. True or false: The copy constructor and the assignment operator are the same thing.

  14. True or false: A member constant must be initialized in a class initialization list.

    What's the Output?


    Given the following class, answer the subsequent questions:

    
    class A
    
      {
    
        public:
    
          A();
    
          ~A();
    
          void Print();
    
      }
    
    A::A()
    
      {
    
        cout << "Constructor called" << endlL
    
      }
    
    A::~A()
    
      {
    
        cout << "Destructor called" << endlL
    
      }
    
    void A::Print()
    
      {
    
        cout << "Print called " << endl;
    
      }

  15. What does the following code produce?

    
    {
    
      A a;
    
      a.Print();
    
    }

  16. What does the following code produce?

    
    {
    
      A* a = new A;
    
      a->Print();
    
      delete a;
    
    }

  17. What does the following code produce?

    
    {
    
      A* a = new A;
    
      a->Print();
    
    }

    Find the Bug


  18. What is wrong with the following code?

    
    class Telephone
    
      {
    
        Telephone();
    
        ~Telephone();
    
        private:
    
         char number[15];
    
      };
    
    void main()
    
    {
    
    
    Telephone t; cout << t.number; }

  19. Why won't this compile?

    
    class A
    
      {
    
        public:
    
          A(int a1);
    
        private:
    
          int a;
    
      };
    
    A::A(int a1)
    
      {
    
        a = a1;
    
      }
    
    a a;

  20. What is missing from the following class declaration?

    
    class String
    
      {
    
        public:
    
          String();
    
          void Assign(const char * s);
    
          const char * GetString() const;
    
        private:
    
          char * string;
    
      };

    Write Code That. . .


  21. Add a member to the Name class of Listing 20.1 that allows the value of the name to be set at the time of construction. This change must still allow the existing code to work.

  22. Add a copy constructor to the Name class of Listing 20.1. Prove that it works by adding a global function call using a Name as a parameter.


    Extra Credit


  23. Write a class to store the titles of the books on your bookshelf, along with their authors and ISBN numbers, minimizing the amount of storage that the class uses. Provide functions to set and display the data. Allow the programmer to directly construct a book, copy a book, or set up a blank book.

  24. If you have not already done so, write a simple string class to allocate just enough storage to hold a string and convert the class you wrote in question 23 to use it for the character array members.

Previous Page Page Top TOC Next Page