User:Jimregan/C Primer chapter 6

From Wikibooks, open books for an open world
Jump to navigation Jump to search

Programming In C++: Classes

The central core concept of C++ is the class, and this chapter describes its essential features in an introductory fashion. The ramifications of classes are highly complicated, and this chapter does not, could not, explain them in detail. It simply provides a basis for further study.

C++ Classes

In C, you define variables of various types and create functions to manipulate them. In C++, you define variables and functions and bind them together as a new data type, or "class".

We've already seen how in C++ structures can encapsulate both data and functions. A C++ class is an extension of the definition of C++ structures that adds several important features.

To start off the discussion, let's define a class to define cubes. This example has a few new features which will be explained in following text:

  // clscube.cpp
  #include <iostream.h>
  class Cube                         // Cube class.
  {
    private:
      int height, width, depth;      // Private data members.
    public:
      Cube( int, int, int );         // Constructor function prototype.
      ~Cube();                       // Destructor function prototype.
      int volume();                  // Member function prototype.
  };
  Cube::Cube( int h, int w, int d )  // Constructor function definition.
  {
    height = h;
    width = w;
    depth = d;
  };
  Cube::~Cube()                      // Destructor function definition.
  {
    // Does nothing.
  };
  int Cube::volume()                 // A member function (computes volume).
  {
    return height * width * depth;
  };


  void main()                        // This program demonstrates the class.
  {
    Cube somecube( 3, 3, 3 );        // Allocate a cube.
    cout << somecube.volume() << '\n';
  }

This example has a few interesting new features. The most important feature is that the class definition is divided into two sections: a "public" section (which, in this case, contains the class's function definitions) and a remaining "private" section (containing variables in this case) which is hidden from view from outside the class.

You could define either functions or variables as either "public" or "private", but in practice the idea is to hide the variables and only allow access to them through functions, so the division in this example is typical and is the recommended usage.

This is one of the essentials of the class concept: to hide data details from the program, allowing it to manipulate data elements through member functions without any knowledge of the actual data structure. This allows a considerable degree of data independence.

The default is "private". If you don't specify if class element is "public" or "private", it is automatically defined as "private".

You can also use "public" and "private" in structure definitions, but once you do so, you might as well declare a class and be done with it. There are some other keywords related to "public" and "private", but we'll put off discussion of them to later.

Note the definition of the function "Cube()":

  Cube::Cube( int h, int w, int d )

This is a special member function, called a "constructor", that is used to initialize an instance of a class when it is created. The declaration of an instance of a class automatically and transparently invokes the constructor.

In this example, an instance of the class is declared and allocated as follows:

  Cube somecube( 3, 3, 3 );

This declaration invokes the constructor function to initialize the instance.

You can also specify default values if desired in the constructor definition:

  Cube::Cube( int h = 1, int w = 1, int d = 1 )

In this case, if you define an instance of the class without parameters:

  Cube defcube;

-- then its three private variables will all default to a value of 1.

A constructor function doesn't return a value and so it is not declared as a type, not even of type "void".

There is another curious definition here, for the function "~Cube()". This is a "destructor" function, and it is automatically and transparently called when the program exits the context in which the class instance was created.

In this case, the destructor doesn't really do anything, it's just there to illustrate the thing exists. A destructor is useful when the constructor allocates memory that must be returned to the free pool after the class has outlived its usefulness. A destructor is also automatically and transparently invoked when the program context in which the class is defined is exited.

Neither constructors nor destructors are required for class definitions.

  • Member functions, which actually do the work the class was defined to do, are declared as they are in C++ structures:
  int Cube::volume()

The member function, as with C++ structures, of course has direct access to the structure's variables and so they don't need to be specified as parameters.

The class can contain functions defined either as conventional functions or as inline functions, discussed in the previous chapter. C++ notation allows the inline functions to be incorporated directly into the class definition.

For example, the class definition in the example above could be made much more compact by eliminating the unneeded destructor function and writing the remaining constructor and member function in inline form:

  class Cube
  {
      int height, width, depth;   // Implied "private" variables.
    public:
      Cube( int h, int w, int d );
        { height = h; width = w; depth = d; }
      int volume();
        { return height * width * depth; }
  };

This gives the complete class definition. Notice that the inline functions are coded on a single line, which is a common convention that emphasizes their nature as inline functions. It is a good rule of thumb that if an inline function can't be coded on a single line, it shouldn't be an inline function.

It is also possible to have overloaded constructor functions that perform different actions when given different parameters:

  class Cube
  {
      int height, width, depth;
    public:
      Cube();                                  
      Cube( int h, int w, int d );
        { height = h; width = w; depth = d; }
      int volume();
        { return height * width * depth; }
  };

In this class definition, there are two constructor functions, one which has no parameters and doesn't initialize the private variables, and one that had parameters and does.

As a class is a C++ data type, it can imply the use of type conversions. If you assign an "int" value to a "float" variable, C or C++ will automatically perform the appropriate conversion. Of course, this requires a certain amount of smarts on the part of the compiler to recognize the need for the conversion, since the number formats are very different and the bits from the "int" variable need to be rearranged.

Since the C++ compiler doesn't have any more knowledge of a class than you define for it, it can't automatically perform type conversions, so you have to provide functions yourself for your class to do the job. This is a somewhat advanced topic and a detailed discussion of it is beyond the scope of this document, but the following example demonstrates:

  // julie.cpp
  #include <iostream.h>
  class Julian   // Julian (date by number of day in year) date class.
  {
      int day, year;
    public:
      Julian() {}
      Julian( int d, int y ) { day = d; year = y; }
      void display() { cout << year << '-' << day << "\n"; }
      int& getday() { return day; }
  };
  class Date     // Month-day-year class.
  {
      int month, day, year;
    public:
      Date( int m, int d, int y ) { month = m; day = d; year = y; }
      operator Julian();  // Conversion function.
  };
  class Tester   // A class that is initialized to a Julian date.
  {
      Julian jd;
    public:
      Tester( Julian j ) { jd = j; }
      void display() { jd.display(); }
  };
  static int ndays[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
  Date::operator Julian()   // Julian <- Date conversion member function.
  {
    Julian jd( 0, year );
    for( int i = 0; i < ( month - 1 ); i++ )
    {
      jd.getday() += ndays[i];
    };
    jd.getday() += day;
    return jd;
  };
  void dispdate( Julian jd )   // A function that expects a Julian date.
  {
    jd.display();
  };
  void main()   // Illustrate the different ways to perform the conversion.
  {
    Date dt( 03, 25, 95 );
    Julian jd;
    jd = dt;                // Convert Date to Julian by assigment.
    cout << "assignment: ";
    jd.display();
    jd = (Julian) dt;       // Convert Date to Julian by cast.
    cout << "cast: ";
    jd.display();
    jd = Julian( dt );      // Convert by calling conversion function.
    cout << "function: ";
    jd.display();
    cout << "parameter: ";
    dispdate( dt );         // Convert by using as function parameter.
    Tester ts( dt );        // Convert by initialization.
    cout << "init: ";
    ts.display();
  }

This example creates an "operator" function associated with the "Date" class:

  Date::operator Julian()

-- that takes an instance of the "Date" class and converts it to the "Julian" class. The C++ compiler is smart enough to figure out how to use this function in the appropriate circumstances.

One interesting point about this example is that it illustrates the properties of references. In accordance with the good principles of data hiding, the only access to the "day" member function of class "Julian" is through the "getday()" member function:

  int& getday() { return day; }

This function returns a reference to the "day" member variable. This allows the function to be used on both sides of an equation, a concept quite alien to C:

  jd.getday() += ndays[i];

This is possible because "jd.getday()", as a reference, is seen by the formula simply as the "day" member variable, so as far as the equation is concerned this is exactly comparable to:

  jd.day += ndays[i];

The example above converts from one class to another. The conversion could be from a to a simpler data type, such as a "long", if desired.

While date hiding is an essential component of C++'s object-oriented philosophy, like all rules sometimes it is wiser to break it, and C++ actually provides a provision for breaking it in a controlled fashion.

One class can allow another class to access their private member functions by declaring the class as "friend":

  // cpfriend.cpp
  #include <iostream.h>
  class Date;    // "Prototype" for Date class definition.
  class Julian   // Julian (date by number of day in year) date class.
  {
      int day, year;
    public:
      Julian() {}
      Julian( int d, int y ) { day = d; year = y; }
      void display() { cout << '\n' << year << '-' << day; }
      friend Date;   // Date members have access to Julian private members.
  };
  class Date     // Month-day-year class.
  {
      int month, day, year;
    public:
      Date( int m, int d, int y ) { month = m; day = d; year = y; }
      operator Julian();  // Conversion function.
  };
  static int ndays[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
  Date::operator Julian()   // Julian <- Date conversion member function.
  {
    Julian jd( 0, year );
    for( int i = 0; i < ( month - 1 ); i++ )
    {
      jd.day += ndays[i];   // This Date member function has access to
    }                       // Julian private variables.
    jd.day += day;
    return jd;
  }
  void main()
  {
    Date dt( 11, 17, 89 );
    Julian jd;
    jd = dt;
    jd.display();
  }

Since the "Julian" class definition refers to the "Date" class definition before it is defined in the program listing, the "Date" class is "prototyped" to keep the compiler from getting confused:

  class Date;

The "Julian" class defines "Date" as a "friend" function with:

  friend Date;

-- and so the "Date" conversion function can have access to a "Julian" private variable:

  jd.day += ndays[i];

Actually allowing an entire class access to another class is not recommended procedure, however, and it is just as easy to only allow access by one member function. In the example above, the "Julian" definition could be modified by adding the declaration:

  friend Date::Date( Julian );

You can declare an element of a class as "static", in which case all instances of the class access one and the same element. For example, a static class variable is a "global" variable, if just for that particular class. You can also have "static" member functions, which are simply member functions that only access static member variables.

Another related C++ concept is the "this" pointer, which is just a pointer to the instance of an object that a member function is executing in. This may seem a little absurd. After all, if the member function is executing in one instance of a particular object, it obviously has access to that instance's member functions:

  void Date::dispmonth()
  {
    cout << month;                      // Display the month.
    cout << this->month;                // Display the month.
  }

-- but the trick is that the member function can return the pointer to let some other function know what object it is executing in:

  return *this;         // Return object pointed to by "this".

C++ Operator Overloading

Along with the ability to overload functions, C++ provides the ability to overload operators. C itself actually has overloaded operators; "+", for instance, can be used to add a variety of data types even though the addition operation is performed differently for each. C++ simply extends this concept to allow the user to define his or her own overloaded operators.

The following example overloads the "+" operator to allow an "integer" to be added to an instance of the "Date" class, returning an instance of the "Date" class:

  // overops.cpp
  #include <iostream.h>
  class Date     // Month-day-year class.
  {
      int month, day, year;
    public:
      Date( int m, int d, int y ) { month = m; day = d; year = y; }
      void display() { cout << month << '/' << day << '/' << year; }
      Date operator+(int)       // Overloaded "+" operator.
  };
  static int ndays[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
  Date Date::operator+(int n)         // Overloaded "+" operator.
  {
    Date dt = *this;                  // Connect to current object instance.
    n += dt.day
    while( n > ndays[dt.month - 1]    // Add integer number to date.
    {
      n -= ndays[dt.month - 1];
      {
        if( ++dt.month == 13 )

{ dt.month = 1; dt.year++; }

      }
      dt.day = n;
      return dt;                      // Return date object.
    }
  void main()
  {
    Date olddate( 5, 3, 95 );
    Date newdate;
    newdate = olddate + 21;           // Add integer to date.
    newdate.display();
  }

Notice the use of the "this" pointer to return the object so it can be assigned to "newdate". This is an extremely important use of the "this" pointer.

Operator overloading can be performed by similar means for other binary arithmetic operations, for relational operations, increment and decrement operators, array subscripting, and so on. Be warned that operator overloading is tricky, can cause a lot of trouble, and must be done wisely. One C++ guru once noted that C++ programmers first learn to overload operators, and then they learn not to.

C++ Class Inheritance

One of the important features of classes is that, once you define a class, you can build, or "derive", classes from it that are a superset of the original, or "base", class's capabilities. This feature is known as "class inheritance". This scheme allows you to define a general base class -- say, as an arbitrary silly example, a class to describe a bird -- and then derive new classes from it to define more specific types -- say, derived classes to describe hawks, owls, ducks, hummingbirds, and so on.

Let's consider a base class definition for a class named "Time" along with a derived class named "Timezone":

  // derive.cpp
  #include <iostream.h>
  // *** Time class definition.  ****************************************
  class Time 
  {
    protected:
      int hours, minutes, seconds;
    public:
      Time( int hr, int min, int sec );
      void display();
  };
  Time::Time( int hr, int min, int sec )
  {
    hours = hr;
    minutes = min;
    seconds = sec;
  }
  void Time::display()
  {
    cout << hours << ':' << minutes << ':' << seconds;
  } 
  enum timezone { gmt, est, cst, mst, pst };
  // *** TimeZone class definition.  ************************************
  class TimeZone : public Time
  {
      timezone zone;
    protected:
      const char *Zone();
    public:
      TimeZone( int h, int min, int sec, timezone zn );
      void display();
  };
  static const char *TZ[] = { "GMT", "EST", "CST", "MST", "PST" };
  TimeZone::TimeZone( int hr, int min, int sec, timezone zn )
             : Time( hr, min, sec )
  {
    zone = zn;
  }
  void TimeZone::display()
  {
    Time::display();
    cout << ' ' << Zone();
  }
  const char *TimeZone::Zone()
  {
    return TZ[zone];
  } 
  // *** Main program. **************************************************
  void main()
  {
    Time tm( 10, 13, 30 );
    tm.display();
    cout << '\n';
    TimeZone tz( 11, 14, 35, est );
    tz.display();
  }

There are several interesting features in this program. First, the "protected" keyword is used in the "Time" class definition. This is almost the same as the "private" keyword, but it allows classes derived from this base class to have access to that keyword, a privilege they don't get if it is are declared "private".

The "TimeZone" class is derived from the "Time" class with the statement:

 class TimeZone : public Time { ... }

The derived class can declare the base class as "private", "public", or "protected".

Calling a constructor function for a derived class implies calling the constructor function for the base class. You have to specify the relationship of the constructor's parameters between the two classes as follows:

  TimeZone::TimeZone( int hr, int min, int sec, timezone zn )
            : Time( hr, min, sec )

C++ class construction also allows a curious reversal of this relationship. A base class can have a "virtual" function, declared, say, as:

  virtual void display();

-- that, when invoked through a derived class, passes execution back to the overloaded function in the derived class.

Both the base and derived class have a function named "display()". This implies function overloading on the part of the derived class, meaning its version of "display()" will be called rather than that of the base class.

It is possible of course to then derive further classes from the derived class, or derive a class from multiple classes ("multiple inheritance").

C++ Input/Output Streams

Previous examples in this discussion have used the "iostream" capabilities of C++ for console I/O:

  cout << "What's your name? ";
  cin >> name;

The "iostream" capabilities are actually defined by classes that permit general-purpose I/O. These classes are derived by a (mostly hidden) base class named "ios".

These classes have much more functionality than has been used so for. For starters, C++ output is "buffered", meaning that you can output from a program for some time before an output buffer is filled and the buffer is dumped to standard output. You can force the buffer to be dumped using the "flush" operator:

  cout << "Danger, Will Robinson!\n"" << flush;

Other useful new functions allow you to set fixed or format numeric output formats (the default is scientific), the output width, and the padding character used to fill out the field:

  // strsci.cpp
  #include <iostream.h>
  void main()
  {
    pi = 3.141;
    cout.setf( ios::fixed, ios::scientific ); // Set fixed, clear scientific.
    cout.width( 10 );                         // 10 character output width.
    cout.fill("*");                           // Set "*" as pad character.
    cout << pi << '\n';
  }

This prints:

  *****3.141

The "iostream" class also contains a number of member functions that duplicate the functionality of C console-I/O routines:

  // strcons.cpp
  #include <iostream.h>
  void main()
  {
    char c, l[100], s[] = "Banzai!";
    // Output a character.
    cout.put( 'X' ); 
    cout.put( '\n' );
    // Get a character.
    cout << "Input a character, then press Enter: ";
    cin.get( c );
    cout << "You input: " << c << '\n';
    cin.get( c );
    
    // Get a line of text terminated by a newline.
    cout << "Type a line of text, then press Enter: " << "\n\n";
    cin.getline( l, 100 );
    cout << '\n' << "You entered: " << l << "\n\n";
  }

Finally, you can use more or less the same operations for reading and writing files. This requires use of the "fstream" class (which is a superset of the "iostream" class and so supports its functionality). For example:

  // strfile.cpp
  #include <fstream.h> 
  void main()
  {
     int n;
     ofstream outfile( "data.txt" );      // Open output file.
     for( n = 1; n <= 10; ++n )           // Dump text to it.
     {
       outfile << n << '\n';
     }
     outfile.close();                     // Close it.
     ifstream infile( "data.txt" );       // Open it again.
     while( 1 )                           // Read to EOF.
     {
       infile >> n;                       // Get number.
    	if( infile.eof())
    	{
    	  break;
    	}
    	cout << n << '\n';                 // Output it.
     }  
     infile.close();
  }

The "get" and "getline" functions work as well.

v2.0.7 / 6 of 7 / 01 feb 02 / greg goebel / public domain