Name: _____________________ Class: CET 375
SSN/ID:   _____________________ Section & Group: ____________
Classes


Objects and Classes


Objects and Classes are two fundamental concepts in Object-Oriented Programming (OOP) languages like C++. In long, involved programs, the number of variables and functions can become quite profuse, sometimes with hundreds of each! This makes creating and maintaining the program code a very difficult task because you have to keep so many things in mind. There may also be unwanted interaction if various functions use variables of the same name. OOP was invented specifically to help make the management of such large programs easier.

The idea behind objects is quite simple — you just break up the program into the various parts, each of which you can easily conceptualize as performing a discrete task, and those are your objects. For example, you may put all the screen-handling parts of a program together into an object named screen. Objects are more powerful than simply functions or sets of variables because an object can hold both functions and variables wrapped up together in a way that makes it easy to use and isolates the internals from other objects. Our screen object may hold not only all the data displayed on the screen but also the functions needed to handle that data, like drawString() or drawLine(). This means that all the screen handling is hidden from the rest of the program in a convenient way, making the rest of the program easier to handle.

As another example, think of a refrigerator. A refrigerator would be far less useful if you had to regulate all the temperatures and pumps and so forth by hand at all times. Making all those functions internal and automatic to the refrigerator makes it both useful and a much easier object to deal with. Wrapping up code and data this way into objects is the basis of OOP.

All about classes


But how do we create objects? That's where classes come in. A class is to an object as a cookie cutter is to a cookie — i.e., a class acts like a template or blueprint for an object. In terms of programming, you might think of the relationship between a data type (like an integer) and the actual variable itself the same way, as in this example, where we set up an integer named the_data:

int the_data;
This is the way we normally create an integer variable. Here, int is the type of variable we are declaring and the_data is the variable itself. This is the same relationship that a class has to an object and, informally, we may think of a class as an object's type.

For example, if we set up a class named screenclass, we can create an object of that class named screen this way:

screenclass screen;
What's important to remember is this: the object is what holds the data we want to work with; the class itself holds no data but just describes how the object should be set up.

Object Oriented Programming (OOP) at heart is nothing more than a way of grouping functions and data together to make your program less cluttered. A class allows this encapsulation of functions and data and an object can simply be thought of as a variable of that class's type (just as our object screen is to the class screenclass).

Our first example


Let's setup a class called DataClass, along with an object of that class which we'll call DataObject. Neither this class nor the object will do much at first but the example should make it clear how classes and objects work.

Use Emacs to create a new file called classes.cpp (e.g., by typing "emacs classes.cpp &" at the command prompt).

As you build your program, make sure you add appropriate documentation (as per the Programming Style Sheet). In addition, each function prototype should be accompanied by a block comment giving the following:
  • In the first line, a description of what the function does
  • A specification of the function in terms of what it receives, returns, and I/O (if any)
  • Any other information that the person using the library needs to know to use the function
Also, make sure you add in-line comments to describe what the pertinent code does.

Now, add in a statement to include the iostream header file:

#include <iostream>
using namespace std;
Next, let's setup the declaration of our new class, DataClass:
#include <iostream>
using namespace std;

class DataClass {

};
This is how we declare a new class — with the class keyword. The rest of the class's declaration will go between the curly braces, { and }.

A big part of C++ objects is storing data and we can setup some data storage in our DataClass class now. A data item in a class is stored in a variable known as a data member. For example, we can setup a new integer variable (i.e., an integer data member) called PrivateDataMember this way:

#include <iostream>
using namespace std;

class DataClass {
private:
   int PrivateDataMember;

};
Note the private keyword here: this means that only objects of our DataClass have access to this data member. This keyword, private, is called an access modifier.

What the heck are Access Modifiers???


Besides the private access modifier, you can also use the public access modifier, which means that the data member is available to all other parts of the program freely (this means, for example, that you can access the public data member directly via the class or the object in your main() function).

We can put the public keyword to use now, declaring a new data member named PublicDataMember:

#include <iostream>
using namespace std;

class DataClass {
private:
   int PrivateDataMember;

public:
   int PublicDataMember;

};
This data member will be available to all other parts of the program, as we'll see soon.

Next, let's add a function to our DataClass class. Functions that are members of a class are named function members or methods. In this case, we'll add a new method named PublicMethod():

#include <iostream>
using namespace std;

class DataClass {
private:
   int PrivateDataMember;

public:
   int PublicDataMember;
   int PublicMethod(void);
};
The methods of a class usually work with the data internal to the class in a way that ides the details from the rest of the program. This is very useful because it helps divide the program up into manageable sections and, in fact, doing tihs was the original idea behind the use of objects.

Notice that you don't have to pass this method any parameters. All member functions automatically have full access to all member variables. That means you don't have to pass member variables as parameters (either by copy, reference, or as pointers)!

Although we've declared our PublicMethod() method, we still need to write the code (or implementation) for this method. We do that by setting up the definition of this method separately from the class declaration and we preface that definition with DataClass::, indicating which class this method is a part of:

#include <iostream>
using namespace std;

class DataClass {
private:
   int PrivateDataMember;

public:
   int PublicDataMember;
   int PublicMethod(void);
};

int DataClass::PublicMethod(void) {

}
Now we're ready to add some code to this method. the method takes no parameters but it returns an integer value. What value should we return? Because our PrivateDataMember member cannot be reached by the rest of the program directly, let's return that data member in PublicMethod() so the rest of the program can gain access to it.

It's standard practice to give the rest of the program access to our private data using methods like PublicMethod(); this way, we can restrict access to our class's private data. Methods which give access to private data members are called accessors.

#include <iostream>
using namespace std;

class DataClass {
private:
   int PrivateDataMember;

public:
   int PublicDataMember;
   int PublicMethod(void);
};

int DataClass::PublicMethod(void) {
   return PrivateDataMember;
}
Initializing Data in a Class using Constructors


Although we've setup PublicMethod() to return the PrivateDataMember, we haven't initialized that variable to hold any data yet; thus, when we call PublicMethod(), the value it returns will be meaningless. This initialization is handled by the constructor, which allocates memory for, in addition to initializing, all data variables.

A class's constructor is a special method that the program runs automatically when an object of that class is created. You can declare a constructor in a class by declaring a method with the same name as the class itself and with no return value. A constructor can have as many parameters passed to it as you like, however (this would be an example of an explicit-value constructor, as opposed to a default constructor, which takes no parameters whatsoever). Here, we'll pass it a single parameter that is the value we should place in the PrivateDataMember varaible when the object is first created.

In memory, each object contains its own local copies of the data members; however, all objects share a single copy of the function members (and static data members).

Declaring this new constructor looks like this;

#include <iostream>
using namespace std;

class DataClass {
private:
   int PrivateDataMember;

public:
   DataClass(int Value);
   int PublicDataMember;
   int PublicMethod(void);
};


int DataClass::PublicMethod(void) {
   return PrivateDataMember;
}
Now we setup the constructor's definition:
#include <iostream>
using namespace std;

class DataClass {
private:
   int PrivateDataMember;

public:
   DataClass(int Value);
   int PublicDataMember;
   int PublicMethod(void);
};


DataClass::DataClass(int Value) {
   PrivateDataMember = Value;
}

int DataClass::PublicMethod(void) {
   return PrivateDataMember;
}
And we're almost done! We've setup a class with public and private data members and a method that we can call. the only thing left is to make use of this new class and we can do that in the main() function.

Using our DataClass


Add the main() function to classes.cpp now. Recall that the main() function holds the code that will be executed first when the program runs. We can start this new function by declaring an object of our DataClass class. At the saem time, we'll pass a value to the class's explicit-value constructor (this value will be placed in the PrivateDataMember data member). Let's just pass a value of 1 for now:

#include <iostream>
using namespace std;

class DataClass {
private:
   int PrivateDataMember;

public:
   DataClass(int Value);
   int PublicDataMember;
   int PublicMethod(void);
};


DataClass::DataClass(int Value) {
   PrivateDataMember = Value;
}

int DataClass::PublicMethod(void) {
   return PrivateDataMember;
}

int main() {
   DataClass DataObject(1);
}
Now we have instantiated an object named DataObject. This object stores the data we've put into it and it also supports the PublicMethod() method. We can set the value in the public data member, PublicDataMember, using the C++ dot operator ("."), as follows:
int main() {
   DataClass DataObject(1);
   DataObject.PublicDataMember = 2;

}
The general way we refer to the members (both data and function) of an object is by using the dot operator. For example, we can display the value in the PublicDataMember by sending it to the cout stream as follows:
int main() {
   DataClass DataObject(1);
   DataObject.PublicDataMember = 2;

   cout << "DataObject.PublicDataMember = " << DataObject.PublicDataMember << endl;

}
But how do we use the value set in PrivateDataMember? Because the code in the main() function is not part of DataObject, we can't reach that data member directly.

To use the value in the private data members, we'll use the PublicMethod() accessor to display that value:

int main() {
   DataClass DataObject(1);
   DataObject.PublicDataMember = 2;

   cout << "DataObject.PublicDataMember = " << DataObject.PublicDataMember << endl;
   cout << "DataObject.PrivateDataMember = " << DataObject.PublicMethod() << endl;

   return 0;
}
Now our classes program is complete; run it now by first compiling it (by typing g++ classes.cpp at the command prompt) and then executing it by typing ./a.out.


A Longer Example!


The first example was very rudimentary; it really did nothing more than show how to setup a C++ program with a class and an object. That example didn't point out what objects are really good for — holding and working on data internally. Let's see a more real-life C++ example now, pointing out the kinds of internal data-management and manipulations that objects are good for.

In this example, we'll assume that we have to manage the test scores of two sets of students, one set in a CET 375 course and the other in a Comp 270 course. In particular, we can create an object that handles all the details of storing those scores internally and also figure out the average score for the students in each course. Let's put this to work now in an example named courses.cpp.

Create the courses file using Emacs now and setup a new class named Course to store the student's scores and manipulate them:

#include <iostream>
using namespace std;

class Course {

};
Now we'll setup storage for the students' scores as a section of memory named CourseData. We will make the actual variable named CourseData a pointer to the beginning of the data we're storing in memory. We'll allocate the memory at that location only when we know how many students there are in the course — a number that will be passed to us in the class's explicit-value constructor — so we can set aside the appropriate amount of space.

Please remember that a pointer is just a variable that holds an address of a memory location. In C and C++, array names are simply (constant) pointers to the beginning of the array's data in memory. To declare a pointer, you preface the variable's name with an asterisk (*).

We also need to setup an integer index, CourseDataIndex, into our data storage area so we know whwere we are in memory when we add new students' scores:

#include <iostream>
using namespace std;

class Course {
private:
   int * CourseData;
   int CourseDataIndex;

};
We can allocate the actual data storage for the students' scores in the class's constructor. We'll pass the number of students to allocate space for to the explicit-value constructor and setup the new data storage in this manner. Note that we also initialize the data storage index, CourseDataIndex, to 0:
#include <iostream>
using namespace std;

class Course {
private:
   int * CourseData;
   int CourseDataIndex;

public:
   Course(int NumberStudents);
};


Course::Course(int NumberStudents) {
   CourseData = new int[NumberStudents];
   CourseDataIndex = 0;
}
Here, we've used the C++ new operator to allocate the new data storage. In this case, we allocated space for NumberStudents integers. We can treat this new data storage space as an array if we like (the names of arrays and pointers are practically interchangeable in C and C++); and so, we can reference the first integer in it as CourseData[0], the next integer as CourseData[1], etc.

At this point, then, we've setup a data space that we will treat as an array, with space enough to hold NumberStudents integer scores. But, now that we've allocated this memory in our new object, how do we get rid of it when we want to? Besides constructors, C++ also supports destructors, which are called whenever an object is destroyed (e.g., when it goes out of scope) and we can put clean-up code in there — in this case, we'll just deallocate the memory we allocated earlier.

Destructors — more than meets the eye!


A destructor is just like a constructor except that it's automatically called when the object is destroyed. We setup a destructor just like a constructor, except we use a tilde ("~") to preface its name. In the destructor, we'll simply deallocate the memory we allocated with the new operator. The opposite of the new operator is the delete operator, which we use to remove our array:

#include <iostream>
using namespace std;

class Course {
private:
   int * CourseData;
   int CourseDataIndex;

public:
   Course(int NumberStudents);
   ~Course(void);
};


Course::Course(int NumberStudents) {
   CourseData = new int[NumberStudents];
   CourseDataIndex = 0;
}

Course::~Course(void) {
   delete [] CourseData;
}
Now we have our constructor and destructor but there's a new concern: this new class can't exist in a vacuum. We'll need some methods to store and retrieve students' scores in our new class. That is, when a new object of the Course class is created, we'll have to load it with data nd provide some means of working with that data. This is fundamental to objects — their whole purpose is usually to store and work on internal data.

Storing, retrieving, and averaging data


To execute our data-handling tasks, we will add three methods to our Course class:

  1. AddScore() to add a new student score to the object's internal array — a mutator
  2. GetScore() to retrieve a score from the object's internal (private) data array — an accessor
  3. AverageScore() to return the average score of all students in this course — an accessor
These three methods point out the standard methods of data-handling: saving data (mutators), retrieving data (accessors), and manipulating data (accessors). The AddScore() and GetScore() methods are easy to write so we'll add them now.

When we call AddScore() we just want to add a new score the object's internal (private) data storage of scores so we write AddScores() like this:

#include <iostream>
using namespace std;

class Course {
private:
   int * CourseData;
   int CourseDataIndex;

public:
   Course(int NumberStudents);
   ~Course(void);
   void AddScore(int Score);
};


Course::Course(int NumberStudents) {
   CourseData = new int[NumberStudents];
   CourseDataIndex = 0;
}

Course::~Course(void) {
   delete [] CourseData;
}

void Course::AddScore(int Score) {
   CourseData[CourseDataIndex++] = Score;
}
In AddScore(), we simply store the new score (passed to us as a parameter) in the internal array of scores and increment the array index. Now, let's write GetScore(), in which we get the score at a certain indx in the internal (private) data storage and return it.

First in GetScore(), we check to make sure the index we're supposed to use does not exceed the total number of scores we've saved so far. If we are being asked for a valid score, we return that score; otherwise, we are being asked for a score that we haven't even stored yet, so we return a value of -1 in the if statement's else clause (executed when the conditional for the if clause is false): this way:

#include <iostream>
using namespace std;

class Course {
private:
   int * CourseData;
   int CourseDataIndex;

public:
   Course(int NumberStudents);
   ~Course(void);
   void AddScore(int Score);
   int GetScore(int Index);
};


Course::Course(int NumberStudents) {
   CourseData = new int[NumberStudents];
   CourseDataIndex = 0;
}

Course::~Course(void) {
   delete [] CourseData;
}

void Course::AddScore(int Score) {
   CourseData[CourseDataIndex++] = Score;
}

int Course::GetScore(int Index) {
   if (Index <= CourseDataIndex) {
      return CourseData[Index];
   } else {
      return -1;
   }
}
All that's left to write now is AverageScore(), which manipulates the stored data by returning the average score of all the students for whom we've saved scores so far. We'll do this by summing all the scores in a floating point variable named Sum and then dividing that value by the total number of students to get the average score (which we'll return to the rest of the program).

To work with the floating point variable Sum, we have to temporarily treat the integer scores as floating point numbers, which we do with a cast, like this:

(float) CourseData[loop_index]
This lets us treat the integer value at CourseData[loop_index] as a floating point temporarily so we can add it to our running total (note that we also return a value of -1 if there is no data to average). Then, we can divide the Sum by the total number of scores to find the average, which we subsequently return as the AverageScore() method's return value:
#include <iostream>
using namespace std;

class Course {
private:
   int * CourseData;
   int CourseDataIndex;

public:
   Course(int NumberStudents);
   ~Course(void);
   void AddScore(int Score);
   int GetScore(int Index);
   float AverageScore(void);
};


Course::Course(int NumberStudents) {
   CourseData = new int[NumberStudents];
   CourseDataIndex = 0;
}

Course::~Course(void) {
   delete [] CourseData;
}

void Course::AddScore(int Score) {
   CourseData[CourseDataIndex++] = Score;
}

int Course::GetScore(int Index) {
   if (Index <= CourseDataIndex) {
      return CourseData[Index];
   } else {
      return -1;
   }
}

float Course::AverageScore(void) {
   float Sum = 0;

   if (CourseDataIndex == 0) {
      return -1;
   }

   for (int loop_index = 0; loop_index < CourseDataIndex; loop_index++) {
      Sum += (float) CourseData[loop_index];
   }

   return Sum / (float) CourseDataIndex;
}
That's it! We've now completed our Course class now, complete with data storage, retrieval, and handling methods. It's time to put this class to work!

Using the Course class in our program


We will put the Course class to work in a main() function, which we add to courses.cpp. Start by declaring two ints to store the number of students in each class. Then prompt the user for the number of students in each. Finally, add two Course objects: CETCourse to keep track of CET 375 students and CompCourse to keep track of our Comp 270 students:

int main() {
   int cet_students, comp_students;

   cout << "Please enter number of students in CET 375: ";
   cin >> cet_students;
   cout << "Please enter number of students in Comp 270: ";
   cin >> comp_students;

   Course CETCourse(cet_students);
   Course CompCourse(comp_students);

   return 0;
}
Next, let's fill up the internal arrays in each of these objects iwth data, using our AddScore() methods. We'll use two new ints, iloop (to keep track of our looping) and iscore (to keep track of the student's score). Finally, we'll print out the average scores for the two courses.
int main() {
   int cet_students, comp_students;

   cout << "Please enter number of students in CET 375: ";
   cin >> cet_students;
   cout << "Please enter number of students in Comp 270: ";
   cin >> comp_students;

   Course CETCourse(cet_students);
   Course CompCourse(comp_students);

   int iloop  = 0;
   int iscore = 0;
   while (iloop < cet_students) {
      cout << "Please enter score for CET375 student " << ++iloop << ": ";
      cin >> iscore;
      CETCourse.AddScore(iscore);
   }

   iloop  = 0;
   iscore = 0;
   while (iloop < comp_students) {
      cout << "Please enter score for Comp270 student " << ++iloop << ": ";
      cin >> iscore;
      CompCourse.AddScore(iscore);
   }

   cout << "Average score for CET 375: " << CETCourse.AverageScore() << endl;
   cout << "Average score for Comp 270: " << CompCourse.AverageScore() << endl;

   return 0;
}
The courses.cpp program is now ready to run. Compile it and run it now to make sure it works as advertised. As you can see, using objects, we were able to break the program up into smaller, more easily managed units, and that's their whole point. In this way, we've gotten some insights into what objects do best and gotten off to a good start with OOP (Object Oriented Programming)!

Hand In: This lab handout with the answers filled in attached to a listing of your final programs, classes.cpp and courses.cpp
(use the enscript command from your Programming Style Sheet to print it out:
enscript -E -G -2rj -M Letter -PECT2_PS <filename>. You can also use a2ps as in: a2ps <filename>).


Ricky J. Sethi <rickys at sethi.org>
Last modified: Tue Mar 29 22:32:55 PST 2005