Chapter 26

Making Constructive Arguments

In This Chapter

arrow Creating and invoking a constructor with arguments

arrow Overloading the constructor

arrow Constructing data members with arguments

arrow Initializing data members with the declaration.

The Student class in Chapter 25 is extremely simple — almost unreasonably so. After all, a student has a name and a student ID as well as a grade-point average and other miscellaneous data. I choose GPA as the data to model in Chapter 25 because I know how to initialize it without someone telling me — I could just zero out this field. But I can’t just zero out the name and ID fields; a no-named student with a null ID probably does not represent a valid student. Somehow I need to pass arguments to the constructor to tell it how to initialize fields that start out with a value that’s not otherwise predictable. This chapter shows you how to pass arguments to the constructor.

Constructors with Arguments

C++ allows the program to define a constructor with arguments as shown here:

  class Student
{
  public:
    Student(const char* pszNewName, int nNewID)
    {
        int nLength = strlen(pszNewName) + 1;
        pszName = new char[nLength];
        strcpy(pszName, pszNewName);
        nID = nNewID;
    }
   ~Student()
    {
        delete[] pszName;
        pszName = nullptr;
    }

  protected:
    char* pszName;
    int   nID;
};

Here the arguments to the constructor are a pointer to an ASCIIZ string that contains the name of the new student and the student’s ID. The constructor first allocates space for the student’s name. It then copies the new name into the pszName data member. Finally it copies over the student ID.

remember.eps A destructor (see Chapter 25) is required to return the memory to the heap once the object is destroyed. Any class that allocates a resource like memory in the constructor must return that memory in the destructor.

Remember, you can’t call a constructor like you call a function, so you have to somehow associate the arguments to the constructor with the object when it is declared. The following code snippet shows how this is done:

  void fn()
{
    // put arguments next to object normally
    Student s1("Stephen Davis", 1234);

    // or next to the class name when allocating
    // an object from the heap
    Student* pS2 = new Student("Kinsey Davis", 5678);
}

The arguments appear next to the object normally, and next to the class name when you’re allocating an object off the heap.

Looking at an example

The following NamedStudent program uses a constructor similar to the one shown in the snippet to create a Student object and display my, I mean his, name:

  //
//  NamedStudent - this program demonstrates the use
//               of a constructors with arguments
//
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <cstring>
using namespace std;

class Student
{
  protected:
    char*  pszName;
    int    nID;

  public:
    Student(const char* pszNewName, int nNewID)
    {
        cout << "Constructing " << pszNewName << endl;
        int nLength = strlen(pszNewName) + 1;
        pszName = new char[nLength];
        strcpy(pszName, pszNewName);
        nID = nNewID;
    }
   ~Student()
    {
        cout << "Destructing " << pszName << endl;
        delete[] pszName;
        pszName = nullptr;
    }

    // getName() - return the student's name
    const char* getName()
    {
        return pszName;
    }

    // getID() - get the student's ID
    int getID()
    {
        return nID;
    }
};

Student* fn()
{
    // create a student and initialize it
    cout << "Constructing a local student in fn()" <<endl;
    Student student("Stephen Davis", 1234);

    // display the student's name
    cout << "The student's name is "
         << student.getName() << endl;

    // now allocate one off of the heap
    cout << "Allocating a Student from the heap" << endl;
    Student *pS = new Student("Kinsey Davis", 5678);

    // display this student's name
    cout << "The second student's name is "
         << pS->getName() << endl;

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

int main(int nNumberofArgs, char* pszArgs[])
{
    // call the function that creates student objects
    cout << "Calling fn()" << endl;
    Student* pS = fn();
    cout << "Back in main()" << endl;

    // delete the object returned by fn()
    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 main() program starts by outputting a message and then calling the function fn(). This function creates a student with the unlikely name “Stephen Davis” and an ID of 1234. The function then asks the object for its name just to prove that the name was accurately noted in the object. The function goes on to create another Student object, this time off the heap, and similarly asks it to display its name.

The fn() function then returns control to main(); this causes the student object to go out of scope, which causes C++ to invoke the destructor. Then main() restores the memory returned from fn() to the heap, using the keyword delete. This invokes the destructor for that object.

The constructor for class Student accepts a pointer to an ASCIIZ string and an int student ID. The constructor allocates a new character array from the heap and then copies the string passed it into that array. It then copies the value of the student ID.

remember.eps Refer to Chapter 16 if you don’t remember what an ASCIIZ string is or what strlen() does.

The destructor for class Student simply restores the memory allocated by the constructor to the heap by passing the address in pszName to delete[].

remember.eps Use delete[] when restoring an array to the heap; use delete when restoring a single object. Use nullptr to zero out the pointer after deleting its contents.

The getName() and getID() member functions are access functions for the name and ID. Declaring the return type of getName() as const char* (read “pointer to constant char”) — as opposed to simply char* — means that the caller cannot change the name using the address returned by getName().

remember.eps Refer to Chapter 18 if you don’t remember the difference between a const char* and a char * const (or if you have no idea what I’m talking about).

The output from this program appears as follows:

  Calling fn()
Constructing a local student in fn()
Constructing Stephen Davis
The student's name is Stephen Davis
Allocating a Student from the heap
Constructing Kinsey Davis
The second student's name is Kinsey Davis
Returning from fn()
Destructing Stephen Davis
Back in main()
Destructing Kinsey Davis
Press Enter to continue …

remember.eps I’ve said it before (and you probably ignored me), but I really must insist this time: You need to invoke the preceding constructor in the debugger to get a feel for what C++ is doing with your declaration.

But what if you need both a named constructor and a default constructor? Keep reading.

Overloading the Constructor

You can have two or more constructors as long as they can be differentiated by the number and types of their arguments. This is called overloading the constructor.

remember.eps Overloading a function means to define two or more functions with the same short name but with different arguments. Refer to Chapter 11 for a discussion of function overloading.

Thus the following Student class from the OverloadedStudent program has three constructors:

  //
//  OverloadedStudent - this program overloads the Student
//                 constructor with 3 different choices
//                 that vary by number of arguments
//
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <cstring>
using namespace std;

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


  public:
    Student(const char* pszNewName, int nNewID,
            double dXferGrade, int nXferHours)
    {
        cout << "Constructing " << pszNewName
              << " as a transfer student." << endl;
        int nLength = strlen(pszNewName) + 1;
        pszName = new char[nLength];
        strcpy(pszName, pszNewName);
        nID = nNewID;
        dGrade = dXferGrade;
        nSemesterHours = nXferHours;
    }
    Student(const char* pszNewName, int nNewID)
    {
        cout << "Constructing " << pszNewName
             << " as a new student." << endl;
        int nLength = strlen(pszNewName) + 1;
        pszName = new char[nLength];
        strcpy(pszName, pszNewName);
        nID = nNewID;
        dGrade = 0.0;
        nSemesterHours = 0;
    }
    Student()
    {
        pszName = 0;
        nID = 0;
        dGrade = 0.0;
        nSemesterHours = 0;
    }
   ~Student()
    {
        cout << "Destructing " << pszName << endl;
        delete[] pszName;
        pszName = nullptr;
    }

    // access functions
    const char* getName()
    {
        return pszName;
    }
    int getID()
    {
        return nID;
    }
    double getGrade()
    {
        return dGrade;
    }
    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
    Student student("Stephen Davis", 1234);

    // now create a transfer student with an initial grade
    Student xfer("Kinsey Davis", 5678, 3.5, 12);

    // give both students a B in the current class
    student.addGrade(3.0, 3);
    xfer.addGrade(3.0, 3);

    // display the student's name and grades
    cout << "Student "
         << student.getName()
         << " has a grade of "
         << student.getGrade()
         << endl;

    cout << "Student "
         << xfer.getName()
         << " has a grade of "
         << xfer.getGrade()
         << endl;

    // 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;
}

Starting with the Student class, you can see that the first constructor within Student accepts a name, a student ID, and transfer credit in the form of an initial grade-point average (GPA) and number of semester hours. The second constructor accepts only a name and ID; this constructor is intended for new students as it initializes the GPA and hours to zero. It’s unclear what the third constructor is for — this default constructor initializes everything to zero.

The main() function creates a new student using the second constructor with the name “Stephen Davis”; then it uses the second constructor to create a transfer student with the name “Kinsey Davis”. The program adds three hours of credit to both (just to show that this still works) and displays the resulting GPA.

The output from this program appears as follows:

  Constructing Stephen Davis as a new student.
Constructing Kinsey Davis as a transfer student.
Student Stephen Davis has a grade of 3
Student Kinsey Davis has a grade of 3.4
Press Enter to continue …

Notice how similar the first two Student constructors are. This is not uncommon. This case is one in which you can create an init() function that both constructors call (only the constructors are shown in this example for brevity’s sake):

  class Student
{
  protected:
    void init(const char* pszNewName, int nNewID,
              double dXferGrade, int nXferHours)
    {
        cout << "Constructing " << pszNewName
              << " as a transfer student." << endl;
        int nLength = strlen(pszNewName) + 1;
        pszName = new char[nLength];
        strcpy(pszName, pszNewName);
        nID = nNewID;
        dGrade = dXferGrade;
        nSemesterHours = nXferHours;
    }
  public:
    Student(const char* pszNewName, int nNewID,
            double dXferGrade, int nXferHours)
    {
        init(pszNewName, nNewID, dXferGrade, nXferHours);
    }
    Student(const char* pszNewName, int nNewID)
    {
        init(pszNewName, nNewID, 0.0, 0);
    }

    // ...class continues as before...
};

In general, the init() function will look like the most complicated constructor. All simpler constructors call init() passing default values for some of the arguments, such as a 0 for transfer grade and credit for new students.

technicalstuff.eps You can also default the arguments to the constructor (or any function, for that matter) as follows:

  class Student
{
  public:
    Student(const char* pszNewName, int nNewID,
            double dXferGrade = 0.0, int nXferHours = 0);

    // ...and so it goes...
};

C++ will supply the defaulted arguments if they are not provided in the declaration. However, default arguments can generate strange error messages and are beyond the scope of this book.

technicalstuff.eps You can also invoke one constructor from another starting with the C++ 2011 standard. The details are a little beyond the scope of a beginner book; for now, just note that this is possible.

The Default default Constructor

As far as C++ is concerned, every class must have a constructor; otherwise, you can’t create any objects of that class. If you don’t provide a constructor for your class, C++ should probably just generate an error, but it doesn’t. To provide compatibility with existing C code, which knows nothing about constructors, C++ automatically provides an implicitly defined default constructor (sort of a default default constructor) that invokes the default constructor for any data members. Sometimes I call this a Miranda constructor. You know: “If you cannot afford a constructor, a constructor will be provided for you.”

If your class already has a constructor, however, C++ doesn’t provide the automatic default constructor. (Having tipped your hand that this isn’t a C program, C++ doesn’t feel obliged to do any extra work to ensure compatibility.)

warning.eps The result is: If you define a constructor for your class but you also want a default constructor, you must define it yourself.

The following code snippets help demonstrate this principle. The following is legal:

  class Student
{
    // ...all the same stuff but no constructors...
};

void fn()
{
    Student s; // create Student using default constructor
}

Here the object s is built using the default constructor. Because the programmer has not provided a constructor, C++ provides a default constructor that doesn’t really do anything in this case.

However, the following snippet does not compile properly:

  class Student
{
  public:
    Student(const char* pszName);

    // ...all the same stuff...
};

void fn()
{
    Student s; // doesn't compile
}

The seemingly innocuous addition of the Student(const char*) constructor precludes C++ from automatically providing a Student() constructor with which to build the s object. Now the compiler complains that it can no longer find Student::Student() with which to build s. You can add a default constructor yourself to solve the problem.

The 2011 C++ standard also allows you to reinstate the default constructor using the following curious syntax:

  class Student
{
  public:
    Student(const char* pszName);
    Student() = default;

    // ...all the same stuff...
};

void fn()
{
    Student s; // this does compile
}

It’s just this type of illogic that explains why C++ programmers make the really big bucks.

Constructing Data Members

In the preceding examples, all the data members have been simple types such as int and double and arrays of char. With these simple types, it’s sufficient to just assign the variable a value within the constructor. But what if the class contains data members of a user-defined class? There are two cases to consider here.

Initializing data members with the default constructor

Consider the following example:

  class StudentID
{
  protected:
    static int nBaseValue;
    int        nValue;

  public:
    StudentID()
    {
        nValue = nBaseValue++;
    }

    int getID()
    {
        return nValue;
    }
};

// allocate space for the class property
int StudentID::nBaseValue = 1000;

class Student
{
  protected:
    char*        pszName;
    StudentID    sID;

  public:
    Student(const char* pszNewName)
    {
        int nLength = strlen(pszNewName) + 1;
        pszName = new char[nLength];
        strcpy(pszName, pszNewName);
    }
   ~Student()
    {
        delete[] pszName;
        pszName = nullptr;
    }

    // getName() - return the student's name
    const char* getName()
    {
        return pszName;
    }

    // getID() - get the student's ID
    int getID()
    {
        return sID.getID();
    }
};

The class StudentID is designed to allocate student IDs sequentially. The class retains the “next value” in a static variable StudentID::nBaseValue.

remember.eps Static data members, also known as class members, are shared among all objects.

Each time a StudentID is created, the constructor assigns nValue the “next value” from nBaseValue and then increments nBaseValue in preparation for the next time the constructor is called.

The Student class has been updated so that the sID field is now of type StudentID. The constructor now accepts the name of the student but relies on StudentID to assign the next sequential ID each time a new Student object is created.

remember.eps The constructor for each data member, including StudentID, is invoked before control is passed to the body of the Student constructor.

All the Student constructor has to do is make a copy of the student’s name — the sID field takes care of itself.

Initializing data members with a different constructor

So now the boss comes in and wants an addition to the program. Now she wants to update the program so that it can assign a new student ID instead of always accepting the default value handed over by the StudentID class.

Accordingly, I make the following changes:

  class StudentID
{
  protected:
    static int nBaseValue;
    int        nValue;

  public:
    StudentID(int nNewID)
    {
        nValue = nNewID;
    }
    StudentID()
    {
        nValue = nBaseValue++;
    }

    int getID()
    {
        return nValue;
    }
};

// allocate space for the class property
int StudentID::nBaseValue = 1000;

class Student
{
  protected:
    char*        pszName;
    StudentID    sID;

    void initName(const char* pszNewName)
    {
        int nLength = strlen(pszNewName) + 1;
        pszName = new char[nLength];
        strcpy(pszName, pszNewName);
    }

  public:
    Student(const char* pszNewName, int nNewID)
    {
        initName(pszNewName);
        StudentID sID(nNewID);
    }
    Student(const char* pszNewName)
    {
        initName(pszNewName);
    }
   ~Student()
    {
        delete pszName;
        pszName = nullptr;
    }

    // getName() - return the student's name
    const char* getName()
    {
        return pszName;
    }

    // getID() - get the student's ID
    int getID()
    {
        return sID.getID();
    }
};

I added a constructor to StudentID to allow the caller to pass a value to use for the student ID rather than accept the default. Now, if the program doesn’t provide an ID, the student is assigned the next sequential ID. If the program does provide an ID, however, then it is used instead; the static counter is left untouched.

I also added a constructor to Student to allow the program to provide a studentID when the student is created. This Student(const char*, int) constructor first initializes the student’s name and then invokes the StudentID(int) constructor on sID.

When I execute the program, however, I am disappointed to find that this seems to have made no apparent difference. Students are still assigned sequential student IDs, whether or not they are passed a value to use instead.

The problem, I quickly realize, is that the Student(const char*, int) constructor is not invoking the new StudentID(int) constructor on the data member sID. Instead, it’s creating a new local object called sID within the constructor, which it then immediately discards without any effect on the data member of the same name.

Remember that the constructor for the data members is called before control is passed to the body of the constructor. Rather than create a new value locally, I need some way to tell C++ to use a constructor other than the default constructor when creating the data member sID.

C++ uses the following syntax to initialize a data member with a specific constructor:

  class Student
{
  public:
    Student(const char* pszName,
            int nNewID) : sID(nNewID)
    {
        initName(pszName);
    }

    // ...remainder of class unchanged...
};

The data member appears to the right of a colon used to separate such declarations from the arguments to the function but before the open brace of the function itself. This causes the StudentID(int) constructor to be invoked, passing the nNewID value to be used as the new student ID.

Looking at an example

The following CompoundStudent program creates one Student object with the default, sequential student ID, while assigning a specific student ID to a second Student object:

  //
//  CompoundStudent - this version of the Student class
//                    includes a data member that's also
//                    of a user defined type
//
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <cstring>
using namespace std;

class StudentID
{
  protected:
    static int nBaseValue;
    int        nValue;

  public:
    StudentID()
    {
        nValue = nBaseValue++;
    }

    StudentID(int nNewValue)
    {
        nValue = nNewValue;
    }

    int getID()
    {
        return nValue;
    }
};

// allocate space for the class property
int StudentID::nBaseValue = 1000;

class Student
{
  protected:
    char*        pszName;
    StudentID    sID;

    void initName(const char* pszNewName)
    {
        int nLength = strlen(pszNewName) + 1;
        pszName = new char[nLength];
        strcpy(pszName, pszNewName);
    }

  public:
    Student(const char* pszNewName,
                  int nNewID) : sID(nNewID)
    {
        initName(pszNewName);
    }
    Student(const char* pszNewName)
    {
        initName(pszNewName);
    }
   ~Student()
    {
        delete[] pszName;
        pszName = nullptr;
    }

    // getName() - return the student's name
    const char* getName()
    {
        return pszName;
    }

    // getID() - get the student's ID
    int getID()
    {
        return sID.getID();
    }
};


int main(int nNumberofArgs, char* pszArgs[])
{
    // create a student and initialize it
    Student student1("Stephen Davis");

    // display the student's name and ID
    cout << "The first student's name is "
         << student1.getName()
         << ", ID is "
         << student1.getID()
         << endl;

    // do the same for a second student
    Student student2("Janet Eddins");
    cout << "The second student's name is "
         << student2.getName()
         << ", ID is "
         << student2.getID()
         << endl;

    // now create a transfer student with a unique ID
    Student student3("Tiffany Amrich", 1234);
    cout << "The third student's name is "
         << student3.getName()
         << ", ID is "
         << student3.getID()
         << endl;

    // 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 Student and StudentID classes are similar to those shown earlier. The main() function creates three students, the first two using the Student(const char*) constructor that allocates the default student ID. The third student is created — using the Student(const char*, int) constructor — and passed an ID of 1234. The resulting display confirms that the default IDs are being allocated sequentially and that the third student has a unique ID:

  The first student's name is Stephen Davis, ID is 1000
The second student's name is Janet Eddins, ID is 1001
The third student's name is Tiffany Amrich, ID is 1234
Press Enter to continue …

The : syntax here can also be used to initialize simple variables if you prefer:

  class SomeClass
{
  protected:
    int nValue;
    const double PI;

  public:
    SomeClass(int n) : nValue(n), PI(3.14159) {}
};

Here the data member nValue is initialized to n, and the constant double is initialized to 3.14159.

In fact, this is the only way to initialize a data member flagged as const. You can’t put a const variable on the left-hand side of an assignment operator.

Notice that the body of the constructor is now empty since all the work is done in the header; however, the empty body is still required (otherwise the definition would look like a prototype declaration).

New with C++ 2011

Starting with the 2011 standard, you can initialize data members to a value in the declaration itself using an “assignment format”, as in the following:

  class SomeClass
{
  protected:
    int nValue;
    const double PI = 3.14159;
    char* pSomeString = new char[128];

  public:
    SomeClass(int n) : nValue(n) {}
};

The effect is the same as if you had written the constructor as follows:

  class SomeClass
{
  protected:
    int nValue;
    const double PI;
    char* pSomeString;

  public:
    SomeClass(int n)
      : nValue(n), PI(3.14159), pSomeString(new char[128])
    {}
};

This assignment format is easier to read and just seems more natural. Though this is a recent addition to C++, you’re likely to see this more and more.

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

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