250 likes | 262 Views
This article explores the structure of a class, including its public and private sections, interface, and utility functions. It also discusses the implementation of relational operators and assignment operators for the class.
E N D
Class Miscellanea Details About Classes
Review We’ve seen that a class has two sections: class Temperature { public: // ... public members private: // ... private members }; Anything that should be accessible to users of the class should be declared in the public section. Anything else should be declared in the private section.
Private Members The private section should contain a class’s data, plus any “utility” function members useful for implementing the class’s function members, but inappropriate for outside users. class Temperature { public: private: int Compare(const Temperature & temp2) const; double myMagnitude; char myScale; };
temp1 temp2 212 100 myMagnitude myMagnitude myScale F myScale C Object State Temperature temp1(212, ‘F’), temp2(100, ‘C’); Each class object has its own copy of each data member. An object’s data members’ values make up its state. Each object can have a distinct state.
Class Scope A class member (data or function) can be directly accessed by any class functionmember or friend. // member inline char Temperature::Scale() const { return myScale; // private access } // friend inline istream & operator>>(istream & in, Temperature & temp) { in >> myMagnitude >> myScale; } Class friends are not function members, but are “normal” functions with permission to access the private section.
Public Members The public section should contain a class’s operations. class Temperature { public: Temperature(); Temperature(double magnitude, char scale); double Magnitude() const; char Scale() const; Temperature Fahrenheit() const; Temperature Celsius() const; Temperature Kelvin() const; friend istream & operator>>(istream & in, Temperature & temp); friend ostream & operator<<(ostream & out, const Temperature & temp); private: // ... private members };
Class Interface The public operations (and a few default ones) make up the interface, or way that a program can use the class. #include “Temperature.h” // ... Temperature temp1; // declare cin >> temp1; // input double itsMagnitude = temp1.Magnitude(); // read char itsScale = temp1.Scale(); // members Temperature temp2 = temp1.Kelvin(); // convert cout << temp2; // output
Interfaces (Ct’d) With a few exceptions, an operation not in a class’s interface cannot be applied to a class object. if (temp1 == temp2) // error! // ... Since operator== is not defined for Temperature objects, the compiler has no means of knowing how they should be compared. Temperature temp1(212, ‘F’), temp2(100, ‘C’); if (temp1 == temp2) // true or false?
Utilities We can always implement such operations ourselves. If we first define a nifty utility function to compare myself with another Temperature object: int Temperature::Compare(const Temperature & t2) const { Temperature leftTemp = Kelvin() // me in K Temperature rightTemp = t2.Kelvin(); // t2 in K return left.Magnitude() - right.Magnitude(); } Compare() returns: • 0 if I am equal to t2, • a negative value if I am less-than t2, and • a positive value if I am greater than t2.
Operations Then we can easily define the relational operators: bool Temperature::operator==(const Temperature & temp2) { return Compare(temp2) == 0; // true iff I == temp2 } bool Temperature::operator!=(const Temperature & temp2) { return Compare(temp2) != 0; // true iff I != temp2 } bool Temperature::operator<(const Temperature & temp2) { return Compare(temp2) < 0; // true iff I < temp2 } // ...
Assignment For any user-defined class, C++ provides a default assignment operator (operator=). #include “Temperature.h” // ... Temperature temp, saveTemp; cin >> temp; saveTemp = temp; // default assignment // ... This default operation simply copies the members of the object to the right of the = into the members of the object to the left of the =.
class SomeClass { // ... }; #include “SomeClass.h” class AnotherClass { // ... }; SomeClass.h AnotherClass.h #include “SomeClass.h” #include “AnotherClass.h” int main() { // ... }; main.h A Problem A program can consist of multiple files. Suppose a programmer (or programming team) unwittingly #includes a class in two different places? When main.h is compiled, an error will be generated, because the declaration of SomeClass will be compiled more than once.
An Ugly Solution The solution is to use a new directive named #ifndef and “wrap” the class declaration as follows: When an #ifndef directive is processed, if the identifer following it is not defined, compilation proceeds normally. But if it is defined, then all subsequent code is skipped until a #else, #elif, or #endif directive appears. #ifndef TEMPERATURE #define TEMPERATURE // ... #include directives class Temperature { // ... details omitted }; // ... inline definitions #endif
An Ugly Solution The solution is to use a new directive named #ifndef and “wrap” the class declaration as follows: So the first time the file is processed, TEMPERATURE is undefined, and execution proceeds as usual. The first thing following the #ifndef is a #define directive that defines the identifier TEMPERATURE. The class declaration is then processed normally. #ifndef TEMPERATURE #define TEMPERATURE // ... #include directives class Temperature { // ... details omitted }; // ... inline definitions #endif
An Ugly Solution The solution is to use a new directive named #ifndef and “wrap” the class declaration as follows: The second the file is processed, TEMPERATURE is defined, and everything between there and the #endif is skipped. While it isn’t elegant, this ensures that the class declaration is processed just once, regardless of what programmers do. #ifndef TEMPERATURE #define TEMPERATURE // ... #include directives class Temperature { // ... details omitted }; // ... inline definitions #endif
#ifndef SOME_CLASS #define SOME_CLASS class SomeClass { // ... }; #endif #ifndef ANOTHER_CLASS #define ANOTHER_CLASS #include “SomeClass.h” class AnotherClass { // ... }; #endif SomeClass.h AnotherClass.h #include “SomeClass.h” #include “AnotherClass.h” int main() { // ... }; main.h The Bottom Line It is “good form” to always wrap a class declaration this way, using the uppercase name of the class: This will prevent SomeClass (or AnotherClass) from being declared more than once, regardless of what a programmer does.
temp1 temp2 212 100 myMagnitude myMagnitude myScale F myScale C A Final Detail The classes we have seen thus far have “normal” data members: class Temperature { // ... private: double myMagnitude; char myScale; }; Each class object has its own “local” data members:
Question Suppose we want class Temperature to keep track of how many Temperature objects have been declared? One way to do so is to use a static data member: class Temperature { public: private: double myMagnitude; char myScale; static int tempCounter; }; int Temperature::tempCounter = 0; Static data members must be initialized externally (outside of the constructor), in the manner shown.
Constructors inline Temperature::Temperature() { myMagnitude = 0.0; myScale = ‘C’; tempCounter++; } Each constructor can then increment this counter, so that each new Temperature will be counted: inline Temperature::Temperature(double magnitude, char scale) { // ... validity-checking omitted myMagnitude = magnitude; myScale = scale; tempCounter++; }
Static Members We can then add a Count() function member to display the value of this counter: inline int Temperature::Count() const { return tempCounter; } Now, the expression: cout << Temperature::Count() << endl; will display the number of Temperature objects currently in existence. Static members are sometimes called class variables because they belong to the class, not its objects.
Destructors To remain accurate, our counter must be decremented each time a Temperature is destroyed. To do this, we can add a destructor function, whose name is the name of the class preceded by tilde (~): class Temperature { public: // ... ~Temperature(); private: // ... }; inline Temperature::~Temperature() { tempCounter--; }
Destructor Details An object’s destructor is called at the end of its scope. Like a constructor, a destructor has no return-type. Unlike a constructor, a destructor can have no parameters so it cannot be overloaded with multiple definitions (a class can have only one destructor function). Destructors are only needed when some action is needed (often “cleanup”) at the end of an object’s lifetime, such as ensuring the consistency of a static member, deallocating memory allocated by a constructor, and so on.
Summary Data members can be “normal” or static. • Normal data members are local to each class object. • Static data members are shared by all class objects. Data members should be declared as private. Function members can be declared • private, for utility functions • public, for operations on the class The public operations on a class make up its interface.
Summary (Ct’d) #ifndef CLASS_NAME #define CLASS_NAME // ... #include directives class ClassName { // ... details omitted }; // ... inline definitions #endif To avoid “redeclaration” errors, “wrap” every class: and the class declaration will only be processed once.
Summary (Ct’d) A class destructor is a function member: • that has no return-type (like a constructor) • whose name is the name of the class preceded by a tilde (~) • that is called automatically at the end of an object’s lifetime • that is only needed when some action must be taken at the end of an object’s lifetime.