C++ Language Basics Part II: Classes

(member functions, private versus public, constructors)


C++ provides the notion of a class which is very useful for implementing Abstract Data Types. A class in C++ is much like a struct in C, but with two important differences:

  1. A class may have member functions. These are elements of a class that are functions rather than variables. Member functions help make clear what operations may be performed on the data.
  2. A class may make certain fields private. This means that the fields are inaccessible to the outside world except by using the member functions. Making implementation details private helps enforce data abstraction.

Let's consider an example of implementing a stack of characters by using an array. To simplify matters, we assume that we will never need to have more than 100 characters in the stack at any one time. First, in the standard way using structs, we might do it like this (to be clear, we are using ``stack_ptr'' instead of ``Stack'' to mean a pointer to a stack_struct):



#define MAX_SIZE 100

/* Stack of CHARs. A stack_ptr is a pointer to a stack_struct.                 */
/* array[0] is bottom of stack and array[counter-1] is top of stack.           */
struct stack_struct {
  char array[MAX_SIZE];
  int counter;
};
typedef struct stack_struct *stack_ptr;

stack_ptr init(void)                    /* initialize  */
{
  stack_ptr s = new stack_struct;
  s->counter = 0;
  return s;
}

void push(stack_ptr s, char c)          /* Push.  Exit if full */
{
  assert(s->counter < MAX_SIZE); /* Quit if count >= MAX_SIZE (needs assert.h) */
  s->array[s->counter] = c;
  s->counter++;
}

char pop(stack_ptr s)                   /* Pop. Return '\0' if empty */
{
  if(s->counter == 0) return '\0'; /* special if empty. */
  s->counter--;
  return s->array[s->counter];
}

int is_empty(stack_ptr s)                /* returns 1 if empty and zero otherwise */
{
  return (s->counter == 0);
}

Now, let's do this with classes. To follow C++ convention, instead of our basic quantity being a pointer as above, a Stack will be an actual object (a class, like a struct). The reason that we used stack_ptrabove is that C is call-by-value. When we write push(s,'a') we don't want the character 'a' to be pushed onto a copy of s (which would then get thrown away when push returned), we want it pushed onto the stack s itself.

First, here is what the class definition looks like:


#define MAX_SIZE 100

class Stack {
  // First, the member functions.  These are the interface.  The keyword
  // "public" means that they can be called by other (non-member) functions.
 public:
  Stack();                    // constructor: like "init"
  void push(char c);
  char pop(void);
  int is_empty(void);

  // The next part is just like in the struct, but it's "private", which
  // means only the member functions are allowed to access this data.
 private:
  char array[MAX_SIZE];
  int counter;
};

What's going on:

First, if you look at the lower part of the definition, you will see it is just like the earlier struct, except the fields are labeled as ``private''. This means that they can only be accessed through the member functions. The purpose for doing this is to enforce our interface. We can be sure no other function we write will mistakenly access or modify the data directly.

The upper part gives function prototypes for the member functions we will use. We have push, pop, and is_emptyas before. You will notice that these don't take a stack as an argument any more. Also, the initializer, which is now called a constructor, has been given the name ``Stack'' and does not have a return type. We will get back to why this is in a moment.

The class definition itself can be placed in a header (.h) file, just like a struct definition.


Writing member functions

Let's now write some member functions. These go into a .c (or .C or .cpp) file. The C++ notation takes a little getting used to. The main things to notice are (1) the names all begin with ``Stack::'', (2) they don't take a stack as an argument, and (3) inside a member function you are ``inside the Stack'', and the fields ``array'' and ``counter'' are just like regular variables. The ``::'' is called the scope operator. Let's begin with push, pop, and is_empty.


void Stack::push(char c)
{
  assert(counter < MAX_SIZE); // Quit if count >= MAX_SIZE (needs assert.h)
  array[counter] = c;
  ++counter;
}

char Stack::pop(void)
{
  if (counter == 0) return '\0';
  --counter;
  return array[counter];
}

int Stack::is_empty(void)
{
  return (counter == 0);
}

These functions are just like the ones we had before, except we can access the fields (array, counter) just like they were variables. The prefix ``Stack::'' tells the computer that these functions are member functions of the Stack class. Let's now write the constructor.


Stack::Stack(void)
{
  counter = 0;
}
Notice two things. First, the constructor has no return type, and second, it doesn't allocate any space. The reason has to do with the meaning of a constructor, which we get to in the next section called....


Using member functions

Here is how we might use the member functions written above. First, to create two stacks called ``s'' and ``t'', we just say:

    Stack s, t;

What happens when we do this is that two classes get created. Then, for each one, the constructor is called, whose job is to initialize the values as we want. For instance, if we changed the constructor to set counter = 1 instead of setting counter to 0, then s and t would have their counter fields set to 1. The reason the constructor doesn't have to allocate a new stack and doesn't have a return type is that its job is just to initialize the values.

Now, say we want to push the characters 'a' and 'b' in order onto s, and 'c' and 'd' in order onto t. To do that we just write:


    s.push('a');
    s.push('b');
    t.push('c');
    t.push('d');
The member function is just like a field in the structure. If you want to think in an ``object-oriented'' way, you can think of writing s.push('a') as asking the Stack s to push the character 'a' onto itself. If we now write:

    cout << t.pop() << endl;
we will get the character 'd' printed out.

If we write:


    cout << s.pop() << endl;
we will get the character 'b' printed out.

We could also write a loop, like


    while(! s.is_empty()) {  blah, blah, blah; }
But, if we try to directly access one of the private fields from a non-member function, like

void foo(void)
{
  Stack s;
  s.counter = 3;
}
then we will get an error, since counter is private.

We could also define a ``stack_ptr'' to be a pointer to a Stack and then use the ``->'' instead of the ``.'' notation. For example,


typedef Stack *stack_ptr;
int main(void)
{
  stack_ptr p;
  p = new Stack;    // Allocates space, then uses constructor to initialize.
  p->push('a');
  cout << p->pop() << endl;
  return 0;
}
will print out the letter 'a'. In fact, writing p->push('a') is a lot like writing push(p,'a') in our definition using structs.

The reason we defined ``Stack'' to be a class rather than a pointer to a class is that we did not need to worry about passing it into the member functions. If you need to pass a Stack into another function, however, then you should be careful and you probably want to pass a pointer (or a reference).

Try this out in objectcenter or your favorite environment/compiler (remember to include <iostream.h> and <assert.h>). Try out simpler examples, or modifying the code, until it starts to feel comfortable.