C++ Development Environment
C++ Development Environment
C++ Development Environment
5 C++ development
environment
5.1 INTEGRATED DEVELOPMENT ENVIRONMENT
• a compiler
Fortunately, if you are working on a personal computer (Macintosh or PC) you will
usually be able to use an "Integrated Development Environment" (IDE).
An IDE packages all the components noted above and makes them available
through some simple to use visual interface.
On a Intel 486-, or Pentium- based PC system, you will be using either
Borland's C++ environment or Microsoft's C++. On a Macintosh system, you will
probably be using Symantec C++. These systems are fairly similar.
They employ a variety of different windows on the screen to present Project
information about a program (usually termed a "project") that you are developing.
At least one of these windows will be an editing window where you can change the
text of the source code of the program. Another window will display some kind of
summary that specifies which files (and, possibly, libraries) are used to form a
program. Figure 5.1 illustrates the arrangement with Symantec 8 for the Power PC.
The editing window is on the left, the project window is to the right.
106 C++ development environments
The figure illustrates the situation when the Symantec system has been asked to
create a new project with the options that it be "ANSI C++ with iostreams". Such
options identify things such as which input output library is to be used. The
environment generates a tiny fragment of C++ code to get you started; this
fragment is shown in the editing window:
#include <stdlib.h>
#include <iostream.h>
int main()
{
cout << "hello world" << endl;
return EXIT_SUCCESS;
}
The two #include lines inform the compiler about the standard libraries that this
program is to use. The code form "int main" to the final "}" is a skeletal main
program. (It is a real working program; if you compiled and ran it then you would
get the message "hello world" displayed.)
The project window lists the components of the program. Here, there is a folder
containing libraries and a single program file "main.cp".
The Borland environment would generate something very similar. One of the
few noticeable differences would be that instead of being separate windows on the
desktop, the project and editing windows would both be enclosed as subwindows
of a single large window.
Menu commands Like spreadsheets, paint programs, and word processor programs, most of the
operations of an IDE are controlled through menu commands. There are a number
of separate menus:
Integrated development environments 107
• File: things like saving to file, opening another file, printing etc.
The various environments have similar menu options though these are named and
organized slightly differently.
Because any interesting program is going to have to have at least some output (and
usually some input), you have to learn a little about the input and output facilities
before you can do anything.
In some languages (FORTRAN, Pascal, etc), the input and output routines are Input/output
fixed in the language definition. C and C++ are more flexible. These languages capabilities defined
by libraries
assume only that the operating system can provide some primitive 'read' and 'write'
functions that may be used to get bytes of data into or out from a program. More
useful sets of i/o routines are then provided by libraries. Routines in these libraries
will call the 'read' and 'write' routines but they will do a lot of additional work (e.g.
converting sequences of digits into the appropriate bit pattern to represent a
number).
Most C programs use an i/o library called stdio (for standard i/o). This library stdio and iostream
can be used in C++; but more often, C++ programs make use of an alternative libraries
library called iostream (i/o stream library).
The i/o stream library (iostream) makes use of some of the "object oriented" Stream objects
features of C++. It uses "stream objects" to handle i/o.
Now in general an object is something that "owns a resource and provides
services related to that resource". A "stream object" owns a "stream", either an
output stream or an input stream. An output stream is something that takes
character data and gets them to an output device. An input stream gets data bytes
from some input device and routes them to a program. (A good way of thinking of
them is as kinds of software analogues of hardware peripheral device controllers.)
An ostream object owns an output stream. The "services" an ostream object ostream objects
provides include the following:
• an ostream object will take data from an integer variable (i.e. a bit pattern!)
and convert it to digits (and ± sign if needed) and send these through its output
stream to an output device;
• similarly, an ostream object can take data from a "real number" variable and
convert to a character sequence with sign character, digits, and decimal point
(e.g. 3.142, -999.9, or it may use scientific notation and produce a character
sequence like 1.70245E+43);
108 C++ development environments
• an ostream object can take a message string (some character data) and copy
these characters to the output stream;
• an ostream object can accept directions saying how many digits to show for a
real number, how much space on a line to use, etc.
istream objects An istream object owns an input stream. The "services" an istream object
provides include the following ...
• an istream object can be asked to find a value for an integer variable, it will
check the input stream, miss out any blank space, read in a series of digits and
work out the value of the number which it will then put into the variable
specified;
• similarly, an istream object can be asked to get a "real number" variable, in
this case it will look for input patterns like 2.5, -7.9, or 0.6E-20 and sort them
out to get the value for the number;
• an istream object can be asked to read in a single character or a complete
multicharacter message.
Error handling with It is unusual for an ostream object not to be able to deal with a request from a
input and output program. Something serious would have had to have gone wrong (e.g. the output is
supposed to be being saved on a floppy disk and the disk is full). It is fairly
common for an istream object to encounter problems. For example, the program
might have asked for an integer value to be input, but when the istream object
looks in the input stream it might not find any digits, instead finding some silly
input entered by the user (e.g. "Hello program").
Response to bad An istream object can't convert "Hello program" into an integer value! So, it
input data doesn't try. It leaves the bad data waiting in the input stream. It puts 0 (zero) in the
variable for which it was asked to find a value. It records the fact that it failed to
achieve the last thing it was asked to do. A program can ask a stream object
whether it succeeded or failed.
"end of file" Another problem that an istream object might encounter is "end of file". Your
condition program might be reading data from a file, and have some loop that says something
like "istream object get the next integer from the file". But there may not be any
more data in the file! In this case, the istream object would again put the value 0
in the variable, and record that it had failed because of end-of-file. A program can
ask an istream object whether it has reached the end of its input file.
Simple use of streams The first few programming exercises that you do will involve the simplest
requests to iostream objects … "Hey input stream object, read an integer for me.",
"Hey output stream object, print this real number." The other service requests
("Did that i/o operation work?", "Are we at the end of the input file?", "Set the
number precision for printing reals to 3 digits", ...) will be introduced as needed.
Standard streams A program can use quite a large number of stream objects (the operating system
may set a limit on the number of streams used simultaneously, 10, 16, 200 etc
depends on the system). Streams can be attached to files so that you can read from
a data file and write to an output file.
The iostream library sets up three standard streams:
You don't have to 'declare' these streams, if your program says it wants to use the
iostream library then the necessary declarations get included. You can create
iostream objects that are attached to data files. Initially though, you'll be using
just the standard iostream objects: cout for output and cin for input.
Requests for output to cout look like the following: Output via cout
int aCounter;
double aVal;
char Message[] = "The results are: ";
...
cout << Message;
cout << aCounter;
cout << " and ";
cout << aVal;
...
The code fragments starts with the declarations of some variables, just so we Explanation of code
have something with values to output.: fragment
int aCounter;
double aVal;
char Message[] = "The results are: ";
The declaration int aCounter; defines aCounter as a variable that will hold an
integer value. (The guys who invented C didn't like typing, so they made up
abbreviations for everything, int is an abbreviation for integer.) The declaration
double aVal; specifies that there is to be a variable called aVal that will hold a
double precision real number. (C and C++ have two representations for real
numbers – float and double . double allows for greater accuracy.) The
declaration " char Message ... " is a bit more complex; all that is really
happening here is that the name Message is being associated with the text The
results....
In this code fragment, and other similar fragments, an ellipsis (…) is used to
indicate that some code has been omitted. Just assume that there are some lines of
code here that calculate the values that are to go in aVal etc.
These are the requests to cout asking it to output some values. The general
form of a request is:
110 C++ development environments
(which you read as "cout takes from some value"). The first request is essential
"Please cout print the text known by the name Message." Similarly, the second
request is saying: "Please cout print the integer value held in variable aCounter."
The third request, cout << " and " simply requests output of the given text.
(Text strings for output don't all have to be defined and given names as was done
with Message. Instead, text messages can simply appear in requests to cout like
this.) The final request is: "Please cout print the double value held in variable
aVal."
Abbreviations C/C++ programmers don't like typing much, so naturally they prefer
abbreviated forms. Those output statements could have been concatenated
together. Instead of
cout << Message << aCounter << " and " << aVal;
Basic formatting of cout will keep appending output to the same output line until you tell it to start
output lines a new line. How do you tell it to start a new line?
Well, this is a bit system dependent. Basically, you have to include one or more
special characters in the output stream; but these characters differ depending on
whether your program is running on Unix, or on DOS, or on ... To avoid such
problems, the iostream library defines a 'thing' that knows what system is being
used and which sends the appropriate character sequence (don't bother about what
this 'thing' really is, that is something that only the guys who wrote the iostream
library need to know).
endl This 'thing' is called " endl" and you can include it in your requests to cout. So,
if you had wanted the message text on one line, and the two numbers on the next
line (and nothing else appended to that second line) you could have used the
following request to cout:
(You can split a statement over more than one line; but be careful as it is easy to
make mistakes when you do such things. Use indentation to try to make it clear
that it is a single statement. Statements end with semicolon characters.)
Input from cin Input is similar in style to output except that it uses ">>" (the 'get from operator')
with cin instead of " <<" (the 'put operator') and cout.
If you want to read two integers from cin, you can write
or
5.3.1 Design
You never start coding until you have completed a design for your program!
But there is not much to design here.
Seems like four integer variables, two for data given as input, one for quotient and
the other for a remainder.
Remember there are different kinds of integers, short integers and long integers.
We had better use long integers so permitting the user to have numbers up to
±2000Million.
Program organization?
Main program could print a prompt message, read the data, do the calculations and
print the results – a nice simple sequence of instructions.
"Pseudo-code" outline
This will be our "main" program. We have to have a routine called main and since
this is the only routine we've got to write it had better be called main.
Here the operations of the routine could be expressed using English sentences.
There are more elaborate notations and "pseudo-codes" that may get used to
specify how a routine works. English phrases or sentences, like those just used,
will suffice for simple programs. You should always complete an outline in some
form of pseudo code before you start implementation.
Test data
If you intend to develop a program, you must also create a set of test data. You
may have to devise several sets of test data so that you can check all options in a
program that has lots of conditional selection statements. In more sophisticated
development environments like Unix, there are special software tools that you can
use to verify that you have tested all parts of your code. In an example like this,
you would need simple test data where you can do the calculations and so check
the numerical results of the program. For example, you know that seven divided by
three gives a quotient of two and a remainder of one; the data set 7 and 3 would
suffice for a simple first test of the program.
5.3.2 Implementation
Our program is going to need to use the iostream library. This better be stated in
the file that contains our main program (so allowing the compiler to read the
"header" file for the iostream library and then, subsequently, check that our code
using the iostream facilities correctly).
The file containing the program had better start with the line
#include <iostream.h>
This line may already be in the "main.cp" file provided by the IDE.
A simple example program: implementation 113
main() routine
int main()
{
... definitions of variables
... code statements
return 0; return statement specifying 'result' for
script
}
The code "int main()" identifies main as being the name of a function (indicated
by the () parentheses), which will compute an integer value. The main function
should then end with a "return" statement that specifies the value that has been
computed and is to be returned to the caller. (Rather than "return 0; ", the code
may be "return EXIT_SUCCESS;". The name EXIT_SUCCESS will have been
defined in one of the header files, e.g. stdlib's header; its definition will specify
its value as 0. This style with a named return value is felt to be slightly clearer than
the bare " return 0;").
The integrated development environments, like Symantec or Borland., don't use
that kind of scripting mechanism to chain programs together. So, there is no need
for the main() routine to return any result. Instead of having the main routine
defined as returning an integer value, it may be specified that the result part of the
routine is empty or "void".
In these IDE environments, the definition for a main() routine may be
something like:
void main()
{
... definitions of variables
... code statements
}
The keyword void is used in C/C++ to identify a procedure (a routine that, unlike a void
function, does not return any value or, if you think like a language designer, has a
return value that is empty or void).
114 C++ development environments
Generally, you don't have to write the outline of the main routine, it will be
provided by the IDE which will put in a few #include lines for standard libraries
and define either "int main()" or "void main() ". The code outline for main()
provided by Symantec 8 was illustrated earlier in section 5.1.
Variable definitions
The four (long) integer values (two inputs and two results) are only needed in the
main routine, so that is where they should be defined. There is no need to make
them "global" (i.e. "shareable") data.
The definitions would be put at the start of the main routine:
int main()
{
long aNum1;
long aNum2;
long aQuotient;
long aRemainder;
…
int main()
{
long aNum1, aNum2, aQuotient, aRemainder;
…
Choice of variable The C/C++ languages don't have any particular naming conventions. But, you
names will find it useful to adopt some conventions. The use of consistent naming
conventions won't make much difference until you get to write larger programs in
years 2 and 3 of your course, but you'll never be able to sustain a convention unless
you start with it.
Some suggestions:
• local variables and arguments for routines should have names like aNum, or
theQuotient.
• shared ("global") variables should have names that start with 'g', e.g. gZeroPt.
• functions should have names that summarize what they do, use multiple words
(run together or separated by underscore characters _)
that would be used for the variables (they were to be 'automatic' variables that
would be stored in main's stack frame).
Sketch of code
int main()
{
long aNum1, aNum2, aQuotient, aRemainder;
cout << "Enter two numbers" << endl;
cin >> aNum1 >> aNum2;
cout << "The quotient of " << aNum1 << " and "
<< aNum2 << " is " << aQuotient << endl;
cout << "and the remainder is " << aRemainder << endl;
return EXIT_SUCCESS;
}
int main()
{
…
}
The definition of the main() routine starts with int main and ends with the final
"}". The { } brackets are delimiters for a block of code (they are equivalent to
Pascal's begin and end keywords).
Here the variables are defined; we want four long integers (long so that we can deal
with numbers in the range ±2000million).
This is the first request to the cout object, asking it to arrange the display of the
prompt message.
This is a request to the cin object, asking it to read values for two long integers.
(Note, we don't ask cin whether it succeeded, so if we get given bad data we'll just
continue regardless.)
These are the statements that do the calculations. In C/C++ the "/ operator"
calculates the quotient, the "% operator" calculates the remainder for an integer
division.
cout << "The quotient of " << aNum1 << " and "
<< aNum2 << " is " << aQuotient << endl;
cout << "and the remainder is " << aRemainder << endl;
These are the requests to cout asking it to get the results printed.
return EXIT_SUCCESS;
This returns the success status value to the environment (normally ignored in an
IDE).
You should always complete a sketch of the code of an assignment before you
come to the laboratory and start working on the computer!
IDEs use font styles The code can be typed into an editor window of the IDE. The process will be fairly
and colours to similar to text entry to a word processor. The IDE may change font style and or
distinguish elements
in the code colour automatically as the text is typed. These colour/style changes are meant to
highlight different program components. So, language keywords ("return, int, long,
...") may get displayed in bold; #include statements and quoted strings may appear
in a different colour from normal text. (These style changes help you avoid errors.
It is quite easy to forget to do something like put a closing " sign after a text string;
such an error would usually cause numerous confusing error messages when you
attempted to run the compiler. But such mistakes are obvious when the IDE uses
visual indicators to distinguish program text, comments, strings etc.)
Save often! You will learn by bitter experience that your computer will "crash" if you type
in lots of code without saving! Use the File/Save menu option to save your work at
regular intervals when you are entering text.
Compile and "Syntax When you have entered the code of your function you should select the
check" menu options "Compile" option from the appropriate menu (the compile option will appear in
different menus of the different IDEs). The IDE may offer an alternative "Syntax
check" option. This check option runs just the first part of the compiler and does
not go on to generate binary code. It is slightly quicker to use "Syntax check"
when finding errors in code that you have just entered.
#include files add to You will be surprised when you compile your first twenty line program. The
cost of compilations compiler will report that it has processed something between 1000 and 1500 lines
of code. All those extra lines are the #included header files!
Compilation errors The compiler will identify all places where your code violates the "syntax rules"
of the language. For example, the program was entered with the calculation steps
given as:
(The code is incorrect. The statement setting a value in aQuotient doesn't end in a
semicolon. The compiler assumes that the statement is meant to continue on the
next line. But then it finds that it is dealing with two variable names, aNum2 and
aRemainder , without any operator between them. Such a construct is illegal.)
When this code was compiled, the Symantec compiler generated the report
The IDE environments all use the same approach when reporting the compilation Error messages are
errors. Each error report actually consists of commands that the IDE can itself commands that select
source code lines
interpret. Thus, the phrase File "main.cp"; can be interpreted by Symantec's IDE as with errors
a command to open an editing window and display the text of the file main.cp (if
the file is already displayed in a window, then that window is brought in front of all
other windows). The second part of the error report, Line 11, is a directive that the
IDE can interpret to move the editing position to the appropriate line. The rest of
the error report will be some form of comment explaining the nature of the error.
If you do get compilation errors, you can select each error report in turn
(usually, by double clicking the mouse button in the text of the error message).
Then you can correct the mistake (quite often the mistake is on a line just before
that identified in the report).
Generally, you should try to correct as many errors as possible before
recompiling. However, some errors confuse the compiler. An initial error message
may be followed by many other meaningless error messages. Fixing up the error
identified in the first message of such a group will also fix all the subsequent errors.
If your code compiles successfully you can go ahead and "Build" your project. Building a project
The build process essentially performs the link-loading step that gets the code of
functions from the libraries and adds this code to your own code.
During the build process, you may get a "link error". A link error occurs when Link errors
your code specifies that you want to use some function that the IDE can not find in
the libraries that you have listed. Such errors most often result from a typing error
where you've given the wrong function name, or from your having forgotten to
specify one of the libraries that you need.
You can simply "Run" your program or you can run it under control from the IDE's
debugger. If you just run the program, it will open a new window for input and
output. You enter your data and get your results. When the program terminates it
will display some message (e.g. via an "Alert" dialog); when you clear ("OK") the
dialog you are returned to the main part of the development environment with its
editor and project windows.
Running under the control of a debugger takes a little more setting up. Debugger
Eventually, the IDE will open a window showing the code of your main program
with an arrow marker at the first statement. Another "Data" window will also have
118 C++ development environments
been opened. You can then control execution of your program, making it proceed
one statement at a time or letting it advance through a group of statements before
stopping at a "breakpoint" that you set (usually by clicking the mouse button on the
line where you want your program to stop).
Stack backtrace Figure 5.2 illustrates the windows displayed by the Symantec 8 debugger. The
left pane of the window summarizes the sequence of function calls;. In this case
there is no interesting information, the data simply show that the program is
executing the main program and that this has been called by one of the startup
routines provided by the IDE environment. (In Borland's IDE, this "stack
backtrace" information is displayed in a separate window that is only shown if
specifically requested by menu command.)
Code of current The right pane of the window shows the code of the procedure currently being
procedure executed. The arrow marks the next statement to be executed. Statements where
the program is to stop and let the debugger regain control are highlighted in some
way (Symantec uses diamond shaped check marks in the left margin, Borland uses
colour coding of the lines). If you execute the program using the "Step" menu
command you only execute one statement, a "Go" command will run to the next
breakpoint.
an incorrect result. It is very hard to find such an error if all you know is that the
program "bombs". It is relatively easy to find such errors if you are able to use the
debugger to stop the program and check what is going.
120 C++ development environments
6
6 Sequence
The simplest programs consist just of sequences of statements with no loops, no
selections amongst alternative actions, no use of subroutines (other than possibly
those provided in the input/output or mathematical libraries).
They aren't very interesting as programs! They do things that you could usually
do on a calculator (at least you could probably do with a calculator that had some
memory to store partial results). Most often they involve simply a change of scale
(e.g. you have an amount in $US and want it in ¥, or you have a temperature on the
Celsius scale and want it in Fahrenheit, or you have an H+ ion concentration and
want a pH value). In all such programs, you input a value, use some formula ("x
units on scale A equals y units on scale B") and print the result.
Such programming problems, which are commonly used in initial examples, are
meant to be so simple that they don't require any real thought as to how to solve the
problem. Because the problem is simple, one can focus instead on the coding, all
those messy little details, such as:
and
• the properties of the "operators" that are used to combine data values.
The first few programs that you will write all have the same structure:
#include <iostream.h>
#include <math.h>
/*
This program converts [H+] into
pH using formula
pH = - log10 [H+]
124 Sequence
*/
int main()
{
double Hplus;
double pH;
#includes The program file will start with #include statements that tell the compiler to
read the "headers" describing the libraries that the program uses. (You will always
use iostream.h, you may use other headers in a particular program.)
#include <iostream.h>
#include <math.h>
Introductory It is sensible to have a few comments at the start of a program explaining what
comments the program does.
/*
This program converts [H+] into
pH using formula
pH = - log10 [H+]
*/
Program outline The outline for the program may be automatically generated for you by the
development environment. The outline will be in the form void main() { ... },
or int main() { ... return 0; }.
Own data definitions You have to insert the code for your program between the { ("begin") and }
and code ("end") brackets.
The code will start with definitions of constants and variables:
double Hplus;
double pH;
and then have the sequence of statements that describe how data values are to be
combined to get desired results.
Libraries
What libraries do you need to "#include"? Someone will have to tell you which
libraries are needed for a particular assignment.
Almost standard The libraries are not perfectly standardized. You do find difference between
libraries! environments (e.g. the header file for the maths library on Unix contains definitions
of the values of useful constants like PI π = 3.14159..., the corresponding header
file with Symantec C++ doesn't have these constants).
Libraries whose headers often get included are:
Overall structure and main() function 125
6.2 COMMENTS
"Comments" are sections of text inserted into a program that are ignored by the
compiler; they are only there to be read by the programmers working on a project.
Comments are meant to explain how data are used and transformed in particular
pieces of code.
Comments are strange things. When you are writing code, comments never
seem necessary because it is "obvious" what the code does. When you are reading
code (other peoples' code, or code you wrote more than a week earlier) it seems
that comments are essential because the code has somehow become
incomprehensible.
C++ has two constructs that a programmer can use to insert comments into the
text of a program.
"Block comments" are used when you need several lines of text to explain the "Block comments"
purpose of a complete program (or of an individual function). These start with the
character pair /* (no spaces between the / and the *), and end with */ (again, no
space) e.g.
/*
Program for assignment 5; program reads 'customer'
records from
....
*/
Integer variables are often declared as being of type int rather than short or long.
This can cause problems because some systems use int as synonymous with short
int while others take it to mean long int. This causes programs using ints to
behave differently on different systems.
Example definitions:
short aCount;
double theTotal;
char aGenderFlag; // M = male, F = Female
You can define several variables of the same type in a single definition:
Definitions do get a lot more complex than this. Later examples will show
definitions of "derived types" (e.g. an array of integers as a type derived from
integer) and, when structs and classes have been introduced, examples will have
variables of programmer defined types. But the first few programs will use just
simple variables of the standard types, so the complexities of elaborate definitions
can be left till later.
Since the first few programs will consist solely of a main() routine, all
variables belong to that routine and will only be used by that routine.
Consequently, variables will be defined as "locals" belonging to main(). This is
Variable definitions 127
int main()
{
long aCount;
double theTotal;
...
}
6.4 STATEMENTS
"input statements" these get the " cin object" to read some data into
variables,
"assignment statements" these change values of variables,
"output statements" these get the " cout object" to translate the data values
held in variables into sequences of printable characters
that are then sent to an output device.
main()
{
get some data // Several input statements, with
// maybe some outputs for prompts
calculate with data // Several assignment statements
output results // Several more output statements
}
Input
where Variable is the name of one of the variables defined in the program.
Examples
short numItems;
double delta;
char flag;
...
cin >> numItems;
...
cin >> delta;
...
cin >> flag;
...
128 Sequence
If several values are to be read, you can concatenate the input statements together.
Example
or
The cin object has to give data to each variable individually with a >> operator.
Common error made You may have seen other languages where input statements have the variable
by those who have names separated by commas e.g. xcoord, ycoord. Don't try to follow that style!
programmed in other
languages The following is a legal C++ statement:
but it doesn't mean read two data values! Its meaning is actually rather odd. It
means: generate the code to get cin to read a new value and store it in xcoord (OK
so far) then generate code that fetches the current value of ycoord but does
nothing with it (i.e. load its value into a CPU register, then ignores it).
cin's handling of >> The cin object examines the characters in the input stream (usually, this stream
operation comes from the keyboard) trying to find a character sequence that can be converted
to the data value required. Any leading "white space" is ignored ("white space"
means spaces, newlines, tabs etc). So if you ask cin to read a character, it will try
to find a printable character. Sometimes, you will want to read the whitespace
characters (e.g. you want to count the number of spaces between words). There are
mechanisms (explained later when you need them) that allow you to tell the cin
object that you want to process whitespace characters.
Output
where Variable is the name of one of the variables defined in the program, or
cout << "The coords are: x " << xcoord << ", y "
<< ycoord << ", z " << zcoord;
(An output statement like this may be split over more than one line; the statement
ends at the semi-colon.)
As previously explained (in the section 5.2), the iostream library defines
something called "endl" which knows what characters are needed to get the next
data output to start on a new line. Most concatenated output statements use endl:
cout << "The pH of the solution is " << pHValue << endl;
In C++, you aren't allowed to define text strings that extend over more than one Long text strings
line. So the following is not allowed:
But the compiler allows you to write something like the following ...
It understands that the two text strings on successive lines (with nothing else
between them) were meant as part of one longer string.
Sometimes you will want to include special characters in the text strings. For Special control
example, if you were producing an output report that was to be sent to a printer you characters
might want to include special control characters that the printer could interpret as
meaning "start new page" or "align at 'tab-stop'". These characters can't be typed
in and included as part of text strings; instead they have to be coded.
You will often see coded characters that use a simple two character code (you
will sometimes see more complex four character codes). The two character code
scheme uses the "backslash" character \ and a letter to encode each one of the
special control characters.
Commonly used two character codes are:
\t tab
\n newline
\a bell sound
\p new page?
\\ when you really want a \ in the output!
Output devices won't necessarily interpret these characters. For example a "tab"
may have no effect, or may result in output of one space (or 4 spaces, or 8 spaces),
130 Sequence
or may cause the next output to align with some "tab-position" defined for the
device.
Generally, '\n' causes the next output to appear at the start of new line (same
effect as endl is guaranteed to produce). Consequently, you will often see
programs with outputs like
rather than
The style with '\n' is similar to that used in C. You will see many examples using
'\n' rather than endl ; the authors of these examples probably have been
programming in C for years and haven't switched to using C++'s endl.
Calculations
Along with input statements to get data in, and output statements to get results
back, we'd better have some ways of combining data values.
The first programs will only have assignment statements of the form
where Variable is the name of one of the variables defined for the program and
Arithmetic Expression is some expression that combines the values of variables
(and constants), e.g.
double s, u, a, t;
cout << "Enter initial velocity, acceleration, and time : ";
cin >> u >> a >> t;
s = u*t + 0.5*a*t*t;
s = u*t + 0.5*a*t*t;
into a sequence of instructions that evaluate the arithmetic expression, and a final
instruction that stores the calculated result into the variable named on the left of the
= (the "assignment operator").
"lvalue" The term "lvalue" is often used by compilers to describe something that can go
on the left of the assignment operator. This term may appear in error messages.
For example, if you typed in an erroneous statement like "3 = n - m;" instead of
say "e = n - m;" the error message would probably be "lvalue required". The
Statements 131
compiler is trying to say that you can't have something like 3 on the left of an
assignment operator, you have to have something like the name of a variable.
As usual, C++ allows abbreviations, if you want several variables to contain the
same value you can have a concatenated assignment, e.g.
The value would be calculated, stored in zcoord, copied from zcoord to ycoord,
then to xcoord. Again be careful, the statement "xcoord, ycoord, zcoord =
1.0/dist;" is legal C++ but means something odd. Such a statement would be
translated into instructions that fetch the current values of xcoord and ycoord and
then does nothing with their values, after which there are instructions that calculate
a new value for zcoord.
Arithmetic expressions use the following "operators" Arithmetic
"operators"
+ addition operator
- subtraction operator (also "unary minus sign")
* multiplication operator
/ division operator
% remainder operator (or "modulo" operator)
v = u + a t
But that isn't a valid expression in C++; rather than a t you must write a*t.
You use these arithmetic operators with both real and integer data values (%
only works for integers).
Arithmetic expressions work (almost) exactly as they did when you learnt about
them at school (assuming that you were taught correctly!). So,
v = u + a * t
means that you multiply the value of a by the value of t and add the result to the
value of u (and you don't add the value of u to a and then multiply by t).
The order in which operators are used is determined by their precedence. The Operator precedence
multiply (*) and divide (/) operators have "higher precedence" than the addition
and subtraction operators, which means that you do multiplies and divides before
you do addition and subtraction. Later, we will encounter more complex
expressions that combine arithmetic and comparison operators to build up logical
tests e.g. when coding something like "if sum of income and bonus is greater than
tax threshold or the interest income exceeds value in entry fifteen then …". The
operators (e.g. + for addition, > for greater than, || for or etc) all have defined
precedence values; these precedence values permit an unambiguous interpretation
of a complex expression. You will be given more details of precedence values of
operators as they are introduced.
132 Sequence
"Arithmetic expressions work (almost) exactly as they did when you learnt them
at school". Note, it does say "almost". There are actually a number of glitches.
These arise because:
• In the computer you only have a finite number of bits to represent a number
• You have to be careful if you want to combine integer and real values in an
expression.
You may run into problems if you use short integers. Even if the correct result
of a calculation would be in the range for short integers (-32768 to +32767) an
intermediate stage in a calculation may involve a number outside this range. If an
out of range number is generated at any stage, the final result is likely to be wrong.
(Some C++ compilers protect you from such problems by using long integers for
intermediate results in all calculations.)
Integer division is also sometimes a source of problems. The "/ " divide
operator used with integers throws away any fraction part, so 10/4 is 2. Note that
the following program says "result = 2" ...
void main()
{
int i, j;
double result;
i = 10;
j = 4;
result = i / j;
cout << "result = " << result << endl;
}
Because the arithmetic is done using integers, and the integer result 2 is converted
to a real number 2.0.
6.5 EXAMPLES
Specification:
2. The program will get the user to input two values, first the current exchange
rate then the money amount. The exchange rate should be given as the
"Exchange rates" example 133
amount of US currency equivalent to $A1, e.g. if $A1 is 76¢ US then the value
given as input should be 0.76.
3. The output should include details of the exchange rate used as well as the final
amount in US money.
Program design
1. What data?
exchange rate
aus dollars
us dollars
If these are "real" numbers (i.e. C++ floats or doubles) we can use the fraction parts
to represent cents.
Data values are only needed in a single main() routine, so define them as local to
that routine.
3. Pseudo-code outline
a) prompt for and then input exchange rate;
b) prompt for and then input Australian dollar amount;
c) calculate US dollar amount;
d) print details of exchange rate and amount exchanged;
e) print details of US dollars obtained.
4. Test data
If the exchange rate has $A1.00 = $US0.75 then $A400 should buy $US300.
These values can be used for initial testing.
Implementation
Use the menu options in you integrated development environment to create a new
project with options set to specify standard C++ and use of the iostream library.
The IDE should create a "project" file with a main program file (probably named
"main.cp" but the name may vary slightly according to the environment that you
are using).
The IDE will create a skeletal outline for main(), e.g. Symantec 8 gives:
#include <stdlib.h>
134 Sequence
#include <iostream.h>
int main()
{
cout << "hello world" << endl;
return EXIT_SUCCESS;
}
(Symantec has the name EXIT_SUCCESS defined in its stdlib; it just equates to zero,
but it makes the return statement a little clearer.) Replace any junk filler code with
your variable declarations and code:
int main()
{
double ausdollar;
double usdollar;
double exchangerate;
usdollar = ausdollar*exchangerate;
cout << "You will get about $" << usdollar <<
" less bank charges etc." << endl;
return EXIT_SUCCESS;
}
Insert some comments explaining the task performed by the program. These
introductory comments should go either at the start of the file or after the #include
statements.
#include <stdlib.h>
#include <iostream.h>
/*
Program to do simple exchange rate calculations
*/
int main()
{
…
Try running your program under control from the debugger. Find how to
execute statements one by one and how to get data values displayed. Select the
data variables usdollar etc for display before starting program execution; each
time a variable is changed the value displayed should be updated in the debugger's
data window. The values initially displayed will be arbitrary (I got values like
1.5e-303); "automatic" variables contain random bit patterns until an assignment
statement is executed.
"Exchange rates" example 135
6.5.2 pH
Specification:
pH = -log10 [H+]
If you have forgotten you high school chemistry, the [H+] concentration
defines the acidity of an aqueous solution; its units are in "moles per litre" and
values vary from about 10 (very acidic) down to 0.00000000000001 (highly
alkaline). The pH scale, from about -1 to about 14, is just a more convenient
numeric scale for talking about these acidity values.
2. The user is to enter the pH value (should be in range -1..14 but there is no need
to check this); the program prints both pH value and [H+].
Program design
1. Data:
Just the two values, pH and HConc, both would be "doubles", both would be
defined as local to the main program.
pH = -log10 [H+]
136 Sequence
so
[H+] = 10^-pH
(meaning 10 to power -pH).
3. Program organization:
Implementation
Finding out about Many assignments will need a little "research" to find out about standard library
library functions functions that are required to implement a program.
Here, we need to check what the math.h library offers. Your IDE will let you
open any of the header files defining standard libraries. Usually, all you need do is
select the name of a header file from among those listed at the top of your program
file and use a menu command such as "Open selection". The header file will be
displayed in a new editor window. Don't change it! Just read through the list of
functions that it defines.
The library math.h has a function
These are "function prototypes" – they specify the name of the function, the type of
value computed (in all these cases the result is a double real number), and identify
the data that the functions need. Apart from pow() which requires two data items
to work with, all these functions require just one double precision data value. The
functions sin(), cos() and tan() require the value of an angle (in radians);
sqrt() and log10() must be given a number whose root or logarithm is to be
calculated.
You can understand how to work with these functions in a program by analogy
with the similar function keys on a pocket calculator. If you need the square root
of a number, you key it in on the numeric keys and get the number shown in the
display register. You then invoke the sqrt function by activating its key. The sqrt
function is implemented in code in the memory of the microprocessor in the
calculator. The code takes the value in the display register; computes with it to get
the square root; finally the result is put back into the display register.
It is very much the same in a program. The code for the calling program works "Passing arguments
out the value for the data that is to be passed to the function. This value is placed to a function"
on the stack (see section 4.8), which here serves the same role as the display in the
calculator. Data values passed to functions are referred to as "arguments of the
function". A subroutine call instruction is then made (section 2.4) to invoke the
code of the function. The code for the function takes the value from the stack,
performs the calculation, and puts the result back on the stack. The function
returns to the calling program ("return from subroutine" instruction). The calling
program collects the result from the stack.
The following code fragment illustrates a call to the sine function passing a data Using a mathematical
value as an argument: function
double angle;
double sine;
cout << "Enter angle : ";
cin >> angle;
sine = sin(angle*3.141592/180.0);
cout << "Sine(" << angle << ") = " << sine << endl;
#include <stdlib.h>
#include <iostream.h>
#include <math.h>
int main()
{
double pH;
double HConc;
The body of the main function must then be filled out with the code prompting for
and reading the pH value and then doing the calculation:
138 Sequence
cout << "If the pH is " << pH << " then the [H+] is " <<
HConc << endl;
C++ has rules that limit the forms of names used for variables.
In addition, you should remember that the libraries define some variables (e.g. the
variable cin is defined in the iostream libray). Use of a name that already has
been defined in a library that you have #included will result in an error message.
Avoid names that start with the underscore character "_". Normally, such
names are meant to be used for extra variables that a compiler may need to invent.
C++'s "reserved C++'s "reserved words" are:
words"
asm auto bool break case
catch char class const const_cast
continue default delete do double
dynamic_cast else enum extern false
float for friend goto if
inline int long mutable namespace
new operator private protected public
register reinterpret_cast return short
signed sizeof static struct switch
template this throw try typedef
typeid typename union unsigned using
virtual void volatile wchar_t while
You can't use any of these words as names of variables. (C++ is still undergoing
standardization. The set of reserved words is not quite finalized. Some words
Naming rules for variables 139
given in the list above are relatively new additions and some compilers won't
regard them as reserved.)
Unlike some other languages, C++ (and C) care about the case of letters. So,
for example, the names largest, Largest, LARGEST would be names of different
variables. (You shouldn't use this feature. Any coworkers who have to read your
program will inevitably get confused by such similar names being used for
different data items.)
6.7 CONSTANTS
It is a bad idea to type in the value of the constant at each place you use it: Don't embed "magic
numbers" in code!
/* cannonball is accelerated to earth by gravity ... */
Vert_velocity = -32.0*t*t;
...
If you need to change things (e.g. you want to calculate in metres rather than feet)
you have to go right through your program changing things (the gravitational
constant is 9.8 not 32.0 when you change to metres per second per second).
It is quite easy to make mistakes when doing such editing. Further, the program
just isn't clear when these strange numbers turn up in odd places (one keeps having
to refer back to separate documentation to check what the numbers might
represent).
C++ allows you to define constants – named entities that have an fixed values
set before the code is run. (The compiler does its best to prevent you from treating
these entities as variables!) Constants may be defined local to a particular routine,
but often they are defined at the start of a file so that they can be used in any
routine defined later in that file.
A constant definition has a form like Definition of a
constant
const Type Name = Value;
Type is the type of the constant, short, long, double etc. Name is its name. Value is
the value (it has to be something that the compiler can work out and is appropriate
to the type specified).
Examples "const"
As shown in last example, the value for a constant isn't limited to being a simple
number. It is anything that the compiler can work out.
Character constants, e.g. Query , have the character enclosed in single quote
marks (if the character is one of those like TAB that is represented by a special two
or four character code then this character code is bracketed by single quotes).
Double quote marks are used for multi-character text strings like those already
shown in output statements of example programs.
Integer constants have the sign (optional if positive) followed by a digit
sequence. Real number constants can be defined as float or double; fixed and
scientific formats are both allowed: 0.00123, or 1.23E-3.
You will sometimes see constant definitions with value like 49L. The L after
the digits informs the compiler that this number is to be represented as a long
integer. You may also see numbers given in hexadecimal, e.g. 0xA3FE, (and even
octal and binary forms); these usually appear when a programmer is trying to
define a specific bit pattern. Examples will be given later when bitwise operations
are illustrated.
#define The keyword const was introduced in C++ and later was retrofitted to the older
C language. Before const, C used a different way of defining constants, and you
will certainly see the old style in programs written by those who learnt C a long
time ago. C used "#define macros" instead of const definitions:
#define g 32.0
#define CREDITMARK 65
(There are actually subtle differences in meaning; for example, the #define
macros don't specify the type of the constant so a compiler can't thoroughly check
that a constant is used only in correct contexts.)
Specification
The program it print the area and the circumference of a circle with a radius given
as an input value.
Circle area example 141
Program design
1. Data:
The three values, radius, aread, and circumference, all defined as local to the
main program.
int main()
{
const double PI = 3.1415926535898;
double radius;
double area;
double circumference;
area = PI*radius*radius;
circumference = 2.0*PI*radius;
cout << "Area of circle of radius " << radius << " is "
<< area << endl;
cout << "\tand its circumference is " << circumference
<< endl;
}
When you plan a program, you think about the variables you need, you decide how
these variables should be initialized, and you determine the subsequent processing
steps. Initially, you tend to think about these aspects separately. Consequently, in
your first programs you have code like:
int main()
{
double x, y, z;
int n;
…
// Initialize
x = y = z = 1.0;
n = 0;
…
The initialization steps are done by explicit assignments after the definitions.
This is not necessary. You can specify the initial values of variables as you
define them.
int main()
142 Sequence
{
double x = 1.0, y = 1.0, z = 1.0;
int n = 0;
int main()
{
int n;
double d;
…
cin >> n;
…
d = n;
The code says assign the value of the integer n to the double precision real number
d.
As discussed in Chapter 1, integers and real numbers are represented using quite
different organization of bit pattern data and they actually require different
numbers of bytes of memory. Consequently, the compiler cannot simply code up
some instructions to copy a bit pattern from one location to another. The data value
would have to be converted into the appropriate form for the lvalue.
The compilers for some languages (e.g. Pascal) are set to regard such an
assignment as a kind of mistake; code equivalent to that shown above would result
in a warning or a compilation error.
Such assignments are permitted in C++. A C++ compiler is able to deal with
them because it has an internal table with rules detailing what it has to do if the
type of the lvalue does not match the type of the expression. The rule for
converting integers to reals are simple (call a "float" routine that converts from
integer to real). This rule would be used when assigning an integer to a double as
ing data values 143
in the assignment shown above, and also if the compiler noticed that a function that
needed to work on a double data value (e.g. sqrt()) was being passed an integer.
Because the compiler checks that functions are being passed the correct types of
data, the following code works in C++:
double d:
…
d = sqrt(2);
Since the math.h header file specifies that sqrt() takes a double, a C+ compiler
carefully puts in extra code to change the integer 2 value into a double 2.0.
Conversions of doubles to integers necessarily involve loss of fractional parts;
so the code:
…
int m, n;
double x, y;
…
x = 17.7; y = - 15.6;
m = x; n = y;
cout << m << endl;
cout << n << endl;
will result in output of 17 and -15 (note these aren't the integers that would be
closest in value to the given real numbers).
The math library contains two functions that perform related conversions from
doubles to integers. The ceil() function can be used to get the next integer
greater than a given double:
x = 17.7; y = - 15.6;
m = ceil(x); n = ceil(y);
cout << m << endl;
cout << n << endl;
You get a single line of output, with apparently just one number; the number 5517.
The default setting for output to cout prints numbers in the minimum amount of
space. So there are no spaces in front of the number, the sign is only printed if the
144 Sequence
number is negative, and there are no trailing spaces printed after the last digit. The
value fifty five can be printed using two 5 digits, the digits 1 and 7 for the value
seventeen follow immediately.
Naturally, this can confuse people reading the output. ("Shouldn't there be two
values on that line?" "Yes. The values are five and five hundred and seventeen, I
just didn't print them tidily.")
If you are printing several numeric values on one line, put spaces (or
informative text labels) between them. At least you should use something like:
EXERCISES
Programs that only use sequences of statements are severely limited and all pretty
much the same. They are still worth trying – simply to gain experience with the
development environment that is to be used for more serious examples.
1. Write a program that given the resistance of a circuit element and the applied voltage
will calculate the current flowing (Ohm's law). (Yes you do remember it – V = IR.)
2. Write a program that uses Newton's laws of motion to calculate the speed and the
distance travelled by an object undergoing uniform acceleration. The program is to take
as input the mass, and initial velocity of an object, and the constant force applied and
time period to be considered. The program is to calculate the acceleration and then use
this value to calculate the distance travelled and final velocity at the end of the specified
time period.
Loop constructs permit slightly more interesting programs. Like most Algol-
family languages, C++ has three basic forms of loop construct:
Repeat-loops are the least common; for-loops can become a bit complex; so, it is
usually best to start with "while" loops.
These have the basic form: While loops
while ( Expression )
body of loop;
Sometimes, the body of the loop will consist of just a single statement (which
would then typically be a call to a function). More often, the body of a loop will
involve several computational steps, i.e. several statements.
Compound Of course, there has to be a way of showing which statements belong to the
statements body of the loop. They have to be clearly separated from the other statements that
simply follow in sequence after the loop construct. This grouping of statements is
done using "compound statements".
C and C++ use { ("begin bracket") and } ("end bracket") to group the set of
statements that form the body of a loop.
while ( Expression ) {
statement-1;
statement-2;
...
statement-n;
}
/* statements following while loop, e.g. some output */
cout << "The result is ";
(These are the same begin-end brackets as are used to group all the statements
together to form a main-routine or another routine. Now we have outer begin-end
brackets for the routine; and inner bracket pairs for each loop.)
Code layout How should "compound statements" be arranged on a page? The code should
be arranged to make it clear which group of statements form the body of the while
loop. This is achieved using indentation, but there are different indenting styles
(and a few people get obsessed as to which of these is the "correct" style). Two of
the most common are:
Sometimes, the editor in the integrated development environment will choose for
you and impose a particular style. If the editor doesn't choose a style, you should.
Be consistent in laying things out as this helps make the program readable.
Comparison Usually, the expression that controls execution of the while loop will involve a
expressions test that compares two values. Such comparison tests can be defined using C++'s
comparison operators:
7.2 EXAMPLES
Those CFC compounds that are depleting the ozone level eventually decay away.
It is a typical decay process with the amount of material declining exponentially.
The rate of disappearance depends on the compound, and is usually defined in
terms of the compound's "half life". For example, if a compound has a half life of
ten years, then half has gone in ten years, only a quarter of the original remains
after 20 years etc.
Write a program that will read in a half life in years and which will calculate the
time taken (to the nearest half life period) for the amount of CFC compound to
decay to 1% of its initial value.
Specification:
1. The program is to read the half life in years of the CFC compound of interest
(the half life will be given as a real value).
2. The program is to calculate the time for the amount of CFC to decay to less
than 1% of its initial value by using a simple while loop.
3. The output should give the number of years that are needed (accurate to the
nearest half life, not the nearest year).
Program design
1. What data?
Some are obvious:
a count of the number of half-life periods
the half life
Another data item will be the "amount of CFC". This can be set to 1.0
initially, and halved each iteration of the loop until it is less than 0.01. (We
don't need to know the actual amount of CFC in tons, or Kilos we can work
with fractions of whatever the amount is.)
These are all "real" numbers (because really counting in half-lives, which may
be fractional, rather than years).
3. Processing:
a) prompt for and then input half life;
b) initialize year count to zero and amount to 1.0;
c) while loop:
terminates when "amount < 0.01"
body of loop involves
halving amount and
adding half life value to total years;
d) print years required.
Implementation
The variable declarations, and a constant declaration, are placed at the start of the
main routine. The constant represents 1% as a decimal fraction; it is best to define
this as a named constant because that makes it easier to change the program if the
calculations are to be done for some other limit value.
int main()
{
double numyears;
double amount;
double half_life;
The code would start with the prompt for, and input of the half-life value. Then,
the other variables could be initialized; the amount is 1.0 (all the CFC remains), the
year counter is 0.
As always, the while loop starts with the test. Here, the test is whether the
amount left exceeds the limit value; if this condition is true, then another cycle
through the body of the loop is required.
In the body of the loop, the count of years is increased by another half life period
and the amount of material remaining is halved.
The program would end with the output statements that print the result.
Problem:
The math library, that implements the functions declared in the math.h header file,
is coded using the best ways of calculating logarithms, square roots, sines and so
forth. Many of these routines use power series polynomial expansions to evaluate
their functions; sometimes, the implementation may use a form of interpolation in a
table that lists function values for particular arguments.
Before the math library was standardized, one sometimes had to write one's own
mathematical functions. This is still occasionally necessary; for example, you
might need a routine to work out the roots of a polynomial, or something to find the
"focus of an ellipse" or any of those other joyous exercises of senior high school
maths.
There is a method, an algorithm, due to Newton for finding roots of Successive
polynomials. It works by successive approximation – you guess, see whether you approximation
algorithm
are close, guess again to get a better value based on how close you last guess was,
and continue until you are satisfied that you are close enough to a root. We can
adapt this method to find the square root of a number. (OK, there is a perfectly
good square root function in the maths library, but all we really want is an excuse
for a program using a single loop).
If you are trying to find the square root of a number x, you make successive
guesses that are related according to the following formula
This new guess should be closer to the actual root the original guess. Then you try
again, substituting the new guess value into the formula instead of the original
guess.
Of course, you have to have a starting guess. Just taking x/2 as the starting
guess is adequate though not always the best starting guess.
You can see that this approach should work. Consider finding the square root of
9 (which, you may remember, is 3). Your first guess would be 4.5; your second
guess would be 0.5(9/4.5 + 4.5) or 3.25; the next two guesses would be 3.009615
and 3.000015.
You need to know when to stop guessing. That aspect is considered in the
design section below.
Now, if you restrict calculations to the real numbers of mathematics, the square
root function is only defined for positive numbers. Negative numbers require the
complex number system. We haven't covered enough C++ to write any decent
checks on input, the best we could do would be have a loop that kept asking the
user to enter a data value until we were given something non-negative. Just to
simplify things, we'll make it that the behaviour of the program is undefined for
negative inputs.
150 Iteration
Specification:
1. The program is to read the value whose root is required. Input is unchecked.
The behaviour of the program for invalid data, such as negative numbers is not
defined.
2. The program is to use half of the input value as the initial guess for its square
root.
4. The program is to print the current estimate for the root at each cycle of the
iterative process.
5. The program is to terminate after printing again the number and the converged
estimate of its square root.
Program design
1. What data?
the number whose root is required
the current estimate of the root
maybe some extra variables needed when checking for convergence
These should all be double precision numbers defined in the main program.
2. Processing:
a) prompt for and then input the number;
b) initialize root (to half of given number)
c) while loop:
test on convergence
body of loop involves
output of current estimate of root
calculation of new root
d) print details of number and root
The only difficult part of the processing is the test controlling iteration. The
following might seem suitable:
while(x != r*r)
This test will keep the iterative process going while x is not equal to r2 (after all,
that is the definition of r being the square root of x). Surprisingly, this doesn't work
in practice, at least not for all values of x.
Example iterative calculation: calculating a square root 151
The reason it doesn't work is "roundoff error". That test would be correct if we
were really working with mathematical real numbers, but in the computer we only
have floating point numbers. When the iteration has progressed as far as it can, the
value of root will be approximated by the floating point number that is closest to its
actual real value – but the square of this floating point number may not be exactly
equal the floating point value representing x.
The test could be rephrased. It could be made that we should keep iterating
until the difference between x and r2 is very small. The difference x - r 2 would be
positive if the estimate r was a little too small and would be negative if r was a
little too large. We just want the size of the difference to be small irrespective of
whether the value difference is positive or negative. So, we have to test the
absolute value of the difference. The maths library has a function fabs() that gets
the absolute value of a double number. Using this function, we could try the
following:
This version would be satisfactory for a wide range of numbers but it still won't
work for all. If you tried a very large value for x, e.g. 1.9E+71, the iteration
wouldn't converge. At that end of the number range, the "nearest floating point
numbers" are several integer values apart. It simply isn't possible to get the fixed
accuracy specified.
The test can't use an absolute value like 1.0E-8. The limit has to be expressed
relative to the numbers for which the calculation is being done.
The following test should work:
Implementation
#include <iostream.h>
#include <stdlib.h>
#include <math.h>
int main()
{
const double SMALLFRACTION = 1.0E-8;
double x;
double r;
cout << "Enter number : ";
cin >> x;
152 Iteration
r = x / 2.0;
while(fabs(x - r*r) > SMALLFRACTION*x) {
cout << r << endl;
r = 0.5 *(x / r + r);
}
cout << "Number was : " << x << ", root is "
<< r << endl;
return EXIT_SUCCESS;
}
Enter number : 81
40.5
21.25
12.530882
9.497456
9.013028
9.000009
Number was : 81, root is 9
Problem
If you are to compare different ways of solving problems, you need to define some
kind of performance scale. As you will learn more later, there are various ways of
measuring the performance of programs.
Often, you can find different ways of writing a program that has to process sets
of data elements. The time the program takes will increase with the number of data
elements that must be processed. Different ways of handling the data may have run
times that increase in different ways as the number of data elements increase. If the
rate of increase is:
linear time for processing 20 data elements will be twice that needed for
10 elements
quadratic time for processing 20 data elements will be 4x that needed for 10
elements
cubic time for processing 20 data elements will be 8x that needed for 10
elements
exponential time for processing 20 data elements will be about one thousand
times that needed for 10 elements
Usually, different ways of handling data are described in terms of how their run
times increase as the number of data elements (N) increases. The kinds of
functions encountered include
N linear
N2 quadratic
When you study algorithms later, you will learn which are linear, which are
quadratic and so on. But you need some understanding of the differences in these
functions, which you can get by looking at their numeric values.
The program should print values for these functions for values N=1, 2, 3, ...
The program is to stop when the values get too large (the function NN, "N to the
power N", grows fastest so the value of this function should be used in the check
for termination).
Specification:
Program design
1. What data?
Value N, also variables to hold values of N2, N3, log(N) etc
Some could be integers, but the ones involving logarithms will be real
numbers. Just for simplicity, all the variables can be made real numbers and
represented as "doubles".
3. Processing ...
a) initialize value of N, also variable that will hold NN, both get initialized to 1
b) while loop
terminates when value in variable for NN is "too large"
body of loop involves
calculating and printing out all those functions of N;
154 Iteration
Implementation
#include <iostream.h>
#include <math.h>
/*
Program to print lists of values ...
for N = 1, 2, 3, ...
values to be printed while N^N is not "too large".
*/
int main()
{
const char TAB = '\t';
// Used to help tidy output a little
const double TOO_LARGE = 1000000000000000.0;
// Arbitrary large value
double N, LGN,N_LGN, NSQ, NCUBE,TWO_POW_N, N_POW_N;
N = 1.0;
N_POW_N = 1.0;
Note the lines with multiple statements, e.g. NSQ = N*N; NCUBE = N*NSQ;. This
is perfectly legal. Statements are terminated by semicolons, not newlines. So you
can have statements that extend over several lines (like the monstrous output
statement), or you can have several statements to a line.
The program produced the following output:
1 0 0 1 1 2 1
2 0.693147 1.386294 4 8 4 4
3 1.098612 3.295837 9 27 8 27
4 1.386294 5.545177 16 64 16 256
5 1.609438 8.04719 25 125 32 3125
6 1.791759 10.750557 36 216 64 46656
Example iterative calculation: tabulating function values 155
7.3 BLOCKS
In the last example program, the variables N and N_pow_N were initialized outside
the while loop; all the others were only used inside the body of the while loop and
weren't referenced anywhere else.
If you have a "compound statement" (i.e. a { begin bracket, several statements,
and a closing } end bracket) where you need to use some variables not needed
elsewhere, you can define those variables within the compound statement. Instead
of having the program like this (with all variables declared at start of the main
function):
int main()
{
const char TAB = '\t';
const double TOO_LARGE = 1000000000000000.0;
double N, LGN,N_LGN, NSQ, NCUBE,TWO_POW_N, N_POW_N;
N = 1.0;
…
while (N_POW_N < TOO_LARGE) {
NSQ = N*N; NCUBE = N*NSQ;
…
}
}
You can make it like this, with only N and N_pow_N defined at start of program,
variables NSQ etc defined within the body of the while loop:
void main()
{
const char TAB = '\t';
const double TOO_LARGE = 1000000000000000.0;
double N, N_POW_N;
N = 1.0;
156 Iteration
…
while (N_POW_N < TOO_LARGE) {
double LGN,N_LGN, NSQ, NCUBE,TWO_POW_N;
NSQ = N*N; NCUBE = N*NSQ;
…
}
}
This is the reason why you will see "funny looking" while loops:
int n;
// read some +ve integer specify number of items to process
cin >> n;
while(n) {
...
...
n = n - 1;
}
True and False 157
The while(n) test doesn't involve a comparison operator. What is says instead is
"keep doing the loop while the value of n is non-zero".
Representing the true/false state of some condition by a 1 or a 0 is not always
clear. For example, if you see code like:
int married;
…
…
married = 1;
bool married;
…
…
married = true;
The compiler would then substitute the word int for all occurrences of Boolean
etc. So you could write:
Boolean married;
…
married = True;
int married;
…
married = 1;
Another programmer working on some other code for the same project might
have used a different approach. This programmer could have used a "typedef"
statement to tell the compiler that boolean was the name of a data type, or they
might have used an "enumeration" to specify the terms { false, true }. (Typedefs
are explained later in Chapter 11; enumerated types are introduced in Chapter 16).
This programmer would have something code like:
Of course, it all gets very messy. There are Booleans, and booleans, and True and
true.
bool type in new C++ The standards committee responsible for the definition of C++ has decided that
standard it is time to get rid of this mess. The proposed standard for C++ makes bool a built
in type that can take either of the constant values true or false . There are
conversions defined that convert 0 values (integer 0, "null-pointers") to false, and
non-zero values to true.
A compiler that implements the proposed standard will accept code like:
bool married;
…
…
married = true;
The simple boolean expressions that can be used for things like controlling while
loops are:
while(n) …
while(j >= k) …
Sometimes you want to express more complex conditions. (The conditions that
control while loops should be simple; the more complex boolean expressions are
more likely to be used with the if selection statements covered in Chapter 8.)
Expressions using AND and OR operators 159
For example, you might have some calculation that should be done if a double
value x lies between 0.0 and 1.0. You can express such a condition by combining
two simple boolean expressions:
The requirement for x being in the range is expressed as "x is greater than 0.0 The "&&" AND
AND x is less than 1.0". The AND operation is done using the && operator (note, operator
in C++ a single & can appear as an operator in other contexts – with quite different
meanings).
There is a similar OR operator, represented by two vertical lines ||, that can The "||" OR operator
combine two simpler boolean expressions. The expression:
is true if either the value of error_estimate exceeds its 0.0001 OR the number of
iterations is less than 10. (Again, be careful; a single vertical line | is also a valid
C++ operator but it has slightly different meaning from ||.)
Along with an AND operator, and an OR operator, C++ provides a NOT The "!" NOT
operator. Sometimes it is easier to express a test in a sense opposite that required; operator
for example, in the following code the loop is executed while an input character is
not one of the two values permitted:
char ch;
…
cout << "Enter patient's gender (M for Male or"
" F for Female)" << endl;
cin >> ch;
while(!((ch == 'M') || (ch == 'F'))) {
cout << "An entry of M or F is required here. "
"Please re-enter gender code" << endl;
cin >> ch;
}
The expression ((ch == 'M') || (ch == 'F')) returns true if the input
character is either of the two allowed values. The NOT operator, !, reverses the
truth value – so giving rise to the correct condition to control the input loop.
Expressions involving AND and OR operators are evaluated by working out the Evaluation of
values of the simple subexpressions one after the other reading the line from left to boolean expressions
right until the result is known (note, as explained below, AND takes precedence
over OR so the order of evaluation is not strictly left to right). The code generated
for an expression like:
A || B || C
(where A, B, and C are simple boolean expressions) would evaluate A and then
have a conditional jump to the controlled code if the result was true. If A was not
true, some code to evaluate B would be executed, again the result would be tested.
The code to evaluate C would only be executed if both A and B evaluated to false.
160 Iteration
Parentheses If you have complex boolean expressions, make them clear by using
parentheses. Even complex expressions like
(A || B) && (C || D) && !E
X || Y && Z
X || (Y && Z)
but your thinking time when you read the expression without parentheses is much
longer. You have to remember the "precedence" of operators when interpreting
expressions without parentheses.
Expressions without parentheses do all have meanings. The compiler is quite
happy with something like:
It "knows" that you don't mean to test whether i is less than limit && n; you
mean to test whether i is less than limit and if that is true you want also to test
that n isn't equal to zero.
The meanings of the expressions are defined by operator precedence. It is
exactly the same idea as how you learnt to interpret arithmetic expressions in your
last years of primary school. Then you learnt that multiply and divide operators
had precedence over addition and subtraction operators.
The precedence table has just been extended. Part of the operator precedence
table for C++ is:
You can think of these precedence relations as meaning that the operators higher in
the table are "more important" and so get first go at manipulating the data. (The ++
increment and -- operators are explained in section 7.5.)
Because of operator precedence, a statement like the following can be
unambiguously interpreted by a compiler:
(It means that res gets a 1 (true) or 0 (false) value; it is true if the value of n (which
should itself be either true or false) is the same as the true or false result obtained
when checking whether the value of m exceeds 7 times the value of k after k has
first been incremented!)
Legally, you can write such code in C++. Don't. Such code is "write only
code". No one else can read it and understand it; everyone reading the code has to
stop and carefully disentangle it to find out what you meant.
If you really had to work out something like that, you should do it in steps:
++k;
int temp = m > 7*k; // bool temp if your compiler has bool
res = n == temp;
This is a perfectly legal C++ test that will be accepted by the compiler which will
generate code that will execute just fine. Curiously, the code generated for this test
returns true for x = -17.5, x = 0.4, x = 109.7, and in fact for any value of x!
The compiler saw that expression as meaning the following:
So, the code, that the compiler generated, says "true" whatever the value of x.
Another intuitive (but WRONG) boolean expression is the following (which a
student once invented to test whether a character that had been read was a vowel)
Once again, this code is perfectly legal C++. Once again, its meaning is something
quite different from what it might appear to mean. Code compiled for this
expression will return true whatever the value in the character variable ch.
The == operator has higher precedence than the ||. So this code means
Now this is true when the character read was 'a'. If the character read wasn't 'a', the
generated code tests the next expression, i.e. the 'e'. Is 'e' zero (false) or non-zero
(true)? The value of 'e' is the numeric value of the ASCII code that represents this
character (101); value 101 isn't zero, so it must mean true.
The student wasn't much better off when she tried again by coding the test as:
162 Iteration
This version said that no character was a vowel. (Actually, there is one character
that gets reported as a vowel, but it is the control character "start of header" that
you can't type on a keyboard).
The student could have expressed the test as follows:
As noted in other contexts, the C and C++ languages have lots of abbreviated forms
that save typing. There are abbreviations for some commonly performed arithmetic
operations. For example, one often has to add one to a counter:
count_customers = count_customers + 1;
count_customers += 1;
The "++" increment
operator or even to
count_customers++;
Actually, these short forms weren't introduced solely to save the programmer
from typing a few characters. They were meant as a way that the programmer
could give hints that helped the early C compilers generate more efficient code.
An early compiler might have translated code like
temp = temp + 5;
into a "load instruction", an "add instruction", and a "store instruction". But the
computer might have had an "add to memory instruction" which could be used
instead of the three other instructions. The compiler might have had an alternative
coding template that recognized the += operator and used this add to memory
instruction when generating code for things like:
temp += 5;
Similarly, there might have been an "increment memory" instruction (i.e. add 1
to contents of memory location) that could be used in a compiler template that
generated code for a statement like
Short forms, C++ abbreviations 163
temp++;
Initially, these short forms existed only for addition and subtraction, so one had
the four cases
++ increment
+= Expression add value of expression
-- decrement (i.e. reduce by 1)
-= Expression subtract value of expression
Later, variants of += and -= were invented for things like * (the multiplication
operator), / (the division operator), and the operators used to manipulate bit-
patterns ("BIT-OR" operator, "BIT-AND" operator, "BIT-XOR" operator etc ---
these will be looked at later, Chapter 18). So one can have:
(Fortunately, the inventors of the C language didn't add things like a** or b// –
because they couldn't agree what these formulae would mean.)
These short forms are OK when used as simple statements:
N++;
N = N + 1;
Years += Half_life;
this changes both xval and n. While legal in both C and C++, such usage should
be avoided (statements with embedded increment, ++, or decrement, --, operators
are difficult to understand and tend to be sources of error).
Expressions with embedded ++ and -- operators are made even more complex Pre-op and post-op
and confusing by the fact that there are two versions of each of these operators. versions of ++ and --
The ++ (or --) operator can come before the variable it changes, ++x, or after
the variable, x++. Now, if you simply have a statement like
x++; or ++x;
164 Iteration
it doesn't matter which way it is written (though x++ is more common). Both
forms produce identical results (x is incremented).
But, if you have these operators buried in more complex expressions, then you
will get different results depending on which you use. The prefix form (++x )
means "increment the value of x and use the updated value". The postfix form
(x++) means "use the current value of x in evaluating the expression and also
increment x afterwards". So:
n = 15;
temp = 7 * n++;
cout << "temp " << temp << endl;
cout << "n " << n << endl;
n = 15;
temp = 7 * ++n;
cout << "temp " << temp << endl;
cout << "n " << n << endl;
anything more complex should be avoided until you are much more confident in
your use of C++.
Why C++? Now you understand why language is called C++:
take the C language and add a tiny bit to get one better.
7.6 DO …WHILE
The standard while loop construct has a variation that is useful if you know that a
loop must always be executed at least once.
This variation has the form
do
statement
while (expression);
Usually, the body of the loop would be a compound statement rather than a simple
statement; so the typical "do" loop will look something like:
do {
…
…
} while (expression);
Repeat (do … while) loop 165
The expression can be any boolean expression that tests values that get updated
somewhere in the loop.
A contrived example –
Often, programs need loops that process each data item from some known fixed
size collection. This can be handled quite adequately using a standard while loop –
// Initialize counter
int loop_count = 0;
// loop until limit (collection size) reached
while(loop_count < kCOLLECTION_SIZE) {
// Do processing of next element in collection
…
// Update count of items processed
loop_count++;
}
The structure of this counting loop is: initialize counter, loop while counter less
than limit, increment counter at the end of the body of the loop.
It is such a common code pattern that it deserves its own specialized statement –
the for statement.
int loop_count;
for(loop_count=0;
loop_count < kCOLLECTION_SIZE;
loop_count++) {
// Do processing of next element in collection
…
}
There are three parts to the parenthesised construct following the keyword for.
The first part will be a simple statement (compound statements with begin { and }
end brackets aren't allowed); normally, this statement will initialize the counter
variable that controls the number of times the loop is to execute. The second part
will be a boolean expression; the loop is to continue to execute as long as this
expression is true. Normally, this second part involves a comparison of the current
value of the loop control variable with some limit value. Just as in a while loop,
this loop termination test is performed before entering the body of the loop; so if
the condition for termination is immediately satisfied the body is never entered
(e.g. for(i=5; i<4; i++) { … }). The third part of the for(…;…;…) contains
expressions that are evaluated after the body has been executed. Normally, the
third part will have code to increment the loop control variable.
A for loop "controls" execution of a statement; usually this will be a compound
statement but sometimes it will be a simple statement (generally a call to a
function). The structures are:
for(…; …; …) for(i=0;i<10;i++)
statement; sum += i;
for(…; …; …) { for(i=0;i<10;i++) {
statement; cout << i << ":";
statement; …
… …
} }
You can have loops that have the loop control variable decreasing –
Don't change the If you are using a for as a counting loop, then you should not alter the value of
loop control variable the control variable in the code forming the body of the loop. It is legal to do so;
in the body of the
code but it is very confusing for anyone who has to read your code. It is so confusing
that you will probably confuse yourself and end up with a loop that has some bug
associated with it so that under some circumstances it doesn't get executed the
correct number of times.
Defining a loop As noted earlier, section 7.3, variables don't have to be defined at the start of a
control variable in block. If you know you are going to need a counting loop, and you want to follow
the for statement
convention and use a variable such as i as the counter, you can arrange your code
like this where i is defined at the start of the block:
For 167
int main()
{
int i, n;
cout << "Enter number of times loop is to execute";
cin >> n;
for(i=0; i < n; i++) {
// loop body with code to do something
// interesting
…
}
cout << "End of loop, i is " << i << endl;
…
}
or you can have the following code where the loop control variable i is defined
just before the loop:
int main()
{
int n;
cout << "Enter number of times loop is to execute";
cin >> n;
int i;
for(i=0; i < n; i++) {
…
}
cout << "End of loop, i is " << i << endl;
…
}
int main()
{
int n;
cout << "Enter number of times loop is to execute";
cin >> n;
for(int i=0; i < n; i++) {
…
}
cout << "End of loop, i is " << i << endl;
…
}
A loop control variable can be defined in that first part of the for(…;…;…) Note area of possible
statement. Defining the control variable inside the for is no different from language change
defining it just before the for . The variable is then defined to the end of the
enclosing block. (There are other languages in the Algol family where loop control
variables "belong" to their loops and can only be referenced within the body of
their loop. The new standard for C++ suggests changing the mechanism so that the
loop control variable does in fact belong with the loop; most compilers don't yet
implement this.)
168 Iteration
You are not restricted in the complexity of the code that you put in the three
parts of the for(…;…;…). The initialization done in the first part can for example
involve a function call: for(int i = ReadStartingValue(); …; …). The
termination test can be something really elaborate with a fearsome boolean
expression with lots of && and || operators.
You aren't allowed to try to fit multiple statements into the three parts of the
for construct. Statements end in semicolons. If you tried to fit multiple
statements into the for you would get something like
The semicolons after the extra statements would break up the required pattern
for(… ; … ; …).
The comma operator The C language, and consequently the C++ language, has an alternative way of
sequencing operations. Normally, code is written as statement; statement;
statement; etc. But you can define a sequence of expressions that have to be
evaluated one after another; these sequences are defined using the comma ","
operator to separate the individual expressions:
Another oddity in C (and hence C++) is that the "assignment statement" is actually
an expression. So the following are examples of single statements:
The first statement involves four assignment expressions that initialize i and sum
to 0 and max and min to the values of the constants SHRT_MAX etc. The second
statement has two expressions updating the values of i and sum. (It was the
comma operator that caused the problems noted in section 6.4 with the erroneous
input statement cin >> xcoord, ycoord;. This statement actually consists of
two comma separated expressions – an input expression and a "test the value of"
expression.)
Although you aren't allowed to try to fit multiple statements into the parts of a
for, you are permitted to use these comma separated sequences of expressions. So
you will frequently see code like:
(It isn't totally perverse code. Someone might want to have a loop that runs
through a set of data elements doing calculations and printing results for each; after
every 5th output there is to be a newline. So, the variable i counts through the
elements in the collection; the variable j keeps track of how many output values
have been printed on the current line.)
Because you can use the comma separated expressions in the various parts of a
for, you can code things up so that all the work is done in the for statement itself
and the body is empty. For example, if you wanted to calculate the sum and
product of the first 15 integers you could write the code the simple way:
A form like:
for(…; …; …);
(with the semicolon immediately after the closing parenthesis) is legal. It means
that you want a loop where nothing is done in the body because all the work is
embedded in the for(…;…;…). (You may get a "warning" from your compiler if
you give it such code; some compiler writer's try to spot errors like an extraneous
semicolon getting in and separating the for(…;…;…) from the body of the loop it is
supposed to control.)
While you often see programs with elaborate and complex operations performed
inside the for(…;…;…) (and, possibly, no body to the loop), you shouldn't write
code like this. Remember the "KISS principle" (Keep It Simple Stupid). Code that
is "clever" requires cleverness to read, and (cleverness)2 to write correctly.
A while loop can do anything that a for loop can do.
Conversely, a for loop can do anything a while loop can do.
Instead of the code given earlier (in 7.2.2):
r = x / 2.0;
while(fabs(x - r*r) > SMALLFRACTION*x) {
cout << r << endl;
r = 0.5 *(x / r + r);
}
cout << "Number was : " << x << ", root is "
<< r << endl;
170 Iteration
r = x / 2.0;
for( ; // an empty initialization part
fabs(x - r*r) > SMALLFRACTION*x; // Terminated?
) { // Empty update part
cout << r << endl;
r = 0.5 *(x / r + r);
}
cout << "Number was : " << x << ", root is "
<< r << endl;
for(r = x / 2.0;
fabs(x - r*r) > SMALLFRACTION*x;
r = 0.5 *(x / r + r))
cout << r << endl;
cout << "Number was : " << x << ", root is "
<< r << endl;
There are two other control statements used in association with loop constructs.
The break and continue statements can appear in the body of a loop. They
appear as the action parts of if statement (section 8.3). The conditional test in the
if will have identified some condition that requires special processing.
break A break statement terminates a loop. Control is transferred to the first
statement following the loop construct.
continue A continue statement causes the rest of the body of the loop to be omitted, at
least for this iteration; control is transferred to the code that evaluates the normal
test for loop termination.
Examples of these control statements appear in later programs where the code is
of sufficient complexity as to involve conditions requiring special processing (e.g.
use of continue in function in 10.9.1).
EXERCISES
1. Implement versions of the "square root" program using the "incorrect" tests for
controlling the iterative loop. Try finding values for x for which the loop fails to
terminate. (Your IDE system will have a mechanism for stopping a program that isn't
terminating properly. One of the command keys will send a stop signal to the program.
Exercises 171
Check the IDE documentation to find how to force termination before starting the
program.)
If your compiler supports "long doubles" change the code of your program to use the
long double data type and rerun using the same data values for which the original
program failed. The modified program may work. (The long double type uses more bits
to represent the mantissa of a floating point number and so is not as easily affected by
round off errors.)
2. Write a program that prints the values of a polynomial function of x at several values of
x. The program should prompt for the starting x value, the increment, and the final x
value.
of
or
The first is the most costly to evaluate because of the pow() function calls. The last is
the most efficient (it involves fewer multiplication operations than the second version).
(For this exercise, use a fixed polynomial function. List its values at intervals of 0.5 for
x in range -5.0 to + 5.0.)
3. Generalize the program from exercise 2 to work with any polynomial of a given
maximum degree 4, i.e. a function of the form c4 x4 + c3 x3 + c2 x2 + c1 x +
c0 for arbitrary values of the coefficients c4, c3, c2, c1, and c0.
The program is to prompt for and read the values of the coefficients and then, as in
exercise 2 it should get the range of x values for which the polynomial is to be
evaluated.
Write a program that reads the height from which the object is dropped and which prints
a table showing the object's velocity and height at one second intervals. The loop
172 Iteration
printing details should terminate when the distance fallen is greater than or equal to the
specified height.
The program is to prompt for and read in the initial value for the funds in the account. It
is then to loop prompting for and reading transactions; deposits are to be entered as
positive values, withdrawals as negative values. Entry of the value 0 (zero) is to
terminate the loop. When the loop has finished, the program is to print the final value
for the funds in the account.
8
8 Selection
8.1 MAKING CHOICES
Loop constructs permit the coding of a few simple numerical calculations; but most
programs require more flexibility. Different input data usually have to be
interpreted in different ways. As a very simple example, imagine a program
producing some summary statistics from lots of data records defining people. The
statistics required might include separate counts of males and females, counts of
citizens and resident-aliens and visitors, counts of people in each of a number of
age ranges. Although similar processing is done for each data record, the particular
calculation steps involved will depend on the input data. Programming languages
have to have constructs for selecting the processing steps appropriate for particular
data.
The modern Algol family languages have two kinds of selection statement:
and
There are typically two or three variations on the "if" statement. Usually, they
differ in only minor ways; this is possibly why beginners frequently make mistakes
with "if"s. Beginners tend to mix up the different variants. Since "if"s seem a little
bit error prone, the "switch" selection statement will be introduced first.
switch statement
The C/C++ switch statement is for selecting one processing option from among a
choice of several. One can have an arbitrary number of choices in a switch
statement.
Each choice has to be given an identifying "name" (which the compiler has to
be able to convert into an integer constant). In simple situations, the different
174 Selection
choices are not given explicit names, the integer numbers will suffice. Usually, the
choice is made by testing the value of a variable (but it can be an expression).
Consider a simple example, the task of converting a date from numeric form
<day> <month> <year>, e.g. 25 12 1999, to text December 25th 1999. The month
would be an integer data value entered by the user:
The name of the month could be printed in a switch statement that used the value of
"month" to select the appropriate name. The switch statement needed to select the
month could be coded as:
...
switch(month) {
case 1:
cout << "January ";
break;
case 2:
cout << "February ";
break;
…
…
case 12:
cout << "December ";
break;
}
(month)
A parenthesised expression that yields the integer value that is used to make the
choice. Often, as in this example, the expression simply tests the value of a
variable.
case
The keyword case. This marks the start of the code for one of the choices.
1
Switch statement 175
The integer value, as a simple integer or a named constant, associated with this
choice.
The code that does the special processing for this choice.
break;
The keyword break that marks the end of the code section for this choice (or case).
case 2:
Finally, the } 'end block' bracket to match the { at the start of the set of choices.
Be careful when typing the code of a switch; in particular, make certain that you
pair your case ... break keywords.
You see, the following is legal code, the compiler won't complain about it:
case 1:
cout << "January ";
case 2:
cout << "February ";
break;
But if the month is 1, the program will execute the code for both case 1 and case 2
and so print "January February ".
C/C++ allows "case fall through" (where the program continues with the code "case fall through"
of a second case) because it is sometimes useful. For example, you might have a
program that uses a switch statement to select the processing for a command
entered by a user; two of the commands might need almost the same processing
with one simply requiring an extra step :
(If you intend to get "case fall through", make this clear in a comment.)
Another example where case fall through might be useful is a program to print
the words of a well known yuletide song:
switch(day_of_christmas) {
case 12:
cout << "Twelve lords a leaping";
case 11:
...
...
case 3:
cout << "Three French hens" << endl;
case 2:
cout << "Two turtle doves, " << endl;
cout << "and " << endl;
case 1:
cout << "A partridge in a pear tree";
};
The "case labels" (the integers identifying the choices) don't have to be in
sequence, so the following is just as acceptable to the compiler (though maybe
confusing to someone reading your code):
switch(month) {
case 4:
cout << "April ";
break;
case 10:
cout << "October ";
break;
...
case 2:
cout << "February";
break;
}
void main()
{
int day, month, year;
...
switch(month) {
case JAN:
cout << "January ";
break;
case FEB:
cout << "February ";
break;
...
case DEC:
cout << "December ";
break;
}
...
As well as printing the name of the month, the dates program would have to Another switch
print the day as 1st, 2nd, 3rd, 4th, ..., 31st. The day number is easily printed --- statement
but what about the suffixes 'st', 'nd', 'rd', and 'th'?
Obviously, you could get these printed using some enormous case statement
switch(day) {
case 1: cout << "st "; break;
case 2: cout << "nd "; break;
case 3: cout << "rd ", break;
case 4: cout << "th "; break;
case 5: cout << "th "; break;
case 6: cout << "th "; break;
case 7: cout << "th "; break;
...
case 21: cout << "st "; break;
case 22: cout << "nd "; break;
...
case 30: cout << "th "; break;
case 31: cout << "st "; break;
}
Fortunately, this can be simplified. There really aren't 31 different cases that Combining similar
have to be considered. There are three special cases: 1, 21, and 31 need 'st'; 2 and cases
22 need 'nd'; and 3 and 23 need 'rd'. Everything else uses 'th'.
The case statement can be simplified to:
switch (day) {
case 1:
case 21:
178 Selection
case 31:
cout << "st ";
break;
case 2:
case 22:
cout << "nd ";
break;
case 3:
case 23:
cout << "rd ";
break;
default:
cout << "th ";
break;
}
Where several cases share the same code, that code can be labelled with all
those case numbers:
case 1:
case 21:
case 31:
cout << "st ";
break;
(You can view this as an extreme example of 'case fall through'. Case 1 --- do
nothing then do everything you do for case 21. Case 21 --- do nothing, then do
everything you do for case 31. Case 31, print "st".)
Default section of Most of the days simply need "th" printed. This is handled in the "default" part
switch statement of the switch statement.
switch (day) {
case 1:
case 21:
case 31:
cout << "st ";
break;
...
...
default:
cout << "th ";
break;
}
The compiler will generate code that checks the value used in the switch test
against the values associated with each of the explicit cases, if none match it
arranges for the "default" code to be executed.
A default clause in a switch statement is often useful. But it is not necessary to
have an explicit default; after all, the first example with the months did not have a
default clause. If a switch does not have a default clause, then the compiler
generated code will be arranged so as to continue with the next statement following
after the end of the switch if the value tested by the switch does not match any of
the explicit cases.
Switch statement 179
switch (month) {
...
}
and
switch (day) {
...
}
Loop and selection constructs are sufficient to construct some quite interesting
programs. This example is still fairly simple. The program is to simulate the
workings of a basic four function calculator
Specification:
the program is to perform the specified calculation and then display the new
current value.
4 The program is to accept the character 'C' (entered instead of an operator) as a
command to clear (set to zero) the "current value".
5 The program is to terminate when the character 'Q' is entered instead of an
operator.
Preliminary design: Programs are designed in an iterative fashion. You keep re-examining the problem
at increasing levels of detail.
First iteration The first level is simple. The program will consist of
through the design
process
• some initialization steps
• a loop that terminates when the 'Q' (Quit) command is entered.
Second iteration The second iteration through the design process would elaborate the "setup" and
through the design "loop" code:
process
• some initialization steps:
set current value to 0.0
display current value
prompt user, asking for a command character
read the command character
The part about prompting for a number and performing the required operation Third iteration
still needs to be made more specific. The design would be extended to something through the design
process
like the following:
display result
prompt for another command,
read command character
The preliminary design phase for a program like this is finished when you've Detailed design
developed a model of the various processing steps that must be performed.
You must then consider what data you need and how the data variables are to be
stored. Later, you have to go through the processing steps again, making your
descriptions even more detailed.
Only three different items of data are needed: Data
int main()
{
double displayed_value;
double new_entry;
182 Selection
char command_character;
The while loop We can temporarily ignore the switch statement that processes the commands
and concentrate solely on the while loop structure. The loop is to terminate when a
'Q' command has been entered. So, we can code up the loop control:
The loop continues while the command character is NOT 'Q'. Obviously we had
better read another command character each time we go round the loop. We have to
start the loop with some command character already specified.
Part of the setup code prompts for this initial command:
#include <iostream.h>
#include <stdlib.h>
int main()
{
double displayed_value;
double new_entry;
char command_character;
displayed_value = 0.0;
The code outlined works even if the user immediately enters the "Quit" command!
Pattern for a loop The pattern
processing input data
Desk calculator example 183
• next input value is read at the end of the body of the loop
is very common. You will find similar while loops in many programs.
Selection of the appropriate processing for each of the different commands can Switch statement
obviously be done with a switch statement:
switch (command_character) {
...
}
Each of the processing options is independent. We don't want to share any code
between them. So, each of the options will be in the form
case ... :
statements
break;
The case of a 'Q' (quit command) is handled by the loop control statement. The
switch only has to deal with:
Each of these cases has to be distinguished by an integer constant and the switch
statement itself must test an integer value.
Fortunately, the C and C++ languages consider characters like 'C', '+' etc to be
integers (because they are internally represented as small integer values). So the
characters can be used directly in the switch() { } statement and for case labels.
switch(command_character) {
case 'C':
displayed_value = 0.0;
break;
case '+':
cout << "number>";
cin >> new_entry;
displayed_value += new_entry;
break;
case '-':
cout << "number>";
184 Selection
The code for the individual cases is straightforward. The clear case simply sets
the displayed value to zero. Each of the arithmetic operators is coded similarly – a
prompt for a number, input of the number, data combination.
Suppose the user enters something inappropriate – e.g. an 'M' command (some
calculators have memories and 'M', memorize, and 'R' recall keys and a user might
assume similar functionality). Such incorrect inputs should produce some form of
error message response. This can be handled by a "default" clause in the switch
statement.
default:
cout << "Didn't understand input!";
If the user has does something wrong, it is quite likely that there will be other
characters that have been typed ahead (e.g. the user might have typed "Hello", in
which case the 'H' is read and recognized as invalid input – but the 'e', 'l', 'l' and the
'o' all remain to be read).
Removing invalid When something has gone wrong it is often useful to be able to clear out any
characters from the unread input. The cin object can be told to do this. The usual way is to tell cin to
input
ignore all characters until it finds something obvious like a newline character. This
is done using the request cin.ignore(...) . The request has to specify the
sensible marker character (e.g. '\n' for newline) and, also, a maximum number of
characters to ignore; e.g.
cin.ignore(100,'\n');
This request gets "cin" to ignore up to 100 characters while it searches for a
newline; that should be enough to skip over any invalid input.
With the default statement to report any errors (and clean up the input), the
complete switch statement becomes:
switch(command_character) {
case 'C':
displayed_value = 0.0;
break;
case '+':
cout << "number>";
Desk calculator example 185
Since there aren't any more cases after the default statement, it isn't necessary to
pair the default with a break (it can't "fall through" to next case if there is no next
case) But, you probably should put a break in anyway. You might come along
later and add another case at the bottom of the switch and forget to put the break
in.
8.3 IF
Switch statements can handle most selection tasks. But, they are not always
convenient.
You can use a switch to select whether a data value should be processed:
be counters for students who had failed (mark < 50), got Ds (mark < 65), Cs (<75),
Bs (<85) and As. The main program structure would be something like:
int main()
{
int A_Count = 0, B_Count = 0, C_Count = 0,
D_Count = 0, F_Count = 0;
int mark;
cin >> mark;
while(mark >= 0) {
code to select and update appropriate counter
cin >> mark; // read next, data terminated by -1
}
// print counts
…
…
}
switch(mark) {
case 0:
case 1:
case 2:
…
case 49:
F_Count++;
break;
case 50:
case 51:
…
case 64:
D_Count++;
break;
…
…
break;
case 85:
case 86:
case 100:
A_Count++;
break;
}
if(boolean expression)
statement;
Example:
If 187
(also, some debuggers won't let you stop on a simple statement controlled by an if
but will let you stop in a compound statement). Note, you don't put a semicolon
after the } end bracket of the compound statement; the if clause ends with the
semicolon after a simple statement or with the } end bracket of a compound
statement.
The if … else … control statement allows you to select between two if … else
alternative processing actions:
if(boolean expression)
statement;
else
statement;
Example:
if(gender_tag == 'F')
females++;
else
males++;
You can concatenate if … else control statements when you need to make a if … else if … else if
selection between more than two alternatives: … else …
if(mark<50) {
F_Count++;
cout << "another one bites the dust" << endl;
}
else
if(mark<65)
D_Count++;
else
if(mark<75)
C_Count++;
else
188 Selection
if(mark<85)
B_Count++;
else {
A_Count++;
cout << "*****" << endl
}
You have to end with an else clause. The statements controlled by different if
clauses can be either simple statements or compound statements as required.
Nested ifs and related You need to be careful when you want to perform some complex test that
problems depends on more than one condition. The following code is OK:
if(mark<50) {
F_Count++;
if((exam_mark == 0) && (assignment_count < 2))
cout << "Check for cancelled enrollment" << endl;
}
else
if(mark<65)
D_Count++;
else
…
Here, the { and } brackets around the compound statement make it clear that the
check for cancelled enrollments is something that is done only when considering
marks less than 50.
The { } brackets in that code fragment were necessary because the mark<50
condition controlled several statements. Suppose there was no need to keep a count
of students who scored less than half marks, the code would then be:
if(mark<50) {
if((exam_mark == 0) && (assignment_count < 2))
cout << "Check for cancelled enrollment" << endl;
}
else
if(mark<65)
…
Some beginners might be tempted to remove the { } brackets from the mark<50
condition; the argument might be that since there is only one statement "you don't
need a compound statement construction". The code would then become:
compiler encounters the keyword else it has to go back through the code that it
thought that it had dealt with to see whether the preceding statement was an if that
it could use to hang this else onto. An else clause gets attached to an
immediately preceding if clause. So the compiler's reading of the buggy code is:
if(mark<50)
if((exam_mark == 0) && (assignment_count < 2))
cout << "Check for cancelled enrollment" << endl;
else
if(mark<65)
D_Count++;
else
…
This code doesn't perform the required data processing steps. What happens now is
that students whose marks are greater than or equal to 50 don't get processed – the
first conditional test eliminates them. A warning message is printed for students
who didn't sit the exam and did at most one assignment. The count D_Count is
incremented for all other students who had scored less than 50, after all they did
score less than 65 which is what the next test checks.
It is possible for the editor in a development environment to rearrange the layout
of code so that the indentation correctly reflects the logic of any ifs and elses. Such
editors reduce your chance of making the kind of error just illustrated. Most
editors don't provide this level of support; it is expected that you have some idea of
what you are doing.
Another legal thing that you shouldn't do is use the comma sequencing operator
in an if:
Sure this is legal, it just confuses about 75% of those who read it.
You might expect this by now – the C and C++ languages have lots of abbreviated
forms that save typing, one of these abbreviated forms exist for the if statement.
Often, you want code like:
or
190 Selection
or
The conditional expression has three parts. The first part defines the condition that
is to be tested. A question mark separates this from the second part that specifies
the result of the whole conditional expression for the case when the condition
evaluates to true. A colon separates this from the third part where you get the
result for cases where the condition is false.
Using this conditional expression, the code examples shown above may be
written as follows:
Caution – watch out Note: remember to use == as the equality test operator and not the =
for typos related to assignment operator! The following is legal code:
== operator
cout << ((gender_tag = 'f') ? "Female : " : "Male : ");
but it changes the value of gender_tag, then confirms that the new value is non
zero and prints "Female : ".
Terminating a program 191
Sometimes, you just have to stop. The input is junk; your program can do no more.
It just has to end.
The easy way out is to use a function defined in stdlib. This function, exit(), exit() function
takes an integer argument. When called, the exit() function terminates the
program. It tries to clean up, if you were using files these should get closed. The
integer given to exit() gets passed back as the return status from the program
(like the 0 or EXIT_SUCCESS value returned by the main program). A typical IDE
will simply discard this return value. It is used in scripting environments, like
Unix, where the return result of a program can be used to determine what other
processing steps should follow. The normal error exit value is 1; other values can
be used to distinguish among different kinds of error.
Several of the later examples use calls to exit() as an easy way to terminate a
program if the data given are found to be invalid. For example, the square root
program (7.2.2) could check the input data as follows:
#include <iostream.h>
#include <stdlib.h>
#include <math.h>
int main()
{
const double SMALLFRACTION = 1.0E-8;
double x;
double r;
cout << "Enter number : ";
cin >> x;
Problem:
You have a collection of data items; each data item consists of a (real) number and
a character (m or f). These data represent the heights in centimetres of some school
children. Example data:
140.5 f
148 m
137.5 m
133 f
192 Selection
129.5 m
156 f
…
You have to calculate the average height, and standard deviation in height, for the
entire set of children and, also, calculate these statistics separately for the boys and
the girls.
Specification:
1. The program is to read height and gender values from cin. The data can be
assumed to be correct; there is no need to check for erroneous data (such as
gender tags other than 'f' or 'm' or a height outside of a 1 to 2 metre range).
The data set will include records for at least three boys and at least three girls;
averages and standard deviations will be defined (i.e. no need to check for zero
counts).
3. When the terminating sentinel data record is read, the program should print the
following details: total number of children, average height, standard deviation
in height, number of girls, average height of girls, standard deviation of height
of girls, number of boys, average height of boys, standard deviation of height
of boys.
Watch out for the Note that this specification excludes some potential problems.
fine print Suppose the specification did not mention that there would be a minimum
number of boys and girls in the data set?
It would become your responsibility to realize that there then was a potential
difficulty. If all the children in the sample were boys, you would get to the end of
program with:
number_girls 0
sum_girls_heights 0.0
Execution of this statement when number_girls was zero would cause your
program to be terminated with arithmetic overflow. You would have to plan for
more elaborate processing with if statements "guarding" the various output
sections so that these were only executed when the data were appropriate.
Remember, program specifications are often drawn up by teams including
lawyers. Whenever you are dealing with lawyers, you should watch out for nasty
details hidden in the fine print.
"sentinel" data The specification states that input will be terminated by a "sentinel" data record
(dictionary: sentinel – 1) soldier etc posted to keep guard, 2) Indian-Ocean crab
Example: calculating some simple statistics 193
with long eye-stalks; this use of sentinel derives from meaning 1). A sentinel data
record is one whose value is easily recognized as special, not a normal valid data
value; the sentinel record "guards" the end of input stopping you from falling over
trying to read data that aren't there. Use of a sentinel data record is one common
way of identifying when input is to stop; some alternatives are discussed in Chapter
9 where we use files of data.
Program design
First, it is necessary to check the formulae for calculating averages (means) and Preliminary design
standard deviations!
Means are easy. You simply add up all the data values as you read them in and
then divide by the number of data values. In this case we will get something like:
The standard deviation is a little trickier. Most people know (or at least once knew
but may have long forgotten) the following formula for the standard deviation for a
set of values xi:
∑ ( x − x)
N 2
i
i =1
S=
N −1
S standard deviation
N number of samples
xi sample i
x average value of samples
i.e. to work out the standard deviation you sum the squares of the differences
between sample values and the average, divide this sum by N-1 and take the square
root.
The trouble with that formula is that you need to know the average. It suggests
that you have to work out the value in a two step process. First, you read and store
all the data values, accumulating their total as you read them in, and working out
their average when all are read. Then in a second pass you use your stored data
values to calculated individual differences, summing the squares of these
differences etc.
Fortunately, there is an alternative version of the formula for the standard
deviation that lets you calculate its value without having to store all the individual
data elements.
∑
N
xi 2 − N x
2
i =1
S=
N −1
194 Selection
This formula requires that as well as accumulating a sum of the data values (for
calculating the average), we need to accumulate a sum of the squares of the data
values. Once all the data values have been read, we can first work out the average
and then use this second formula to get the standard deviation.
First iteration If we are to use this second formula, the program structure is roughly:
through design
process initialize count to zero, sum of values to zero
and sum of squares to zero
read in the first data element (height and gender flag)
while height ≠ 0
increment count
add height to sum
add square of height to sum of squares
read next data element
Of course, we are going to need three counts, three sums, three sums of squares
because we need to keep totals and individual gender specific values. We also
need to elaborate the code inside the loop so that in addition to updating overall
counts we selectively update the individual gender specific counts.
Second iteration We can now begin to identify the variables that we will need:
through design
process three integers children, boys, girls
Data three doubles cSum, bSum, gSum
three doubles cSumSq, bSumSq, gSumSq
double height
char gender_tag
double average
double standard_dev
The averages and standard deviations can be calculated and immediately printed
out; so we can reuse the same variables for each of the required outputs.
Loop code The code of the loop will include the selective updates:
while height ≠ 0
increment count
add height to sum
add square of height to sum of squares
if(female)
update girls count, girls sum, girls sumSq
else
update boys count, boys sum, boys sumSq
read next data element
Some simple data would be needed to test the program. The averages will be easy
to check; most spreadsheet packages include "standard deviation" among their
standard functions, so use of a spreadsheet would be one way to check the other
results if hand calculations proved too tiresome.
Implementation
#include <iostream.h>
#include <math.h>
int main()
{
int children, boys, girls;
double cSum, bSum, gSum;
double cSumSq, bSumSq, gSumSq;
double height;
char gender_tag;
while(height != 0.0) {
children++;
cSum += height;
cSumSq += height*height;
bSumSq += height*height;
}
standard_dev = sqrt(
(cSumSq - children*average*average) /
(children-1));
standard_dev = sqrt(
(gSumSq - girls*average*average) /
(girls-1));
…
…
return 0;
}
Note, in this case the variables average and standard_dev were not defined at the
start of main()'s block; instead their definitions come after the loop at the point
where these variables are first used. This is typical of most C++ programs.
However, you should note that some people feel quite strongly that all variable
declarations should occur at the beginning of a block. You will need to adjust your
coding style to meet the circumstances.
Expression passed as Function sqrt() has to be given a double value for the number whose root is
argument for needed. The call to sqrt() can involve any expression that yields a double as a
function call
result. The calls in the example use the expression corresponding to the formula
given above to define the standard deviation.
Test run When run on the following input data:
135 f
140 m
139 f
Example: calculating some simple statistics 197
151.5 f
133 m
148 m
144 f
146 f
142 m
144 m
0 m
The program produced the following output (verified using a common spreadsheet
program):
The standard deviations are printed with just a few too many digits. Heights Dubious trailing
measured to the nearest half centimetre, standard deviations quoted down to about digits!
Angstrom units! The number of digits used was determined simply by the default
settings defined in the iostream library. Since we know that these digits are
spurious, we really should suppress them.
There are a variety of ways of controlling the layout, or "format", of output if Formatting
the defaults are inappropriate. They work, but they aren't wildly convenient. Most
modern programs display their results using graphics displays; their output
operations usually involve first generating a sequence of characters (held in some
temporary storage space in memory) then displaying them starting at some fixed
point on the screen. Formatting controls for sending nice tabular output to line
printers etc are just a bit passe.
Here we want to fix the precision used to print the numbers. The easiest way is iomanip
to make use of an extension to the standard iostream library. The library file
iomanip contains a number of extensions to standard iostream. Here, we can use
setprecision(). If you want to specify the number of digits after the decimal
point you can include setprecision() in your output statements:
This would limit the output to two fraction digits so you would get numbers like
12.25 rather than 12.249852. If your number was too large to fit, e.g. 35214.27, it
will get printed in "scientific" format i.e. as 3.52e4.
Once you've told cout what precision to use, that is what it will use from that precision is a
point on. This can be inconvenient. While we might want only one fraction digit "sticky" format
setting
for the standard deviations, we need more for the averages (if we set
setprecision(1), the an average like 152.643 may get printed as 1.5e2). Once
you've started specifying precisions, you are committed. You are going to have to
do it everywhere. You will need code like:
198 Selection
standard_dev = sqrt(
(cSumSq - children*average*average) /
(children-1));
Problem:
You have to find the root of a polynomial; that is given some function of x, like
13.5*x 4 - 59*x3 - 28*x2 + 16.5*x + 30, you must find the values of x for which this
is 0.
The method is similar to that used earlier to find square roots. You keep
guessing, in a systematic controlled way, until you are happy that you are close
enough to a root. Figure 8.1 illustrates the basis for a method of guessing
systematically. You have to be given two values of x that bracket (lie on either
side of) a root; for one ("posX") the polynomial has positive value, the value is
negative for the other ("negX"). Your next guess for x should be mid-way between
the two existing guesses.
If the value of the polynomial is positive for the new x value, you use this x
value to replace the previous posX, otherwise it replaces the previous negX. You
then repeat the process, again guessing halfway between the updated posX, negX
pair. If the given starting values did bracket a single root, then each successive
guess should bring you a little closer.
Specification:
2. The program is to take as input guesses for two values of x that supposedly
bracket a root.
Finding roots of polynomials 199
Next guess
between existing
guesses
3. The program is to verify that the polynomial does have values of opposite sign
for the given x values. If the polynomial has the same sign at the two given
values (no root, or a pair of roots, existing between these values) the program
is to print a warning message and then stop.
4. The program is to use the approach of guessing a new value mid way between
the two bracketing values, using the guessed value to replace one of the
previous values so that it always has a pair of guesses on either side of the root.
5. The program is to print the current estimate for the root at each cycle of the
iterative process.
6. The loop is to terminate when the guess is sufficiently close to a root; this
should be taken as meaning the value for the polynomial at the guessed x is
less than 0.00001.
Program design
A first iteration through the design process gives a structure something like the Preliminary design
following:
Detailed design The data? Rather a large number of simple variables this time. From the
preliminary design outline, we can identify the following:
What is the simplest way of checking whether fun1 and fun2 have the same
sign?
You don't need a complex boolean expression like:
All you have to do is multiply the two values; their product will be positive if both
were positive or both were negative.
Implementation
#include <stdlib.h>
#include <iostream.h>
#include <math.h>
int main()
{
double posX, negX;
x = 0.5*(negX + posX);
fun3 = 13.5*pow(x,4) - 59*pow(x,3)- 28*pow(x,2)
+ 16.5*x + 30;
while(fabs(fun3) > 0.00001) {
cout << x << ", " << fun3 << endl;
if(fun3 < 0.0) negX = x;
else posX = x;
x = 0.5*(negX + posX);
fun3 = 13.5*pow(x,4) - 59*pow(x,3)- 28*pow(x,2)
+ 16.5*x + 30;
}
cout << "My final guess for root : " << endl;
cout << x << endl;
return EXIT_SUCCESS;
}
Convince yourself that the code does initialize posX and negX correctly from the
guesses g1 and g2 with its statements:
In example 8.5.1, the statistics that had to be calculated were just mean values and
standard deviations. Most such problems also require identification of minimum
and maximum values.
Suppose we needed to find the heights of the smallest and tallest of the children.
The program doesn't require much extension to provide these statistics. We have to
define a couple more variables, and add some code in the loop to update their
values:
int main()
{
int children, boys, girls;
double cSum, bSum, gSum;
double cSumSq, bSumSq, gSumSq;
double height;
char gender_tag;
while(height != 0.0) {
children++;
cSum += height;
cSumSq += height*height;
Of course, we need smallest and tallest to have some initial values. The
initial value of tallest would have to be less than (or equal) to the (unknown)
height of the tallest child, that way it will get changed to the correct value when the
data record with the maximum height is read. Similarly, smallest should initially
be greater than or equal to the height of the smallest child.
How should you chose the initial values?
Initialize minima and Generally when searching for minimum and maximum values in some set of
maxima with actual data, the best approach is to use the first data value to initialize both the minimum
data
and maximum values:
while(height != 0.0) {
children++;
…
Note: smallest gets initialized with HiLimit (it is to start greater than or equal
to smallest value and HiLimit should be safely greater than the minimum);
similarly tallest gets initialized with LowLimit.
You don't have to define the named constants; you could simply have:
smallest = 100;
tallest = 200;
but, as noted previously, such "magic numbers" in the code are confusing and often
cause problems when you need to change their values. (You might discover that
some of the children were aspiring basket ball players; your 200 limit might then be
insufficient).
In this example, the specification for the problem provided appropriate values Don't go guessing
that could be used to initialize the minimum and maximum variables. Usually, this limits
is not the case. The specification will simply say something like "read in the
integer data values and find the maximum from among those that ...". What data
values? What range? What is a plausible maximum?
Very often beginners guess.
They guess badly. One keeps seeing code like the following:
min_val = 10000;
max_val = 0;
204 Selection
Who said that 10000 was large? Who said that the numbers were positive? Don't
guess. If you don't know the range of values in your data, initialize the "maximum"
with the least (most negative) value that your computer can use; similarly, initialize
the "minimum" with the greatest (largest positive) value that your computer can
use.
Limits.h The system's values for the maximum and minimum numbers are defined in the
header file limits.h. This can be #included at the start of your program. It will
contain definitions (as #defines or const data definitions) for a number of limit
values. The following table is part of the contents of a limits.h file. SHRT_MAX is
the largest "short integer" value, LONG_MAX is the largest (long) integer and
LONG_MIN (despite appearances) is the most negative possible number (-
2147483648). If you don't have any better limit values, use these for initialization.
Once again, things aren't quite standardized. On many systems but not all, the
limits header file also defines limiting values for doubles (the largest, the smallest
non-zero value etc).
Unsigned values The list given above included limit values for "unsigned shorts" and "unsigned
longs". Normally, one-bit of the data field used by an integer is used in effect for
the ± sign; so, 16-bits can hold at most a 15-bit value (numbers in range -32768 to
+32767). If you know that you only need positive integers, then you can reclaim
the "sign bit" and use all 16 bits for the value (giving you a range 0…65535).
Unsigned integers aren't often used for arithmetic. They are more often used
when the bit pattern isn't representing a number – it is a bit pattern where specific
bits are used to encode the true/false state of separate data values. Use of bit
patterns in this way economises on storage (you can fit the values of 16 different
boolean variables in a single unsigned short integer). Operations on such bit
patterns are illustrated in Chapter X.
The definitions for the " MIN " values are actually using bit operations. That
character in front of the digits isn't a minus sign; it is ~ ("tilde") – the bitwise not
operator. Why the odd bit operations? All will be explained in due time!
EXERCISES
Loops and selection statements provide considerable computational power. However, the
range of programs that we can write is still limited. The limitation is now due mainly to a
Exercises 205
lack of adequate data structures. We have no mechanism yet for storing data. All we can do
is have a loop that reads in successive simple data values, like the children's heights, and
processes each data element individually combining values to produce statistics etc. Such
programs tend to be most useful when you have large numbers of simple data values from
which you want statistics (e.g. heights for all year 7 children in your state); but large
amounts of data require file input. There are just a few exercises here; one or two at the end
of Chapter 9 are rather similar but they use file input.
1. Implement a working version of the Calculator program and test its operation.
2. Extend the calculator to include a separate memory. Command 'm' copies the contents
of the display into the memory, 'M' adds the contents of the display to memory; similar
command 'r' and 'R' recall the memory value into the display (or add it to the display).
3. Write a program that "balances transactions on a bank account" and produces a financial
summary.
The program is to prompt for and read in the initial value for the funds in the account. It
is then to loop prompting for and reading transactions.
Transactions are entered as a number and a character, e.g.
128.35 u
79.50 f
10.60 t
66.67 r
9.80 e
213.50 j
84.30 f
66.67 r
…
debits (expenditures):
u utilities (electricity, gas, phone)
f food
r rent
c clothing
e entertainment
t transport
w work related
m miscellaneous
credits (income)
j job
p parents
l loan
Credit entries should increase available funds, debit entries decrease the funds.
Entry of the value 0 (zero) is to terminate the loop (the transaction code with a 0 entry
will be arbitrary).
206 Simple use of files
When the loop has finished, the program is to print the final value for the funds in the
account. It is also to print details giving the total amounts spent in each of the possible
expenditure categories and also giving these values percentages of the total spending.
The program should check that the transaction amounts are all greater than zero and that
the transaction codes are from the set given. Data that don't comply with these
requirements are to be discarded after a warning message has been printed; the program
is to continue prompting for and processing subsequent data entires.
4. Extend the childrens' heights program to find the heights of the smallest boy and the
smallest girl in the data set.
5. Modify the heights program so that it can deal correctly with data sets that are empty (no
children) or have children who all are of the same gender. The modified program
should print appropriate outputs in all cases (e.g. "There were no girls in the data
given").
6. Write a program to find a root( for polynomial of a given maximum degree 4, i.e. a
function of the form c4 x4 + c3 x3 + c2 x2 + c1 x + c0 for arbitrary values
of the coefficients c4, c3, c2, c1, and c0.
The program is to prompt for and read the values of the coefficients.
The program should then prompt for a starting x value, an increment, and a final x value
and should tabulate the value the polynomial at each successive x value in the range.
This should give the user an idea of where the roots might be located.
Finally, the program should prompt for two x values that will bracket a root and is to
find the root (if any) between these given values.
9
9 Simple use of files
9.1 DEALING WITH MORE DATA
Realistic examples of programs with loops and selection generally have to work
with largish amounts of data. Programs need different data inputs to test the code
for all their different selections. It is tiresome to have to type in large amounts of
data every time a program is tested. Also, if data always have to be keyed in, input
errors become more likely. Rather than have the majority of the data entered each
time the program is run, the input data can be keyed into a text file once. Then,
each time the program is subsequently run on the same data, the input can be taken
from this file.
While some aspects of file use have to be left to later, it is worth introducing
simple ways of using files. Many of the later example programs will be organized
as shown in Figure 9.1.
An ofstream
sending output
to a file
Program
cout to computer
screen
Figure 9.1 Programs using file i/o to supplement standard input and output.
These programs will typically send most of their output to the screen, though
some may also send results to output files on disks. Inputs will be taken from file
208 Simple use of files
or, in some cases, from both file and keyboard. (Input files can be created with the
editor part of the integrated development environment used for programming, or
can be created using any word processor that permits files to be saved as "text
only".)
Text files Input files (and any output files) will be simply text files. Their names should
be chosen so that it obvious that they contain data and are not "header" (".h") or
"code" (".cp") files. Some environments may specify naming conventions for such
data files. If there are no prescribed naming schemes, then adopt a scheme where
data files have names that end with ".dat" or ".txt".
Binary files Data files based on text are easy to work with; you can always read them and
change them with word processors etc. Much later, you will work with files that
hold data in the internal binary form used in programs. Such files are preferred in
advanced applications because, obviously, there is no translation work (conversion
from internal to text form) required in their use. But such files can not be read by
humans.
"Redirection of input Some programming environments, e.g. the Unix environment, permit inputs and
and output". outputs to be "redirected" to files. This means that:
1) you can write a program that uses cin for input, and cout for output, and test
run it normally with the input taken from keyboard and have results sent to
your screen,
then
2) you can subsequently tell the operating system to reorganize things so that
when the program needs input it will read data from a file instead of trying to
read from the keyboard (and, similarly, output can be routed to a file instead of
being sent to the screen).
Such "redirection" is possible (though a little inconvenient) with both the Borland
Symantec systems. But, instead of using cin and cout redirected to files, all the
examples will use explicitly declared "filestream" objects defined in the program.
The program will attach these filestream objects to named files and then use
them for input and output operations.
If a program needs to make explicit connections to files then it must declare some
"filestream" objects.
fstream.h header file The header file fstream.h contains the definitions of these objects. There are
basically three kinds:
• ifstream objects --- these can be used to read data from a named file;
• ofstream objects --- these can be used to write data to a named file;
and
Defining filestream objects 209
• fstream objects --- these are used with files that get written and read (at
different times) by a program.
The examples will mainly use ifstream objects for input. Some examples may
have ofstream objects.
The fstream objects, used when files are both written and read, will not be
encountered until much later examples (those dealing with files of record
structures).
A program that needs to use filestreams must include both the iostream.h and
fstream.h header files:
#include <iostream.h>
#include <fstream.h>
int main()
{
...;
ifstream objects are simply specialized kinds of input stream objects. They ifstream objects
can perform all the same kinds of operations as done by the special cin input
stream, i.e. they can "give" values to integers, characters, doubles, etc. But, in
addition, they have extra capabilities like being able to "open" and "close" files.
Similarly, ofstream objects can be asked to do all the same things as cout – ofstream objects
print the values integers, doubles etc – but again they can also open and close
output files.
The declarations in the file fstream.h make ifstream, ofstream, and fstream
"types". Once the fstream.h header file has been included, the program can contain
definitions of variables of these types.
Often, filestream variables will be globals because they will be shared by many
different routines in a program; but, at least in the first few examples, they will be
defined as local variables of the main program.
There are two ways that such variables can be defined.
Here variable input is defined as an input filestream attached to the file called
theinput.txt (the token ios::in specifies how the file will be used, there will be
more examples of different tokens later). This style is appropriate when the name
of the input file is fixed.
The alternative form of definition is:
210 Simple use of files
This simply names in1 as something that will be an input filestream; in1 is not
attached to an open file and can not be used for reading data until an "open"
operation is performed naming the file. This style is appropriate when the name of
the input file will vary on different runs of the program and the actual file name to
be used is to be read from the keyboard.
The open request uses a filename (and the ios::in token). The filename will
be a character string; usually a variable but it can be a constant (though then one
might as well have used the first form of definition). With a constant filename, the
code is something like:
void main()
{
ifstream in1;
...
in1.open("file1.txt", ios::in);
// can now use in1 for input ...
...
(Realistic use of open() with a variable character string has to be left until arrays
and strings have been covered.)
Once an input filestream variable has been defined (and its associated file opened
either implicitly or explicitly), it can be used for input. Its use is just like cin:
#include <iostream.h>
#include <fstream.h>
void main()
{
ifstream input("theinput.txt", ios::in);
long l1, l2; double d3; char ch;
...
input >> d3; // read a double from file
...
input >> ch; // read a character
...
input >> l1 >> l2; // read two long integer
Using input and output filestreams 211
#include <iostream.h>
#include <fstream.h>
void main()
{
// create an output file
ofstream out1("results.txt", ios::out);
int i; double d;
...
// send header to file
out1 << "The results are :" << endl; ...
for(i=0;i<100; i++) {
...
// send data values to file
out1 << "i : " << i << ", di " << d << endl;
...
}
out1.close(); //finished, close the file.
This code illustrates "close", another of the extra things that a filestream can do that
a simple stream cannot:
out1.close();
Programs should arrange to "close" any output files before finishing, though the
operating system will usually close any input files or output files that are still open
when a program finishes.
All stream objects can be asked about their state: "Was the last operation
successful?", "Are there any more input data available?", etc. Checks on the states
of streams are more important with the filestreams that with the simple cin and
cout streams (where you can usually see if anything is wrong).
It is easy to get a program to go completely wrong if it tries to take more input
data from a filestream after some transfer operations have already failed. The
program may "crash", or may get stuck forever in a loop waiting for data that can't
arrive, or may continue but generate incorrect results.
Operations on filestreams should be checked by using the normal stream
functions good(), bad(), fail(), and eof() . These stream functions can be
applied to any stream, but just consider the example of an ifstream:
If you wanted to give up if the next data elements in the file aren't two integers, you
could use code like the following:
if(in1.fail()) {
cout << "Sorry, can't read that file" << endl;
exit(1);
}
(There are actually two status bits associated with the stream – the badbit and the
failbit. Function bad() returns true if the badbit is set, fail() returns true if
either is set.)
Naturally, this being C++ there are abbreviations. Instead of phrasing a test
like:
if(in1.good())
…
if(in1)
…
if(!in1)
for
if(in1.bad())
Most people find these particular abbreviated forms to be somewhat confusing so,
even though they are legal, it seems best not to use them. Further, although these
behaviours are specified in the iostream header, not all implementations comply!
One check should always be made when using ifstream files for input. Was the
file opened?
There isn't much point continuing with the program if the specified data file isn't
there.
The state of an input file should be checked immediately after it is opened
(either implicitly or explicitly). If the file is not "good" then there is no point in
continuing.
Stream' states 213
A possible way of coding the check for an OK input file should be as follows:
#include <stdlib.h>
#include <iostream.h>
#include <fstream.h>
int main()
{
ifstream in1("mydata.txt", ios::in);
switch(in1.good()) {
case 1:
cout << "The file is OK, the program continues" << endl;
break;
case 0:
cout << "The file mydata.txt is missing, program"
" gives up" << endl;
exit(1);
}
...
int main()
{
ifstream in1("mydata.txt", ios::in | ios::nocreate);
switch(in1.good()) {
…
The token combination ios::in | ios::nocreate specifies that an input file is required
and it is not to be created if it doesn't already exist.
You specify the options that you want for a file by using the following tokens
either individually or combination:
(The translate option may not be in your selection; you may have extras, e.g.
ios::binary .) Obviously, these have to make sense, there is not much point
trying to open an ifstream while specifying ios::out!
Typical combinations are:
"Appending" data to an output file, ios::app, might seem to mean the same as
adding data at the end of the file, ios::ate. Actually, ios::app has a
specialized meaning – writes always occur at the end of the file irrespective of any
subsequent positioning commands that might say "write here rather than the end".
The ios::app mode is really intended for special circumstances on Unix systems
etc where several programs might be trying to add data to the same file. If you
simply want to write some extra data at the end of a file use ios::ate.
"Bitmasks" The tokens ios::in etc are actually constants that have a single bit set in bit
map. The groupings like ios::open | ios::ate build up a "bitmask" by bit-
oring together the separate bit patterns. The code of fstream's open() routines
checks the individual bit settings in the resulting bit mask, using these to select
processing options. If you want to combine several bit patterns to get a result, you
use an bit-or operation, operator |.
Don't go making the following mistakes!
The first example is wrong because the boolean or operator, || , has been used
instead of the required bit-or operator |. What the code says is "If either the
Options when opening filestreams 215
constant ios::out or ios::noreplace is non zero, encode a one bit here". Now
both these constants are non zero, so the first statement really says
outl("results.dat",1). It may be legal C++, but it sure confuses the run-time
system. Fortuitously, code 1 means the same as ios::in. So, at run-time, the
system discovers that you are opening an output file for reading. This will
probably result in your program being terminated.
The second error is a conceptual one. The programmer was thinking "I want to
specify that it is an input file and it is not to be created". This lead to the code
ios::in & ios::nocreate. But as explained above, the token combinations are
being used to build up a bitmask that will be checked by the open() function. The
bit-and operator does the wrong combination. It is going to leave bits set in the
bitmask that were present in both inputs. Since ios::in and ios::nocreate
each have only one bit, a different bit, set the result of the & operation is 0. The
code is actually saying in1("mydata.dat", 0). Now option 0 is undefined for
open() so this isn't going to be too useful.
Programs typically have loops that read data. How should such loops terminate?
The sentinel data approach was described in section 8.5.1. A particular data Sentinel data
value (something that cannot occur in a valid data element) is identified (e.g. a 0
height for a child). The input loop stops when this sentinel value is read. This
approach is probably the best for most simple programs. However, there can be
problems when there are no distinguished data values that can't be legal input (the
input requirement might be simply "give me a number, any number").
The next alternative is to have, as the first data value, a count specifying how Count
many data elements are to be processed. Input is then handled using a for loop as
follows:
int num;
ifstream in1("indata.txt", ios::in);
…
// read number of entries to process
in1 >> num;
for(int i = 0; i < num; i++) {
// read height and gender of child
char gender_tag;
double height;
cin >> height >> gender_tag;
…
}
The main disadvantage of this approach is that it means that someone has to count
the number of data elements!
The third method uses the eof() function for the stream to check for "end of eof()
file". This is a sort of semi-hardware version of a sentinel data value. There is
conceptually a mark at the end of a file saying "this is the end". Rather than check
for a particular data value, your code checks for this end of file mark.
216 Simple use of files
This mechanism works well for "record structured files", see Figure 9.2A. Such
files are explained more completely in section 17.3. The basic idea is obvious.
You have a file of records, e.g. "Customer records"; each record has some number
of characters allocated to hold a name, a double for the amount of money owed,
and related information. These various data elements are grouped together in a
fixed size structure, which would typically be a few hundred bytes in size (make it
400 for this example). These blocks of bytes would be read and written in single
transfers. A record would consist of several successive records.
A
"Record structured" file:
B end of file
Text file:
Three "customer records" would take up 1200 bytes; bytes 0…399 for
Customer-1, 400…799 for Customer-2 and 800…1199 for Customer-3. Now, as
explained in Chapter 1, file space is allocated in blocks. If the blocksize was 512
bytes, the file would be 1536 bytes long. Directory information on the file would
specify that the "end of file mark" was at byte 1200.
You could write code like the following to deal with all the customer records in
the file:
As each record is read, a "current position pointer" gets advanced through the file.
When the last record has been read, the position pointer is at the end of the file.
The test sales.eof() would return true the next time it is checked. So the while
loop can be terminated correctly.
The eof() scheme is a pain with text files. The problem is that text files will
contain trailing whitespace characters, like "returns" on the end of lines or tab
characters or spaces; see Figure 9.2B. There may be no more data in the file, but
because there are trailing whitespace characters the program will not have reached
the end of file.
If you tried code like the following:
You would run into problems. When you had read the last data entry (148 m) the
input position pointer would be just after the final 'm'. There remain other
characters still to be read – a space, a return, another space, and two more returns.
The position marker is not "at the end of the file". So the test kids.eof() will
return false and an attempt will be made to execute the loop one more time.
But, the input statement kids >> height >> gender_tag; will now fail –
there are no more data in the file. (The read attempt will have consumed all
remaining characters so when the failure is reported, the "end of file" condition will
have been set.)
You can hack around such problems, doing things like reading ahead to remove
"white space characters" or "breaking" out of a loop if an input operation fails and
sets the end of file condition.
But, really, there is no point fighting things like this. Use the eof() check on
record files, use counts or sentinel data with text files.
along with some manipulators defined in the standard iostream library such as
The manipulators setprecision(), hex, and dec are "sticky". Once you set them they
remain in force until they are reset by another call. The width manipulators only
affect the next operation. Fill affects subsequent outputs where fill characters are
needed (the default is to print things using the minimum number of characters so
that the filler character is normally not used).
The following example uses several of these manipulators:
int main()
{
int number = 901;
double d1 = 3.141592;
double d2 = 45.9876;
double d3 = 123.9577;
return EXIT_SUCCESS;
}
There are alternative mechanisms that can be used to set some of the format Alternative form of
options shown above. Thus, you may use specification
There are a number of other output options that can be selected. These are Other strange format
defined by more of those tokens (again, these are actually defined bit patterns); options
they include:
These options are selected by telling the output stream object to "set flags" (using
its setf() function) and are deselected using unsetf(). The following code
fragment illustrates their use:
int main()
{
long number = 8713901;
cout.setf(ios::showpos);
cout << number << endl;
cout.unsetf(ios::showpos);
cout << number << endl;
cout << hex << number << endl;
cout.setf(ios::uppercase);
cout << number << endl;
return EXIT_SUCCESS;
}
ios::left
220 Simple use of files
ios::right
these control the "justification" of data that is printed in a field of specified width:
int main()
{
cout.width(24);
cout.fill('!');
cout << "Hello" << endl;
cout.setf(ios::left, ios::adjustfield);
cout.width(24);
cout << "Hello" << endl;
cout.setf(ios::right, ios::adjustfield);
cout.width(24);
cout << "Hello" << endl;
return EXIT_SUCCESS;
}
!!!!!!!!!!!!!!!!!!!Hello
Hello!!!!!!!!!!!!!!!!!!!
!!!!!!!!!!!!!!!!!!!Hello
Note the need to repeat the setting of the field width; unlike fill which lasts, a width
setting affects only the next item. As shown by the output, the default justification
is right justified. The left/right justification setting uses a slightly different version
of the setf() function. This version requires two separate data items – the
left/right setting and an extra "adjustfield" parameter.
You can specify whether you want doubles printed in "scientific" or "fixed
form" styles by setting:
ios::scientific
ios::fixed
int main()
{
double number1 = 0.00567;
double number2 = 1.746e-5;
double number3 = 9.43214e11;
double number4 = 5.71e93;
double number5 = 3.08e-47;
cout << number1 << ", " << number2 << ", "
<< number3 << endl;
cout << " " << number4 << ", " << number5 << endl;
cout.setf(ios::fixed, ios::floatfield);
cout << number1 << ", " << number2 << ", "
<< number3 << endl;
// cout << " " << number4 ;
cout << number5 << endl;
cout << number1 << ", " << number2 << ", "
<< number3 << endl;
cout << " " << number4 << ", " << number5 << endl;
return EXIT_SUCCESS;
}
Default output
0.00567, 1.746e-05, 9.43214e+11
5.71e+93, 3.08e-47
Fixed style
0.00567, 0.000017, 943214000000
0
Scientific style
5.67e-03, 1.746e-05, 9.43214e+11
5.71e+93, 3.08e-47
cin.unsetf(ios::skipws);
222 Simple use of files
By default, the skipws ("skip white space") option is switched on. (You may get
into tangles if you unset skipws and then try to read a mixture of character and
numeric data.)
The following program illustrates use of this control to change input options:
int main()
{
/*
Program to count the number of characters preceding a
period '.' character.
*/
int count = 0;
char ch;
// cin.unsetf(ios::skipws);
cin >> ch;
while(ch != '.') {
count++;
cin >> ch;
}
cout << endl;
cout << "I think I read " << count << " characters."
<< endl;
return EXIT_SUCCESS;
}
Finished, any questions? OK, go; wake that guy in the back row,
he must have been left here by the previous lecturer.
because the spaces and carriage returns will then have been counted as well as the
normal letters, digits, and punctuation characters.
9.8 EXAMPLE
Problem
Botanists studying soya bean production amassed a large amount of data relating to
different soya plantations. These data had been entered into a file so that they
could be analyzed by a program.
Example 223
Some soya plantations fail to produce a harvestable crop. The botanists had no
idea of the cause of the problem – they thought it could be disease, or pest
infestation, or poor nutrition, or maybe climatic factors. The botanists started by
recording information on representative plants from plots in each of the plantations.
Each line of the file contained details of one plant. These details were encoded
as 0 or 1 integer values. The first entry on each line indicated whether the plant
was rated healthy (1) or unhealthy (0) at harvest time. Each of the other entries
represented an attribute considered in the study. These attributes included "late
spring frosts", "dry spring", "beetles", "nitrogen deficient soil", "phosphate
deficient soil", etc; there were about twenty attributes in the study (for simplicity
we will reduce it to five).
The file contains hundreds of lines with entries. Examples of individual entries
from the file were:
0 1 0 0 0 0
(an unhealthy plant that had experienced a late spring frosts but no other problems),
and
1 1 0 1 0 0
(a plant that was healthy despite both a late spring frost and beetles in summer).
The botanists wanted these data analyzed so as to identify any correlations
between a plant's health and any chosen one of the possible attributes.
Consider a small test with results for 100 plants. The results might show that 45
were unhealthy, and that beetles had been present for 60 of them. If we assume
that the presence of beetles is unrelated to plant health, then there should be a
simple random chance distribution of beetles on both healthy and the unhealthy
plants. So here, 60% of the 45 diseased plants (i.e. 27 plants) should have had
beetle infestations as should 33 of the healthy plants.
Expected distribution
beetles clean
unhealthy 27 18 = 45
healthy 33 22 = 55
60 40
While it is unlikely that you would see exactly this distribution, if health and beetle
presence are unrelated then the observed results should be something close to these
values.
The actual observed distribution might be somewhat different. For example one
might find that 34 of the unhealthy plants had had lots of beetles:
Observed distribution
beetles clean
unhealthy 34 11 = 45
healthy 26 29 = 55
60 40
224 Simple use of files
Results such as these would suggest that beetles, although certainly not the sole
cause of ill health, do seem to make plants less healthy.
Of course, 100 samples is really quite small; the differences between expected
and observed results could have arisen by chance. Larger differences between
observed and expected distributions occur with smaller chance. If the differences
are very large, they are unlikely to have occurred by chance. It does depend on the
number of examples studied; a smallish difference may be significant if it is found
consistently in very large numbers of samples.
Statisticians have devised all sorts of formulae for working out "statistics" that
can be used to estimate the strength of relationships among values of different
variables. The botanists' problem is one that can be analyzed using the χ 2 statistic
(pronounced ≈ "kiye-squared") This statistic gives a single number that is a
measure of the overall difference between observed and expected results and which
also takes into account the size of the sample.
The χ 2 statistic is easily computed from the entries in the tables of observed and
expected distributions:
∑ (O − E ) / E
4 2
χ2 =
i =1 i i i
≈ 8
If the observed values were almost the same as the expected values (Oi ≈ Ei ) then
the value of χ2 would be small or zero. Here the "large" value of χ2 indicates that
there is a significant difference between expected and observed distributions of
attribute values and class designations. It is very unlikely that such a difference
could have occurred solely by chance, i.e. there really is some relation between
beetles and plant health.
You can select a chance level at which to abandon the assumption that an
attribute (e.g. beetle presence) and a class (e.g. unhealthy) are unrelated. For
example, you could say that if the observed results have a less than 1 in 20 chance
of occurring then the assumption of independence should be abandoned. You
could be more stringent and demand a 1 in 100 chance. Tables exist that give a
limit value for the χ2 statistic associated with different chance levels. In a case like
this, if the χ2 statistic exceeds ≈3.8 then an assumption of independence can be
abandoned provided that you accept that a 1 in 20 chance as too unlikely. Someone
demanding a 1 in 100 chance would use a threshold of ≈5.5.
The botanists would be able to get an idea of what to focus on by performing a
χ2 analysis to measure the relation between plant health and each of the attributes
Example 225
for which they had recorded data. For most attributes, the observed distributions
would turn out to be pretty similar to the expected distributions and the χ2 values
would be small (0…1). If any of the attributes had a large χ 2 value, then the
botanists would know that they had identified something that affected plant health.
Specification:
This file contains several hundred lines of data. Each line contains details of
one plant. The data for a plant take the form of six integer values, all 0 or 1;.
The first is the plant classification – unhealthy/healthy. The remaining five are
the values recorded for attributes of that plant.
This data value is terminated by a sentinel. This consist of a line with a single
number, -1, instead of a class and set of five attributes.
2. The program is to start by prompting for and reading input from cin. This
input is to be an integer in the range 1 to 5 which identifies the attribute that is
to be analyzed in this run of the program.
3. The program is to then read all the data records in the file, accumulating the
counts that provide the values for the observed distribution and overall counts
of the number of healthy plants etc.
4. When all the data have been read, the program is to calculate the values for the
expected distribution.
6. Finally, the program is to calculate and print the value of the χ2 statistic.
Program design
A first iteration through the design process gives a structure something like the Preliminary design
following:
calculate χ2
print χ2
Second iteration There are several points in the initial design outline that require elaboration.
through design What counters are needed and how can the "observed" and "expected" tables be
process
represented? How can the "attribute of interest" be selected? How should an
"appropriate counter" be identified and updated? All of these issues must be sorted
out before the complete code is written. However, it will often be the case that the
easiest way to record a design decision will be to sketch out a fragment of code.
Each of the tables really needs four variables:
Observed Expected
attribute attribute
0 1 0 1
unhealthy 0 Obs00 Obs01 Exp00 Exp01
healthy 1 Obs10 Obs11 Exp10 Exp11
Three additional counters are needed to accumulate the data needed to calculate the
"expected" values Exp00 … Exp11; we need a count of the total number of plants, a
count of healthy plants, and a count for the number of times that the attribute of
interest was true.
Selecting the attribute of interest is going to be fairly easy. The value (1…5)
input by the user will have been saved in some integer variable interest. In the
while loop, we can have five variables a1 … a5; these will get values from the file
using an input statement like:
We can have yet another variable, a , which will get the value of the attribute of
interest. We could set a using a switch statement:
switch(interest) {
case 1: a = a1; break;
…
case 5: a = a5; break;
}
(Use of arrays would provide an alternative mechanism for holding the values and
selecting the one of interest.)
The values for variables a and pclass (plant's 0/1 class value) have to be used
to update the counters. There are several different ways that these update
operations could be coded.
Example 227
Select an approach that makes the meaning obvious even if this results in
somewhat wordier and less efficient code than one of the alternatives. The
updating of the counters is going to be a tiny part of the overall processing cost
(most time will be devoted to conversion of text input into internal numeric data).
There is no point trying to "optimize" this code and make it "efficient" because this
is not the code where the program will spend its time.
The counts could be updated using "clever" code like:
It should take you less than a minute to convince yourself that those lines of code
are appropriate. But fifteen seconds thought per line of code? That code is too
subtle. Try something cruder and simpler:
#define TRUE 1
…
atotalpos += (a == TRUE) ? 1 : 0;
healthy += (pclass == TRUE) ? 1 : 0;
or even
Before implementation, you should also compose some test data for yourself. Data
You will need to test parts of your program on a small file whose contents you
know; there is no point touching the botanists' file with its hundreds of records until
you know that your program is doing the right thing with data. You would have to
create a file with ten to twenty records, say 12 healthy and 6 unhealthy plants. You
can pick the attribute values randomly except for one attribute that you make true
(1) mainly for the unhealthy plants.
Implementation
This program is just at the level of complexity where it becomes worth building in
stages, testing each stage in turn.
The first stage in implementation would be to create something like a program First stage partial
that tries to open an input file, stopping sensibly if the file is not present, otherwise implementation
continuing by simply counting the number of records.
#include <stdlib.h>
#include <iostream.h>
228 Simple use of files
#include <fstream.h>
int main()
{
// Change file name to plants.dat when ready to fly
ifstream infile("test.dat", ios::in | ios::nocreate);
int count = 0;
if(!infile.good()) {
cout << "Sorry. Couldn't open the file." <<endl;
exit(1);
}
int pclass;
while(pclass != -1) {
count++;
int a1, a2, a3, a4, a5;
infile >> a1 >> a2 >> a3 >> a4 >> a5;
// Discard those data values
// Read 0/1 classification of next plant
// or sentinel value -1
infile >> pclass;
}
Second stage partial With a basic framework running, you can start adding (and testing) the data
implementation processing code. First we could add the code to determine the attribute of interest
and to accumulate some of the basic counts.
#include <stdlib.h>
#include <iostream.h>
#include <fstream.h>
#define TRUE 1
#define FALSE 0
int main()
{
// Change file name to plants.dat when ready to fly
ifstream infile("test.dat", ios::in | ios::nocreate);
int count = 0;
if(!infile.good()) {
cout << "Sorry. Couldn't open the file." <<endl;
exit(1);
}
int pclass;
Example 229
int atotalpos = 0;
int healthy = 0;
int interest;
while(pclass != -1) {
count++;
if(pclass == TRUE)
healthy++;
cout <<
"Data read for " << count << " plants." << endl;
cout <<
healthy << " were healthy." << endl;
cout <<
"Attribute of interest was " << interest << endl;
cout <<
"This was true in " << atotalpos << " cases."
<< endl;
return EXIT_SUCCESS;
Note the "sanity check" on the input. This is always a sensible precaution. If
the user specifies interest in an attribute other than the five defined in the file there
is really very little point in continuing.
The next sanity check is one that you should perform. You would have to run
this program using the data in your small test file. Then you would have to check
that the output values were appropriate for your test data.
The next stage in the implementation would be to i) define initialized variables Third stage partial
that represent the entries for the "observed" and "expected" tables, ii) add code implementation
inside the while loop to update appropriate "observed" variables, and iii) provide
code that prints the "observed" table in a suitable layout. The variables for the
230 Simple use of files
"observed" variables could be integers, but it is simpler to use doubles for both
observed and expected values. Getting the output in a good tabular form will
necessitate output formatting options to set field widths etc. You will almost
certainly find it necessary to make two or three attempts before you get the layout
that you want.
These extensions require variable definitions just before the while loop:
Output code like the following just after the while loop:
cout.setf(ios::fixed, ios::floatfield);
Note that the use of setprecision() and setw() requires the iomanip library,
i.e. #include <iomanip.h>. You might also chose to use set option
ios::showpoint to get a value like 7 printed as 7.0.
Final stage of Finally, you would add the code to compute the expected distributions, print
implementation these and calculate the χ2 statistic. There is a good chance of getting a bug in the
code to calculate the expected values:
The formulae are correct in principle. Value Exp11 is the expected value for the
number of healthy plants with the attribute value also being positive. So if, as in
the example, there are 55 healthy plants, and of the 100 total plants 60 have beetles,
we should get 55 * 60/100 or 33 as value for Exp11. If you actually had those
numeric values, there would be no problems and Exp11 would get the value 33.0.
However, if 65 of the 100 plants had had beetles, the calculation would be
55*65/100 with the correct result 35.75. But this wouldn't be the value assigned to
Exp11; instead it would get the value 35.
Example 231
All of the variables healthy, atotalpos, and count are integers. So, by
default the calculation is done using integer arithmetic and the fractional parts of
the result are discarded.
You have to tell the compiler to code the calculation using doubles. The
following code is OK:
The code to print the expected distribution can be obtained by "cutting and
pasting" the working code that printed the observed distribution and then changing
variable names.
The final few statements needed to calculate the statistic are straightforward:
double chisq;
chisq = (Obs00 - Exp00)*(Obs00-Exp00)/Exp00;
chisq += (Obs01 - Exp01)*(Obs01-Exp01)/Exp01;
chisq += (Obs10 - Exp10)*(Obs10-Exp10)/Exp10;
chisq += (Obs11 - Exp11)*(Obs11-Exp11)/Exp11;
All the variables here are doubles so that there aren't the same problems as for
calculating Exp11 etc. You could make these calculations slightly more efficient
by introducing an extra variable to avoid repeated calculation of similar
expressions:
double chisq;
double temp;
temp = (Obs00 - Exp00);
chisq = temp*temp/Exp00;
temp = (Obs01 - Exp01);
chisq += temp*temp/Exp01;
But honestly, it isn't worth your time bothering about such things. The chances are
pretty good that your compiler implements a scheme for spotting "common
subexpressions" and it would be able to invent such temporary variables for itself.
You would have to run your program on your small example data set and you
would need to check the results by hand. For a data set like the following:
1 0 0 0 0 0
1 0 1 0 1 0
1 1 0 0 0 1
1 0 1 1 0 1
1 0 1 0 1 1
1 0 1 1 0 1
1 1 0 0 1 0
1 0 1 0 1 0
1 0 0 1 1 0
1 0 1 0 0 1
1 0 0 1 0 1
1 0 0 0 0 1
232 Simple use of files
0 1 0 1 0 1
0 0 1 0 0 0
0 1 0 1 1 0
0 1 0 1 0 1
0 1 1 0 1 1
0 0 0 1 1 0
0 1 1 0 1 0
-1
Observed distribution
Attribute 1
0 1
Unhealthy 2 5
Healthy 10 2
Expected distribution
Attribute 1
0 1
Unhealthy 4.4 2.6
Healthy 7.6 4.4
Chisquared value 5.7
and
Observed distribution
Attribute 4
0 1
Unhealthy 3 4
Healthy 7 5
Expected distribution
Attribute 4
0 1
Unhealthy 3.7 3.3
Healthy 6.3 5.7
Chisquared value 0.4
Once you had confirmed that your results were correct, you could start processing
the real file with the hundreds of data records collected by the botanists.
With larger programs, the only way that you can hope to succeed is to complete
a fairly detailed design and then implement stage by stage as illustrated in this
example.
EXERCISES
1. Modify the bank account program, exercise 3 in Chapter 8, so that it reads transaction
details from a file "trans.dat".
2. The file "temperatures.dat" contains details of the oral temperatures of several hundred
pre-med, public health, and nursing students. Each line of the file has a number
(temperature in degrees Centigrade) and a character ('m' or 'f') encoding the gender of
the student. The file ends with a sentinel record (0.0 x). Example:
Constants 233
37.3 m
36.9 f
37.0 m
…
37.1 f
0.0 x
Write a program to process these data. The program is to calculate and print:
A preliminary study on a smaller sample gave slightly different mean temperatures for
males and females. A statistical test is to be made on this larger set of data to determine
whether this difference is significant.
Let
µM = average temperature of males
µF = average temperature of females
NM = number of males
NF = number of females
If there is no significant difference in the two means, then values for Z should be small;
there is less than one chance in twenty that you would find a Z with an absolute value
exceeding 2.
If a Z value greater than 2 (or less than -2) is obtained, then the statistic suggests that
one should NOT presume that the male and female temperatures have the same mean.
Your program should calculate and print the value of this statistic and state whether any
difference in means is significant.
234 Simple use of files
10
10 Functions
The most complex of the programs presented in chapters 6…9 consisted of a single
main() function with some assignment statements that initialized variables, a loop
containing a switch selection statement, and a few final output statements. When your
data processing task grows more complex than this, you must start breaking it down
into separate functions.
We have already made use of functions from the maths library. Functions like Functions that
sin() obviously model the mathematical concept of a function as something that maps compute values
(converts) an input value in some domain into an output value in a specified range (so
sine maps values from the domain (0..2π) into the range (-1.0...1.0)).
We have also used functions from the iostream and related libraries. An output
statement such as:
actually involves two function calls. The << "takes from" operator is a disguised way Functions with "side
of calling a function. The calls here are to a "PrintText" function and to a effects"
"PrintDouble" function. Unlike mathematical functions such as sin(), these functions
don't calculate a value from a given input; instead they produce some desired "side
effect" (like digits appearing on the screen). Things like cout.setf(ios::show
point) again involve a function call executed to achieve a side effect; in this case the
side effect is to change the way the numbers would be printed subsequently.
We will be using an increasing number of different functions from various standard
libraries. But now we need more, we need to be able to define our own functions.
If you can point at a group of statements and say "these statements achieve X", then
you have identified a potential function. For example, in the program in section 9.8, the
following statements were used to "get an integer in a given range"
int interest;
main program
get checked integer data item using function
other initialization
…
The example in section 8.5.2 (Newton's method for roots of a polynomial) illustrates
another situation where it would be appropriate to invent a special purpose function.
The following expression had to appear at several points in the code where we required
the value of the polynomial at a particular value of x:
fun1 = polyfunction(g1);
fun2 = polyfunction(g2);
double x = g1;
fun1 = 13.5*pow(x,4) - 59*pow(x,3)- 28*pow(x,2)
+ 16.5*x + 30;
x = g2;
Functions 239
If you are reading the code with the "calls" to polyfunction(), you can see that fun1
and fun2 are being initialized with values depending on the input data values g1, and
g2. The details of how they are being initialized don't appear and so don't disrupt you
chain of thought as you read the code. In addition, the use of a separate function to
evaluate the polynomial would actually save some memory. Code using calls to a
function would require less instructions than the code with repeated copies of the
instructions to evaluate the expression.
A long time ago, function calls were expensive. In the computers of those days, the
instruction sets were different and there were limitations on use of CPU registers;
together these factors meant that both the processes of setting up a call to a function,
and then of getting a result back required many instructions. So, in those days,
functions had to be reasonably long to make the overheads of a function call
worthwhile. But, that was a very long time ago (it was about the time when the
"Beatles", four young men in funny suits, were starting to attract teenagers to the
Cavern club in Liverpool).
The overhead for a function call is rarely going to be of any significance on a
modern computer architecture. Because functions increase understandability of
programs, they should be used extensively. In any case, as explained in 10.6, C++ has a
special construct that allows you to "have your cake and eat it". You can define
functions and so get the benefit of additional clarity in the code, but at the same time
you can tell the compiler to avoid using a function call instruction sequence in the
generated code. The code for the body of the function gets repeated at each place where
the function would have been called.
return_type
function_name(
details of data that must be given to the function
)
{
statements performing the work of the function
return value of appropriate type ;
}
For example:
int res;
do {
cout << "Enter an integer in the range " << low
<< " ... " << high << " :";
cin >> res;
} while (! ((res >= low) && (res <= high)));
return res;
}
The line
defines the name of the function to be "GetIntegerInRange", the return type int, and
specifies that the function requires two integer values that its code will refer to as low
and high. The definition of the local variable res, and the do … while loop make up
the body of the function which ends with the return res statement that returns an
acceptable integer value in the required range. (The code is not quite the same as that in
the example in section 9.8. That code gave up if the data value input was inappropriate;
this version keeps nagging the user until an acceptable value is entered.)
Function names The rules, given in section 6.6, that define names for variables also apply to function
names. Function names must start with a letter or an underscore character (_) and can
contain letters, digits, and underscore characters. Names starting with an underscore
should be avoided; these names are usually meant to be reserved for the compiler which
often has to invent names for extra variables (or even functions) that it defines. Names
should be chosen to summarize the role of a function. Often such names will be built
up from several words, like Get Integer In Range.
When you work for a company, you may find that there is a "house style"; this style
will specify how capitalization of letters, or use of underscores between words, should
be used when choosing such composite names. Different "house styles" might require:
GetIntegerInRange, or Get_integer_in_range, or numerous variants. Even if you are not
required to follow a specified "house style", you should try to be consistent in the way
you compose names for functions.
Argument list After the function name, you get a parenthesised list of "formal parameters" or
argument declarations (the terminology "arguments", "formal parameters" etc comes
from mathematics where similar terms are used when discussing mathematical
functions). You should note that each argument, like the low and high arguments in
the example, requires its own type specification. This differs from variable definitions,
where several variables can be defined together (as in earlier examples where we have
had things like double Obs00, Obs01, Obs10, Obs11;).
Functions with no Sometimes, a function won't require arguments. For example, most IDEs have some
arguments form of "time used" function (the name of this function will vary). This function, let's
call it TimeUsed(), returns the number of milliseconds of CPU time that the program
has already used. This function simply makes a request to the operating system (using
Form of a function definition 241
some special "system call") and interprets the data returned to get a time represented as
a long integer. Of course the function does not require any information from the user's
program so it has no arguments. Such a function can be specified in two ways:
long TimeUsed()
i.e. a pair of parentheses with nothing in between (an empty argument list), or as
long TimeUsed(void)
i.e. a pair of parentheses containing the keyword void. ("Void" means empty.) The
first form is probably more common. The second form, which is now required in
standard C, is slightly better because it make it even more explicit that the function does
not require any arguments.
A call to the TimeUsed() function (or any similar function with a void argument list Trap for the unwary
e.g. int TPSOKTSNW(void)) just has the form: Pascal programmer
(or other careless
coder)
TPSOKTSNK()
e.g.
if(TPSOKTSNW()) {
// action
…
}
In Pascal, and some other languages, a function that takes no arguments is called by just
writing its function name without any () parentheses. People with experience in such
languages sometimes continue with their old style and write things like:
if(TPSOKTSNW) {
// action
…
}
This is perfectly legal C/C++ code. Its meaning is a bit odd – it means check whether
the function TPSOKTNSW() has an address. Now if the program has linked successfully,
TPSOKTNSW() will have an address so the if condition is always true. (The function
TPSOKTNSW() – it was "The_President_Says_OK_To_Start_Nuclear_War()", but
that name exceeded the length limits set by the style guides for the project. Fortunately,
the code was checked before being put into use.)
242 Functions
Like mathematical functions that have a value, most functions in programs return a
value. This value can be one of the standard data types (char, long, double, etc) or may
be a structure type (Chapter 16) or pointer type (introduced in Part IV, Chapter 20).
Default return type of If you don't define a return type, it is assumed that it should be int. This is a left
int over from the early days of C programming. You should always remember to define a
return type and not simply use this default (which may disappear from the C++
language sometime).
The compiler will check that you do return a value of the appropriate type. You will
probably already have had at least one error report from the compiler on some occasion
where you forgot to put a return 0; statement at the end of your int main()
program.
A return statement will normally have an expression; some examples are:
return res;
return n % MAXSIZE;
You will sometimes see code where the style convention is to have an extra pair of
parentheses around the expression:
return(res);
return(n % MAXSIZE);
Don't worry about the extra parentheses; they are redundant (don't go imagining that it
is a call to some strange function called return()).
Functions that don't If you have a function that is used only to produce a side effect, like printing some
return results results, you may not want to return a result value. In the 1970s and early 1980s when
earlier versions of C were being used, there was no provision for functions that didn't
return results. Functions executed to achieve side effects were expected to return an
integer result code reporting their success or failure. You can still see this style if you
get to use the alternate stdio input-output library. The main print function, printf(),
returns an integer; the value of this integer specifies the number of data items
successfully printed. If you look at C code, you will see that very few programmers
ever check the success status code returned by printf(). (Most programmers feel that
if you can't print any results there isn't anything you can do about it; so why bother
checking).
void Since the success/failure status codes were so rarely checked, the language designers
gave up on the requirement that all "side effect" functions return an integer success
Result types 243
code. C++ introduced the idea of functions with a void return type (i.e. the return
value slot is empty). This feature was subsequently retrofitted into the older C
language.
For example, suppose you wanted to tidy up the output part the example program
from section 8.5.1 (the example on childrens' heights). The program would be easier to
follow if there was a small separate function that printed the average and standard
deviation. This function would be used purely to achieve the side effect of producing
some printout and so would not have a return value.
It could defined as follows:
When you start building larger programs where the code must be organized in separate
files, you will often need to "declare" functions. You will have defined your function in
one file, but code in another file needs a call to that function. The second file will need
to contain a declaration describing the function, its return type, and its argument values.
This is necessary so that the compiler (and linker) can know that the function does exist
and so that they can check that it is being used correctly (getting the right kinds of data
to work with, returning a result that suits the code of the call).
A function declaration is like a function definition – just without the code! A
declaration has the form:
return_type
function_name(
details of data that must be given to the function
) ;
The end of a declaration has to be marked (because otherwise the compiler, having read
the argument list, will be expecting to find a { begin bracket and some code). So,
function declarations end with a semicolon after the closing parenthesis of the argument
list. (Similarly, it is a mistake to put a semicolon after the closing parenthesis and
before the body in a function definition. When the compiler sees the semicolon, it
"knows" that it just read a function declaration and so is expecting to find another
244 Functions
declaration, or the definition of a global variable or constant. The compiler gets very
confused if you then hit it with the { code } parts of a function definition.)
Function Examples of function declarations (also known as function "prototypes") are:
"prototypes"
void PrintStats(int num, double ave, double std_dev);
int GetIntegerInRange(int low, int high);
In section 6.5.2, part of the math.h header file was listed; this contained several function
declarations, e.g.:
double sin(double);
double sqrt(double);
double tan(double);
double log10(double);
Most declarations are Most function declarations appear in header files. But, sometimes, a function
in header files declaration appears near the start of a file with the definition appearing later in the same
file.
Unlike languages such as Pascal (which requires the "main program" part to be the
last thing in a file), C and C++ don't have any firm rules about the order in which
functions get defined.
For example, if you were rewriting the heights example (program 8.5.1) to exploit
functions you could organize your code like this:
#include <iostream.h>
#include <math.h>
int main()
{
int children, boys, girls;
double cSum, bSum, gSum;
Function declarations 245
return 0;
}
#include <iostream.h>
#include <math.h>
double Average(double total, int number); Forward declarations
double StandardDev(double sumsquares, double average, int num);
void PrintStats(int num, double ave, double std_dev);
int main()
{
int children, boys, girls;
double cSum, bSum, gSum;
double cSumSq, bSumSq, gSumSq;
return 0;
}
This version requires declarations for functions Average() etc at the start of the file.
These declarations are "forward declarations" – they simply describe the functions that
are defined later in the file.
These forward declarations are needed for the compiler to deal correctly with the
function calls in the main program (average = Average(cSum, children); etc). If
the compiler is to generate appropriate code, it must know that Average() is a function
that returns a double and takes two arguments – a double and an integer. If you didn't
put in the forward declaration, the compiler would give you an error message like
"Function Average has no prototype."
Normally, you should use the first style when you are writing small programs that
consist of main() and a number of auxiliary functions all defined in the one file. Your
programs layout will be vaguely reminiscent of Pascal with main() at the bottom of
the file. There are a few circumstances where the second arrangement is either
necessary or at least more appropriate; examples will appear later.
There is another variation for the code organization:
#include <iostream.h>
#include <math.h>
int main()
{
Prototypes declared double Average(double total, int number);
in the body of the double StandardDev(double sumsquares,
code of caller double average, int num);
void PrintStats(int num, double ave, double std_dev);
…
average = Average(cSum, children);
standard_dev = StandardDev(cSumSq, average children);
PrintStats(children, average, standard_dev);
…
return 0;
}
Function declarations 247
Some functions need to be given lots of information, and therefore they need lots of
arguments. Sometimes, the arguments may almost always get to be given the same data
values.
For example, if you are writing are real window-based application you will not be
using the iostream output functions, instead you will be using some graphics functions.
One might be DrawString(), a function with the following specification:
In most programs, the current pen position is set before a call to DrawString() so,
usually, both Hoffset and Voffset are zero. Graphics programs typically have a
default font size for text display, most often this is 12 point. Generally, text is
248 Functions
displayed in the normal ("Roman") font with only limited use of italics and bold fonts.
About the only thing that is different in every call to DrawString() is the message.
So, you tend to get code like:
…
DrawString("Enter your bet amount", 0, 12, 0, 0);
…
DrawString("Press Roll button to start game", 0, 12, 0, 0);
…
DrawString("Lock any columns that you don't want rolled again",
0, 12, 0, 0);
…
DrawString("Congratulations you won", 3, 24, 0, 0);
…
DrawString("Double up on your bet?", 3, 12, 0, 0);
…
C++ permits the definition of default argument values. If these are used, then the
arguments don't have to be given in the calls.
If you have a declaration of DrawString() like the following:
Omitting trailing …
arguments that have DrawString("Enter your bet amount");
default values …
DrawString("Press Roll button to start game");
…
DrawString("Lock any columns that you don't want rolled
again");
…
DrawString("Congratulations you won", 3, 24);
…
DrawString("Double up on your bet?", 3);
…
You are permitted to omit the last argument, or last pair of arguments, or last three
arguments or whatever if their values are simply the normal default values. (The
compiler just puts back any omitted values before it goes and generates code.) You are
only allowed to omit trailing arguments, you aren't allowed to do something like the
following:
The C++ compiler and its associated linking loader don't use the function names that
the programmer invented!
Before it starts into the real work of generating instructions, the C++ systematically
renames all functions. The names are elaborated so that they include the name provided
by the programmer and details of the arguments. The person who wrote the compiler
can choose how this is done; most use the same general approach. In this approach,
codes describing the types of arguments are appended to the function name invented by
the programmer.
Using this kind of renaming scheme, the functions TimeUsed, Average,
StandardDev, and PrintStats:
void TimeUsed(void);
double Average(double total, int number);
double StandardDev(double sumsquares,
double average, int num);
void PrintStats(int num, double ave, double std_dev);
would be assigned compiler generated (or "mangled") names like: Mangled names
__TimeUsed__fv
__Average__fdfi
__StandardDev_f2dfi
__PrintStats__fif2d
You might well think this behaviour strange. Why should a compiler writer take it
on themselves to rename your functions?
One of the reasons that this is done is this makes it easier for the compiler and Type safe linkage
linking loader to check consistency amongst separate parts of a program built up from
several files. Suppose for instance you had a function defined in one file:
In another file you might have incorrectly declared this function as:
250 Functions
Process(11, 0.55);
If there were no checks, the program would be compiled and linked together, but it
would (probably) fail at run-time and would certainly produce the wrong results
because the function Process() would be getting the wrong data to work with.
The renaming process prevents such run-time errors. In the first file, the function
gets renamed as:
__Process__fdfi
__Process__fifd
When the linking loader tried to put these together it would find the mismatch and
complain (the error message would probably say something like "Can't find
__Process_fifd", this is one place where the programmer sometimes gets to see the
"mangled" names).
Suppressing the This renaming occasionally causes problems when you need to use a function that is
name mangling defined in an old C language library. Suppose the C library has a function void
process for C
libraries seed(int), normally if you refer to this in a piece of C++ code the compiler renames
the function making it __seed__fi. Of course, the linking loader can't find this
function in the C library (where the function is still called seed). If you need to call
such a function, you have to warn the C++ compiler not to do name mangling. Such a
warning would look something like:
extern "C" {
void seed(int);
}
It is unlikely that you will encounter such difficulties when working in your IDE
because it will have modernised all the libraries to suit C++. But if you get to use C
libraries from elsewhere, you may have to watch out for this renaming problem.
Overloaded names Another affect of this renaming is that a C++ compiler would see the following as
distinct functions:
short abs(short);
long abs(long);
double abs(double);
Type safe linkage, overloading, and name mangling 251
__abs__fs
__abs__fl
__abs__fd
This has the advantage that the programmer can use the single function name abs()
(take the absolute value of ...) to describe the same operation for a variety of different
data types. The name abs() has acquired multiple (though related) meanings, but the
compiler is smart enough to keep them distinct. Such a name with multiple meanings is
said to be overloaded.
It isn't hard for the compiler to keep track of what is going on. If it sees code like:
double x;
…
double v = -3.9 + x*(6.5 +3.9*x);
The compiler reasons that since v is a double the version of abs() defined for doubles
(i.e. its function __abs__fd) is the one that should be called.
This approach, which is more or less unique to C++, is much more attractive than
the mechanisms used in other languages. In most languages, function names must be
distinct. So, if a programmer needs an absolute value function (just checking, you do
know that that simply means you force any ± sign to be + don't you?) for several
different data types, then several separately named functions must be defined. Thus, in
the libraries used by C programs, you have to have:
double fabs(double);
long labs(long);
int abs(int);
Note that because of a desire for compatibility with C, most versions of the maths Note: math library
library do not have the overloaded abs() functions. You still have to use the does NOT have
overloaded
specialized fabs() for doubles and so forth. C++ will let you use abs() with doubles definitions of abs()
– but it truncates the values to integers so you lose the fraction parts of the numbers.
Function abs() illustrates the use of "overloading" so that a common name is used Overloading for
to describe equivalent operations on different data. There is a second common use for alternative interface
overloading. You will most likely encounter this use in something like a graphics
package. There you might well find a set of overloaded DrawRectangle() functions:
There is one basic operation of drawing a rectangle. Sometimes, the data available are
in the form of four integers – it is then convenient to use the first overloaded function.
Sometimes you may have a "Rectangle" data structure (a simple structure, see Chapter
17, that has constituent data elements defining top-left and bottom-right corners); in this
case you want the second variant. Sometimes your data may be in the form of "Point"
data structures (these contain an 'x' integer value, and a 'y' integer value) and either the
third of fourth form of the function would be convenient. Here, overloading is used to
provide multiple interfaces to a common operation.
Should you use overloading and define multiple functions with the same base name?
Probably not. Treat this as a "read only feature" of C++. You need to be able to
read code that uses this feature, but while you are a beginner you shouldn't write such
code. Stick to the KISS principle (remember Keep It Simple Stupid). Fancy styles like
overloaded functions are more likely to get you into trouble than help you in your initial
efforts.
Section 4.8, on Algol, introduced the idea of a "stack" and showed how this could be
used both to organize allocation of memory for the variables of a function, and to
provide a mechanism for communicating data between a calling function and the
function that it calls. It is worth looking again at these details.
The following code fragment will be used as an example:
#include <iostream.h>
int main()
{
const int LOWLIMIT = 1;
int highval;
cout << "Please enter high limit for parameter : ";
cin >> highval;
…
int val;
…
How functions work 253
Imagine that the program is running, the value 17 has been entered for highval, the
function call (marked ⇒) has been made, and the function has run almost to completion
and is about to return a value 9 for res (it is at the point marked ⇒). The state of the
"stack" at this point in the calculation would be as shown in Figure 10.1.
The operating system will allocate some specific fixed area in memory for a
program to use for its stack. Usually, the program starts using up this space starting at
the highest address and filling down.
Each routine has a "frame" in the stack. The compiler will have sorted this out and
worked out the number of bytes that a routine requires. Figure 10.1 shows most of the
stack frame for the main program, and the frame for the called GetIntegerInRange
function.
Local, "automatic", variables are allocated space on the stack. So the frame for
main() will include the space allocated for variables such as highval; these spaces are
where these data values are stored.
The compiler generates "calling" code that first copies the values of arguments onto
the stack, as shown in Figure 10.1 where the data values 1 and 17 appear at the top of
the "stack frame" for GetIntegerInRange(). The next slot in the stack would be left
blank; this is where the result of the function is to be placed before the function returns.
17 highval
"Stack frame" for int
main()
"Stack Frame"
register holds
this address
Following the code that organizes the arguments, the compiler will generate a
subroutine call instruction. When this gets executed at run-time, more "housekeeping
information" gets saved on the stack. This housekeeping information will include the
"return address" (the address of the instruction in the calling program that immediately
follows the subroutine call). There may be other housekeeping information that gets
saved as well.
The first instructions of a function will normally claim additional space on the stack
for local variables. The function GetIntegerInRange() would have to claim
sufficient space to store its res variable.
When a program is running, one of the cpu's address registers is used to hold the
address of the current stack frame (the address will be that of some part of the
housekeeping information). As well as changing the program counter, and saving the
old value of the program counter on the stack, the subroutine call instruction will set
this stack frame register (or "stack frame pointer sfp").
A function can use the address held in this stack frame pointer to find its local
variables and arguments. The compiler determines the arrangement of stack frames and
can generate address specifications that define the locations of variables by their offsets
relative to the stack frame pointer. The res local variable of function GetInteger-
InRange() might be located 8-bytes below the address in the stack frame pointer. The
location for the return value might be 12-bytes above the stack frame pointer. The
argument high might be 16-bytes above the stack frame pointer, and the argument low
would then be 20-bytes above the stack frame pointer ("sfp"). The compiler can work
all of this out and use such information when generating code for the function.
The instruction sequences generated for the function call and the function would be
along the following lines:
for result)
return from subroutine
As illustrated, a function has space in the stack for its local variables and its
arguments (formal parameters). The spaces on the stack for the arguments are
initialized by copying data from the actual parameters used in the call. But, they are
quite separate. Apart from the fact that they get initialized with the values of the actual
parameters, these argument variables are just like any other local variable. Argument
values can be incremented, changed by assignment or whatever. Changes to such value
parameters don't have any affect outside the function.
really is quite reasonable. In most circumstances, the overheads involved in calling this
function and getting a result returned will be negligible. The fact that the function is
defined helps make code more intelligible.
But, sometimes it isn't appropriate to pay the overhead costs needed in calling a
function even though the functional abstraction is desired because of the way this
enhances intelligibility of code.
When might you want to avoid function call overheads?
Usually, this will be when you are doing something like coding the "inner loop" of
some calculation where you may have a piece of code that you know will be executed
hundreds of millions of times in a single run of a program, cutting out a few instructions
might save the odd minute of cpu time in runs taking several hours. Another place
where you might want to avoid the extra instructions needed for a function call would
be in a piece of code that must be executed in the minimal time possible (e.g. you are
writing the code of some real time system like the controller in your car which has to
determine when to inflate the air-bags).
C++ has what it calls "inline" functions:
This presents the same abstraction as any other function definition. Here Average() is
again defined as a function that takes a double and an integer as data and which returns
256 Functions
their quotient as a double. Other functions can use this Average() function as a
primitive operation and so avoid the details of the calculation appearing in the code of
the caller.
The extra part is the inline keyword. This is a hint to the compiler. The
programmer who defined Average() in this way is suggesting to the compiler that it
should try to minimize the number of instructions used to calculate the value.
Consider a typical call to function Average():
The instruction sequence normally generated for the call would be something like the
following:
This is a rather extreme example in that the inline code does require significantly
fewer instructions.
If things really are going to be time critical, you can request inline expansion for
functions. You should only do this for simple functions. (Compilers interpretations of
"simple" vary a little. Basically, you shouldn't declare a function inline if it has loops,
or if it has complex selection constructs. Compilers are free to ignore your hints and
compile the code using the normal subroutine call sequence; most compilers are polite
enough to tell you that they ignored you inline request.)
Of course, if a compiler is to "expand" out the code for a function inline, then it must
know what the code is! The definition of an inline function must appear in any file
that uses it (either by being defined there, or by being defined in a file that gets
#included). This definition must occur before the point where the function is first used.
The idea of recursion was introduced in section 4.8. Recursion is a very powerful
problem solving strategy. It is appropriate where a problem can be split into two
subproblems, one of which is easy enough to be solved immediately and the second of
which is simply the original problem but now with the data slightly simplified.
Recursive algorithms are common with problems that involve things like finding
ones way through a maze. (Maze walk problem: moving from room where you are to
exit room; solution, move to an adjacent room and solve maze walk for that room.)
Surprisingly, many real programming problems can be transformed into such searches.
Section 4.8 illustrated recursion with a pseudo-code program using a recursive
number printing function. How does recursion help this task? The problem of printing
a number is broken into two parts, printing a digit (easy), and printing a (smaller)
number (which is the original problem in simpler form). The recursive call must be
done first (print the smaller number) then you can print the final digit. Here it is
implemented in C++:
#include <stdlib.h>
#include <iostream.h>
void PrintDigit(short d)
{
switch(d) {
case 0 : cout << '0'; break;
case 1 : cout << '1'; break;
258 Functions
int main()
{
PrintNumber(9623589); cout << endl;
PrintNumber(-33); cout << endl;
PrintNumber(0); cout << endl;
return 0;
}
Enter this program and compile with debugging options on. Before running the
program, use the debugger environment in your IDE to set a breakpoint at the first line
of the PrintDigit() function. Then run the program.
The program will stop and return control to the debugger just before it prints the first
digit (the '9' for the number 9623589). Use the debugger controls to display the "stack
backtrace" showing the sequence of function calls and the values of the arguments.
Figure 10.2 illustrates the display obtained in the Symantec 8 environment.
A recursive function 259
The stack backtrace shows the sequence of calls: PrintDigit() was called from
PrintNumber() which was called from PrintNumber() … which was called from
main() . The display shows the contents of the stack frames (and, also, the return
addresses) so you can see the local variables and arguments for each level of recursion.
You should find the integrated debugger a very powerful tool for getting to
understand how functions are called; it is particularly useful when dealing with
recursion.
10.9.1 GetIntegerInRange
Well, it is conceptually correct. But if you were really wanting to provide a robust
function that could be used in any program you would have to do better.
What is wrong with the simple function?
First, what will happen if the calling code is mixed up with respect to the order of
the arguments. ("I need that GetIntegerInRange; I've got to give it the top and bottom
limits – count = GetIntegerInRange(100,5).") A user trying to run the program is
going to find it hard to enter an acceptable value; the program will just keep nagging
away for input and will keep rejecting everything that is entered.
There are other problems. Suppose that the function is called correctly ( GetInteger
InRange(5,25)) but the user mistypes a data entry, keying y instead of 7. In this case
the cin >> res statement will fail. Error flags associated with cin will be set and the
value 0 will be put in res. The function doesn't check the error flag. But it does check
the value of res. Is res (0) between 5 and 25? No, so loop back, reprompt and try to
read a value for res.
That is the point where it starts to be "fun". What data are waiting to be read in the
input?
The next input data value is that letter y still there waiting to be read. The last read
routine invoked by the cin >> operation didn't get rid of the y . The input routine
simply saw a y that couldn't be part of a numeric input and left it. So the input
operation fails again. Variable res is still 0. Consequently, the test on res being in
range fails again. The function repeats the loop, prompting for an input value and
attempting to read data.
Try running this version of the function with such input data (you'd best have first
read your IDE manuals to find how to stop an errant program).
Fundamental law of Remember Murphy's law: If something can go wrong it will go wrong. Murphy's
programming law is the fundamental law of programming. (I don't know whether it is true, but I read
somewhere that the infamous Mr. Murphy was the leader of one of IBM's first
programming projects back in the early 1950s.)
You need to learn to program defensively. You need to learn to write code that is
not quite as fragile as the simple GetIntegerInRange().
Verify the data given One defensive tactic that you should always consider is verifying the data given to a
to a function function. If the data are wrong (e.g. high < low) then the function should not
continue.
What should a function do under such circumstances?
In Part IV, we will explore the use of "exceptions". Exceptions are a mechanism
that can sometimes be used to warn your caller of problems, leaving it to the caller to
sort out a method of recovery. For the present, we will use the simpler approach of
terminating the program with a warning message.
Examples of simple functions: GetIntegerInRange 261
A mechanism for checking data, and terminating a program when the data are bad, is Using assert
so commonly needed that it has been packaged and provided in one of the standard
libraries. Your IDE will have a library associated with the assert.h header file.
This library provides a "function" known as assert() (it may not be implemented
as a true function, some systems use "macros" instead). Function assert() has to be
given a "Boolean" value (i.e. an expression that evaluates to 0, False, or non-zero True).
If the value given to assert() is False, the function will terminate the program after
printing an error message with the general form "Assertion failed in line … of function
… in file …".
We would do better starting the function GetIntegerInRange() as follows:
#include <stdlib.h>
#include <iostream.h>
#include <limits.h>
#include <assert.h>
The function now starts with the assert() check that low and high are appropriate.
Note the #include <assert.h> needed to read in the declaration for assert(). (Another
change: the function now has default values for low and high; the constants LONG_MAX
etc are taken from limits.h.)
You should always consider using assert() to validate input to your functions.
You can't always use it. For example, you might be writing a function that is supposed
to search for a specific value in some table that contains data "arranged in ascending
order"; checking that the entries in the table really were in ascending order just wouldn't
be worth doing.
But, in this case and many similar cases, validating the arguments is easy. Dealing
with input errors is a little more difficult.
Some errors you can't deal with. If the input has gone "bad", there is nothing you Handling input
can do. (This is going to be pretty rare; it will necessitate something like a hardware errors
failure, e.g. unreadable file on a floppy disk, to make the input go bad.)
If all the data in a file has been read, and the function has been told to read some
more, then once again there is nothing much that can be done.
So, for bad input, or end of file input, the function should simply print a warning
message and terminate the program.
If an input operation has failed but the input stream, cin, is not bad and not at end of Clearing the failure
file, then you should try to tidy up and recover. The first thing that you must do is clear flag
the fail flag on cin. If you don't do this, anything else you try to do to cin is going to
be ignored. So, the first step toward recovery is:
cin.clear();
262 Functions
(The clear() operation is another of the special functions associated with streams like
the various format setting setf() functions illustrated in 9.7.)
Next, you have to deal with any erroneous input still waiting to be read (like the
letter y in the discussion above). These characters will be stored in some data area
owned by the cin stream (the "input buffer"). They have to be removed.
We are assuming that input is interactive. (Not much point prompting a file for new
data is there!) If the input is interactive, the user types some data and ends with a
return. So we "know" that there is a return character coming.
We can get rid of any queued up characters in the input buffer if we tell the cin
stream to "ignore" all characters up to the return (coded as ' \n'). An input stream has an
ignore() function, this takes an integer argument saying how many characters
maximum to skip, and a second argument identifying the character that should mark the
end of the sequence that is to be skipped. A call of the form:
cin.ignore(SHRT_MAX, '\n');
should jump over any characters (up to 30000) before the next return.
All of this checking and error handling has to be packaged in a loop that keeps
nagging away at the user until some valid data value is entered. One possible coding
for this loop is:
for(;;) {
Prompting cout << "Enter a number in the range " << low
<< " to " << high << " : ";
int val;
Trying to read cin >> val;
if(cin.eof()) {
cout << "Have unexpected end of file; quitting."
<< endl;
exit(1);
}
Examples of simple functions: GetIntegerInRange 263
cin.ignore(SHRT_MAX, '\n');
}
This is an example of a "forever" loop. The program is stuck in this loop until either
an acceptable value is entered or something goes wrong causing the program to
terminate. The loop control statement:
for(;;)
has nothing in the termination test part (i.e. nothing terminates this loop).
After the read (cin >> val) , the first check is whether cin is "good". It will be
good if it was able to read an integer value and store this value in variable val . Of
course, the value entered might have been an integer, but not one in the acceptable
range. So, that had better be checked. Note the structure of the nested ifs here. If cin
is good then if the number is in range return else complain and loop.
Here the return statement is in the middle of the body of the loop. This style will
offend many purists (who believe that you should only return from the end point of the
function). However, it fits logically with the sequence of tests and the code in this form
is actually less complex than most variants.
Note also the use of continue. This control statement was introduced in section 7.8
but this is the first example where it was used. If the user has entered an out of range
value, we need to print a warning and then repeat the process of prompting for and
reading data. So, after the warning we want simply to "continue" with the next iteration
of the for loop.
The remaining sections of the loop deal with the various input problems already
noted and the attempt to clean up if inappropriate data have been entered.
Although it won't survive all forms of misuse, this version of
GetIntegerInRange() is a little more robust. If you are writing functions that you
hope to use in many programs, or which you hope to share with coworkers, you should
try to toughen them like this.
This second example is a lot simpler! It is merely the program for finding a square root
(7.2.2) recoded to use a function:
#include <iostream.h>
#include <stdlib.h>
#include <math.h>
#include <assert.h>
264 Functions
double NewtRoot(double x)
{
double r;
const double SMALLFRACTION = 1.0E-8;
Sanity check on assert(x > 0.0);
argument value
r = x / 2.0;
while(fabs(x - r*r) > SMALLFRACTION*x) {
cout << r << endl;
r = 0.5 *(x / r + r);
}
return r;
}
int main()
{
double x;
cout << "Enter number : ";
cin >> x;
cout << "Number was : " << x << ", root is "
<< NewtRoot(x) << endl;
return EXIT_SUCCESS;
}
Once again, it is worth putting in some checking code in the function. The assert()
makes certain that we haven't been asked to find the root of a negative number.
The stdlib library includes a function called rand(). The documentation for stdlib
describes rand() as a "simple random number generator". It is supposed to return a
random number in the range 0 to 32767.
What is your idea of a "random number" generator? Probably you will think of
something like one of those devices used to pick winners in lotteries. A typical device
for picking lottery winners has a large drum containing balls with the numbers 1 to 99
(or maybe some smaller range); the drum is rotated to mix up the balls and then some
mechanical arrangement draws six or so balls. The order in which the balls are drawn
doesn't matter; you win the lottery if you had submitted a lottery coupon on which you
had chosen the same numbers as were later drawn. The numbers drawn from such a
device are pretty random. You would have won this weeks lottery if you had picked the
numbers 11, 14, 23, 31, 35, and 39; last week you wished that you had picked 4, 7, 17,
23, 24, and 42.
The rand() function don't seem to work quite that way. For example, the program:
Rand() 265
int main()
{
for(int i = 0;i<20;i++)
cout << rand() << endl;
return 0;
}
16838
5758
10113
17515
31051
5627
23010
…
…
25137
25566
Exactly the same numbers are produced if I run the program on a Sun workstation
(though I do get a different sequence on a PowerPC).
There is an auxiliary function that comes with rand(). This is srand(). The "Seeding" the
function srand() is used to "seed the random number generator". Function srand() random number
generator
takes an integer as an argument. Calling srand() with the argument 5 before the loop
generating the "random numbers":
srand(5);
for(int i = 0;i<20;i++)
cout << rand() << endl;
and (on a Macintosh with Symantec 7.06) you will get the sequence
18655
8457
10616
31877
…
…
985
32068
24418
Try another value for the "seed" and you will get a different sequence.
For a given seed, the same numbers turn up every time.
266 Functions
• The counts for the frequency of each of the possible last digits, obtained by taking
the number modulo 10 (i.e. rand() % 10), will be approximately equal.
• If you look at pairs of successive pseudo-random numbers, then the number times
that the second is larger will be approximately equal to the number of times that the
first is larger.
• You will get about the same number of even numbers as odd numbers.
• If you consider a group of three successive random numbers, there is a one in eight
chance that all will be odd, a one in eight chance that all are even, a three in eight
chance that you will get two odds and one even. These are the same (random)
chances that you would get if you were tossing three coins and looking for three
heads etc.
Although the generated numbers are not random, the statistical properties that they
share with genuinely random numbers can be exploited.
If you've lost your dice and want a program to roll three dice for some game, then
the following should be (almost) adequate:
int main()
{
// Roll three dice for game
char ch;
…
do {
for(int i=0; i < 3; i++) {
int roll = rand() % 6;
// roll now in range 0..5
roll++; // prefer 1 to 6
cout << roll << " ";
}
cout << endl;
Rand() 267
About one time in two hundred you will get three sixes; you have the same chance of
getting three twos. So the program does approximate the rolling of three fair dice.
But it is a bit boring. Every time you play your game using the program you get the
same dice rolls and so you get the same sequence of moves.
3 5 4
Roll again? y
2 2 6
Roll again? y
1 4 1
Roll again? y
1 2 6
Roll again? n
You can extend the program so that each time you run it you get a different sequence
of arbitrary numbers. You simply have to arrange to seed the random number generator
with some "random" value that will be different for each run of the program.
Of course, normal usage requires everything to be the same every time you run the Finding a seed
same program on the same data. So you can't have anything in your program that
provides this random seed; but you can always ask the operating system (OS) about
other things going on in the machine. Some of the values you can get through such
queries are likely to be different on different runs of a program.
Most commonly, program's ask the OS for the current time of day, or for some
related time measure like the number of seconds that the machine has been left switched
on. These calls are OS dependent (i.e. you will have to search your IDE documentation
for details of the name of the timing function and its library). Just for example, I'll
assume that your system has a function TickCounter() that returns the number of
seconds that the machine has been switched on. The value returned by this function call
can be used to initialize the random number generator:
int main()
{
// Roll three dice for game
char ch;
srand(TickCounter());
do {
for(int i=0; i < 3; i++) {
int roll = rand() % 6;
…
…
268 Functions
With this extension to "randomize" the number sequence, you have a program that will
generate a different sequence of dice rolls every time you run it.
If "TickCounter()" is random enough to seed the generator, why not just use it to
determine the roll of the dice:
do {
for(int i=0; i < 3; i++) {
int roll = TickCounter() % 6;
roll++; // prefer 1 to 6
Of course this doesn't work! The three calls to TickCounter() are likely to get exactly
the same value, if not identical then the values will be in increasing order. A single call
to TickCounter() has pretty much random result but inherently there is no
randomness in successive calls; the time value returned is monotonically increasing.
When developing and testing a program, you will often chose to pass a constant
value in the call to srand(). That way you get the same sequence of numbers every
time. This can help debugging because if you find something going wrong in your
program then, after adding trace statements, you can rerun the program on identical
data. When the program has been developed, you arrange either to get the user to enter
the seed or you get a seed value from a system call like TickCounter().
10.11 EXAMPLES
10.11.1 π-Canon
A mathematics professor once approached a colonel of his state's National Guard asking
for the Guards assistance in a new approach to estimating the value of π.
The professor reckoned that if a cannon was fired at a distant target flag, the balls
would fall in a random pattern around the target. After a large number of balls had been
fired, he would be able to get an estimate of π by noting where the balls had fallen. He
used the diagram shown in Figure 10.3, to explain his approach to estimating π:
The colonel refused. He reckoned that his gunners would put every shot within 50
paces of the target flag and that this would lead to the result π = 4 which everyone knew
was wrong.
Write a computer program that will allow the maths. professor to simulate his
experiment.
The program should let the professor specify the number of shots fired. It should
simulate the experiment printing estimates of π at regular intervals along with a final
estimate when all ammunition has been expended.
Examples: π-canon 269
33 π
--- = --- so π ≈ 3.219
41 4
Design
//initialization phase
get professor to say how much ammunition he has
get some seed for the random number generator
loop until ammunition exhausted
increment count of shots fired
270 Functions
get x, y coordinates
test position and, if appropriate,
update count of shots in circle
maybe print next estimate of π (not for every shot,
What is frequency of these reports?)
print final estimate of π
Some "functions" appear immediately in this description, for example, "estimate π",
"test position", "get coordinate". All of these tasks involve several calculation steps that
clearly belong together, can be thought about in isolation, and should be therefore
separate functions. Even something like "initialization phase" could probably be a
function because it groups related operations.
Second iteration We should now start to identify some of these functions:
through design
get ammunition
prompts professor for inputs like amount of ammunition,
and seed for random number generator
use seed to seed the random number generator
return amount of ammunition
π-estimator function
must be given total shot count and circle shot count
calculates π from professor's formula
returns π estimate
get-coordinate function
use random number generator, converts 0..32767 result
into a value in range -100.0 to +100.0 inclusive
returns calculated value
print π-estimate
produce some reasonably formatted output,
probably should show π and the number of shots used to
get this estimate, so both these values will
be needed as arguments
test in circle
given an x and y coordinate pair test whether they
represent a point within 100.0 of origin
return 0 (false) 1 (true) result
main function
get ammunition
loop until ammunition exhausted (How checked?)
increment count of shots fired
(Where did this get initialized?)
x = get-coordinate
y = get-coordinateget x,y coordinates
if test in circle (x, y)
increment circle count
Examples: π-canon 271
There are some queries. These represent issues that have to be resolved as we proceed
into more detailed design.
This problem is still fairly simple so we have already reached the point where we Third iteration
can start thinking about coding. At this point, we compose function prototypes for each through design
process
of the functions. These identify the data values that must be given to the function and
the type of any result:
double GetCoordinate(void);
int main();
Next, we have to consider the design of each individual function. The process here is
identical to that in Part II where we designed the implementations of the little programs.
We have to expand out our english text outline for the function into a sequence of
variable definitions, assignment statements, loop statements, selection statements etc.
Most of the functions that we need here are straightforward:
long GetAmmunition(void)
{
long munition;
long seed;
cout << "Please Professor, how many shots should we fire?";
// Read shots, maybe we should use that GetIntegerInRange
// to make certain that the prof enters a sensible value
// Risk it, hope he can type a number
cin >> munition;
// Need seed for random number generator,
// Get prof to enter it (that way he has chance of
// getting same results twice). Don't confuse him
// with random number stuff, just get a number.
cout << "Please Professor, which gunner should lay the
gun?"
272 Functions
<< endl;
cout << "Please enter a gunner identifier number 1..1000
:";
cin >> seed;
srand(seed);
return munition;
}
double GetCoordinate(void)
{
int n = rand();
// Take n modulo 201, i.e. remainder on dividing by 201
// That should be a number in range 0 to 200
// deduct 100
n = n % 201;
return double(n-100);
}
The main() function is where we have to consider what data values are needed for
the program as a whole. We appear to need the following data elements:
double x coordinate
double y coordinate
Examples: π-canon 273
int main()
{
long fired = 0, ontarget = 0, ammunition;
double pi = 4.0; // Use the colonel's estimate!
long freq, count;
ammunition = GetAmmunition();
count++;
if(count == freq) {
// Time for another estimate
pi = PiEstimate(fired, ontarget);
PrintPIEstimate(pi, fired);
// reset count to start over
count = 0;
}
}
Problem
When doing introductory engineering and science subjects, you often need plots of
functions, functions such as
This function represents an exponentially decaying cosine wave. Such functions turn
up in things like monitoring a radio signal that starts with a pulse of energy and then
decays away. The parameters a, b, c, and d determine the particular characteristics of
the system under study.
It is sometimes useful to be able to get rough plots that just indicate the forms of
such functions. These plots can be produced using standard iostream functions to print
characters. The width of the screen (or of a printer page) can be used to represent the
function range; successive lines of output display the function at successive increasing
values of x. Figure 10.4 illustrates such a plot.
Specification
Write a program that will plot function values using character based output following
the style illustrated in Figure 10.4. The program is to plot values of a fixed function
f(x) (this function can only be changed by editing and recompilation). The program is
to take as input a parameter R that defines the range of the function, the page (screen)
width is to be used to represent the range -R to +R. Values of the function are to be
plotted for x values, starting at x = 0.0, up to some maximum value X. The maximum,
and the increments for the x-axis, are to be entered by the user when the program runs.
Design
"Plotting a function" – the "top function" of this program is again simple and serves as
the starting point for breaking the problem down.
Examples: function plotter 275
------------------------------------+-----------------------------------
| *
| *
| *
| *
* |
* |
* |
* |
* |
* |
* |
* |
* |
* |
*|
| *
| *
| *
| *
| *
| *
| *
| *
| *
|*
* |
* |
* |
* |
* |
* |
* |
* |
* |
The obvious functions are: "work out function value", "plot current value", and "plot
axis".
276 Functions
plotaxis
prints a horizontal rule (made from '-' characters) with a
centre marker representing the zero point of the
range
probably should also print labels, -R and +R, above the
rule
so the value for the range should be given as an
argument
(? width of screen ? will need this defined as a constant
that could be adjusted later to suit different
screen/page sizes)
plot function
given a value for f(x), and details of the range,
this function must first work out where on line
a mark should appear
a line should then be output made up from spaces, a |
character to mark the axis, and a * in the
appropriate position
Functions main(), plotaxis(), and fun() look fairly simple. You wouldn't need
to iterate further through the design process for these; just have to specify the
prototypes and then start on coding the function bodies.
Example, screen width 60 places, range represented -2.0 to +2.0, value to be plotted 0.8:
Example, screen width 60 places, range represented -2.0 to +2.0, value to be plotted
-1.5:
A C++ statement defining the actual character position on the line is:
The constant HALF represents half the screen width. Its value will depend on the
environment used.
Since we now know how to find where to plot the '*' character we need only work
out how to do the correct character output.
There are a number of special cases that may occur:
• The value of where is outside of the range 1..WIDTH (where WIDTH is the screen
width). This will happen if the user specified range is insufficient. If the value is
out of range, we should simply print the '|' vertical bar character marking the x-axis
itself.
• where's value is HALF, i.e. the function value was 0.0. We don't print the usual '|'
vertical bar axis marker, we simply plot the '*' in the middle of the page.
• where is less than HALF. We need to output the right number of spaces, print the
'*', output more spaces until half way across the page, print the axis marker.
• where is greater than HALF. We need to output spaces until half way across page,
print the axis marker, then output additional spaces until at point where can print
the '*' symbol.
There are smarter ways to deal with the printout, but breaking it down into these
possibilities is about the simplest.
The problem has now been simplified to a point where coding can start.
Implementation
There isn't much to the coding of this example and the complete code is given below.
278 Functions
Conditional The code is used to provide a first illustration of the idea of "conditional
compilation directives compilation". You can include in the text of your program various "directives" that
instruct the compiler to take special actions, such as including or omitting a particular
piece of code.
Conditional There are two common reasons for having conditionally compiled code. The first is
compilation for that you want extra "tracing" code in your program while you are developing and
debugging and
customisation debugging it. In this case, you will have conditionally compiled sections that have extra
output statements ("trace" output); examples will be shown later. This code provides a
rather simple instance of the second situation where conditional compilation is required;
we have a program that we want run on different platforms – Symantec, Borland-
compiled DOS application, and Borland-compiled EasyWin application. These
different environments require slightly different code; but rather than have three
versions of the code, we can have a single file with conditionally compiled
customizations.
The differences are very small. The default font in an EasyWin window is rather
large so, when measured in terms of characters, the window is smaller. If you compile
and run the program as a DOS-standard program it runs, and exits back to the Borland
IDE environment so quickly that you can't see the graph it has drawn! The DOS
version needs an extra statement telling it to slow down.
The directives for conditional compilation, like most other compiler directives, occur
on lines starting with a # character. (The only other directives we've used so far has
been the #include directive that loads a header file, and the #define directive that
defines a constant.) Here we use the #if, #elif ("else if"), #else, and #endif directives
and use #define in a slightly different way. These directives are highlighted in bold in
the code shown below; the italicised statements are either included or omitted according
to the directives.
#include <math.h>
#include <iostream.h>
#include <stdlib.h>
#define SYMANTEC
#if defined(SYMANTEC)
const int WIDTH = 71;
#elif defined(EASYWIN)
const int WIDTH = 61;
#else
const int WIDTH = 81;
#endif
#if defined(DOS)
#include <dos.h>
#endif
Examples: function plotter 279
The line #define SYMANTEC "defines" SYMANTEC as a compile time symbol (it
doesn't get a value, it just exists); this line is specifying that this version of the code is to
be compiled for the Symantec environment. The line would be changed to #define
EASYWIN or #define DOS if we were compiling for one of the other platforms.
The #if defined() … #elif defined() … #else … #endif statements select
the WIDTH value to be used depending on the environment. The other #if … #endif
adds an extra header file if we are compiling for DOS. The DOS version uses the
standard sleep() function to delay return from the program. Function sleep() is
defined in most environments; it takes an integer argument specifying a delay period for
a program. Although it is widely available, it is declared in different header files and is
included in different libraries on different systems.
The definition of constant HALF illustrates compile time arithmetic; the compiler will
have a value for WIDTH and so can work out the value for HALF. The other constants
define the parameters that affect the function that is being plotted.
The code for PlotAxis() is simple; it prints the range values, using the loop
printing spaces to position the second output near the right of the window. Note that
variable i is defined in the first for statement; as previously explained, its scope extends
to the end of the block where it is defined. So, the other two loops can also use i as
their control variables. (Each of the cases is coded as a separate compound statement
enclosing a for loop. Each for loop defines int i. Each of these 'i's has scope that is
limited to just its own compound statement.)
return;
}
Your instructor may dislike the coding style used in this version of PlotFunction
Value(); multiple returns are often considered poor style. The usual recommended
style would be as follows:
if(where == HALF) {
…
}
else
if(where < HALF) {
…
}
else {
…
}
return;
}
double fun(double x)
{
double val;
val = a*cos(b*x+c)*exp(-d*x);
return val;
}
int main()
{
double r;
double xinc;
double xmax;
PlotAxis(r);
double x = 0.0;
while(x<=xmax) {
double f = fun(x);
PlotFunctionValue(f, r);
x += xinc;
282 Functions
#if defined(DOS)
// delay return from DOS to Borland IDE for
// a minute so user can admire plotted function
sleep(60);
#endif
return 0;
There are no checks on input values, no assertions. Maybe there should be. But
there isn't that much that can go seriously wrong here. The only problem case would be
the user specifying a range of zero; that would cause an error when the function value is
scaled. You can't put any limit on the range value; it doesn't matter if the range is given
as negative (that flips the horizontal axis but does no real harm). A negative limit for
xmax does no harm, nothing gets plotted but no harm done.
EXERCISES
1 Write a program that will roll the dice for a "Dungeons and Dragons" (DD) game.
DD games typically use several dice for each roll, anything from 1 to 5. There are many
different DD dice 1…4, 1…6, 1…8, 1…10, 1…12, 1…20. All the dice used for a particular
roll will be of the same type. So, on one roll you may need three six sided dice; on the next
you might need one four sided dice.
The program is to ask how many dice are to be rolled and the dice type. It should print
details of the points on the individual dice as well as a total score.
The program is to loop printing a cookie message (a joke, a proverb, cliche, adage). After
each message, the program is to ask the user for a Y(es) or N(o) response that will determine
whether an additional cookie is required.
Use functions to select and print a random cookie message, and to get the Y/N response.
You can start with the standard fortunes – "A new love will come into your life.", "Stick with
your wife.", "Too much MSG, stomach pains tomorrow.", "This cookie was poisoned.". If
you need others, buy a packet of cookies and plagiarise their ideas.
11
11 Arrays
Suppose that, in the example on children's heights (example in 8.5.1), we had wanted to
get details of all children whose heights were more than two standard deviations from
the mean. (These would be the tallest 5% and the smallest 5% of the children).
We would have had problems. We couldn't have identified these children as we read
the original data. All the data have to be read before we can calculate the standard
deviation and the mean. We could use a clumsy mechanism where we read the data
from file once and do the first calculations, then we "rewind the file" and read the data
values a second time, this time checking each entry against the mean. (This scheme sort
of works for files, but "rewinding" a human user and requesting the rekeying of all
interactive input is a less practical proposition.)
Really, we have to store the data as they are read so we can make a second pass
through the stored data picking out those elements of interest.
But how can these data be stored?
A scheme like the following is obviously impractical:
int main()
{
double average, stand_dev;
double height1, height2, height3, height4, …,
height197, height198, …
height499, height500;
cout << "Enter heights for children" << endl;
cin >> height1 >> height2 >> height3 >> … >> height500;
Quit apart from the inconvenience, it is illogical. The program would have had to be
written specifically for the data set used because you would need the same number of
variables defined, and input statements executed, as you have values in your data set.
Collections of data elements of the same type, like this collection of different The beginnings in
heights, are very commonly needed. They were needed back in the earliest days of assembly language
programs
programming when all coding was done in assembly language. The programmers of
284 Arrays
the early 1950s got their assemblers to reserve blocks of memory locations for such data
collections; an "assembler directive" such as
would have reserved 500 memory locations for the collection of heights data. The
programmers would then have used individual elements in this collection by writing
code like:
load address register with the address where heights data start
add on "index" identifying the element required
// now have address of data element required
load data register from address given in address register
By changing the value of the 'index', this code accesses different elements in the
collection. So, such code inside loops can access each element in turn.
High level languages Such collections were so common that when FORTRAN was introduced in 1956 it
and "arrays" made special provision for "arrays". Arrays were FORTRAN's higher level abstraction
of the assembler programmers' block of memory. A FORTRAN programmer could
have defined an array using a statement like:
DIMENSION HEIGHTS(500)
HEIGHTS(N) = H;
…
IF(HEIGHTS(I) .LT. HMAX) GOTO 50
…
The definition specifies that there are 500 elements in this collection of data (numbered
1 to 500). The expressions like HEIGHTS(N) and HEIGHTS(I) access chosen
individual elements from the collection. The integers N and I are said to "index" into
the array. The code generated by the compiler would have been similar to the fragment
of assembler code shown above. (The character set, as available on the card punches
and line printers of the 1950s and 1960s, was limited, so FORTRAN used left ( and
right ) parentheses for many purposes including array indexing; similarly a text string
such as .LT. would have had to be used because the < character would not have been
available.)
Arrays with bounds As languages evolved, improved versions of the array abstraction were generally
checks and chosen adopted. Thus, in Pascal and similar 1970s languages, arrays could be defined to have
subscript ranges
chosen subscript ranges:
heights[n] := h;
…
if(heights[i] > hmax) hmax := heights[i];
The compilers for these languages generally produced more complex code that included
checks based on the details of the subscript range as given in the definition. The
assembly language generated when accessing array elements would typically include
extra instructions to check that the index was in the appropriate range (so making
certain that the requested data element was one that really existed). Where arrays were
to be used as arguments of functions, the function declaration and array definition had
to agree on the size of the array. Such checks improve reliability of code but introduce
run-time overheads and restrict the flexibility of routines.
When C was invented, its array abstraction reverted more to the original assembly C's arrays
language programmer's concept of an array as a block of memory. This was partly a
consequence of the first version of C being implemented on a computer with a
somewhat primitive architecture that had very limited hardware support for "indexing".
But the choice was also motivated by the applications intended for the language. C was
intended to be used instead of assembly language by programmers writing code for
components of an operating system, for compiler, for editors, and so forth. The final
machine code had to be as efficient as that obtained from hand-coded assembly
language, so overheads like "array bounds checking" could not be countenanced.
Many C programmers hardly use the array abstraction. Rather than array
subscripting, they use "pointers" and "pointer arithmetic" (see Part IV) to access
elements. Such pointer style code is often closely related to the optimal set of machine
instructions for working through a block of memory. However, it does tend to lose the
abstraction of an array as a collection of similar data elements that can be accessed
individual by specifying an index, and does result in more impenetrable, less readable
code.
The C++ language was intended to be an improvement on C, but it had to maintain C++'s arrays
substantial "backwards compatibility". While it would have been possible to introduce
an improved array abstraction into C++, this would have made the new language
incompatible with the hundreds of thousands of C programs that had already been
written. So a C++ array is just the same as a C array. An array is really just a block of
memory locations that the programmer can access using subscripting or using pointers;
either way, access is unchecked. There are no compile time, and no run time checks
added to verify correct usage of arrays; it is left to the programmer to get array code
correct. Inevitably, array usage tends to be an area where many bugs occur.
286 Arrays
Simple C++ variables are defined by giving the type and the name of the variable.
Arrays get defined by qualifying the variable name with square brackets [ and ], e.g.:
double heights[500];
"Vector "or "table" This definition makes heights an array with 500 elements. The terms vector and table
are often used as an alternative names for a singly subscripted array like heights.
Array index starts at The low-level "block of memory" model behind C's arrays is reflected in the
zero! convention that array elements start with 0. The definition for the heights[] array
means that we can access heights[0] ... heights[499]. This is natural if you are
thinking in terms of the memory representation. The first data element is where the
heights array begins so to access it you should not add anything to the array's address
(or, if you prefer, you add 0).
This "zero-base" for the index is another cause of problems. Firstly, it leads to
errors when adapting code from examples written in other languages. In FORTRAN
arrays start at 1. In Pascal, the programmer has the choice but zero-based Pascal arrays
are rare; most Pascal programs will use 1-based arrays like FORTRAN. All the loops
in FORTRAN and Pascal programs tend to run from 1..N and the array elements
accessed go from 1 .. N. It is quite common to find that you have to adapt some
FORTRAN or Pascal code. After all, there are very large collections of working
FORTRAN routines in various engineering and scientific libraries, and there a
numerous text books that present Pascal implementations of standard algorithms. When
adapting code you have to be careful to adjust for the different styles of array usage.
(Some people chicken out; they declare their C arrays to have N+1 elements, e.g.
double X[N+1], and leave the zeroth element, X[0], unused. This is OK if you have
one dimensional arrays of simple built in types like doubles; but you shouldn't take this
way out for arrays of structures or for multidimensional arrays.)
A second common problem seems just to be a consequence of a natural, but
incorrect way of thinking. You have just defined an array, e.g. xvals[NELEMENTS]:
executed at run-time without any complaint. The value actually changed by this
assignment will be (probably) either xmax, or ymax (it depends on how your compiler
and, possibly, link-loader choose to organize memory). A program with such an error
will appear to run but the results are going to be incorrect.
In general, be careful with array subscripts!
You should use named constants, or #defined values, when defining the sizes of Constants in array
arrays rather than writing in the number of array elements. So, the definitions: definitions
are better than the definition given at the start of this section. They are better because
they make the code clearer (MAXCHILDREN has some meaning in the context of the
program, 500 is just a "magic number"). Further, use of a constant defined at the start
of the code makes it easier to change the program to deal with larger numbers of data
values (if you have magic numbers, you have to search for their use in array definitions
and loops).
Technically, the [ and ] brackets together form a [] "postfix operator" that The [] operator
modifies a variable in a definition or in an expression. (It is a "postfix operator"
because it comes after, "post", the thing it fixes!) In a definition (or declaration), the []
operator changes the preceding variable from being a simple variable into an array. In
an expression, the [] operator changes the variable reference from meaning the array as
a whole to meaning a chosen element of the array. (Usually, you can only access a
single array element at a time, the element being chosen using indexing. But, there are
a few places where we can use an array as a whole thing rather than just using a selected
element; these will be illustrated shortly.)
Definitions of variables of the same basic type can include both array definitions and
simple variable definitions:
This defines three simple data variables (xmax , ymax , and zmax ), and three arrays
(xcoord, ycoord, and zcoord)each with 50 elements.
Arrays can be defined as local, automatic variables that belong to functions: Local and global
arrays
int main()
{
double heights[MAXCHILDREN]; Local (automatic)
…
288 Arrays
But often you will have a group of functions that need to share access to the same array
data. In such situations, it may be more convenient to define these shared arrays as
"global" data:
Variables declared outside of any function are "globals". They can be used in all the
other functions in that file. In fact, in some circumstances, such variables may be used
by functions in other files.
static qualifier You can arrange things so that such arrays are accessible by the functions in one file
while keeping them from being accessed by functions in other files. This "hiding" of
arrays (and simple variables) is achieved using the static qualifier:
Here, static really means "file scope" – restrict access to the functions defined in this
file. This is an unfortunate use of the word static, which C and C++ also use to mean
other things in other contexts. It would have been nicer if there were a separate
specialized "filescope" keyword, but we must make do with the way things have been
defined.
Caution on using Some of you will have instructors who are strongly opposed to your use of global
"globals" and "file data, and even of file scope data. They have reasons. Global data are often a source of
scope" variables
problems in large programs. Because such variables can be accessed from almost
anywhere in the program there is no control on their use. Since these data values can be
changed by any function, it is fairly common for errors to occur when programmers
Defining one dimensional arrays 289
writing different functions make different assumptions about the usage of these
variables.
However for small programs, file scope data are safe, and even global data are
acceptable. (Small programs are anything that a single programmer, scientist, or
engineer can create in a few weeks.) Many of the simple programs illustrated in the
next few chapters will make some use of file scope and/or global data. Different ways
of structuring large programs, introduced in Parts IV and V, can largely remove the
need for global variables.
Arrays are used mainly for data that are entered at run time or are computed. But
sometimes, you need arrays that are initialized with specific data. Individual data
elements could be initialized using the = operator and a given value:
long count = 0;
long min = LONG_MAX;
long max = LONG_MIN;
Of course, if we are to initialize an array, we will normally need an initial value for each
element of the array and we will need some way of showing that these initializing
values form a group. The grouping is done using the same { begin and } end brackets
as are used to group statements into compound statements. So, we can have initialized
arrays like:
The various initializing values are separated by commas. The } end bracket is
followed by a semicolon. This is different from the usage with compound statements
and function definitions; there you don't have a semicolon. Inevitably, the difference is
a common source of minor compilation errors because people forget the semicolon after
the } in an array initialization or wrongly put one in after a function definition.
Generally, if an array has initial values assigned to its elements then these are going
to be constants. Certainly, the marks and letter grades aren't likely to be changed when
the program is run. Usually, initialized arrays are const:
290 Arrays
You don't have to provide initial values for every element of an array. You could
have a definition and initialization like the following:
Here, things[0], things[1], and things[2] have been given explicit initial values;
the remaining seven things would be initialized to zero or left uninitialized.
(Generally, you can only rely on zero initialization for global and file scope variables.)
There aren't any good reasons why you should initialize only part of an array, but it is
legal. Of course, it is an error to define an array with some specified number of
elements and then try to use more initial values than will fit!
Naturally, there are short cuts. If you don't feel like counting the number of array
elements, you don't have to. An array can be defined with an initialization list but no
size in the []; for example:
The compiler can count four initialization values and so concludes that the array is
mystery[4].
Seems crazy? It is quite useful when defining arrays containing entries that are
character strings, for example a table of error messages. Examples are given in 11.7.
11.3.1 Histogram
Lets get back to statistics on childrens' heights. This time we aren't interested in gender
specific data but we want to produce a simple histogram showing the distribution of
heights and we want to have listed details of the tallest and smallest 5% of children in
the sample.
Specification:
1. The program is to read height and gender values from the file heights.dat. The
height values should be in the range 100 … 200, the gender tags should be m or f.
The gender tag information should be discarded. The data set will include records
for at least ten children; averages and standard deviations will be defined (i.e. no
need to check for zero counts).
Example: histogram 291
3. When the terminating sentinel data record is read, the program should print the
following details: total number of children, average height, standard deviation in
height.
100 |
110 |*
120 |***
130 |*******************
…
5. Finally, the program is to list all heights more than two standard deviations from
the mean.
Program design
The program breaks down into the following phases: Preliminary design
Each of these is a candidate for being a separate function (or group of functions).
The main data for the program will consist of an array used to store the heights of Decision in favour of
the children, and a count that will define the number of children with records in the data "shared" data
file. The array with the heights, and the count, will be regarded as data that are shared
by all the functions. This will minimize the amount of data that need to be passed
between functions.
The suggested functions must now be elaborated: First iteration
through design
get_data
initialization – zeroing of some variables, opening file
loop reading data, heights saved, gender-tags discarded
mean
use shared count and heights array,
loop summing values
calculate average
292 Arrays
std_dev
needs mean, uses shared count and heights array
calculate sum of squares etc and calculate
standard deviation
histogram
use shared count and heights array, also need
own array for the histogram
zero out own array
loop through entries in main array identifying
appropriate histogram entry to be incremented
loop printing histogram entries
extreme_vals
needs to be given average and standard deviation
uses shared count and heights array
main
get_data
m = mean(), print m (+ labels and formatting)
sd = std_dev(), print sd
histogram
extreme_vals(m,sd)
Second iteration, The "get_data" and "histogram" functions both appear to be moderately elaborate
further and so they become candidates for further analysis. Can these functions be simplified
decomposition into
simpler functions by breaking them down into subfunctions?
Actually, we will leave the histogram function (though it probably should be further
decomposed). Function getdata really involves two separate activities – organizing file
input, and then the loop actually reading the data. So we can further decompose into:
open_file
try to open named file,
if fail terminate program
read_data
read first data (height and gender tag)
while not sentinel value
save height data
read next data
get_data
initialize count to zero
call open_file
call read_data
The ifstream used for input from file will have to be another shared data element
because it is used in several functions.
Example: histogram 293
The final stage before coding involves definition of i) constants defining sizes for Third iteration,
arrays, ii) shared variables, and iii) the function prototypes. finalize shared data,
derive function
prototypes
const int MAXCHILDREN = 500;
void open_file(void);
void read_data(void);
void get_data(void);
double mean(void);
void histogram(void);
int main();
The shared data have been defined as static. As this program only consists of one
source file, this "file scoping" of these variables is somewhat redundant. It has been
done on principle. It is a way of re-emphasising that "these variables are shared by the
functions defined in this file". The names have also been selected to reflect their role; it
is wise to have a naming scheme with conventions like 's_' at the start of the name of a
static, 'g_' at the start of the name for a global.
Implementation
void read_data(void)
{
…
}
open_file() The open_file() routine would specify the options ios::in | ios::nocreate,
the standard options for a data input file, in the open() call. The ifstream should then
be checked to verify that it did open; use exit() to terminate if things went wrong.
void open_file(void)
{
s_input.open("heights.dat",ios::in | ios::nocreate);
if(!s_input.good()) {
cout << "Couldn't open the file. Giving up."
<< endl;
exit(1);
}
return;
}
read_data() The read_data() routine uses a standard while loop to read and process input data
marked by a terminal sentinel record, (i.e. read first record; while not sentinel record {
process record, read next record }). The data get copied into the array in the statement
s_heights[s_count] = h;.
void read_data(void)
{
double h;
char gender_tag;
if(s_count == MAXCHILDREN)
cout << "Ignoring some "
"data records" << endl;
s_count++;
s_input >> h >> gender_tag;
}
if(s_count>MAXCHILDREN) { Report if data
cout << "The input file contained " << s_count discarded
<< " records." << endl;
cout << "Only the first " << MAXCHILDREN <<
" will be processed." << endl;
s_count = MAXCHILDREN;
}
s_input.close();
}
Note the defensive programming needed in read_data(). The routine must deal
with the case where there are more entries in the file than can be stored in the array. It
can't simply assume that the array is large enough and just store the next input in the
next memory location because that would result in other data variables being
overwritten. The approach used here is generally suitable. When the array has just
become full, a warning gets printed. When the loop terminates a more detailed
explanation is given about data elements that had to be discarded.
Because it can rely on auxiliary functions to do most of its work, function get_data() get_data()
itself is nicely simple:
void get_data(void)
{
s_count = 0;
open_file();
read_data();
}
The mean() and std_dev() functions both involve simple for loops that mean() and std_dev()
accumulate the sum of the data elements from the array (or the sum of the squares of
these elements). Note the for loop controls. The index is initialized to zero. The loop
terminates when the index equals the number of array elements. This is required
because of C's (and therefore C++'s) use of zero based arrays. If we have 5 elements to
process, they will be in array elements [0] ... [4]. The loop must terminate before we
try to access a sixth element taken from [5].
double mean(void)
{
double sum = 0.0;
for(int i=0; i<s_count; i++)
sum += s_heights[i];
return sum / s_count;
}
296 Arrays
histogram() Function histogram() is a little more elaborate. First, it has to have a local array.
This array is used to accumulate counts of the number of children in each 10cm height
range.
Assumptions about the particular problem pervade the routine. The array has ten
elements. The scaling of heights into array indices utilizes the fact that the height
values should be in the range 100 < height < 200. The final output section is set to
generate the labels 100, 110, 120, etc. Although quite adequate for immediate
purposes, the overall design of the routine is poor; usually, you would try to create
something a little more general purpose.
Note that the local array should be zeroed out. The main array of heights was not
initialized to zero because the program stored values taken from the input file and any
previous data were just overwritten. But here we want to keep incrementing the counts
in the various elements of the histo[] array, so these counts had better start at zero.
The formula that converts a height into an index number is:
Say the height was 172.5, this should go in histo[7] (range 170 ... 179); (172.5-
100)/10 gives 7.25, the floor() function converts this to 7.
The code uses assert() to trap any data values that are out of range. The
specification said that the heights would be in the range 100 … 200 but didn't promise
that they were all correct. Values like 86 or 200 would result in illegal references to
histo[-2] or to histo[10] . If the index is valid, the appropriate counter can be
incremented ( histo[index] ++ ; – this applies the ++, increment operator, to the
contents of histo[index]).
Nested for loops Note the "nested" for loops used to display the contents of the histo[] array. The
outer loop, with index i , runs through the array elements; printing a label and
terminating each line. The inner loop, with the j index, prints the appropriate number
of stars on the line, one star for each child in this height range. (If you had larger data
sets, you might have to change the code to print one star for every five children.)
void histogram(void)
{
int histo[10];
Initialize elements of for(int i = 0; i < 10; i++)
local array to zero histo[i] = 0;
Example: histogram 297
The function that identifies examples with extreme values of height simply involves
a loop that works through successive entries in the heights[] array. The absolute
value of the difference between a height and the mean is calculated and if this value
exceeds twice the standard deviation details are printed.
Note the introduction of a local variable set to twice the standard deviation. This
avoids the need to calculate sdev*2.0 inside the loop. Such a definition is often not
needed. If two_std_devs was not defined, the test inside the loop would have been
if(diff > sdev*2.0) … . Most good compilers would have noticed that sdev*2.0
could be evaluated just once before the loop; the compiler would have, in effect,
invented its own variable equivalent to the two_std_devs explicitly defined here.
Because the real work has been broken down amongst many functions, the main()
function is relatively simple. It consists largely of a sequence of function calls with just
a little code to produce some of the required outputs.
298 Arrays
int main()
{
get_data();
double avg = mean();
double sd = std_dev(avg);
cout << "Number of records processed : " << s_count <<
endl;
cout << "Mean (average) : " << avg << endl;
cout << "Standard deviation :" << sd << endl;
histogram();
extreme_vals(avg, sd);
return 0;
}
The plotting program, presented in section 10.11.2, can be simplified through the use of
an array.
We can use an array to hold the characters that must be printed on a line. Most of
the characters will be spaces, but the mid-point character will be the '|' axis marker and
one other character will normally be a '*' representing the function value.
The changes would be in the function PlotFunctionValue(). The implementation
given in section 10.11.2 had to consider a variety of special cases – the '*' on the axis,
the '*' left of the axis, the '*' to the right of the axis. All of these special cases can be
eliminated. Instead the code will fill an array with spaces, place the '|' in the central
element of the array, then place the '*' in the correct element of the array; finally, the
contents of the array are printed. (Note the -1s when setting array elements to
compensate for the zero-based array indexing.)
return;
}
The functions illustrated so far have only had simple variables as arguments and results.
What about arrays, can arrays be used with functions?
Of course, you often need array data that are manipulated in different functions. The Limitations of shared
histogram example, 11.3.1, had a shared filescope (static) array for the heights and this global and filescope
data
was used by all the functions. But use of filescope, or global, data is only feasible in
certain circumstances. The example in 11.3.1 is typical of where such shared data is
feasible. But look at the restrictions. There is only one array of heights. All the
functions are specifically designed to work on this one array.
But, generally, you don't have such restrictions. Suppose we had wanted histograms
for all children, and also for boys and girls separately? The approach used in 11.3.1
would have necessitated three different variations on void histogram(void); there
would be c_histogram() that worked with an array that contained details of the
heights of all children, b_histogram() – a version that used a different filescope array
containing the heights for boys, and g_histogram() – the corresponding function for
processing the heights of girls. Obviously, this would be unacceptably clumsy.
Usually, you will have many arrays. You will need functions that must be
generalized so that they can handle different sets of data, different arrays. There has to
be a general purpose histogram() function that, as one of its arguments, is told which
array contains the data that are to be displayed as a histogram. So, there needs to be
some way of "passing arrays to functions".
But arrays do present problems. A C (C++) array really is just a block of memory. Reasons for
When you define an array you specify its size (explicitly as in x_coord[100] or restrictions on arrays
as arguments to
implicitly as in grades[] = { 'f', 'e', 'd', 'c', 'b', 'a' }; ), but this size functions
information is not "packaged" with the data. Arrays don't come with any associated
data specifying how many elements they have. This makes it more difficult for a
compiler to generate instructions for functions that need argument arrays passed by
value (i.e. having a copy of the array put onto the stack) or that want to return an array.
A compiler might not be able to determine the size of the array and so could not work
out the amount of space to be reserved on the stack, nor the number of bytes that were
to be copied.
300 Arrays
No arrays as value The authors of the first C compilers decided on a simple solution. Arrays can't be
arguments or as passed by value. Functions can not return arrays as results. The language is slightly
results
restricted, but the problems of the compiler writer have gone. What is more, a
prohibition on the passing of arrays as value arguments or results preemptively
eliminated some potentially inefficient coding styles. The copying operations involved
in passing array arguments by value would have been expensive. The doubling of the
storage requirement, original plus copy on the stack, could also have been a problem.
This decision restricting the use of arrays with functions still stands. Which makes
C, and therefore C++, slightly inconsistent. Simple variables, struct types (Chapter 16),
and class instances (Chapter 19) can all be passed by value, and results of these type can
be returned by functions. Arrays are an exception.
Arrays are always When an array is passed as an argument to a function, the calling routine simply
passed by reference passes the address of the array; i.e. the caller tells the called function where to find the
array in memory. The called function then uses the original array. This mechanism,
where the caller tells the called function the address of the data, is called pass by
reference.
As well as passing the array, the calling function (almost always) has to tell the
called function how many elements are in the array. So most functions that take arrays
are going to have prototypes like:
For example, we could take the mean() function from 11.3.1 and make it more useful.
Instead of
we can have
This version could be used any time we want to find the mean of the values in an array
of doubles. Note, the array argument is specified as an "open array" i.e. the []
contains no number.
int main()
{
double heights[100];
double h;
int n = 0;
cout << "Enter heights:" << endl;
cin >> h; // Not checking anything here
while(h > 0.0) {
heights[n] = h;
n++;
cin >> h;
}
double m = mean(heights, n); Function call with
array as argument
cout << "The mean is " << m << endl;
return 0;
}
Figure 11.1 illustrates the stack during a call to mean(). The hexadecimal numbers,
e.g. , shown are actual addresses when the program was run on a particular machine.
By convention, data addresses are always shown in hexadecimal ("hex").
The instruction sequence generated for the call m = mean(heights, n) would be
something like:
Address
170 heights[0] 0xeffff390
"Stack frame" for int 144
main() 130
136
162
Address argument on
stack specifies start
of array
0 heights[99]
162 h
5 n
0xeffff390
"Stack frame" for Arguments values on stack
5
double mean() Space left for result
Housekeeping info
860 sum
"Stack Frame"
register holds
this address
Figure 11.1 Stack with when calling double mean(double data_array[], int
numitems).
This code uses the array identified by its address as provided by the calling routine.
Typically, it would hold this "base" address in one of the CPU's address registers. Then
when array elements have to be accessed, the code works out the offset relative to this
base address to get the actual address of a needed element. The data are then fetched
from the memory location whose address has just been determined. (The bit about
"multiplying an integer index value by 8" is just part of the mechanics of converting the
index into a byte offset as needed by the hardware addressing mechanisms of a
machine. Programmers may think in terms of indexed arrays, machines have to use
Arrays as arguments to functions 303
byte addresses so these have to be calculated. The value of the 'multiplier', the 8 in this
code, depends on the type of data stored in the array; for an array of longs, the
multiplier would probably be 4. The operation wouldn't actually involve a
multiplication instruction, instead the required effect would be achieved using
alternative quicker circuits.)
The function mean() doesn't change the data array it is given; it simply reads values
from that array. However, functions can change the arrays that they work on. So you
could have the function:
If called with the heights[] array, this function would change all the values (or, at
least, the first n values) in that array. (Coding style: no return – that is OK in a void
function, though I prefer a function to end with return; the smart h[i] /= 2.54; – it
is correct, I still find it easier to read h[i] = h[i] / 2.54.)
Function mean() treats its array argument as "read only" data; function
change_cm_to_inches() scribbles all over the array it was given. That is a
substantial difference in behaviour. If you are going to be calling a function and giving
it an array to work with, you would usually like to know what is going to happen to the
contents of that array.
C++ lets you make this distinction explicit in the function prototype. Since mean()
doesn't change its array, it regards the contents as – well constant. So, it should state
this in its prototype:
As well as appearing in function declarations, this const also appears where the
function is defined:
The compiler will check the code of the function to make certain that the contents of the
array aren't changed. Of course, if you have just declared an argument as const, it is
unlikely that you will then write code to change the values.
The main advantage of such const (and default non const) declarations is for
documenting functions during the design stage. In larger projects, with many
programmers, each programmer works on individual parts and must make use of
functions provided by their colleagues. Usually, all they see of a function that you have
written is the prototype that was specified during design. So this prototype must tell
them whether an array they pass to your function is going to be changed.
Sometimes you want arrays of characters, like the grade symbols used in one of the
examples on array initialization:
The example in 11.8.2 uses a character array in a simple letter substitution approach to
the encryption of messages. If you were writing a simple word processor, you might
well use a large array to store the characters in the text that you were handling.
Most arrays of characters are slightly specialized. Most arrays of character are used
to hold "strings" – these may be data strings (like names of customers), or prompt
messages from the program, or error messages and so forth. All these strings are just
character arrays where the last element (or last used element) contains a "null"
character.
"null character" The null character has the integer value 0. It is usually represented in programs as
'\0'. All the standard output routines, and the string library discussed below, expect to
work with null terminated arrays of characters. For example, the routine inside the
iostream library that prints text messages uses code equivalent to the following:
(The real code in the iostream library uses pointers rather than arrays.) Obviously, if
the message msg passed to this function doesn't end in a null character, this loop just
keeps printing out the contents of successive bytes from memory. Sooner or later, you
are going to make a mistake and you will try to print a "string" that is not properly
terminated; the output just fills up with random characters copied from memory.
You can define initialized strings:
Strings 305
The compiler checks the string and determines the length, e.g. "Write … disk." is 20
characters. The compiler adds one for the extra null character and, in effect, turns the
definition into the following:
If you want string data, rather than string constants, you will have to define a
character array that is going to be large enough to hold the longest string that you use
(you have to remember to allow for that null character). For example, suppose you
needed to process the names of customers and expected the maximum name to be thirty
characters long, then you would define an array:
char customername[31];
(Most compilers don't like data structures that use an odd number of bytes; so usually, a
size like 31 gets rounded up to 32.)
You can initialize an array with a string that is shorter than the specified array size:
The compiler arranges for the first nine elements to be filled with the character
constants 'U', 'n', …, 'e', 'd', and '\0'; the contents of the remaining 31 elements are not
defined.
The contents of the character array containing a string, e.g. filename, can be printed
with a single output statement:
(The compiler translates "cout takes from character array" into a call to a
PrintString() function like the one illustrated above. Although filename has 40
characters, only those preceding the null are printed.)
306 Arrays
cin >> character The input routines from the iostream library also support strings. The form "cin
array gives to character array", e.g.:
is translated into a call to a ReadString() function which will behave something like
the following code:
The input routine skips any leading "whitespace" characters (not just the ' ' space
character, whitespace actually includes things like tab characters and newlines). After
all leading whitespace has been consumed, the next sequence of characters is copied
into the destination target array. Copying continues until a whitespace character is read.
Caution, area where Of course there are no checks on the number of characters read! There can't be. The
bugs lurk compiler can't determine the size of the target array. So, it is up to the programmer to
get the code correct. An array used in an input statement must be large enough to hold
the word entered. If the target array is too small, the extra characters read just overwrite
other variables. While not a particularly common error, this does happen; so be
cautious. (You can protect yourself by using the setw() "manipulator". This was
illustrated in section 9.7 where it was shown that you could set the width of an output
field. You can also use setw() to control input; for example, cin >> setw(5) >>
data will read 4 characters into array data, and as the fifth character place a '\0'.)
The iostream library code is written with the assumption that any character array
filled in this way is going to be used as a string. So, the input routine always places a
null character after the characters that were read.
getline() With "cin >> array", input stops at the first white space character. Sometimes, it is
necessary to read a complete line containing several words separated by spaces. There
is an alternative input routine that can be used to read whole lines. This routine,
Strings 307
getline() , is part of the repertoire of things that an istream object can do. Its
prototype is (approximately):
Function getline() is given the name of the character array that should hold the input
data, a limit on the number of characters to read, and a delimiter character. A call to
getline() has a form like:
(read this as "cin object: use your getline function to fill the array target with up to 39
characters of input data").
The delimiter character can be changed. Normally, getline() is used to read
complete lines and the delimiter should be the '\n' character at the end of the line. But
the function can be used to read everything up to the next tab character (use '\t' as the
delimiter), or any other desired character. (Caution. The editors used with some
development environments use '\r' instead of '\n'; if you try reading a line from a text
data file you may not find the expected '\n'.)
Characters read from input are used to fill the target array. Input stops when the
delimiter character is read (the delimiter character is thrown away, it doesn't get stored
in the array). A null character is placed in the array after the characters that were read.
The integer limit specifies the maximum number of characters that should be read.
This should be one less than the size of the target array so as to leave room for the null
character. Input will stop after this number of characters have been read even though
the specified delimiter has not been encountered. If input stops because this limit is
reached, all unread characters on the input line remain in the input buffer. They will
probably confuse the next input operation! (You can clear left over characters by using
cin.ignore() whose use was illustrated earlier in section 8.2.)
There is a standard library of routines for manipulating strings. The header file string.h string.h
contains declarations for all the string functions. Some of the more commonly used
functions are:
(These aren't the exact function prototypes given in the string.h header file! They have
been simplified to use only the language features covered so far. The return types for
some functions aren't specified and character arrays are used as arguments where the
actual prototypes use pointers. Despite these simplifications, these declarations are
"correct".)
You can't assign The following code does not compile:
arrays:
char baskin_robertson[50]; // Famous for ice creams
cout << "Which flavour? (Enter 1..37) : ";
int choice;
cin >> choice;
switch(choice) {
case 1: baskin_robertson = "strawberry"; break;
case 2: baskin_robertson = "chocolate"; break;
…
case 37: baskin_robertson = "rum and raisin"; break;
default: baskin_robertson = "vanilla"; break;
}
If you try to compile code like that you will get some error message (maybe "lvalue
required"). In C/C++, you can not do array assignments of any kind, and those "cases"
in the switch statement all involve assignments of character arrays.
You can't do array assignments for the same reason that you can't pass arrays by
value. The decision made for the early version of C means that arrays are just blocks of
memory and the compiler can not be certain how large these blocks are. Since the
compiler does not know the size of the memory blocks, it can not work out how many
bytes to copy.
strcpy() However, you do often need to copy a string from some source (like the string
constants above) to some destination (the character array baskin_robertson[]). The
function strcpy() from the string library can be used to copy strings. The code given
above should be:
switch(choice) {
case 1: strcpy(baskin_robertson, "strawberry"); break;
…
default: strcpy(baskin_robertson, "vanilla"); break;
}
Function strcpy() takes two arguments, the first is the destination array that gets
changed, the second is the source array. Note that the function prototype uses the
Strings 309
The strcat() function appends ("catenates") the source string onto any existing strcat()
string in the destination character array. Once again it is your responsibility to make
sure that the destination array is large enough to hold the resulting string. An example
use of strcat() is:
to:
Function strncat() works similarly but like strncpy() it transfers at most the
specified number of characters.
Function strlen() counts the number of characters in the string given as an strlen()
argument (the terminating '\0' null character is not included in this count).
These functions compare complete strings (strcmp()) or a specified number of strcmp() and
characters within strings (strncmp()). These functions return 1 if the first string (or strncmp()
310 Arrays
first few characters) are "greater" than the second string, 0 if the strings are equal, or -1
if the second string is greater than the first.
Collating sequence of The comparison uses the "collating" sequence of the characters, normally this is just
characters their integer values according to the ASCII coding rules:
32: ' ' 33: '!' 34: '"' 35: '#' 36: '$' 37: '%'
38: '&' 39: ''' 40: '(' 41: ')' 42: '*' 43: '+'
44: ',' 45: '-' 46: '.' 47: '/' 48: '0' 49: '1'
50: '2' 51: '3' 52: '4' 53: '5' 54: '6' 55: '7'
56: '8' 57: '9' 58: ':' 59: ';' 60: '<' 61: '='
62: '>' 63: '?' 64: '@' 65: 'A' 66: 'B' 67: 'C'
…
86: 'V' 87: 'W' 88: 'X' 89: 'Y' 90: 'Z' 91: '['
92: '\' 93: ']' 94: '^' 95: '_' 96: '`' 97: 'a'
…
…
should print -1, i.e. msg1 is less than msg2 . The H's at position 0 are equal, but the
strings differ at position 1 where msg2 has i (value 105) while msg1 has e (value 101).
ctype Another group of functions, that are useful when working with characters and
strings, have their prototypes defined in the header file ctype.h These functions include:
You can have higher dimensional arrays. Two dimensional arrays are common. Arrays
with more than two dimensions are rare.
Two dimensional arrays are used:
Multidimensional arrays 311
Salesperson
5
Week
1 12 6 10 9 23
2 9 11 12 4 18
3 13 9 8 7 19
4 8 4 6 6 13
5 3 2 7 7 15
6 9 11 13 10 19
7 0 0 0 0 0
or something like light intensities in a grey scale image (usually a very large 1000x1000
array):
Although two dimensional arrays are not particularly common in computing science
applications, they are the data structures that are most frequently needed in scientific
and engineering applications. In computing science applications, the most common two
dimensional arrays are probably arrays of fixed length strings (since strings are arrays
of characters, an array of strings can be regarded as a two dimensional array of
characters).
In C (C++) two dimensional arrays are defined by using the [] operator to specify
each dimension, e.g:
char screen[25][80];
double datatable[7][11];
double t_matrix[4][4];
The first [] defines the number of rows; the second defines the number of columns. So
in the example, screen[][] is a two dimensional array of characters that could be used
to record the information displayed in each character position of a terminal screen that
had 25 rows each with 80 columns. Similarly, datatable[][] is an array with 7 rows
each of 11 columns and t_matrix[][] is a square 4x4 array.
As in the case of one dimensional arrays, the array indices start at zero. So, the top-
left corner of the screen[][] array is screen[0][0] and its bottom right corner is
screen[24][79].
Two dimensional arrays can be initialized. You give all the data for row 0 (one
value for each column), followed by all the data for row 1, and so on:
double mydata[4][5] = {
{ 1.0, 0.54, 0.21, 0.11, 0.03 },
{ 0.34, 1.04, 0.52, 0.16, 0.09 },
{ 0.41, 0.02, 0.30, 0.49, 0.19 },
{ 0.01, 0.68, 0.72, 0.66, 0.17}
};
Multidimensional arrays, definition and initialization 313
You don't have to put in the internal { begin and } end brackets around the data for
each individual row. The compiler will understand exactly what you mean if you write
that initialization as:
double mydata[4][5] = {
1.0, 0.54, 0.21, 0.11, 0.03, 0.34, 1.04, 0.52, 0.16, 0.09,
0.41, 0.02, 0.30, 0.49, 0.19, 0.01, 0.68, 0.72, 0.66, 0.17
};
However, leaving out the brackets around the row data is likely to worry human readers
– so put the { } row bracketing in!
With one dimensional arrays, you could define an initialized array and leave it to the
compiler to work out the size (e.g. double vec[] = { 2.0, 1.5, 0.5, 1.0 };). You
can't quite do that with two dimensional arrays. How is the compiler to guess what you
want if it sees something like:
double guess[][] = {
1.0, 0.54, 0.21, 0.11, 0.03, 0.34, 1.04, 0.52, 0.16, 0.09,
0.41, 0.02, 0.30, 0.49, 0.19, 0.01, 0.68, 0.72, 0.66, 0.17
};
You could want a 4x5 array, or a 5x4 array, or a 2x10 array. The compiler can not
guess, so this is illegal.
You must at least specify the number of columns you want in each row, but you can
leave it to the compiler to work out how many rows are needed. So, the following
definition of an initialized array is OK:
double mydata[][5] = {
{ 1.0, 0.54, 0.21, 0.11, 0.03 },
{ 0.34, 1.04, 0.52, 0.16, 0.09 },
{ 0.41, 0.02, 0.30, 0.49, 0.19 },
{ 0.01, 0.68, 0.72, 0.66, 0.17}
};
The compiler can determine that mydata[][5] must have four rows.
At one time, you could only initialize aggregates like arrays if they were global or
filescope. But that restriction has been removed and you can initialize automatic arrays
that are defined within functions (or even within blocks within functions).
Individual data elements in a two dimensional array are accessed by using two []
operators; the first [] operator picks the row, the second [] operator picks the column.
Mostly, two dimensional arrays are processed using nested for loops:
314 Arrays
Quite often you want to do something like initialize a square array so that all off-
diagonal elements are zero, while the elements on the diagonal are set to some specific
value (e.g. 1.0):
double t_matrix[4][4];
…
for(int i = 0; i < 4; i++) {
for(int j = 0; j < 4; j++)
t_matrix[i][j] = 0.0;
t_matrix[i][i] = 1.0; // set element on diagonal
}
The "cost" of These "cost" of these double loops is obviously proportional to the product of the
calculations number of rows and the number of columns. As you get to work with more complex
algorithms, you need to start estimating how much each part of your program will
"cost".
The idea behind estimating costs is to get some estimate of how long a program will
run. Estimates can not be very precise. After all, the time a program takes run depends
on the computer (120MHz Pentium based machine runs a fair sight faster than 15MHz
386). The time that a program takes also depends on the compiler that was used to
compile the code. Some compilers are designed to perform the compilation task
quickly; others take much longer to prepare a program but, by doing a more detailed
compile time analysis, can produce better code that results in a compiled program that
may run significantly faster. In some cases, the run time of a program depends on the
actual data values entered.
Since none of these factors can be easily quantified, estimates of how long a
program will take focus solely on the number of data elements that must be processed.
For some algorithms, you need a reasonable degree of mathematical sophistication in
order to work out the likely running time of a program implementing the algorithm.
These complexities are not covered in this book. But, the estimated run times will be
described for a few of the example programs.
These estimates are given in a notation known as "big-O". The algorithm for
initializing an MxN matrix has a run time:
Such run time estimates are independent of the computer, the compiler, and the specific
data that are to be processed. The estimates simply let you know that a 50x50 array is
going to take 25 times as long to process as a 10x10 array.
One of the most frequent operations in scientific and engineering programs has a
cost O(N 3 ). This operations is "matrix multiplication". Matrix multiplication is a basic
step in more complex mathematical transforms like "matrix inversion" and "finding
eigenvalues". These mathematical transforms are needed to solve many different
problems. For example, you have a mechanical assembly of weights and springs you
can represent this using a matrix whose diagonal elements have values related to the
weights of the components and whose off diagonal elements have values that are
determined by the strengths of the springs joining pairs of weights. If you want to
know how such an assembly might vibrate, you work out the "eigenvalues" of the
matrix; these eigenvalues relate to the various frequencies with which parts of such an
assembly would vibrate.
Restricting consideration to square matrices, the "formula" that defines the product
of two matrices is:
∑ M a0i * bi 0 ∑
M
a0i * bi1 … … … ∑
M
a0i * biM
i=0 i=0 i=0
M
∑ i = 0 a1i * bi 0 … … … … …
… … … … … …
=
… … … … … …
… … … … … …
M
∑ i = 0 aMi * bi 0 ∑i = 0 aMi * biM
M
… … … …
(These are "NxN" arrays with subscripts running from 0 … M, where M = N - 1.).
Each element in the product matrix is the sum of N product terms; these product terms
combine an element from a row in the first array and an element from a column in the
second array. For example, if N = 6, then entry product[2][3] is given by:
(The variable prdct is introduced as a minor optimization. The code would be slower
if you had product[row][col] += a[row][i] * b[i][col] because in this form it
would be necessary to reevaluate the subscripts each time it needed to update
product[row][col].)
The O(N3 ) cost of this triple loop code means that if you had a program that
involved mainly matrix multiplication that ran in one minute for a data set of size 10,
then you would need more than two hours to process a data set of size 50.
Another trap for Just a warning, you must use two separate [] operators when accessing an element in
Pascal programmers a two dimensional array. Now in languages like Pascal, elements of two dimensional
arrays are accessed using square brackets surrounding a list of array indices separate by
commas:
double d[10][10];
…
…
… d[ i, j] …
That form d[ i, j] will get past the compiler. But, like several earlier examples of
legal but freaky constructs, its meaning is odd.
As explained in section 7.7, a comma separated list can be used to define a set of
expressions that must be evaluated in sequence. So i, j means "evaluate i, e.g.
getting the value 4, and throw this value away, then evaluate j, eg. getting the value 9,
and use this value as the final value for the complete expression".
Multidimensional arrays, accessing individual elements 317
Consequently,
d[ 4, 9]
d[9]
What is d[9]?
The expression d[9] means the tenth row of the d[][] array (tenth because row 0
is the first row so row 9 is the tenth row).
In C++ it is legal to refer to a complete row of an array. There aren't many places
where you can do anything with a complete row of an array, but there are a few.
Consequently, d[9] is a perfectly legal data element and the compiler would be happy if
it saw such an expression.
Now, if you do accidentally type d[ i, j] instead of d[i][j] you are going to end up
getting an error message from the compiler. But it won't be anything like "You've
messed up the array subscripts again.". Instead, the error message will be something
like "Illegal operand type." Your code would be something like: double t = 2.0*
d[i,j];. The compiler would see 2.0*… and expect to find a number (an int, or a long,
or a double). It wouldn't be able to deal with "d[j] row of array" at that point and so
would thing that the data type was wrong.
Where might you use an entire row of an array?
There aren't many places where it is possible (and even fewer where it is sensible). Using a row of a two
But you could do something like the following. Suppose you have an array dimensional array
representing the spreadsheet style data shown at the start of this section. Each row
represents the number of sales made by each of a set of salespersonnel in a particular
week. You might want the average sales, and have a function that finds the average of
the elements of an array:
double salesfigures[20][10];
…
// Get average sales in week j
cout << "Average for this week " <<
average(salesfigures[j], 10);
…
The average() function can be given a complete row of the array as shown in this code
fragment.
While this is technically possible, it really isn't the kind of code you want to write. It
is "clever" code using the language to its full potential. The trouble with such code is
that almost everyone reading it gets confused.
318 Arrays
The example just shown at the end of the preceding section illustrated (but discouraged)
the passing of rows of a two-dimensional array to a function that expected a one
dimensional array. What about passing the entire array to a function that wants to use a
two dimensional array?
This is feasible, subject to some minor restrictions. You can't have something like
the following:
The idea would be to have a PrintMatrix() function that could produce a neatly
formatted printout of any two dimensional array that it was given – hence the "doubly
open" array d[][].
The trouble is that the compiler wouldn't be able to work out the layout of the array
and so wouldn't know where one row ended and the next began. Consequently, it
wouldn't be able to generate code to access individual array elements.
You can pass arrays to functions, but the function prototype has to specify the
number of columns in the arrays that it can deal with:
The compiler will check calls made to PrintMatrix() to make certain that if you do
call this function then any array you pass as an argument has five columns in each row.
We may have a program that needs to use lots of "Messages", which for this example
can be character arrays with 30 characters. We may need many of the Message things
to hold different strings, and we may even need arrays of "Messages" to hold
collections of strings. Before illustrating examples using arrays of strings, it is worth
introducing an additional feature of C++ that simplifies the definition of such variables
Typedefs C and C++ programs can have "typedef" definitions. Typedefs don't introduce a
new type; they simply allow the programmer to introduce a synonym for an existing
type. Thus, in types.h header used in association with several of the libraries on Unix
you will find typedefs like the following:
The type daddr_t is meant to be a "disk address"; a pid_t is a "process type". They
are introduced so that programmers can specify functions where the prototype conveys
a little bit more about the types of arguments expected, e.g.:
The fact that where is specified as being a daddr_t makes it a little clearer that this
function needs to be given a disk location.
Variables of these "types" are actually all long integers. The compiler doesn't mind
you mixing them:
time_t a = 3;
pid_t b = 2;
id_t r = a*b;
cout << r << endl;
So these typedef "types" don't add anything to the compiler's ability to discriminate
different usage of variables or its ability to check program correctness.
However, typedefs aren't limited to defining simple synonyms, like alternative
words for "long". You can use typedefs to introduce synonyms for derived types, like
arrays of characters:
This typedef makes " Message" a synonym for the type char … [30] (i.e. the type that
specifies an array of 30 characters). After this typedef has been read, the compiler will
accept the definition of variables of type Message:
Message m1;
Message error_msgs[10]:
Message errors[] = {
"Disk full",
"Disk write locked",
320 Arrays
"Write error",
"Can't overwrite file",
"Address error",
"System error"
};
(Each Message in the array errors[] has 30 characters allocated; for most, the last ten
or more characters are either uninitialized or also to '\0' null characters because the
initializing strings are shorter than 30 characters. If you try to initialize a Message with
a string longer than 30 characters the compiler will report something like "Too many
initializers".)
As far as the compiler is concerned, these Messages are just character arrays and so
you can use them anywhere you could have used a character array:
#include <iostream.h>
#include <string.h>
Message m1;
Message m2 = "Elvis lives in Wagga Wagga";
Message errors[] = {
"Disk full",
"Disk write locked",
"Write error",
"Can't overwrite file",
"Address error",
"System error"
};
int main()
{
strlen() and Message cout << strlen(m2) << endl;
PrintMessage(errors[2]);
strcpy() and Message strcpy(m1, m2);
PrintMessage(m1);
return 0;
}
This little test program should compile correctly, run, and produce the output:
Arrays of fixed length strings 321
26
Write error
Elvis lives in Wagga Wagga
A Message isn't anything more than a character array. You can't return any kind of an
array as the result of a function so you can't return a Message as the result of a function.
Just to restate, typedefs simply introduce synonyms, they don't really introduce new
data types with different properties.
As in some previous examples, the initialized array errors[] shown above relies
on the compiler being able to count the number of messages and convert the open array
declaration [] into the appropriate [6].
You might need to know the number of messages; after all, you might have an
option in your program that lists all possible messages. Of course you can count them:
Message errors[NumMsgs] = {
"Disk full",
"…
"System error"
};
but that loses the convenience of getting the compiler to count them. (Further, it
increases the chances of errors during subsequent revisions. Someone adding a couple
of extra error messages later may well just change errors[NumMsgs] to errors[8]
and fail to make any other changes so introducing inconsistencies.)
You can get the compiler work out the number of messages and record this as a sizeof()
constant, or you can get the compiler to provide data so you can work this out at run
time. You achieve this by using the "sizeof()" operator.
Although sizeof() looks like a call to a function from some library, technically
sizeof() is a built in operator just like + or %. It can be applied either to a type name
or to a variable. It returns the number of bytes of memory that that type or variable
requires:
Using such calls in your code would let you work out the number of Messages in the
errors array:
cout << "Data type Message :" << sizeof(Message) << endl;
int numMsgs = sizeof(errors)/sizeof(Message);
cout << "Number of messages is " << numMsgs << endl;
Message errors[] = {
"Disk full",
"…
"System error"
};
This might be convenient if you needed a second array that had to have as many entries
as the errors array:
Message errors[] = {
"Disk full",
…
};
int err_counts[NumMsgs];
Problem
The program is to read a text file and produce a table showing the total number of
occurrences for each of the letters a … z.
This isn't quite as futile an exercise as you think. There are some uses for such
counts.
For example, before World War 1, the encryption methods used by diplomats (and
spies) were relatively naive. They used schemes that are even simpler than the
substitution cipher that is given in the next example. In the very simplest encryption
schemes, each letter is mapped onto a cipher letter e.g. a -> p, b ->n, c->v, d->e, e->r,
…. These naive substitution ciphers are still occasionally used. But they are easy to
crack as soon as you have acquired a reasonable number of coded messages.
The approach to cracking substitution ciphers is based on the use of letter
frequencies. Letter 'e' is the most common in English text; so if a fixed substitution like
e->r is used, then letter 'r' should be the most common letter in the coded text. If you
Example: Letter Counts 323
tabulate the letter frequencies in the encoded messages, you can identify the high
frequency letters and try matching them with the frequently used letters. Once you have
guessed a few letters in the code, you look for patterns appearing in words and start to
get ideas for other letters.
More seriously, counts of the frequency of occurrence of different letters (or, more
generally, bit pattern symbols with 8 or 16 bits) are sometimes needed in modern
schemes for compressing data. The original data are analyzed to get counts for each
symbol. These count data are sorted to get the symbols listed in order of decreasing
frequency of use. Then a compression code is made up which uses fewer bits for high
frequency symbols then it uses for low frequency symbols.
Specification:
3. When all data have been read from the file, a report should be printed in the
following format:
Design:
Again, some "functions" are immediately obvious. Something like "zero the counters"
is a function specification; so make it a function.
324 Arrays
open file
prompt user for filename
read name
try opening file with that name
if fail give up
initialize
zero the counters
processfile
while not end of file
read character
if is_alphabetic(character)
convert to lower
increment its counter
printcounters
line count = 0
for each letter a to z
print letter and associated count in appropriate fields
increment line count
if line count is 4 print newline and zero line count
main
open file
initialize
processfile
printcounters
Third iteration At this stage it is necessary to decide how to organize the array of letter counts. The
array is going to be:
long lettercounts[26]
We seem to need an array that you can index using a letter, e.g. lettercounts['a'];
but that isn't going to be correct. The index is supposed to be a number. The letter 'a'
interpreted as a number would be 97. If we have an 'a', we don't want to try
incrementing the non-existent l e t t e r c o u n t s [ 9 7 ] , we want to increment
lettercounts[0]. But really there is no problem here. Letters are just integers, so
you can do arithmetic operations on them. If you have a character ch in the range 'a' to
'z' then subtracting the constant 'a' from it will give you a number in the range 0 to 25.
We still have to decide where to define the array. It could be global, filescope, or it
could belong to main(). If it is global or filescope, all routines can access the array. If
it belongs to main(), it will have to be passed as a (reference) argument to each of the
other routines. In this case, make it belong to main().
Example: Letter Counts 325
The only other point that needs further consideration is the way to handle the
filename in the "open file" routine. Users of Macs and PCs are used to "File dialog"
windows popping up to let them explore around a disk and find the file that they want
to open. But you can't call those run-time support routines from the kinds of programs
that you are currently writing. All you can do is ask the user to type in a filename.
There are no problems provided that the file is in the same directory ("folder") as the
program. In other cases the user is supposed to type in a qualified name that includes
the directory path. Most Windows users will have seen such names (e.g.
c:\bc4\examples\owlexamples\proj1\main.cpp) but most Mac users will never have seen
anything like "HD:C++ Examples:Autumn term:Assignment 3:main.cp". Just let the
user type in what they will; use this input in the file open call. If the name is invalid,
the open operation will fail.
The array used to store the filename (a local to the openfile function) should allow
for one hundred characters; this should suffice even if the user tried to enter a path
name. The file name entered by the user should be read using getline() (this is
actually only necessary on the Mac, Mac file names can include spaces and so might
not be read properly with a "cin >> character_array" style of input).
The ifstream that is to look after the file had better be a filescope variable.
This involves nothing more than finalising the function prototypes before we start Fourth Iteration
coding:
void Openfile(void);
int main()
and checking the libraries needed so that we remember to include the correct header
files:
Note the difference between the prototype for PrintCounters() and the others.
PrintCounters() treats the counts array as read only; it should state this in its
prototype, hence the const . None of the functions using the array takes a count
argument; in this program the array size is fixed.
326 Arrays
Implementation:
We might as well define a constant ALPHABETSIZE to fix the size of arrays. The
"includes", filescopes, and const declaration parts of the program are then:
#include <iostream.h>
#include <fstream.h>
#include <iomanip.h>
#include <ctype.h>
#include <stdlib.h>
Reusable OpenFile() The OpenFile() routine should be useful in other programs (the prompt string
routine would need to be replaced). Standard "cut and paste" editing will suffice to move this
code to other programs; in Chapter 13 we start to look at building up our own libraries
of useful functions.
void Openfile(void)
{
const int kNAMESIZE = 100;
char filename[kNAMESIZE];
if(!infile.good()) {
cout << "Can't open that file. Quitting." << endl;
exit(1);
}
}
The check whether the file opening was unsuccessful could have been coded
if(!infile) but such shortcuts really make code harder to read the more long winded
if(!infile.good()).
The Initialize() function is pretty simple:
In Processfile() we need all the alphabetic characters and don't want whitespace.
It would have been sufficient to use "infile >> ch" (which would skip whitespace in the
file). However the code illustrates the use of the get() function; many related
programs will want to read all characters and so are likely to use get().
ch = tolower(ch);
int ndx = ch - 'a';
counts[ndx]++;
}
infile.close();
}
The isalpha() function from ctype.h can be used to check for a letter. Note the use
of "continue" in the loop. Probably your instructor will prefer the alternative coding:
…
infile.get(ch);
if(isalpha(ch)) {
ch = tolower(ch);
int ndx = ch - 'a';
counts[ndx]++;
}
}
ch = tolower(ch);
calls tolower() for all the alphabetic characters. You might have expected something
like:
if(isupper(ch))
ch = tolower(ch);
but that is actually more expensive. Calling tolower() for a lower case letter does not
harm.
328 Arrays
The main() function is nice and simple as all good mains should be:
int main()
{
int lettercounts[ALPHABETSIZE];
Openfile();
Initialize(lettercounts);
Processfile(lettercounts);
PrintCounters(lettercounts);
return 0;
}
Someone can read that code and immediately get an overall ("abstract") idea of how
this program works.
Problem
Two programs are required; the first is to encrypt a clear text message, the second is to
decrypt an encrypted message to get back the clear text.
The encryption mechanism is a slightly enhanced version of the substitution cipher
noted in the last example. The encryption system is for messages where the clear text is
encoded using the ASCII character set. A message will normally have printable text
characters and a few control characters. The encryption scheme leaves the control
characters untouched but substitutes for the printable characters.
Characters whose integer ASCII codes are below 32 are control characters, as is the
character with the code 127. We will ignore characters with codes 128 or above (these
Example: Simple encryption 329
will be things like π, ¤, fi, ‡, etc.). Thus, we need only process characters whose codes
are in the range 32 (space) to 126 (~) inclusive. The character value will be used to
determine an index into a substitution array.
The program will have an array containing the same characters in randomized order,
e.g.:
(Note the way of handling problem characters like the single quote character and the
backslash character.; these become \' and \\.)
You can use this to get a simple substitution cipher. Each letter of the message is Letter substitution for
converted to an index (by subtracting 32) and the letter in the array at that location is enciphering
substituted for the original.
int ch = message[i];
int ndx = ch - 32;
int subs = substitutions[ndx];
cout << char(subs)
comes out as
u//m;A<S[TUn/HmUS[[S>VUaA[\U[\S[U/yUu/5/<T5U'^K^u^U`S<;Tm,^UUN/
U,HEE/m[USm[A55TmnUS%SA5Sq5T^UUx<,[TS;cUn/HUaA55UqTUmTA<y/m>T;U
qnU(/>VT[U2S[[TmnUF)^
(Can you spot some of the give away patterns. The message ends with ^ and ^UU
occurs a couple of times. You might guess ^ was '.' and U was space. That guess
330 Arrays
would let you divide the text into words. You would then focus on two letter words N/,
qn, etc.)
You might imagine that it was unnecessary to analyze letter frequencies and spend
time trying to spot patterns. After all decrypting a message just involves looking up the
encrypted letters in a "reverse substitution" table. The entry for an encrypted letter
gives the original letter:
(The underlined entries correspond to indices for 'u' and '/', the first characters in the
example encrypted message.)
Reversing the letter If you have the original substitutions table, you can generate the reverse table:
substitution process
void MakeRTable(void)
{
for(int i = 0;i < len; i++) {
int tem = substitutions[i];
tem -= ' ';
rtable[tem] = i+32;
}
}
Of course, the enemy's decryption specialist doesn't know your original substitution
table and so must guess the reverse table. They can fill in a guessed table, try it on the
message text, and see if the "decrypted" words were real English words.
How could the enemy guess the correct table? Why not just try different possible
tables? A program could easily be devised to generate the possible tables in some
systematic way, and then try them on the message; the success of the decryption could
also be automatically checked by looking up "words" in some computerized dictionary.
The difficulty there relates to the rather large number of possible tables that would
have to be tried. Suppose all messages used only the letters a, b, c; the enemy decrypter
would have to try the reverse substitution tables bac, bca, cba, cab, acb, (but presumably
not abc itself). If there were four letters, there would be more possibilities: abcd, abdc,
acbd, acdb, …, dbca; in fact, 24 possibilities. For five letters, it is 120 possibilities.
The number of possibilities is the number of different permutations of the set of letters.
There are N! (i.e. factorial(N)) permutations of N distinct symbols.
Because there are N! permutations to be tried, a dumb "try everything" program O(N!) !
would have a running time bounded by O(N!). Now, the value of N! grows rather
quickly with increasing N:
N N! time scale
1 1 1 second
2 2
3 6
4 24
5 120 two minutes ago
6 720
7 5040
8 40320 half a day ago
9 362880
10 3628800 ten weeks ago
11 39916800
12 479001600
13 6227020800 more than a lifetime
14 87178291200
15 1307674368000 back to the stone age!
16 20922789888000
17 355687428096000
18 6402373705728000 Seen any dinosaurs yet?
With the letter permutations for a substitution cipher using a full character set, N is
equal to 95. Any intelligence information in a message will be "stale" long before the
message is decrypted.
Trying different permutations to find the correct one is impractical. But letter You can't just guess,
counting and pattern spotting techniques are very easy. As soon as you have a few but the cipher is still
weak
hundred characters of encrypted messages, you can start finding the patterns. Simple
character substitution cipher schemes are readily broken.
332 Arrays
A safer encryption A somewhat better encryption can be obtained by changing the mapping after each
scheme letter. The index used to encode a letter is:
the value k is incremented after each letter has been coded. (The modulo operator is
used to keep the value of index in the range 0 to 94. The value for k can start at zero or
at some user specified value; the amount it gets incremented can be fixed, or to make
things a bit more secure, it can change in some systematic way.) Using this scheme the
message becomes:
uEgH\<[\~<OQX{^EQ@,%UF^L6dJYQGv7Y-2y!Wbb^b %e,_mjz@FuJC
=ie$r&j`:(``leyx$jR40`SAE`APjq4eTt]X~o|,Hi;\w@LaGO{HnI#m%W@nGQ"
-c"LL8Lh.,WMYrfoDr7.W.3*T\?
This cipher text is a bit more secure because there aren't any obvious repeating patterns
to give the enemy cryptanalyst a starting point. Apparently, such ciphers are still
relatively easy to break, but they aren't trivial.
Specification:
1. Write a program "encrypt" and a program "decrypt" that encode and decode short
messages. Messages are to be less than 1000 characters in length.
2. The encrypt program is to prompt the user for a filename, the encrypted message
will be written to this file.
3. The encrypt program is then to loop prompting the user to enter the next line of the
clear text message; the loop terminates when a blank line is entered.
4. If the line is not blank, the encrypt program checks the message line just entered. If
the line contains control characters (apart from newline) or characters from the
extended set (those with integer values > 127), a warning message should be
printed and the line should be discarded. If the addition of the line would make the
total message exceed 1000 characters, a warning message should be printed and the
program should terminate.
5. If the line is acceptable, the characters are appended to those already in a main
message buffer. The newline character that ended the input line should be replaced
by a space.
6. When the input loop terminates, the message now held in a main message buffer
should be encrypted according to the mechanism described above, the encrypted
message is to be written to the file.
Example: Simple encryption 333
8. The decrypt program is to prompt the user for a filename, and should then open the
file (the decrypt program terminates if the file cannot be opened).
9. The message contained in the file should read and decoded character by character.
The decoded message should be printed on cout. Since newlines have been
removed during the encrypting process, the decrypt program must insert newlines
where needed to keep the output text within page bounds.
Design:
The requirement that the two programs share the same substitutions array is easily
handled. We can have a file that contains just the definition of the initialized
substitutions array, and #include this file in both programs.
The principal "functions" are immediately obvious for both programs, but some (e.g.
"Getthe message") may involve additional auxiliary functions.
The rough outline of the encrypt program now becomes: Second iteration
Open file
prompt user for filename
read name
try opening an output file with that name
if fail give up
Encrypt
replace each character in buffer by its encryption
Save
write string to file
Close
just close the file
encrypt_main
Open output file
Get message
Encrypt
Save
Close
Open file
prompt user for filename
read name
try opening an input file with that name
if fail give up
MakeRTable
basics of algorithm given in problem statement, minor
changes needed to incorporate the stuff about
the changing offset
Decrypt
initialize
read first character from file
while not end of file
decrypt character
print character
maybe formatting to do
read next character from file
decrypt_main
Open intput file
MakeRTable
Decrypt
Some of these function might be "one-liners". The Close() function probably will
be just something like outfile.close();. But it is worth keeping them in as functions
rather than absorbing their code back into the main routine. The chances are that sooner
or later the program will get extended and the extended version will have additional
work to do as it closes up.
Example: Simple encryption 335
The only function that looks as if it might be complex is "Get the message". It has
to read input, check input, add valid input to the buffer, check overall lengths etc. That
is too much work for one function.
The reading and checking of a single line of input can be factored out into an
auxiliary routine. This main "Get the message" routine can call this "Get and check
line" from inside a loop.
An input line may be bad (illegal characters), in which case the "GetMessage"
controlling routine should discard it. Alternatively, an input line may be empty; an
empty line should cause "GetMessage" to terminate its loop. Otherwise, GetMessage
will need to know the actual length of the input line so that it can check whether it fits
into the buffer. As well as filling in an array of characters with input, function
"GetAndCheckLine" can return an integer status flag.
The "Get the message" outline above should be expanded to:
GetAndCheckLine
use getline() to read a complete line
if linelength is zero (final blank input line) return 0
GetMessage
initialize message buffer and count
n = GetAndCheckLine()
while n != 0
if n > 0
(message is valid)
check if buffer can hold new line (allow
for extra space and terminating null)
if buffer can't then print warning and quit
(The development environment you use may have specific requirements regarding file
names. You want a name that indicates this file is not a header file, nor a separately
compiled module; it simply contains some code that needs to be #included into .cp files.
A suffix like ".inc" is appropriate if permitted in your environment.)
The ifstream that is to look after the file in the decrypt program can be a filescope
variable as can the ofstream object used in the encrypt program. The "reverse table"
used in the decrypt program can also be filescope.
Other character arrays can be local automatic variables of specific routines, getting
passed by reference if required. The encrypt program will require a character array
capable of holding a thousand character message; this can belong to its main() and get
passed to each of the other functions. Function GetMessage() will have to define an
array long enough to hold one line (≈120 characters should be plenty); this array will
get passed to the function GetAndCheckLine().
The Open-file functions in both programs will have local arrays to store the
filenames entered by the users. The Openfile() function from the last example should
be easy to adapt.
Fourth Iteration This involves nothing more than finalising the function prototypes and checking the
libraries needed so that we remember to include the correct header files:
Program encrypt:
void OpenOutfile(void);
void CloseFile(void);
int main()
i/o iostream.h
files fstream.h
termination on error stdlib.h
copying strings string.h
Program decrypt:
void OpenInputfile(void)
void MakeRTable(void)
void Decrypt(void)
int main()
i/o iostream.h
files fstream.h
termination on error stdlib.h
Implementation:
The first file created might as well be "table.inc" with its character substitution table.
This could simply be copied from the text above, but it would be more interesting to
write a tiny auxiliary program to create it.
It is actually quite common to write extra little auxiliary programs as part of the "Scaffolding"
implementation process for a larger program. These extra programs serve as
"scaffolding" for the main program as it is developed. Most often, you write programs
that produce test data. Here we can generate a substitution table.
The code is simple. The table is initialized so that each character is substituted by
itself! Not much security in that, the message would be copied unchanged. But after
this initialization, the data are shuffled. There are neater ways to do this, but a simple
way is to have a loop that executes a thousand times with each cycle exchanging one
randomly chosen pair of data values. The shuffled table can then be printed.
#include <iostream.h>
#include <stdlib.h>
cin >> s;
srand(s);
char table[NSYMS];
// shuffle
for(int j = 0; j < NSHUFFLES; j++ ) {
int ndx1 = rand() % NSYMS;
int ndx2 = rand() % NSYMS;
// Output
j = 0;
for(i = 0; i < NSYMS; i++) {
cout << "'" << table[i] << "', ";
j++;
if(j == 8) { cout << endl; j = 0; }
}
cout << endl;
return 0;
}
The output from this program has to be edited to deal with special characters. You have
to change ''' to '\'' and '\' to '\\'. Then you can substitute the generated table
into the outline given above for "table.inc" with its const definitions and the definition
of the array substitutions.
Encrypt
Program encrypt would start with #includes for the header files describing the
standard input/output libraries etc, and a #include for the file "table.inc".
#include <iostream.h>
#include <fstream.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include "table.inc"
Example: Simple encryption 339
Note that the #include statements are different. The system files (the headers that #includes of system
describe the system's libraries like iostream) are #included with the filename in pointy files and project files
brackets <…>. A project file, like table.inc, is #included with the filename in double
quotes "…". The different brackets/quotes used tell the compiler where to look for the
file. If the filename is in quotes, then it should be in the same directory as the program
code. If the filename is in <> brackets, then it will be located in some standard
directory belonging to the development environment.
Program encrypt would continue with a definition of the maximum message size,
and of a variable representing the output file. Then the functions would be defined.
void OpenOutfile(void)
{
const int kNAMESIZE = 100;
char filename[kNAMESIZE];
outfile.open(filename, ios::out);
if(!outfile.good()) {
cout << "Can't open that file. Quitting." << endl;
exit(1);
}
}
return 0;
// Check contents
// all characters should be printable ASCII
for(int i = 0; i< actuallength; i++)
if(!isprint(line[i])) {
cout << "Illegal characters,"
" line discarded"; return -1; }
return actuallength;
}
Function GetMessage() is passed the array where the main message text is to be
stored. This array doesn't need to be blanked out; the text of the message will overwrite
any existing contents. The message will be terminated by a null character, so if the
message is shorter than the array any extra characters at the end of the array won't be
looked at by the encryption function. Just in case no data are entered, the zeroth
element is set to the null character.
Function GetMessage() defines a local array to hold one line and uses a standard
while loop to get and process data terminated by a sentinel. In this case, the sentinel is
the blank line and most of the work of handling input is delegated to the function
GetAndCheckLine().
Function GetMessage() is the one where bugs are most likely. The problem area
will be the code checking whether another line can be added to the array. The array
must have sufficient space for the characters of the newline, a space, and a null.
Hopefully, the checks are done correctly!
It is easy to get errors where you are one out so that if you do by chance get a 999 Beware of "out by 1"
character input message you will fill in txt[1000]. Of course, there is no element errors
txt[1000], so filling in a character there is an error. You change something else.
Finding such bugs is difficult. They are triggered by an unusual set of data (most
messages are either definitely shorter than or definitely longer than the allowed size)
and the error action will often do nothing more than change a temporary automatic
variable that was about to be discarded anyway. Such bugs can lurk undetected in code
for years until someone makes a minor change so that the effect of the error is to change
a variable that is still needed. Of course, the bug won't show just after that change. It
still will only occur rarely. But sometime later, it may cause the program to crash.
How should you deal with such bugs?
The first strategy is never to make "out by 1" errors. There is an entire research area Prove correctness
on "proof of program correctness" that comes up with mechanisms to check such code
and show that it is correct. This strategy is considered elegant; but it means you spend a
week proving theorems about every three line function.
The second strategy is to recognize that such errors are possible, code carefully, and Code carefully and
test thoroughly. The testing would focus on the special cases (no message, and a test thoroughly
message that is just smaller than, equal to, or just greater than the maximum allowed).
(To make testing easier, you might compile a version of the program with the maximum
message size set to something much smaller, e.g. 25).
A final strategy (of poor repute) is to eliminate the possibility of an error causing Avoid
harm in your program. Here the problem is that you might be one out on your array
subscript and so trash some other data. OK. Make the array five elements larger but
only use the amount originally specified. You can be confident that that extra "slop" at
the end of the array will absorb any over-runs (you know you won't be six out in your
subscripting). Quick. Easy. Solved for your program. But remember, you are sending
the contents of the array to some other program that has been told to deal with messages
with "up to 1000 characters". You might well surprise that program by sending 1001.
The Encrypt() function is simple. If kSTART and kINCREMENT are both zero it will
work as a simple substitution cipher. If kINCREMENT is non zero, it uses the more
sophisticated scheme:
Functions SaveToFile() and CloseFile() are both tiny to the point of non
existence. But they should still both be functions because they separate out distinct,
meaningful phases of the overall processing.
void CloseFile(void)
{
outfile.close();
}
A good main() should be simple, little more than a sequence of function calls.
Function main() declares the array msg and passes it to each of the other functions that
needs it.
int main()
{
char msg[kMSGSIZE];
OpenOutfile();
cout << "Enter text of message:" << endl;
GetMessage(msg);
cout << "\nEncrypting and saving." << endl;
Encrypt(msg);
SaveToFile(msg);
CloseFile();
return 0;
}
Decrypt
Program decrypt is a bit shorter. It doesn't need quite as many system's header files
(reading header files does slow down the compilation process so only include those that
you need). It #includes the table.inc file to get the chosen substitution table and details
of the modifying kSTART and kINCREMENT values. It defines filescope variables for its
reverse table and its input file.
#include <iostream.h>
#include <fstream.h>
#include <stdlib.h>
#include "table.inc"
The program has an OpenInputfile() function that is not shown here; this
function is similar to that in the letter counting example. Function MakeRTable()
makes up the reverse substitution table from the data in the #included file with the
definition of array substitutions. The code here is slightly different from that illustrated
in the problem description. Rather than have rtable hold the character, it holds the
index number of the character. This is done to simplify the arithmetic in the Decrypt()
function.
void MakeRTable(void)
{
for(int i = 0;i < len; i++) {
int tem = substitutions[i];
tem -= ' ';
rtable[tem] = i;
}
}
Function Decrypt() reads characters of the encrypted message from the file and
outputs the original message characters. Newlines are inserted at convenient points.
The decryption mechanism reverses the encryption. The crypto character is looked up
in the array, getting back the index of the character selected in the Encrypt() routine.
This isn't the original character from the clear text message because it will have been
offset by the k term. So this offset is reversed. That process gives the index number of
the original character, adding ' ' converts this index into the ASCII code.
void Decrypt(void)
{
int ch;
int linepos = 0;
int k = kSTART;
ch = infile.get();
while(ch != EOF) {
int t = rtable[ch - 32];
t -= k;
while(t<0) t+= len;
cout << char(t + ' ');
int main()
{
OpenInputfile();
MakeRTable();
Decrypt();
return 0;
}
Problem
Most pictures input to computers are simply retouched, resized, recoloured, and then
displayed or printed. But in some areas like "Artificial Intelligence" (AI), or "Pattern
Recognition" (PR), or "Remote Sensing", programs have to be written to interpret the
data present in the image.
One simple form of processing needed in some AI and PR programs involves
finding the outlines of objects in an image. The image data itself will consist of an
array; the elements in this array may be simple numbers in the range 0 (black) … 255
(white) for a "grey scale" image, or they could be more complex values representing the
colour to be shown at each point in the image (grey scale data will be used to simplify
the example). A part of an image might be:
This image has a jagged "edge" running approximately diagonally leftwards to a corner
and then starting to run to the right. Its position is marked by the sudden change in
intensities.
Finding and tracing edges is a little too elaborate to try at this stage. But a related
simpler task can be performed. It is sometimes useful to know which row had the
largest difference in intensities in successive columns (need to know row, column
Example: Simple image processing 345
number of first of column pair, and intensity difference) and, similarly, which column
had the largest difference between successive rows.
These calculations involve just searches through the array considering either rows or
columns. If searching the rows, then for each row you find the largest difference
between successive column entries and compare this with the largest difference found
in earlier rows. If you find a larger value, you update your records of where the
maximum difference occurred.
The searches over the rows and over the columns can be handled by rather similar
functions – a ScanRows() function and a ScanCols() function. The functions get
passed the image data. They initialize a record of "maximum difference" to zero, and
then use a nested double loop to search the array.
There is one slightly odd aspect to these functions – they need to return three data Multiple results from
values. Functions usually compute a value i.e. a single number. But here we need three a function
numbers: row position, column position, and maximum intensity difference. So, there
is a slight problem.
Later (Chapters 16 and 17) we introduce struct types. You can define a struct
that groups together related data elements, like these three integer values (they don't
really have independent meaning, these value belong together as a group). A function
can return a struct, and that would be the best solution to the current problem.
However, there is an alternative approach that, although not the best, can be used
here. This mechanism is required in many other slightly more elaborate problems that
will come up later. This approach makes the functions ScanRows() and ScanCols()
into procedures (i.e. void return type) and uses "reference" arguments to communicate
the results.
As explained in section 11.4, arrays are always "passed by reference". The calling Pass by reference
function places the address of the start of the array in the stack. The called function again
takes this address from the stack and places it in one of the CPU's address registers.
When the called function needs to access an element of an array, it uses the contents of
this address register while working out where required data are in memory. The data
value is then fetched and used, or a new value is stored at the identified location.
Simple types, like integers and doubles, are usually passed by value. However, you Simple types as
can request the "pass by reference" mechanism. If you specify pass by reference, the reference arguments
calling function places details of the address of the variable in the stack (instead of its
value). The called function, knowing that it is being given an address, can proceed in
much the same way as it does when passed an array. Its code arranges to load the
address of the argument into an address register. When the argument's value is needed
the function uses this address register to help find the data (just like accessing arrays
except that there is no need to add on an "index" value).
Since the function "knows" where the original data variables are located in memory,
it can change them. This makes it possible to have a function that "returns" more than
one result. A function can have any number of "reference" arguments. As it runs, it
can change the values in those variables that were passed by the calling function. When
the function finishes, its local stack variables all disappear, but the changes it made to
346 Arrays
the caller's variable remain in effect. So, by changing the values of several reference
arguments, a function can "return" several values.
Specifying pass by When you define the function prototype, you chose whether it uses pass by value or
reference pass by reference for simple variables (and for structs). If don't specify anything, you
get pass by value. If you want pass by reference you write something like:
Reference types Here, & is used as a qualifier that turns a simple type, e.g. int, into a "reference
type", e.g. int&. Apart from a few very rare exceptions, the only place you will see
variables of reference types being declared is in function prototypes.
In the C++ source code for MultiResultFunction(), the reference variable ncount
and dmax will be treated just like any other int and double variables:
Because ncount and dmax are references, the compiler generates instructions for
dealing with them that differ from those used to access ordinary variables like m and d.
The compiler "knows" that the value in the reference variables are essentially addresses
defining where to find the data. Consequently, the generated machine code is always of
the form "load address register from reference variable, use address register's contents
when accessing memory to get data value, use data value, (store back into memory at
place specified by contents of address register)".
Input, output, and Variables passed by values are sometimes referred to as "input parameters"; they
input-output provide input to a function but they allow only one way communication. Reference
parameters
variables allow two way communication. The function can use the values already in
these variable (as above where the value of ncount is printed) or ignore the initial
values (as with dmax in the code fragment above). Of course they allow output from a
function – that was why we introduced them. So here, ncount is an "input output" (or
"value result") parameter while dmax is an output (or "result") parameter.
There are some languages where the "function prototypes" specify whether a
reference argument is an output (result) or an input-output (value result) parameter.
Example: Simple image processing 347
C++ does not have any way of indicating this; but this kind of information should form
part of a function's description in the program design documentation.
When passing arrays, we sometimes used const to indicate that the function treated
the array as "read only" (i.e. the array was an "input parameter"), so why is the
following inappropriate?
This is legal, but it is so much simpler to give Testfunction() its own copy of a
simple variable like an int. The following would be much more appropriate:
void Testfunction(int n)
{
…
}
When we get to structs , we will find that these can be passed by value or by
reference. Passing a struct by value means that you must make a copy. As this copying
process may be expensive, structs are often passed by reference even when they are
"input parameters". In such circumstances, it is again appropriate to use the const
qualifier to flag the role of the argument.
Now, to return to the original problem of the image processing program, we can
achieve the required multi-result functions ScanRows() and ScanCols() by using
pass by reference. These functions will be given the image data as const array
argument along with three integers that will be passed by reference (these will be
"output" or "result" arguments because their initial values don't matter).
Specification:
1. Write a program that will analyze "image data" to identify the row with the
maximum difference in intensity between successive columns, and the column with
the maximum difference in intensity between successive rows.
2. For the purposes of this program, an image is a rectangular array of 40 rows and 50
columns. The intensity data can be encoded as short integers (or as unsigned short
integers as all data values should be positive).
3. The program is to prompt the user for a filename; the image data will be read from
this file. The file will contain 2000 numbers; the first 50 values represent row 0,
the next 50 represent row 1 and so forth. The image data can be assumed to be
"correct"; there will be 2000 numbers in the file, all numbers will be positive and in
348 Arrays
a limited range. The format will have integer values separated by spaces and
newlines.
4. The program is identify the positions of maximum difference and print details in
the following style:
Design:
scan
initialize max_difference, row, col all to zero
for row 0 to maxrows - 1 do
for col = 0 to maxcols -2 do
difference = image[row][col+1] -
image[row][col]
difference = abs (difference);
if( difference > max_difference)
replace max_difference, row, col
Example: Simple image processing 349
As this is a simple problem, we have already finished most of the design. work. All Third iteration
that remains is deciding on data representations and function prototypes.
Here it seems worth while using a "typedef" to introduce the name Image as name
for a two dimensional array of short integers:
This will make the function prototypes a little clearer – we can specify an Image as an
argument.
The program only needs one Image. This Image could be made a filescope variable
but it is generally better style to have it defined as local to main() and arrange that it is
passed to the other functions. (It doesn't make much difference in this simple program;
but eventually you might need to generalize and have more than one Image. Then you
would find it much more convenient if the ScanRows() and ScanCols() functions
used an argument to identify the Image that is to be processed.)
Following these decisions, the function prototypes become: Function prototypes
void ScanRows(const Image picy, int& row, int& col, int& delta)
void ScanCols(const Image picy, int& row, int& col, int& delta)
int main();
Obviously LoadImage() changes the Image belonging to main(); the "scan" functions
should treat the image as read-only data and so define it as const.
The two scan functions have three "reference to integer" (int&) arguments as
explained in the problem introduction.
The header files will just be iostream and fstream for the file input, and stdlib
(needed for exit() which will be called if get an open failure on the image file).
Implementation:
There is little of note in the implementation. As always, the program starts with the
#includes that load the header files that describe the library functions employed; then
there are const and typedef declarations:
#include <iostream.h>
#include <fstream.h>
350 Arrays
#include <stdlib.h>
In this example, the file opening and input of data are all handled by the same
routine so it has the ifstream object as a local variable:
Generally, one would need checks on the data input. (Checks such as "Are all values in
the specified 0…255 range? Have we hit the end of file before getting all the values
needed?".) Here the specification said that no checks are needed.
The input routine would need to be changed slightly if you decided to save space by
making Image an array of unsigned char. The restricted range of intensity values
would make it possible to use an unsigned char to represent each intensity value and
this would halve the storage required. If you did make that change, the input routine
would have to use a temporary integer variable: { unsigned short temp; infile
>> temp; picy[i][j] = temp; }. If you tried to read into an "unsigned char"
array, using infile >> picy[i][j];, then individual (non-blank) characters would be
read and interpreted as the intensity values to initialize array elements!
The ScanRows() function is:
void ScanRows(const Image picy, int& row, int& col, int& delta)
{
delta = 0;
row = 0;
Example: Simple image processing 351
col = 0;
Function ScanCols() is very similar. It just switches the roles of row and column.
Apart from the function calls, most of main() consists of output statements:
int main()
{
int maxrow, maxcol, maxdiff;
Image thePicture;
LoadImage(thePicture);
cout << "Image loaded" << endl;
return 0;
}
You would have to test this program on known test data before trying it with a real
image! The values of the consts kROWS and kCOLS can be changed to something
much smaller and an artificial image file with known data values can be composed. As
noted in the previous example, it is often necessary to build such "scaffolding" of
auxiliary test programs and test data files when developing a major project.
352 Arrays
EXERCISES
1 The sending of encrypted text between parties often attracts attention – "What do they want to
hide?"!
If the correspondents have a legitimate reason for being in communication, they can proceed
more subtly. Rather than have M. Jones send K. Enever the message
'q%V"Q \IH,n,h^ps%Wr"XX)d6J{cM6M.bWu6rhobh7!W.NZ..Cf8
qlK7.1_'Zt2:q)PP::N_jV{j_XE8_i\$DaK>yS
(i.e. "Kathy Darling, the wife is going to her mother tonight, meet me at Spooners 6pm,
Kisses Mike")
this secret message can be embedded in a longer carrier message that appears to be part of a
more legitimate form of correspondence. No one is going to look twice at the following item
of electronic mail:
From: M. Jones
To: K. Enver
Hi Kate, could you please check this rough draft report that I've typed; hope
you can call me back today if you have any problems.
--------
Meeting of the Kibrary Committee, Tuesday Augast 15th,
Present: D. Ankers, Rt Bailey, J.P. Cauroma, …
…
Letters from the secret message are substituted for letters in the body of the original (the
underlines shown above mark the substitutions, of course the underlining would not really be
present). Obviously, you want to avoid any regularity in the pattern of "typing errors" so the
embedding is done using a random number generator.
The random number generator is first seeded (the seed is chosen by agreement between
the correspondents and then set each time the program is run). The program then loops
processing characters from the carrier message and characters from the secret text. In these
loops, a call is first made to the random number generator – e.g. n = (rand() % IFREQ)
+ 3;, the value obtained is the number of characters from the carrier message that should be
copied unchanged; the next character from the secret text is then substituted for the next
character of the carrier text. The value for IFREQ should be largish (e.g. 50) because you
don't want too many "typing errors". Of course, the length of the carrier text has to be
significantly greater than (IFREQ/2) times the secret text. "Encrypting" has to be
abandoned if the carrier text is too short (you've been left with secret text characters but have
hit end of file on the carrier). If the carrier is longer, just substitute a random letter at the next
point where a secret text letter is required (you wouldn't want the apparent typing accuracy to
Example: Simple image processing 353
suddenly change). These extra random characters don't matter; the message will just come to
an end with something like "Kisses MikePrzyqhn Tuaoa". You can rely on an intelligent
correspondent who will stop reading at the point where the extracted secret text no longer
makes sense.
Write the programs needed by your good fiends Mike and Kate.
2. Change the encrypt and decrypt programs so that instead of sharing a fixed substitutions
table, they incorporate code from the auxiliary program that generates the table.
The modified programs should start by prompting for three small positive integers.
Two of these define values for variables that replace kSTART and kINCREMENT. The third
serves as the seed for the random number generator that is used to create the
substitutions table.
3. Write a program that will lookup up details of distances and travel times for journeys
between pairs of cities.
The program is to use a pair of initialized arrays to store the required data, one array to
hodl distances, the other travel times. For example, if you had just three cities, you might
have a distances array like:
Given a pair of city identification numbers, the program prints the details of distance
and travel time. (Remember, users generally prefer numbering schemes that start at 1 rather
354 Arrays
than 0; convert from the users' numbering scheme to an internal numbering suitable for zero
based arrays.)
You should plan to use several functions. The city names should also be stored in an
array.
4. The arrays of distance and travel time in exercise 3 are both symmetric about the diagonal.
You can halve the data storage needed by using a single array. The entries above the
diagnoal represent distances, those below the diagonal represent travel times.
Rewrite a solution to assignment 3 so as to exploit a single travel-data array.
12
12 Programs with functions
and arrays
This chapter has a few examples that show some of the things that you can do using
functions and arrays. A couple are small demonstration programs. Some, like the first
example are slightly different. These examples– "curses", "menus", and "keywords" –
entail the development of useful group of functions. These groups of functions can be
used a bit like little private libraries in future examples.
12.1 CURSES
Curses?
Yes, well programs and curses are strongly associated but this is something
different. On Unix, there is a library called "curses". It allows programs to produce
crude "graphical" outputs using just low "cost cursor addressable" screens. These
graphics are the same quality as the character based function plotting illustrated in
10.11.2. Unix's curses library is actually quite elaborate. It even allows you to fake a
"multi-windowing" environment on a terminal screen. Our curses are less vehement,
they provide just a package of useful output functions.
These functions depend on our ability to treat the computer screen (or just a single "Cursor
window on the screen) as a "cursor addressable terminal screen". Basically, this means addressable" screens
and windows
that this screen (or window) can be treated as a two-dimensional array of individually
selectable positions for displaying characters (the array dimension will be up to 25 rows
by 80 columns, usually a bit smaller). The run-time support library must provide a
gotoxy() function and a putcharacter() function. Special character oriented input
functions must also be used.
Such facilities are available through the run time support libraries provided with Run-time support in
Symantec C++ for PowerPC, and Borland's IDE. In the Borland system, if you want common IDEs
cursor addressing facilities you have to create your project as either a "DOS Standard"
356 Programs with functions and arrays
project, or an "EasyWin" project (there are slight restrictions on EasyWin and cursor
graphics).
Naturally, the run-time support libraries in Symantec and Borland differ. So here we
have to develop code that will contain conditional compilation directives so that
appropriate code is produced for each different environment.
Separate header and This code is to be in a separate file so that it can be used in different programs that
implementation files require simple graphics output. Actually, there will be two files. Like the standard
libraries, our curses "package" will consist of an implementation file with the
definitions of the functions and a header file. The header file contains just the function
prototypes and some constants. This header will be #included into the programs that
need to use the curses display facilities.
We will need a small program to test the library. A "drawing program" would do.
This could start by displaying a blank window with a "pen position" shown by a
character such as a '*'. Keyed commands would allow the user to move the pen up,
down, left and right; other commands could let the "ink" to be changed from a drawing
pattern ('#') to the background ('.'). This would allow creation of pictures like that
shown in Figure 12.1.
+----------------------------------------------------------------------------+
|............................................................................|
|............................................................................|
|............................................................................|
|............................................................................|
|.........####.....#......#..................................................|
|........#.........#......#.............##.....##............................|
|........#.......#####..#####...........##.....##............................|
|........#.........#......#..................................................|
|........#.........#......#..................................................|
|..........####.........................#.......#*...........................|
|........................................#######.............................|
|............................................................................|
|............................................................................|
|............................................................................|
|............................................................................|
|............................................................................|
|............................................................................|
|............................................................................|
+----------------------------------------------------------------------------+
>u
The IDE projects for programs built using the curses functions will have to identify
the CG.cp ("Curses Graphics") file as one of the constituent files along with the normal
main.cp. However, the header file, CG.h, will not be explicitly listed among the project
files.
As noted earlier, the curses functions depend on a system's library that contains
primitives like a gotoxy() function. IDEs normally have two groups of libraries –
those that always get checked when linking a program, and those that are only checked
when explicitly identified to the linker. In both the Symantec and Borland IDEs, the
special run-time routines for cursor addressing are in files separate from the more
common libraries. However, in both Symantec 8 and Borland, this extra library file is
Curses example 357
among those checked automatically. In Symantec 7, the library with the cursor routines
has to be explicitly made part of the project. If you do get linking errors when you try
to build these programs, you will have to check the detailed documentation on your IDE
to find how to add non-standard libraries to the set used by the linking loader.
Figure 12.2 illustrates how these programs will be constructed. A program using the
curses routines will have at least two source files specified in its project; in the figure
they are test.cp (which will contain main() and other functions) and CG.cp. Both
these source files #include the CG.h routine. The CG.cp file also contains #include
statements that get the header files defining the run-time support routines that it uses
(indicated by the file console.h in the figure). The compiler will produce the linkable
files test.o and CG.o. The linking loader combines these, and then adds all necessary
routines from the normally scanned libraries and from any specifically identified
additional libraries.
#include
console.h
non-standard
library routines
test.cp test.o
Linking Loader
CG.cp CG.o
Program in memory
Curses functions
The curses functions present the applications programmer with a simple window
environment as shown in Figure 12.3. Characters are displayed by moving a "cursor" to
a specific row and column position and then outputting a single character. If there are
no intervening cursor movements, successive output characters will go in successive
columns of the current row. (Output to the last column or last row of the screen should
be avoided as it may cause unwanted scrolling of the display.)
As shown in Figure 12.3, the screen is normally divided into a work area (which
may be marked out by some perimeter border characters), and a few rows at the bottom
of the screen. These last few rows are used for things such as prompt line and data
input area.
Input is usually restricted to reading single characters. Normally, an operating
system will collect all characters input until a newline is entered; only then can the data
be read by a program. This is not usually what one wants in an interactive program.
So, instead, the operating system is told to return individual characters as they are
entered.
≈80 columns
+------------------ ----------------------------+
|.................. ............................|
|.................. ............................|
|.................. ............................|
…
≈25 …
…
rows …
|.................. ............................|
|.................. ............................|
|.................. ............................|
+------------------ ----------------------------+
>u
Outline of window
or screen
Often interactive programs need to display data that change. A program may need
to arrange to pause for a bit so that the user can interpret one lot of data on the screen
Curses example 359
before the display gets changed to show something else. Consequently, curses
packages generally include a delay() function.
A curses package will have to provide at least the following: Minimal
functionality required
in a curses package
1 Details of the width and height of the effective display area.
2 An initialize function.
3 A reset function.
This would make the equivalent request to the operating system to put the terminal
back into normal mode.
This will clear all rows and columns of the display area, setting them to a chosen
"background" character.
Clears the window, then fills in the perimeter (as in the example shown in Figure
12.1).
This will use the IDE's gotoxy() function to select the screen position for the
next character drawn ("positioning the cursor").
/*
Routines for a "cursor graphics" package.
/*
Initialize --- call before using cursor screen.
Reset --- call when finished.
*/
void CG_Initialize();
void CG_Reset();
/*
Movement and output
*/
void CG_MoveCursor(int x, int y);
void CG_PutCharacter(char ch);
void CG_PutCharacter(char ch, int x, int y);
void CG_ClearWindow(char background = ' ');
void CG_FrameWindow(char background = ' ');
/*
Delay for graphics effects
*/
void CG_Delay(int seconds);
/*
Prompting and getting input
*/
void CG_Prompt(const char prompt[]);
char CG_GetChar();
#endif
Curses example 361
This file organization illustrates "good style" for a package of related routines.
Firstly, the entire contents a bracketed by a conditional compilation directive:
This mechanism is used to avoid compilation errors if the same header file gets
#included more than once. Before the file is read, the compile time "variable"
__CGSTUFF__ will be undefined, so #ifndef ("if not defined") is true and the rest of
the file is processed. The first operation defines the "compile time variable"
__CGSTUFF__; if the file is read again, the contents are skipped. If you don't have such
checks, then multiple inclusions of the file lead to compilation errors relating to
redefinition of constants like CG_WIDTH. (In larger programs, it is easy to get headers
multiply included because header files may themselves have #include statements and so
a particular file may get referenced in several places.)
A second stylistic feature that you should follow is illustrated by the naming of Naming conventions
constants and functions. All constants and functions start with the character sequence
CG_; this identifies them as belonging to this package. Naming conventions such as this
make it much easier for people working with large programs that are built from many
separate source files. When they see a function called in some code, the function name
indicates the "package" where that it is defined.
The functions are provided by this curses package are grouped so as to make it
easier for a prospective user to get an idea of what the package provides. Where
appropriate, default argument values are defined.
The different development environments provide slightly different versions of the same Run-time support
low-level support functions. Although the functions are provided, there is little in the functions of the IDEs
IDEs' documentation to indicate how they should be used. The Borland Help system
and manuals do at least provide full details of the individual functions.
In the Borland environment, the header file conio.h contains prototypes for a number
of functions including:
These specialized calls work directly with the DOS operating system and do not require
any special changes to "modes" for keyboards or terminals. Borland "DOS/Standard"
applications can use a sleep() function defined in dos.h.
In the Symantec environment, the header file console.h contains a prototype for
(the FILE* argument actually is set to a system provided variable that identifies the
window used for output by a Symantec program). The prototype for the sleep()
function is in unix.h, and the file stdio.h contains prototypes for some other functions
for getting (fgetc()) and putting (fputc() and fflush()) individual characters. The
handling of input requires special initializing calls to switch the input mode so that
characters can be read immediately.
Delays in programs The best way to "pause" a program to allow a user to see some output is to make a
call to a system provided function. Following Unix, most operating systems provide a
sleep() call that suspends a program for a specified number of seconds; while the
program is suspended, the operating system runs other programs or deals with
background housekeeping tasks. Sometimes, such a system call is not available; the
Borland "EasyWin" environment is one such case. EasyWin programs are not
permitted to use the DOS sleep() function.
If you need a delay in a program and don't have a sleep() function, or you need a
delay shorter than one second, then you have a couple of alternatives. Both keep the
program busy using the CPU for a specified time.
The more general approach is to use a compute loop:
The idea is that the calculation involving floating point division will take the CPU some
microseconds; so a loop with thousands of divisions will take a measurable time. The
fudgefactor is then adjusted empirically until appropriate delays are obtained.
There are a couple of problems. You have to change the fudgefactor when you
move to a machine with a different CPU speed. You may get caught by an optimising
compiler. A good optimising compiler will note that the value of x in the above code is
never used; so it will eliminate the assignment to x. Then it will note that the loop has
essentially no body. So it eliminates the loop. The assignment to lim can then be
omitted; allowing the optimising compiler to reduce the function to void delay(int)
Curses example 363
{ } which doesn't have quite the same properties. The compilers you usually use are
much less aggressive about optimizing code so computational loops often work.
An alternative is to use system functions like TickCounter() (see 10.10). If your
system has TickCounter() function that returns "seconds since machine switched on",
you can achieve a delay using code like:
(The function call in the loop will inhibit an optimising compiler, it won't try to change
this code). Most compilers will give a warning about the empty body in the while loop;
but it is exactly what we would need here.
The code to handle the cursor graphics functions is simple; it is largely comprised of
calls to the run-time support functions. This code makes fairly heavy use of conditional
compilation directives that select the specific statements required. The choice amongst
alternatives is made by #defining one of the compiler time constants SYMANTEC,
DOS, or EASYWIN.
The first part of the file #includes the appropriate system header files:
#define SYMANTEC
#if defined(SYMANTEC)
/*
stdio is needed for fputc etc;
console is Symantec's set of functions like gotoxy
unix for sleep function
*/
#include <stdio.h>
#include <console.h>
#include <unix.h>
364 Programs with functions and arrays
#endif
#if defined(DOS)
/*
conio has Borland's cursor graphics primitives
dos needed for sleep function
*/
#include <conio.h>
#include <dos.h>
#endif
#if defined(EASYWIN)
/*
Still need conio, but can't use sleep, achieve delay by
alternative mechanism
*/
#include <conio.h>
#endif
#include "CG.h"
All version need to include CG.h with its definitions of the constants that determine the
allowed width and height of the display area.
The various functions in the package are then defined. In some versions, the body of
a function may be empty.
void CG_Reset()
{
#if defined(SYMANTEC)
csetmode(C_ECHO, stdin);
#endif
}
Curses example 365
The MoveCursor() function makes the call to the appropriate run-time support
routine. Note that you cannot predict the behaviour of the run-time routine if you try to
move the cursor outside of the screen area! The run-time routine might well crash the
system. Consequently, this MoveCursor() function has to constrain the arguments to
fit within the allowed range:
y = (y < 1) ? 1 : y;
y = (y > CG_HEIGHT) ? CG_HEIGHT : y;
#if defined(SYMANTEC)
cgotoxy(x,y,stdout);
#else
gotoxy(x,y);
#endif
}
The FrameWindow() function uses ClearWindow() and then has loops to draw top,
bottom, left and right edges and individual output statements to place the four corner
points.
The Delay() function can use the system sleep() call if available; otherwise it
must use something like a computational loop:
#else
Curses example 367
sleep(seconds);
#endif
}
Function Prompt() outputs a string at the defined prompt position. This code uses
a loop to print successive characters; your IDE's run time routines may include a "put
string" function that might be slightly more efficient.
The GetChar() routine uses a run time support routine to read a single character
from the keyboard.
char CG_GetChar()
{
#if defined(SYMANTEC)
return fgetc(stdin);
#elif
return getche();
#endif
}
Specification
1 Display a window with a "pen" that the user can move by keyed commands.
2 The initial display is to show a "framed window" with '.' as a background character
and the pen (indicated by a '*') located at point 10, 10 in the window. A '>' prompt
symbol should be displayed on the promptline.
3 The pen can either be in "draw mode" or "erase mode". In "draw mode" it is to
leave a trail of '#' characters as it is moved. In "erase mode" it leaves background '.'
characters. Initially, the pen is in "draw mode".
368 Programs with functions and arrays
4 The program is to loop reading single character commands entered at the keyboard.
The commands are:
5 The pen movement commands are to be restricted so that the pen does not move
onto the window border.
Design
First iteration There is nothing much to this one. The program structure will be something like:
move pen
output ink or background symbol at current pen position
update pen position, subject to restrictions
output pen character, '*', at new pen position
main
initialize
loop until quit command
get command character
switch to select
change of pen mode
movements
reset
The different movements all require similar processing. Rather than repeat the code
for each, a function should be used. This function needs to update the x, y coordinate of
the pen position according to delta-x and delta-y values specified as arguments. The
various movement cases in the switch will call the "move pen" function with different
delta arguments.
Second iteration The pen mode can be handled by defining the "ink" that the pen uses. If it is in
"erase mode", the ink will be a '.' character; in "draw mode" the character will be '#'.
The variables that define the x, y position and the ink have to be used in both main()
and "move pen"; the x and y coordinates are updated by both routines. The x, y values
could be made filescope – and therefore accessible to both routines. Alternatively, they
Curses example 369
could be local to main() but passed by reference in the call to "move pen"; the pass by
reference would allow them to be updated.
The only filescope data needed will be constants defining limits on movement and
the various characters to be used for background, pen etc. The drawing limit constants
will be defined in terms of the window-limit constants in the #included CG.h file.
File CG.h would be the only header file that would need to be included.
The loop structure and switch statement in main() would need a little more planning Third iteration
before coding. The loop could be a while (or a for) with a termination test that checks
an integer (really a boolean but we don't have those yet in most C++ implementations)
variable. This variable would initially be false (0), and would get set to true (1) when a
'quit' command was entered.
The switch should be straightforward. The branches for the movement commands
would all contain just calls to the move pen function, but with differing delta x and
delta y arguments.
Function MovePen() will have the prototype:
Implementation
The file test.cp starts with its one #include (no need for the usual #include <iostream.h>
etc). The CG files (CG.h and CG.cp) will have to be in the same directory as the test
program. This doesn't cause any problems in the Borland environment as you can have
a project folder with several different target programs that share files. In the Symantec
environment, you may find it necessary to copy the CG files into the separate folders
associated with each project.
After the #include, the constants can be defined:
#include "CG.h"
x += dx;
x = (x >= XMIN) ? x : XMIN;
x = (x <= XMAX) ? x : XMAX;
y += dy;
y = (y >= YMIN) ? y : YMIN;
y = (y <= YMAX) ? y : YMAX;
CG_PutCharacter(pensym,x,y);
}
int main()
{
int x = 10;
int y = 10;
int done = 0;
CG_Initialize();
CG_FrameWindow('.');
CG_PutCharacter(pensym,x,y);
Here, the loop is done using for(;!done;); a while loop might be more natural:
for(;!done;) {
CG_Prompt(">");
int ch;
ch = CG_GetChar();
switch(ch) {
case 'i':
case 'I':
// put pen in drawing mode, the default
ink = drawsym;
break;
case 'e':
case 'E':
// put pen in erase mode
Curses example 371
ink = background;
break;
case 'u':
case 'U':
MovePen(x, y, 0, -1, ink);
break;
case 'd':
case 'D':
MovePen(x, y, 0, 1, ink);
break;
case 'l':
case 'L':
MovePen(x, y, -1, 0, ink);
break;
case 'r':
case 'R':
MovePen(x, y, 1, 0, ink);
break;
case 'q':
case 'Q':
done = 1;
break;
default:
break;
}
}
CG_Reset();
return 0;
}
Back around section 4.2.2, we left an engineer wanting to model heat diffusion in a
beam. We can now do it, or at least fake it. The engineer didn't specify the heat
diffusion formula so we'll just have to invent something plausible, get the program to
work, and then give it to him to code up the correct formula.
Figure 12.4 illustrates the system the engineer wanted to study. A flame is to heat
the mid point of a steel beam (of undefined thickness), the edges of which are held at
ambient temperature. The heat diffusion is to be studied as a two dimensional system.
The study uses a grid of rectangles representing areas of the beam, the temperatures in
each of these rectangles are to be estimated. The grid can obviously be represented in
the program by two dimensional array.
372 Programs with functions and arrays
The system model assumes that the flame is turned on and immediately raises the
temperature of the centre point to flame temperature. This centre point remains at this
temperature for the duration of the experiment.
25 25 25 25 25 …
25 25 25 25 25 …
25 25 1000 25 …
25 25 25 25 …
Heat diffusion example 373
10 iterations
...................................................
60 iterations
...................................................
................................................... ...................................................
................................................... ...................................................
................................................... ...................................................
................................................... .......................-----.......................
................................................... ......................--+++--......................
........................-+-........................ ......................-+===+-......................
........................+#+........................ ......................-+=#=+-......................
........................-+-........................ ......................-+===+-......................
................................................... ......................--+++--......................
................................................... .......................-----.......................
................................................... ...................................................
................................................... ...................................................
................................................... ...................................................
................................................... ...................................................
30 iterations
................................................... 100 iterations
...................................................
................................................... ...................................................
................................................... ...................................................
................................................... .......................-----.......................
................................................... ......................-------......................
.......................-----....................... .....................--+++++--.....................
.......................-+=+-....................... .....................--+=*=+--.....................
.......................-=#=-....................... .....................--+*#*+--.....................
.......................-+=+-....................... .....................--+=*=+--.....................
.......................-----....................... .....................--+++++--.....................
................................................... ......................-------......................
................................................... .......................-----.......................
................................................... ...................................................
................................................... ...................................................
................................................... ...................................................
The average of its eight neighbors is 146.8 ((1000 + 7*125)/8). So, this points
temperature should increase toward this average. The rate at which it approaches the
average depends on the thermal conductivity of the material; it can be defined by a
formula like:
with the value of conduct being a fraction, e.g. 0.33. If the formula is defined like this,
the new temperature for the marked point would be 65.1. This results in the
temperature distribution
…
25 25 25 25 25 …
25 65 65 65 25 …
25 65 1000 65 …
25 65 65 65 …
…
(The centre point is held at 1000°C by the flame.) On the next iteration, the average
temperature of this point's neighbors would be 156.8, and its temperature would
increase to 95°C.
This process has to be done for all points on the grid. Note that this will require two
copies of the grid data. One will hold the existing temperatures so that averages can be
computed; the second is filled in with the new values. You can't do this on the same
grid because if you did then when you updated one point you would change
inappropriately the environment seen by the next point.
Once the values have been completed, they can be displayed. Changes in single
iterations are small, so normally plots are needed after a number iterative cycles have
374 Programs with functions and arrays
been completed. The "plotting" could use the cursor graphics package just
implemented or standard stream output. The plot simply requires a double loop to print
the values in each grid point. The values can be converted into characters by dividing
the range (1000 - 25) into ten intervals, finding the interval containing a given
temperature and using a plotting character selected to represent that temperature
interval.
Specification
1 The program will model the process using a two dimensional array whose bounds
are defined by constants. Other constants in the program will specify the ambient
and flame temperatures, and a factor that determines the "thermal conductivity" of
the material.
2 The program will prompt the user for the number of iterations to be performed and
the frequency of displays.
3 Once modelling starts, the temperature of the centre point is held at the "flame
temperature", while the perimeter of the surface is held at ambient temperature.
4 In each iteration, the temperatures at each grid point (i.e. each array element) will
be updated using the formulae described in the problem introduction.
5 Displays will show the temperature at the grid points using different characters to
represent each of ten possible temperature ranges.
Design
main
get iteration limit
get print frequency
initialize grid
loop
update values in grid to reflect heat diffusion
if time to print, show current values
Second iteration Naturally, this breaks down into subroutines. Function Initialize() is obvious,
as are HeatDiffuse() and Show().
Initialize
Heat diffusion example 375
Show
double loop
for each row do
for each column do
get temperature code character
for this grid point
output character
newline
HeatDiffuse
get copy of grid values
double loop
for each row do
for each col do
using copy work out average temp.
of environment of grid pt.
calculate new temp based on current
and environment
store in grid
reset centre point to flame temperature
CodeTemperature
// find range that includes temperature to be coded
set value to represent ambient + 0.1 * range
index = 0;
while value < temp
index++, value += 0.1*range
use index to access array of characters representing
the different temperature ranges
Copy grid
double loop filling copy from original
New temperature
formula given conductivity*environment + (1- cond)*current
Average of neighbors
find average of temperatures in three neighbors in row
above, three neighbors in row below, and left, right
neighbors
376 Programs with functions and arrays
While most of these functions are fairly straightforward, the "average of neighbors"
does present problems. For grid point r, c, you want:
TemperatureAt(row, col)
if row outside grid
return ambient
if col outside grid
return ambient
return grid[r][c]
The sketch for the revised "Average of Neighbors" uses a double loop to run though
nine squares on the grid. Obviously that includes the temperature at the centre point
when just want to consider its neighbors; but this value can be subtracted away.
Final stages All that remains is to decide on function prototypes, shared data, and so forth. As in
earlier examples, the function prototypes are simplified if we use a typedef to define
"Grid" to mean an array of doubles:
int main();
The main Grid variable could have been made filescope and shareable by all the
functions but it seemed more appropriate to define it as local to main and pass it to the
other functions. The const qualifier is used on the Grid argument to distinguish those
functions that treat it as read only from those that modify it.
There should be no filescope variables, just a few constants. This version of the
program uses stream output to display the results so #includes <iostream.h>.
Implementation
The file starts with the #includes and constants. The array TemperatureSymbols[]
has increasingly 'dense' characters to represent increasingly high temperatures.
#include <iostream.h>
#include <assert.h>
Function CodeTemperature() sets the initial range to (25, 120) and keeps
incrementing until the range includes the specified temperature. This should result in
an index in range 0 .. 9 which can be used to access the TemperatureSymbols[] array.
return TemperatureSymbols[i];
}
Both Show() and CopyGrid() use similar double loops to work through all
elements of the array, in one case printing characters and in the other copying values.
Show() outputs a '\f' or "form feed" character. This should clear the window (screen)
and cause subsequent output to appear at the top of the window. This is more
convenient for this form of display than normal scrolling.
The curses library could be used. The routine would then use CG_PutCharacter(
char, int, int) to output characters at selected positions.
if(col < 0)
return kAMBIENT;
return g[row][col];
}
As explained previously, there is nothing wrong with one line functions if they help
clarify what is going on in a program. So function NewTemperature() is justified (you
could define it as inline double NewTemperature() if you didn't like paying the
cost of a function call):
Function HeatDiffuse() has to have a local Grid that holds the current values
while we update the main Grid. This copy is passed to CopyGrid() to be filled in with
current values. Then get the double loop where fill in the entries of the main Grid with
new computed values. Finally, reset the centre point to flame temperature.
void HeatDiffuse(Grid g)
{
Grid temp;
CopyGrid(temp, g);
for(int r = 0; r < kWIDTH; r++)
for(int c = 0; c < kLENGTH; c++) {
void Initialize(Grid g)
{
for(int r=0;r<kWIDTH;r++)
for(int c=0;c<kLENGTH; c++) g[r][c] = kAMBIENT;
g[kMIDY][kMIDX] = kFLAME;
}
The main program is again simple, with all the complex processing delegated to
other functions.
int main()
{
int nsteps;
int nprint;
cout << "Number of time steps to be modelled : ";
cin >> nsteps;
cout << "Number of steps between printouts : ";
cin >> nprint;
Grid g;
Initialize(g);
If you have a very fast computer, you may need to put a delay after the call to
Show(). However, there are enough floating point calculations being done in the loops
to slow down the average personal computer and you should get sufficient time to view
one display before the next is generated. A suitable input data values are nsteps ==
100 and nprint == 10. The patterns get more interesting if the flame temperature is
increased to 5000°C.
int MenuSelect(…);
Menu selection example 381
Normal requirements include printing a prompt, possibly printing the list of choices,
asking for an integer that represents the choice, validating the choice, printing error
message, handling a ? request to get the options relisted, and so forth. This function can
be packaged in a little separately compiled file that can be linked to code that requires
menu selection facilities.
The function has to be given:
2 an indication as to whether the list of choices should be printed before waiting for
input;
print prompt
if need to list options
list them
repeat
ask for integer in specified range
deal with error inputs
deal with out of range
until valid entry
Dealing with out of range values will be simple, just requires an error message
reminding the user of the option range. Dealing with errors is more involved.
Errors like end of file or "unrecoverable input" error are probably best dealt with by Design, second
returning a failure indicator to the calling program. We could simply terminate the iteration
program; but that isn't a good choice for a utility routine. It might happen that the
calling program could have a "sensible default processing" option that it could use if
there is no valid input from the user. Decisions about terminating the program are best
left to the caller.
There will also be recoverable input errors. These will result from a user typing
alphabetic or punctuation characters when a digit was expected. These extraneous
characters should be discarded. If the first character is a '?', then the options should be
displayed.
Sorting out unrecoverable and recoverable errors is too elaborate to be expanded in
line, we need an auxiliary function. These choices lead to a refined design:
handle error
check input status,
if end of file or bad then
return "give up" indicator (?0)
382 Programs with functions and arrays
menuselect
print prompt
if need to list options
list them
repeat
ask for integer in specified range
read input
Design, third The activity "list options" turns up twice, so even though this will probably be a one
iteration or two line routine it is worth abstracting out:
list options
for each option in array
print index number, print option text
The MenuSelect() function has to be given the prompt and the options. These will
be arrays of characters. Again it will be best if a typedef is used to introduce a name
that will allow simplification of argument lists and so forth:
The UT_ prefix used in these names identifies these as belonging to a "utility" group of
functions.
Conventions have to be defined for the values returned from the MenuSelect()
function. A value -1 can indicate failure; the calling routine should terminate the
program or find some way of managing without a user selection. Otherwise, the value
can be in the range 0 … N-1 if there are N entries in the options table. When
Menu selection example 383
communicating with the user, the options should be listed as 1 … N; but a zero based
representation is likely to be more convenient for the calling routine.
Hiding implementation only functions
Although there appear to be three functions, only one is of importance to other
programmers who might want to use this menu package. The "list options" and "handle
error" routines are details of the implementation and should be hidden. This can be
done in C and C++. These functions can be defined as having filescope – their names
are not known to any other part of the program.
The function prototypes can now be defined: Finalising the design
The two static functions are completely private to the implementation. Other
programmers never see these functions, so their names don't have to identify them as
belonging to this package of utility functions. Function MenuSelect() and the Text
(character array) data type are seen by other programmers so their finalised names make
clear that they belong to this package.
Implementation
Two files are needed. The "header" file describes the functions and types to other parts
of the program (and to other programmers). The implementation file contains the
function definitions.
The header file has the usual structure with the conditional compilation bracketing to
protect against being multiply included:
#endif
384 Programs with functions and arrays
The implementation file will start with #includes on necessary system files and on
UT.h. The functions need the stream i/o facilities and the iomanip formatting extras:
#include "UT.h"
Function ListOptions() simply loops through the option set, printing them with
numbering (starting at 1 as required for the benefit of users).
cin.clear();
char ch;
cin.get(ch);
if(ch == '?')
ListOptions(options, numopts);
else
cout << "Illegal input, discarded" << endl;
while(ch != '\n')
cin.get(ch);
Menu selection example 385
return 1;
}
Function MenuSelect() prints the prompt and possibly gets the options listed. It
then uses a do … while loop construct to get data. This type of loop is appropriate
here because we must get an input, so a loop that must be traversed at least once is
appropriate. If we do get bad input, choice will be zero at the end of loop body; but
zero is not a valid option so that doesn't cause problems.
The value of choice is converted back to a 0 … N-1 range in the return statement.
if(!cin.good()) {
if(!HandleError(options, numopts))
return -1;
else continue;
}
Naturally, the code has to be checked out. A test program has to be constructed and
linked with the compiled UT.o code. The test program will #include the UT.h header
file and will define an array of UT_Text data elements that are initialized to the
possible menu choices:
386 Programs with functions and arrays
#include <stdlib.h>
#include <iostream.h>
#include "UT.h"
int main()
{
int choice = UT_MenuSelect("Lifestyle?", choices, 3, 0);
switch(choice) {
case 0:
cout << "Yes, our most popular line" << endl;
break;
case 1:
cout << "Not the best choice" << endl;
break;
case 2:
cout << "Are you sure you wouldn't prefer "
"the first option?" << endl;
break;
}
return EXIT_SUCCESS;
}
When the code has been tested, the UT files can be put aside for use in other
programs that need menus.
Although not quite as common as menu selection, many programs require users to enter
keyword commands. The program will have an array of keywords for which it has
associated action routines. The user enters a word, the program searches the table to
find the word and then uses the index to select the action. If you haven't encountered
this style of input elsewhere, you will probably have met it in one of the text oriented
adventure games where you enter commands like "Go North" or "Pick up the box".
These systems generally allow the user to abbreviate commands. If the user simply
types a couple of letters then this is acceptable provided that these form a the start of a
unique keyword. If the user entry does not uniquely identify a keyword, the program
can either reject the input and reprompt or, better, can list the choices that start with the
string entered by the user.
The function PickKeyWord() will need to be given:
Pick the keyword example 387
1 an initial prompt;
print prompt
loop
read input
search array for an exactly matched keyword
if find match
return index of keyword
search array for partial matches
if none,
(maybe) warn user that there is no match
start loop again!
if single partial match,
(maybe) identify matched keyword to user
return index of keyword
list all partially matching keywords
The "search array for …" steps are obvious candidates for being promoted into Design, second
separate auxiliary functions. The search for an exact match can use the strcmp() iteration
function from the string library to compare the data entered with each of the possible
keywords. A partial match can be found using strncmp(), the version of the function
that only checks a specified number of characters; strncmp() can be called asking it to
compare just the number of characters in the partial word entered by the user.
Partial matching gets used twice. First, a search is made to find the number of
keywords that start with the characters entered by the user. A second search may then
get made to print these partial matches. The code could be organized so that a single
function could fulfil both roles (an argument would specify whether the partially
matched keywords were to be printed). However, it is slightly clearer to have two
functions.
The searches through the array of keywords will have to be "linear". The search will
start at element 0 and proceed through to element N-1.
Thus, we get to the second more detailed design:
FindExactMatch
for each keyword in array do
if strcmp() matches keyword & input
return index
return -1 failure indicator
CountPartialMatches
count = 0
388 Programs with functions and arrays
PrintPartialMatches
for each keyword in array do
if strncmp() matches input & start of keyword
print keyword
PickKeyWord
print prompt
loop
read input
mm = FindExactMatch(…)
if(mm>=0)
return mm
partial_count = CountPartialMatches(…)
if partial_count == 0,
(maybe) warn user that there is no match
start loop again!
if partial_count == 1,
(maybe) identify matched keyword to user
return index of keyword ????
PrintPartialMatches(…)
The sketch outline for the main PickKeyWord() routine reveals a problem with the
existing CountPartialMatches() function. We really need more than just a count.
We need a count and an index. Maybe CountPartialMatches() could take an extra
output argument ( int& – integer reference) that would be set to contain the index of the
(last) of the matching keywords.
Design, third We have to decide what "keywords" are, and what the prompt argument should be.
iteration The prompt could be made a UT_Text, the same "array of ≈60 characters" used in the
MenuSelect() example. The keywords could also be UT_Texts but given that most
words are less than 12 characters long, an allocation of 60 is overly generous. Maybe a
new character array type should be used
The PickKeyWord() function might as well become another "utility" function and so
can share the UT_ prefix.
Pick the keyword example 389
These functions can go in the same UT.cp file as the MenuSelect() group of
functions.
Implementation
The only point of note in the PickKeyWord() function is the "forever" loop –
for(;;) { … } . We don't want the function to return until the user has entered a valid
keyword.
int match;
int partial_count =
CountPartialMatches(keywords,
nkeys, input, match);
if(partial_count == 0) {
cout << "There are no keywords like "
<< input << endl;
continue;
}
if(partial_count == 1) {
cout << "i.e. " << keywords[match] << endl;
return match;
}
As usual, we need a simple test program to exercise the functions just coded:
#include <stdlib.h>
#include <iostream.h>
#include "UT.h"
int main()
{
cout << "You have dropped into a maze" << endl;
int quit = 0;
while(!quit) {
cout << "From this room, passages lead "
392 Programs with functions and arrays
}
cout << "Chicken, you haven't explored the full maze"
<< endl;
return EXIT_SUCCESS;
}
12.5 HANGMAN
You must know the rules of this one. One player selects a word and indicates the
number of letters, the other player guesses letters. When the second player guesses a
letter that is in the word, the first player indicates all occurrences of the letter. If the
second player guesses wrongly, the first player adds a little more to a cartoon of a
corpse hung from a gibbet. The second player wins if all letters in the word are
guessed. The first player wins if the cartoon is completed while some letters are still
not matched.
In this version, the program takes the role of player one. The program contains a
large word list from which it selects a word for the current round. It generates a display
showing the number of letters, then loops processing letters entered by the user until
either the word is guessed or the cartoon is complete. This version of the program is to
use the curses functions, and to produce a display like that shown in Figure 12.6.
Specification
Implement the hangman program; use the curses package for display and interaction.
Hangman example 393
+----------------------------------------------------------------------------+
| |
| |
| gavotte |
| ======== |
| # / | |
| # / | |
| #/ |@ |
| # --H--- |
| # - H |
| # H |
| # H |
| # | | |
| # | | |
| # | | |
| # |
| # |
| ##### |
| |
+----------------------------------------------------------------------------+
You lost?
Design
This program involves some higher level design decisions than earlier examples where Initial design
we could start by thinking how the code might work. Here, we have to make some
other decisions first; decisions about how to organize the collection of words used by
the program, and how to organize the program into files. (This is partly a consequence
of the looser specification "implement hangman"; but, generally, as programs get more
elaborate you do tend to have to consider more general issues in the design process.)
The words used by the program might be held in a data file. The program would
open and read this file at each round, picking "at random" one of the words in the file.
However, that scheme has a couple of disadvantage. The program and vocabulary file
have to be kept together. When you think how programs get shuffled around different
disks, you will see that depending on a data file in the same directory isn't wise. Also,
the scheme makes it rather difficult to "pick at random"; until you've read the file you
don't know how many choices you've got. So, you can't pick a random number and
read that many words from the file and then take the next as the word to use in a round.
The words would have to be read into an array, and then one could be picked randomly
from the array.
If the words are all going to have to be in an array anyway, we might as well have
them as an initialized array that gets "compiled" and linked in with the code. This way
avoids problems about the word file getting lost. It seems best to keep this separate
from the code, it is easier to edit small files and a long word list would get in the way.
So we could have one file "vocab.cp" that contains little more than an initialized array
with some common English words, and a compiler set count defining the number of
entries in the array.
What are "words"? They small character arrays; like the UT_Word typedef in the
last example. In fact we might as well use the UT.h header file with the definition.
394 Programs with functions and arrays
• Hangm.cp
This file will contain the main() function and all the other functions defined for the
program.
• vocab.cp
The file with the words.
• UT.h
The header file with the typedef defining a "word" as a character array.
tidy up
Given the relative complexity of the code, this is going to require breaking down into
many separate functions. Further, the implementation should be phased with parts of
the code made to work while before other parts get implemented.
The main line shown above is too complex. It should be simplified by "abstracting
out" all details of the inner loop that plays the game. This reduces it to:
main()
Initialize()
do
PlayGame()
while AnotherGame()
TidyUp()
Hangman example 395
PlayGame()
get curses window displayed
pick random word
display word as "****" at some point in window
!!!delay
!!!display actual word
The two steps marked as !!! are only for this partial implementation and will be
replaced later.
Function AnotherGame() can be coded using the CG functions like CG_Prompt()
and CG_GetChar(), so it doesn't require further decomposition into simpler functions.
However, a number of additional functions will be required to implement even the
partial PlayGame().
These functions obviously include a "display word" function and a "pick random
word" function. There is also the issue of how the program should record the details of
the word to be guessed and the current state of the users guess.
One approach would be to use two "words" – gGuess and gWord, and a couple of
integer counters. The "pick random word" function could select a word from the
vocabulary, record its length in one of the integer counters, copy the chose word into
gWord and fill gGuess with the right number of '*'s. The second integer counter would
record the number of characters matched. This could be used later when implementing
code to check whether the word has been guessed completely.
So, we seem to have:
PickRandomWord
pick random number i in range 0 ... N-1
where N is number of word in vocab
record length of vocab[i] in length
copy vocab[i] into gWord
fill gGuess with '*'s
ShowGuess
move cursor to suitable point on screen
copy characters from gGuess to screen
396 Programs with functions and arrays
ShowWord
move cursor to suitable point on screen
copy characters from gWord to screen
Final design iteration The functions identified at this point are sufficiently simple that coding should be
for simplified straightforward. So, the design of this part can be finished off by resolving outstanding
program
issues of data organization and deciding on function prototypes.
The data consist of the four variables (two words and two integer counters) that can
be made globals. If these data are global, then the prototypes for the functions
identified so far are:
int AnotherGame(void);
void Initialize(void);
void PickWord(void);
void PlayGame(void);
void ShowGuess(void);
void ShowWord(void);
int main()
External declarations The code in the main file Hangm.cp needs to reference the vocabulary array and
word count that are defined in the separate vocab.cp file. This is achieved by having
"external declarations" in Hangm.cp. An external declaration specifies the type and
name of a variable; it is basically a statement to the compiler "This variable is defined
in some other file. Generate code using it. Leave it to the linking loader to find the
variables and fill in the correct addresses in the generated code."
The project has include the files Hangm.cp (main program etc), vocab.cp (array with
words), and CG.cp (the curses functions); the header files CG.h and UT.h are also
needed in the same directory (UT.h is only being used for the definition of the UT_Word
type).
The file Hangm.cp will start with the #includes of the standard header files and
definitions of global variables. The program needs to use random numbers; stdlib
provides the rand() and srand() functions. As explained in 10.10, a sensible way of
"seeding" the random number generator is to use a value from the system's clock. The
clock functions vary between IDE's. On Symantec, the easiest function to use is
TickCount() whose prototype is in events.h; in the Borland system, either the function
time() or clock() might be used, their prototypes are in time.h. The header ctype is
#included although it isn't required in the simplified program.
#include <stdlib.h>
#include <ctype.h>
#include <string.h>
// change events.h to time.h for Borland
Hangman example 397
#include <events.h>
#include "CG.h"
#include "UT.h"
Here are the extern declarations naming the variables defined in the separate vocab.cp
file:
UT_Word gGuess;
UT_Word gWord;
int gLength;
int gMatched;
void Initialize(void)
{
CG_Initialize();
// change to srand(time(NULL)); on Borland
srand(TickCount());
}
int AnotherGame(void)
{
CG_Prompt("Another Game?");
char ch = CG_GetChar();
CG_Prompt(" ");
return ((ch == 'y') || (ch == 'Y'));
}
void ShowGuess(void)
{
const xGuess = 6;
const yGuess = 4;
CG_MoveCursor(xGuess, yGuess);
for(int i=0;i<gLength; i++)
CG_PutCharacter(gGuess[i]);
}
void ShowWord(void)
{
const xGuess = 6;
const yGuess = 4;
398 Programs with functions and arrays
CG_MoveCursor(xGuess, yGuess);
for(int i=0;i<gLength; i++)
CG_PutCharacter(gWord[i]);
}
Function PickWord() uses the random number generator to pick a number that is
converted to an appropriate range by taking its value modulo the number of possible
words. The chosen word is then copied and the guess word is filled with '*'s as
required.
void PickWord(void)
{
int choice = rand() % numwords;
gLength = strlen(vocab[choice]);
for(int i = 0; i < gLength; i++) {
gWord[i] = vocab[choice][i];
gGuess[i] = '*';
}
gWord[gLength] = gGuess[gLength] = '\0';
gMatched = 0;
}
void PlayGame(void)
{
CG_FrameWindow();
PickWord();
ShowGuess();
CG_Delay(2);
ShowWord();
return;
}
int main()
{
Initialize();
do {
Hangman example 399
PlayGame();
}
while( AnotherGame());
CG_Reset();
return 0;
}
The other source file is vocab.cp. This should have definitions of the words and the
number of words. This file needs to #include UT.h to get the declaration of type
UT_Word. The array vocab[] should contain a reasonable number of common words
from a standard dictionary such as the Oxford English Dictionary:
#include "UT.h"
UT_Word vocab[] = {
"vireo",
"inoculum",
"ossuary",
"…
"thurible",
"jellaba",
"whimbrel",
"gavotte",
"clearcole",
"theandric"
};
Although the vocabulary and number of words are effectively constant, they should not
be defined as const. In C++, const carries the implication of filescope and the linking
loader might not be able to match these names with the external declarations in the
Hangm.cp file.
The code as shown should be executable. It allows testing of the basic structure and
verifies that words are being picked randomly.
The next step of a phased implementation would be to handle the user's guesses
(without exacting any penalties for erroneous guesses).
The PlayGame() function needs to be expanded to contain a loop in which the user
enters a character, the character is checked to determine whether any additional letters
have been matched, and counts and displays are updated.
This loop should terminate when all letters have been guessed. After the loop
finishes a "You won" message can be displayed.
400 Programs with functions and arrays
The '*'s in the gGuess word can be changed to appropriate characters as they are
matched. This will make it easy to display a partially matched word as the
ShowGuess() function can be called after each change is complete.
Some additional functions are needed. Function GetGuessedCharacter() should
prompt the user for a character and get input. If the character is a letter, it should be
converted to lower case and returned. Otherwise GetGuessedCharacter() should just
loop, repeating the prompt. Function CheckCharacter() should compare the
character with each letter in the gWord word; if they match, the character should
overwrite the '*' in gGuess and a count of matches should be incremented. There will
also be a ShowResult() function that can display a message and cause a short pause.
GetGuessed charater
loop
prompt for character
get input
if letter
convert to lower case and return
CheckCharacter ch
count = 0;
for each letter in gWord
if letter == ch
count++
set character in gGuess
Show Result
display message
delay a couple of seconds
PlayGame()
CG_FrameWindow()
PickWord()
ShowGuess()
gameover = false
char GetGuessedChar();
void ShowResult(const char msg[]);
The code implementing these additional and modified function functions is straight-
forward. Function GetGuessedCharacter() uses a do … while loop to get an
alphabetic character. Function CheckChar() uses a for loop to check the match of
letters.
char GetGuessedChar()
{
CG_Prompt(" ");
CG_Prompt(">");
char ch;
do
ch = CG_GetChar();
while (!isalpha(ch));
ch = tolower(ch);
return ch;
}
void PlayGame(void)
{
CG_FrameWindow();
PickWord();
402 Programs with functions and arrays
ShowGuess();
int count = 0;
int gameOverMan = 0;
while(!gameOverMan) {
char ch = GetGuessedChar();
int matched = CheckChar(ch);
if(matched > 0) {
ShowGuess();
gMatched += matched;
}
ShowResult("You won");
return;
}
Again, this code is executable and a slightly larger part of the program can be tested.
The final phase of the implementation would deal with the programs handling of
incorrect guesses. Successive incorrect guesses result in display of successive
components of the cartoon of the hung man. The game ends if the cartoon is
completed.
The cartoon is made up out of parts: the frame, the horizontal beam, a support, the
rope, a head, a body, left and right arms, and left and right legs. Each of these ten parts
is to be drawn in turn.
The PlayGame() function can keep track of the number of incorrect guesses; the
same information identifies the next part to be drawn. Selection of the parts can be left
to an auxiliary routine – "show cartoon part".
Each part of the cartoon is made up of a number of squares that must be filled in
with a particular character. The simplest approach would appear to be to have a little
"fill squares" function that gets passed arrays with x, y coordinates, the number of
points and the character. This routine could loop using CG_PutCharacter() to display
a character at each of the positions defined in the arrays given as arguments.
Details of the individual parts are best looked after by separate routines that have
their own local arrays defining coordinate data etc.
Thus, this phase of the development will have to deal with the following functions:
Fill squares
loop
Hangman example 403
Show Frame
call Fill squares passing that function arrays
defining the points that make up the frame
Show Head
call Fill squares passing that function arrays
defining the points that make up the head
PlayGame()
CG_FrameWindow()
PickWord()
ShowGuess()
gameover = false
count = 0;
while not gameover
Get guessed character
matched = CheckCharacter
if(matched > 0)
ShowGuess
gMatched += matched
else
ShowCartoonPart
count++
The extra routines that get the cartoon parts drawn are all simple, only a couple of
representatives are shown:
void ShowBeam(void)
{
int x[] = { 51, 52, 53, 54, 55, 56, 57, 58 };
int y[] = { 5, 5, 5, 5, 5, 5, 5, 5 };
int n = sizeof(x) / sizeof(int);
FillSquares('=', n, x, y);
}
void ShowHead(void)
{
int x[] = { 59 };
int y[] = { 8 };
int n = sizeof(x) / sizeof(int);
FillSquares('@', n, x, y);
}
void PlayGame(void)
{
CG_FrameWindow();
PickWord();
ShowGuess();
int count = 0;
int gameOverMan = 0;
while(!gameOverMan) {
char ch = GetGuessedChar();
int matched = CheckChar(ch);
if(matched > 0) {
ShowGuess();
gMatched += matched;
}
else {
ShowCartoonPart(count);
count++;
}
gameOverMan = (count >= kNMOVES) ||
(gMatched == gLength);
}
(The constant kNMOVES = 10 gets added to the other constants at the start of the file.)
You will find that most of the larger programs that you must write will need to be Phased
implemented in phases as was done in this example. implementation
strategy
12.6 LIFE
Have you been told "You are a nerd – spending too much time on your computer."?
Have you been told "Go out and get a life."?
OK. Please everyone. Get a Life in your computer – Conway's Life.
Conway's Life is not really a computer game. When it was first described in
Scientific American it was introduced as a "computer recreation" (Sci. Am. Oct 1970,
Feb 1971). The program models a "system" that evolves in accord with specified rules.
The idea of the "recreation" part is that you can set up different initial states for this
"system" and watch what happens.
406 Programs with functions and arrays
The system consists of a two-dimensional array of cells. This array is, in principle,
infinite in its dimensions; of course for a computer program you must chose a finite
size. Cells contain either nothing, or a "live organism". Each cell (array element) has
eight neighbors – three in the row above, three in the row below, a left neighbor and a
right neighbor.
Evolution proceeds one generation at a time. The rules of the recreation specify
what happens to the contents of each cell at a change of generation. The standard rules
are:
4 An empty cell is filled with a new live organism if it had exactly three neighbors.
These deaths and births are coordinated. They happen simultaneously (this affects how
the simulation may be programmed).
The recreation is started by assigning an initial population of organisms to selected
cells. Most distributions are uninteresting. The majority of the population quickly die
out leaving little groups of survivors that cling tenaciously to life. But some patterns
have interesting behaviours. Figure 12.7 shows three examples – "Cheshire Cat",
"Glider", and "Tumbler"; there are others, like "glider guns" and "glider eaters" that are
illustrated in the second of the two Scientific American articles. A "Glider" moves
slowly down and rightwards; a "Cheshire Cat fades away to leave just a grin".
Specification
Implement a version of Conway's Life recreation. The program should use the curses
package for display. It should start with some standard patterns present in the array.
The program should model a small number of generational turns, then ask the user
whether to continue or quit. The modelling and prompting process continues until the
user enters a quit command.
Design
This program will again have multiple source files – Life.cp and CG.cp (plus the CG.h First Iteration
header). All the new code goes in the Life.cp file. The code that scans the array and
sorts out which cells are "live" at the next generation will have some points in common
with the "heat diffusion" example.
The overall structure of the program will be something like:
As always, some steps are obvious candidates for being promoted into separate
functions. These "obvious" functions include an initialization function, a to set an
initial configuration, a display function, a "run" function that looks after the loops, and a
function to step forward one generation.
The "life arrays" will be similar to the arrays used in the heat diffusion example. As
in that example, there have to be two arrays (at least while executing the function that
computes the new state). One array contains the current state data; these data have to be
analyzed to allow new data to be stored in the second array. Here the arrays would be
character arrays (really, one could reduce them to "bit arrays" but that is a lot harder!).
A space character could represent and empty cell, a '*' or '#' could mark a live cell.
This example can illustrate the use of a "three dimensional array"! We need two
nxm arrays (current generation, next generation). We can define these as a single data
aggregate:
char gWorlds[2][kROWS][kCOLS];
408 Programs with functions and arrays
We can use the two [kROWS][kCOLS] subarrays alternately. We can start with the
initial life generation in subarray gWorlds[0], and fill in gWorlds[1] with details for
the next generation. Subarray gWorlds[1] becomes the current generation that gets
displayed. At the next step, subarray gWorlds[0] is filled in using the current
gWorlds[1] data. Then gWorlds[0] is the current generation and is displayed. As
each step is performed, the roles of the two subarrays switch. All the separate functions
would share the global gWorlds data aggregate.
Second iteration Each function has to be considered in more detail, possibly additional auxiliary
through design functions will be identified. The initial set of functions become:
process
Initialize
fill both gWorld[] subarrays with spaces
(representing empty cells)
initialize curses routines
Display state
double loop through "current gWorld" using
curses character display routines to plot spaces
and stars
Run
Display current state
loop
ask user whether to quit,
and break loop if quit command entered
loop ≈ 5 times
step one generation forward
display new sate
Step
?
main()
Initialize
Set Starting Configuration
Run
reset curses
This second pass through the program design revealed a minor problem. The
specification requires "some standard patterns present in the array"; but doesn't say
what patterns! It is not unusual for specifications to turn out to be incomplete. Usually,
the designers have to go back to the people who commissioned the program to get more
Conway's "Life" example 409
details specified. In a simple case, like this, the designers can make the decisions – so
here "a few standard patterns" means a glider and something else.
There are still major gaps in the design, so it is still too early to start coding.
The "standard patterns" can be placed in much the same way as the "cartoon
components" were added in the Hangman example. A routine, e.g. "set glider", can
have a set of x, y points that it sets to add a glider. As more than one glider might be
required, this routine should take a pair of x, y values as an origin and set cells relative
to the origin.
Some of the patterns are large, and if the origin is poorly chosen it would be easy to
try to set points outside the bounds of the array. Just as in the heat diffusion example
where we needed a TemperatureAt(row, column) function that checked the row and
column values, we will here need a SetPoint(row, column) that sets a cell to live
provided that the row and column are valid.
These additions make the "set starting configuration" function into the following
group of functions:
Step
identify which gWorld is to be filled in
410 Programs with functions and arrays
The Step() function and the display function both need to know which of the
gWorlds is current (i.e. is it the subarray gWorld[0] or gWorld[1]). This information
should be held in another global variable gCurrent. The Step() function has to use
the value of this variable to determine which array to fill in for the next generation, and
needs to update the value of gCurrent once the new generation has been recorded.
We also need a "count live neighbors" function. This will be a bit like the "Average
of neighbors" function in the Heat Diffusion example. It will use a double loop to run
through nine elements (a cell and its eight neighbors) and correct for the cell's own
state:
void DisplayState(void);
void Initialize(void);
void Run(void);
void SetStartConfig(void);
void Step(void);
int main();
Implementation
For the most part, the implementation code is straightforward. The files starts with the
#includes; in this case just ctype.h (need tolower() function when checking whether
user enters "quit command") and the CG header for the curses functions. After the
#includes, we get the global variables (the three dimensional gWorld aggregate etc) and
the constants.
#include <ctype.h>
#include "CG.h"
char gWorlds[2][kROWS][kCOLS];
int gCurrent = 0;
void Initialize(void)
{
for(int w=0; w < 2; w++)
for(int r = 0; r < kROWS; r++)
for(int c = 0; c < kCOLS; c++)
gWorlds[w][kROWS][kCOLS] =
EMPTY;
CG_Initialize();
}
void DisplayState(void)
{
for(int r = 0; r < kROWS; r++)
for(int c = 0; c < kCOLS; c++)
CG_PutCharacter(gWorlds[gCurrent][r][c],
c+1, r+1 );
}
Function Get() looks indexes into the current gWorld subarray. If "wrapping" was
needed, a row value less than 0 would have kROWS added before accessing the array;
similar modifications in the other cases.
Function Live() is another very small function; quite justified, its role is different
from Get() even if it is the only function here that uses Get() they shouldn't be
combined.
return gWorlds[gCurrent][row][col];
}
{
int count = -Live(row,col); // Don't count self!
for(int r = row - 1; r <= row + 1; r++)
for(int c = col - 1; c <= col + 1; c++)
count += Live(r,c);
return count;
}
Function Step() uses a "tricky" way of identifying which gWorld subarray should
be updated. Logically, if the current gWorld is subarray [0], then subarray gWorld[1]
should be changed; but if the current gWorld is [1] then it is gWorld[0] that gets
changed. Check the code and convince yourself that the funny modulo arithmetic
achieves this:
void Step(void)
{
int Other = (gCurrent + 1) % 2;
for(int r = 0; r < kROWS; r++)
for(int c = 0; c < kCOLS; c++) {
int nbrs = CountNbrs(r,c);
switch(nbrs) {
case 2:
gWorlds[Other][r][c] =
gWorlds[gCurrent][r][c];
break;
case 3:
gWorlds[Other][r][c] = LIVE;
break;
default:
gWorlds[Other][r][c] = EMPTY;
}
}
gCurrent = Other;
}
Function SetPoint() makes certain that it only accesses valid array elements. This
function might need to be changed if "wrapping" were required.
gWorlds[gCurrent][row][col] = LIVE;
414 Programs with functions and arrays
Function SetGlider() uses the same approach as the functions for drawing cartoon
parts in the Hangman example:
void SetStartConfig(void)
{
SetGlider(4,4);
…
}
Function Run() handles the user controlled repetition and the inner loop advancing
a few generation steps:
void Run(void)
{
char ch;
DisplayState();
int quit = 0;
for(;;) {
CG_Prompt("Cycle (C) or Quit (Q)?");
ch = CG_GetChar();
ch = tolower(ch);
if(ch == 'q')
break;
CG_Prompt(" ");
int main()
{
Initialize();
SetStartConfig();
Conway's "Life" example 415
Run();
CG_Reset();
return 0;
}
EXERCISES
1 Use the curses package to implement a (fast) clock:
.........*****.........
.....*...........*.....
...*........#.......*..
.*..........#........*.
*..........#..........*
*..........#..........*
*...........@.........*
*............@........*
.*............@......*.
...*...........@....*..
.....*..........@*.....
.........*****.........
The "minutes" hand should advance every second, and the "hours" hand every minute.
2 In his novel "The Day of the Triffids", John Wyndham invented triffids – hybrid creatures
that are part plant, part carnivore. Triffids can't see, but they are sensitive to sound and
vibrations. They can't move fast, but they can move. They tend to move toward sources of
sounds – sources such as animals. If a triffid gets close enough to an animal, it can use a
flexible vine- like appendage to first kill the animal with a fast acting poison, and then draw
nutrients from the body of the animal
At one point in the story, there is a blind man in a field with a triffid. The man is lost
stumbling around randomly. The triffid moves (at less than one fifth of the speed of the
man), but it moves purposively toward the source of vibrations – the triffid is hunting its
lunch.
Provide a graphical simulation using the curses library.
+----------------------------------------------+
| |
| |
| T |
| m |
| |
| |
| |
| |
+----------------------------------------------+
3 Somewhere, on one of the old floppies lost in your room is a copy of a 1980s computer game
"Doctor Who and the Daleks". Don't bother to search it out. You can now write your own
version.
The game consist of a series of rounds with the human player controlling the action of
Dr. Who, and the program controlling some number of Daleks. A round is comprised on
many turns. The human player and the program take alternate turns; on its turn the program
416 Programs with functions and arrays
will move all the Daleks. Things (Daleks and Dr) can move one square vertically,
horizontally, or diagonally. Nothing can move out of the playing area. The display is
redrawn after each turn. Dr. Who's position is marked by a W, the Daleks are Ds, and
wrecked Daleks are #s.
+----------------------------------------------+
| |
| |
| D D |
| W # |
| D |
| |
| |
| |
+----------------------------------------------+
Command>
The human player can enter commands that move Dr. Who (these are one character
commands, common choices of letter and directions are 'q' NW, 'w' N, 'e' NE, 'a' W, … with
North being up the screen).
If a Dalek moves onto the square where Dr. Who is located, the game ends. If a Dalek
moves onto a square that is occupied, it is wrecked and does not participate further in the
round. The wreckage remains for the rest of that round; other Daleks may crash into
wreckage and destroy themselves. Dr. Who can not move onto a square with wreckage.
The Daleks' moves are determined algorithmically. Each Dalek will move toward the
current position of Dr. Who. The move will be vertical, horizontal, or diagonal; it is chosen
to minimize the remaining distance from the Dalek's new position to that of Dr. Who.
A round of the game starts with Dr. Who and some number of Daleks being randomly
placed on a clear playing area. (There are no constraints, Dr. Who may discover that he is
totally surrounded by Daleks; he may even start dead if a Dalek gets placed on his square.)
The human player's winning strategy is to manoeuvre Dr. Who so as to cause the
maximum number of collisions among Daleks.
The player has two other options. Dr. Who may be teleported to some random location
(command 't') so allowing escape from converging Daleks. There are no guarantees of safe
arrival; he may rematerialise on wreckage (and be killed), on a Dalek (and again be killed), or
next to a Dalek (and be killed when the Dalek moves). Once per round, Dr. Who may use a
"sonic screwdriver" that destroys Daleks in the immediate vicinity (immediate? radius 2,
radius 3, depends how generous you feel).
If Dr. Who survives a round in which all Daleks are destroyed, a new round starts.
Each new round increases the number of Daleks up to a maximum. Initially, there are three
Daleks, the maximum depends on the size of the playing area but should be around 20.
4. The professor who commissioned the π-cannon is disappointed at the rate of convergence and
believes that the cannoneers are doing something wrong.
Combine the code from the π-cannon example with the curses package to produce a
visual display version that the professor may watch:
The program should show the cannon ball moving from gun barrel to point of impact (getting
the successive x positions is easy, you need to do a bit of trigonometry to get the y positions).
Conway's "Life" example 417
+----------------------------------------------+
| |
| * * |
| * |
| -- + * |
| ----- * |
| -- ** |
| |
| |
+----------------------------------------------+
5. Modify Conway's Life so that the playing area is "wrapped" (i.e. gliders moving off the top of
the play area reappear at the bottom etc.)
In "Mastermind", the program picks a four digit (or four letter code), e.g. EAHM. The
human player tries to identify the code in the minimum number of guesses. The player enters
a guess as four digits or letters. The program indicates 1) how many characters are both
correct and in the correct place, and 2) how many characters are correct but in the wrong
place (these reports do not identify which characters are the correct ones). The player is to
use these clues to help make subsequent guesses.
Rules vary as to whether you can have repeated characters. Make up your own rules.
The "refresh" rate when drawing to a window is poor in the curses package given in the text.
You can make things faster by arranging that the curses package make use of a character
array screen[kHEIGHT][kWIDTH]. This is initialized to contain null characters. When
characters are drawn, a check is made against this array. If the array element corresponding
to the character position has a value that differs from the required character, then the required
character is drawn and stored in the array. However, if the array indicates that the character
on the screen is already the required character then no drawing is necessary.
8. Combine the Life program with the initial curses drawing program to produce a system where
a user can sketch an initial Life configuration and then watch it evolve.
418 Programs with functions and arrays
13
13 Standard algorithms
Each new program has its own challenges, its own unique features. But the same old
subproblems keep recurring. So, very often at some point in your program you will
have to search for a key value in an ordered collection, or sort some data. These
subproblems have been beaten to death by generations of computer theoreticians and
programmers. The best approaches are now known and defined by standard algorithms.
This chapter presents four of these. Section 13.1 looks at searching for key values.
The next three sections explore various "sorting" algorithms.
The study of algorithms once formed a major part of sophomore/junior level studies
in Computer Science. Fashions change. You will study algorithms in some subsequent
subjects but probably only to a limited extent, and mainly from a theoretical perspective
while learning to analyse the complexity of programs.
The code in the "keyword picker' (the example in section 12.4) had to search linearly
through the table of keywords because the words were unordered. However, one often
has data that are ordered. For instance you might have a list of names:
If you are searching for a particular value, e.g. Sreckovic, you should be able to take
advantage of the fact that the data are ordered. Think how you might look up the name
420 Standard Algorithms
in a phone book. You would open the book in the middle of the "subscriber" pages to
find names like "Meadows". The name you want is later in the alphabet so you pick a
point half way between Meadows and Zylstra, and find a name like Terry. Too far, so
split again between Meadows and Terry to get to Romanows. Eventually you will get
to Sreckovic. May take a bit of time but it is faster than checking of Aavik, Abbas,
Abbot, ….
Subject to a few restrictions, the same general approach can be used in computer
programs. The restrictions are that all the data must be in main memory, and that it be
easy to work out where each individual data item is located. Such conditions are easily
satisfied if you have something like an ordered array of names. The array will be in
memory; indexing makes it easy to find a particular element. There are other situations
where these search techniques are applicable; we will meet some in Part IV when we
look at some "tree structures".
The computer version of the approach is known as "binary search". It is "binary"
because at each step, the portion of the array that you must search is cut in half ("bi-
sected"). Thus, in the names example, the first name examined ("Meadows")
established that the name sought must be in the second half of the phone book. The
second test (on the name "Terry") limited the search to the third quarter of the entire
array.
The binary search algorithm for finding a key value in an array uses two indices that
determine the array range wherein the desired element must be located. (Of course, it is
possible that the element sought is not present! A binary search routine has to be able
to deal with this eventuality as well.) If the array has N elements, then these low and
high indices are set to 0 and N-1.
The iterative part involves picking an index midway between the current low and
high limits. The data value located at this midway point is compared with the desired
key. If they are equal, the data has been found and the routine can return the midway
value as the index of the data.
If the value at the midway point is lower than that sought, the low limit should be
increased. The desired data value must be located somewhere above the current
midway point. So the low limit can be set one greater than the calculate midway point.
Similarly, if the value at the midway point is too large, the high limit can be lowered so
that it is one less than the midway point.
Each iteration halves the range where the data may be located. Eventually, the
required data are located (if present in the array). If the sought key does not occur in
the array, the low/high limits will cross (low will exceed high). This condition can be
used to terminate the loop.
A version of this binary search for character strings is:
Binary search 421
while(low<=high) {
int midpoint = (low + high) / 2;
int result = strcmp(key, theNames[midpoint]);
if(result == 0)
return midpoint;
if(result < 0)
high = midpoint - 1;
else
low = midpoint + 1;
}
return -1;
}
The function returns the index of where the given key is located in the array argument.
The value -1 is returned if the key is not present. The strings are compared using
strcmp() from the string library. Versions of the algorithm can be implemented for
arrays of doubles (the comparison test would then be expressed in terms of the normal
greater than (>), equals (==) and less than (<) operators).
Binary search is such a commonly used algorithm that it is normally packaged in
stdlib as bsearch() . The stdlib function is actually a general purpose version that can
be used with arrays of different types of data. This generality depends on more
advanced features of C/C++.
The binary search problem fits well with the "recursive" model of problem solving
(section 4.8). You can imagine the binary search problem being solved by a
bureaucracy of clerks. Each clerk follows the rules:
The "setting up" int RecBinarySearch(const Name key, const Name theNames[],
function int num, )
{
return AuxRecBinSearch(key, theNames, 0, num-1);
}
In this case, the recursive solution has no particular merit. On older computer
architectures (with expensive function calls), the iterative version would be much faster.
On a modern RISC architecture, the difference in performance wouldn't be as marked
but the iterative version would still have the edge.
Essentially, binary search is too simple to justify the use of recursion. Recursive
styles are better suited to situations when there is reasonable amount of work to do as
the recursion is unwound (when you have to do something to the result returned by the
Recursive implementation of binary search 423
clerk to whom you gave a subproblem) or where the problem gets broken into separate
parts that both need to be solved by recursive calls.
The introduction to this section argued that binary search was going to be better than
linear search. But how much better is it?
You can get an idea by changing the search function to print out the names that get
examined. You might get output something like:
Meadows
Terry
Romanows
Smith
Stone
Sreckovic
In this case, six names were examined. That is obviously better than checking "Aavik",
"Abbas", "Abbot", "Abet", …; because the linear search would involve examination of
hundreds (thousands maybe) of names.
Of course, if you wanted the name Terry, you would get it in two goes. Other names
might take more than six steps. To get an idea of the cost of an algorithm, you really
have to consider the worst case.
The binary search algorithm can be slightly simplified by having the additional
requirement that the key has to be present. If the array has only two elements, then a
test against element [0] determines the position of the key; either the test returned
"equals" in which case the key is at [0], or the key is at position [1]. So one test picks
from an array size of 2. Two tests suffice for an array size of 4; if the key is greater
than element [1] then a second test resolves between [2] and [3]. Similarly, three
tests pick from among 8 (or less elements).
In general, k tests are needed to pick from 2k items. So how many tests are needed to
pick from an array of size N?
You need:
424 Standard Algorithms
2k ≥ N
This equation can be solved for k. The equation is simplified by taking the logarithm of
both sides:
k log(2) = log(N)
You can have logarithms to the base 10, or logs to any base you want. You can have
logarithms to the base 2. Now log2 (2) is 1. This simplifies the equation further:
The cost of the search is proportional to the number of tests, so the cost of binary
search is going to increase as O(lg(N)). Now a lg(N) rate of growth is a lot nicer than
linear growth :
10 3.3
50 5.6
100 6.6
250 7.9
500 8.9
1000 9.9
5000 12.2
10000 13.2
50000 15.6
100000 16.6
500000 18.9
1000000 19.9
A linear search for a key in a set of a million items could (in the worst case) involve you
in testing all one million. A binary search will do it in twenty tests.
Of course, there is one slight catch. The data have to be in order before you can do
binary search. You are going to have to do some work to get the data into order.
Sometimes, you get data that are relatively easy to put into order. For example,
consider a class roll ordered by name that identifies pupils and term marks (0 … 100):
Armstrong, Alice S. 57
Azur, Hassan M. 64
Bates, Peter 33
…
Yeung, Chi Wu 81
Young, Paula 81
Establishing order 425
A common need would be a second copy of these data arranged in increasing order by
mark rather than by name.
You can see that a pupil with a mark of 100 would be the last in the second
reordered array, while any getting 0 would be first. In fact, you can work out the
position in the second array appropriate for each possible mark.
This is done by first checking the distribution of marks. For example, the lowest
recorded mark might be 5 with two pupils getting this mark; the next mark might be 7,
then 8, each with one pupil. Thus the first two slots in the reordered copy of the array
would be for pupils with a mark of 5, the next for the pupil with mark 7, and so on.
Once this distribution has been worked out and recorded, you can run through the
original list, looking at each pupil's mark and using the recorded details of the
distribution to determine where to place that pupil in the reordered array. The following
algorithm implements this scheme:
1. Count the number of pupils with each mark (or, more generally, the number of
"records" with each "key").
These counts have to be stored in an array whose size is determined by the range of
key values. In this case, the array has 101 elements (mark_count[0] …
mark_count[100]). The data might be something like
Mark 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, …
mark_count 0, 0, 0, 0, 0, 2, 0, 1, 1, 0, …
2. Change the contents of this mark_count array so that its elements record the
number of pupils with marks less than or equal to the given mark.
for(i=1; i<101;i++)
mark_count[i] += mark_count[i-1];
Mark 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, …
mark_count 0, 0, 0, 0, 0, 2, 2, 3, 4, 4, …
3. Run through the array of pupils, look up their mark in the mark_count array. This
gives their relative position in the reordered array (when implementing, you have to
remember C's zero-based arrays where position 2 is in [1]). The data should be
copied to the other array. Then the mark_count entry should be reduced by one.
For example, when the first pupil with mark 5 is encountered, the mark_count
array identifies the appropriate position as being the 2nd in the reordered array (i.e.
element [1]). The mark_count array gets changed to:
426 Standard Algorithms
mark_count 0, 0, 0, 0, 0, 1, 2, 3, 4, 4, …
so, when the next pupil with mark 5 is found his/her position can be seen to be the
first in the array (element [0]).
Turn counts into // Make that count of number of pupils with marks less than
"positions" // or equal to given mark
Note how both data elements, name and mark, have to be copied; we have to keep
names and marks together. Since arrays can not be assigned, the strcpy() function
must be used to copy a Name.
The function can be tested with the following code:
#include <iostream.h>
#include <iomanip.h>
Establishing order 427
#include <string.h>
Name pupils[] = {
"Armstrong, Alice S.",
"Azur, Hassan M.",
"Bates, Peter",
…
…
"Ward, Koren",
"Yeung, Chi Wu",
"Young, Paula",
"Zarra, Daniela"
};
int marks[] = {
57, 64, 33, 15, 69, 61, 58,
…
45, 81, 81, 78
};
int main()
{
Name ordnms[500];
int ordmks[500];
MakeOrderedCopy(pupils, marks, num, ordnms, ordmks);
cout << "Class ordered by names: " << endl;
for(int i = 0; i< num; i++)
cout << setw(40) << pupils[i] << setw(8)
<< marks[i] << endl;
cout << "\n\nOrdered by marks:" << endl;
for( i = 0; i< num; i++)
cout << setw(40) << ordnms[i] << setw(8)
<< ordmks[i] << endl;
return 0;
}
Zarra, Daniela 78
Ordered by marks:
McArdie, Hamish 5
Hallinan, Jack 5
…
…
Young, Paula 81
Yeung, Chi Wu 81
Horneman, Sue 87
Goodman, Jerry 91
These example data illustrate another minor point. The original list had Yeung and
Young in alphabetic order, both with mark 81. Since their marks were equal, there was
no need to change them from being in alphabetic order. However the code as given
above puts pupils with the same mark in reverse alphabetic order.
"Stable sorts" Sometimes, there is an extra requirement: the sorting process has to be "stable".
This means that records that have the same key values (e.g. pupils with the same mark)
should be left in the same order as they originally were.
It is easy to change the example code to achieve stability. All you have to do is
make the final loop run backwards down through the array:
Implementation This implementation is of course very specific to the given problem (e.g. the
limitations mark_count array is explicitly given the dimension 101, it does not depend on a data
argument). The function can be made more general but this requires more advanced
features of the C/C++ language. Nevertheless, the code is simple and you should have
no difficulty in adapting it to similar problems where the items to be sorted have a
narrow range of keys and you want two copies of the data.
To get an idea of the cost of this sorting mechanism, you have to look at the number of
times each statement gets executed. Suppose that num, the number of data elements,
was 350, then the number of times that each statement was executed would be as shown
below:
Establishing order 429
The costs of these loops is directly proportional to the number of times they are
executed. So the cost of this code is going to be proportional to the number of entries in
the array (or the range of the key if this was larger, the '101' loops would represent the
dominant cost if you had less than 100 records to sort).
This algorithm is O(N) with N the number of items to sort. That makes it a
relatively cheap algorithm.
The last example showed that in some special cases, you could take data and get them
sorted into order in "linear time". However, the approach used is limited.
There are two limitations. Firstly, it made a sorted copy of the data so that you had Normally require
to sufficient room in the computer's memory for two sets of data (the initial set with the sorting "in situ"
pupils arranged in alphabetic order, and the sorted set with them arranged by mark).
This is usually not acceptable. In most cases where it is necessary to sort data, you can't
have two copies of the entire data set. You may have one or two extra items (records)
430 Standard Algorithms
but you can't duplicate the whole set. Data have to be sorted "in situ" (meaning "in
place"); the original entries in the data arrays have to be rearranged to achieve the
required order.
Small range of key The other simplifying factor that does not usually apply was the limited range of the
values keys. The keys were known to be in the range 0…100. This made it possible to have
the mark_count[101] array. Usually the keys span a wider range, e.g. bank accounts
can (I suppose) have any value from -2000000000 to +2000000000. You can't allocate
an array equivalent to mark_count[101] if you need four thousand million entires.
When you don't have the special advantages of data duplication and limited key
range, you must use a general purpose sorting function and you will end up doing a lot
more work.
The late 1950s and early 1960s was a happy time for inventors of sorting algorithms.
They invented so many that the topic has gone quite out of fashion. There is a whole
variety of algorithms from simple (but relatively inefficient) to complex but optimized.
There are also algorithms that define how to sort data collections that are far too large to
fit into the memory of the computer . These "external sorts" were important back when
a large computer had a total of 32768 words of memory. Now that memory sizes are
much larger, external sorts are of less importance; but if you ever do get a very large set
of data you can dig out those old algorithms, you don't have to try to invent your own.
There is a fairly intuitive method for sorting the data of an array into ascending
order. The first step is to search through the array so as to select the smallest data
value; this is then exchanged for the data element that was in element [0]. The
second step repeats the same process, this time searching the remaining unsorted
subarray [1]…[N-1] so as to select its smallest element, which gets swapped for the
data value originally in [1]. The selection and swapping processes are repeated until
the entire array has been processed.
A version of this code, specialized to handle an array of characters is:
The old cliche states "A picture is worth a thousand words"; so how about a few
pictures to illustrate how this sort process works. The following program uses the
curses package to draw pictures illustrating the progress of the sort:
A simple sort 431
#include "CG.h"
char testdata[] = {
'a', 'r', 'g', 'b', 'd',
'i', 'q', 'j', 'm', 'o',
't', 'p', 'l', 's', 'n',
'f', 'c', 'k', 'e', 'h'
};
int n = sizeof(testdata) /sizeof(char);
void ShowArray()
{
CG_ClearWindow();
for(int i=0; i < n; i++) {
int x = i + 1;
int y = 1 + testdata[i] - 'a';
CG_PutCharacter(testdata[i], x, y);
}
CG_Prompt("continue>");
char ch = CG_GetChar();
}
void SelectionSort(char data[], int n)
{
ShowArray();
for(int i = 0; i < n-1; i++) {
int min = i;
for(int j=i+1; j<n; j++)
if(data[j] < data[min]) min = j;
char temp = data[min];
data[min] = data[i];
data[i] = temp;
ShowArray();
}
}
int main()
{
CG_Initialize();
SelectionSort(testdata, n);
CG_Reset();
return 0;
}
The cost is determined by the number of data comparisons and data exchanges that
must be done in that inner loop. If there are N elements in the array, then:
432 Standard Algorithms
i iterations of j loop
0 N-1
1 N-2
2 N-3
… …
N-2 1
Each iteration of the j loop involves a comparison (and, if you are unlucky, an
assignment). The cost will be proportional to the sum of the work of all the j loops:
∑
i = N −1
cost ∝
i =1
( N − i)
It is an arithmetic progression and its sum is "first term plus last term times half the
number of terms:
(N-1 + 1) (N-1)/2
0.5N2 - 0.5N
An N2 cost The cost of this sorting process increases as the square of the number of elements.
Quicksort 433
An N2 sorting process is not that wildly attractive; doubling the number of elements
increases the run time by a factor of four. Since you know that a data element can be
found in an ordered array in time proportional to lg(N), you might reasonably expect a
sort process to be cheaper than N 2 . After all, you can think of "sorting" as a matter of
adding elements to an ordered set; you have N elements to add, finding the right place
for each one should be lg(N), so an overall cost of Nlg(N) seems reasonable.
That argument is a little spurious, but the conclusion is correct. Data can be sorted
in Nlg(N) time. There are some algorithms that guarantee an Nlg(N) behaviour. The
most popular of the sophisticated sorting algorithms does not guarantee Nlg(N)
behaviour; in some cases, e.g. when the data are already more or less sorted, its
performance deteriorates to N 2 . However, this "Quicksort" algorithm usually runs well.
1 If you are given an array with one element, say that you have sorted it.
2 If you are given an array with several data elements, "shuffle them" into two
groups – one group containing all the "large" values and the other group with all
the "small" values. Pass these groups to two colleagues; one colleague sorts the
"large" values, the other sorts the "small" values.
Ideally, each clerk breaks the problem down into approximately equal sized parts so
that the next two clerks get similar amounts of work. So, if the first clerk in a group is
given the data:
37, 21, 12, 103, 82, 97, 64, 62, 62, 28, 44, 50, 33, 91
and
In the example just shown, the clerk knew "intuitively" that the best partitioning
value would be around 60 and moved values less than this into the "small" group with
the others in the "large" group. Usually, the best partitioning value is not known. The
scheme works even if a non-optimal value is used. Suppose that the first clerk picked
37, then the partitions could be:
and
The first partition contains the values less than 37, the second contains all values greater
than or equal to 37. The partitions are unequal in size, but they can still be passed on to
other clerks to process.
The entire scheme for dealing with the data is shown in Figure 13.2. The pattern of
partitions shown reflects the non-optimal choice of partitioning values. In this example,
it is quite common for the partitioning to separate just one value from a group of several
others rather than arranging an even split. This necessitates extra partitioning steps later
on. It is these deviations from perfect splitting that reduce the efficiency of Quicksort
from the theoretical Nlg(N) optimum.
How should the data be shuffled to get the required groups?
You start by picking the value to be used to partition the data, and for lack of better
information you might as well take the first value in your array, in this case 37. Then
you initialize two "pointers" – one off the left of the array, and one off the right end of
the array:
37, 21, 12, 103, 82, 97, 64, 62, 62, 28, 44, 50, 33, 91
Left ⇑ ↑Right
Move the "right" pointer to the left until its pointing to a data value less than or equal to
the chosen partitioning value:
37, 21, 12, 103, 82, 97, 64, 62, 62, 28, 44, 50, 33, 91
Left ⇑ ↑Right
Next, move the "left" pointer to the right until its pointing to a data value greater than or
equal to the partitioning value:
37, 21, 12, 103, 82, 97, 64, 62, 62, 28, 44, 50, 33, 91
Left ⇑ ↑Right
You have now identified a "small" value in the right hand part of the array, and a
"large" value in the left hand part of the array. These are in the wrong parts; so
exchange them:
Quicksort 435
37, 21, 12, 103, 82, 97, 64, 62, 62, 28, 44, 50, 33, 91
1
33, 21, 12, 28
--------------
82, 97, 64, 62, 62, 103, 44, 50, 37, 91
9
2
28, 21, 12 37, 50, 64, 62, 62, 44
---------- ----------------------
33 103, 97, 82, 91
8 10
21
3
12, 21 33 37 91, 97, 82
----- -- ----------
28 50, 64, 62, 62, 44 103
7
4 11 12 22 27
12 28 37 44 82 103
-- -- --
21 64, 62, 62, 50 97, 91
5 14 23 24
6 13
12 21 44 50, 62, 62 82 91
---------- --
64 97
20
15 25 26
50 64 91 97
--
62, 62
16 17
50 62
--
62
18 19
62 62
12 21 28 33 37 44 50 62 62 64 82 91 97 103
Figure 13.2 A bureaucracy of clerks "Quicksorts" some data. The circled numbers
identify the order in which groups of data elements are processed.
436 Standard Algorithms
33, 21, 12, 103, 82, 97, 64, 62, 62, 28, 44, 50, 37, 91
Left ⇑ ↑Right
Do it again. First move the right pointer down to a value less than or equal to 37;
then move the left pointer up to a value greater than or equal to 37:
33, 21, 12, 103, 82, 97, 64, 62, 62, 28, 44, 50, 37, 91
Left ⇑ ↑Right
Once again, these data values are out of place, so they should be exchanged:
33, 21, 12, 28, 82, 97, 64, 62, 62, 103, 44, 50, 37, 91
Left ⇑ ↑Right
Again move the right pointer down to a value less than 37, and move the left pointer
up to a greater value:
33, 21, 12, 28, 82, 97, 64, 62, 62, 103, 44, 50, 37, 91
↑Right
Left ⇑
In this case, the pointers have crossed; the "right" pointer is to the left of the "left"
pointer. When this happens, it means that there weren't any more data values that could
be exchanged.
The "clerk" working on this part of the problem has finished; the rest of the work
should be passed on to others. The data in the array up to the position of the "right"
pointer are those values less than the value that the clerk chose to use to do the
partitioning; the other part of the array holds the larger values. These two parts of the
array should be passed separately to the next two clerks.
The scheme works. The rules are easy enough to be understood by a bureaucracy of
clerks. So they are easy enough to program.
13.4.2 An implementation
The recursive Quicksort function takes as arguments an array, and details of the range
that is to be processed:
The details of the range will be given as the indices of the leftmost (low) and rightmost
(high) data elements. The initial call will specify the entire range, so if for example you
had an array data[100], the calling program would invoke the Quicksort() function
as:
Quicksort(data, 0, 99);
Quicksort 437
The Partition() function should return details of where the partition point is located.
This "split point" will be the index of the array element such that d[left] …
d[split_point] all contain values less than the value chosen to partition the data
values. Function Partition() has to have some way of picking a partitioning value;
it can use the value in the leftmost element of the subarray that it is to process.
The code for Quicksort() itself is nice and simple:
If the array length is 1 (left == right), then nothing need be done; an array of one
element is sorted. Otherwise, use Partition() to shuffle the data and find a split
point and pass the two parts of the split array on to other incarnations of Quicksort().
The Partition() function performs the "pointer movements" described in the
algorithm. It actually uses to integer variables (lm = left margin pointer, and rm = right
margin pointer). These are intialized "off left" and "off right" of the subarray to be
processed. The partitioning value, val , is taken as the leftmost element of the
subarray.
The movement of the pointers is handled in a "forever" loop. The loop terminates
with a return to the caller when the left and right pointers cross.
Inside the for loop, the are two do … while loops. The first do … while moves
the right margin pointer leftwards until it finds a value less than or equal to val. The
438 Standard Algorithms
second do … while loop moves the left margin pointer rightwards, stopping when it
finds a value greater than or equal to val.
If the left and right margin pointers have crossed, function Partition() should return
the position of the right margin pointer. Otherwise, a pair of misplaced values have
been found; these should be exchanged to get low values on the left and high values on
the right. Then the search for misplaced values can resume.
for(;;) {
Move right margin do
pointer leftwards rm--;
while (d[rm] > val);
if(lm<rm) {
Exchange misplaced int tempr = d[rm];
data d[rm] = d[lm];
d[lm] = tempr;
}
else
Return if all done return rm;
}
}
If you really want to know what it costs, you have to read several pages of a theoretical
text book. But you can get a good idea much more simply.
The partitioning step is breaking up the array. If it worked ideally, it would keep
splitting the array into halves. This process has to be repeated until the subarrays have
only one element. So, in this respect, it is identical to binary search. The number of
splitting steps to get to a subarray of size one will be proportional to lg(N).
In the binary search situation, we only needed to visit one of these subarrays of size
one – the one where the desired key was located. Here, we have to visit all of them.
There are N of them. It costs at least lg(N) to visit each one. So, the cost is Nlg(N).
You can see from Figure 13.2 that the partitioning step frequently fails to split a
subarray into halves; instead it produces a big part and a little part. Splitting up the big
part then requires more partitioning steps. So, the number of steps needed to get to
subarrays of size one is often going to be greater than lg(N); consequently, the cost of
the sort is greater than Nlg(N).
In the worst case, every partition step just peels off a single low value leaving all the
others in the same subarray. In that case you will have to do one partitioning step for
Quicksort 439
the first element, two to get the second, three for the third, … up to N-1 for the last.
The cost will then be approximately
1 + 2 + 3 + … + (N-2) + (N-1)
or 0.5N 2 .
For most data, a basic Quicksort() works well. But there are ways of tweaking the
algorithm. Many focus on tricks to pick a better value for partitioning the subarrays. It
is a matter of trade offs. If you pick a better partitioning value you split the subarrays
more evenly and so require fewer steps to get down to the subarrays of size one. But if
it costs you a lot to find a better partitioning value then you may not gain that much
from doing less partitions.
There is one tweak that is usually worthwhile when you have big arrays (tens of
thousands). You combine two sort algorithms. Quicksort() is used as the main
algorithm, but when the subarrays get small enough you switch to an alternative like
selection sort. What is small enough? Probably, a value between 5 and 20 will do.
The following program illustrates this composite sort (Borland users may have to
reduce the array sizes or change the "memory model" being used for the project):
#include <iostream.h>
#include <stdlib.h>
int Partition( int d[], int left, int right) Partition code
{
int val =d[left];
int lm = left-1;
int rm = right+1;
440 Standard Algorithms
for(;;) {
do
rm--;
while (d[rm] > val);
do
lm++;
while( d[lm] < val);
if(lm<rm) {
int tempr = d[rm];
d[rm] = d[lm];
d[lm] = tempr;
}
else
return rm;
}
}
Quicksort driver void Quicksort( int d[], int left, int right)
{
if(left < (right-kSMALL_ENOUGH)) {
int split_pt = Partition(d,left, right);
Quicksort(d, left, split_pt);
Quicksort(d, split_pt+1, right);
}
Call SelectionSort on else SelectionSort(d, left, right);
smaller subarrays }
int main()
{
int i;
long sum = 0;
Get some data, with for(i=0;i <kBIG;i++)
lots of duplicates sum += data[i] = rand() % 15000;
Quicksort(data, 0, kBIG-1);
return 0;
}
graphs that have to be calculated, e.g. the distances between all pairs of cities, or the
shortest path, or the existence of cycles (rings) in a graph that represents a molecule.
Because graphs turn up in many different practical contexts, graph algorithms have
been extensively studied and there are good algorithmic solutions for many graph
related problems.
Searching, sorting, strings, and graphs; these are the main ingredients of the
algorithm books. But, there are algorithms for all sorts of problems; and the books
usually illustrate a variety of the more esoteric.
Bipartite graph Suppose you had the task of making up a list of partners for a high school dance. As
matching illustrated in Figure 13.4 you would have groups of boys and girls. There would also be
links between pairs, each link going from a boy to a girl. Each link would indicate
sufficient mutual attraction that the pair might stay together all evening. Naturally,
some individuals are more attractive than others and have more links. Your task is to
pair off individuals to keep them all happy.
Figure 13.4 The bipartite matching (or "high school prom") problem. Find a way of
matching elements from the two sets using the indicated links.
Quicksort 443
What would you do? Hack around and find your own ad hoc scheme? There is no
need. This is a standard problem with a standard solution. Of course, theoreticians
have given it an imposing name. This is the "bipartite graph matching" problem. There
is an algorithm to solve it; but you would never know if you hadn't flicked through an
algorithms book.
How about the problem in Figure 13.5? You have a network of pipes connecting a
"source" (e.g. a water reservoir) to a "sink" (the sewers?). The pipes have different
capacities (given in some units like cubic metres per minute). What is the maximum
rate that which water might flow out of the reservoir? Again, don't roll your own.
There is a standard algorithm for this one, Maximal Flow.
1000 900
2100 1300
Sink
700 1800 750 2200
400
2400
Figure 13.5 Maximal flow problem: what is the maximum flow from source to sink
through the "pipes" indicated.
Figure 13.6 illustrates the "package wrapping problem" (what strange things
academic computer scientists study!). In this problem, you have a large set of points (as
x,y coordinate pairs). The collection is in some random order. You have to find the
perimeter points.
Despite their five hundred to one thousand pages, the algorithms books are just are The CACM libraries
beginning. There are thousands more algorithms, and implementations as C functions
or FORTRAN subroutines out there waiting to be used. The "Association for
Computing Machinery" started publishing algorithms in its journal "Communciations of
the ACM" back in the late 1950s. There is now a special ACM journal, ACM
Transactions on Mathematical Software, that simply publishes details of new algorithms
together with their implementation. In recent years, you could for example find how to
code up "Wavelet Transform Algorithms for Finite Duration Discrete Time Signals" or
a program to "Generate Niederreiter's Low Discrepancy Sequences".
444 Standard Algorithms
y y
x x
Numerical Several groups, e.g. Numerical Algorithms Group, have spent years collecting
algorithms algorithms and coded implementation for commonly needed things like "matrix
inversion", "eigenvalues", "ordinary differential equations", "fast fourier transforms"
and all the other mathematical processing requirements that turn up in scientific and
engineering applications. These numerical routines are widely applicable. Some
collections are commercially available; others are free. You may be able to get at the
free collections via the "netlib" service at one of ATT's computers.
Discipline specific Beyond these, each individual engineering and scientific group has established its
function libraries own library of functions. Thus, "Quanutm Chemists" have functions in their QCPE
library to calculate "overlap integrals" and "exchange integrals" (whatever these may
be).
Surf the net These collections of algorithms and code implementations are a resource that you
need to be aware of. In later programs that you get to write, you may be able to utilize
code from these collections. Many of these code libraries and similar resources are
available over the World Wide Web (though you may have to search diligently to locate
them).
14
14 Tools
This chapter introduces two powerful support tools.
A "code coverage" tool helps you determine whether you have properly tested your Role of a code
program. Think about all the if … else … and switch() statements in your code. coverage tool
Each involves optionally executed code, with calls to other functions where there are
further conditional constructs. These conditional constructs mean that there can be a
very large number of different execution paths through your code.
You can never be certain that you have tested all paths through a complex program;
so there will always be chances that some paths have errors where data are combined
incorrectly. But if you have never tested certain paths, there can be gross errors that
will crash your program.
The simple programs that we have considered so far don't really justify the use of a
code coverage tool; after all, things simulating the π-cannon don't involve that many
choices and there is very little dependence on different inputs. But as you move to
more complex things, like some of the "tree algorithms" in Part IV, code coverage tools
become more and more necessary. The "tree algorithms" build up complicated data
structures that depend on the input data. Rules implemented in the algorithms define
how these structures are to change as new data elements get added. Some of these rules
apply to relatively rare special cases that occur only for specific combinations of data
values. The code for the "tree algorithms" has numerous paths, including those for
these rare special cases.
How can you test all paths? Basically, you have to chose different input data so as
to force all processing options to be executed. Choosing the data is sometimes hard;
you may think that you have included examples that cover all cases, but it is difficult to
be certain.
This is where code coverage tools get involved. The basic idea is that the compiler
and run-time system should help you check that your tests have at least executed the
446 Tools
#include <iostream.h>
int main()
{
cout << "Enter Number ";
int num;
cin >> num;
if(num >= 0)
cout << "that was positive" << endl;
else
cout << "that was negative" << endl;
return 0;
}
#include <iostream.h>
int __counters[3];
extern void __InitializeCounters(int d[], int n);
extern void __SaveCountersToFile(int d[], int n);
int main()
{
__InitializeCounters(__counters, 3);
__counters[0]++;
cout << "Enter Number ";
int num;
cin >> num;
if(num >= 0) {
__counters[1]++;
cout << "that was positive" << endl;
}
else {
__counters[2]++;
cout << "that was negative" << endl;
Code Coverage Tool 447
}
__SaveCountersToFile(__counters, 3);
return 0;
}
The "instrumented" program will run just like the original, except that it saves the
counts to a file before terminating. In a properly implemented system, each time the
program is run the new counts are added to those in any existing record file.
When you have run your program several times on different data, you get the Analysis tool
analysis tool to interpret the contents of the file with the counts. This analysis tool
combines the counts data with the original source code to produce a listing that
illustrates the number of times each different conditional block of code has been
executed. Sections of code that have never been executed are flagged, warning the
tester that checks are incomplete. The following output from the Unix tcov tool run on
a version of the Quicksort program from Chapter 13.
else
6245 -> return rm;
##### -> }
##### -> }
int main()
1 -> {
int i;
long sum = 0;
for(i=0;i <kBIG;i++)
50000 -> sum += data[i] = rand() % 15000;
1 -> return 0;
All the parts of the program that we want executed have indeed been executed.
For real programs, code coverage tools are an essential part of the test and
development process.
A code coverage tool helps you check that you have tested your code, a "Profiler" helps
you make your code faster.
Profiler 449
General advice 1:
1. Make it work.
2. Make it fast.
Step 2 is optional.
General advice 2:
The slow bit isn't the bit you thought would be slow.
General advice 3:
Fiddling with "register" declarations, changing from arrays to pointers and general
hacking usually makes not one measurable bit of difference.
General advice 4
The only way to speed something up significantly will involve a fundamental change
of algorithm (and associated data structure).
General advice 5
Don't even think about making changes to algorithms until you have acquired some
detailed timing statistics.
Most environments provide some form of "profiling" tool that can be used to get the
timing statistics that show where a program really is spending its time. To use a
profiling tool, a program has to be compiled with special compile-time flags set. When
these flags are set, the compiler and link-loader set up auxiliary data structures that
allow run time addresses to be mapped back to source code. They also arrange for
some run-time component that can be thought of as regularly interrupting a running
program and asking where its program counter (PC) is. This run-time component takes
the PC value and works out which function it corresponds to and updates a counter for
that function. When the program finishes, the counts (and mapping data) are saved to
file. The higher the count associated with a particular bit of code, the greater the time
spent in that code.
A separate utility program can take the file with counts, and the source files and
produce a summary identifying which parts of a program use most time. The following
data were obtained by using the Unix profiler to analyze the performance of the
extended Quicksort program:
450 Tools
Both the Symantec and the Borland IDEs include profilers that can provide information
to that illustrated.
The largest portion of this program's time is spent in the Partition() function, the
Quicksort() function itself doesn't even register because it represents less than 1% of
the time. (The odd names, like __0FNSelectionSortPiiTC are examples of
"mangled" names produced by a C++ compiler.)
Once you know where the program spends its time, then you can start thinking about
minor speed ups (register declarations etc) or major improvements (better algorithms).
15
15 Design and
documentation : 1
The examples given in earlier chapters, particularly Chapter 12, have in a rather
informal way illustrated a design style known as "top-down functional decomposition".
"Top down functional decomposition" is a limited approach to design. It works very
well for simple scientific and engineering applications that have the basic structure:
initialize
get the input data
process the data
print the results
and where the "data" are something simple and homogeneous, like the array of double
precision numbers in the heat diffusion example. Top down functional decomposition
is not a good strategy for the overall design of more complex programs, such as those
that are considered in Parts IV and V. However, it does reappear there too, in a minor
role, when defining the individual "behaviours of objects".
Although the approach is limited, it is simple and it does apply to a very large
number of simple programs. So, it is worth learning this design approach at least as a
beginning.
The first section in this chapter simply summarizes materials from earlier examples
using them to illustrate a basic strategy for program development. The second section
looks briefly at some of the ways that you can document design decisions. Here, simple
textual documentation is favoured.
As it says, you start the top with "program", decompose it into functions, then you
iterate along the list of functions going down one level into each to decompose into
452 Design and documentation
auxiliary functions. The process is repeated until the auxiliary functions are so simple
that they can be coded directly.
Note the focus on functions. You don't usually have to bother much about the data
because the data will be simple – a shared array or something similar.
Beginning You begin with a phrase or one sentence summary that defines the program:
and try to get a caricature sketch of the main() function. This should be some slight
variation of the code at the start of this chapter.
Figure 15.1 illustrates initial decompositions for main() in the two example
programs.
Initialize()
Top down functional decomposition 453
do
PlayGame()
while AnotherGame()
You aim for this first stage is to get this initial sketch for main() and a list of "top Products of
level functions", each of which should be characterized by a phrase or sentence that beginning step
summarizes what it does:
AnotherGame()
Get and use a Yes/No input from user,
return "true" if Yes was input
PlayGame()
Organize the playing of one complete hangman game
HeatDiffuse()
Recalculate temperature at each point on grid.
You now consider each of these top-level functions in isolation. It is all very well to Second step
say "Organize playing of game" or "Recalculate temperatures" but what do these
specifications really mean.
It is essentially the same process as in the first step. You identify component
operations involved in "organizing the playing of the game" or whatever, and you work
out the sequence in which these operations occur. This gives you a caricature sketch for
the code of the top-level function that you are considering and a list of second level
functions. Thus, HeatDiffuse() gets expanded to:
HeatDiffuse
get copy of grid values
double loop
for each row do
for each col do
using copy work out average temp.
of environment of grid pt.
calculate new temp based on current
and environment
store in grid
reset centre point to flame temperature
CopyGrid()
Copy contents of grid into separate data area.
AverageofNeighbors()
454 Design and documentation
N-steps This process is repeated until no more functions need be introduced. The
relationships amongst the functions can be summarized in a "call graph" like that shown
in Figure 15.2.
Program Heat
main
TemperatureAt
Sorting out data and The next stage in the design process really focuses on data. You have to resolve
communications how the functions communicate and decide whether they share access to any global (or
filescope) data. For example, you may have decided that function TemperatureAt()
gets the temperature at a specified grid point, but you have still to determine how the
function "knows" which point and how it accesses the grid.
The "input/output" argument lists of all the functions, together with their return
types should now be defined. A table summarizing any filescope or global data should
be made up.
Planning test data Generally, you need to plan some tests. A program like Hangman does not require
any elaborate preplanned tests; the simple processing performed can be easily checked
through interaction with the program. The Heat program was checked by working out
in advance the expected results for some simple cases (e.g. the heated point at 1000°C
and eight neighbors at 25°C) and using the debugger to check that the calculated "new
temperatures" were correct. Such methods suffice for simple programs but as you get
to move elaborate programs, you need more elaborate preplanned tests. These should
be thought about at this stage, and a suitable test plan composed.
Finalising the design The final outputs that you want from the design process will include:
2. A list of function prototypes along with one sentence descriptions of what these
functions do.
Top down functional decomposition 455
3. A list of any typedef types (e.g. typedef double Grid[kROWS][kCOLS]) and a list
defining constants used by the program.
5. A more detailed listing of the functions giving the pseudo-code outlines for each.
6. A test plan.
It is only once you have this information that it becomes worth starting coding.
Although you will often complete the design for the entire program and then start on
implementation, if the program is "large" like the Hangman example then it may be
worth designing and implementing a simplified version that is then expanded to
produce the final product.
For most programs, the best documentation will be the design outlines just described.
You essentially have two documents. The first contains items 1…4. It is a kind of
executive summary and is what you would provide to a manager, to coworkers, or to the
teaching assistant who will be marking your program. The second document contains
the pseudo-code outlines for the functions. This you will use to guide your
implementation. Pseudo-code is supposed to be easier to read than actual code, so you
keep this document for use by future "maintenance programmers" who may have to
implement extensions to your code (they should also be left details of how to retest the
code to check that everything works after alterations).
This documentation is all textual. Sometimes, diagrams are required. Call graphs,
like that in Figure 15.2, are often requested. While roughly sketched call graphs are
useful while you are performing the design process and need to keep records as you
move from stage to stage, they don't really help that much in documenting large
programs. Figure 15.3 illustrates some of the problems with "call graphs".
The call graph for the sort program using Quicksort may be correct but it really fails
to capture much of the behaviour of that recursive system. The call graph for the
Hangman program is incomplete. But even the part shown is overwhelming in its
complexity.
456 Design and documentation
Quicksort
Partition SelectionSort
CG_Initialize TickCount
srand
strlen
CG_PutCharacter
Psychologists estimate that the human brain can work with about seven items of
information; if you attempt to handle more, you forget or confuse things. A complete
call graph has too much information present and so does not provide a useful abstract
overview.
You could break the graph down into subparts, separating PlayGame's call graph
from the overall program call graph. But such graphs still fail to convey any idea of the
looping structure (the fact that some functions are called once, others many times).
Overall, call graph diagrams aren't a particularly useful form of documentation.
Sometimes, "flowcharts" are requested rather than pseudo-code outlines for
functions. "Flowcharts" are old; they are contemporary with early versions of
FORTRAN. Flowcharting symbols correspond pretty much to the basic programming
constructs of that time, and lack convenient ways of representing multiway selections
(switch() statements etc).
At one time, complete programs were diagrammed using flowcharts. Such program
flowcharts aren't that helpful, for the same reason that full call graphs aren't that helpful.
There is too much detail in the diagram, so it doesn't effectively convey a program's
structure.
It is possible to "flowchart" individual functions, an example in flowcharting style is
shown in Figure 15.4. Most people find pseudo-code outlines easier to understand.
Documenting a design 457
Frame window
Pick word
Show guess
initialize counts
etc
Yes No
Is game
over?
Yes No
Match any?
On the whole, diagrams aren't that helpful. The lists of functions, the pseudo-code
outlines etc are easier to understand and so it isn't worth trying to generate
diagrammatic representations of the same information.
However, you may have access to a CASE ("Computer Assisted Software
Engineering") program. These programs have a user interface somewhat similar to a
drawing package. There is a palette of tools that you can use to place statement
sequences, conditional tests, iterative constructs etc. Dialogs allow you to enter details
of the operations involved.
The CASE tool can generate the final function prototypes, and the code for their
implementation, from the information that you provide in these dialogs. If you use such
a program, you automatically get a diagrammatic representation of your program along
with the code.
458 Design and documentation
16
16 Enum, Struct, and Union
This chapter introduces three simple kinds of programmer defined data types.
You aren't limited to the compiler provided char, int, double data types and their
derivatives like arrays. You can define your own data types. Most of Part IV of this
text is devoted to various forms of programmer defined type. Here we introduce just
three relatively simple kinds. These are almost the same as the equivalents provided in
C; in contrast, C has no equivalent to the more sophisticated forms of programmer
defined type introduced in Part IV.
Enumerated types, treated in section 16.1, are things that you should find useful in
that they can improve the readability of your programs and they allow the compiler to
do a little extra type checking that can eliminate certain forms of error. Although
useful, enumerated types are not that major a factor in C++ programming.
Structs are the most important of the three language elements introduced here,
section 16.2. They do have wider roles, but here we are interested in their primary role
which is the grouping of related data.
Unions: treat them as a "read only" feature of C++. You will sometimes see unions
being employed in library code that you use, but it is unlikely that you will find any real
application for unions in the programs that you will be writing.
You often need to use simple integer constants to represent domain specific data. For
example, suppose you needed to represent the colour of an automobile. You could have
the following:
int auto_colour;
460 Enum, struct, and union
auto_colour = cBLUE;
This is quite workable. But there are no checks. Since variable auto_colour is an
integer, the following assignments are valid:
auto_colour = -1;
…
auto_colour = rand();
But of course, these statements lose the semantics; variable auto_colour doesn't any
longer represent a colour, it is just an integer.
It is nice to be able to tell the compiler:
and then have the compiler check, pretty thoroughly, that auto_colour is only used in
this way throughout the code.
enums and type This is the role of enums or enumerated types. If you think how they are actually
checking implemented, they are just an alternative way of declaring a set of integer constants and
defining some integer variables. But they also introduce new distinct types and allow
the compiler to do type checking. It is this additional type checking that makes enums
worthwhile.
The following is a simple example of an enum declaration:
Naming convention The entries in the enumeration list are just names (of constant values). The same rules
for enums apply as for any other C++ names: start with a letter, contain letters digits and
underscores. However, by convention, the entries in the enum list should have names
that start with 'e' and continue with a sequence of capital letters. This makes enum
values stand out in your code.
With Colour now defined, we can have variables of type Colour:
Colour auto_colour;
…
auto_colour = eBURGUNDY;
The compiler will now reject things like auto_colour = 4. Depending on the
compiler you are using you may get just "Warning – anachronism", or you may get an
error (really, you should get an error).
enumerators What about the "enumerators" eRED, eBLUE etc? What are they?
Enumerated types 461
Really, they are integers of some form. The compiler may chose to represent them
using shorts, or as unsigned chars. Really, it isn't any of your business how your
compiler represents them.
The compiler chooses distinct values for each member of an enumeration.
Normally, the first member has value 0, the second is 1, and so forth. So in this
example, eRED would be a kind of integer constant 0, eSILVERGREY would be 4.
Note the effect of the type checking:
It isn't the values that matter, it is the types. The value 4 is an integer and can't be
directly assigned to a Colour variable. The constant eSILVERGREY is a Colour
enumerator and can be assigned to a Colour variable.
You can select for yourself the integer values for the different members of the
enumeration, provided of course that you keep them all distinct. You won't have cause
to do this yourself; but you should be able to read code like:
Treat this as one of those "read only" features of C++. It is only in rare circumstances
that you want to define specific values for the members of the enumeration. (Defining
specific values means that you aren't really using the enum consistently; at some places
in your code you intend to treat enums as characters or integers.)
Output of enums
Enums are fine in the program, but how do you get them transferred using input and
output statements?
Well, with some difficulty!
An enum is a form of integer. You can try:
and you might get a value like 0, or 3 printed. More likely, the compiler will give you
an error message (probably something rather obscure like "ambiguous reference to
overloaded operator function"). While the specific message may not be clear, the
compiler's unhappiness is obvious. It doesn't really know how you want the enum
printed.
You can tell the compiler that it is OK to print the enum as an integer:
462 Enum, struct, and union
The function like form int(auto_colour) tells the compiler to convert the data value
auto_colour to an integer. The statement is then cout << integer which the
compiler knows to convert into a call to a PrintInteger() routine. (The compiler
doesn't need to generate any code to do the conversion from enum to integer. This
conversion request is simply a way for the programmer to tell the compiler that here it
is intended that the enum be regarded as just another integer).
Printing an enum as an integer is acceptable if the output is to a file that is going to
be read back later by the program. Human users aren't going to be pleased to get output
like:
If you are generating output that is to be read by a human user, you should convert the
enum value into an appropriate character string. The easiest way is to use a switch
statement:
switch(auto_colour) {
eRED: cout << "Red"; break;
eBLUE: cout << "Blue"; break;
…
eBURGUNDY:
cout << "Burgundy"; break;
}
Input
If your program is reading a file, then this will contain integer values for enums; for
example, the file could have the partial contents
1.8 4 5
for an entry describing a four door, burgundy coloured car with 1.8 litre engine But you
can't simply have code like:
double engine_size;
int num_doors;
Colour auto_colour;
…
input >> engine_size;
input >> num_doors ;
input >> auto_colour;
Enumerated types 463
The compiler gets stuck when it reaches input >> auto_colour;. The compiler's
translation tables let it recognize input >> engine_size as a form of "input gives to
double" (so it puts in a call to a ReadDouble() routine), and similarly input >>
num_doors can be converted to a call to ReadInt(). But the compiler's standard
translation tables say nothing about input >> Colour.
The file contains an integer; so you had better read an integer:
int temp;
input >> temp;
Now you have an integer value that you hope represents one of the members of the
enum Colour. Of course you must still set the Colour variable auto_colour. You
can't simply have an assignment:
auto_colour = temp;
because the reason we started all of this fuss was to make such assignments illegal!
Here, you have to tell the compiler to suspend its type checking mechanisms and
trust you. You can say "I know this integer value will be a valid Colour, do the
assignment." The code is
auto_colour = Colour(temp);
Input from file will use integers, but what of input from a human user?
Normally, if you are working with enumerated types like this, you will be prompting
the user to make a selection from a list of choices:
Colour GetColour()
{
cout << "Please enter preferred colour, select from "
<< endl;
cout << "1\tRed" << endl;
cout << "2\tBlue" << endl;
…
cout << "6\tBurgundy" << endl;
for(;;) {
int choice;
cin >> choice;
switch(choice) {
case 1: return eRED;
case 2: return eBLUE;
…
case 6: return eBURGUNDY;
}
cout << "There is no choice #" << choice << endl;
464 Enum, struct, and union
(Note, internally eRED may be 0, eBLUE may be 1 etc. But you will find users generally
prefer option lists starting with 1 rather than 0. So list the choices starting from 1 and
make any adjustments necessary when converting to internal form.)
but the compiler can't check that you only use valid styles from the set of defined
constants and so erroneous code like
DrawString("silly", 12345);
Enumerated types 465
are stomped on by the compiler (again, you may simply get a warning, but it is more
likely that you will get an error message about a missing function).
16.2 STRUCTS
It is rare for programs to work with simple data values, or even arrays of data values, The need for
that are individually meaningful. Normally, you get groups of data values that belong "structs"
together.
Let's pick on those children again. This time suppose we want records of children's
heights in cm, weights in kilos, age (years and months), and gender. We expect to have
a collection of about one thousand children and need to do things like identify those
with extreme heights or extreme weights.
We can simply use arrays:
double heights[kMAXCHILDREN];
double weights[kMAXCHILDREN];
int years[kMAXCHILDREN];
int months[kMAXCHILDREN];
char gender[kMAXCHILDREN];
…
// read file with data, file terminated by sentinel data
// value with zero height
count = 0;
infile >> h;
while(h > 0.0) {
heights[count] = h;
infile >> weights[count] >> years[count] >>
months[count] >> gender[count];
466 Enum, struct, and union
count++;
infile >> h;
}
…
Now heights[5], weights[5], …, gender[5] all relate to the same child. But if
we are using arrays, this relationship is at most implicit.
There is no guarantee that we can arrange that the data values that logically belong
together actually stay together. After all, there is nothing to stop one from sorting the
heights array so that these values are in ascending order, at the same time losing the
relationship between the data value in heights[5] and those in weights[5] …
gender[5].
Have to be able to A programming language must provide a way for the programmer to identify groups
group related data of related data. In C++, you can use struct.
elements
Declaring structs A C++ struct declaration allows you to specify a grouping of data variables:
struct Child {
double height;
double weight;
int years;
int months;
char gender;
};
After reading such a declaration, the compiler "knows what a Child is"; for the
purposes of the rest of the program, the compiler knows that a Child is a data structure
containing two doubles, two integers, and a character. The compiler works out the
basic size of such a structure (it would probably be 25 bytes); it may round this size up
to some larger size that it finds more convenient (e.g. 26 bytes or 28 bytes). It adds the
name Child to its list of type names.
This grouping of data is the primary role of a struct. In C++, structs are simply a
special case of classes and they can have perform roles other than this simple grouping
of data elements. However it is useful to make a distinction. In the examples in this
book, structs will only be used as a way of grouping related data elements.
"record", "field", The term "record" is quite often used instead of struct when describing programs.
"data member" The individual data elements within a struct are said to be "fields", or "data members".
The preferred C++ terminology is "data members".
Defining variables of A declaration doesn't create any variables, it just lets the compiler know about a new
struct types type of data element that it should add to the standard char, long, double etc. But, once
a struct declaration has been read, you can start to define variables of the new type:
Child surveyset[kMAXCHILDREN];
Each of these Child variables has its own doubles recording height and weight, its own
ints for age details, and a char gender flag.
The definition of a variable of struct type can include the data needed to initialize its Initialization of
data members: structs
Child example = {
125.0, 32.4, 13, 2, 'f'
};
struct Rectangle {
int left, top;
int width, height;
} r1, r2, r3;
This declares the form of a Rectangle and defines three instances. This style is widely
use in C programs, but it is one that you should avoid. A declaration should introduce a
new data type; you should make this step separate from any variable definitions. The
construct is actually a source of some compile-time errors. If you forget the ';' that
terminates a structure declaration, the compiler can get quite lost trying to interpret the
next few program elements as being names of variables (e.g. the input struct Rect {
… } struct Point {…} int main() { … } will make a compiler quite unhappy).
Programs have to be able to manipulate the values in the data members of a struct Accessing data
variable. Consequently, languages must provide a mechanism for referring to a members of a
variable of a struct
particular data member of a given struct variable. type
Most programming languages use the same approach. They use compound names Fields of a variable
made up of the variable name and a qualifying data member name. For example, if you identified using
wanted to check whether brat's height exceeded a limit, then in C++ you would write: compound names
Similarly, if you want to set cute's weight to 35.4 kilos, you would write:
cute.weight = 35.4;
Most statements and expressions will reference individual data members of a struct, Assignment of structs
but assignment of complete structures is permitted:
Child Tallest;
468 Enum, struct, and union
Tallest.height = 0;
for(int i = 0; i < NumChildren; i++)
if(surveyset[i].height > Tallest.height)
Tallest = surveyset[i];
In C, struct (and enum) declarations don't make the struct name (or enum name) a
new type. You must explicitly tell the compiler that you want a new type name to be
available. This is done using a typedef. There are a variety of styles. Two common
styles are:
typedef
struct Point {
int x;
int y;
};
or:
You will see such typedefs in many of the C libraries that you get to use from your C++
programs.
Structs 469
A function can have arguments of struct types. Like simple variables, structs can be Structs and functions
passed by value or by reference. If a struct is passed by value, it is handled like an
assignment – a blockmove is done to copy the bytes of the structure onto the stack.
Generally, because of the cost of the copying and the need to use up stack space, you
should avoid passing large structs by value. If a function uses a struct as an "input
parameter", its prototype should specify the struct as const reference, e.g.:
Functions can have structs as their return values. The following illustrates a function
that gets an "car record" filled in:
struct car {
double engine_size;
int num_doors;
Colour auto_colour;
};
Although this is permitted, it should not be overused. Returning a struct result doesn't
matter much with a small structure like struct car, but if your structures are large
this style becomes expensive both in terms of space and data copying operations.
Code using the GetAutoDetails() function would have to be something like:
car purchasers_choice;
470 Enum, struct, and union
…
purchasers_choice = GetAutoDetails();
The instructions generated would normally be relatively clumsy. The stack frame setup
for the function would have a "return value area" sufficient in size to hold a car record;
there would be a separate area for the variable temp defined in the function. The return
statement would copy the contents of temp into the "return value area" of the stack
frame. The data would then again be copied in the assignment to purchasers_choice.
If you needed such a routine, you might be better to have a function that took a
reference argument:
car purchasers_choice;
…
GetAutoDetails(purchasers_choice);
16.3 UNIONS
Essentially, unions define a set of different interpretations that can be placed on the data
content area of a struct. For you, "unions" should be a "read-only" feature of C++. It
may be years before you get to write code where it might be appropriate for you to
define a new union. However, you will be using libraries of C code, and some C++
libraries, where unions are employed and so you need to be able to read and understand
code that utilizes unions.
Unions are most easily understood from real examples The following examples are
based on code from Xlib. This is a C library for computers running Unix (or variations
like Mach or Linux). The Xlib library provides the code needed for a program running
on a computer to communicate with an X-terminal. X-terminals are commonly used
when you want a multi-window style of user interface to Unix.
An X-terminal is a graphics display device that incorporates a simple
microprocessor and memory. The microprocessor in the X-terminal does part of the
work of organizing the display, so reducing the computational load on the main
computer.
Unions 471
When the user does something like move the mouse, type a character, or click an
action button, the microprocessor packages this information and sends it in a message to
the controlling program running on the main computer.
In order to keep things relatively simple, all such messages consist of a 96 byte
block of data. Naturally, different actions require different data to be sent. A mouse
movement needs a report of where the mouse is now located, a keystroke action needs
to be reported in terms of the symbol entered.
Xlib-based programs use XEvent unions to represent these 96 byte blocks of data.
The declaration for this union is
This declaration means that an XEvent may simply contain an integer (and 92 bytes of
unspecified data), or it may contain an XAnyEvent, or it may contain an XButtonEvent,
or …. There are about thirty different messages that an Xterminal can send, so there are
thirty different alternative interpretations specified in the union declaration.
Each of these different messages has a struct declaration that specifies the data that
that kind of message will contain. Two of these structs are:
typedef struct {
int type;
unsigned long serial;
Bool send_event;
472 Enum, struct, and union
Display *display;
Window window;
int x, y;
int width, height;
int border_width;
Bool override_redirect;
} XCreateWindowEvent;
As illustrated in Figure 16.1, the first part of any message is a type code. The way
that the rest of the message bytes are used depends on the kind of message.
XEvent XCreateWindowEvent
type type type type
serial serial serial
send_event send_event send_event
window window window
root x root
subwindow y subwindow
time width time
x height x
y border y
x_root override x_root
y_root y_root
state state
button hint
same_screen same_screen
XButtonEvent XMotionEvent
If you have an XEvent variable ev , you can access its type field using the "."
operator just like accessing a data field in a normal structure:
XEvent ev;
…
switch(ev.type) {
…
}
If you know that ev really encodes an XCreateWindowEvent and you want to work Doubly qualified
out the area of the new window, you can use code like: names to access data
members of variants
in union
area = ev.xcreatewindow.width * eve.xcreatewindow.height;
The appropriate data fields are identified using a doubly qualified name. The variable
name is qualified by the name of the union type that is appropriate for the kind of record
known to be present (so, for an XCreateWindowEvent , you start with
ev.xcreatewindow ). This name is then further qualified by the name of the data
member that should be accessed ( ev.xcreatewindow.width).
A programmer writing the code to deal with such messages knows that a message
will start with a type code. The Xlib library has a series of #defined constants, e.g.
ButtonPress, DestroyNotify, MotionNotify; the value in the type field of a message will
correspond to one of these constants. This allows messages from an Xterminal to be
handled as follows:
XEvent eV;
…
/* Code that gets calls the Unix OS and
gets details of the next message from the Xterminal
copied into eV */
…
switch(ev.type) {
caseButtonPress:
/* code doing something depending on where the button was
pressed, access using xbutton variant from union */
break;
case MotionNotify:
/* user has moved pointer, get details of when this
happened
and decide what to do, access using xmotion variant from
union */
thetime = ev.xmotion.time;
…
474 Enum, struct, and union
break;
case CreateNotify:
/* We have a new window, code here that looks at where and
what size, access using xcreatewindow variant of union */
int hpos = ev.xcreatewindow.x;
…
break;
…
}
17
17 Examples using structs
This chapter contains a few simple examples illustrating the use of structs in programs.
In future programs, you will use structs mainly when communicating with existing C Using structs with
libraries. For example, you will get to write code that uses the graphics primitives on existing libraries
your system. The "applications programmer interface" (API) for the graphics will be
defined by a set of C functions (in some cases based on an earlier interface defined by a
set of Pascal functions). If you a programming on Unix, you will probably be using the
Xlib graphics package, on an Apple system you would be using Quickdraw, and on an
Intel system you would use a Windows API. The functions in the API will make
extensive use of simple structs. Thus, the Xlib library functions use instances of the
following structs:
typedef struct {
short x, y;
} XPoint;
typedef struct {
short x, y;
unsigned short width, height;
} XRectangle;
and
typedef struct {
short x, y;
unsigned short width, height;
short angle1, angle2;
} XArc;
So, if you are using the Xlib library, your programs will also define and use variables
that are instances of these struct types.
For new application-specific code you will tend to use variables of class types more Your own structs?
often than variables of struct types.
476 Examples using structs
The examples in this chapter are intended merely to illustrate how to declare struct
types, use instances of these types, access fields and so forth. The example programs in
section 17.3, introduce the idea of a "file of records". These programs have a single
struct in memory and a whole set of others in a disk file. The struct in memory gets
loaded with data from a requested record on disk. Record files are extremely common
in simple business data processing applications, and represent a first step towards the
more elaborate "databases" used in industry. You will learn more about "files of
records" and "databases" in later subjects. While you can continue to use C/C++ when
working with database systems, you are more likely to use specialized languages like
COBOL and SQL.
The example in section 13.2 illustrated a "sorting" algorithm that was applied to reorder
pupil records. The original used two arrays, pupils and marks:
Name pupils[] = {
"Armstrong, Alice S.",
…
"Zarra, Daniela"
};
int marks[] = {
57,
…
78
};
But the data in the two arrays "belong together" – Miss Armstrong's mark is 57 and she
shouldn't be separated from it.
This is a typical case where the introduction of a simple struct leads to code that
reflects the problem structure more accurately (and is also slightly clearer to read). The
struct has to package together a name and a mark:
struct PupilRec {
Name fName;
int fMark;
};
(Naming conventions again: use 'f' as the first character of the name of a data member
in all structs and classes that are defined for a program.)
With this struct declared, an initialized array of PupilRecs can be defined:
Reordering the class list 477
PupilRec JuniorYear[] = {
{ "Armstrong, Alice S.", 57 } ,
{ "Azur, Hassan M.", 64 } ,
{ "Bates, Peter", 33 } ,
…
{ "Zarra, Daniela", 81 }
};
The function takes two arrays of PupilRec structs. The orig parameter is the array
with the records in alphabetic order; it is an "input parameter" so it is specified as
const. The reord parameter is the array that is filled with the reordered copy of the
data.
An expression like:
orig[i].fMark
illustrates how to access a data member of a chosen element from an array of structures.
Note the structure assignment:
478 Examples using structs
reord[position] = orig[i];
The compiler "knows" the size of structs so allows struct assignment while assignment
of arrays is not allowed.
The following structs and functions illustrate the functionality that you will find in the
graphics libraries for your system. Many of the basic graphics functions will use points
and rectangles; for example, a line drawing function might take a "start point" and an
"end point" as its parameters, while a DrawOval() function might require a rectangle to
frame the oval.
The graphics packages typically use short integers to represent coordinate values. A
"point" will need two short integer data fields:
struct Point {
short fx;
short fy;
};
struct Rectangle {
short ftop;
short fleft;
short fwidth;
short fheight;
};
and
struct Rectangle {
Point fcorner;
short fwidth;
short fheight;
};
Provided that struct Point has been declared before struct Rectangle, it is
perfectly reasonable for struct Rectangle to have data members that are instances
of type Point. The second declaration will be used for the rest of the examples in this
section.
The graphics libraries define numerous functions for manipulating points and
rectangles. The libraries would typically include variations of the following functions:
Points and Rectangles 479
These functions are slightly inconsistent in their prototypes, some return structs as
results while others have output parameters. This is deliberate. The examples are
meant to illustrate the different coding styles. Unfortunately, it is also a reflection of
most of the graphics libraries! They tend to lack consistency. If you were trying to
design a graphics library you would do better to decide on a style; functions like
Add_Point() and MidPoint() should either all have struct return types or all have
output parameters. In this case, there are reasons to favour functions that return structs.
Because points and rectangles are both small, it is acceptable for the functions to return
structs as results. Code using functions that return structs tends to be a little more
readable than code using functions with struct output parameters.
This example is like the "curses" and "menu selection" examples in Chapter 12. We
need a header file that describes the facilities of a "points and rectangles" package, and
an implementation file with the code of the standard functions. Just so as to illustrate
the approach, some of the simpler functions will be defined as "inline" and their
definitions will go in the header file.
The header file would as usual have its contents bracketed by conditional
compilation directives:
480 Examples using structs
#ifndef __MYCOORDS__
#define __MYCOORDS__
#endif
struct Point {
short fx;
short fy;
};
struct Rectangle {
…
};
inline functions inline int Equal_Points(const Point& p1, const Point& p2)
defined in the header {
file return ((p1.fx == p2.fx) && (p1.fy == p2.fy));
}
#endif
"Inline" functions have to be defined in the header. After all, if the compiler is to
replace calls to the functions by the actual function code it must know what that code is.
When compiling a program in another file that uses these functions, the compiler only
knows the information in the #included header.
The definitions of the other functions would be in a separate mycoords.cp file:
#include "mycoords.h"
Points and Rectangles 481
return 1;
}
A rectangle has a Point data member which itself has int fx, fy data members.
The name of the data member that represents the x-coordinate of the rectangle's corner
is built up from the name of the rectangle variable, e.g. r1, qualified by the name of its
point data member fcorner, qualified by the name of the field, fx.
Function Add_Point() returns its result via the res argument:
while MidPoint() returns its value via the stack (it might as well use Add_Point() in
its implementation).
return m;
}
Rectangle r;
int left, right, top, bottom;
r.fcorner.fx = left;
r.fcorner.fy = top;
return r;
}
If you were developing a small library of functions for manipulation of points and
rectangles, you would need a test program like:
#include <iostream.h>
#include "mycoords.h"
int main()
{
Point p1;
p1.fx = 1;
p1.fy = 7;
Rectangle r1;
r1.fcorner.fx = -3;
r1.fcorner.fy = -1;
r1.fwidth = 17;
r1.fheight = 25;
if(Point_In_Rect(p1, r1))
cout << "Point p1 in rect" << endl;
else cout << "p1 seems to be lost" << endl;
Point p2;
p2.fx = 11;
p2.fy = 9;
Point p3;
Add_Point(p1, p2, p3);
Point p4;
p4.fx = 12;
Points and Rectangles 483
p4.fy = 16;
if(Equal_Points(p3, p4))
cout << "which is where I expected to be" << endl;
else cout << "which I find surprising!" << endl;
…
…
return 0;
}
The test program would have calls to all the defined functions and would be organized
to check the results against expected values as shown.
You may get compilation errors relating to struct Point. Some IDEs
automatically include the system header files that define the graphics functions, and
these may already define some other struct Point. Either find how to suppress this
automatic inclusion of headers, or change the name of the structure to Pt.
Rather than writing their own program, anyone who really wants to keep files of
business records would be better off using the "database" component of one of the
standard "integrated office packages". But someone has to write the "integrated office
package" of the future, maybe you. So you had better learn how to manipulate simple
files of records.
The idea of a file of records was briefly previewed in section 9.6 and is again
illustrated in Figure 17.1.
A file record will be exactly the same size as a memory based struct. Complete
structs can be written to, and read from, files. The i/o transfer involves simply copying
bytes (there is no translation between internal binary forms of data and textual strings).
The operating system is responsible for fitting the records into the blocks on disk and
keeping track of the end of the file.
484 Examples using structs
End of file
Sequential access Files of record can be processed sequentially. The file can be opened and records
read one by one until the end of file marker is reached. This is appropriate when you
want to all records processed. For example, at the end of the month you might want to
run through the complete file identifying customers who owed money so that letters
requesting payment could be generated and dispatched.
Random access Files of records may also be processed using "random access". Random access does
not mean that any data found at random will suffice (this incorrect explanation was
proposed by a student in an examination). The "randomness" lies in the sequence in
which records get taken from file and used. For example, if you had a file with 100
customer records, you might access them in the "random" order 25, 18, 49, 64, 3, ….
File systems allow "random access" because you can move the "get" (read) or "put"
(write) pointer that the operating system associates with a file before you do the next
read or write operation. You can read any chosen record from the file by moving the
get pointer to the start of that record. This is easy provided you know the record
number (imagine that the file is like an array of records, you need the index number into
this array). You simply have to multiply the record number by the size of the record,
this gives the byte position where the "get pointer" has to be located.
You have to be able to identify the record that you want. You could do something
like assign a unique identifying number (in the range 0…?) to each customer and
require that this is specified in all correspondence, or you could make use of an
auxiliary table (array) of names and numbers.
You would want to use "random access" if you were doing something like taking
new orders as customers came to, or phoned the office. When a customer calls, you
want to be able to access their record immediately, you don't want to read all the
preceding records in the file. (Of course reading the entire file wouldn't matter if you
have only 100 records; but possibly you have ambitions and wish to grow to millions.)
File of records 485
Working with record files and random access requires the use of some additional New iostream and
facilities from the iostream and fstream libraries. fstream features
First, the file that holds the data records will be an "input-output" file. If you need to fstream
do something like make up an order for a customer, you need to read (input) the
customer's record, change it, then save the changed record by writing it back to file
(output). Previously we have used ifstream objects (for inputs from file) and
o f s t r e a m objects (for outputs to file), now we need an fstream object (for
bidirectional i/o). We will need an fstream variable:
fstream gDataFile;
The open request would specify ios::in (input) and ios::out (output); in some
environments, the call to open() might also have to specify ios::binary. As this file
is used for output, the operating system should create the file if it does not already exist.
Next, we need to use the functions that move the "get" and "put" pointers. Positioning the
(Conceptually, these are separate pointers that can reference different positions in the get/put pointers
file; in most implementations, they are locked together and will always refer to the same
position.) The get pointer associated with an input stream can be moved using the
seekg() function; similarly, the put pointer can be moved using the seekp() function.
These functions take as arguments a byte offset and a reference point. The reference
point is defined using an enumerated type defined in the iostream library; it can be
ios::beg (start of file), ios::cur (current position), or ios::end (end of the file).
So, for example, the call:
gDataFile.seekg(600, ios::beg);
would position the get pointer 600 bytes after the beginning of the file; while the call:
gDataFile.seekp(0, ios::end);
gDataFile.seekg(0, ios::end);
long pos = gDataFile.tellg();
gNumRecs = pos / sizeof(Customer);
486 Examples using structs
The call to seekg() moves the get pointer to the end of the file; the call to tellg()
returns the byte position where the pointer is located, and so gives the length of the file
(i.e. the number of bytes in the file). The number of records in the file can be obtained
by dividing the file length by the record size.
Read and write Data bytes can be copied between memory and file using the read and write
transfers functions:
(The prototypes for these functions may be slightly different in other versions of the
iostream library. The type size_t is simply a typedef equivalent for unsigned int.)
These functions need to be told the location in memory where the data are to be placed
(or copied from) and the number of bytes that must be transferred.
Memory address The first argument is a void*. In the case of write(), the data bytes are copied
"Pointers" from memory to the disk so the memory data are unchanged, so the argument is a const
void*. These void* parameters are the first example that we've seen of pointers.
(Actually, the string library uses pointers as arguments to some functions, but we
disguised them by changing the function interfaces so that they seemed to specify
arrays).
A pointer variable is a variable that contains the memory address of some other data
element. Pointers are defined as derived types based on built in or programmer defined
struct (and class) types:
int *iptr;
double *dptr;
Rectangle *rptr;
void *ptr_to_somedata;
These definitions make iptr a variable that can hold the address of some integer data
item, dptr a variable that holds the address of a double, and rptr a variable that holds
the address where a Rectangle struct is located.
The variable, ptr_to_somedata , is a void*. This means that it can hold the
address of a data item of any kind. (Here void is not being used to mean empty, it is
more that it is "unknown" or at least "unspecified").
As any C programmer who has read this far will have noted, we have been carefully
avoiding pointers. Pointers are all right when you get to know them, but they can be
cruel to beginners. From now on, almost all your compile time and run-time errors are
going to relate to the use of pointers.
But in these calls to the read() and write() functions, there are no real problems.
All that these functions require is the memory address of the data that are involved in
File of records 487
the transfer. In this example, that is going to mean the address of some variable of a
struct type Customer.
In C and C++, you can get the address of any variable by using the & "address-of" The & "address-of"
operator. Try running the following program (addresses are by convention displayed in operator
hexadecimal rather than as decimal numbers):
float pi = 3.142;
int array[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int main()
{
int data = 101;
void *ptr;
ptr = &data;
cout << "data is at " << hex << ptr << endl;
ptr = π
cout << "pi is at " << hex << ptr << endl;
ptr = &(array[2]);
cout << "array[2] is at " << hex << ptr << endl;
ptr = &(array[3]);
cout << "array[3] is at " << hex << ptr << endl;
return 0;
}
You should be able to relate the addresses that you get to the models that you now have
for how programs and data are arranged in memory. (If you find it hard to interpret the
hexadecimal numbers, switch back to decimal outputs.)
The & operator is used to get the addresses that must be passed to the read() and
write() functions. If we have a variable:
Customer c;
we can get it loaded with data from a disk file using the call:
gDataFile.read(&c, sizeof(Customer));
gDataFile.write(&c, sizeof(Customer));
(Some compilers and versions of the iostream library may require "(char*)" before the
& in these calls. The & operator and related matters are discussed in Chapter 20.)
488 Examples using structs
Specification
Write a program that will help a salesperson keep track of customer records.
The program is to:
1 Maintain a file with records of customers. These records are to include the
customer name, address, postcode, phone number, amount owed, and details of any
items on order.
3 Have a list of all products stocked, along with their costs, and should provide a
simple way for the salesperson to select a product by name when making up a
customer order.
The program is not to load the entire contents of the data file into memory. Records are
to be fetched from the disk file when needed. The file will contain at most a few
hundred records so sophisticated structures are not required.
Design
First Iteration The overall structure of the program will be something like:
Obviously, each of the options like "list all customers" becomes a separate function.
Several of these functions will have to "get" a record from the file or "put" a record
to file, so we can expect that they will share "get record", "put record" and possibly
some other lower level functions.
The UT functions developed in Chapter 12 (selection from menu, and keyword Second Iteration
lookup) might be exploited. Obviously, the menu selection routine could handle the
task of getting a command entered by the salesperson. In fact, if that existing routine is
to be used there will be no further design work necessary for "Run the shop". The
sketched code is easy to implement.
So, the next major task is identifying the way the functions will handle the tasks like The "list …"
listing customers and getting orders. The routines for listing all customers and listing functions
debtors are going to be very similar:
"list debtors"
initialize a counter to zero
You might be tempted to fold these functions into one, using an extra boolean argument
to distinguish whether we are interested in all records or just those with debts.
However, if the program were later made more elaborate you would probably find
greater differences in the work done in these two cases (e.g. you might want the "list
490 Examples using structs
"print details"
output name, address, postcode, phone number
if amount owed
print the amount
else ? (either blank, or "nothing owing")
The specification didn't really tie down how information was to be displayed; we can
chose the formats when doing the implementation.
Functions for other The other required functions are those to add a new customer record to the file, show
processing options the record of a customer identified by name, and place an order for a customer (the
specification is imprecise, but presumably this is again going to be a customer identified
by name).
Adding a customer should be easy. The "add customer" routine would prompt the
salesperson for the name, address, postcode, and phone number of the new customer.
These data would be filled into a struct, then the struct would be written to the end of
the existing file. The routine would have to update the global counter that records the
number of records so that the listing routines would include the new record.
"add customer"
? check any maximum limits on the number of customers
The file itself would not limit the number of records (well, not until it got to contain so
many millions of records that it couldn't fit on a disk). But limits could (and in this case
do) arise from other implementation decisions.
Writing the struct to file would be handled by a "put record" function that matches
the "get record":
"put record"
convert record number to byte position
copy data from memory struct to file
if i/o error
print error message and exit
The other two functions, "show customer" and "record order", both apparently need Getting the record for
to load the record for a customer identified by name. The program could work reading a named customer
each record from the file until it found the correct one. This would be somewhat costly
even for little files of a few hundred records. It would be easier if the program kept a
copy of the customer names in memory. This should be practical, the names would
require much less storage than the complete records.
It would be possible for the "open file" routine to read all existing records, copying
the customer names into an array of names. This only gets done when the program
starts up so later operations are not slowed. When new records are added to the file, the
customer name gets added to this array.
The memory array could hold small structures combining a name and a record
number; these could be kept sorted by name so allowing binary search. This would
necessitate a sort operation after the names were read from file in the "open file"
routine, and an "insert" function that would first find the correct place for a new name
and then move other existing names to higher array locations so as to make room for it.
Alternatively, the array could just contain the names; the array indexes would
correspond to the record numbers. Since the names wouldn't be in any particular order,
the array would have to be searched linearly to find a customer. Although crude, this
approach is quite acceptable in this specific context. These searches will occur when
the salesperson enters a "show customer" or "place order" command. The salesperson
will be expecting to spend many seconds reading the displayed details, or minutes
adding new orders. So a tiny delay before the data appear isn't going to matter. A
program can check many hundreds of names in less than a tenth of a second; so while a
linear search of the table of names might be "slow" in computer terms, it is not slow in
human terms and the times of human interaction are going to dominate the workings of
this program.
Further, we've got that UT_PickKeyWord() function. It searches an array of
"words", either finding the match or listing the different possible matches. This would
492 Examples using structs
actually be quite useful here. If names can be equated with the UT_Words we can use
the pick key word function to identify a name from its first few characters. Such an
"intelligent" interactive response would help the salesperson who would only have to
enter the first few characters of a name before either getting the required record or a list
of the name and similar names.
So, decisions: Customer names will be UT_Words (this limits them to less than 15
characters which could be a problem), things like addresses might as well be UT_Texts.
There will be a global array containing all the names. Functions that require the
salesperson to select a customer will use the keyword search routine (automatically
getting its ability to handle abbreviations.
These decisions lead to the following design sketches for the two remaining
processing options:
"show customer"
use keyword picker function to prompt for data (customer name)
and find its match in names array
(returns index of match)
and
"record order"
use keyword picker function to prompt for data (customer name)
and find its match in names array
(returns index of match)
loop
get next order item
update amount owed
until either no more order items or maximum number ordered
The loop getting items in function "record order" is once again a candidate for
promotion to being a separate function. If it had to do all the work, function "record
order" would become too complex. Its role should be one of setting up the context in
which some other function can get the order items. So, its sketch should be revised to:
"record order"
use keyword picker function to prompt for data (customer name)
and find its match in names array
File of records 493
get items
This new "get items" function now has to be planned. The specification stated that
this routine should either make up an order for a customer or clear the record for goods
delivered and paid for. This might not prove ideal in practice because it means that you
can't deliver partial orders, and you can't accept supplementary orders; but it will do to
start with. It means that the "get items" routine should start by clearing any existing
record of amount owed and items ordered. Then the routine should ask the user
whether any item is to be ordered. While the reply is "yes", the routine should get
details of the item, add it to the record and its cost to the amount owed, and then again
ask whether another item is to be ordered. The Customer struct can have an array to
hold information about ordered items. Since this array will have a fixed size, the loop
asking for items would need to terminate if the array gets filled up.
The specification requires the program to have a table of goods that can be ordered
and a convenient mechanism allowing the salesperson to enter the name of a chosen
article. The key word picking function can again be pressed into service. There will
need to be a global array with the names of the goods articles, and an associated array
with their costs.
An initial sketch for "get items" is:
"get items"
change amount owing data member of record to zero
This has sketch has identified another function, YesNo(), that gets a yes/no input from
the user.
494 Examples using structs
Open File revisited Earlier consideration of the open file function was deferred. Now, we have a better
idea of what it must do. It should open the file (or terminate the program if the file
won't open). It should then determine the number of records. Finally, it should scan
through all records copying the customer names into an array in memory.
The array for names has a fixed size. So there will be a limit on the number of
customers. This will have to be catered for in the "add customer" function.
Third iteration What is a Customer? It is about time to make some decisions regarding the data.
through the design A Customer had better be a struct that has data members for:
6 an array with the names of the items on order, need to fix a size for this.
Other fields might need to be added later. The following structs declarations should
suffice:
#ifndef __MYCUSTOMER__
#define __MYCUSTOMER__
#include "UT.h"
struct Date {
int fDay, fMonth, fYear;
};
struct Customer {
UT_Word fName;
UT_Text fAddress;
UT_Word fPostcode;
UT_Word fDialcode;
UT_Word fOrders[kMAXITEMS];
int fNumOrder;
double fAmountOwing;
Date fLastOrder;
};
#endif
File of records 495
An extra Date struct has been declared and a Date data member has been included in
the Customer struct. This code doesn't use dates; one of the exercises at the end of the
chapter involves implementing a code to handle dates.
As a Customer uses UT_Word and UT_Text this header file has to include the UT.h
header that contains the typedef defining these character array types.
The main implementation file is going to contain a number of global arrays:
Other global (or filescope) variables will be needed for the file name, the fstream object
that gets attached to the file, and for a number of integer counters (number of records,
number of commands, number of items in stock list).
The functions have already been considered in detail and don't require further
iterations of design. Their prototypes can now be defined:
void ShowCustomer(void);
void ListAll(void);
void ListDebtors(void);
void AddCustomer(void);
int YesNo(void);
void RecordOrder(void);
void RunTheShop(void);
void OpenTheDataFile(void);
void CloseTheDataFile(void);
int main();
496 Examples using structs
Other linked files The code written specifically for this problem will have to be linked with the UT code
from Chapter 12 so as to get the key word and menu selection functions.
Header files needed Quite a large number of systems header files will be needed. The programs uses
both iostream and fstream libraries for files. The exit() function will get used to
terminate the program if an i/o error occurs with the file accesses, so stdlib is also
needed. Strings representing names will have to be copied using strcpy() from the
string library. The "yes/no" function will have to check characters so may need the
ctype header that defines standard functions like tolower(). Error checking might use
assert.
Implementation
#include <iostream.h>
#include <fstream.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <ctype.h>
#include "UT.h"
#include "mycustomer.h"
(If you think about it carefully, you will see that we end up #including UT.h twice; that
sort of thing happens very easily and it is why we have those #ifdef … #endif brackets
on all header files).
The declarations of globals come next. Several are initialized:
fstream gDataFile;
int gNumRecs;
UT_Word gNames[kMAXCUSTOMERS];
UT_Word gStock[] = {
"Floppy disks",
…
"Marker pens",
"Laser pointer"
};
double gItemCosts[] = {
18.50, /* disks $18.50 per box */
…
6.50, /* pens */
File of records 497
UT_Text gCommands[] = {
"Quit",
"List all customers",
"List all debtors",
"Add Customer",
"Show Customer",
"Record Order"
};
In many ways it would be better to introduce a new struct that packages together an
item name and its cost. Then we could have an array of these "costed item" structs
which would reduce the chance of incorrect costs being associated with items.
However, that would preclude the use of the existing keyword functions that require a
simple array of UT_Words.
The function definitions come next:
if(!gDataFile.good()) {
cout << "Sorry, can't read the customer file"
<< endl;
exit(1);
}
}
if(!gDataFile.good()) {
cout << "Sorry, can't write to the customer file"
<< endl;
exit(1);
}
498 Examples using structs
Terminating the program after an i/o error may seem a bit severe, but really there isn't
much else that we can do in those circumstances.
The PrintDetails() function gets a little elaborate, but it is trying to provide a nice
listing of items with commas and the word "and" in the right places.
void ShowCustomer(void)
{
UT_Text aPrompt = "Customer Name : ";
int who = UT_PickKeyWord(aPrompt, gNames, gNumRecs);
if(who < 0)
return;
Customer c;
GetRecord(c, who);
PrintDetails(c);
}
There is one problem in using the "pick keyword" function. If the salesperson enters
something that doesn't match the start of any of the names, the error message is "there is
no keyword …". Possibly the "pick keyword" function should have been designed to
take an extra parameter that would be used for the error message.
void ListAll(void)
File of records 499
{
if(gNumRecs == 0) {
cout << "You have no customers" << endl;
return;
}
void ListDebtors(void)
{
int count = 0;
for(int i = 0; i < gNumRecs; i++) {
Customer c;
GetRecord(c, i);
if(c.fAmountOwing > 0.0) {
PrintDetails(c);
count++;
}
}
if(count == 0) cout << "Nothing owed" << endl;
}
Function AddCustomer() checks whether the program's array of names is full and
prevents extra names being added. The input statements use getline() . This is
because addresses are going to be things like "234 High Street" which contain spaces.
If we tried to read an address with something like cin >> c.fAddress, the address
field would get "234", leaving "High …" etc to confuse the input to post code. The
routine isn't robust; you can cause lots of troubles by entering names that are too long to
fit in the specified data member.
void AddCustomer(void)
{
if(gNumRecs == kMAXCUSTOMERS) {
cout << "Sorry, you will have to edit program "
"before it can handle" << endl;
cout << " more customers." << endl;
return;
}
Customer c;
// N.B. This input routine is "unsafe"
// there are no checks successful reads etc
c.fNumOrder = 0;
c.fAmountOwing = 0.0;
PutRecord(c, gNumRecs);
strcpy(gNames[gNumRecs], c.fName);
gNumRecs++;
}
int YesNo(void)
{
char ch;
cout << "Order an item? (Y or N)";
cin >> ch;
ch = tolower(ch);
return (ch == 'y');
}
void GetItems(Customer& c)
{
c.fAmountOwing = 0.0;
int count = 0;
while(YesNo() && (count <kMAXITEMS)) {
UT_Text aPrompt = "Identify Type of Goods";
int which =
UT_PickKeyWord(aPrompt, gStock, gNStock);
strcpy(c.fOrders[count], gStock[which]);
c.fAmountOwing += gItemCosts[which];
count++;
}
c.fNumOrder = count;
}
Look a bug in GetItems()! Can you spot it? It isn't serious, the program won't
crash. But it makes the user interaction clumsy.
If you can't spot the bug, run the code and try to order more than the limit of five
items. You should then observe a certain clumsiness.
The bug can be fixed by a trivial change to the code.
File of records 501
void RecordOrder(void)
{
UT_Text aPrompt = "Enter Customer Name";
int who = UT_PickKeyWord(aPrompt, gNames, gNumRecs);
Customer c;
GetRecord(c, who);
GetItems(c);
PutRecord(c, who);
void RunTheShop(void)
{
UT_Text aPrompt = "Command";
int quitting = 0;
while(!quitting) {
int command =
UT_MenuSelect(aPrompt,gCommands,
gNCommands, 0);
switch(command) {
case 0: /* Quit */
quitting = 1;
break;
case 1: /* List all customers */
ListAll();
break;
case 2: /* List all debtors */
ListDebtors();
break;
case 3: /* Add Customer */
AddCustomer();
break;
case 4: /* Show Customer */
ShowCustomer();
break;
case 5: /* Record Order */
RecordOrder();
break;
}
}
502 Examples using structs
The RunTheShop() function has one complication. The salesperson has to enter a
number when picking the required menu option. The digits get read but any trailing
spaces and newlines will still be waiting in the input stream. These could confuse
things if the next function called needed to read a string, a character, or a complete line.
So the input stream is cleaned up by reading characters until get the '\n' at the end of the
input line.
void OpenTheDataFile(void)
{
// Open the file, allow creation if not already there
gDataFile.open(gFileName, ios::in | ios::out );
// may need also ios::binary
if(!gDataFile.good()) {
cout << "? Couldn't open the file. Sorry." << endl;
exit(1);
}
gDataFile.seekg(0, ios::end);
long pos = gDataFile.tellg();
gNumRecs = pos / sizeof(Customer);
Logically, there is no way that the file could hold more than the maximum number of
records (the file has to be created by this program and the "add customer" function
won't let it happen).
Don't believe it. Murphy's law applies. The program can go wrong if the file is too
large, so it will go wrong. (Something will happen like a user concatenating two data
files to make one large one.) Since the program will overwrite arrays if the file is too
large, it better not even run. Hence the assert() checking the number of records.
void CloseTheDataFile(void)
{
gDataFile.close();
}
int main()
{
OpenTheDataFile();
RunTheShop();
File of records 503
CloseTheDataFile();
return 0;
}
Command
Enter option number in range 1 to 6, or ? for help
6
Enter Customer Name
Ga
Possible matching keywords are:
Gates, B.
Garribaldi, J
Gamble,P
Gam
i.e. Gamble,P
Order an item? (Y or N)y
Identify Type of Goods
Las
i.e. Laser pointer
Order an item? (Y or N)Y
Identify Type of Goods
Tone
i.e. Toner
Order an item? (Y or N)n
Command
Enter option number in range 1 to 6, or ? for helpEnter option
number in range 1 to 6, or ? for help
3
----
Customer Name : Gates, B.
Address : The Palace, Seattle, 923138
Phone : 765 456 222
Owes : $186.5
On order
Laser pointer, and Marker pens
---
----
Customer Name : Gamble,P
Address : 134 High St, 89143
Phone : 1433
Owes : $220
On order
Laser pointer, and Toner
---
504 Examples using structs
EXERCISES
2 Change the way that the Customer records program handles the names to the alternative
approach described in the text (sorted array of name-index number structures).
3 Implement code to support the Date data structure and arrange that Customer orders include a
Date.
18
18 Bits and pieces
Although one generally prefers to think of data elements as "long integers" or "doubles"
or "characters", in the machine they are all just bit patterns packed into successive bytes
of memory. Once in a while, that is exactly how you want to think about data.
Usually, you only get to play with bit patterns when you are writing "low-level"
code that directly controls input/output devices or performs other operating system
services. But there are a few situations where bits get used as parts of high level
structures. Two are illustrated in sections 2 and 3 of this chapter. The first section
summarizes the facilities that C++ provides for bit manipulations.
Section 18.4 covers another of the more obscure "read only" features of C++. This
is the ability to cut up a (long integer) data word into a number of pieces each
comprising several bits.
If you need to work with bits, you need a data type to store them in. Generally, Use unsigned longs
unsigned longs are the most convenient of the built in data types. An unsigned long to store bit patterns
will typically hold 32 bits. (There should be a header file specifying the number of bits
in an unsigned long, but this isn't quite standardized among all environments.) If you
need bit records with more than 32 bits you will have to use an array of unsigned longs
and arrange that your code picks the correct array element to test for a required bit. If
you need less than 32 bits, you might chose to use unsigned shorts or unsigned chars.
Unsigned types should be used. Otherwise certain operations may result in '1' bits
being added to the left end of your data.
You can't actually define bit patterns as literal strings of 0s and 1s, nor can you have Input and output and
them input or output in this form. You wouldn't want to; after, all they are going to be constants
things like
01001110010110010111011001111001
00101001101110010101110100000111
506 Bits and pieces
Instead, hexadecimal notation is used (C and C++ offer octal as an alternative, no one
still uses octal so there is no point your learning that scheme). The hexadecimal (hex)
scheme uses the hex digits '0', '1', … '9', 'a', 'b', 'c', 'd', 'e', and 'f' to represent groups of
four bits:
(The characters 'A' … 'F' can be used instead of 'a' … 'f'.) Two hex digits encode the
bits in an unsigned byte, eight hex digits make up an unsigned long. If you define a
constant for say an unsigned long and only give five hex digits, these are interpreted as
being the five right most digits with three hex 0s added at the left.
Hexadecimal constants are defined with a leading 0x (or 0X) so that the compiler
gets warned that the following digits are to be interpreted as hex rather than decimal:
#include <iostream.h>
void main()
{
Bits b1 = 0x1ef2;
cout.setf(ios::showbase);
cout << hex << b1 << endl;
Bits b2;
cin >> b2;
cout << b2 << endl;
When entering a hex number you must start with 0x; so an input for that program could
be 0xa2. You should set the " ios::showbase" flag on the output stream otherwise the
Bit manipulations 507
numbers don't get printed with the leading 0x; it is confusing to see output like 20 when
it really is 0x20 (i.e. the value thirty two).
You have all the standard logical operations that combine true/false values. They What can you do with
are illustrated here for groups of four bits (both binary and equivalent hex forms are bit patterns?
shown):
result = ~value;
(Ouch. We have just met & as the "get the address of" operator. Now it has decided to
be the "bitwise And operator". It actually has both jobs. You simply have to be careful
when reading code to see what its meaning is. Both these meanings are quite different
from the logical and operator, &&, that was introduced in Part II.)
We have had many examples with the | operator being used to make up bit patterns
used as arguments, e.g. the calls to open() that had specifiers like ios::in |
ios::out. The enumerators ios::in and ios::out are both values that have one bit
encoded; if you want both bits, for an input-output file, you or them to get a result with
both bits set.
0110 xor 1010 -> 1100 exclusive or
0x6 0xa 0xc
(The "exclusive or" of two bit patterns has a one bit where either one or the other but
not both of its two inputs had a 1 bit.)
You should test out these basic bit manipulations with the following program:
#include <iostream.h>
int main()
{
Bits val1, val2, val, result;
cout.setf(ios::showbase);
cout.setf(ios::hex,ios::basefield);
cout << "Enter two bit patterns to be Anded : " << endl;
cout << "Val1 :"; cin >> val1;
cout << "Val2 :"; cin >> val2;
result = val1 & val2;
cout << "Val1 : " << val1 << ", Val2 : " << val2
<< ", And gives " << result << endl;
cout << "Enter two bit patterns to be Ored : " << endl;
cout << "Val1 :"; cin >> val1;
cout << "Val2 :"; cin >> val2;
result = val1 | val2;
cout << "Val1 : " << val1 << ", Val2 : " << val2
<< ", Or gives " << result << endl;
cout << "Enter two bit patterns to be Xored : " << endl;
cout << "Val1 :"; cin >> val1;
cout << "Val2 :"; cin >> val2;
result = val1 ^ val2;
cout << "Val1 : " << val1 << ", Val2 : " << val2
<< ", Xor gives " << result << endl;
return 0;
}
Try to work out in advance the hex pattern that you expect as a result for the inputs that
you choose. Remember that an input value gets filled out on the left with 0 hex digits
so an input of 0x6 will become 0x00000006 and hence when complemented will give
0xfffffff9.
Abbreviated forms Of course there are abbreviated forms. If you are updating a bit pattern by
combining it with a few more bits, you can use the following:
Bit manipulations 509
In addition to the bitwise Ands, Ors etc, there are also operators for moving the bits Moving the bits
around, or at least for moving all the bits to the left or to the right. around
These bit movements are done with the shift operators. They move the bits in a data
element left or right by the specified number of places. A left shift operator moves the
bits leftwards, adding 0s at the right hand side. A right shift operator moves bits
rightwards. This is the one you need to be careful with regarding the use of unsigned
values. The right shift operator adds 0s at the left if a data type is unsigned; but if it is
given a signed value (e.g. just an ordinary long) it duplicates the leftmost bit when
shifting the other bits to the right. This "sign extension" is not usually the behaviour
that you want when working with bit patterns. Bits "falling out" at the left or right end
of a bit pattern are lost.
The shift operators are: Shift operators
Again ouch. You know these as the "takes from" and "gives to" operators that work
with iostreams.
In C++, many things get given more than one job (or, to put it another way, they get Overloaded operators
"overloaded"). We've just seen that & has sometimes got to go and find an address and
sometimes it has to combine bit patterns. The job at hand is determined by the context.
It is just the same for the >> and << operators. If a >> operator is located between an
input stream variable and some other variable it means "gives to", but if a >> operator
is between two integer values it means arrange for a right shift operation.
The following code fragment illustrates the working of the shift operators:
#include <iostream.h>
int main()
{
Bits val, result;
int i;
cout.setf(ios::showbase);
cout.setf(ios::hex,ios::basefield);
cout << "Enter val "; cin >> val;
cout << "Enter shift amount "; cin >>i;
return 0;
}
Moving by 8 places as requested in the example means that two hex digits (4-bits each)
are lost (replaced by zeros).
While you probably find no difficulty in understanding the actual shift operations,
you may be doubting whether they have any useful applications. They do; some uses
are illustrated in the examples presented in the next two sections.
2 As a summary signature of some data that can be used to check that these data are
unchanged since the summary was generated.
The idea of the search filter is that you use the key to eliminate most of the data in a
collection, selecting just those data elements that have equal keys. These data elements
can then be checked individually, using more elaborate comparisons to find an exact
match.
Hashing The process of generating a key is known as "hashing", and it is something of an art
form (i.e. there aren't any universal scientific principles that can lead you to a good
hashing function for a specific type of data). Here we consider only the case of text
strings because these are the most common data to which hashing is applied
Making a hash of it 511
If you have a string that you want to summarize in a key, then that key should depend
on every character in the string. You can achieve this by any algorithm that mixes up
(or "smashes together" of "hashes") the bits from the individual characters of the string.
The "XOR" function is a good way to combine bits because it depends equally on
each of its two inputs. If we want a bit pattern that combines bits from all the characters
in a string we need a loop that xors the next character into a key, then moves this key
left a little to fill up a long integer.
Of course, when you move the key left, some bits fall out the left end. These are the
bits that encode the first few characters. If you had a long string, the result could end
up depending only on the last few characters in the string.
That problem can be avoided by saving the bits that "fall out the left end" and
feeding them back in on the right, xoring them with the new character data.
The following function implements this string hashing:
The statements:
get the bits occupying the left most five bits of the current key and move them back to
the right. The variable Top5Bits would commonly be referred to as a "mask". It has
bits set to match just those bits that are required from some other bit pattern; to get the
bits you want (from Result) you "perform an and operation with the mask".
The bits in the key are moved left five places by the statement:
512 Bits and pieces
Then the saved leftmost bits are fed back in, followed by the next character from the
string:
Result ^= Carry;
Result ^= str[i];
The following outputs show the encodings obtained for some example strings:
mellow : 0xdc67bd97
yellow : 0xf467bd97
meadow : 0xdc611d97
callow : 0xc027bd97
shallow : 0x1627bd8b
shade : 0x70588e5
2,4,6-trinitrotoluene : 0xabe69e14
2,4,5-trinitrotoluene : 0xabe59e14
Bailey, Beazley, and Bradley : 0x64c55ad0
Bailey, Beazley, and Bradney : 0x64c552d0
This small sample of test strings doesn't have any cases where two different strings get
the same key, but if tried a (much) larger sample of strings you would find some cases
where this occurred.
Checking a hash key is quicker than comparing strings character by character. A
single comparison of the key values 0x64c55ad0 and 0x64c442d0 reveals that two
strings (the two B, B, & Bs) are dissimilar. If you were to use strcmp() , the loop
would have to compare more than twenty pairs of characters before the dissimilarity
was noted.
If you have a table of complex strings that has to be searched many times, then you
could gain by generating the hash codes for each string and using these values during
the search process. The modified table would have structs that contained the hash key
and the string. The table might be sorted on hash key value and searched by binary
search, or might simply be searched linearly. A hash key would also be generated for
any string that was to be found in the table. The search would check test the hash keys
for equality and only perform the more expensive string match in the (few) cases where
the hash keys matched.
You can make the lookup mechanism even faster. A hash key is an integer, so it
could represent the index of where an item (string) should be stored in an array. The
array would be initialized by generating hash keys for each of the standard strings and
then copying those strings into the appropriate places in the table. (If two strings have
the same hash key, the second one to be encoded gets put into the next empty slot in the
array.) When a string has to be found in the table, you generate the hash key and look
at that point in the array. If the string there didn't match, you would look at the strings
Hashing a string 513
in the next few locations just in case two strings had ended up with the same key so
causing one to be placed at a location after the place where it really should go.
Of course there is a slight catch. A hash key is an integer, but it is in the range 0…
4000million. You can't really have an array with four thousand million strings.
The idea of using the hash key as a lookup index is nice, even though impractical in its
most basic form. A slightly modified version works reasonably well.
The computed hash key is reduced so that instead of ranging from zero to four Modulo arithmetic
thousand million, its range is something more reasonable – zero to a few hundred or
few thousand. Tables (arrays) with a few thousand entries are quite feasible. The hash
key can be reduced to a chosen range 0…N-1 by taking its value modulo N:
key = HashString(data);
key = key % N;
Of course that "folds together" many different values. For example, if you took Hash collisions
numbers modulo 100, then the values 115, 315, 7915, 28415 etc all map onto the same
value (15). When you reduce hash keys modulo some value, you do end up with many
more "hash collisions" where different data elements are associated with the same final
key. For example, if you take those key values shown previously and reduce them
modulo 40, then the words meadow and yellow "collide" because both have keys that
are converted to the value 7.
The scheme outlined in the previous subsection works with these reduced size keys.
We can have a table of strings, initially all null strings (just the '\0' character in each).
Strings can be inserted into this table, or the table can be "searched" to determine
whether it already contains a string. (The example in the next section illustrates an
application using essentially this structure).
Figure 18.1 illustrates the structure of this slightly simplified hash table. There is an
array of one thousand words (up to 19 characters each). Unused entries have '\0'; used
entries contain the strings. The figure shows the original hash keys as generated by the
function given earlier; these are reduced modulo 1000 to get the index.
The following data structures and functions are needed:
LongWord theTable[kTBLSIZE];
514 Bits and pieces
99 Function\0 (2584456099)
\0
604 Application\0 (2166479604)
\0
\0
\0
void InitializeHashTable(void);
Initializes all table entires to '\0'
int NullEntry(int ndx);
Checks whether the entry at [ndx] is a null string
int MatchEntry(int ndx, const char str[]);
Checks whether the entry at [ndx] equals str (strcmp())
int SearchForString(const char str[]);
Searches table to find where string str is located,
returns position or -1 if string not present
void InsertAt(int ndx, const char str[]);
Copies string str in table entry [ndx]
int InsertString(const char str[]);
Organizes insertion, finding place, calling InsertAt();
A simple hash table 515
The code for these functions is simple. The "initialize table" function sets the
leading byte in each word to '\0' marking the entry as unused.
The insertion process has to find an empty slot for a new string, function "null entry"
tests whether a specified entry is empty. Function "match entry" checks whether the
contents of a non-empty slot matches a sought string.
Insertion of entries is handled by the two functions "insert at" and "insert string".
Function "insert at" simply copies a string into a (previously empty) table location:
The insert string function does most of the work. First, it "hashes" the string using
the function shown earlier to get a hash key. This key is then reduced modulo the
length of the array to get a possible index value that defines where the data might go.
The function then has a loop that starts by looking at the chosen location in the
array. If this location is "empty" (a null string), then the word can be inserted at that
point. The "insert at" function is called, and the insertion position returned.
If the location is not empty, it might already contain the same string (the same word
may be being entered more than once). This is checked, and if the word does match the
table entry then the function simply returns its position.
In other cases, a "collision" must have occurred. Two words have the same key. So
the program has to find an empty location nearby. This function uses the simplest
scheme; it looks at the next location, then the one after that and so forth. The position
516 Bits and pieces
indicator is incremented each time around the loop; taking its value modulo the array
length (so on an array with one thousand elements, if you started searching at 996 and
kept finding full, non-matching entries, you would try entries 997, 998, 999, then 0, 1,
etc.)
Eventually, this search should either find the matching string (if the word was
entered previously), or find an empty slot. Of course, there is a chance that the array is
actually completely full. If you ever get back to looking at the same location as where
you started then you know that the array is full. In this case the function returns the
value -1 to indicate an error.
for(;;) {
if(NullEntry(pos)) {
InsertAt(pos, str);
return pos;
}
if(MatchEntry(pos, str)) return pos;
pos++;
if(pos >= kTBLSIZE)
pos -= kTBLSIZE;
if(pos == startpos)
return -1;
}
}
The function that searches for a string uses a very similar strategy:
Specification
Write a program that will produce a table giving in order the fifty words that occur most
frequently in a text file, and details of the number of times that each occurred.
The program is to:
1 Prompt for a file name, and then to either open the file or terminate if the file
cannot be accessed.
2 Read characters from the file. All letters are to be converted to lower case.
Sequences of letters are to be assembled into words (maximum word length < 20
characters). A word is terminated by any non-alphabetic character.
3 Save all distinct words and maintain a count with each; this count is initialized to 1
on the first occurrence and is incremented for each subsequent occurrence.
4 When the file has been read completely, the words are to be sorted by frequency
and details of the fifty most frequent words are to be printed.
Design
The program will have a number of distinct phases. First, it has to read the file First iteration
identifying the words and recording counts. If we had a hash table containing little
structures with words and counts, an insert of a new word could initialize a count while
518 Bits and pieces
an insert of an already existing word would update the associated count. A hash table
makes it easy to keep track of unique words. These word and count data might have to
be reorganised when the end of the file is reached; the structs would need to be moved
into an array that can be sorted easily using a standard sort. Next, the data would get
sorted (modified version of Quicksort() from Chapter 13 maybe?). Finally, the
printouts are needed. An initial sketch for main() would be:
open file
get the words from file
reorganize words
sort
print selection
Most of these "functions" are simple. We've dealt with file opening many times
before. The hard part will be getting words from the file. While we still have to work
out the code for this function, we do know that it is going to end up with a hash table
some of whose entries are "null" and others are words with their counts. The sort step
is going to need an array containing just the data elements that must be sorted. But that
is going to be easy, we will just have to move the data around in the hash table so as to
collect all the words at the start of the array. Sorting requires just a modification of
Quicksort. The print out is trivial.
open file
prompt for filename
attempt to open file
if error
print warning message and exit
reorganize words
i = 0
for j = 0 ; j < hash-table-size; j++
if(! null_entry(j))
entry[i] = entry[j], i++
sort
modified version of quicksort and partition
uses an array of word structures,
sorting on the "count" field of these structures
(sorts in ascending order so highest frequency in
last array elements)
print selection
(need some checks, maybe less than 50 words!)
for j = numwords-1, j>= numwords - 50, j--
print details of entry[j]
Example: identifying the common words 519
The sort routine will need to be told the number of elements. It would be easy for this
to be determined by the "reorganize" routine; it simply has to return the value of its 'i'
counter.
These functions shouldn't require any further design iterations. They can be coded
directly from these initial specifications. With "sort" and "open file" it is just a matter
of "cutting and pasting" from other programs, followed by minor editing. The data type
of the arguments to the Quicksort() function must be changed, and the code that
accesses array elements in Partition() must be adjusted. These elements are now
going to be structs and the comparison tests in Partition() will have to reference
specific data members.
The "get the words" process requires more analysis, but the basic structure of the
function is going to be something simple like:
(It would be sensible for the insertion to be checked; the program should stop if the
hash table has become full causing the insert step to fail.)
The program needs slightly modified versions of the hash table functions given in Second iteration
the last section. There is no need for a search function. The other functions used to through design
manipulate the hash table have to be changed to reflect the fact that the table entries
aren't just character arrays, instead they are little structs that contain a character array
and a count. The main change will be in the insert function. If a word is already in the
array, its count is to be updated:
insert word
Get hash key
reduce key modulo array size
initialize search position from reduced key
loop
if entry at position is null
insert at …
return position
if entry at position has string equal to argument
update count in entry at position
return position
increment position (mod length of array)
check for table full (return failure -1 indicator)
The "get word" function should fill in a character array with letters read from the
file. The function should return true if it finds a word, false if there is no word (this will
only occur at the end of file). When called, the function should start by discarding any
leading whitespace, digit, or punctuation characters. It can start building up a word
with the first letter that it encounters.
520 Bits and pieces
Successive letters should be added to the word; before being added they must be
converted to lower case. This letter adding loop terminates when a non-letter is found.
The function has to check for words in the file that are larger than the maximum
allowed for. The program can be terminated if excessive length words are found.
The function has to put a '\0' after the last letter it adds to the word.
These considerations result in the following sketch for "get word":
Third iteration All that remains is to decide on data structures and function prototypes. We need a
struct that combines a character array and a count:
Twenty characters (19 + terminating '\0') should suffice for most words. The struct can
have a LongWord and a long count.
The "hash table" will need a large array of these structs:
(This gTable array needs about 90,000 bytes. Symantec 8 on the PowerPC handles
this; but Symantec 7 on a Mac-Quadra cannot handle static data segment arrays this
large. You will also have problems in the Borland environment; you will have to use
32-bit memory addressing models for this project.)
The only other global data element would be the ifstream for the input file.
Example: identifying the common words 521
void InitializeHashTable(void);
Modified version of function given previously. Initializes
both data members of all structs in "hash table" array;
struct's fWord character array set to null string, fCount
set to zero.
void OpenFile();
void GetTheWords(void);
Organize input or words and insertion into hash table.
int CompressTable(void);
Closes up gaps in table prior to sorting.
int main();
522 Bits and pieces
Implementation
Only a few of the functions are shown here. The rest are either trivial to encode or are
minor adaptations of functions shown earlier.
All the hash table functions from the previous section have modifications similar to
that shown here for the case of InitializeHashTable():
void InitializeHashTable(void)
{
for(int i=0; i< kTBLSIZE; i++) {
gTable[i].fWord[0] ='\0';
gTable[i].fCount = 0;
}
}
for(;;) {
if(NullEntry(pos)) {
InsertAt(pos, str);
return pos;
}
if(MatchEntry(pos, str)) {
gTable[pos].fCount++;
return pos;
}
pos++;
if(pos >= kTBLSIZE)
pos -= kTBLSIZE;
if(pos == startpos)
return -1;
}
}
The GetWord() function uses two loops. The first skips non alphabetic characters;
the second builds the words:
theWord[0] = '\0';
do {
gDataFile.get(ch);
if(gDataFile.eof())
return 0;
} while (!isalpha(ch));
while(isalpha(ch)) {
theWord[n] = tolower(ch);
n++;
if(n==(kLSIZE-1)) {
cout << "Word is too long" << endl;
exit(1);
}
gDataFile.get(ch);
if(gDataFile.eof())
break;
}
theWord[n] = '\0';
return 1;
}
The GetTheWords() function reduces to a simple loop as all the real work is done
by the auxiliary functions GetWord() and InsertWord():
void GetTheWords(void)
{
InitializeHashTable();
LongWord aWord;
while(GetWord(aWord)) {
int pos = InsertWord(aWord);
if(pos < 0) {
cout << "Oops, table full" << endl;
exit(1);
}
}
}
The CompressTable() function shifts all entries down through the array. Index i
identifies the next entry to be filled in. Index j runs up through all entries. If a non null
entry is found, it is copied and i incremented:
int CompressTable(void)
{
int i = 0;
int j = 0;
for(j = 0; j < kTBLSIZE; j++)
if(!NullEntry(j)) {
gTable[i] = gTable[j];
524 Bits and pieces
i++;
}
return i;
}
The Partition() function has minor changes to argument list and some
statements:
…
…
…
}
}
Function PrintDetails() uses a loop that runs backwards from the last used entry
tin the table, printing out entries:
void PrintDetails(int n)
{
int min = n - 50;
min = (min < 0) ? 0 : min;
for(int i = n-1, j = 1; i >= min ; i --, j++)
cout << setw(5) << j << ": "
<< gTable[i].fWord << ", \t"
<< gTable[i].fCount << endl;
}
int main()
{
OpenFile();
GetTheWords();
int num = CompressTable();
Quicksort(gTable, 0, num - 1);
PrintDetails(num);
return 0;
}
Example: identifying the common words 525
Part of the output from a run against a test data file is:
Enter filename
txtf.txt
1: the, 262
2: of, 126
3: to, 104
4: a, 86
5: and, 74
6: in, 70
7: is, 50
8: that, 41
9: s, 39
10: as, 32
11: from, 30
12: it, 29
13: by, 29
14: are, 27
15: for, 27
16: be, 27
Most of these are exactly the words that you would expect. What about things like 's'?
You get oddities like that. These will be from all the occurrence of 's at the ends of
words; the apostrophe ended a word, a new word starts with the s, then it ends with the
following space.
Because the common words are all things like "the", they carry no content
information about the document processed. Usually programs used to analyze
documents have filters to eliminate these standard words. An exercise at the end of this
chapter requires such an extension to the example program.
Where might you want actual bit data, that is collections of bits that individually have
meaning?
One use is in information retrieval systems.
Suppose you have a large collection of news articles taken from a general science
magazine (thousands of articles, lengths varying from 400 to 2000 words). You want to
have this collection arranged so that it can be searched for "articles of interest".
Typically, information retrieval systems allow searches in which articles of interest
are characterized as those having some minimum number of keywords from a user
specified group of keywords. For example, a request might require that at least four of
the following keywords be found in an article:
This example query would find articles describing scientific studies on the HIV virus
and/or the related virus that infects monkeys (SIV).
You wouldn't want the search program to read each article into memory and check
for occurrences of the user specified keywords, that would be much too slow.
However, if the keywords used for searches are restricted to those in a predefined set,
then it is fairly easy to implement search schemes that are reasonably fast.
These search schemes rely on indexes that relate keywords to articles. The simplest
approach is illustrated in Figure 18.2. The main file contains the source text of the
news articles. A second "index file" contains small fixed size data records, one for each
article in the main file.
Articles file
Wind
gene
rato
bein
cost
s as
pass
near
Aste
roid
hous
Eart
time
Eart
are
big
the
100
the
h."
rs
as
es
g
s
Index file
Location of article
Each index record contains a "bit map". Each bit corresponds to one of the
keywords from the standard set. If the bit is '1' it means that the article contained that
keyword; if the keyword is not present in the article, the corresponding bit in the map is
a zero. Because the articles vary in size, it is not possible to work out where an article
starts simply from knowing its relative position in the sequence of articles. So, the
index record corresponding to an article contains details of where that article starts in
the main file.
Two programs are needed. One adds articles to the main articles file and appends
index records to the index file. The second program performs the searches.
Both programs share a table that identifies the (keyword, bit number) details. It is
necessary to allow for "synonyms". For example, the articles used when developing
Example: coding property vectors 527
this example included several that discussed chimpanzees (evolution, ecology, social
habits, use in studies of AIDS virus, etc); in some articles the animals were referred to
as chimpanzees, in others they were "chimps". "Chimp" and chimpanzee are being
used as synonyms. If both keywords "chimpanzee" and "chimp" are associated with the
same bit number, it is possible to standardise and eliminate differences in style and
vocabulary.
A small example of a table of (keyword, bit numbers) is:
struct VocabItem {
LongWord fWord;
short fItemNumber;
};
VocabItem vTable[] = {
{ "aids", 0 },
{ "hiv", 1 },
{ "immunity", 2 },
{ "immune", 2},
{ "drug", 3 },
{ "drugs", 3 },
{ "virus", 4 },
…
};
A real information retrieval system would have thousands of "keywords" that map onto
a thousand or more standard concepts each related to a bit in a bit map. The example
here will be smaller; the bit maps will have 128 bits thus allowing for 128 different key
concepts that can be used in the searches.
The program used to add articles to the file would start by using the data in the Adding data to the
VocabItem table to fill in some variation on the hash table used in the last example. file
The various files needed would be opened (index file, articles file, text file with new
article). Words can then be read from the source text file using code similar to that in
the last example (the characters can be appended to the main articles file as they are
read from the source text file). The words from the file are looked up in the hash table.
If there is no match, the word is discarded. If the word is matched, then the word is one
of the keywords; its bit number is taken the matched record and used to set a bit in a bit
map record that gets built up. When the entire source text has been processed, the bit
map and related location data get appended to the index file.
The query program starts by prompting the user to enter the keywords that are to Running a query
characterize the required articles. The user is restricted to the standard set of keywords
as defined in the VocabItem table; this is easy to do by employing code similar to the
"pick keyword" function that has been illustrated in earlier examples. As keywords are
picked, their bit numbers are used to set appropriate bits in a bit map. Once all the
keywords have been entered, the user is prompted to specify the minimum number of
528 Bits and pieces
matches. Then each index record gets read in turn from the index file. The bit map for
the query and that from the index file are compared to find the number of bits that are
set in both. If this number of common bits exceeds the minimum specified, then the
article should be or interest. The "matched" article is read and displayed.
Bitmaps The bitmaps needed in this application will be simply small arrays of unsigned long
integers. If we have to represent 128 "concepts", we need four long integers. If you go
down to the machine code level, you may find that a particular machine architecture
defines a numbering for the bits in a word. But for a high level application like this,
you can chose your own. The chosen coding is shown in Figure 18.3.
Bit 32 ("ozone")
Setting a required bit In order to set a particular bit, it is necessary to first determine which of the unsigned
longs contains that bit (divide by 32) and then determine the bit position (take the bit
number modulo 32). Thus bit 77 would be the 13th bit in bitmap[2].
Counting the bits in Bits in common to two bit maps can be found by "Anding" the corresponding array
common entries, as shown in Figure 18.4 The number of bits in common can be calculated by
adding up the number of bits set in the result. There are several ways of counting the
number of bits set in a bit pattern. The simplest (though not the fastest) is to have a
loop that tests each bit in turn, e.g. to find the number of bits in an unsigned long x:
int count = 0;
int j = 1;
for(int i=0;i<32;i++) {
if(x & j)
count++;
j = j << 1;
}
Example: coding property vectors 529
Common bits
Specification
Implement an article entry program and a search program that together provide an
information retrieval system that works in the manner described above.
Design
These programs are actually easier than some of the earlier examples! The first
sketches for main()s for the two programs are:
and
search program
open files(
get the query
search for matches
530 Bits and pieces
Addition program
The open files routine for the addition program has to open an input file with the source
text of the article that is to be processed and two output files the main articles file and
the index file. New information is to be appended to these files. The files could be
opened in "append mode" (but this didn't work with one of the IDEs which, at least in
the version used, must have a bug in the iostream run time library). The files can be
opened for output specifying that the write position be specified "at the end" of any
existing contents.
open file
prompt for and open file with text input
(terminate on open error)
open main and index files for output, position "at end"
(terminate on open error)
The close files routine will be trivial, it will simply close all three files.
The main routine is the "process text" function. This has to initialize the hash table,
then fill it with the standard words, before looping getting and dealing with words from
the text file. When all the words have been dealt with, an assembled bit map must be
written to the index file; this bit map has to be zeroed out before the first word gets
dealt with.
Hash table The hash table will be like that in the example in the previous section. It will
contain small structs. This time instead of being a word and a count, they are "vocab
items" that consist of a name and a bit number.
The words are easy to "deal with". The hash table is searched for a match, if one is
found its bit number is set in the bit map.
Search program
The open files routine for this program needs to open both index and articles file. No
other special actions are required.
The task of "getting the query" can be broken down as follows:
The loop will be similar to those in previous examples. After each keyword is dealt
with, the user will be asked for a Yes/No response to a "More Keywords?" prompt.
The "search for matches" routine will have roughly the following structure:
show match
move "get pointer" to appropriate position in articles'
file
read character
while not end marker
print character
read next character
The articles in the file had better be separated by some recognizable marker character!
A null character ('\0') would do. This had better be put at the end of each article by the
addition program when it appends data to that file. The "show matching article"
function would probably need some extra formatting output to pretty things up. It
might be useful if either it, or the calling search routine, printed details of the number of
keywords matched.
Both programs share a need for bit maps and functions to do things like zero out the bit
maps when initializing things, setting chosen bits, and counting common bits. These
requirements can be met by having a small separate package that deals with bitmaps.
This will have a header file that contains a definition of what a bit map is and a list of
function prototypes. A second implementation file will contain the function definitions.
A bit map will be a small array of unsigned longs. This can be specified using a
typedef in the header file. The required functions seem to be:
532 Bits and pieces
Zero bits
clear all bits in the bit map
Set bit
work out which array element and which bit
or a 1 into the appropriate bit
Many of the functions in both programs are already either simple enough to be coded
directly, or are the same as functions used in other examples (e.g. the YesNo() function
used when asking for more keywords in the search program). A few require further
consideration.
The "Get Word" function for the addition program can be almost identical to that in
the example in 18.2. The only addition is that every character read must be copied to
the output articles file.
The "process text" function needs a couple of additions:
These additions make certain that there is a null character separating articles as required
by the search program's "show match" function. The other additions clarify the "related
info" comment in the original outline. Each record written to the index file has to
contain the location of the start of the corresponding article as well as a bit map. So the
"process" function had better note the current length of the articles file before it adds
any words. This is the "related info" that must then be written to the index file.
The "get a keyword" function needed in search can be modelled on the
PickKeyWord() function and its support routines developed in Section 12.4. Apart
from PickKeyWord() itself, there were the associated routines FindExactMatch(),
Example: coding property vectors 533
/* Filevocab.inc
Vocabulary file for information retrieval example.
*/
struct VocabItem {
LongWord fWord;
short fItemNumber;
};
VocabItem vTable[] = {
{ "aids", 0 },
{ "hiv", 1 },
…
…
{ "cancer", 28 },
{ "tumour", 29 },
{ "therapy", 30 },
{ "carcinogen", 31 },
{ "ozone", 32 },
{ "environment", 33 },
{ "environmental", 33 },
…
…
{ "toxin", 50 },
{ "poison", 50 },
{ "poisonous", 50 },
…
};
The vTable array and the count NumItems will be "globals" in both the programs.
Both programs will require a number of ifstream and/or ofstream variables that
get attached to files. These can be globals.
534 Bits and pieces
The addition program requires a "hash table" whose entries are VocabItems. This
won't be particularly large as it only has to hold a quick lookup version of the limited
information in the vTable array.
The search program could use globals for the bit map that represents a query and for
the minimum acceptable match.
The typedef defining a "bit map" would go in a header file along with the associated
function prototypes:
#endif
The function prototypes for the remaining functions in the two programs are:
void InitializeHashTable(void);
Fills hash table with "null VocabItems" (fWord field
== '/0', fItemNumber field == -1).
void InsertKeyWords(void);
Loops through all entries in vTable, inserting copies into
hash table.
void OpenFiles(void);
void CloseFiles(void);
void ProcessText(void);
Main loop of addition program.
int main();
search program
void OpenFiles(void);
int YesNo(void);
void GetTheQuery(void);
Loop building up bit map that represents the query.
void SearchForMatches(void);
int main();
Implementation
Only a few of the functions are shown here. The others are either identical to, or only
minor variations, of functions used in earlier examples.
536 Bits and pieces
Function SetBit() uses the scheme described earlier to identify the array element and
bit position. A "mask" with one bit set is then built by shifting a '1' into the correct
position. This mask is then Or-ed into the array element that must be changed.
Function CountCommonBits() Ands successive elements of the two bit patterns and
passes the result to function CountBits(). The algorithm used by CountBits() was
illustrated earlier.
The standard hash table functions all have minor modifications to cater for the
different form of a table entry:
void InitializeHashTable(void)
{
for(int i=0; i< kTBLSIZE; i++) {
gTable[i].fWord[0] ='\0';
gTable[i].fItemNumber = -1;
}
}
The OpenFiles() function for the articles addition program has a mode that is
slightly different from previous examples; the ios::ate parameter is set so that new
data are added "at the end" of the existing data:
void OpenFiles(void)
{
char fname[100];
cout << "Enter name of file with additional news article"
<< endl;
Example: coding property vectors 537
Function GetWord() is similar to the previous version, apart from the extra code to
copy characters to the output file:
do {
gTextFile.get(ch);
if(gTextFile.eof())
return 0;
gInfoData.write(&ch,1);
} while (!isalpha(ch));
while(isalpha(ch)) {
…
…
gInfoData.write(&ch,1);
}
theWord[n] = '\0';
return 1;
}
void ProcessText(void)
{
InitializeHashTable();
InsertKeyWords();
LongWord aWord;
538 Bits and pieces
Bitmap aMap;
ZeroBits(aMap);
long where;
where = gInfoData.tellp();
while(GetWord(aWord)) {
int pos = SearchForWord(aWord);
if(pos >= 0) {
int keynum = gTable[pos].fItemNumber;
SetBit(keynum, aMap);
}
}
char ch = '\0';
gInfoData.write(&ch, 1);
gInfoIndex.write(&aMap,sizeof(aMap));
gInfoIndex.write(&where,sizeof(long));
}
The call to tellp() gets the position of the end of the file because the open call
specified a move to the end. The value returned from tellp() is the starting byte
address for the article that is about to be added.
The principal functions from the search program are:
void GetTheQuery(void)
{
ZeroBits(gQuery);
cout << "Enter the terms that make up the query" << endl;
do {
int k = PickKeyWord(vTable, NumItems);
int bitnum = vTable[k].fItemNumber;
SetBit(bitnum, gQuery);
}
while (YesNo());
cout << "How many terms must match ? ";
cin >> gMinMatch;
Function GetTheQuery() builds up the query bit map in the global gQuery then set
gMinMatch.
Function ShowMatch() basically copies characters from the articles' file to the
output. There is one catch here. The file will contain sequences of characters separated
by an end of line marker. The actual marker character used will be chosen by the text
editor used to enter the original text. This "end of line" character may not result in a
new line when it is sent to the output (instead an entire article may get "overprinted" on
a single line). This is catered for in the code for ShowMatch(). A check is made for the
character commonly used by editors to mark an end of line (character with hex
representation 0x0d). Where this occurs a newline is obtained by cout << endl. (You
Example: coding property vectors 539
might have to change that hex constant if in your environment the editors use a different
character to mark "end of line".)
The SearchForMatches() function simply reads and checks each record from the
index file, using ShowMatch() to print any matches. An error message is printed if
nothing useful could be found.
void SearchForMatches(void)
{
Bitmap b;
long wh;
int matches = 0;
gIndexfile.seekg(0,ios::beg);
while(!gIndexfile.eof()) {
gIndexfile.read(&b, sizeof(Bitmap));
gIndexfile.read(&wh, sizeof(long));
int n = CountCommonBits(b, gQuery);
if(n>=gMinMatch) {
cout << "Matched on " << n << " keys"
<< endl;
ShowMatch(wh);
matches++;
}
}
if(matches == 0)
cout << "No matches" << endl;
}
A test file was built using approximately seventy articles from a popular science
magazine as input. The results of a typical search are:
------
This is definitely a "read only" topic in C++, and outside of a few text books and some
special purpose low-level code it is topic where you won't find much to read. Any low-
level code using the features described in this section will relate to direct manipulation
of particular groups of bits in the control registers of hardware devices. We will not
cover such machine specific detail and consider only the (relatively rare) usage in
higher level data structures.
Suppose you have some kind of data object that has many properties each one of
which can take a small number of values (mostly the same sorts of thing that you would
consider suitable for using enumerated types) e.g.:
If you did chose to work with enumerated types, the compiler would make each either a
single unsigned character, or an unsigned short integer. Your records would need at
least three bytes for colour, size, and finish; another unsigned byte would be needed
for the style. Using enumerated types, the typical struct representing these data would
be at least four bytes, 32-bits, in size.
But that isn't the minimum storage needed. There are five colors, for that you need
at most three bits. Another three bits could hold the size. The four finishes would fit in
two bits. Six bits would suffice for the style. In principle you could pack those data
into 14 bits or two bytes.
This is permitted using "bit fields":
#include <stdlib.h>
#include <iostream.h>
struct meany {
Bit fields 541
unsigned colour : 3;
unsigned size : 3;
unsigned finish : 2;
unsigned style : 6;
};
int main()
{
meany m;
m.colour = 4;
m.size = 1;
m.finish = 2;
m.style = 15;
cout << m.style << endl;
return EXIT_SUCCESS;
}
(With the compiler I used, a "meany" is 4 bytes, so I didn't get to save any space
anyway.)
Note, you are trading space and speed. For a small reduction in space, you are
picking up quite an overhead in the code needed to get at these data fields.
There is one possible benefit. Normally, if you were trying to pack several small A real advantage of
data fields into a long integer, you would need masking and shift operations to pick out bit fields
the appropriate bits. Such operations obscure your code.
If you use bit fields, the compiler generates those same masking and shift operations
to get at the various bit fields. The same work gets done. But it doesn't show up in the
source level code which is consequently slightly easier to read.
19
19 Beginners' Class
Chapter 17 illustrated a few applications of structs. The first was a reworking of an
earlier example, from Section 13.2, where data on pupils and their marks had to be
sorted. In the original example, the names and marks for pupils were in separate arrays.
Logically, a pupil's name and mark should be kept together; the example in Section 17.1
showed how this could be done using a struct. The marks data in these structs were
only examined by the sort function, and about the only other thing that happened to the
structs was that they got copied in assignment statements. In this example, the structs
were indeed simply things that kept together related data.
In all the other examples from Chapter 17, the structs were used by many functions. More than just a data
Thus the Points could be combined with an AddPoint() function, while the Customer collection
records in 17.3 were updated, transferred to and from disk, and printed. Although they
used arrays rather than structs, the examples in Chapter 18 also had data structures that
had many associated functions. Thus, the different forms of "hash table" all had
functions for initialization, searching for a key, inserting a key, along with associated
support functions (like the check for a "null" hash table entry). Similarly, the "bit
maps" for the information retrieval system in Section 18.3 had a number of associated
functions that could be used to do things like set specific bits.
You would have difficulties if you were asked "Show me how this code represents a Data and associated
'Customer' (or a 'Hash Table', or a 'Bit map')". The program's representation of these functions
concepts includes the code manipulating the structures as well as the structures
themselves.
This information is scattered throughout the program code. There is nothing to
group the functions that manipulate Customer records. In fact, any function can
manipulate Customer records. There may have been a PrintDetails(Customer&)
function for displaying the contents of a Customer record but there was nothing to stop
individual data members of a Customer record being printed from main().
When programs are at most a couple of hundred lines, like the little examples in
Chapters 17 and 18, it doesn't much matter whether the data structures and associated
functions are well defined. Such programs are so small that they are easy to "hack out"
using any ad hoc approach that comes to mind.
546 Beginners' class
As you get on to larger programs, it becomes more and more essential to provide
well defined groupings of data and associated functionality. This is particularly
important if several people must collaborate to build a program. It isn't practical to
have several people trying to develop the same piece of code, the code has to be split up
so that each programmer gets specific parts to implement.
Functional You may be able to split a program into separately implementable parts by
abstraction? considering the top level of a top-down functional decomposition. But usually, such
parts are far from independent. They will need to share many different data structures.
Often lower levels of the decomposition process identify requirements for similar
routines for processing these structures; this can lead to duplication of code or to
inconsistencies. In practice, the individuals implementing different "top level
functions" wouldn't be able to go off and proceed independently.
or "data abstraction" An alternative decomposition that focussed on the data might work better. Consider
the information retrieval example (Section 18.3), you could give someone the task of
building a "bit map component". This person would agree to provide a component that
transferred bit maps between memory and disk, checked equality of bit maps, counted
common bits in two maps, and performed any other necessary functions related to bit
maps. Another component could have been a "text record component". This could
have been responsible for displaying its contents, working with files, and (by curtesy of
the bit map component) building a bit map that encoded the key words that were present
in that text record.
The individuals developing these two components could work substantially
independently. Some parts of a component might not be testable until all other
components are complete and brought together but, provided that the implementors start
by agreeing to the component interfaces, it is possible to proceed with the detailed
design and coding of each component in isolation.
Data abstraction: its The code given in Chapter 18 had an interface (header) file that had a typedef for the
more than just a "bit map" data and function prototypes for the routines that did things like set specific
header file!
bits. A separate code file provided the implementations of those routines. As a coding
style, this is definitely helpful; but it doesn't go far enough. After all, a header file
basically describes the form of a data structure and informs other programmers of some
useful functions, already implemented, that can be used to manipulate such structures.
There is nothing to enforce controls; nothing to make programmers use the suggested
interface.
Building a wall Of course, if implementors are to work on separate parts of a program, they must
around data stick to the agreed interfaces. Consider the example of programmers, A, B, C, and D
who were working on a geometry modelling system. Programmer A was responsible
for the coordinate component; this involved points ( struct pt { double x, y; }; )
and functions like offset_pt(double dx, double dy). Programmer B was
uncooperative and did not want to wait for A to complete the points component.
Instead of using calls to offset_pt() B wrote code that manipulated the data fields
directly (pt p; … p.x += d1; ). B then left the project and was replaced by D.
Intoduction 547
identifying those functions that are allowed to access and change data members in an
object. These functions are mainly "member functions". Member functions form part
of the definition of the new data type.
The code for member functions is not normally included in the class declaration.
Usually, a class declaration is in a header file; the code implementing the member
functions will be in a separate code file (or files). However, at the point where they are
defined, member functions clearly identify the class of which they are a part.
Consequently, although the description of a class may be spread over a header file and
several code files, it is actually quite easy to gather it all together in response to a
request like "Show me how this code represents a 'Customer'."
Classes facilitate The major benefit of classes is that they encourage and support a "component oriented"
program design view of program development. Program development is all about repeatedly
reanalyzing a problem, breaking it down into smaller more manageable pieces that can
be thought about independently. The decomposition process is repeated until you know
you have identified parts that are simple enough to be coded, or which may already
exist. We have seen one version of this overall approach with the "top down functional
decomposition" designs presented in Part III. Components (and the classes that
describe them) give you an additional way of breaking up a problem.
Finding components In fact, for large programs you should start by trying to identify the "components"
needed in a program. You try to partition the problem into components that each own
some parts of the overall data and perform all the operations needed on the data that
they own. So you get "bit map" components, "hash table" components, "word"
components and "text record" components. You characterize how the program works in
terms of interactions among these components – e.g. "if the hash_table identifies a
vocab_item that matches the word, then get the bit number from that vocab_item and
ask the bit_map to set that bit". You have to identify all uses of a component. This
allows you to determine what data that component owns and what functions it performs.
Given this information, you can define the C++ class that corresponds to the identified
component.
Designing the classes Once you have completed an initial decomposition of the programming problem by
identifying the required classes, you have to complete a detailed design for each one.
But each class is essentially a separate problem. Instead of one big complex
programming problem, you now have lots little isolated subproblems. Once the data
and member functions of a class have been identified, most of the work on the design of
a class relates to the design and implementation of its individual member functions.
Designing the When you have reached the level of designing an individual member function of a
member functions class you are actually back in the same sort of situation as you were with the examples
in Parts II and III. You have a tiny program to write. If a member function is simple,
just assignments, loops, and selection statements, it is going to be like the examples in
Intoduction 549
Part II where you had to code a single main() . If a member function has a more
complex task to perform, you may have to identify additional auxiliary functions that do
part of its work. So, the techniques of top down functional decomposition come into
play. You consider coding the member function as a problem similar to those in Part
III. You break the member function down into extra functions (each of which becomes
an extra member function of the class) until you have simplified things to the point
where you can implement directly using basic programming constructs.
Breaking a problem into separately analyzable component parts gives you a way of Reusing components
handling harder problems. But there are additional benefits. Once you start looking for
component parts, you tend to find that your new program requires components that are
similar to or identical to components that you built for other programs. Often you can
simply reuse the previously developed components.
This is a bit like reusing algorithms by getting functions from a function library,
such as those described in Chapter 13. However, the scale of reuse is greater. If you
get a sort() function from a function library, you are reusing one function. If you get
a ready built bitmap class, you get an integrated set of functions along with a model for
a basic data structure that you require in your program. Increasingly, software
developers are relying on "class libraries" that contain classes that define ready made
versions of many commonly required components.
Issues relating to the decomposition of problems into manageable parts, and reuse,
exist as an underlying leitmotif or theme behind all the rest of the materials in the part
of the text.
Section 19.1 introduces C++ classes. It covers the declaration of a simple class and the
definition of its member functions. Classes can be complex. Many aspects are deferred
to later chapters (and some aspects aren't covered in this text at all).
The next two sections present simple examples. Class Bitmap, section 19.2, is a
more complete representation of the concept of a bit map data object similar to that
introduced in the information retrieval example in section 19.3. Class Number, section
19.3, represents an integer. You might think that with shorts, longs, (and long longs)
there are already enough integers. Those represented as instances of class Number do
however have some rather special properties.
A class declaration introduces the name of a new data type, identifies the data members
present in each variable of this type (i.e. each "class instance" or "object"), identifies the
550 Beginners' class
member functions that may be used to manipulate such variables, and describes the
access (security) controls that apply. A declaration has the form:
class Name {
details …
more details …
};
The declaration starts with the keyword class. The name of the class is then given.
The usual naming rules apply, the name starts with a letter and contains letters, digits
and underscore characters. (Sometimes, there may be additional naming conventions.
Thus you may encounter an environment where you are expected to start the name of a
class with either 'T' or 'C'. Such conventions are not followed here.)
The body of the declaration, with all the details, comes between { and } brackets.
The declaration ends with a semicolon. Compilers get really upset, and respond with
incomprehensible error messages, if you omit that final semicolon.
The details have to include a list of the associated functions and data members.
Initially the examples will be restricted slightly, the associated functions will all be
"member functions", that is they all are functions that are inherently part of the
definition of the concept represented by the class. (There are other ways of associating
functions with classes, they get touched on later in Section 23.2.) These lists of
member functions and data members also specify the access controls.
Keywords 'public' There are only two kinds of access that need be considered at this stage. They are
and 'private' defined by the keywords public and private. A public member function (or data
member) is one that can be used anywhere in the code of the program. A private data
member (or member function) is one that can only be used within the code of the
functions identified in the class declaration itself. Note the wording of the explanation.
Generally, data members are all private, there may also be a few private member
functions. The main member functions are all public (sometimes, but very rarely,
there may be public data members).
You can specify the appropriate access control for each data member and member
function individually, or you can have groups of public members interspersed with
groups of private members. But most often, the following style is used:
class Name {
public:
details of the "public interface" of the class
private:
private implementation details
and description of data members of each instance
of the class…
};
In older text books, you may find the order reversed with the private section defined
first. It is better to have the public interface first. Someone who wants to know how
Form of a class declaration 551
to use objects of this class need only read the public interface and can stop reading at
the keyword private.
The following examples are just to make things a little more concrete (they are
simplified, refinements and extensions will be introduced later). First, there is a class
defining the concept of a point in two dimensional space (such as programmer A, from
the earlier example, might have wished to define):
class Point {
public:
…
// Get cartesian coords
double X();
double Y();
// Get polar coords
double Radius();
double Theta();
// Test functions
int ZeroPoint();
int InFirstQuad();
…
// Modify
void Offset(double deltaX, double deltaY);
…
void SetX(double newXval);
…
// Comparisons
int Equal(const Point& other);
…
private:
void FixUpPolarCoords();
…
double fX, fY, fR, fTheta;
};
(The ellipses, "…", indicate places where additional member functions would appear in
actual class declaration; e.g. there would be several other test functions, hence the
ellipsis after function InFirstQuad(). The ellipsis after the public keyword marks
the place where some special initialization functions would normally appear; these
"constructor" functions are described in section 19.1.4 below.)
The public interface specifies what other programmers can do with variables that are The public role of
instances of this type (class). Points can be asked where they are, either in cartesian or Points
polar coordinate form. Points can be asked whether they are at the coordinate origin or
whether they are in a specified quadrant (functions like ZeroPoint() and
InFirstQuad()). Points can be asked to modify their data members using functions
like Offset() . They may also be asked to compare themselves with other points
through functions like Equal().
The private section of the class declaration specifies things that are concern of the The private lives of
programmer who implemented the class and of no one else. The data members Points
552 Beginners' class
generally appear here. In this case there are the duplicated coordinate details – both
cartesian and polar versions. For the class to work correctly, these data values must be
kept consistent at all times.
There could be several functions that change the cartesian coordinate values
(Offset(), SetX() etc). Every time such a change is made, the polar coordinates
must also be updated. During the detailed design of the class, the code that adjusts the
polar coordinates would be abstracted out of the functions like SetX() . The code
becomes the body for the distinct member function FixUpPolarCoords().
This function is private. It is an implementation detail. It should only be called
from within those member functions of the class that change the values of the cartesian
coordinates. Other programmers using points should never call this function; as far as
they are concerned, a point is something that always has consistent values for its polar
and cartesian coordinates.
For a second example, how about class Customer (based loosely on the example
problem in Section 17.3):
class Customer {
public:
…
// Disk transfers
int ReadFrom(ifstream& input);
int WriteTo(ofstream& output);
// Display
void PrintDetails(ofstream& out);
// Query functions
int Debtor();
…
// Changes
void GetCustomerDetails();
void GetOrder();
…
private:
UT_Word fName;
…
UT_Word fOrders[kMAXITEMS];
int fNumOrder;
double fAmountOwing;
Date fLastOrder;
};
Class Customer should define all the code that manipulates customer records. The
program in Section 17.3 had to i) transfer customer records to/from disk (hence the
WriteTo() and ReadFrom() functions, ii) display customer details (Print-
Details()), iii) check details of customers (e.g. when listing the records of all who
owed money, hence the Debtor() function), and had to get customer details updated.
A Customer object should be responsible for updating its own details, hence the
member functions like GetOrder().
Form of a class declaration 553
The data members in this example include arrays and a struct (in the example in
Section 17.3, Date was defined as a struct with three integer fields) as well as simple
data types like int and double. This is normal; class Point is atypical in having all its
data members being variables of built in types.
There is no requirement that the data members have names starting with 'f'. It is Minor point on
simply a useful convention that makes code easier to read and understand. A name naming conventions
starting with 'g' signifies a global variable, a name with 's' is a "file scope" static
variable, 'e' implies an enumerator, 'c' or 'k' is a constant, and 'f' is a data member of a
class or struct. There are conventions for naming functions but these are less frequently
adhered to. Functions that ask for yes/no responses from an object (e.g. Debtor() or
InFirstQuad()) are sometimes given names that end with '_p' (e.g Debtor_p()); the
'p' stands for "predicate" (dictionary: 'predicate' assert or affirm as true or existent).
ReadFrom() and WriteTo() are commonly used as the names of the functions that
transfer objects between memory and disk. Procedures (void functions) that perform
actions may all have names that involve or start with "Do" (e.g. DoPrintDetails()).
You will eventually convince yourself of the value of such naming conventions; just try
maintaining some code where no conventions applied.
A class declaration promises that the named functions will be defined somewhere.
Actually, you don't have to define all the functions, you only have to define those that
get used. This allows you to develop and test a class incrementally. You will have
specified the class interface, with all its member functions listed, before you start
coding; but you can begin testing the code as soon as you have a reasonable subset of
the functions defined.
Usually, class declarations go in header files, functions in code files. If the class
represents a single major component of the program, e.g. class Customer, you will
probably have a header file Customer.h and an implementation file Customer.cp.
Something less important, like class Point, would probably be declared in a header file
("geom.h") along with several related classes (e.g. Point3D, Rectangle, Arc, …) and
the definitions of the member functions of all these classes would be in a corresponding
geom.cp file.
A member function definition has the general form:
return type
class_name::function_name(arguments)
{
body of function
}
e.g.
554 Beginners' class
double
Point::Radius()
{
…
}
(Some programmers like the layout style where the return type is on a separate line so
that the class name comes at the beginning of the line.)
Scope qualifier The double colon :: is the "scope qualifier" operator. Although there are other uses,
operator the main use of this operator is to associate a class name with a function name when a
member function is being defined. The definitions of all functions of class Point will
appear in the form Point::function_name, making them easy to identify even if they
occur in a file along with other definitions like Point3D::Radius() or Arc::Arc-
Angle()).
Some examples of function definitions are:
double Point::Radius()
{
return fR;
}
Now you may find something slightly odd about these definitions. The code is
simple enough. Offset() just changes the fX and fY fields of the point and then
calls the private member function FixUpPolarCoords() to make the fR, and fTheta
members consistent.
The oddity is that it isn't clear which Point object is being manipulated. After all, a
program will have hundreds of Points as individual variables or elements of arrays of
Points. Each one of these points has its own individual fX and fY data members.
The code even looks as if it ought to be incorrect, something that a compiler should
stomp on. The names fX and fY are names of data members of something that is like a
Member function definition 555
struct, yet they are being used on their own while in all previous examples we have only
used member names as parts of fully qualified names (e.g. thePt.fX).
If you had been using the constructs illustrated in Part III, you could have had a
struct Pt and function OffsetPt() and FixPolars():
struct Pt { double x, y, r, t; };
The functions that change the Pt struct have a Pt& (reference to Pt) argument; the
value of this argument determines which Pt struct gets changed.
This is obviously essential. These functions change structs (or class instances). The
functions have to have an argument identifying the one that is to be changed. However,
no such argument is apparent in the class member function definitions.
The class member functions do have an implicit extra argument that identifies the Implicit argument
object being manipulated. The compiler adds the address of the object to the list of identifying the object
being manipulated
arguments defined in the function prototype. The compiler also modifies all references
to data members so that they become fully qualified names.
Rather than a reference parameter like that in the functions OffsetPt() and A "pointer
FixPolars() just shown, this extra parameter is a "pointer". Pointers are covered in parameter" identifies
the object
the next chapter. They are basically just addresses (as are reference parameters).
However, the syntax of code using pointers is slightly different. In particular a new
operator, ->, is introduced. This "pointer operator" is used when accessing a data
member of a structure identified by a pointer variable. A few examples appear in the
following code. More detailed explanations are given in the next chapter.
You can imagine the compiler as systematically changing the function prototypes
and definitions. So, for example:
gets converted to
556 Beginners' class
while
int Customer::Debtor()
{
return (fAmountOwing > 0.0);
}
gets converted to
The this pointer argument will contain the address of the data, so the compiler can
generate code that modifies the appropriate Point or checks the appropriate Customer.
Normally, you leave it to the compiler to put in "this->" in front of every reference
to a data member or member function; but you can write the code with the "this->"
already present. Sometimes, the code is easier to understand if the pointer is explicitly
present.
Once a class declaration has been read by the compiler (usually as the result of
processing a #include directive for a header file with the declaration), variables of that
class type can be defined:
#include "Customer.h"
…
void RunTheShop()
{
Customer theCustomer;
…
}
or
#include "geom.h"
Using class instances 557
Just like structs, variables of class types can be defined, get passed to functions by value
or by reference, get returned as a result of a function, and be copied in assignment
statements.
Most code that deals with structs consists of statements that access and manipulate
individual data members, using qualified names built up with the "." operator. Thus in
previous examples we have had things like:
strcpy(theTable[ndx].fWord, aWord);
With variables of class types you can't go around happily accessing and changing their
data; the data are walled off! You have to ask a class instance to change its data or tell
you about the current data values:
Point thePoint;
…
thePoint.SetX(5.0);
…
if(thePoint.X() < 0.0) …
These requests, calls to member functions, use much the same syntax as the code that
accessed data members of structures. A call uses the name of the object qualified by the
name of the function (and its argument list).
You can guess how the compiler deals with things. You already know that the
compiler has built a function:
thePoint.SetX(5.0);
__Point__SetX_ptsd(&thePoint, 5.0);
(The & "get the address of" operator is again used here to obtain the address of the point
that is to be changed. This address is then passed as the value of the implicit Point*
this parameter.)
558 Beginners' class
class Point {
public:
Point(double initalX = 0.0, double initialY = 0.0);
…
};
class Customer {
public:
Customer();
…
};
The constructor for class Point has two arguments, these are the initial values for
the X, Y coordinates. Default values have been specified for these in the declaration.
The constructor for class Customer takes no arguments.
These functions have to have definitions:
Customer::Customer()
{
strcpy(fName, "New customer");
strcpy(fAddress, "Delivery address");
…
fAmountOwing = 0.0;
}
Here, the constructors do involve calls to functions. The constructor for class Point
initializes the fX , fY data members but must also make the polar coordinates
consistent; that can be accomplished by a call to the member function FixUpPolar-
Coords(). The constructor for Customer makes calls to strcpy from the string library.
A class can have an overloaded constructor; that is there can be more than one Overloaded
constructor function, with the different versions having different argument lists. The constructors
class Number, presented in Section 19.3, illustrates the use of multiple constructors.
Constructors can get quite elaborate and can acquire many specialized forms.
Rather than introduce these now in artificial examples, these more advanced features
will be illustrated later in contexts where the need arises.
When you need to define intialized instances of a class, you specify the arguments
for the constructor function in the definition of the variable. For example:
Variable p1 is a Point initially located at 17.0, 0.5; p2 is located at 6.4, 0.0 (using the
default 0.0 value for initialY). Point p3 is located at 0.0, 0.0 (using the defaults for
both initialX and initialY).
double Point::Y()
{
return fY;
}
doesn't change the Point for which this function is invoked; whereas the Point is
changed by:
class Point {
public:
…
// Get cartesian coords
double X() const;
double Y() const;
// Get polar coords
double Radius() const;
double Theta() const;
// Test functions
int ZeroPoint() const;
int InFirstQuad() const;
…
// Modify
void Offset(double deltaX, double deltaY);
void SetX(double newXval);
…
// Comparisons
int Equal(const Point& other) const;
Functions that don't change the object should be qualified by the keyword const both in
the declaration, and again in their definitions:
const class instances Apart from acting as an additional documentation cue that helps users understand a
class, const functions are necessary if the program requires const instances of a class.
Now constancy isn't something you often want. After all, a const Customer is one
who never places an order, never sets an address, never does anything much that is
useful. But there are classes where const instances are meaningful. You can imagine
uses for const Points:
But there is a catch. The compiler is supposed to enforce constancy. So what is the
compiler to do with code like:
The second is a legitimate use of the variable MinWindowSize, the first changes it. The
first should be disallowed, the second is permitted.
The compiler can't know the rules unless you specify them. After all, the code for
class Point may have been compiled years ago, all the compiler has to go on is the
description in the header file. The compiler has to assume the worst. If you don't tell it
that a member function leaves an object unchanged, the compiler must assume that it is
changed. So, by default, a compiler refuses to let you use class member functions to
manipulate const instances that class.
If your class declaration identifies some members as const functions, the compiler
will let you use these with const instances.
Because the data in a class instance are protected, access functions have to be provided,
e.g.:
Code needing the x-coordinate of a point must call this access function:
Point p1;
…
while(p1.X() > kMIN) { …; p1.Offset(delta, 0.0); }
Calling a function simply to peek at a data member of an object can be a bit costly.
In practice, it won't matter much except in cases where the access function is invoked in
some deeply nested inner loop where every microsecond counts.
Now inline functions were invented in C++ to deal with situations where you want
function semantics but you don't want to pay for function overheads. Inlines can be
used to solve this particular version of the problem.
As always, the definition of an inline function has to have been read so that the
compiler can expand a function call into actual code. Since classes are going to be
declared in header files that get #included in all the code files that use class instances,
the inline functions definitions must be in the header file.
The following is the preferred style:
#ifndef __MYGEOM__
#define __MYGEOM__
class Point {
public:
562 Beginners' class
…
double X();
…
private:
…
double fX, fY, fR, fTheta;
};
#endif
#ifndef __MYGEOM__
#define __MYGEOM__
class Point {
public:
…
double X() { return this->fX; }
…
private:
…
double fX, fY, fR, fTheta;
};
Here, the body of the inline function appears in the function declaration (note, an
explicit this-> qualifier on the data members is highly advisable when this style is
used). The disadvantage of this style is that it makes the class declaration harder to
read. Remember, your class declaration is going to be read by your colleagues. They
want to know what your class can do, they don't really care how the work gets done. If
you put the function definitions in the class declaration itself, you force them to look at
the gory details. In contrast, your colleagues can simply ignore any inlines defined at
the end of a header file, they know that these aren't really their concern.
The information retrieval example, Section 18.3, illustrated one simple use of a bitmap
in an actual application The bitmaps in that example used '1/0' bits to indicate whether
a document contained a specific keyword.
Class Bitmap 563
Really, the example illustrated a kind of "set". The bitmap for a document
represented the "set of keywords" in that document. The system had a finite number of
keywords, so there was a defined maximum set size. Each possible keyword was
associated with a specific bit position in the set.
Such sets turn up quite often. So a "bit map" component is something that you
might expect to "reuse" in many applications.
This section illustrates the design and implementation of a class that represents such
bitmaps and provides the functions necessary to implement them. The implementation
is checked out with a small test program.
Designing a general component for reuse is actually quite hard. Normally, you have
a specific application in mind and the functionality that you identify is consequently
biased to the needs of that application. When you later attempt to reuse the class, you
find some changes and extensions are necessary. It has been suggested that a reusable
class is typically created, and then reworked completely a couple of times before it
becomes a really reusable component. So, the class developed here may not exactly
match the needs of your next application, but it should be a good starting point.
• Zeroing; reset all the bits to zero (the constructor function should call this, bit maps
should start with all their bits set to 0).
• Test bit i;
564 Beginners' class
• Flip bit i;
Change setting of a specified bit, if it was '1' it becomes '0' and vice versa.
• PrintOn
Produce a (semi)-readable printout of a bitmap as a series of hex values.
• Count
Get Bitmap to report how many bits it has set to '1'.
• Invert
Not clear whether this function would be that useful, but it might. The invert
function would flip all the bits; all the '1's become '0's, all the '0's become '1's (the
bitmap applies a "Not" operation to itself).
The next group of functions combine two bitmaps to produce a third as a result.
Having a class instance as a function's result is just the same as having a struct as a
result. As explained for structs, you must always consider whether this is wise. It can
involve the use of lots of space on the stack and lots of copying operations. In this case
it seems reasonable. Bitmap objects aren't that large and the function semantics are
sensible.
• "Not"
Returns a bit map that is the inverse of this bitmap.
Finally, we should probably include an "Equals" function that checks equality with a
second bitmap.
The discussion of design based on top down functional decomposition (Chapter 15)
favoured textual rather than diagrammatic representation. Diagrams tend to be more
useful in programs that use classes. Simple diagrams like that shown in Figure 19.1 can
provide a concise summary of a class.
Class Bitmap 565
class Bitmap
Bitmap() Constructor
Zero() clears all bits
SetBit() set, clear, specific bits
ClearBit()
Public interface
SetAs()
TestBit() examine specific bit
FlipBit() change bit
ReadFrom() binary transfers from/to
WriteTo() disk
PrintOn()
Count() readable printout
Invert() number of bits set
And() complement
Or() build new bit map by
XOr() Anding, Oring, Xoring
Not() with other
Equals() build complemented copy
The figure shows the class with details of its private data and functions all enclosed
within the class boundary. The public member functions stick outside the boundary. In
this example, all the member functions are public.
The first iteration through the design of a class is complete when you have
composed a diagram like Figure 19.1 that summarizes the resources owned by class
instances and their responsibilities.
The next stage in class design involves firming up the function prototypes, deciding
on the arguments, return types, and const status. (There may not be much use for const
Bitmaps, but you should still make the distinction between access functions that merely
look at the data of a class member and modifying procedures that change data.)
In general, it would also be necessary to produce pseudo-code outlines for each
function. This isn't done here as the functions are either trivial or are similar to those
explained in detail in Section 18.3.
The prototypes become:
Bitmap();
void Zero(void);
void SetBit(int bitnum);
void ClearBit(int bitnum);
void SetAs(int bitnum, int setting);
566 Beginners' class
About the only point to note are the const Bitmap& arguments in functions like
And(). We don't want to have Bitmaps passed by value (too much copying of data) so
pass by reference is appropriate. The "And" operation does not affect the two bitmaps
it combines, so the argument is const . The i/o functions take fstream reference
arguments.
Only 0 and 1 values are appropriate for argument setting in function SetAs(). It
would be possible to define an enumerated type for this, but that seems overkill. The
coding can use 0 to mean 0 (reasonable) and non-zero to mean 1.
Implementation
Header file for class The Bitmap class will be declared in a header file bitmap.h:
declaration
#ifndef __MYBITSCLASS__
#define __MYBITSCLASS__
class Bitmap {
public:
Bitmap();
Class Bitmap 567
void Zero(void);
void SetBit(int bitnum);
void ClearBit(int bitnum);
void SetAs(int bitnum, int setting);
int TestBit(int bitnum) const;
void FlipBit(int bitnum);
void ReadFrom(fstream& in);
void WriteTo(fstream& out) const;
void PrintOn(ostream& printer) const;
int Count(void) const;
void Invert(void);
#endif
This header file has a #include on fstream.h. Attempts to compile a file including
bitmap.h without fstream.h will fail when the compiler reaches the i/o functions. Since
there is this dependency, fstream.h is #included (note the use of conditional compilation
directives, if fstream.h has already been included, it isn't read a second time). There is
no need to #include iostream.h; the fstream.h header already checks this.
The functions implementing the Bitmap concept are defined in bitmap.c: Function definitions
in the separate code
#include "bitmap.h" file
Bitmap::Bitmap()
{
Zero();
}
void Bitmap::Zero(void)
{
for(int i=0;i<NUMWORDS; i++)
fBits[i] = 0;
}
The constructor, Bitmap::Bitmap(), can use the Zero() member function to initialize
a bitmap. Function Zero() just needs to loop zeroing out each array element.
{
if((bitnum < 0) || (bitnum >= MAXBITS))
return;
int word = bitnum / 32;
int pos = bitnum % 32;
Function TestBit() uses the same mechanism for identifying the array element and
bit and creating a mask with the chosen bit set. This mask is "Anded" with the array
element. If the result is non-zero, the chosen bit must be set.
Functions ReadFrom() and WriteTo() perform the binary transfers that copy the
entire between file and memory:
These functions do not check for transfer errors. That can be done in the calling
environment. When coding class Bitmap, you don't know what should be done if a
transfer fails. (Your IDE's version of the iostream library, and its compiler, may differ
slightly; the calls to read and write may need "(char*)" inserted before &fBits.)
The files produced using WriteTo() are unreadable by humans because they
contain the "raw" binary data. Function PrintOn() produces a readable output:
}
printer.flags(savedformat);
}
Function PrintOn() has to change the output stream so that numbers are printed in
hex. It would be somewhat rude to leave the output stream in the changed state! So
PrintOn() first uses the flags() member function of ostream to get the current
format information. Before returning, PrintOn() uses another overloaded version of
the flags() function to set the format controls back to their original state.
The Count() function uses a double loop, the outer loop steps through the array
elements, the inner loop checks bits in the current element. It might be worth changing
the code so that the inner loop was represented as a separate CountBitsInElement()
function. This would be a private member function of class Bitmap.
Functions Invert(), Not() and Equals() all have similar loops that check,
change, or copy and change successive array elements from fBits:
void Bitmap::Invert(void)
{
for(int i=0; i < NUMWORDS; i++)
fBits[i] = ~fBits[i];
}
Note the explicit use of the this-> qualifier in Not() and Equals(). These functions
manipulate more than one Bitmap, and hence more than one fBits array. There are the
fBits data member of the object executing the function, and that of some second
object. The this-> qualifier isn't needed but sometimes it makes things clearer.
You will also note that the Bitmap object that is performing the Equals() operation Accessing data
is looking at the fBits data member of the second Bitmap (other). What about the members of another
object of the same
"walls" around other's data? class
Classes basically define families of objects and there are no secrets within families.
An object executing code of a member function of the class is permitted to look in the
private data areas of any other object of that class.
The functions And(), Or(), and XOr() are very similar in coding. The only
difference is the operator used to combine array elements:
It isn't sufficient to write the class, you must also test it! The test program should Test program
automatically exercise all aspects of the class. This test program becomes part of the
class documentation. It has to be available to other programmers who may need to
extend or modify the existing class and who will need to retest the code after their
modifications.
572 Beginners' class
int main()
{
Test1();
Test2();
Test3();
return EXIT_SUCCESS;
}
There are three Test functions (and an auxiliary function). As the names imply, the
functions were developed and implemented in sequence. Basic operations were
checked out using Test1() before the code of Test2() and Test3() was
implemented.
The first test function checks out simple aspects like setting and clearing bits and
getting a Bitmap printed:
void Test1()
{
// Try setting some bits and just printing the
// resulting Bitmap
int somebits[] = { 0, 3, 4, 6, 9, 14, 21, 31,
32, 40, 48, 56,
64, 91, 92, 93, 94, 95,
500, 501
};
int n = sizeof(somebits) / sizeof(int);
Bitmap b1;
Set bits SetBits(b1, somebits, n);
Check the count cout << "Number of bits set in b1 is " << b1.Count()
function << endl;
cout << "(that should have said " << n << ")" << endl;
if(!b1.TestBit(3))
cout << "Something just ate my bitmap" << endl;
Inversion b1.Invert();
b1.Zero();
Class Bitmap 573
An auxiliary function SetBits() was defined to make it easier to set several bits:
Function Test2() checks the transfers to/from files, and also the Equals()
function:
void Test2()
{
int somebits[] = { 0, 1, 2, 3, 5, 7, 11, 13,
17, 19, 23, 29, 31, 37,
41, 47
};
int n = sizeof(somebits) / sizeof(int);
Bitmap b1;
SetBits(b1, somebits, n);
fstream tempout("tempbits", ios::out);
if(!tempout.good()) {
cout << "Sorry, Couldn't open temporary output file"
<< endl;
exit(1);
}
b1.WriteTo(tempout); Write a bit map to file
if(!tempout.good()) {
cout << "Seem to have had problems writing to file"
<< endl;
exit(1);
}
tempout.close();
Bitmap b2; Read a bitmap from
fstream tempin("tempbits", ios::in | ios::nocreate); file
if(!tempin.good()) {
cout << "Sorry, couldn't open temp file with"
574 Beginners' class
Check equality if(b1.Equals(b2)) cout << "so i got as good as i gave" <<
endl;
else cout << "which is sad" << endl;
The final function checks whether the implementations of And(), Or(), XOr() and
Not() are mutually consistent. (It doesn't actually check whether they are right, just
that they are consistent). It relies on relationships like: "A Or B" is the same as "Not
(Not A And Not B)".
void Test3()
{
int somebits[] = {
2, 4, 6, 8, 16,
33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55,
68, 74, 80, 86,
102, 112, 122, 132,
145, 154, 415, 451
};
int n = sizeof(somebits) / sizeof(int);
Bitmap b1;
SetBits(b1, somebits, n);
int otherbits[] = {
2, 3, 6, 9, 16,
23, 25, 27, 49, 52, 63, 75, 87, 99,
102, 113, 132, 143,
145, 241, 246, 362, 408, 422, 428, 429, 500, 508
};
int m = sizeof(otherbits) / sizeof(int);
Bitmap b2;
SetBits(b2, otherbits, m);
if(b6.Equals(b3)) cout << "The ands, ors and nots are " Check consistency of
"consistent" << endl; results
else cout << "back to the drawing board" << endl;
b6 = b6.And(b5);
if(b3.Equals(b6)) cout << "XOr kd" << endl;
else cout << "XOr what?" << endl;
}
A class isn't complete until you have written a testing program and checked it out.
Class Bitmap passed these tests. It may not be perfect but it appears useable.
Bitmaps are all very well, but you don't use them that often. So, we need an example of
a class that might get used more widely.
How about integers . You can add them, subtract them, multiply, divide, compare
for equality, assign them, ... You may object "C ++ already have integers". This is
true, but not integers like
96531861715696714500613406575615576513487655109
or
-33444555566666677777777888888888999999
or
176890346568912029856350812764539706367893255789655634373681547
453896650877195434788809984225547868696887365466149987349874216
93505832735684378526543278906235723256434856
The integers we want to have represented by a class are LARGE integers; though just
for our examples, we will stick to mediumish integers that can be represented in less
than 100 decimal digits.
576 Beginners' class
Why bother with such large numbers? Some people do need them. Casual users
include: journalists working out the costs of pre-election political promises, economists
estimating national debts, demographers predicting human population. The frequent
users are the cryptographers.
There are simple approaches to encrypting messages, but most of these suffer from
the problem that the encryption/decryption key has to be transmitted, before the
message, by some separate secure route. If you have a secure route for transmitting the
key, then why not use it for the message and forget about encryption?
Various more elaborate schemes have been devised that don't require a separate
secure route for key transmission. Several of these schemes depend in strange ways on
properties of large prime numbers, primes with 60...100 digits. So cryptographers often
do get involved in performing arithmetic with large numbers.
The largest integer that can be represented using hardware capabilities will be defined
by the constant INT_MAX (in the file limits.h):
but that is tiny, just ten decimal digits. How might you represent something larger?
One possibility would be to use a string:
"123346753245098754645661"
Strings make some things easy (e.g. input and output) but it would be hard to do the
arithmetic. The individual digits would have to be accessed and converted into numeric
values in the range 0…9 before they could be processed. The index needed to get a
digit would depend on the length of the string. It would all be somewhat inconvenient.
A better representation might be an array of numbers, each in the range 0…9, like
that shown in Figure 19.2. The array dimension defines largest number that can be
represented, so if we feel ambitious and want 1000 digit numbers then
fDigits[1000];
0 0 0 0 0 9 0 8 5 7 6 6 3 0 4 2 1
+
0 0 1 0 2 0 0 0 4 0 0 0 8 0 0 7 0
=
0 0 1 0 2 9 0 8 9 7 6 7 1 0 4 9 1
Arithmetic operations would involve loops. We could simply have the loops check
all one hundred (or one thousand) digits. Generally the numbers are going to have
fewer than the maximum number of digits. Performance will improve if the loops only
process the necessary number of digits. Consequently, it will be useful if along with the
array each "Number" object has details of its current number of digits.
Of course there is a problem with using a fixed size array. Any fixed sized number
can overflow . Overflow was explained in Part I where binary hardware representations
of numbers were discussed. Figure 19.3 illustrates the problem in the context of large
integers. The implementation of arithmetic operations will need to include checks for
overflow.
9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 3
+
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 7
=
Overflow
Figure 19.3 "Overflow" may occur with any fixed size number representation.
There has to be a way of representing signed numbers. The smart way would be to Signed numbers
use a representation know as "tens-complement". But that is getting really way out of
our depth. So instead, we can use "sign and magnitude". The array of digits will
represent the size (magnitude) of the number; there will be a separate data member that
represents the sign.
Sign and magnitude representations do have a problem. They complicate some
arithmetic operations. For example, you have to check the signs of both numbers that
578 Beginners' class
you are combining in order to determine whether you should be doing addition or
subtraction of the magnitude parts:
As well as performing the operation, you have to determine the sign of the result.
Figure 19.4 illustrates the mechanism of addition with "carry". This can be encoded
using a simple loop:
carry = 0;
for(int i=0; i < lim; i++) {
int temp;
temp = digit[i] of first number +
digit[i] of second number +
Carry;
if(temp>=10) {
Carry = 1; temp -= 10;
}
else Carry = 0;
digit [i] of result = temp;
}
The limit for the loop will be determined by the number of digits in the larger
magnitude value being added. The code has to check for overflow; this is easy, if
Carry is non-zero at the end of the loop then overflow has occurred.
The process of subtraction with "borrow" is generally similar. Of course, you must
subtract the smaller magnitude number from the larger (and reverse the sign if
necessary).
Class Number 579
0 9 0 4 8 3
+
0 0 0 5 2 8
1
1 carry
1
1 carry
0
0 carry
0 9 1
One could do multiplication by repeated addition, but that is a bit slow. Instead a
combination of "shifts", simple products, and addition steps should be used. The
"shifts" handle multiplying by powers of ten, as shown in Figure 19.5. The process
would have to be coded using several separate functions. An overall driver routine
could step through the digits of one of the numbers, using a combination of a "times
ten" and a "product" routine, and employing the functions written to handle addition.
0 0 0 0 0 0 4 1 5 2
Shifts
for multiplying
by 10, 100, 0 0 0 0 0 4 1 5 2 0
etc
0 0 0 0 0 0 4 1 5 2
0 0 0 0 4 1 5 2 0 0
Simple 0 1 4 0 3 7
product *6
=
0 8 4 2 2 2
Specification
Implement a class that represents multi digit integer numbers. This "Numbers" class
should support the basic arithmetic operations of addition, subtraction, multiplication,
and division. It should be possible to transfer Numbers to/from disk, and to print
readable representations of numbers. Comparison functions for numbers should be
defined. It should be possible to initialize a Number from an ordinary long integer
value, or from a given string of digits.
Class Number 581
Design
First, consider the data. What data does an individual Number own? Each instance of The data owned
class Number will have an array of unsigned long characters to store the digits. In
addition, there has to be a "sign" data member (another unsigned character) and a length
data member (a short integer). The size of the array of unsigned characters can be
defined by a #define constant.
The next step is to decide what "services" a Number object should provide. As in the Services provided by
previous example, you make up a list of useful functions – things that a Number might a Number
be expected to do. The list is definitely not going to be the complete list of member
functions. As already noted, functions like "Multiply" and "Divide" are complex and
will involve auxiliary routines (like the "times 10" and "product" needed by
"Multiply"). These extra auxiliary routines will be added to the list of member
functions as they are identified during the design process. These extra functions will
almost always be private; they will be simply an implementation detail.
When writing a test program to exercise a class, you may identify the need for
additional member functions. Getting a class right does tend to be an iterative process.
The following functions form a reasonable starting point:
• Constructors.
Several of these. The simplest should just initialize a Number to zero. The
specification required other constructors that could initialize a Number to a given
long and to a given string.
Another constructor that would probably be useful would initialize a Number given
an existing Number whose value is to be copied.
• Zero.
Probably useful, to have a function that tests whether a Number is zero, and another
function that zeros out an existing Number.
• ReadFrom, WriteTo.
Binary transfer to files.
582 Beginners' class
• PrintOn.
Output. Probably just print digit sequence with no other formatting. One hundred
digit numbers just about fit on a line. If Numbers were significantly larger, it would
be necessary to work out an effective way of printing them out over several lines.
• Compare.
Can return a -1, 0, or 1 result depending on whether a Number is less than, equal to,
or bigger than the Number that it was asked to compare itself with.
• ChangeSign.
Probably useful.
The functions that combine Numbers to get a Number as a result will take the second
Number as a reference argument.
Prototypes We can jump immediately to function prototypes and complete a partial declaration
of the class:
class Number {
public:
Number();
Number(long);
Number(char numstr[]);
…
It is worthwhile coming up with this kind of partial class outline fairly early in the
design process, because the outline can then provide a context in which other design
aspects can be considered. (The extra digit in the fDigits data member simplifies one
part of the division routines.)
There aren't many general design issues remaining in this class. One that should be Error handling
considered is error handling. There are several errors that we know may occur. We can
ignore i/o errors, they can be checked by the calling environment that transfers Numbers
to and from disk files. But we have to deal with things like overflow. Another problem
is dealing with initialization of a Number from a string. What should we do if we are
told to initialize a Number with the string "Hello World" (or, more plausibly,
"1203o5l7" where there are letters in the digit string)?
Chapter 26 introduces the C++ exception mechanism. Exceptions provide a means
whereby you can throw error reports back to the calling code. If you can't see a general
way of dealing with an error when implementing code of your class, you arrange to pass
responsibility back to the caller (giving the caller sufficient information so that they
know what kind of error occurred). If the caller doesn't know what to do, the program
can terminate. But often, the caller will be able to take action that clears the error
condition.
For now, we use a simpler mechanism in which an error report is printed and the
program terminates.
When defining a class, you always need to think about "assignment": Assignment of
Numbers
Number a:
Number b;
…
b = a;
Some of the member functions of class Number are simple, and for these it is possible to
sketch pseudo-code outlines, or even jump straight to implementation. These member
functions include the default constructor:
Number::Number()
{
fPosNeg = 0;
for(int i=0; i<= kMAXDIGITS; i++) fDigits[i] = 0;
fLength = 0;
584 Beginners' class
Number::Number(long lval)
{
fPosNeg = 0;
if(lval<0) { fPosNeg = 1; lval = -lval; }
for(int i=0; i<= kMAXDIGITS; i++) fDigits[i] = 0;
fLength = 0;
while(lval) {
fDigits[fLength++] = lval % 10;
lval = lval / 10;
}
}
Number::Number(char numstr[])
{
for(int i=0;i<= kMAXDIGITS; i++) fDigits[i] = 0;
fPosNeg = 0; /* Positive */
i = 0;
fLength = strlen(numstr);
length of the string is used to determine the number of digits in the number, and hence
the digit position in the array to be filled in with the first digit taken from the string.)
The final loop just corrects for any cases where there were leading zeros in the string.
Very often you need to say something like "Give me a Number just like this one". Copy constructor
This is achieved using a copy constructor. A copy constructor takes an existing class
instance as a reference argument and copies the data fields:
Other simple functions include the comparison function and the input/output
functions:
return 0;
}
while(i>=0) {
printer << char('0' + fDigits[i]);
i--;
}
The WriteTo() function uses a separate call to write() for each data member. (the
ReadFrom() function uses equivalent calls to read() ). Sometimes this gets a bit
clumsy; it may be worth inventing a struct that simply groups all the data members so
as to facilitate disk transfers.
A few of the member functions are sufficiently simple that they can be included as
inlines:
As noted previously, such functions have to be defined in the header file with the class
declaration.
Designing little The detailed design of the harder functions, like Subtract(), Multiply() and
programs! Divide() , really becomes an exercise in "top down functional decomposition" as
illustrated by the many examples in Part III. You have the same situation. You have a
"program" to write (e.g. the "Multiply program") and the data used by this program are
simple (the data members of two class instances).
The "Add Program" The addition and subtraction functions are to be called in code like:
and the "Subtract
Program" Number a("1239871154378100173165461515");
Number b("71757656755466753443546541431765765137654");
Number c;
Number d;
c = a.Add(b);
d = a.Subtract(b);
Class Number 587
The Add() (and Subtract() ) functions are to return, via the stack, a value that Sign and magnitude
represents the sum (difference) of the Numbers a and b. As noted in the introduction, representation
complicates the
sign and magnitude representations make things a bit more complex. You require lower process
level "do add" and "do subtract" functions that work using just the magnitude data. The
actual Add() function will use the lower level "do add" if it is called to combine two
values with the same sign, but if the signs are different the "do subtract" function must
be called. Similarly, the Subtract()function will combine the magnitudes of the two
numbers using the lower level "do add" and "do subtract" functions; again, the function
used to combine the magnitude values depends upon the signs of the values.
There is a further complication. The actual subtraction mechanism (using "borrows"
etc) only works if you subtract the smaller magnitude number from the larger
magnitude number. The calls must be set up to get this correct, with the sign of the
result being changed if necessary.
The following are sketches for the top level routines:
Add
// Returns sum of "this" and other
initialize result to value of "this"
if(other is zero)
return result;
The code starts by initializing the result to the value of "this" (so for a.Add(b), the
result is initialized to the value of a ). If the other argument is zero, it is possible to
return the result. Otherwise we must chose between using a "do add" function to
combine the partial result with the other argument, or calling DoSubtract() which will
sort out subtractions.
The code for the top-level Subtract() function is similar, the conditions for using
the lower-level addition and subtraction functions are switched:
Subtract
// Returns difference of "this" and other
initialize result to value of "this"
if(other is zero)
return result;
New member These sketches identify at least one new "private member function" that should be
functions identified added to the class. It will be useful to have:
This function checks the fPosNeg data members of an object and the second Number
(other) passed as an argument (i.e. return (this->fPosNeg == other.fPosNeg);).
This can again be an inline function.
We also need to initialize a variable to the same value as "this" (the object executing
the function). This can actually be done by an assignment; but the syntax of that kind
of assignment involves a deeper knowledge of pointers and so is deferred until after the
next chapter. Instead we can use a small CopyTo() function:
Both Add() and Subtract() invoked the auxiliary function DoSubtract(). This
function has to sort out the call to the actual subtraction routine so that the smaller
magnitude number is subtracted from the larger (and fix up the sign of the result):
DoSubtract
// Set up call to subtract smaller from larger magnitude
return result;
Yet another auxiliary function shows up. We have to compare the magnitude of
numbers. The existing Compare() routine considers sign and magnitude but here we
just want to know which has the larger digit string. The extra function, LargerThan(),
becomes an additional private member function. Its code will be somewhat similar to
that of the Compare() function involving checks on the lengths of the numbers, and
they have the same number of digits then a loop checking the digits starting with the
most significant digit.
An outline for the low level "do add" routine was given earlier when the "carry"
mechanism was explained (see Figure 19.4). The lowest level subtraction routine has a
rather similar structure with a loop that uses "borrows" between units and tens, tens and
hundreds, etc.
With the auxiliary functions needed for addition and subtraction the class declaration Class handling
becomes: additions and
subtractions
class Number {
public:
Number();
…
// Public Interface unchanged
…
int Compare(const Number& other) const;
private:
int SameSign(const Number& other) const;
void DoAdd(const Number& other);
Number DoSubtract(const Number& other, int subop) const;
void SubtractSub(const Number& other);
if(this->SameSign(other))
return DoSubtract(other, 1);
590 Beginners' class
else {
result.DoAdd(other);
return result;
}
}
The Add() routine is very similar. It should be possible to code it, and the auxiliary
DoSubtract() from the outlines given.
Both the DoAdd() and the SubtractSub() routines work by modifying a Number,
combining its digits with the digits of another number. The implementation of
DoAdd(), "add with carry", is as follows:
Handling overflow A routine has to exist to deal with overflow (indicated by carry out of the end of the
number). This would not need to be a member function. The implementation used a
static filescope function defined in the same file as the Numbers code; the function
printed a warning message and terminated.
The function SubtractSub() implements the "subtract with borrow" algorithm:
}
fLength = newlen;
if(fLength==0) fPosNeg = 0;
}
(If the result is ±0, it is made +0. It is confusing to have positive and negative version
of zero.)
In a phased implementation of the class, this version with just addition and
subtraction would be tested.
The usage of the multiplication routine is similar to that of the addition and The "Multiply
subtraction routines: Program"
Number a("1239871154378100173165461515");
Number b("71757656755466753443546541431765765137654");
Number c;
c = a.Multiply(b);
Function Multiply() again returns a Number on the stack that represents the product
of the Number executing the routine ('a') and a second Number ('b').
Sign and magnitude representation doesn't result in any problems. Getting the sign
of the product correct is easy; it the two values combined have the same sign the
product is positive otherwise it is negative. The basics of the algorithm was outlined in
the discussion related to Figure 19.5. The code implementing the algorithm is:
if(SameSign(other))
Result.fPosNeg = 0;
else Result.fPosNeg = 1;
return Result;
}
19704 * 6381
by calculating
Further private Two more auxiliary functions are needed. One, Times10(), implements the shifts to
member functions multiply by a specified power of ten. The other, Product(), multiplies a Number by a
value in the range 0…9. Both change the Number for which they are invoked. Partial
results from the individual steps accumulate in Result.
Function Times10() needs to check that it isn't going to cause overflow by shifting a
value too far over. If the operation is valid, the digits are just moved leftwards and the
number filled in at the right with zeros.
The Product() routine is a bit like "add with carry":
void Number::Product(int k)
{
int lim;
int Carry = 0;
if(k==0) {
MakeZero();
return;
}
lim = fLength;
for(int i=0;i<lim; i++) {
int temp;
temp = fDigits[i]*k + Carry;
Carry = temp / 10;
temp = temp % 10;
fDigits[i] = temp;
}
if(Carry) {
if(fLength == kMAXDIGITS) Overflow();
fDigits[fLength] = Carry;
Class Number 593
fLength++;
}
}
(unless your primary school teachers let you use a calculator). You found long division
hard then; it still is.
As with multiplication, getting the sign right is easy. If the dividend and divisor
have the same sign the result is positive, otherwise it is negative. The Divide()
routine itself can deal with the sign, and also with some special cases. If the divisor is
zero, you are going to get overflow. If the divisor is larger than the dividend the result
is zero. A divisor that is a one digit number is another special case handled by an
auxiliary routine. The Divide() function is:
if(!LargerThan(other))
return Result; // return zero
CopyTo(Result);
if(other.fLength == 1)
Result.Quotient(other.fDigits[0]);
else
Result.LongDiv(other);
return Result;
}
The auxiliary (private member) function Quotient() deals with simple divisions
(e.g. 77982451 ÷ 3 = 2599…). You start with the most significant digit, do the division
to get the digit, transfer any remainder to the next less significant digit:
void Number::Quotient(int k)
{
594 Beginners' class
int Carry = 0;
int newlen = 0;
for(int i= fLength-1;i>=0;i--) {
int temp;
temp = 10*Carry + fDigits[i];
fDigits[i] = temp / k;
if((newlen==0) && (fDigits[i] !=0)) newlen = i+1;
Carry = temp % k;
}
fLength = newlen;
}
int newlen = 0;
loop filling in for(int k = n - m; k>=0; k--) {
successive digits of int qt;
quotient qt = r.Trial(d,k,m);
if(qt==0) {
fDigits[k] = 0;
continue;
}
Number dq(d);
dq.Product(qt);
if(r.Smaller(dq,k,m)) { qt--; dq = dq.Subtract(d); }
if((newlen==0) && (qt !=0)) newlen = k+1;
fDigits[k] = qt;
r.Difference(dq,k,m);
}
fLength = newlen;
}
int km = k + m;
int r3;
int d2;
km = k + m;
r3 = (fDigits[km]*10 + fDigits[km-1])*10 + fDigits[km-2];
d2 = other.fDigits[m-1]*10 + other.fDigits[m-2];
int temp = r3 / d2;
return (temp<9) ? temp : 9;
}
With all these auxiliary private member functions, the final class declaration Final class
becomes: declaration
class Number {
public:
Number();
Number(long);
Number(const Number& other);
Number(char numstr[]);
Number(istream&);
void MakeZero();
int Equal(const Number& other) const;
void ChangeSign();
int Compare(const Number& other) const;
private:
int SameSign(const Number& other) const;
void DoAdd(const Number& other);
Number DoSubtract(const Number& other, int subop) const;
void SubtractSub(const Number& other);
Testing
int main()
{
Number a("123456789");
Number b("-987654");
Number c;
c = a.Add(b); cout << "Sum "; c.PrintOn(cout); cout <<
endl;
c = a.Subtract(b); cout << "Difference ";
c.PrintOn(cout); cout << endl;
c = a.Multiply(b); cout << "Product ";
c.PrintOn(cout); cout << endl;
c = a.Divide(b); cout << "Quotient ";
c.PrintOn(cout); cout << endl;
return 0;
}
Class Number 597
check the basics. As long the values are less than ten digits you can verify them on a
calculator or with a spreadsheet program (or even by hand).
You would then proceed to programs such as one calculating the expressions (a- Automatic test
b)*(c+d) and (ac-bc+ad-bd) for different randomly chosen values for the Numbers a, b, programs
c, and d (the two expressions should have the same value). A similar test routine for the
multiplication and divide operations would check that (a*b*c*d)/(a*c) did equal (b*d);
again the expressions would be evaluated for hundreds of randomly selected values. (A
small random value can be obtained for a Number as follows: long lv; lv = rand()
% 25000; lv -= 12500; Number a(lv); …. Alternatively, an additional
"Randomize()" function can be added to the class that will fill in Numbers with
randomly chosen 40 digit sequences and a random sign. You limit the size of these
random values so that you could multiply them without getting overflows). Such test
programs need only generate outputs if they encounter cases where the pair of
supposedly equal calculated values are not in fact equal.
If the class were intended for serious use in an application, you would then use a Optimising the code
profiling tool like that described in Chapter 14. This would identify where a test
program spent most time. The test program has to use Numbers in the same manner as
the intended application. If the application uses a lot of division so should the test
program.
Such an analysis was done on the code and the Product() routine showed up as
using a significant percentage of the run time. If you look back at the code of this
routine you will see that it treats multiplication by zero as a special case (just zero out
the result and return); other cases involve a loop that multiplies each digit in the
Number, getting the carry etc. Multiplying by 1 could also be made a special case. If
the multiplier is 1, the Number is unchanged. This special case would be easy to
incorporate by an extra test with a return just before the main loop in the Product()
routine. Retesting showed that this small change reduced the time per call to
Product() from 0.054 to 0.049µs (i.e. worthwhile). Similar analysis of other member
functions could produce a number of other speed ups; however, generally, such
improvements would be minor, in the order of 5% to 10% at most.
Now that you have seen example class declarations and functions, and seen objects
(class instances) being asked to perform functions, the syntax of some of the input and
output statements should be becoming a little clearer.
The iostream header file contains many class declarations, including:
Functions like put(), and write() are public member functions of the class. So, if
you have an ostream object out, you can ask that object to perform a write operation
using the expression:
out.write(&dataval, sizeof(dataval));
In Chapter 23, the strange "takes from" << and "gives to" >> operators will get
explained.
EXERCISES
1 Implement class Numbers, filling in all the simple omitted routines and check that these
Numbers do compute.
2 Add an "integer square root" member function that will use Newton's method to find the
integer x such that x is the largest integer for which x 2 <= N for a given Number N.
3 Often, users of these large numbers want the remainder and not the quotient of a division
operation. The LongDiv() routine actually computes but discards the remainder. Add an
extra member function that will calculate the remainder. It should be based on the existing
Divide() and LongDiv() functions.
4 Write a program that tests whether a given Number is prime. (The program will be
somewhat slow, probably not worth testing any numbers bigger than 15 digits.)
20
20 Dynamic data and
pointers
In all the examples considered so far, we have known how many data elements there
will be, or we have at least decided on some maximum number that we are prepared to
deal with. The data may be global, their space can be sorted out at compile-time (or
link-load time); or the data may be temporary such as variables that belong to a function
and which exist on the stack only while the code of that function is being executed. But
a lot of problems involve data that aren't like that. You know that you have to deal with
data objects, but you really don't know how many there will be, and nor do you know
for how long you will need them.
The main example in this chapter is an "Air Traffic Control" game. The game Objects with variable
involves the player controlling the movements of aircraft, giving them orders so that lifetimes
they line up with a runway, moving at an appropriate speed and rate of descent so that
they can land safely. The game simulates the passage of time and introduces new
aircraft into the controlled air space at a predefined frequency. The "lifetime" of an
individual aircraft varies depending on the skill of the player. Aircraft get created by
the game component; they get removed on successful landing (or by running out of
fuel). If the player is skilful, the correct sequence orders is given to adjust the aircraft's
speed, height and bearing through a number of stages until it is aligned with the
runway. If the player makes a mistake, an aircraft may have to overshoot and circle
around for a second try (or may run out of fuel). So the lifetime of aircraft does vary in
essentially arbitrary ways. The number of aircraft in existence at a particular moment
will also vary. At simple game levels there will be one or two (plus any making second
attempts after aborted landings); at higher game levels there could be dozens.
The game program would represent the aircraft using data structures (either structs
or instances of some class Aircraft). But these data structures have to be handled in
ways that are quite different from any data considered previously.
Section 20.1 introduces the "heap" and the operators that can be used to create and The "heap"
destroy data objects in the heap. The "heap" is essentially an area of memory set aside
600 Dynamic data and pointers
for a program to use to create and destroy data structures that have varied lifetimes like
the example aircraft.
Addresses and The actual allocation of space in the heap for a new data object is handled by a run-
"Pointers" time support routine. This "memory manager" routine will select some space in the
heap when asked to create an object of a given size. The memory manager reports
where the object has been placed by returning its address. This address becomes the
value held in a "pointer variable". The heap-based object has to be accessed "indirectly
via the pointer". Section 20.2 looks at issues like the definition of pointer variables, the
use of pointers to access data members of an object, and operations on entire structures
that are accessed through pointers.
"Air Traffic A simple Air Traffic Control game is used in Section 20.3 as a practical illustration.
Controller" This version doesn't have a particularly attractive user interface; it is just a framework
with which to illustrate creation, access, and destruction of objects.
The "address of" You can work with pointers to data elements other than those allocated on the heap.
operator We have already had cases where the ' &' address of operator was used to get a pointer
value (when passing an address to the read() and write() low level i/o functions).
Section 20.4 looks at the use of the & operator and a number of related issues. Many of
the libraries that you use are still C language libraries and so you must learn something
of C's styles of pointer usage.
Pointers and arrays Another legacy of C is the rather poorly defined concept of an array. C
programming idioms often abandon the concept of an array as a composite structure of
many elements identifiable and accessible using their index value. Instead the C hacker
style uses the assembly language level notions of a block of memory that can be
accessed via address registers that have been loaded with the addresses of byte locations
in memory. In this style, the contents of arrays are accessed using pointers and
arithmetic operations are performed on pointers to change their values so as to identify
different array elements. Because of the large amount of existing C code that you will
need to work with, you need a "reading knowledge" of some of these pointer idioms.
This is the content of Section 20.5. Note, it should be a "reading knowledge"; you
should not write code employing these coding idioms.
Section 20.6 discusses "networks", showing how complicated structures can be built
out of separate parts linked together with pointers.
As illustrated in Figure 20.1, the memory allocated to a program is divided into four
parts or "segments". The segments are: "code", "static data", "stack", and "heap". (This
organization is conceptual only. The actual realization on a given computer architecture
may well be considerably more complex.)
The Heap 601
Stack Heap
Frame for
"runtime" structure A
Frame for free
main()
structure D
Frame for
free
DoCommand()
structure X
Frame for
Move()
structure Z
free
Figure 20.1 Program "segments" in memory: code, static data, stack, and "heap".
The "code" segment contains the bit patterns for the instructions. The contents of Code segment
the code segment are composed largely by the compiler; the linking loader finalises
some of the addresses needed in instructions like function calls. The code segment will
include library routines that have been linked with the code specifically written for the
program. On machines with "memory protection" hardware, the code segment will be
effectively "read only".
The "static data segment" is used for those data variables that are global or at least "Static data
filescope. These are the variables that are defined outside of the body of a function. (In segment"
addition, there may be some variables that have been defined within functions, and
which have function scope, but which have been explicitly allocated to the static data
602 Dynamic data and pointers
segment. Such variables are rarely used; there are none in this text book.) Space for
variables in the static data segment is allocated by the linking loader. The variables are
initialized by a run-time support routine prior to entry to main(). In some cases, special
"at exit" functions may manipulate these variables after a return from main() (i.e. after
the program is nominally finished!). Such variables remain in existence for the entire
duration of the program (i.e. their lifetime exceeds that of the program).
Stack The stack holds stack frames. The stack frames hold the local variables of a function
together with the housekeeping details needed to record the function call and return
sequence. A stack frame is created as a function is called and is freed on exit. Local
variables of a function remain in existence during the execution of their own function
and all functions that it calls.
The "heap" The heap is a completely separate region of memory controlled by a run-time
"memory manager" support routine. (This is not the operating system's memory
manager that sorts out what space should be given to different programs. This run-time
memory manager is a library function linked to your code.) This run-time memory
manager handles requests for fixed sized blocks of memory.
"new" operator In C it is normal to make requests direct to the memory manager specifying the size
of blocks in terms of the number of bytes required. C++ has provided a higher level
interface through the new operator explained below. Using new, a function can request
the creation of a heap-based struct, a class instance, an array of variables of simple
types (e.g. an array of characters) or even an array of class instances. The new operator
works out the number of bytes needed and deals with all the other low level details
associated with a call to the actual run-time memory manager.
When a program starts, the operating system gives it some amount of memory for its
heap segment. The amount obviously varies with the system, but typical initial
allocations would be in the range from a quarter megabyte to eight megabytes. On
some systems, a program may start with a small area for its heap but is able to request
that the OS enlarge the heap later. One of the start up routines would record details of
the heap allocation and mark it all as "free".
Allocating space for When asked for a block of memory, the memory manager will search the heap
data structures looking for a "free" area that is large enough provide the space required and hold a little
additional housekeeping information. The memory manager needs to keep track of the
space it allocates. As illustrated in Figure 20.2, its "housekeeping records" are placed
as "headers" and "trailers" in the bytes just before and just after the space reserved for a
new structure. These housekeeping records note the size of the block and mark it as "in
use". When the memory manager function has finished choosing the space to be
allocated and has filled in its records, it returns the address of the start of the data area.
Freeing unneeded Structures allocated on the heap eventually get discarded by the program (as when,
structures in the example, the aircraft land or crash). Even if your programs starts with an eight
megabyte heap, you will eventually run out of memory if you simply created, used, and
then discarded the structures.
The Heap 603
Block with
Another Heap space
N
block not yet used
bytes
Block "trailer"
If you create dynamic structures in the heap, you are expected to give them back to delete operator
the memory manager when you no longer need them. The memory manager can then
reclaim the space they occupied. The memory manager will mark their headers as
"free". Subsequently, these blocks may get reallocated or merged with neighboring free
blocks. In C++, you pass discarded data structures back to the memory manager using
the delete operator.
It is common for programmers to be a bit careless about giving discarded data "Memory leaks"
structures back to the memory manager. Some dynamically allocated structures just get
forgotten. Such structures become "dead space" in the heap. Although they aren't used
they still occupy space. A part of the program's code that creates, uses, but fails to
delete structures introduces a "memory leak". If that code gets called many times, the
available heap space steadily declines ("my memory appears to be leaking away").
If your code has a memory leak, or if you are allocating exceptionally large Failure to allocate
structures you may eventually run out of heap space. The C++ language has defined memory
how the memory manager should handle situations where it is asked to create a new
structure and it finds that there is insufficient space. These language features are a bit
advanced; you will get to them in later studies. Initially, you can assume that requests
for heap space will always succeed (if a request does fail, your program will be
terminated and an error message will be printed).
604 Dynamic data and pointers
Overheads when The headers and trailers added by the memory manager would typically come to
allocating heap about 16 bytes, maybe more. This is a "space overhead" associated with every structure
structures
allocated in the heap. A program that tries to allocate individual char or int variables in
the heap almost certainly has a fundamental design error. The heap is meant to be used
for creation of reasonable sized data objects like structs or arrays.
The work that the memory manager must perform to handle calls via new and
delete is non-trivial. Quite commonly, profiling a program will reveal that a
measurable percentage of its time is spent in the memory management routines. You
should avoid creating and destroying structures inside deeply nested loops. The heap is
meant to be used for the creation of data objects that have reasonable lifetimes.
Using the new The following illustrate use of the new operator to create structures in the heap:
operator
struct Point3d { double fX, fY, fZ, fR, fTheta, fPhi; }
class Bitmap; // as declared in Chapter 19
class Number; // as declared in Chapter 19
…
… = new Point3d;
…
… = new Bitmap;
…
… = new Number("77777666555");
…
… = new char[kMAX];
Each of these uses of the new operator results in the return of the address of the start of a
newly allocated block of memory in the heap. These address values must be assigned
to pointer variables, as explained in the next section.
The first example creates a Point3d data structure. The data block allocated in the
heap would be just the right size to hold six double precision numbers.
The second example does a little more. The memory manager would allocate block
of heap space sufficient to hold a bit map object (an instance of class Bitmap from
chapter 19). Class Bitmap has a constructor function that initializes a bit map. The
code generated for the new operator has a call to this constructor function so as to
initialize the newly allocated data area.
The third example is similar, except that it involves an instance of class Number .
Class Number has several possible constructors; the one needed here is the one that
takes a character string. The code generated for the new operator would include a call to
that constructor so the newly allocated Number would be correctly initialized to a value
a little over seventy seven thousand million.
new [] operator The final example creates an array of characters. (The size of the array is
determined by the value in the [ ] brackets. Here the value is a constant, but an
expression is allowed. This make it possible to work out at run time the size of the
array needed for some specific data.) Technically, this last example is using a different
operator. This is the new [] operator (the "make me an array operator").
The Heap 605
20.2 POINTERS
Pointers are a derived data type. The pointyness, represented by a '*', is a modifier to
some basic data type (either built in like int or a programmer defined struct or class
type). The following are definitions of pointer variables:
int *ptr1;
char *ptr2;
Bitmap *ptr3;
Aircraft *ptr4;
These definitions make ptr1 a pointer to a data element that is an int; ptr2 is a pointer
to a character; ptr3 is a pointer to a Bitmap object; and ptr4 is a pointer to an
Aircraft object. Each of these pointer variables can be used to hold an address; it has
to be the address of a data variable of the specified type.
Pointers are type checked to the same degree as anything else is in C++. If you want
to store a value in ptr1, the value will have to be the address of a variable that is an
integer.
Definitions of pointer variables can cause problems. Some of the problems are due
to the free format allowed. As far as a C++ compiler is concerned, the following are
identical:
int *ptr1;
int* ptr1;
int * ptr1;
but strictly the * belongs with the variable name. It does matter. Consider the
following definition:
In this case pa is a pointer to an integer (something that can hold the address of an
integer variable) while pb is an integer variable. The * belongs on the variable name;
the definition really is int *pa, pb;. If you wanted to define two pointers you would
have to write int *pa, *pb;.
Although the "pointyness" qualifier * associates with a variable name, we need to
talk about pointer types independent of any specific instance variable. Thus, we will be
referring to int* pointers, char* pointers, and Aircraft* pointers.
At the end of the last section, there was an example that involved creating an array of
characters on the heap. The address returned by new char[10] has the type "address
of array of characters". Now char* is a pointer to a character can therefore hold the
address of a character. What would be the correct type declaration for a pointer to an
array of characters (i.e. something that can hold the address of an array of characters)?
For reasons partly explained in 20.5, a pointer to an array of characters is also
char*. This makes reading code a bit more difficult. If you see a variable of pointer
type being defined you don't know whether it is intended to hold the address of a single
instance of the specified data type or is meant to be used to refer to the start of an array
of data elements.
"Generic pointers"
Although pointers have types, you quite often need to have functions that use a pointer
to data of arbitrary type. A good example is the low-level write() function. This
function needs a pointer to the memory area that contains the data to be copied to disk
and an integer specifying the number of bytes to be copied. The actual write operation
involves just copying successive bytes from memory starting at the address specified by
the pointer; the same code can work for any type of data. The write() function will
accept a pointer to anything.
Originally in C, a char* was used when the code required a "pointer to anything".
After all, a "pointer to anything" must hold the address of a byte, a char is a byte, so a
pointer to anything is a char*. Of course, this just increases the number of possible
interpretations of char*. It may mean a pointer to a character, or it may mean a pointer
to an array of characters, or it may mean a pointer to unspecified data.
void* pointer type These days, the special type void* is generally preferred when a "pointer to
anything" is needed. However, a lot of older code, and almost all the older C libraries
that you may use from C++, will still use char*.
Pointer basics 607
Pointer casts
C++ checks types, and tries to eliminate errors that could arise if you assign the wrong
type of data to a variable. So, C++ would quite reasonably object to the following:
char *aPtr;
…
aPtr = new Point3d;
Here the new operator is returning "address of a Point3d", a char* is something that
holds an "address of a character". The type "address of a Point3d" is not the same as
"address of a character". So, the assignment should be challenged by the compiler,
resulting in at least a warning if not an error message.
But you might get the same error with the code:
Point3d *bPtr;
…
bPtr = new Point3d;
…
theOutputFile.write(bPtr, sizeof(Point3d));
Function write() requires a pointer with the address of the data object, you want it to
write the contents of the Point3d whose address is held in bPtr. You would get an
error (depends on your compiler and version of iostream) if the function prototype was
something like:
write(char*, int);
The compiler would object that the function wanted a char* and you were giving it a
Point3d*.
In situations like this, you need to tell the compiler that you want it to change the
interpretation of the pointer type. Although the bPtr really is a "pointer to a Point3d"
you want it to be treated as if it were a "pointer to char".
You achieve this by using a "type cast": Casting to char* or
void*
char *aPtr;
Point3d *bPtr;
…
…
bPtr = new Point3d;
…
theOutputFile.write((char*)bPtr, sizeof(Point3d));
The construct:
(char*)
(some address value from a pointer, a function,
or an operator)
tells the compiler to treat the address value as being the address of a character. This
allows the value to be assigned to a char* variable or passed as the value of a char*
argument.
If a function requires a void* argument, most compilers allow you to use any type
of pointer as the actual argument. A compiler allowing this usage is in effect putting a
(void*) cast into your code for you. Occasionally, you might be required to put in an
explicit (void*) cast.
Casting from specific pointer types like Aircraft*, Point3d*, or Bitmap* to
general types like char* and void* is safe. Casts that convert general pointers back to
specific pointer types are often necessary, but they do introduce the possibility of errors.
Casting a void* to a In Chapter 21, we look at a number of general purpose data structures that can be
specific pointer type used to hold collections of data objects. The example collection structures don't store
copies of the information from the original data objects, instead they hold pointers to
the data objects. These "collection classes" are intended to work with any kind of data,
so they use void* data pointers. There is a difficulty. If you ask the object that
manages the collection to give you back a pointer to one of the stored data objects you
are given back a void* pointer.
Thus you get code like the following:
class Job {
public:
Job(int codenum, Name customer, ....);
…
int JobNumber(void) const;
…
};
class Queue {
public:
…
void Append(void* ptr_to_newitem);
int Length(void) const;
void *First(void);
…
};
?? = theQueue.First();
The Queue object returns a void* pointer that holds the address of one of the Job
objects that was created earlier. But it is a void* pointer. You can't do anything much
with a void* .
A type cast is necessary:
A cast like this is perfectly reasonable and safe provided the program is properly
designed. The programmer is telling the compiler, "you think it could be a pointer to
any kind of data, I know that it is a pointer to a Job object, let me use it as such".
These casts only cause problems if there are design flaws. For example, another
programmer might incorrectly imagine that the queue held details of customers rather
than jobs and write code like:
Customer* c;
// get customer from queue, type cast that void*
c = (Customer*) theQueue.First();
The compiler has to accept this. The compiler can't tell that this programmer is using
the queue incorrectly. Of course, the second programmer will soon be in difficulties
with code that tries to treat a Job object as if it were a Customer object.
When you write or work with code that type casts from general (void*) to specific
(Job* or Customer*) you should always check carefully to verify the assumptions
being made in relation to the cast.
Pointers don't have any meaningful value until you've made them point somewhere! NULL
You make a pointer variable point somewhere by assigning a value; in C++ this will
most often be a value returned by the new operator. There is a constant, NULL, defined
in several of the header files, that represents the concept of "nowhere". You can assign
the constant NULL to a pointer variable of any type:
Often you will be working with collections of pointers to data items, a pointer whose
value is NULL is frequently used to mark the last element of the collection. The loops
that control working through the collection are set up so that they stop on finding a
610 Dynamic data and pointers
NULL pointer. These NULL pointers serve much the same role as "sentinel values" used
in loops that read input (as discussed in Chapter 9).
You can test whether a pointer is NULL using code like:
if(ptrA != NULL) {
// Process Aircraft accessed via ptrA
…;
}
or:
if(ptrA) {
// Process Aircraft accessed via ptrA
…;
}
In effect, NULL equates to 0 or "false". The second form is extremely common; the first
version is actually slightly clearer in meaning.
Global and filescope pointer variables are initialized to NULL by the linking-loader.
Automatic pointer variables, those defined as local to functions, are not normally
initialized. Their initial contents are arbitrary; they contain whatever bit pattern was in
memory at the location that corresponds to their place in the function's stack frame.
Beware of An amazingly large proportion of the errors in C and C++ programs are due to
uninitialized pointers programmers using pointers that have never been set to point anywhere. The arbitrary
bit pattern in an uninitialized pointer may represent an "illegal address" (e.g. address -5,
there is no byte whose address is -5). These illegal addresses are caught by the
hardware and result in the operating system stopping the program with an error such as
"segmentation fault", "bus error", "system error 2" etc.
Other uninitialized pointers may by chance hold addresses of bytes in one of the
program's segments. Use of such an address may result in changes to arbitrary static,
stack-based, or heap-based variables or even overwriting of code. Such errors can be
quite hard to track down because the problems that they cause frequently don't show up
until long after the time that the uninitialized pointer was used. Fortunately, modern
compilers can spot many cases where code appears to be using an uninitialized variable;
these cases result in warning messages that must be acted on.
cout << "ptrA now holds address " << hex << ptrA << endl;
Pointer basics 611
In some cases you will need to convert the pointer to a long integer, e.g. long(ptrA).
There is no purpose in writing the value of a pointer to a disk file (nor of writing out
a structure that contains pointer data members). The data written would be useless if
read back in on a subsequent run of the program.
The value in a pointer is going to be an address, usually one chosen by the run-time
memory manager and returned by the new operator. If you run the program another
time, the run-time memory manager might start with a different area of memory to work
with and almost certainly will allocate data areas differently. Consequently, data
objects end up at quite different places in memory on different runs. Yesterday's
addresses are no use.
Assignment of pointers
class Demo {
public:
Demo(int i, char c);
…
private:
int fi;
char fc;
};
Demo::Demo(int i, char c) { fi = i; fc = c; }
int main()
{
Demo *ptr1 = NULL;
Demo *ptr2 = NULL;
// stage 1
ptr1 = new Demo(6, 'a');
ptr2 = new Demo(7, 'b');
// stage 2
ptr2 = ptr1;
// stage 3
…
}
1 2
Stack Heap
Frame for
runtime 6, A
Frame for
main 7, B
ptr1
ptr2
In stage 3, the contents of ptr1 (the address of the Demo object with values 6, A) is
copied into ptr2. This makes both pointers hold the same address and so makes them
point to the same object. (Note that this code would have a memory leak; the second
Demo object, 7,B, has been abandoned but remains in the heap.)
Assignment of pointer variables simply means copying an address value from one to
another. The data addressed by the pointers are not affected.
Using pointers to access the data members and member functions of structures
If a structure can be accessed by a pointer, its data members can be manipulated. C and
C++ have two styles by which data members can be referenced.
Using pointers 613
The more common style uses the -> (data member access) operator: The -> operator
struct Thing {
int fNum;
double fD;
char fX;
};
int main()
{
Thing *pThing;
pThing = new Thing;
pThing->fNum = 17;
pThing->fX = '?';
pThing->fD = 0.0;
…
…
if(pThing->fNum < kLIM)
…;
…
xv += pThing->fD;
The -> operator takes the name of a (typed) pointer variable on its left (e.g. pThing, a
Thing* pointer), and on its right it takes the name of a data member defined as part of
that type (e.g. fX ; the compiler checks that fX is the name of a data member of a
Thing ). The -> operator produces an address: the address of the specified data
member of a structure starting at the location held in the pointer. (So pThing->fNum
would typically return the address held in pThing, pThing->fD would return an
address value 4 greater, while pThing ->fX would return an address value 12 greater
than the starting address.)
If the expression involving the -> operator is on the left side of an = assignment
operator (i.e. it is an "lvalue"), the calculated address specifies where something is to be
stored (e.g. as in pThing->fX = '?', where the address calculated defines where the '?'
is to be stored). Otherwise, the address is interpreted as the place from where a data
value is to be fetched (e.g. as in if(pThing->fNum …) or += pThing->fD;).
There is a second less commonly used style. The same operations could be coded as (*). operator
follows: combination
(*pThing).fNum = 17;
(*pThing).fX = '?';
(*pThing).fD = 0.0;
…
…
if((*pThing).fNum < kLIM)
…;
…
xv += (*pThing).fD;
614 Dynamic data and pointers
Job *my_next_task;
…
cout << "Now working on " << my_next_task->JobNumber() << endl;
Though you usually want to access individual data members (or member functions) of
an object, you sometimes need to manipulate the object as a whole.
You get the object by dereferencing the pointer using the * operator. Once you
have the object, you can do things like assignments:
int main()
{
Demo *ptr1 = NULL;
Demo *ptr2 = NULL;
ptr1 = new Demo(6, 'a');
ptr2 = new Demo(7, 'b');
The *ptr2 on the left side of the = operator yields an address that defines the target
area for a copying (assignment) operation. The *ptr1 on the right hand side of the =
operator yields an address that is interpreted as the address of the source of the data for
the copying operation.
*this In the example on class Number, we sometimes needed to initialize a new Number
(result) to the same value as the current object. The code given in the last chapter
used an extra CopyTo() member function. But this isn't necessary because we can
code the required operation more simply as follows:
{
Number result;
result = *this;
…
}
The implicitly declared variable this is a Number*. It has been initialized to hold the
address of the object that is executing the Subtract() function. If we want the object
itself, we need to dereference this (hence *this ). We can then directly assign its
value to the other Number result.
Alternatively we could have used the copy constructor Number(const Number&).
This function has to be passed the Number that is to be copied. So we need to pass
*this:
The following code fragment illustrates how you could work with an array allocated in
the heap. The array in this example is an array of characters. As explained previously,
the pointer variable that is to hold the address returned by the new [] ("give me an
array" operator) is just a char*. But once the array has been created, we can use the
variable as if it were the name of a character array (i.e. as if it had been defined as
something like char ptrC[50]).
#include <iostream.h>
int main()
{
cout << "How big a string do you want? ";
int len;
cin >> len;
for(;;) {
int lnum;
char ch;
cout << "# ";
cin >> lnum;
if((lnum < 0) || (lnum >= len-1)) break;
ptrC[lnum] = ch;
}
delete [] ptrC;
return 0;
}
Note that the programmer remembered to invoke the delete [] operator to get rid of
the array when it was no longer required.
Awful warning on The code shown makes certain that character change operations are only attempted
dynamically allocated on characters that are in the range 0 … N-2 for a string of length N (the last character in
arrays
position N-1 is reserved for the terminating '\0'). What would happen if the checks
weren't there and the code tried to change the -1th element, or the 8th element of an
array 0…7?
The program would go right ahead and change these "characters". But where are
they in memory?
If you look at Figure 20.2, you will see that these "characters" would actually be
bytes that form the header or trailer housekeeping records of the memory manager.
These records would be destroyed when the characters were stored.
Now it may be a long time before the memory manager gets to check its
housekeeping records; but when it does things are going to start to fall apart.
Bugs related to overwriting the ends of dynamically allocated arrays are very
difficult to trace. Quite apart from the delay before any error is detected, there are other
factors that mean such a the bug will be intermittent!
It is rare for programs to have mistakes that result in negative array indices so
overwriting the header of a block containing an array is not common. Usually, the
errors relate to use of one too many data element (and hence overwriting of the trailer
record). But often, the trailer record isn't immediately after the end of the array, there is
a little slop of unused space.
The program will have asked for an array of 40 bytes; the memory manager may
have found a free block with 48 bytes of space (plus header and trailer). This is close
Using pointers 617
enough; there is no need to find something exactly the right size. So this block gets
returned and used. An overrun of one or two bytes won't do any damage.
But the next time the program is run, the memory manager may find a free block of
exactly the right size (40 bytes plus header and trailer). This time an overrun causes
problems.
Be careful when using dynamically allocated arrays!
If you have a pointer to a dynamically allocated struct or class instance you don't want, delete operator
simply invoke the delete operator on that pointer:
Aircraft *thePlane;
…
if(thePlane->OutOfFuel())
delete thePlane;
The delete operator passes details back to the memory manager which marks the space
as free, available for reallocation in future.
The pointer isn't changed. It still holds the value of the now non-existent data
structure. This is dangerous. If there is an error in the design of the code, there may be
a situation where the data object is accessed after it is supposed to have been deleted.
Such code will usually appear to work. Although the memory area occupied by the data
object is now marked as "free" it is unlikely to be reallocated immediately; so it will
usually contain the data that were last saved there. But eventually, the bug will cause
problems.
It is therefore wise to change a pointer after the object it points to is deleted:
if(thePlane->OutOfFuel()) {
delete thePlane;
thePlane = NULL;
}
Setting the pointer to NULL is standard. Some programmers prefer to use an illegal
address value:
thePlane = (Aircraft*)0xf5f5f5f5
Any attempt to reuse such a pointer will immediately kill the program with an address
error; such an automatic kill makes it easier to find the erroneous code.
The delete [] operator should be used to free an array created using the new [] delete [] operator
operator.
618 Dynamic data and pointers
These new pointer types permit slightly better solutions to some of the examples that
we have looked at previously.
Strings
Section 11.7 introduced the use of character arrays to hold constant strings and string
variables. Things like Message were defined by typedefs:
(making Message a synonym for an array of 50 characters) and arrays of Message were
used to store text:
Message gMessage[] {
"Undefined symbol",
"Illegal character",
…
"Does not compute"
};
Similar constructs were used for arrays of keywords, or menu options, and similar
components introduced in Chapter 12 and later chapters.
These things work. But they are both restricting and wasteful. They are restricting
in that they impose a maximum length on the keywords or menu messages. They are
wasteful in that usually the vast majority of the keywords are very much shorter than
the specified size, but each occupies the same amount of memory.
Sometimes it is helpful to have all data elements the same size. For example, if we
want to write some Messages to a binary disk file it is convenient to have them all the
same size. This gives structure to the file (we can find a specific entry in the Message
file) and simplifies input.
But if the data are only going to be used in main memory, then there is less
advantage in having them the same size and the "wasted space" becomes a more
important issue.
The following structure would take up much less overall memory than the
gMessage[] table defined above:
char *gMsgPtrs[] = {
"Undefined symbol",
"Illegal character",
…
"Does not compute"
};
Strings and hash tables revisited 619
Figure 20.4 illustrates how this gMsgPtrs[] structure might represented in memory.
This representation has the overhead of a set of pointers (estimate at 4 bytes each) but
each individual message string occupies only the amount of space that it needs instead
of the 50 bytes previously allocated. Usually, this will result in a significant saving in
space.
gMsgPtrs[0]
gMsgPtrs[1]
…
…
gMsgPtrs[20]
U n d e
f i n e
d s y
m b o l
0 I l l Part of the static
e g a l
c h a data segment
r a c t
e r 0 T
…………
…………
…………
…… 0 D
o e s
n o t
c o m p
u t e 0
The messages referred to by the gMsgPtrs[] array would probably be intended as Pointers to const data
constants. You could specify this:
This makes gMsgPtrs[] an array of pointers to constant character strings. There will
be other examples later with definitions of pointers to constant data elements.
620 Dynamic data and pointers
Many standard functions take pointers to constant data as arguments. This is very
similar to const reference arguments. The function prototype is simply indicating that it
doesn't change the argument that it can access by the pointer.
Fixed size character arrays are wasteful for program messages and prompts, and they
may also be wasteful for character strings entered as data. If you had a program that
had to deal with a large number of names (e.g. the program that sorted the list of pupils
according to their marks), you could use a struct like the following to hold the data:
struct pupil_rec {
int fMark;
char fName[60];
};
but this has the same problem of wasting space. Most pupils will have names less than
sixty characters (and you are bound to get one with more than 60).
You would be better off with the following:
struct Pupil_Rec {
int fMark;
char *fName;
};
The following example code fragment illustrates creation of structs and heap-based
strings:
Pupil_Rec *GetPupilRec(void)
{
cout << "Enter mark, or -1 if no more records" << endl;
int mark;
cin >> mark;
Return NULL if no if(mark < 0)
structure needed return NULL;
Hash table
Sections 18.2.2 and 18.2.3 contained variations on a simple hash table structure. One
version had an array of with fixed 20 character words, the other used an array of structs
incorporating fixed sized words and an integer count. These arrays again "wasted"
space.
Ideally, a hash table should be not much more than half occupied; so many, perhaps
almost half of the table entries will not be used even when all the data have been
loaded. Most of the words entered into the hash table would have been less than twelve
characters so much of the space allocated for each word is wasted (and of course the
program can't deal with the few words that do exceed twenty characters).
A slightly more space-efficient variation would use a table of pointers to character
arrays (for the version that is to store words) or pointers to structs for the version that
needs words and associated integer data values). This arrangement for the table for
words would be as shown in Figure 20. 5 (compare with version in Figure 18.1). Note
that the words represented as separately allocated arrays in the heap do require those
headers and trailers. The space costs of these cut into the savings made by only using
the number of characters required for each word. The only significant saving is going
to be that due to the fact that only half the table is populated.
622 Dynamic data and pointers
Index
NULL
NULL A p p l
i c a t
i o n 0
99
F u n c
t i o n
0
297
P r o g
r a m 0
NULL
604 T e s t
i n g 0
618
NULL
NULL
NULL
The changes to the code needed to use the modified structure aren't substantial. The
following are rewrites of a couple of the functions given in Section 18.2.
char *theTable[kTBLSIZE];
void InitializeHashTable(void)
{
for(int i=0; i< kTBLSIZE; i++)
theTable[i] = NULL;
}
Strings and hash tables revisited 623
The InsertAt() function has been changed so that it allocates an array on the heap
to store the string that has to be inserted. The string's characters are copied into this
new array. Finally, the pointer in the table at the appropriate index value is filled with
the address of the new array.
Problem
Implement the simulated "Air Traffic Control" (ATC) trainer/game described below.
The ATC trainer is to give users some feel for the problems of scheduling and
routing aircraft that are inbound to an airport. Aircraft are picked up on radar at a range
of approximately150 miles (see Figure 20.6). They are identified by call sign, position,
velocity, acceleration, and details of the number of minutes of fuel remaining. An
aircraft's velocity and acceleration are reported in terms of x', y', z' and x'', y'', z''
components. The user (game-player, trainee air-controller, or whatever) must direct
aircraft so that they land successfully on the single east-west runway (they must
approach from the west).
624 Dynamic data and pointers
Inbound
aircraft
Limit of
radar
coverage
Approach Runway
path
There are a number of potential problems that the controller should try to avoid. If
an aircraft flies to high, its engines stall and it crashes. If it flies too low it runs into one
of the hills in the general vicinity of the airport. If it runs out of fuel, it crashes. If its
(total) horizontal speed is too low, it stalls and crashes. If the horizontal speed is too
high, or if it is descending too fast, the wings fall off and, again, it crashes.
At each cycle of the simulation, the controller may send new directives to any
number of inbound aircraft. Each cycle represents one minute of simulated time. A
directive specifies the accelerations (as x'', y'', z'' components) that should apply for a
specified number of minutes. Subsequent directives will override earlier settings. The
aircraft pilot will check the directive. If accelerations requested are not achievable, or it
is apparent that they would lead to excessive velocities within a minute, the directive is
ignored. An acceptable directive is acknowledged.
At each cycle of the simulation, every aircraft recomputes its position and velocity.
Normally, one unit of fuel is burnt each minute. If an aircraft has a positive vertical
acceleration ("Climb!", "Pull out of that dive!", …) or its horizontal accelerations lead
to an increase in its overall horizontal speed, it must have been burning extra fuel; in
such cases, its remaining fuel has to be decremented by one extra unit.
Aircraft must be guided so that they line up with the approach path to the runway. If
they are flying due east at an appropriate velocity, within certain height limits and with
some remaining fuel, they may be handed over to the airport's automated landing
Example: Air traffic controller 625
system. These aircraft are considered to have been handled successfully, and are
deleted from the simulation.
Details
Controlling aircraft by specifying accelerations is unnatural, but it makes it easy to Equations of motion
write code that defines how they move. You need simply remember your high school for aircraft
physics equations:
u initial velocity
v final velocity
s distance travelled
α acceleration
t time
v = u + α* t
s = u * t + 0.5 * α * t2
When recomputing the position and velocity of an aircraft, the x, y (and x' , y')
components can be treated separately. Just calculate the distance travelled along each
axis and update the coordinates appropriately. The total ground speed is given by:
The following limits can be used for aircraft performance (pure inventions, no Aircraft limits
claims to physical reality):
The maximum rate of ascent is simply a limit value, you cannot go faster even if you
try, but nothing disastrous happens if you do try to exceed this limit. The other speed
limits are critical, if you violate them the aircraft suffers.
626 Dynamic data and pointers
Landing conditions For an aircraft to achieve a safe landing, the controller must bring it into the pick up
area of the automated landing system. The aircraft must then satisfy the following
constraints:
Reports and A relatively crude status display, and command line interface for user input will
command interface suffice. For example, a status report listing details of all aircraft in the controlled space
could be something like:
Each line identifies an aircraft's call sign; the first triple gives its x, y, z position
(horizontal distances in miles from the airport, height in feet); the second triple gives
the x', y', z' velocities (miles per minute, feet per minute); the third triple is the
accelerations x'', y'', z'' (miles per minute per minute, feet per minute per minute);
finally the number minutes of fuel remaining is given. For example:
This aircraft is flying at 17000 feet. Currently it is 128 miles west of the airport and 4.5
miles north of the runway. Its total ground speed is approximately 4.03 miles per
minute (241mph). It is descending at 1000 feet per minute. It is not accelerating in any
way, and it has 61 minutes of fuel remaining.
The user has to enter commands that tell specific aircraft to accelerate (decelerate).
These commands will have to include the aircraft's call sign, and details of the new
accelerations. For example:
BA009 0 0 100 3
This command instructs plane BA009 to continue with no horizontal accelerations (so
no change to horizontal velocities), but with a small positive vertical acceleration that is
to apply for next three minutes (unless changed before then in a subsequent command).
This will cause the aircraft to reduce its rate of descent.
Example: Air traffic controller 627
Entry of planes into controlled air-space can be handled by a function that uses Adding aircraft to the
arrays of static data. One array of integers can hold the arrival times (given in minutes controlled airspace
from the start of simulation), another array of simple structs can hold details of flight
names and the initial positions and velocities of the aircraft. If this function is called
once on each cycle of the simulation, it can return details of any new aircraft (assume at
most one aircraft arrives per minute).
Design
To start, what are the objects needed in this program? The objects and their
Aircraft are obvious candidates. The program is all about aircraft moving around, classes
responding to commands that change their accelerations, crashing into hills, and diving
so fast that their wings fall off. Each aircraft owns some data such as its call sign, its
current x, y, z position, its x', y', z' velocities. Each aircraft provides all services related
to its data. For example, there has to be a way of determining when an aircraft can be
handed over to the automated landing system. The controller shouldn't ask an aircraft
for details of its position and check these, then ask for velocities and check these.
Instead the controller should simply ask the aircraft "Are you ready to auto land?"; the
aircraft can check all the constraints for itself.
So we can expect class Aircraft. Class Aircraft will own a group of data members class Aircraft
and provide functions like "Update accelerations, change to these new values" and
"Check whether ready to auto-land", "Fly for another minute and work out position".
A class representing the "air traffic controller" is not quite so obvious. For a start, class Aircontroller?
there will only ever be one instance of this class around. It gets created at the start of
the game and is used until the game ends.
When you've programmed many applications like this you find that usually it is
helpful to have an object that owns most of the other program elements and provides the
primary control functions.
In this example, the aircontroller will own the aircraft and will provide a main "run"
function. It is in this "run" function that we simulate the passage of time. The "run"
function will have a loop, each cycle of which represents one minute of elapsed time.
The run-loop will organize things like letting each aircraft update its position,
prompting the user to enter commands, updating some global timer, and checking for
new aircraft arriving.
The main program can be simplified. It will create an "AirController" and tell it to
"run".
The process of completing the design will be iterative. Each class will get
considered and its details will be elaborated. The focus of attention may then switch to
some other class, or maybe to a global function (a function that doesn't belong to any
individual class). When other details have been resolved, a partial design for a class
may get reassessed and then expanded with more detail.
628 Dynamic data and pointers
class Aircontroller
Naturally, most of these steps are going to be handled by auxiliary private member
functions of the Aircontroller class.
The timer will be a global integer variable. It is going to get used by the function
that adds aircraft to the airspace. (In this program, the timer could be a local variable of
the "run" function and get passed as an argument in the call to the function that adds
aircraft. However, most simulations require some form of global variable that
represents the current time; so we follow the more general style.)
The array of pointers to aircraft will start with all the pointers NULL . When an
aircraft is added, a NULL pointer is changed to hold the address of the new aircraft.
When an aircraft crashes or lands, it gets deleted and the pointer with its address is reset
to NULL. At any particular moment in the simulation, the non-NULL entries will be
scattered through the array. When aircraft need to be activated, a loop like the
following can be used:
Similar loop constructs will be needed in several of the auxiliary private member
functions of class Aircontroller.
Example: Air traffic controller 629
For example, the function that lets all the planes move is going to be:
move planes
for i = 0; i < max; i++
if aircraft[i] != NULL
aircraft[i] move
while that which checks for transfers to the auto lander would be:
check landings
for i = 0; i < max; i++
if aircraft[i] != NULL
if aircraft[i] can transfer
report handoff to auto lander
delete the aircraft
aircraft[i] = NULL;
reduce count of aircraft in controlled space
Another major role for the Aircontroller object will be to get commands from the Getting user
user. The user may not want to enter any commands, or may wish to get one plane to commands
change its accelerations, or may need to change several (or might even wish to quit
from the game). It would be easiest to have a loop that kept prompting the user until
some "ok no more commands" indicator was received. The following outline gives an
idea for the structure:
Handling commands will involve first finding the aircraft with the call sign entered,
then telling it of its new accelerations:
Handle command
identify target aircraft (one with call sign entered)
if target not found
warn of invalid call sign
else tell target to adjust accelerations
630 Dynamic data and pointers
The target can be found in another function that loops asking each aircraft in turn
whether it has the call sign entered.
class Aircraft
struct PlaneData {
double x, y, z; // coords
double vx, vy, vz; // velocities
…
short fuel; // minutes remaining
char name[7]; // 6 character call name
};
An Aircraft object would have a PlaneData as a data member, and maybe some others.
What do Aircraft do? Aircraft are going to be created; when created they will be given information to fill
in their PlaneData data member. They will get told to move, to print their details for
reports, to change their accelerations, to check whether they can land, and to check
whether they are about to suffer misfortune like flying into the ground. There were
quite a number of "terminating conditions" listed in the problem description; each could
be checked by a separate private member function.
Some of the member functions will be simple:
toofast
return true if speed exceeds safe maximum
tooslow
return true if speed exceeds safe minimum
The overall horizontal speed would have to be a data member, or an extra auxiliary
private member function should be added to calculate the speed.
The function that checks for transfer to auto lander will also be fairly simple:
The "move" and "update" functions are more complex. The update function has to
check that the new accelerations are reasonable:
update
check time value (no negatives!)
The move function has to recompute the x, y, z coordinates and the velocities. If the
specified number of minutes associated with the last accelerations has elapsed, these
need to be zeroed. The fuel left has to be reduced by an appropriate amount.
move
fix up coordinates
allow for normal fuel usage
632 Dynamic data and pointers
This would use a global array with "arrival times" of aircraft, another global array with
aircraft details, and an integer counter identifying the number of array entries already
used. A sketch for the code is:
return aircraft
The initial designs would then be refined. For the most part, this would involve further
expansion of the member functions and the identification of additional auxiliary private
member functions. The process used to refine the individual functions would be the
same as that illustrated in the examples in Part III.
The design process lead to the following class definitions and function
specifications:
struct PlaneData {
double x,y,z;
double vx,vy,vz;
double ax,ay,az;
short fuel;
char name[7];
};
class Aircraft {
public:
Aircraft(const PlaneData& d);
void PrintOn(ostream& os) const;
void Update(double a1,
double a2, double a3, short timer);
void Move();
Boolean MakingApproach() const;
Boolean Terminated() const;
Boolean CheckName(const char str[]) const;
const char* ID() const;
private:
Boolean Stalled() const;
Example: Air traffic controller 633
void Move();
Recompute position and velocity.
Check fuel.
class AirController {
public:
AirController();
void Run();
private:
void Report();
void MovePlanes();
void AddPlanes();
void Validate();
void CheckCrashes();
void CheckLandings();
Boolean GetCommands();
void HandleCommands(const char name[],
double,double,double,short);
Aircraft *IdentifyByCall(const char name[]);
Aircraft *fAircraft[kMAXPLANES];
int fControlling;
};
AirController();
Constructor, initialize air space to empty.
void Run();
Run main loop moving aircraft, checking commands etc
void Report();
Get reports from each aircraft.
Example: Air traffic controller 635
void MovePlanes();
Tell each aircraft in turn to move to new position.
void AddPlanes();
Call the plane generating function, if get plane returned add it to array
and notify player of arrival.
void Validate();
Arrange checks for landings and crashes.
void CheckCrashes();
Get each plane in turn to check if any constraints violated.
void CheckLandings();
Get each plane in turn to check if it can autoland, sign off those
that can autoland.
Boolean GetCommands();
Get user commands, returns true if "Quit"
(Really, the program should check for aircraft collisions as well as landings and crash
conditions. Checking for collisions is either too hard or too boring. The right way to
do it is to calculate the trajectories (paths) flown by each plane in the past minute and
determine if any trajectories intersect. This involves far too much mathematics. The
boring way of doing the check is to model not minutes of elapsed times but seconds.
Each plane moves for one second, then distances between each pair of planes is
checked. This is easy, but clumsy and slow.)
Implementation
The following are an illustrative sample of the functions. The others are either similar,
or trivial, or easy to code from outlines given earlier.
The main() function is concise:
int main()
{
AirController me;
636 Dynamic data and pointers
me.Run();
return 0;
}
This is actually a fairly typical main() for an object-based program – create the
principal object and tell it to run.
The file with the application code will have the necessary #includes and then start
with definition of constants representing limits:
…
const double kMaxVDownAcceleration = -400.0;
There are a couple of globals, the timer and a counter that is needed by the function
that adds planes:
The arrays with arrival times and prototype aircraft details would have to be defined:
short PArrivals[] = {
5, 19, 31, 45, 49,
…
};
PlaneData ExamplePlanes[] = {
{ -149.0, 12.0, 25000.0,
7.0, 0.0, -400.0,
0.0, 0.0, 0.0,
120, "BA009" },
{ -144.0, 40.0, 25000.0,
4.8, -1.4, 0.0,
0.0, 0.0, 0.0,
127, "QF040" },
…
};
Example: Air traffic controller 637
Aircraft *NewArrival(void)
{
The constructor for controller simply sets all elements of the array fAircraft to
NULL and zeros the fControlling counter. The main Run() function is:
void AirController::Run()
{
for(Boolean done = false; !done;) {
PTimer++;
MovePlanes();
Report();
Validate();
if(fControlling < kMAXPLANES)
AddPlanes();
if(fControlling>0)
done = GetCommands();
}
}
The Report() and MovePlanes() functions are similar; their loops involve telling
each aircraft to execute a member function ( PrintOn() and Move() respectively).
Note the way that the member function is invoked using the -> operator
(fAircraft[i] is a pointer to an Aircraft).
void AirController::Report()
{
if(fControlling < 1) cout << "Airspace is empty\n";
else
for(int i=0;i<kMAXPLANES; i++)
if(fAircraft[i]!= NULL)
fAircraft[i]->PrintOn(cout);
638 Dynamic data and pointers
The AddPlanes() function starts with a call to the plane generator function
NewArrival(); if the result from NewArrival() is NULL, there is no new Aircraft
and AddPlanes() can exit.
void AirController::AddPlanes()
{
Aircraft* aPlane = NewArrival();
if(aPlane == NULL)
return;
In other cases, the count of aircraft in the controlled space is increased, and an empty
slot found in the fAircraft array of pointers. The empty slot is filled with the address
of the newly created aircraft.
The CheckCrashes() and CheckLanding() functions called from Validate()
are similar in structure. They both have loops that check each aircraft and dispose of
those no longer required:
void AirController::CheckCrashes()
{
for(int i=0;i<kMAXPLANES; i++)
if((fAircraft[i] != NULL) &&
(fAircraft[i]->Terminated())) {
cout << fAircraft[i]->ID();
cout << " CRASHED!" << endl;
delete fAircraft[i];
fAircraft[i] = NULL;
fControlling--;
}
}
Example: Air traffic controller 639
Input to GetCommands() will start with a string; this should be either of the key
words OK or Quit or the name of a flight. A buffer of generous size is allocated to store
this string temporarily while it is processed.
The function checks for, and acts on the two key word commands. If the input string
doesn't match either of these it is assumed to be the name of a flight and the function
therefore tries to read three doubles (the new accelerations) and an integer (the time).
Checks on input errors are limited but they are sufficient for this kind of simple
interactive program. If data are read successfully, they are passed to the
HandleCommand() function.
Boolean AirController::GetCommands()
{
char buff[120];
cout << "Enter Flight Commands:\n"; cout.flush();
for(;;) {
cin >> buff;
if(0 == ::strcmp(buff,"Quit"))return true;
if(0 == ::strcmp(buff,"OK")) {
cin.ignore(SHRT_MAX,'\n');
return false;
}
double a1,a2,a3;
short t;
cin >> a1 >> a2 >> a3 >> t;
if(cin.fail()) {
cout << "Bad input data\n";
cin.clear();
cin.ignore(SHRT_MAX,'\n');
}
else HandleCommands(buff,a1,a2,a3,t);
}
if(target == NULL) {
cout << "There is no aircraft with " << call
<< " as call sign.\n";
return;
}
640 Dynamic data and pointers
target->Update(a1,a2,a3,n);
}
The constructor for Aircraft initializes its main data from the PlaneData struct
passed as an argument and zeros the command timer. The PrintOn() function simply
outputs the data members:
Aircraft::Aircraft(const PlaneData& d)
{
fData = d;
fTime = 0;
}
os << "\t(" << fData.x << "," << fData.y << ","
<< fData.z << ")\t(";
…
os << endl;
}
Function Move() sorts out the changes to position, velocity, and fuel:
void Aircraft::Move()
{
double dx, dy, dz;
dx = fData.vx;
dy = fData.vy;
dz = fData.vz;
if(fTime>0) {
/* Still accelerating */
fData.vx += fData.ax; dx += fData.ax * 0.5;
fData.vy += fData.ay; dy += fData.ay * 0.5;
fData.vz += fData.az; dz += fData.az * 0.5;
if(fData.vz>kMaxAscentRate) {
fData.vz = kMaxAscentRate;
fData.az = 0.0;
}
fTime--;
if(fTime==0)
fData.az = fData.ay = fData.ax = 0.0;
}
fData.x += dx;
fData.y += dy;
fData.z += dz;
fData.fuel--;
}
The Update() function performs a series of checks on the new acceleration values.
If all checks are passed, the values in the data members are changed and an
acknowledgment is printed:
/* In height bracket? */
if((fData.z < kminh) ||
(fData.z > kmaxh)) return false;
/* In velocity bracket? */
if((fData.vx < kxprimelow) ||
(fData.vx > kxprimehigh)) return false;
return true;
}
Function Terminated() uses the various auxiliary member functions to check for
terminating conditions:
The remaining functions of class Aircraft are all simple; representative examples
are:
The program runs OK but as a game it is a little slow (and directing an aircraft to
land safely is surprisingly hard). The following is a fragment from a recording of the
game:
Airspace is empty
New Aircraft:
BA009 (-149,12,25000) (7,0,-400) (0,0,0) fuel: 120
Enter Flight Commands:
BA009 is west of the runway, a little to the north and coming
in much too fast; slow it down, make it edge south a bit
BA009 -1 -0.2 0 3
644 Dynamic data and pointers
BA009: Roger
OK
BA009 (-142.5,11.9,24600) (6,-0.2,-400) (-1,-0.2,0) fuel:
119
…
BA009 (-124.5,9.9,23000) (4,-0.6,-400) (0,0,0) fuel:
115
Enter Flight Commands:
Slow up a little more, increase rate of descent
BA009 -0.5 0 -100 2
BA009: Roger
OK
BA009 (-120.75,9.3,22550 (3.5,-0.6,-500) (-0.5,0,-100) fuel:
114
…
BA009 (-15.5,0,3350) (3,-0,-500) (0,0,0) fuel: 78
QF040 (-52.77,0.18,11600) (3.01,-0.05,-600) (0,0,0) fuel: 100
NZ164 (-71,-45.95,20500) (1,5.3,-500) (0,0,0) fuel: 70
CO1102 (116,-87,32500) (-5,1,-500) (0,0,0) fuel: 73
Enter Flight Commands:
BA009 0 0 150 1
BA009: Roger
OK
BA009 (-12.5,0,2925) (3,-0,-350) (0,0,0) fuel: 76
QF040 (-49.76,0.13,11000) (3.01,-0.05,-600) (0,0,0) fuel: 99
NZ164 (-70,-40.65,20000) (1,5.3,-500) (0,0,0) fuel: 69
CO1102 (111,-86,32000) (-5,1,-500) (0,0,0) fuel: 72
Enter Flight Commands:
OK
BA009 (-9.5,0,2575) (3,-0,-350) (0,0,0) fuel: 75
QF040 (-46.75,0.08,10400) (3.01,-0.05,-600) (0,0,0) fuel: 98
NZ164 (-69,-35.35,19500) (1,5.3,-500) (0,0,0) fuel: 68
CO1102 (106,-85,31500) (-5,1,-500) (0,0,0) fuel: 71
BA009 transferred to airport traffic control, bye!
One landed safely, QF040 on course as well
Enter Flight Commands:
The primary reason for having pointers is to hold addresses of data objects that have
been dynamically allocated in the heap, i.e. for values returned by the new operator.
You need pointers to heap based structures when representing simple data objects
with variable lifetimes like the Aircraft of the last example. You also need pointers
when building up complex data structures that represent networks of different kinds.
Networks are used for all sorts of purposes; they can represent electrical circuits, road
maps, the structure of a program, kinship relations in families, the structure of a
molecule, and thousands of other things. Networks are built up at run-time by using
pointers to thread together different dynamically created component parts.
&: The "address of" operator 645
Why would you want pointers to data in the static data segment or to automatics on
the stack? After all, if such variables are "in scope" you can use them directly, you
don't have to work through pointer intermediaries. You should NEVER incorporate the
address of an automatic (stack based) data item in an elaborate network structure. It is
rare to want to incorporate the address of a static data item.
Really, you shouldn't be writing code that needs addresses of things other than heap-
based objects. Usually you just want the address of an entire heap based object (the
value returned by new), though sometimes you may need the address of a specific data
member within a heap based object.
But C and C++ have the & "address of" operator. This allows you to get the address
of any data element. Once an address has been obtained it can be used as the value to
be stored in a pointer of the appropriate type. If you look at almost any C program, and
most C++ programs, you will see & being used all over the place to get addresses of
automatics and statics. These addresses are assigned to pointers. Pointers are passed as
arguments. Functions with pointer arguments have code that has numerous expression
using pointer dereferencing (*ptr). Why are all these addresses needed?
For the most part, the addresses are needed because the C language does not support No "pass by
pass by reference. In C, arguments for functions are passed by value. So, simple reference" in C
variables and struct instances are copied onto the stack (ignore arrays for now, they are
discussed in the next section). The called function works with a copy of the original
data. There are sound reasons for designing a language that way. If you are trying to
model the mathematical concept of a function, it is something that simply computes a
value. An idealized, mathematical style function should not have side effects; it
shouldn't go changing the values of its arguments.
Function arguments that are "passed by reference" have already been used for a Uses of "pass by
number of reasons. Thus, in the example in section 12.8.3, there was a function that reference"
had to return a set of values (actually, an int and a double) rather than a single value.
Since a function can only return a single value, it was necessary to have reference
arguments. The example function had an integer reference and a double reference as
arguments; these allowed the function to change the values of the caller's variables.
Other examples have used pass by reference for structures. In the case of const
reference arguments, this was done to avoid unnecessary copying of the struct that
would occur if it were passed by value. In other cases, the structs are passed by
reference to allow the called function to modify the caller's data.
As previously explained, a compiler generates different code for value and reference Implementation of
arguments. When a function needs a value argument, the compiler generates code that, pass by value and
pass by reference
at the point of call, copies the value onto the stack; within the function, the code using
the "value" variable will have addresses that identify it by its position in the current
stack frame. Reference arguments are treated differently. At the point of call their
646 Dynamic data and pointers
addresses are loaded onto the stack. Within the function, the address of a reference
argument is taken from the stack and loaded into an address register. When the code
needs to access the actual data variable, it uses indirect addressing via this address
register; which is the same way that code uses pointers.
Consider the Exchange() function in the following little program:
int main()
{
cout << "Enter two numbers : ";
double a, b;
cin >> a >> b;
if(b < a)
Exchange(a, b);
cout << "In ascending order: " << a << ", " << b << endl;
return 0;
}
Function Exchange() swaps the values in the two variables it is given as arguments;
since they are reference variables, the function changes data in the calling environment.
The code generated for this program passes the addresses of variables a and b . The
value in a gets copied into temp, replaced by the value in b, then the value of temp is
stored in b . These data movements are achieved using indirect addressing through
address registers loaded with the address of a and b.
Compiler uses Pass by reference really involves working with addresses and pointers. But the
addresses and compiler takes care of the details. The compiler arranges to get the addresses of the
pointers for pass by
reference arguments at the point of call. The compiler generates code that uses indirection,
pointer style, for the body of the function.
Programmer uses Programs need the "pass by reference" mechanism. The C language didn't have it.
addresses and But C allowed programmers to hand code the mechanism everywhere it was needed.
pointers
C has pass by value. An address is a value and can be passed as an argument if the
function specifies that it wants a pointer as an argument. It is easy to use the & operator
to get an address, so at the point of call the addresses can be determined and their values
put onto the stack.
Faking pass by The code of the function has to use pointers. So, for example, a function that needs
reference using to change a double argument will need a double* pointer (i.e. its argument list will
explicit addresses and
pointers have to include something like double *dptr). When the code of the function needs
the value of that double, it will have to access it indirectly via the pointer, i.e. it will
have to use the value *dptr.
&: The "address of" operator 647
int main()
{
cout << "Enter two numbers : ";
double a, b;
cin >> a >> b;
if(b < a)
Exchange(&a, &b);
cout << "In ascending order: " << a << ", " << b << endl;
return 0;
}
temp = *dptr1;
gets the double value accessed indirectly via dptr1 and saves it in the double temp.
The statement
*dptr1 = *dptr2;
gets the value accessed indirectly via dptr2 and uses it to overwrite the double in the
memory location referenced via dptr1. The final statement changes the value in the
location reference by dptr2 to be the value stored temporarily in temp.
High level source code using explicit pointers and addresses corresponds almost Explicit pointers and
directly to the actual machine instruction sequences generated by the compiler. addresses represent
the underlying
Identical instruction sequences are generated for the high level code that uses mechanism
references; it is just that the code with references leaves the mechanism implicit. There
are programmers who prefer the pointer style, arguing that "it is better to see what is
really going on". (Of course, if you follow such arguments to their logical conclusion,
all programs should be written in assembly language because in assembler you can see
the instructions and really truly know what is going on.)
There are a few situations where the pointer style is more natural. The low level Pointer style or
read() and write() functions are good examples. These need to be given the reference style?
648 Dynamic data and pointers
address of a block of bytes; the code of the function uses a byte pointer to access the
first byte, then the second, and so on as each byte gets transferred to/from file.
In most other cases, reference style is easier to read. The reference style leaves the
mechanisms implicit, allowing the reader to focus on the data transformations being
performed. The use of explicit pointers, and the continual pointer dereferencing that
this necessitates, makes it harder to read the code of functions written using the pointer
style.
While you can adopt the style you prefer for the functions that you write for
yourself, you are often constrained to using a particular style. All the old C libraries
still used from C++ will use pointers and will need to be passed addresses. Because of
the C heritage, many C++ programmers write code with pointer arguments;
consequently, many of the newer C++ libraries use pointers and pointer dereferencing.
You can get the address of any program element. You automatically get the address of
any data object you create in the heap (the return value from new is the address); in
most other cases you need to use the & operator. The following contrived code
fragment illustrates how addresses of a variety of different data elements can be
obtained and used in calls to write(). The prototype for function write() specifies
either a char* or a void* (depending on the particular version of the iostream library
used in your IDE); so at the point of call, a value representing an address has to be
specified.
out.write(&(d2.f1), sizeof(int));
out.write(demoptrA, sizeof(demo)); // No & needed!
out.write(&(demoptrB->f2), sizeof(double));
}
The call
out.write(demoptrA, sizeof(demo));
doesn't need an & "get address of"; the pointer demoptrA contains the address needed,
so it is the value from demoptrA has to be passed as the argument.
There are two kinds of program element whose address you can get by just using Addresses of
their names. These are functions and arrays. You shouldn't use the & address of functions and arrays
operator with these.
The use of function addresses, held in variables of type "pointer to function", is
beyond the scope of this book. You will learn about these later.
As explained more in the next section, C (and hence C++) regards an array name as
having an address value; in effect, an array name is a pointer to the start of the array.
So, if you needed to pass an array to the write() function, your code would be:
A function that has a pointer return type will return an address value. This address
value will get used after exit from the function. The address is going to be that of some
data element; this data element had better still be in existence when the address gets
used!
This should be obvious. Surprisingly often, beginners write functions that return
addresses of things that will disappear at the time the functions exits. A simple example
is:
650 Dynamic data and pointers
This doesn't work. The array buff is an automatic, it occupies space on the stack while
the function GetName() is running but ceases to exist when the function returns.
The compiler will let such code through, but it causes problems at run time. Later
attempts to use the pointer will result in access to something on the stack, but that
something won't be the character buffer.
A function should only return the address of an object that outlives it; which means
an object created in the heap. The following code will work:
char *GetName()
{
char buff[100];
cout << "Enter name";
cin.getline(buff, 99, '\n');
char *ptr = new char[strlen(buff) + 1];
strcpy(ptr, buff);
return ptr
}
When arrays were introduced in Chapter 11, it was noted that C's model for an array is
relatively weak. This model, which of course is shared by C++, really regards an array
as little more than a contiguous block of memory.
The instruction sequences generated for code that uses an array have the following
typical form:
which is somewhat similar to the code for using a pointer to get at a simple data element
and really very similar to code using a pointer to access a data member of a struct:
load address register with the address of the start of the struct
calculate offset for required data member
add offset to address register
load data value using indirect addressing via address register
If you think about things largely in terms of the instruction sequences generated, you
will tend to regard arrays as similar to pointer based structs. Hence you get the idea of
the name of the array being a pointer to the start of that array.
Once you start thinking mainly about instructions sequences, you do tend to focus on
different issues. For example, suppose that you have an array of characters that you
need to process in a loop, you know that if you use the following high level code:
char msg[50];
…
for(int i=0; i < len; i++) {
…
… = msg[i];
…
}
then the instruction sequence for accessing the ith character will be something like
which is a little involved. (Particularly as on the very first machine used to implement
C, a PDP-7, there was only one register so doing subscripting involved a lot of shuffling
of data between the CPU and memory.)
Still thinking about instructions, you would know that the following would be a
more efficient way of representing the entire loop construct:
This instruction sequence, which implements a pointer based style for accessing the
array elements, is more "efficient". This approach needs at most two instructions to get
a character from the array where the other scheme required at least three instructions.
652 Dynamic data and pointers
(On many machines, the two operations "load character indirectly using address in
address register" and "add 1 to address register" can be combined into a single
instruction.)
C was meant to compile to efficient instruction sequences. So language constructs
were adapted to make it possible to write C source code that would be easy to compile
into the more efficient instruction sequence:
char msg[50];
char *mptr;
…
mptr = msg;
for(int i=0; i < len; i++) {
…
… = *mptr;
mptr++;
…
}
This initializes a pointer with the address of the first array element:
mptr = msg;
(which could also be written as mptr = &(msg[0])). When the character is needed,
pointer dereferencing is used:
… = *mptr;
Finally, "pointer arithmetic" is performed so as to make the pointer hold the address of
the next character from the array.
Pointer arithmetic C (and C++) have to allow arithmetic operations to be done on pointers. Once
you've allowed operations like ++, you might as well allow other addition and
subtraction operations (I don't think anyone has ever found much application for pointer
multiplication or division). For example, if you didn't have the strlen() function, you
could use the following:
subtracts the address of the start of the array from the address where the '\0' character is
located.
Arithmetic operations on pointers became a core part of the language. Many of the
C libraries, still used from C++, are written in the expectation that you will be
performing pointer arithmetic. For example, the string library contains many functions
in addition to strlen(), strcmp() , and strcpy() ; one of the other functions is
strchr():
this finds the first occurrence of a character c in a string s. It returns a pointer to the
position where the character occurs (or NULL if it isn't there). Usually, you would want
to know which array element of the character array contained the character, but instead
of an integer index you get a pointer. Of course, you can use pointer arithmetic to get
the index:
Obviously, character arrays can not be a special case. What works for characters has
to work for other data types. This requirement has ramifications. If ++ changes a
char* pointer so that it refers to the next element of a character array, what should ++
do for an array of doubles?
The following program fragment illustrates the working of ++ with different data
types (all pointer values are printed as long integers using decimal output to make
things clearer):
int main()
{
char cArray[10];
short sArray[10];
long lArray[10];
double dArray[10];
zdemo zArray[10];
return 0;
}
The arithmetic operations take account of the size of the data type to which the pointer
refers. Increment a char* changes it by 1 (a char occupies one byte), incrementing a
double* changes it by 8 (on the machine used, a double needs 8 bytes). Similarly the
value zptr (22465680) - zArray (22465600) is 5 not 80, because this operation on
zdemo pointers involves things whose unit size is 16 bytes not one byte.
A lot of C code uses pointer style for all operations on arrays. While there can be
advantages in special cases like working through the successive characters in a string, in
most cases there isn't much benefit. Generally, the pointer style leads to code that is
much less easy to read (try writing a matrix multiply function that avoids the use of the
[] operator).
Although "pointer style" with pointer dereferencing and pointer arithmetic is a well
established style for C programs, it is not a style that you should adopt. You have
arrays because you want to capture the concept of an indexable collection of data
elements. You lost the benefit of this "indexable collection" abstraction as soon as you
start writing code that works with pointers and pointer arithmetic.
The Air Traffic Controller program represented an extreme case with respect to
dynamically created objects – its Aircraft were all completely independent, they didn't
relate in a specific ways.
There are other programs where the structures created in the heap are components of
a larger construct. Once created, the individual separate structures are linked together
Building networks 655
using pointers. This usage is probably the more typical. Real examples appear in the
next chapter with standard data structures like "lists" and "trees".
In future years, you may get to build full scale "Macintosh" or "Windows"
applications. In these you will have networks of collaborating objects with an
"application" object linked (through pointers) to "document" objects; the documents
will have links to "views" (which will have links back to their associated documents),
and other links to "windows" that are used to frame the views.
The data manipulated by such programs are also likely to be represented as a
network of components joined via pointers. For example, a word processor program
will have a data object that has a list of paragraphs, tables, and pictures. Each
paragraph will have links to things such as structures that represent fonts, as well as to
lists of sentences.
Each of the components in one of these complex structures will be an instance of
some struct type or a class. These various structs and classes will have data members
that are of pointer types. The overall structure is built up by placing addresses in the
pointer data members.
Figure 20.7 illustrates stages in building up a simple kind of list or queue. The
overall structure is intended to keep a collection of structures representing individual
data items. These structures would be called "list cells" or "list nodes". They would be
instances of a struct or class with a form something like the following:
struct list_cell {
data_type1 data_member_1;
data_type2 data_member_2;
…;
list_cell *fNext;
};
Most of the data members would be application specific and are not of interest here. Pointer data member
But one data member would be a pointer; its type would be "pointer to list cell". It is for link
these pointer data members, the fNext in the example, that are used to link the parts
together to form the overall structure.
A program using a list will need a pointer to the place where it starts. This will also Head pointer
be a pointer to list_cell It would be a static data segment variable (global or
filescope) or might be a data member of yet another more complex structure. For the
example in Figure 20.7, it is assumed that this "head pointer" is a global:
list_cell *Head;
Initially, there would be no list_cells and the head pointer would be NULL (stage
1 in Figure 20.7). User input would somehow cause the program to create a new
list_cell:
list_cell *MakeCell()
{
656 Dynamic data and pointers
Static Static
data Heap data Heap
Data 1
NULL
Head NULL Head
1 2
Data 1 Data 1
Head Head
Data 2 Data 2
NULL
Data 3
NULL
Figure 20.7 Building a "list structure" by threading together "list nodes" in the heap.
The MakeCell() function would obviously fill in all the various specialized data
members; the only general one is the link data member fNext which has to be
initialized to NULL.
The function that created the new list_cell would add it to the overall structure:
Building networks 657
As this would be the first list_cell, the Head pointer is changed to point to it. This
results in the situation shown as stage 2, in Figure 20.7.
When, subsequently, other list_cells get created, the situation is slightly
different. The Head pointer already points to a list_cell. Any new list_cell has
to be attached at the end of the chain of existing list_cell(s) whose start is identified
by the Head pointer.
The correct place to attach the new list_cell will be found using a loop like the Finding the end of a
following: list
The list_cell* variable ptr starts pointing to the first list_cell. The while loop Attaching a new cell
moves it from list_cell to list_cell until a list_cell with a NULL fNext link is at the end of the list
found. Such a list_cell represents the end of the current list and is the place where
the next list_cell should be attached. The new list_cell is attached by changing
the fNext link of that end list_cell:
ptr->fNext = newdata;
Stages 3 and 4 shown in Figure 20.7 illustrate the overall structure after the addition
of the second and third list_cells.
A lot of the code that you will be writing over the next few years will involve
building up networks, and chasing along chains of pointers. Two of the examples in the
next chapter look at standard cases of simple pointer based composite structures. The
first of these is a simple List class that represents a slight generalization of the idea of
the list as just presented. The other example illustrates a "binary search tree". The code
for these examples provides a more detailed view of how such structures are
manipulated.
658 Dynamic data and pointers
21
21 Collections of data
As illustrated in the "Air Traffic Controller" example in Chapter 20, arrays can be used Standard "collection
to store collections of data. But arrays do have limitations (e.g. you have to specify a classes"
maximum size), and they don't provide any useful support function that might help
manage a data collection. Over the years, many different structures have been devised
for holding collections of data items. These different structures are adapted to meet
specific needs; each has associated functionality to help manage and use the collection.
For example, a simple Queue provides a structure where new data items can be Queue
added "at the rear"; the data item "at the front" of the Queue can be removed when it is
time for it to be dealt with. You probably studied queues already today while you stood
in line to pay at the college cafeteria.
A "Priority Queue" has different rules. Queued items have associated priorities. Priority Queue
When an item is added to the collection, its place is determined by its priority. A real
world example is the queue in the casualty department of a busy hospital. Car crash and
gun shot patients are priority 1, they go straight to the front of the queue, patients
complaining of "heavy" pains in their chests - mild heart attacks – are priority 2 etc.
"Dynamic Arrays" and "Lists" provide different solutions to the problem of keeping Dynamic arrays and
a collection of items where there are no particular access rules like those of Queue. lists
Items can be added at the front or rear of a list, and items can be removed from the
middle of lists. Often, lists get searched to find whether they contain a specific item;
the search is "linear" (start at the front and keep checking items until either you find the
one that is wanted or you get to the end of the list).
A "Binary Tree" is more elaborate. It provides a means for storing a collection of Binary tree
data items that each include "key" values. A binary tree provides a fast search system
so you can easily find a specific data element if you know its "key".
Sections 21.1 and 21.2 illustrate a simple Queue and a "Priority Queue". These use Data storage
fixed size arrays to store (pointers to) data elements. You have to be able to plan for a
maximum number of queued items. With the "Dynamic Array", 21.3, things get a bit
more complex. A "Dynamic Array" supports "add item", "search for item", and
"remove item" operations. Like a simple Queue, the Dynamic Array uses an array of
pointers to the items for which it is responsible. If a Dynamic Array gets an "add item"
660 Collections of data
request when its array is full, it arranges to get additional memory. So it starts with an
initial array of some default size, but this array can grow (and later shrink) as needed.
With Lists, 21.4, you abandon arrays altogether. Instead, a collection is made up using
auxiliary data structures ("list cells" or "list nodes"). These list cells hold the pointers to
the data items and they can be linked together to form a representation of the list.
Binary trees, 21.5, also utilize auxiliary structures ("tree nodes") to build up a
representation of an overall data structure.
Data items? What data items get stored in these lists, queues and trees?
Ideally, it should be any data item that a programmer wants to store in them. After
all, these Queue, List, and Binary Tree structures represent general concepts (in the
preferred jargon, they are "abstract data types"). You want to be able to use code
implementing these types. You don't want to have to take outline code and re-edit it for
each specific application.
"Generic code" There are at least three ways in C++ in which you can get some degree of generality
in your code (code that is independent of the type of data manipulated is "generic").
The examples in this chapter use the crudest of these three approaches. Later examples
will illustrate more sophisticated techniques such as the use of (multiple) inheritance
and the use of templates. The approach used here is an older, less sophisticated (and
slightly more error prone) technique but it is also the simplest to understand.
The data items that are to be in the Queues, Lists, and Dynamic Arrays are going to
be structures that have been created in the heap. They will be accessed via pointers.
We can simply use void* pointers (pointers to data items of unspecified type). Code
written to use void* pointers is completely independent of the data items accessed via
those pointers.
The Priority Queue and Binary Tree both depend on the data items having associated
"key values" (the "keys" of the Binary Tree, the priorities of the Priority Queue). These
key values will be long integers. The interface for these classes require the user to
provide the integer key and a pointer to the associated data object.
down modem lines to remote terminals or other machines, and queues of characters
already typed but not yet processed.
Outside of operating systems, queues most frequently turn up in simulations. The
real world has an awful lot of queues, and real world objects spend a lot of time
standing in queues. Naturally, such queues appear in computer simulations.
What does a queue own? A queue owns "something" that lets it keep pointers to the A queue owns …
queued items; these are kept in "first come, first served" order. (The "something" can
take different forms.)
A queue can respond to the following requests: A queue does …
• First
Remove the front element from the queue and return it.
• Add (preferred name Append)
Add another element at the rear of the queue.
• Length
Report how many items are queued.
• Full
If the queue only has a finite amount of storage, it may get to be full, a further
Add operation would then cause some error. A queue should have a "Full"
member function that returns true if the queue is full.
• Empty
Returns true if there are no data elements queued; an error would occur if a
First operation was performed on an empty queue.
Several different data structures can be used to represent a queue in a program. This Representation:
example uses a structure known as a "circular buffer". This version of a queue is most "circular buffer"
commonly used for tasks such as queuing characters for output devices.
The principal of a circular buffer is illustrated in Figure 21.1. A fixed sized array,
the "buffer", is used to store the data values. In this implementation, the array stores
void* pointers to other data structures that have been allocated in the heap; but if a
circular buffer were being used to queue characters going to a modem, the array would
hold the actual characters. (In Figure 21.1, the data items are shown as characters). As
well as the array, there are two integers used to index into the array; these will be
referred to as the "get-index" and the "put-index". Finally, there is a separate integer
that maintains a count of the number of items currently stored.
An Append ("put") operation puts a data item at "the end of the queue". Actually the
Append operation puts it in the array at the index position defined by fp (the put index)
and then increments the fp index and the counter (fcount). So, successive characters
fill in array elements 0, 1, 2, …. In the example shown in Figure 21.1, the array fdata
has six elements. The fp index is incremented modulo the length of the array. So, the
next element after fdata[5] is fdata[0] (this makes the index "circle round" and
hence the name "circular buffer").
662 Collections of data
fp 0 1 2
fg 0 0 0
fcount 0 1 2
fdata[0] /0 A A
fdata[1] /0 /0 B
fdata[2] /0 Append /0 Append /0
fdata[3] /0 (put) /0 (put) /0
fdata[4] /0 /0 /0
fdata[5] /0 /0 /0
1 2 3
Append
(put)
3 4 4
0 0 1
3 4 3
A A A
B B B
C Append C First C
/0 (put) D (get) D
/0 /0 /0
/0 /0 /0
4 5 6
Figure 21.1 Queue represented as a "circular buffer" and illustrative Append (put)
and First (get) actions.
The First (get) operation that removes the item from "the front of the queue" works
in a similar fashion. It uses the "get index" (fg ). "First" takes the data item from
fdata[fg], updates the value of fg (modulo the array length so it too "circles around"),
and reduces the count of items stored. (The data value can be cleared from the array,
but this is not essential. The data value will be overwritten by some subsequent Append
operation.)
Error conditions Obviously there are a couple of problem areas. A program might attempt a First
(get) operation on an empty queue, or might attempt to add more data to a queue that is
already full. Strictly a program using the queue should check (using the "Empty" and
"Full" operations) before performing a First or Append operation. But the code
implementing the queue still has to deal with error conditions.
Exceptions When you are programming professionally, you will use the "Exception" mechanism
(described in Chapter 26) to deal with such problems. The queue code can "throw an
Class Queue 663
exception"; this action passes information back to the caller identifying the kind of error
that has occurred. But simpler mechanisms can be used for these introductory
examples; for example, the program can be terminated with an error message if an
"Append" operation is performed on a full queue.
The queue will be represented by a class. The declaration for this class (in a header Class declaration
file Q.h) specifies how this queue can be used:
#ifndef __MYQ__
#define __MYQ__
#define QSIZE 6
class Queue {
public:
Queue();
private:
void *fdata[QSIZE];
int fp;
int fg;
int fcount;
};
The public interface specifies a constructor (initializor), the two main operations public
(Append() and First()) and the three functions that merely ask questions about the
state of the queue.
The private part of the class declaration defines the data structures used to represent private
a queue. A program can have any number of Queue objects; each has its own array,
count, and array indices.
The three member functions that ask about the queue status are all simple, and are inline functions
good candidates for being inline functions. Their definitions belong in the header file
with the rest of the class declaration. These are const functions; they don't change the
state of the queue.
The other member functions would be defined in a separate Q.cp file:
#include <iostream.h>
664 Collections of data
#include <stdlib.h>
#include "Q.h"
The QueueStuffed() function deals with cases where a program tries to put too
many data items in the queue or tries to take things from an empty queue. (Error
messages like these should really be sent to cerr rather than cout; but on many IDEs,
cout and cerr are the same thing.)
Constructor Queue::Queue()
{
fp = fg = fcount = 0;
}
The constructor has to zero the count and the pointers. There is no need to clear the
array; any random bits there will get overwritten by "append" operations.
The Append() function first checks that the operation is legal, using the Queue-
Stuffed() function to terminate execution if the queue is full. If there is an empty slot
in the array, it gets filled in with the address of the data item that is being queued. Then
the fcount counter is incremented and the fp index is incremented modulo the length
of the array. The code incrementing fp could have been written:
fp++;
fp = fp % QSIZE;
The code with the if() is slightly more efficient because it avoids the divide operation
needed for the modulo (%) operator (divides are relatively slow instructions).
void *Queue::First(void)
Class Queue 665
{
if(Empty())
QueueStuffed();
void* temp = fdata[fg];
fg++;
if(fg == QSIZE)
fg = 0;
fcount--;
return temp;
}
Test program
Naturally, a small test program must be provided along with a class. As explained in The test program is
the context of class Bitmap, the test program is part of the package. You develop a part of the package!
class for other programmers to use. They may need to make changes or extensions.
They need to be able to retest the code. You, as a class implementor, are responsible
for providing this testing code.
These test programs need not be anything substantial. You just have to use all the
member functions of the class, and have some simple inputs and outputs that make it
easy to check what is going on. Usually, these test programs are interactive. The tester
is given a repertoire of commands; each command invokes one of the member functions
of the class under test. Here we need "add", "get", and "length" commands; these
commands result in calls to the corresponding member functions. The implementation
of "add" and "get" can exercise the Full() and Empty() functions.
We need data that can be put into the queue, and then later get removed from the
queue so that they can be checked. Often, it is necessary to have the tester enter data;
but here we can make the test program generate the data objects automatically.
The actual test program creates "Job" objects (the program defines a tiny class Job)
and adds these to, then removes these from the queue.
current->PrintOn(cout);
}
break;
case 'c':
if(current != NULL) {
cout << "Current job is ";
current->PrintOn(cout);
}
else cout << "No current job" << endl;
break;
case '?':
cout << "Commands are:" << endl;
cout << "\tq Quit\n\ta Add to queue\t" << endl;
cout << "\tc show Current job\n"
\tg Get job at front of queue" << endl;
cout << "\tl Length of queue\n" << endl;
break;
default:
;
}
}
return EXIT_SUCCESS;
}
Priority queues are almost as rare as simple queues. They turn up in similar
applications – simulations, low-level operating system's code etc. Fortuitously, they
have some secondary uses. The priority queue structure can be used as the basis of a
sorting mechanism that is quite efficient; it is a reasonable alternative to the Quicksort
668 Collections of data
function discussed in Chapter 13. Priority queues also appear in the implementation of
some "graph" algorithms such as one that finds the shortest path between two points in
a network (such graph algorithms are outside the scope of this text, you may meet them
in more advanced courses on data structures and algorithms).
As suggested in the introduction to this chapter, a priority queue would almost
certainly get used in a simulation of a hospital's casualty department. Such simulations
are often done. They allow administrators to try "What if …?" experiments; e.g. "What
if we changed the staffing so there is only one doctor from 8am to 2pm, and two doctors
from 2pm till 9pm?". A simulation program would be set up to represent this situation.
Then a component of the program would generate "incoming patients" with different
problems and priorities. The simulation would model their movements through
casualty from admission through queues, until treatment. These "incoming patients"
could be based on data in the hospital's actual records. The simulation would show how
long the queues grew and so help determine whether a particular administrative policy
is appropriate. (The example in Chapter 27 is a vaguely similar simulation, though not
one that needs a priority queue.)
The objects being queued are again going to be "jobs", but they differ slightly from
the last example. These jobs are defined by a structure:
struct Job {
long prio;
char name[30];
};
(this simple struct is just to illustrate the working of the program, the real thing would
be much larger with many more data fields). The prio data member represents the
priority; the code here will use small numbers to indicate higher priority.
A over simple priority You can implement a very simple form of priority queue. For example:
queue!
class TrivialPQ {
public:
TrivialPQ();
Job First();
void Insert(const Job& j);
private:
Job fJobs[kMAXSIZE];
int fcount;
};
A TrivialPQ uses an array to store the queued jobs. The entries in this array are kept
ordered so that the highest priority job is in element 0 of the array.
If the entries are to be kept ordered, the Insert() function has to find the right
place for a new entry and move all less urgent jobs out the way:
int pos = 0;
while((pos < fcount) && (fJobs[pos].prio < j.prio)) pos++;
// j should go at pos
// less urgent jobs move up to make room
for(int i = fcount; i> pos; i--)
fJobs[i] = fJobs[i-1];
fJobs[pos] = j;
fcount++;
}
Similarly, the First() function can shuffle all the lower priority items up one slot after
the top priority item has been removed:
Job TrivialPQ::First()
{
Job j = fJobs[0];
for(int i = 1; i < fcount; i++)
fJobs[i-1] = fJobs[i];
fcount--;
return j;
}
The class TrivialPQ does define a workable implementation of a priority queue. Cost is O(N)
The trouble is the code is rather inefficient. The cost is O(N) where N is the number of
jobs queued (i.e. the cost of operations like First() and Insert() is directly
proportional to N). This is obvious by inspection of First(); it has to move the N-1
remaining items down one slot when the front element is removed. On average, a new
Job being inserted will go somewhere in the middle. This means the code does about
N/2 comparisons while finding the place, then N/2 move operations to move the others
out of the way.
You could obviously reduce the cost of searching to find the right place. Since Jobs
in the fJobs array are in order, a binary search mechanism could be used. (Do you still
remember binary search from Chapter 13?) This would reduce the search costs to
O(lgN) but the data shuffling steps would still be O(N).
If you could make the data shuffling steps as efficient as the search, then the overall
cost would be introduced to lgN. It doesn't make that much difference if the queues are
short and the frequency of calls is low (when N≈10, then O(lgN)/O(N) ≈ 3/10.) If the
queues are long, or operations on the queue are very frequent, then changing to a
O(lgN) algorithm gets to be important (when N≈1000, then O(lgN)/O(N) ≈ 10/1000.).
There is an algorithm that makes both searching and data shuffling operations have An alternative O(lgN)
costs that are O(lgN). The array elements, the Jobs, are kept "partially ordered". The algorithm
highest priority job will be in the first array element; the next two most urgent jobs will
be in the second and third locations, but they won't necessarily be in order. When the
most urgent job is removed, the next most urgent pops up from the second or the third
location; in turn, it is replaced by some other less urgent job. Once all the changes have
been made, the partial ordering condition will still hold. The most urgent of the
670 Collections of data
remaining jobs will be in the first array element, with the next two most urgent jobs in
the successive locations.
The algorithm is quite smart in that in manages to keep this partial ordering of the
data with a minimum number of comparison and data shuffling steps. Figures 21.2
illustrates the model used to keep the data "partially ordered".
Trees, roots, and tree- You should imagine the data values placed in storage elements that are arranged in a
nodes branching "tree"-like manner. Like most of the "trees" that appear in illustrations of
data structures, this tree "grows" downwards. The topmost "node" (branch point) is the
"root". It contains the data value with the most urgent priority (smallest "prio" value).
1
"Tree" structure: 2 Index Content
1 2
2 3 2 3
3 17 3 17
4 29
4 5 6 7 5 19
29 19 49 39 6 49
7 39
8 31
8
31
Figure 21.2 "Tree" of partially ordered values and their mapping into an array.
Child nodes and Each "node" in the tree can have up to two nodes in the level below it. (These are
leaves referred to as its "child nodes"; the terminology is very mixed up!) A node with no
children is a "leaf".
At every level, the data value stored in a node is smaller than the data values stored
in its children. As shown in Figure 21.2, you have the first node holding the value 2,
the second and third nodes hold the values 3 and 17. The second node, the one with the
3 has children that hold the values 29 and 19 and so on.
This particular tree isn't allowed to grow arbitrarily. The fourth and fifth nodes
added to the tree become children of the second node; the sixth and seventh additions
are children of the third node. The eight to fifteenth nodes inclusive form the next level
of this tree; with the 8th and 9th "children" of node 4 and so on.
"Tree" structure can Most of the "trees" that you will meet as data structures are built by allocating
be stored in an array separating node structs (using the new operator) and linking them with pointers. This
particular form of tree is exceptional. The restrictions on its shape mean that it can be
mapped onto successive array elements, as also suggested in Figure 21.2
The arrangement of data values in successive array entries is:
Class PriorityQueue 671
(#1, 2) (#2, 3) (#3, 17) (#4, 29) (#5, 19) (#6, 49) (#7, 39)
(#8, 31)
The values are "partially sorted", the small (high priority) items are near the top of the
array while the large values are at the bottom. But the values are certainly not in sorted
order (e.g. 19 comes after 29).
The trick about this arrangement is that when a new data value is added at the end, it
doesn't take very much work to rearrange data values to restore the partial ordering.
This is illustrated in Figure 21.3 and 21.4.
Figure 21.3 illustrates the situation immediately after a new data value, with priority
5, has been added to the collection. The collection is no longer partially ordered. The
data value in node 4, 29, is no longer smaller than the values held by its children.
1
2 Index Content
1 2
2 3 2 3
3 17 3 17
4 29
4 5 6 7 5 19
29 19 49 39 6 49
7 39
8 31
9 5
8 9
31 5 New value, added at end,
disrupts partial ordering
The partial ordering condition can be restored by letting the new data value work its
way up the branches that link its starting position to the root. If the new value is
smaller than the value in its "parent node", the two values are exchanged.
In this case, the new value in node 9 is compared with the value in its parent node,
node 4. As the value 5 is smaller than 29, they swap. The process continues. The
value 5 in node 4 is compared with the value in its parent node (node 2). Here the
comparison is between 3 and 5 and the smaller value is already in the lower node, so no
changes are necessary and the checking procedure terminates. Figure 21.4 illustrates
the tree and array with the partial ordering reestablished.
Note that the newly added value doesn't get compared with all the other stored data
values. It only gets compared with those along its "path to root".
672 Collections of data
1
2 Index Content
1 2
2 3 2 3
3 17 3 17
4 5
4 5 6 7 5 19
5 19 49 39 6 49
7 39
8 31
9 29
8 9
31 29
If the newly added value was smaller than any already in the storage structure, e.g.
value 1, it would work its way all the way up until it occupied the root-node in array
element 1. In this "worst case", the number of comparison and swap steps would then
be the same as the number of links from the root to the leaf.
O(lgN) algorithm Using arguments similar to those presented in Chapter 13 (when discussing binary
search and Quicksort), you can show that the maximum number of links from root to
leaf will be proportional to lgN where N is the number of data values stored in the
structure. So, the Insert operation has a O(lgN) time behaviour.
Removing the top When the "first" (remove) operation is performed, the top priority item gets
priority item removed. One of the other stored data values has to replace the data value removed
from the root node. As illustrated in Figure 21.5; the restoration scheme starts by
"promoting" the data value from the last occupied node.
Of course, this destroys the partial ordering. The value 29 now in the root node
(array element 1) is no longer smaller than the values in both its children.
Tidying up after So, once again a process of rearrangement takes place. Starting at the root node, the
removing the first data value in a node is compared with those in its children. The data values in parent
element
node and a child node are switched as needed. So, in this case, the value 29 in node 1
switches places with the 3 in node 2. The process is then repeated. The value 29 now
in node 2 is switched for the value 5 in node 4. The switching process can then
terminate because partial ordering has again been restored.
As in the case of insert, this tidying up after a removal only compares values down
from root to leaf. Again its cost is O(lgN).
Although a little more involved than the algorithms for TrivialPQ, the functions
needed are not particularly complex. The improved O(lgN) (as opposed to O(N))
performance makes it worth putting in the extra coding effort.
Class PriorityQueue 673
1
29 Index Content
1 29
2 3 2 3
3 17 3 17
4 5
4 5 6 7 5 19
5 19 49 39 6 49
7 39
8 31
8
31
8
31
Figure 21.5 Removal of top priority item followed by restoration of partial ordering.
Values get entered in random order, but they will be removed in increasing order. "Heapsort"
You can see that the priority queue could be used to sort data. You load up the priority
queue with all the data elements (using for "priorities" the key values on which you
want the data elements sorted). Once they are all loaded, you keep removing the first
element in the priority queue until it becomes empty. The values come out sorted.
This particular sorting algorithm is called "heapsort". Its efficiency is O(NlgN)
(roughly, you are doing O(lgN) insert and remove operations for each of N data
elements). Heapsort and Quicksort have similar performances; different degrees of
674 Collections of data
ordering in the initial data may slightly favour one algorithm relative to the other. Of
course, heapsort has the extra space overhead of the array used to represent the queue.
The name "heap" in heapsort is somewhat unfortunate. It has nothing to do with
"the Heap" used for free storage allocation.
A priority queue What does a priority queue own? This priority queue owns an array in which it keeps
owns … pointers to the queued items along with details of their priorities; there will also be an
integer data member whose value defines the number of items queued.
A priority queue does A priority queue can respond to the following requests:
…
• First
Remove the front element from the queue and return it.
• Add (preferred name Insert)
Insert another item into the queue at a position determined by its priority
• Length
Report how many items are queued.
• Full
As the queue only has a finite amount of storage, it may get to be full, a further
Insert operation would then cause some error. The queue has to have a "Full"
member function which returns true it is full.
• Empty
Returns true if there are no data elements queued; an error would occur if a
First operation was performed on an empty queue.
and supplementary In cases like this where the algorithms are getting a little more complex, it often
debugging functions helps to have some extra functions to support debugging. When building any kind of
tree or graph, you will find it useful to have some form of "print" function that can
provide structural information. As this is not part of the interface that would normally
be needed by clients, it should be implemented as conditionally compiled code.
Auxiliary private The Insert() and First() member functions make initial changes to the data in
member functions the array, then the "partial ordering" has to be reestablished. With Insert(), a data
element gets added at the bottom of the array and has to find its way up through the
conceptual tree structure until it gets in place. With First() a data element gets put in
at the root and has to drop down until it is in place. These rearrangements are best
handled by auxiliary private member functions (they could be done in Insert() and
First() but it is always better to split things up into smaller functions that are easier to
analyze and understand).
A struct to store The storage array used by a PriorityQ will be an array of simple structs that
details of queued incorporate a void* pointer to the queued object and a long integer priority. This can
objects and priorities
mean that the priority value gets duplicated (because it may also occur as an actual data
member within the queued object ). Often though, the priority really is something that
Class PriorityQueue 675
is only meaningful while the object is associated with the priority queue; after all,
casualty patients cease to have priorities if they get to be admitted as ward patients.
The type of struct used to store these data is only used within the priority queue
code. Since nothing else in the program need know about these structs, the struct
declaration can be hidden inside the declaration of class PriorityQ.
The "pq.h" header file with the class declaration is:
#ifndef __MYPQ__
#define __MYPQ__
#define DEBUG
#define k_PQ_SIZE 50
class PriorityQ { Class declaration
public:
PriorityQ();
void *First(void);
#ifdef DEBUG
void PrintOn(ostream& out) const;
#endif
private:
void TidyUp(void);
void TidyDown(void);
#endif
Here, the token DEBUG is defined so the PrintOn() function will be included in the
generated code. (Coding is slightly easier if element zero of the array is unused; to
allow for this, the array size is k_PQ_SIZE+1.)
As in the case of class Queue, the simple member functions like Length() can be
defined as "inlines" and included in the header file.
The constructor has to zero out the count; it isn't really necessary to clear out the Constructor
array but this is done anyway:
676 Collections of data
PriorityQ::PriorityQ()
{
for(int i=0; i<= k_PQ_SIZE; i++) {
fQueue[i].fd = NULL;
fQueue[i].fK = 0;
}
fcount = 0;
}
The Insert() function increments the count and adds the new queued item by
filling in the next element in the fQueue array. Function TidyUp() is then called to
restore partial ordering. First() works in an analogous way using TidyDown():.
Note that neither includes any checks on the validity of the operations; it would be
wiser to include Full() and Empty() checks with calls to an error function as was
done for class Queue.
#ifdef DEBUG
void PriorityQ::PrintOn(ostream& out) const
{
if(fcount == 0) {
cout << "Queue is empty" << endl;
return;
}
}
#endif
The TidyUp() function implements that chase up through the "branches of the tree"
comparing the value of the data item in a node with the value of the item in its parent
node. The loop stops if a data item gets moved up to the first node, or if the parent has
a data item with a lower fK value (higher priority value). Note that you find the array
index of a node's parent by halving its own index; e.g. the node at array element 9 is at
9/2 or 4 (integer division) which is as shown in the Figures 21.3 etc.
The code for TidyDown() is generally similar, but it has to check both children
(assuming that there are two child nodes) because either one of them could hold a data
item with a priority value smaller than that associated with the item "dropping down"
through levels of the "tree". (The code picks the child with the data item with the
smaller priority key and pushes the descending item down that branch.)
void PriorityQ::TidyDown(void)
{
int Nelems = fcount;
int k = 1;
keyeddata v = fQueue[k];
for(;;) {
int nk;
if(k>(Nelems/ 2)) break;
nk = k + k;
if((nk <= Nelems-1) &&
(fQueue[nk].fK > fQueue[nk+1].fK)) nk++;
Test program
Once again, a small test program must be provided along with the class. This test
program first checks out whether "heapsort" works with this implementation of the
priority queue. The queue is loaded up with some "random data" entered by the user,
then the data values are removed one by one to check that they come out in order. The
second part of the test is similar to the test program for the ordinary queue. It uses an
interactive routine with the user entering commands that add things and remove things
from the queue.
The test program starts by including appropriate header files and defining a Job
struct; the priority queue will store Jobs (for this test, a Job has just a single character
array data member):
#include <stdlib.h>
#include <iostream.h>
#include "pq.h"
struct Job {
char name[30];
};
Job* GetJob()
{
cout << "job name> ";
Job *j = new Job;
cin >> j->name;
return j;
}
main()
{
PriorityQ thePQ;
int i;
/*
Code doing a "heapsort"
*/
cout << "enter data for heap sort test; -ve prio to end data"
<< endl;
Load the queue with for(i = 0; ; i++)
some data {
int prio;
cout << "job priority ";
cin >> prio;
if(prio < 0)
break;
Job* j = GetJob();
thePQ.Insert(j, prio);
Class PriorityQueue 679
if(thePQ.Full())
break;
}
switch(ch) {
case 'q': done = 1;
break;
case 'l' : cout << "Queue length now " << thePQ.Length()
<< endl;
break;
case 'a':
if(thePQ.Full()) cout << "Queue is full!" << endl;
else {
int prio;
cout << "priority "; cin >> prio;
j = GetJob();
thePQ.Insert(j,prio);
}
break;
case 's':
thePQ.PrintOn(cout);;
break;
case 'g':
if(thePQ.Empty()) cout << "Its empty!" << endl;
else {
j = (Job*) thePQ.First(); Type case from void*
cout << "Removed " << j->name << endl; to Job*
delete j;
}
break;
case '?':
cout << "Commands are:" << endl;
cout << "\tq Quit\n\ta Add to queue\t" << endl;
cout << "\ts show queue status\n\tg"
"Get job at front of queue" << endl;
cout << "\tl Length of queue\n" << endl;
break;
default:
;
}
680 Collections of data
return 0;
}
Note the (Job*) type casts. We know that we put pointers to Jobs into this priority
queue but when they come back they are void* pointers. The type cast is necessary,
you can't do anything with a void*. (The type cast is also safe here; nothing else is
going to get into this queue). The code carefully deletes Jobs as they are removed from
the priority queue; but it doesn't clean up completely (there could be Jobs still in the
queue when the program exits).
Test output …
job priority 5
job name> data5
job priority 107
job name> data107
job priority -1
data5
data11
data17
data35
data55
data92
data107
data108
>a
priority 18
job name> drunk
>a
priority 7
job name> spider-venom
>a
priority 2
job name> pain-in-chest
>a
priority 30
job name> pain-in-a---
>a
priority 1
job name> gunshot
>g
Removed gunshot
>g
Removed pain-in-chest
>g
Removed spider-venom
>g
Removed drunk
>q
Class PriorityQueue 681
• add data objects to the collection (as in the rest of this chapter, the data objects are
identified by their addresses and these are handled as void* pointers);
In these two kinds of collection there is an implied ordering of the stored data items.
There is a "first item", a "second item" and so forth. In the simple versions considered
here, this ordering doesn't relate to any attributes of the data items themselves; the
ordering is determined solely by the order of addition. As the items are in sequence,
these collections also allow client programs to ask for the first, second, …, n -th item.
Figure 21.6 illustrates a dynamic array. A dynamic array object has a main data
block which contains information like the number of items stored, and a separate array
of pointers. The addresses of the individual data items stored in the collection are held
in this array. This array is always allocated in the heap (or simplicity, the header and
trailer information added by the run-time memory manager are not shown in
illustrations). The basic data block with the other information may be in the heap, or
may be an automatic in the stack, or may be a static variable.
As shown in Figure 21.6, the array of pointers can vary in size. It is this variation in
size that makes the array "dynamic".
When a dynamic array is created it gets allocated an array with some default number
of entries; the example in Figure 21.6 shows a default array size of ten entries. As data
objects get added to the collection, the elements of the array get filled in with data
addresses. Eventually, the array will become full.
When it does become full, the DynamicArray object arranges for another larger "Growing the array"
array to be allocated in the heap (using operator new []). All the pointers to stored data
items are copied form the existing array to the new larger array, and then the existing
array is freed (using operator delete []). You can vary the amount of growth of the
array, for example you could make it increase by 25% each time. The simplest
implementations of a dynamic array grow the array by a specified amount, for example
the array might start with ten pointers and grow by five each time it gets filled.
682 Collections of data
January
fSize 10 February
fInc 5
fNum 10 March
fCSize 10
fItems
DynamicArray
structure
October
Auxiliary array of
pointers to data items
January
fSize 10 February
fInc 5
fNum 11 March
fCSize 15
fItems
DynamicArray
structure
October
Removing items and Stored items do get removed from the array. It isn't like a queue where only the first
"Shrinking" the item can be removed, a "remove" request will identify the item to be removed. When
array
an item is removed, the "gap" in the array is filled up by moving up the data items from
the later array elements. So, if the array had had seven elements, removal of the fifth
would result in the pointers in the sixth and seventh elements each moving up one
place. If lots of items are removed, it may be worth shrinking the array. "Shrinking" is
similar to "growing". The DynamicArray object again creates a new array (this time
smaller) in the heap and copies existing data into the new array. The old large array is
deleted. Typically, the array is only shrunk when a remove operation leaves a
significant number of unused elements. The array is never made smaller than the
initially allocated size.
What does a dynamic The main thing a DynamicArray object owns is the array of pointers to the data
array own? items. In addition, it needs to have integer data members that specify: initial size (this
is also the minimum size), the size of the increment, the current array size, and the
number of data elements currently stored.
A DynamicArray You can vary the behaviours of DyanamicArrays and Lists a little. These classes
does … have to have functions to support at least some version of the following behaviours:
Class DynamicArray 683
• Length
Report how many items are stored in the collection.
• Add (preferred name Append)
Add another item to the collection, placing it "after" all existing items.
• Position
Find where an item is in the collection (returning its sequence number); this
function returns an error indicator if the item is not present. (This function may be
replaced by a Member function that simply returns a true or false indication of
whether an item is present).
• Nth
Get the item at a specified position in the collection (the item is not removed
from the collection by this operation).
• Remove
Removes a specified item, or removes the item at a specified position in the
collection.
Note that there is neither a "Full" nor an "Empty" function. DynamicArrays and Lists
are never full, they can always grow. If a client program needs to check for an "empty"
collection, it can just get the length and check whether this is zero.
You have to chose whether the sequence numbers for stored items start with 0 or 1.
Starting with 0 is more consistent with C/C++ arrays, but usually a 1-base is more
convenient for client applications. So, if there are N elements in the collection, the
"Position" function will return values in the range 1…N and functions like "Nth", and
"Remove" will require arguments in the range 1…N.
The remove function should return a pointer to the item "removed" from the
collection.
If desired, a debugging printout function can be included. This would print details
like the current array size and number of elements. It is also possible for the function to
print out details of the addresses of stored data items, but this information is rarely
helpful.
A declaration for a simple version of the DynamicArray class is:
private:
Auxiliary "Grow" void Grow(int amount);
function as private
member int fNum;
int fSize;
int fCSize;
int fInc;
The "pointer to void **fItems;
pointer" };
#endif
For a class like this, it is reasonable for the class declaration to provide default initial
values for parameters like the initial array size and the increment, so these appear in the
declaration for the constructor.
The next group of functions are all const functions; they don't change the contents of
a DynamicArray. Once again, the Length() function is simple and can be included in
the header file as an inline function.
Along with the Append() function, there are two overloaded versions of Remove();
one removes an item at a specified position in the collection, the other finds an item and
(if successful) removes it. The Append() and Remove() functions require auxiliary
"grow" and "shrink" functions. Actually, a single Grow() function will suffice, it can
be called with a positive increment to expand the array or a negative increment to
contract the array. The Grow() function is obviously private as it is merely an
implementation detail.
void** ! ? Most of the data members are simple integers. However, the fItems data member
that holds the address of the array of pointers is of type void**. This seemingly odd
type requires a little thought. Remember, a pointer like char* can hold the address of a
char variable or the address of the start of an array of chars; similarly an int* holds
the address of an int variable or the address of an array of ints. A void* holds an
address of something; it could be interpreted as being the address of the start of "an
array of voids" but you can't have such a thing (while there are void* variables you
can't have void variables). Here we need the address of the start of an array of void*
variables (or a single void* variable if the array size is 1). Since fItems holds the
address of a void* variable, its own type has to be void** (pointer to a pointer to
something).
The code implementing the class member functions would be in a separate "D.cp"
file. The constructor fills in the integer data members and allocates the array:
#include <stdlib.h>
#include <assert.h>
#include "D.h"
Class DynamicArray 685
fNum = 0;
fCSize = fSize = size;
fInc = inc;
Note that the definition does not repeat the default values for the arguments; these only
appear in the initial declaration. (The assert() checks at the start protect against silly
requests to create arrays with -1 or 1000000 elements.) The new [] operator is used to
create the array with the specified number of pointers.
Function Length() was defined as an inline. The other two const access functions
involve loops or conditional tests and so are less suited to being inline.
The class interface defines usage in terms of indices 1…N. Of course, the
implementation uses array elements 0…N-1. These functions fix up the differences
(e.g. Nth() decrements its argument n before using it to access the array).
Both these functions have to deal with erroneous argument data. Function
Position() can return zero to indicate that the requested argument was not in the
collection. Function Nth() can return NULL if the index is out of range. Function
Position() works by comparing the address of the item with the addresses held in
each pointer in the array; if the addresses match, the item is in the array.
The Append() function starts by checking whether the current array is full; if it is, a
call is made to the auxiliary Grow() function to enlarge the array. The new item can
686 Collections of data
then be added to the array (or the new enlarged array) and the count of stored items gets
incremented:
The Grow() function itself is simple. The new array of pointers is created and the
record of the array size is updated. The addresses are copied from the pointers in the
current array to the pointers in the new array. The old array is freed and the address of
the new array is recorded.
The Remove(void *item) function has to find the data item and then remove it.
But function Position() already exists to find an item, and Remove(int) will
remove an item at a known position. The first Remove() function can rely on these
other functions:
The Remove(int) function verifies its argument is in range, returning NULL if the
element is not in the array. If the required element is present, its address is copied into
a temporary variable and then the array entries are "closed up" to remove the gap that
would otherwise be left.
The scheme for shrinking the array is fairly conservative. The array is only shrunk if
remove operations have left it less than half full and even then it is only shrunk a little.
Test program
The test program for this class creates "book" objects and adds them to the collection.
For simplicity, "books" are just character arrays allocated in the heap. Rather than force
the tester to type in lots of data, the program has predefined data strings that can be used
to initialize the "books" that get created.
#include <stdlib.h>
#include <iostream.h>
#include <string.h>
#include "D.h"
num--;
cout << "You picked : " << BookStore[num] << endl;
Book ptr;
ptr = new char[strlen(BookStore[num]) + 1];
strcpy(ptr, BookStore[num]);
return ptr;
}
Function PickABook() allows the tester to simply enter a number; this selects a string
from the predefined array and uses this string to initialize a dynamically created Book.
Function GetPos() is another auxiliary function used to allow the tester to enter the
sequence number of an item in the collection:
Like most such test programs, this one consists of a loop in which the user is
prompted for commands. These commands exercise the different member functions of
the class:
int main()
{
Book lastadded = NULL;
DynamicArray c1;
break;
case '?':
cout << "Commands are:" << endl;
cout << "\tq Quit\n\ta Add to collection\t" << endl;
cout << "\tf Find item in collection (use number)"
<< endl;
cout << "\tF Find last added item in collection"
" (use pointer)" << endl;
cout << "\tr Remove item from collection"
" (use number)" << endl;
cout << "\tl Length (size) of collection\n" << endl;
break;
default:
;
}
}
return EXIT_SUCCESS;
}
like Nth() more costly (as it must then involve a loop that finds the n-th item among
those not yet removed).
A list is the hardy perennial that appears in every data structures book. It comes second
in popularity only to the "stack". (The "stack" of the data structures books is covered in
an exercise. It is not the stack used to organize stackframes and function calls; it is an
impoverished simplified variant.)
There are several slightly different forms of list, and numerous specialized list
classes that have extra functionality required for specific applications. But a basic list
supports the same functionality as does a dynamic array:
• Length
• Append
• Position
• Nth
• Remove
The data storage, and the mechanisms used to implement the functions are however
quite different.
The basics of operations on lists were explained in Section 20.6 and illustrated in List cells, next
Figure 20.7. The list structure is made up from "list cells". These are small structs, pointers, and the
head pointer
allocated on the heap. Each "list cell" holds a pointer to a stored data element and a
pointer to the next list cell. The list structure itself holds a pointer to the first list cell;
this is the "head" pointer for the list.
class List {
…
private:
ListCell *fHead;
…
};
This arrangement of a "head pointer" and the "next" links makes it easy to get at the
first entry in the list and to work along the list accessing each stored element in turn.
An "append" operation adds to the end of the list. The address of the added list cell A "tail" pointer
has to be stored in the "next" link of the cell that was previously at the end of the list. If
you only have a head pointer and next links, then each "append" operation involves
searching along the list until the end is found. When the list is long, this operation gets
to be a little slow. Most implementations of the list abstract data type avoid this cost by
having a "tail" pointer in addition to the head pointer. The head pointer holds the
692 Collections of data
address of the first list cell, the tail pointer holds the address of the las list cell; they will
hold the same address if the list only has one member. Maintaining both head and tail
pointers means that there is a little more "housekeeping" work to be done in functions
that add and remove items but the improved handling of append operations makes this
extra housekeeping worthwhile.
"Previous" links in Sometimes it is worth having two list cell pointers in each list cell:
the list cells
struct D_ListCell {
void *fData;
D_ListCell *fNext;
D_ListCell *fPrev;
};
These "previous" links make it as easy to move backwards toward the start of the list as
it is to move forwards toward the end of the list. In some applications where lists are
used it is necessary to move backwards and forwards within the same list. Such
applications use "doubly linked" lists. Here, the examples will consider just the singly
linked form.
The class declaration for a simple singly linked list is:
class List {
public:
List();
private:
struct ListCell { void *fData; ListCell *fNext; };
int fNum;
ListCell *fHead;
ListCell *fTail;
};
#endif
Class List 693
Apart from the constructor, the public interface for this class is identical to that of class
DynamicArray.
The struct ListCell is declared within the private part of class List. ListCells
are an implementation detail of the class; they are needed only within the List code and
shouldn't be used elsewhere.
The data members consist of the two ListCell* pointers for head and tail and fNum
the integer count of the number of stored items. You could miss out the fNum data
member and work out the length each time it is needed, but having a counter is more
convenient. The Length() function, which returns the value in this fNum field, can
once again be an inline function defined in the header file.
The remaining member functions would be defined in a separate List.cp
implementation file.
The constructor is simple, just zero out the count and set both head and tail pointers
to NULL:
#include <stdlib.h>
#include <assert.h>
#include "List.h"
List::List()
{
fNum = 0;
fHead = fTail = NULL;
}
Figure 21.7 illustrates how the first "append" operation would change a List object.
The List object could be a static, an automatic, or a dynamic variable; that doesn't
matter. The data item whose address is to be stored is assumed to be a dynamic (heap
based) variable. Initially, as shown in part 1 of Figure 21.7, the List object's fNum field
would be zero, and the two pointers would be NULL. Part 2 of the figure illustrates the
creation of a new ListCell struct in the heap; its fNext member has to be set to NULL
and its fData pointer has to hold the address of the data item.
Finally, as shown in part 3, both head and tail pointers of the List object are changed
to hold the address of this new ListCell. Both pointers refer to it because as it is the
only ListCell it is both the first and the last ListCell. The count field of the List object
is incremented.
The code implementing Append() is:
1 2
Data Item
xxxx
fNum 1
fHead
fTail
NULL
List
object ListCell
Figure 21.8 illustrates the steps involved when adding an extra item to the end of an
existing list.
Part 1 of Figure 21.8 shows a List that already has two ListCells; the fTail pointer
holds the address of the second cell.
An additional ListCell would be created and linked to the data. Then the second
cell is linked to this new cell (as shown in Part 2):
fTail->fNext = lc;
Class List 695
fNum 2 xxxx
fHead
fTail
NULL
List
object ListCell ListCell
List
object
NULL
List
object
NULL
Finally, the fTail pointer and fNum count are updated as shown in Part 3 of the
figure.
The Position() function, that finds where a specific data element is stored,
involves "walking along the list" starting at the cell identified by the head pointer. The
ListCell* variable ptr is initialized with the address of the first list cell and the
position counter pos is intialized to 1. The item has been found if its address matches
the value in the fData member of a ListCell ; when found, the value for pos is
returned. If the data address in the current list cell does not match, ptr is changed so
696 Collections of data
that it holds the address of the next list cell. The item will either be found, or the end of
the list will be reached. The last list cell in the list will have the value NULL in its fNext
link, this gets assigned to ptr. The test in the while clause will then terminate the loop.
As with the DynamicArray, a return value of 0 indicates that the sought item is not
present in the list.
The function Nth() is left as an exercise. It also has a while loop that involves
walking along the list via the fNext links. This while loop is controlled by a counter,
and terminates when the desired position is reached.
Removing an item The basic principles for removing an item from the list are illustrated in Figure 21.9.
from the middle of a The ListCell that is associated with the data item has first to be found. This is done
list
by walking along the list, in much the same way as was done the Position() function.
A ListCell* pointer, tmp, is set to point to this ListCell.
There is a slight complication. You need a pointer to the previous ListCell as well.
If you are using a "doubly linked list" where the ListCells have "previous" as well as
"next" links then getting the previous ListCell is easy. Once you have found the
ListCell that is to be unhooked from the list, you use its "previous" link to get the
ListCell before it. If you only have a singly linked list, then its slightly more
complex. The loop that walks along the list must update a "previous" pointer as well as
a pointer to the "current" ListCell
Once you have got pointer tmp to point to the ListCell that is to be removed, and
prev to point to the previous ListCell, then unhooking the ListCell from the list is
easy, you simply update prev's fNext link:
prev->fNext = tmp->fNext;
Parts 1 and 2 of Figure 21.9 shows the situation before and after the resetting of
prev's fNext link.
When you have finished unhooking the ListCell that is to be removed, you still
have to tidy up. The unhooked ListCell has to be deleted and a pointer to the
associated data item has to be returned as the result of the function.
Class List 697
Item 2
to be removed
Data Item 1 Data Item 2 Data Item 3
1
xxxx xxxx xxxx
fNum 3
fHead
fTail
xxxx xxxx
fNum 2
fHead
fTail
List
object
A Remove() function has to check for special cases where the ListCell that gets Complications at the
removed is the first, or the last (or the only) ListCell in the list. Such cases are special beginning and end of
a list
because they make it necessary to update the List's fHead and/or fTail pointers.
If the ListCell is the first in the list (fHead == tmp) then rather than "unhooking" Removing the first
it, the fHead pointer can just be set to hold the address of the next ListCell: entry
if(fHead == tmp)
fHead = tmp->fNext;
If the ListCell should happen to be the last one in the current list (fTail == tmp), Removing the last
the List's fTail pointer needs to be moved back to point to the previous entry: entry
if(fTail == tmp)
698 Collections of data
fTail = prev;
The two versions of Remove() are similar. The functions start with the loop that
searches for the appropriate ListCell (and deal with the ListCell not being present, a
List may get asked to remove an item that isn't present). Then there is code for
unhooking the ListCell and resetting fHead and fTail pointers as required. Finally,
there is code that tidies up by deleting the discarded ListCell and returning a pointer
to the associated data item.
The following code illustrates the version that finds an item by matching its address.
The version removing the item at a specified position is left as another exercise.
Tidy up fNum--;
void *dataptr = tmp->fData;
delete tmp;
return dataptr;
}
Go through the code and convince yourself that it does deal correctly with the case of a
List that only has one ListCell.
Test program
You've already seen the test program! Apart from two words, it is identical to the test
program for class DynamicArray. One word that gets changed is the definition of the
datatype for the collection. This has to be changed from
Class List 699
DynamicArray c1;
to
List c1;
The other word to change is the name of the header file that has to be #included; this
changes from "D.h" to "List.h".
The test program has to be the same. Lists and dynamic arrays are simply different
implementations of the same "collection" abstraction.
Overheads with the List implementation are significantly higher. If you just look at
the code, the overheads may not be obvious. The overheads are the extra space required
for the ListCells and the time required for calls to new and delete.
With the ListCells, a List is in effect using two pointers for each data item while
the DynamicArray managed with one. Further, each ListCell incurs the space
overhead of the storage manager's header and trailer records. In the DynamicArray , the
array allocated in the heap has a header an trailer, but the cost of these is spread out
over the ten or more pointers in that array.
The two implementations have similar costs for their Position() functions. The
Append() for a List always involves a call to new, whereas this is relatively rare for a
DynamicArray. A DynamicArray has to reshuffle more data during a Remove(); but
the loop moving pointers up one place in an array is not particularly costly. Further,
every (successful) Remove() operation on a List involves a call to delete while
delete operations are rare for a DynamicArray. The difference in the pattern of calls
to new and delete will mean that, in most cases, processing as well as space costs
favour the DynamicArray implementation of a collection.
Although DynamicArrays have some advantages, Lists are more generally used
and there are numerous special forms of list for specific applications. You had better
get used to working with them.
Just as there are many different kinds of specialized list, there are many "binary trees".
The binary tree illustrated in this section is one of the more common, and also one of
the simplest. It is a "binary search tree".
A "binary search tree" is useful when: When to use a binary
search tree
• you have data items (structs or instances of some class) that are characterized by
unique "key" values, e.g. the data item is a "driver licence record" with the key
being the driver licence number;
700 Collections of data
• you need to maintain a collection of these items, the size of the collection can not
be readily fixed in advanced and may be quite large (but not so large that the
collection can't fit in main memory);
The "keys" are most often just integer values, but they can be things like strings. The
requirement that keys be unique can be relaxed, but this complicates searches and
removal operations. If the keys are not unique, the search mechanism has to be made
more elaborate so that it can deal in turn with each data item whose key matches a given
search key. For simplicity, the rest of this section assumes that the keys are just long
integers and that they will be unique.
Alternatives: a simple You could just use an array to store such data items (or a dynamic array so as to
array? Bit slow. avoid problems associated with not knowing the required array size). Addition of items
would be easy, but searches for items and tidying up operations after removal of items
would be "slow" (run times would be O(N) with N the number of items in the
collection). The search would be O(N) because the items wouldn't be added in any
particular order and so to find the one with a particular key you would have to start with
the first and check each in turn. (You could make the search O(lgN) by keeping the
data items sorted by their keys, but that would make your insertion costs go up because
of the need to shuffle the data.) Removal of an item would require the movement of all
subsequent pointers up one place in the array, and again would have a run time O(N).
Alternatives: a hash If you simply needed to add items, and then search for them, you could use a hash
table?? Removals too table. (Do you still remember hash tables from Chapter 18?) A "search" of a hash table
hard.
is fast, ideally it is a single operation because the key determines the location of the data
item in the table (so, ideally, search is O(1)). The keys associated with the data items
would be reduced modulo the size of the hash table to get their insertion point. As
explained in Chapter 18, key collisions would mean that some data items would have to
be inserted at places other than the location determined simply by their key. It is this
factor that makes it difficult to deal with removals. A data item that gets removed may
have been one that had caused a key collision and a consequent shift of some other data
item. It is rather easy to lose items if you try doing removals on a hash table.
Binary search tree Binary trees offer an alternative where insertions, searches, and removals are all
performance practical and all are relatively low cost. On a good day, a binary tree will give you
O(lgN) performance for all operations; the performance does deteriorate to O(N) in
adverse circumstances.
A binary search tree gets its O(lgN) performance in much the same way as we got
O(lgN) performance with binary search in a sorted array (Chapter 13). The data are
going to be organized so that testing a given search key against a value from one of the
Class BinaryTree 701
data items is going to let you restrict subsequent search steps to either one of two
subtrees (subsets of the collection). One subtree will have all the larger keys, the other
subtree will have all the smaller keys. If your search key was larger than the key just
tested you search the subtree with the large keys, if your key is smaller you search the
other subtree. (If your search key was equal to the value just checked, then you've
found the data item that you wanted.)
If each such test succeeded in splitting the collection into equal sized subtree , then
the search costs would be O(lgN). In most cases, the split results in uneven sized
subtree and so performance deteriorates.
Tree structure
A binary search tree is built up from "tree-node" structures. In this section, "tree-nodes" Simple "tree-nodes"
(tn) are defined roughly as follows:
struct tn {
long key;
void *datalink;
tn *leftlink;
tn *rightlink;
};
The tree-node contains a key and a pointer to the associated data record (as in the rest of
this chapter, it is assumed that the data records are all dynamic structures located in the
heap). Having the key in the tree-node may duplicate information in the associated data
record; but it makes coding these initial examples a little simpler. The other two
pointers are links to "the subtree with smaller keys" (leftlink) and "the subtree with
larger keys" (rightlink). (The implementation code shown later uses a slightly more
sophisticated version where the equivalent of a "tree-node" is defined by a class that has
member functions and private data members.)
The actual binary tree object is responsible for using these tn structures to build up BinaryTree object
an overall structure for the data collection. It also organizes operations like searches. It
keeps a pointer, the "root" pointer, to the "root node" of the tree.
Figure 21.10 illustrates a simple binary search tree. The complete tree start starts
with a pointer to the "root" tree-node record. The rest of the structure is made up from
separate tree-nodes, linked via their "left link" and "right link" pointers.
The key in the root node is essentially arbitrary; it will start by being the key
associated with the first data record entered; in the example, the key in the root node is
1745.
All records associated with keys less than 1745 will be in the subtree attached via
the root node's "left link". In Figure 21.10, there is only one record associated with a
smaller key (key 1642). The tree node with key 1642 has no subtrees, so both its left
link and right link are NULL.
702 Collections of data
"Root" pointer in
"binary tree" object
Key 1745
Data Link (some data record)
Left Link
Right Link
1642 1811
NULL
NULL
1791
1871
NULL NULL
1848
1770
NULL
NULL NULL
1775
NULL
NULL
All records having keys greater than 1745 must be in the right subtree. The first
entered might have been a record with key 1811. The left subtree of the tree node
associated with key 1811 will hold all records that are less than 1811 but greater than
1745. Its right subtree will hold all records associated with keys greater than 1811.
The same structuring principles apply "recursively" at every level in the tree.
The shape of the tree is determined mainly by the order in which data records are
added. If the data records are added in some "random" order, the tree will be evenly
balanced. However, it is more usual for the addition process to be somewhat orderly,
for example records entered later might tend to have larger keys than those entered
earlier. Any degree of order in the sequence of addition results in an unbalanced tree.
Class BinaryTree 703
Thus, if the data records added later do tend to have larger keys, then the "right
subtrees" will tend to be larger than the left subtrees at every level.
Searching
You can find a data record associated with a given "search key" (or determine that there
is no record with that key) by a simple recursive procedure. This procedure takes as
arguments a pointer to a subtree and the desired key value:
if(compare_result is EQUAL)
return sub_tree_ptr->Data;
else
if(compare_result is LESS)
return recursive_search(
sub_tree_ptr->left_link, search_key)
else
return recursive_search(
sub_tree_ptr->right_link, search_key)
This function would be called with the "root" pointer and the key for the desired record.
Like all recursive functions, its code needs to start by checking for termination Termination test of
conditions. One possible terminating condition is that there is no subtree to check! The recursive search
function
function is meant to chase down the left/right links until it finds the specified key. But
if the key is not present, the function will eventually try to go down a NULL link. It is
easiest to check this at the start of the function (i.e. at the next level of recursion). If the
subtree is NULL, the key is not present so the function returns NULL.
If there is a subtree to check, the search key should be compared with the key in the Comparison of keys
tree-node record at the "root of the current subtree". If these keys are equal, the desired
record has been found and can be returned as the result of the function.
Otherwise the function has to be called recursively to search either the left or the Recursive call to
right subtree as shown. search either the left
or the right subtree
Two example searches work as follows:
Addition of a record
The function that adds a new record is somewhat similar to the search function. After
all, it has to "search" for the point in the tree structure where the new record can be
attached.
Of course, there is a difference. The addition process has to change the tree
structure. It actually has to replace one of the tn* (pointers to treenode) pointers.
Changing an existing A pointer has to be changed to hold the address of a new tree node associated with
pointer to refer to a the new data record. It could be the root pointer itself (this would be the case when the
new treenode
first record is added to the tree). More usually it would be either the left link pointer, or
the right link pointer of an existing tree-node structure that is already part of the tree.
The recursive function that implements the addition process had better be passed a
reference to a pointer. This will allow the pointer to be changed.
The addition process is illustrated in Figure 21.11. The figure illustrates the addition
of a new treenode associated with key 1606. The first level of recursion would have the
addition function given the root pointer. The key in the record identified by this
pointer, 1745, is larger than the key of the record to be inserted (1606), so a recursive
call is made passing the left link of the first tree node.
The treenode identified by this pointer has key 1642, again this is larger than the key
to be inserted. So a further recursive call is made, this time passing the left link of the
treenode with key 1642.
The value in this pointer is NULL. The current tree does not contain any records with
keys less than 1642. So, this is the pointer whose value needs to be changed. It is to
point to the subtree with keys less than 1642, and there is now going to be one – the
new treenode associated with key 1606.
Class BinaryTree 705
1745
1 new data
record
1642 1811
NULL
NULL
new data
record
1642 1811
NULL
NULL
1745
3 1642 1811
NULL
NULL
1606
if(compare_result is EQUAL)
warn user that duplicate keys are not allowed
return without making any changes
else
if(compare_result is LESS)
return recursive_add(
sub_tree_ptr->left_link, data item, key)
else
return recursive_add(
sub_tree_ptr->right_link, data item, key)
Recursion terminates if the treenode pointer argument contains NULL. A NULL pointer is
the one that must be changed; so the function creates the new treenode, fills in its data
members, changes the pointer, and returns.
If the pointer argument identifies an existing treenode structure, then its keys must
be compared with the one that is to be inserted. If they are equal, you have a "duplicate
key"; this should cause some warning message (or maybe should throw an exception).
The addition operation should be abandoned.
In other cases, the result of comparing the keys determines whether the new record
should be inserted in the left subtree or the right subtree. Appropriate recursive calls
are made passing either the left link or the right link as the argument for the next level
of recursion.
Removal of a record
Removing a record can be trickier. As shown in Figures 21.12 and 21.13, some cases
are relatively easy.
If the record that is to be removed is associated with a "leaf node" in the tree
structure, then that leaf can simply be cut away (see Figure 21.12).
If the record is on a vine (only one subtree below its associated treenode) then its
treenode can be cut out. The treenode's only subtree can be reattached at the point
where the removed node used to be attached (Figure 21.13).
Things get difficult with "internal nodes" that have both left and right subtrees. In
the example tree, the treenodes associated with keys 1645 and 1811 are both "internal"
nodes having left and right subtrees. Such nodes cannot be cut from the tree because
this would have the effect of splitting the tree into two separate parts.
Class BinaryTree 707
NULL
NULL NULL NULL NULL
NULL NULL
NULL NULL
"Root" pointer in
"binary tree" object
NULL NULL
NULL NULL
1848 1848
1770
NULL NULL
NULL NULL NULL
1775
1775 Removing a node
NULL NULL
NULL
on a "vine"
NULL
The key, and pointer to data record, can be removed from a treenode provided that
they are replaced by other appropriate data. Only the key and the pointer to data record
get changed; the existing left link and right link from the treenode are not to be
changed. The replacement key and pointer have to be data that already exist
somewhere in the tree. They have to be chosen so that the tree properties are
maintained. The replacement key must be such that it is larger than all the keys in the
subtree defined by the existing left link, and smaller than all the keys in the subtree
defined by the existing right link.
Promoting a These requirements implicitly define the key (and associated pointer to data record)
"successor" that must be used to replace the data that are being removed. They must correspond to
that data record's "successor". The successor will be the (key, data record) combination
that has the smallest key that is larger than the key being deleted from an internal node.
Find the (key, data) Figure 21.14 illustrates the concept of deletion with promotion of a successor. The
combination to be (key, data) combination that is to be deleted is the entry at the root, the one with key
deleted
1745. The key has first to be found; this would be done in the first step of the process.
Find the successor The next stage involves finding the successor – the (key, data) combination with the
smallest key greater than 1745. This will be the combination with key 1770. You find
it by starting down the right subtree (going to the treenode with key 1811) and then
heading down left links for as far as you can go.
Promoting the The key and pointer to data record must then be copied from the successor treenode.
successor They overwrite the values in the starting treenode, replacing the key and data pointer
that are supposed to get deleted. Thus, in the example in Figure 21.14, the "root"
treenode gets to hold the key 1770 and a pointer to the data item associated with this
key.
Removal of duplicate Of course, key 1770 and its associated data record are now in the tree twice! They
entry exist in the treenode where the deleted (key, data record) combination used to be, and
they are still present in their original position. The treenode where they originally
occurred must be cut from the tree. If, as in the case shown in Figure 21.14, there is a
right subtree, this replaces the "successor treenode". This is shown in Figure 21.14,
where the treenode with key 1775 becomes the new "left subtree" of the treenode with
key 1791.
If you had to invent such an algorithm for yourself, it might be hard. But these
algorithms have been known for the last forty years (and for most of those forty years,
increasing numbers of computing science students have been given the task of coding
the algorithms again). Coding is not hard, provided the process is broken down into
separate functions.
One function will find the (key, data record) combination that is to be deleted; the
algorithm used will be similar to that used for both the search and insert operations.
Once the (key, data) combination has been found, a second function can sort out
whether the deletion involves cutting off a leaf, excising a node on a vine, or replacing
the (key, data record) by a successor. Cutting off a leaf and excising a vine node are
both straightforward operations that don't require further subroutines. If a successor has
to be promoted, it must be found and removed from its original position in the tree.
Class BinaryTree 709
1745 1745
1 2
NULL NULL
NULL NULL
1848 1848
1770 1770
NULL NULL
NULL NULL NULL NULL
1775 1775
Finding the Finding the
NULL NULL
NULL internal node NULL successor
1770 1770
3 4
1848 1848
1770
NULL NULL
NULL NULL NULL
if(compare_result is EQUAL)
return result of Delete function applied 'c'
else
if(compare_result is LESS)
return recursive_remove(
sub_tree_ptr->left_link, data item, key)
else
return recursive_remove(
sub_tree_ptr->right_link, data item, key)
The function should return a pointer to the data record associated with the key that is to
be removed (or NULL if the key is not found). Like the search function, the remove
function has to check whether it has run off the end of the tree looking for a key that
isn't present; this is done by the first conditional test.
Passing a pointer by The rest of code is similar to the recursive-add function. Once again, a pointer has
reference to be passed by reference. The actual pointer variable passed as a reference argument
could be the root pointer for the tree (if we are deleting the root node of a tree with one
or two records); more usually, it will be either the "left link" or "right link" data
member of another treenode. The contents of this pointer may get changed. At the
stage where the search process is complete, the argument pointer will hold the address
of the treenode with the key that is to be removed. If the treenode with the "bad" key is
a leaf, the pointer should be changed to NULL. If the treenode with the bad key is a vine
node, the pointer should be changed to hold the address of that treenode's only child.
The actual changes are made in the auxiliary function Delete. But the pointer argument
has to be passed by reference to permit changes to its value.
Determining the Pseudo code for the Delete function is as follows:
deletion action
delete(a treenode pointer passed by reference ('c'))
save pointer to data record associated with bad key
successor
delete the successor treenode
Most of the code of Delete is made up by the conditional construct that checks for, and
deals with the different possible situations.
A pseudo code outline for the function to find the successor treenode and remove it
from its current position in the tree is:
More detailed outlines of the search, add, and remove operations can be found in
numerous text books; many provide Pascal or C implementations. Such
implementations have the functions as separate, independent global functions. We need
these functions packaged together as part of a BinaryTree class.
A BinaryTree object doesn't have much data, after all most of the structure What does a
representing the tree is made up from treenodes. A BinaryTree object will own a BinaryTree object
own?
pointer to the "root" treenode; it is useful if it also owns a count of the number of (key,
data record) combinations currently stored in the tree.
A BinaryTree should do the following: What does a
BinaryTree object
do?
• "Num items" (equivalent to Length of lists etc)
Report how many items are stored in the collection.
• Add
Add another (key, data record) combination to the tree, placing it at the
appropriate position (the Add operation should return a success/failure code, giving
a failure return for a duplicate key).
• Find
Find the data record associated with a given key, returning a pointer to this
record (or NULL if the specified key is not present).
• Remove
Removes the (key, data record) combination associated with a specified key,
returning a pointer to the data record (or NULL if the specified key is not present).
712 Collections of data
Because the structure is moderately complex, and keeps getting rearranged, it would be
worth including a debugging output function that prints a representation of the structure
of the tree.
The discussion of operations like remove identified several additional functions, like
the one to find a treenode's successor. These auxiliary functions would be private
member functions of the class.
The declaration for the class would be in a header file:
#define DEBUG
class TreeNode;
class BinaryTree
{
public:
BinaryTree();
void *Delete(TreeNode*&c);
TreeNode *Successor(TreeNode*& c);
#ifdef DEBUG
void AuxPrint(TreeNode* c, ostream& out, int depth)
const;
#endif
TreeNode *fRoot;
int fNum;
};
#endif
class TreeNode;
Implementation
class TreeNode {
public:
TreeNode(long k, void *d);
TreeNode*& LeftLink(void);
TreeNode*& RightLink(void);
long Key(void) const;
void *Data(void) const;
void Replace(long key, void *d);
private:
long fKey;
void *fData;
TreeNode *fLeft;
TreeNode *fRight;
};
714 Collections of data
Class TreeNode is simply a slightly fancy version of the tn treenode struct illustrated
earlier. It packages the key, data pointer, and links and provides functions that the
BinaryTree code will need to access a TreeNode.
The function definitions would specify most (or all) as "inline":
When TreeNodes are created, the key and data link values will be known so these can
be set by the constructor. TreeNodes always start as leaves so the left and right links
can be set to NULL.
The Key() and Data() functions provide access to the private data. The Replace()
function is used when the contents of an existing TreeNode have to be replaced by data
values promoted from a successor TreeNode (part of the deletion process explained
earlier).
The LeftLink() and RightLink() functions return "references" to the left link
and right link pointer data members. They are the first examples that we have had of
functions that return a reference data type. Such functions are useful in situations like
this where we need in effect to get the address of a data member of a struct or class
instance.
The code for class BinaryTree follows the declaration and definition of the
auxiliary TreeNode class. The constructor is simple; it has merely to set the root
pointer to NULL and zero the count of data records.
BinaryTree::BinaryTree()
{
fRoot = NULL;
fNum = 0;
}
Class BinaryTree 715
The public functions Find(), Add(), and Remove() simply set up the appropriate
recursive mechanism, passing the root pointer to the auxiliary recursive function. Thus,
Find() is:
if(compare == 0)
return c->Data();
if(compare < 0)
return AuxFind(c->LeftLink(), key);
else
return AuxFind(c->RightLink(), key);
}
if(compare == 0) {
cout << "Sorry, duplicate keys not allowed" << endl;
return 0;
}
if(compare < 0)
716 Collections of data
The following three functions implement the remove mechanism. First there is the
AuxRemove() function that organizes the search for the TreeNode with the "bad" key:
if(compare == 0)
return Delete(c);
if(compare < 0)
return AuxRemove(c->LeftLink(), key);
else
return AuxRemove(c->RightLink(), key);
}
The Delete() function identifies the type of deletion action and makes the appropriate
rearrangements:
void *BinaryTree::Delete(TreeNode*& c)
{
void *deaddata = c->Data();
if((c->LeftLink() == NULL) && (c->RightLink() == NULL))
{ delete c; c = NULL; }
else
if(c->LeftLink() == NULL) {
TreeNode* temp = c;
c = c->RightLink();
delete temp;
}
else
if(c->RightLink() == NULL) {
TreeNode* temp = c;
c = c->LeftLink();
delete temp;
}
else {
TreeNode* temp = Successor(c->RightLink());
c->Replace(temp->Key(), temp->Data());
delete temp;
}
return deaddata;
Class BinaryTree 717
TreeNode *BinaryTree::Successor(TreeNode*& c)
{
if(c->LeftLink() != NULL)
return Successor(c->LeftLink());
else {
TreeNode *temp = c;
c = c->RightLink();
return temp;
}
}
The print function is only intended for debugging purposes. It prints a rough
representation of a tree. For example, if given the tree of Figure 21.10, it produces the
output:
1848
1811
1791
1775
1770
1745
1642
With a little imagination, you should be able to see that this does represent the structure
of the tree (the less imaginative can check by reference to Figure 21.15).
Given that everything else about this tree structure is recursive, it is inevitable that
the printout function is too! The public interface function sets up the recursive process
calling the auxiliary print routine to do the work. The root pointer is passed in this
initial call.
#ifdef DEBUG
void BinaryTree::PrintOn(ostream& out) const
{
AuxPrint(fRoot, out, 0);
}
The (recursive) auxiliary print function takes three arguments – a pointer to treenode
to process, an indentation level (and also an output stream on which to print). Like all
recursive functions, it starts with a check for a terminating condition; if the function is
called with a NULL subtree, there is nothing to do and an immediate return can be made.
718 Collections of data
1745
1745
1848
1642
1811
1811
1791
1848
1791
1642
1811
1770
1775
1775
1770
1745
1791
1848
1642
1770
1775
Figure 21.15 Printed representation of binary tree.
}
#endif
The recursive AuxPrint() function gets to process every node in the tree – it is said Tree traversal
to perform a "tree traversal". There are many circumstances where it is useful to visit
every node and so traversal functions are generally provided for trees and related data
structures. Such functions are described briefly in the section on "Iterators" in Chapter
23.
Test Program
As with the other examples in this chapter, the class is tested using a simple interactive
program that allows data records to be added to, searched for, and removed from the
tree.
The test program starts with the usual #includes, then defines a trivial DataItem
class. The tree is going to have to hold some kind of data, class DataItem exists just so
as to have a simple example. A DataItem owns an integer key and a short name string.
#include <stdlib.h>
#include <iostream.h>
#include <string.h>
#include "biny.h"
class DataItem {
public:
DataItem(long k, char txt[]);
void PrintOn(ostream& out) const;
long K() const;
private:
long fK;
char fName[20];
};
The actual BinaryTree object has been defined as a global. Variable gTree is
initialized by an implicit call to the constructor, BinaryTree::BinaryTree() that gets
executed in "preamble" code before entry to main().
BinaryTree gTree;
The test program uses auxiliary functions to organize insertion and deletion of
records, and searches for records with given keys. Insertion involves creation of a new
DataItem, initializing it with data values entered by the user. This item is then added to
the tree. (Note, the code has a memory leak. Find it and plug it!)
void DoInsert()
{
char buff[100];
int k;
cout << "Enter key and name" << endl;
cin >> k >> buff;
DataItem *d = new DataItem(k, buff);
if(gTree.Add(d , k)) cout << "OK" << endl;
else cout << "Problems" << endl;
}
The search function gets a key from the user, and asks the tree to find the associated
DataItem. If found, a record is asked to print itself:
void DoSearch()
{
cout << "Enter search key : ";
int k;
cin >> k;
DataItem *item = (DataItem*) gTree.Find(k);
if(item == NULL) cout << "Not found " << endl;
else {
cout << "Found record : ";
item->PrintOn(cout);
}
}
Similarly, the delete function prompts for a key, and asks the tree for the record. If a
DataItem is returned, it is deleted so recovering the space that it occupied in the heap.
void DoDelete()
{
cout << "Enter key : ";
int k;
cin >> k;
DataItem *item = (DataItem*) gTree.Remove(k);
if(item == NULL) cout << "Wasn't there" << endl;
Class BinaryTree 721
else {
cout << "Removed item: " ;
item->PrintOn(cout);
cout << "Destroyed" << endl;
delete item;
}
}
The main() function has the usual interactive loop typical of these small test
programs:
int main()
{
for(int done = 0; ! done; ) {
cout << ">";
char ch;
cin >> ch;
switch(ch) {
case 'q': done = 1; break;
case 's':
DoSearch();
break;
case 'i':
DoInsert();
break;
case 'd':
DoDelete();
break;
case 'p':
gTree.PrintOn(cout);
break;
case '?':
cout << "q to quit, s to Search, i to "
"Insert,d to Delete, p Print" << endl;
break;
default:
;
}
}
return EXIT_SUCCESS;
}
You should not need to implement the code for any of the standard collection classes.
Your IDE will come with a class library containing most of the more useful collection
classes. Actually, it is likely that your IDE comes with several class libraries, more
than one of which contains implementations of the frequently used collection classes.
722 Collections of data
"Standard Template Your IDE may have these classes coded as "template classes". Templates are
Library" introduced in Chapter 25. Templates offer a way of achieving "generic" code that is
more sophisticated than the void* pointer to data item used in the examples in this
chapter. In the long run, templates have advantages; they provide greater flexibility and
better type security than the simpler approaches used in this chapter. They have some
minor disadvantages; their syntax is more complex and they can complicate the
processes of compilation and linking. There is a set of template classes, the "Standard
Template Library", that may become part of "standard C++"; these classes would then
be available on all implementations. Some of the classes in the Standard Template
Library are collection classes.
Your IDE will also provide a "framework class library". A framework class library
contains classes that provide prebuilt parts of a "standard Macintosh" program (as in
Symantec's TCL2 library) or "standard Windows" program (as with Borland's OWL or
Microsoft's MFC). Framework libraries are discussed more in Chapter 31. Your IDE's
framework class library contains additional collection classes. These classes will have
some restrictions. Normally they can only be used if you are building your entire
program on top of the framework.
The IDE may have additional simpler examples of collection classes. They will be
similar to, or maybe a little more sophisticated than the examples in this chapter and in
Chapter 24. Many other libraries of useful components and collection classes are in the
public domain and are available over the Internet.
Use and re-use! You should not write yet another slightly specialized version of "doubly linked list",
or "queue based on list". Instead, when analysing problems you should try to spot
opportunities to use existing components.
Read the problem specification. It is going to say either "get and put a file record"
(no need for collection classes), or "do something with this block of data" (arrays rather
than collection classes), or "get some items from the user and do something with them"
(probably a collection class needed).
Once you have established that the problem involves a variable number of data
items, then you know that you probably need to use a collection class. Read further.
Find how items are added to the collection, and removed. Find whether searches are
done on the collections or whether collections of different items are combined in some
way.
Once you have identified how the collection is used, you more or less know which
collection class you want. If items are frequently added to and removed from the
collection and the collection is often searched to find an item, you may be dealing with
a case that needs a binary tree. If you are building collections of different items, and
then later combining these, then you are probably going to need a list. If much of the
processing consists of gathering items then sorting them, you might be able to use a
priority queue.
For example, a problem may require you to represent polynomials like 7x3 + 6x2 -
4x+5, and do symbolic arithmetic like multiplying that first polynomial by 4x 2 -3x+7.
These polynomials are collections (the items in the collection represent the coefficients
Collection class libraries 723
for different powers of x); they vary in size (depending on the person who types in the
examples). The collections are combined. This is going to be a case where you need
some form of list. You could implement your own; but it is much better to reuse a
prebuilt, debugged, possibly optimized version.
Initially, stick to the simple implementations of collection classes like those
illustrated in this chapter or the versions in most public domain class libraries. As you
gain confidence in programming, start to experiment with the more advanced templates
provided by your IDE.
The good programmer is not the one who can rapidly hack out yet another version of
a standard bit of code. The good programmer is the one who can build a reliable
program by combining small amounts of new application specific code with a variety of
prebuilt components.
EXERCISES
A stack owns a collection of data items (void* pointers in this implementation). It supports
the following operations:
Your stack can use a fixed size array for storing the void* pointers, or a dynamic array or a
list. (Function Full should always return false if a list or dynamic array is used.)
2. Complete the implementation of class List by writing the Nth() and Remove(int pos)
member functions.
Modify the implementation of List::Remove() by changing the last part of the code
from:
fNum--;
void *dataptr = tmp->fData;
delete tmp;
724 Collections of data
return dataptr;
to
fNum--;
void *dataptr = tmp->fData;
delete prev;
return dataptr;
Note that this introduces two errors. First, there is a memory leak. The listcell pointed to by
tmp is being discarded, but as it is not being deleted it remains occupying space in the heap.
There is a much more serious error. The listcell pointed to by prev is still in use, it is still
part of the list, but this code "deletes it".
Compile and run the modified program. Test it. Are any of the outputs wrong? Does your
system "crash" with an address error?
Unless your IDE has an unusually diligent memory manager, the chances are that you will
find nothing wrong in your tests of your program. Everything will appear to work.
Things work because the listcells that have been deleted exist as "ghosts" in the heap. Until
their space gets reallocated, they still appear to be there. Their space is unlikely to be
reallocated during the course of a short test with a few dozen addition, search, and removal
operations.
Although you know the code is wrong, you will see it as working.
This should be a warning to all of us who happily say "Ah it's working!" after running a few
tests on a piece of code.
23
23 Intermediate class
The class construct has many ramifications and extensions, a few of which are
introduced in this chapter.
Section 23.1 looks at the problem of data that need to be shared by all instances of a "static" class
class. Shared data are quite common. For example, the air traffic control program in members for shared
data
Chapter 20 had a minimum height for the aircraft defined by a constant; but it might be
reasonable to have the minimum height defined by a variable (at certain times of the
day, planes might be required to make their approaches to the auto lander somewhat
higher say 1000 feet instead of 600 feet). The minimum height would then have to be a
variable. Obviously, all the aircraft are subject to the same height restriction and so
need to have access to the same variable. The minimum height variable could be made
a global; but that doesn't reflect its use. If really is something that belongs to the
aircraft and so should somehow belong to class Aircraft. C++ classes have "static"
members; these let programmers define such shared data.
Section 23.2 introduces "friends". One of the motivations for classes was the need Friends – sneaking
to build privacy walls around data and specialist housekeeping functions. Such walls through the walls of
privacy
prevent misuse of data such as can occur with simple structs that are universally
accessible. Private data and functions can only be used within the member functions of
the class. But sometimes you want to slightly relax the protection. You want private
data and functions to be used within member functions, and in addition in a few other
functions that are explicitly named. These additional functions may be global
functions, or they may be the member functions of some second class. Such functions
are nominated as "friends" in a class declaration. (The author of a class nominates the
friends if any. You can't come along later and try to make some new function a "friend"
of an existing class because, obviously, this would totally defeat the security
mechanisms.) There aren't many places where you need friend functions. They
sometimes appear when you have a cluster of separate classes whose instances need to
work together closely. Then you may get situations where there a class has some data
members or functions that you would like to make accessible to instances of other
members of the class cluster without also making them accessible to general clients.
788 Intermediate class
Iterators Section 23.3 introduces iterators. Iterator classes are associated with collection
classes like those presented in Chapter 21. An Iterator is very likely to be a "friend" of
the collection class with which it is associated. Iterators help you organize code where
you want to go through a collection looking at each stored item in turn.
Operator functions My own view is that for the most part "operator functions", the topic of Section 23.4,
are an overrated cosmetic change to the ordinary function call syntax. Remember how
class Number in Chapter 19 had functions like Multiply() (so the code had things like
a.Multiply(b) with a and b instances of class Number)? With operator functions,
you can make that a * b. Redefining the meaning of operator * allows you to pretty
up such code.
Such cosmetic uses aren't that important. But there are a few cases where it is useful
to redefine operators. For instance, you often want to extend the interface to the
iostream library so that you can write code like Number x; … cout << "x = " << x
<< endl. This can be done by defining a new global function involving the <<
operator. Another special case is the assignment operator, operator =; redefinition of
operator = is explained in the next section on resource manager classes. Other operators
that you may need to change are the pointer dereference operator, -> and the new
operator. However, the need to redefine the meanings of these operators only occurs in
more advanced work, so you wont see examples in this text.
Resource manager Instances of simple classes, like class Number, class Queue, class Aircraft are all
classes represented by a single block of data. But there are classes where the instances own
other data structures (or, more generally, other resources such as open files, network
connections and so forth). Class DynamicArray is an example; it owns that separately
allocated array of void* pointers. Classes List and BinaryTree also own resources;
after all, they really should be responsible for those listcells and treenodes that they
create in the heap.
Destructor functions Resource managers have special responsibilities. They should make certain that any
for resource manager resources that they claim get released when no longer required. This requirement
classes
necessitates a new kind of function – a "destructor". A destructor is a kind of
counterpart for the constructor. A constructor function initializes an object (possibly
claiming some resources, though usually additional resources are claimed later in the
object's life). A destructor allows an object to tidy up and get rid of resources before it
is itself discarded. The C++ compiler arranges for calls to be made to the appropriate
destructor function whenever an object gets destroyed. (Dynamic objects are destroyed
when you apply operator delete ; automatic objects are destroyed on exit from
function; and static objects are destroyed during "at_exit" processing that takes place
after return from main().)
Operator = and There is another problem with resource manager classes – assignment. The normal
resource manager meaning of assignment for a struct or class instance is "copy the bytes". Now the bytes
classes
in a resource manager will include pointers to managed data structures. If you just copy
the bytes, you will get two instances of the resource manager class that both have
pointers to the same managed data structure. Assignment causes sharing. This is very
rarely what you would want.
Introduction 789
A class declaration describes the form of objects of that class, specifying the various
data members that are present in each object. Every instance of the class is separate,
every instance holds its own unique data.
Sometimes, there are data that you want to have shared by all instance of the class.
The introduction section of this chapter gave the example of the aircraft that needed to
"share" a minimum height variable. For second example, consider the situation of
writing a C++ program that used Unix's Xlib library to display windows on an
Xterminal. You would probably implement a class Window. A Window would have
data members for records that describe the font to be used for displaying text, an integer
number that identifies the "window" actually manipulated by the interpretive code in
the Xterminal itself, and other data like background colour and foreground colour.
Every Window object would have its own unique data in its data members. But all the
windows will be displayed on the same screen of the same display. In Xlib the screen
and the display are described by data structures; many of the basic graphics calls require
these data structures to be included among the arguments.
790 Intermediate class
You could make the "Display" and the "Screen" global data structures. Then all the
Window objects could use these shared globals.
But the "Display" and the "Screen" should only be used by Windows. If you make
them globals, they can be seen from and maybe get misused in other parts of the
program.
The C++ solution is to specify that such quasi globals be changed to "class
members" subject to the normal security mechanisms provided by C++ classes. If the
variable that represents the minimum height for aircraft, or those that represent the
Display and Screen used by Windows, are made private to the appropriate classes, then
they can only be accessed from the member functions of those classes.
Of course, you must distinguish these shared variables from those where each class
instance has its own copy. This is done using the keyword static . (This is an
unfortunate choice of name because it is a quite different meaning from previous uses
of the keyword static.) The class declarations defining these shared variables would
be something like the following:
class Window {
public:
…
private:
static Screen sScreen;
static Display sDisplay;
GC fGC;
XRectangle fRect;
…
};
(As usual, it is helpful to have some naming convention. Here, static data members of
classes will be given names starting with 's'.)
Defining the static The class declarations specify that these variables will exist somewhere, but they
variables don't define the variables. The definitions have to appear elsewhere. So, in the case of
class Aircraft , the header file would contain the class declaration specifying the
existence of the class data member sMinHeight, the definition would appear in the
Aircraft.cp implementation file:
#include "Aircraft.h"
Shared class properties 791
…
int Aircraft::TooLow()
{
return (fData.z < sMinHeight);
}
The definition must use the full name of the variable; this is the member name qualified
by the class name, so sMinHeight has to be defined as Aircraft::sMinHeight. The
static qualifier should not be repeated in the definition. The definition can include an
initial value for the variable.
The example TooLow() function illustrates use of the static data member from
inside a member function.
Quite often, such static variables need to be set or read by code that is not part of
any of the member functions of the class. For example, the code of the AirController
class would need to change the minimum safe height. Since the variable sMinHeight is
private, a public access function must be provided:
Most of the time the AirController worked with individual aircraft asking them to Static member
perform operations like print their details: fAircraft[i]->PrintOn(cout). But functions
when the AirController has to change the minimum height setting, it isn't working
with a specific Aircraft. It is working with the Aircraft class as a whole. Although
it is legal to have a statement like fAircraft[i]->SetMinHeight(600), this isn't
appropriate because the action really doesn't involve fAircraft[i] at all.
A member function like SetMinHeight() that only operates on static (class) data
members should be declared as a static function:
This allows the function to be invoked by external code without involving a specific
instance of class Aircraft, instead the call makes clear that it is "asking the class as a
whole" to do something.
792 Intermediate class
Use of statics You will find that most of the variables that you might initial think of as being
"globals" will be better defined as static members of one or other of the classes in
your program.
One fairly common use is getting a unique identifier for each instance of a class:
class Thing {
public:
Thing();
…
private:
static int sIdCounter;
int fId;
…
};
int Thing::sIdCounter = 0;
Each instance of class Thing has its own identifier, fId. The static (class) variable
sIdCounter gets incremented every time a new Thing is created and so its value can
serve as the latest Thing's unique identifier.
23.2 FRIENDS
As noted in the introduction to this chapter, the main use of "friend" functions will be to
help build groups (clusters) of classes that need to work closely together.
In Chapter 21, we had class BinaryTree that used a helper class, TreeNode.
BinaryTree created TreeNodes and got them to do things like replace their keys.
Other parts of the program weren't supposed to use TreeNodes . The example in
Chapter 21 hid the TreeNode class inside the implementation file of BinaryTree. The
header file defining class BinaryTree merely had the declaration class TreeNode;
which simply allowed it to refer to TreeNode* pointers. This arrangement prevents
other parts of a program from using TreeNodes. However, there are times when you
can't arrange the implementation like that; code for the main class (equivalent to
Friends 793
BinaryTree ) might have to be spread over more than one file. Then, you have to
properly declare the auxiliary class (equivalent of TreeNode) in the header file. Such a
declaration exposes the auxiliary class, opening up the chance that instances of the
auxiliary class will get used inappropriately by other parts of the program.
This problem can be resolved using a friend relation as follows:
All the member functions and data members of class Auxiliary are declared private,
even the constructor. The C++ compiler will systematically enforce the private
restriction. If it finds a variable declaration anywhere in the main code, e.g. Auxiliary
a 1 ; , it will note that this involves an implicit call to the constructor
Auxiliary::Auxiliary() and, since the constructor is private, the compiler will
report an access error. Which means that you can't have any instances of class
Auxiliary!
However, the friend clause in the class declaration partially removes the privacy
wall. Since class MainClass is specified to be a friend of Auxiliary, member
functions of MainClass can invoke any member functions (or data members) of an
Auxiliary object. Member functions of class MainClass can create and use instances
of class Auxiliary.
There are other uses of friend relations but things like this example are the main
ones. The friend relation is being used to selectively "export" functionality of a class to
chosen recipients.
23.3 ITERATORS
With collection classes, like those illustrated in Chapter 21, it is often useful to be able
to step through the collection processing each data member in turn. The member
functions for List and DynamicArray did allow for such iterative access, but only in a
relatively clumsy way:
794 Intermediate class
DynamicArray d1;
…
…
for(int i = 1; i < d1.Length(); i++) {
Thing* t = (Thing*) d1.Nth(i);
t->DoSomething();
…
}
That code works OK for DynamicArray where Nth() is basically an array indexing
operation, but it is inefficient for List where the Nth() operation involves starting at
the beginning and counting along the links until the desired element is found.
The PrintOn() function for BinaryTree involved a "traversal" that in effect
iterated though each data item stored in the tree (starting with the highest key and
working steadily to the item with the lowest key). However the BinaryTree class
didn't provide any general mechanism for accessing the stored elements in sequence.
Mechanisms for visiting each data element in turn could have been incorporated in
the classes. The omission was deliberate.
Increasingly, program designers are trying to generalize, they are trying to find
mechanisms that apply to many different problems. General approaches have been
proposed for working through collections.
The basic idea is to have an "Iterator" associated with the collection (each collection
has a specialized form of Iterator as illustrated below). An Iterator is in itself a simple
class. Its public interface would be something like the following (function names may
differ and there may be slight variations in functionality):
class Iterator {
public:
Iterator(…);
void First(void);
void Next(void);
int IsDone(void);
void *CurrentItem(void);
private:
…
};
The idea is that you can create an iterator object associated with a list or tree collection.
Later you can tell that iterator object to arrange to be looking at the "first" element in
the collection, then you can loop examining the items in the collection, using Next() to
move on to the next item, and using the IsDone() function to check for completion:
Collection c1;
…
Iterator i1(c1);
i1.Start();
while(!i1.IsDone()) {
Iterators 795
This same code would work whether the collection were a DynamicArray, a List, or a
BinaryTree.
As explained in the final section of this chapter, it is possible to start by giving an An "abstract base
abstract definition of an iterator as a "pure abstract class", and then define derived class" for Iterators?
subclasses that represent specialized iterators for different types of collection. Here, we
won't bother to define the general abstraction, and will just define and use examples of
specialized classes for the different collections.
The iterators illustrated here are "insecure". If a collection gets changed while an Insecure iterators
iterator is working, things can go wrong. (There is an analogy between an iterator
walking along a list and a person using stepping stones to cross a river. The iterator
moves from listcell to listcell in response to Next() requests; it is like a person
stepping onto the next stone and stopping after each step. Removal of the listcell where
the iterator is standing has an effect similar to magically removing a stepping stone
from under the feet of the river crosser.) There are ways of making iterators secure, but
they are too complex for this introductory treatment.
23.3.1 ListIterator
An iterator for class List is quite simple to implement. After all, it only requires a
pointer to a listcell. This pointer starts pointing to the first listcell, and in response to
"Next" commands should move from listcell to listcell. The code implementing the
functions for ListIterator is so simple that all its member functions can be defined
"inline".
Consequently, adding an iterator for class List requires only modification of the
header file:
#ifndef __MYLIST__
#define __MYLIST__
class ListIterator;
class List {
public:
List();
Friend nomination There are several points to note in this header file. Class List nominates class
ListIterator as a friend; this means that in the code of class ListIterator, there can
be statements involving access to private data and functions of class List.
Access function Here, an extra function is defined – List::Head(). This function is private and
List::Head() therefore only useable in class List and its friends (this prevents clients from getting at
the head pointer to the chain of listcells). Although, as a friend, a ListIterator can
directly access the fHead data member, it is still preferable that it use a function style
interface. You don't really want friends becoming too intimate for that makes it
difficult to locate problems if something goes wrong.
Declaration of The class declaration for ListIterator is straightforward except for the type of its
ListIterator class fPos pointer. This is a pointer to a ListCell . But the struct ListCell is defined
within class List. If, as here, you want to refer to this data type in code outside of that of
class List, you must give its full type name. This is a ListCell as defined by class
List. Hence, the correct type name is List::ListCell.
ListIterator 797
The member functions for class ListIterator are all simple. The constructor Implementation of
keeps a pointer to the List that it is to work with, and initializes the fPos pointer to the ListIterator
first listcell in the list. Member function First() resets the pointer (useful if you want
the iterator to run through the list more than once); Next() advances the pointer;
CurrentItem() returns the data pointer from the current listcell; and IsDone() checks
whether the fPos pointer has advanced off the end of the list and become NULL. (The
code for Next() checks to avoid falling over at the end of a list by being told to take
the "next" of a NULL pointer. This could only occur if the client program was in error.
You might choose to "throw an exception", see Chapter 26, rather than make it a "soft
error".)
The test program used to exercise class List and class DynamicArray can be
extended to check the implementation of class ListIterator:. It needs a new branch
in its switch() statement, one that allows the tester to request that a ListIterator
"walk" along the List:
case 'w':
{
ListIterator li(&c1);
li.First();
cout << "Current collection " << endl;
while(!li.IsDone()) {
Book p = (Book) li.CurrentItem();
cout << p << endl;
li.Next();
}
}
break;
The statement:
ListIterator li(&c1);
creates a ListIterator, called li, giving it the address of the List, cl, that it is to
work with (the ListIterator constructor specifies a pointer to List, hence the need
for an & address of operator).
The statement, li.First() , is redundant because the constructor has already
performed an equivalent initialization. It is there simply because that is the normal
pattern for walking through a collection:
li.First();
while(!li.IsDone()) {
… li.CurrentItem();
…
li.Next();
}
798 Intermediate class
In the example program, Book is a pointer type (actually just a char*). The Current-
Item() function returns a void*. The programmer knows that the only things that will
be in the cl list are Book pointers; so the type cast is safe. It is also necessary because
of course you can't really do anything with a void* and here the code needs to process
the books in the collection.
Backwards and Class List is singly linked, it only has "next" pointers in its listcells. This means
forwards iterators in that it is only practical to "walk forwards" along the list from the head to the tail. If the
two way lists
list class uses listcells with both "next" and "previous" pointers, it is practical to walk
the list in either direction. Iterators for doubly linked lists usually take an extra
parameter in their constructor; this is a "flag" that indicates whether the iterator is a
"forwards iterator" (start at the head and follow the next links) or a "backwards iterator"
(start at the tail and follow the previous links).
23.3.2 TreeIterator
Like doubly linked lists that can have forwards or backwards iterators, binary trees can
have different kinds of iterator. An "in order" iterator process the left subtree, handles
the data at a treenode, then processes the right subtree; a "pre order" iterator processes
the data at a tree node before examining the left and right subtrees. However, if the
binary tree is a search tree, only "in order" traversal is useful. An in order style of
traversal means that the iterator will return the stored items in increasing order by key.
An iterator that can "walk" a binary tree is a little more elaborate than that needed
for a list. It is easy to descend the links from the root to the leaves of a tree, but there
aren't any "back pointers" that you could use to find your way back from a leaf to the
root. Consequently, a TreeIterator can't manage simply with a pointer to the current
TreeNode, it must also maintain some record of information describing how it reached
that TreeNode.
Stack of pointers As illustrated in Figure 23.1, the iterator uses a kind of "stack" of pointers to
maintain state of TreeNodes. In response to a First() request, it chases down the left vine from the
traversal
root to the left most leaf; so, in the example shown in Figure 23.1 it stacks up pointers
to the TreeNodes associated with keys 19, 12, 6.
A CurrentItem() request should return the data item associated with the entry at
the top of this stack.
A Next() request has to replace the topmost element by its successor (which might
actually already be present in the stack). As illustrated in Figure 23.1, the Next()
request applied when the iterator has entries for 19, 12, and 6, should remove the 6 and
add entries for 9 and 7.
TreeIterator 799
Example Tree: 19
12 28
6 26 33
TreeIterator's "stack"
First() 19 19 19 19 19 28 28 33
12 12 12 12 26
6 9 9
7
6 7 9 12 19 26 28 33
A subsequent Next() request removes the 7, leaving 19, 12, and 9 on the stack.
Further Next() requests remove entries until the 19 is removed, it has to be replaced
with its successor so then the stack is filled up again with entries for 28 and 26.
The programmer implementing class TreeIterator has to chose how to represent Representing the
this stack. If you wanted to be really robust, you would use a DynamicArray of stack
TreeNode pointers, this could grow to whatever size was needed. For most practical
purposes a fixed size array of pointers will suffice, for instance an array with one
hundred elements. The size you need is determined by the maximum depth of the tree
and thus depends indirectly on the number of elements stored in the tree. If the tree
were balanced, a depth of one hundred would mean that the tree had quite a large
number of nodes (something like 299). Most trees are poorly balanced. For example if
you inserted 100 data items into a tree in decreasing order of their keys, the left branch
would be one hundred deep. Although a fixed array will do, the code needs to check for
the array becoming full.
800 Intermediate class
Class BinaryTree has to nominate class TreeIterator as a "friend", and again for
style its best to provide a private access function rather than have this friend rummage
around in the data:
class BinaryTree
{
public:
BinaryTree();
…
friend class TreeIterator;
private:
TreeNode *Root(void);
…
};
Class TreeIterator has the standard public interface for an iterator; its private data
consist of a pointer to the BinaryTree it works with, an integer defining the depth of
the "stack", and the array of pointers:
class TreeIterator {
public:
TreeIterator(BinaryTree *tree);
void First(void);
void Next(void);
int IsDone(void);
void *CurrentItem(void);
private:
int fDepth;
TreeNode *fStack[kITMAXDEPTH];
BinaryTree *fTree;
};
The constructor simply initializes the pointer to the tree and the depth counter. This
initial value corresponds to the terminated state, as tested by the IsDone() function.
For this iterator, a call to First() must be made before use.
TreeIterator::TreeIterator(BinaryTree *tree)
{
fTree = tree;
fDepth = -1;
}
int TreeIterator::IsDone(void)
{
return (fDepth < 0);
}
TreeIterator 801
Function First() starts at the root and chases left links for as far as it is possible to
go; each TreeNode visited during this process gets stacked up. This process gets things
set up so that the data item with the smallest key will be the one that gets fetched first.
void TreeIterator::First(void)
{
fDepth = -1;
TreeNode *ptr = fTree->Root();
while(ptr != NULL) {
fDepth++;
fStack[fDepth] = ptr;
ptr = ptr->LeftLink();
}
}
Data items are obtained from the iterator using CurrentItem(). This function just
returns the data pointer from the TreeNode at the top of the stack:
void *TreeIterator::CurrentItem(void)
{
if(fDepth < 0) return NULL;
else
return fStack[fDepth]->Data();
}
The Next() function has to "pop" the top element (i.e. remove it from the stack)
and replace it by its successor. Finding the successor involves going down the right
link, and then chasing left links as far as possible. Again, each TreeNode visited during
this process gets "pushed" onto the stack. (If there is no right link, the effect of Next()
is merely to pop an element from the stack.)
void TreeIterator::Next(void)
{
if(fDepth < 0) return;
Use of the iterator should be tested. An additional command can be added to the test
program shown previously:
802 Intermediate class
case 'w':
{
TreeIterator ti(&gTree);
ti.First();
cout << "Current tree " << endl;
while(!ti.IsDone()) {
DataItem *d = (DataItem*) ti.CurrentItem();
d->PrintOn(cout);
ti.Next();
}
}
break;
Those Add(), Subtract(), and Multiply() functions in class Number (Chapter 19)
seem a little unaesthetic. It would be nicer if you could write code like the following:
Number a("97417627567654326573654365865234542363874266");
Number b("65765463658764538654137245665");
Number c;
c = a + b;
The operations '+', '-', '/' and '*' have their familiar meanings and c = a + b does read
better than c = a.Add(b). Of course, if you are going to define '+', maybe you should
define ++, +=, --, -=, etc. If you do start defining operator functions you may have
quite a lot of functions to write.
Operator functions are overrated. There aren't that many situations where the
operators have intuitive meanings. For example you might have some "string" class
that packages C-style character strings (arrays each with a '\0' terminating character as
its last element) and provides operations like Concatenate (append):
String a("Hello");
String b(" World");
c = a.Concatenate(b); // or maybe? c = a + b;
You could define a '+' operator to work for your string class and have it do the
concatenate operation. It might be obvious to you that + means "append strings", but
other people won't necessarily think that way and they will find your c = a + b more
difficult to understand than c = a.Concatenate(b).
When you get to use the graphics classes defined in association with your IDE's
framework class library, you will find that they often have some operator functions
defined. Thus class Point may have an operator+ function (this will do something
Operator functions 803
like vector addition). Or, you might have class Rectangle where there is an
"operator+(const Point&)" function; this curious thing will do something like move
the rectangle's topleft corner by the x, y amount specified by the Point argument (most
people find it easier if the class has a Rectangle::MoveTopLeftCorner() member
function).
Generally, you should not define operator functions for your classes. You can make
exceptions for some. Class Number is an obvious candidate. You might be able to
pretty up class Bitmap by giving it "And" and "Or" functions that are defined in terms
of operators.
Apart from a few special classes where you may wish to define several operator
functions, there are a couple of operators whose meanings you have to redefine in many
classes.
double + double load floating point register with first data item
add second data item to contents of register
The translation may specify a sequence of instructions like those shown. But some
machines don't have hardware for all arithmetic operations. There are for example
RISC computers that don't have "floating point add" and "floating point multiply";
some don't even have "integer divide". The translations for these operators will specify
the use of a function:
In most languages, the compiler's translation tables are fixed. C++ allows you to add
extra entries. So, if you have some "add" code for a class Point that you've defined and
you want this called for Point + Point, you can specify this to the compiler. It takes
details from your specification and appends these to its translation tables:
The specifications that must appear in your classes are somewhat unpronounceable.
An addition operator would be defined as the function:
operator+()
(say that as "operator plus function"). For example, you could have:
class Point {
public:
Point();
…
Point operator+(const Point& other) const;
…
private:
int fh, fv;
};
This example assumes that the + operation shouldn't change either of the Points that it
works on but should create a temporary Point result (in the return part of a function
stackframe) that can be used in an assignment; this makes it like + for integers and
doubles.
It is up to you to define the meaning of operator functions. Multiplying points by
points isn't very meaningful, but multiplying points by integers is equivalent to scaling.
So you could have the following where there is a multiply function that changes the
Point object that executes it:
class Point {
public:
Point();
…
Point operator+(const Point& other) const;
Point& operator*(int scalefactor);
…
private:
int fh, fv;
};
Defining operator functions 805
with a definition:
with these definitions you can puzzle anyone who has to read and maintain your code
by having constructs like:
Point a(6,4);
…;
a*3;
Sensible maintenance programmers will eventually get round to changing your code
to:
class Point {
public:
Point();
…
Point operator+(const Point& other) const;
void ScaleBy(int scalefactor);
…
};
Point a(6,4);
…;
a.ScaleBy(3);
806 Intermediate class
Avoid the use of operator functions except where their meanings are universally
agreed. If their meanings are obvious, operator function can result in cosmetic
improvements to the code; for example, you can pretty up class Number as follows:
class Number {
public:
// Member functions declared as before
…
Number operator+(const Number& other) const;
…
Number operator/(const Number& other) const;
private:
// as before
…
};
You will frequently want to extend the meanings of the << and >> operators. A C++
compiler's built in definition for these operators is quite limited:
long >> long load integer register with first data item
shift right by the specified number of places
But if you #include the iostream header files, you add all the "takes from" and "gives
to" operators:
istream >> long push the istream id and the address of the long
onto the stack
call the function "istream::operator>>(long&)"
These entries are added to the table as the compiler reads the iostream header file with
its declarations like:
class ostream {
public:
…
ostream& operator<<(long);
ostream& operator<<(char*);
…
};
Such functions declared in the iostream.h header file are member functions of class
istream or class ostream. An ostream object "knows" how to print out a long integer,
a character, a double, a character string and so forth.
How could you make an ostream object know how to print a Point or some other
programmer defined class?
Typically, you will already have defined a PrintOn() member function in your
Point class.
class Point {
public:
…
void PrintOn(ostream& out);
private:
int fh, fv;
};
and all you really want to do is make it possible to write something like:
rather than:
p2.PrintOn(cout);
cout << endl;
You want someway of telling the compiler that if it sees the << operator involving an
ostream and a Point then it is to use code similar to that of the Point::PrintOn()
function (or maybe just use a call to the existing PrintOn() function).
You could change the classes defined in the iostream library. You could add extra
member functions:
class ostream {
// everything as now plus
ostream& operator<<(const Point& p);
…
};
(the appropriate return type will be explained shortly). The compiler invents a name for
the function (it will be something complex like __leftshift_Tostreamref_
cTPointref) and adds the new meaning for << to its table:
This definition then allows constructs like: Point p; …; cout << p;.
Of course, the ideal is for the stream output operations to be concatenated as in:
Takes from and gives to operators 809
cout << "Start point " << p1 << ", end point " << p2 << endl;
This requirement defines the return type of the function. It must return a reference to
the ostream:
Having a reference to the stream returned as a result permits the concatenation. Figure
23.2 illustrates the way that the scheme works.
cout << "Start point " << p1 << ", end point " << p2 << endl;
calls
ostream::operator<<(char*),
returning ostream&, i.e. cout
calls global
cout << p1 operator<<(ostream&, const Point),
returning ostream&, i.e. cout
This section explains some of the problems associated with "resource manager" classes.
Resource manager classes are those whose instances own other data structures.
Usually, these will be other data structures separately allocated in the heap. We've
already seen examples like class DynamicArray whose instances each own a separately
allocated array structure. However, sometimes the separately allocated data structures
810 Intermediate class
may be in operating system's area; examples here are resources like open files, or "ports
and sockets" as used for communications between programs running on different
computers.
The problems for resource managers are:
The first subsection, 23.5.1, provides some examples illustrating these problems. The
following two sections present solutions.
Instances of classes can acquire resources when they are created, or as a result of
subsequent actions. For example, an object might require a variable length character
string for a name:
class DataItem {
public:
DataItem(const char* dname);
…
private:
char *fName;
…
};
class SessionLogger {
public:
SessionLogger();
…
int OpenLogFile(const char* logname);
…
private:
…
ofstream fLfile;
…
};
Resource management 811
Instances of the DataItem and SessionLogger classes will be created and destroyed
in various ways:
void DemoFunction()
{
while(AnotherSession()) {
char name[100];
cout << "Session name: "; cin >> name;
SessionLogger s1;
if(0 == s1.OpenLogFile(name)) {
cout << "Can't continue, no file.";
break;
}
for(;;) {
char dbuff[100];
…
DataItem *dptr = new DataItem(dbuff);
…
delete dptr;
}
}
}
In the example code, a SessionLogger object is, in effect, created in the stack and
subsequently destroyed for each iteration of the while loop. In the enclosed for loop,
DataItem objects are created in the heap, and later explicitly deleted.
Figure 23.3 illustrates the representation of a DataItem (and its associated name) in
the heap, and the effect of the statement delete dptr . As shown, the space occupied
by the primary DataItem structure itself is released; but the space occupied by its name
string remains "in use". Class DataItem has a "memory leak".
Figure 23.4 illustrates another problem with class DataItem , this problem is sharing
due to assignment. The problem would show up in code like the following (assume for
this example that class DataItem has member functions that change the case of all
letters in the associated name string):
heap structure
containing a DataItem fName fName
In use In use
heap structure D E M O D E M O
containing a string 1 1
…
d1.MakeLowerCase();
d2.MakeUpperCase();
d1.PrintOn(cout);
…
d1 In use d1 In use
T h i s T h i s
fName o n e fName o n e
d2 d2
fName In use fName In use
a n o t a n o t
h e r h e r
o n e o n e
However, an operating system normally limits the number of file descriptors that a
program can own. If SessionLogger objects don't close their files, then eventually the
program will run out of file descriptors (its a bit like running out of heap space, but you
can make it happen a lot more easily).
Structure sharing will also occur if a program's code has assignment statements
involving SessionLoggers:
Both SessionLogger objects use the same file. So if one does something like cause a
seek operation (explicitly repositioning the point where the next write operation should
occur), this will affect the other SessionLogger.
814 Intermediate class
Some of the problems just explained can be solved by arranging that objects get the
chance to "tidy up" just before they themselves get destroyed. You could attempt to
achieve this by hand coding. You would define a "TidyUp" function in each class:
You would have to include explicit calls to these TidyUp() functions at all appropriate
points in your code:
while(AnotherSession()) {
…
SessionLogger s1;
…
for(;;) {
…
DataItem *dptr = new DataItem(dbuff);
…
dptr->TidyUp();
delete dptr;
}
s1.TidyUp();
}
That is the problem with "hand coding". It is very easy to miss some point where an
automatic goes out of scope and so forget to include a tidy up routine. Insertion of
these calls is also tiresome, repetitious "mechanical" work.
Tiresome, repetitious "mechanical" work is best done by computer program. The
compiler program can take on the job of putting in calls to "TidyUp" functions. Of
course, if the compiler is to do the work, things like names of functions have to be
standardized.
For each class you can define a "destructor" routine that does this kind of tidying up.
In order to standardize for the compiler, the name of the destructor routine is based on
the class name. For class X, you had constructor functions, e.g. X() , that create
instances, and you can have a destructor function ~X() that does a tidy up before an
object is destroyed. (The character ~, "tilde", is the symbol used for NOT operations on
bit maps and so forth; a destructor is the NOT, or negation, of a constructor.)
Rather than those "TidyUp" functions, class DataItem and class SessionLogger
would both define destructors:
class DataItem {
public:
DataItem(const char *name);
Destructors 815
~DataItem();
…
};
class SessionLogger {
public:
SessionLogger();
~SessionLogger() { this->fLfile.close(); }
…
};
Just as the compiler put in the implicit calls to constructor functions, so it puts in the
calls to destructors.
You can have a class with several constructors because there may be different kinds
of data that can be used to initialize a class. There can only be one destructor; it takes
no arguments. Like constructors, a destructor has no return type.
Destructors can exacerbate problems related to structure sharing. As we now have a
destructor for class DataItem, an individual DataItem object will dutifully delete its
name when it gets destroyed. If assignment has lead to structure sharing, there will be a
second DataItem around whose name has suddenly ceased to exist.
You don't have to define destructors for all your classes. Destructors are needed for
classes that are themselves resource managers, or classes that are used as "base classes"
in some class hierarchy (see section 23.6).
Several of the collection classes in Chapter 21 were resource managers and they
should have had destructors.
Class DynamicArray would be easy, it owns only a single separately allocated array,
so all that its destructor need do is get rid of this:
class DynamicArray {
public:
DynamicArray(int size = 10, int inc = 5);
~DynamicArray();
private:
…
void **fItems;
};
Note that the destructor does not delete the data items stored in the array. This is a
design decision for all these collection classes. The collection does not own the stored
items, it merely looks after them for a while. There could be other pointers to stored
816 Intermediate class
items elsewhere in the program. You can have collection classes that do own the items
that are stored or that make copies of the original data and store these copies. In such
cases, the destructor for the collection class should run through the collection deleting
each individual stored item.
Destructors for class List and class BinaryTree are a bit more complex because
instances of these classes "own" many listcells and treenodes respectively. All these
auxiliary structures have to be deleted (though, as already explained, the actual stored
data items are not to be deleted). The destructor for these collection class will have to
run through the entire linked network getting rid of the individual listcells or treenodes.
A destructor for class List is as follows:
List::~List()
{
ListCell *ptr;
ListCell *temp;
ptr = fHead;
while(ptr != NULL) {
temp = ptr;
ptr = ptr->fNext;
delete temp;
}
}
The destructor for class BinaryTree is most easily implemented using a private
auxiliary recursive function:
BinaryTree::~BinaryTree()
{
Destroy(fRoot);
}
void BinaryTree::Destroy(TreeNode* t)
{
if(t == NULL)
return;
Destroy(t->LeftLink());
Destroy(t->RightLink());
delete t;
}
The recursive Destroy() function chases down branches of the tree structure. At each
TreeNode reached, Destroy() arranges to get rid of all the TreeNodes in the left
subtree, then all the TreeNodes in the right subtree, finally disposing of the current
TreeNode. (This is an example of a "post order" traversal; it processes the current node
of the tree after, "post", processing both subtrees.)
Assignment operator 817
There are two places where structures or class instances are, by default, copied using a
byte by byte copy. These are assignments:
void test()
{
DataItem anItem("Hello world");
…
foo(anItem);
…
}
This second case is an example of using a "copy constructor". Copy constructors are
used to build a new class instance, just like an existing class instance. They do turn up
in other places, but the most frequent place is in situations like the call to the function
requiring a value argument.
As illustrated in section 23.5.1, the trouble with the default "copy the bytes"
implementations for the assignment operator and for a copy constructor is that they
usually lead to undesired structure sharing.
If you want to avoid structure sharing, you have to provide the compiler with
specifications for alternative ways of handling assignment and copy construction. Thus,
for DataItem, we would need a copy constructor that made a copy of the character
string fName:
Though similar, assignments are a little more complex. The basic form of an Assignment operator
operator= function for the example class DataItem would be:
delete [] fName;
fName = new char[strlen(other.fName) + 1];
strcpy(fName, other.fName);
…
}
gets rid of the existing character array owned by the DataItem; this plugs the memory
leak that would otherwise occur. The next two statements duplicate the content of the
other DataItem's fName character array.
If you want to allow assignments at all, then for consistency with the rest of C++
you had better allow concatenated assignments:
DataItem d1("XXX");
DataItem d2("YYY");
DataItem d3("ZZZ";
…
d3 = d2 = d1;
There is a small problem. Essentially, the code says "get rid of the owned array,
duplicate the other's owned array". Suppose somehow you tried to assign the value of
a DataItem to itself; the array that has then to be duplicated is the one just deleted.
Such code will usually work, but only because the deleted array remains as a "ghost" in
the heap. Sooner or later the code would crash; the memory manager will have
rearranged memory in some way in response to the delete operation.
You might guess that "self assignments" are rare. Certainly, those like:
DataItem d1("xyz");
…
d1 = d1;
Assignment operator 819
are rare (and good compilers will eliminate statements like d1 = d1). However, self
assignments do occur when you are working with data referenced by pointers. For
example, you might have:
DataItem *d_ptr1;
DataItem *d_ptr2;
…
// Copy DataItem referenced by d_ptr1 into the DataItem
// referenced by pointer d_ptr2
*dptr2 = *dptr1;
It is of course possible that dptr1 and dptr2 are pointing to the same DataItem.
You have to take precautions to avoid problems with self assignments. The
following arrangement (usually) works:
It checks the addresses of the two DataItems. One address is held in the (implicit)
pointer argument this, the second address is obtained by applying the & address of
operator to other . If the addresses are equal it is a self assignment so don't do
anything.
Of course, sometimes it is just meaningless to allow assignment and copy Preventing copying
constructors. You really wouldn't want two SessionLoggers working with the same
file (and they can't really have two files because their files have to have the same
name). In situations like this, what you really want to do is to prevent assignments and
other copying. You can achieve this by declaring a private copy constructor and a
private operator= function;
class SessionLogger {
public:
SessionLogger();
~SessionLogger();
…
private:
// No assignment, no copying!
void operator=(const SessionLogger& other);
SessionLogger(const SessionLogger& other);
…
};
820 Intermediate class
You shouldn't provide an implementation for these functions. Declaring these functions
as private means that such functions can't occur in client code. Code like
SessionLogger s1, s2; …; s2 = s1; will result in an error message like "Cannot
access SessionLogger::_assign() here". Obviously, such operations won't occur in
the member functions of the class itself because the author of the class knows that
assignment and copying are illegal. The return type of the operator= function does
not matter in this context, so it is simplest to declare it as void.
Assignment and copy construction should be disabled for collection classes like
those from Chapter 24, e.g.:
class BinaryTree {
public:
…
private:
void operator=(const BinaryTree& other);
BinaryTree(const BinaryTree& other);
…
};
23.6 INHERITANCE
Most of the programs that you will write in future will be "object based". You will
analyze a problem, identify "objects" that will be present at run-time in your program,
and determine the "classes" to which these objects belong. Then you will design the
various independent classes needed, implement them, and write a program that creates
instances of these classes and allows them to interact.
Independent classes? That isn't always the case.
In some circumstances, in the analysis phase or in the early stages of the design
phase you will identify similarities among the prototype classes that you have proposed
for your program. Often, exploitation of such similarities leads to an improved design,
and sometimes can lead to significant savings in implementation effort.
You have used "Draw" programs so you know the kind of interface that such a
program would have. There would be a "palette of tools" that a user could use to add
components. The components would include text (paragraphs describing the circuit),
and circuit elements like the batteries and light bulbs. The editor part would allow the
user to select a component, move it onto the main work area and then, by doubly
clicking the mouse button, open a dialog window that would allow editing of text and
setting parameters such as a resistance in ohms. Obviously, the program would have to
let the user save a partially designed circuit to a file from where it could be restored
later.
What objects might the program contain?
The objects are all pretty obvious (at least they are obvious once you've been playing
this game long enough). The following are among the more important:
• A "document" object that would own all the data, keep track of the components Objects needed
added and organize transfers to and from disk.
• Various collections, either "lists" or "dynamic arrays" used to store items. Lets call
them "lists" (although, for efficiency reasons, a real implementation would
probably use dynamic arrays). These lists would be owned by the "document".
There might be a list of "text paragraphs" (text describing the circuit), a "list of
wires", a "list of resistors" and so forth.
• A "palette object". This would respond to mouse-button clicks by giving the
document another battery, wire, resistor or whatever to add to the appropriate list.
• A "window" or "view" object used when displaying the circuit.
• Some "dialog" objects" used for input of parameters.
• Lots of "wire" objects.
• Several "resistor objects".
• A few "switch" objects".
• A few "lamp bulb" objects".
and for a circuit that actually does something
• At least one battery object.
For each, you would need to characterize the class and work out a list of data owned
and functions performed.
During a preliminary design process your group would be right to come up with
classes Battery, Document, Palette, Resistor, Switch. Each group member could work
on refining one or two classes leading to an initial set of descriptions like the following:
• class Battery
Owns:
Position in view, resistance (internal resistance), electromotive force,
possibly a text string for some label/name, unique identifier, identifiers
of connecting wires…
Does:
GetVoltStuff() – uses a dialog to get voltage, internal resistance etc.
TrackMouse() – respond to middle mouse button by following mouse to
reposition within view;
DrawBat() - draws itself in view;
AddWire() – add a connecting wire;
Area() – returns rectangle occupied by battery in display view;
…
Put() and Get() – transfers parameters to/from file.
• class Resistor
Owns:
Position in view, resistance, possibly a text string for some label/name,
unique identifier, identifiers of connecting wires…
Does:
GetResistance() – uses a dialog to get resistance, label etc.
Move() – respond to middle mouse button by following mouse to
reposition within view;
Display() - draws itself in view;
Place() – returns area when resistor gets drawn;
…
ReadFrom() and WriteTo() – transfers parameters to/from file.
You should be able to sketch out pseudo code for some of the main operations. For
example, the document's function to save data to a file might be something like the
following:
write BatteriesList.Length()
iterator i2(BatteriesList)
for i2.First, !i2.IsDone() do
battery_ptr = i2.CurrentItem()
Discovering similarities 823
battery_ptr->Put()
The function to display all the data of the document would be rather similar:
Document::Draw
iterator i1(paragraphList)
for i1.First(), !i1.IsDone() do
paragraph_ptr = i1.CurrentItem();
paragraph_ptr->DisplayText()
i1.Next();
iterator i2(BatteriesList)
for i2.First, !i2.IsDone() do
battery_ptr = i2.CurrentItem()
battery_ptr->DrawBat()
Another function of "Document" would sort out which data element was being picked
when the user wanted to move something using the mouse pointer:
Document::LetUserMoveSomething(Point mousePoint)
iterator i1(paragraphList)
Paragraph *pp = NULL;
for i1.First(), !i1.IsDone() do
paragraph_ptr = i1.CurrentItem();
Rectangle r = paragraph_ptr->Rect()
if(r.Contains(mousePoint) pp = paragraph_ptr;
i1.Next();
if(pp != NULL)
pp->FollowMouse()
return
iterator i2(BatteriesList)
battery *pb
for i2.First, !i2.IsDone() do
battery_ptr = i2.CurrentItem()
Rectangle r = battery_ptr ->Area()
if(r.Contains(mousePoint) pb = battery_ptr ;
i2.Next();
if(pb != NULL)
pb->TrackMouse()
return
…
824 Intermediate class
Design problems? By now you should have the feeling that there is something amiss. The design with
its "batteries", "wires", "text paragraphs" seems sensible. But the code is coming out
curiously clumsy and unattractive in its inconsistencies.
Batteries, switches, wires, and text paragraphs may be wildly different kinds of
things, but from the perspective of "document" they actually have some similarities.
They are all "things" that perform similar tasks. A document can ask a "thing" to:
Similarities among Some "things" are more similar than others. Batteries, switches, and resistors will
classes all have specific roles to play in the circuit simulation, and there will be many
similarities in their roles. Wires are also considered in the circuit simulation, but their
role is quite different, they just connect active components. Text paragraphs don't get
involved in the circuit simulation part. So all of them are "storable, drawable, editable"
things, some are "circuit things", and some are "circuit things that have resistances".
A class hierarchy You can represent such relationships among classes graphically, as illustrated in
Figure 23.5. As shown there, there is a kind of hierarchy.
An pure "abstract" Class Thing captures just the concept of some kind of data element that can draw
class itself, save itself to file and so forth. There are no data elements defined for Thing, it is
purely conceptual, purely abstract.
Concrete class A TextParagraph is a particular kind of Thing. A TextParagraph does own data, it
TextParagraph owns its text, its position and so forth. You can also define actual code specifying
exactly how a TextParagraph might carry out specific tasks like saving itself to file.
Whereas class Thing is purely conceptual, a TextParagraph is something pretty real,
pretty "concrete". You can "see" a TextParagraph as an actual data structure in a
running program.
Partially abstract In contrast, a CircuitThing is somewhat abstract. You can define some properties of
class CircuitThing a CircuitThing. All circuit elements seem to need unique identifiers, they need
coordinate data defining their position, and they need a character string for a name or a
label. You can even define some of the code associated with CircuitThings – for
instance, you could define functions that access coordinate data.
Concrete class Wire Wires are special kinds of CircuitThings. It is easy to define them completely. They
have a few more data fields (e.g. identifiers of the components that they join, or maybe
coordinates for their endpoints). It is also easy to define completely how they perform
all the functions like saving their data to file or drawing themselves.
Partially abstract Components are a different specialization of CircuitThing. Components are
class Component CircuitThings that will have to be analyzed by the circuit simulation component of the
program. So they will have data attributes like "resistance", and they may have many
additional forms of behaviour as required in the simulation.
Discovering similarities 825
Thing
TextParagraph
CircuitThing
Wire Component
Naturally, Battery, Switch, and Resistor define different specializations of this idea Concrete classes
of Component. Each will have its unique additional data attributes. Each can define a Battery, Switch, …
real implementation for functions like Draw().
OK, such a hierarchy provides a nice conceptual structure when talking about a
program but how does it really help?
One thing that you immediately gain is consistency. In the original design sketch, Consistency
text paragraphs, batteries and so forth all had some way of defining that these data
elements could display themselves, save themselves to file and so forth. But each class
was slightly different; thus we had TextParagraph::Save(), Battery::Put() and
Resistor:: WriteTo() . The hierarchy allows us to capture the concept of
"storability" by specifying in class Thing the ability WriteTo() . While each
826 Intermediate class
Document::Draw
iterator i1(thingList)
for i1.First(), !i1.IsDone() do
thing_ptr = i1.CurrentItem();
thing_ptr->Draw()
i1.Next();
Document::LetUserMoveSomething(Point mousePoint)
iterator i1(thingList)
Thing *pt = NULL;
for i1.First(), !i1.IsDone() do
thing_ptr = i1.CurrentItem();
Rectangle r = thing_ptr ->Area()
if(r.Contains(mousePoint) pt = thing_ptr ;
i1.Next();
if(pt != NULL)
pt->TrackMouse()
return
The code is no longer obscured by all the different special cases. The revised code is
shorter and much more intelligible.
Extendability Note also how the revised Document no longer needs to know about the different
kinds of circuit component. This would prove useful later if you decided to have
another component (e.g. class Voltmeter); you wouldn't need to change the code of
Document in order to accommodate this extension.
Code sharing The most significant benefit is the resulting simplification of design, and
simultaneous acquisition of extendability. But you may gain more. Sometimes, you
can define the code for a particular behaviour at the level of a partially abstract class.
Thus, you should be able to define the access function for getting a CircuitThing's
Discovering similarities 827
identifier at the level of class CircuitThing while class Component can define the code
for accessing a Component's electrical resistance. Defining these functions at the level
of the partially abstract classes saves you from writing very similar functions for each
of the concrete classes like Battery, Resistor, etc.
C++ allows you to define such hierarchical relations amongst classes. So, there is a
way of specifying "class Thing represents the abstract concept of a storable, drawable,
moveable data element", "class TextParagraph is a kind of Thing that looks after text
and …".
You start by defining the "base class", in this case that is class Thing which is the Base class
base class for the entire hierarchy:
class Thing {
public:
virtual ~Thing() { }
/* Disk I/O */
virtual void ReadFrom(istream& i s) = 0;
virtual void WriteTo(ostream& os) const = 0;
/* Graphics */
virtual void Draw() const = 0;
/* mouse interactions */
virtual void DoDialog() = 0; // For double click
virtual void TrackMouse() = 0; // Mouse select and drag
virtual Rect Area() const = 0;
…
};
Class Thing represents just an idea of a storable, drawable data element and so naturally
i t is simply a list of function names.
The situation is a little odd. We know that all Things can draw themselves, but we
can't say how. The ability to draw is common, but the mechanism depends very much
on the specialized nature of the Thing that is asked to draw itself. In class Thing, we
have to be able to say "all Things respond to a Draw() request, specialized Thing
subclasses define how they do this".
This is what the keyword virtual and the odd = 0 notation are for. virtual keyword and
Roughly, the keyword virtual identifies a function that a class wants to define in =0 definition
such a way that subclasses may later extend or otherwise modify the definition. The =0
part means that we aren't prepared to offer even a default implementation. (Such
undefined virtual functions are called "pure virtual functions".)
In the case of class Thing , we can't provide default definitions for any of the
functions like Draw() , WriteTo() and so forth. The implementations of these
functions vary too much between different subclasses. This represents an extreme case;
828 Intermediate class
often you can provide a default implementation for a virtual function. This default
definition describes what "usually" should be done. Subclasses that need to something
different can replace, or "override", the default definition.
virtual destructor The destructor, ~Thing(), does have a definition: virtual ~Thing() { }. The
definition is an empty function; basically, it says that by default there is no tidying up to
be done when a Thing is deleted. The destructor is virtual. Subclasses of class
Thing may be resource managers (e.g. a subclass might allocate space for an object
label as a separate character array in the heap). Such specialized Things will need
destructors that do some cleaning up.
Thing* variables A C++ compiler prevents you from having variables of type Thing:
This is of course appropriate. You can't have Things. You can only have instances of
specialized subclasses. (This is standard whenever you have a classification hierarchy
with abstract classes. After all, you never see "mammals" walking around, instead you
encounter dogs, cats, humans, and horses – i.e. instances of specialized subclasses of
class mammal). However, you can have variables that are Thing* pointers, and you
can define functions that take Thing& reference arguments:
Thing *first_thing;
The pointer first_thing can hold the address of (i.e. point to) an instance of class
TextParagraph, or it might point to a Wire object, or point to a Battery object.
Derived classes Once you have declared class Thing, you can declare classes that are "based on" or
"derived from" this class:
};
};
In later studies you will learn that there are a variety of different ways that Different forms of
"derivation" can be used to build up class hierarchies. Initially, only one form is derivation
important. The important form is "public derivation". Both TextParagraph and
CircuitThing are "publicly derived" from class Thing:
Public derivation acknowledges that both TextParagraph and CircuitThing are public derivation
specialized kinds of T h i n g s and so code "using T h i n g s " will work with
TextParagraphs or CircuitThings. This is exactly what we want for the example
where the Document object has a list of "pointers to Things" and all its code is of the
form thing_ptr->DoSomething().
We need actual TextParagraph objects. This class has to be "concrete". The class TextParagraph, a
declaration has to be complete, and all the member functions will have to be defined. concrete class
830 Intermediate class
Naturally, the class declaration starts with the constructor(s) and destructor. Then it
will have to repeat the declarations from class Thing; so we again get functions like
Draw() being declared. This time they don't have those = 0 definitions. There will
have to be definitions provided for each of the functions. (It is not actually necessary to
repeat the keyword virtual; this keyword need only appear in the class that introduces
the member function. However, it is usually simplest just to "copy and paste" the block
of function declarations and so have the keyword.) Class TextParagraph will
introduce some additional member functions describing those behaviours that are
unique to TextParagraphs. Some of these additional functions will be in the public
interface; most would be private. Class TextParagraph would also declare all the
private data members needed to record the data possessed by a TextParagraph object.
CircuitThing, a Class CircuitThing is an in between case. It is not a pure abstraction like Thing,
partially implemented nor yet is it a concrete class like TextParagraph. Its main role is to introduce those
abstract class
member functions needed to specify the behaviours of all different kinds of
CircuitThing and to describe those data members that are possessed by all kinds of
CircuitThing.
Class CircuitThing cannot provide definitions for all of those pure virtual
functions inherited from class Thing; for instance it can't do much about Draw(). It
should not repeat the declarations of those functions for which it can't give a definition.
Virtual functions only get re-declared in those subclasses where they are finally defined.
Class CircuitThing can specify some of the processing that must be done when a
CircuitThing gets written to or read from a file on disk. Obviously, it cannot specify
everything; each specialized subclass has its own data to save. But CircuitThing can
define how to deal with the common data like the identifier, location and label:
These member functions can be used by the more elaborate WriteTo() and
ReadFrom() functions that will get defined in subclasses. (Note the deletion of fLabel
Defining class hierarchies 831
and allocation of a new array; this is another of those places where it is easy to get a
memory leak.)
The example illustrates that there are three possibilities for additional member
functions:
protected. A protected member is not accessible from the main program code but
can be accessed in the member functions of the class declaring that member, or in the
member functions of any derived subclass.
Here, variables like fLocation should be defined as protected. Subclasses can
then use the fLocation data in their Draw() and other functions. (Actually, it is
sometimes better to keep the data members private and provide extra protected access
functions that allow subclasses to get and set the values of these data members. This
technique can help when debugging complex programs involving elaborate class
hierarchies).
Once the definition of class CircuitThing is complete, you have to continue with
its derived classes: class Wire, and class Component:
Class Wire is meant to be a concrete class; the program will use instances of this class.
So it has to define all member functions.
The class repeats the declarations for all those virtual functions, declared in
classes from which it is derived, for which it wants to provide definitions (or to change
existing definitions). Thus class Wire will declare the functions like Draw() and
Current(). Class Wire also declares the ReadFrom() and WriteTo() functions as
these have to be redefined to accommodate additional data, and it declares Area() as it
wants to use a different size.
Class Wire would also define additional member functions characterising its unique
behaviours and would add some data members. The extra data members might be
declared as private or protected. You would declare them as private if you knew
that no-one was ever going to try to invent subclasses based on your class Wire. If you
wanted to allow for the possibility of specialized kinds of Wire, you would make these
Defining class hierarchies 833
extra data members (and functions) protected. You would then also have to define the
destructor as virtual.
The specification of the problem might disallow the user from dragging a wire or
clicking on a wire to open a dialog box. This would be easily dealt with by making the
Area() function of a Wire return a zero sized rectangle (rather than the fixed 16x16
rectangle used by other CircuitThings):
(The program identifies the Thing being selected by testing whether the mouse was
located in the Thing's area; so if a Thing's area is zero, it can never be selected.) This
definition of Area() overrides that provided by CircuitThing.
A Wire has to save all the standard CircuitThing data to file, and then save its
extra data. This can be done by having a Wire::WriteTo() function that makes use of
the inherited function:
This provides another illustration of how inheritance structures may lead to small
savings of code. All the specialized subclasses of CircuitThing use its code to save
the identifier, label, and location.
The example hierarchy illustrates that you can define a concept like Thing that can save
itself to disk, and you can define many different specific classes derived from Thing
that have well defined implementations – TextParagraph::WriteTo(), Battery::
WriteTo(), Wire::WriteTo(). But the code for Document would be something like:
iterator i1(thingList);
i1.First();
while(!i1.IsDone()) {
Thing* thing_ptr = (Thing*) i1.CurrentItem();
834 Intermediate class
thing_ptr ->WriteTo(out);
i1.Next();
}
}
thing_ptr ->WriteTo()
isn't supposed to invoke function Thing::WriteTo(). After all, this function doesn't
exist (it was defined as = 0). Instead the code is supposed to invoke the appropriate
specialized version of WriteTo().
But which is the appropriate function? That is going to depend on the contents of
thingList. The thingList will contain pointers to instances of class TextParagraph,
class Battery, class Switch and so forth. These will be all mixed together in whatever
order the user happened to have added them to the Document. So the appropriate
function might be Battery::WriteTo() for the first T h i n g in the list,
Resistor::WriteTo() for the second list element, and Wire::WriteTo() for the
third. You can't know until you are writing the list at run-time.
The compiler can't work things out at compile time and generate the instruction
sequence for a normal subroutine call. Instead, it has to generate code that works out
the correct routine to use at run time.
virtual tables The generated code makes use of tables that contain the addresses of functions.
There is a table for each class that uses virtual functions; a class's table contains the
addresses of its (virtual) member functions. The table for class Wire would, for
example, contain pointers to the locations in the code segment of each of the functions
Wire::ReadFrom(), Wire::WriteTo(), Wire::Draw() and so forth. Similarly, the
virtual table for class B a t t e r y will have the addresses of the functions
Battery::ReadFrom() and so on. (These tables are known as "virtual tables".)
In addition to its declared data members, an object that is an instance of a class that
uses virtual functions will have an extra pointer data member. This pointer data
member holds the address of the virtual table that has the addresses of the functions that
are to be used in association with that object. Thus every Wire object has a pointer to
the Wire virtual table, and every Battery object has a pointer to the Battery virtual
table. A simple version of the scheme is illustrated in Figure 23.6
The instruction sequence generated for something like:
thing_ptr ->WriteTo()
involves first using the link from the object pointed to by thing_ptr to get the location
of the table describing the functions. Then, the required function, WriteTo() , is
"looked up" in this table to find where it is in memory. Finally, a subroutine call is
made to the actual WriteTo() function. Although it may sound complex, the process
requires only three or four instructions!
How inheritance works: dynamic binding 835
Heap
Function lookup at run time is referred to as "dynamic binding". The address of the Dynamic binding
function that is to be called is determined ("bound") while the program is running
(hence "dynamically"). Normal function calls just use the machine's JSR (jump to
subroutine) instruction with the function's address filled in by the compiler or linking
loader. Since this is done before the program is running, the normal mechanism of
fixing addresses for subroutine calls is said to use static binding (the address is fixed,
bound, before the program is moving, or while it is static).
It is this "dynamic binding" that makes possible the simplification of program
design. Things like Document don't have to have code to handle each special case.
Instead the code for Document is general, but the effect achieved is to invoke different
special case functions as required.
Another term that you will find used in relation to these programming styles is Polymorphism
"polymorphism". This is just an anglicisation of two Greek words – poly meaning
many, and morph meaning shape. A Document owns a list of Things; Things have
many different shapes – some are text paragraphs, others are wires. A pointer like
thing_ptr is a "polymorphic" pointer in that the thing it points to may, at different
times, have different shapes.
836 Intermediate class
You are not limited to single inheritance. A class can be derived from a number of
existing base classes.
Multiple inheritance introduces all sorts of complexities. Most uses of multiple
inheritance are inappropriate for beginners. There is only one form usage that you
should even consider.
Multiple inheritance can be used as a "type composition" device. This is just a
systematic generalization of the previous example where we had class Thing that
represented the type "a drawable, storable, editable data item occupying an area of a
window".
Instead of having class Thing as a base class with all these properties, we could
instead factor them into separate classes:
class Storable {
public:
virtual ~Storable() { }
virtual void WriteTo(ostream&) const = 0;
virtual void ReadFrom(istream&) const = 0;
…
};
void Drawable {
public:
virtual ~Drawable() { }
virtual void Draw() const = 0;
virtual Rect Area() const = 0;
…
};
This allows "mix and match". Different specialized subclasses can derive from chosen
base classes. As a TextParagraph is to be both storable and drawable, it can inherit
from both base classes:
You might have another class, Decoration , that provides some pretty outline or
shadow effect for a drawable item. You don't want to store Decoration objects in a
file, they only get used while the program is running. So, the Decoration class only
inherits from Drawable:
class Printable {
public:
virtual ~Printable() { }
virtual void PrintOn(ostream& out) const = 0;
};
class Comparable {
public:
virtual ~Comparable() { }
virtual int Compare(const Comparable* ptr) const = 0;
int Compare(const Comparable& other) const
{ return Compare(&other); }
Class Printable packages the idea of a class with a PrintOn() function and
associated global operator<<() functions. Class Comparable characterizes data items
that compare themselves with similar data items. It declares a Compare() function that
is a little like strcmp(); it should return -1 if the first item is smaller than the second,
zero if they are equal, and 1 if the first is greater. The class also defines a set of
operator functions, like the "not equals function" operator !=() and the "greater than"
function operator>(); all involve calls to the pure virtual Compare() function with
suitable tests on the result code. (The next chapter has some example Compare()
functions.)
As noted earlier, another possible pure virtual base class would be class Iterator:
class Iterator {
public:
virtual ~Iterator() { }
virtual void First(void) = 0;
838 Intermediate class
This would allow the creation of a hierarchy of iterator classes for different kinds of
data collection. Each would inherit from class Iterator.
Now inventing classes like Storable, Comparable, and Drawable is not a task for
beginners. You need lots of experience before you can identify widely useful abstract
concepts like the concept of storability. However you may get to work with library
code that has such general abstractions defined and so you may want to define classes
using multiple inheritance to combine different data types.
What do you gain from such use of inheritance as a type composition device?
Obviously, it doesn't save you any coding effort. The abstract classes from which
you multiply inherit are exactly that – abstract. They have no data members. All, or
most, of their member functions are pure virtual functions with no definitions. If any
member functions are defined, then as in the case of class Comparable, these definitions
simply provide alternative interfaces to one of the pure virtual functions.
You inherit, but the inheritance is empty. You have to define the code.
The advantage is not for the implementor of a subclass. Those who benefit are the
maintenance programmers and the designers of the overall system. They gain because
if a project uses such abstract classes, the code becomes more consistent, and easier to
understand. The maintenance programmer knows that any class whose instances are to
be stored to file will use the standard functions ReadFrom() and WriteTo(). The
designer may be able to simplify the design by using collections of different kinds of
objects as was done with the Document example.
There are many further complexities related to inheritance structures. One day you may
learn of things like "private inheritance", "virtual base classes", "dominance" and
others. You will discover what happens if a subclass tries to "override" a function that
was not declared as virtual in the class that initially declared it.
But these are all advanced, difficult features.
The important uses of inheritance are those illustrated – capturing commonalities to
simplify design, and using (multiple) inheritance as a type composition device. These
uses will be illustrated in later examples. Most of Part V of this text is devoted to
simple uses of inheritance.
24
24 Two more "trees"
Computer science students often have great difficulty in explaining to their parents why
they are spending so much time studying "trees" and "strings". But I must impose upon
you again. There are a couple more trees that you need to study. They are both just
more elaborate versions of the binary tree lookup structure illustrated in Section 21.5.
The first, the "AVL" tree, is an "improved" binary tree. The code for AVL deals AVL tree
with some problems that can occur with binary trees which reduce the performance of
the lookup structure. AVL trees are used for the same purpose as binary trees; they
hold collections of keyed data in main memory and provide facilities for adding data,
searching for data associated with a given key, and removing data.
The second, the "BTree" tree, is intended for data collections that are too large to fit BTree
in main memory. You still have data records with a "key" field and other information;
it is just that you may have hundreds of thousands of them. A BTree provides a means
whereby most of the data are kept in disk files, but a fast search is still practical. The
BTree illustrated is only slightly simplified; it is pretty close to the structures that are
used to implement lookup systems for many large databases.
These two examples make minor use of "inheritance" as presented in Section 23.6.
The AVL tree is to store data items that are instances of some concrete class derived
from class KeyedItem:
class KeyedItem {
public:
virtual ~KeyedItem() { }
virtual long Key(void) const = 0;
virtual void PrintOn(ostream& out) const { }
};
A KeyedItem is just some kind of data item that has a unique long integer key
associated with it (it can also print itself if asked).
The BTree stores instances of a concrete class derived from class KeyedStorable-
Item:
840 Two more trees
class KeyedStorableItem {
public:
virtual ~KeyedStorableItem() { }
virtual long Key(void) const = 0;
virtual void PrintOn(ostream& out) const { }
virtual long DiskSize(void) const = 0;
virtual void ReadFrom(fstream& in) = 0;
virtual void WriteTo(fstream& out) const = 0;
};
These data items must be capable of transferring themselves to/from disk files using
binary transfers (read() and write() calls). On disk, the data items must all use the
same amount of space. In addition to any other data that they possess, these items must
have a unique long integer key value (disallowing duplicate keys simplifies the code).
Take a look at a binary tree after a few "random" insertions. The following tree resulted
when keyed data items were inserted with the keys in the following order: 50, 60, 65,
80, 70, 45, 75, 90, 105, 100:
50
|
+------+------+
45 60
|
+-----+-----+
65
|
+-----+-----+
80
|
+-------+-------+
70 90
| |
+-----+-----+ +-----+-----+
75 105
|
+-----+-----+
100
The tree is a little out of balance. Most binary trees that grow inside programs tend to
be imbalanced.
This imbalance does matter. A binary search tree is supposed to provide faster
lookup of a keyed data item than does an alternative structure like a list. It is supposed
What is wrong with binary trees? 841
to give O(lgN) performance for searches, insertions, and deletions. But when a tree gets
out of balance, performance decreases.
Fast lookup of keyed data items is a very common requirement. So the problems of
simple binary search trees become important.
You can change the code so that the tree gets rearranged after every insertion and
deletion. Figure 24.1 illustrates a couple of rearrangements that could be used to keep
the tree balanced as those data items were inserted.
root root
50 60
60 50 65
65
root root
60 65
50 65 60 80
80 50 70
70
Such rearrangements are possible. But a tree may take quite a lot of rearranging to
get it balanced after some data item gets inserted. In some cases, you may have to alter
almost all of the left- and right- subtree pointers. This makes the cost of rearrangement
directly proportional to the number of items in the tree, i.e. O(N) performance.
There is no point in trying to get a perfectly balanced tree if balancing cost are O(N).
Although rebalancing does keep search costs at O(lgN) you are interested in the overall
costs, and thus O(N) costs for rebalancing after insertions and deletions count against
the O(lg(N)) searches.
However, it has been shown that a tree can be kept "more or less balanced". These
more or less balanced trees have search costs that are O(lgN) and the cost of
842 Two more trees
rebalancing the tree until it is "more or less balanced" is also O(lg(N)). (The analyses
of the algorithms to demonstrate these costs is far too difficult for this introductory
treatment).
There are many different schemes for keeping a binary search tree "more or less
balanced". The best known was invented by a couple of Russians (Adelson-Velskii and
Landis) back around 1960. They defined rules to characterize a "more or less balanced
tree" and worked out the rearrangements that would be necessary if an insertion
operation or a deletion operation destroyed the existing balance. The rearrangements
are localized to the "vine" that leads from the root to the point where the change
(insertion/ deletion) has just occurred and it is this that keeps the cost of rearrangements
down to O(lg(N)). A tree that satisfies their rules is called an AVL tree.
AVL tree The following definitions together characterize an AVL tree:
• AVL property:
A node in a binary tree has the "AVL property" if the heights of the left and right
subtrees are either equal or differ by 1.
• AVL tree:
An AVL tree is a binary tree in which every node has the AVL property.
Figure 24.2 illustrates some trees with examples of both AVL and non-AVL trees.
You can check a tree by starting at the leaf nodes. The "left and right subtrees" of a
leaf node don't exist, or from a different perspective they are both size zero. Since they
are both size zero they are equal so a leaf node has evenly balanced subtrees.
You climb up from a leaf node to its parent node and check its "left and right
subtrees". If the node has two subtrees both with just leaves, then it is even. If it has
only one leaf below it, it is either "left long" or "right long".
As you climb further up toward the root, you would need to keep a count of the
longest path down through a link to its furthest leaf. As you reached each node, you
would have to compare these longest paths down both the left and right subtrees from
that node. If the longest paths are equal or differ by at most one, you can label the
node as "even", or "left long", or "right long". If the lengths of the paths differ by more
than one, as is the case with the some of the nodes in the second pair of trees shown in
Figure 24.2, then you have found a situation that violates the AVL requirements.
Checking the "AVL-ness" of an arbitrary tree might involve quite a lot of work. But
if you are building up the tree, you can keep track of its state by just having an indicator
on each node that says whether it is currently "even", "left long", or "right long". This
information is sufficient to let you work out what rearrangements might be needed to
maintain balance after a change like the addition or deletion of a node.
Keeping your balance 843
E = even
L = Left subtree larger
R = Right subtree larger
L
R
L R
E L
E E R R E L
R E
AVL E
E E R E
L
L E
?
E R R R
not AVL
E R E
E
Figure 24.3 illustrates some of the possible situations that you might encounter when
adding an extra node below an existing node. The numbers shown on the nodes
844 Two more trees
represent the keys for the data items associated with the tree node. Of course it is still
essential to have the binary search tree property: data items whose keys are less than the
key on a given node will be located in its left subtree, those whose keys are greater will
be in its right subtree. (The key values will also be used as "names" for the nodes in
subsequent discussions.)
The addition of a node can change the balance at every point on the path that leads
from the root to the parent node where the new node gets attached. The first example
shown in Figure 24.3 starts with all the existing nodes "even". The new data value must
go to the left of the node 27; it was "even" but is going to become "left long". The
value 6 has to go to the left of 19; so 19 which was "even" also becomes "left long".
The second example shown, the addition of 21, shows that additions sometimes
restore local balance. Node 19 that was "left long" now gets back to "even". Node 27
is still "left long".
The next two examples shown in Figure 24.3 illustrate additions below node 6 that
make nodes 27 and 19 both "left too long". They have lost their AVL properties. Some
rearrangements are going to be performed to keep the tree "more or less balanced".
The final example shows another case where the tree will require rebalancing.
Although this case does not need any changes in the immediate vicinity of the place
where the new node gets added, changes are necessary higher up along the path to the
root.
Rearrangements to Adelson-Velskii and Landis explored all the possible situations that could arise
the tree when additions were made to a tree. Then, they worked out what local rearrangements
could be made to restore "more or less balance" (i.e. the AVL property) in the
immediate vicinity of an out of balance node.
The tree has to be reorganized whenever a node becomes "left too long" or "right too
long". Obviously, there is a symmetry between the two cases and it is only necessary to
consider one; we will examine the situation where a node is "left too long".
As illustrated in Figure 24.4, there are two variations. In one, the left subtree of the
"left too long" node is itself "left long"; in the second variation, the node at the start of
the left subtree is actually "right long". Adelson-Velskii and Landis sorted out the
slightly different rearrangements of the tree structure that would be needed in these two
cases. Their proposed local rearrangements are also shown in Figure 24.4.
Local The problem that has got to be resolved by these rearrangements is that the left
rearrangements to fix branch of the tree below the current root (node 27) now has a height that is two greater
up an imbalanced
node than the right branch. The left branch must shrink, the right branch must grow. Since
the tree must be kept ordered, the only way the right branch can grow and the left shrink
is to push the current root node down into the right branch, replacing it at the root by an
appropriate node from the left branch.
Keeping your balance 845
6
27 27
21 Node 27 still
"left long",
19 31 19 31
19 again
"even"
6 6 21
27 27
2 Nodes 27 and
31 19 both "left
19 31 19
too long"!
Tree must
6 6 be rearranged.
2
27 27
11 Nodes 27 and
31 19 both "left
19 31 19
too long"!
Tree must
6 6 be rearranged.
11
27 27
11 No problems
31 for 6 or 19,
19 31 19
but 27 is
"left too
6 21 6 21 long". Tree
must be
11 rearranged.
27 27 19
19 31 19 31 6 27
6 21 6 21 2 21 31
27 27 21
19 31 19 31 19 27
6 21 6 21 6 25 31
25
In the first case, the left subtree starting at node 19 has a small right subtree (just
node 21). The tree can be rearranged by moving the current root node 27 down into the
right tree, rehooking the small tree with node 21 to the left of node 27 and moving node
19 up to the root. The right subtree of the overall tree has grown by one (node 27
pushed into the subtree), and the left subtree has shrunk as node 19 moves upwards
pulling its left subtree up with it. The tree is now balanced, all nodes are "even" except
node 6 which is "left long". The order of nodes is maintained. Keys less than the new
root value, 19, are down the left subtree, keys greater than 19 are in the right tree.
Nodes with keys greater than 19 and less than 27 can be found by going first down the
right tree from 19 to 27, then down the left tree below node 27.
In the second case, it is the right subtree below node 19 that is too large. This time
the rearrangements must shorten this subtree while growing the right branch of the
overall tree. Once again, node 27 gets pushed into the right subtree; this time being
replaced by its left child's (19) right child (21). Any nodes attached below node 21
must be reattached to the tree. Things in its right subtree (e.g. 25) will have values
greater than 21 and less than 27. This right subtree can be reattached as the left subtree
of node 27 once this has been moved into position. The left subtree below node 21
Keeping your balance 847
(there is none in the example shown) would have nodes whose keys were less than 21
and greater than 19. This left subtree (if any) should be reattached as the right subtree
below node 19 after node 21 is detached.
The tree is of course defined by pointer data members in the tree nodes.
Rearrangements of the tree involve switching these pointers around. The principles are
defined in the following algorithm outline. At the start, the pointer t is supposed to
hold the address of the node that has got out of balance (node 27 in the example). This
pointer t could be the "root pointer" for the entire tree; more commonly it will be either
the "left subtree" pointer or the "right subtree" pointer of some other tree node. The
value in this pointer gets changed because a subtree (if not the entire tree) is getting "re-
rooted".
else
// get pointer to node 21 Second case shown if
tLeftRight = tLeft->right_subtree / Figure 24.4
tLeftRight->balance = EVEN
Organizing the The addition of a new node below an existing node may make that node "left long"
overall process or "right long" by changing the tree height. (When a node has one leaf below it, the
addition of the other possible leaf does not change the tree's height, it simply puts the
node back into even balance.) If the tree's height changes, this may necessitate
rearrangement at the next level above where a node may have become "left too long"
(or "right too long"). But it is possible that the change of height in one branch of a tree
only produces an imbalance several levels higher up. For example, in the last example
shown in Figure 24.3, the addition of node 11 did not cause problems at node 6, or at
node 19, but did cause node 27 to become unbalanced.
The mechanism used to handle an insertion must keep track of the path from root to
the point where a new node gets attached. Then after a new node is created, its data are
filled in, and it gets attached, the process must work back up the path checking each
node for imbalance, and performing the appropriate rebalancing rituals where
necessary.
Recursive driver The process of recording the path and then unwinding and checking the nodes is
function most easily handled using a recursive routine. It starts like the recursive insertion
function shown for the simpler binary tree; the key for the new item, is compared with
that in the current node and either the left or right branch is followed in the next
recursive call. When there is no subtree, you have found the point where the new node
is to be attached, so you build the node and hook it in. The difference from the simple
recursive insertion function is that there is a lot of checking code that reexamines
"balance" when the recursive call returns.
The algorithm is:
insert(dataitem, link)
Terminate recursion if (the link is null)
when have position to create a new node, fill in data
attach new node set the link to point to the new node
Notify caller that tree set a flag saying that the tree has grown larger
has grown return
return
Deletion of nodes
The driver function will be given the key for the data item that is to be removed, and the
root pointer. It involves a recursive search down through the tree. On each recursive
call the argument t will be either the left or right subtree link from one of the tree
nodes traversed.
delete(bad_key, t)
Hit null pointer, key if(t is NULL)
wasn't present set flags to say tree not changed
return
DeleteRec(t)
Replace node with if(t->RightLink() is NULL)
only one child by its x = t
sole child t = t->LeftLink()
fResizing = CHANGED_SIZE
delete x;
else
if(t->LeftLink() is NULL)
similar
else
Use auxiliary Del(t,t->LeftLink())
function to promote
data from left subtree if(fResizing == CHANGED_SIZE)
Left subtree may Check_balance_after_Left_Delete(t);
have shrunk, fix up
The auxiliary function Del() is given a pointer to the node that is being changed
and, in the initial call, a pointer to the node's left subtree. It has to find the replacement
Keeping your balance 851
data that are to be promoted. The data will be that associated with the largest entry in
this left subtree, i.e. the rightmost entry in the subtree. Naturally, Del() starts by
recursively searching down to find the necessary data.
Del(t, r)
if(r->RightLink() is not NULL) Code for the
Del(t,r->RightLink()) recursive search
if(fResizing equals CHANGED_SIZE) and the fixup as
Check_balance_after_Right_Delete(r); unwind recursion
else {
t->Replace(r->Data()) Code that handles the
promotion when data
// unlink the node from which data have been are found
// promoted, replacing it by its left subtree
// (if any exists)
x = r
r = r->LeftLink()
The main issues still to be resolved are how to check the balance at a node after
deletions in its left or right subtrees and how to fix things up if the balance is wrong.
Once again, Adelson-Velskii and Landis had to sort out all the possible situations AVL rearrangements
and work out the correct rearrangements that would both keep the entries in the tree for deletions
ordered, and the overall tree "more or less balanced".
The checking part is relatively simple, the code is something like the following
(which deals with the case where something has been removed from a node's right
subtree):
Check_balance_after_Right_Delete(t)
switch (t->Balance()) {
case LEFT_LONG:
// Right branch from current node was already
// shorter than left branch, and it has shrunk.
// Have to rebalance
Rebalance_Right_Short(t);
break;
case EVEN:
t->ResetBalance(LEFT_LONG);
fResizing = UNCHANGED;
break;
case RIGHT_LONG:
t->ResetBalance(EVEN);
break;
}
852 Two more trees
If the node had been "even", all that has happened is that it becomes "left long". This
can simply be noted, and there is no need to consider changes at higher levels. If it had
been "right long" it has now become "even". Its "right longedness" may have been
balancing something else's "left longedness"; so it is possible that there will still be a
need to make changes at higher levels. The real work occurs when you have a node that
was already "left long" and whose right subtree has grown shorter. In that case,
rebalancing operations are needed. There are symmetrically equivalent rebalancing
operations for a node that was "right long" and whose left subtree has grown shorter.
The actual rearrangements are illustrated in Figure 24.5 for the case where a node
was right long and whose left branch has shrunk.
27 27 28
19 35 19 35 27 35
6 28 44 28 44 19 32 44
32 32
27 27 35
19 35 19 35 27 44
28 44 28 44 19 28 58
6
32 58 32 58 32
The rearrangements needed depend on the shape of the right subtree of the node that
has become "left too short". If this right subtree is itself "left long" (the first example
shown in Figure 24.5), then the tree is restored by pushing the current root down into
the left branch (making that longer) and pulling a node up from the "left subtree" of the
"right subtree" to make the new root for this tree (or subtree). If the right subtree is
Keeping your balance 853
evenly balanced (second example in Figure 24.5) or right long (not shown) then slightly
different rearrangements apply.
Once again the rearrangements involve shifting pointers around and resetting the
balance records associated with the nodes.
24.1.3 An implementation
The following code provides an example implementation of the AVL tree algorithms.
The code implementing some functions has been omitted; as already noted, there are
symmetrically equivalent "left" and "right" operations so only the code of one version
need be shown.
The AVL tree is meant to be used to store pointers to any kind of object that is an
instance of a class derived from abstract class KeyedItem. The header file should
contain declarations for both KeyedItem and AVLTree. The implementation for class
AVLTree uses an auxiliary class, AVLTreeNode , whose instances represent the tree
nodes. This is essentially a private structure and is defined in the implementation file.
Since class AVLTree has data members that are AVLTreeNode* pointers, there has to be
a declaration of the form "class AVLTreeNode;" in the header file.
class AVLTreeNode;
The public interface for class AVLTree is similar to that for the simple binary tree
class. There are several private member functions that deal with issues like those
rebalancing manoeuvres.
private:
Auxiliary functions void Insert1(KeyedItem* d, AVLTreeNode*& t);
for insertion and void Rebalance_Left_Long(AVLTreeNode*& t);
consequent void Rebalance_Right_Long(AVLTreeNode*& t);
rebalancing
KeyedItem *fReturnItem;
int fResizing;
int fAddOK;
};
The principal data members are a pointer to the root of the tree and a count for the
number of entries in the tree. The other three data members are essentially "work"
variables for all those recursive routines that scramble around the tree; e.g. fResizing
is the flag used to record whether there has been a change in the size of a subtree.
The tree does not "own" the data items that are inserted. The Remove() function
returns a pointer to the data item associated with the "bad key". "Client code" that uses
this AVL implementation can delete data items when appropriate. The destructor for
the tree gets rid of all its AVLTreeNodes but leaves the data items untouched.
The implementation file starts with declarations of some integer flags and an
enumerated type used to represent node balance. Then class AVLTreeNode is defined:
AVLTreeNode*& LeftLink(void);
An implementation of class AVLTree 855
AVLTreeNode*& RightLink(void);
An AVLTreeNode is something that has a balance factor, a pointer to some keyed data,
and pointers to the AVLTreeNodes at the head of left and right subtrees. It provides
three member functions that provide read access to data such as the balance factor, and
two functions for explicitly changing the data associated with the node, or changing the
balance.
In addition, there are the functions LeftLink() and RightLink(). These return Note functions that
references to the nodes left and right tree links. Because these functions return return reference
values
reference values, calls to these functions can appear on the left hand side of
assignments. Although a little unusual, such functions help simplify the code of class
AVLTree. Such coding techniques are somewhat sophisticated. You probably shouldn't
yet attempt to write anything using such techniques, but you should be able to read and
understand code that does.
All the member functions of class AVLTreeNode are simple; most can be defined as
"inline".
The constructor for class AVLTree needs merely to set the root pointer to NULL and AVLTree
the count of items to zero. The destructor is not shown. It is like the binary tree
856 Two more trees
destructor illustrated at the end of Section 23.5.2. It does a post order traversal of the
tree deleting each AVLTreeNode as it goes.
Constructor AVLTree::AVLTree()
{
fRoot = 0;
fNum = 0;
}
The Find() function is just a standard search that chases down the left or right links
as needed. It could be implemented recursively but because of its simplicity, an
iterative version is easy:
The Add() and Remove() functions provide the client interface to the real working
functions. They set up initial calls to the recursive routines, passing in the root pointer
for the tree. Private data members are used rather than have the functions return their
results; again this is just so as to slightly simplify the code in a few places.
tptr->RightLink() = tptr2->LeftLink();
tptr2->LeftLink() = tptr;
t->LeftLink() = tptr2->RightLink();
tptr2->RightLink() = t;
t->ResetBalance(
(tptr2->Balance() == LEFT_LONG) ?
RIGHT_LONG : EVEN);
tptr->ResetBalance(
(tptr2->Balance() == RIGHT_LONG) ?
LEFT_LONG : EVEN);
t = tptr2;
}
}
The code highlighted in bold shows calls to the "reference returning" function
LeftLink(). The first call is on the right hand side of an assignment so the compiler
arranges to get the value from the fLeft field of the object pointed to by t . In the
second case, the call is on the left of an assignment. The compiler gets the address of
t's fLeft data field, and then stores, in this location, the value obtained by evaluating
tptr->RightLink(). The code highlighted in italics illustrates where the function is
changing the value of the pointer passed by reference (in effect, "re-rooting" the current
subtree).
The corresponding function Rebalance_Right_Long() is similar and so is not
shown.
Deletion functions The main driver routine for deletion and the functions for checking balance after left
or right deletions are simple to implement from the outline algorithms given earlier.
The DeleteRec() function (which removes nodes with one or no children and arranges
for promotion of data in other cases) is:
void AVLTree::DeleteRec(AVLTreeNode*& t)
{
fReturnItem = t->Data();
if(t->RightLink() == NULL) {
An implementation of class AVLTree 859
AVLTreeNode *x = t;
t = t->LeftLink();
fResizing = CHANGED_SIZE;
delete x;
}
else
if(t->LeftLink() == NULL) {
AVLTreeNode *x = t;
t = t->RightLink();
fResizing = CHANGED_SIZE;
delete x;
}
else {
Del(t,t->LeftLink());
if(fResizing == CHANGED_SIZE)
Check_balance_after_Left_Delete(t);
}
}
The Del() function deals with the processes of finding the data to promote and the
replacement action:
There are symmetrically equivalent routines for rebalancing a node after deletions in
its left or right subtrees. This is the code for the case where the right subtree has
shrunk:
if(tptr->Balance() != RIGHT_LONG) {
t->LeftLink() = tptr->RightLink();
tptr->RightLink() = t;
860 Two more trees
if(tptr->Balance() == EVEN) {
t->ResetBalance(LEFT_LONG);
tptr->ResetBalance(RIGHT_LONG);
fResizing = UNCHANGED;
}
else {
t->ResetBalance(EVEN);
tptr->ResetBalance(EVEN);
}
t = tptr;
}
else {
AVLTreeNode *tptr2 = tptr->RightLink();
tptr->RightLink() = tptr2->LeftLink();
tptr2->LeftLink() = tptr;
t->LeftLink() = tptr2->RightLink();
tptr2->RightLink() = t;
t->ResetBalance((tptr2->Balance() == LEFT_LONG) ?
RIGHT_LONG : EVEN);
tptr->ResetBalance((tptr2->Balance() == RIGHT_LONG) ?
LEFT_LONG : EVEN);
t = tptr2;
tptr2->ResetBalance(EVEN);
}
}
The functions not shown are all either extremely simple or are the left/right images
of functions that have been given.
24.1.4 Testing!
Just look at the AVL algorithm! It has special cases for left subtrees becoming too long
on their own left sides, and left subtrees becoming too long on their right subtrees, code
for right branches that are getting shorter, and …. It has special cases where data
elements must be promoted from other tree cells. These operations may involve
searches down branches of trees. The tree has to be quite large before there is even a
remote possibility of some these special operations being invoked.
The simpler abstract data types like the lists and the queues shown in Chapter 21
could be tested using small interactive programs that allowed the tester to exercise the
various options like getting the length or adding an element. Such an approach to
testing something like the AVL tree is certain to prove inadequate. When arbitrarily
selecting successive addition and deletion operations, the tester simply won't pick a
sequence that exercises some of the more exotic operations.
The approach to testing has to be more systematic. You should provide a little
interactive program, like those in Chapter 21, that can be used for some preliminary
tests. A second non-interactive test program would then be needed to thoroughly test
Testing class AVLTree 861
all aspects of the code. This second program would be used in conjunction with a "code
coverage tool" like that described in Chapter 14.
Both the test programs would need some data objects that could be inserted into the
tree. You would have to define a class derived from class KeyedItem, e.g. class
TextItem:
TextItem::~TextItem()
{
delete [] fText;
}
A TextItem object is just something that holds a long integer key and a string. The
interactive test program can get the user to enter these data; the way the data are
generated and used in the automated program is explained later.
The main function for an interactive test program, HandTest(), is shown below. It
has the usual structure with a loop offering user commands.
AVLTree gTree;
void HandTest(void)
{
KeyedItem* d;
for(;;) {
862 Two more trees
char ch;
Get command cout << "Action (a = Add, d = Delete, f = Find,"
" p = Print Tree, q = Quit) : ";
cin >> ch;
switch (ch) {
An "add" command results in the creation of an extra TextItem that gets put in the tree.
(The function AVLTree::Add() returns a success/failure indicator. A failure should
only occur if an attempt is made to insert a record with a duplicate key. If the add
operation fails, the "duplicate" record should be deleted.)
A "delete" command gets the key for the TextItem to be removed then invokes the
trees remove function. Function AVLTree::Remove() returns NULL if an item with the
given key was not present. If the item was found, a pointer is returned. The item can
then be deleted.
There would also be a "find" command (not shown, is trivial to implement), a "quit"
command, and possibly a "print" command. During testing it would be useful to get the
tree displayed so it might be worth adding an extra public member function AVLTree::
PrintTree(). The algorithm required will be identical to that used for the simpler
binary tree.
else {
cout << "Removing " << *d << endl;
delete d;
}
}
break;
case 'f':
case 'F':
…
…
break;
case 'p':
case 'P':
gTree.PrintTree();
break;
case 'q':
case 'Q':
return;
default:
cout << "?" << endl;
}
}
}
Hand testing will never build up the large complex trees where less common
operations, like promotion of data, get fully tested. You need code that performs
thousands of insertion, find, and deletion operations on the tree and which checks that
each operation returns the correct result.
This is not as hard as it might seem. Basically, you need a testing function that starts Mechanism for an
by loading some records into the tree and then "randomly" chooses to add more records, automated test
delete records, or search for records. The function will need a couple of control
parameters. One determines the number of cycles (should be 10000 to 20000). The
other parameter, testsisze , determines the range used for keys; there is a limit,
kTESTMAX, for this parameter. The use of the testsisze parameter is explained below.
void AutoTest()
{
int testsize;
int runsize;
cout << "Enter control parameters for auto-test ";
cin >> testsize >> runsize;
assert(testsize > 1);
assert(testsize < kTESTMAX);
Initialize(testsize);
for(int i=0; i < testsize / 2; i++)
Add(testsize);
int r = rand() % 4;
switch(r) {
case 0: Add(testsize); break;
case 1: Find(testsize); break;
case 2:
case 3:
Remove(testsize); break;
}
}
cout << "Test complete, counters of actions: " << endl;
for(i = 0; i < 6; i++)
cout << i << ": " << gCounters[i] << endl;
}
As you can see, the loop favours removal operations. This makes it likely that at some
stage all records will be removed from the tree. There are often obscure special cases
related to collections becoming empty and then being refilled so it is an aspect that you
want to get checked.
Function AutoTest() uses the auxiliary functions, Add(), Find(), and Remove() to
do the actual operations. These must be able to check that everything works correctly.
Keeping track of Correct operation can be checked by keeping track of the keys that have been
valid keys allocated to TextItem records inserted in the tree. When creating a new TextItem, the
test program gives it a "random" key within the permitted range:
The Add() function keeps track of the keys that it has allocated and for which it has
inserted a record into the tree. (It only needs an array of "booleans" that record whether
a key has been used):
If the same randomly chosen key has already been used, an addition operation should
fail; otherwise it should succeed. The Add() function can check these possibilities. If
something doesn't work correctly, the program can stop after generating some statistics
on the tree (function ReportProblem(), not shown). If things seem OK, the function
can increment a count of operations tested:
The functions Find() and Remove() can also use the information in the gUsed[]
array. Function Find() (not shown) randomly picks a key, inspects the corresponding
gUsed[] array to determine whether or not a record should be found, then attempts the
gTree.Find() operation and verifies whether the result is as expected.
866 Two more trees
Runs can be made with different values for the testsize parameter. Large values
(3000 - 5000) result in complicated deep trees (after all, the first step involves filling the
tree with testsize/2 items). These trees have cases where data have to be promoted
from remote nodes, leading to a long sequence of balance checks following the deletion.
Small values of the testsize parameter keep the tree small, force lots of "duplicate"
checks, and make it more likely that all elements will be deleted from the tree at some
stage in the processing.
The test program can use the gCounters[] counts to provide some indication as to
whether the tests are comprehensive. But this is still not adequate. You may know that
your tree survived 10,000 operations but you still can't be certain that all its functions
have been executed.
Complex algorithms like the AVL code require testing with the code coverage tools.
The code was run on a Unix system where the tcov tool (Chapter 14) was available.
Testing class AVLTree 867
Several different runs were performed and the final accumulated statistics were
analyzed using tcov. A fragment of tcov's output is as follows:
}
void AVLTree::Check_balance_after_Left_Delete(
AVLTreeNode*& t)
4338 -> {
switch (t->Balance()) {
case LEFT_LONG:
1480 -> t->ResetBalance(EVEN);
break;
case EVEN:
2256 -> t->ResetBalance(RIGHT_LONG);
fResizing = UNCHANGED;
break;
case RIGHT_LONG:
602 -> Rebalance_Left_Short(t);
break;
}
4338 -> }
Such results give greater confidence in the code. (The tcov record, along with the test
programs, form part of the "documentation" that you should provide if your task was to
build a complex component like an AVL tree.)
868 Two more trees
Code coverage by If you can't get access to something like tcov, you have to achieve something
hand similar. This means adding conditionally compiled code. You will have to define a
global array to hold the counters:
#ifdef TCOVING
int __my__counters[1000];
#endif
…
and you will have to edit every function, and every branch statement within a function,
to increment a counter:
void AVL::Insert(
{
#ifdef TCOVING
__my__counters[17]++;
#endif
Finally, you have to provide a function that prints contents of the table when the
program terminates.
Unfortunately, this process is very clumsy. It is easy to make mistakes such as
forgetting to associate a counter with some branch, or to have two bits of code that use
the same counter (this is quite common if the main code is still being finalized at the
same time as being tested). The printouts of counts aren't directly related to the source
listings, the programmer has to read the two outputs together.
Further, the entire process of hand editing is itself error prone. Careless editing can
easily cut a controlled statement from the if() condition that controls it!
The best solution is to use a code coverage tool on some other platform while
pressing your IDE supplier for such a tool in the next version of the software. (The
supplier should oblige, code coverage tools are as easy, or easier, to add to a compiler
than the time profilers which are commonly available.)
Don't really trust it A final warning, even if tcov (or equivalent) says that you've tested all the branches
even if tcov says its in your code, you still can't be certain that it is correct. You really should try that
tested
exercise at the end of Chapter 21 where "live" listcells were "deleted" from a list and
the program still ran "correctly" (or at least it ran long enough to convince any typical
tester that it was working correctly).
Memory problems (leaks, incorrect deletions, other use of "deleted" data) are the
reason for having an automated program that performs tens of thousands operations. If
the tests are lengthy enough you have some chance of forcing memory bugs to manifest
themselves (by crashing the program several minutes into a test run). Unfortunately,
some memory related bugs are "history dependent" and will only show up with specific
sequences of operations; such bugs are exceptionally difficult to find.
Testing can establish that your code has bugs; testing can not prove that a program is
bug free. Despite that, it is better if code is extensively tested rather than left untested.
Testing class AVLTree 869
24.2 BTREE
You are not restricted to "binary trees", there are alternative tree structures. In fact,
there are numerous forms of "multiway tree". They all serve much the same role as an
AVL tree. They are "lookup" structures for keyed data. These trees provide Add(),
Find(), and Remove() functions. They provide a guarantee that their performance on
all operations in close to O(lg(N)) (with N the number of items stored in the tree).
These trees have more than one key in each of their tree nodes and, consequently,
more than two links down to subtrees. The data inserted into a tree are kept ordered;
data items with small keys are in "left subtrees", data with "middling" keys can be
found down in other subtrees, and data with large keys tend to be in "right subtrees".
The trees keep themselves "more or less balanced" by varying the number of data items
stored in each node. The path from root to leaf is kept the same for all leaves in the tree
(a major factor in keeping costs O(lg(N))).
Although the structures of the nodes, and the forms for the trees, are radically
different from those in the AVL tree, there are some similarities in the overall
organization of the algorithms.
An operation like an insertion is done by recursively chasing down through the tree
to the point where the extra data should go. The new item is added. Then, as the
recursion unwinds, local "fix ups" are performed on each tree node so as to make
certain that they all store "appropriate" numbers of data items and links.
What is an "appropriate" number of data items for a node? How are "fix ups" done
when nodes don't have appropriate number of items? Each different form of multiway
tree has slightly different rules with regard to these issues.
Deletions are also handled in much the same way as in AVL and binary trees. You
search, using a recursive routine, for the item that is to be removed. Items can easily be
cut out of "leaf nodes". Items that are in "internal nodes" (those with links down to
subtrees) have to be replaced by data "promoted" from a leaf node lower in the tree (the
successor, or predecessor, item with the key immediately after, or before, that of the
item being removed). If data are promoted from another node, the original copy of the
promoted data must then be cut out from its leaf node.
Once the deletion step has been done, the recursion unwinds. As in the case of
insertion, the "unwinding" process must "fix up" each of the nodes on the path back to
the root. Once again, different forms of multiway tree have slightly different "fix up"
rules.
Of course, searches are fairly simple. You can chase down through the tree
following the links. As nodes can have more than one key, there is an iterative search
through the various keys in each node reached.
870 Two more trees
2-3 Trees
You will probably get to study different multiway trees sometime later in your
computer science career. Here, only one simple version need be considered. It is
usually called a "two-three" tree because each node can hold two keys and three links
down to subtrees. It has similarities to, and can act as an introduction to the BTree
which the real focus of this section.
For simplicity, these 2-3 trees will be shown storing just integer keys. If you were
really implementing this kind of tree, the "integer key" fields used in the following
discussion would be replaced by structures that comprised an integer key and a pointer
to the real data item (as was done in the binary tree example in Chapter 21).
Figure 24.6 illustrates the form of a tree node for this simplified 2-3 tree, while
Figure 24.7 illustrates an actual tree built using these nodes. The tree node has an array
of two longs for the keys, an array of three pointers for the links to subtrees and a "flag"
indicating whether it is a "2-node" (one key, two links used), or a "3-node" (both keys
and all three links used). In leaf nodes, the link fields will be NULL.
Search The search algorithm is simple:
Node structure:
a key
81
44 101 140
else
if less
search down subtree 0
else
if greater
if this is a 2-node then search down subtree 1
else
compare search key with 2nd key in node
if equal
report record as found
else
if less
search down subtree 1
else
search down subtree 2
New keys are inserted into leaf nodes. In some cases this is easy. In the example Insertion
tree shown in Figure 24.7, insertion of the key value 33 is easy. The search for the
correct place goes left down link[0] from the node with key 81, and again left down
link[0] from the node with key 44. The next node reached is a leaf node. This one
has only one entry, 19, so there is room for the key 33. The key can be inserted and the
node's flag changed to mark it as a "3-node" (two keys, potentially three links though
currently all these links are NULL).
Insertion of the key value 71 would be more problematic. Its place is in the leaf Splitting nodes to
node with the keys 52 and 69; but this node is already fully occupied. Insertion into a make room for
another inserted key
full leaf node is handled by "splitting the node". There will then be three keys, and two
leaf nodes. The least valued of the three keys goes in the "left" node resulting from the
split; the key with the largest value goes in the "right" node; while the middle valued
key gets moved up one level to the parent node. This is illustrated in Figure 24.8.
The two leaf nodes are both "2-nodes", while their parent (the node that used to hold
just key 44) now has two keys and three links and so it is now a "3-node".
The results of two additional insertions are illustrated in Figures 24.9 and 24.10.
872 Two more trees
81
44 69 101 140
Median key, 69,
moved up into
parent
19 33 52 71 93
Figure 24.8 Splitting a full leaf node to accommodate another inserted key.
First, the key 20 is inserted. This should go into the leaf currently occupied by keys
19 and 33. Since this leaf is full, it has to be split. Key 19 goes in the left part, key 33
in the right part and key 20 (the median) has to be inserted into the parent. But the
parent, the node with 44 and 69 is itself full. So, it too must be split. The left part will
hold the smallest key (the 20) and have links down to the nodes with 19 and 33. The
right part will hold the key 69 and links down to the nodes with keys 52 and 71. The
median key, 44, must be pushed into the parent, the root node with the 81. This node
has room; it gets changed from a 2-node to a 3-node. The resulting situation is shown
in Figure 24.9.
20 69 101 140
Insertion of the next key value, 161, causes more problems. If should go in the leaf
node where the values 162 and 189 are currently location. As this leaf is full, it must be
split. The new key 161 can go in the left part; the large key 189 can go in the new right
node; and the median key, 162, (and the link to the new right node) get passed back to
be inserted into the parent node. But this node, the one with the 101 and 140 keys is
also full. So it gets split. One part gets to hold the key 101 and links down to the node
with 93 and the node with 111 and 133. The new part gets to hold the key 162 and its
Multiway trees 873
links down to the node with 161 and the node with 189. The median value, 140, has to
go in the parent node. But this is full. So, once again a split occurs. One node takes
the 44, another takes the 140 and the median value, 81, has to be pushed up to the
parent level.
There isn't a parent. The node with keys 44 and 81 used to be the root node of the Growing a new root
tree. So, it is time to "grow a new root". The new root holds the 81 key. The result is
as shown in Figure 24.10.
New root
81
44 140
20 69 101 162
Trees in computer programs are always strange. Their branch points have "children"
and their leaves have "parents". They grow downwards, so "up" means closer to the
root not nearer to the leaves. These multiway trees add another aberrant behaviour;
they grow at the root rather than at the ends of existing branches. It is done this way to
keep those paths from root to leaf the same for all leaves; this is required as its part of
the mechanism that guarantees that searches, insertions, (and deletions) have a cost that
is proportional to O(lg(N)).
BTrees
A BTree is simply a 2-3 tree on steroids. Its nodes don't have two keys and three links;
instead its going to be something like 256 keys and 257 links. A fully populated BTree
(one where all the nodes held the maximum possible number of keys) could hold 256
keys in a one level tree, around 60000 keys in a two level tree, sixteen million keys in a
three level tree. Figure 24.11 gives an idea as to the form of a node and shape of a
BTree. The node now has a count field rather than a flag; the count defines the number
of keys in the node.
A BTree can be searched, and data can be inserted into a BTree, using algorithms
very similar to those that have just been illustrated for the 2-3 tree. You can implement
BTrees that work this way, where all the links are memory pointers, and the real data
records are accessed using pointers that are stored in the nodes along with their key
values.
874 Two more trees
"Links" to subtrees
Node in a BTree
Count field Entries ≈ a key and a
"pointer" to some other data.
level 1
But you don't have any really good reasons for using a memory resident BTree like
that. If all your data fit in main memory, you might as well use something more
standard like an AVL tree.
Exceeding memory However, if you have a really large collection of keyed data records, it is likely that
limits they won't all fit in memory. A large company (e.g. a utility like an electricity
company) may have records on two million customers. Each of these customer records
is likely to be a thousand bytes or more (name, address, payment records, …). Now
two thousand million bytes of data requires rather more "SIM" chips than fit in the
average computer. You can't keep such data in memory instead they must be kept on
disk.
This is where the BTree becomes useful As will be explained more in the next two
sections, it allows you to keep both your primary data records, and your search tree
structure, out on disk. Only a few nodes from the tree and a single data record ever
need be in primary memory. So you can have very large data collections, provided that
you have sufficient disk space (and most PCs support disks with up to 4 gigabytes
capacity).
A binary file can always be treated as an array of bytes. If you know where a data
record is located (i.e. the "array index" of its first byte) you can use a "seek" operation
on a file to set the position for the next read or write operation. Then, provided you
know the size of the data record, you can use a low level read or write operation to
A tree on a disk? 875
transfer the necessary number of bytes. These operations have been illustrated
previously with the examples in Chapters 18 (the customer records example), 19 and 23
(the different InfoStore examples).
This ability to treat a file as a byte array makes it practical to map something like a
tree structure onto a disk file. We can start by considering simple binary trees that hold
solely an integer key. A memory version of such a tree would use structures like the
following:
struct binr {
long key;
binr *left_p;
binr *right_p;
};
with address pointers left_p and right_p holding the locations in memory of the first
node in the corresponding subtree. If we want something like that on a disk file, we
will need a record like the following:
struct dbinr {
long key;
daddr_t left;
daddr_t right;
};
The values in the left and right data members of a dbinr structure will be byte
locations where a node is located in the disk file. These will be referred to below as
"disk addresses", though they are more accurately termed "file offsets". The type
daddr_t ("disk address type) is an alias for long integer. It is usually defined in one of
the standard header files (stdlib, unistd or unix, or sys_types, or …). If you can't locate
the right header you can always provide the typedef yourself:
Figure 24.12 illustrates how a binary tree might be represented in a disk file (the Storing the tree
numbers used for file offsets assume that the record size is twelve bytes which is what structure in a disk file
most systems would allocate for a record with three long integer fields).
The first record inserted would go at the start of the file (disk address 0); in the
example shown this was the record with key 45.. Initially, the first node would have -1s
in its left and right link fields (-1 is not a valid disk address, this value serves the
same role as NULL in a memory pointer repesentation of a tree).
The second record added had key 11. Its record gets written at the end of the
existing file, so it starts at byte 12 of the file. The left link for the first node would be
changed to hold the value 12. Similarly, addition of a record with key 92 results in a
new node being created on disk (at location 24) and this disk address would then be
written into the appropriate link field of the disk record with key 45.
876 Two more trees
3rd node
0024
2nd node 11 92
0012
4th node
84 0036 103
7th node 16
0072
5th node
0048
6th node 71
0060
You should have no difficulty in working out how the rest of the file gets built up
and the links get set.
Such a tree on disk can be searched to determine whether it contains a record with a
given key. The code would be something like the following:
fstream treefile;
int search(long sought)
{
// Assume that treefile has already been opened successfully
dbinr arec;
A tree on a disk? 877
This is just another version of an iterative search on a binary tree (similar to the search
function illustrated in Section 24.1 for searching an AVL tree). It is a relatively
expensive version; each cycle of the loop involving disk transfer operations.
Normally, you would have data records as well as keys. You would use two files; Store data in a
one file stores the tree structure, the other file would store the data records (a bit like the separate file
index file and the articles file in the InfoStore example). If all the data records are the
same size, there are no difficulties. The record structure for a tree node would be
changed to something like:
struct dbinr {
long key;
daddr_t dataloc; // extra link to datafile
daddr_t left;
daddr_t right;
};
With the extra field being the location of the data associated with the given key; this
would be an offset into the second data file.
The search routine to get the data record associated with a given key would be:
fstream treefile;
fstream datafile
int search(long sought, datarec& d)
{
// Assume both files have already been opened successfully
dbinr arec;
long diskpos = 0;
for(;;) {
treefile.seekg(diskpos);
treefile.read((char*)&arec, sizeof(dbinr));
if(sought == arec.key) {
datafile.seekg(arec.dataloc);
datafile.read((char*)&d, sizeof(datarec));
return 1;
}
878 Two more trees
The records are stored separately from the tree structure because you don't want to read
each record as you move from tree node to tree node. You only want to read a data
record, which after all might be quite large, when you have found the correct one.
It should be obvious that there are no great technical problems in mapping binary
trees (or more elaborate things like AVL trees) onto disk files. But it isn't something
that you would really want to do.
The iterative loops in the search, and the recursive call sequences involved in the
insertion and deletion operations require many tree nodes to be read from the "tree file".
As illustrated in Figure 24.13, the tree nodes are going to be stored in disk blocks that
may be scattered across the disk. Each seek and read operation may involve relatively
lengthy disk operations (e.g. as much as 0.02 seconds for the operating system to read
in the disk block containing the next tree node).
If the trees are deep, then many of the operations will involve reading multiple
blocks. After all, a binary tree that has a million keys in it will be twenty levels deep.
Consequently each search operation on a disk based binary tree may require as many as
twenty disk seeks to find the required tree node (and then one more seek to find the
corresponding data).
A BTree with a million keys is going to be only three levels deep if its nodes have
≈250 keys. Searching such a tree will involve three seeks to get just three tree nodes,
and then the extra seek to find the data. Three disk operations is much better than
twenty. BTrees are just as easy to map onto disks as are other trees. But because of
their shallow depths, their use doesn't incur so much of a penalty.
The structure for a BTree tree node, and the form of the BTree index file, are
illustrated in Figure 24.14. The example node has space for only a few keys where a
real BTree has hundreds. Small sized nodes are necessary in order to illustrate
algorithms and when testing the implementation. Many of the more complex tree
rearrangements occur only when a node becomes full or empty. You would have to
insert several million items if you wanted to fill most of the nodes in a three level tree
with 250 keys per node; that would make testing difficult. Testing is a lot easier if the
nodes have only a few keys.
The node has a count specifying the number of key/location pairs that are filled, an
array of these key/location pairs, and an array of links. The links array is one larger
than the keys array and a BTree node either has no links (a leaf node) or has one more
link than it has keys. The entries in the links array are again disk addresses; they are the
addresses of the BTree nodes that represent subtrees hung below the current BTree
node. A BTree node has the following data members:
A tree on a disk? 879
Disk
BTree node
"Links" to other
BTree nodes
Count
Key/location records
Key
Location of data record
(in separate data file)
BTree file
BTree nodes
"Housekeeping"
data
int n_data;
KLRec data[MAX]; /* 0..n_data-1 are filled */
daddr_t links[MAX+1]; /* 0..n_data are filled */
class BTree
{
public:
BTree(const char* filename);
~BTree();
fstream fTreeFile;
fstream fDataFile;
};
This implementation is simplified. Data items once written to disk will always
occupy space. Deletion of a data item simply removes its key/location entry from the
index file. Similarly, BTree nodes that become empty and get discarded also continue
to occupy space on disk; once again, they are simply unlinked from the index structure.
A real implementation would employ some extra "housekeeping data" to keep track
of deleted records and discarded BTree nodes. If there are "deleted records", the next
request for a new record can be satisfied by reusing existing allocated space rather than
by extending the data file. The implementation of this recycling scheme involves
A tree on a disk? 881
keeping two lists, one of deleted data records and the other of discarded BTree nodes.
The links of these lists are stored in the "deleted" data records of the corresponding disk
files (i.e. a "list on a disk"). The starting points of the two "free lists" are included with
the other "housekeeping information" at the start of the index file.
All the more elaborate trees have rules that define how their nodes should be organized. Rules defining a
You may find minor variations in different text books for the rules relating to BTrees. BTree
The rules basically specify:
• A BTree is a tree with nodes that have the data members previously illustrated:
• If a node is a leaf, all its link fields are "NULL". (i.e. the -1 value for "no disk
address", NO_DADDR).
• The keys within a node are kept ordered: x.data.fKey[0] < x.data.fKey[1] <
….
• The keys in a node separate the ranges of keys stored in subtrees. So x.link[0]
links to the start of a subtree containing records whose keys will all be less than
x.data.fKey[0]; x.link[1] points to a subtree containing records whose keys
(k) are in the range x.data.fKey[0] < k < x.data.fKey[1]. The final link,
x.link[x.n_data], connects to a subtree with records having keys greater than
x.data.fKey[x.n_data].
• There are lower and upper bounds on the number of keys in a node. Apart from the
root node, every node must contain at least MAX/2 keys and at most MAX keys.
Find
Searching the tree for a record with a given key is relatively simple. It involves just a
slight generalization of the algorithm suggested earlier for searching a 2-3 tree.
You start by loading the root node of the tree (reading it from the index file into
memory). Next you must search in the current node for the key. You stop when you
find the key, or when you find a key greater than the value sought. If the key was
found, the associated location information identifies where the data record can be found
in the data file. The data record can then be loaded and the Find routine can return a
success indicator.
If the key is not matched, the search should continue in a subtree (provided that there
is a subtree). The search for the key will have stopped with an index set so that it either
identifies the position of the matching key or the link that should be used to get to the
BTree node at the start of the required subtree.
The driver routine is iterative. It keeps searching until either the key is found, or a
"null" link (i.e. a -1 disk address) is encountered in a link field.
Some of the work is done the BTree::Find() function itself. But it is worth
making a class BTreeNode to look after details of links and counts etc. A BTreeNode ,
once loaded from disk, can be asked to check itself for the key.
Searching inside a The BTreeNode object would have to find the required key, returning a success or
node failure indicator. It would also have to set an index value identifying the position of the
key (or of the link down to the subtree where the required key might be located). Since
the keys in a node are ordered, you should use binary search. For simplicity, a linear
search is shown in the following implementation. The loop checks successive keys,
incrementing index each time, until either the key is found or all keys have been
checked. (You should work through the code and convince yourself that, if the key is
not present, the final value of index will identify the link to the correct subtree).
Operations on BTrees 883
Add
The algorithm is essentially the same as that illustrated for the 2-3 tree:
recursively...
This is another case where a substantial number of auxiliary functions are needed to
handle the various different aspects of the work. The implementation given in the next
section uses the following functions for class BTree:
Add() The Add() function is the client interface. It sets up the initial call to the main
DoAdd() recursive function. It also has to deal with the special case of growing a new
root for the tree (as illustrated for the 2-3 tree in Figure 24.10):
Add
invoke DoAdd()
passing it as arguments the new data record, and
the disk address of the current root of the tree
Recursive DoAdd() The recursive function DoAdd() is the most complex. It has three aspects. There is
function an inward recursion aspect; this chases down subtree links through the tree. When the
recursion process is complete and the correct point for the record has been found in the
tree, the data get saved to disk. The final aspect is organizing the "fix up" operations as
recursion is unwind.
Several BTreeNodes Each recursive call to DoAdd() will load another BTreeNode into the stack BTree-
on the stack Nodes are going to be a few hundred to a few thousand bytes in size. Since the
maximum limit of the recursion is defined by the depth of the tree (which won't be
large), this stacking up of the BTreeNodes will not use excessive memory. When the
insertion point is found, the BTreeNodes in the stack are those that define the path back
to the root. These are the nodes that may need to be "fixed up" if a node was full and
had to be split.
Inward recursion to Inwards recursion aims to get to the point in the tree where the new data should go.
find place for record Since there are now many subtrees below each tree node, one aspect of the inward
recursion process is a search through the current node to find the appropriate link to
follow for a given key value.
Terminating The inwards recursive phase terminates on either of two conditions. There is the
recursion, by special case of finding an existing record with the key. In this case, the data are
replacing a data
record
Operations on BTrees 885
replaced in the data file and a flag is set to indicate that no work is necessary as the
recursion unwinds.
The other terminating condition is that the recursive call has been made with a "null" Terminating
disk address passed as an argument. This means that recursion has reached the bottom recursion by inserting
a new record
of the tree. The new data record should be added to the data file. Its disk address and its
key get placed in a KLRec . This is returned to the preceding level of recursion for
processing.
The final aspect of DoAdd() is the mechanism for unwinding recursion. This starts Unwinding recursion
by checking a "work flag" returned by the recursive call. If the flag is not set, function and fixing up records
DoAdd() can simply return; but if the flag is set then a KLRec and link value have to be
inserted into the BTreeNode in the current stack frame and the updated node must be
written back to disk.
Most of the remaining complexities relate to insertions into a BTreeNode. These
operations are outlined after the complete DoAdd() algorithm.
Naturally, like all recursive routines, the termination conditions for DoAdd() come
first. So the actual structure of the function involves the termination tests, then the
setting up of a further recursive call, and finally, after the recursive call, the fix-up code.
The algorithm for DoAdd() is as follows:
Ask current node to search for given key Check for key
if(found) {
Find location in data file for record with If find key, terminate
this key by replacing data
Replace with new data
Set flag to say fix up not required
return;
}
// If key not found, 'index' variable will have been set Recursive call
// so as to identify link to subtree
DoAdd(newData, current.links[index],
…);
886 Two more trees
Insertion into a Insertion of an extra key into a partially filled BTreeNode is the simplest case, see
partially filled Figure 24.15. The BTreeNode can be given details of the new key (more strictly, a
BTreeNode
KLRec, key/location pair, defining the data item), and the position where this is to go in
the node's array of KLRecs. Existing entries with keys that are greater in value should
be moved to the right in the array to make room; the new KLRec entry can be inserted
and the count of entries incremented. Every time a BTreeNode is changed, it has to be
written back to the index file.
4 19 33 78 93 -1 -1 -1 -1 -1
45
5 19 33 45 78 93 -1 -1 -1 -1 -1 -1
If the insertion is into an "internal" BTreeNode, then there will be an extra link that
has to be inserted in addition to the KLRec. This extra link is the disk address of an
extra BTreeNode that results from a "split" operation at a lower level..
The BTreeNode can have a single member function that deals with insertions of a
KLRec and associated extra link (the extra link argument will be -1 in the case of
insertion into a leaf node):
If a BTreeNode already has MAX keys, then it has to be split, just like a full 2-3 node Insertion into a full
would get split. There will be a total of MAX+1 keys (the MAX keys already in the node BTreeNode – splitting
the node
and the extra one). Half (those with the lowest values) are left in the existing node; half
(those with the greatest values) are copied into a newly created BTreeNode, and one, the
median value, gets moved up into the parent BTreeNode.
The basic principles are as illustrated in Figure 24.16. This shows an insertion into a
full leaf node (all its subtree links are -1). An extra BTreeNode is created (shown as
"node02"). Half the data are copied across. Both nodes are marked as half empty.
Then, both would be written to disk (the new BTreeNode, "node02", going at the end of
the index file, the original BTreeNode, "node01", being overwritten on disk with the
updated information). The median key, and the disk address for the new "node02"
would then have to be inserted into the BTreeNode that is the parent of node01 (and
now of node02 as well).
There are really three slightly different versions of this split process. In the first, the
extra key has a low value and it gets inserted into the left (original) node. In the second,
the new key is the median value; it gets moved to the parent. Finally, there is the
situation where the new key is large and it belongs in the right (new) node. The
reshuffling processes that move data around are slightly different for the three cases and
so are best handled by separate auxiliary functions.
The overall BTree::Split() function has to be organized along the following
lines:
Split
given: a BTreeNode to be split "node_to_split"
an extra KLRec key/data-location pair
an extra link (disk address of a BTreeNode that
is to become a subtree of "node_to_split"
888 Two more trees
6 32 47 56 64 67 73 -1 -1 -1 -1 -1 -1 -1
"node01"
3 32 41 47 -1 -1 -1 -1
5 78
19 67
3 64 93
33 73
45 -1
-1 -1
-1 -1
-1 -1
-1
"node02"
56 "node02"
information to be inserted
into parent node
The auxiliary functions have loops that shuffle KLRec records and links between the
old and new BTreeNode records. Though the code is fairly simple in structure, there are
lots of niggling little details relating to which link values end up in the link arrays of the
two nodes.
Operations on BTrees 889
The basic operations are as shown in Figure 24.17 for the case of inserting the new
data in the right hand node; the process is:
Insert in Right
given: a BTreeNode to be split "node_to_split"
an extra KLRec key/data-location pair
an extra link (disk address of a BTreeNode that
is to become a subtree of "node_to_split"
an index specifying where new entry to go
return
Median value to get put into parent node
(nodetosplit.data [MIN])
disk address of the newly created node.
(return_diskpos)
The operations all take place on a temporary BTreeNode created in the stack (as a
local variable of the "insert in right" function). When this has been filled in
successfully, an auxiliary function gets it into the data file (at the end of the file) and
returns its disk address for future reference.
The KLRec with the median valued key, and the address of the extra BTreeNode, are
passed back to the calling level of the recursion. There they have to be inserted into the
parent, which may again split. The process is identical in concept to that shown in
Figures 24.8, 24.9 and 24.10 for the 2-3 trees.
As also illustrated previously for the 2-3 trees, if there is no parent node, a new root
node has to be created for the tree. This process is illustrated in Figure 24.18.
The figure shows a BTree index file that initially has a single full node containing Initially a single full
the keys 20, 33, 45, 56, 67, and 79 (links to data records in the datafile are not shown). BTreeNode
There are no subtrees; so all the BTree structure link fields are -1; and the count field is
6. The BTreeNode is assumed to be 80 bytes in size; starting at byte 4 (after a minimal
housekeeping record that contains solely the byte address of the first record).
Insertion of key 37 will force the record to split as there would now be seven keys
and these nodes have a maximum of six. The three highest keys (56, 67, and 79) are
890 Two more trees
shifted into a new BTreeNode (see Figure 24.18). This would start at byte 84 of the
disk file. The node would be assembled in a structure on the stack and then be written
to the disk file.
75
6 30 40 50 60 70 80 -1 -1 -1 -1 -1 -1 -1
6 30 40 50 60 70 80 -1 -1 -1 -1 -1 -1 -1 Copy1:
largest keys into
right node
80 -1 -1 -1 -1 -1 -1 -1
6 30 40 50 60 70 80 -1 -1 -1 -1 -1 -1 -1 Insertion:
new key into
right node
75 80 -1 -1 -1 -1 -1 -1 -1
Copy2:
6 30 40 50 60 70 80 -1 -1 -1 -1 -1 -1 -1 fill up remainder
of right node
70 75 80 -1 -1 -1 -1 -1 -1 -1
Clean up:
3 30 40 50 -1 -1 -1 -1 fix counts in
both nodes
3 70 75 80 -1 -1 -1 -1
60 Return:
median KLRec, and
disk address of new node
4 6 20 33 45 56 67 79 -1-1-1-1-1-1-1
BTreeNode
Insertion of 37, splits node:
Original node at
3 20 33 37 -1-1-1-1-1-1-1
location 4 in file.
Extra "right" node at
3 56 67 79 -1-1-1-1-1-1-1
location 84 in file.
The three lowest keys (20, 33, 37) would go in the original BTreeNode starting at
byte 4 of the file. This BTreeNode would first be composed in memory and then would
overwrite the existing record on disk.
The median, with key 45, would have to go into a new root BTreeNode. It would
need links down to the original BTreeNode at 4 (link[0], all the keys less than 45) and
the BTreeNode just created at location 84 in the file (link[1], all the keys greater than
45). The new root BTreeNode would get written to the file starting at byte 164.
Finally, the housekeeping data at the front of the file would be updated to hold the
disk address of the new root node.
Remove
As already noted, records are removed in much the same way as in AVL trees. A
recursive routine hunts down through the tree to find the key for the record that is to be
removed. (If the key is not found, the routine returns some failure indication.) As it
892 Two more trees
recurses down through the tree, the routine loads BTreeNodes from the file into the
stack, as in previous examples these define the path back to the root and are the nodes
that may need to be fixed up.
If the key is found in an internal node, data must be promoted from a leaf node (the
implementation in the next section promotes the successor key – the smallest key
greater than the key to be deleted). Once the promotion has been done, the original
promoted data must be deleted. So the routine further recurses down through the tree
until it has the leaf node from where data were taken.
All actual deletions take place on leaf nodes (either because the key to be deleted
was itself in a leaf node, or because a key was taken from a leaf node to replace a key in
an internal node). A deletion reduces the number of keys in the node. The remaining
keys are rearranged to close up the space left by the key that was removed. If there are
still at least MAX/2 keys in the leaf, then essentially everything is finished. The node
can be written back to the file. Recursion can simply unwind (if an internal node was
modified by having data replaced with promoted data, then it gets written to the file
during the unwinding process).
Deficient nodes need The difficulties arise when a node gets left with less than MAX/2 keys. Such a node
"fixing up" violates the BTree conditions (unless it happens to be the root node); it is termed a
"deficient node". A deficient node can't do anything to "fix itself up". All it can do is
report to its parent node. This is achieved, in the recursive procedure, by a node that
detects deficiency setting a return flag; the flag is checked at the next level above as the
recursion unwinds.
If a parent node sees that a child node has become deficient, it can "fix up" that child
node by shifting data from "sibling nodes". There are a couple of different situations
that must be handled. These are illustrated in Figures 24.19 and 24.20 .
Moving data from a Figure 24.19 illustrates a "move" operation. The initial tree (shown in Pane 1 of
sibling node Figure 24.19) would have five nodes (only four are shown). The root has three keys
(300, 400, and 500) and four links down to subtrees. The first subtree (not shown)
would contain the keys less than 300. The second subtree, node n1, contains keys
greater than 300 and less than 400. The third subtree (in link[2]) has the keys
between 400 and 500. The final subtree has the keys greater than 500.
Node n2 has exactly MAX/2 keys. If one of its keys is removed, e.g. 440, it is left
"deficient" (Pane 2 of Figure 24.19). It cannot do anything to fix itself up. But, it can
report to its problem to its parent node (which in this case is the root node).
The root node can examine the sibling nodes (n1 and n3) on either side of the node
that has just become deficient. Node n1 has four keys (> MAX/2). The deficiency in
node n2 could be made up by "transferring a key" from n1. That would in this case
leave both nodes n1 and n2 with exactly MAX/2 keys.
But of course, you can't simply transfer a key across between nodes because the keys
also have to be in order. There has to be a key in the root node such that it is greater
than all keys in its left subtree and smaller than all keys in its right subtree.
Operations on BTrees 893
1 Root
3 300 400 500 ? n1 n2 n3
node
Root node
3
3 300 388 500 ? n1 n2 n3
(n1)
Figure 24.19 Moving data from a sibling to restore BTree property of a "deficient"
BTreeNode.
The "transfer of a key" actually involves taking the largest key in a left sibling (or
smallest key in a right sibling) and using this to replace a key in the parent. The key
from the parent is then used to restore the deficient node to having at least the minimum
number of keys.
In the example shown (Pane 3 of Figure 24.19), the key 388 is taken from node n1
(leaving it with three keys) and moved up into the parent (root) node where it replaces
the key 400. Key 400 is moved down into node n2.
Everything has been restored. The link down "between" keys 300 and 388 ( link[1]
of the root node) leads to all keys in this range (i.e. to node n1 with keys 333, 360 and
385). The link down between keys 388 and 500 leads to the "subtree" (i.e. node n2)
with keys between 388 and 500 (keys 400, 420, and 480). All nodes continue to satisfy
the BTree requirements on their minimum number of keys.
For a "move" or transfer to take place, at least one of the siblings of a deficient node
must have more than MAX/2 keys. Move operations can take a key from the left sibling
or the right sibling of a deficient node. Of course, if the deficient node is on link[0]
894 Two more trees
of its parent then it has no left sibling and a move can only occur from a right sibling. If
the deficient node is in the last subtree link of its parent, only a move from a left sibling
is possible. If a node has both left and right sibling, and both siblings have more than
MAX/2 keys, then either can be used. In such cases, it is best to move a key from the
sibling that has the most keys.
Combine operations Sometimes, both siblings have just the minimum MAX/2 keys. In such situations,
"move" operations cannot be used. Instead, there is another way of "fixing up" the
node. This alternative way combines all the existing keys in the deficient node and one
of its siblings into a single node and ceases to use one of the BTreeNodes in the file.
Figure 24.20 illustrates a combine operation.
Root node
1
3 300 400 500 ? n1 n2 n3
2 Root node
2 300 500 ? n1 n3
(n1)
6 333 360 385 400 420 480 -1 -1 -1 -1
(n2)
discarded
Since a BTreeNode has to be removed, there will be one fewer link down from the
parent. Since all the links in a BTreeNode must either be NULL or links down to
subtrees, this means that the keys in the parent node have to be squeezed up a bit.
There will be exactly MAX/2 keys from a sibling, MAX/2 - 1 keys from the node that
became deficient. These are combined along with one key from the parent to produce a
full BTreeNode.
Operations on BTrees 895
In the example shown in Figure 24.20, the initial state has each of the three nodes
n1, n2, and n3 with three keys. Removal of key 440 from n2 leaves it deficient. The
parent can not shift a key from either n1 or n3 for that would leave the donor deficient.
So, instead, key 400 from the parent, and the remaining keys 420 and 480 are shifted
into n1, filling it up so that it has six keys. (Alternative rearrangements are possible; for
example, key 500 from the parent and the three keys from node n3 could be shifted into
n2 to fill it up and leave n3 empty. It doesn't matter which rearrangement is used.)
The BTreeNode that becomes empty, n2 in Figure 24.20, is "discarded". It is no
longer linked into the tree structure. (In a simple implementation, it becomes "dead
space" in the file; it continues to occupy part of the disk even though it isn't again used.)
As shown in Figure 24.20, the parent node now only has three links down. One goes
to a node (not shown) with keys less than 300. The second is to the full node n2 with
all the keys between 300 and 500. The third link is to the node, n3, with the keys
greater than 500. This leaves the parent node with just two keys.
Since the parent provides one key, it may become "deficient". If the parent becomes
deficient, it has to report this fact to its parent during the unwinding of the recursion. A
move or combine operation would then be necessary at that level. Removal of a key
from a leaf can in some circumstances cause changes at every node on the path back to
the root.
The root node is allowed to have fewer than MAX/2 keys. If the root node is involved
in a combine operation, it ends up with fewer keys. Of course, it is possible to end up
with no keys in the current root! Figure 24.21 illustrates such an occurrence.
1 Root
node 1 100 a b
Root node
In this case, two BTreeNodes get discarded and the tree is "re-rooted" on the node
that originally formed the top of the left subtree. The "housekeeping" information at the
start of the BTree index file would have to be updated to reflect such a changed.
Overall removal Removal of a record thus involves:
process
• finding the leaf node with the key (if the key is in an internal node, a promotion
operation must be done and then the original copy of the promoted data must be
found in a leaf node);
• removal of the key from the leaf;
• unwinding the recursive search, performing "fix ups" on any nodes that become
deficient;
• nodes get "fixed up" by parents performing move or combine operations affecting
the deficient node, a sibling, and the parent node.
Unlike Remove() for the AVL tree which returns the address of a data record in
memory, BTree::Remove(…) simply performs the action. (The data record is in the
data file. It doesn't actually get destroyed; the reference to it in the index is removed
making it inaccessible. Again, the space it occupies becomes "dead space" on disk).
Obviously, many auxiliary functions have to be used in the implementation of
BTree::Remove() . The implementation given in the following section has the
following functions:
Function Remove() provides the interface; its only argument is the key corres- Remove() driver
ponding to the data record that must be deleted. The function has to check that a tree function
exists, someone might try to delete data before any have been entered. If there is a
BTree to search, the Remove() function should load in the root node and set up the call
to the main recursive DoRemove() function. When DoRemove() returns, a check
should be made for the special case of needing a new root node (the situation illustrated
in Figure 24.21).
Remove(key)
if(there is no tree!)
return;
DoRemove(key, root_node);
if (root_node has no keys left)
set housekeeping data to record new root
else
save root_node back on disk
Function DoRemove() is a recursive function with some parallels to the DoAdd() Main recursive
function already considered. It has to recursively chase down through the tree to find DoRemove()
the key. There has to be a check to stop recursion. As recursion unwinds, any
necessary fix up operations are performed.
At each recursive level, the BTreeNode to be worked on has already been loaded at Termination of
the previous level (e.g. Remove() loads the root node). Since the BTreeNode is already recursion and
handling of delete
on the stack, it can be checked for the key; if the key is present the auxiliary
DeleteKeyInNode() function is called to remove it (this will involve a further
recursive call to DoRemove() if the node is an internal node). When the deletion has
been done, function DoRemove() can return. Function DoRemove() returns a flag
indicating whether it has left a node deficient.
If the key was not found, the function has to set up a further recursive call using the Setting up a recursive
appropriate link down to a subtree. If it encounters a "null" link, this means that the call
specified key was not present, in that case the function can simply return. Usually,
there will be a valid disk address in the link. The BTreeNode at this location in the
index file should be loaded onto the stack and the recursive call gets made.
The unwinding process uses the auxiliary function Restore() to fix up any Unwinding recursion
deficient nodes. Other nodes that may have been modified just get written back to the
index file.
Deletion of a key The function DeleteKeyInNode() sorts out how to delete a key. The auxiliary
function DeleteInLeaf() deals with the easy case of deletion in a leaf (leaves are easy
to recognize, all subtree links are "null"). (Deletion in a leaf is trivial; all higher valued
keys are moved one place left so overwriting the key that has to be removed. The count
field for the node is then decremented.) If the node is an internal node, the auxiliary
function Successor() is employed to get the key/location pair of next higher key (the
lowest valued key in the right subtree). Then, DoRemove() must be called recursively
to get rid of the original copy of the promoted key/location pair.
Fixup
Restore() function The Restore() function has to determine whether the deficient node is the leftmost
child (in which case can only merge with right sibling), or the rightmost child (can only
merge with a left sibling), or an intermediate case with both left and right siblings. If
both siblings exist, the merge operation should use the sibling with more keys. The
checks involve loading the sibling nodes into memory.
MergeOrCombineRight(…);
else
if(index == parent.n_data)
MergeOrCombineLeft(…);
else
Load left sibling into a temporary BTreeNode
The "MergeOrCombine Left / Right" functions check the occupancy of the selected MergeOrCombine
sibling node. If it has sufficient keys, a "move" is done; otherwise the more complex
combine operation is performed. The MoveLeft() function is similar in organization.
The MoveRight() operation takes a KLRec (key/data location pair) from the parent MoveRight()
and the rightmost link down from the left node and inserts these into the deficient node
(the BTreeNode does the actual insertion, it moves all existing entries one place right
then inserts the extra data). Then the rightmost KLRec is moved from the left node up
into the parent.
Figures like 24.19 were simplified in that the nodes operated on were leaf nodes.
Often they will be internal nodes with links down to lower levels. Thus, to the right of
key 388 there would be a link down to a subtree with all keys greater than 388 and less
than 400. This link down would have to be moved into link[0] of the deficient node.
The Combine() shifts a KLRec from the parent and remaining data from the other
node.
24.2.4 An implementation
The header file, BTree.h, would contain the declaration for the pure abstract class
KeyedStorableItem along with the main class BTree:
900 Two more trees
class KeyedStorableItem {
public:
virtual ~KeyedStorableItem() { }
virtual long Key(void) const = 0;
virtual void PrintOn(ostream& out) const { }
virtual long DiskSize(void) const = 0;
virtual void ReadFrom(fstream& in) = 0;
virtual void WriteTo(fstream& out) const = 0;
};
A KeyedStorableItem is essentially something that can report its key and transfer
itself to/from a disk file.
The BTree code is parameterized according to the number of keys in each node.
This number needs to be small during testing but should be enlarged for a production
version of the code.
Class BTree makes use of BTreeNode objects and KLRec objects and its functions
have pointers and references of these types. They are basically a detail of the
implementation so their definitions go in the ".cp" implementation file. The types
however must be declared in the header:
#define MIN 3
#define MAX (2 * MIN)
class BTreeNode;
struct KLRec;
Class BTree has a simple public interface. The constructor takes a string that will be
the "base name" for the index and data files (e.g. if the given name is "test", the files
used will be "test.ndx" and "test.dat").
private:
struct HK {
daddr_t fRoot;
long fNumItems;
};
void SaveHK(void);
void LoadHK(void);
The next declarations will be of all the auxiliary functions needed for the Add
operation followed by all the auxiliary functions needed for the Remove operation. The
main recursive DoAdd() function has a complex argument list because it most pass
back data defining any new information that needs to be inserted into a node (the
reference arguments like return_diskpos).
902 Two more trees
Data Members Once all the auxiliary functions have been declared, the data members can be
specified. A BTree object needs somewhere to store its housekeeping information in
memory, two input/output file streams, and records of the size of the files.
HK fHouseKeeping;
fstream fTreeFile;
fstream fDataFile;
long fTreefile_size;
long fDatafile_size;
};
BTree implementation 903
The implementation file would start with the full declarations for struct KLRec (given
earlier) and class BTreeNode:
class BTreeNode {
friend class BTree;
private:
int n_data;
KLRec data[MAX]; /* 0..n_data-1 are filled */
daddr_t links[MAX+1]; /* 0..n_data are filled */
};
Class BTree is made a friend of BTreeNode so that code of member functions of class
BTree can work directly with things like the n_data count or the links[] array.
The member functions of class BTreeNode are all simple (some are just inline functions
in the class declaration). Most of the rest involve iterative loops running through the
entries. Functions InsertInNode() and Search() were both shown earlier.
The InsertAtLeft() and InsertAtRight() functions are used during move
operations when fixing up deficient nodes. The InsertAtLeft() moves existing data
over to make room, adds the new data and increments the counter. The
InsertAtRight() function (not shown) is simpler; it merely adds the extra data in the
first unused positions and then increments the counter.
n_data++;
}
Function ShiftLeft() moves KLRec and link entries leftwards after the leftmost
entry has been removed. It gets used to tidy up a node after its least key has been
removed during a "move" operation required to fix up a deficient sibling. Function
Compress() is used to tidy up a parent node after one of its keys (that identified by
argument index) has been removed as part of a Combine operation.
void BTreeNode::ShiftLeft(void)
{
n_data--;
links[0] = links [1];
for(int i = 0; i < n_data; i++) {
data[i] = data[i+1];
links[i+1] = links[i+2];
}
}
The constructor has to make up the names for the index and data files and then open
them for both input and output. If the files can not be opened, the program should
terminate (you could make the code "throw an exception" instead, see Chapter 29).
cerr << "Sorry, can't open BTree data file." << endl;
exit(1);
}
If the files have zero length, then the program must be creating a new tree;
otherwise, details of current size and location of current root node must be obtained
from the index file. The housekeeping data record needs to be set appropriately:
The destructor, not shown, should save the housekeeping details and then close the
two data files.
These functions are all very similar, so only a few representative examples are shown:
void BTree::SaveHK(void)
{
fTreeFile.seekp(0);
fTreeFile.write((char*)&fHouseKeeping, sizeof(HK));
}
fDataFile.seekg(diskpos);
datarec.ReadFrom(fDataFile);
}
void BTree::Add(KeyedStorableItem& d)
{
KLRec rec_returned;
daddr_t filepos_returned;
int workflag;
DoAdd(d, fHouseKeeping.fRoot, workflag, Make initial call to
rec_returned, filepos_returned); DoAdd() passing root
if(workflag != 0) { node
BTreeNode new_root;
new_root.n_data = 1; Create new root if
new_root.links[0] = fHouseKeeping.fRoot; necessary
new_root.data[0] = rec_returned;
new_root.links [1] = filepos_returned;
fHouseKeeping.fRoot = MakeNewDiskBNode(new_root);
}
}
The recursive DoAdd() function has two input arguments and three output Recursive DoAdd()
arguments. The input arguments are the new KeyedStorableItem (which is passed by
reference) and the disk address of the next BTreeNode that is to be considered. The
output arguments (all naturally passed by reference) are the flag variable (whose setting
will indicate if any fix up operation is needed) together with any KLRec and disk address
that needed to be returned to the caller.
return_flag = 0;
if (filepos == NO_DADDR) {
return_KLRec.fKey = key; Create new data
return_KLRec.fLocation = MakeNewDataRecord(newData); record
return_diskpos = NO_DADDR;
return_flag = 1;
fHouseKeeping.fNumItems +=1;
return;
}
908 Two more trees
if(found) {
Replace old data with daddr_t location = current.data[index].fLocation;
new data SaveDataRecord(newData, location);
return;
}
DoAdd(newData, current.links[index],
need_insert_or_split,
rec_coming_up, diskpos_coming_up);
if (need_insert_or_split == 0)
return;
Node splitting The algorithm for Split() was given earlier. Its implementation is simple as it
merely needs to sort out whether the extra data are to be inserted belong in the existing
(left) node, a new right node, or should be returned to the parent node. The different
SplitInsert functions get called as required. The algorithm for SplitInsertRight()
was given earlier. The example implementation code shown here is for the other two
cases.
newNode.links[0] = extralink;
newNode.n_data = nodetosplit.n_data = MIN;
return_KLRec = extradata;
return_diskpos = MakeNewDiskBNode(newNode); Save new node
}
Function Remove() itself is simple. As explained earlier, it merely needs to set up the
initial recursive call and check for the (uncommon!) case of a need to change the root
when the existing root becomes empty:
BTreeNode root_node;
GetBTreeNode(root_node, fHouseKeeping.fRoot);
if (root_node.n_data == 0)
fHouseKeeping.fRoot = root_node.links [0];
else
SaveBTreeNode(root_node, fHouseKeeping.fRoot);
}
(The housekeeping data don't have to be saved immediately. They are saved by the
destructor that closes the BTree files.)
DoRemove() The DoRemove() function implements the algorithm given earlier:
BTreeNode nextNode;
int repairsneeded;
GetBTreeNode(nextNode, subtree);
Recursive call repairsneeded = DoRemove(bad_key, nextNode);
if (repairsneeded)
Restore(cNode, nextNode, index);
else
SaveBTreeNode(nextNode, subtree);
return cNode.Deficient();
}
The result returned by the function indicates whether the given node has become
deficient. If it is deficient, then the caller will discover that "repairs (are) needed".
DeleteKeyInNode() The DeleteKeyInNode() function shows the details of setting up the mechanism to
find a key to promote followed by the call back to DoRemove() to get rid of the original
copy of this key.
The DeleteKeyInLeaf() function is trivial (shift higher keys left inside node,
decrement count) and so is not shown.
The Successor() function involves an iterative search that runs down the left links Successor
as far as possible. The function returns the KLRec (key/data location pair) for the next
key larger than that in the call to Remove().
The Restore() function (which chooses which sibling gets used to move data or Restore()
combine with the deficient node) is simple to implement from the algorithm given
earlier.
The function MergeOrCombineLeft() illustrates the implementation for one of the MergeOrCombine
two MergeOrCombine functions. The "right" function is similar. Left()
GetBTreeNode(left_nbr, left_daddr);
if(left_nbr.MoreThanMinFilled()) {
MoveRight (parent, left_nbr, deficient, index-1);
SaveBTreeNode(left_nbr, left_daddr);
SaveBTreeNode(deficient, parent.links[index]);
}
else {
912 Two more trees
MoveLeft() The explanation given in the previous section included an algorithm for Move
Right(); this is the implementation for MoveLeft():
Combine() Function Combine() removes all data from the node given as argument right ,
shifting these values along with information from the parent down into the left node:
parent.Compress(index);
}
24.2.5 Testing
The problems involved in testing the BTree code, and their solution, are exactly the
same as for the AVL tree. The BTree algorithms are complex. There are many special
cases. Things like promoting a key from a leaf several levels down in the tree are only
going to occur once the tree has grown quite large. Operations like deleting the current
root node are going to be exceedingly rare. You can't rely on simple interactive testing.
Instead, you use the technique of a driver program that invokes all the basic
operations tens of thousands of times. The driver program has to be able to test the
success of each operation, and terminate the program if it detects something like a
supposedly deleted item being "successfully" found by a later search. A code coverage
BTree testing 913
tool has to be used in conjunction with the driver to make certain that every function
has been executed.
The driver needed to test the BTree can be adapted from that used for the AVL tree.
There are a few changes. For example, insertion of a "duplicate" key is not an error,
instead the old data are overwritten. Data records are not dynamically created in main
memory. Instead, the program can use a single data record in memory filling it in with
data read from the tree during a search operation, or setting its data before an insert.
EXERCISES
1 Complete the implementation and testing of the AVL class.
Versions for different You can imagine actual implementations for different types of data:
data
Instance of outline short larger_of_two(short& thing1, short& thing2)
for short integer data {
if(thing1 > thing2)
return thing1;
else
return thing2;
}
or
or even
which would be practical provided that class Box defines an operator>() function and
permits assignment e.g.:
…
Box b1;
Box b2;
…
Box bigBox = larger_of_two(b1,b2); Assignment operator
needed to use result
The italicised outline version of the function is a "template" for the other special of function
purpose versions. The outline isn't code that can be compiled and used. The code is the
specialized versions made up for each data type.
Here, the different specializations have been hand coded by following the outline
and changing data types as necessary. That is just a tiresome coding exercise,
something that the compiler can easily automate.
Of course, if you want the process automated, you had better be able to explain to
the compiler that a particular piece of text is a "template" that is to be adapted as
needed. This necessitates language extensions.
The obvious first requirement is that you be able to tell the compiler that you want a
"function definition" to be an outline, a template for subsequent specialization. This is
achieved using template specifications.
For the example larger_of_two() function, the C++ template would be:
template<class Thing>
Thing larger_of_two(Thing& thing1, Thing& thing2)
{
if(thing1 > thing2)
return thing1;
else
return thing2;
}
The compiler reading the code first encounters the keyword template. It then knows template keyword
that it is dealing with a declaration (or, as here, a definition) of either a template
function or template class. The compiler suspends the normal processes that it uses to
generate instruction sequences. Even if it is reading a definition, the compiler isn't to
generate code, at least not yet. Instead, it is to build up some internal representation of
the code pattern.
Following the template keyword, you get template parameters enclosed within a < Template parameters
begin bracket and a > end bracket. The outline code will be using some name(s) to
918 Templates
represent data type(s); the example uses the name Thing . The compiler has to be
warned to recognize these names. After all, it is going to have to adapt those parts of
the code that involve data elements of these types.
You can have template functions and classes that are parameterized by more than
one type of data. For example, you might want some "keyed" storage structure
parameterized according to both the type of the data element stored and the type of the
primary key used to compare data elements. (This might allow you to have data
elements whose keys were really strings rather than the usual integers.) Here, multiple
parameters are considered as an "advanced feature". They will not be covered; you will
get to use such templates in your later studies.
Once it has read the template <…> header, the compiler will consume the
following function or class declaration, or (member) function definition. No
instructions get generated. But, the compiler remembers what it has read.
The compiler uses its knowledge about a template function (or class) when it finds that
function (class) being used in the actual code. For example, assuming that class Box
and template function larger_of_two() have both been defined earlier in the file, you
could have code like the following:
int main()
{
Box b1(6,4,7);
Box b2(5,5,8);
…
Need version for Box Box bigbox = larger_of_two(b1,b2);
cout << "The bigger box is "; bigbox.PrintOn(cout);
…
double d1, d2;
cout << "Enter values : "; cin >> d1 >> d2;
…
Need version for cout << "The larger of values entered was : " <<
double larger_of_two(d1, d2) << endl;
…
}
At the point where it finds bigbox = larger_of_two(b1,b2), the compiler notes that it has
to generate the version of the function that works for boxes. Similarly, when it gets to
the output statement later on, it notes that it must generate a version of the function that
works for doubles.
When it reaches the end of the file, it "instantiates" the various versions of the
template.
Figure 25.1 illustrates the basic idea (and is close to the actual mechanism for some
compilers).
Template instantiation 919
… …
long Box::Size() { … }; long Box::Size() { … };
… …
double larger_of_two(const
double& thing1, const double&
thing2)
Instantiated {
if(thing1 > thing2)
versions of return thing1;
else
template return thing2;
}
The scheme shown in Figure 25.1 assumes that the compiler does a preliminary
check through the code looking for use of templates. When it encounters uses of a
template function, the compiler generates a declaration of a version specialized for the
particular types of data used as arguments. When it gets to the end of the file, it uses
the template to generate the specialized function, substituting the appropriate type (e.g.
double) for the parametric type (Thing).
920 Templates
The specialized version(s) of the function can then be processed using the normal
instruction generation mechanisms.
Watch out for Templates are relatively new and the developers of compilers and linkers are still
compiler specific sorting out the best ways of dealing with them. The scheme shown in Figure 25.1 is
mechanisms
fine if the entire program, with all its classes and template functions, is defined in a
single file.
The situation is a little more complex when you have a program that is made up
from many separately compiled and linked files. You may get references to template
functions in several different files, but you probably don't want several different copies
of " double larger_of_two(const double& d1, const double& d2)" – one copy
for each file where this "function" is implicitly used.
Compiler "pragmas" Compiler and linker systems have different ways of dealing with such problems.
to control You may find that you have to use special compiler directives ("pragmas") to specify
instantiation
when and how you want template functions (and classes) to be instantiated. These
directives are system's dependent. You will have to read the reference manuals for your
IDE to see what mechanism you can use.
Another problem that you may find with your system is illustrated by the following:
template<class Whatsit>
void SortFun(Whatsit stuff[], int num_whatsists)
{
// a standard sort algorithm
…
}
int main()
{
long array1[10], array2[12], array3[17];
int n1, n2, n3;
// Get number of elements and data for three arrays
…
// Sort array1
SortFun(array1, n1);
…
// Deal with array2
SortFun(array2, n2);
…
}
Although in each case you would be sorting an array of long integers, the template
instantiation mechanism might give you three versions of the code. One would be
specialized for sorting an array of long integers with ten elements; the second would
handle arrays with twelve elements; while the third would be specialized for arrays with
seventeen elements.
This doesn't matter that much, it just wastes a little memory.
Templates 921
Why does the compiler have to generate the specialized versions of template?
You should realize while that the code for the specialized data types may embody a
similar pattern, the instruction sequences cannot be identical. All versions of the
example larger_of_two() function take two address arguments (as previously
explained references are handled internally as addresses). Thus, all versions will start
with something like the following instruction sequence where the addresses of the two
data elements are loaded into registers:
After that they have differences. For example, the generated code for the version with
short integers will be something like:
The code for the version specialized for doubles would be similar except that it would
be working with 8-byte (or larger) data elements that get loaded into "floating point
registers" and it would be a floating point comparison instruction that would be used.
There would be more significant differences in the case of Boxes . There the
generated code would be something like:
The idea maybe the same, but the realization, the instantiation differs.
922 Templates
The Quicksort function group from Chapter 13 provides a slightly more realistic
example than larger_of_two().
The Quicksort function illustrated earlier made use of two auxiliary functions.
There was a function Partition() that shuffled data elements in a particular subarray
and an auxiliary SelectionSort() function that was used when the subarrays were
smaller than a given limit size.
The version in Chapter 13 specified integer data. But we can easily make it more
general purpose by just recasting all the functions as templates whose arguments (and
local temporary variables) are of some generic type Data:
template<class Data>
Template void SelectionSort(Data d[], int left, int right)
SelectionSort {
for(int i = left; i < right; i++) {
int min = i;
for(int j=i+1; j<= right; j++)
if(d[j] < d[min]) min = j;
Data temp = d[min];
d[min] = d[i];
d[i] = temp;
}
}
template<class Data>
Template Partition int Partition(Data d[], int left, int right)
function {
Data val =d[left];
int lm = left-1;
int rm = right+1;
for(;;) {
do
rm--;
while (d[rm] > val);
do
lm++;
while( d[lm] < val);
if(lm<rm) {
Data tempr = d[rm];
d[rm] = d[lm];
d[lm] = tempr;
}
else
A template version of Quicksort 923
return rm;
}
}
template<class Data>
void Quicksort(Data d[], int left, int right) Template QuickSort
{
if(left < (right-kSMALL_ENOUGH)) {
int split_pt = Partition(d,left, right);
Quicksort(d, left, split_pt);
Quicksort(d, split_pt+1, right);
}
else SelectionSort(d, left, right);
}
The following test program instantiates two versions of the Quicksort group:
int main()
{
int i;
for(i=0;i <kBIG;i++)
data[i] = rand() % 15000;
for(i=0;i <kBIG*2;i++)
data2[i] = (rand() % 30000)/7.0;
This test program runs successfully producing its two lots of sorted output:
Beware of silly errors But you must be a little bit careful. How about the following program:
char *msgs[10] = {
"Hello World",
"Abracadabra",
"2,4,6,8 who do we appreciate C++ C++ C++",
"NO",
"Zanzibar",
"Zurich",
"Mystery",
"help!",
"!pleh",
"tenth"
};
int main()
{
int i;
for(i = 0; i < 10; i++)
cout << msgs[i] << ", " << endl;
cout << "----" << endl;
Quicksort(msgs, 0,9);
for(i = 0; i < 10; i++)
cout << msgs[i] << ", " << endl;
cout << "----" << endl;
return 0;
}
Hello World,
Abracadabra,
2,4,6,8 who do we appreciate C++ C++ C++,
NO,
Zanzibar,
Zurich,
Mystery,
help!,
!pleh,
"tenth",
A template version of Quicksort 925
----
Hello World,
Abracadabra,
2,4,6,8 who do we appreciate C++ C++ C++,
NO,
Zanzibar,
Zurich,
Mystery,
help!,
!pleh,
"tenth",
----
Problems! They aren't sorted; they are still in the original order! Has the sort routine A sort that didn't
got a bug in it? Maybe there is something wrong in the SelectionSort() part as its sort?
the only bit used with this small set of data.
Another trial, one that might be more informative as to the problem: What is really
happening?
int main()
{
int i;
char *msgs2[10];
msgs2[0] = msgs[3]; msgs2[1] = msgs[2]; Shuffle the data
msgs2[2] = msgs[8]; msgs2[3] = msgs[9];
msgs2[4] = msgs[7]; msgs2[5] = msgs[0];
msgs2[6] = msgs[1]; msgs2[7] = msgs[4];
msgs2[8] = msgs[6]; msgs2[9] = msgs[5];
for(i = 0; i < 10; i++)
cout << msgs2[i] << ", " << endl;
cout << "----" << endl;
Quicksort(msgs2, 0,9);
for(i = 0; i < 10; i++)
cout << msgs2[i] << ", " << endl;
return 0;
}
NO,
2,4,6,8 who do we appreciate C++ C++ C++,
!pleh,
tenth,
help!,
Hello World,
Abracadabra,
Zanzibar,
Mystery,
926 Templates
Zurich,
----
Sorted back into the Hello World,
order of definition! Abracadabra,
2,4,6,8 who do we appreciate C++ C++ C++,
NO,
Zanzibar,
Zurich,
Mystery,
help!,
!pleh,
tenth,
This time the call to Quicksort() did result in some actions. The data in the array
msgs2[] have been rearranged. In fact, they are back in exactly the same order as they
were in the definition of the array msgs[].
What has happened is that the Quicksort group template has been instantiated to sort
an array of pointers to characters. No great problems there. A pointer to character is
internally the same as an unsigned long. You can certainly assign unsigned longs, and
you can also compare them.
Sorting by location in We've been sorting the strings according to where they are stored in memory, and
memory! not according to their content! Don't get caught making silly errors like this.
You have to set up some extra support structure to get those strings sorted. The
program would have to be something like the following code. Here a struct Str has
been defined with associated operator>() and operator<() functions. A Str struct
just owns a char* pointer; the comparison functions use the standard strcmp()
function from the string library to compare the character strings that can be accessed via
the pointers. A global operator<<(ostream&, …) function had also to be defined to
allow convenient stream output.
struct Str {
char* mptr;
int operator<(const Str& other) const
{ return strcmp(mptr, other.mptr) < 0; }
int operator>(const Str& other) const
{ return strcmp(mptr, other.mptr) > 0; }
};
Str msgs[10] = {
"Hello World",
…
"tenth"
};
A template version of Quicksort 927
int main()
{
int i;
Quicksort(msgs, 0,9);
for(i = 0; i < 10; i++)
cout << msgs[i] << ", " << endl;
return 0;
}
!pleh,
2,4,6,8 who do we appreciate C++ C++ C++,
Abracadabra,
Hello World,
Mystery,
NO,
Zanzibar,
Zurich,
help!,
tenth,
which is sorted. (In the standard ASCII collation sequence for characters, digits come
before letters, upper case letter come before lower case letters.)
Some languages, e.g. Pascal, have "bounded arrays". A programmer can define an
array specifying both a lower bound and an upper bound for the index used to access
the array. Subsequently, every access to the array is checked to make sure that the
requested element exists. If an array has been declared as having an index range 9 to 17
inclusive, attempts to access elements 8 or 18 etc will result in the program being
terminated with an "array bounds violation" error.
Such a structure is easy to model using a template Array class. We define a
template that is parameterized by the type of data element that is to be stored in the
array. Subsequently, we can have arrays of characters, arrays of integers, or arrays of
any user defined structures.
What does an Array own? It will have to remember the "bounds" that are supposed An Array owns …
to exist; it needs these to check accesses. It will probably be convenient to store the
number of elements, although this could information could always be recomputed An
Array needs some space for the actual stored data. We will allocate this space on the
heap, and so have a pointer data member in the Array. The space will be defined as an
928 Templates
array of "things" (the type parameter for the class). Of course it is going to have to be
accessed as a zero based array, so an Array will have to adjust the subscript.
An Array does … An Array object will probably be asked to report its lower and upper subscript
bounds and the number of elements that it owns; the class should provide access
functions for these data. The main thing that an Array does is respond to the [] (array
subscripting) operator.
The [] is just another operator. We can redefine operator[] to suit the needs of
the class. We seem to have two ways of using elements of an array (e.g. Counts – an
array of integers), as "right values":
n = Counts[6];
Counts[2] += 10;
(The "left value" and "right value" terminology refers to the position of the reference to
an array element relative to an assignment operator.) When used as a "right value" we
want the value in the array element; when used as a "left value" we want the array
element itself.
Reference to an array Both these uses are accommodated if we define operator[] as returning a
element reference to an array element. The compiler will sort out from context whether we need
a copy of the value in that element or whether we are trying to change the element.
Assertions How should we catch subscripting errors? The easiest way to reproduce the
behaviour of a Pascal bounded array will be to have assert() macros in the accessing
function that verify the subscript is within bounds. The program will terminate with an
assertion error if an array is out of bounds.
template<class DType>
class Array {
public:
Array(int low_bnd, int high_bnd);
~Array();
As in the case of a template function, the declaration starts with the keyword template,
and then in < > brackets, we get the parameter specification. This template is
parameterized by the type DType. This will represent the type of data stored in the array
– int, char, Aircraft, or whatever.
The class has a constructor that takes the lower and upper bounds, and a destructor. Constructor
Actual classes based on this template will be "resource managers" – they create a
structure in the heap to store their data elements. Consequently, a destructor is needed
to get rid of this structure when an Array object is no longer required.
There are three simple access functions, Size() etc, and an extra – PrintOn(). Access functions
This may not really belong, but it is useful for debugging. Function PrintOn() will
output the contents of the array; its implementation will rely on the stored data elements
being streamable.
The operator[]() function returns a "reference to a DType " (this will be a Array subscripting
reference to integer, or reference to character, or reference to Aircraft depending on
the type used to instantiate the template).
The declaration declares the copy constructor and operator=() function as private. Copying of array
This has been done to prevent copying and assignment of arrays. These functions will disallowed
not be defined. They have been declared as private to get the compiler to disallow
array assignment operations.
You could if you preferred make these functions public and provide definitions for
them. The definitions would involve duplicating the data array of an existing object.
The data members are just the obvious integer fields needed to hold the bounds and
a pointer to DType (really, a pointer to an array of DType).
All of the member functions must be defined as template functions so their definitions
will start with the keyword template and the parameter(s).
The parameter must also be repeated with each use of the scope qualifier operator
(::). We aren't simply defining the Size() or operator[]() functions of class Array
because there could be several class Arrays (one for integers, one for Aircraft etc).
We are defining the Size() or operator[]() functions of the class Array that has
been parameterized for a particular data type. Consequently, the parameter must
modify the class name.
930 Templates
Destructor and The destructor simply gets rid of the storage array. The simple access functions are
simple access all similar, only Size() is shown:
functions
template<class DType>
Array<DType>::~Array()
{
delete [] fData;
}
template<class DType>
int Array<DType>::Size() const
{
return fsize;
}
Output function Function PrintOn() simply runs through the array outputting the values. The
statement os << fData[i] has implications with respect to the instantiation of the
array. Suppose you had class Point defined and wanted a bounded array of instances
of class Point . When the compiler does the work of instantiating the Array for
Points, it will note the need for a function operator<<(ostream&, const Point&).
If this function is not defined, the compiler will refuse to instantiate the template. The
error message that you get may not be particularly clear. If you get errors when
instantiating a template class, start by checking that the data type used as a parameter
does support all required operations.
template<class DType>
void Array<DType>::PrintOn(ostream& os)
{
for(int i=0;i<fsize;i++) {
int j = i + flow;
os << "[" << setw(4) << j << "]\t:";
os << fData[i];
Template class Bounded Array 931
os << endl;
}
}
The operator[]() function sounds terrible but is actually quite simple: Array indexing
template<class DType>
DType& Array<DType>::operator[](int ndx)
{
assert(ndx >= flow);
assert(ndx <= fhigh);
return fData[j];
}
The function starts by using assertions to check the index given against the bounds. If
the index is in range, it is remapped from [flow … fhigh] to a zero based array and
then used to index the fData storage structure. The function simply returns fData[j];
the compiler knows that the return type is a reference and it sorts out from left/right
context whether it should really be using an address (for left value) or contents (for right
value).
The following code fragments illustrate simple applications using the template Array Code fragments using
class. The first specifies that it wants an Array that holds integers. Note the form of Array template
the declaration for Array a; it is an Array, parameterized by the type int.
#include <iostream.h>
#include <iomanip.h>
#include <ctype.h>
#include <assert.h>
template<class DType>
class Array {
public:
…
};
int main()
{
932 Templates
a.PrintOn(cout);
return 0;
}
The second example uses an Array<int> for counters that hold letter frequencies:
int main()
{
Array<int> letter_counts('a', 'z');
for(char ch = 'a'; ch <= 'z'; ch++)
letter_counts[ch] = 0;
cout << "Enter some text,terminate with '.'" << endl;
cin.get(ch);
while(ch != '.') {
if(isalpha(ch)) {
ch = tolower(ch);
letter_counts[ch] ++;
}
cin.get(ch);
}
(The character constants 'a' and 'z' are perfectly respectable integers and can be used as
the low, high arguments needed when constructing the array.)
These two examples have both assumed that the template class is declared, and its
member functions are defined, in the same file as the template is employed. This
simplifies things, there are no problems about where the template should be
instantiated. As in the case of template functions, things get a bit more complex if your
program uses multiple files. The mechanisms used by your IDE for instantiating and
linking with template classes will be explained in its reference manual..
The collection classes from Chapter 21 are all candidates for reworking as template
classes. The following is an illustrative reworking of class Queue . The Queue in
Chapter 21 was a "circular buffer" employing a fixed sized array to hold the queued
items.
The version in Chapter 21 held pointers to separately allocated data structures. So,
its Append() function took a void* pointer argument, its fData array was an array of
pointers, and function First() returned a void*.
The new version is parameterized with respect to the data type stored. The Choice of data copies
declaration specifies a Queue of Objs, and an Obj is anything you want it to be. We or data pointers
can still have a queue that holds pointers. For example, if we already have separately
allocated Aircraft objects existing in the heap we can ask for a queue that makes Obj
an Aircraft* pointer. But we can have queues that hold copies of the actual data
rather than pointers to separate data. When working with something like Aircraft
objects, it is probably best to have a queue that uses pointers; but if the data are of a
simpler data type, it may be preferable to make copies of the data elements and store
these copies in the Queue 's array. Thus, it is possible to have a queue of characters
(which wouldn't have been very practical in the other scheme because of the substantial
overhead of creating single characters as individual structures in the heap).
Another advantage of this template version as compared with the original is that the Type safety
compiler can do more type checking. Because the original version of
Queue::Append() just wanted a void* argument, any address was good enough. This
is a potential source of error. The compiler won't complain if you mix up all addresses
of different things (char*, Aircraft*, addresses of functions, addresses of automatic
variables), they are all just as good when all that is needed is a void*.
When something was removed from that queue, it was returned as a void* pointer
that then had to be typecast to a useable type. The code simply had to assume that the
cast was appropriate. But if different types of objects were put into the queue, all sorts
of problems would arise when they were removed and used.
This version has compile time checking. The Append() function wants an Obj –
that is an Obj that matches the type of the instantiated queue. If you ask for a queue of
Aircraft* pointers, the only thing that you can append is an Aircraft*; when you
take something from that queue it will be an Aircraft* and the compiler can check
that you use it as such. Similarly, a queue defined as a queue of char will only allow
you to append a char value and will give you back a char. Such static type checking
can eliminate many errors that result from careless coding.
The actual declaration of the template is as follows:
template<class Obj>
class Queue {
public:
Queue();
private:
Obj fdata[QSIZE];
int fp;
int fg;
int fcount;
};
template<class Obj>
inline int Queue<Obj>::Full() const {
return fcount == QSIZE;
}
template<class Obj>
inline int Queue<Obj>::Empty() const {
return fcount == 0;
}
The Append() and First() functions use assert() macros to verify correct usage:
template<class Obj>
void Queue<Obj>::Append(Obj newobj)
{
assert(!Full());
fdata[fp] = newobj;
fp++;
if(fp == QSIZE)
fp = 0;
fcount++;
return;
}
template<class Obj>
Obj Queue<Obj>::First(void)
{
assert(!Empty());
Template class Queue 935
The following is an example code fragment that will cause the compiler to generate
an instance of the template specialized for storing char values:
int main()
{
Queue<char> aQ;
for(int i=0; i < 100; i++) {
int r = rand() & 1;
if(r) {
if(!aQ.Full()) {
char ch = 'a' + (rand() % 26);
aQ.Append(ch);
cout << char(ch - 'a' + 'A');
}
}
else {
if(!aQ.Empty()) cout << aQ.First();
}
}
return 0;
}
26
26 Exceptions
With class based programs, you spend a lot of time developing classes that are intended
to be used in many different programs. Your classes must be reliable. After all, no one
else is going to use your classes if they crash the program as soon as they run in to data
that incorrect, or attempt some input or output transfer that fails.
You should aim to build defences into the code of your classes so that bad data won't
cause disasters. So, how might you get given bad data and what kind of defence can
you build against such an eventuality?
"Bad data" most frequently take the form of inappropriate arguments passed in a call
to a member function that is to be executed by an instance of one of your classes, or, if
your objects interact directly with the user, will take the form of inappropriate data
directly input by the user.
You can do something about these relatively simple problems. Arguments passed to Simple checks on
a function can be checked using an assert() macro. Thus, you can check that the data
pointers supposed to reference data structures are not NULL, and you can refuse to do
things like sort an array with -1234 data elements (e.g. assert(ptr1 != NULL);
assert((n>1) && (n<SHRT_MAX))). Data input can be handled through loops that
only terminate if the values entered are appropriate; if a value is out of range, the loop is
again cycled with the user prompted to re-enter the data.
Such basic checks help. Careless errors in the code that invokes your functions are
soon caught by assert() macros. Loops that validate data are appropriate for
interactive input (though they can cause problems if, later, the input is changed to come
from a file).
But what happens when things go really wrong? For example, you hit "end of file"
when trying to read input, or you are given a valid pointer but the referenced data
structure doesn't contain the right information, or you are given a filename but you find
that the file does not exist.
Code like:
or
catches the error and terminates via the "assertion failed" mechanism or the explicit
exit() call.
Terminating the In general, terminating is about all you can do if you have to handle an error at the
program – maybe the point that it is detected.
only thing to do?
You don't know the context of the call to X::GetParams() . It might be an
interactive program run directly from the "console" window; in that case you could try
to handle the problem by asking for another filename. But you can't assume that you
can communicate with the user. Function X::GetParams() might be being called from
some non-interactive system that has read all its data from an input file and is meant to
be left running to generate output in a file that gets examined later.
But other parts of You don't know the context, but the person writing the calling code does. The caller
code might have been may know that the filename will have just been entered interactively and so prompting
able to deal with
error! for a new name is appropriate when file with a given name cannot be found.
Alternatively, the caller may know that a "params" file should be used if it can be
opened and read successfully; if not, a set of "default parameters" should be used in
subsequent calculations.
Error detection and It is a general problem. One part of the code can detect an error. Knowledge of
error handling what to do about the error can only exist in some separate part of the code. The only
separated by long call
chain connection between the two parts of the code is the call chain; the code that can detect
an error will have been called, directly or indirectly, from the code that knows what to
do about errors.
The call chain between the two parts of the code may be quite lengthy:
While that might be an extreme case, there is often a fairly lengthy chain of calls
separating originator and final routine. (For example, when you use menu File/Save in
a Mac or PC application, the call chain will be something like
Application::HandleEvent(), View::HandleMenu() , Document::Handle-
Menu(), Document::DoSaveMenu(), Document::DoSave() , Document::Open-
File(); error detection would be in OpenFile(), error handling might be part way up
the chain at DoSaveMenu().)
If you don't want the final function in the call chain, e.g. function Z, to just terminate Pass an error report
the program, you have to have a scheme whereby this function arranges for a back up the call
chain
description of the error to be passed back up the call chain to the point where the error
can be handled.
The error report has to be considered by each of the calling functions as the call
chain is unwound. The error handling code might be at any point in the chain.
Intermediate functions, those between the error handler and the error detector, might not
be able to resolve the error condition but they might still need to do some work if an
error occurs.
To see why intermediate functions might need to do work even when they can't Need to tidy up as
resolve an error, consider a slight elaboration of the X::GetParams() example. The call chain unwinds.
X::GetParams() function now creates two separately allocated, semi-permanent data
structures. These are a ParamBlock structure that gets created in the heap and whose
address is stored in the assumed global pointer gParamData, and some form of "file
descriptor" structure that gets built in the system's area of memory as part of the process
of "constructing" the ifstream. Once it has built its structures, GetParams() calls
X::LoadParamBlock() to read the data:
some code that deals with cases where can't open the file
If LoadParamBlock() runs into difficulties, like unexpected end of file (for some
reason, the file contains fewer data items than it really should), it will need to pass back
an error report.
The function GetParams() can't deal with the problem; after all, the only solution
will be to use another file that has a complete set of parameters. So X::GetParams()
has to pass the error report back to the function from where it was called.
However, if X::GetParams() simply returns we may be left with two "orphaned"
data structures. The file might not get closed if there is an abnormal return from the
function. The partly filled ParamBlock structure won't be of any use but it will remain
in the heap.
The mechanism for passing back error reports has to let intermediate functions get a
chance to do special processing (e.g. delete that useless ParamBlock structure) and has
to make sure that all normal exit code is carried out (like calling destructors for
automatic objects – so getting the file closed).
Describing errors A function that hopes to deal with errors reported by a called function has to
that can be handled associate with the call a description of the errors that can be handled. Each kind of
error that can be handled may have a different block of handling code.
Code to handle Obviously, the creation of an error report, the unwinding of the call chain, and the
exceptional eventual handling of the error report can't use the normal "return from function"
conditions
mechanisms. None of the functions has defined its return type as "error report". None
of the intermediate functions is terminating normally; some may have to execute special
code as part of the unwinding process.
The error handling mechanism is going to need additional code put into the
functions; code that only gets executed under exceptional conditions. There has to be
code to create an error report, code to note that a function call has terminated
abnormally with an error report, code to tidy up and pass the error report back up the
call chain, code to check an error report to determine whether it is one that should be
dealt with, and code to deal with chosen errors.
The C language always provided a form of "emergency exit" from a function that
could let you escape from problems by jumping back up the call chain to a point where
you could deal with an error (identified by an integer code). But this setjmp(),
longjmp() mechanism isn't satisfactory as it really is an emergency exit. Intermediate
functions between the error detector and the error handler don't get given any
opportunity to tidy up (e.g. close files, delete unwanted dynamic structures).
Language extensions A programming language that wants to support this form of error handling properly
needed has to have extensions that allow the programmer to specify the source for all this
"exception handling" code. The implementation has to arrange that error reports pass
back up the call chain giving each function a chance to tidy up even if they can't
actually deal with the error.
C++ has added such "exception handling". (This is a relatively recent addition, early
1990s, and may not be available in older versions of the compilers for some of the
IDEs). Exception handling code uses three new constructs, throw, try, and catch:
Introduction 941
• throw
Used in the function that detects an error. This function creates an error report and
"throws" it back up the call chain.
• try
Used in a function that is setting up a call to code where it can anticipate that errors
might occur. The try keyword introduces a block of code (maybe a single
function call, maybe several statements and function calls). A try block has one or
more catch blocks associated with it.
• catch
The catch keyword specifies the type of error report handled and introduces a
block of code that has to be executed when such an error report gets "thrown" back
at the current function. The code in this block may clear up the problem (e.g. by
substituting the "default parameters" for parameters from a bad file). Alternatively,
this code may simply do special case tidying up, e.g. deleting a dynamic data
structure, before again "throwing" the error further back up the call chain.
Rather than "error report", the usual terminology is "exception". A function may throw
an exception. Another function may catch an exception.
As just explained, the throw construct allows the programmer to create an exception
(error report) and send it back up the call chain. The try construct sets up a call to code
that may fail and result in an exception being generated. The catch construct specifies
code that handles particular exceptions.
The normal arrangement of code using exceptions is as follows. First, there is the Driver with try {… }
main driver function that is organizing things and calling the code that may fail: and catch { … }
void GetSetup()
{
// Create objects ...
gX = new X;
…
// Load parameters and perform related initialization,
// if this process fails substitute default params
try {
LoadParams();
…
}
catch (Error e) {
// File not present, or data no good,
// copy the default params
gParamData = new ParamBlock(defaultblock);
…
942 Exceptions
This driver has the try clause bracketing the call that may fail and the following catch
clause(s). This one is set to catch all exceptions of type Error. Details of the exception
can be found in the variable e and can be used in the code that deals with the problem.
Intermediate There may be intermediate functions:
functions
void LoadParams()
{
NameStuff n;
n.GetFileName();
…
gX->GetParams(n.Name());
…
return;
}
As this LoadParams() function has no try catch clauses, all exceptions thrown by
functions that it calls get passed back to the calling GetSetup() function.
As always, automatics like NameStuff are initialized by their constructors on entry
and their destructors, if any, are executed on exit from the function. If NameStuff is a
resource manager, e.g. it allocates space on the heap for a name, it will have a real
destructor that gets rid of that heap space. The compiler will insert a call to the
destructor just before the final return. If a compiler is generating code that allows for
exceptions (most do), then in addition there will be code that calls the destructor if an
exception passes back through this intermediate LoadParams() function.
Functions that throw The X::GetParams() function may itself throw exceptions, e.g. if it cannot open its
exceptions file:
if(!in.good())
throw(Error(Error::eBADFILE));
It may have to handle exceptions thrown by functions it calls. If it cannot fully resolve
the problem, it re-throws the same exception:
C++'s Exception Mechanism 943
But what are these "exceptions" like the Errors in these examples? Things to throw
Essentially, they can be any kind of data element from a simple int to a complex
struct. Often it is worthwhile defining a very simple class:
class Error{
public:
enum ErrorType { eBADFILE, eBADCONTENT };
Error(ErrorType whatsbad) { this->fWhy = whatsbad; }
ErrorType Type() { return this->fWhy; }
private:
ErrorType fWhy;
};
(You might also want a Print() function that could be used to output details of
errors.)
You might implement your code for class X using exceptions but your colleagues Suppose there are no
using your code might not set up their code to catch exceptions. Your code finds a file catchers
that want open and so throws an eBADFILE Error. What now?
944 Exceptions
The exception will eventually get back to main() . At that point, it causes the
program to terminate. It is a bit like an assertion failing. The program finishes with a
system generated error message describing an uncaught exception.
Class Number, presented in Section 19.3, had to deal with errors like bad inputs for its
string constructor (e.g. "14l9O70"), divide by zero, and other operations that could lead
to overflow. Such errors were handled by printing an error message and then
terminating the program by a call to exit(). The class could have used exceptions
instead. A program using Numbers might not be able to continue if overflow occurred,
but an error like a bad input might be capable of resolution.
We could define a simple class whose instances can be "thrown" if a problem is
found when executing a member function of class Number. This NumProblem class
could be defined as follows:
class NumProblem {
public:
enum NProblemType
{ eOVERFLOW, eBADINPUT, eZERODIVIDE };
NumProblem(NProblemType whatsbad);
void PrintOn(ostream& out);
private:
NProblemType fWhy;
};
NumProblem::NumProblem(NProblemType whatsbad)
{ fWhy = whatsbad; }
Some of the previously defined member functions of class Number would need to be
modified. For example, the constructor that creates a number from a string would now
become:
Number::Number(char numstr[])
{
Example: Exceptions for class Number 945
(The version in Section 19.3 had a call to exit() where this code throws the "bad
input" NumProblem exception.)
Similarly, the Number::DoAdd() function would now throw an exception if the sum
of two numbers became too large:
return Result;
}
946 Exceptions
Code using instances of class Number can take advantage of these extensions. If the
programmer expects that problems might occur, and knows what to do when things do
go wrong, then the code can be written using try { ... } and catch (...) { ...
}. So, for example, one can have an input routine that handles cases where a Number
can't be successfully constructed from a given string:
Number GetInput()
{
int OK = 0;
Number n1;
while(!OK) {
try {
char buff[120];
cout << "Enter number : ";
cin.getline(buff,119,'\n');
Number n2(buff);
OK = 1;
n1 = n2;
}
catch (NumProblem p) {
cout << "Exception caught" << endl;
p.PrintOn(cout);
}
}
return n1;
}
int main()
{
Number n1;
Number n2;
n1 = GetInput();
n2 = GetInput();
Number n3;
n3 = n1.Divide(n2);
eturn 0;
}
$ Numbers
Enter number : 71O
Exception caught
Bad input
Enter number : 7l0
Exception caught
Bad input
Enter number : 710
Enter number : 5
n1 710
n2 5
n3 142
The erroneous data, characters 'l' (ell) and 'O' (oh), cause exceptions to be thrown.
These are handled in the code in GetNumber().
The main code doesn't have any try {} catch { } constructs around the
calculations. If a calculation error results in an exception, this will cause the program to
terminate:
$ Numbers
Enter number : 69
Enter number : 0
Run-time exception error; current exception: NumProblem
No handler for exception.
Abort - core dumped
The input values of course cause a "zero divide" NumProblem exception to be thrown.
(These examples were run on a Unix system. The available versions of the Symantec
compiler did not support exceptions.)
If you intend that your function or class member function should throw exceptions, then
you had better warn those who will use your code.
A function (member function) declaration can include "exception specifications" as
part of its prototype. These specifications should be repeated in the function definition.
An exception specification lists the types of exception that the function will throw.
Thus, for class Number, we could warn potential users that some member functions Exception
could result in NumProblem exceptions being thrown: specification
class Number {
public:
Number();
Number(long);
948 Exceptions
…
};
The constructor that uses the string throws exceptions if it gets bad input. Additions,
subtraction, and multiplication operations throw exceptions if they get overflow.
Division throws an exception if the divisor is zero.
The function definitions repeat the specifications:
Exception specifications are appropriate for functions that don't involve calls to
other parts of the system. All the functions of class Number are "closed"; there are no
calls to functions from other classes. Consequently, the author of class Number knows
exactly what exceptions may get thrown and can provide the specifications.
Don't always try to You can not always provide a complete specification. For example, consider class
specify BTree that stores instances of any concrete class derived from some abstract
KeyedStorable class. The author of class BTree may foresee some difficulties (e.g.
disk transfer failures) and so may define some DiskProblem exceptions and specify
these in the class declaration:
However, problems with the disk are not the only ones that may occur. The code for
Insert() will ask the KeyedStorable data item to return its key or maybe compare its
key with that of some other KeyedStorable item. Now the specific concrete subclass
derived from KeyedStorable might throw a BadKey exception if there is something
wrong with the key of a record.
Exception specifications 949
This BadKey exception will come rumbling back up the call chain until it tries to
terminate execution of BTree::Insert. The implementation of exceptions is supposed
to check what is going on. It knows that BTree::Insert() promises to throw only
DiskProblem exceptions. Now it finds itself confronted with something that claims to
be a BadKey exception.
In this case, the exception handling mechanism calls unexpected() (a global run- unexpected()
time function that terminates the program).
If you don't specify the exceptions thrown by a function, any exception can pass
back through that function to be handled at some higher level. An exception
specification is a filter. Only exceptions of the specified types are allowed to be passed
on; others result in program termination.
In general, if you are implementing something concrete and closed like class Number
then you should use exception specification. If you are implementing something like
class BTree, which depends indirectly on other code, then you should not use exception
specifications.
Of course, the documentation that you provide for your function or class should Remember the
describe any related exceptions irrespective of whether the (member) function documentation
declarations contain explicit exception specifications.
950 Exceptions
952 Supermarket example
Time period The supermarket opens its doors at 8 a.m.. Once opened, the doors can be pictured
27.1 BACKGROUND AND PROGRAM SPECIFICATION simulated as delivering another batch of frantic shoppers every minute. This continues until
Helping the supermarket's manager the manager closes the doors, which will happen at the first check time after the
supermarket's nominal closing time of 7 p.m.. All customers then in the
The manager of a large supermarket wants to explore possible policies regarding supermarket are allowed to complete their shopping, and must queue to pay for
the number of checkout lanes that are necessary to provide a satisfactory service to their purchases before leaving. During this closing period, the manager checks the
customers. state of the shop every 5 minutes and closes any checkouts that have become idle.
The number of customers, and the volume of their purchases, varies quite The supermarket finally closes when all customers have been served. The closing
markedly at different times of day and the number of open checkout lines should be time should be displayed along with the statistics that have been gathered to
adjusted to match the demand. Customers will switch to rival stores if they find characterize the simulation run.
Checkout numbers The supermarket has a maximum of 40 checkout lanes, but the maximum
that they have to queue too long in this supermarket so, at times, it may be
necessary to have a large number of checkouts open. However, the manager must number to be used in any run of the simulation is one of the parameters that the
also try to minimize idle time at checkouts; there is no point keeping checkouts manager wishes to vary. The checkouts may be "fast" or "standard" (as noted
open if there are no customers to serve. earlier, another of the parameters for a simulation run defines the limit on purchases
permitted to users of fast checkout lanes). The supermarket always has at least one
954 Supermarket example
Supermarket example: Introduction 953
standard checkout open; the manager wants the option of specifying a minimum of
1..3. There is no requirement that a "fast" checkout be always open, though again Rules for • Checkout closing rule:
the manager wants to be able to specify a minimum number of fast checkouts (in opening/closing If the number of shoppers in the aisles has decreased since the last check,
checkouts then all idle checkouts should be closed (apart from those that need to be left
the range 0..3). One of the other rules proposed by the manager specifies when to
open fast checkouts if there are none already open. open to meet minimum requirements).
Checkouts process a current customer, and have a "first-come-first-served" Queues at checkouts
• Minimum checkouts rules:
queue of customers attached. A checkout processes ten items per minute (this is Open fast and standard checkouts as needed to make up the specified
not one of the parameters that the manager normally wishes to vary, but obviously minimum requirements.
the processing rate, as defined in the program, should be easy to change.) The If the total number of customers in the supermarket (shoppers and queuers)
minimum processing time at a checkout is one minute (customers have to find their exceeds 20, there must be at least one fast checkout open.
change and dispute the bill even if they buy only one item). Once a checkout
finishes processing a customer, that customer leaves the shop (and disappears from • Extra checkouts rules:
the simulation). Checkout operators record any time period that they are idle and,
when reassigned to other duties, report their idle time to a supervisor who If either the number of customers queuing, or the number of customers
accumulates the total idle time. shopping has increased since the last check, then the following rules for
Customers entering the supermarket have some planned number of purchases Customers opening extra checkouts should be applied:
(see below for how this is determined). Customers have to wander the aisles
a) If the average queue length at fast checkouts exceeds a minimum
finding the items that they require before they can join a queue at a checkout. It is a (specified as an input for the simulation run), then one additional fast
big store and the minimum time between entry and joining a checkout queue would checkout should be opened (but not if this would cause the total number of
be two minutes for a customer who doesn't purchase anything. Most customers checkouts currently open to exceed the overall limit).
spend a reasonable amount of time doing their shopping before they get to join a
queue at a checkout. This shopping time is determined by the number of items that b) A number of additional standard checkouts may have to be opened
must be purchased and the customer's "shopping rate". Shopping rates vary; for (subject to limits on the overall number of checkouts allowed to be open).
these simulations the shopping rates should be distributed in the range 1..5 items Checkouts should be opened until the average queuing time at standard
per minute (again, this range is not a parameter that the manager wishes to change checkouts falls below a maximum limit entered as an input parameter for
on different runs of the simulation, but the program should define the range in a the simulation. (The queuing times can be estimated from the workloads at
way that it is easy to change if necessary). the checkouts and the known processing rate of checkouts.)
Once they have completed their shopping, customers must choose the checkout Choosing where to
where they wish to queue. You can divide customers into "fast" and "standard" queue In the real world, when a new checkout opens, customers at the tails of existing
categories. "Fast" customers are those who are purchasing a number of items less queues move to the queue forming at the newly opened checkout if this would
than or equal to the supermarket's "fast checkout limit" (together with that 1% of result in their being served more quickly. For simplicity, the simulation will omit
other customers who don't wish to obey such constraints). When choosing a this detail.
Customer traffic The rate of arrival of customers is far from uniform and cannot be simulated by
checkout, a "fast customer" will, in order of preference, pick: a) an idle "fast"
checkout, b) an idle "standard" checkout, or c) the checkout ("fast" or "standard") random drawing from a uniform distribution. Instead, it must be "scripted". The
with the minimum current workload. Similarly, "standard customers" will pick a) pattern of arrivals averaged for a number of days serves as the basis for this script.
an idle "standard" checkout, or b) the "standard" checkout with the minimum Typically, there is fairly brisk traffic just after opening with people purchasing
current workload. items while on their way to work. After a lull traffic builds to a peak around 10.30
The workload of a checkout can be taken as the sum of all the items in the Workloads at a.m. and then slackens off. There is another brisk period around lunch time, a quiet
trolleys of customers already in the queue at that checkout plus the number of items checkouts afternoon, and a final busy period between 5.30 p.m. and 6.30 p.m.. The averaging
still remaining to be scanned for the current customer. of several days records has already been done; the results are in a file in the form of
The manager is proposing a scheme whereby checks are made at regular Scheduling changes a definition of an initialized array Arrivals[]:
intervals. This interval is one of the parameters for a simulation run; it should be to checkouts
static int Arrivals[] = {
something in the range 5..60 minutes. // Arrivals at each 1 minute interval starting 8am
When running a check on the state of the supermarket, the manager may need to 12, 6, 2, 0, 0, 1, 2, 2, 3, 4, 3, 0, 2, 0, 0,
close idle checkouts, or to open checkouts to meet the minimum requirements for ...
fast and standard checkouts, or to open extra checkouts to avoid excessively long };
queues at checkouts. The suggested rules are:
956 Supermarket example
Supermarket example: Introduction 955
This has entries giving the average number of people entering in each one minute 1 The program will start by prompting for the input parameters:
time interval. Thus, in the example, 12 customers entered between 8.00 and 8.01 • frequency of floor manager checks;
am etc. These values are to be used to provide "randomized" rates of customer • maximum number of checkouts;
arrival. If for time period i, Arrivals[i] is n, then make the number of customers • minimum number of fast and standard checkouts;
entering in that period a random number in the range 0..2n-1. • purchase limit for use of fast checkout;
Similarly, the number of purchases made by customers is far from uniform. In Purchase amounts • length of queues at fast checkout necessary that, if exceeded, will cause
the manager to open an extra fast checkout;
fact, the distribution is pretty close to exponential. Most customers buy relatively
• queuing time at standard checkout that, if exceeded, will cause the
few items, but a few individuals do end up filling several trolleys with three manager to open an extra fast checkout;
hundred or more items. The number of items purchased by customers can be
approximated by taking a random number from an exponential distribution with a 2 The program is then to simulate activities in the shop from 8.00 a.m. until final
given mean value. closing time.
It has been noted that the mean values for these distributions are time dependent. At intervals corresponding to the floor manager's checks, the program is to
Early customers, and those buying items for their lunch, require relatively few print a display of the state of the shop. This should include summaries of the
items (<10); so at these times the means for the exponential distributions are low. total number of customers currently in the shop, those in queues, and details of
The customers shopping around 10.30 a.m., and those shopping between 5.30 and the checkouts. Active checkouts should indicate their queue lengths. The
6.30 p.m., are typically purchasing the family groceries for a week and so the mean number of idle checkouts should be stated.
numbers of items at these times are high (100+). Details of any changes to the number of open checkouts should also be printed.
Again, these patterns are accommodated through scripting. The file with the
3 When all customers have left, the final closing time should be printed along
array Arrivals[] has a commensurate array Purchases[]: with the histograms of the statistics acquired. The histograms should include
those showing number of purchases, total time spent in the shop, and queuing
static int Purchases[] = { times. Details of total and idle time of checkouts should also be printed.
// Average number purchases of customer
4, 3, 4, 5, 3, 5, 7, 6, 5, 4, 3, 5, 2, 5, 4,
…
}; 27.2 DESIGN
The entries in this array are the mean values for the number of purchases made by 27.2.1 Design preliminaries
those customers entering in a particular one minute period. The number of
purchases made by an individual customer should be generated as a random The simulation mechanism
number taken from an exponential distribution with the given mean.
The manager wants the program to provide an informative statistical summary at Reports for the boss Loop representing The simulation mechanism is similar but not identical to that used in the
the end of each run. This summary should include: passage of time AirController/Aircraft example. The core of the simulation is again going to be a
loop. Each cycle of this loop will represent one (or more) minute(s) of simulated
• the shop's closing time; time. Things happen almost every minute (remember, another bunch of frantic
• the number of customers served; customers comes through the door). Most activities will run for multiple minutes
• a histogram showing the number of items purchased by customers; (two minute minimum shopping time, one minute minimum checkout time etc).
• histograms summarizing the shopping, queuing and total times spent by On each cycle do … Each cycle of the simulation loop should allow any object that needs to perform
customers;
• details of checkout operations such as total operating and idle times of some processing to "run". This is again similar to the AirController/Aircraft
checkouts example where all the Aircraft got a chance to Move() and update their
positions. The Supermarket program could arrange to tell all objects to "run" for
There should also be a mechanism for displaying the state of the supermarket at one minute (shoppers complete more of their purchasing, checkouts scan a few
each of the check times. more items). However, as there are now going to be hundreds of objects it is better
to use a slightly more efficient mechanism whereby objects suspend themselves for
a period of time and only those that really need to run do so in any one cycle.
Priority queue used There is a standard approach for simulations that makes use of a priority queue.
Specification
in simulation Objects get put in this queue using "priorities" that represent the time at which they
Implement the Supermarket program: are going to be ready to switch to a new task (all times can be defined as integer
values – minutes after opening time). For example, a customer who starts doing 15
958 Supermarket example
Design preliminaries 957
minutes worth of shopping at 8.30 a.m. can be inserted into this queue with priority they still don't organize much. There has to be some other thing that keeps track of
45 (i.e. ready at 8.45 a.m.), a customer starting at the same time but with only 3 the currently active checkouts.
minutes worth of shopping will get entered with priority 33 (i.e. ready at 8.33). Finding the objects The other objects, in this example the more important control objects, have still
The simulation loop doesn't have to advance time by single units. Instead, it to be identified.
pulls an item from the front of the priority queue. Simulated time can then be Underline the nouns? An approach sometimes suggested is to proceed by underlining the nouns in the
advanced to the time at which this next event is supposed to happen. So, if the problem description and program specification. After all, nouns represent objects.
front item in the queue is supposed to occur at 8.33 a.m., the simulated time is You will get a lengthy and rather strange list: supermarket, door, minute, time,
advanced to 8.33 a.m.. In this example, there will tend to be activities scheduled shopping, purchases, manger, employee, statistics, aisles, …. You drop those that
for every minute; but you often have examples where there are quiet times where you feel confident don't add much; so minute, shopping, aisles can all go
nothing happens for a period. The priority queue mechanism lets a simulation jump immediately. The remaining nouns are considered in more detail.
these quiet periods. Purchases? How about purchases? Not an object. The only thing we need to know about a
When items are taken from the queue, they are given a chance to "run". Now customer's purchases is the number. Purchases can be a data member of the
"run" will typically mean finishing one operation and starting another, though for Customer class. The class had better provide an access function to let other objects
some objects it will just mean doing the same thing another time. This mechanism get this number when needed.
for running a simulation is efficient because "run" functions are only called when it Time? Time? The simulation has to represent time. But basically it is just an integer
is time for something important to happen. Thus, there no need to disturb every counter (minutes after opening time or maybe minutes after midnight). Something
one of the hundred customers still actively shopping to ask if they are ready go to a owns the system's timer; something updates it (by advancing this timer when items
checkout; instead, the customers are scheduled to be at the front of the priority are taken from the priority queue). Many objects will need access to the time value.
queue when they are ready to move to checkouts. But the time is not an object.
Usually, the result of an object's "run" function will be some indication that the Manager? Manager? This seems a better candidate. Something needs to hold the
it wants to be put back in the priority queue at some later time (larger priority parameters used in the checkout opening/closing rules. Something needs to execute
number), or that it needs to be transferred to some other queue, or that it is finished the code embodying those rules. There would only be one instance of this class.
and can be removed from the simulation. Despite that, it seems plausible. It offers a place to group some related data and
There will be something in the program that handles this main loop. But before behaviours.
we get too deep into those details we need to identify the objects. What are these Employee? Employees? No. The simulation doesn't really need to represent them. Their
things that get to run, and what do they want to do when they have a chance to run? activities when they are not operating checkouts are irrelevant to the simulation.
There is no need to simulate both employee and checkout. In the simulation the
checkouts embody all the intelligence needed to perform their work.
The objects Avoid over faithful You can do design by underlining nouns, and then choosing behaviours for the
models of real worldobjects that these nouns represent. It doesn't always work well. Often it results in
So, what are the objects? What they own? What they do? over faithfully modelling the real world. You tend to end up with "Employee"
Some are obvious. There are going to be "Customer" objects and "Checkout" Obvious objects objects and Checkout objects, and a scheme for assigning "Employee" objects to
objects. There will also be "Queue" objects, and "Histogram" objects. Now while run Checkout objects. This just adds unnecessary complication to the program.
all these will be important it should be clear that they aren't the ones that really Alternative approach Use of scenarios is an alternative way to find objects. Scenarios, and the
define the overall working of the simulation. for finding objects: patterns of object interactions that they reveal, were used in Chapter 22. There we
Customers Scenarios
Customer objects are going to be pretty passive. They will just hang around already had a good idea as to the classes and needed just to flesh out our
"shopping" until they eventually chose to move to a checkout queue. They will understanding of their behaviours and interactions. However, we can use scenarios
then hang around in a queue until they get "processed" by a Checkout after which at an earlier stage where we are still trying to identify the classes. Sometimes, a
they will report their shopping time, queuing time, etc and disappear. But scenario will have a "something" object that asks a Customer object to provide
something has got to create them. Something has to ask them for their statistics some data. We can try and identify these "somethings" as we go along.
before they leave. Something has to keep count of them so that these data are Starting points for The example programs in Chapter 22 were driven by user entered commands.
available when the rules for opening/closing checkouts are used. scenarios So, we could proceed by working out "What happens when the user requests action
Checkout objects might be a little more active. They will add customers to their Checkouts X?" and following the interactions between the objects that were involved in
queues (if customers were to be permitted to switch queues, the checkouts would satisfying the users request.
have to allow customers to be removed from the tails of their queues). They pull This program isn't command driven. It grabs some initial input, then runs itself.
customers off their queues for processing. They report how busy they are. But So we can't start by following the effect of each command.
Where else might we start?
960 Supermarket example
Design preliminaries 959
The creation and destruction of objects are important events in any program. Door Customer Shop
We have already identified the need for Customer objects and Checkout objects. object objects object
So, a possible starting point is looking at how these get created and destroyed.
Something has to create Customers, tell them how much they want to buy, and Scenarios for the Run()
then pass them to some other something that looks after them until they leave the creation and +
destruction of objects
shop. Note, we have already found the need for two (different kinds of) call
somethings. The first something knows about those tables of arrivals and purchase constructor AddCustomer()
amounts. The second something owns data like a list of current customers. passing info.
loop in like pointer
When Customer objects leave, they have to report their statistics. How? To Run() to Shop
what? A Customer object has several pieces of data to report – its queuing time, its
?
number of purchases, its shopping time, etc. These data are used to update different add to
Histogram objects. It would be very inconvenient if every Customer needed to simulation
know about each of the different Histogram objects (too many pointers). Instead,
the gathering of statistics would be better handled by some object that knew about
Customer objects and also knew about (possibly owned?) the different Histogram
objects.
Checkout objects also get created and destroyed. The "Manager" chooses when Figure 27.1 Scenario for creation of Customer objects.
creation and destruction occurs and may do the operations or may ask some other
object to do the actual creation (destruction). Something has to keep track of the Customers are supposed to come into the shop every minute. The main
checkout objects. When a checkout is destroyed, it has to report its idle time to simulation loop is going to get "active objects" (like the Door) to "run" each minute
something. (simulated time). Consequently, the activities shown in Figure 27.1 represent the
Scenarios relating to these creation and destruction steps of known objects will "run" behaviour of class Door . It can use the table of arrival rates and average
help identify other objects that are needed. Subsequently, we can examine purchases to choose how many Customer objects to create on each call to its Run()
scenarios related to the main simulation loop. "Active objects" are going to get function.
taken from the front of the priority queue and told to "run". We will have to see This initial rough scenario for creating a Customer object cannot clarify exactly
what happens when objects of different types "run". what the Shop object must do when it "adds a customer". Obviously it could have a
list of some form that holds all customers; but it might keep separate lists of those
27.2.2 Scenarios: identifying objects, their classes, their shopping and those queuing. Some activity by the customer (switch from shopping
responsibilities, their data to queuing) has also got to be scheduled into the simulation. The Shop object might
be able to organize getting the customer into the simulation's priority queue.
Creation and destruction scenarios
Results:
Creating customers
We appear to need:
A supermarket without customers is uninteresting, so we might as well start by
looking at how Customer objects come into the system. • a Door object.
It uses (owns?) those arrays (with details of when customers arrive and how
Figure 27.1 provides a first idea. We could have a "Door" object that creates the
much they want to purchase), and a pointer to a Shop object.
Customers, providing them with the information they need (like their number of
In its Run() member function, called from the main simulation loop, it creates
purchases). The Customer objects will have to complete any initialization (e.g. customers.
choose a rate at which to shop), work out how long their shopping will take, and
then they will have to add themselves to some collection maintained by a "Shop" • Constructor
object. To do that, they will have to be given a pointer to the shop object The constructor function gets given a pointer to a Shop, and a value for the
(customers need to know which shop they are in). The Door object can provide this number of purchases. The function is to pick an actual number of purchases
pointer (so long as it knows which shop it is attached to). (exponentially distributed with given number as mean, minimum of 1 item),
and select a shopping rate. The Customer better record starting time so that
later it is possible to calculate things like total time spent in the shop. (How
does it get the current time? Unknown, maybe the time is a global, or maybe
962 Supermarket example
Scenarios 961
the Customer object could ask the Shop. Decide later.) These data get stored Creating and Destroying Checkout objects
in private data members.
Customer objects will be getting created and deleted every minute. Changes to the
Constructor should invoke "AddCustomer" member function of the Shop Checkouts are less common. Checkout objects are only added or removed when
object.
the floor manager is performing one of his/her regular checks (the problem
• a Shop object. specification suggested that these checks would be at intervals of between 5 and 60
This keeps track of all customers, and gets them involved in the main minutes). Checkouts are added/removed in accord with the rules given earlier.
simulation. There seems to be a definite roll for a "Manager" object. It will get to "run" at
regular intervals. Its "run" function will apply the rules for adding and removing
checkouts.
Removing customers from simulation The Manager object will need to be able to get at various data owned by the
Shop object, like the number of customers present. Maybe class Manager should be
Customers leave the simulation when they finish being processed by a Checkout. a friend of class Shop, otherwise class Shop is going to have to provide a large set
The Checkout object would be executing its Run() member function when it gets of access functions for use by the Manager object.
to finish with a current customer; it can probably just delete the Customer object. Figure 27.3 illustrates ideas for scenarios involving the creation and deletion of
We can arrange that the destructor function for class Customer notify the Shop checkout objects. These operations will occur in (some function called) from
object. The Shop object can update its count of customers present. Somehow, the Manager::Run(). The overall processing in Manager::Run() will start with
Customer has to report the total time it spent in the shop. Probably the report some interactions between the Manager object and the Shop object. These will give
should be made to the Shop object; it can log these times and use the data to update the Manager the information needed to run the rules for opening/closing checkouts.
the Histogram objects that will be used to display the data.
Manager Shop
Figure 27.2 illustrates the interactions that appear to be needed.
object object
Run() Requests
Results: for queue lengths, Checkout
details of number objects
of shoppers
Responsibilities of Shop object becoming clearer. It is going to gather the statistics
+ constructor
needed by the various Histogram s. It probably owns these Histogram objects.
CreateExtra
Checkouts()
OpenCheckout()
Checkout Customer Shop
object object object 1
Run()
delete
Run() Requests
~Customer() LogTotalTime() for queue lengths,
details of number
of shoppers
delete
destructor
CloseIdle
CustomerLeaves()
Checkouts()
LogIdleTime()
2
Pane 1 of Figure 27.3 illustrates an idea as to events when the shop is getting let the thing run
busier. The M a n a g e r will create additional checkouts. As part of their
check status of thing
"constructor" operations, these checkout objects can register with the Shop (it will if terminated
need to put them into lists and may have to perform other operations). delete it
Pane 2 of Figure 27.3 shows the other case where there are idle checkouts that if idle
should be deleted. The Manager object will have to get access to a list of idle ignore it
checkouts that would presumably be kept by the Shop object. It could then remove if running
renter into priority queue
one or more of these, deleting the Checkout objects once they had been removed.
The destructor of the Checkout object would have to pass information to the report final statistics
Shop object so that it could maintain details of idle times and so forth.
Note the use of destructors in this example, and the preceding case involving Expanded role for Need for a class The priority queue contains anything that wants a chance to "run", and so it
Customer objects. Normally, we've used destructors only for tidying up operations. destructors hierarchy includes the Door object, a Manager object, some Checkout objects and some
But sometimes, as here, a destructor should involve sending a notification to Customer objects. But as far as this part of the simulation is concerned, these are
another object. all just objects that can "run", can specify their "ready time", and can be asked their
status (terminated, idle, running).
A simulation using the priority queue mechanism depends on our being able to
Other acts of creation and destruction treat the different objects in the priority queue as if they were similar. Thus here
we are required to employ a class hierarchy. We need a general abstraction: class
The other objects appear all to be long lived. The simulation could probably start "Activity".
with the main() function creating the principal object which appears to be the class Activity Class Activity is an abstraction that describes an Activity object as
Shop: something that can:
int main() • Run()
{
// Some stuff to initialize random number generator
An Activity's Run() function will complete work on a current task and
select the next task. This will be a pure virtual function. Specialized
Shop aSuperMart; subclasses of class Activity define their own task agendas.
aSuperMart.Setup();
aSuperMart.Run(); • Status()
return 0; An Activity can report its status (value will be an enumerated type). Its
} status is either "running" (the Activity is in, or should be added to, the main
priority queue used by the simulation), or "idle" (the Activity is on some
The Shop object could create the Door, and the Manager objects, either as part of "idle" list, some other object may invoke a specialized member function that
the work of its constructor or as some separate Setup() step. Things like the change the Activity 's status), or "terminated" (the main simulation loop
PriorityQueue and Histogram objects could be ordinary data members of class should get rid of those Activity objects that report they have terminated).
Shop and so would not need separate creation steps.
The Shop, Door, and Manager objects can remain in existence for the duration of • Ready_At()
An Activity can report when it is next going to be ready; the result will be in
the program.
simulation time units (in this example, these units will be minute times during
the day).
Scenarios related to the main Run() loop in the simulation
The current hierarchy of Activity classes is illustrated in Figure 27.4.
The scenarios shown in Figures 27.1 and 27.3 related to Run() member
The main Shop::Run() loop working with the priority queue will have to be functions of two of the classes. The other activities triggered from this main loop
something like the following:
will involve actions by Customer objects and Checkout objects.
A Customer object should be initially scheduled so that it "runs" when it
while Priority Queue is not empty
remove first thing from priority queue completes its shopping phase. At this time, it should choose the Checkout where it
move time forward to time at which this thing is wants to queue.
supposed to run Probably, a Customer object should ask the Shop to place it on the shortest
suitable queue; see Figure 27.5. This would avoid having the Customer objects
966 Supermarket example
Scenarios 965
interacting directly with the Checkout objects. Once it is in a Checkout queue, the Customer Shop Checkout
Customer object can remove itself from the main simulation (it has nothing more to object object objects
AddCustomer()
Door Customer
owns:
owns: Figure 27.5 Interactions with Customer::Run().
? info. for creating customers
?number of items to purchase,
does:
starting time
Run()
does: These two normally reschedule themselves as running; the Door object setting
create some customers,
then reschedule Manager Checkout
Run() its next "ready at" time for one minute later, the Manager selecting a time based on
pick checkout, join queue
change state to idle the frequency of checks. The Manager is responsible for getting the Shop to close
its Door sometime after 7 p.m.. This is probably the basis for getting the Door
owns: owns: removed from the simulation. If it has been "closed", instead of rescheduling itself
? info. for rules for running ? queue of customers, work time,
checkouts idle time the Door should report that it has "terminated". The simulation loop will then get
does: does: rid of it.
Run() Run()
examine state of shop, maybe finish current customer (and Similarly, the Manager object should "terminate" once all its work has been
open or close checkouts; delete), if no customers in done. This will be when the door has been closed and the last customer has been
then reschedule queue notify shop that now
… idle and change state finished. The termination of the Manager will leave the priority queue empty and
else start serving next, the simulation will stop.
work out next ready time
reschedule When the main simulation loop ends, the Shop can printout the statistics that it
has gathered.
Although there are disadvantages associated with partial "prototype" implement- Advantages of
ations they do have benefits. If you "analyse a little, design a little, and implement partial, prototype The delete a operation has to cause the appropriate destructor to be executed. If
implementations you did not specify virtual ~Activity() (e.g. you either didn't include a
a little", you get a better understanding of the problem. Besides, you may find it
boring to have to work out everything on paper in advance; sometimes it is "fun" to destructor or you declared it simply as ~Activity()) the compiler would assume
dive in and hack a little. that it was sufficient either to do nothing special when Activity objects were
The simplified version of the program could omit the Checkouts. Customer deleted, or to generate code that included a call to Activity::~Activity().
objects would just change their state to "terminated" when they finish shopping. However, because virtual has been specified correctly, the compiler know to put
The Manager's role can be restricted to arranging for regular reports to be produced; in the extra code that uses the table lookup mechanism (explained in Chapter 26)
these would show the number of customers present. Such changes eliminate most and so at run-time the program determines whether a call should be made to
of the complexities but still allow leave enough to allow testing of the basic Customer:: ~Customer(), or Manager::~Manager(), or Door::~Door().
simulation mechanisms. If a subclass has no work to be done when its instances are deleted, it need not
The classes needed in the reduced version are: Door, Manager, Customer (all of Classes to be defined define a destructor of its own. (A definition has to be given for Activity::
which are specialized subclasses of class Activity) and Shop. ~Activity(); it is just an empty function, { }, because an Activity has nothing of
In addition, the Shop will use an instance of class PriorityQ and possibly some Reused classes its own to do.)
instances of class List. These will simply be the standard classes from Chapter 24. Class Door Class Door must provide implementations for Run() and Ready_At(). It will
The priority queue needs one change from the example given there; the maximum obviously have its own constructor but it isn't obvious that a Door has to do
size of the queue should now be 1000 rather than 50 (change the constant anything when it is deleted so there may not be a Door::~Door() destructor (the
k_PQ_SIZE in the class header). The List class should not need any changes. "empty" Activity::~Activity() destructor function will get used instead).
The abstract class, class Activity, should be defined first: Class Activity Apart from Run(), Status(), and Ready_At(), the only other thing that a Door
might be asked to do is Close().
class Activity { The specification implies that a file already exists with definitions of the arrays
public: of arrival times and purchase amounts. These are already defined as "filescope"
enum State { eTERMINATED, eIDLE, eRUNNING };
Activity(State s = eRUNNING) : fState(s) { } variables. If you had the choice, it would be better to have these arrays as static
virtual ~Activity() {} data members of class Door. However, given the specification requirements, the
virtual void Run() = 0; file with the arrays should just be #included into the file with the code file Door.cp.
virtual long Ready_At() = 0; The Door functions can just use the arrays even though they can't strictly be
State Status() { return fState; } "owned" by class Door.
protected:
State fState; The other data that a Door needs include: a pointer to the Shop, a flag indicating
}; whether the Door is "open", a counter that lets it step through the successive
elements in the Arrivals[] and Purchases[] arrays, and a long integer that
It is simply an interface. Activity objects are things that "run" and report when Pure virtual holds the "time" at which the Door is next ready to run.
they will next be ready; how they do this is left for definition in subclasses. functions to be The class declaration should be along the following lines:
defined in subclasses
Functions Run() and Ready_At() are defined as pure virtual functions; the
subclasses must provide the implementations. class Door : public Activity {
Definitions for other public:
The abstract class can however define how the Status() function works as this Door(Shop* s);
function is simply meant to give read access to the fState variable. The member functions
void Close();
constructor sets the fState data member; the default is that an Activity is virtual void Run();
"running". Data member fState is protected; this allows it to be accessed and virtual long Ready_At() { return fready; }
changed within the functions defined for subclasses. private:
virtual destructor Shop *fs; // Link to Shop object
Note the virtual destructor. Objects are going to be accessed via Activity* long fready; // Ready time
pointers; so we will get code like: short fopen; // Open/closed status
short fndx; // Next entries to use from
Activity* a = (Activity*) fSim.First(); // Arrivals[], Purchases[]
… };
switch (a->Status()) {
case Activity::eTERMINATED: The initial part of the declaration:
delete a;
break;
class Door : public Activity {
970 Supermarket example
Design for a prototype partial implementation 969
long fstartqueue;
essentially states that "A Door is a kind of Activity". Having seen this, the C++ long fitems; // # purchases
};
compiler knows that it should accept a programmer using a Door wherever an
Activity has been specified.
The behaviours for the functions are: The behaviours for the functions are:
Constructor Constructor
Set pointer to Shop; initialize fready with time from Shop object; Set pointer to Shop. The second argument to the constructor is to
be used to pick the number of items to purchase; the value is to be
set fopen to true; and fndx to 0.
an integer from an exponential distribution with the given mean
(The Shop object will create the Door and can therefore arrange
value. Pick fitems accordingly.
to insert it into the priority queue used by the simulation.)
Record the start time (getting the current time from the Shop
object).
Run()
Choose a shopping rate (random in range 1…5) and use this and
If fopen is false, set fState (inherited from Activity) to
the value of fitems to calculate the time the Customer will be
"terminated".
ready to queue.
Otherwise, pick number of arriving customers (use entry in
Arrivals[], get random number based on that value as default).
Tell Shop to "Add Customer".
Loop creating Customer objects.
Run()
Close() In this limited version, just set fState to terminated.
Set fopen to false.
Destructor
This version of class Door should not need any further elaboration for the final Get Shop to log total time; then tell shop that this Customer is
program. leaving.
Class Customer is again a specialized Activity that provides implementations class Customer
for Run() and Ready_At() . The constructor will involve a Customer object The final program will require changes to Run(). The Customer object should get
interacting with the Shop so that the counts of Customers can be kept and the Shop to transfer it to the queue at one of the existing Checkouts and set its own
Customer objects can be incorporated into the simulation mechanism.
state to "idle" rather than "terminated".
class Manager Class Manager is the third of the specialized subclasses of class Activity.
The destructor will also involve interactions with the Shop ; as they leave,
Customers are supposed to log details of their total service times etc.
Once again, it has to provide implementations for Run() and Ready_At(). Its
The only other thing that a Customer object might be asked is to report the constructor might be a good place to locate the code that interacts with the user to
number of purchases it has made. This information will be needed by the get the parameters that control the simulation. It does not appear to need to take
Checkout objects once that class has been implemented.
any special actions on deletion so may not need a specialized destructor.
A Customer object needs a pointer to the Shop, a long to record the time that it So far, it seems that the Manager will only be asked to Run() (and say when it
is next ready to run, a long for the number of items, and two additional long is ready) so it may not need any additional functions in its public interface. Its
Run() behaviour will eventually become quite complex (it has to deal with the
integers to record the time of entry and time that it started queuing at a checkout.
rules for opening and closing checkouts). Consequently, the final version may have
A declaration for the class is:
several private member functions.
class Customer : public Activity{ The Manager object seems a good place to store most of the control parameters
public: like the frequency of floor checks, the minimum numbers of fast and standard
Customer(Shop* s, short mean); checkouts and the control values that trigger the opening of extra checkouts. Other
~Customer(); information required would include the number of customers shopping and queuing
virtual void Run();
virtual long Ready_At() { return fready; }
at the last check time (the Shop object can be asked for the current numbers). The
long ItemsPurchased() { return fitems; } only parameter that doesn't seem to belong solely to the Manager is the constraint
private: on the number of purchases allowed to users of fast checkouts. This gets used
Shop *fs; // Link to shop when picking queues for Customers and so might instead belong to Shop.
long fready; // Ready time A declaration for the class is:
long fstarttime; // Other time data
972 Supermarket example
Design for a prototype partial implementation 971
(All three classes uses the same style of implementation of the function // closing
void CloseDoor();
Ready_At(); this suggest that it could be defined as a default in the Activity class
itself.) // To save having lots of access functions
The behaviours for the functions are: friend class Manager;
private:
Constructor PriorityQ fSim;
Set pointer to Shop; initialize fready with time from Shop object; Manager *fManager;
set fopen to true; and records from "last check" to -1. Door *fDoor;
(The Shop object will create the Manager and can therefore
arrange to insert it into the priority queue used by the simulation.) long fTime;
short fCustomersPresent;
Prompt the user to input the values of the control parameters for short fCustomersQueuing;
the simulation. short ffastlanemax;
data members that will be used to store the statistics, e.g. an integer fidle to store 27.3 A PARTIAL IMPLEMENTATION
the total time that there were checkouts that were open but idle.
The behaviours for the functions are: main() and auxiliary functions
Constructor Member functions of Most of the code for main() was given earlier (just before introduction of the
Initialize all data members (fidle = 0; fTime = kSTARTTIME; class Shop "activity" class hierarchy). There is one extra feature. The manager using the
etc). program wants to be able to run simulations of slightly different patterns of
customer arrivals, and simulations using an identical pattern of arrivals but different
Setup() parameters.
Create Manager and Door objects, insert them into the Seeding the random This can be achieved by allowing the user to "seed" the pseudo random number
PriorityQueue fSim. number generator generator. Since "pseudo random numbers" are generated algorithmically, different
runs using the same seed will get identical sequences of numbers. Use of a
Run() different seed results in a different number sequence.
Implementation of loop shown earlier in which Activity objects
get removed from the priority queue and given a chance to run. int main(int,char**)
{
AddCustomer() long aseed;
Update count of customers present, and insert customer into cout << "Enter a positive integer to seed the random "
priority queue. "number generator\n";
cin >> aseed;
CustomerLeaves()
srand(aseed);
Decrement counter.
Shop aSuperMart;
DisplayState() … // as shown above
Show time and number of customers (might need an auxiliary
private member function to "pretty print" time). Random numbers The random number generator in the standard maths library produces numbers
Full implementation will need display details of active and idle from an exponential that are uniformly distributed. This program also requires some numbers that are
distribution
checkouts as well. taken from an exponential distribution with a defined mean (such a distribution has
large numbers of small values, and a tail with large values). Most versions of the
Log functions maths library don't include this version of the number generator. The required
All empty "stubs" in this version. function, erand(), uses the normal random number generator rand() and a few
mathematical conversions to produce random numbers with the required
Time() characteristics:
Access function allowing instances of other classes to have read
access to fTime. erand() int erand(int mean)
{
return int(-mean *log(
CloseDoor() double(SHRT_MAX-rand() + 1)/SHRT_MAX )
If fDoor pointer not NULL, tell door to close then set fDoor to + 0.5);
NULL. }
There doesn't appear to be any need for a destructor. Output of the time in a This function can be defined in the file with the code for class Customer ; the
"pretty" format (e.g. 9.07 a.m., 3.56 p.m.) might be handled in some extra private header files math.h, stdlib.h, and limits.h must be #included. (This implementation
PrintTime() member function. of erand() assumes that rand() generates values in the range 0…SHRT_MAX. Your
implementation may use a different range in its random number generator, so you
may need to modify this definition of erand(). The range used by rand() is
supposed to be defined by constants in the limits.h header file but many systems do
not comply.)
976 Supermarket example
Partial implementation 975
class Door;
Like the examples in Chapter 22, this program should be built from many separate Header file
class Manager;
files (a header file and an implementation file for essentially every class). dependencies
class Customer;
Consequently, you have to sort out "header dependencies".
A possible arrangement of files, and most of the header dependencies, is shown class Shop {
public:
in Figure 27.6. (Standard header files provided by the IDE are not shown.) Shop();
The main program defines an instance of class Shop and therefore must #include …
the Shop.h header. The class definition in Shop.h will specify that a Shop object void AddCustomer(Customer* c);
contains as a PriorityQ as a data member. Consequently, the header Shop.h can …
only be handled if the header declaring the PriorityQ has been read; hence the friend class Manager;
private:
dependency from Shop.h to pq.h.
PriorityQ fSim;
The declaration of class Shop also mentions Manager, Customer, and Door but …
these only appear as pointer types. It isn't essential to read the declarations of these
classes to compile Shop.h; but these names must be defined as class names. So file Manager *fManager;
Shop.h starts with simple type declarations: Door *fDoor;
…
};
pq.h #endif
Activity.h
Protect against All header files must be bracketed with #ifdef __XX__ … #endif conditional
multiple #includes compilation directives to avoid problems from multiple inclusion. (You can see
from Figure 27.6, that the file "Activity.h" will in effect be #included three times by
Shop.h Customer.h Manager.h Door.h Shop.cp.) The italicised lines in the file listing above illustrate these directives.
Reducing compile By now you must have noticed that the majority of time used by your compiler
times is devoted to reading header files (both Symantec and Borland IDE's have
compilation-time displays that show what the compiler is working on, just watch).
Having #ifdef … #endif compiler directives in the files eliminates errors due to
multiple #includes but the compiler may still have to read the same file many times.
The example above illustrates a technique that can slightly reduce compile times;
main.cp Shop.cp pq.cp Customer.cp Manager.cp Door.cp you will note that the file "pq.h" will only get opened and read if it has not already
been read.
The constants The two constants define the start and end times for the partial simulation; these
kSTARTTIME … values are need in Manger.cp and Shop.cp. The values will be changed for the full
main.o Shop.o pq.o Customer.o Manager.o Door.o implementation.
The implementation file Shop.cp contains calls to member functions of the
various "activity classes"; consequently, Shop.cp must #include their headers (so
that the compiler can check the correctness of the calls). These dependencies are
Figure 27.6 Module structure and header dependencies for partial
shown in Figure 27.6 by the links from Shop.cp to Customer.h etc.
implementation of "Supermarket" example.
The three "activity classes" all need to #include Activity.h into their own header
files. As they all have Shop* data members, their headers will need a declaration
#ifndef __SHOP__ Shop.h header file
#define __SHOP__ of the form class Shop;. Classes Customer and Manager both use features of class
Shop, so their implementation files have to #include the Shop.h header.
#ifndef __MYPQ__ The final implementation will add classes List, Histogram, and Checkout.
#include "pq.h" Their files also have to be incorporated into the header dependency scheme. Class
#endif
978 Supermarket example
Partial implementation 977
List and Histogram have to be handled in the same way as class PriorityQ; class
Checkout will be the same as class Manager. void Shop::DisplayState()
{
cout << "------" << endl;
cout << "Time ";
class Shop PrintTime();
cout << endl;
The constructor for class Shop is trivial, just a few statements to zero out counters cout << "Number of customers " << fCustomersPresent << endl;
and set fTime to kSTARTTIME. The Setup() function creates the collaborating }
objects. Both need to have a Shop* pointer argument for their constructors that is void Shop::PrintTime()
supposed to identify the Shop object with which they work; hence the this {
arguments. long hours = fTime/60;
long minutes = fTime % 60;
void Shop::Setup() if(hours < 13) {
{ cout << hours << ":" << setw(2) <<
fDoor = new Door(this); setfill('0') << minutes;
fManager = new Manager(this); if(hours < 12) cout << "a.m.";
fSim.Insert(fManager, fManager->Ready_At()); else cout << "p.m.";
fSim.Insert(fDoor, fDoor->Ready_At()); }
} else cout << (hours - 12) << ":" << setw(2) <<
setfill('0') << minutes << "p.m.";
}
Once created, the Manager and Door objects get inserted into the PriorityQ fSim
using their "ready at" times as their priorities.
The main simulation loop is defined by Shop::Run(): File Shop.cp has to #include the iomanip.h header file in order to use facilities like
setw() and setfill() (these make it possible to print a time like seven minutes
void Shop::Run() past eight as 8.07).
{ Most of the "logging" functions should have empty bodies, but for this test it
while(!fSim.Empty()) { would be worthwhile making Shop::LogNumberPurchases(int num) print the
Activity* a = (Activity*) fSim.First(); argument value. This would allow checks on whether the numbers were suitably
fTime = a->Ready_At();
a->Run();
"exponentially distributed".
switch (a->Status()) {
case Activity::eTERMINATED:
delete a; class Door
break;
case Activity::eIDLE: The constructor for class Door initialises its various data members, getting the
break;
case Activity::eRUNNING: current time from the Shop object with which it is linked.
fSim.Insert(a, a->Ready_At()); The only member function with any complexity is Run():
}
} void Door::Run()
} {
if(fopen) {
Adding a Customer object to the simulation is easy: int count = Arrivals[fndx];
if(count>0) {
count = 1 + (rand() % (count + count));
void Shop::AddCustomer(Customer* c) for(int i=0;i<count;i++)
{ Customer* c = new
fSim.Insert(c, c->Ready_At()); Customer(fs,Purchases[fndx]);
fCustomersPresent++; }
} fndx++;
fready = fs->Time() + 1;
The DisplayState() and auxiliary PrintTime() functions provide a limited }
view of what is going on at a particular time: else fState = eTERMINATED;
}
980 Supermarket example
Partial implementation 979
The Run() function will have to be elaborated considerably in the final version
On successive calls, Run() takes data from successive locations in the file scope of the program. In this partial implementation it has merely to get the shop to print
Arrivals[] and Purchases[] arrays. These data are used to determine the its state, then if the time is before the closing time, it reschedules the Manager to
number of Customer objects to create. The first time that Run() gets executed run again after the specified period. If it is later than the closing time, the Shop
after the Close() function, it changes the state to "terminated". should be reminded to close the door (if it isn't already closed).
void Manager::Run()
class Manager {
fs->DisplayState();
if(fs->Time() < kCLOSETIME) {
The constructor for class Manager can be used to get the control parameters for the fready = fs->Time() + ft;
current run of the program. Most input data are used to set data members of the }
Manager object; the "maximum number of items for fast checkout" is used to set else {
the appropriate data member of the Shop object. (The Manager is a friend so it can fs->CloseDoor();
fready = fs->Time() + 5;
directly change the Shop's data member.) if(fs->fCustomersPresent == 0) fState = eTERMINATED;
}
Manager::Manager(Shop* s) }
{
fs = s; // Set link to shop
cout << "Enter time interval for managers checks on " The Manager object can "terminate" if it is after closing time and there are no
"queues (min 5 max 60)\n"; Customer objects left in the store.
short t;
cin >> t;
class Customer
if((t<5) || (t>60)) {
t = 10;
cout << "Defaulting to checks at 10 minute " The constructor is probably the most elaborate function for this class. It has to pick
"intervals\n"; the number of items to purchase and a shopping rate (change that SHRT_MAX to
} RAND_MAX if this is defined in your limits.h or other system header file). The
ft = t; shopping time is at least 2 minutes (defined as the constant kACCESSTIME) plus the
cout << "Enter maximum number of checkouts (15-40) "; time needed to buy items at the specified rate. This shopping time determines when
… the Customer will become ready.
fmaxcheckouts = t; The Customer object can immediately log some data with the Shop.
cout << "Enter minimum number of fast checkouts (0-3) "; Customer::Customer(Shop* s, short t)
… {
… fs = s;
fitems = 1 + erand(t);
cout << "Enter maximum purchases for fast lane " while (fitems>250)
"customers (5-15)\n"; fitems = 1 + erand(t);
cin >> t; fstarttime = fs->Time();
// Code to check value entered double ItemRate = 1.0 + 4.0*rand()/SHRT_MAX;
… int shoptime = kACCESSTIME + int(0.5 + fitems/ItemRate);
// then copy into Shop object's data member fready = fstarttime + shoptime;
fs->ffastlanemax = t; Exploit "friendship" fs->AddCustomer(this);
with Shop fs->LogShopTime(shoptime);
… fs->LogNumberPurchases(fitems);
}
fready = fs->Time();
fQueuingLast = fShoppingLast = -1;
} The destructor arranges for the Customer object to "check out" of the Shop:
Customer::~Customer()
{
982 Supermarket example
Partial implementation 981
fs->LogTotalTime(fs->Time()-fstarttime); The histogram class keeps counts of the number of items (integer values) that
fs->CustomerLeaves(); belong in each of a set of "bins". It automatically adjusts the integer range
}
represented by these bins. When all data have been accumulated, the contents of
the bins can be displayed. Other statistics (number of items, minimum and
maximum values, mean and standard deviation) are also output.
Testing class myhistogram The interface for the class is simple. Its constructor takes two character strings
for labels in the final printout, and a number of integers that define things like the
The other functions not given explicitly are all simple and you should find it easy to
number of bins to be used. The Add() member function inserts another data item
complete the implementation of this partial version of the program.
while Print() produces the output summary. There are several data members;
Traces from LogNumberPurchases() should gives number sequences like: 2,
these include char* pointers to the label strings, records of minimum and
2, 11, 2, 6, 3, 6, 12, 2, 6, 1, 15, 3, 6, 3, 5, 16, 6, 11, 3, 3, 19, …; pretty much the sort
maximum values observed, sum and sum of squares (needed to calculate mean and
of exponential distribution expected. A test run produced the following output
standard deviation) and an array of "bins".
showing the state of the shop at different times:
// Based on code in ATT C++ "tasks" library.
Time 8:00a.m. class myhistogram {
Number of customers 8 public:
------ myhistogram( char* title = "", char* units = "",
Time 8:10a.m. int numbins = 16,int low = 0,int high = 16);
Number of customers 8 void Add(int);
------ void Print();
Time 8:20a.m. private:
Number of customers 5 int l,r; // total range covered
------ int binsize; // range for each "bin"
Time 8:30a.m. int nbin; // number of "bins"
Number of customers 19 int *h; // the array of "bins" with counts
------ long sum; // data for calculating average
… long sqsum; // standard deviation etc
Time 9:10a.m. short count;
Number of customers 2 long max; // nax and min values recorded
------ long min;
Time 9:15a.m. char *atitle; // pointers to labels
Number of customers 0 char *aunits;
};
27.4 FINALISING THE DESIGN The constructor works out the number of bins needed, allocates the array, and
performs related initialization tasks. (It simply stores pointers to the label strings,
There are two main features not yet implemented – operation of checkouts with rather than duplicating them; consequently the labels should be constants or global
customers queuing at checkouts, and the histograms. The histograms are easier so variables.)
they will be dealt with first.
myhistogram::myhistogram(char* title, char* units,
int numbins, int low, int high)
{
27.4.1 Histograms atitle = title;
aunits = units;
The Histogram class comes from the "tasks" class library.
The "tasks" library is often included with C++ systems. Since it is about fifteen int i;
years old its code is old fashioned. (Also, it contains some "coroutine" components if (high<=low || numbins<1) {
cerr << "Illegal arguments for histogram\n";
that depend on assembly language routines to modify a program's stack. If the exit(1);
tasks library is not included with your compiler, it is probably because no one }
converted these highly specialized assembly language routines. A "Coroutine" is a Allocate the bins if (numbins % 2) numbins++;
specialized kind of control structure used mainly in sophisticated forms of array
simulation.) while ((high-low) % numbins) high++;
984 Supermarket example
Histograms 983
void myhistogram::Print()
binsize = (high-low)/numbins; {
h = new int[numbins]; if(count <= 1) {
cout << "Too little data for histogram!\n";
for (i=0; i<numbins; i++) h[i] = 0; return;
l = low; }
r = high;
nbin = numbins; cout << "\n\n" << atitle << "\n\n";
sum = 0; cout << "Number of samples " << count << "\n";
sqsum = 0;
count = 0; double average = ((double)sum)/count;
max = LONG_MIN; double stdev =
min = LONG_MAX; sqrt(((double)sqsum - sum*average)/(count-1));
}
cout << "Average = " << average << aunits << "\n";
The Add() member function does quite a lot more than just increment a cout << "Standard deviation = " << stdev << aunits <<
"\n";
counter. Most of the code concerns recalculation of the total range represented and
size of each bin in accord with the data values entered. The various counters and cout << "Minimum value = " << min << aunits << "\n";
sums etc are also updated. cout << "Maximum value = " << max << aunits << "\n";
}; [ 8 :12] : 29
…
The constructor for class Shop has to arrange for the initialization of its Changed constructor …
for class Shop [ 44 :48] : 1
myhistogram data members. The constructor for the myhistogram class has
defaults for its arguments, but we would want the histograms to have titles so Shopping times
explicit initialization is required:
Number of samples 234
Shop::Shop() : fPurchases("Number items purchased"), Average = 4.602564
fQTimes("Queuing times"), fShopTimes("Shopping times"), Standard deviation = 3.583485
fTotalTimes("Total time customer in shop") Minimum value = 2
{ Maximum value = 39
fTime = kSTARTTIME; [ 0 :4] : 112
fidle = fworktime = 0; [ 4 :8] : 96
fCustomersPresent = fCustomersQueuing = 0; …
} [ 36 :40] : 1
Constructors for data members have to be invoked prior to entry to the body of the
constructor for the class. As previously illustrated, Chapter 25, the calls to the 27.4.2 Simulating the checkouts and their queues
constructors for data members come after the parameter list for the class's
constructor and before the { begin bracket of the function body. A colon separates All that remains is the checkouts. The checkouts are relatively simple in
the list of data member constructors from the parameter list. themselves; the complexities lie in the operations of Shop and Manager that involve
The "log" functions simply request that the corresponding myhistogram object Effective Checkout objects.
"add" an item to its record: implementation for
the "log" functions
void Shop::LogNumberPurchases(int num) What do Checkout objects get asked to do?
{
fPurchases.Add(num);
} Checkout objects will get created at the behest of the Manager (the Manager might
create the Checkout and pass it to the Shop, or the Manager might direct the Shop
The statistics gathered in a run are to be printed when the main loop in Extra Shop:: object to handle creation). They are created as either "fast" or "standard" and don't
Shop::Run() completes. Class Shop should define an additional private member ReportStatistics() subsequently change their type. When initially created, they are "idle".
private member Adding customers Checkout objects will be asked to "add a customer" to a queue. The queue can
function, ReportStatistics(), that gets called from Run() to print the final function
information. A preliminary implementation would be: be represented by a List data member; customers get appended to the end of the
list and get removed from the front. The Shop object will be responsible for giving
void Shop::ReportStatistics() Customer objects to the Checkout objects. The Shop can take responsibility for
{ dealing with cases where a Customer gets added to an idle Checkout; after it has
fPurchases.Print(); dealt with the addition operation, the Checkout should get put into the Shop's
fShopTimes.Print(); PriorityQ fSim. The Checkout object however will have to do things like noting
}
the time for which it has been idle.
Ready_At() and Since a Checkout is a kind of Activity, it must provide implementations of
The existing partial implementation can be extended to test the histogram Retesting Run() Run() and Ready_At(). The Ready_At() function can be handled in the same
extensions. It should produce outputs like the following:
way as was done for the other specialized Activity classes. The Run() function
Number items purchased will get called when a Checkout is due to have finished with a Customer; that
Customer can be deleted. If there are other Customer objects queued, the
Number of samples 234 Checkout can remove the first from its list. The Customer should be told to report
Average = 6.371795 the length of time that it has been queuing. If the queue is empty, the Checkout
Standard deviation = 6.753366 should mark its state as "idle" and should notify the Shop.
Minimum value = 1 Queuelength and
Maximum value = 47 The Manager object's rules for creating and destroying Checkout objects depend
workload on details like average queuelengths and workloads. The Shop object is supposed
[ 0 :4] : 100
[ 4 :8] : 72 to pick the best Checkout for a Customer; choosing the best depends on details of
988 Supermarket example
Checkouts 987
workload and type ("fast" or "standard"). Consequently, a Checkout will have to Such extra functions would be noted while the implementation of class
be able to supply such information. Checkout was sketched out. Subsequently, the extra functions would be defined in
Checkout objects that are not idle should display their queues as part of the DisplayState the other classes.
Shop object's DisplayState() process. class Checkout : class name and details of
When a Checkout object gets deleted, it should report details of its total time of Destructor public Activity inheritance
operation and its idle time. These reports get made to the Shop object so that
private data and functions;
relevant overall statistics can be updated. long fready, fcreated, fidle,
fstart;
List fQueue; own data;
short ft;
What do Checkout objects own?
Customer *fCurrentCustomer; links to collaborators
Shop *fs;
Checkout objects will need: public interface
Checkout(Shop* s, short ctype); constructor
• a pointer to the Shop object;
~Checkout(); destructor, logs times with Shop
• long integers representing time current task completed, time current task
virtual void Run(); rinishes current customer, start next or become idle
started, time of creation, total of idle periods so far;
• a list to hold the queuing Customer objects; virtual long Ready_At(); report "ready time"
• a pointer to the Customer currently being served; long WorkLoad(); return an estimate of workload
• a short to hold the "fast" or "standard" type designation. short QueueLength(); return queue length
void AddCustomer(Customer*); if working , just add customer to queue, otherwise deal
with records of idle time and start to serve customer
Design diagram for class Checkout short CheckoutType(); report type
void DisplayState(); display queue "graphically"
Figure 27.6 is a class "design diagram" that summarizes the features of class
Checkout. Such diagrams are often used as a supplement to (or alternative to)
textual descriptions like those given earlier for the other Activity classes like class
Manager. Figure 27.7 Design diagram for class Checkout.
The diagram in Figure 27.7 is simpler than the styles that you will be using later
Checkout's Previous examples have shown cases where data members that were instances of
when you have been taught the current standard documenting styles, but it still constructor
provides an adequate summary. The header should give details of inheritance. classes had to have their constructors invoked prior to entry to the main body of a
There should be a means of distinguishing the public interface and private constructor. This time, we need to invoke the base class's constructor (Activity::
implementation. Public functions should have a brief comment explaining their Activity()). The other specialized subclasses of Activity relied on the default
role (often details of return types and argument lists are omitted because these parameters to initialize the "activity" aspect of their objects. But, by default, the
design diagrams can be sketched out before such fine details have been resolved). initializer for class Activity creates objects whose state is "running". Here we
It is often useful to distinguish between "own data" and "links to collaborators". want to create an object that is initially "idle". Consequently, we have to explicitly
Both are implemented as "data members". It is just a matter of a different role, but invoke the constructor with the appropriate eIDLE argument; while we are doing
it is worth highlighting such differences in documentation. that we might as well initialize some of the other data members as well:
The destructor should finalize the estimate of idle time and then log the Destructor while(!LL.IsDone()) {
Checkout's idle and open times with the Shop: Customer *c = (Customer*) LL.CurrentItem();
res += c->ItemsPurchased();
LL.Next();
Checkout::~Checkout()
}
{
if(fCurrentCustomer != NULL) {
fidle += fs->Time() - fready;
long items = fCurrentCustomer->ItemsPurchased();
fs->LogIdleTime(fidle);
long dlta = (items - kSCANRATE*(fs->Time()-fstart));
fs->LogOpenTime(fs->Time() - fcreated);
dlta = (dlta < 0) ? 0 : dlta;
}
res += dlta;
}
The Run() function starts by getting rid of the current customer (if any). Then, return res;
if there is another Customer in the queue, this object is removed from the queue to }
become the current customer, is notified that it has finished queuing, and a new
completion time calculated based on the processing rate (kSCANRATE ) and the The work involved in adding a customer depends on whether the Checkout is
number of items purchased. currently idle or busy. Things are simple if the Checkout is busy; the new
Customer is simply appended to the list associated with the Checkout. If the
void Checkout::Run() Checkout is idle, it can immediately start to serve the customer; it should also
{ notify the Shop so that the records of "idle" and "busy" checkouts can be updated.
if(fCurrentCustomer != NULL) {
delete fCurrentCustomer;
void Checkout::AddCustomer(Customer* c)
fCurrentCustomer = NULL;
{
}
if(fState == eIDLE) {
fidle += fs->Time() - fready;
if(fQueue.Length() > 0) {
fCurrentCustomer = c;
fCurrentCustomer = (Customer*) fQueue.Remove(1);
fCurrentCustomer->FinishQueuing();
fCurrentCustomer->FinishQueuing();
fstart = fs->Time();
fstart = fs->Time();
short t = fCurrentCustomer->ItemsPurchased();
short t = fCurrentCustomer->ItemsPurchased();
t /= kSCANRATE;
t /= kSCANRATE;
t = (t < 1) ? 1 : t;
t = (t < 1) ? 1 : t;
fready = fstart + t;
fready = fstart + t;
fState = eRUNNING;
}
fs->NoteCheckoutStarting(this);
else {
}
fState = eIDLE;
else fQueue.Append(c);
fs->NoteCheckoutStopping(this);
}
}
} The final DisplayState() function just has to provide some visual indication
of the number of customers queuing, e.g. a line of '*'s.
Alternatively, if its queue is empty, the Checkout becomes idle and notifies the
Shop of this change.
The version of List used for this example could be that given in Chapter 26 Using a ListIterator 27.4.3 Organizing the checkouts
with the extra features like a ListIterator. The Checkout::WorkLoad()
function can use a ListIterator to run down the list of queuing customers getting The addition of Checkout objects in the program requires only a minor change in
each to state the number of items purchased. Unscanned items from the current class Customer . Its Customer::Run() function no longer sets its state to
customer should also be factored into this work load estimate. terminated, instead a Customer should set its state to eIDLE and ask the Shop object
to find it a Checkout where it can queue.
However, classes Shop and Manager must have major extensions to allow for
long Checkout::WorkLoad() Checkouts.
{ Additional The Manager object will be telling the Shop to add or remove Checkouts; the
long res = 0; responsibilities for type (fast or standard) will be specified in these calls:
ListIterator LL(&fQueue); ListIterator class Shop
LL.First();
992 Supermarket example
Organizing the Checkouts 991
The Shop has to keep track of the various Checkouts, keeping idle and busy Extra data members There are two additional calls to new functions that will become extra private
checkouts separately. It would probably be easiest if the Shop had four List member functions of class Manager . The function Sortoutcheckouts() will
objects in which it stored Checkouts: apply the full set of rules that apply during opening hours; the Closing-
checkouts() function will apply the simpler "after 7 p.m. rule".
List fFastIdle; A function like Closingcheckouts() is straightforward. The Manager can get
List fStandardIdle; details of the numbers of checkouts (fast and standard, idle and working) by
List fFastWorking; interrogating the List data members of the Shop. Using these data, it can arrange
List fStandardWorking; to close checkouts as they become idle (making certain that some checkouts are left
open so long as there are Customers still shopping):
The AddCheckout(), RemoveIdleCheckout() will both involve "idle" lists; the
ctype argument can identify which list is involved. void Manager::Closingcheckouts()
When a Checkout notifies the Shop that it has stopped working, the Shop can {
move that Checkout from a "working" list to an "idle" list (the Shop can ask the /* It is after 7pm, close the fast checkouts as they
Checkout its type and so determine which lists to use). Similarly, when a become idle. */
int fastidle = fs->fFastIdle.Length();
Checkout starts, the Shop can move it from an "idle" list to a "working" list (and for(int i=0; i < fastidle; i++)
also get it involved in the simulation by inserting it into the priority queue as well). fs->RemoveIdleCheckout(kFAST);
The only one of these additional member functions that is at all complex is the
Checkout::QueueMe() function which is outlined below. int standardidle = fs->fStandardIdle.Length();
The Manager object will need information on the numbers of idle and busy Manager exploits int standardworking = fs->fStandardWorking.Length();
checkouts etc. Since the Manager is a friend of class Shop, it can simply make calls friend relation
/*
like: So long as there are checkouts working can get rid of
all idle ones (if any).
long num_working = fs->fFastWorking.Length() + */
fs->fStandardWorking.Length(); if((standardworking > 0) && (standardidle > 0)) {
long num_idle = fs->fFastIdle.Length() + for(i=0; i < standardidle; i++)
fs->fStandardIdle.Length(); fs->RemoveIdleCheckout(kSTANDARD);
return;
Here, the Manager uses its Shop* pointer fs to access the Shop object, exploiting }
its friend relation to directly use a private data member like fFastWorking; the /*
List::Length() function is then invoked for the chosen List object. If there are no customers left close all idle checkouts
The additional responsibilities for class Manager all relate to the application of */
those rules for choosing when to open and close checkouts. The Manager::Run() if(fs->fCustomersPresent == 0) {
function needs to get these rules applied each time a check on the shop is made; this for(i=0; i < standardidle; i++)
fs->RemoveIdleCheckout(kSTANDARD);
function now becomes something like: return;
}
void Manager::Run()
{ /*
994 Supermarket example
Organizing the Checkouts 993
May end up with state where there are customers still class name and details of
class Manager :
shopping but all checkouts idle. Close all but one. public Activity inheritance
*/
for(i=0; i < standardidle - 1; i++) private data and functions;
fs->RemoveIdleCheckout(kSTANDARD); short ft, fready
} fmaxcheckouts, fminfast, own data;
fminstandard,
fQueuingLast,
fShoppingLast
The rules that define normal operation are too complex to be captured in a single Use "top down fqlen, fwork;
function. Once again, the functional decomposition approach has to be applied. functional Shop *fs;
decomposition" for links to collaborators
The Manager has to consider three aspects: checking that minimum numbers of complex functions public interface
checkouts are open, checking for idle checkouts that could be closed, and checking
for excessive queues that necessitate opening of checkouts. Inevitably, the Manager(Shop* s); constructor
Sortoutcheckouts() function becomes: virtual void Run(); Arranges display of shop and running of
checkout rules
Figures 27.8 and 27.9 show the finalized design diagrams for classes Manager and Figure 27.8 Final design diagram for class Manager.
Shop.
Class Manager retains a very simple public interface. The Manager object is
only used in a small number of ways by the rest of the program. However, the
things that a Manager gets asked to do are complex, and consequently the class has
a large number of private implementation functions.
Class Shop has an extensive public interface – many other objects ask the Shop
to perform functions. Most of these member functions are simple (just increment a
count, or move the requestor from one list to another); consequently, there is only
limited need for auxiliary private member functions.
Diagram 27.9 also indicates that a "friend" relationship exists that allows a
Manager object to break the normal protection around the private data of the Shop
object.
996 Supermarket example
Organizing the Checkouts 995
class Shop Next, the function checks for idle Checkouts (both types if the customer is
allowed to used the fast lanes, otherwise just the "idle standard" Checkouts). If
PriorityQ fSim; private data and functions;
long fTime, fidle, fworktime;
there is a suitable idle Checkout, the Customer can be queued and the function
short fCustomersPresent, own data; includes instances finishes:
fCustomersQueuing
ffastlanemax; of simple classes like
List, myhistogram, and void Shop::QueueMe(Customer* cc)
myhistogram fPurchases, fQTimes, PriorityQ
fShopTimes, fTotalTimes;
{
int fast = (cc->ItemsPurchased() <= ffastlanemax) ||
List fFastIdle, fStandardIdle,
friend class Manager (99 == (rand() % 100));
fFastWorking, fStandardWorking;
Manager *fManager; /*
Door *fDoor; links to collaborators If appropriate, try for an idle checkout
public interface */
Shop(); if(fast && (fFastIdle.Length() > 0)) {
constructor, initialize simple variables
void Setup();
Checkout *c = (Checkout*) fFastIdle.Nth(1);
create Manager, Door etc; insert in PriorityQ c->AddCustomer(cc);
void Run();
run simulation loop return;
void AddCustomer(Customer* c);
update customer counts etc }
void CustomerLeaves();
void DisplayState();
organize display of time, outputs from checkouts
if(fStandardIdle.Length() > 0) {
void LogShopTime(int); functions that record statistics on Customers
Checkout *c = (Checkout*) fStandardIdle.Nth(1);
void LogQueueTime(int); c->AddCustomer(cc);
void LogTotalTime(int); return;
void LogNumberPurchases(int); }
void LogIdleTime(int);
functions that record statistics on Checkouts If there is no idle Checkout, the function has to search through either both lists
void LogOpenTime(int);
of working Checkouts (or just the list of standard checkouts) to find the one with
long Time();
Access function for "system time" the least load. These searches through the lists can again take advantage of
ListIterators:
void CloseDoor();
void AddCheckout(short ctype); Functions changing state of system, number
long least_load = LONG_MAX;
void RemoveIdleCheckout(short ctype);
Checkout *best = NULL;
of checkouts etc
if(fast) {
void NoteCheckoutStarting(Checkout* c);
Functions rearranging Checkouts, placing ListIterator L1(&fFastWorking);
void NoteCheckoutStopping(Checkout* c);
Customers on Checkout queues L1.First();
void QueueMe(Customer* cc);
while(!L1.IsDone()) {
Checkout *c = (Checkout*) L1.CurrentItem();
long load = c->WorkLoad();
void PrintTime(); Pretty print time as part of a report
if(load < least_load) {
void ReportStatistics();
least_load = load;
Print final statistics
best = c;
}
L1.Next();
Figure 27.9 Final design diagram for class Shop. }
if((shopping>fShoppingLast) || (queuing > fQueuingLast)) { If getting busier, add If the average queue length is too great, an extra checkout should be added:
CheckQueuesAtFastCheckouts(); more checkouts
CheckTimesAtStandardCheckouts(); if(average < fqlen)
} return;
fShoppingLast = shopping;
fQueuingLast = queuing; fs->AddCheckout(kFAST);
} cout << "Added a fast checkout." << endl;
}
The function that adds fast checkouts is representative of the remaining
functions. If identifies circumstances that preclude the opening of new checkouts Execution
(e.g. maximum number already open) and if any pertain it abandons processing:
You should find it fairly simple to complete the implementation (Exercise 1). The
void Manager::CheckQueuesAtFastCheckouts() program should produce outputs like the following:
{
/* Time 8:30a.m. Fast Checkouts:
if there are any idle checkouts do nothing. Number of customers 65 *|******************
*/ Fast Checkouts: *|************
*|*********************** Standard Checkouts:
int idle = fs->fFastIdle.Length() + Standard Checkouts: *|*********
fs->fStandardIdle.Length(); *|******************** *|**************
if(idle != 0) Added a fast checkout. Time 9:30a.m.
return; Added 1 standard checkouts. Number of customers 34
------ There are 2 idle fast
/* Time 8:40a.m. checkouts.
If don't have any fast checkouts open, can't check Number of customers 84 Fast Checkouts:
1000 Supermarket example
Organizing the Checkouts 999
EXERCISES
1 Complete a working version of the Supermarket program.
• decomposition, i.e. breaking a problem into smaller subparts that can be dealt with
largely independently;
and
• iteration, a problem gets worked through many times at increasing levels of detail;
provisional decisions get made, are tested through prototyping, and subsequently
may be revised.
Sometimes, the things that an object must do are complex (e.g. the Manager object's
application of the scheduling rules in the Supermarket example). In such cases, you can
adopt a "top-down functional decomposition approach" because you are in the same
situation as before – there is one clearly defined function to perform and the data are
relatively simple (the data needed will all be represented as data members of the object
performing the action).
You can code and test a class that defines any one type of data and the related
functionality. Then you can return to the whole problem. But now the problem has
been simplified because some details are now packaged away into the already built
component.
When you return to the whole problem, you try to characterize the workings
program in terms of interactions amongst example objects: e.g. "the structure will ask
each part in its components list to draw itself".
If you design using top-down functional decomposition, you tend to see each
problem as unique. If take an object based approach, you are more likely to see
commonalities.
As just noted, most object-based programs seem to have "composite" structures that Opportunity for reuse
group separate components. The mechanisms for "grouping" are independent of the
specific application. Consequently, you can almost always find opportunities for
reusing standard components like lists, dynamic arrays, priority queues. You don't need
to create a special purpose storage structure; you reuse a standard class.
An object-based approach to design has two major benefits: i) a cleaner, more
effective decomposition of a complex problem, and ii) the opportunity to reuse
components.
The use of abstract classes and inheritance, "object oriented design", brings further
benefits. These have been hinted at in the Supermarket example, and in the discussion
above regarding the CAD program and the parts that it manipulates. These design
benefits are explored a little more in Part V.
You start by thinking about prototypical objects, not the classes and certainly not
abstract class hierarchies.
Identifying Some ideas for the prototypical objects can come from a detailed reading of the full
prototypical objects specification of the program (the "underline the nouns" approach). As previously
noted, there are problems with too literally underlining the nouns; you may end
modelling the world in too much detail. But it is a starting point for getting ideas as to
things that might be among the more important objects – thus, you can pick up the need
for Customers and Checkouts from the description of the Supermarket problem.
Usually, you will find that the program has a few objects that seem to be in for the
duration, e.g. the UserInteraction and CardCollection objects in the RefCards
program, or the Shop , Manager, and Door objects in the Supermarket example. In
addition there are other objects that may be transient (e.g. the Customers). An
important aspect of the design will be keeping track of when objects get created and
destroyed.
Scenarios-1 Once you have formed at least some idea as to the objects that might be present in
the executing program, it is worthwhile focussing on "events" that the program deals
with and the objects that are involved. This process helps clarify what each kind of
object owns and does, and also begins to establish the patterns of communication
amongst classes.
You make up scenarios for each of the important "events" handled by the program.
They should include the scenarios that show how objects get created and destroyed.
You must then compare the scenarios to check that you are treating the objects in a
consistent manner. At the same time, you make up lists of what objects are asked to do
and what data values you think that they should own.
Products of the first Once you have seen the ways that your putative objects behave in these scenarios
step you compose your initial class descriptions. These will include:
• data owned: e.g. "several histograms, some Lists to store Checkouts, a timer value,
…"
• uses: (summary of requests made to instances of other classes), e.g. "Run() (all
Activity subclasses), Checkout::AddCustomer(), …"
Object based design 1009
The responsibilities of the classes are all the things that you have seen being asked of
prototypical instances in the scenarios that you have composed. It is often worthwhile
noting the classes of client objects that use the functions of a class. In addition, you
should note all the requests made to instances of other classes.
The pattern of requests made to and by an instance of a class identify its "Collaborators"
collaborators. If two objects are to collaborate, they have to have pointers to one
another (e.g. the Shop had a Manager* pointer, and the collaborating Manager had a
Shop* pointer). These pointers must get set before they need to be used.
Setting up the pointers linking collaborators hasn't been a problem in the examples
presented so far. In more complex programs, the establishment of collaboration links
can become an issue. Problems tend to be associated with situations where instances of
one class can be created in different ways (e.g. read from a file or interactive command
from a user). In one situation, it may be obvious that a link should be set to a
collaborator. In the other situation, it may not be so obvious, and the link setting step
may be forgotten. Forgetting links results in problems where a program seems to work
intermittently.
The highlighting of collaborations in the early design stage can act as a reminder so
that later on, when considering how instances of classes are created, you can remember
to check that all required links are being set.
Sometimes you will get a class whose instances get asked to look after data and Isolable components
perform various tasks related to their data, but which don't themselves make requests to
any other objects in the system. They act as "servers" rather than "clients" in all the
"collaborations" in which they participate.
Such classes represent completely isolable components. They should be taken out of
the main development. They can be implemented and tested in isolation. Then they
can be reintroduced as "reusable" classes with the same standing as classes from
standard libraries. The InfoStore example program provides an example; its Vocab
class was isolable in this way.
You will get class hierarchies in two ways. Occasionally, the application problem Class hierarchies
will already have a hierarchy defined. The usual example quoted is a program that must
manipulate "bank accounts". Now "bank accounts" are objects that do various things
like accept debits and credits, report their balance, charge bank fees, and (sometimes)
pay interest. A bank may have several different kinds of account, each with rather
different rules regarding fees and interest payments. Here a hierarchy is obvious from
the start. You have the abstract class "bank_account" which has pure virtual functions
"DeductCharges()" and "AddInterest()". Then there are the various specialized
subclasses ("loan_account", "savings", "checking", "checking_interest") that implement
distinct versions of the virtual functions.
Other cases are more like the Supermarket example. There we had classes Manager,
Door, Checkout, and Customer whose instances all had to behave "in the same way" so
as to make it practical for the simulation system to use a single priority queue. This was
handled by the introduction of an abstraction, class Activity, that became the base
class for the other classes. Class Activity wasn't essential (the Shop could have used
1010 Design and documentation: 2
four different priority queues); but its introduction greatly simplified design. The class
hierarchy is certainly not one that you would have initially expected and doesn't reflect
any "real world" relationship. (How many common features can you identify between
"doors" and "customers"?)
Second step Your initial classes are little more than "fuzzy blob" outlines. You have some idea
as to the data owned and responsibilities but details will not have been defined. For
example, you may have decided that "class Vocab owns the vocabulary and provides
both fast lookup of words and listings of details", or that "class Manager handles the
scheduling rules". You won't necessarily have decided the exact form of the data (hash-
table or tree), you won't have all the data members (generally extra counters and flags
get added to the data members when you get into more detail), and you certainly won't
have much idea as to how the functions work and whether they necessitate auxiliary
functions.
The next step in design is, considering the classes individually, to try to move from a
"fuzzy blob" outline to something with a firm definition. You have to define the types
of all data members (and get into issues like how data members should be initialized).
Each member function has to be considered, possibly being decomposed into simpler
auxiliary private member functions.
Outputs from design The output of this step should be the class declarations and lists of member functions
step like those illustrated in the various examples. Pseudo-code outlines should be provided
for all the more complex member functions.
main() The main() function is usually trivial: create the principle object, tell it to run.
Module structure These programs are generally built from many separate files. The design process
should also cover the module (file) structure and the "header dependencies". Details
should be included with the rest of the design in the form of a diagram like that shown
in 27.6.
Tests As always, some thought has to be given to the testing of individual components and
of the overall program.
Diagrams are a much more important part of the documentation of the design of an
object-based program than they were for the "top-down functional decomposition"
programs.
Your documentation should include:
• "fuzzy" blob diagrams showing the classes and their principle roles (e.g. Figures
22.1 and 22.9);
• a hierarchy diagram (if needed); this could be defined in terms of the fuzzy blob
classes (e.g. Figure 27.4) or the later design classes;
• class "design diagrams" that summarize the data and function members of a class,
(e.g. Figures 27.8 and 27.9);
• module structure;
object itself. In addition there will have to be various forms of "window" object
used to display other information. Since there will be many "monsters" and many
"items", standard collection classes will be needed.
Windows hierarchy There are two separate class hierarchies as well as a number of independent
classes. One limited class hierarchy defines "windows". There is a basic "window"
class for display of data with a couple of specializations such as a window used to
output numeric data (e.g. player's "health") and a window to get input. (These
"window" classes are further elaborated in the next chapter.)
"Dungeon Items" There is also a hierarchy of "things found in the dungeon". Some, like the
hierarchy "items" that must be collected, are passive, they sit waiting until an action is
performed on them. Others, like the player and the monsters, are active. At each
cycle of the game, they perform some action.
Polymorphic pointers Naturally, there are many different kinds of monster. Each different kind
(subclass) employs a slightly different strategy when trying to achieve their
common objective of "eliminating the player". This is where the polymorphism
comes in. The "dungeon" object will work with a collection of monsters. When it
is the monsters' turn, the code has a loop that lets each monster "run" ( Monster *m;
…; m->Run(); ). The pointer m is polymorphic – pointing to different forms of
monster at different times. Each different form has its own interpretation of Run().
• Read game details from a text file. These details are to include the map layout,
the initial positions and other data defining collectable items, the monsters, and
the player.
• Provide a display similar to that shown in Figure 29.1. This display is to
include the main map window (showing fixed features like walls, position of
collectable items, and current positions of active items) and other windows that
show the player's status.
• Run the game. The game terminates either by the player object acquiring all
collectable items or by its "health" falling to zero.
• Operate a "run cycle" where the user enters a movement command (or "magic
action" command – see below), the command is executed, and then all other
active items get a chance to run.
• Arrange that the player object acquire a collectable item by moving over the
point where the item is located. Acquisition of a collectable item will change
one or more of the player object's "health", "wealth", or "manna" attributes.
Once taken by the player object, collectable items are to be deleted from the
game.
• Employ a scheme where single character commands identify directional
movements or "magic actions" of the player.
Introduction 1017
"Monsters" Collectable
Main window
Items
(map display)
+----------------------------------------------------------------+
|w # w # # g # # $ * = # |
| ###### # # ##### # ######## # ####### ###### |
| # # ######## # # # #### # # # # $ # # # # |
| # #### # # w # # # # p # # # # # # # # # # |
| # # # # # $ # # # # # # # ##########w### # # |
| # # ## # # ###### ##### # # # # #### # # # # |
| # # # # # # # ## # # # # ##### #|
| # # # # # # ######### # # # ## ############ ##### # # |
| # # # # # # # # # # # # # # # #### |
| # # ## # # ### ###### # ######### # # # # # |
| # # # # # # ##### # ####### # # # # # ## |
| # ######## # # # # # # # # # = # # # # |
|#### # # # # # # # g # # # # # # |
| ###### # #### # # ####### # # # # # |
| # d # # ################# # # # # # |
| $ # $ # # # g # # # # # |
| $ # # h # # # # ## |
| # #### # # ######## |
+----------------------------------------------------------------+
|Health 37| |Manna 6| |Wealth 30|
+-----------------+ +-----------------+ +-----------------+
|Direction 4 | Number window
+-----------------+ Player
Edit window object
• Handle movements. The player and most types of monster are limited in their
scope for movement and cannot pass through walls or outside of the map area.
More than one monster may be located on the same point of the map; monsters
are allowed to occupy the same points as collectable items. When several
dungeon items are located at the same point, only one is shown on the map.
The player's health and manna ratings increase slowly as moves are made.
Inheritance and Polymorphism 1018
If not immediately adjacent to the player object, some monsters "look" for the
player and, if they can "see" the player, they may advance toward it or launch a
projectile.
If they are not able to detect the player object, a monster object will perform its
"normal movement" function. This might involve random movement, no
movement, or some more purposive behaviour.
29.2 DESIGN
29.2.1 Preliminaries
This "preliminaries" section explores a few aspects of the program that seem pretty
much fixed by the specification. The objective is to fill out some details and get a
few pointers to things that should be considered more thoroughly.
For example the specification implies the existence of "class Dungeon", "class
Player", "class Monster", a class for "collectable items" and so forth. We might as
well jot down a few initial ideas about these classes, making a first attempt to
answer the perennial questions "What does class X do? What do instances of class
X own?". Only the most important responsibilities will get identified at this stage;
more detailed consideration of specific aspects of the program will result in further
responsibilities being added. Detailed analysis later on will also show that some of
the classes are interrelated, forming parts of a class hierarchy.
Design preliminaries 1019
Other issues that should get taken up at this preliminary stage are things like the
input files and the form of the main program. Again, they are pretty much defined
by the specification, but it is possible to elaborate a little.
main()
We can start with the easy parts – like the main() function! This is obviously
going to have the basic form "create the principal object, tell it to run":
int main()
{
Dungeon *d;
d = new Dungeon;
d->Load(aName);
return 0;
}
The principal object is the "Dungeon" object itself. This has to load data from a file
and run the game. When the game ends, a message of congratulations or commis-
erations should be printed. The Dungeon::Run() function can return a win/lose
flag that can be used to select an appropriate message that is then output by some
simple Terminate() function.
The files are to be text files, created using some standard editor. They had better
specify the size of the map. It would be simplest if the map itself were represented
by the '#' and ' ' characters that will get displayed. If the map is too large, the top-
left portion should be used.
Following the map, the file should contain the data necessary to define the
player, the collectable items, and the monsters. The program should check that
these data define exactly one player object and at least one collectable item. The
program can simply terminate if data are invalid. (It would help if an error message
printed before termination could include some details from the line of input where
something invalid was found.)
Collectable items and other objects can be represented in the file using a
character code to identify type, followed by whatever number of integers are
needed to initialize an object of the specified type. A sentinel character, e.g. 'q', can
mark the end of the file.
A plausible form for an input file would be:
Inheritance and Polymorphism 1020
Any additional details can be resolved once the objects have been better
characterized.
class Dungeon
Dungeon::Load
Open file with name given
Load map
Load other data
Dungeon::Run()
Finalize setting up of displays
Draw initial state
while(player "alive")
player "run"
Design preliminaries 1021
The displays must be set up. Obviously, the Dungeon object owns some window
objects. (Some might belong to the Player object; this can be resolved later.) The
Dungeon object will get primary responsibility for any work needed to set up
display structures.
The main loop has two ways of terminating – "death" of player, and all
collectable objects taken. The game was won if the player is alive at the end.
The Dungeon object owns the collection of monsters, the collection of
collectable items, and the player object. Collections could use class List or class
DynamicArray.
The Player object will need to access information held by the Dungeon object.
For example, the Player object will need to know whether a particular square is
accessible (i.e. not part of a wall), and whether that square is occupied by a
collectable item or a monster. When the Player takes a collectable item, or kills a
monster, the Dungeon should be notified so that it can update its records. Similarly,
the monsters will be interested in the current position of the Player and so will
need access to this information.
Consequently, in addition to Load() and Run(), class Dungeon will need many
other member functions in its public interface – functions like "Accessible()", and
"Remove Monster()". The full set of member functions will get sorted out steadily
as different aspects of the game are considered in detail.
Most "dungeon items" will need to interact with the Dungeon object in some
way or other. It would be best if they all have a Dungeon* data member that gets
initialized as they are constructed.
class Player
The Player object's main responsibility will be getting and interpreting a command
entered by the user.
Commands are input as single characters and define either movements or, in this Movement commands
game, directional applications of destructive "magic". The characters 1…9 can be
used to define movements. If the keyboard includes a number pad, the convenient
mapping is 7 = "north west", 8 = "north", 9 = "north east", 4 = "west" and so forth
(where "north" means movement toward the top of the screen and "west" means
movement leftwards). Command 5 means "no movement" (sometimes a user may
want to delay a little, e.g. to let the player object recover from injury).
The "magic action" commands can use the keys q, w, e, a, d, z, x, and c (on a Magic action
standard QWERTY keyboard, these have the same relative layout and hence define commands
the same directional patterns as the keys on the numeric keypad).
The main Player::Run() function will be something like: Player::Run()
Inheritance and Polymorphism 1022
Player::Run()
char ch = GetUserCommand();
if(isdigit(ch)) PerformMovementCommand(ch);
else PerformMagicCommand(ch);
UpdateState();
ShowStatus();
class Collectable
The collectable items could be made instances of a class Collectable. It does not
seem that there will be any differences in their behaviours, so there probably won't
be any specialized subclasses of class Collectable. At this stage, it doesn't appear
as if Collectable objects will do much at all.
They have to draw themselves when asked (presumably by the Dungeon object
when it is organizing displays); they will own a character that they use to represent
themselves. They will also need to own integer values representing the amounts by
which they change the Player object's health etc when they get taken. Some
access functions will have to exist so that the Player object can ask for the relevant
data.
A monster object moving onto the same point as a Collectable object will
hide it. When the monster object moves away, the Collectable object should be
redrawn. The Dungeon object had better arrange to get all Collectable objects
draw themselves at regular intervals; this code could be added to the while() loop
in Dungeon::Run().
class Monster
Attack();
else
if(CanDetect())
Advance();
else
NormalMove();
Previous experience with practical windowing systems has influenced the approach
developed here for handling the display. As illustrated in Figure 29.2, the display
system uses class WindowRep and class Window (and its specialized subclasses).
WindowRep
Owns
position relative to screen
WindowRep arrays of characters
with background and
current content
Owns dimensions, framing flag
array of characters Window Does
with screen content put character in current
Does (or background) image
put character on screen organize drawing (all or
move cursor just content area)
get input clear current image
delays return size details etc.
Singleton class;
NumberItem EditText
Owns
Owns
numeric value and label label, text buffer,
Does
size limit
set value (and change display) Does
return current value accept input characters
return text buffer
set text buffer
"Singleton" pattern A class for which there can only be a single instance, an instance that must be
accessible to many other objects, an instance that may have to create auxiliary data
structures or perform some specialized hardware initialization – this is a common
pattern in programs. A special term "singleton class" has been coined to describe
this pattern. There are some standard programming "cliches" used when coding
such classes, they are followed in this implementation.
The unique WindowRep object used in a program provides the following
services:
• PutCharacter
Outputs a character at a point specified in screen coordinates.
• MoveCursor
Positions the "cursor" prior to an input operation.
• GetChar
Inputs a character, echoing it at the current cursor position
• Clear
Clears the entire screen.
• CloseDown
Closes down the windowing system and gets rid of the program's WindowRep
object
WindowRep and Window classes 1025
Window
Window objects, instances of class Window or its subclasses, are meant to be things
that own some displayable data (an array of characters) and that can be "mapped
onto the screen". A Window object will have coordinates that specify where its
"top-left" corner is located relative to the screen. (In keeping with most cursor-
addressable screen usage, coordinate systems are 1-based rather than 0-based so the
top left corner of the screen is at coordinate (1,1).) Window objects also define a
size in terms of horizontal and vertical dimensions. Most Window objects are
"framed", their perimeters are marked out by '-', '|', and '+' characters (as in Figure
29.1). A Window may not fit entirely on the screen (the fit obviously depends on
the size and the origin). The WindowRep object resolves this by ignoring output
requests that are "off screen".
Window objects have their own character arrays for their displayable content. Background and
Actually, they have two character arrays: a "background array" and a "current current (foreground)
window contents
array". When told to "draw" itself, a Window object executes a function involving a
double loop that takes characters from the "current array", works out where each
should be located in terms of screen coordinates (taking into account the position of
the Window object's top-left corner) and requests the WindowRep object to display
the character at the appropriate point.
The "background array" defines the initial contents of the window (possibly all
blank). Before a window is first shown on the screen, its current array is filled
from the background. A subsequent "clear" operation on a specific point in the
window, resets the contents of the current window to be the same as the
background at the specified point.
A specific background pattern can be loaded into a window by setting the
characters at each individual position. In the dungeon game, the Dungeon object
owns the window used to display the map; it sets the background pattern for that
window to be the same as its map layout.
The Window class has the following public functions: What does a Window
do?
• Constructor
Sets the size and position fields; creates arrays.
• Destructor
Gets rid of arrays. (The destructor is virtual because class Window is to serve
as the base class of a hierarchy. In class hierarchies, base classes must always
define virtual destructors.)
Inheritance and Polymorphism 1026
• Set, Clear
Change the character at a single point in the current (foreground) array.
• SetBkgd
Change the character at a single point in the background array.
• PrepareContent
Initialize current array with copy of background and, if appropriate, add frame.
• ShowAll, ShowContent
Output current array via the WindowRep.
The class requires a few auxiliary member functions. For example, the coordinates
passed to functions like Set() must be validated.
What does a Window A Window owns its dimension data and its arrays. These data members should
own? be protected; subclasses will require access to these data.
Provision for class The functionality of class Window will be extended in its subclasses. However
hierarchy the subclasses don't change the existing functions like ShowAll(). Consequently,
these functions are not declared as virtual.
"Inheritance for The relationships between class Window and its subclasses, and class Monster
extension" and and its subclasses, are subtly different. The subclasses of Window add functionality
"inheritance for
redefinition" to a working class, but don't change its basic behaviours. Consequently, the
member functions of class Window are non-virtual (apart from the destructor).
Class Monster defines a general abstraction; e.g. all Monster object can execute
some "NormalMove" function, different subclasses redefine the meaning of
"NormalMove". Many of the member functions of class Monster are declared as
virtual so as to permit such redefinition. Apart from the differences with respect to
the base class member function being virtual or non-virtual, you will also see
differences in the accessibility of additional functions defined by subclasses. When
inheritance is being used to extend a base class, many of the new member functions
appear in the public interface of the subclass. When inheritance is being used to
specialize an existing base class, most of the new functions will be private
functions needed to implement changes to some existing behaviour. Both styles,
"inheritance for extension" and "inheritance for redefinition", are common.
This program has two subclasses for class Window: NumberItem and EditText.
Instances of class NumberItem are used to display numeric values; instances of
class EditText can be used to input commands. As illustrated in Figure 29.3, these
are displayed as framed windows that are 3 rows deep by n-columns wide. The left
part of the window will normally contain a textual label. The right part is used to
display a string representing a (signed) numeric value or as input field where input
characters get echoed (in addition to being stored in some data buffer owned by the
object).
WindowRep and Window classes 1027
Frame
+------------------+ +------------------+
|Health 100| |Direction |
+------------------+ +------------------+
NumberItem
In addition to the character arrays and dimension data that a NumberItem object What does a
already has because it is a kind of Window, a NumberItem owns a long integer NumberItem own?
holding the value to be displayed (and, also, an integer to record the number of
characters used by the label so that numeric outputs don't overwrite the label).
The constructor for class NumberItem completes the normal initialization What does a
processes of class Window. The auxiliary private member function SetLabel() is NumberItem do?
used to copy the given label string into the background array. The inherited
PrepareContent() function loads the current array from the background and adds
the frame. Finally, using the auxiliary ShowValue() function, the initial number is
converted into characters that are added to the current image array.
Once constructed, a NumberItem can be used by the program. Usually, usage
will be restricted to just three functions – GetVal() (return fVal;), SetVal()
(changes fVal and uses ShowValue() to update the current image array), and
ShowAll() (the function, inherited from class Window , that gets the window
displayed).
Inheritance and Polymorphism 1028
EditText
What does a EditText In addition to the data members inherited from class Window, a EditText owns
own? a (large) character buffer that can be used to store the input string, integers to record
the number of characters entered so far and the limit number. Like the
NumberItem, an EditText will also need a record of the width of its label so that
the input field and the label can be kept from overlapping.
What does a The constructor for class EditText completes the normal initialization
NumberItem do? processes of class Window. The auxiliary private member function SetLabel() is
used to copy the given label string into the background array. The inherited
PrepareContent() function loads the current array from the background and adds
the frame. The buffer, fBuf, can be "cleared" (by setting fBuf[0] to '\0')..
The only member functions used in most programs would be GetInput() ,
GetVal() and ShowAll(). Sometimes, a program might want to set an initial text
string (e.g. a prompt) in the editable field (function SetVal()).
WindowRep and Window classes 1029
Class Monster is meant to be an abstraction; the real inhabitants of the dungeon are
instances of specialized subclasses of class Monster.
Class Monster has to provide the following functions (there may be others
functions, and the work done in these functions may get expanded later, this list
represents an initial guess):
• Read
A Monster is supposed to read details of its initial position, its "health" and
"strength" from an input file. It will need data members to store this
information.
• A Move function.
Calls Erase(), changes coords to new coords given as argument, and calls
Draw().
• GetHit function
Reduces "health" attribute in accord with damage inflicted by Player.
• has a CanDetect() function that returns true when the player is within a fixed
distance of the point where the Ghost is located (the presence of intervening
walls makes no difference to a Ghost object's power of detection);
Inheritance and Polymorphism 1030
• has an Advance() function that moves the Ghost one square vertically,
diagonally, or horizontally so as to advance directly toward the player; a Ghost
can move through walls;
Class Ghost needs to redefine only the Advance() and CanDetect() functions.
Since a Ghost does not require any additional data it does not to change the
Read() function.
class Patrol A Patrol is a Monster that:
• has a CanDetect() function that returns true there is a clear line of sight
between it and the Player object;
• has an Advance() function that instead of moving it toward the Player allows
it to fire a projectile that follows the "line of sight" path until it hits the Player
(causing a small amount of damage), the movement of the projectile should
appear on the screen;
The patrol route should be defined as a sequence of points. These will have to be
read from the input file and so class Patrol will need to extend the Monster::
Read() function. The Patrol::Read() function should check that the given
points are adjacent and that all are accessible (within the bounds of the dungeon
and not blocked by walls).
Class Patrol will need to define extra data members to hold the route data. It
will need an array of Pt objects (this can be a fixed sized array with some
reasonable maximum length for a patrol route), an integer specifying the number of
points in the actual route, an integer (index value) specifying which Pt in the array
the Patrol is currently at, and another integer to define the direction that the
Patrol is walking. (The starting point given for the Patrol will be the first
element of the array. Its initial moves will cause it to move to successive elements
of the Pt array; when it reaches the last, it can retrace its path by having the index
decrease through the array.)
class Wanderer A Wanderer is a Monster that:
• has a CanDetect() function that returns true there is a clear line of sight
between it and the Player object;
• has an Advance() function that causes the Wanderer to move one step along
the line of sight path toward the current position of the Player;
A Wanderer will need to remember its current direction of movement so that it can
keep trying to go in the same direction. Integer data members, representing the
current delta-x, delta-y changes of coordinate, could be used.
There are similarities between the Monster and Player classes. Both classes Commonalities
define things that have a Pt coordinate, a health attribute and a strength attribute. between class Player
and class Monster
There are similarities in behaviours: both the Player and the Monsters read initial
data from file, get told that they have been hit, get asked whether they are still alive,
get told to draw themselves (and erase themselves), have a "move" behaviour that
involves erasing their current display, changing their Pt coordinate and then
redrawing themselves. These commonalities are sufficient to make it worth
defining a new abstraction, "active item", that subsumes both the Player and the
Monsters.
This process of abstracting out commonalities can be repeated. There are
similarities between class Collectable and the new class ActiveItem. Both are
things with Pt coordinates, draw and erase behaviours; they respond to queries
about where they are (returning their Pt coordinate). These common behaviours
can be defined in a base class: DungeonItem.
The DungeonItem class hierarchy used in the implementation is shown in Figure
29.4.
Completion of the design stage involves coming up with the class declarations of
all the classes, possibly diagramming some of the more complex patterns of
interaction among instances of different classes, and developing algorithms for any
complicated functions.
Class Dungeon
owns
link to Dungeon, point
DungeonItem coord, display symbol
does
draws itself, erases
itself, reads data from
file, reports where it is
Collectable ActiveItem o w n s
health & strength
attributes
does
owns reads extra data
3 integer value fields "runs" (define in subclass)
does gets hit
reads extra data, reports if still "alive"
reports values moves
Player Monster
owns owns
"manna", wealth attributes, (nothing extra)
movecount, links to windows does
used to display status "runs" using
does "Can Attack", "Attack",
reads extra data "Can Detect", "Advance",
gets user command, takes "NormalMove"
collectables, attacks
monsters, interprets
movement & magic commands
class Dungeon {
public:
Dungeon();
~Dungeon();
void Load(const char filename[]);
int Run();
int Accessible(Pt p) const;
Window *Display();
Player *Human();
private:
int ClearRow(Pt p1, Pt p2, int max, Pt path[]);
int ClearColumn(Pt p1, Pt p2, int max, Pt path[]);
int ClearSemiVertical(Pt p1, Pt p2, int max,
Pt path[]);
int ClearSemiHorizontal(Pt p1, Pt p2, int max,
Pt path[]);
void LoadMap(ifstream& in);
void PopulateDungeon(ifstream& in);
void CreateWindow();
DynamicArray fProps;
DynamicArray fInhabitants;
Player *fPlayer;
char fDRep[MAXHEIGHT][MAXWIDTH];
Window *fDWindow;
int fHeight;
int fWidth;
};
The Dungeon object owns the map of the maze (represented by its data elements What does a
fDRep[][] , fHeight , and f W i d t h ). It also owns the main map window Dungeon own?
(fDWindow), the Player object (fPlayer ) and the collections of Monsters and
Collectables. Data members that are instances of class DynamicArray are used
for the collections (fInhabitants for the Monsters , f P r o p s for the
Collectables).
The Load() and Run() functions are used by the main program. Function What does a
Load() makes uses of the auxiliary private member functions LoadMap() and Dungeon do?
PopulateDungeon() ; these read the various data and create Monster ,
Collectable, and Player object(s) as specified by the input. The auxiliary private
member function CreateWindow() is called from Run() ; it creates the main
window used for the map and sets its background from information in the map.
Access functions like Display() and Human() allow other objects to get
pointers to the main window and the Player object. The ActiveItem objects that
move are going to need access to the main Window so as to tell it to clear and set
the character that is to appear at a particular point.
The ValidPoint() function checks whether a given Pt is within the bounds of
the maze and is not a "wall".
The functions M_at_Pt() and PI_at_Pt() involve searches through the
collections of Monsters and Collectables respectively. These function return the
first member of the collection present at a Pt (or NULL if there are no objects at that
Pt). The Remove… function eliminate members of the collections.
Class Dungeon has been given responsibility for checking whether a "clear line
of sight" exists between two Pts (this function is called in both Wanderer::
CanDetect() and Patrol::CanDetect()). The function takes as arguments the
two points, a maximum range and a Pt array in which to return the Pts along the
Inheritance and Polymorphism 1034
line of sight. Its implementation uses the auxiliary private member functions
ClearRow() etc.
The algorithm for the ClearLineOfSight() function is the most complex in
the program. There are two easy cases; these occur when the two points are in the
same row or the same column. In such cases, it is sufficient to check each point
from start to end (or out to a specified maximum) making certain that the line of
sight is not blocked by a wall. Pseudo code for the ClearRow() function is:
Cases where the line is oblique are a bit more difficult. It is necessary to check
the squares on the map (or screen) that would be traversed by the best
approximation to a straight line between the points. There is a standard approach to
solving this problem; Figure 29.5 illustrates the principle.
The squares shown by dotted lines represent the character grid of the map or
screen; they are centred on points defined by integer coordinates. The start point
and end point are defined by integer coordinates. The real line between the points
has to be approximated by a sequence of segments joining points defined by integer
coordinates. These points define which grid squares are involved. In Figure 29.5
the squares traversed are highlighted by • marks.
(11,5)
(1,1)
The algorithm has to chose the best sequence of integer points, for example
choosing point (2, 1) rather than (2, 2) and (8, 4) rather than (8, 3) or (8, 5). The
algorithm works by calculating the error associated with different points (shown by
the vertical lines in Figure 29.5). The correct y value for say x=8 can be calculated;
the errors of taking y = 3, or 4, or 5 are easy to calculate and the best approximation
is chosen. When the squares traversed have been identified, they can be checked to
determine that they are not blocked by walls and, if clear, added to the path array.
The algorithm is easiest to implement using two separate versions for the cases
where the change in x is larger or the change in y is larger. A pseudo-code outline
for the algorithm for where the change in x is larger is as follows:
current point = p1
for i < abs(xchange) do
if(error*deltay>0.5) Pick best next point
current point y += deltay
error -= deltay
error += slope*deltax
current point x += deltax
if(current point not accessible) return fail Check accessibility
Class Pt
class Pt {
public:
Pt(int x = 0, int y = 0);
int X() const;
int Y() const;
void SetPt(int newx, int newy);
Inheritance and Polymorphism 1036
Most of the member functions of Pt are sufficiently simple that they can be defined
as inline functions.
class WindowRep {
public:
static WindowRep *Instance();
void CloseDown();
void PutCharacter(char ch, int x, int y);
void Clear();
void Delay(int seconds) const;
char GetChar();
void MoveCursor(int x, int y);
private:
WindowRep();
void Initialize();
void PutCharacter(char ch);
static WindowRep *sWindowRep;
char fImage[CG_HEIGHT][CG_WIDTH];
};
The size of the image array is defined by constants CG_HEIGHT and CG_WIDTH (their
values are determined by the area of the cursor addressable screen, typically up to
24 high, 80 wide).
The static member function Instance(), and the static variable sWindowRep
are used in the implementation of the "singleton" pattern as explained in the
implementation section. Another characteristic of the singleton nature is the fact
that the constructor is private; a WindowRep object can only get created via the
public Instance() function.
Most of the members of class Window have already been explained. The actual
class declaration is:
class Window {
public:
Window(int x, int y, int width, int height,
char bkgd = ' ', int framed = 1);
virtual ~Window();
void Clear(int x, int y);
void Set(int x, int y, char ch);
void SetBkgd(int x, int y, char ch);
Finalising the clases 1037
The declarations for the specialized subclasses of class Window were given
earlier.
The base class for the hierarchy defines a DungeonItem as something that can read
its data from an input stream, can draw and erase itself, and can say where it is. It
owns a link to the Dungeon object, its Pt coordinate and a symbol.
int fMoveCount;
int fWealth;
int fManna;
NumberItem *fWinH;
NumberItem *fWinW;
NumberItem *fWinM;
EditText *fWinE;
};
Class Ghost defines the simplest specialization of class Monster. It has no extra
data members. It just redefines the default (do nothing) CanDetect() and
Advance() member functions.
The other two specialized subclasses of Monster have additional data members.
In the case of class Patrol, these must be initialized from input so the Read()
function is redefined. Both classes redefine Normal Move() as well as
CanDetect() and Advance().
Inheritance and Polymorphism 1040
Object Interactions
Figure 29.6 illustrates some of the interactions involved among different objects
during a cycle of Dungeon::Run().
Run()
Nth(i)
CanAttack()
Run()
loop Where()
Human()
Human() GetHit()
Attack()
The diagram illustrates aspects of the loop where each Monster object in the
fInhabitants collection is given a chance to run. The Dungeon object will first
interact with the DynamicArray fInhabitants to get a pointer to a particular
Monster . This will then be told to run; its Run() function would call its
CanAttack() function.
Finalising the clases 1041
In CanAttack(), the Monster would have to get details of the Player object's
position. This would involve first a call to member function Human() of the
Dungeon object to get a pointer, and then a call to Where() of the Player object.
The diagram in Figure 29.7 illustrates the case where the Player is adjacent and
the Monster object's Attack() function is called. This will again involve a call to
Dungeon::Human(), and then a call to the GetHit() function of the Player object.
Figure 29.7 illustrates some of the interactions that might occur when the a
movement command is given to Player:Run().
GetUserCommand()
Run()
GetVal()
…Movement…
MonsterAt()
loop
Attack()
GetHit()
Alive()
RemoveM()
delete
The Player object would have had to start by asking its EditText window to
get input. When EditText::GetInput() returns, the Player object could inspect
the character string entered (a call to the EditText's GetVal() function, not shown
in diagram). If the character entered was a digit, the Player object's function
PerformMovementCommand would be invoked. This would use Step() to
determine the coordinates of the adjacent Pt where the Player object was to move.
The Player would have to interact with the Dungeon object to check whether the
destination point was occupied by a Monster (or a Collectable).
The diagram in Figure 29.7 illustrates the case where there is an adjacent
Monster. The Player object informs the Monster that it has been hit. Then it
checks whether the Monster is still alive. In the illustration, the Monster object
has been destroyed, so the Player must again interact with the Dungeon object.
This removes the Monster from the fInhabitants list (interaction with the
DynamicArray is not shown) and deletes the Monster.
29.3 AN IMPLEMENTATION
The files used in the implementation, and their interdependencies are summarized
in Figure 29. 8.
Inheritance and Polymorphism 1042
Geom.h D.h
WindowRep.h
Dungeon.h Ditem.h
The files D.h and D.cp contain the DynamicArray code defined in Chapter 21.
The Geom files have the definition of the simple Pt class. The WindowRep files
contain WindowRep, Window and its subclasses. DItem.h and DItem.cp contain the
declaration and definition of the classes in the DungeonItem hierarchy while the
Dungeon files contain class Dungeon.
An outline for main() has already been given; function Terminate(), which
prints an appropriate "you won" or "you lost" message, is trivial.
There are two aspects to class WindowRep : its "singleton" nature, and its
interactions with a cursor addressable screen.
The constructor is private. WindowRep objects cannot be created by client code
(we only want one, so we don't want to allow arbitrary creation). Clients (like the
code of class Dungeon) always access the unique WindowRep object through the
static member function Instance().
WindowRep *WindowRep::Instance()
{
if(sWindowRep == NULL)
sWindowRep = new WindowRep;
Implementation: Windows classes 1043
return sWindowRep;
}
The first time that it is called, function Instance() creates the WindowRep object;
subsequent calls always return a pointer to the same object.
The constructor calls function Initialize() which performs any system
dependent device initialization. It then sets up the image array, and clears the
screen.
WindowRep::WindowRep()
{
Initialize();
for(int row = 0; row < CG_HEIGHT; row++)
for(int col = 0; col< CG_WIDTH; col++)
fImage[row][col] = ' ';
Clear();
}
void WindowRep::Initialize()
{
#if defined(SYMANTEC)
/*
Have to change the "mode" for the 'console' screen.
Putting it in C_CBREAK allows characters to be read one by
one as they are typed
*/
csetmode(C_CBREAK, stdin);
#else
/*
No special initializations are needed for Borland IDE
*/
#endif
}
#if defined(SYMANTEC)
cgotoxy(x,y,stdout);
#else
gotoxy(x,y);
#endif
}
#if defined(SYMANTEC)
fputc(ch, stdout);
fflush(stdout);
#elif
putch(ch);
#endif
}
if(ch != fImage[y-1][x-1]) {
MoveCursor(x,y);
PutCharacter(ch);
fImage[y-1][x-1] = ch;
}
}
The CloseDown() function clears the screen, performs any device specific
termination, then after a short delay lets the WindowRep object self destruct.
void WindowRep::CloseDown()
{
Clear();
#if defined(SYMANTEC)
csetmode(C_ECHO, stdin);
#endif
sWindowRep = NULL;
Delay(2);
delete this;
}
Window
The constructor for class Window initializes the simple data members like the width
and height fields. The foreground and background arrays are created. They are
vectors, each element of which represents an array of characters (one row of the
image).
fWidth = width;
fHeight = height;
fFramed = framed;
Naturally, the main task of the destructor is to get rid of the image arrays:
Window::~Window()
{
for(int row = 0; row < fHeight; row++) {
delete [] fCurrentImg[row];
delete [] fBkgd[row];
}
delete [] fCurrentImg;
delete [] fBkgd;
}
Functions like Clear(), and Set() rely on auxiliary routines Valid() and
Change() to organize the real work. Function Valid() makes certain that the
coordinates are within the window's bounds. Function Change() is given the
coordinates, and the new character. It looks after details like making certain that
the window frame is not overwritten (if this is a framed window), arranging for a
request to the WindowRep object asking for the character to be displayed, and the
updating of the array.
(Function Change() has to adjust the x, y values from the 1-based scheme used for
referring to screen positions to a 0-based scheme for C array subscripting.)
Function PrepareContent() loads the current image array from the background
and, if appropriate, calls SetFrame() to add a frame.
void Window::PrepareContent()
{
for(int row = 0; row < fHeight; row++)
for(int col = 0; col < fWidth; col++)
fCurrentImg[row][col] = fBkgd[row][col];
if(fFramed)
SetFrame();
}
void Window::SetFrame()
{
for(int x=1; x<fWidth-1; x++) {
fCurrentImg[0][x] = '-';
fCurrentImg[fHeight-1][x] = '-';
}
for(int y=1; y < fHeight-1; y++) {
fCurrentImg[y][0] = '|';
fCurrentImg[y][fWidth-1] = '|';
}
fCurrentImg[0][0] = '+';
fCurrentImg[0][fWidth-1] = '+';
fCurrentImg[fHeight-1][0] = '+';
fCurrentImg[fHeight-1][fWidth-1] = '+';
}
A Window object's frame uses its top and bottom rows and leftmost and rightmost
columns. The content area, e.g. the map in the dungeon game, cannot use these
Implementation: Windows classes 1047
perimeter points. (The input file for the map could define the perimeter as all
"wall".)
The access functions like X(), Y(), Height() etc are all trivial, e.g.:
The functions ShowAll() and ShowContent() are similar. They have loops
take characters from the current image and send the to the WindowRep object for
display. The only difference between the functions is in the loop limits; function
ShowContent() does not display the periphery of a framed window.
The only complications in class NumberItem involve making certain that the
numeric value output does not overlap with the label. The constructor checks the
length of the label given and essentially discards it if display of the label would use
too much of the width of the NumberItem.
Function ShowValue() starts by clearing the area used for number display. A
loop is then used to generate the sequence of characters needed, these fill in the
display area starting from the right. Finally, a sign is added. (If the number is too
large to fit into the available display area, a set of hash marks are displayed.)
void NumberItem::ShowValue()
{
int left = 2 + fLabelWidth;
int pos = fWidth - 1;
long val = fVal;
for(int i = left; i<= pos; i++)
fCurrentImg[1][i-1] = ' ';
if(val<0) val = -val;
if(val == 0)
fCurrentImg[1][pos-1] = '0';
while(val > 0) {
int d = val % 10;
val = val / 10;
char ch = d + '0';
fCurrentImg[1][pos-1] = ch;
pos--;
if(pos <= left) break;
}
if(pos<=left)
for(i=left; i<fWidth;i++)
fCurrentImg[1][i-1] = '#';
else
if(fVal<0)
fCurrentImg[1][pos-1] = '-';
ShowContent();
}
Class EditText adopts a similar approach to dealing with the label, it is not
shown if it would use too large a part of the window's width. The contents of the
buffer have to be cleared as part of the work of the constructor (it is sufficient just
to put a null character in the first element of the buffer array).
The ShowValue() function displays the contents of the buffer, or at least that
portion of the buffer that fits into the window width.
void EditText::ShowValue()
{
int left = 4 + fLabelWidth;
int i,j;
for(i=left; i<fWidth;i++)
fCurrentImg[1][i-1] = ' ';
for(i=left,j=0; i<fWidth; i++, j++) {
char ch = fBuf[j];
if(ch == '\0') break;
fCurrentImg[1][i-1] = ch;
}
ShowContent();
}
Function GetInput() positions the cursor at the start of the data entry field
then loops accepting input characters (obtained via the WindowRep object). The
loop terminates when the required number of characters has been obtained, or when
a character like a space or tab is entered.
char EditText::GetInput()
{
int left = 4 + fLabelWidth;
fEntry = 0;
ShowValue();
WindowRep::Instance()->MoveCursor(fX+left, fY+2);
char ch = WindowRep::Instance()->GetChar();
while(isalnum(ch)) {
fBuf[fEntry] = ch;
fEntry++;
if(fEntry == fSize) {
ch = '\0';
break;
}
ch = WindowRep::Instance()->GetChar();
}
fBuf[fEntry] = '\0';
return ch;
}
Inheritance and Polymorphism 1050
The function does not prevent entry of long strings from overwriting parts of the
screen outside of the supposed window area. You could have a more sophisticated
implementation that "shifted existing text leftwards" so that display showed only
the last few characters entered and text never went beyond the right margin.
The constructor and destructor for class Dungeon are limited. The constructor will
simply involve initializing pointer data members to NULL, while the destructor
should delete "owned" objects like the main display window.
The Load() function will open the file, then use the auxiliary LoadMap() and
PopulateDungeon() functions to read the data.
if(!in.good()) {
cout << "Sorry, problems reading that file. "
"Quitting." << endl;
exit(1);
}
Implementation: class Dungeon 1051
The DungeonItem objects can appear in any order in the input file, but each
starts with a character symbol followed by some integer data. The
PopulateDungeon() function can use the character symbol to control a switch()
statement in which objects of appropriate kinds are created and added to lists.
break;
default:
cout << "Unrecognizable data in input file."
<< endl;
cout << "Symbol " << ch << endl;
exit(1);
}
in >> ch;
}
if(fPlayer == NULL) {
cout << "No player! No Game!" << endl;
exit(1);
}
if(fProps.Length() == 0) {
cout << "No items to collect! No Game!" << endl;
exit(1);
}
cout << "Dungeon population read" << endl;
}
The function verifies the requirements for exactly one Player object and at least
one Collectable item.
The Run() function starts by creating the main map window and arranging for
all objects to be drawn. The main while() loop shows the Collectable items,
gets the Player move, then lets the Monsters have their turn.
int Dungeon::Run()
{
CreateWindow();
int n = fInhabitants.Length();
for(int i=1; i <= n; i++) {
Monster *m = (Monster*) fInhabitants.Nth(i);
m->Draw();
}
fPlayer->Draw();
fPlayer->ShowStatus();
WindowRep::Instance()->Delay(1);
while(fPlayer->Alive()) {
for(int j=1; j <= fProps.Length(); j++) {
Collectable *pi =
(Collectable*) fProps.Nth(j);
pi->Draw();
}
fPlayer->Run();
if(fProps.Length() == 0)
break;
int n = fInhabitants.Length();
for(i=1; i<= n; i++) {
Monster *m = (Monster*)
fInhabitants.Nth(i);
m->Run();
Implementation: class Dungeon 1053
}
}
return fPlayer->Alive();
}
(Note the need for type casts when getting members of the collections; the function
DynamicArray::Nth() returns a void* pointer.)
The CreateWindow() function creates a Window object and sets its background
from the map.
void Dungeon::CreateWindow()
{
fDWindow = new Window(1, 1, fWidth, fHeight);
for(int row = 1; row <= fHeight; row++)
for(int col = 1; col <= fWidth; col++)
fDWindow->SetBkgd(col, row,
fDRep[row-1][col-1]);
fDWindow->PrepareContent();
fDWindow->ShowAll();
}
Collectable *Dungeon::PI_at_Pt(Pt p)
{
int n = fProps.Length();
for(int i=1; i<= n; i++) {
Collectable *pi = (Collectable*) fProps.Nth(i);
Pt w = pi->Where();
if(w.Equals(p)) return pi;
Inheritance and Polymorphism 1054
}
return NULL;
}
if(p1.Y() == p2.Y())
return ClearRow(p1, p2, max, path);
else
if(p1.X() == p2.X())
return ClearColumn(p1, p2, max, path);
The explanation of the algorithm given in the previous section dealt with cases
involving rows or oblique lines that were more or less horizontal. The
implementations given here illustrate the cases where the line is vertical or close to
vertical.
int x = p1.X();
int y = p1.Y();
for(int i=0;i<abs(ychange);i++) {
if(error*deltax>0.5) {
x += deltax;
error -= deltax;
}
error += slope*deltay;
y += deltay;
Pt p(x, y);
if(!Accessible(p)) return 0;
path[i] = p;
if(p.Equals(p2)) return 1;
}
return 0;
}
29.3.3 DungeonItems
DungeonItem
Class DungeonItem implements a few basic behaviours shared by all variants. Its
constructor sets the symbol used to represent the item and sets the link to the
Dungeon object. The body of the destructor is empty as there are no separate
resources defined in the DungeonItem class.
DungeonItem::~DungeonItem() { }
The Erase() and Draw() functions operate on the Dungeon object's main map
Window. The call fd->Display() returns a Window* pointer. The Window
referenced by this pointer is asked to perform the required operation.
void DungeonItem::Erase()
{
Inheritance and Polymorphism 1056
fD->Display()->Clear(fPos.X(), fPos.Y());
}
void DungeonItem::Draw()
{
fD->Display()->Set( fPos.X(), fPos.Y(), fSym);
}
All DungeonItem objects must read their coordinates, and the data given as
input must be checked. These operations are defined in DungeonItem::Read().
Collectable
The constructor for class Collectable passes the Dungeon* pointer and char
arguments to the DungeonItem constructor:
Class Collectable's access functions (Wlth() etc) simply return the values of
the required data members. Its Read() function extends DungeonItem::Read().
Note the call to DungeonItem::Read() at the start; this gets the coordinate data.
Then the extra integer parameters can be input.
ActiveItem
The constructor for class ActiveItem again just initializes some data members to
zero after passing the given arguments to the DungeonItem constructor. Function
ActiveItem::Read() is similar to Collectable::Read() in that it invokes the
DungeonItem::Read() function then reads the extra data values (fHealth and
fStrength).
There are a couple of trivial functions (GetHit() { fHealth -= damage; };
and Alive() { return fHealth > 0; }). The Move() operation involves calls
to the (inherited) Erase() and Draw() functions. Function Step() works out the
x, y offset (+1, 0, or -1) coordinates of a chosen neighboring Pt.
Pt ActiveItem::Step(int dir)
{
Pt p;
switch(dir) {
case 1: p.SetPt(-1,1); break;
case 2: p.SetPt(0,1); break;
case 3: p.SetPt(1,1); break;
case 4: p.SetPt(-1,0); break;
case 6: p.SetPt(1,0); break;
case 7: p.SetPt(-1,-1); break;
case 8: p.SetPt(0,-1); break;
case 9: p.SetPt(1,-1); break;
}
return p;
}
Player
The constructor for class Player passes its arguments to its parents constructor and
then sets its data members to 0 (NULL for the pointer members). The Read()
function is similar to Collectable::Read(); it invokes the inherited Dungeon
Item::Read() and then gets the extra "manna" parameter.
The first call to ShowStatus() creates the NumberItem and EditText windows
and arranges for their display. Subsequent calls update the contents of the
NumberItem windows if there have been changes (the call to SetVal() results in
execution of the NumberItem object's ShowContents() function so resulting in
changes to the display).
void Player::ShowStatus()
{
if(fWinH == NULL) {
Inheritance and Polymorphism 1058
void Player::Run()
{
char ch = GetUserCommand();
if(isdigit(ch)) PerformMovementCommand(ch);
else PerformMagicCommand(ch);
UpdateState();
ShowStatus();
}
void Player::UpdateState()
{
fMoveCount++;
if(0 == (fMoveCount % 3)) fHealth++;
if(0 == (fMoveCount % 7)) fManna++;
}
if(m != NULL) {
Attack(m);
return;
}
TryMove(x + p.X(), y + p.Y());
}
The auxiliary functions, Take(), Attack() , and TryMove() are all simple.
Function Take() updates the Player objects health and related attributes with data
values from the Collectable item, and then arranges for the Dungeon to dispose of
that item. Function Attack() reduces the Monster object's health (via a call to its
GetHit() function) and, if appropriate, arranges for the Dungeon object to dispose
of the Monster. Function TryMove() validates and then performs the appropriate
movement.
The function GetUserCommand() arranges for the EditText window to input
some text and then inspects the first character of the text entered.
char Player::GetUserCommand()
{
fWinE->GetInput();
char *str = fWinE->GetVal();
return *str;
}
The function PerformMagicCommand() identifies the axis for the magic bolt.
There is then a loop in which damage is inflicted (at a reducing rate) on any
Monster objects found along a sequence of points in the given direction:
int power = 8;
fManna -= power;
if(fManna < 0) {
fHealth += 2*fManna;
fManna = 0;
}
while(power > 0) {
x += dx;
Inheritance and Polymorphism 1060
y += dy;
if(!fD->ValidPoint(Pt(x,y))) return;
Monster* m = fD->M_at_Pt(Pt(x,y));
if(m != NULL) {
m->GetHit(power);
if(!m->Alive())
fD->RemoveM(m);
}
power /= 2;
}
}
Monster
The constructor and destructor functions of class Monster both have empty bodies
for there is no work to be done; the constructor passes its arguments back to the
constructor of its parent class (ActiveItem):
int Monster::CanAttack()
{
Player *p = fD->Human();
Pt target = p->Where();
return fPos.Adjacent(target);
}
void Monster::Attack()
{
Player *p = fD->Human();
p->GetHit(fStrength);
}
void Monster::Advance() { }
Ghost
int Ghost::CanDetect()
{
Player *p = fD->Human();
Implementation: DungeonItems 1061
The Advance() function determines the change in x, y coords that will bring
the Ghost closer to the Player.
void Ghost::Advance()
{
Player *p = fD->Human();
Pt p1 = p->Where();
int dx, dy;
dx = dy = 0;
if(p1.X() > fPos.X()) dx = 1;
else
if(p1.X() < fPos.X()) dx = -1;
if(p1.Y() > fPos.Y()) dy = 1;
else
if(p1.Y() < fPos.Y()) dy = -1;
Wanderer
int Wanderer::CanDetect()
{
Player *p = fD->Human();
return
fD->ClearLineOfSight(fPos, p->Where(), 10, fPath);
}
void Wanderer::Advance()
{
Move(fPath[0]);
}
void Wanderer::NormalMove()
{
int x = fPos.X();
int y = fPos.Y();
Inheritance and Polymorphism 1062
Patrol
The patrol route has to be read, consequently the inherited Read() function must
be extended. There are several possible errors in route definitions, so Patrol::
Read() involves many checks:
if(!p.Adjacent(fRoute[i-1])) {
cout << "Non adjacent points in patrol"
"route" << endl;
cout << "(" << x << ", " << y << ")" << endl;
exit(1);
}
fRoute[i] = p;
}
if(!in.good()) {
cout << "Problems reading patrol route" << endl;
exit(1);
}
}
void Patrol::NormalMove()
{
if((fNdx == 0) && (fDelta == -1)) { Reverse direction at
fDelta = 1; start
return;
}
if((fNdx == fRouteLen) && (fDelta == 1)) { Reverse direction at
fDelta = -1; end
return;
}
fNdx += fDelta; Move one step along
Move(fRoute[fNdx]); route
}
void Patrol::Advance()
{
Player *p = fD->Human();
Pt target = p->Where();
Pt arrow = fPath[0];
int i = 1;
while(!arrow.Equals(target)) {
fD->Display()->Set( arrow.X(), arrow.Y(), ':');
WindowRep::Instance()->Delay(1);
fD->Display()->Clear( arrow.X(), arrow.Y());
arrow = fPath[i];
i++;
}
p->GetHit(2);
}
EXERCISES
Why should the monsters wait while the user thinks? If they know what they want to do,
they should be able to continue!
The current program requires user input in each cycle of the game. If there is no input,
the program stops and waits. The game is much more interesting if this wait is limited.
If the user doesn't type any command within a second or so, the monsters should get
their chance to run anyway.
First, the main while() loop in Dungeon::Run() should have a call WindowRep::
Instance()->Delay(1). This results in a 1 second pause in each cycle.
The Player::Run() function only gets called if there have been some keystrokes. If there
are no keystrokes waiting to be processed, the Dungeon::Run() function skips to the loop
that lets each monster have a chance to run.
All that is required is a system function, in the "console" library package, that allows a
program to check whether input data are available (without "blocking" like a normal read
function). The Borland conio library includes such a function.
Using the on-line help system in the Borland environment, and other printed
documentation, find how to check for input. Use this function in a reorganized version
of the dungeon program.
(You can achieve the same result in the Symantec system but only by utilising
specialized system calls to the "Toolbox" component of the Macintosh operating system.
It is all a little obscure and clumsy.)
(There are various ways that this might be done. The easiest is probably to define a new
class DungeonLevel . The Dungeon object owns the main window, the Player, and a list
of DungeonLevel objects. Each DungeonLevel object owns a map, a list of collectables,
and a list of monsters. You will need some way of allowing a user to go up or down
levels. When you change level, the new DungeonLevel resets the background map in
the main window and arranges for all data to be redrawn.)
Approaches to Reuse
Reusable algorithms
If you are using the "top down functional decomposition" strategy that was Reuse with functions
illustrated in Part III, you are limited to reusing standard algorithms; the code in the
function libraries implements these standard algorithms. Reusing algorithms is
better than starting from scratch. These days, nobody writes their own sin()
function, they use the version in the maths library. Computer science students are
often made to rewrite the standard sorting and searching functions, but
professionals use qsort() and bsearch() (standardized sort and binary search
functions that are available in almost every development environment). As noted in
Chapter 13, over the years huge libraries of functions have been built up,
particularly in science and engineering, to perform standard calculations.
1066 Reusable designs
Reusable functions are helpful in the case where all you need to do is calculate
something. But if you want to build an interactive program with windows and
menus etc, you soon discover problems.
Function libraries for There are function libraries that are intended to be used when building such
interactive programs programs. Multi-volume reference manuals exist to describe them. For example,
the Xlib reference manuals define the functions you can used to work with the X-
windows interface on Unix. The series "Inside Macintosh" describes how to create
windows and menus for the Mac. OS. These books include the declarations of
literally hundreds of functions, and dozens of data structures. But these function
libraries are very difficult to use.
Limitations of The functions defined in these large libraries are disjoint, scattered,
function libraries inconsistently named. There is no coherence. It is almost impossible to get a clear
picture of how to organize a program. Instead you are faced with an arbitrary
collection of functions, and the declarations of some types of structures that you
have to have as globals. Programs built using just these function libraries acquire
considerable entropy (chaotic structure). Each function call takes you off to some
other arbitrary piece of code that rapes and pillages the global data structures.
Reusable components
Reusable classes and The "object based" techniques presented in Part IV give you a better handle on
object based design reuse. Class libraries and object based programs allow you to reuse abstract data
types. The functions and the data that they operate on are now grouped. The data
members of instances of classes are protected; the compiler helps make sure that
data are only accessed via the appropriate functions.
Program design is different. You start by identifying the individual objects that
are responsible for particular parts of the overall data. You define their classes.
Often, you will find that you can reuse standard classes, like the collection classes
in Chapters 21 and 24. As well as providing working code, these classes give you a
way of structuring the overall program. The program becomes a sequence of
interactions between objects that are instances of standard and application specific
classes.
Essentially, the unit of reuse has become larger. Programs are built at least in
part from reusable components. These reusable components include collection
classes and, on Unix, various forms of "widget". (A widget is essentially a class
that defines a "user interface" component like a menu or an alert box.)
Can we reuse more? When inheritance was introduced in Chapter 23, it was shown that this was a way
of representing and exploiting similarities. Many application programs have
substantial similarities in their behaviour; such similarities lead to reusable designs.
Similar patterns of You launch a program. Once it starts, it presents you with some form of "file
interactions in dialog" that allows you to create a new file, or open an existing file. The file is
different programs
opened. One or more windows are created. If the file existed previously, some
Introduction 1067
portions of its current contents get displayed in these new windows. The system's
menu bar gets changed, or additional menus or tool bars appear associated with the
new window(s). You use the mouse pointer and buttons to select a menu option
and a new "tools palette" window appears alongside the document window. You
select a tool from the palette. You use the tool to add data to the document.
The behaviour is exactly the same. It doesn't matter whether it is a drawing
program or spreadsheet. The same patterns of behaviour are repeated.
Object oriented programming techniques provide a way of capturing common Reusable patterns of
patterns of behaviour. These patterns involve standardized interactions between interaction?
instances of different classes.
The "opening sequence for a program" as just described would involve interactions
between an "application" object, a "document" object, several different "window"
objects, maybe a "menu manager" object and several others.
An "opening sequence" pattern could specify that the "application" object
handle the initial File/New or File/Open request. It should handle such a request by
creating a document object and giving it the filename as an argument to an
"OpenNew()" or "OpenOld()" member function. In "OpenOld()", the document
object would have to create some objects to store the data from the file and arrange
to read the existing data. Once the "open" step is complete, the application object
would tell the new document object to create its display structure. This step would
result in the creation of various windows.
Much is standard. The standard interactions among the objects can be defined
in code:
Application::HandleCommand( command#, …)
switch(command#) Default
newCommand: implementation
doc = this->DoMakeDocument(); defined
doc->OpenNew();
doc->CreateDisplay();
break;
openCommand:
filename = this->PoseFileDialog();
doc = this->DoMakeDocument();
doc->OpenOld(filename, …);
doc->CreateDisplay();
break;
…
Document::OpenOld(filename, …) Default
this->DoMakeDataStructures() implementation
this->DoRead(filename, …) defined
Of course, each different program does things differently. The spreadsheet and
drawing programs have to create different kinds of data structure and then have to
read differently formatted files of data.
Utilize class This is where inheritance comes in.
inheritance The situation is very much like that in the dungeon game with class Monster
and its subclasses. The Dungeon code was written in terms of interactions between
the Dungeon object and instances of class Monster . But there were never any
Monster objects. Class Monster was an abstraction that defined a few standard
behaviours, some with default implementations and some with no implementation.
When the program ran, there were instances of specialized subclasses of class
Monster ; subclasses that owned their own unique data and provided effective
implementations of the behaviours declared in class Monster.
An abstract class Now, class Document is an abstraction. It defines something that can be asked
Document to open new or old files, create displays and so forth. All kinds of document
exhibit such behaviours; each different kind does things slightly differently.
Possible specialized Specialized subclasses of class Document can be defined. A SpreadSheetDoc
subclasses would be a document that owns an array of Cell objects where each Cell is
something that holds either a text label, or a number, or a formula. A DrawDoc
would be a document that owns a list of PictureElements. Each of these
specialized subclasses would provide effective definitions for the empty
Document::DoRead() and Document::DoMakeDataStructures() functions (and
for many other functions as well!).
Building complete A particular program won't create different kinds of document! Instead, you
programs build the "spreadsheet" program or the "draw" program.
For the "draw" program, you would start by creating class DrawApp a minor
specialization of class Application. The only thing that DrawApp does differently
is that its version of the DoMakeDocument() function creates a DrawDoc. A
DrawDoc is pretty much like an ordinary Document, but it has an extra List data
member (to store its PictureElements) and, as already noted, it provides effective
implementations of functions like DoRead().
Such a program gets built with much of its basic structure defined in terms of
classes that are specializations of standardized, reusable classes taken from a
library. These reusable classes are the things like Application, Document, and
Window. Some of their member functions are defined with the necessary code in
the implementation files. Other member functions may have empty (do nothing)
implementations. Still other member functions are pure virtual functions that must
be given definitions in subclasses.
Reusing a design
Reusing a design A program built in this fashion illustrates reuse on a new scale. It isn't just
individual components that are being reused. Reuse now extends to design.
Design ideas are embedded in the code of those functions that are defined in the
library. Thus, the "standard opening sequence" pattern implements a particular
design idea as to how programs should start up and allow their users to select the
data files that are to be manipulated. Another defined pattern of interactions might
Introduction 1069
specify how an Application object was to handle a "Quit" command (it should
first give any open document a chance to save changes, tell the document to close
its windows and get rid of data, delete the document, close any application
windows e.g. floating tool palettes, and finally quit).
The code given for the standard classes will embody a particular "look and feel" Default code
as might be required for all applications running on a particular type of machine. implements the
"standard look and
The specifications for a new application would normally require compliance with feel"
"standard look and feel". If you had to implement a program from scratch, you
would have to sort out things like the "standard opening sequence" and "standard
quit" behaviours and implement all the code. If you have a class library that
embodies the design, you simply inherit it and get on with the new application
specific coding.
It is increasingly common for commercial products to be built using Framework class
standardized framework class libraries. A "framework class library" has the classes libraries
that provide the basic structure, the framework, for all applications that comply
with a standardized design. The Integrated Development Environment that you
have on your personal computers includes such a class library. You will eventually
get to use that library.
The real framework class libraries are relatively complex. The rest of this chapter
illustrates a simplified framework that can serve as an introduction.
While real frameworks allow for many different kinds of data and document;
this "RecordFile" framework is much more restricted. Real frameworks allow for
multiple documents and windows; here you make do with just one of each. Real
frameworks allow you to change the focus of activity arbitrarily so one moment
you can be entering data, the next moment you can be printing some graphic
representation of the data. Here, the flow of control is much more predefined. All
these restrictions are needed to make the example feasible. (The restrictions on
flow of control are the greatest simplifying factor.)
The "RecordFile" framework embodies a simple design for any program that
involves updating "records" in a data file. The "records" could be things like the
customer records in the example in Chapter 17. It is primarily the records that vary
between different program built using this framework.
Figure 30.1 shows the form of the record used in a program, "StudentMarks", Example program
built using the framework. This program keeps track of students and their marks in and record
a particular subject. Students' have unique identifier numbers, e.g. the student
number 938654. The data maintained include the student's name and the marks for
assignments and exams. The name is displayed in an editable text field; the marks
are in editable number entry fields. When a mark is changed, the record updates
the student's total mark (which is displayed in a non-editable field.)
1070 Reusable designs
+--------------------------------------------------------------------+
|Record identifier 938654 Unique record identifier |
| |
| +----------------------------------------------------------+ |
| |Student Name Norman, Harvey Text in editable field | |
| +----------------------------------------------------------+ |
| |
| +----------------------------+ +----------------------------+|
| |Assignment 1 (5) 4 | |MidSession (15) 11 ||
| +----------------------------+ +----------------------------+|
| |Assignment 2 (10) 9 | |Examination (50) 0 ||
| +----------------------------+ +----------------------------+|
| |Assignment 3 (10) 4 | |
| +----------------------------+ +----------------------------+|
| |Assignment 4 (10) 0 | |Total 28 ||
| +----------------------------+ +----------------------------+|
| |
| |
| Number in editable field |
+--------------------------------------------------------------------+
Starting: "New", When the "StudentMarks" program is started, it first presents the user with a
"Open", "Quit" menu offering the choices of "New (file)", "Open (existing file)", or "Quit". If the
user selects "New" or "Open", a "file-dialog" is used to prompt for the name of the
file.
Changing the Once a file has been selected, the display changes, see Figure 30.2. It now
contents of a file displays details of the name of the file currently being processed, details of the
number of records in the file, and a menu offering various options for adding,
deleting, or modifying records.
Handling a "New If the user selects "New record", the program responds with a dialog that
record" command requires entry of a new unique record identifier. The program verifies that the
number entered by the user does not correspond to the identifier of any existing
record. If the identifier is unique, the program changes to a record display, like that
shown in Figure 30.1, with all editable fields filled with suitable default values.
Handling "Delete If the user picks "Delete record" or "View/edit record", the program's first
…" and "View/edit response is to present a dialog asking for the identifier number of an existing
…" commands
record. The number entered is checked; if it does not correspond to an existing
record, no further action is taken.
A "Delete record" command with a valid record identifier results in the deletion
of that record from the collection. A "View/edit" command leads to a record
display showing the current contents of the fields for the record.
Handling a "Close" After performing any necessary updates, a "Close" command closes the existing
command file. The program then again displays its original menu with the options "New",
"Open", and "Quit".
Another program, "Loans" is another program built on the same framework. It is very similar in
another record behaviour, but this program keeps track of movies that a customer has on loan from
display
a small video store. Its record is shown in Figure 30.3.
RecordFile Framework: Concepts 1071
+-----------------------------+--------------------------------------+
|CS204 Filename |Number of records: 138 |
| +--------------------------------------+
| ==> New record Record count |
| |
| |
| Delete record Menu, |
| (current choice highlighted, changed |
| by tabbing between choices, "enter" |
| View/edit record to select processing) |
| |
| |
| Close file |
| |
| |
| |
| |
| |
|(use 'option-space' to switch between choices, 'enter' to select) |
+--------------------------------------------------------------------+
+--------------------------------------------------------------------+
|Record identifier 16241 |
| |
| +----------------------------+ +-----------------------+ |
| |Customer Name Jones, David | |Phone 818672 | |
| +----------------------------+ +-----------------------+ |
| |
| Movie title Charge $ |
| +----------------------------+ +------+ |
| |Gone With The Wind | | 4 | |
| +----------------------------+ +------+ +---------------+ |
| |Casablanca | | 4 | | Total 12 | |
| +----------------------------+ +------+ +---------------+ |
| |Citizen Kane | | 4 | |
| +----------------------------+ +------+ +---------------+ |
| | | | 0 | | Year 290 | |
| +----------------------------+ +------+ +---------------+ |
| | | | 0 | |
| +----------------------------+ +------+ |
+--------------------------------------------------------------------+
The overall behaviours of the two programs are identical. It is just the records
that change. With the StudentMarks program, the user is entering marks for
different pieces of work. In the Loans program, the user enters the names of
movies and rental charges.
1072 Reusable designs
The classes used in programs like "StudentMarks" and "Loans" are illustrated in the
class hierarchy diagram shown in Figure 30.4.
Class browser Most IDEs can produce hierarchy diagrams, like that in Figure 30.4, from the
code of a program. Such a diagram is generated by a "class browser". A specific
class can be selected, using the mouse, and then menu commands (or other
controls) can be used to open the file with the class declaration or that with the
definition of a member function chosen from a displayed list. A class declaration
or function definition can be edited once it has been displayed. As you get more
deeply into the use of classes, you may find the "browser" provides a more
convenient editing environment than the normal editor provided by the IDE.
MyApp
MyRec
MyDoc
InputFileDialog
ArrayDoc OR BTDoc
MyDoc inherits from
NumberDialog
RecordWindow
MenuWindow
TextDialog
EditText
ArrayDoc
EditNum
BTDoc
ADCollection
BTCollection
Application
NumberItem
EditWindow
Document
Record
KeyedStorableItem
CommandHandler
DynamicArray
Collection
WindowRep
BTreeNode
Window
BTree
As illustrated in Figure 30.4, a program built using the framework may need to
define as few as three classes. These are shown in Figure 30.4 as the classes MyApp,
MyDoc, and MyRec; they are specializations of the framework classes Application,
Document, and Record.
Programs work with sets of records: e.g. all the students enrolled in a course, or all
the customers of the video store.
A program that has to work with a small number of records might chose to hold
them all in main memory. It would use a simple collection class like a dynamic
array, list, or (somewhat better) something like a binary tree or AVL tree. It would
work by loading all its records from file into memory when a file was opened,
1074 Reusable designs
letting the user change these records and add new records, and finally write all
records back to the file.
A program that needed a much larger collection of records would use something
like a BTree to store them.
Collection classes The actual "collection classes" are just those introduced in earlier chapters. The
examples in this chapter use class DynamicArray and class BTree, but any of the
other standard collection classes might be used. An instance of the chosen
collection class will hold the different Record objects. Figure 30.4 includes class
DynamicArray, class BTree and its auxiliary class BTreeNode.
The different collection classes have slightly different interfaces and behaviours.
For example, class BTree looks after its own files. A simpler collection based on
an in-memory list or dynamic array will need some additional component to look
after disk transfers. But we don't want such differences pervading the main code of
the framework.
Adapter classes for Consequently, the framework uses some "adapter" classes. Most of the
different collections framework code can work in terms of a "generic" Collection that responds to
requests like "append record", "delete record". Specialized "adapter" classes can
convert such requests into the exact forms required by the specific type of
collection class that is used.
ADCollection and Figure 30.4 shows two adapter classes: ADCollection and BTCollection. An
BTCollection ADCollection objection contains a dynamic array; a BTCollection owns a BTree
object (i.e. it has a BTree* data member, the BTree object is created and deleted by
code in BTCollection). These classes provide implementations of the pure virtual
functions Collection::Append() etc. These implementations call the approp-
riate functions of the actual collection class object that is used to store the data
records. The adapter classes also add any extra functions that may be needed in
association with a specific type of collection.
Although classes Application and Document have quite different specific roles
there are some similarities in their behaviour. In fact, there are sufficient
similarities to make it worth introducing a base class, class CommandHandler, that
embodies these common behaviours.
A CommandHandler is something that has the following two primary behaviours.
Firstly it "runs". "Running" means that it builds a menu, loops handling commands
entered via the menu, and finally tidies up.
void CommandHandler::CommandLoop()
{
while(!fFinished) {
…
int c = pose menu dialog …
this->HandleCommand(c);
}
}
A CommandHandler object will continue in its command handling loop until a flag,
fFinished, gets set. The fFinished flag of an Application object will get set by
a "Quit" command. A Document object finishes in response to a "Close" command.
As explained in the previous section, the Application object will have a menu Application
with the choices "New", "Open" and "Quit". Its HandleCommand() function is:
The "New" and "Open" commands result in the creation of some kind of Document
object (obviously, this will be an instance of a specific concrete subclass of class
Document). Once this Document object has been created, it will be told to open a
new or an existing file, and then it will be told to "run".
The Document object will continue to "run" until it gets a "Close" command. It
will then tidy up. Finally, the Document::Run() function, invoked via fDoc
->Run(), will return. The Application object can then delete the Document
object, and resume its "run" behaviour by again displaying its menu.
How do different applications vary? class MyApp
The application objects in the "StudentMarks" program and "Loans" program
differ only in the kind of Document that they create. A "MyApp" specialized
1076 Reusable designs
Document *MyApp::DoMakeDocument()
{
return new MyDoc;
}
and those, like the BTree based collection, where individual records are fetched as
needed.
There has to be a kind of parallel hierarchy between specialized collection ArrayDoc and BTDoc
classes and specialized Document classes. This is shown in Figure 30.4 with the
classes ArrayDoc and BTDoc . An ArrayDoc object creates an instance of an
ADCollection as its Collection object while a BTDoc creates a BTCollection.
Apart from DoMakeCollection() (the function that makes the Collection
object), these different specialized subclasses of Document differ in their
implementations of the functions that deal with opening and closing of files.
Different programs built using the framework must provide their own MyDoc
specialized Document classes – class StudentMarkDoc for the StudentMarks
program or LoanDoc for the Loans program. Figure 30.4 uses class MyDoc to
represent the specialized Document subclass needed in a specific program.
Class MyDoc won't be an immediate subclass of class Document, instead it will
be based on a specific storage implementation like ArrayDoc or BTDoc.
Window hierarchy
As is commonly the case with frameworks, most of the classes are involved with
user interaction, both data display and data input. In Figure 30.4, these classes are
represented by the "Window" hierarchy. (Figure 30.4 also shows class WindowRep.
This serves much the same role as the WindowRep class used Chapter 29; it
encapsulates the low level details of how to communicate with a cursor addressable
screen.)
The basic Window class is an extended version of that used in Chapter 29. It class Window
possesses the same behaviours of knowing its size and location on the screen,
maintaining "foreground" and "background" images, setting characters in these
images etc. In addition, these Window objects can own "subwindows" and can
arrange that these subwindows get displayed. They can also deal with display of
text strings and numbers at specific locations.
Class NumberItem is a minor reworking of the version from Chapter 29. An NumberItem
instance of class NumberItem can be used to display the current value of a variable
and can be updated as the variable is changed.
The simple EditText class of Chapter 29 has been replaced by an entire EditWindow
hierarchy. The new base class is EditWindow. EditWindow objects are things that
can be told to "handle input". "Handling input" involves accepting and processing
input characters until a ' \n' character is entered.
Class EditNum and EditText are simple specializations that can be used for EditNum and
verified numeric or text string input. An EditNum object accepts numeric EditText
characters, using them to determine the (integer) value input. An EditNum object
can be told the range permitted for input data; normally it will verify that the input
value is in this range (substituting the original value if an out of range value is
input). An EditText object accepts printable characters and builds up a string
(with a fixed maximum length).
A MenuWindow allows a user to pick from a displayed list of menu items. A MenuWindow
MenuWindow is built up by adding "menu items" (these have a string and a numeric
1078 Reusable designs
identifier). When a MenuWindow is displayed, it shows its menu items along with
an indicator of "the currently selected item" (starting at the first item in the menu).
A MenuWindow handles "tab" characters (other characters are ignored). A "tab"
changes the currently selected item. The selection moves cyclically through the list
of items. When "enter" ('\n ') is input, the MenuWindow returns the numeric
identifier associated with the currently selected menu item.
Classes The "dialogs" display small windows centered in the screen that contain a
NumberDialog and prompt and an editable field (an instance of class EditNum or class EditText). The
TextDialog
user must enter an acceptable value before the dialog will disappear and the
program continue. Class InputFileDialog is a minor specialization of
TextDialog that can check whether a string given as input corresponds to the name
of an existing file.
RecordWindow Class RecordWindow is a slightly more elaborate version of class MenuWindow.
A RecordWindow owns a list of EditNum and EditText subwindows. "Tabbing" in
a RecordWindow selects successive subwindows for further input.
The "MyRec" class used in a particular program implements a function,
AddFieldsToWindow() , that populates a RecordWindow with the necessary
EditNum and EditText subwindows.
class CommandHandler {
public:
CommandHandler(int mainmenuid);
virtual ~CommandHandler();
MenuWindow *fMenu;
int fFinished;
int fMenuID;
};
tidying up. The command loop will involve an update of status, acceptance of a
command number from the MenuWindow and execution of HandleCommand(). One
of the commands must set the fFinished flag.
Some of the functions are declared with "empty" implementations, e.g. Functions with
PrepareToRun() . Such functions are fairly common in frameworks. They empty bodies
represent points where the framework designer has made provision for "unusual"
behaviours that might be necessary in specific programs.
Usually there is nothing that must be done before an Application displays its
main menu, or after its command handling loop is complete. But it is possible that
a particular program would need special action (e.g. display of a "splash screen"
that identifies the program). So, functions PrepareToRun() and Finish() are
declared and are called from within defined code, like that of function Run().
These functions are deliberately given empty definitions (i.e. { }) ; there is no
need to force every program to define actual implementations.
In contrast functions like MakeMenu() and HandleCommand() are pure virtual. Pure virtual
These have to be given definitions before you have a working program. functions
Application
Document
Document's data As shown in Figure 30.5, in addition to the data members that it has because it is
members a CommandHandler (e.g. the MenuWindow) a Document has, as owned data members,
the file "base name" (a character array), an integer flag whose setting determines
whether file names should be verified, and a Collection object separately created
in the heap and accessed via the fStore pointer. (Of course, this will point to an
instance of some specialized subclass of class Collection.) A Document also has
a link a NumberItem; this (display window) object gets created by the Document but
ownership is transferred to the MenuWindow.
In some cases, the contents of fFileName will be the actual file name. But in
other cases, e.g. with the BTree storage structure, there will be separate index and
data files and fFileName is used simply as a base name. If a single file is used for
storage, then file names can be checked when opening an old file.
All the member functions, and the destructor, are virtual so as to allow
redefinition as needed by subclasses. Quite a few functions are still pure virtual;
Command Handler classes: declarations 1081
examples include the functions for creating Record objects and some of those
involved in file handling. The data members, and auxiliary member functions, are
all protected (rather than private). Again, this is done so as to maximize the
potential for adaptation in subclasses.
The public interface defines a few additional functions; most of these involve
file handling and related initialization and are used in the code of Application::
HandleCommand().
The other additional public function is one of two used to create records.
Function MakeEmptyRecord() is used (by a Collection object) to create an
empty record that can then be told to read data from a file:
Default definitions of Class Document can provide default definitions for the functions that handle
command handling "new record", "view record" and "delete record" commands. Naturally, these
functions
commands are handled using auxiliary member functions called from Handle
Command():
Function DoEditRecord() can ask the Record to create its own RecordWindow
display and then arrange for this RecordWindow to handle subsequent input (until
an "enter", '\n', character is used to terminate that interaction).
Pure virtual member The remaining member functions are all pure virtual. The function that defines
functions the type of Collection to use is defined by a subclass defined in the framework,
e.g. ArrayDoc or BTDoc. These classes also provide effective definitions for the
remaining file handling functions.
Data members The class declaration ends with the specification of the data members and links
to collaborators:
char fFileName[64];
NumberItem *fNumDisplay;
Collection *fStore;
int fVerifyInput;
};
Command Handler classes: declarations 1083
Classes BTDoc and ArrayDoc are generally similar. They have to provide
implementations of the various file handling and collection creation functions
declared in class Document. Class BTDoc has the following declaration:
The constructor for class BTDoc simply invokes the inherited Document constructor
and then changes the default setting of the "file name verification" flag (thus
switching off verification). Since a BTree uses multiple files, the input file dialog
can't easily check the name.
The declaration of class ArrayDoc is similar. Since it doesn't require any special
action in its constructor, the interface consists of just the three protected functions.
30.3.2 Interactions
Figure 30.6 illustrates some of the interactions involved when an application object
deals with an "Open" command from inside its HandleCommand() function. The
illustration is for the case where the concrete "MyDoc" document class is derived
from class BTDoc.
All the interactions shown in Figure 30.6 are already implemented in the Inherited pattern of
framework code. A program built using the framework simply inherits the interactions
behaviour patterns shown
The only program specific aspect is the implementation of the highlighted call to A single program
DoMakeDocument(). This call to DoMakeDocument() is the first step in the specific function call
process; it results in the creation of the specialized MyDoc object.
Once created, the new document object is told to perform its Create the collection
DoInitialState() routine in which it creates a Collection object. Since the
supposed MyDoc class is derived from class BTDoc , this step results in a new
BTCollection.
The next step, opening the file, is relatively simple in the case of a BTDoc. First Open the file
the document uses a dialog to get the file name; an InputFileDialog object is
created temporarily for this purpose. Once the document has the file name, it
proceeds by telling its BTreeCollection to "open the BTree". This step leads to
the creation of the actual BTree object whose constructor opens both index and data
files.
1084 Reusable designs
DoMakeDocument()
+ constructor
DoMakeCollection()
DoInitialState() +
InputFile
Dialog
+
OpenOld()
PoseModally() BTee
object
OpenOldFile()
+
OpenBTree()
Run()
CommandLoop()
Other interactions
involved in record
loop handling
DoCloseDoc()
delete
CloseBTree()
delete delete
The next step would get the document to execute its Run() function. This
would involve processing any record handling commands (none shown in Figure
30.6) and eventually a "Close" command.
Closing A "Close" command gets forwarded to the collection. This responds by deleting
the BTree object (whose destructor arranges to save all "housekeeping data" and
then close its two files).
Command Handler classes: interactions 1085
A document based on an "in memory" storage structure would have a slightly more
elaborate pattern of interactions because it has to load the existing records when the
file is opened. The overall pattern is similar. However, as shown in Figure 30.7,
the OpenOldFile() function results in additional interactions.
In this case, the collection object (an ADCollection) will be told to read all its
data from the file. This will involve control switching back and forth among
several objects as shown in Figure 30.7. The collection object would read the
number of records and then have a loop in which records get created, read their
own data, and are then added to the DynamicArray. The collection object has to
ask the document object to actually create the new Record (this step, and the
MyRec::ReadFrom() function, are the only program specific parts of the pattern).
ReadFrom()
MakeEmptyRecord() +
constructor
loop
ReadFrom()
Append()
Figure 30.8 illustrates another pattern of interactions among class instances that is
almost entirely defined within the framework code. It shows the overall steps
involved in creating a new record (Document::DoNewRecord() function).
The interactions start with the document object using a NumberDialog object to
get a (new) record number from the user. The check to determine that the number
is new involves a request to the collection object to perform a Find() operation
(this Find() should fail).
1086 Reusable designs
"MyDoc" "Collection"
object object
Number
Dialog
GetKey() +
PoseModally()
"MyRec"
Find() object
LoadRecord()
+
DoMakeRecord()
DoEditRecord() "RecordWindow"
object
+
DoMakeRecordWindow()
ShowText()
ShowNumber()
AddSubwindow()
PoseModally()
loop
delete
Append()
Save()
If the newly entered record identifier number is unique, a new record is created
with the given record number. This involves the program specific implementation
of the function MyDoc::DoMakeRecord().
The next step involves the new MyRec object being asked to create an
appropriate RecordWindow. The object created will be a standard RecordWindow;
but the code for MyRec::DoMakeRecordWindow() will involve program specific
actions adding a specific set of text labels and editable fields.
Once created, the RecordWindow will be "posed modally". It has a loop dealing
with subsequent input. In this loop it will interact with the MyRec object (notifying
it of any changes to editable fields) as well as with the editable subwindows that it
contains.
When control is returned from RecordWindow::PoseModally(), the MyRec
object will already have been brought up to date with respect to any changes. The
Command Handler classes: interactions 1087
RecordWindow can be deleted. The new record can then be added to the
Collection (the Append() operation).
Although the new record was created by the document object, it now belongs to
the collection. The call to Save() represents an explicit transfer of ownership.
Many collections will have empty implementations for Save() (because they don't
have to do anything special with respect to ownership). A collection that uses a
BTree will delete the memory resident record because it will already have made a
permanent copy on disk (during its Append() operation).
CommandHandler
The constructor for class CommandHandler sets is state as "unfinished" and creates
a MenuWindow. (All Window objects have integer identifiers. In the current code,
these identifiers are only used by RecordWindow and Record objects.)
CommandHandler::CommandHandler(int mainmenuid)
{
fMenuID = mainmenuid;
fMenu = new MenuWindow(fMenuID);
fFinished = 0;
}
CommandHandler::~CommandHandler()
{
delete fMenu;
}
The Run() function completes the construction of the MenuWindow and other
initial preparations and then invokes the CommandLoop() function. When this
returns, any special tidying up operations are performed in Finish().
void CommandHandler::Run()
{
this->MakeMenu();
this->PrepareToRun();
this->CommandLoop();
this->Finish();
}
It may appear that there is something slightly odd about the code. The
MenuWindow is created in the CommandHandler's constructor, but the (virtual)
function MakeMenu() (which adds menu items to the window) is not called until
the start of Run(). It might seem more natural to have the call to MakeMenu() as
part of the CommandHandler's constructor.
1088 Reusable designs
However, that arrangement does not work. Constructors do not use dynamic
calls to virtual functions. If the call to MakeMenu() was part of the Command
H a n d l e r 's constructor, it would be the (non existent) CommandHandler::
MakeMenu() function that was invoked and not the desired Application::
MakeMenu() or Document::MakeMenu() function.
Beware: no "virtual Constructors that do invoke virtual functions dynamically (so called "virtual
constructors" constructors") are a frequently requested extension to C++ but, for technical
reasons, they can not be implemented.
Class CommandHandler provides the default implementation for one other
function, the main CommandLoop():
void CommandHandler::CommandLoop()
{
while(!fFinished) {
this->UpdateState();
int c = fMenu->PoseModally();
this->HandleCommand(c);
}
}
Application
Application::Application() : CommandHandler(kAPPMENU_ID)
{
}
Application::~Application()
{
}
Its MakeMenu() function adds the three standard menu items to the
MenuWindow. (Constants like kAPPEMENU_ID, cNEW and related constants used by
class Document are all defined in a common header file. Common naming
conventions have constants that represent "commands" take names starting with 'c'
while those that define other parameters have names that begin with 'k'.)
void Application::MakeMenu()
{
fMenu->AddMenuItem("New", cNEW);
fMenu->AddMenuItem("Open",cOPEN);
fMenu->AddMenuItem("Quit",cQUIT);
}
Document
The constructor for class Document has a few extra data members to initialize. Its
destructor gets rid of the fStore (Collection object) if one exists.
Document::Document() : CommandHandler(kDOCMENU_ID)
{
fStore = NULL;
fFileName[0] = '\0';
fVerifyInput = 1;
}
The MakeMenu() function adds the standard menu options to the document's
MenuWindow() function. Member DoInitialState() simply uses the (pure
virtual) DoMakeCollection() function to make an appropriate Collection
object. The function actually called would be BTDoc::DoMakeCollection() or
ArrayDoc::DoMakeCollection (or other similar function) depending on the
particular type of document that is being built.
void Document::DoInitialState()
{
fStore = DoMakeCollection();
}
Functions PrepareToRun() and UpdateState() deal with the initial display Display of document
and subsequent update of the display fields with document details. (The details
NumberItem used for output is given to the MenuWindow as a "subwindow". As
explained in Section 30.6, subwindows "belong" to windows and, when
appropriate, get deleted by the window. So although the Document creates the
NumberItem and keeps a link to it, it never deletes it.)
void Document::PrepareToRun()
{
fNumDisplay = new NumberItem(0, 31, 1, 40,
"Number of records:",0);
fMenu->AddSubWindow(fNumDisplay);
}
void Document::UpdateState()
{
fNumDisplay->SetVal(fStore->Size());
fMenu->ShowText(fFileName, 2, 2, 30, 0, 1);
}
1090 Reusable designs
Getting keys for new The functions GetKey() and GetExistingRecordNum() both use dialogs for
and existing records input. Function GetKey(), via the DoLoadRecord() function, lets the document
interact with the Collection object to verify that the key is not already in use:
long Document::GetKey()
{
NumberDialog n("Record identifier", 1, LONG_MAX);
long k = n.PoseModally(1);
if(NULL == DoLoadRecord(k))
return k;
Alert("Key already used");
return -1;
}
long Document::GetExistingRecordNum()
{
if(fStore->Size() == 0) {
Alert("No records defined.");
return -1;
}
NumberDialog n("Record number", 1, LONG_MAX);
return n.PoseModally(1);
}
Provision for further Function DoLoadRecord() might seem redundant, after all the request to
extension fStore to do a Find() operation could have been coded at the point of call.
Again, this is just a point where provision for modification has been built into the
framework. For example, someone working with a disk based collection of records
might want to implement a version that maintained a small "cache" or records in
memory. He or she would find it convenient to have a redefinable
DoLoadRecord() function because this would represent a point where the
"caching" code could be added.
Function Alert() is defined along with the windows code. It puts up a dialog
displaying an error string and waits for the user to press the "enter" key.
Running and Class Document uses the inherited CommandHandler::Run() function. Its own
handling commands HandleCommand() function was given earlier (Section 30.2). Document::
HandleCommand() is implemented in terms of the auxiliary member functions
DoNewRecord(), DoDeleteRecord(), DoViewEditRecord(), and DoCloseDoc().
Function DoCloseDoc() is pure virtual but the others have default definitions.
Functions DoNewRecord() and DoEditRecord() implement most of the
interactions shown in Figure 30.8; getting the key, making the record, arranging for
it to be edited through a temporary RecordWindow object, adding and transferring
ownership of the record to the Collection object:
void Document::DoNewRecord()
{
long key = GetKey();
Command Handler classes: implementation 1091
if(key<=0)
return;
Record *r = DoMakeRecord(key);
DoEditRecord(r);
fStore->Append(r);
fStore->Save(r);
}
void Document::DoDeleteRecord()
{
long recnum = GetExistingRecordNum();
if(recnum<0)
return;
if(!fStore->Delete(recnum))
Alert(NoRecMsg);
}
void Document::DoViewEditRecord()
{
long recnum = GetExistingRecordNum();
if(recnum<0)
return;
Record *r = DoLoadRecord(recnum);
if(r == NULL) {
Alert(NoRecMsg);
return;
}
DoEditRecord(r);
fStore->Save(r);
}
Functions OpenNew() and OpenOld() handle standard aspects of file opening Standard file
(such as display of dialogs that allow input of a file name). Aspects that depend on handling
the type of collection structure used are handled through the auxiliary (pure virtual)
functions InitializeNewFile() and OpenOldFile():
void Document::OpenNew()
{
TextDialog onew("Name for new file");
1092 Reusable designs
onew.PoseModally("example",fFileName);
InitializeNewFile();
}
void Document::OpenOld()
{
InputFileDialog oold;
oold.PoseModally("example",fFileName, fVerifyInput);
OpenOldFile();
}
ArrayDoc
The OpenOldFile() function has to actually open the file for input, then it
must get the ADCollection to load all the data. Note the typecast on fStore. The
type of fStore is Collection* . We know that it actually points to an
ADCollection object. So we can use the typecast to get an ADCollection*
pointer. Then it is possible to invoke the ADCollection::ReadFrom() function:
Function DoCloseDoc() is very similar. It opens the file for output and then
arranges for the ADCollection object to save the data (after this, the collection has
to be told to delete all its contents):
void ArrayDoc::DoCloseDoc()
{
fstream out;
out.open(fFileName, ios::out);
if(!out.good()) {
Alert("Can not open output");
return;
}
Command Handler classes: implementation 1093
((ADCollection*)fStore)->WriteTo(out);
out.close();
((ADCollection*)fStore)->DeleteContents();
BTDoc
As noted earlier, a BTDoc needs to change the default setting of the "verify file
names" flag (normally set to true in the Document constructor):
BTDoc::BTDoc() { fVerifyInput = 0; }
Collection *BTDoc::DoMakeCollection()
{
return new BTCollection(this);
}
The other member functions defined for class BTDoc are simply an interface to
functions provided by the BTCollection object:
void BTDoc::OpenOldFile()
{
((BTCollection*)fStore)->OpenBTree(fFileName);
}
void BTDoc::DoCloseDoc()
{
((BTCollection*)fStore)->CloseBTree();
}
The underlying collection classes are identical to the versions presented in earlier
chapters. Class DynamicArray, as used in ADCollection, is as defined in Chapter
21. (The Window classes also use instances of class DynamicArray .) Class
BTCollection uses an instance of class BTree as defined in Chapter 24.
Class Collection itself is purely an interface class with the following
declaration:
1094 Reusable designs
protected:
DynamicArray fD;
};
and
private:
BTree *fBTree;
};
Each class defines implementations for the basic Collection functions like
Append(), Find(), Size(). In addition, each class defines a few functions that
relate to the specific form of storage structure used.
In the case of Append() and Size(), these collection class adapters can simply
pass the request on to the actual collection used:
long ADCollection::Size()
{
return fD.Length();
}
long BTCollection::Size()
{
return fBTree->NumItems();
}
A DynamicArray doesn't itself support record identifiers, so the Find() Adapting a dynamic
operation on an ADCollection must involve an inefficient linear search: array to fulfil the role
of Collection
Record *ADCollection::Find(long recnum)
{
int n = fD.Length();
for(int i = 1; i<=n; i++) {
Record *r = (Record*) fD.Nth(i);
long k = r->Key();
if(k == recnum)
return r;
}
return NULL;
}
delete r;
return NULL;
}
(The record is created via a request back to the document object: fDoc->MakeEmpty
Record();.)
The function BTree::Remove() does not return any success or failure
indicator; "deletion" of a non-existent key fails silently. A Collection is supposed
to report success or failure. Consequently, the BTCollection has to do a Find()
operation on the BTree prior to a Remove(). If this initial BTree::Find() fails,
the BTCollection can report a failure in its delete operation.
The other member functions for these classes mostly relate to file handling.
Function ADCollection::ReadFrom() implements the scheme shown in Figure
30.7 for loading the entire contents of a collection into memory:
Record *r = fDoc->MakeEmptyRecord();
r->ReadFrom(fs);
fD.Append(r);
}
}
The WriteTo() function handles the output case, getting called when a
document is closed. It writes the number of records, then loops getting each record
to write its own data:
A DynamicArray does not delete its contents. (It can't really. It only has void*
pointers). When a document is finished with, all in memory structures should be
freed. The ADCollection has to arrange this by explicitly removing the records
from the DynamicArray and deleting them.
void ADCollection::DeleteContents()
{
int len = fD.Length();
for(int i = len; i>= 1; i--) {
Record* r = (Record*) fD.Remove(i);
delete r;
}
}
void BTCollection::CloseBTree()
{
delete fBTree;
}
class KeyedStorableItem {
public:
virtual ~KeyedStorableItem() { }
virtual long Key(void) const = 0;
virtual void PrintOn(ostream& out) const { }
virtual long DiskSize(void) const = 0;
virtual void ReadFrom(fstream& in) = 0;
virtual void WriteTo(fstream& out) const = 0;
};
Class Record provides a default implementation for the Key() function and
adds the responsibilities related to working with a RecordWindow that allows
editing of the contents of data members (as defined in concrete subclasses).
Data members Class Record defines two data members itself. One is a long integer to hold the
defined by class unique identifier (or "key"), the other is a link to the RecordWindow collaborator.
Record
Function DoMakeRecordWindow() uses an auxiliary function CreateWindow()
to actually create the window. Once created, the window is "populated" by adding
subwindows (in AddFieldsToWindow()).
RecordFile Framework: Record class 1099
RecordWindow *Record::DoMakeRecordWindow()
{
CreateWindow();
AddFieldsToWindow();
return fRW;
}
void Record::CreateWindow()
{
fRW = new RecordWindow(this);
}
// typical code
// EditText *et = new EditText(1001, 5, 4, 60,
// "Student Name ");
//fRW->AddSubWindow(et);
//EditNum *en = new EditNum(1002, 5, 8, 30,
// "Assignment 1 (5) ", 0, 5,1);
//fRW->AddSubWindow(en);
}
//case 1001:
// ((EditText*)e)->SetVal(fStudentName, 1);
// break;
//case 1002:
// ((EditNum*)e)->SetVal(fMark1,1);
// break;
// …
}
The role of the unique WindowRep object is unchanged from that in the version
given in Chapter 29. It "owns" the screen and deals with the low level details
involved in input and output of characters.
The version used for the "RecordFile" framework has two small extensions.
There is an extra public function, Beep(); this function (used by some dialogs)
produces an audible tone to indicate an error. (The simplest implementation
involves outputting '\a' "bell" characters.)
The other extension arranges for any characters input via GetChar() to be
copied into the fImage array. This improves the consistency of screen updates that
occur after data input.
The implementation of this extension requires additional fXC, fYC integer data
members in class WindowRep. These hold the current position of the cursor. They
Window class hierarchy: class responsibilities 1101
Window
Class Window is an extended version of that given in Chapter 29. The major
extension is that a Window can have a list (dynamic array) of "subwindows" and so
has some associated functionality. In addition, Window objects have integer
identifiers and there are a couple of extra member functions for things like
positioning the cursor and outputting a string starting at a given point in the
Window.
The additional (protected) data members are:
Function Id() returns the Window object's identifier. The constructor for class
Window is changed so that this integer identifier number is an extra (first)
parameter. (The destructor is extended to get rid of subwindows.)
Provision for The first of the extra functions needed to support subwindows is
subwindows CanHandleInput(). Essentially this returns "true" if a Window object is actually an
instance of some class derived from class EditWindow where a GetInput()
function is defined. In some situations, it is necessary to know which
(sub)windows are editable so this function has been added to the base class for the
entire windows hierarchy. By default, the function returns "false".
The three main additions for subwindows are the public functions
AddSubWindow() and DisplayWindow() and the protected function Offset().
NumberItem
The role of class NumberItem is unchanged from that of the version presented in
the last chapter; it just displays a numeric value along with a (possibly null) label
string.
The constructor now has an additional "int id " argument at the front of the
argument list that is used to set a window identifier. Function SetVal() also has
an extra "int redraw " parameter; if this is false (which is the default) a change to
the value does not lead to immediate updating of the screen.
The implementation of NumberItem was changed to use the new functionality
like ShowText() and ShowNumber() added to the base Window class. The
implementation code is not given, being left as an exercise.
Window class hierarchy: class responsibilities 1103
EditWindow
The significant changes to the previously illustrated Window classes start with class
EditWindow.
Class EditWindow is intended to be simply an abstraction. It represents a
Window object that can be asked to "get input".
"Getting input" means different things in different contexts. When an EditNum Getting input
window is "getting input" it consumes digits to build up a number. A MenuWindow
"gets input" by using "tab" characters to change the selected option. However,
although they differ in detail, the various approaches to "getting input" share a
similar overall pattern.
There may have to be some initialization. After this is completed, the actual Overall input process
input step can be performed. Sometimes, it is necessary to validate an input. If an
input value is unacceptable the user should be notified and then the complete
process should be repeated.
The actual input step itself will involve a loop in which characters are accepted Consuming
(via the WindowRep object) and are then processed. Different kinds of EditWindow characters
process different kinds of character; thus a MenuWindow can basically ignore
everything except tabs, an EditNum only wants to handle digits, while an EditText
can handle more or less any (printable) character. The character input step should
continue until some terminator character is entered. (The terminator character may
be subclass specific.) The terminator character is itself sometimes significant; it
should get returned as the result of the GetInput() function.
This overall pattern can be defined in terms of a GetInput() function that
works with auxiliary member functions that can be redefined in subclasses.
Pseudo-code for this GetInput() function is as follows:
InitializeInput();
do {
SetCursorPosition();
Get character (ch)
while(ch != '\n') { Inner loop getting
if(!HandleCharacter(ch)) characters until
break; terminator
Get character (ch)
}
TerminateInput();
v = Validate(); Validate
} while (fMustValidate && !v);
return ch;
The outer loop, the do … while() loop, makes the EditWindow keep on
processing input until a "valid" entry has been obtained. (An entry is "valid" if it
satisfies the Validate() member function, or if the fMustValidate flag is false).
Prior to the first character input step, the cursor is positioned, so that input
characters appear at a suitable point within the window.
The "enter" key ('\n ') is to terminate all input operations. The function
HandleCharacter() may return false if the character ch corresponds to some
1104 Reusable designs
other (subclass specific) terminator; if this function returns true it means that the
character was "successfully processed" (possibly by being discarded).
The virtual function TerminateInput() gets called when a terminating
character has been read. Class EditText is an example of where this function can
be useful; EditText::TerminateInput() adds a null character ('\0') at the end of
the input string.
In addition to checking the data entered, the Validate() function should deal
with aspects like notifying a user of an incorrect value.
Class EditWindow can provide default definitions for most of these functions.
These defaults make all characters acceptable (but nothing gets done with input
characters), make all inputs "valid", do nothing special at input termination etc.
Most subclasses will need to redefine several if not all these virtual functions.
By default, an EditWindow is expected to be a framed window with one line of
content area (as illustrated in Figure 30.1 with its editable name and number fields).
The class declaration is:
EditText
An EditText is an EditWindow that can accept input of a string. This string is held
(in a 256 character buffer) within the EditText object. Some other object that
needs text input can employ an EditText, require it to perform a GetInput()
operation, and, when input is complete, can ask to read the EditText object's
character buffer.
Naturally, class EditText must redefine a few of those virtual functions
declared by class EditWindow. An EditWindow can simply discard characters that
are input, but an EditText must save printable characters in its text buffer;
consequently, HandleCharacter() must be redefined. An EditWindow positions
the cursor in the bottom right corner of the window (an essentially arbitrary
position), an EditText should locate the cursor at the left hand end of the text input
field; so function SetCursorPosition() gets redefined.
Normally, there are no "validation" checks on text input, so the default "do
nothing" functions like EditWindow::Validate() do not need to be redefined.
The declaration for class EditText is:
Class EditText adds extra public member functions SetVal() and GetVal()
that allow setting of the initial value, and reading of the updated value in its fBuf
1106 Reusable designs
text buffer. (There is an extra protected function ShowValue() that gets used in
the implementation of SetVal().)
Data members Class EditText adds several data members. In addition to the text buffer, fBuf,
there is an integer fLabelWidth that records how much of the window's width is
taken up by a label. The fSize parameter has the same role as the size parameter
in the EditText class used in Chapter 29. It is possible to specify a maximum size
for the string. The input process will terminate when this number of characters has
been entered. The class uses an integer flag, fChanged, that gets set if any input
characters get stored in fBuf (so changing its previous value).
EditNum
An EditNum is an EditWindow that can deal with the input of a integer. This
integer ends up being held within the EditNum object. Some other object that needs
integer input can employ an EditNum , require it to perform a GetInput()
operation, and, when input is complete, can ask the EditNum object for its current
value.
The extensions for class EditNum are similar to those for class EditText. The
class adds functions to get and set its integer. It provides effective implementations
for HandleCharacter() and SetCursorPosition(). It has an extra (protected)
ShowValue() function used by its SetVal() public function.
Validating numeric Class EditNum() redefines the Validate() function. The constructor for class
input EditNum requires minimum and maximum allowed values. If the "must validate"
flag is set, any number entered should be checked against these limits.
The class declaration is:
protected:
Redefining auxiliary virtual void InitializeInput();
functions of virtual void SetCursorPosition();
GetInput() virtual int HandleCharacter(char ch);
virtual void TerminateInput();
virtual int Validate();
The data members include the minimum and maximum limits, the value of the
EditNum, its "set value", and a label width. The fsign field is used during input to
note the ± sign of the number.
MenuWindow
};
RecordWindow
The data members include the link to the associated Record, a count of the number
of editable subwindows, an integer identifying the sequence number of the current
editable subwindow and a pointer to that subwindow. (Pointers to subwindows are
of course stored in the fSubWindows data member as declared in class Window.)
The constructor for class RecordWindow simply identifies it as something
associated with a Record. The RecordWindow constructor makes it a full size
(70x20) window.
The only additional public member function is PoseModally(). This is the
function that arranges for each editable subwindow to have a turn at getting input.
The function is defined as follows:
void RecordWindow::PoseModally()
{
char ch;
DisplayWindow(); Initialization
CountEditWindows();
InitEditWindows();
fCurrent = fNumEdits;
Loop until user ends
do { input with "enter'
The initialization steps deal with things like getting the window displayed and
determining the number of editable subwindows.
The main body of the PoseModally() function is its loop. This loop will
continue execution until the user terminates all input with the "enter" key.
The code of the loop starts with calls to an auxiliary private member function
that picks the "next" editable subwindow. The associated Record object is then
told to (re)initialize the value in the subwindow. Once its contents have been reset
to correspond to those in the appropriate member of the Record, the edit window is
given its chance to "get input".
The editable item will return the character that stopped its "get input" loop. This
might be the '\ n ' ("enter") character (in which case, the driver loop in
PoseModally() can finish) or it might be a "tab" character (or any other character
that the edit window can not handle, e.g. a '*' in an EditNum).
When an editable subwindow has finished processing input, it is asked whether
its value has changed. If the value has been changed, the associated Record object
is notified. A Record will have to read the new value and copy it into the
corresponding data member. Sometimes, there is additional work to do (the
"consistency update" call).
Example trace of Figure 30.9 illustrates a pattern of interactions between a RecordWindow and a
interactions Record. The example shown is for a StudentRec (as shown in Figure 30.1). It
illustrates processes involved in changing the mark for assignment 1 from its
default 0 to a user specified value.
When the loop in RecordWindow::PoseModally() starts, the EditText
subwindow for the student's name will become the active subwindow. This results
in the first interactions shown. The RecordWindow would ask the Record to set that
display field; the Record would invoke EditText::SetVal() to set the current
string. Then, the EditText object would be asked to GetInput().
If the user immediately entered "tab", the GetInput() function would return
leaving the EditText object unchanged.
After verifying that the subwindow's state was unchanged, the RecordWindow
would arrange to move to the next subwindow. This would be the EditNum with
the mark for the first assignment.
The Record would again be asked to set a subwindow's initial value. This
would result in a call to EditNum::SetVal() to set the initial mark.
The EditNum subwindow would then have a chance to GetInput(). It would
loop accepting digits (not shown in Figure 30.9) and would calculate the new value.
When this input step was finished, the RecordWindow could check whether the
value had been changed and could get the Record to deal with the update.
This section contains example code for the implementation of the window classes.
Not all functions are given, but the code here should be sufficient to allow
implementation. (The complete code can be obtained by ftp over the Internet as
explained in the preface.)
Window class hierarchy: implementation 1111
EditText EditNum
RecordWindow Record (Window id = 1001) (id 1002)
PoseModally()
SetDisplayField() SetVal()
GetInput()
ContentChanged()
NextEditWindow()
SetDisplayField() SetVal()
GetInput()
ContentChanged()
ReadDisplayField() GetVal()
ConsistencyUpdate()
Window
Most of the code is similar to that given in Chapter 29. Extra functions like
ShowText() and ShowNumber() should be straightforward to code.
The DisplayWindow() function gets the contents of a window drawn, then
arranges to get each subwindow displayed:
void Window::DisplayWindow()
{
PrepareContent();
PrepareToDisplay();
ShowAll();
int n = fSubWindows.Length();
for(int i=1; i<=n; i++) {
Window* sub = (Window*) fSubWindows.Nth(i);
sub->DisplayWindow();
}
}
The destructor will need to have a loop that removes subwindows from the
fSubWindows collection and deletes them individually.
1112 Reusable designs
EditWindow
The constructor for class EditWindow passes most of the arguments on to the
constructor of the Window base class. Its only other responsibility is to set the
"validation" flag.
EditText
The EditText constructor is mainly concerned with sorting out the width of any
label and getting the label copied into the "background" image array (via the call to
ShowText()). The arguments passed to the EditWindow base class constructor fix
the height of an EditText to three rows (content plus frame).
ShowText(label, 2, 2, fLabelWidth);
fBuf[0] = '\0';
fChanged = 0;
}
The SetVal() member function copies the given string into the fBuf array
(avoiding any overwriting that would occur if the given string was too long):
The ShowValue() member function outputs the string to the right of any label
already displayed in the EditText. The output area is first cleared out by filling it
with spaces and then characters are copied from the fBuf array:
Normally, an EditText will start by displaying the default text. This should be
cleared when the first acceptable character is entered. Variable fEntry (set to zero
in InitializeInput()) counts the number of characters entered. If its value is
zero a ClearArea() operation is performed. Acceptable characters fill out the
fBuf array (until the size limit is reached).
The remaining EditText member functions are all trivial:
void EditText::InitializeInput()
{
EditWindow::InitializeInput();
fEntry = 0;
}
void EditText::SetCursorPosition()
{
SetPromptPos(fLabelWidth, 2);
}
void EditText::TerminateInput()
{
fBuf[fEntry] = '\0';
}
EditNum
It also sets the data members that record the allowed range of valid inputs and sets
the "value" data member to zero.
The SetVal() member function restricts values to the permitted range:
{
if(val > fMax) val = fMax;
if(val < fMin) val = fMin;
fSetVal = fVal = val;
ShowValue(redraw);
}
Member ShowValue() has to convert the number to a string of digits and get
these output (if the number is too large to be displayed it is replaced by '#' marks).
There is a slight inconsistency in the implementation of EditNum. The initial value
is shown "right justified" in the available field. When a new value is entered, it
appears "left justified" in the field. (Getting the input to appear right justified is not
really hard, its just long winded. As successive digits are entered, the display field
has to be cleared and previous digits redrawn one place further left.)
As the digits are entered, they are used to calculate the numeric value which gets
stored in fVal.
The ± sign indication is held in fsign . This is initialized to "plus" in
InitializeInput() and is used to set the sign of the final number in
TerminateInput():
void EditNum::InitializeInput()
{
EditWindow::InitializeInput();
fsign = 1;
}
void EditNum::TerminateInput()
{
fVal = fsign*fVal;
}
If the number entered is out of range, the Validate() function fills the entry
field with a message and then, using the Beep() and Delay() functions of the
WindowRep object, brings error message to the attention of the user:
else return 1;
}
void EditNum::SetCursorPosition()
{
SetPromptPos(fLabelWidth, 2);
}
MenuWindow
The constructor for class MenuWindow initializes the count of menu items to zero
and adds a line of text at the bottom of the window:
Menu items are "added" by writing their text into the window background (the
positions for successive menu items are predefined). The menu numbers get stored
in the array fCmds.
The PoseModally() member function gets the window displayed and then uses
GetInput() (as inherited from class EditWindow ) to allow the user to select a
menu option:
GetInput();
return fCmds[fChosen];
}
(The "tab" character is defined by the character constant kTABC. On some systems,
e.g. Symantec's environment, actual tab characters from the keyboard get filtered
out by the run-time routines and are never passed through to a running program. A
substitute tab character has to be used. One possible substitute is "option-space"
(const char kTABC = 0xCA;).
The auxiliary member functions SetCursorPosition() and
ClearCusorPostion() deal with the display of the ==> cursor indicator:
void MenuWindow::SetCursorPosition()
{
int x = 5;
int y = 4 + 3*fChosen;
ShowText("==>", x, y, 4,0,0);
SetPromptPos(x,y);
}
void MenuWindow::ClearCursorPosition()
{
int x = 5;
int y = 4 + 3*fChosen;
ShowText(" ", x, y, 4, 0, 0);
}
Dialogs
The basic dialogs, NumberDialog and TextDialog, are very similar in structure
and are actually quite simple. The constructors create an EditWindow containing a
Window class hierarchy: implementation 1119
prompt string (e.g. "Enter record number"). This window also contains a
subwindow, either an EditNum or an EditText.
The PoseModally() functions get the window displayed, initialize the editable
subwindow, arrange for it to handle the input and finally, return the value that was
input.
InputFileDialog::InputFileDialog() : InputFileDialog
TextDialog("Name of input file")
{
}
RecordWindow
RecordWindow::RecordWindow(Record *r)
: EditWindow(0, 1, 1, 70, 20)
{
fRecord = r;
}
void RecordWindow::CountEditWindows()
{
Window class hierarchy: implementation 1121
fNumEdits = 0;
int nsub = fSubWindows.Length();
for(int i = 1; i <= nsub; i++) {
Window* w = (Window*) fSubWindows.Nth(i);
if(w->CanHandleInput())
fNumEdits++;
}
}
void RecordWindow::InitEditWindows()
{
int nsub = fSubWindows.Length();
for(int i = 1; i <= nsub; i++) {
Window* w = (Window*) fSubWindows.Nth(i);
if(w->CanHandleInput())
fRecord->SetDisplayField((EditWindow*)w);
}
}
void RecordWindow::NextEditWindow()
{
if(fCurrent == fNumEdits)
fCurrent = 1;
else
fCurrent++;
int nsub = fSubWindows.Length();
for(int i = 1, j= 0; i <= nsub; i++) {
Window* w = (Window*) fSubWindows.Nth(i);
if(w->CanHandleInput()) {
j++;
if(j == fCurrent) {
fEWin = (EditWindow*) w;
return;
}
}
}
}
void Document::OpenOld()
{
InputFileDialog oold;
oold.PoseModally("example",fFileName, fVerifyInput);
OpenOldFile();
}
When working with a framework, your primary concerns are getting to understand
the conceptual application structure that is modelled in the framework, and the role
of the different classes.
However, you must also learn how to build a program using the framework.
This is a fairly complex process, though the integrated development environments
may succeed in hiding most of the complexities.
There are a couple of sources of difficulties. Firstly, you have all the "header" Problems with
files with the class declarations. When writing code that uses framework classes, "header" files and
linking of compiled
you have to #include the appropriate headers. Secondly, there is the problem of modules
linking the code. If each of the classes is in a separate implementation file, you will
have to link your compiled code with a compiled version of each of the files that
contains framework code that you rely on.
Headers
One way of dealing with the problem of header files is in effect to #include them "The enormous
all. This is normally done by having a header file, e.g. "ClassLib.h", whose header file"
contents consist of a long sequence of #includes:
#include "Cmdhdl.h"
#include "Application.h"
#include "Document.h"
#include "WindowRep.h"
…
#include "Dialog.h"
This has the advantage that you don't have to bother to work out which header files
need to be included when compiling any specific implementation (.cp) file.
The disadvantage is that the compiler has to read all those headers whenever a Slow compilations,
piece of code is compiled. Firstly, this makes the compilation process slow. With large memory usage
a full size framework, there might be fifty or more header files; opening and
reading the contents of fifty files takes time. Secondly, the compiler has to record
the information from those files in its "symbol tables". A framework library may
have one hundred or more classes; these classes can have anything from ten to three
hundred member functions. Recording this information takes a lot of space. The
compiler will need many megabytes of storage for its symbol tables (and this had
better be real memory, not virtual memory on disk, because otherwise the process
becomes too slow).
There are other problems related to the headers, problems that have to be sorted Interdependencies
out by the authors of the framework. For example, there are dependencies between among class
declarations
different classes and these have to be taken into account when arranging
declarations in a header file. In a framework, these dependencies generally appear
as the occurrence of data members (or function arguments) that are pointers to
instances of other classes, e.g:
1124 Reusable designs
class Record {
…
RecordWindow *fWin;
…
};
class RecordWindow {
…
Record *fRec;
…
};
class CommandHandler;
class Application;
class Document;
class Window;
class Record;
class RecordWindow;
…
// Now get first real class declaration
class CommandHandler {
…
};
Such a list might appear at the start of the composite "ClassLib.h" file.
An alternative mechanism is to include the keyword class in all the member
and argument declarations:
class RecordWindow {
public:
RecordWindow(class Record *r);
…
protected:
…
class Record *fRec;
…
};
Linking
The linking problem is that a particular program that uses the framework must have
the code for all necessary framework classes linked to its own code.
Window classes: implementation 1125
One way of dealing with this is, once again, to use a single giant file. This file
contains the compiled code for all framework classes. The "linker" has to scan this
file to find the code that is needed.
Such a file does tend to be very large (several megabytes) and the linkage
process may be quite slow. The linker may have to make multiple passes through
the file. For example, the linker might see that a program needs an
InputFileDialog; so it goes to the library file and finds the code for this class.
Then it finds that an InputFileDialog needs the code for TextDialog; once
again, the linker has to go and read through the file to find this class. In unfortunate
cases, it might be necessary for a linker to read the file three or four times (though
there are alternative solutions).
If the compiled code for the various library classes is held in separate files, the
linker will have to be given a list with the names of all these files.
Currently the Symantec system has a particularly clumsy approach to dealing
with the framework library. In effect, it copies all the source files of the framework
into each "project" that is being built using that framework. Consequently, when
you start, you discover that you already have 150 files in your program. This leads
to lengthy compilation steps (at least for the first compilation) as well as wastage of
disk space. (It also makes it easier for programmers to change the code of the
framework; changing the framework is not a wise idea.)
RecordFile Framework
Figure 30.10 illustrates the module structure for a program built using the
RecordFile framework. Although this example is more complex than most
programs that you would currently work with, it is typical of real programs.
Irrespective of whether you are using OO design, object based design, or functional
decomposition, you will end up with a program composed from code in many
implementation files. You will have header files declaring classes, structures and
functions. There will be interdependencies. One of your tasks as a developer is
sorting out these dependencies.
The figure shows the various files and the #include relationships between files.
Sometimes a header file is #included within another header; sometimes a header
file is #included by an implementation (.cp) file.
When you have relationships like class derivation, e.g. class MyApp : public
Application, the header file declaring a derived class like MyApp (My.h) has to
#include the header defining the base class Application (Application.h).
If the relationship is simply a "uses" one, e.g. an instance of class RecordWin
uses an instance of class Record, then the #include of class Record can be placed in
the RecordWin.cp file (though you will need a declaration like class Record
within the RecordWin.h file in order to declare Record* data members).
The files used are as follows:
• commands.h
Declares constants for "command numbers" (e.g. cNEW etc). Used by all
CommandHandler classes so #included in CmdHdl.h
1126 Reusable designs
commands.h
Keyd.h D.h
CmdHdl.h
BTree.h Document.h
WindowRep.h Application.h
BTDoc.h Record.h
RecordWin.h
My.h
Figure 30.10 Module structure for a program using the RecordFile framework.
• Keyd.h
Declares pure abstract class KeyedStorableItem. This has to be #included
by BTree.h and Record.h. (The class declaration in Record.h tells the compiler
that a Record is a KeyedStorableItem so we had better let the compiler
know what a KeyedStorableItem is.)
• D.h
Declares the dynamic array. A Window contains an instance of class
DynamicArray so the header D.h will have to be #included in WindowRep.h.
• CmdHdl.h
Declares the abstract base class CommandHandler.
• BTree.h
Declares class BTree.
• WindowRep.h
Declares classes: WindowRep, Window, NumberItem, EditWindow, EditText,
EditNum, and the "dialog" classes. A program that uses the window code
typically uses all different kinds of windows so they might as well be declared
and define as a group.
Window classes: implementation 1127
• BTDoc.h
Declares class BTDoc, #including the Document.h class (to get a definition of
the base class Document), and BTree.h to get details of the storage structure
used.
• My.h
Declares the program specific "MyApp", "MyDocument", and "MyRec"
classes.
The main program, in file main.cp, will simply declare an instance of the
"MyApp" class and tell it to "run".
We might as well start with the main program. It has the standard form:
int main()
{
StudentMarkApp a;
a.Run();
return 0;
}
Record *StudentMarkDoc::MakeEmptyRecord()
{
return new StudentRec (0);
}
Record class The specialized subclass of class Record does have some substance, but it is all
quite simple:
NumberItem *fN;
long fTotal;
};
The data required include a student name and marks for various assignments and
examinations. There is also a link to a NumberItem that will be used to display the
total mark for the student.
The constructor does whatever an ordinary Record does to initialize itself, then
sets all its own data members:
strcpy(fStudentName,"Nameless");
}
break;
case 1006:
((EditNum*)e)->SetVal(fMidSession,1);
break;
case 1007:
((EditNum*)e)->SetVal(fFinalExam,1);
break;
}
}
The ReadFrom() and WriteTo() functions involve a series of low level read
(write) operations that transfer the data for the individual data members of the
record:
EXERCISES
1. Implement all the code of the framework and the StudentMark application.
2. Create new subclasses of class Collection and class Document so that an AVL tree can
be used for storage of data records.
3. Implement the Loans program using your AVLDoc and AVLCollection classes.
The records in the files used by the Loans program are to have the following data fields:
The program is to use a record display similar to that illustrated in Figure 30.3.
The commonly encountered frameworks provide a model for what might be termed The "classic
"the classic Macintosh application". With some earlier prototypes at Xerox's Macintosh
application"
research center, this form of program has been around since 1984 on the Macintosh
(and, from rather later, on Intel-Windows machines). It is the model embodied in
1134 Frameworks
Example frameworks
The frameworks that you are most likely to encounter are, for the Macintosh:
the factors that contribute to complexity of the frameworks (like the three hundred
member functions for a scroll bar class).
The structure of the "RecordFile" framework, a "forest" of separate trees, is
similar to that of the more modern frameworks. These more modern frameworks
can still provide abstractions representing abilities like persistence (the ability of an
object to transfer itself to/from a disk file). In modern C++, this can be done by
defining a pure abstract (or maybe partially implemented abstract) base class, e.g.
class Storable. Class Storable defines the interface for any object that can save
itself (functions like DiskSize(), ReadFrom(), WriteTo()). Persistent behaviours
can be acquired by a class using multiple inheritance to fold "storability" in with the
class's other ancestry.
In the older frameworks, the "collection classes" can only be used for objects
that are instances of concrete classes derived from the base "TObject" class. More
modern frameworks use template classes for their collections.
The frameworks are evolving. Apart from ET++, each of the named
frameworks is commercially supported and new versions are likely to appear. This
evolutionary process tends to parallelism rather than divergence. Clever features
added in the release of one framework generally turn up in the following releases of
the competitors.
Tutorial examples It will take you at least a year to fully master the framework library for your
development environment. But you should be able to get their tutorial examples to
run immediately and be able to implement reasonably interesting programs within
two or three weeks of framework usage. ET++ provides the source code for some
examples but no supporting explanations or documentation. The commercial
products all include documented tutorial examples in their manuals. These tutorials
build a very basic program and then add more sophisticated features. The tutorial
for Borland OWL is probably the best of those currently available.
There is no point in reproducing three or four different tutorials. Instead the rest
of this chapter looks at some general aspects common to all the frameworks.
Chapter topics
Resources, code The following sections touch on topics like "Resources", "Framework Macros", the
generators, etc auxiliary code-generating tools (the so called "Wizards" and "Experts"), and
"Graphics" programming. There are no real complexities. But together these
features introduce a nearly impenetrable fog of terminology and new concepts that
act as a barrier to those just starting to look at a framework library.
Persistence There is then a brief section on support for "persistent objects". This is really a
more advanced feature. Support for persistence becomes important when your
program needs to save either complex "network" data structures involving many
separate objects interlinked via pointer data members, or collections that are going
to include instances of different classes (heterogeneous collections). If your
framework allows your program to open ordinary fstream type input/output files
(like the files used in all previous examples), you should continue to use these so
long as your data are simple. You should only switch to using the more advanced
Introduction 1137
forms of file usage when either forced by the framework or when you start needing
to save more complex forms of data.
The final sections look at the basic "event handling" structure common to these Events
frameworks, and other common patterns of behaviour.
Resources
Most of the frameworks use "Resources". "Resources" are simply predefined, pre- "Resource" data
initialized data structures. structures
Resources are defined (using "Resource Editors") externally to any program. A
file containing several "resources" can, in effect, be include in the list of compiled
files that get linked to form a program. Along with the resources themselves, a
resource file will contain some kind of index that identifies the various data
structures by type and by an identifier name (or unique identifier number).
When a program needs to use one these predefined data structures, it makes a
call to the "Resource Manager" (a part of the MacOS and Windows operating
systems). The Resource Manager will usually make a copy, in the heap, of the
"Resource" data structure and return a pointer to this copy. The program can then
do whatever it wants with the copy.
Resources originated with the Macintosh OS when that OS and application
programs were being written in a dialect of Pascal. Pascal does not support direct
initialization of the elements of an array or a record structure. It requires the use of
functions with separate statements that explicitly initialise each of the various
individual data elements. Such functions are very clumsy. This clumsiness
motivated the use of the separate "resource" data structures. These were used for
things like arrays of strings (e.g. the words that appear as menu options, or the error
messages that get displayed in alert box), and simple records like a "rectangle"
record that defines the size and position of a program's main window.
It was soon found that resources were convenient in other ways. If the error
messages were held in an external data structure, rewording of a message to make it
clearer (or translation into another language) was relatively easy. The resource
editor program would be used to change the resource. There was no need to change
program source text and laboriously recompile and relink the code.
New resource types were defined. Resources were used for sound effects, All data types –
pictures, fonts and all sorts of other data elements needed by programs. Resource sound, graphics, text,
windows
bitmap pictures could be used to define the iconic images that would represent
programs and data files. An "alert" resource would define the size and content of a
window that could be used to display an error message. A "dialog" resource could
define the form of a window, together with the text and number fields used for
input of data.
The very first resources were defined as text. The contents of such text files
would be a series of things very like definitions of C structs. The files were
"compiled" (by a simplified C compiler) to produce the actual resource files.
1138 Frameworks
Resource editors Subsequently, more sophisticated resource editors have been created. These
resource editors allow a developer to chose the form of resource that is to be built
and then to enter the data. Textual data can be typed in; visual elements are entered
using "graphics editor" components within the resource editor itself. These
graphics editors are modelled on standard "draw" programs and offer palettes of
tools. An "icon editor" component will have tools for drawing lines, arcs, and
boxes; a "window editor" component will have tools that can add "buttons", "text
strings", "check boxes" and so forth. Depending on the environment that you are
working with, you may have a single resource editor program that has a range of
associated editor components, one for each type of resource; alternatively, you may
have to use more than one resource editor program.
Macintosh resource editors generally produce resources in "compiled form" (i.e.
the final resource file, with binary data representing the resources, is generated
directly). The resource editors for the Windows development environments create
a text file representation that gets compiled during the program "build" process.
You can view the Windows resources that you define in the text format although
you will mostly use the graphically oriented components of the resource editor.
There are auxiliary programs for the Macintosh development environments that
convert compiled resources into a textual form; it would be unusual for you to need
to use these.
"Shared" resources The Windows development environments allow resources to be "shared". A
for Windows OS resource file that you create for a new project can in effect "#include" other
resource files. This has the advantage of saving a little disk space in that
commonly used resources (e.g. the icons that go in a toolbar to represent commands
like "File/Open", or "Print") need only exist in some master resource file rather than
being duplicated in the resource files for each individual project. However there
are disadvantages, particularly for beginners. Frequently, a beginner has the
opinion that any resource associated with their project can be changed, or even
deleted. Deleting the master copy of something like the main "file dialog" resource
causes all sorts of problems. If you are working in a Windows development
environment, check the text form of a resource file to make sure the resource is
defined in the file rather than #included. The resource editor will let you duplicate
a resource so that you can always get a separate, and therefore modifiable copy of a
standard resource.
The individual resources of a given type have identifier numbers (e.g. MENU
1001, MENU 1002 , etc). These need to be used in both the resource file and the
program text where the resources are used. If the numbers used to identify
particular resources are inconsistent, the program won't work. In the Macintosh
environments, you just have to keep the numbers consistent for yourself.
In the Windows environments, you can use a header file that contains a series of
#define constants (e.g. #define MYFILEMENU 1001). This header file can be
#included in both the (source text) resource file and in program (.cp) files. This
reduces the chance of inconsistent usage of resource numbers. Instead of a separate
header file, you can put such declarations are the start of the resource (.rc) file.
Any resource #includes and actual definitions of new resources can then be
bracketed by C++ compiler directive (e.g. #ifdef RESOURCEOMPILER … #endif)
that hide these data from the C++ compiler. The resource (.rc) file may then get
Resources and Macros 1139
#included in the .cp source text files. You will see both styles (separate header and
direct #include of a resource .rc file) in your framework's examples.
Resources were invented long before development switched to C++ and OO Resources and
programming approaches. Resources are just simple data structures. The classes
frameworks will define classes that correspond to many of the resource types. For
example, you might have a program that needs bitmap iconic pictures (or
multicoloured pixmap pictures); for these you would use instances of some class
"BitMapPicture" provided by the framework. The framework's "BitMapPicture"
class will have a member function, (e.g. void BitMapPicture::GetResource(
int idnum)) that allows a BitMapPicture object to load a BITMAP resource from a
file and use the resource data to initialize its own data fields. On the whole, you
won't encounter problems in pairing up resource data structures and instances of
framework classes.
However, there can be problems with some forms of "window" resources (these
problems are more likely to occur with the Macintosh development environments
as the Windows environments handle things slightly differently). You may want to
define a dialog window that contains "static text" strings for prompts, "edit text"
strings for text input fields, and "edit number" components for entering numeric
data. Your resource editor has no problem with this; it can let you design your
dialog by adding EditText and EditNum components selected from a tools palette.
The data structure that it builds encodes this information in a form like an
instruction list: "create an EditNum and place it here in the window, create an
EditText and place it here".
The corresponding program would use an instance of some framework class
"DialogWindow". Class DialogWindow might have a constructor that simply
creates an empty window. Its GetResource() function would be able to load the
dialog window resource and interpret the "instruction list" so populating the new
window with the required subwindows. It is at this point that there may be
problems.
The basic problem is that the program cannot create an instance of class
EditText if the code for this class is not linked. Now the only reference to class
EditText in the entire program may be the implicit reference in something like a
resource data structure defining an "InputFileDialog". The linker may not have
seen any reason to add the EditText code to the program.
You can actually encounter run-time errors where an alert box pops up with a Missing component,
message like "Missing component, consult developer". These occur when the consult developer!
program is trying to interpret a resource data structure defining a window that
contains a subwindow of a type about which the program knows nothing.
Your framework will have a mechanism for dealing with this possible problem.
The exact mechanism varies a lot between frameworks. Generally, you have to
have something like an "initialization function" that "names" all the special window
related classes that get used by the program. This initialization function might have
a series of (macro) calls like "ForceReference(EditText); ForceReference(
EditNum) " (which ensure that the compiled program would have the code
necessary to deal with EditText and EditNum subwindows).
You may find that there are different kinds of resource that seem to do more or "Outdated" resource
less the same thing. For example on the Macintosh you will find a "view" resource types
1140 Frameworks
type and a " DLOG" (dialog) and its related "DITL" (dialog item list) resource types.
They may seem to allow you do the same thing – build a dialog window with
subwindows. They do differ. One will be the kind of resource that you use with
your framework code, the other (in this case the DLOG and DITL resources) will be
an outdated version that existed for earlier Pascal based development systems.
Check the examples of resource usage that come with the tutorials in your IDE
manuals and use the resources illustrated there.
Macros
A "macro" is a kind of textual template that can be expanded out to give possibly
quite a large number of lines of text. Macros may have "arguments"; these are
substituted into the expansion text at appropriate points. Macros can appear in the
source text of programs. They are basically there to save the programmer from
having to type out standard code again and again.
For example, you could define the following macro:
This defines a macro named "out". The out macro takes two arguments, which it
represents by 'o' and 'x'. Its expansion says that a call to "out" should be replaced
by code involving a call to the write() function, involving a type cast, an address
operator and the sizeof() operator.
Macro call This would allow you to use "macro calls" like the following:
out(ofile, fMark1);
out(ofile, fStudentName);
Macro expansion These "macro calls" would be "expanded" before the code was actually seen by the
main part of the C++ compiler. Instead of the lines out(ofile, fMark1) etc the
compiler would see the expansions:
Framework defined The frameworks, particularly ET++ and those for the Windows environment,
macros require lots of standard code to be added to each program-defined class that is
derived from a framework-defined class. Macros have been provided so as to
simplify the process of adding this standard code.
Declare and define Most of these framework macros occur in pairs. There will be a macro that
macros must be "called" from within the declaration of a class, and another macro that must
be called somewhere in the file containing the implementation code for that class.
The first macro would declare a few extra member functions for the class; the
second would provide their implementation.
For example, you might have something like the following:
Resources and Macros 1141
If you wanted class MyDoc based on the framework's Document class, you might
have to have something like the following:
// MyDoc class
DEFINE_CLASS(MyDoc)
Record *MyDoc::MakeEmptyRecord()
{
…
}
If you are curious as to what such macros add to your code, you can always ask the
compiler to stop after the macro preprocessing stage. You can then look at the
expanded text which will contain the extra functions. A typical extra function
would be one that returns the class's name as a text string. You don't really need to
know what these functions are; but because they may get called by the framework
code they have to be declared and defined in your classes.
Your framework's documentation will list (but possibly not bother to explain) Check the macros
the macros that you must include in your class declarations and implementation that you must use
files. If you use the "Class Expert/Wizard" auxiliary programs (see next section) to
generate the initial outlines for your classes, the basic macro calls will already have
been added (you might still need to insert some extra information for the calls).
If you look at the code for the example "StudentMarks" program in Chapter 30, you
will see that some is "standard", and much of the rest is concerned only with the
initial construction and subsequent interaction with a display structure.
Every application built using a framework needs its own specialized subclasses
of class Application and class Document. Although new subclasses are defined
for each program, they have very much the same form in every program. The
specialized Application classes may differ only in their class names (LoanApp
versus StudentRecApp ) and in a single DoMakeDocument() function. The
Document classes also differ in their names, and have different additional data
members to store pointers to program specific data classes. But again, the same
1142 Frameworks
Project generation
The project generation process may involve little more than entry of the basename
for the new project and specification of a few parameters. The basename is used
when choosing the names for specialized Application and Document classes; for
example, the basename "My" would typically lead to the generation of a skeletal
class MyApp : public Application { } and a skeletal class MyDocument :
public Document { }. Some of the parameters that have to be entered in dialogs
relate to options. For example, one of the dialogs presented during this project
generation process might have a "Supports printing" checkbox. If this is checked,
the menus and toolbars generated will include the standard print-related commands,
and provision will be made for linking with the framework code needed to support
printing. Other parameters define properties like the Windows' memory model to
be used when code is compiled. (The Windows OS basically has a choice of 16-bit
or 32-bit addressing.) When confronted with a dialog demanding some obscure
parameter, follow the defaults specified in the framework's reference manuals.
In the Windows environments, once you have entered all necessary information
into the dialogs, the Application Wizard/Expert completes its work by generating
the initial project files in a directory that you will have specified. These generated
files will include things equivalent to the following:
• a resource file (.rc) which will contain a few standard resources (maybe just a
File/Quit menu!);
• a main.cp file which will contain the "main" program (naturally this has the
standard form along the lines "create a MyApp object and tell it to run");
Architects, Experts, and Wizards 1143
• "build" and/or "make" file(s) (.mak); these file(s) contain information for the
project management system including details like where to find the framework
library files that have to be linked when the program gets built.
(Symantec's Visual Architect does not generate the project files at this stage.
Eventually it creates a rather similar set of files, e.g. a .rsrc file with the resources, a
main.cp, and so forth. The information equivalent to that held in a .mak file is
placed in the main project file; it can be viewed and changed later if necessary by
using one of the project's menu options.)
The MyApp.h, MyApp.cp files now contain the automatically generated text.
The contents would be along the following lines:
DEFINE_CLASS(MyApp)
void MyApp::DoSplashScreen()
{
// Put some code here if you want a 'splash screen'
// on start up
}
void MyApp::DoAbout()
{
// Replace standard dialog with a dialog about
// your application
Dialog d(kDEFAULD_ABOUT); // The default dialog
d.PoseModally();
}
Document *MyApp::DoMakeDocument()
{
return new MyDocument;
}
The generated code will include effective implementations for some functions
(e.g. the DoMakeDocument() function) and "stubs" for other functions (things like
1144 Frameworks
the DoSplashScreen() function in the example code shown). These stub routines
will be place holders for code that you will eventually have to add. Instead of code,
they will just contain some comments with "add code here" directives.
Including headers As noted in Chapter 30, you typically have to include numerous header files
when compiling the code for any classes that are derived from framework classes or
code that uses instances of framework classes. The framework may provide some
kind of master header file, e.g. "Framework.h", which #includes all the separate
header files.
Precompiled headers This master header file may be supplied in "precompiled" form.
It takes a lot of time to open and read the contents of fifty or more separate
header files and for the compiler to interpret all the declarations and add their
information to its symbol table. A precompiled header file speeds this process up.
Essentially, the compiler has been told to read a set of header files and construct the
resulting symbol table. It is then told to save this symbol table to file in a form that
allows it to be loaded back on some future occasion. This saved file is the
"precompiled header". Your IDE manuals will contain a chapter explaining the use
of precompiled headers.
It is possible to build display structures in much the same way as was done in the
example in the previous chapter. But you have to go through an entire edit-
compile-link-run cycle in order to see the display structure. If one of the
components is not in quite the right position, you have to start over and again edit
the code.
It is much easier if the user interface components are handled using resources.
The resource editor builds the data structure defining the form of the display. This
data structure gets loaded and interpreted at run-time, with all the subwindows
being created and placed at the specified points. Instead of dozens of statements
creating and placing individual subwindows, the program code will have just a
couple of lines like RecordWindow rw; rw->GetResource(1001).
The resource editor used to build the views required in dialogs will allow you to
move the various buttons, check boxes, edit-texts and other components around
until they are all correctly positioned. The editor may even simulate the behaviour
of the dialog so that you can do things like check the behaviour of "clusters of radio
buttons" (if you select one radio button, the previously selected button should be
deselected).
You will use the resource editor just after the initial project generation step in
order to build the main windows and any dialogs that you expect to use in your
program.
Command numbers When you add an interactive element (button, checkbox, edit-text etc) to a
associated with window you will usually be prompted for an associated "command number" (and,
controls
possibly, an identifying name). These command numbers serve much the same role
as the window identifiers did in the RecordFile framework. When a control gets
activated, a report message gets generated that will have to be handled by some
other object. These report messages use the integer command numbers to indicate
Architects, Experts, and Wizards 1145
what action is being requested. The process is somewhat similar to that by which
changes to the EditNum subwindows in the StudentMarks program got reported to
the StudentRec object so that it could collect the new data values.
The next stage in developing the initial outline for a program typically involves
establishing connections between a command number associated with a visual
control (or menu item) and the object that is to respond to the request that this
command number represents.
In the Windows environments, this is where you use the Class Expert (or Class
Wizard). In the Symantec environment, this is just another aspect of using the
Visual Architect.
The ideas of command handling are covered in more detail in Section 31.5. As
explained there, the operating system and framework will work together to convert
user actions (like key presses and mouse button clicks) into "commands" that get
routed to appropriate "command handler objects". Commands are represented by
little data structures with an integer command number and sometimes other data.
If you were using a framework to build something like the StudentMarks Example of
program, you would have the following commands to deal with: 1) New Document, Commands and
Command Handlers
2) Open Document, 3) Quit, 4) New Record, 5) Delete Record, 6) ViewEdit record,
7) Change name, 8) Change assignment 1, …. You would have to arrange that the
Application object dealt with commands 1…3; that the current Document object
dealt with commands 4…6, and the current Record object dealt with the
remainder.
Usually, you would need a separate member function in a class for each
command that instances of that class have to deal with. So, the Document class
would have to have NewRecord(), DeleteRecord() and ViewEditRecord()
member functions . There would also have to be some mechanism to arrange that a
Document object would call the correct member function whenever it received a
command to handle.
Once a command has reached the correct instance of the appropriate class it Dispatching a
could be dispatched to the right handler routine using a function like the following: command to the
class's handler
function
void StudentMarksDoc::HandleCommand(int commandnum)
{
switch(commandnum) {
case cNEW_REC: NewRecord(); break;
…
}
This was basically the mechanism used in the RecordFile framework example and
is essentially the mechanism used in Symantec's Think Class Library.
In the Windows environment, the idea is much the same but the implementation
differs. Instead of a HandleCommand() member function with a switch statement,
a Windows' "command handler" class will define a table that pairs command
1146 Frameworks
numbers with function identifiers. You can imagine a something like the
following:
Dispatch_table StuMarkDocTbl[] = {
{ cNEW_REC, StudentMarkDoc::NewRecord }
{ cDEL_REC, StudentMarkDoc::DeleteRecord }
{ cVIEW_REC, StudentMarkDoc::ViewEditRecord }
};
Dispatch by table (It is not meant to be syntactically correct! It is just meant to represent an array
lookup whose entries consist of the records pairing command numbers and function
names.) There will be code in the framework to search through such an array
checking the command numbers; when the number matching the received
command is found, the corresponding function gets called.
Now a substantial part of the code for these command handling mechanisms can
be standardized. Stub functions like:
void StudentMarkDoc::NewRecord()
{
// Put some code here
}
and the HandleCommand() function (or the corresponding table for a Windows
program) can all be produced once the generator has been told the command
numbers and the function names.
The "Class Expert" (or equivalent) will have a set of dialogs that you use to
select a command number, pick the class whose instances are to handle that
command, and name the function that will handle the command. (For many
standard Windows commands, the name of the handler function is predefined.)
Once these data have been entered, the "Class Expert" will add the extra code
with function declarations, and stub definitions to the appropriate files. Macros are
used for the function dispatch tables; again, there are separate DECLARE and DEFINE
macros. If you looked at the contents of the files at this stage you would find
something along the following lines:
DECLARE_RESPONSE_TABLE(StudentMarkDoc)
};
Architects, Experts, and Wizards 1147
DEFINE_CLASS(StudentMarkDoc)
DEFINE_RESPONSE_TABLE(StudentMarkDoc, Document)
COMMAND(cNEW_REC, NewRecord)
COMMAND(cDEL_REC, DeleteRecord)
COMMAND(cVIEW_REC, ViewEditRecord)
END_RESPONSE_TABLE
void StudentMarkDoc::NewRecord()
{
// Put some code here
}
In the Symantec Visual Architect, this is the point where all the project specific
files get created and then all the framework source files are copied into the project.
The code that is generated can be compiled and will run. The main window will be
displayed, a File/Quit menu option will work, a default "About this program …"
dialog can be opened. Your program may even be able to read and write empty
files of an appropriate type (file types are specified in the Application Expert phase
of this generation process).
It does all the standard things correctly. But of course all the real routines are
still just stubs:
void StudentMarkDoc::NewRecord()
{
// Put some code here
}
You must now start on the real programming work for your project.
31.3 GRAPHICS
The programs that you build using the frameworks work with the "windows"
supported by the underlying operating system (MacOS, Windows, or X-terminal
and Unix). Output is all done using the OS graphics primitives (of course
filestreams are still used for output to files).
The OS will provide a "graphics library" (strictly, the X-lib graphics library is Library of graphics
not part of the Unix OS). This will be a function library with one hundred or more functions
output related functions. These will include functions for drawing lines, arcs,
rectangles, individual characters, and character strings.
1148 Frameworks
Because these graphics functions are provided by the OS and not by the
development environment, they may not be listed in the IDE manuals. You can get
a list of the functions by opening the appropriate header file (Quickdraw.h on the
Macintosh, Windows.h on Windows). The environment's help system may contain
descriptions of the more frequently used graphics functions. Examples illustrating
the use of the graphics library are not normally included in the manuals for the
development environment. There are however many books that cover basic
graphics programming for Windows or for Macintosh (the ET++ library uses
graphics functions similar to those of the Macintosh, internally these are converted
into calls to X-lib functions).
The basic graphics calls shouldn't in fact cause much problem; they are all pretty
much intuitive. The function library will have functions like the following:
(If you look in the header files, you may encounter "extern Pascal" declarations.
These are actually requests to the C++ compiler to put arguments on the stack in
"reverse" order, as was done by a Pascal compiler. This style is largely an
historical accident relating to the earlier use of Pascal in both Macintosh and
Windows OSs. )
Although the basic functions are quite simple to use, there can be complications.
These usually relate to the maintenance of collections of "window attributes".
Window attributes Each window displayed on the screen has many associated attributes. For
example, a window must have a defined depth (1-bit for black and white, or 8-bit
colour, or 16-bit colour etc). There will be default foreground (black) and
background (white) colours. There will be one or more attributes that define how
bitmaps get copied onto the screen; these "mode" (or "brush" or "pen") attributes
allow the program to chose whether to simply copy an image onto the screen or to
use special effects like using the pattern of bits in an image to invert chosen bits
already displayed on the screen. Other attributes will define how the characters are
to be drawn; these attributes will include one that defines the "font" (e.g. 'Times',
'Helvetica' – different fonts define differently shaped forms for the letters of the
normal "Roman" alphabet and, possibly, define different special characters),
another attribute will specify the size of characters. There will be attribute to
specify the thickness of lines. Still another will define the current x, y drawing
point.
Ports, Graphics These attributes are normally held in a data structure, the form of which is
Contexts, Device defined by the graphics library. On the Macintosh, this will be a Port data
Contexts
Graphics 1149
If the data for your program are something simple like a list of "student records" (as
in the example in Chapter 30), you should not have any problems with input and
output. The automatically generated code will include stub routines like the
following (the function names will differ):
The code that you would have to add would be similar to that illustrated for the
ArrayDoc class in Chapter 30. You would output the length of your list, then you
would iterate down the list getting each StudentRec object to write its own data.
The input routine would again be similar to that shown for class ArrayDoc. The
number of records would be read, there would then be a loop creating StudentRec
objects and getting them to read themselves. After you had inserted your code, the
input function would be something like:
You would however have problems if you were writing a program, like that
discussed briefly in Chapter 23, where your data was a list of different
1150 Frameworks
Input problems! The problems arise with the input. Obviously, the input routine cannot create
CircuitElement objects because CircuitElement is a pure abstract class. The
input routine has to create a Battery, or a Wire, or whatever else is appropriate for
the next data item that is to be read from the file.
Solution: class If you have to deal with heterogeneous collections, you need some scheme
identifying tokens in whereby objects that write themselves to file start by outputting some token that
the file
identifies their class. Then you can have an input routine along the following lines:
This business about outputting class tokens when objects write themselves to
file, then inputting these tokens and interpreting them so as to create an instance of
the correct class, all involves a fair amount of code. But this code is now pretty
much standardized.
Framework support The standard code can be implemented by the framework. It will be called
something like the "Streamable" component, or "Persistent" component, or
"Storable" component. It requires a few extra functions in each class (e.g. a
function to write a "class token", which is normally a string incorporating the class
name). The framework may require extra macros in the class, e.g. a
Persistent data 1151
Document::fList
Paragraph
object Style: Header, font
Helvetica, size 12
Style: MainText,
font Times, size 10
void StudentRecDoc::Do … … }
class Paragraph {
DECLARE_CLASS(Paragraph)
DECLARE_STREAMABLE
public:
1152 Frameworks
…
void WriteTo(fstream& out)
…
private:
char *fText;
Style *fStyle;
}
That Paragraph::WriteTo() function does not save any useful data. Two
addresses have been sent to a file. Those memory addresses will mean nothing
when the file gets reread.
The following is slightly better, but still not quite right:
This would save the character data and, assuming Style objects know how to save
their data, would save the associated styles.
However, you should be able to see the error. The Style record named
"MainText" will get saved twice. When the data are read back, the second and
fourth paragraphs will end up with separate styles instead of sharing the same style
record. The word processor program may assume that paragraphs share styles so
that if you change a style (e.g. the "MainText" style should now use 11 point) for
one paragraph all others with the same style will change. This won't work once the
file has been read and each paragraph has its own unshared style record.
What you want is for the output procedure to produce a file equivalent to the
following:
When the output has to deal with an object that has already been saved, like the
style object for the "MainText" style, a reference back to the earlier copy is all that
gets sent to file.
If a file in this form is read, it is possible to reconstruct the original network.
Now getting all of this to work is relatively complicated. It involves modified
stream classes that may seem generally similar to the familiar fstream classes but
which perform additional work. It is these modified streams that know how to deal
with pointers and know not to save an object more than once.
The code is pretty much standardized and should be in your framework's
"Streamable" component. You will have to read the reference manuals for your
framework and the related tutorial examples to see how you are supposed to deal
with pointer data members in your objects.
The framework's support for streamability may also include definitions of Operator functions
overloaded operator functions. These will allow you to write code in the form:
rather than
fStyle->WriteTo(outfile);
Programs written for personal computers, and Unix/X-windows, are "event driven".
User actions like keystrokes and mouse clicks are picked up by the operating Events
system. This generates "event" data structures that it queues for processing by the
currently running program. Event data structures contain a number of data fields.
One will be an integer event code (distinguishing keystroke event, left/right mouse
button event etc). The interpretation of the others data fields depends on the event.
The data structure for a mouse event will contain x, y coordinate information, while
a key event has data to identify which key.
1154 Frameworks
As well as these primary input events, there are others like an "update window"
event. The OS will itself generate this type of event. An "update window" event
gets sent to a program when the viewable area of an associated window is changed
(for instance, another window, possibly belonging to some other program, has been
closed letting the user see more of the window associated with the "update" event.
The program picks these events up and interprets them. At the heart of all these
programs you will find an event loop like the following:
More typically, the objects that are given the low-level events to handle will turn Conversion of low-
them into "commands". Like "events", these commands are going to be represented level events to higher
level commands
by simple data structures (the most important, and only mutually common data field
being the identifying command number). "Commands" get handed back to the
Application object which must route them to the appropriate handler.
For example, if the Application object determines that the low level event was MenuManager turns
a mouse-down in a menu bar, or keying of an accelerator key, it will forward details a mouse event into
command
to a MenuManager object asking it to sort out what the user wants. The
MenuManager can deal with an accelerator key quite easily; it just looks up the key
in a table and returns a "command" data structure with the matching command
number filled in. Dealing with a mouse-initiated menu selection takes a little more
time. The MenuManager arranges with the OS for the display of the menu and waits
until the user completes a selection. Using information on the selection (as
returned by the OS) the MenuManager object can again determine the command
number corresponding to the desired action. It returns this to the Application
object in a "command" data structure.
A mouse click in a control window (an action button, a scroll bar, or something A "control" window
similar) will result in that window object executing its DoClick() function (or turns a mouse event
into a command
something equivalent). This will simply create a command data structure, filling in
the command number that was assigned to the control when it was built in the
resource editor.
The Application then has the responsibility of delivering these command data
structures (and, also, ordinary keystrokes) to the appropriate object for final
processing.
Normally, there will be many potential "handlers" any one of which might be
responsible for dealing with a command. For example, the application might have
two documents both open; each document will have at least one window open;
these windows would contain one or more views. Views, windows, documents and
even the application itself are all potentially handlers for the command.
The frameworks include code that establishes a "chain of command". This "Command Handler
organization simplifies the process of finding the correct handler for a given Chain"
command.
At any particular time, there will be a current "target" command handler object "First handler",
that gets the first chance at dealing with a command. This object will be identified "Target", or
"Gopher"
by a pointer data member, CommandHandler *fTarget, in the Application
object. Usually, the target will be a "View" object inside the frontmost window.
The Application starts the process by asking the target object to deal with the
command (fTarget->DoCommand(aCommand);). The target checks to determine
whether the command aCommand is one that it knows how to deal with. If the target
is the appropriate handler, it deals with the command as shown in earlier examples.
If the target cannot deal with the command, it passes the buck. Each Passing the buck
CommandHandler object has a data member, CommandHandler *fNextHandler,
that points to the next handler in the chain. A View object's fNextHandler will
identify the enclosing Window (or subwindow). A Window that is enclosed within
another Window identifies the enclosing Window as its "next handler". A top level
window usually identifies the object that created it (a Document or Application)
1156 Frameworks
as its next handler. Document objects pass the buck to the Application that
created them.
The original target passes the command on: fNextHandler->DoCommand(
aCommand). The receiving CommandHandler again checks the command, deals
with it if appropriate, while passing on other commands to its next handler.
For example, suppose the command is cQUIT (from the File/Quit menu). This
would be offered to the frontmost View. The View might know about cRECOLOR
commands for changing colours, but not about cQUIT. So the command would get
back to the enclosing Window . Window objects aren't responsible for cQUIT
commands so the command would get passed back to the Document. A Document
can deal with things like cCLOSE but not cQUIT. So, the command reaches the
Application object. An Application object knows what should happen when a
cQUIT command arrives; it should execute Application::DoQuit().
Keyed input can be handled in much the same way. If an ordinary key is typed,
the fTarget object can be asked to perform its DoKeyPress() function (or
something equivalent). If the target is something like an EditText, it will consume
the character; otherwise, it can offer it to the next handler. Most CommandHandler
classes have no interest in keyed input so the request soon gets back to the end of
the handler chain, the Application object. It will simply discard keyed input (and
any other unexpected commands that might arrive).
Defining the initial The initial target changes as different windows are activated. Most of the code
target for dealing with these changes is implemented in the framework. A programmer
using the framework has only a few limited responsibilities. If you create a Window
that contains several Views, you will normally have to identify which of these is the
default target when that Window is displayed. (The user may change the target by
clicking the mouse within a different View.) You may have to specify your chosen
target when you build the display structure in the resource editor (one of the dialogs
may have a "Target" field that you have to fill in). Alternatively, it may get done at
run time through a function call like Application::SetTarget(Command
Handler*).
Exceptions to the In most situations, an input event gets handled and the program soon gets back
normal event handler to the main event loop. However, drawing actions are an exception.
pattern
A mouse down event in a Window (or View) that supports drawing results in that
Window taking control. The Window remains in control until the mouse button is
released. The Window will be executing a loop in which it picks up mouse
movements. (Operating systems differ. There may be actual movement events
generated by the OS that get given to the program. Alternatively, the program may
keep "polling the mouse", i.e. repeatedly asking the OS for the current position of
the mouse.) Whenever the mouse moves, a drawing action routine gets invoked.
(This loop may have to start with some code that allows the Window to specify that
it wants to "capture" incoming events. When the mouse is released, the Window
may have to explicitly release its hold on the event queue.)
Drawing routines Most of this code is framework supplied. The application programmer using the
framework has to write only the function that provides visual feedback during the
drawing operation and, possibly, a second function that gets called when drawing
activity ends. This second function would be responsible for giving details of the
Event Handlers 1157
drawn item to the Document object so that this can add the new item to its list of
picture elements.
CommandHandler classes
The Class Expert program (or equivalent) should be used to generate the initial
stubs for these additional command handling routines. The developer can then
place the effective data manipulation code in the appropriate stub functions.
With the overall flow of control largely defined by the framework, the concerns
of the development programmer change. You must first decide on how you want to
hold your data. In simple cases, you will have a "list" of data elements; with this
list (a framework supplied collection class) being a data member that you add to the
document. Then, you can focus in turn on each of the commands that you want to
support. You decide how that command should be initiated and how it should
change the data. You then add the necessary initiating control or menu item; get
the stub routines generated using the "Class Expert"; finally, you fill in these stub
routines with real code.
On the whole, the individual commands should require only a small amount of
processing. A command handling routine is invoked, the data are changed, and
control quickly returns to the framework's main event loop. Of course, there will
be exceptions where lengthy processing has to be done (e.g. command "Recalculate
Now" on a spreadsheet with 30,000 formulae cells).
As you gain confidence by programming with your framework you should also
study these design patterns to get more ideas on proven problem solving strategies.
You will find the patterns in "Design Patterns: Elements of Reusable Object-
Oriented Software" by E. Gamma, R. Helm, R. Johnson, and J. Vlissides (Addison-
Wesley).
EXERCISES
1 Use your framework to implement a version of the StudentMarks program. Employ a
(framework provided) list or a dynamic array class for the collection.