Classes, objects


Classes are a major expansion on the abstract data type concept: encapsulating all the data and operations associated with a kind of item into a single logical unit.

For instance, if we created a List class, it might include all the information and routines needed to use a list of items. We could then declare and use individual list variables, or objects.

In fact, classes go further than this by also providing mechanisms to limit what parts of the class are accessible by other parts of the program, by creating routines which run automatically when an object is created or destroyed, and by allowing one class to be based on another, automatically inheriting all the properties and functionalities of the original and expanding on them.

In C++, the syntax for classes is based on the struct syntax. For example, to declare a List class we could use

class List {
   // listing of the class data fields and routines would go here
};
To declare actual list variables (each one presumably capable of storing and handling its own list of items) we would use the syntax:
   List  L;          // a List variable named L
   List anotherone;  // and another list variable
   List mylist;      // and another

Fields

The data stored within each object is divided into fields, just as with structs, e.g. suppose our list stores data as an array of integers, and keeps track of how many items are currently stored in the list:
class List {
   int listItems[100];  // array for storing the list items
   int currentItems;    // track how many items are stored right now
};
We can access the fields using the same syntax as with structs, e.g.
List L1, L2;  // L1 and L2 are both Lists
L1.listItems[0] = 10;  // put value 10 in the first spot in L1's array
L1.currentItems = 1;   // update how many items L1 holds
// put two items in L2 and update its currentItems field
L2.listItems[0] = 3;   
L2.listItems[1] = 7;
L2.currentItems = 2;

Methods

The routines that operate on a class are called methods, and the syntax is very similar to the syntax for functions.

Each method is given a prototype in the class definition, e.g.

class List {
   // data fields
   int listItems[100];  // array for storing the list items
   int currentItems;    // track how many items are stored right now

   // operations to manipulate the list
   bool insert(int i);  // routine to insert value i into the list
                        // and return true if successful, false otherwise
   bool remove(int v);  // routine to search the list for value v
                        // and remove it, returning true iff successful
};
As with data fields, we apply an operation to a specific list by specifying which list variable and which method, e.g.
List L;
// try to insert value 1 into list L,
//     and print a message based on whether 
//     the insert routine returned true or false
if (L.insert(1)) {
   cout << "inserted value 1 successfully" << endl;
} else {
   cout << "something when wrong when trying to insert value 1" << endl;
}
The syntax for implementing the routines needs to specify the name of the class and the name of the method being implemented, so for the two routines above the implementations might look something like the example below.

(Note that each method in the class automatically has access to all its data fields.)

// define the class itself
class List {
   // data fields
   int listItems[100];  // array for storing the list items
   int currentItems;    // track how many items are stored right now

   // operations to manipulate the list
   bool insert(int i);  // routine to insert value i into the list
                        // and return true if successful, false otherwise
   bool remove(int v);  // routine to search the list for value v
                        // and remove it, returning true iff successful
};

// then we can provide full implementations of the routines 

// first the insert routine for the List class
bool List::insert(int i)
{
   if (currentItems >= 100) {
      // the list is already full, so we cannot insert
      return false;
   } else {
      // insert value i in the next available spot and
      // increment the count of the number of stored items
      listItems[currentItems] = i;
      currentItems++;
      return true;
   }
}
// next the remove routine for the List class
bool List::remove(int v)
{
   // if there is nothing in the list return false
   if (currentItems < 1) {
      return false;
   }

   // otherwise search for the first spot containing v
   int p = 0;
   while ((p < currentItems) && (listItems[p] != v)) {
      p++;
   }

   // if we never found v then simply return false
   if (p >= currentItems) {
      return false;
   }

   // otherwise 'remove' v by moving the last item
   //    in the list into v's spot
   // and then decrementing the number of items 
   //    stored in the list
   currentItems--;
   listItems[p] = listItems[currentItems];
   return true;
}

Constructors and destructors

One of the things we haven't discussed yet is how to initialize the fields in a class.

A constructor is a routine that is automatically run when a class variable (object) is first declared, and a destructor is a routine that is automatically run when a class variable is destroyed/deleted.

In the case of C++, a constructor always has the same name as the class itself, and a destructor has the same name but with the ~ symbol tacked on before hand. Constructors and destructors never have return types.

class List {
   // data fields
   int listItems[100];  // array for storing the list items
   int currentItems;    // track how many items are stored right now

   // operations to manipulate the list
   List();              // constructor to initialize the list
   ~List();             // destructor to do any necessary cleanup on
                        //    termination of the list
   bool insert(int i);  // routine to insert value i into the list
                        // and return true if successful, false otherwise
   bool remove(int v);  // routine to search the list for value v
                        // and remove it, returning true iff successful
};

// constructor
List::List()
{
   // automatically initialize the number of stored items to 0
   //    when a list is first created
   currentItems = 0;
}

// destructor
List::~List()
{
   // doesn't really need to do anything in this example
}

Access permissions

We can also control which parts of a program can access the fields and methods of a class by dividing them up into three sections: public, private, and protected.

Public fields/methods can be accessed from any part of the program that has access to the object itself, but private fields/methods can only be accessed from inside the methods of that class. (We'll discuss protected fields/methods when we discuss inheritance.)

Typically we make the data fields private, so that only the class methods can directly access the data fields (ensuring that the only way the data content is adjusted is through our carefully designed and debugged class methods).

class List {
   private:
      // data fields
      int listItems[100];  // array for storing the list items
      int currentItems;    // track how many items are stored right now

   public:
      // operations to manipulate the list
      List();              // constructor to initialize the list
      ~List();             // destructor to do any necessary cleanup on
                           //    termination of the list
      bool insert(int i);  // routine to insert value i into the list
                           // and return true if successful, false otherwise
      bool remove(int v);  // routine to search the list for value v
                           // and remove it, returning true iff successful
};
In the example above, the list methods (constructor, destructor, insert and remove) are the only things that can access currentItems and listItems, but any part of the program can call the methods themselves.
int main()
{
   List L;  // note the constructor automatically runs here on L
   
   // try a couple of inserts
   bool result = L.insert(1);
   result = L.insert(17);
   result = L.insert(7); 

   // try a remove
   result = L.remove(17);

   return 0;  // note the destructor automatically runs here on L
}

Example: a price class

The example below provides a class for storing prices (in dollars and cents) and a short application using the class.
// the class definition
#include <iostream>
#include <sstream>
#include <string>
using namespace std;

// class to store and manipulate prices as a combination of
//    dollars and cents (100 cents per dollar)
class price {
   public:
      // constructor to initialize price to 0.00
      price();

      // constructor to initialixe price to d.cc
      price(long d, long cc);

      // destructor 
      // (currently displays price just to show when it runs)
      ~price();

      // return price as string, e.g. "$5.99"
      string priceString();

      // copy dollars to d and cents to c
      void get(long &d, long &c);

      // set dollars to d and cents to c
      void set(long d, long c);

      // add d to dollars and c to cents,
      // then fix so cents are in range 0..99
      void adjust(long d, long c);

      // multiply dollars and cents by n,
      // then fix so cents are in range 0..99
      void multiply(long n);

   private:
      // store dollars and cents seperately
      long dollars, cents;

      // method to correct cents to range 0..99
      void fix();
};
// a sample main routine using the class 
int main()
{
   // create a price using the default constructor
   // and display the price as a formatted string
   price p1;
   cout << p1.priceString() << endl;

   // set the price to 1 dollar, 25 cents
   // and display the price as a formatted string
   p1.set(1, 25);
   string s;
   s = p1.priceString();
   cout << "formatted price after set(1,25): " << s << endl;
   cout << endl;

   // set the price to 5 dollars and 99 cents using the
   //     alternate constructor
   price p2(5, 99);

   // copy the dollar and cents values using the get method
   //    then display them
   long d, c;
   p2.get(d, c);
   cout << "price(" << d << "," << c << "): " << p2.priceString() << endl;

   // multiply the price by 2 then display it
   p2.multiply(2);
   cout << "multiplied by 2: " << p2.priceString() << endl;
   cout << endl;

   // use new to dynamically allocate a price and 
   //     store its address in a pointer
   price *p3;
   p3 = new price(3, 75);
   if (p3 != NULL) {
      // if new succeeded display the price
      cout << "price(3, 75) formatted: " << p3->priceString() << endl;
      // add 1 dollar and 50 cents to the price
      //     then display the result
      p3->adjust(1, 50);
      cout << "after adding 1.50: " << p3->priceString() << endl;

      // run the destructor on the price
      delete p3;
      cout << endl;
   }

   // at the end of main the destructors for p1 and p2 are
   //    run automatically (since they are local to the main
   //    routine, and the main routine is  now over)
   return 0;
}
// resulting output 
$0.00
formatted price after set(1,25): $1.25

price(5,99): $5.99
multiplied by 2: $11.98

price(3, 75) formatted: $3.75
after adding 1.50: $5.25
destructor for $5.25

destructor for $11.98
destructor for $1.25
// implementation of the class methods

// sets dollars and cents to 0
price::price()
{
   dollars = 0;
   cents = 0;
}

// set dollars to d and cents to c and
// correct so that cents is in range 0.99
price::price(long d, long c)
{
   dollars = d;
   cents = c;
   fix();
}

// just here to show when destructor runs
price::~price()
{
   cout << "destructor for " << priceString() << endl;
}

// correct dollars/cents so that cents is in range 0.99
void price::fix()
{
   if (cents > 99) {
       dollars += cents/100;
       cents = cents%100;
   } else if (cents < 0) {
       dollars -= cents/100;
       cents = 100 - cents;
   }
}

// return string in format $x.yy
// where x is dollars and yy is cents
string price::priceString()
{
   ostringstream result;
   if (dollars < 0) result << "-$" << (-dollars) << ".";
   else result << "$" << (dollars) << ".";
   if (cents < 10) result << "0";
   result << cents;
   return result.str();
}

// copy out dollars and cents
void price::get(long &d, long &c)
{
   d = dollars;
   c = cents;
}

// set dollars to d, cents to c and
// correct so that cents is in range 0.99
void price::set(long d, long c)
{
   dollars = d;
   cents = c;
   fix();
}

// add d to dollars, c to cents
// (either/both can be positive/negative)
// and correct so that cents is in range 0.99
void price::adjust(long d, long c)
{
   dollars += d;
   cents += c;
   fix();
}

// multiply both dollars and cents by n and
// correct so that cents is in range 0.99
void price::multiply(long n)
{
   dollars *= n;
   cents *= n;
   fix();
}