Chapter 25

Getting Objects Off to a Good Start

In This Chapter

arrow Creating a constructor

arrow Examining limitations on how constructors are invoked

arrow Reviewing an example constructor

arrow Constructing data members

arrow Introducing the “NOT constructor” — the destructor

Normally an object is initialized when it is created, as in the following:

  double PI = 3.14159;

This is true of class objects as well:

  class Student
{
  public:
    int nHours;
    double dGrade;
};

Student s = {0, 0.0};

However, this is no longer possible when the data elements are declared protected if the function that’s creating the objects is not a friend or member of the class (which, in most cases it would not be — see Chapter 24 for more about these relationships).

Some other mechanism is required to initialize objects when they’re created, and that’s where the constructor comes in.

The Constructor

One approach to initializing objects with protected members would be to create an init() member function that the application could call when the object is created. This init() function would initialize the object to some legal starting point. In fact, that’s exactly what I do in Chapter 24.

This approach would work, but it doesn’t exactly fit the “microwave oven” rules of object-oriented programming (see Chapter 21) because it’s akin to building a microwave oven that requires you to hit the Reset button before you could do anything with it. It’s as if the manufacturer put some big disclaimer in the manual: “DO NOT start any sequence of commands without FIRST depressing the RESET button. Failure to do so may cause the oven to explode and kill everyone in the vicinity or WORSE.” (What could be worse than that?)

Now I’m no lawyer, but even I know that putting a disclaimer like that in your manual is not going to save your butt when you end up in court because someone forgot to hit Reset and got cut with shrapnel from an exploding microwave, even though you say very clearly to hit Reset first.

Fortunately, C++ takes the responsibility for calling the initialization function away from the applications programmer: It calls the function automatically whenever an object is created.

You could call this initialization function anything you want, as long as there is a rule for everyone to follow. (I’m kind of partial to init() myself, but I didn’t get a vote.) The rule is that this initialization function is called a constructor, and it has the same name as the name of the class.

Outfitted with a constructor, the Student class appears as follows:

  class Student
{
  protected:
    int nSemesterHours;
    double dGrade;

  public:
    Student()
    {
        nSemesterHours = 0;
        dGrade = 0.0;
    }

    // ...other public member functions...
};
void fn()
{
    Student s;  // create an object and invoke the
                // constructor on it
}

At the point of the declaration of s, C++ embeds a call to Student::Student().

Notice that the constructor is called once for every object created. Thus the following declaration calls the constructor five times in a row:

  void fn()
{
    Student s[5];
}

It first calls the constructor for s[0], then for s[1], and so forth.

Limitations on constructors

The constructor can only be invoked automatically by C++. You cannot call a constructor as you would a normal member function. That is, you cannot do something like the following:

  void fn()
{
    Student s;

    // ...do stuff...

    // now reinitialize s back to its initial state
    s.Student();    // this doesn't work
}

The constructor is not just any ol’ function.

In addition, the constructor has no return type, not even void. The default constructor has no arguments, either.

tip.eps The next chapter shows you how to declare and use a constructor with arguments.

Finally, the constructor must be declared public, or else you’ll only be able to create objects from within other member functions.

The constructor can call other functions. Thus your constructor could invoke a publicly available init() function that could then be used by anyone to reset the object to its initial state.

Can I see an example?

The following StudentConstructor program looks a lot like the SimpleStudent program from Chapter 24, except that this version includes a constructor that outputs every time it’s creating an object. The interesting part to this program is seeing the cases during which the constructor is invoked.

tip.eps I highly encourage you to single-step this program in the debugger, using the Step-Into debugger command from Chapter 20. Use the Step Into debugger command near the declaration of the Student objects to step into the constructor automatically.

  //
//  StudentConstructor - this program demonstrates the use
//               of a default constructor to initialize
//               objects when they are created
//
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;

class Student
{
  protected:
    double dGrade;         // the student's GPA
    int    nSemesterHours;

  public:
    // constructor - init the student to a legal state
    Student()
    {
        cout << "Constructing a Student object" << endl;
        dGrade = 0.0;
        nSemesterHours = 0;
    }

    // getGrade() - return the current grade
    double getGrade()
    {
        return dGrade;
    }

    // getHours() - get the class hours towards graduation
    int getHours()
    {
        return nSemesterHours;
    }

    // addGrade - add a grade to the GPA and total hours
    double addGrade(double dNewGrade, int nHours)
    {
        double dWtdHrs = dGrade * nSemesterHours;
        dWtdHrs += dNewGrade * nHours;
        nSemesterHours += nHours;
        dGrade = dWtdHrs / nSemesterHours;
        return dGrade;
    }
};

int main(int nNumberofArgs, char* pszArgs[])
{
    // create a student and initialize it
    cout << "Creating the Student s" << endl;
    Student s;

    // add the grades for three classes
    s.addGrade(3.0, 3);  // a B
    s.addGrade(4.0, 3);  // an A
    s.addGrade(2.0, 3);  // a C (average should be a B)

    // now print the results
    cout << "Total # hours = " << s.getHours()
         << ", GPA = " << s.getGrade()
         << endl;

    // create an array of Students
    cout << "Create an array of 5 Students" << endl;
    Student sArray[5];

    // now allocate one off of the heap
    cout << "Allocating a Student from the heap" << endl;
    Student *pS = new Student;

    // wait until user is ready before terminating program
    // to allow the user to see the program results
    cout << "Press Enter to continue..." << endl;
    cin.ignore(10, ' '),
    cin.get();
    return 0;
}

The output from this program appears as follows:

  Creating the Student s
Constructing a Student object
Total # hours = 9, GPA = 3
Create an array of 5 Students
Constructing a Student object
Constructing a Student object
Constructing a Student object
Constructing a Student object
Constructing a Student object
Allocating a Student from the heap
Constructing a Student object
Press Enter to continue …

The Student class has been outfitted with a constructor that not only initializes the number of semester hours and grade-point average to zero, but also outputs a message to the console to announce that a Student object is being created.

The main() program then simply creates Student objects in various ways:

  • The first declaration creates a single Student object s resulting in C++ invoking the constructor.
  • The second declaration creates an array of five Student objects. C++ calls the constructor five times, once for each object in the array.
  • The program allocates a Student object from the heap. C++ invokes the constructor again to initialize the object.

Constructing data members

The data members of a class are created at the same time as the object itself. Consider the following simple class TutorPair that consists of a Student and a Teacher:

  class TutorPair
{
  protected:
    Student s;
    Teacher t;

    int nNumberOfMeetings;

  public:
    TutorPair()
    {
        nNumberOfMeetings = 0;
    }

    // ...other stuff...
};

It’s not the responsibility of the TutorPair class to initialize the member Student or the member Teacher; these objects should be initialized by constructors in their respective classes.

Thus, when a TutorPair is created, C++ does the following (in the order shown):

  • It invokes the constructor for the Student s.
  • It invokes the constructor for the Teacher t.
  • It enters the constructor for TutorPair itself.

technicalstuff.eps The constructors for the data members are invoked in the order in which they appear in the class definition.

The following TutorPairConstructor program demonstrates:

  //
//  TutorPairConstructor - this program demonstrates
//          how data members are constructed automatically
//
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;

class Student
{
  protected:
    double dGrade;         // the student's GPA
    int    nSemesterHours;

  public:
    // constructor - init the student to a legal state
    Student()
    {
        cout << "Constructing a Student object" << endl;
        dGrade = 0.0;
        nSemesterHours = 0;
    }
};

class Teacher
{
  public:
    // constructor - init the student to a legal state
    Teacher()
    {
        cout << "Constructing a Teacher object" << endl;
    }
};

class TutorPair
{
  protected:
    Student s;
    Teacher t;

    int nNumberOfMeetings;

  public:
    TutorPair()
    {
        cout << "Constructing the TutorPair members"
             << endl;
        nNumberOfMeetings = 0;
    }
};

int main(int nNumberofArgs, char* pszArgs[])
{
    // create a TutorPair and initialize it
    cout << "Creating the TutorPair tp" << endl;
    TutorPair tp;

    // wait until user is ready before terminating program
    // to allow the user to see the program results
    cout << "Press Enter to continue..." << endl;
    cin.ignore(10, ' '),
    cin.get();
    return 0;
}

The main() program does nothing more than output a message and then creates an object tp of class TutorPair. This causes C++ to invoke the constructor for TutorPair. However, before the first line of that function is executed, C++ goes through the data members and constructs any objects that it finds there.

The first object C++ sees is the Student object s. This constructor outputs the first message that you see on the output. The second object that C++ finds is the Teacher member t. This constructor generates the next line of output.

With all the data members out of the way, C++ passes control to the body of the TutorPair constructor that outputs the final line of output:

  Creating the TutorPair tp
Constructing a Student object
Constructing a Teacher object
Constructing the TutorPair members
Press Enter to continue …

Destructors

Just as objects are created, so they are destroyed. (I think there’s a Biblical passage to that effect.) If a class can have a constructor to set things up, it should also have a special member function to take the object apart and put back any resources that the constructor may have allocated. This function is known as the destructor.

A destructor has the name of the class, preceded by a tilde (~). Like a constructor, the destructor has no return type (not even void), and it cannot be invoked like a normal function.

technicalstuff.eps Technically, you can call the destructor explicitly: s.~Student(). However, this is rarely done, and it’s needed only in advanced programming techniques, such as allocating an object on a predetermined memory address.

tip.eps In logic, the tilde is sometimes used to mean “NOT” — so the destructor is the “NOT constructor.” Get it? Cute.

C++ automatically invokes the destructor in the following three cases:

  • A local object is passed to the destructor when it goes out of scope.
  • An object allocated off the heap is passed to the destructor when it is passed to delete.
  • A global object is passed to the destructor when the program terminates.

Looking at an example

The following StudentDestructor program features a Student class that allocates memory off of the heap in the constructor. Therefore this class needs a destructor to return that memory to the heap.

tip.eps Any class whose constructor allocates resources, in particular, a class that allocates memory off the heap, requires a destructor to put that memory back.

The program creates a few objects within a function fn() and then allows those objects to go out of scope and get destructed when the function returns. The function returns a pointer to an object that fn() allocates off the heap. This object is returned to the heap back in main().

  //
//  StudentDestructor - this program demonstrates the use
//               of the destructor to return resources
//               allocated by the constructor
//
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;

class Student
{
  protected:
    double* pdGrades;
    int*    pnHours;

  public:
    // constructor - init the student to a legal state
    Student()
    {
        cout << "Constructing a Student object" << endl;
        pdGrades = new double[128];
        pnHours  = new int[128];
    }
   ~Student()
    {
        cout << "Destructing a Student object" << endl;
        delete[] pdGrades;
        pdGrades = nullptr;

        delete[] pnHours;
        pnHours = nullptr;
    }
};

Student* fn()
{
    cout << "Entering fn()" << endl;

    // create a student and initialize it
    cout << "Creating the Student s" << endl;
    Student s;

    // create an array of Students
    cout << "Create an array of 5 Students" << endl;
    Student sArray[5];

    // now allocate one off of the heap
    cout << "Allocating a Student from the heap" << endl;
    Student *pS = new Student;

    cout << "Returning from fn()" << endl;
    return pS;
}

int main(int nNumberofArgs, char* pszArgs[])
{
    // now allocate one off of the heap
    Student *pS = fn();

    // delete the pointer returned by fn()
    cout << "Deleting the pointer returned by fn()"
         << endl;
    delete pS;
    pS = nullptr;

    // wait until user is ready before terminating program
    // to allow the user to see the program results
    cout << "Press Enter to continue..." << endl;
    cin.ignore(10, ' '),
    cin.get();
    return 0;
}

The output from the program appears as follows:

  Entering fn()
Creating the Student s
Constructing a Student object
Create an array of 5 Students
Constructing a Student object
Constructing a Student object
Constructing a Student object
Constructing a Student object
Constructing a Student object
Allocating a Student from the heap
Constructing a Student object
Returning from fn()
Destructing a Student object
Destructing a Student object
Destructing a Student object
Destructing a Student object
Destructing a Student object
Destructing a Student object
Deleting the pointer returned by fn()
Destructing a Student object
Press Enter to continue …

The first message is from fn() itself as it displays an opening banner to let us know that control has entered the function. The fn() function then creates an object s that causes the constructor to output a message. It then creates an array of five Student objects, which causes the Student constructor to output five more messages. And finally fn() allocates one more Student object from the heap, using the new keyword.

The last thing fn() does before returning is output an exit banner message. C++ automatically calls the destructor six times: five times for the elements of the array, and once for the s object created at the beginning of the function.

tip.eps You can’t tell from the output, but the objects are destructed-in the reverse order from that in which they were constructed.

The destructor is not invoked for the object allocated off the heap until main() deletes the pointer returned by fn().

remember.eps A memory block allocated off the heap does not go out of scope when the pointer to it goes out of scope. It is the programmer’s responsibility to make sure that the object is returned to the heap using the delete command. (See Chapter 17 for a discussion of delete.)

remember.eps Return a pointer to a non-array with delete. Return an array using delete[].

Destructing data members

The destructor also destructs data members automatically. Destruction occurs in the reverse order to the order of construction: The body of the destructor is invoked first, and then the destructor for each data member in the reverse order that the data members were constructed.

To demonstrate this, I added a destructor to the TutorPairConstructor program. The entire listing is a bit lengthy to include here, but it is contained in the online material as TutorPairDestructor. I include just the TutorPair class here:

  class TutorPair
{
  protected:
    Student s;
    Teacher t;

    int nNumberOfMeetings;

  public:
    TutorPair()
    {
        cout << "Constructing the TutorPair members"
             << endl;
        nNumberOfMeetings = 0;
    }
   ~TutorPair()
    {
        cout << "Destructing the TutorPair object"
             << endl;
    }
};

void fn()
{
    // create a TutorPair and initialize it
    cout << "Creating the TutorPair tp" << endl;
    TutorPair tp;

    cout << "Returning from fn()" << endl;
}

The output from this program appears as follows:

  Creating the TutorPair tp
Constructing a Student object
Constructing a Teacher object
Constructing the TutorPair members
Returning from fn()
Destructing the TutorPair object
Destructing a Teacher object
Destructing a Student object
Press Enter to continue …

This program creates the TutorPair object within the function fn(). The messages from the constructors are identical to the TutorPairConstructor program. The messages from the TutorPair destructor appear as control is returning to main, and they appear in the exact reverse of the order of messages from the constructors, coming first from ~TutorPair itself, then from ~Teacher, and finally from ~Student.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.128.204.5