Classes Representing Non-Trivial Objects Problem Write a program that reads a temperature (either Fahrenheit or Celsius), and displays that same temperature in both scales. Preliminary Analysis An object must be directly representable using a single type. A temperature has two attributes: – its magnitude (a double), and – its scale (a character). When an object cannot be directly represented by any of the available types, build a class! Building Classes Begin by defining variables to store the attributes of the object being represented (a temperature) in a class header file (e.g., Temperature.h): double myMagnitude; char myScale; Pretending that we are the temperature object, we begin the name of each attribute with the prefix my, to reinforce this internal perspective. Building Classes We then wrap these variables in a class declaration: class Temperature { public: private: double myMagnitude; char myScale; }; When declared within a class declaration, such variables are called class data members. Information Hiding Classes have a public section and a private section. Items declared in the public section are accessible to users of the class; items declared in the private section are inaccessible to users of the class. Data members should go in the private section, to prevent programmers from writing programs that access data members directly. Why? Software must often be updated. A program that uses a class should still work after the class has been updated. Updating a class may require that its data members be replaced by others. If a program accesses class data members directly, then that program “breaks” if they are replaced. This increases software maintenance costs. A New Type A programmer can now write: #include “Temperature.h” // class Temperature ... Temperature aTemp; and object aTemp can be visualized as follows: aTemp myMagnitude myScale The data members myMagnitude and myScale within aTemp are uninitialized. Operations and Messages Operations on a class object are usually implemented as class function members. A call to a function member: Object.Function() can be thought of as the caller sending the object Object a message named Function. The definition of a function member details what Object does in response to the message. Example: Output Suppose we want to be able to display the value of a Temperature object by sending it the Print() message: aTemp.Print(cout); Using the internal perspective (where I am the Temperature object receiving the message): Specification: Receive: out, an ostream. Output: myMagnitude and myScale, via out. Passback: out, containing the new values. Function Member Definitions In Temperature.cpp: void Temperature::Print(ostream & out) const { out << myMagnitude << myScale; } The function returns nothing, so its return-type is void. The full name Temperature::Print() tells the compiler this is a Temperature function member. The caller passes and changes an ostream argument, so we need a reference parameter to store this argument. Function members that change no data members should be defined as const function members. Function Member Prototypes class Temperature { public: void Print(ostream & out) const; private: double myMagnitude; char myScale; }; By declaring the prototype within the class, we tell the compiler that class Temperature has a function member named Print(), and that Temperature objects should “understand” the Print() message. Most function prototypes go in the class public section. Problem At present, a Temperature declaration: Temperature aTemp; leaves aTemp’s data members of uninitialized. To auto-initialize them to a default value (e.g., 0C), we can use a special function called a constructor. Constructors A constructor function is a class function member whose task is to initialize the class data members. Because they just initialize data members, they don’t return anything, and so their specification is often given as a postcondition (a boolean expression that is true when the function terminates). Default Constructor Specification: Postcondition: myMagnitude == 0.0 && myScale == ‘C’. Constructor Definition Temperature::Temperature() { myMagnitude = 0.0; myScale = ‘C’; } Since it returns nothing, a constructor has no return type (not even void). The name of a constructor is always the name of the class (in this case Temperature()). As a function member of class Temperature, its full name is Temperature::Temperature(). Constructor Prototype class Temperature { public: Temperature(); void Print(ostream & out) const; private: double myMagnitude; char myScale; }; Since they specify the first thing a user of the class needs to know (i.e., how to define class objects), constructor prototypes are usually the first function members listed in the public section of the class. Object Definitions A programmer can now write: Temperature aTemp; and object aTemp can be visualized as follows: aTemp myMagnitude myScale 0.0 C The class constructor is automatically called whenever a class object is defined. Testing To test this much, we can write: // ... documentation // ... other #includes #include “Temperature.h” int main() { Temperature aTemp; aTemp.Print(cout); // displays 0C } Problem 2 At present, we can only initialize a Temperature to a default value: Temperature aTemp; We have no means of initializing a Temperature to any other value. To initialize a Temperature to a particular value (e.g., 98.6F), we can overload the constructor with a second definition. Constructor 2 To overload the constructor, we just provide a second definition (and prototype) that differs from all other constructors in at least one parameter. To initialize the data members of our class, this second constructor must receive the initial values via its parameters. Constructor 2 Specification: Receive: magnitude, a double; scale, a char. Precondition: scale == ‘F’ || scale == ‘C’. Postcondition: myMagnitude == magnitude && myScale == scale. Constructor 2 Definition Temperature::Temperature(double magnitude, char scale) { assert(scale == ‘F’ || scale == ‘C’); myMagnitude = magnitude; myScale = scale; } As before, we place this (second) constructor definition in the implementation file -- Temperature.cpp. Constructor 2 Prototype class Temperature { public: Temperature(); Temperature(double magnitude, char scale); void Print(ostream & out) const; private: double myMagnitude; char myScale; }; The same name can be used to define different functions, provided the signature (the list of the parameter types) of each function is different. Object Definitions A programmer can now write: Temperature temp1, temp2(98.6, ‘F’); and temp1 and temp2 are defined as follows: temp1 myMagnitude myScale 0.0 C temp2 myMagnitude myScale 98.6 F The compiler uses the number of arguments in a declaration to decide which constructor to use in initializing an object. Testing To test this much, we can write: // ... documentation // ... other #includes #include “Temperature.h” int main() { Temperature temp1, temp2(98.6, ‘F’); temp1.Print(cout); cout << endl; temp2.Print(cout); } // displays 0C // displays 98.6F Summary When no existing type is appropriate to represent an object, the C++ class allows a type to be created. Classes have data members (private), and function members (public). In its definition, a function member’s name must be preceded by the name of the class and the scope operator (::). Class constructor functions allow class objects to be initialized to default, or to specified values.