8 Concrete examples of C++

Contents of this section

We're always interested in getting feedback. E-mail us if you like this guide, if you think that important material is omitted, if you encounter errors in the code examples or in the documentation, if you find any typos, or generally just if you feel like e-mailing. Mail to Karel Kubat (karel@icce.rug.nl) or use an e-mail form . Please state the concerned document version, found in the title. If you're interested in a printable PostScript copy, use the form . or better yet, pick up your own copy via ftp at ftp.icce.rug.nl/pub/http ,

This chapter presents a number of concrete examples of programming in C++. Items from this document such as virtuality, static members, etc. are rediscussed. Examples of container classes are shown.

8.1 Storing objects: Storable and Storage

A reoccurring task of many programs is the storage of data, which are then sorted, selected, etc.. Storing data can be as simple as maintaining an array of ints, but can also be much more complex, such as maintaining file system information by the kernel of an operating system.

In this section we take a closer look at the storage of generic objects in memory (i.e., during the execution of a program). Conforming to the object-oriented recipe we shall develop two classes: a class Storage, which stores objects, and a class Storable, the prototype of objects which can be stored.

The global setup

As far as the functionality of the class Storage is concerned, objects can be added to the storage and objects can be obtained from the storage. Also it must be possible to obtain the number of objects in the storage.

As far as the internal data organization of the storage is concerned, we opt for an approach in which Storage maintains an array which can be reallocated, consisting of pointers to the stored objects.

The internal organization of the class Storage is illustrated in the following figure:

picture

Interface functions of the class Storage

The usage (interface) of the class Storage is contained in three member functions. The following list describes these member functions and mentions the class Storable, more on this later.

To copy or not to copy?

There are two distinct design alternatives for the function add(). These considerations address the choice whether the stored objects (the squares on the right side of figure StorageFigure ) should be copies of the original objects, or the objects themselves.

In other words, should the function add() of the class Storage:

These considerations are not trivial. Consider the following example:

    Storage
        store;
    Storable
        something;

    store.add (something);          // add to storage

    // let's assume that Storable::modify () is defined
    something.modify ();            // modify original object,

    Storable
        *retrieved = store.get (0); // retrieve from storage

    // NOW: is "*retrieved" equal to "something" ?!

If we choose to store (addresses of) the objects themselves, then at the end of the above code fragment, the object pointed to by retrieved will equal something. A manipulation of previously stored objects thereby alters the contents of the storage.

If we choose to store copies of objects, then obviously *retrieved will not equal something but will remain the original, unaltered, object. This approach has a great merit: objects can be placed into storage as a `safeguard', to be retrieved later when an original object was altered or even ceased to exist. In this implementation we therefore choose for this approach.

Who makes the copy?

The fact that copies of objects should be stored presents a small problem. If we want to keep the class Storage as universal as possible, then the making of a copy of a Storable object cannot occur here. The reason for this is that the actual type of the objects to store is not known in advance. A simplistic approach, such as the following:

    void Storage::add (Storable const *obj)
    {
        Storable
            *to_store = new (*obj);
        // now add to_store instead of obj
        .
        .
    }

shall not work. This code attempts to make a copy of obj by using the operator new, which in turn calls the copy constructor of Storable. However, if Storable is only a base class, and the class of the object to store is a derived class (say, a Person), how can the copy constructor of the class Storable create a copy of a Person?

The making of a copy therefore must lie with the actual class of the object to store, i.e., with the derived class. Such a class must have the functionality to create a duplicate of the object in question and to return a pointer to this duplicate. If we call this function duplicate() then the code of the adding function becomes:

    void Storage::add (Storable const *obj)
    {
        Storable
            *to_store = obj->duplicate ();
        // now add to_store instead of obj
        .
        .
    }

The function duplicate() is called in this example by using a pointer to the original object (this is the pointer obj). The class Storable is in this example only a base class which defines a protocol, and not the class of the actual objects which will be stored. Ergo, the function duplicate() need not be defined in Storable, but must be concretely implemented in derived classes. In other words, duplicate() is a pure virtual function.

The class Storable

Using the above discussed approach we can now define the class Storable. The following questions are of importance:

The class definition and its functions are given below:

    class Storable
    {
        public:
            virtual ~Storable (void);
            virtual Storable *duplicate (void) const = 0;
    };

    Storable::~Storable ()
    {
    }

Converting an existing class to a Storable

To show how (existing) classes can be converted to derivation from a Storable, consider the below class Person from section Person . This class is re-created here, conforming to Storable's protocol (only the relevant or new code is shown):

    class Person: public Storable
    {
        // copy constructor
        Person (Person const &other);

        // assignment
        Person const &operator= (Person const &other);

        // duplicator function
        Storable *duplicate () const;

        .
        .
    }

When implementing the function Person::duplicate() we can use either the copy constructor or the default constructor with the overloaded assignment operator. The implementation of duplicate() is quite simple:

    // first version: 
    Storable *Person::duplicate () const
    {
        // uses default constructor in new Person
        Person
            *dup = new Person;

        // uses overloaded assignment in *dup = *this
        *dup = *this;

        return (dup);
    }

    // second version:
    Storable *Person::duplicate () const
    {
        // uses copy constructor in new(*this)
        return (new (*this));
    }

The above conversion from a class Person to the needs of a Storable supposes that the sources of Person are at hand and can be modified. However, even if the definition of a Person class is not available, but is e.g., contained in a run-time library, the conversion to the Storable format poses no difficulties:

    class StorablePerson: public Person, public Storable
    {
        public:
            // duplicator function
            Storable *duplicate () const;
    };

    Storable *StorablePerson::duplicate () const
    {
        return (new ( * (Person*)this ));
    }

The class Storage

We can now implement the class Storage. The class definition is given below:

    class Storage: public Storable
    {
        public:
            // destructors, constructor
            ~Storage (void);
            Storage (void);
            Storage (Storage const &other);

            // overloaded assignment
            Storage const &operator= (Storage const &other);

            // functionality to duplicate storages
            Storable *duplicate (void) const;

            // interface
            void add (Storable *newobj);
            int nstored (void) const;
            Storable *get (int index);

        private:
            // copy/destroy primitives
            void destroy (void);
            void copy (Storage const &other);

            // private data
            int n;
            Storable **storage;
    };

Concerning the class definition we remark:

The destructor, constructors and the overloaded assignment function are listed below:

    // default constructor
    Storage::Storage ()
    {
        n = 0;
        storage = 0;
    }

    // copy constructor
    Storage::Storage (Storage const &other)
    {
        copy (other);
    }

    // destructor
    Storage::~Storage ()
    {
        destroy ();
    }

    // overloaded assignment
    Storage const &Storage::operator= (Storage const &other)
    {
        if (this != &other)
        {
            destroy ();
            copy (other);
        }
        return (*this);
    }

The primitive functions copy() and destroy() unconditionally copy another Storage object, or destroy the contents of the current one. Note that copy() calls duplicate() to duplicate the other's stored objects:

    void Storage::copy (Storage const &other)
    {
        n = other.n;
        storage = new Storable* [n];
        for (int i = 0; i < n; i++)
            storage [i] = other.storage [i]->duplicate ();
    }

    void Storage::destroy ()
    {
        for (register int i = 0; i < n; i++)
            delete storage [i];
        delete storage;
    }

The function duplicate(), which is required since Storage itself should be a Storable, uses the copy constructor to duplicate the current object:

    Storable *Storage::duplicate () const
    {
        return (new (*this));
    }

Finally, here are the interface functions which add objects to the storage, return them, or determine the number of stored objects:

    void Storage::add (Storable const *newobj)
    {
        // reallocate storage array
        storage = (Storable **) realloc (storage,
                    (n + 1) * sizeof (Storable *));
        // put duplicate of newobj in storage
        storage [n] = newobj->duplicate ();
        // increase number of obj in storage
        n++;
    }

    Storable *Storage::get (int index)
    {
        // check if index within range
        if (index < 0 || index >= n)
            return (0);
        // return address of stored object
        return (storage [index]);
    }

    int Storage::nstored () const
    {
        return (n);
    }

8.2 A binary tree

This section shows an implementation of a binary tree in C++. Analogously to the classes Storage and Storable (see section Storage ) two separate classes are used: one to represent the tree itself, and one to represent the objects which are stored in the tree. The classes will be appropriately named Tree and Node.

The Node class

The class Node is an abstract (pure virtual) class, which defines the protocol for the usage of derived classes with a Tree. Concerning this protocol we remark the following:

The complete definition and declaration of the class Node is given below:

    class Node
    {
        public:
            // destructor
            virtual ~Node ();

            // duplicator
            virtual Node* duplicate (void) const = 0;

            // comparison of 2 objects
            virtual int compare (Node const *other) const = 0;

            // function to do whatever is needed to the node
            virtual void process (void) = 0;

            // called when object to add was already in the tree
            virtual void already_stored (void);
    };

    Node::~Node ()
    {
    }

    void Node::already_stored ()
    {
    }

The Tree class

The class Tree is responsible for the storage of objects which are derived from a Node. To implement the recursive tree structure, the class Tree has two private pointers as its data, pointing to subtrees: a Tree *left and Tree *right. The information which is contained in a node of the tree is represented as a private field Node *info.

To scan a binary tree, the class Tree offers three methods: preorder, inorder and postorder. When scanning in preorder first a leaf in a node is processed, then the left subtree is scanned and finally the right subtree is scanned. When scanning inorder first the left subtree is scanned, then the leaf itself is processed and finally the right subtree is scanned. When scanning in postorder first the left and right subtrees are scanned and then the leaf itself is processed.

The definition of the class Tree is given below:

    class Tree
    {
        public:
            // destructor, constructors
            ~Tree (void);
            Tree (void);
            Tree (Tree const &other);

            // assignment
            Tree const &operator= (Tree const &other);

            // addition of a Node
            void add (Node *what);

            // processing order in the tree
            void preorder_walk (void);
            void inorder_walk (void);
            void postorder_walk (void);

        private:
            // primitives
            void copy (Tree const &other);
            void destroy (void);

            // data
            Tree *left, *right;
            Node *info;
    };

The `standard' functions

As can be seen from the class definition, Tree contains pointer fields. This means that the class will need a destructor, a copy constructor and an overloaded assignment function to ensure that no allocation problems occur.

The destructor, the copy constructor and the overloaded assignment function are implemented with two primitive operations copy() and destroy() (these functions are presented later):

    // destructor: destroys the tree
    Tree::~Tree ()
    {
        destroy ();
    }

    // default constructor: initializes to 0
    Tree::Tree ()
    {
        left = right = 0;
        info = 0;
    }

    // copy constructor: initializes to contents of other object
    Tree::Tree (Tree const &other)
    {
        copy (other);
    }

    // overloaded assignment
    Tree const &Tree::operator= (Tree const &other)
    {
        if (this != &other)
        {
            destroy ();
            copy (other);
        }
        return (*this);
    }

Adding an object to the tree

The addition of a new object to the tree is a recursive process. When the function add() is called to insert an object into the tree, there are basically only a few possibilities:

The function add() is listed below:

    void Tree::add (Node *what)
    {
        if (! info)
            info = what->duplicate ();
        else
        {
            register int
                cmp = info->compare (what);

            if (cmp < 0)
            {
                if (! left)
                {
                    left = new Tree;
                    left->info = what->duplicate ();
                }
                else
                    left->add (what);
            }
            else if (cmp > 0)
            {
                if (! right)
                {
                    right = new Tree;
                    right->info = what->duplicate ();
                }
                else
                    right->add (what);
            }
            else
                info->already_stored ();
        }
    }

Scanning the tree

The class Tree offers three methods of scanning a binary tree: preorder, inorder and postorder. The three functions which define these actions are recursive:

    void Tree::preorder_walk ()
    {
        if (info)
            info->process ();
        if (left)
            left->preorder_walk ();
        if (right)
            right->preorder_walk ();
    }

    void Tree::inorder_walk ()
    {
        if (left)
            left->inorder_walk ();
        if (info)
            info->process ();
        if (right)
            right->inorder_walk ();
    }

    void Tree::postorder_walk ()
    {
        if (left)
            left->postorder_walk ();
        if (right)
            right->postorder_walk ();
        if (info)
            info->process ();
    }

The primitive operations copy() and destroy()

The functions copy() and destroy() are two private member functions which implement primitive operations of the class Tree: the copying of the contents of another Tree or the destroying of the tree.

    void Tree::destroy ()
    {
        delete info;
        if (left)
            delete left;
        if (right)
            delete right;
    }

    void Tree::copy (Tree const &other)
    {
        info = other.info ? other.info->duplicate () : 0;
        left = other.left ? new Tree (*other.left) : 0;
        right = other.right ? new Tree (*other.right) : 0;
    }

Concerning this implementation we remark the following:

Using Tree and Node

We illustrate the usage of the classes Tree and Node with a program that counts words in files. Words are defined as series of characters, separated by white spaces. The program shows which words are present in which file, and how many times.

Below is the listing of a class Strnode. This class is derived from Node and implements the virtual functions. Note also how this class implements the counting of words; when a given word occurs more than one time, Tree will call the member function already_stored(). This function simply increases the private counter variable times.

    class Strnode: public Node
    {
        public:
            // destructor, constructors
            ~Strnode (void);
            Strnode (void);
            Strnode (Strnode const &other);
            Strnode (char const *s);

            // assignment
            Strnode const &operator= (Strnode const &other);

            // functions required by Node protocol
            Node* duplicate (void) const;
            int compare (Node const *other) const;
            void process (void);
            void already_stored (void);

        private:
            // data
            char *str;
            int times;
    };

    Strnode::~Strnode ()
    {
        delete str;
    }

    Strnode::Strnode ()
    {
        str = 0;
        times = 0;
    }

    Strnode::Strnode (Strnode const &other)
    {
        str = strdup (other.str);
        times = other.times;
    }

    Strnode::Strnode (char const *s)
    {
        str = strdup (s);
        times = 1;
    }

    Strnode const &Strnode::operator= (Strnode const &other)
    {
        if (this != &other)
        {
            delete str;
            str = strdup (other.str);
            times = other.times;
        }
        return (*this);
    }

    Node *Strnode::duplicate () const
    {
        return (new Strnode (*this));
    }

    int Strnode::compare (Node const *other) const
    {
        Strnode
            *otherp = (Strnode *) other;

        if (str && otherp->str)
            return (strcmp (str, otherp->str));

        if (! str && ! otherp->str)
            return (0);

        return ( (int) otherp->str - (int) str );
    }

    void Strnode::process ()
    {
        if (str)
            printf ("%4d\t%s\n", times, str);
    }

    void Strnode::already_stored ()
    {
        times++;
    }

    void countfile (FILE *inf, char const *name)
    {
        Tree
            tree;
        char
            buf [255];

        while (1)
        {
            fscanf (inf, " %s", buf);
            if (feof (inf))
                break;
    
            Strnode
                *word = new Strnode (buf);
    
            tree.add (word);
            delete word;
        }
        tree.inorder_walk ();
    }

    int main (int argc, char **argv)
    {
        register int
            exitstatus = 0;
    
        if (argc > 1)
            for (register int i = 1; i < argc; i++)
            {
                FILE
                    *inf = fopen (argv [i], "r");

                if (! inf)
                {
                    fprintf (stderr, "wordc: can't open \"%s\"\n",
                             argv [i]);
                    exitstatus++;
                }
                else
                {
                    countfile (inf, argv [i]);
                    fclose (inf);
                }
            }
            else
                countfile (stdin, "--stdin--");

        return (exitstatus);
    }

Next Chapter, Previous Chapter

Table of contents of this chapter, General table of contents

Top of the document, Beginning of this Chapter