CS302 Lecture notes -- Objects instead of Structs: The Stack Example

  • Jim Plank, patterned after notes by Brad Vander Zanden.
  • Directory: /blugreen/homes/plank/cs302/notes/Stack
  • Lecture notes: http://www.cs.utk.edu/~plank/plank/classes/cs302/302/notes/Stack/index.html
    This lecture covers the stylistic differences between C and C++. Remember from the first lecture that the primary stylistic difference is that C is procedurally-oriented whereas C++ is object-oriented. In other words, in C the data structure is passed to a procedure, along with any data that the procedure needs in order to manipulate the data structure. By contrast, in C++, you list the name of the object first, then the method that should be called to manipulate the object, and then the data that should be passed to the method to aid in this manipulation. Also recall that in a C struct only data can be declared but that in a C++ class both data and methods can be declared.

    In this lecture, we cover how you would implement a general-purpose stack in both C and C++. Part of the goal is to get you to start thinking in an object-oriented manner.


    Stacks in C

    Suppose we wanted to implement a general-purpose stack data structure in C. One rather elegant way to do this is to define a (void *) called a Stack, and define procedures that work on this (void *). For example, here is c_stack.h:

    #ifndef _JSTACK_
    #define _JSTACK_
    #include "jval.h"
    
    typedef void *Stack;
    
    /* Allocate and deallocate a stack.  New_stack() does call malloc. */
    
    extern Stack new_stack();
    extern free_stack(Stack);
    
    /* Test to see if the stack is empty -- or how many elements it as */
    
    extern int stack_empty(Stack);
    extern int stack_size(Stack);
    
    
    /* Put something on the stack */
    
    extern stack_push(Stack, Jval);
    
    
    /* Take something off the stack */
    
    extern Jval stack_pop(Stack);
    
    
    /* Look at the top element, but don't take it off */
    
    extern Jval stack_top(Stack);
    
    #endif
    

    Note a few things about this header file:

    Writing code to use a stack is pretty easy. For example, here is a program that takes a list of numbers on standard input and prints them reversed. This is in c_numrev.c. Those of you who are still having trouble compiling from multiple files should make sure you can compile and run this code.

    #include "c_stack.h"
    
    main()
    {
      Stack s;
      int i;
    
      s = new_stack();
    
      while(scanf("%d", &i) == 1) {
        stack_push(s, new_jval_i(i));
      }
      while(!stack_empty(s)) {
        printf("%d\n", jval_i(stack_pop(s)));
      }
      free_stack(s);
    }
    

    Try it out: (note, when compiling, you have to include c_stack.o, which is where the stack functions are implemented -- we'll go over this in a bit. Also you have to include libfdr.a because that is where the Jval routines are implemented).

    UNIX> make c_numrev
    gcc -I/blugreen/homes/plank/cs302/include -c c_numrev.c
    gcc -I/blugreen/homes/plank/cs302/include -c c_stack.c
    gcc -I/blugreen/homes/plank/cs302/include -o c_numrev c_numrev.o c_stack.o /blugreen/homes/plank/cs302/cobjs/libfdr.a
    UNIX> c_numrev
    1 2 3 4 5
    6 7 8 9 10
    < CNTL-D >
    10
    9
    8
    7
    6
    5
    4
    3
    2
    1
    UNIX>
    
    Also, note that you didn't need to call free_stack(). I just did it so that I can correlate it to the C++ example.

    The routines defined in c_stack.h are implemented in c_stack.c. I won't go over it -- it is straightforward, and if you would like some description, see the stack implementation lecture from CS140.

    The key things to note about this is the following:


    Stacks in C++

    Now, here is a way to implement stacks in C++. Here is the C++ header file:

    #ifndef _CPP_STACK_
    #define _CPP_STACK_
    
    #include "jval.h"
    
    class Stack {
      public:
    
        Stack();             // Create a stack
        ~Stack();            // Destroy a stack
        void push(Jval);
        Jval pop();
        bool isEmpty();
        Jval top();
    
      protected:
    
        class StackNode *top_node;
    };
    
    #endif
    

    As in last lecture, our member variables are protected. The only public members of the Stack class are its methods. The member variable is of type (class StackNode *) -- we don't know what that is, but that's ok; we don't need to know what it is to use the Stack class. The C++ compiler knows that top_node will be a pointer, even if it does not know what it will point to.

    We can write the C++ version of numrev using the Stack class. Here's the code (from numrev.cpp):

    #include "stack.h"
    #include < stdio.h >
    
    main()
    {
      Stack *s;
      int i;
    
      s = new Stack;
    
      while(scanf("%d", &i) == 1) {
        s->push(new_jval_i(i));
      }
      while(!s->isEmpty()) {
        printf("%d\n", jval_i(s->pop()));
      }
    
      delete s;
    }
    
    A few things to note. Unlike the Golfer class from the last lecture (the puttproc lecture), we don't need to pass any parameters when we create an instance of the Stack class. Instead, we just call ``new Stack.'' Everything else is pretty straightforward. Note, I don't have to call ``delete s'' at the end, but I do just to show you a destructor method being called.

    Try it out -- you'll see it works just like c_numrev.

    Now, the implementation of stacks is in stack.cpp. Note that this is where the StackNode class is defined and implemented. We do both in stack.cpp because we don't want to allow others to see or make use of the StackNode class.

    Here is the StackNode definition and implementation:

    class StackNode {
      public:
    
        StackNode(Jval, StackNode *);   // Create a new node with given val and next
        ~StackNode();
        Jval val;
        StackNode *next;
    };
    
    StackNode::StackNode(Jval v, StackNode *n)
    {
      val = v;
      next = n;
    }
    
    StackNode::~StackNode()
    {
      if (next != NULL) delete next;
    }
    
    Everything is pretty straightforward. Note that the destrictor is actually recursive -- it will delete every StackNode on the stack. Moreover, note that the member variables val and next are public rather than protected. This is because StackNode is only used in this file, so we don't worry about protecting it. In fact, we don't want it protected because we want the Stack methods to be able to access these variables directly.

    Now, here are the constructor/destructors for the Stack class:

    Stack::Stack()                // Create an empty stack
    {
      top_node = NULL;
    }
    
    Stack::~Stack()               // Destroy a stack
    {
      if (top_node != NULL) delete top_node;
    }
    
    Once again, these are all straightforward, as are the rest of the methods:
    void Stack::push(Jval v) 
    {
      top_node = new StackNode(v, top_node);
    }
    
    Jval Stack::pop() 
    {
      StackNode *tmp;
      Jval v;
      
      if (top_node == NULL) {
        fprintf(stderr, "ERROR: Trying to pop empty stack\n");
        exit(1);
      }
    
      tmp = top_node;
      v = tmp->val;
    
      /* Unhook the top node */
      top_node = tmp->next;
    
      /* Delete the top node */
      tmp->next = NULL;
      delete tmp;
    
      return v;
    }
    
    Jval Stack::top() 
    {
      if (top_node == NULL) {
        fprintf(stderr, "ERROR: Trying to get the top of an empty stack\n");
        exit(1);
      }
    
      return top_node->val;
    }
    
    bool Stack::isEmpty() 
    {
      return (top_node == NULL);
    }
    
    Now, compile everything and give it a try. You might even try using stack.h and stack.cpp to do a different task, like reversing the lines of standard input.

    Writing numrev without using new

    C++ encourages you to declare objects as local variables more than C does. When the procedure is entered, the constructor is called on the object, and when the procedure exits, the destructor is called. For example, look at numrev2.cpp:
    #include "stack.h"
    #include < stdio.h >
    
    main()
    {
      Stack s;
      int i;
    
      while(scanf("%d", &i) == 1) {
        s.push(new_jval_i(i));
      }
      while(!s.isEmpty()) {
        printf("%d\n", jval_i(s.pop()));
      }
    }
    
    Note, we don't call new or delete -- these are called automatically (If you don't believe me, put print statements in Stack::Stack() and Stack::~Stack() ). This is nice because you don't have to keep track of memory quite as much.