Section 14e

More on Records, Implementing a C++ Class


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Getting the Class Work Done

Like almost all C and C++ code, there is a header file that programmers can use to see what is available for use in a given set of functions and/or a class. In the previous topic, you were provided the contents of what would be the header (.h) file for the StudentClass. In this topic, you will learn about the implementation of some of the methods used in the class.

The Constructors

The default constructor. Remember that the default constructor simply sets the object into a ready state but initializes any data to default or standard conditions. Here is the StudentClass default constructor

/*
Name: StudentClass - default constructor
Process: initializes all data to default states
Function Input/Parameters: none
Function Output/Parameters: none
Function Output/Returned: none
Device Input: none
Device Output: none
Dependencies: none
*/
StudentClass::StudentClass
(
// no parameters
)
{
// set name data to NULL, not used
name = NULL;

// set student ID to zero, not used
studentId = 0;

// set gender to 'o', not used
gender = 'o';
}

You can see that the object data items are all set to default or non-use data. However, C++ has a handy little helping tool called initializers or initializer lists that allow the programmer to set these values prior to the code block, as long as they are simple assignments. Here is an example of using initializer lists. The specification block is not included as it is the same as the previous one.

StudentClass::StudentClass
(
// no parameters
)
: name( NULL ), studentId( 0 ), gender( 'o' )
{
// data loaded via initializers
}

Now consider some of the other parts of this constructor. The first thing to notice is the name of the class and then the double colon which is called a scope resolution operator. So in English, the name of the class with the scope resolution operator says, "Whatever follows is part of the StudentClass. And since all constructors have the same name as the class, you see the class name, the scope resolution operator and then the name of the class again.

Next, the single colon identifies the initializer list. You won't find the colon in constructors without initializer lists but when there is an initializer list, it must be preceded by the single colon.

After that, check out the spacing in the second example. There are seven spaces prior to each parameter list parenthesis; there are five spaces prior to the single colon and initializer list, and as usual, three spaces prior to the curly braces that identify the constructor code block. This is not an absolute standard but strongly recommended as you are first starting out with C++ constructors. It will keep you away from confusing the different parts of the constructor header.

The initialization constructor. The initialization constructor actually sets up the data for the object as was described in the previous topic. Here is an example of this constructor.

/*
Name: StudentClass - initialization constructor
Process: initializes all data to parameters
Function Input/Parameters: name (char *), student id (int),
gender (char), gpa (double)
Function Output/Parameters: none
Function Output/Returned: none
Device Input: none
Device Output: none
Dependencies: resizeName
*/
StudentClass::StudentClass
(
const char *inName, // in: student name
int inId, // in: student id
char inGender // in: student gender
)
: studentId( inId ), gender( inGender )
{
// initialize name to NULL prior to setting
name = NULL;

// resize string as needed, load name
// function: setNameString
setNameString( inName );
}

This does not look much different from the default constructor except that data provided from the user/programmer is entered into the object inside of default values. However, note that the name is not used in the initializer list. If you recall from above, the initializer list can set items that are simple assignments. However, creating a name requires accessing some dynamic memory and then assigning the name character by character into the string. The initializer list cannot do this so it is done inside the constructor code block as shown. More will be discussed about the name data later.

The copy constructor. This constructor does exactly what it says it does. In any given class, one object can be duplicated to initialize another object. This is helpful when you need a new data quantity to work with and you don't want to modify the one with which you are currently working. You will see several examples of this as you continue with your learning. Here is an example of the copy constructor for the StudentClass.

/*
Name: StudentClass - copy constructor
Process: initializes all data from copied object
Function Input/Parameters: copied object (StudentClass &)
Function Output/Parameters: none
Function Output/Returned: none
Device Input: none
Device Output: none
Dependencies: resizeName
*/
StudentClass::StudentClass
(
const StudentClass &copied // in: StudentClass object to be copied
)
: studentId( copied.studentId ), gender( copied.gender )
{
// initialize name to NULL prior to setting
name = NULL;

// resize string as needed, load name
// function: setNameString
setNameString( copied.name );
}

Constructors are always necessary for initializing objects but there may not be a need for all three of the ones shown. This is up to the application. It can also be noted that C++ will create a default constructor if one is not created by the programmer but that would be like letting other people write your programs; you don't want to let that happen.

The destructor.

In C++, some objects need cleaning up when they go out of scope. As previously noted, C++ keeps track of when the scope ends and it will call the destructor without any help from the programmer. And in many cases, the destructor does not have to do anything. However, for this class, the name is dynamically allocated so it must be deallocated. In other words, the allocated memory must be returned to the operating system. Here is the destructor for this class. Note the key word delete that is used for deallocating the name memory.

/*
Name: StudentClass - destructor
Process: releases acquired memory to OS as needed
Function Input/Parameters: none
Function Output/Parameters: none
Function Output/Returned: none
Device Input: none
Device Output: none
Dependencies: none
*/
StudentClass::~StudentClass
(
// no parameters
)
{
// check for name data acquired
if( name != NULL )
{
// release string data
// function: delete
delete( name );
}
}

The destructor is indicated by using the class name as the constructors do but the destructor has a tilde ('~') in front of the method name.

Accessors

There are three accessors for this class but they all pretty much work the same way. Getting the gender or student id are implemented by simply returning the data. Here is an example of the name accessor which actually returns a pointer to the C-Style string.

/*
Name: getName
Process: returns name data
Function Input/Parameters: none
Function Output/Parameters: none
Function Output/Returned: name (char *)
Device Input: none
Device Output: none
Dependencies: none
*/
const char *StudentClass::getName
(
// no parameters
) const
{
// return data
return name;
}

Returning the name is as simple as returning the name variable because the variable name itself is the pointer. As you recall from the header file, this value is returned as a constant so it cannot be modified inside the class by a user/programmer who is accessing this data. Almost all individual data returned like this will be protected by the const directive.

Mutators

There are also three mutators in this class for the same reason there are three accessors. There are three data items that this class will provide to the user/programmer on demand. Here is a mutator example. The name is used again because it is slightly different than just assigning data to a member variable.

/*
Name: setName
Process: returns name data
Function Input/Parameters: none
Function Output/Parameters: none
Function Output/Returned: name (char *)
Device Input: none
Device Output: none
Dependencies: none
*/
void StudentClass::setName
(
const char *inName // in: student name
)
{
// set data member
// function: setNameString
setNameString( inName );
}

Once again, there is not much to this method, or most mutator methods, but as you can see this is the third time the setNameString method has been used. This is a good example of code reuse and modular programming. It keeps the programmer from rewriting the same code in different places and gets the details out of the way of the task at hand in each method in which it is used. Also note that this method is not one the user/programmer needs so it is declared private in the header file. Take a look.

Private Utility Method

The following is an example of a private method, one that the user/programmer does not need but that helps take care of the business of being a StudentClass object behind the scenes.

/*
Name: setNameString
Process: acquires memory for string as needed,
assigns string to student name
Function Input/Parameters: student name (const char *)
Function Output/Parameters: none
Function Output/Returned: none
Device Input: none
Device Output: none
Dependencies: new
*/
void StudentClass::setNameString
(
const char *inName // in: pointer to incoming name string
)
{
// initialize variables

// get length of incoming string
int inStrLen = strlen( inName );

// set Boolean for memory acquisition
bool needsAcquisition = false;

// check for current name string not set
if( name == NULL )
{
// set needs acquisition boolean
needsAcquisition = true;
}

// check for current name string too short
else if( inStrLen > signed( strlen( name ) + 1 ) )
{
// release the current string memory
// function: delete
delete( name );

// set needs acquisition boolean
needsAcquisition = true;
}

// check for needs acquisition
if( needsAcquisition )
{
// acquire memory for name string
// function: new
name = new char[ inStrLen + 1 ];
}

// set name data
// function: strcp
strcpy( name, inName );
}

The setNameString method first gets the length of the new string to be set and then sets the needsAcquisition Boolean to false. One possibility for this method is that the name string has not been set at all yet, in which case it would be NULL so that is checked.

Another possibility is that a previous string has been set but it is not as long as the current name so this is checked. If this is true, the old string is deleted (i.e., the memory is deallocated). After these two tests and actions, a new string is created as needed and then the new string is placed in the name string.

This method is a good example of how an Abstract Data Type (ADT) uses Object Oriented Programming (OOP) to manage its own data.

Class Implementation

The process of implementing the methods in a class is pretty straight forward. Once you have designed the class and what it should do in the header file, you are really just filling in the blanks of each necessary method. Check this video out to see the whole class created and then tested. It is not a terribly complicated class but it will get you the foundation you need to move forward with creating your own classes.