Point and Stare at Objects - Introduction to Classes - C++ For Dummies (2014)

C++ For Dummies (2014)

Part III

Introduction to Classes

Chapter 13

Point and Stare at Objects

In This Chapter

arrow Examining the object of arrays of objects

arrow Getting a few pointers on object pointers

arrow Strong typing — getting picky about our pointers

arrow Navigating through lists of objects

C++ programmers are forever generating arrays of things — arrays of ints, arrays of doubles — so why not arrays of students? Students stand in line all the time — a lot more than they care to. The concept of Student objects all lined up quietly awaiting their names to jump up to perform some mundane task is just too attractive to pass up.

Declaring Arrays of Objects

Arrays of objects work the same way arrays of simple variables work. (Chapter 7 goes into the care and feeding of arrays of simple — intrinsic — variables, and Chapters 8 and 9 describe simple pointers in detail.) Take, for example, the following snippet from the ArrayOfStudents program:

// ArrayOfStudents - define an array of student objects
// and access an element in it. This
// program doesn't do anything
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;

class Student
{
public:
int semesterHours;
double gpa;
double addCourse(int hours, double grade){return 0.0;}
};

void someFn()
{
// declare an array of 10 students
Student s[10];

// assign the 5th student a gpa of 4.0 (lucky guy)
s[4].gpa = 4.0;
s[4].semesterHours = 32;

// add another course to the 5th student;
// this time he failed - serves him right
s[4].addCourse(3, 0.0);
}

Here s is an array of Student objects. s[4] refers to the fifth Student object in the array. By extension, s[4].gpa refers to the GPA of the fifth student. Further, s[4].addCourse() adds a course to the fifth Student object.

Declaring Pointers to Objects

Pointers to objects work like pointers to simple types, as you can see in the example program ObjPtr:

// ObjPtr - define and use a pointer to a Student object
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;

class Student
{
public:
int semesterHours;
double gpa;
double addCourse(int hours, double grade);
};

int main(int argc, char* pArgs[])
{
// create a Student object
Student s;
s.gpa = 3.0;

// now create a pointer pS to a Student object
Student* pS;

// make pS point to our Student object
pS = &s;

// now output the gpa of the object, once thru
// the variable name and a second time thru pS
cout << "s.gpa = " << s.gpa << "\n"
<< "pS->gpa = " << pS->gpa << 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, '\n');
cin.get();
return 0;
}

The program declares a variable s of type Student. It then goes on to declare a pointer variable pS of type “pointer to a Student object,” also written as Student*. The program initializes the value of one of the data members in s. It then proceeds to assign the address of s to the variable pS.Finally, it refers to the same Student object, first using the object’s name, s, and then using the pointer to the object, pS. I explain the strange notation pS->gpa; in the next section of this chapter.

Dereferencing an object pointer

By analogy of pointers to simple variables, you might think that the following refers to the GPA of student s:

int main(int argc, char* pArgs[])
{
Student s;
Student* pS = &s; // create a pointer to s

// access the gpa member of the obj pointed at by pS
// (this doesn't work)
*pS.gpa = 3.5;

return 0;
}

As the comments indicate, this doesn’t work. The problem is that the dot operator (.) is evaluated before the pointer (*). Thus, *ps.gpa is interpreted as if written *(ps.gpa). Parentheses are necessary to force the pointer operator to be evaluated before the dot:

int main(int argc, char* pArgs[])
{
Student s;
Student* pS = &s; // create a pointer to s

// access the gpa member of the obj pointed at by pS
// (this works as expected)
(*pS).gpa = 3.5;

return 0;
}

The *pS evaluates to the pointer’s Student object pointed at by pS. The .gpa refers to the gpa member of that object.

Pointing toward arrow pointers

Using the asterisk operator together with parentheses works just fine for dereferencing pointers to objects; however, even the most hardened techies would admit that this mixing of asterisks and parentheses is a bit tortured.

C++ offers a more convenient operator for accessing members of an object to avoid clumsy object pointer expressions. The -> operator is defined as follows:

ps->gpa is equivalent to(*pS).gpa

This leads to the following:

int main(int argc, char* pArgs[])
{
Student s;
Student* pS = &s; // create a pointer to s

// access the gpa member of the obj pointed at by pS
pS->gpa = 3.5;

return 0;
}

The arrow operator is used almost exclusively because it is easier to read; however, the two forms are completely equivalent.

Passing Objects to Functions

Passing pointers to functions is just one of the many ways to entertain yourself with pointer variables.

Calling a function with an object value

As you know, C++ passes arguments to functions by reference when the argument type is flagged with the & property (see Chapter 8). However, by default, C++ passes arguments to functions by value. (You can check Chapter 6 on this one, if you insist.)

Complex, user-defined class objects are passed the same as simple int values, as shown in the following PassObjVal program:

// PassObjVal - attempts to change the value of an object
// in a function fail when the object is
// passed by value
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;

class Student
{
public:
int semesterHours;
double gpa;
};

void someFn(Student copyS)
{
copyS.semesterHours = 10;
copyS.gpa = 3.0;
cout << "The value of copyS.gpa = "<<copyS.gpa<< endl;
}

int main(int argc, char* pArgs[])
{
Student s;
s.gpa = 0.0;

// display the value of s.gpa before calling someFn()
cout << "The value of s.gpa = " << s.gpa << endl;

// pass the address of the existing object
cout << "Calling someFn(Student)" << endl;
someFn(s);
cout << "Returned from someFn(Student)" << endl;

// the value of s.gpa remains 0
cout << "The value of s.gpa = " << s.gpa << 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, '\n');
cin.get();
return 0;
}

The function main() creates an object s and then passes s to the function someFn().

image It is not the object s itself that is passed, but a copy of s.

The object copyS in someFn() begins life as an exact copy of the variable s in main(). Since it is a copy, any change to copyS made within someFn() has no effect on s back in main(). Executing this program generates the following understandable but disappointing response:

The value of s.gpa = 0
Calling someFn(Student)
The value of copyS.gpa = 3
Returned from someFn(Student)
The value of s.gpa = 0
Press Enter to continue...

Calling a function with an object pointer

Most of the time, the programmer wants any changes made in the function to be reflected in the calling function as well. For this, the C++ programmer must pass either the address of an object or a reference to the object. The following PassObjPtr program uses the address approach:

// PassObjPtr - change the contents of an object in
// a function by passing a pointer
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;

class Student
{
public:
int semesterHours;
double gpa;
};

void someFn(Student* pS)
{
pS->semesterHours = 10;
pS->gpa = 3.0;
cout << "The value of pS->gpa = " << pS->gpa << endl;
}

int main(int nNumberofArgs, char* pszArgs[])
{
Student s;
s.gpa = 0.0;

// display the value of s.gpa before calling someFn()
cout << "The value of s.gpa = " << s.gpa << endl;

// pass the address of the existing object
cout << "Calling someFn(Student*)" << endl;
someFn(&s);
cout << "Returned from someFn(Student*)" << endl;

// the value of s.gpa is now 3.0
cout << "The value of s.gpa = " << s.gpa << 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, '\n');
cin.get();
return 0;
}

The type of the argument to someFn() is a pointer to a Student object (otherwise known as Student*). This is reflected in the way that the program calls someFn(), passing the address of s rather than the value of s. Giving someFn() the address of s allows him to modify whatever value that is stored there. Conceptually, this is akin to writing down the address of the house s on the piece of paper pS and then passing that paper to someFn(). The function someFn() uses the arrow syntax for dereferencing the pS pointer.

The output from PassObjPtr is much more satisfying (to me, anyway):

The value of s.gpa = 0
Calling someFn(Student*)
The value of pS->gpa = 3
Returned from someFn(Student*)
The value of s.gpa = 3
Press Enter to continue...

Calling a function by using the reference operator

Chapter 6 introduces the concept of passing simple argument types to functions by reference using the “&” operator. The following PassObjRef demonstrates the same for user-defined objects:

// PassObjRef - change the contents of an object in
// a function by using a reference
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;

class Student
{
public:
int semesterHours;
double gpa;
};

// same as before, but this time using references
void someFn(Student& refS)
{
refS.semesterHours = 10;
refS.gpa = 3.0;
cout << "The value of copyS.gpa = " <<refS.gpa<< endl;
}

int main(int nNumberofArgs, char* pszArgs[])
{
Student s;
s.gpa = 0.0;

// display the value of s.gpa before calling someFn()
cout << "The value of s.gpa = " << s.gpa << endl;

// pass the address of the existing object
cout << "Calling someFn(Student*)" << endl;
someFn(s);
cout << "Returned from someFn(Student&)" << endl;

// the value of s.gpa is now 3.0
cout << "The value of s.gpa = " << s.gpa << 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, '\n');
cin.get();
return 0;
}

In this example, C++ passes a reference to s rather than a copy. The output from this version is identical to the PassObjPtr program — changes made in someFn() are retained in main().

Why Bother with Pointers or References?

Okay, so both pointers and references provide relative advantages, but why bother with either one? Why not just always pass the object? I mentioned one obvious answer earlier in this chapter: You can’t modify the object from a function that gets nothing but a copy of the structure object.

Here’s a second reason: Some objects are large — I mean really large. An object representing a screen image can be many megabytes in length. Passing such an object by value means copying the entire thing into the function’s memory.

The object will need to be copied again should that function call another, and so on. After a while, you can end up with dozens of copies of this object. That consumes memory, and copying all the objects can make execution of your program slower than booting up Windows.

image The problem of copying objects gets worse. You see in Chapter 17 that making a copy of an object can be even more painful than simply copying some memory around.

Passing a pointer (or a reference) is very fast. A pointer is 4 bytes, no matter how big the object being pointed at is.

Returning to the Heap

The problems that exist for simple types of pointers plague class object pointers as well. In particular, you must make sure that the pointer you’re using actually points to a valid object. For example, don’t return a reference to an object defined local to the function:

MyClass* myFunc()
{
// the following does not work
MyClass mc;
MyClass* pMC = &mc;
return pMC;
}

Upon return from myFunc(), the mc object goes out of scope. The pointer returned by myFunc() is not valid in the calling function.

image The problem of returning memory that’s about to go out of scope is discussed in Chapter 9.

Allocating the object off the heap solves the problem:

MyClass* myFunc()
{
MyClass* pMC = new MyClass;
return pMC;
}

Here the memory allocated off the heap is not returned when the variable pMC goes out of scope.

image Programmers allocate memory from the heap if they don’t want the memory to be lost when any particular variable goes out of scope. The programmer is responsible for both allocating and returning heap memory.

Allocating heaps of objects

It is also possible to allocate an array of objects off the heap using the following syntax:

class MyClass
{
public:
int nValue;
};
void fn()
{
MyClass* pMC = new MyClass[5]

// reference individual members like any array
for (int i = 0; i < 5; i++)
{
pMC[i].nValue = i;
}

// uses a different delete keyword to return memory
// to the heap
delete[] pMC;
};

Notice that once allocated, pMC can be used like any other array, with pMC[i] referring to the ith object of type MyClass. Notice also that you use the slightly different keyword delete[] to return arrays of class objects to the heap.

When memory is allocated for you

Many classes (particularly the containers described in Chapter 27) manage heap memory for you. For example, the string class maintains a character string in memory that it allocates off of the heap. The authors of these classes are careful to return heap memory in all the right places so that it's safe to write a function like the following:

string myFunc()
{
string localString;
localString << cin;
return localString;
}

The object localString allocates heap memory when it is created but carefully returns said memory when it goes out of scope at the end of the function. (You will see in Chapters 16 and 17 how this magic is performed.)

Linking Up with Linked Lists

The second most common structure after the array is called a list. Lists come in different sizes and types; however, the most common one is the linked list. In the linked list, each object points to the next member in a sort of chain that extends through memory. The program can simply point the last element in the list to an object to add it to the list. This means that the user doesn’t have to declare the size of the linked list at the beginning of the program — you can add and remove objects from the list by merely unlinking them. In addition, you can sort the members of a linked list — without actually moving data objects around — by changing the links.

The cost of such flexibility is speed of access. You can’t just reach in and grab the tenth element, for example, like you would in the case of an array. Instead, you have to start at the beginning of the list and link ten times from one object to the next.

A linked list has one other feature besides its run-time expandability (that’s good) and its difficulty in accessing an object at random (that’s bad): A linked list makes significant use of pointers. This makes linked lists a great tool for giving you experience in manipulating pointer variables (that's very good).

image The C++ standard library offers a number of different types of lists. You can see them in action in Chapter 27; however, it’s always good to implement your first linked list yourself to get practice in manipulating pointers.

Not every class can be used to create a linked list. You declare a linkable class as follows:

class LinkableClass
{
public:
LinkableClass* pNext;

// other members of the class
};

The key to a linkable class is the pNext pointer. At first blush, this seems odd indeed — a class contains a pointer to itself? Actually, pNext is not a pointer to itself but to another, different object of the same type.

A linked list is similar to a chain of school children crossing the street. The pNext pointer corresponds to a child's arm reaching out and grabbing the child next to him.

Somewhere outside the linked list is a pointer to the first element of the list, the head pointer. The head pointer is simply a pointer of type LinkableClass*:, sort of like the teacher holding onto the first kid in the chain.

image Always initialize any pointer to nullptr, the pointer that doesn’t point to anything, the non-pointer.

LinkableClass* pHead = nullptr;

image For C++ compilers prior to the ’11 standard that don’t implement nullptr, use a hardcoded 0 or an equivalent #define instead:#define NULLPTR 0.

LinkableClass* pHead = NULLPTR;

To see how linked lists work in practice, consider the following function, which adds the argument passed it to the beginning of a list:

void addHead(LinkableClass* pLC)
{
pLC->pNext = pHead;
pHead = pLC;
}

Here, the pNext pointer of the object is set to point to the first member of the list. This is akin to grabbing the hand of the first kid in the chain. For one instruction, both you and the teacher have hold of this first kid in the list. The second line points the head pointer to the object, sort of like having the teacher let go of the kid you’re holding onto and grabbing you. That makes you the first kid in the chain.

Performing other operations on a linked list

Adding an object to the head of a list is the simplest operation on a linked list. Moving through the elements in a list gives you a better idea about how a linked list works:

// navigate through a linked list
LinkableClass* pL = pHead;
while(pL)
{
// perform some operation here

// get the next entry
pL = pL->pNext;
}

The program initializes the pL pointer to the first object of a list of LinkableClass objects through the pointer pHead. (Grab the first kid’s hand.) The program then enters the while loop. If the pL pointer is non-null, it points to some LinkableClass object. Control enters the loop, where the program can then perform whatever operations it wants on the object pointed at by pL.

The assignment pL = pL->pNext “moves” the pL pointer over to the next kid in the list of objects. The program checks to see if pL is null, meaning that we’ve exhausted the list … I mean run out of kids, not exhausted all the kids in the list.

Hooking up with a LinkedListData program

The LinkedListData program shown here implements a linked list of objects containing a person’s name. The program could easily contain whatever other data you might like, such as Social Security number, grade point average, height, weight, and bank account balance. I’ve limited the information to just a name to keep the program as simple as possible.

// LinkedListData - store data in a linked list of objects
#include <cstdio>
#include <cstdlib>
#include <iostream>

using namespace std;

// NameDataSet - stores a person's name (these objects
// could easily store any other information
// desired).
class NameDataSet
{
public:
string sName;

// the link to the next entry in the list
NameDataSet* pNext;
};

// the pointer to the first entry in the list
NameDataSet* pHead = nullptr;

// add - add a new member to the linked list
void add(NameDataSet* pNDS)
{
// point the current entry to the beginning of list
pNDS->pNext = pHead;

// point the head pointer to the current entry
pHead = pNDS;
}

// getData - read a name and social security
// number; return null if no more to read
NameDataSet* getData()
{
// read the first name
string name;
cout << "Enter name:";
cin >> name;

// if the name entered is 'exit'...
if (name == "exit")
{
// ...return a null to terminate input
return nullptr;
}

// get a new entry and fill in values
NameDataSet* pNDS = new NameDataSet;
pNDS->sName = name;
pNDS->pNext = nullptr; // zero link

// return the address of the object created
return pNDS;
}

int main(int nNumberofArgs, char* pszArgs[])
{
cout << "Read names of students\n"
<< "Enter 'exit' for first name to exit"
<< endl;

// create (another) NameDataSet object
NameDataSet* pNDS;
while (pNDS = getData())
{
// add it to the list of NameDataSet objects
add(pNDS);
}

// to display the objects, iterate through the
// list (stop when the next address is NULL)
cout << "\nEntries:" << endl;
for(NameDataSet *pIter = pHead;
pIter; pIter = pIter->pNext)
{
// display name of current entry
cout << pIter->sName << 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, '\n');
cin.get();
return 0;
}

Although somewhat lengthy, the LinkedListData program is simple if you take it in parts. The NameDataSet structure has room for a person’s name and a link to the next NameDataSet object in a linked list. I mentioned earlier that this class would have other members in a real-world application.

image I have used the class string to contain the person’s name. Although I don’t describe all the methods of the string class until Chapter 27, it is much easier to use than zero-terminated character strings. You will see the string class used in preference to character strings in most applications these days. The string class has become about as close to an intrinsic type in the C++ language as possible.

The main() function starts looping, calling getData() on each iteration to fetch another NameDataSet entry from the user. The program exits the loop if getData() returns a null, the “nonaddress,” for an address.

The getData() function prompts the user for a name and reads in whatever the user enters. If the string entered is equal to exit, the function returns a null to the caller, thereby exiting the while loop. If the string entered is not exit, the program creates a new NameDataSet object, populates the name, and zeroes out the pNext pointer.

image Never leave link pointers uninitialized. Use the old programmer’s wives’ tale: “When in doubt, zero it out.” (I mean “Old tale,” not “Tale of an old wife.”)

Finally, getData() returns the object’s address to main().

main() adds each object returned from getData() to the beginning of the linked list pointed at by the global variable pHead. Control exits the initial while loop when getData() returns a null. main() then enters a second section that iterates through the completed list, displaying each object.

This time I used a for loop that is functionally equivalent to the earlier while loop. The for loop initializes the iteration pointer pIter to point to the first element in the list through the assignment pIter = pHead. It next checks to see if pIter is null, which will be the case when the list is exhausted. It then enters the loop. On each round trip through the for loop, the third clause moves pIter from one object to the next with the assignment pIter = pIter->pNext before repeating the test and the body of the loop. This pattern is commonly followed for all list types.

The output of a sample run of the program appears as follows:

Read names of students
Enter 'exit' for first name to exit
Enter name:Randy
Enter name:Loli
Enter name:Bodi
Enter name:exit

Entries:
Bodi
Loli
Randy
Press Enter to continue...

image The program outputs the names in the opposite order in which they were entered. This is because each new object is added to the beginning of the list. Alternatively, the program could have added each object to the end of the list — doing so just takes a little more code. I included just such a version in the programs on the web site. Called LinkedListForward, it links newly added objects to the end of the list so that the list comes out in the same order it was entered. The only difference is in the add() function. See if you can create this forward version before you peek at my solution.

Ray of Hope: A List of Containers Linked to the C++ Library

I believe everyone should walk before they run, should figure out how to perform arithmetic in their heads before using a calculator, and should write a linked list program before using a list class written by someone else. That being said, in Chapter 27, I describe the list class provided by the C++ environment.