| Name: _____________________ | Class: CET 375 |
| SSN/ID: _____________________ | Section & Group: ____________ |
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:
|
Now, add in a statement to include the iostream header file:
#include <iostream> using namespace std;
#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);
}
int main() {
DataClass DataObject(1);
DataObject.PublicDataMember = 2;
}
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;
};
#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:
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;
}
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>).