course.wilkes.edu/CS125Labs
Welcome to the site for CS 125 labs!

 

Lab 10: Building Classes

Prev | Next | Lab 10

Introduction

Most of the problems we have examined have had relatively simple solutions, because the data objects in the problem could be represented using the predefined C++ types. For example, we can represent a menu with a string, a choice from that menu with a char, the radius of a circle with a double, and so on.

The problem is that real-world problems often involve data objects that cannot be directly represented using the predefined C++ types. For example, suppose that we know a certain gourmet chef named PiËrre whose recipes are written to make 12 servings. The difficulty is

  1. PiËrre frequently must prepare a dish for numbers of customers different than 12 (e.g., 1 customer = 1/12 of a recipe, 2 customers = 1/6 of a recipe, 15 customers = 15/12 = 5/4, and so on.)
  2. PiËrre's recipes are written using fractions (e.g., 1/2 tsp., 3/4 cup, etc.) so that he must multiply fractions to reduce the size of a recipe, and
  3. PiËrre is so poor at multiplying fractions, that he has hired us to write a program that will enable him to conveniently multiply two fractions.
We have provided two programs for today's exercise: pierre1.cpp and pierre2.cpp. In the first part of the exercise, we will do what is needed to get pierre1.cpp operational, and in the second part of the exercise, we will extend the first part so that pierre2.cpp is operational.

Getting Started

Begin by making a new directory in which to store your work for this exercise. Then save copies of the files pierre1.cpp, pierre2.cpp, Fraction.h, Fraction.cpp, Fraction.doc, and Makefile10 in this directory. Take a moment to compare the programs in pierre1.cpp and pierre2.cpp. Each program implements the same basic algorithm:

   1. Get oldMeasure, the fractional measure to be converted.
   2. Get scaleFactor, the fractional conversion factor.
   3. Compute newMeasure = oldMeasure * scaleFactor.
   4. Output newMeasure.
Note that a solution to PiËrre's problem is quite simple, given the ability to define, input, multiply and output fraction objects. The two programs differ only in how they perform input and output Fraction objects. Note also that some lines are "commented out" at present. We will be "uncommenting" these lines as we develop the functionality needed in order for such lines to work properly.

The difficulty is that there is no predefined C++ type Fraction by which such objects can be defined or operated upon. In such situations, C++ provides a mechanism by which a programmer can create a new type and its operations. This mechanism is called the class, which is the subject of today's exercise.

Creating Classes

In C++, a new type can be created by

  1. Defining the data objects that make up the attributes of an object of the new type (i.e., the things of which an object of that type consists); and
  2. Surrounding those definitions with a class structure, which has the form:
       class TypeName
       {
        public:

    private:

    };

    where TypeName is a name describing the new type.
As indicated, a class has two sections, a public section and a private section. The public section is where class operations are declared, and the private section is where class attributes are declared.

To illustrate, suppose that we want to define a new type whose objects can be used to store Cartesian coordinates. Such an object has two attributes: an X-value, and a Y-value, both reals. We can define data objects for these attributes as follows:

   double myX,
          myY;
(To distinguish attribute identifiers from other identifiers, we will place the word my at the beginning of a data member's name.) We then surround them with an appropriately-named class structure:
   class Coordinate 
   {
    public:

private: double myX, myY; };

The result is a new type, named Coordinate, which can be used to declare data objects, such as a point:
   Coordinate point;
The object point then has two real components, one named myX and the other named myY.

In general, the data portion of a class definition can be thought of as following this pattern:

   class TypeName
   {
    public:

private: Type1 AttributeList1 ; Type2 AttributeList2; ... TypeN AttributeListN ; };

where each Typei is any defined type; and each AttributeListi is a list of the Typei attributes of an object of type TypeName.

Now, if we apply this approach to the problem we are trying to solve, we see that we need to identify the attributes of a Fraction object. If we examine a few fractions

   1/2     4/3   4/16  16/4
we can see that each fraction has the form:
   number1/number2
where number1 is called the numerator and number2 is called the denominator. The numerator and denominator are different from one fraction to another, and so these quantities must be recorded for any given fraction value; however the / symbol is common to all fractions, and so it need not be recorded as an attribute. A fraction thus has two attributes, its numerator and its denominator, both of which are integers.

Begin editing the file Fraction.h, and define two integer data objects named myNumerator and myDenominator to represent these two attributes. (Yes, you should prepend my to the beginning of each name, for reasons that will be apparent shortly). Then surround these definitions with a class structure that declares the name Fraction as a new type whose objects contain these two attributes. Be sure to arrange your class so that myNumerator and myDenominator are within the class, but following the keyword private:.

Given this declaration of Fraction, object declarations like this

   Fraction oldMeasure;
   ...
   Fraction scaleFactor;
can be thought of as defining two data objects with the following forms:

p1:

The objects within a class object that store its attributes are usually called the data members of that object.

Note that each object of type Fraction has its own copy of each of the attributes we defined. This is why we preface the names of these attributes with my, to indicate that from the perspective of the object, attributes are characteristics of that object. That is, within the Fraction object named oldMeasure, myNumerator refers to its first data member myDenominator refers to its second data member. However, within the object named scaleFactor, myNumerator refers to its first data member myDenominator refers to its second data member. Each object thus has its own separate data members.

Since a class may contain an arbitrary number of attributes, a software model can be constructed for virtually any real-world object, simply by defining data objects for each of its attributes, and then surrounding those data objects with an appropriately-named class structure.

In the source program pierre1.cpp, the definition of oldMeasure is currently commented out. Modify your source program so that this declaration is no longer commented out (but the subsequent lines are). Then compile your source program, to test the syntax of what you have written. When it is correct, continue to the next part of the exercise.

Function Members

Besides having data members, a class can also have functions which are called the function members (or member functions) of the class. Function members provide a means by which the operations on a class object can be encoded.

Function members must be prototyped within the class structure itself; and are normally defined in the class implemention file. However, very simple function members (i.e., that fit on a single line) are by convention defined in the header file, following the class declaration, and prefixed by the keyword inline. (If we designate a function as inline, we are suggesting to the compiler that it replace calls to the function with the actual body of the function, substituting arguments for parameters as appropriate. This should only be done in the header file, not in the implementation file.)

Class Structure and Information Hiding

One of the characteristics of a class is that its data members are kept private, meaning that a program using the class is not permitted to directly access them. (This is a good idea because a program that directly accesses the data members of a class becomes dependent on those particular data members. If those data members are changed (which is not uncommon in class maintenance), then such programs must also be changed, increasing the cost of software maintenance.)

While it is a good idea for the data members of a class to be kept private, we want users of the class to be able to perform operations on class objects. As a result, the operations should be declared as public members, in contrast to the data members.

This is the reason for the keywords public: and private: within the class: the space between public: and private: defines a public section where function members can be declared, and the space between private: and the end of the class defines a private section where the data members can be declared. The pattern is thus as follows:

   class TypeName
   {
    public:
        // Class Operation Declarations - public!
    private:   
        // Class Attribute Declarations - private!
   };
All of the declarations that follow the keyword public: can be accessed by programs using the class; and all of the declarations that follow the keyword private: cannot be accessed by programs using the class.

By convention, the "public section" of a class comes first, so that a user of the class can quickly see what operations it provides. It is good style to have one "public part" and one "private part" in a class; however the keywords public: (and private:) can appear an arbitrary number of times in a class declaration, if multiple public sections are needed.

Function Members As Messages

We have seen that function members are called differently from "normal" functions: if two string objects named greeting and closing are defined as follows:

   string greeting = "hello",
          closing = "goodbye";
then the expression
   greeting.size()
returns the value 5, while the expression
   closing.size()
returns the value 7.

Object-oriented programmers like to think of a call to a function member as a message to which the object responds. To illustrate, when we send the size() message to greeting, greeting responds with the number of characters it contains (5); and if we send the same size() message to closing, closing responds with the number of characters it contains (7). The effect of this approach is to shift the point of view from the function to the object.

Defining a function member is thus a bit different from defining a "normal" function, because the definition of a function member describes what a class object does when it receives that message. Put differently, function members are usually written from the perspective of the class object.

Part I. pierre1.cpp

In this first part of today's exercise, we will focus on adding the function members to class Fraction needed to get pierre1.cpp operational.

An Output Function

To facilitate debugging a class, it is often a good idea to begin with a function member that can be used to display class objects -- an output function.

From the perspective of our Coordinate class, we can specify the task of such a function as follows:

   Receive: out, an ostream to which I am to write my values.
   Output: myX and myY, as a pair.
   Passback: out, containing the output values.
Function members must be prototyped within the class declaration, so if we were to name our function Print(), we would write:
   class Coordinate 
   {
    public:
      void Print(ostream & out) const;
    private:
      double myX,
             myY;
   };
Function members that do not modify the class data members are declared as const function members, by placing the keyword const after the function's parameter list, as shown.

As it happens, this is a fairly simple function, so we would define it within the header file Coordinate.h, following the declaration of class Coordinate, and precede its definition with the keyword inline.

To define Print(), we must inform the compiler that this is a function member, as opposed to a "normal" function. This is done by preceding the name of the function with (i) the name of the class of which the function is a member; and (ii) the scope operator (::). That is, we would define Print() as a function member of our Coordinate class as follows:

   inline void Coordinate::Print(ostream & out) const
   {
      out << '(' << myX << ',' << myY << ')';
   }
As explained earlier
  • inline suggests to the compiler that calls such a simple function should be replaced with the body of the function;
  • void tells the compiler that this function returns nothing;
  • Coordinate:: tells the compiler that this function is a member of class Coordinate;
  • Print is the name of the function;
  • (ostream & out) is the parameter list of the function; and
  • const tells the compiler that this function should not modify any of the data members of class Coordinate.
Note that const must be present in both the prototype and the definition of a function member that does not modify its function members.

Note also that the prefix my helps to reinforce the notion that this is a message to which a Coordinate object responds. That is, if point is a Coordinate object whose X-value is 3 and whose Y-value is 4, then the statement

   point.Print(cout);
displays via cout:
   (3,4)
Similarly ,if origin is a Coordinate object whose X-value is 0 and whose Y-value is 0, then the statement
   origin.Print(cerr);
displays via cerr:
   (0,0)
Note that as a message to an object, a function member must be invoked using dot notation, which specifies the object to which the message is being sent.

Note finally that as a message to which an object responds, a function member can directly access the private data members of the object. (If we were to omit the Coordinate:: in the definition, a compilation error would result.)

Using this information as a pattern, prototype and define a similar Print() function for your Fraction class, such that if oldMeasure is a Fraction whose numerator is 3 and whose denominator is 4, then a message:

   oldMeasure.Print(cout);
will display
   3/4
Prototype this function in the public section of class Fraction, and define it as an inline function following the declaration of class Fraction, in the Fraction.h. Check the syntax of your function, and continue when it is correct.

The Class Constructor

An output operation for a class is of little use unless we are able to define and initialize objects of that class. The action of defining and initializing an object is called constructing that object. To allow the designer of a class to control the construction of class objects, C++ allows us to define a function called a class constructor function that specifies exactly what actions are to be taken when a class object is constructed. When a class object is defined, the C++ compiler calls this function to initialize the object's data members.

For example, suppose we would like for a definition of a Coordinate object:

   Coordinate point;
to initialize the data members of point to zeros. We might specify this task as follows:
   Postcondition: myX == 0.0 && myY == 0.0.
Note that a constructor function does not return anything to its caller, it simply initializes the data members of an object when that object is defined. We specify this behavior through a boolean expression that is true when the function terminates. Such an expression is called a postcondition, since it is a condition that holds when execution reaches the end of the function.

In order for a function to be a function member of a class, its prototype must appear within the class, and the name of a constructor function is always the name of the class, so we declare this function in the public section of class Coordinate, as follows:

   class Coordinate
   {
    public:
      Coordinate();
      void Print(ostream & out) const;
    private:
      double myX,
             myY;
   };
Note that unlike our Print() function, a constructor initializes (i.e., modifies) the data members of a class. As a result, it must not be prototyped or defined as a const function member.

As we said earlier, a function member definition must have the name of the function preceded by the name of the class and the scope operator (::), and simple definitions should be placed in the class header file, designated as inline functions. To define a Coordinate constructor function, we thus write this funny-looking definition:

   inline Coordinate::Coordinate()
   {
      myX = 0.0;
      myY = 0.0;
   }
The first Coordinate is the name of the class, telling the compiler that this is a function member of class Coordinate. The second Coordinate is the name of the function, telling the compiler that this is a class constructor function for class Coordinate.

Given this function member, when a Coordinate object is defined, the C++ compiler will automatically call this function to initialize this object, which sets the object's myX and myY members to zero values.

Note that a constructor has no return type (not even void). As was mentioned earlier, this is because a constructor never returns anything to its caller -- it merely initializes objects of its class.

The pattern for a constructor function definition is thus:

   ClassName::ClassName(ParameterList)
   {
      StatementList
   }
where the first ClassName refers to the name of the class, the second ClassName names the constructor function, and StatementList is a sequence of statements that initialize the data members of the class. Constructors can take parameters, which are defined as they would be for any other function, and any valid C++ statement can appear in the body of such a function.

Using this information, prototype and define a constructor for your Fraction class, that satisifies the following specification:

   Postcondition: myNumerator == 0 && myDenominator == 1.
That is, the definition:
   Fraction oldMeasure;
should initialize the data members of oldMeasure appropriately to represent the fraction 0/1.

Store the prototype in the public section of class Fraction, and define it as inline, following the declaration of class Fraction, in Fraction.h. Test the syntax of what you have written, and continue when it is correct.

Prev | Next | Lab 10

 
  Home

Help

Lab 0

Lab 1

Lab 2

Lab 3a

Lab 3b

Lab 4

Lab 5

Lab 6

Lab 7

Lab 8

Lab 9

Lab 10

Lab 11

Lab 12

Lab 13



Membership

Login




Last update: Thursday, November 30, 2000 at 12:20:18 PM.
visitors to this page.