Chapter 2 : C++ Classes

Chapter 2 : C++ Classes

Contents

2.1. Introduction

The last chapter introduced the idea of Abstract Data Types (ADTs) as an approach to building modular systems in programming teams. This chapter deals with the specifics of realising ADTs in C++ through the new concept of CLASSES.

The chapter introduces necessary terminology and covers the fundamental stages of defining, implementing and using classes. Some initial guidelines for good class design are also provided.

The chapter follows three examples: a simple Counter class, used for counting the occurrences of events, a Money class used to manipulate values of pounds and pence, and a more complex Set class which allows programmers to work with sets of characters and supports set operations such as union and intersection.

2.2. First example - the Counter Class

The Class is a new concept which can be used to introduce new C++ types for programmers to use. We will introduce classes through a simple example - a generalised Counter which counts the number of occurrences of some event in a program. Effectively, a Counter is an object which starts with an initial value of zero, and whose value can be increased one step at a time. At any point the value can be read by the program, tested whether it is zero or compared against another counter. The value can also be reset to zero at any time.

Our goal is to build a general Counter class that can be used in many different programs. Bear in mind that there are two kinds of people involved with the Counter. Implementors, who design and build it, and its user(s) who use it in their programs. We shall look at each role in turn. Overall, there are three stages to building the class.

Stages 2 and 3 can occur in parallel.

2.2.1. Definition of Counter

Definition of the Counter means exactly specifying its interface (what it does and how to use it) and writing this as a C++ class definition in a header file. The name of the header file is usually (but not necessarily) that of the class with the added suffix .h.

In our example, the following C++ code is written in the file Counter.h.

The class definition starts with the word class, followed by the name of the class and then the rest of the definition between braces. The definition must be terminated with a semi-colon. The definition is divided into two parts.

The public part defines the interface to the class in terms of a set of function declarations. These follow the word public:. Each declaration gives the name of a function along with its argument and result types (argument names are optional, but it is good style to include them). Another name for these declarations is function prototypes. Being in the public part means that users of the class can invoke these functions.

The interface functions define all the actions that users can take on variables of the class. Thus, the reset function simply sets the value to zero (hence no argument or result); the read function returns the current value as an integer; the inc function adds one to the current value; the iszero function tests whether the current value is zero and the equals function tests against another counter (hence one argument of type Counter). The last two functions return integer results (1 indicates TRUE, 0 indicates FALSE).

The Counter interface function is special. It is called the constructor function for the class and is used to initialise variables of the class. In this case it will set the initial count value to zero. Most classes will have a constructor function which must have the same name as the class. The constructor doesn't return a result (not even void) but can take arguments. It is automatically invoked whenever a variable of the class is declared in a program.

Note that good comments are an essential guide to how to use the class.

The private section defines the data that is needed to implement the class. In the case of the Counter we need only remember the current count value. This requires a single integer variable. Being in the private section means that users of the class are prohibited from directly accessing the variable "count". It can only be accessed through the interface functions.

Now for some terminology. Anything that appears in the class definition is a member of the class. In general we refer to member functions and data members. A member function is also often called a method of the class. Thus, when someone talks about "invoking a method in a class" they mean using a class interface function.

A variable of the class which is declared in a users program is called an object (or sometimes an instance of the class). Thus, the distinction between classes and objects is the same as for types and variables.

You should note that data members and methods can appear in both the public and private sections of a class. It is quite common to see private methods (which can only be used by other methods). It is very rare (and probably very bad practice) to see public data members.

It is a convention that the private section is written before the public section.

You may wonder about the lines:

These are needed to stop multiple definitions of the same class occurring when a header file is accidently referred to more than once by a user program. We shall see later that this easily happens when classes use other classes (see chapter 3).

The statements are a kind of if-statement for the compiler. The first line says: "has the name COUNTER_H been defined yet?" If the answer is no, everything up to #endif is compiled. This includes defining the name COUNTER_H as the very next statement. If the answer is yes, the compiler ignores everything up to the #endif. The logic of this is that the code between the statements will only be looked at once by the compiler. If you don't understand this, you should at least be aware that YOU MUST ALWAYS PLACE SIMILAR STATEMENTS AROUND CLASS DEFINITIONS THAT YOU WRITE. You can chose the name to be defined yourself.

To summarise, the general structure of a class definition is:

Watchout for the semi-colon at the end and also remember the use of #ifndef, #define and #endif in the header file.

2.2.2. Implementation of the Counter Class

Now let's play the role of the implementor. Once it has been defined, the Counter class needs to be implemented. This means implementing all of the functions specified in its definition. It is a good idea to write this implementation in a separate file from the definition. For example, the following code could be placed in a file called Counter.C.

The statement #include "Counter.h" tells the compiler to insert all of the code from the class definition file at this point of the implementation file. This must be done by any program which refers to the Counter class. You can think of it as merging the two files together. The UNIX pathname of the file appears between double quotes, so in this case it looks in the current directory.

The rest of the file consists of code implementing the interface functions. This is mostly straight forward as the functions are very simple. However, you should note the following points:

  • There is no function main() so this is not a complete program.
  • The code Counter:: appears before each function name. This is necessary to tell the compiler to which class the function belongs. After all, there might be many "read" functions defined for different classes!
  • Each function can refer directly to private data members of the class. Hence the statement count = 0; in the reset function.
  • The functions can also access the data members of other objects of the same Class. To do this they specify the object name. Thus the equals functions compares its own data member against that of its argument "c" via the statement if (count == c.count).

    2.2.3. Using the Counter Class

    Now let's assume the users perspective. The user wants to employ the class to solve a specific problem. The following program reads in a sentence of text ending with a full-stop, a character at a time. It counts the number of occurrences of the letter e (lower or uppercase) and prints out the result.

    This program must also include the "Counter.h" header file. The statement Counter count; declares an object (variable) of Class (type) Counter, called count. This statement automatically calls the constructor function which initialises its internal data member to zero.

    Inside the while loop, we see the statement count.inc(). This says invoke the inc() function on the object count. Its effect is to increase the internal value of count by one.

    The statement count.read() invokes the read() function on count which returns its current value ready to be printed out.

    Notice how easy it is to use the Counter class. Notice also how the user is oblivious of its internal implementation (and also how they cannot tamper with internal details!).

    The program doesn't use the equals function. However, the following fragment of code shows it might be used.

    Notice how equals, a binary operation between two objects, is expressed - invoke the method ON one object and pass the other as an argument. This may be a little confusing at first and is important to take some time to understand.

    2.3. Compiling classes

    A consequence of splitting our code between different files is that the compilation process becomes a little more complex.

    The implementor must compile the implementation file (Counter.C). However, because this is incomplete (no main()) they need to tell the compiler to stop short of linking the code into a final executable program. This is done via the -c flag to the g++ command. Thus, they enter the command:

  • g++ -c Counter.C

    This places compiled versions of each method in a file called Counter.o.

    The user must compile their own code and also link in the implementors complied methods. They do this with the command:

  • g++ myprog.C Counter.o

    Where myprog.C is the name of their program file. Note that to use someone elses class you need to i) include the header file in your program ii) include the compiled functions when you compile your program. The following diagram summarises the files and commands involved in the process of implementing and using the Counter class.

    If the class user has access to the source code for the class implementation they could also enter the command:

  • g++ myprog Counter.C

    This bundles togther all of the compilation and linking steps in the above sequences of commands into a single command.

    2.4. Second example - the Money class

    Our second example is rather more complex and will highlight some of the issues involved in designing good classes. Our goal is to define and implement a general purpose Money class which can be used to manipulate pounds and pence values. This class might be very useful for a variety of accounting applications (as we shall see in chapter 3).

    2.4.1. Defining the Money class

    Bearing in mind that we want a generally useful class, our biggest problem is how to design the right interface. Unfortunately there is no easy formula to be applied. Good class design is a matter of experience and trial-and-error. In spite of the impression that you might get in lectures, it is often an iterative process. This means that there might be many revisions before the definition is satisfactory. However, I will give you some guidelines that represent a good starting point. Remember they are guidelines - not a formula!

    First, think what are the general properties of the Money class? Well, it stores pounds and pence values. You can add and subtract money. Subtraction implies that we can have negative values. You can compare money values (<, >, == etc). You might want to change an overall value or just inspect the pounds or pence components. You might want to print out money values in a special format.

    My guidline is to think about methods which fall into five general categories: -

  • 1. Constructors - how should objects of the class be initialised?
  • 2. Combination - methods which combine existing objects into new ones (e.g. addition).
  • 3. Access - methods which return of alter internal data members of an object.
  • 4. Tests - methods which test or compare objects in some way.
  • 5. Input/Output - methods which deal with input and output of objects.

    After several iterations, I arrived at the following methods for the Money class.

  • Constructors: initialise to given pounds and pence values.
  • Combination: add, subtract.
  • Access: negate, return pounds, return pence, set pounds and pence, increase and decrease by another Money value.
  • Tests: ==, <, >, <=, >=, !=, is it zero?, is it positive or negative?
  • I/O: print

    These translate into the following C++ class definition which I put in the file Money.h.

    The most difficult design decision was how to build negative Money values. At first, I thought that the user could supply negative values of pounds in the constructor (e.g. Money a(-6,40)). However, initialising a sum such as -40 pence would be impossible this way because the computer interprets -0 as 0. In the end I decided that the constructor could only build positive values and the user would have to subsequently use the negate() method to make them negative. This is clumsy but consistent.

    2.4.2. Implementing the Money class

    There are several possible implementations of the money class. My first thought was to use two internal integer data members called pounds and pence. Each method would then manipulate these as appropriate. For example, adding would involve adding the pounds and pence values from two objects. However, because the pence data member should have a maximum value of 99, this would involve some quite complex calculations in all combination and test operations (particularly for subtraction involving negative values). So, in the end I chose a second approach - use only one data member to represent the total number of pence. This simplifies most calculations. The only extra work is now in the print() pounds() and pence() methods. However, I think that this is easier. This kind of trade off is common to a variety of problems. Here is the implementation file for the Money class (in Money.C).

    Note the use of the abs function to get absolute (i.e. positive) values and the remainder (%) operation in the print() and pence() functions.

    2.4.3. Testing the Money class

    The following user program shows how Money objects might be built, added and subtracted. The program does nothing useful other than show the syntax of manipulating Money objects.

    2.5. Third example - the Set class

    The final example in the chapter is a Set class used for manipulating sets of characters. Even though it is a complex example, these notes only present the source code and a few comments about its design. It is left as an "exercise for the student" to work through the example in detail.

    A set is a collection of items of some type (in this case single characters). Each item can only appear once in the set and the items are in no particular order. Thus, unlike the arrays and structured from last term, there is no way of saying "give me the 5th item" or "give me the item called Fred". Instead, the Set provides the following basic operations:

  • Include a new element in the set.
  • Remove an element from the set.
  • Form the intersection of two sets (i.e. a new set consisting of the elements in common).
  • Form the union of two sets (i.e. a new set consisting of the elements in either).
  • Form the difference of two sets (i.e. a new set consisting of elements in the first but not in the second).
  • It is also possible to test whether an element is in the Set.
  • It is also possible to compare sets (do they contain the same items, is one a subset of the other etc).

    2.5.1. Definition of the Charset class

    The following header file contains the class definition for the Charset (set of characters) class. Notice how the operations are divided into the five categorise mentioned above.

    Notice how we use the & symbol in front of the variable name whenever we pass a Charset as a parameter. For example, we see,

    We call this passing paramters by reference (whereas the usual way is called passing by value). This is discussed in detail in chapter 4. For now, you need to know that this reduces the amount of copying of data that the computer does whenever the function is called. Pass by value means that the function uses a copy of the parameter not the original whereas pass by reference means that the code inside the function actually accesses the original value.

    When passing large paramters such as a Charset, pass by reference makes the program run faster and use less of the computer's memory. HOWEVER, this does mean that the function can acually change the original value. We call this a side-effect and sometimes it can be useful and sometimes it can be positively dangerous. Putting the word const in front of the parameter as we see in the Charset example prevents side-effects. In other words, the definition

    Can be read as "there is a function called equals with one parameter of type Charset called s. For reasons of efficiency, use pass by reference for s by for reasons of safety disallow any side effects from happening."

    There is one further wrinkle to be considered. We shall see below in the implementation file that some of these functions call the member function in. In order for const to work as required, the programmer has to inform the compiler that this function (in) will never try to alter the value of its object. This is why the word const appears after its decalration:

    2.5.2. Implementation of the Charset class

    The implementation of the Charset class uses an internal array to store the elements of the set. The integer data member n_elems indicates how many elements are in the set and, consequently, how much of the array is currently being used.

    It is important to note that using an array introduces a practical restriction on how many elements can be in the set. This is determined by the constant MAXSIZE.

    The implementation of the Charset methods involves the kind of array processing that you saw last term (e.g. finding the elements in common). The implementation file (Charset.C) is listed below.

    Several points are worth noting about this implementation.

  • Removal of an element involves deleting an array element and shuffling the rest of the array one place down.
  • The later methods build on the earlier ones. Thus, include and remove use in, and disjoint uses intersection and isempty. This saves alot of implementation effort.
  • When one method calls another method on the same object it only needs to give the method name and not the object name as well.

    2.5.3. Testing the Charset class

    The following simple test program reads text from the keyboard until end of file (control-D if from the keyboard) and prints out all of the characters used in the text. This is gracefully achieved by building up a set of characters.

    Note how the program tests the results of the include method in case of array overflow.


    Notes converted from troff to HTML by an Eric Foxley shell script, email errors to me!