C++, Java and Octave Numerical Programming With Free Software Tools PDF
C++, Java and Octave Numerical Programming With Free Software Tools PDF
David Yevick
Cambridge, New York, Melbourne, Madrid, Cape Town,
Singapore, São Paulo, Delhi, Mexico City
Cambridge University Press
The Edinburgh Building, Cambridge CB2 8RU, UK
Published in the United States of America by Cambridge University Press, New York
www.cambridge.org
Information on this title: www.cambridge.org/9780521116817
David Yevick 2012
A catalogue record for this publication is available from the British Library
1 Introduction page 1
1.1 Objective 1
1.2 Presentation 1
1.3 Programming languages 2
1.4 Language standards 3
1.5 Chapter summary 4
1.6 How to use this text 4
1.7 Additional and alternative software packages 4
2 Octave programming 5
2.1 Obtaining octave 5
2.2 Command summary 5
2.3 Logistic map 14
5 Fundamental concepts 26
5.1 Overview of program structure 26
5.2 Tokens, names and keywords 26
vii
viii Contents
6.28 Arrays 54
6.29 Program errors 55
6.30 Numerical errors with floating-point types 56
11 References 101
11.1 Basic properties 101
11.2 References as function arguments 102
11.3 Reference member variables 103
11.4 const reference variables 103
11.5 Reference return values 104
Index 259
Chapter 1
Introduction
1.1 Objective
This textbook overviews modern scientific programming, including numerical
analysis, object-oriented programming, scientific graphics, software engineer-
ing, numerical analysis and physical system modeling. Consequently, knowl-
edge of the material will provide sufficient background to enable the reader
to analyze and solve nearly all normally encountered scientific programming
tasks.
1.2 Presentation
The text is concise, focusing on essential concepts. Examples are intention-
ally short and free of extraneous features. To promote retention, the book
repeats key topics in cycles of gradually increasing difficulty. Further, since
the process of learning computer language shares many similarities with that of
acquiring a spoken language, important code is highlighted in gray. Memoriz-
ing these features greatly decreases the time required to achieve proficiency in
programming.
1
2 Introduction
such as C++ are relatively minor and do not affect core functionality. However,
programs employing elements of a new standard will not necessarily function on
older compilers.
For small programs or rapid prototyping of ideas and methods, the commercial
MATLAB R
language, or its freeware alternatives, offers a practical alternative
to C++ or FORTRAN. In this book, the free GNU Octave implementation is
discussed from a scientific programming perspective. After becoming familiar
with the central language constructs summarized below, the built-in Octave help
facilities conveniently provide information on specialized, infrequent commands.
5
6 Octave programming
dir or ls, which display the contents of a directory, .., which moves to one directory
higher in the directory tree, ., which represents the current directory, rename file1.1
file2.2, which renames the file file1.1 to the name file2.2, and copy, which similarly
copies a file.
(3) MS-DOS and Unix commands. Standard DOS commands on Windows systems and
Unix commands on Unix systems are issued in Octave by typing e.g. dos 'copy file.1
file.2' or, on a Unix system, unix 'cp file.1 file.2' (single or double apostrophe). In
MATLAB such commands can also be preceded with !.
(4) Command structure and continuation lines. Octave commands end at a carriage
return, comma or semicolon; however, only a semicolon suppresses the output of
the statement from being written to the terminal. Two or more commands situated
on the same line must be separated by commas or semicolons. A statement can
span several lines but each line must normally be terminated by a three-period
continuation character, . . . .
(5) Creating and editing files. The command diary on stores subsequent commands
entered from the keyboard in a file named diary until diary off is issued. To examine
this file or to create or edit an Octave program, after navigating to the directory in
which the file resides, type edit at the command prompt, followed, where applicable,
by the name of the file to be edited or created.
(6) Comments. Any text to the right of a comment character, %, constitutes a comment
and is consequently ignored by the Octave interpreter. The beginning of a program
should contain the date, version number, title and author. Every set of statements
(a paragraph) performing a certain task should be preceeded by one or more blank
lines followed by comment lines explaining the purpose of the program unit. When
a variable is introduced, its meaning should be made clear by a comment either
above the line or on the same line to the right of the statement. Such annotations
insure the long-term readability of programs.
(7) Using help. To find and implement rarely employed commands, type first lookfor
subject to obtain a list of all commands involving the operation ‘subject’. Issuing
help commandName (or doc commandName) then provides help on the command
commandName.
(8) Octave programs. Octave program and function files must possess a .m extension;
that is, to program in Octave, first type edit from within the Octave command
window and create a file such as
S = 2;
S * . . . % Illustrates the comment and continuation symbols
S
Then, when saving the file, specify test in the "File Name" text entry field while in
the "Save File as Type" drop-down text box select MATrix LABoratory (.m). This
automatically appends the correct .m extension to the file name. If the file is saved in
a directory, e.g. X:\testDirectory, then, at the Octave prompt, type cd followed by
the directory (including, if necessary, the partition name, e.g. cd X:\testDirectory)
2.2. Command summary 7
where test.m resides, press enter and then type test. The program test can also be
called from within another .m file within the same directory.
(9) Variable-naming conventions. For clarity, variable names should start with a small
letter, while subsequent words in the name should be capitalized, e.g. numberOf-
Points. However, in Octave a name can represent an array of any size and number
of dimensions, which can result in subtle errors. To prevent this, quantities with a
row dimension can be indicated by a trailing R, those with a column dimension as
C and a matrix with a trailing RC, e.g, systemMatrixRC. If matrices with different
dimensions are present, the row and column sizes can be further specified as in
systemMatrixR4C8. Since Octave is not a typechecked language, the above con-
ventions can still lead to severe and difficult-to-locate errors for some compound
words such as wavefunction, which can be treated as one word in certain places
and as two words (waveFunction) in others. Typing a single character incorrectly
generates similar problems. These errors, however, can be immediately identified
by typing who at the command line, which displays a list of all the currently defined
variables. Any spelling error will then be evidenced by a variable name seemingly
appearing twice in the list.
(10) Formatting conventions. Every binary operator (+, − etc.) should be surrounded
by spaces, but not unary operators as in 3 + −4.0. Indentation should be employed
for every set of statements that are under the logical control of a control statement
such as for, if, while. Commas, semicolons, parentheses and braces should, where
appropriate, be followed by spaces.
(11) Program input. To prompt the user from within a .m file to enter a single variable
or array x from the keyboard, employ x = input( 'user prompt ' ). A variable y that
can later be employed in a logical control statement to branch into different program
units is conveniently entered with y = menu( 'Select the method', 'Method A',
'Method B', 'Method C' ); which assigns the value 1, 2 or 3 to y according to the
user selection.
(12) Output formatting. The more command pages subsequent output. To write out
subsequent floating-point output with 16 digits of precision, type in format long
e, to revert to the default 5 digits, type format short e or, equivalently, format
compact.
(13) Built-in constants and functions. Important predefined scalar quantities are e, pi,
i and j, both of which represent the unit complex number, and eps, the smallest
number which when added to 1 gives a number different from 1 (machine epsilon).
However, a major problem arises if these variables are redefined, for example, the
command i = [1, 3] overwrites the intrinsic definition of i, which is not reinstated
until a further command clear i is issued. Note that i and j are frequently employed
as loop variables, so that all loop variables should instead be labeled, for example,
loop, innerLoop, outerLoop.
(14) Complex numbers. A complex number is introduced as c = 2.0 + 4.0i and then
manipulated with functions such as real( ), imag( ), conj( ), norm( ). Complex
numbers are e.g. multiplied, divided and exponentiated in standard fashion either
8 Octave programming
by real or by other complex numbers. Functions such as cos( ), sin( ), sinh( ) yield
complex results when applied to complex quantities.
(15) Loading arrays. A variable name can represent a scalar or an array of any dimension.
A row vector is introduced as
vR = [1 2 3 4];
or
vR = [1, 2, 3, 4];
vC = [1
2
3
4];
vC = [1; 2; 3; 4];
With the transpose operator .' the above column vector can also be entered as
vC = [1 2 3 4].';
A matrix
1 2
m RC =
3 4
can therefore be entered in any of the following ways:
mRC = [1 2; 3 4];
mRC = [1 2
3 4];
vR = [1 3 2 4]; mRC = reshape( vR, 2, 2 );
vC = [1
3
2
4]; mRC = reshape( vC, 2, 2 );
(The order in which a vector is reshaped indicates that matrix elements with succes-
sive values of the leftmost, column, index are stored next to each other in memory.)
The matrix element (mRC)12 is subsequently accessed by mRC(1, 2). Since scalars
and arrays are manipulated identically, arrays with multiple dimensions are con-
structed from component vectors or from subarrays in the same manner as from
scalar quantities, e.g. vR4 = [[1 2] [1 2]] yields [1 2 1 2];, while mBlockRC = [mRC
mRC; mRC mRC]; is a matrix of twice the dimension of mRC. While a vector
or matrix expands dynamically as new elements are added as in vR = [1 2]; vR(3)
= 3; this is computationally inefficient and memory should instead be preallocated
through a statement such as vR = zeros( 1, n ), which creates a row vector of n zero
elements.
2.2. Command summary 9
(16) Size and length. Octave maintains a record of the size of an array to prevent element
access outside this range. Hence mRC(1, 3) yields an error if mRC is a 2 × 2 matrix.
The command size( mRC ) for an M × N matrix returns the array [M, N]. For a
single-dimension array, length( vR ) returns the length of the array vR. However,
when applied to a two-dimensionsal array length( mRC ) returns the maximum
value of M and N, possibly leading to unexpected errors.
(17) Matrix operations. The n × n identity matrix is represented by eye( n ) or eye( n, n ),
while the n × n matrix with all unity elements is denoted ones( n ) or ones( n, n ).
If s = 2 and mRC = [1 2; 3 4] as in item (15) above, then
3 4
s + mRC = s ∗ ones (2,2) + mRC ⇒
5 6
while
3 2
s ∗ eye (2, 2) + mRC ⇒
3 6
Very often, errors arise because of failure to differentiate between these. Multiplica-
tion similarly possesses different meanings depending on variable type. Multiplying
or dividing a matrix mRC by a scalar s multiplies (divides) all elements of mRC
by s while mRC * mRC symbolizes normal matrix multiplication and
1 4
mRC. ∗ mRC ⇒
9 16
implements component-by-component multiplication. Similarly mRCˆ2 is mRC
* mRC, while mRC.ˆ2 instead squares the individual elements of mRC. The
dot operator functions analogously for other arithmetic operations such as mRC
./ nRC, which yields a matrix whose (i, j)th element is simply (mRC)ij /(nRC)ij .
Standard functions such as cos( mRC ) operate on the individual elements of mRC,
here returning the matrix formed by taking the cosine of each element. Two easily
confused operations are array (matrix) transpose without complex conjugation, .',
and transpose with complex conjugation, '. Note that the simpler syntax is applied to
the Hermitian conjugate operation, since this yields the standard norm of complex
(as well as real) arrays. For a vector vR = [1, 2] with elements (vR)ij , the dot or inner
product without complex conjugation is given by vR * vR.', while the (Kronecker)
outer product vR.' * vR yields the 2 × 2 matrix with (i, j)th element (vR)i (vR)j ,
namely [1 2; 2 4].
(18) Matrix functions. A few functions ending in the letter m such as the matrix expo-
nential expm act on matrix arguments and are defined (although not implemented)
through power-series expansions such as
The determinant, trace, inverse, logarithm and square root of aRC are similarly
given by det( aRC ), trace( aRC ), inv( aRC ), logm( aRC ) and sqrtm( aRC ),
10 Octave programming
(24) Cell arrays. To form an array composed of different types of variables (including
additional cell arrays) as members, write e.g. cA = {{1 2; 3 4} 's'; 3 5}. Then cA
{1,2} is the letter s while cA{1,1}{2,2} is 4. In MATLAB cellplot( cA ) additionally
yields a graphical display of the contents of cA.
(25) Iterators. In Octave, for loops are implemented far less efficiently than vectorized
expressions containing array variables. The colon operator generates a row vec-
tor, by default with unit step, that can replace iterators. For example, 1 : 3 yields
the row array [1 2 3], while vR = sin( pi: -pi / 10: -1.e-4 ); produces the array
vR = [sin( pi ) sin( 9 pi / 10 ) . . . sin( pi / 10 ) 0]. A non-vectorized for loop
possesses the form for loop = 0.9 : 0.1 : 0.3 . . . statements to be iterated over
. . . end or, equivalently, for l = [0.9 : 0.1 : 0.3] . . . . A common error here is
to write for l = .9, -.1, .3 in place of one of the statements above. This assigns
the value .9 to l and then executes the meaningless statements -1 and 3 without
an error message. Note that the common choice of i or j as iteration (or ordi-
nary) variables precludes their further interpretation as complex numbers, so that i
and j should not be employed as variable or iterator names. Iteration can also be
implemented with a while loop, as in while loop > 0.3, loop = loop - 0.1, end,
which takes the place of the for loop introduced at the beginning of this para-
graph. A break statement when encountered terminates the iteration and passes
control to the statement following the end statement of the loop in which the break
occurs.
An isolated colon, :, iterates through all the rows or columns of a matrix, so that
mR2C4( :, 1 ) = vR2( : ) (or equivalently = vR2.'( : ), which is a column vector),
places all the elements of the two-element row vector vR2 into the first column
of the matrix mR2C4. A matrix can also be mapped into a vector with the colon
operator; for a matrix mRC = [1 2; 3 4], writing V = M( : ) yields the column vector
V = [1; 3; 2; 4] (since, as noted earlier, in Octave successive column elements are
adjacent in memory). Related constructs are linspace( S1, S2, N ) (and logspace),
which generate N equally (logarithmically) spaced points between S1 and S2 that
include the endpoints.
(26) Control logic. The logical operators in Octave are given by ==, <, >, >=, <=, ∼=
(not equal) and the and, or and not operators &, | and ∼, respectively. These are
typically employed in while . . . end, if . . . elseif . . . end (note that elseif is a
single word) and switch statements. The last of these is rather complex, so the help
subsystem should be consulted when coding.
(27) Functions. A function, with a name functionName, must be placed in a separate
(and similarly capitalized) file functionName.m (this requirement can be circum-
vented somewhat in Octave). The first line of this file must further be of the form
[mOutRC, vOutR, . . . ] = functionName( mInRC, vInR, . . . ). To share internal
variables with other program units a global statement that includes the names of
the shared variables separated by spaces (not commas) is introduced as follows
(the endfunction statement is generally not required; and a helpful convention is to
capitalize global variables):
12 Octave programming
%file functionName.m:
Spring1.position = 0;
Spring1.velocity = 1;
Spring1.material = 'Steel';
Spring1.position = Spring1.position + deltaTime ...
k / m * Spring1.velocity
#include <cstdlib>
#include <iostream>
using namespace std;
15
16 The Dev-C++ programming environment
return EXIT_SUCCESS
}
and places the cursor at the beginning position of the window. Using the tab key for
indentation, add the additional line
immediately after the first opening brace, {. The effect of cout << is to display the
quantity to the right of the insertion operator << on the terminal, while << endl
terminates the output line. Also add the following comment lines at the beginning of
the program:
// Hello world v. 1.0
// Aug. 11 2000
// (your name)
// This program tests the C++ environment
Any text on a line to the right of two forward slashes is treated as a comment and is
not processed as part of the C++ program. The lines before int main( . . . ) together
with the first two of the three last lines of the program will not be repeated in most
of the remaining programs in this textbook, although they will be present in every
console program that is created by Dev-C++. The initial statements can normally
be replaced by a single statement #include <iostream.h>, which is, however, an
antiquated programming strategy.
To compile and run a single file without first creating a project, after opening
Dev-C++ depress the third button marked “New” on the button bar in the program
window. The program above can now be entered or, alternatively,
// Hello world v. 1.0
// Apr. 18 2011
// (your name)
// This program tests the C++ environment
#include <iostream>
#include <windows.h>
using namespace std;
main ( ) {
cout << ''Hello World'' << endl;
Sleep(4000);
}
Another variant, which as in the first program of this section pauses the program
indefinitely until any key is depressed:
main ( ) {
cout << ''Hello World'' << endl;
cout << ''Press any key to continue'' << endl;
getch( );
}
3.2 Using the Dev-C++ debugger 17
main ( ) {
cout << ''Hello World'' << endl;
cout << ''Press any key to continue'' << endl;
int endProgram;
cin >> endProgram;
}
{
int i = 0;
cout << ''The value of i is '' << i << endl;
i = 5;
cout << ''The value of i is '' << i << endl;
int j = 0;
cout << ''The value of j is '' << j << endl;
int k = i / j;
cout << k << endl;
}
Save this file by clicking on the fourth icon on the top button bar that resembles
a floppy disk with the float-over text “Save”.
To activate or deactivate debugging, select “Tools → Compiler Options”
from the menu bar, followed by the Settings tab (the arrow indicates that the
“Tools” menu option should be selected, followed by the “Compiler Options”
submenu item). Click on “linker” in the left windowpane; “Generate Debugging
Information” appears as a text label on the right-hand pane. Clicking on “Yes”
or “No” to the right of this text generates a drop-down text box from which the
desired behavior can be selected. Profiling, which details the amount of time spent
in different code regions, can similarly be activated by selecting “Code Profiling”
in the left-hand windowpane. Increasing the compiler optimization level through
the menu option “Optimization” often reduces the execution time of the program,
but should generally be attempted only at the end of the development process.
With debugging activated, recompile the program by selecting the first icon
in the lower toolbar. Inside the editor window, click in the gray area just to the
left of the line int i = 0;. A check symbol should appear and the line should
turn red; this is termed a breakpoint. Click again on the Debug icon. A set of
debugging menu items will appear at the bottom of the editor window. Locate
the windowpane to the left of the main editor window and right click inside this
area. A pop-up menu with the selection “Add Watch” will appear (if this fails
simply click on the “Add Watch” icon in the debugging toolbar). Click on this
icon. A secondary window requesting a variable name appears. Type in i and
depress the OK pushbutton. An icon labeled i becomes visible in the left-hand
windowpane. Depress the “Run to Cursor” icon in the debug toolbar. The active
line advances to the first breakpoint. Now identify the upper left-hand icon in
the debug toolbar labeled “Next Step” and select this icon repeatedly. The active
line, marked in blue, will move through the program synchronously as the value
of the variable i is seen to change. When the position of the error due to the
invalid division by zero is reached, the active line cannot be further updated. Of
course, in this example the error can also be identified from the data displayed
on the terminal through the cout lines. For future reference, the Step Into icon
is employed to enter a function. That is, to step through lines within a function
(change the scope to the function), the Step Into icon should be selected once the
active line has been located at the position of the function call.
3.4 A first graphics program 19
Now select the tab entitled “Directories” and then the subtab “C++ Includes”. At the
bottom of the list of directories enter
X:\dislin
present for DISLIN to function properly on a C++ file. Now select the third
“Compile and Run” icon from the left on the bottom button bar. Type in a
suitable name for the file (again do not enter the .cpp extension). A graph of the
two points should appear.
You can generate, among many other options, a TIFF, Adobe PDF or postscript
file in place of the screen plot by placing one of the lines
metafl(''TIFF'');
metafl(''PDF'');
metafl(''POST'');
respectively, into the main( ) program before the line containing qplot. A file
named dislin.xxx, where xxx is respectively tif, pdf or eps, is then placed in your
directory when the program is executed. If e.g. an .eps file dislin.eps already
exists, the new .eps file will instead be dislin_1.eps, etc.
The contents of any window can also be printed by clicking the left mouse
button anywhere inside the window to make it active and subsequently depressing
the Print Screen key while holding down the ALT key. (Using the CTRL key
instead of the ALT key instead captures the contents of the entire screen.) You can
then open an application program that accepts graphics such as Paint (Start →
Programs → Accessories → Paint) or an appropriate word processor and select
Edit->Paste from its menu bar to insert a bitmap of the captured window that can
subsequently be printed through the application program’s print function.
3.6 Example
The following code calculates the magnitude of the gravitational field of a point
particle both inside and outside the Earth and displays the result as a contour
plot, a color graph or a three-dimensional line plot:
#include <iostream.h>
#include “dislin.h”
const double KM = 1000;
const double GRAVITATIONALCONSTANT = 6.67e-11;
const double EARTHMASS = 5.97e24;
const double EARTHRADIUS = 6380 * KM;
const int MATSIZE = 20; // Must be a const int
double gravitationalField( double aX, double aY ) {
double radius = sqrt( aX * aX + aY * aY )
if ( radius <= EARTHRADIUS )
return GRAVITATIONALCONSTANT *
3.6 Example 21
EARTHMASS * radius /
( EARTHRADIUS * EARTHRADIUS * EARTHRADIUS );
else
return GRAVITATIONALCONSTANT * EARTHMASS /
( aX * aX + aY * aY );
}
main ( ) {
double position[matSize]; // x and y coordinate positions
float field[matSize][matSize]; // gravitational field
float offset = matSize / 2 - 0.5; // starting grid point
for ( int loop = 0; loop < matSize; loop++ ) {
position[loop] = 0.1 * earthRadius * (loop - offset);
}
float x, y;
for (int outerLoop = 0; outerLoop < matSize; outerLoop++) {
x = position[outerLoop];
for ( int innerLoop = 0; innerLoop < matSize; innerLoop++ ) {
y = position[innerLoop];
field[outerLoop][innerLoop] = gravitationalField( x, y );
}
}
metafl( “XWIN” );
disini( ); // required for 3-dimensional plots
int iPlot = 2; // set to 1 for surface plot, 2 for color plot.
if ( iPlot == 1 ) // surface plot
qplsur( (float*) field, matSize, matSize );
else if (iPlot == 2) // color plot
qplclr( (float*) field, matSize, matSize );
else { // contour plot
int numberOfContours = 30;
qplcon( (float*) field, matSize, matSize, numberOfContours );
}
}
Note that DISLIN routines require float arrays as data arguments. The syntax
(float *) casts (converts) the subsequent variable to a float array (more precisely,
pointer) type.
Chapter 4
Introduction to computer and
software architecture
22
4.2 Hardware architecture 23
Calculator MyCalculator;
MyCalculator.inputValue( 0.5 );
MyCalculator.depressSineButton( );
MyCalculator.displayValue( );
is far shorter, the object construct organizes the properties and functions of
a physical object into a single self-contained unit that is easily modified and
transferred between programs. Object-oriented languages additionally provide a
foundation for still higher-level programming idioms such as graphical program-
ming, for which right-clicking on a calculator icon reveals a list of its attributes,
which in this case could be the value, calculatorValue, held in the calcula-
tor’s internal memory register and the inputValue( ), depressSineButton( ) and
displayValue( ) functions. Other icons represent objects or graphical user-
interface parts that obtain data from or display data to the user. Drawing a line
between two objects enables one object to call a function in the second object.
26
5.5 Constant and variable types 27
A statement is a control point in that the compiler analyzes a statement only after
all previous statements have been processed.
int m = 1;
m = m + 1;
requires the keyword int to indicate that the storage space corresponding to m
stores integer values so that the compiler can establish that 1 can be meaningfully
inserted into this memory space. Removing the int keyword yields a compile-time
error.
The most frequently occurring variable types can be distinguished first by
the number of bytes each employs for storage. Here a byte refers to a unit
consisting of eight binary memory values or bits that are either zero or one.
Therefore, a byte can possess 28 = 256 different values. The number of bytes
reserved for variable types such as int depends on the computer and compiler.
In earlier versions of C++ created for 16-bit machines, an int occupies two
bytes, limiting the number of possible int values to 65,536. (The actual stor-
age size of a variable m in bytes can be determined at runtime by calling
sizeof( m ).) In a 32-bit machine, memory is accessed through a bus of 32
wires that simultaneously send or retrieve four bytes and therefore can (opti-
mally) address 232 = 4,294,967,296 different memory locations. An int variable
is conveniently stored in four bytes on such machines and can then acquire 232 =
4,294,967,296 possible values that are mapped to integers from −2,147,483,647
to 2,147,483,648. Although an integer between these limits is represented exactly,
incrementing the largest allowable int by one generates the smallest allowable
int, while decrementing the smallest allowable int by one similarly yields the
largest int.
Floating-point numbers are represented in scientific notation with an accuracy
of approximately 7 digits for a float and 14 digits for a double. The first bits in the
variable’s memory store the mantissa (the 7 or 14 significant digits) and the last
few bits store the exponent. Such a representation, unlike that of an int, is inexact
but spans a large range of magnitudes, up to ≈10±38 for a float and ≈10±308 for a
double (the exact values can be found by including the header file <float.h>
(or <limits>) and introducing the statement cout << FLT_MAX << '' '' <<
DBL_MAX << endl;). In most C++ compilers both float and double reserve
eight bytes of storage space. Consequently the double keyword is generally pre-
ferred because float variables are often in any case inefficiently stored as double
values after setting the least-significant bits to zero. A double constant is distin-
guished through use of the decimal point and can also be written in exponential
notation by appending the suffix e or E followed by the mantissa. That is, 3e-1
represents the same double constant as 3.0e-1 or 0.3. Twice the normal amount
of memory is allocated to an int or double variable if its definition statement is
prefixed with the long keyword.
5.5 Constant and variable types 29
Table 5.1
A char stores a single byte of data that is interpreted at runtime as the code
for a single character (letter). That is,
char aC = 'a';
Standard single-byte char variables can store 28 = 256 possible values that cor-
respond to the elements of the ASCII character set. The first 32 ASCII characters
are exclusively non-printing control characters such as backspace, bell, tab, etc.
The most important ASCII values are shown in Table 5.1. Thus
char c;
cout << (c = 65) << (c = 10) << (c = 97) << endl ;
A
a
A character such as 'a' should not be confused with the character string "a",
which is a two-element array consisting of the single character 'a' followed by a
byte with all of its component bits equal to zero termed the null character. The
presence of a null character enables functions of string arguments to determine
when a string terminates and thus to stop processing its bit pattern. As noted
30 Fundamental concepts
int M = 3;
Since M = 10; is not a definition, unlike double n = 6.0;, a new memory location
is not allocated for M within the innermost block.
A variable such as M in the program above that is defined outside all blocks
(and is conventionally capitalized) is termed a global variable. Global variables,
even if hidden by another variable of the same name, can be accessed throughout
the program through the syntax ::M, where :: is termed the scope-resolution
operator.
To summarize, suppose an outer region R contains an inner block B. Then the
following conditions apply.
whereas a binary operator resembles an implied function for which one argument
is located on the left of the operator symbol and the second on the right.
Recall now that in the arithmetic expression 2 * 3 + 4 multiplication is
performed before addition unless parentheses are employed to indicate a different
order of operations, as in 2 * (3 + 4), since the unary parentheses operator,
which acts by evaluating the expression it encloses, is always applied before any
arithmetic operators. This relative ordering of operations is termed precedence.
Accordingly, the parenthesis operation possesses a higher level of precedence
than the * and / operators, which in turn occupy a level above + and –. While 13
levels of precedence exist in C++, the basic structure is summarized by
const int m = 3;
m = 4; // Compilation error
5.12 Comments 33
(1) Employ descriptive names. For non-object variables and functions, capitalize all
words with the exception of the first, as in numberOfGridPoints.
(2) Place spaces to the right and left of binary operators, but not unary operators, e.g.
m = n + –1.0;.
(3) Insert spaces after commas, the opening delimiters ( and { and before the closing
delimiters } and ) as in myFunction( int aI, int aJ );, except for array indices.
(4) Indent each successive enclosed block by one further tab stop.
(5) Enclose segments of code that perform related functions with blank lines.
(6) Begin the names of function arguments with a small a.
(7) Capitalize names of classes, structures and objects.
(8) Begin names of internal class variables with a small i.
(9) Begin names of boolean variables with is and boolean functions with enable.
(10) Capitalize all letters of global constants.
5.12 Comments
A non-trivial program cannot be easily understood unless adequately commented
and accompanied by a separate program description. To enable comments, the
C++ compiler does not process text to the right of the delimiter // or between the
delimiters /* and */. However, while the second procedure facilitates the removal
of large blocks of code from compilation, if two non-adjacent segments of code
are each enclosed in such delimiters and the line containing the first end delimiter
*/ is deleted by mistake, all code from the first occurrence of /* to the single
remaining delimiter */ is ignored, yielding unexpected errors.
As a rule, a program should commence with “prolog“ lines specifying a
descriptive title, the revision number, author, revision date and program objective.
Each section of code should be preceded by comment lines that explain its
purpose. The interpretation of each significant statement should be placed either
directly above or to the right of the line. Comments can contain data values that
can be employed as test cases during debugging. To illustrate,
// comment.cpp
// Revision 1.0
34 Fundamental concepts
Superfluous comments are, however, suppressed in the remainder of this text for
space reasons.
Chapter 6
Procedural programming basics
The following three chapters introduce basic C++ program structure and syntax
in the context first of procedural programming and subsequently, in the two
later chapters, of object-oriented programming. The material in these chapters
addresses the significant challenges encountered by beginning programmers.
35
36 Procedural programming basics
main( )
{
... statements ...
}
which can appear anywhere in the source file (program) after the external con-
structs appearing in main( ) are declared. In the first of the two above implemen-
tations, return 0; or, equivalently in DEV-C++, return EXIT_SUCCESS; (the
global constant EXIT_SUCCESS equals zero) should preferably immediately
precede the closing brace.
6.3 Namespaces 37
6.3 Namespaces
C++ can group program elements into separate namespaces such that e.g. a
function or variable in a namespace A is referred to from outside the namespace
by appending a prefix A:: before its name. The resulting ability to segregate code
into non-interacting code segments facilitates library reuse. To illustrate:
namespace A {
int M = 1;
}
namespace B {
int M = 2;
}
using namespace std;
main( ) {
int M = 3;
cout << A::M << '' '' << B::M << '' '' << M << endl; // Output : 1 2 3
}
namespace A {
int M = 1;
}
namespace B {
int N = 2;
}
using namespace A;
using namespace std;
main( )
{
{
using namespace B;
cout << M << '' '' << N << '' '' << endl; // Output : 1 2
}
}
The using namespace B is effective only inside its containing block, while the
A and std namespaces are present from their using directives to the end of the
program. If the using namespace std; statement is omitted in the above pro-
gram, each element of the include files, such as cout, cin, exp( ), etc., must be
individually prefixed with std:: to indicate its membership in the std names-
pace. However, if #include <iostream> is replaced by the antiquated #include
<iostream.h> directive, which does not enclose definitions in a namespace, std::
must be omitted.
38 Procedural programming basics
#include “includeFile”
which instructs the compiler to search for and incorporate a file named include-
File first in the directory from which the program is being run and then, for most
compilers, in the include-file subdirectory(s) of the compiler’s main directory.
Alternatively, writing
#include <includeFile>
#ifndef_MYFLAG
#define_MYFLAG
6.5 Arithmetic and logical operators 39
int m;
... additional definitions and code statements
#endif
will be compiled only once even if it is included into a given program multiple
times.
m = 4, and n = 5, while
int n = 4;
m = ++n;
or, equivalently,
enum suites { HEARTS, SPADES, CLUBS = 4, DIAMONDS } mySuite;
mySuite can be set to any one of the four literal values HEARTS, SPADES,
CLUBS or DIAMONDS. The values HEARTS, SPADES, CLUBS and DIA-
MONDS are then numerically equivalent to 0, 1, 4 and 5, respectively, as evi-
denced when a suites variable containing one of the literal values is converted to
an int as below:
enum suites { HEARTS, SPADES, CLUBS = 4, DIAMONDS };
suites firstSuite = 3;
suites secondSuite = DIAMONDS;
cout << firstSuite << '\t' << secondSuite << endl;
// Output : 3 5
if ( secondSuite == DIAMONDS ) cout << ''Diamond'' << endl;
// Output: Diamond
if ( A ) {
... statements ...
}
executes the block labeled statements when the logical expression A evaluates
to true.
Control constructs share several features that lead to programming errors.
If the block governed by any control construct contains a single statement, the
enclosing braces can be omitted, but the block structure is still implicitly present.
if ( A ) statement;
Although the above compound statement is often written on two lines, this
should be avoided where space permits, in order to avoid the following errors.
First, separating a control condition from the following statement by a semicolon,
if ( A ); statement; // WRONG!
although braces do not surround int i = m; the statement is still implicitly enclosed
in a separate block. Therefore m is subsequently destroyed and is unavailable to
the remainder of the program. Conversely, braces are often mistakenly omitted in
multi-line if statements. Only the first of the statements following the if statement
is then influenced by the control condition. Finally, troublesome errors occur
when the assignment operator = is employed in place of the logical equality
operator == as in the statement if ( B = C ). As a result, the value of B is
initially set to that of C and the logical statement is then evaluated with this
unintended value as an argument. Typically, C differs from zero, so that the
logical statement evaluates to true and the subsequent block is executed.
The extent of an if statement can be extended by appending an else statement
that is executed if the assertion in the if statement is false. An abbreviated form
for if ( A ) {B} else {C} is the ternary conditional operator A ? {B} : {C}. If the
else keyword is followed by an if statement, else and if are generally placed on a
single line, as in if ( !grade ) { . . . } else if ( grade == 1 ) { . . . } else { }.
Repeated else if statements can be collected into a single switch construct.
Because of the subtleties of the syntax, the following example should be consulted
during coding:
int grade = 8;
switch ( grade / 10 ) {
case ( 7 ): cout << ''C'' << endl; break;
case ( 8 ): cout << ''B'' << endl; break;
case ( 9 ): cout << ''A'' << endl; break;
default: cout << ''F'' << endl;
}
42 Procedural programming basics
The break statements in the above program transfer control to the statement
immediately following the switch block. Removing these statements leaves con-
trol within the block so that the (optional) default: statement is always executed.
A switch construct can be applied to other types of variables, as in char c = 'b';
switch( c ) { case ( 'a' ): . . . }.
for ( int loop = 0; loop < 5; loop++ ) cout << loop << ' ';
// output: 0 1 2 3 4
int loop;
for ( loop = 5; loop > 0; loop-- ) cout << loop << ' ';
// output: 4 3 2 1
cout << endl << loop << endl;
// output: 1
int loop = 0;
while ( loop < 5 ) {
cout << loop++ << '' '';
}
The break statement can be employed to exit manually from a control construct.
That is, placing
int r;
cout << ''Enter 0 to terminate '';
cin >> r;
if ( r == 0 ) break;
inside a control loop enables termination of the loop from the keyboard; the
variable r is then termed a sentinel. An alternative procedure is
int r = 1;
while ( r != 0 ) {
statements;
cout << ''Enter 0 to terminate '';
cin >> r;
}
A running program can be terminated with the exit( int aM ) function (here
#include <stdlib.h> may be required). While the function parameter, which is
44 Procedural programming basics
main( ) {
myType d1 = 3.0;
myType d2 = 4.0;
cout << d1 << '\t' << d2 << endl;
}
int m, n;
cin >> m >> n;
reads first the value of m and then that of n from the input stream. Since C++
equates single and multiple whitespace characters, two values entered from the
keyboard separated by any combination of whitespace characters such as spaces,
tabs or carriage returns (but not commas!) are stored successively in m and n.
Similarly, values are piped from the program into the standard output stream
cout that is attached to the terminal through the insertion operator <<. For
example,
or, equivalently,
displays the value of m, a tab ('\t' is called a tab character) and the value of n
and terminates with a newline character '\n'. The endl statement corresponds to
'\n' followed by flush. The keyword flush insures that the contents of the stream
buffer are displayed on the terminal, while a cout statement that does not end
with flush or endl can delay displaying the output. In this case, a program that
terminates abnormally after the output statement, but before the output appears on
the terminal, will incorrectly imply that the error occurred before the statement.
Other formatting and nonprinting characters include
'\v' \\ vertical tab
'\b' \\ backspace
'\r' \\ carriage return
'\f' \\ formfeed
'\a' \\ alert (bell)
'\?' \\ question mark
'\" \\ single quote
'\"' \\ double quote
'\\' \\ the \ character.
Here the disk file input.dat in the directory from which the program executes
is associated with a stream, fs, that can be employed for both input and output.
To limit the stream to either input or output (corresponding to cin or cout)
fstream should be replaced by ifstream or ofstream, respectively. The input file
input.dat can be created by entering the exact keystrokes including whitespace
characters into a text or program as would be entered into the program from the
keyboard through cin.
6.14 Casts
In C++, conversion of built-in variables between closely related types occurs
automatically, as in char c = 56; in which the integer 56 is converted to the
character '8' (the 56th member of the ASCII character set). If variables of different
types that can be automatically converted into each other are combined through
a binary operator as in
46 Procedural programming basics
char c = 'a';
int m = 10;
cout << ( c + m ) << endl; // Output: 107
then the variable that occupies less memory space (in this case the char) is
converted (promoted) to the type of the other variable (an int) before the operator
is applied.
Conversions can be forced through explicit type-conversion operators (casts)
as in cout << char( m ) << endl;. Casts have several additional forms in C++.
A C-cast is written (char) m. The syntax static_cast<char>( m ) forces the
conversion of m from an int to a char at compile time. Casts with system-
dependent behavior and those that remove the const property of variables are seg-
regated into calls to reinterpret_cast<typename> and const_cast<typename>.
Finally, dynamic_cast<typename> reserves the implementation of the cast until
runtime.
6.15 Functions
A C++ function acts on a set of input variables and returns zero or one output
variables. Variables defined within a function are normally isolated from the
remainder of the program, facilitating testing, implementation and subsequent
reuse by other programs. A modular program consists almost exclusively of
functions and control structures, thus representing a physical problem as a log-
ical flow among individual program elements. Code for a function should thus
perform a single, well-defined task.
A general function can be represented as returnType myFunction( type1
aP1, type2 aP2, . . . . , typeN aPN ). The input variables aP1 . . . aPN are termed
arguments. The convention that all argument names begin with a lower-case a
greatly enhances programming clarity. The function is called (activated) through
a statement such as returnType x = myFunction( p1, p2, . . . , pN ); in which
p1, . . . , pN, which can in general be either constants or variables, are referred
to as formal parameters or more simply as parameters. A function can possess
any number (including zero) of arguments but can only return zero or one values.
The return type of a function that lacks a return value must be specified as void,
except for constructor (and destructor) functions as noted in Section 8.5. (In
many situations C++ assumes a default type of int when no type is specified;
for example, const i; is generally interpreted as const int i; while f( ) { return
5; } is compiled as int f( ) { return 5; } and is not a void function.) A function
without arguments must still be called with parentheses as in int j = f( );.
Since a function can contain numerous commands and allocate arbitrary amounts
of memory for its internal variables, memory space for the function is not auto-
matically assigned when a program is initialized, unlike space for global variables
or variables defined within main( ). Rather, the machine-language instructions
associated with the function body are written to memory. The function name in the
calling program is then associated with the 4-byte (for a 32-bit machine) memory
address of the start of this instruction set or function record, as is verified by
void print( ) { cout << ''test''; }
main ( ) {
cout << print << endl; // Note: print is not followed by ( )
}
main ( ) {
48 Procedural programming basics
int m = 3;
change ( m );
cout << m << endl; // output 3
}
main ( ) {
int m = 3;
change ( m );
cout << m << endl; // output 3
}
Errors can therefore arise if this function is called with a double argument
expecting that a double rather than an int will be returned. Unless the absolute
value of the difference between a and b exceeds 1 below, the abs function will in
such cases return 0, and the logical condition in the if statement argument then
evaluates to true:
#include <math.h>
50 Procedural programming basics
#include <assert.h>
main ( ) {
int m = 1;
assert ( m == 2 ); // test for an error condition
}
For production runs, the preprocessor directive #define NDEBUG precludes the
compiler from processing all subsequent assert statements.
Preconditions and postconditions can also be implemented through try and
catch blocks. When a throw statement is invoked within a try block, program
control passes to the catch block with the closest valid match to the arguments of
the try block. After the catch block has terminated, the program resumes unless
an exit statement is encountered inside the catch block. A variable defined within
a try block is destroyed when the block is exited. To insure in this manner that
the input to a function checkSquare( double aT ) is positive while its output is
≤100:
char get( ) {
int m;
cout << ''Insert a value'' << endl;
cin >> m;
if ( m == 1 ) return 'y';
return 'n';
}
52 Procedural programming basics
main ( ) {
myFunction( 3 ); // output: 3 1
myFunction( 3, 2 ); // output: 3 2
}
void myFunction( int a, int b ) { cout << a << '\t' << b << endl; }
constitutes a valid program, but the default parameter value cannot be additionally
introduced either into the first function declaration or into the function definition.
main( ) {
GLOBALI = 0;
GLOBALJ = 1;
change( );
cout << GLOBALI << '\t' << GLOBALJ << endl; // Output: 2 3
}
Such a technique, however, can lead to severe errors, since an inadvertent change
to a global variable in any section of the program propagates to all other program
units. This can yield unforeseen side effects separated by a large code distance
from the error source, impeding its identification.
6.26 Recursive functions 53
Thus, for int k = factorial( 3 );, in the first call to factorial( 3 ), temp is set to
3 * factorial( 2 ), which cannot be evaluated until factorial( 2 ) returns. How-
ever, when factorial( 2 ) executes, a new temp variable in a separate memory
space is created for 2 * factorial( 1 ). Finally factorial( 1 ) returns 1, enabling
factorial( 2 ) and then factorial( 3 ) to complete. While the overhead associated
with multiple function calls and the simultaneous allocation of memory resources
can be considerable, this is often outweighed by increased programming sim-
plicity.
As another example, the square root of 2 can be expressed by the continued
fraction
√ 2−1 1 1 1
2−1= √ = √ = √ ≈
2+1 1+ 2 2 + ( 2 − 1)
1
2+
1
2+
2 + ···
54 Procedural programming basics
main ( ) {
int maxLoop = 20;
for ( int outerLoop = 1; outerLoop < maxLoop; outerLoop++ ){
double root = 0;
for ( int loop = 0; loop < outLoop; loop++ ){
root = 1. / ( 2. + root );
}
cout << 1. + root << endl;
}
}
int getInput( ) {
int r;
cin >> r;
return r;
}
int multiply( int a1, int a2 ) {
return a1* a2;
}
void print( int aI ) {
cout << aI << endl;
}
main ( ) {
int input1 = getInput( );
int input2 = getInput( );
int result = multiply( input1, input2 );
print ( result );
}
Such code is easily analyzed and corrected, since each function can be tested
separately.
6.28 Arrays
An array comprises an indexed set of variables of the same type. The array
definition int v[3]; reserves memory for an array of three int elements and
associates the type of v with that of an array of int elements. The content of the
storage location for the ith element is accessed by following the array name with
the index operator [ ], as in
6.29 Program errors 55
main ( ) {
int v[3];
for ( int loop = 0; loop < 3; loop++ ) v[loop] = loop;
cout << v[1] << endl; // Output: 1
}
The syntax
which can be employed only when the array is defined, initializes the first element
of v to 1, the second element to 2 and all remaining elements to 0.
main ( ) {
m = 2;
cout << m << endl;
}
where the .cpp extension indicates that the error originates from a source-code
file.
56 Procedural programming basics
Link errors. Typically, link errors result when a program attempts to call a
function or use a variable that is not defined either in the program file or in the
additional files that are linked with the program, as in the program below:
int mySquare( int ); // declares but does not define the function
main ( ) {
int m = 3;
cout << mySquare( m ) << endl;
}
The missing function definition (e.g. function body) required by the linker must
accordingly be supplied in the source code or in a second object file.
Runtime errors. The most severe mistakes arise during program execution.
For example, a program can fail because of an incorrect numerical algorithm or
improper program flow. These issues are often most easily detected by comparing
the program results with those of an analytic evaluation or of a second program
that employs a different calculational approach. Alternatively, a construct can be
syntactically correct but yield unintended results; for example, if cin >> m, n; is
employed in place of cin >> m >> n;, only the first input value is read into m.
An incorrect result or overflow or underflow condition could then be generated
with or without an accompanying error message.
Even more subtle runtime errors are associated with improper memory access.
If a program reads or writes to a memory address beyond the space allocated to the
program through, for example, an array index equal to or larger than the number
of elements allocated to the array, the operating system often intercepts the illegal
access. In this case, the program is terminated and a system error message such
as “segmentation fault” is displayed inside a pop-up window (i.e. the program
is addressing memory outside the segment permitted by the operating system).
However, if the array variable accesses a memory location of another variable in
the program, seemingly random errors occur when the program is executed with
different input values.
in the <float.h> header file. In contrast, the overflow and underflow thresholds
correspond to the largest and smallest number that each data type can represent,
namely 10±38 for a float (FLT_MIN and FLT_MAX) and 10±308 for a double
(DBL_MIN and DBL_MAX). In this context, it should be noted that a number
that exceeds the overflow bound or is obtained by dividing zero by itself is repre-
sented by a symbolic value such as Inf or NaN, which subsequently propagates
through the calculation according to rules such as 1 / Inf = 0 and 2 * NaN =
NaN.
While the roundoff error of a single calculation is approximated by εm , the
signs of these terms fluctuate if the calculation is repeated N times. Accordingly,
the combined error describes a random walk as a function of N, yielding on
average a total error of N 1/2 εm . However, the relative error in the difference of
two nearly equal quantities, such as a function computed at two closely sepa-
rated points, with different roundoff errors, C(1 + δ + εm ) and C(1 + εm′ ), where
√
ε ≪ δ ≪ 1, is C(ε′ − ε)/(Cδ) ≈ 2 ε/δ ≫ ε. Accordingly the number of signifi-
cant digits in the result that propagates to subsequent calculations is reduced by
≈ −log10 δ . Such cancellations, which occur, for example, in the numerical deriva-
√
tives of nearly constant functions and in the quadratic formula for b ≈ b2 − 4ac,
yield significant errors and must be carefully avoided.
A more serious issue relates to the stability of a numerical algorithm. Consider
the computation of the path of a ball starting at x = y = z = 0 and rolling in the
z-direction along the line given by the maximum of the parabolic surface defined
by y = −x 2 for all values of z. Any numerical error increases with time, leading to
rapidly increasing or decreasing values of x. Numerical methods that respond in a
similar fashion to rounding errors or numerical fluctuations are termed unstable
and yield intrinsically unreliable results unless the accumulated effect of the error
terms is carefully analyzed and limited.
Chapter 7
An introduction to object-oriented
analysis
where the initial position and velocity are stored in the first elements of the 50-
component arrays position[50] and velocity[50]. The propagate( ) routine cal-
culates subsequent positions and velocities by advancing time over numberOf-
Steps time steps of duration timeIncrement. Finally, the arrays are graphed by
the plot( ) routine.
While procedural programming generally proves optimal for small program-
ming projects, difficulties are encountered when applied to complex systems.
First, a program that takes into account all physically realizable situations
58
7.1 Procedural versus object-oriented 59
Several springs with given load masses and spring constants are extended and
released from a launcher. The trajectory is recorded and then plotted by a
detector.
The italicized nouns in the description then map to physical or abstract objects,
while the underlined nouns and verbs are, respectively, the attributes and actions
(e.g. variables or functions) associated with these objects.
The implementation of time presents significant subtleties. To model physical
time evolution, every object should in principle experience time independently
so that e.g. the spring oscillates and the detector periodically records the trajec-
tory by polling the internal computer clock. Unfortunately, code in which two or
more processes or “threads” access the CPU requires complex, “multithreading”
facilities to manage e.g. resource contention and time-sharing among decoupled
processes. Therefore generally the unique main( ) program sequences time evo-
lution. Alternatively, a “time-server” object can be responsible for sequencing
interactions or different objects can control sequencing during disparate periods
of execution.
7.5 Classes and objects 61
When more than four springs are activated by the spring launcher, an error
message will be displayed and the spring launcher will cease to function.
(1) The problem definition and requirements specification are summarized by use cases
that depict the general manner in which the system components interact with the
external users (actors) and with each other.
(2) Sequence and/or collaboration diagrams establish the order in which functions are
called (messages are sent) by the objects that comprise the system.
(3) A class diagram incorporates the objects and functions identified in the
sequence/collaboration diagram into distinct classes.
(4) The class diagram is converted to C++ skeleton code by selecting a menu item.
Figure 7.1
certain variables (properties) that characterize its state and, finally, a set of spe-
cific functions (behaviors). A class describes a group of all objects that share
common properties and behaviors and provides a form (template) for the creation
(instantiation) of these objects.
Normally the internal variables of a physical object are inaccessible except to
the object. For example, the elements of a person’s state such as his degree of
thirst remain private, that is, accessible only to himself. Other people or objects
can access these variables only by asking questions, i.e. directing messages to his
public interface. Similarly, class components are private by default and can then
be accessed or changed only by calling one of the public functions belonging to
the class. Internal (private) variables can thus be thought of abstractly as located
in an inaccessible core region, while public functions that govern the interaction
of the outside world with the variables (the object’s state) are situated in a user-
accessible shell surrounding this region. Thus, a LoadedMetalSpring class for
a spring with a certain spring constant that ages with time can be represented
schematically as in Figure 7.1, in which internal variables with get and set
functions can be externally viewed and altered, respectively.
7.6 Object discovery 63
Class LoadedSpring
Variables:
iLoadMass
iPosition
iVelocity
Class SpringLauncher
Variables: Functions:
iNumberOfSpringsToLaunch launch( )
Class Trajectory
Variables:
iPosition[ ]
iVelocity[ ]
Class Detector
Variables: Functions:
iSpringTrajectory record( )
plot( )
Class PropagationRoutines
Variables: Functions:
iSpring propagate( )
iSpringTrajectory[ ]
Class SpringSystem
Variables: Functions:
7.7 Inheritance 65
iLoadMass launch( )
iPosition[ ] propagate( )
iVelocity[ ]
7.7 Inheritance
Inheritance enables specialized classes, termed derived classes, to be assem-
bled from preexisting, more generic base classes. A derived class acquires the
attributes and behaviors (internal variables and functions) of its base classes
except for those that are added or explicitly redefined. Semantically, a derived
class exhibits an “is-a” or a “kind-of” relationship to its base classes, as opposed to
the “has-a” relationship that exists when a class contains an object of a second
class as a member variable. Classes can inherit through several levels of derived
classes. Elements that are shared by several derived classes should then reside at
the highest applicable level of the inheritance tree.
To illustrate, consider the relationship between LoadedSpring and Load-
edMetalSpring classes. If the latter class merely adds an age( ) function to
the iPosition, iMass and iVelocity variables of the former class, it can acquire
these variables through inheritance. In public inheritance, public variables of the
base class remain public in the derived class and can be directly accessed any-
where in the program. Private inheritance, in contrast, converts all inherited
elements of the base class into private elements that cannot be accessed from
outside the derived class. Protected inheritance converts base class elements into
variables that are visible to and therefore can be inherited from the derived class
by further derived class levels but are not accessible elsewhere in the program.
Chapter 8
C++ object-oriented programming syntax
class Trajectory;
Once a class has been declared, the class name becomes functionally equivalent
to any built-in type identifier such as int or float. Therefore, a compiler can
subsequently resolve the function declaration:
but has not yet been informed of the nature of the class components. Thus the
second of the two statements below generates a compiler error:
Trajectory T;
T.plot( ); // Error: T.plot( ) not defined
66
8.2 Class definition and member functions 67
which the function body is not specified is (the semicolons in void plot( ); and
at the end of the class definition are both required)
class TrajectoryPlotter {
public:
float iPosition[100], iVelocity[100];
int iNumberOfPoints;
void plot( ); // This statement cannot be repeated!
};
The statement void plot( ); cannot be repeated within the class definition, since
at each occurrence the compiler reserves memory for an address of a function
record. Repeating this statement would therefore incorrectly store this address in
two locations.
The body (definition) of the plot( ) function must subsequently be supplied
once at global scope after the class declaration. The function prototype must be
preceded by the class name TrajectoryPlotter followed by the scope-resolution
operator :: which indicates that this plot( ) function belongs to the Trajectory-
Plotter class, i.e.
void TrajectoryPlotter::plot( ) {
metafl(''XWIN'');
qplot(iPosition, iVelocity, iNumberOfPoints);
}
Supplying the body of the function within the class definition instead leads to (a
semicolon is now not required after the plot( ) function body)
class TrajectoryPlotter {
public:
int iNumberOfPoints;
float iPosition[100], iVelocity[100];
void plot( ) {
metafl(''XWIN'');
qplot(iPosition, iVelocity, iNumberOfPoints);
}
};
class Second;
class First;
// The compiler now knows Second and First are classes
class First {
public:
int iFirst;
// so it can resolve Second here
void print ( Second aSecond );
};
class Second {
public:
int iSecond;
// and it can resolve First here
void print ( First aFirst );
};
// The compiler is now aware of First’s and Second’s members
A very common error in writing code for classes occurs when one or more
internal member variables are redefined within a member function as below:
class C {
int iC;
public:
void setI (int aI ) {
int iC = aI; // Error!
}
};
The new variable iC hides the internal class member variable of the same name,
so that calls to setI( ) do not affect the internal member variable, which remains
uninitialized.
syntax for the creation (instantiation) of objects coincides with that for built-in
variables, for example,
TrajectoryPlotter TP1;
TrajectoryPlotter TP2;
generates two objects of type TrajectoryPlotter. Since these statements are type
declarations and do not call functions, parentheses do not appear after TP1
and TP2 in the above statements. In the same manner as that in which int m;
assigns a random value to m, the values of all internal variables in TP1 and TP2
above are random. For public variables, to replace the random bit pattern with a
meaningful value, the member-of operator, ., which selects an element from the
class with the name specified after the period, can be employed. Thus, values are
assigned to the first two position variables, the first two velocity variables and
the iNumberOfPoints variable in TP1 through
main ( ) {
TrajectoryPlotter TP1;
TrajectoryPlotter TP2;
TP1.iNumberOfPoints = 2;
TP1.iPosition[0] = TP1.iVelocity[0] = 0;
TP1.iPosition[1] = TP1.iVelocity[1] = 1;
TP1.plot( );
}
While this code is placed inside main( ), it could equally well be located in any
other function. The first two lines can also appear in the global space outside all
function bodies after the TrajectoryPlotter class definition.
Classes implement polymorphism in a transparent manner because the behav-
ior of a function is bound to the type of the calling object. For example, suppose
the plot( ) functions in the Circle and Ellipse classes plot a circle and an ellipse,
respectively. Then, Circle C1; C1.plot( ); draws a circle, while Ellipse E1;
E1.plot( ); instead draws an ellipse. This facility parallels real-world behavior,
leading to concise and understandable code.
An object that contains only public members (and does not possess a user-
defined constructor) can be conceptualized as a generalized array in which, unlike
in a standard array, elements can belong to different types. These are accessed
through the object name followed by the element name (possibly followed by an
array index) instead of through an array index as in a standard array. Consistently
with this paradigm, internal object variables can be assigned values at the point
of definition following the same syntax as arrays. Thus the iNumberOfPoints
variable and variables iPosition[0] and iPosition[1] of TP2 are set to 2, 0 and
1, respectively, while all other variables are set to zero at the point of definition
through the statement
TrajectoryPlotter TP2 = { 2, 0, 1 }
70 C++ object-oriented programming
main( ) {
C C1, C2 = { 1, 2 };
C1 = C2;
cout << C1.iC[1] << endl; // Output: 2
}
int iPrivateReadOnly;
public:
int privateReadOnly( ) { return iPrivateReadOnly; }
};
main( ) {
InformationHidingExample IHE1;
IHE1.setPrivateReadWrite( 4 );
cout << IHE1.privateReadWrite( ) << endl // output: 4
cout << IHE1.privateReadOnly( ) << endl; // output: 16
}
8.5 Constructors
Now consider the private read-only internal variable iPrivateReadOnly in the
program of the previous section. Since this variable cannot be changed from
outside the class, when an InformationHidingExample object is created, the
variable contains a random value that can be accessed only by member functions
72 C++ object-oriented programming
within the class body. While this procedure is valid in Java, C++ requires that
internal member variables be initialized within constructor functions. These can
possess any number of arguments, including zero, and are called at most once by
each object at the point of definition. While e.g. print statements can be included
in a constructor to facilitate debugging, in the final version of the program, a
constructor should be used only for its intended purpose, namely to allocate and
initialize class objects (or possibly for type conversion, as will be explained in
Section 14.8).
A zero-argument constructor (which can equally well be a multiple-argument
constructor for which default values are specified for all arguments) is labeled
a default constructor and is invoked by a standard definition statement
as in
InformationHidingExample I1;
Note the absence of parentheses after the object name. C++ supplies a default
constructor, which generally assigns random values to all internal class vari-
ables, only in the case that no user-defined constructors are present. A class
can contain any number of additional constructors, each of which must possess
a unique sequence of arguments, differing in number and/or type. Since con-
structors are employed only to define objects, they do not possess a return
type and are named identically to the enclosing class, which is consistent
with the syntax of a definition statement, which similarly employs the class
identifier.
Two non-equivalent options exist for initializing an internal variable in the
body of a constructor function. In a zero-argument default constructor, both
internal variables in the InformationHidingExample class can be initialized to
zero, either within the constructor body,
class InformationHidingExample {
...
InformationHidingExample( ) {
iPrivateReadOnly = iPrivateReadWrite = 0;
} // Default constructor
...
};
or prior to the execution of the code in the constructor body through an initial-
ization list:
class InformationHidingExample {
...
8.6 Examples 73
InformationHidingExample( ) : iPrivateReadOnly( 0 ),
iPrivateReadWrite( 0 ) { } // Default constructor
...
};
The initialization list, which is processed before the class object is actually
constructed, must be employed to initialize e.g. const internal variables, since, if
the object were constructed prior to initialization, these variables would contain
random bit patterns that could not subsequently be changed in the constructor
body.
A two-argument constructor could take the form
8.6 Examples
As a first object-oriented program, the following code defines a class Rectangle
with two private double precision members iLength and iWidth, public set and
get member functions for these two members, a two-argument constructor that
sets the length and width to any two user-specified values and a void function
area( ) that computes and then prints the area. Subsequently, the main function
creates a Rectangle with a length of 10 and width of 20 and displays its area:
#include <iostream.h>
class Rectangle {
public:
Rectangle( double aLength, double aWidth ) :
iLength( aLength ), iWidth( aWidth ) { }
double length( ) { return iLength; }
74 C++ object-oriented programming
The second program defines a Vector class that encapsulates (wrappers) double
arrays to enable bounds checking. The private member variables of Vector are
the double array iArray[1000] and the integer iArraySize, indicating the number
of actual stored array elements. The public class functions called by main( ) are
as follows.
}
int getArraySize( ) { return iArraySize; }
void addLastElement( double aArrayElement ) {
if ( iArraySize == 1000 ) exit( 0 );
iArray[iArraySize++] = aArrayElement;
}
void changeArrayElement( int aPosition, double aElementValue ) {
if ( aPosition < 0 aPosition > iArraySize - 1 ) exit( 0 );
iArray[aPosition] = aElementValue;
}
void printArray( ) {
for (int loop = 0; loop < iArraySize; loop++ ) {
cout << iArray[loop] << endl;
}
}
private:
double iArray[1000];
int iArraySize;
};
main( ) {
double a[100] = { 1, 2, 4, 9 };
Vector V1( a, 4 );
V1.addLastElement( 10 );
cout << V1.getArraySize( ) << '' '' << V1.getArrayElement( 4 ) << endl;
V1.printArray( );
}
into a class, functions related to plotting are packaged together with the data that
they process. That is, the arrays x and y, together with numberOfPoints, are
converted into internal class members. A constructor is supplied to transfer data
76 C++ object-oriented programming
into the internal variables and a draw( ) function introduced to plot these values
as follows:
class Graph { public:
int iNPoints;
float iX[1000], iY[1000];
Graph( float aX[ ], float aY[ ], int aN ) : iNPoints( aN ) {
for ( int loop = 0; loop < iNPoints; loop++ ) {
iX[loop] = aX[loop];
iY[loop] = aY[loop];
}
}
void draw( ) {
metafl( ''XWIN'' );
qplot( iX, iY, iNPoints );
}
};
main ( ) {
float x[2] = { 0, 1 }, y[2] = { 1, 2 };
Graph G1( x, y, 2 );
y[1] = 3;
Graph G2( x, y, 2 );
G1.draw( );
G2.draw( );
}
In contrast to the procedural case, two Graph objects rather than four disjoint
arrays are required in order to store two sets of position and velocity data.
8.8 Inheritance
As noted earlier, in object-oriented programming functionality in one class shared
by a second, derived, class can be automatically incorporated into the derived
class through inheritance. However, while a derived class inherits all the non-
private variables and functions of its base class that are not explicitly redefined
in the derived class, the access privileges of the class members can be altered
depending on the form of inheritance. If a class D inherits from a class C through
public inheritance, the access privileges of all public and protected elements in
the base class are preserved in the derived class unless an element is explicitly
redefined in the derived class with a different access privilege. A less commonly
employed form of inheritance is private inheritance, in which all public members
of class C become private members of class D. Public inheritance is implemented
by commencing the definition of class D with class D : public C { . . . To recall
8.8 Inheritance 77
this syntax, observe that, since the elements of the base class C are constructed
before those of the derived class D, C forms a type of initialization list for D.
A derived class constructor constructs its base class components by calling the
default base class constructor before the derived class properties are constructed.
A user-defined base class constructor can be employed in place of the default
constructor but then must be placed in the initialization list of the derived class
constructor. If the base class lacks a default constructor, a non-default base class
constructor must be present in every derived class as illustrated below:
class D {
public:
int iD;
D( int aI ) : iD( aI ) { } // Note: default constructor absent
};
class C : public D {
public:
C( D aD ) { iD = aD.iD; } // Error: initialization list required
C( D aD ) : D( aD.iD ) { } // OK
};
main( ) {
D D1( 1 );
C C1( D1 );
cout << C1.iD << endl; // Output: 1
}
Since a derived class is a specialized form of the base class, a derived class
object is by definition also a base class object. That is, suppose a derived class
“AirFilter” is a derived class of the base class “Filter”. Clearly an AirFilter is a
particular type of Filter, therefore it can be employed anywhere in the program
where a Filter is expected. However, except as discussed in Chapter 14, the object
then behaves as a Filter, rather than an AirFilter, as illustrated by the following
generic example:
class C {
public:
void print ( ) { cout << ''C '';}
};
class D : public C {
public:
void print ( ) { cout << ''D '';}
};
main( ) {
C C1;
D D1;
C CArray[2];
CArray[0] = C1;
CArray[1] = D1; // Valid: D1 is also a C object!
D1.print( ); // Output: D
CArray[1].print( ); // Output: C
}
78 C++ object-oriented programming
//printsquare.h
void printSquare( int );
//printFourthPower.h
void printFourthPower( int );
Next, the function definitions corresponding to each header file and the main( )
program are each placed into three different code files with .cpp extensions by
80 C++ object-oriented programming
repeating the above steps except for saving the files as a C++ source file instead
of a header file. Each .cpp file can include any number of the above header files,
in particular
//square.cpp
#include ''square.h''
int SquareCalculator::calculate( ){ return iValue * iValue; }
SquareCalculator::SquareCalculator( int aValue ) :
iValue( aValue ) { }
//printsquare.cpp
#include <iostream>
#include ''square.h''
using namespace std;
void printSquare ( int aM ) {
SquareCalculator SC( aM );
cout << SC.calculate( ) << endl;
}
//printfourthpower.cpp
#include <iostream>
#include ''square.h''
using namespace std;
void printFourthPower ( int aM ) {
SquareCalculator SC( aM );
cout << SC.calculate( ) * SC.calculate( ) << endl;
}
main ( ) {
int m = 4;
printSquare( m );
printFourthPower( m );
system( ''PAUSE'' );
return EXIT_SUCCESS;
}
The double apostrophes surrounding the name of an include file direct the com-
piler to search for the header file first in the user’s current directory and afterwards
in the compiler’s include file subdirectories. Finally, depress the “Compile and
Run” button to compile and run the project. This invisibly writes compilation and
linker commands into a text makefile entitled Makefile.win that can be directly
inspected in the program directory. The makefile determines which source-code
files require recompilation so that only changed files and any new required files
are recompiled by the “Compile and Run” command.
In multifile programs, typedef, enum, const and inline definitions are local
to the file in which they reside and therefore should be placed in a header file
8.11 const member functions 81
that is included in each component file in which they are employed. A non-
const variable defined in one file and accessed in a second file must be declared
in the second file with matching type through the extern keyword (without an
initializer, which would convert the declaration into a definition). For example, if
we place a global variable int M = 10; in the file mysquare.cpp in the program
above, it can be accessed in square.cpp only if the declaration extern int M; is
present in this file. If the extern declaration statement appears inside a block in
a given file, the variable M acquires the scope of the block.
class C {
public:
int iM;
void print( const aN ) const { cout << iM * aN << endl; }
};
The compiler would then generate an error message if the print( ) function alters
either its argument aN or the internal variable iM of the C class. A function
that cannot alter internal class members is termed a const member function. Get
member functions should preferably be coded as const member functions.
If a const member function is overloaded by a non-const member func-
tion with the same name, the const member function is called if the object
that invokes the function is declared const (indicating that its internal data
members cannot be changed), otherwise the non-const member function is
activated:
class C {
public:
int iI;
void print( const int aJ ) const { cout << iI * aJ << endl; }
void print( const int aJ ) { cout << ++iI * aJ << endl; }
};
main( ) {
const C C1 = { 1 };
C1.print( 1 ); // Output: 1
82 C++ object-oriented programming
C C2 = { 1 };
C2.print( 1 ); // Output: 2
}
If the first print( ) function is not present, a warning message is generated by the
Dev-C++ compiler and the output 2 2 is obtained. While the const keyword is
generally omitted below for brevity, its use can significantly reduce inadvertent
programming errors.
Chapter 9
Arrays and matrices
In this and the following chapter, arrays, references and pointers are examined.
These will prove essential to a more detailed discussion of objects and classes.
83
84 Arrays and matrices
changed at runtime, the array size must be an expression formed from positive
integers and const int variables, such as
const int m = 3;
const int n = 4;
double a[2 * m * n]; // 24-element double array
The uninitialized array elements above contain random values. Were the array
size not a const int it could be altered during program execution, as in
int m = 3;
cin >> m;
double a[m]; // compile-time error
The number of elements in brackets above must not exceed 3, the determining
array size specified in the definition statement. If it is less than 3, the final
elements are set to zero. Hence all elements in an array can be assigned zero
values by
int a[3] = { 0 };
Otherwise the array elements will contain random values that cannot subse-
quently (simply) be set to meaningful data.
The memory location displayed as the first value in the output indicates that the
name of the int array variable, a, constitutes an alias (that is, an alternative name)
for the address of the first array variable in the same manner as that in which a
function name evaluates to the starting location of its instruction record.
To access a value stored in the array, the index operator [ ] is employed so that
a[0] yields the int value stored at the starting location in memory, 0012FF84, in
the above example. Similarly, a[m], where m is an integer, addresses the int value
stored at a position shifted from the starting location by m times the size of the
memory occupied by a single element of the int array type. This implementation
of the index operator explains the zero-based index of the n-element array, a[0],
a[1], . . . , a[n − 1] as well as the requirement that all array elements possess the
same type. However, since the definition does not make reference to the array
size, the index operator can access memory locations located at any positive or
negative integer offset from a starting address. Consequently, an array can be
initialized without specifying its size, as in
int a[ ] = { 1, 2, 3 };
which initializes a to a variable of int array type and allocates memory for three
elements. That is, to be semantically precise, since a[ ] is interchangeable with
a[3] in the above statement, the type of a in both cases is that of an int array, to
which memory for three elements has been allocated, rather than a three-element
int array.
Except in a definition statement where the purpose of the array index is not to
address an array element but to determine the extent of memory allocation, the
index of an array can be any const or non-const integer expression such as
However, such expressions must not evaluate to values beyond the array bounds
(i.e. the limits of the memory allocated to the array).
A frequently occurring error is to assign an array to a second array,
When an array index equals or exceeds the array size established by the array
definition, any one of three possible consequences can result. If the addressed
memory location is located outside the memory space reserved by the operating
system for the running program, the operating system intercepts the attempted
86 Arrays and matrices
illegal memory access, terminates the program and issues a “segmentation fault”
message in a pop-up window, indicating that the program has violated its allotted
memory segment. Since the array index then often greatly exceeds its permissible
limits, the problem can usually be isolated either by printing out intermediate
variables and viewing these in a debugger or by directly inspecting the source
code.
In the event that the memory location accessed by the array variable instead
occupies a region reserved for program operation but that is either not initialized
or is associated with a variable of an incompatible type (e.g. a double instead
of an int), the array element acquires a large, random value. The program then
either generates unphysical results or raises an exception such as overflow or
underflow, which, however, typically appears at a different code line than the
actual error.
The third, and by far the most troublesome possibility, appears if the array
index only slightly exceeds the array size and a second initialized variable of a
compatible type is resident at the corresponding memory position, as in
main( ) {
double b[2] = { 0.02, 6.0 };
double a[2] = { 0.01, 3.0 };
cout << b[0] << endl;
cout << b[-1] << endl;
cout << a[2] << endl;
double wrongNorm = sqrt( a[0] * a[0] + a[1] * a[1] + a[2] * a[2] );
cout << wrongNorm << endl;
}
which yields
0.02
3
0.02
3.00008
a function can possess an array argument (since within the function an “array
argument” is implemented as a constant pointer that can be equated to an array
variable). If one of the elements of the array argument is altered within the func-
tion, however, the corresponding array element in the calling program similarly
changes. The reason for this “pass (call) by reference“ behavior is demonstrated
by (note that the array size need not be included in the function argument, which
requires only specification of the variable type)
main( ) {
int p[ ] = { 1, 2, 3 };
cout << p << endl;
zero( p );
cout << p[0] << endl;
}
which yields
0012FF80
0012FF80
0
This indicates that the starting memory address of the array p is passed as a
parameter to the function by the call zero( p ). Accordingly the starting memory
location associated with the array argument aA is equated in the function to that
of p in the calling program. Thus changing the value of an element of aA within
the function body generates a corresponding change in p in the calling program.
In this manner, memory for a copy of the array is not allocated in the function,
avoiding substantial overhead for large arrays. It should here be remarked that
a frequent error occurs when an array element is mistakenly employed in place
of an array name as a function parameter in the calling program, e.g. replacing
zero( p ) by zero( p[0] ) in the above program.
Call by reference semantics connects variables across two disjoint blocks,
compromising the intent of block structure. That is, if an array argument is
assigned an unintended value in the function block, the corresponding parameter
value in the calling block, which can be separated from the function by a large
code distance, will change as well, leading to a concealed error that is difficult
to detect.
the latter object. Therefore, an array can be returned from a function if first
wrappered in an object:
class Matrix {
public:
int iA[10];
};
Matrix f( ) {
Matrix M1;
M1.iA[0] = 10;
return M1;
}
main ( ){
Matrix B = f( );
cout << B.iA[0] << '' '' << f( ).iA[0] << endl;
} // Output: 10 10
The effective equivalence of user-defined and built-in types also enables arrays
of objects to be defined in exactly the same manner as arrays of built-in variables.
For example,
class Position {
public:
double iPosition;
};
main ( ) {
Position P1[2];
P1[0].iPosition = 1;
P1[1].iPosition = 2;
cout << P1[1].iPosition << endl; // Output: 2
}
main ( ) {
const int b[2] = {1, 2};
print( b ); // Error: aI not a const array
}
yields a compiler error since the function argument aI is not declared const
in print( ). This results from the pass by reference semantics since otherwise
the const property of the array in the calling program would be circumvented
through changes within the function.
int M[2][3];
The syntax, however, unfortunately reverses the significance of the array indices
and should be construed as
(int[3])[2] M
int M[ ][3] = { { 1, 2 }, { 4, 5, 6 } };
90 Arrays and matrices
where the elements in the first set of braces initialize the three-component array
M[0], while the elements in the second set initialize the components of M[1].
Alternatively, the initialization statement can be written as a single set of elements
placed in the initialization list memory in the order in which they will appear in
memory.
int M[ ][3] = { 1, 2, 0, 4, 5, 6 };
int N[2][3][4];
In the latter case, memory is not addressed sequentially by the innermost, most
rapidly varying, for loop (although modern compilers automatically optimize
the loop order). For larger array dimensions, addressing successive elements can
accordingly require accessing high-level cache memory rather than fast low-
level cache or CPU memory registers. In the most unfavorable case, accessing
neighboring elements induces a page fault in which portions of the matrix are
exchanged (swapped) between the hard disk and fast memory, increasing exe-
cution times by orders of magnitude. Transversing array elements can therefore
require careful planning. For example, two matrices A and B are normally mul-
tiplied as follows:
9.9 Arrays as function arguments 91
Here the elements of A are traversed linearly (stride 1), but those of B are
accessed suboptimally (stride rightDimension). If, however, B is replaced by
its transpose, BT, the matrix product of A with B is obtained by replacing the
innermost loop by
C[leftLoop][rightLoop] += A[leftLoop][innerLoop] *
BT[rightLoop][innerLoop];
In the incrementation step of the second for loop innerLoop in has mistakenly
been replaced by outerLoop, yielding an infinite loop.
const int N = 2;
void f1( double aV[ ] ) {
aV[0] = 1;
}
void f2( double aM[ ][N] ) {
aM[0][0] = 2;
}
main ( ) {
double M[N][N] = { 0 };
f1( M[1] ); // M[1][0] is now 1
f2( M ); // M[0][0] is now 2
cout << M[1][0] << '\t' << M[0][0] << endl; // Output: 1 2
}
92 Arrays and matrices
As with array parameters, the call f2( M ) passes the multidimensional array
name, which evaluates to its starting memory location, to f2( ). Therefore, aM
in f2( ) occupies the same memory space as M in main( ), resulting in pass-by-
reference semantics.
Chapter 10
Input and output streams
93
94 Input and output streams
the flag value is typically assigned to a specific bit or bits of one or more bytes,
which therefore can store several flags.) Flags that are common to all input and
output classes are defined through an enum (set as a bitmask with members that
evaluate to values 0, 1, 2, 4, . . . ) in the ios base class and are addressed through
the scope-resolution operator ios::. Important set member functions for flags and
other internal member variables of the cout class are
cout.precision( n ) // n figures written out
// in floating pt.
cout.setf( ios::fixed ) // retains trailing zeros, turns
// off scientific notation
cout.setf( ios::left ) // left-justifies output
cout.fill( '.' ) // replaces all blank fields with a
// period
cout.setf( ios::scientific ) // scientific (floating point)
// notation
cout.width( m ) // reserves m spaces for output
Generally, if a property of the stream object is changed, the flag values are altered
and the stream persists in the new state. The output width, set to m above, is,
however, set to revert to its default value every time a new value is written, since
data of a different type are generally extracted from the stream at the subsequent
operation. Therefore
main ( ) {
float f = .0001;
cout.setf( ios::fixed );
cout.setf( ios::left );
cout.fill( '.' );
cout.width( 20 );
cout << f << endl;
cout.precision( 20 );
cout.setf( ios::scientific );
cout << f << endl;
}
yields
0.000100 ... ... ... ...
9.999999747378751636e-05
The flags defined in iostream and its base classes can also be altered through
stream manipulators defined in the header file <iomanip>. These alter the stream
flag values when piped directly into an output stream:
#include <iomanip.h>
Output can be sent directly to the printer by replacing all instances of the
standard output stream cout by the printer stream cprn. Other standard streams
are cerr (the “standard error” stream) and clog, both of which are associated by
C++ with the terminal, and caux, which is bound to the serial communication
port.
int r;
myInputFileStream >> r;
myOutputFileStream << setw( 8 ) << r << endl;
Assuming that file.dat containing only int values has been attached to the file
stream myFileStream, data can be read up to the end of the file with either
where the eof( ) member function of fstream returns true when the null termi-
nation character is reached and false otherwise.
When a file is opened for writing, its previous contents are normally deleted.
To append data to a preexisting file, ios::app must be included in the stream
definition:
ofstream fout( ''out1.dat'', ios::app );
The stream definition can also be separated from the process of attaching the
stream to a file by introducing an open statement, as in
ofstream fout;
fout.open( ''out1.dat'', ios::out | ios::app );
After a subsequent close statement, the stream can be reassigned to another file,
fout.close( );
fout.open( ''out2.dat'' );
A file that is repeatedly opened in append mode, written to and then immediately
closed can be copied during program execution, enabling output from a long-
running program. All files are automatically closed by the operating system when
a program terminates.
Storage space and access time can be reduced by storing information in
binary as opposed to text (ASCII) format. This is accomplished by including
the ios::binary specifier in either the open or the file definition statement as in
fstream fout( ''myFile.dat''', ios::out | ios::binary );. However, writing e.g. a
double variable a then requires the somewhat cryptic syntax fout.write( (char *)
&a, sizeof( a ) );.
A stream such as cin, cout or a user-defined file stream such as fout above can
be passed to a function. However, the function must act on the same file stream
defined in the calling program, since a standard file stream cannot be copied.
To implement pass-by-reference semantics and thus avoid copying, a function
must employ an array, reference or pointer argument. Since only the first of these
variable types has been introduced, the procedure is illustrated by passing an
array of two file streams to a function:
#include <fstream>
void print ( ofstream aFstream[ ] ) {
aFstream[0] << ''Hello World'' << endl;
}
main ( ) {
ofstream fout[2];
fout[0].open( ''test1.dat'' );
fout[1].open( ''test2.dat'' );
fout[0] << ''Line 1 '' << endl;
print ( fout );
}
10.3 The string class and string streams 97
string S;
or by
string S = '''';
The operator + is overloaded for strings; for example, S + '' : '' + S yields the
string “A string : A string”. Lexographic comparisons (comparison according to
dictionary order) are performed through the overloaded operators ==, !=, <, <=,
> and =.
Some important functions that act on string objects are
S.length( )
which returns the number of non-null characters in the string S such that the
length of a blank string is zero, and
S.erase( 3, 4 ); // result: A sg
which erases four characters in the string S starting at the position to the right
of the third character (writing simply S.erase( ); or S = ""; deletes the entire
contents of the string). A single element of a string can be accessed either through
S[m] or through S.at( m ), while the entire string can be converted into a standard
“C-type” character array terminated by the null character by employing S.c_str( ).
When m >> length( S ), S[m] returns the null character. Characters can be read
directly into or out of a string from a stream through the operators >>, and <<
and the getline( ) function. The find( ), replace( ) and insert( ) functions can be
respectively employed to find a sequence of characters in a given string, replace
a character or a sequence of characters in a string with other values and insert
additional characters into a stream.
To illustrate the application of string streams, suppose that data, represented
below by the string “some input data”, are to be written to a large number of
files labeled output1.dat, output2.dat, output3.dat, etc. Such names can be
98 Input and output streams
main ( ) {
char temp[10] = ''input'';
string S;
int numberOfFile = 1;
stringstream inoutStream;
ofstream fout;
inoutStream << temp << numberOfFile << ''.dat'' << endl;
inoutStream >> S; // Reads from string stream
fout.open( S.c_str( ) ); // Converts string object to c string
fout << ''input data'' << endl; // Data are placed in input1.dat
}
#include <strstream>
#include <iostream>
using namespace std;
class C {
public:
int iC;
C( int aC = 0 ) : iC( aC ) { }
string toString( ) {
strstream s;
s << ''iC = '' << iC <<ends;
10.5 The printf function 99
return s.str( );
}
};
class D {
public:
C iC[2];
int iD;
D( int aD, C aC[ ] ) : iD( aD ) {
iC[0] = aC[0];
iC[1] = aC[1];
}
string toString( ) {
strstream s;
for ( int loop = 0; loop < 2; loop++ )
s << iC[loop].toString( ) << '' '';
s << endl;
s << ''iD = '' << iD;
s << ends;
return s.str( );
}
};
main( ) {
C C1[2] = {1, 2};
D D1( 2, C1 );
cout << D1.toString( );
}
main( ) {
int m = 10;
double x = 1.e2;
char a[10] = ''and '';
printf( ''x = %3.4f, %10s m = %10d'', x, a, m );
}
100 Input and output streams
to the standard output device. A related function, sprintf( buf, "x = %3.4f,
%10s, m = %10d", x, a, m );, instead places the output into a memory buffer
such as char buf[100];.
Chapter 11
References
Reference and pointer variables, like arrays, store memory locations rather than
values. The memory location of a reference cannot be changed and must be
identified with that of a preexisting variable. Hence a reference can be handled
in effectively the same manner as the variable it refers to. The address stored in
a pointer can in contrast be changed at compile or run time.
int A1 = 0;
int &grade = A1;
grade = 5;
cout << A1 << endl; // Output: 5
Since for e.g. grade above to constitute an alternative name for A1 all proper-
ties of the two must coincide, a reference cannot be reassigned to a new variable
(since A1 cannot be reassigned) and must be initialized to a variable of the same
type as in its definition. That is, declaring double &grade = A1 yields a com-
piler error. Additionally, since a reference is an alternative name for a variable it
cannot be initialized to a constant. Memorizing the above will prevent countless
programming errors.
101
102 References
main( ) {
int m = 4;
zero( m );
cout << m << endl;
} // Output: 0
Recall that when a function is invoked it first defines and initializes the variables in
its argument list. Thus zero( ) above implicitly initializes the reference according
to
int &aM = m;
For this reason, the parameter passed to a function reference argument cannot be
a constant or a variable of a different type, so that the following yield compile-
time errors:
double d = 4.0;
zero ( d ); // Error: an int reference cannot
// be assigned to a double.
zero ( 4 ); // Error: a reference variable cannot
// be assigned to a constant.
This feature can be leveraged to insure that a function accepts only array param-
eters of a certain size and type. In particular, if a function signature contains a
reference to an array with e.g. 20 int elements, as in
void print( int (&aI)[20] ) { cout << aI[6] << endl; }
only 20-element int arrays can be passed to print( ) from the calling routine.
11.4 const reference variables 103
class PositionSensor {
public:
Printer &aPrinter;
PositionSensor ( Printer &aPrinter ) : iPrinter(aPrinter){ }
};
main( ) {
Printer P1 = {3};
PositionSensor PS1( P1 );
PositionSensor PS2( P1 );
P1.iPrinterSetting = 5;
cout << PS1.iPrinter.iPrinterSetting << '' '' <<
PS2.iPrinter.iPrinterSetting << endl; // Output: 5 5
}
const int m = 4;
int n;
int &k = m; // Compile-time error or warning
const int &l = n; // Valid
main ( ) {
const int n = 3;
pr( n ); // Output: 3
pr2( n ); // Error
}
#include <fstream.h>
main ( ) {
ofstream myStream( ''output.dat'' );
print( print ( myStream,
''This is the first line'' ), ''This is the second line'' );
}
Chapter 12
Pointers and dynamic memory allocation
106
12.3 Address-of and dereferencing operators 107
A hexadecimal value can also be read in from the keyboard (in this case not
preceded by 0x) through the following syntax (if the hex manipulator is omitted,
the integer equivalent of the hexadecimal value should instead be entered):
int m, *pM;
cin >> hex >> m; // Input: 0012FF88
pM = (int *) m;
This procedure is of limited value since, except for those associated with physical
devices such as a sound card or serial port, most memory locations in a program
are specified as offsets from a starting location determined by the operating
system at runtime.
int m;
cout << &m << endl; // Output: e.g. 0012FJ4A
displays the address of the variable m on the terminal. This address can be stored
in a preallocated int pointer variable:
Unlike a reference, this memory address can be later reassigned to the address
of a different variable as in the lines below:
int l;
pM = &l;
108 Pointers and dynamic memory
int m = 4;
int *pM = &m;
*pM = 5;
cout << m << ' ' << *pJ; // Output: 5 5
Thus *pM constitutes an alias for the variable m, the int variable located
at the value of pM. Further, since *(&m) yields the same variable as m, the
address-of and dereferencing operators comprise inverse operators. The variable
pM can also be dereferenced through pM[0], which possesses the same meaning
as *pM.
A special form of the dereferencing operator exists for objects. Consider
class C {
public:
int b;
void f( ) { cout << ''test'' << endl; }
};
main( ) {
C C1 = { 1 };
C *pC = &C1;
cout << (*pC).b << endl; // Output 1: parenthesis required!
pC -> f( ); // Output: test
}
Here the pointer variable pI contains the address of a random memory location
that statistically is almost certain to be located outside the memory space allo-
cated to the program by the operating system. Placing a value into this location
therefore raises a general protection fault leading to an operating-system run-
time error message. Accordingly, every uninitialized pointer or pointer that is
detached from a meaningful memory location should be assigned the address 0
or equivalently NULL. A logical test then insures that each pointer is initialized
before dereferencing:
int *p = NULL;
cout << p << endl; // Output: 00000000
... additional lines such as int l; p = &l; ...
if ( p ) *p = 3; // Insures p is initialized
then the address that m stores (points to) is fixed, but the int value at this address
can be changed through the dereferencing operator. Indeed, reading the definition
from right to left indicates that the value of pM (the address stored in pM) is
declared const through (const pM), while dereferencing this address through
*(const pM) yields a non-constant int variable. Since the address of the pointer
cannot be changed, pM must be initialized to a sensible address when defined.
For the above definition, we have
int k = 2;
*pK = 30; // VALID, the int value can be changed.
pK = &k; // ERROR, the address in k is constant.
or equivalently
As the declaration indicates, the value *pK located at the address held in pK is
a const int in the sense that it cannot be changed through pM, but the value of
pK (the memory location stored in the pointer) can be altered. That is,
int k = 1;
pK = &k; // OK: address held in k can be changed.
*pK = 4; // Error: k == l cannot be changed through pK
the memory location stored in the pointer variable cannot be altered, while the
value stored at this location cannot be modified through the pointer variable.
int a[3] = { 1, 2, 3 };
cout << a << endl; // Possibly: 001H2B00
cout << a + 2 << '\t' << *( a + 2 ) << endl; // Output:001H2B08 3
int *p = a;
cout << p[2]; // Output: 3
12.9 Pointers to pointers and matrices 111
the first if statement compares the values of the pointer variables pA and pB,
which are the addresses of the integer variables that pA and pB point to, and thus
determines whether pA and pB point to the same variable. While this could be
intended, generally the objective is to compare the values of the variables which
pA and pB point to. The pointers must then be dereferenced before effecting
the comparison as in the second if statement above. A similar error occurs if the
index operator, [ ], is omitted in comparing two array variables. Since each array
occupies a unique, compiler-assigned, memory location, the logical condition
will then always evaluate to false.
Note that, unlike for a matrix, the rows of ppP do not necessarily contain the
same number of elements.
112 Pointers and dynamic memory
or
char *pS2 = ''World'';
The second procedure reserves space for the terminating, null character in the
string “World”; however, the size of the character array (6 bytes) allocated to pS2
cannot be manipulated and would prove insufficient if a longer string were later
stored in pS2.
The insertion and extraction operators can be employed to read a character
string from the keyboard, or to write a string to the terminal. However, to preserve
whitespaces in an input line, the getline( s1, i1 ) function can be employed to read
the first i1 - 1 characters of the line up to a carriage return, including whitespace
characters. The function then appends a null character and stores the result in
the array s1, which therefore must be at least i1 characters long. Alternatively,
getline( s1, i1, 'c' ), reads i1 - 1 characters from the input stream up to the
termination character, c. That is,
char s[10];
cin.getline( s, 10, '|' );
cout << s;
#include <string>
main( ) {
string S1 = ''Hello'';
string S2 = ''World'';
cout << S1.length( ) << endl; // Output: 5
cout << S1.replace( 0, 2, S2, 0, 2 ) << endl; // Output: Wollo
cout << S1.compare( 0, 2, S2, 0, 2 ) << endl; // Output: 0
}
The member functions begin( ) and end( ) return pointers to the initial and final
elements of a string object. This facilitates iterating over the characters in a
string, as in
float a[10];
void fun( ) { int b[10]; }
void main( ) { int c[10]; { int d[10]; } }
the lifetime, or the period during which memory is reserved, of the global array
a extends from the beginning to the end of the program while the lifetime of the
local arrays b, c and d extends from their definition statements to the end of the
encompassing block.
Dynamic memory allocation. Despite the efficiency of compiler memory man-
agement, the amount of memory required by a program often depends on the
outcome of a user interaction or a logical condition evaluated during execu-
tion. By postponing memory allocation until runtime, a program can exploit
the resources of large computers while still executing successfully on small
machines. For example, a computer game that reserves memory for a new player
upon request should function on any system, but the maximum number of players
will necessarily be limited by the available hardware.
In C++ a running program can request memory from the operating system for
a variable of a certain type by invoking the new operator. This operator returns
114 Pointers and dynamic memory
These two steps are often combined into the single statement
int *pM;
cin >> n;
if ( n > 0 ) {
pM = new int [n];
if ( pM == 0 ) cout << ''out of memory'';
}
The lifetime of memory allocated through a call to new extends until the end
of the program unless it is deallocated with a delete or delete [ ] statement. The
second of these statements must be employed whenever an array is allocated as
in
The square brackets after delete in the fifth line insure that the memory asso-
ciated with the entire array will be deleted. If the brackets are omitted, only
the memory at the position of the first array element is certain to be deleted,
possibly yielding a memory leak. In contrast, delete [ ] can be safely applied
to non-array variables. The delete or delete[ ] statements do not affect the pro-
gram when applied to null pointers. Note that the compiler-allocated pointer pM
remains allocated until the block in which it is defined terminates – the delete
statement frees the memory which pM points to but does not delete pM itself.
Accordingly, following a delete statement the value of pM can still be set to the
memory address of a different variable or to a new dynamically allocated mem-
ory address. Whenever a dynamically allocated variable is deleted, all pointers
to this variable should be set to the null pointer so that assigning a value to
the dereferenced pointers yields a runtime error instead of accessing deallocated
memory.
While the above statement only results in the loss of a single byte, if located in
a for loop, all memory available to the running program can be exhausted, as
in
for ( ; ; ; ) int *pGarbage = new char( 'a' );
which will typically slow down and then halt the computer, forcing a reboot.
the program will often continue to run normally. However, if this memory is
later accessed by the program for another purpose such as a function call or a
variable definition, or, in the case of dynamically allocated memory, for another
running program, then unphysical results are produced or the program terminates
unexpectedly. Such errors occur intermittently at unpredictable times widely
separated from the execution of the incorrect source line, and are therefore often
extremely difficult to identify in the absence of specialized software tools.
A dangling pointer is defined as a pointer to deallocated memory such as
Subsequently
places the value 30 into the unsafe memory location originally assigned to pDan-
gle[9]. Subsequently dereferencing pDangle, however, still yields 30 unless the
contents at this location were in the meantime overwritten by the program or
operating system. As mentioned above, a recommended procedure for eliminat-
ing dangling pointers is to assign the NULL or 0 address to all pointers upon
deallocation, as in
pDangle = NULL;
now yields a runtime error since the operating system prohibits access to the null
address.
A further dangling pointer error can occur if two pointers store the same
memory location. If this memory is deallocated through one of these pointers
whose address is set to NULL, the second pointer still accesses deallocated
memory, e.g.
A dangling pointer can also result from the deallocation of compiler allocated
memory at the end of a block, as in
double *pDangle;
{
double d = 3;
12.14 Pointers in function blocks 117
pDangle = &d;
} // Error: d’s memory deallocated
The above problem does not, however, occur with dynamically allocated memory,
which persists until an explicit delete statement is issued:
double *pR;
{
double *pD = new double[20];
pR = pD;
}
pR[10] = 10; // OK: pointer variable pD destroyed but not its memory
main( ) {
int n = 1, m = 2;
int *pN = &n;
int *pM = &m;
test( pN, pM );
cout << n << endl; // Output: 4
cout << *pM << endl; // Output: maybe 4 but unpredictable
}
The arguments aPN and aPM are references to pointers, and therefore correspond
to alternative names for the pointers pN and pM defined in main( ). The value
of the variable pointed to by aPN is changed inside test( ), so that pN, which
points to the address space of the variable n in the main program, remains
correctly allocated after the function call. On the other hand, the value of the
pointer reference variable (i.e. the variable to which the pointer points) aPM is
reassigned inside test to the memory address of the local int variable k. After
the function block has terminated, k is destroyed and pM subsequently points
to deallocated memory. If aPM were instead defined as a pointer argument,
call-by-value semantics would apply and it would therefore contain a copy of
118 Pointers and dynamic memory
the memory address in pN. Setting aPM to the address of k within the function
block then does not affect the value of pN, which would therefore still point to
the address of n defined in the main block rather than to deallocated memory.
main( ) {
int *pI;
allocate( pI );
pI[5] = 3;
}
Memory can also be assigned within a function and its address transferred to
the calling program through a pointer return value:
float *pAssign( ) {
float *pA = new float [20];
return pA;
} // OK: memory persists
float *pDangle = a;
return pDangle;
} // Error: memory destroyed
To deallocate the memory assigned to the matrix (but not ppA, the compiler-
assigned pointer variable) the dynamically allocated array pointed to by each
element of ppA must be deleted before the dynamically allocated pointer array
assigned to ppA is deleted:
for ( int loop = 0; loop < N; loop++ ) delete [ ] ppA[loop];
delete [ ] ppA;
If just the second of these lines is present, the addresses to the arrays that store the
actual matrix elements are (eventually) lost and the associated memory becomes
garbage.
Optimal memory allocation. A significant drawback with the above dynamic
allocation strategy is that memory for each matrix row is allocated through
a separate call to new. Consequently, the operating system can assign widely
separated areas in memory to successive rows. Numerous page faults can then
result when iterating through all matrix elements. To insure contiguous memory,
a one-dimensional array with N × M elements should first be allocated. The
values of the pointers of a dynamically allocated array of N pointers are then set
to the address of every Mth element of the array. For a 2 × 3 matrix of doubles,
double *pB = new double[6]; // contiguous
double **ppA = new double*[2];
for ( int loop = 0; loop < 2; loop++ ) ppA[loop] = pB + 3 * loop;
To deallocate the dynamically assigned memory, the memory associated with the
underlying array pB should be deleted after the pointer array ppA:
delete [ ] ppA;
delete [ ] pB;
120 Pointers and dynamic memory
If pB is deallocated first, the matrix ppA will point to unallocated memory, poten-
tially leading to incorrect memory access if the matrix is mistakenly employed
before deletion.
main ( ) {
int **ppA = new int *[2];
ppA[0] = new int[2];
ppA[1] = new int[2];
ppA[0][0] = ppA[0][1] = ppA[1][0] = ppA[1][1] = 6;
print ( ppA, 2, 2 ); // Output: 6 6
} // 6 6
However, the above print function cannot be used for a statically allocated matrix,
int B[2][2] = { 1, 2, 3, 4 };
print ( B, 2, 2 ); // Error!
since, in this case, the compiler requires that the second matrix dimension be
specified in the function parameter list (e.g. void print( double *A[2], int aN,
int aM )) because the type of B is that of an array of two-component integer
arrays.
Similarly, a dynamically but not a statically allocated two-dimensional array
can be assigned to a variable of type pointer to a pointer:
int A[2][2] = { 1, 2, 3, 4 };
int **ppB = new int*[2];
ppB[0] = new int[2];
ppB[1] = new int[2];
int **ppD = B; // OK
int (*ppAE)[2] = A; // OK
ppD = A; // Error!
class Node {
public:
Node *iPNext;
double iValue;
};
A pointer of type Node* containing the address of the first list element constitutes
the “head” of a linked list, while iPNext in the last element or tail of the list is
set to 0 or NULL. The latter choice enables a logical test for the end of the list in
the same manner as a zero character terminates a string. Hence, a three-element
list can be generated by
Node NLast = { 0, 3 };
Node NMiddle = { &NLast, 1 };
Node NFirst = { &NMiddle, 8 };
Node *pHead = &NFirst;
Exploiting the presence of the null pointer at the tail, a for loop can iterate
through the list items as in the following printList( ) function that writes out the
data in the list:
A List class combines the Node class with functions that manipulate lists
while employing dynamic memory allocation to generate and remove nodes.
A bidirectional list that can be traversed in either the forward or the reverse
direction is generated by including a second pointer, iPPrevious, in the Node
class that points to the preceeding node. A binary tree is then obtained if each
of the pointers iPPrevious and iPNext of a given node points either to NULL
or to a separate node that is not pointed to by any other node. That is, each
node may point to zero, one or two other nodes, but is pointed to by just a single
node. Operations on tree structures are again implemented through recursion and
are initially somewhat difficult to program. In general, for dealing with physical
problems that are naturally formulated in terms of non-trival data structures one
should employ an appropriate collection class package.
Chapter 13
Memory management
this -> iA is interchangeable with iA since *this represents the object that calls
the print( ) function.
A fundamental application of the this pointer is given by
class C {
public:
int iM;
C& print ( ) {
cout << iM << '' '' '' '';
return *this;
}
C* pAdd( int aN ) {
iM += aN;
123
124 Memory management
return this;
}
};
main ( ) {
C C1 = { 2 };
C1.pAdd( 3 ) -> print( ).pAdd( 3 ) -> print( ); //Output: 5 8
}
Since the . and -> functions are left-associative, the last line of the main( )
program is interpreted as ( ( ( C1.pAdd( 3 ) ) -> print( ) ).pAdd( 3 ) ) -> print( );.
Consequently the pAdd( ) function of C1 is applied first, returning a pointer to
the calling object, namely C1 itself. Next, this pointer is dereferenced through
the pointer-to-member operator and the print( ) function is called, which returns
a reference to C1. Thus the combined effect of these operations is to return the
initial object C1 with modified internal data members, to which a further series
of add and print functions is applied. While the above program still functions
if C is employed in place of C& as the return type of the print( ) function, a
superfluous and potentially problematic (see Section 13.6) copy operation occurs
when the C object is returned.
class A {
int iJ;
friend class B;
};
all the private data and member functions of A appear as public members in class
B (which can be a subclass of A) so that the following code is valid:
class B {
A* iPA;
public:
void printA( ) { cout << iPA -> iJ << endl; }
B( int aJ ) {
iPA = new A;
iPA -> iJ = aJ;
}
};
main ( ) {
B B1( 5 );
B1.printA( ); // Output: 5
}
13.3 Operators 125
To grant a single function int bfun( int bI ) in class B access to the private
members of A, a line
friend int B::bfun( int );
should be included in the (public or private) interface of class A. Since the friend
function, bfun, is not a member of the class A, it does not have an associated
this pointer to A. Although often convenient, the friend construct violates the
object-oriented principles of encapsulation and information hiding.
13.3 Operators
C++ operators can be redefined or extended to user-defined classes through
overloading. However, although any C++ operator, with the exception of the
scope operator, ::, the dereferencing operator, *, the member-of operator, ., the
sizeof operator and the if-then-else ternary operator, ?:, can be overloaded, new
operator symbols cannot be introduced and existing precedence rules cannot be
altered. Accordingly, certain operators have illogical precedence. For example,
the C++ stream extraction and insertion operators overloaded preexisting high-
precedence C operators for bit insertion and extraction, invalidating seemingly
meaningful constructs such as cout << i = 3 << endl;. Two procedures for
overloading operators exist and are discussed individually below.
Overloading through friend functions. An operator acting on class members
can be introduced through a friend function that accesses the internal class vari-
ables. To illustrate, the class Vector below possesses two private data members,
iX and iY. The binary addition operator + is overloaded such that adding two
Vector objects Vector1 and Vector2 with internal data members iX1, iY1 and
iX2, iY2 yields a new Vector object with data members iX1 + iX2 and iY1 +
iY2. When expressed as a friend function, the + operator possesses the special
signature operator+ ( V1, V2 ), in which the first and second arguments are the
expressions to the left and to the right of the + sign, respectively. This yields the
class definition
class Vector {
public:
double iX, iY;
friend Vector operator+ ( Vector, Vector );
Vector( double aX, double aY ) : iX( aX ), iY( aY ) { }
void printVector( ) { cout << iX << ''\t'' << iY << endl; }
};
main ( ) {
Vector V1( 1, 2 ), V2( 2, 3 );
Vector V3( 0, 0 );
V3 = V1 + V2;
V3.printVector( ); // Output: 3 5
}
class Vector {
public:
double iX, iY;
Vector operator+ ( Vector );
Vector( double aX, double aY ) : iX( aX ), iY( aY ) { }
void printVector( ) { cout << iX << ''\t'' << iY << endl; }
};
Observe that here the binary operator + possesses only a single argument and
can therefore be called either through the standard notation V3 = V1 + V2;
or the alternate notation V3 = V1.operator+ ( V2 );. However, the friend
implementation of binary operators is generally preferable, since both operator
arguments are handled symmetrically with respect to implicit or user-defined
conversions.
The extraction operator is often overloaded at global scope to output the
internal state of an object in a convenient format, as, for example,
The statement cout << V3; then yields The vector components are 3 and 5. If
the internal class variables iX and iY are private, the operator must be a friend
of the Vector class.
13.4 Destructors
Having introduced operators and friend functions, we now discuss memory man-
agement in classes that dynamically allocate memory within constructors, as
in
class A {
public:
A( int aValue = 0, int aSize = 2 ):iValue( aValue ), iSize( aSize )
{
iPArray = new int [aSize];
}
void print ( )
{
cout << iValue << endl;
}
int iSize;
int *iPArray;
int iValue;
};
This class definition will in general create memory leaks unless three auxiliary
functions, the destructor, assignment operator and copy constructor, are intro-
duced.
A destructor is called when an object of type A goes out of scope. Without a
destructor, the compiler-assigned internal variables of A1, including the pointer
iPArray, are automatically deallocated. However, the memory that the iPArray
variable points to is dynamically assigned through the new statement in the
constructor of A1 and is therefore not deallocated, yielding a memory leak. Of
course, since the internal class variables in A are public, this could be rectified
by
{
A A1( aValue );
delete [ ] A1.iPArray;
}
not followed by the class name is intended as a mnemonic for object destruction).
In the above example, an appropriate destructor is
~A( ) {
delete [ ] iPArray;
}
{
A *pA1 = new A;
delete pA1; // Calls pA1’s destructor
}
Deleting the memory of the object assigned to the pointer pA1 calls its destruc-
tor. If memory is not always dynamically assigned to a given pointer by the
constructor, since delete or delete [ ] operators can always safely be applied
to null pointers, any pointer that will be the target of a delete statement in the
destructor should be set to the null pointer in the constructor when dynamic
memory is not allocated.
Before an object is constructed, its base classes are first constructed and
initialized in the order in which they are declared. Within each class, class
members are again initialized in order of declaration. The destructor destroys
memory in the opposite order to that in which it is allocated by the constructor.
main ( ) {
A A1( 3 );
A AGarbage;
AGarbage = A1;
AGarbage.print( ); // Output: 3
}
memory as iPArray in A1. While the program then functions properly (except
that a change to the array at iPArray in either A1 or AGarbage will propagate
to the other object), a memory leak has occurred. In particular, AGarbage
when created through the default constructor dynamically allocates memory for
an array of two integers and the address of this memory is stored in its iPArray
pointer. When the value of (the memory address stored in) AGarbage.iPArray is
overwritten by A1.iPArray through the assignment statement AGarbage = A1;,
the memory address of the original dynamically allocated array in AGarbage is
lost and cannot subsequently be reclaimed by the operating system until program
termination, even if a destructor function is specified.
A related but more severe error occurs in
A ADangle(2);
{
A A1(3);
ADangle = A1;
} // A1 is destroyed: ADangle.iPArray is deallocated.
ADangle.iPArray[0] = 1;//Intermittent error: illegal memory access
class A {
// ... code for internal variables, constructor, destructor, etc.
A& operator = ( const A& aA ) {
// Ensure that the object is not copied onto itself
if ( this == &aA ) return *this;
// Copy compiler-assigned variables
130 Memory management
iValue = aA.iValue;
iSize = aA.iSize;
// Assign new memory to the pointer variable if the
// dimension of the preexisting array is smaller
// than that required by the new array.
if ( iSize < aA.iSize ) {
delete[ ] iPArray;
iPArray = new int[aA.iSize];
}
// Perform a deep copy of the dynamically allocated elements
// in aA to those of the calling object.
for ( int loop = 0; loop < aA.iSize; loop++ )
iPArray[loop] = aA.iPArray[loop];
return *this;
}
};
The first line of the above function prevents the object’s memory from being
released in the case that the object is copied onto itself. A simpler procedure for
transferring the elements of the array aA.iPArray to iPArray is provided by the
memcpy function (the last argument of this function is the number of bytes to
copy):
The memcopy function resembles strcpy(s2, s1), which copies a string s1 into a
second string s2, and can duplicate arrays of any dimension.
A A1;
and
{
A A2Garbage( A1 );
A A3Garbage = A1;
}
which are implemented identically, although the equality sign in the declaration
of A3Garbage incorrectly implies that the assignment operator is invoked. The
last two arise when an object of type A is passed to or returned by a value from
a function, both of which occur when test( ) is called in the examples below:
A test( A aAGarbage ) {
aAGarbage.print( );
13.6 Copy constructors 131
return aAGarbage;
}
and
main( ) {
A A1( 1 );
A A2Garbage;
A2Garbage = test( A1 );
}
In each of the above cases, compiler-assigned variables are copied from one A
object to a second A object through a shallow copy and the address of the dynam-
ically assigned memory in the second object is again lost. However, these copies
do not proceed through the assignment operator but rather through the default
copy constructor, which possesses the signature classType ( const classType& ).
A copy constructor that performs a deep copy is implemented almost identically
to the overloaded assignment operator:
A ( const A& aA ) {
// Copy compiler-assigned variables.
iValue = aA.iValue;
iSize = aA.iSize;
// Assign new array memory to the pointer member variable.
iPArray = new int[aA.iSize];
// Perform a deep copy of the dynamically allocated elements in aA
// to those of the calling object.
memcpy( iPArray, aA.iPArray, iSize * sizeof(int) );
}
The reference parameter in the function signature is required since C++ does not
allow a constructor in a class to have a non-reference object of its own class type
as a parameter. A copy constructor can also be coded by invoking the overloaded
assignment operator:
A( const A& aA ) {
iPArray = new int[aA.Size];
*this = aA;
}
void f( ) {
static int iStatic = 1;
cout << iStatic++ << endl;
}
main( ) {
f( );
f( );
} // Output is 1 then 2.
The static keyword often simplifies programming but violates the principle of
encapsulation.
132
14.2 Static class members 133
its value is the same for all objects of the class. Since static members are inde-
pendent of the state or even existence of the objects, they can be accessed through
their name preceded by their class name followed by the scope-resolution oper-
ator as well as through the normal member-of or pointer-to-member operators.
Class member variables, whether public, protected or private, that are declared
static must have a corresponding initialization statement at global scope (outside
the class definition) that does not include the static keyword, since these variables
exist even if no objects of the class are instantiated and therefore cannot be initial-
ized within a constructor. If a value is not specified in the initialization statement,
the variable is automatically initialized to zero as in the following example:
class C {
public:
C( ) { cout << ++iStatic << '' '';}
private:
static int iStatic;
};
main ( ) {
C C1;
C C2;
} // Output: 1 2
class C {
public:
static int iStatic;
int iVariable;
C ( int aVariable ) : iVariable( aVariable ) { }
static void setStatic( int aStatic ) { iStatic = aStatic; }
static int calculate( C& aC ) { return iStatic * aC.iVariable; }
};
main( ) {
C::setStatic( 3 );
C::iStatic( 4 );
C C1( 2 );
C1.iStatic = 5;
cout << C::calculate( C1 ) << endl; // Output: 10
}
134 The static keyword
(Note that, since calculate( ) does not depend on any particular C object, it can
be declared static.)
main( ) {
Student Student1;
Person* pPointerToStudent1 = &Student1;
Person& RefToStudent1 = Student1;
pPointerToStudent1 -> print( );
RefToStudent1.print( );
Person Person2 = Student1;
Person2.print( );
}
yields the output Person Person Person since, although the pointer and reference
variables refer to Student objects, they are declared Person* and Person&,
respectively. However, this behavior can be altered for any function that appears
both in the base class and in the derived class by including the keyword virtual in
its base class definition (including the virtual keyword also in the derived class,
while unnecessary, is still recommended for clarity). If a derived class pointer or
object is then assigned to a base class pointer or reference variable, the derived
class object properties are instead employed at runtime. Hence, if different base
14.4 Heterogeneous object collections 135
or derived class objects are stored in a base class pointer in response to e.g.
user input and subsequently accessed, different behaviors result. To illustrate,
replacing the Person class above by the code
class Person{
public:
virtual void print( ) { cout << ''Person '' << endl; }
};
main( ) {
Person **ppArray = new Person*[2];
Person P1;
Student S1;
int select;
for ( int loop = 0; loop < 2; loop++ ) {
cout << ''Insert 0 to create a Person, 1 to create a Student '';
cin >> select; // Sample input: 0 1
cout << endl;
if ( !select ) ppArray[loop] = &P1;
else ppArray[loop] = &S1;
}
for ( int loop = 0; loop < 2; loop++ ) {
136 The static keyword
#include <iostream.h>
class Person{
public:
virtual void print( ) = 0;
};
main( ) {
Student Student1;
Worker Worker1;
Person *PArray[2] = { &Student1, &Worker1 };
PArray[1] -> print( ); // Output: Worker
}
Abstract base classes provide an interface that derived classes must conform to,
as opposed to standard inheritance, which instead provides an implementation
that is adopted by the derived classes. In interface inheritance, the compiler
verifies that the derived classes provide all required behaviors.
Since the constructor of class C passes arguments to the base class constructors of
A and B through its initialization list, these are invoked before C is constructed.
However, since iA and iB are inherited member variables of C, its constructor
can also be written as
C( aA, aB, aC ) : iA( aA ), iB( aB ), iC( aC ) { }
Note that the print( ) functions of the parent classes of C are distinguished in
class C through the scope-resolution operator.
138 The static keyword
main( ) {
StudentEmployee SE1;
SE1.print( ); // Error: ambiguous member
Person *pP1 = &SE1; // Error: cannot convert to Person*
pP1 -> print( );
}
which yields the error messages shown. To insure that only a single copy of the
base class and hence print( ) is inherited by StudentEmployee, the Student and
Worker class signatures must be replaced with
class Student : virtual public Person {
};
class Worker : virtual public Person {
};
class B, a conversion operator can be introduced in the A class with the special
signature operator B( ); that implements a cast from A to B. Alternatively, a
single-argument constructor can be defined with the signature B( A ) in the B
class. To illustrate both procedures, the following converts a Fahrenheit to a
Celsius temperature according to ◦ F = 9/5 ◦ C + 32 through a single-argument
constructor and converts a Celsius temperature to a double:
class Celsius;
class Fahrenheit {
public:
double iDegrees;
Fahrenheit( Celsius aCelsius );
Fahrenheit( double aDegrees ) : iDegrees( aDegrees ) { }
operator double( ) { return iDegrees; }
};
class Celsius {
public:
double iDegrees;
Celsius ( double aDegrees ) { iDegrees = aDegrees; }
Celsius( Fahrenheit aFahrenheit ) {
iDegrees = ( aFahrenheit.iDegrees - 32.0 ) * 5.0 / 9.0; }
operator double( ) { return iDegrees; }
};
main ( ) {
Fahrenheit F( 34.0 );
Celsius C = F;
cout << double( C ) << endl; // Output: 1.11111
printState( F ); // Output: above freezing
}
The keywords class and typename are interchangeable in the template argument
list. The above function can be called without template parameters as in the first
call to copy( ) in the program below. The class identifiers C1 and C2 are then
automatically determined from the types of the function parameters so that C1
and C2 evaluate to double and int, respectively. Alternatively, as in the next
line of the program, the template parameters can be explicitly supplied in the
parameter list. Finally, in the third call to copy( ), the template function arguments
are implemented as char arrays:
main( ){
double myDouble[10], myDoubleNew[10];
int myInt[10] = { 1 };
copy ( myDouble, myInt, 10 );
cout << myDouble[0] << endl ; //Output : 33
copy <double, double> ( myDoubleNew, myDouble, 10 );
char c1[5], c2[5] = { 'A' };
copy ( c1, c2, 5 );
cout << c1[0] << endl; //Output: a
}
this would be called in place of the template function whenever the copy function
is passed two character arrays, as in the third function call in main( ) above.
#include <sstream>
is
0 3
0 0
Note the placement of the scope operator in the asString( ) function definition.
The “non-type” template parameter aN cannot be a floating-point variable, class,
pointer or array and must be assigned a compile-time constant of a compatible
type (for example, if aN is of type bool it can be set to either of the bool constant
values true or false or to a variable of type const bool). If default parameters
are omitted from the template parameter list, their default values are employed.
As in the case of functions, default parameters must appear last in the template
argument list.
A static member variable of a templated class possesses a separate realization
for each template instantiation, i.e. each time the template is called with a distinct
set of template parameters. Thus
template< class T > struct MyStatic { static int iI; };
template< class T > MyStatic < bool >::iI;
template< class T > MyStatic < char >::iI;
main( ) {
MyStatic< bool > MB1, MB2;
MyStatic< char > MC1, MC2;
MB1.iI = 2;
MyStatic< char >::iI = 'a';
cout << MB2.iI << ' ' << MyStatic<bool>::iI
<< ' ' << MC2.iI << endl;
}
142 The static keyword
yields the output 2 2 97. Referring to static template variables through the syntax
classname<typename>::staticvariablename identifies the variable as static and
is therefore recommended.
Template arguments can encompass further template parameters, constant
expressions except for float or double types and the addresses of external objects,
which includes function names, references and pointer variables. While a full
discussion of templates far exceeds the scope of this book, the example below
illustrates some of these features:
template <class T = int> struct square {
double operator ( ) ( T aX ) { return aX * aX; }
};
template < typename T, int (*aF) (int) > double test( int aI ) {
cout << (*aF)( 10 ) << ' ' ;
return aF( aI );
}
main ( ) {
cout << test<square<double>, cube > ( 6.0 );
} // Output: 1000 216
Template classes can simplify program structure and hasten execution. To create
a template class, a specialized case should be first coded and verified and only
then subsequently generalized by successively introducing template parameters.
The real and imaginary parts of c can be accessed through the real( ) and
imag( ) member functions, e.g. c.real( ) and c.imag( ). Standard arithmetic
operators such as +, -, *, /, += . . . as well as the stream insertion and extraction
operators << and >> are overloaded for complex objects. Additional functions
in the complex class include arg( ), conj( ), abs( ), polar( r, t ), which yields
r eit , where r and t must be floats or doubles, cos( ), cosh( ), exp( ), log( ),
log10( ), pow( ), sqrt( ), sinh( ), tan( ) and tanh( ). Hence
14.12 The standard template library 143
#include <complex>
main( ) {
complex<double> c1 = complex<double>( 1., 2. ), c2( -1., 1. );
complex<double> c3 = ( c1 + c2 ) / 3.0;
c1 = exp( M_PI * c3 );
c2 = polar( 1., M_PI / 2. );
cout << c1 << '\n' << c3 << '\n' << c2 << endl;
}
yields
(-1,1.22461e-16)
(0,1)
(6.12303e-17,1)
possesses two member functions, it.begin( ) and it.end( ), that respectively return
a pointer to the first member of the container and to a fictitious member one
element beyond the end of the container. The iterator class includes an increment
operator (++) and a decrement operator (− −) that displace the pointer by one
container element, the comparison operators = = and != and the assignment
144 The static keyword
operator, =. The iterators of the random-access vector and deque classes further
permit random access in the container through the index operator it[n], the at
member function it.at( n ) or the corresponding pointer expression *( it + n ).
We illustrate the above concepts through a simple example that stores and
then prints four values {0, 1, 2, 3} in a four-component STL vector object (note
that the elements of an STL object are initialized to zero when defined):
#include <vector>
#include <iterator>
main ( ) {
// Elements initialized to zero
vector < int > aV( 3 );
vector < int > :: iterator pItV;
// Size expanded to 4 elements; aV[3] = 3.
aV.push_back( 3 );
aV[1] = 1;
aV.at( 2 ) = 2;
for ( pItV = aV.begin( ); pItV < aV.end( ); pItV++ )
cout << *pItV << endl;
}
0
1
2
3
#include <set>
#include <iterator>
main ( ) {
double a[3] = { 1.3, 2.5, 0.3 };
14.12 The standard template library 145
This yields
0
0.3
1.3
2.5
For user-defined data types, however, a comparator functor, when required, must
be specified as in
#include <set>
#include <iterator>
class C {
public:
int value1, value2;
};
class myGreater {
public:
myGreater( ) {
}
bool operator( ) ( const C &C1, const C &C2 ) {
return( C1.value1 < C2.value1 );
}
};
main ( ) {
C C1 = { 1, 2 };
C C2 = { 3, 4 };
set <C, myGreater> s;
set <C, myGreater> :: iterator pItV;
s.insert( C1 );
s.insert( C2 );
for ( pItV = s.begin( ); pItV != s.end( ); pItV++ )
cout << (*pItV).value1 << ' ';
}
which yields 1 3. The principle of operation of the functor is that, since the
constructor has no effect in the class myGreater, the function myGreater( C1,
C2 ) is invoked without first instantiating a myGreater object. The member
operator function operator( ) of myGreater is then called directly.
Finally, the STL contains mathematical functions that operate on STL data
structures as in
#include <algorithm>
#include <iterator>
#include <vector>
146 The static keyword
// for accumulate
#include <numeric>
main( ) {
ostream_iterator <int> myOut( cout, ''loop element \n'' );
vector < int > myVector ( 20 );
// places 5 in all 20 positions
fill( myVector.begin( ), myVector.end( ), 5 );
// changes first three values to 3
replace( myVector.begin( ), myVector.begin( ) + 3, 5, 3 );
// three-element zero vector
vector < int > tripleResult( 3 );
// third and fourth elements copied and tripled
transform( myVector.begin( ) + 2, myVector.begin( ) + 4,
tripleResult.begin( ), tripleFunction );
// new vector sorted
sort( tripleResult.begin( ), tripleResult.end( ) );
copy( tripleResult.begin( ), tripleResult.end( ), myOut );
cout << endl;
// sums elements
cout << accumulate( tripleResult.begin( ), tripleResult.end( ), 0 );
cout << endl;
// triples and sums
cout << accumulate( tripleResult.begin( ), tripleResult.end( ), 0,
tripleAccumulateFunction );
}
which yields
0 loop element
9 loop element
15 loop element
24
72
In the program, an ostream_iterator object myOut is first created for int val-
ues. Iterators of this type are associated with cout so that the copy function
can be employed to print out the elements of the container (here a vector) by
sending these to the ostream_iterator. A 20-element and a 3-element vector
object, which store int values, that are by default initialized to zero are defined.
Subsequently, all elements of the 20-element vector, myVector, are set to 5 and
then 5 in the first three elements is replaced by 3. The third and fourth elements
in myVector are tripled through the user-defined function tripleFunction( ) that
is passed as a parameter to the STL transform( ) function, which places the
result into the first two positions of the three-element zero vector tripleResult.
The three elements are sorted and the result printed. Finally, two forms of the
accumulate( ) function process the elements of the tripleResult vector. The first
simply adds the elements while the second calls the user-supplied global function
tripleAccumulateFunction( ) to evaluate the sum of three times each element.
14.13 Structures, unions and nested classes 147
class MyClass {
int m;
public:
double n:
}
is functionally identical to
struct MyClass {
double n;
private:
int m;
}
A struct often groups public variables of different types into a generalized array.
A union is identical to a struct except that all data members share the same
storage:
union U{
int m;
int n;
}
main ( ) {
U U1;
U1.m = 3;
U1.n = 0;
cout << U1.n << endl; // Output: 0
}
148 The static keyword
A union reduces the memory required by a program, but only a single variable
in the union can be accessed at a given time. Unions can also simplify access to
different memory locations in a data type, as illustrated by
union U{
int m;
char c[4];
};
main( ) {
U U1;
U1.m = 65;
cout << U1.c[0] << endl; // Output: A
}
struct S {
int iS;
struct C {
int iS;
};
print( ) { C C1 = { 2 }; cout << iS << '\t' << C1.iS; }
};
main( ) {
S S1 = { 1 };
S1.print( ); // output: 1 2
cout << S1.C.iS; // compile error: C not visible outside S
}
However, a nested class is accessible only to the members and friends of the
class.
struct myBitField {
unsigned firstBit: 1;
unsigned secondBit: 2;
unsigned thirdBit: 1;
};
depending on the computer hardware; however, the first bit appearing in the bit-
field is placed in the least-significant bit (bit zero) of the memory space reserved
for the field.
C++ provides operators that manipulate individual bits within a given vari-
able. These comprise the bitwise logical operators and (&), or (|), exclusive or (ˆ),
not (∼) and the right and left shift operators >> and << that shift the bit pattern
of the variable right and left by a given number of bits (specified to the right of
the operator), respectively, and introduce 0 in place of bits that are dropped. The
& and >> operators are illustrated below through two methods for printing out
the bit pattern associated with an arbitrary character variable (the union overlaps
the single-bit field with the least significant bit of the character variable):
struct aBit {
unsigned bit: 1;
};
union charBit {
char c;
aBit aB;
};
main( ) {
charBit CB;
CB.c = 'e';
for ( int loop = 0; loop < 7; loop ++ ) cout << '' '';
for ( int loop = 0; loop < 8; loop++ ) {
( CB.aB.bit ) ? cout << ''1'' : cout << ''0'';
CB.c = CB.c >> 1;
// alternative code
// ( CB.c & 1 ) ? cout << ''1'' : cout << ''0'';
// CB.c = CB.c >> 1;
}
}
r Replacing division with multiplication and small integer powers by repeated products.
r Insuring that e.g. a product that is repeatedly evaluated in an inner loop but always
results in the same value is moved to an outer loop or outside all loops. Similarly,
if the same calculation is repeated in an inner loop, its result should be stored in an
appropriate array or matrix variable. As an example:
r Improving the speed of numerical algorithms. For complex tasks, this often requires a
sophisticated program library. However, for well-behaved problems, compact programs
such as those found in this text often perform more rapidly. Such code can be tailored
to the specific problem, as, for example, if certain input variables are never altered or
all elements of an array are identical.
14.15 Program optimization 151
r Use of appropriate data structures. The efficiency of numerical methods that frequently
access data can often be improved by replacing arrays with appropriate data structures,
such as representing a sparse array by a linked list.
r Selecting the highest correctly functioning optimization flag. Compilers generally pos-
sess optimization flags that require the absence of certain programming structures if
they are to function as intended. Consequently, a program should be compiled and run
without optimization and the results compared with those obtained at higher optimiza-
tion levels.
r Inlining functions: small functions that are called frequently during execution should
be declared inline (recall, however, that functions appearing in the body of a class
definition are normally automatically inlined).
Chapter 15
Creating a Java development
environment
While scientific programs are less frequently written in Java than in C++, in
certain contexts, such as internet applications, the enhanced Java feature set sig-
nificantly shortens development time. Since many high-level constructs in Java
reflect an involved and largely hidden underlying structure that often precludes
a description in terms of a compact set of underlying principles, the subsequent
discussion focuses on compact code samples that illustrate the most significant
aspects of the language. Once a basic understanding of Java has been acquired,
specialized programming tasks can often be addressed by extending these sam-
ples while consulting a full list of the specialized functions available in the
language.
While numerous free integrated Java development environments exist, flags
can be set inadvertently, leading to anomalous behavior that often proves difficult
to correct. Additionally, the details of the Java file structure which constitutes
an important feature of the language are often obscured. This text therefore
employs a command-line compiler, the Java DISLIN graphics package and a
text editor. Downloading, installing and testing these components is described
below.
152
15.2 Command-line operation 153
X:\dislin, created at the beginning of the C++ section of this book will be
overwritten.
Next double click on Control Panel and then double click on the System icon.
Click on the Advanced System Settings pushbutton or the Advanced tab and then
the Environment Variables pushbutton near the bottom of the pop-up window.
In the System Variables listbox find the entry marked Path. Click on this entry
to highlight it and select Edit. Right click in the text-entry field labeled Variable
Value and with the right-arrow on your keyboard advance to the end of the text
string. Without introducing a space, add (after again replacing X by the letter of
the appropriate drive, C or D)
;X:\Program Files\Java\jdk1.6.0_17\bin;X:\dislinjava\win;
to the end of the string (be sure to place a semicolon between each directory
entry and the next, as above). Replace, however, 1.6.0_17 in the line above
with the corresponding number of the JDK version that you have downloaded.
This number can be found by employing e.g. Windows Explorer to navigate
to the X:\Program Files\Java directory and examining the name of the JDK
subdirectory. Finally, if DISLIN for C++ is not installed, repeat this last step with
the Variable Name DISLIN and Variable Value field X:\dislinjava and depress
OK a third time. If DISLIN for C++ has, however, already been installed, find
DISLIN in the list of system variables, highlight it, press Edit and replace the
C++ install directory, normally X:\dislin, with X:\dislinjava. Depress OK a
final time to exit the Environment Variables menu page. However, the DISLIN
environment variable now does not evaluate to the directory required for C++.
Consequently, either the preceding step should be reversed when programming
in C++ or, before compiling DISLIN C++ programs, enter the command (be
sure not to include spaces around the equality sign)
set DISLIN=X:\dislin
from within the Command Prompt window (see below). You can determine
whether the system variables have been properly set afterward by entering e.g.
echo %DISLIN%
Alternatively, retain the C++ system variable setting and enter set
DISLIN=X:\dislinjava in the command window before compiling Java DIS-
LIN programs.
operation is reviewed in this section. The normal procedure for opening a com-
mand window is to select Start → Programs → Accessories → Command Prompt
from the Start button. Next type
cd X:
where X should again be replaced by the letter of the (logical) drive on which the
programs will be stored. To navigate into the root directory of X:, type
cd \
mkdir programs
dir
del filename
deletes the file named filename, which must include the three-letter extension
when present, such as program.cpp
rmdir subdirectoryname
from the directory above an empty subdirectory with the name subdirectory-
name removes it. In specifying file names, the asterisk * can be employed to
match any sequence of numbers or letters except for the period that separates the
file name from the file extension, while a question mark matches any single valid
character. Thus del * deletes all files without three-letter extensions, del *.cpp
deletes all files with the extension .cpp, del h?.* deletes all files with names
15.4 DISLIN applet 155
beginning in h and that possess a two-letter file name and del *.* deletes all files
in the directory.
Save this code in the directory created in the preceeding section as the .java
source file HelloWorldApp by selecting File → Save from the menu bar at the
top of the editor or depressing the third (save) floppy-disk icon on the button bar
and navigating to the myprograms directory. Be sure to save the file as type Java
source file so that it acquires a .java extension (it will automatically be saved as
HelloWorldApp.java). The file name must be properly capitalized.
Java commands can now be entered directly into the command-line window
opened in the previous section. However, Notepad++ provides a more conve-
nient toolbar item through the Execute → Open current dir cmd menu item.
After either selecting this item or opening a command-line window, enter cd
X: followed by cd myprograms inside the resulting command window to navi-
gate to the directory containing the program above, and then issue the command
javac HelloWorldApp.java. If the program was entered correctly, a new file
HelloWorldApp.class will be created, as can be verified by typing dir or dir h*.
If error messages appear, return to Notepad++, correct and save the program
and recompile with javac. Once the .class file has successfully been created,
entering java HelloWorldApp generates the message “Hello World” together
with a graph.
Save this as a .html file by selecting File → Save as menu item from the menu
bar. To view the .html page select Run → Launch in (the desired browser) on the
Notepad++ menu bar. If the page is changed, the browser should be closed and
this menu item reselected.
called and the enclosed graphics directives executed. Since such constructs,
which cannot be predicted from a set of underlying language principles, occur
frequently, the reader is advised at least initially to solve problems by modifying
preexisting programs wherever possible.
15.6 Packages
Every Java type must be encapsulated within a class or a related entity, namely an
interface, enumeration or annotation (a Java version of a template). Each Java file
must contain a single public class with the same name (including capitalization)
as the file but without a .java extension.
In order to avoid name collisions and thus facilitate dynamic loading of classes
at runtime Java replaces the C++ namespace with a package structure such that
every class belongs to a package. While a class name is a single, normally capital-
ized word, a package name normally consists of several words joined with periods,
such as java.awt. This name serves as an effective namespace for each element
in the package, so that e.g. an object (reference) of a class Printer in java.awt
can be defined by java.awt.Printer P1;. The current directory functions as an
unnamed default package such that any class file is visible to other classes in the
directory. A user-defined package labeled packageName is generated by insert-
ing package packageName; at the beginning of every file in the package. These
files must further be placed in a subdirectory of the active directory with the same
name, packageName, as the package. If the file is instead included in a subdirec-
tory, packageName1, of packageName, then packageName must be replaced
by packageName.packageName1 throughout. Suppose a class MyClass exists
inside the file MyClass.java that is in turn situated in the package packageName
that is a subdirectory of the current directory. Then, a Java program in the cur-
rent directory can create an object of type MyClass, either with the statement
packageName.MyClass1.MC1 = new packageName.MyClass( ); or by begin-
ning the program with import packageName.* to import all files in the
package followed by MyClass MC1 = new MyClass( );. A single file
in this package is imported through import packageName.MyFile. If the
class MyClass appears also in a second imported package, the full quali-
fier packageName.MyClass.MC1 is required when referring to MC1. If the
packages are located in subdirectories of different directories than the cur-
rent directory, these directories can be automatically incorporated by set-
ting the CLASSPATH environment variable. For example, if CLASSPATH
=.;C:\dir1\packageName package names are referred either to the current
directory (.) or to C:\dir1\packageName.
The basic Java library comprises 23 packages. The “java.lang” core language
classes are implictly imported; that is, the Java environment functions as if the
statement import java.lang.*; were present at the beginning of each program.
Classes belonging to java.lang include
158 A Java development environment
as well as the Byte, Double, Float, Integer, Long and Short classes, which
contain functions that act on their respective data types. Additional Java packages
that appear in this text include
java.applet // classes for applets
java.awt // GUI classes
java.awt.event // event classes
java.io // input/output classes
java.lang.reflect // reflection API classes
Note that package or import statements must appear first in a program, which
must contain a single public class definition with the same name as the file. To
illustrate,
Program PackageExample.java in the directory X:\rootdirectory
and
entity, which is defined without an access specifier, is visible only from within
its package (even if it is imported into another program). A protected entity,
which is less restrictive than default, is visible from its package as well as from
its subclasses (that can be located in other packages) and, finally, a private
entity is visible only from within its enclosing class. In the above program the
internal class ExponentialConstants within PackageExample is private and
can be accessed only by member functions of the class itself. However, the
internal members of the class Multiply are all declared public so that they
can be accessed by PackageExample from outside the myFunction package.
If, however, the keyword public is removed from public class Multiply, the
elements of Multiply, while declared public, acquire the package access of their
enclosing class and are no longer accessible to main( ) in PackageExample. The
return type of a function must immediately precede its name, thus static public
int f( ) is valid but not int public f( void ).
import myFunction.*;
class ExponentialConstants {
double iE = 2.7;
}
package myFunction;
public class Multiply{
public class MyConstant {
public double iPi = 3.14;
}
public MyConstant iMC1 = new MyConstant( );
public void f( int a1, int a2 ) {
Multiply M1 = new Multiply( );
System.out.println( M1.iMC1.iPi * a1 * a2 );
}
}
Chapter 16
Basic Java programming constructs
While in Java all code must be contained within an encompassing class definition,
inside this class the code can closely resemble a procedural program. Accordingly,
we first examine these basic features, postponing a detailed discussion of classes
and objects until the subsequent chapter.
16.1 Comments
Text to the right of the delimiter // is ignored by the compiler, as is any text
enclosed between the starting and terminating delimiters /* and */, as in
int m = 10; // Comment 1
int n = 10; /* Comment
2 */
Additionally, text between the starting delimiter /** and ending delimiter */ is
employed by the javadoc utility included with the Java runtime library to generate
HTML documentation. Some relevant tags are
/**
@ author (author of a class)
@ version (version of a class)
@ see (link to related topic)
@ return (method return value)
@ param (method parameters)
@ exception (exception thrown by a method)
*/
The @ must be located in the first column unless a star (*) is present in this
column.
161
162 Basic Java programming constructs
newline \n
horizontal tab \t
vertical tab \v
backspace \b
carriage return \r
formfeed \f
question mark \?
single quote \’
double quote \”
16.3 Conversions 163
16.3 Conversions
Java automatically performs widening conversions from smaller to larger vari-
ables of compatible type. Since all numeric types are considered compatable,
byte n = 10;
int m = n;
If the range of m is larger than that of n, the value placed in n is the remainder
when m is divided by the range of n, whereas if m is a floating-point type and n
is an integer, any fractional component of m is discarded. Hence n above equals
10. Conversions occur automatically within expressions such that, if an operation
involves variables of two types, the smaller type is automatically promoted to
the larger type. Narrowing conversions of e.g. a double to an int are, however,
precluded even in assignment statements. For example, if d is a double and m
an int,
m = 3 / 2; // automatically truncated by removing the
// decimal part to m = 1
m = -3 / 2; // rounded in a similar fashion to m = -1
d = -3 / 2; // truncated and then converted to d = -1.0
d = -3. / 2; // yields d = -1.5
m = 3. / 2; // invalid
byte and short variables are automatically converted to int in arithmetic expres-
sions. Hence
byte b = 1;
byte c = b * b;
byte c = (byte) ( b * b );
is required.
16.4 Operators
Arithmetic operations in Java include +, -, *, / and, for integer a and b, the
remainder operator, a % b, which yields the remainder of a divided by b,
such that −3 % 2 is −1. Every operator possesses a precedence level and an
associativity rule. For example, the precedence of * and / exceeds that of + and –
, so that, in an expression containing both types of operators, * and / are evaluated
first. If the expression instead contains a sequence of operators with the same
precedence, the order of operation is determined by associativity. For example, =
is right-associative, so that a = b = c; means a = ( b = c );. In general, operators
are left-associative except where this generates logical inconsistencies. Thus, 5
/ 6 * 7 is evaluated as ( 5 / 6 ) * 7, not as 5 / ( 6 * 7 ), which causes numerous
programming errors.
As in C++, the basic precedence rules are
In all control expressions, the subsequent braces can be omitted if they enclose
a single statement, although braces are frequently erroneously omitted when
several statements are under the control of the logical construct. The variable
loop above is defined by the first of the three statements in the for structure,
cannot be subsequently redefined within the loop and is destroyed when the body
of the loop is exited.
A for loop can be replaced by the while statement:
166 Basic Java programming constructs
int loop = 0;
while ( loop < 10 ) {
sum += loop;
loop++;
}
int loop = 0;
do{
sum += loop;
loop++;
} while ( loop < 10 );
if ( logical expression ) {
statements1
}
else {
statements2
}
Control can be passed out of a running loop through the continue and break
statements. These can contain a label, permitting control to be transferred to the
end of an enclosing labeled block (which for continue must be an enclosing
loop):
switch ( loop ) {
case 1: { statements1 } break; // executed if loop == 1
case 2: { statements2 } break; // executed if loop == 2
default: { statements3 }
}
16.6 Enumerations 167
The break statements pass control to the first statement following the switch
block; otherwise, if present, the optional default statement will always be exe-
cuted together with the statements following all logical conditions that are
satisfied.
16.6 Enumerations
An enum type can be assigned only specific alphanumeric values. The syntax of
this construct is illustrated by
enum enumName {a, b, c};
public class test {
static public void main(String[ ] args ) {
enumName myEnum = enumName.b;
System.out.println( myEnum );
}
}
The only valid assignments to the enumName type variable myEnum are
enumName.a, enumName.b and enumName.c.
Chapter 17
Java classes and objects
class MyClass {
private int iVariable1 = 6;
public static int iVariable2;
public int getIVariable1( ) { return iVariable1; }
public void setIVariable1 ( int aVariable1 )
{ iVariable1 = aVariable1; }
MyClass ( int aVariable1 ) {
iVariable1 = aVariable1;
}
MyClass( ) { }
}
168
17.1 Class definition 169
Unlike C++, variables and functions within a Java file can be placed in any order,
since Java generates an internal list of the elements before compilation. Further,
javac checks all dependences on external files and will compile or recompile
any required external .java files that are either uncompiled or newer than their
corresponding .class files.
17.2 Inheritance
A class, DerivedClass, defined with the syntax
acquires (inherits) all public and protected internal variables and functions in
the parent, base class, BaseClass, except for elements that are explicitly redefined
in the derived class. A derived class also inherits all of the variables and functions
of a parent class with default (package) access, assuming that the two classes
are located in the same package. Additional variables and functions are then
provided in the derived class. In general, the relationship of a derived class to
its parent class represents an “is-a” specialization between two physical objects
such as “a pencil is a writing instrument”. A “has-a” relationship, typified by “a
pencil has an eraser” is termed containment, and is implemented by one class
possessing an internal class member of a different class type.
A class member whose definition is preceded by the keyword final cannot be
overridden in a subclass. If the class definition is preceded by final, it cannot be
subclassed. Final class variables (internal member variables) must therefore be
assigned values either in class constructors or, more commonly, through synthetic
constructors in the member variable list.
The super keyword refers to the immediate parent in a class hierarchy. For
example, the following procedure modifies (overrides) a parent class print( )
method while still exploiting its functionality:
MyClass MC1;
MC1 = new MyClass( );
Any reference variable that does not point to an object of its specified type
is assigned a null value so that attempting to access its internal members leads
to an error condition. To simultaneously avoid memory leaks, memory that is
dynamically allocated once an object has been created through a new state-
ment is deallocated after the system has determined that no references to the
object exist. This automatic reclaiming of memory – which occurs at periodic
time intervals – is termed garbage collection and can be forced through the
statements
Runtime R = Runtime.getRuntime( );
R.gc( );
void finalize( ) {
...
}
Accordingly, when a Java object “reference” is passed, the address of the object
is copied. Thus interchanging two objects or assigning a new object to a ref-
erence variable inside a function leaves the parameters in the calling program
unchanged:
For the same reason, if a value in the function reference argument is altered, it
changes in the calling program:
Note again that the function newValue must be declared static since the static
main( ) method cannot address a non-static function without first generating
an object of type Test. However, the static function newValue can change the
internal variables of aC since it is passed a reference to a preexisting object
through the parameter list.
The standard mathematics functions with minor modifications are accessible
by prefixing the name of the function with Math, as for example Math.exp(
doubleValue ) and Math.pow( a, b ), which is a b . Similarly, M_PI in Dev-C++
must be replaced by Math.Pi, while M_E (the value of e) is replaced by Math.E.
The prefix Math. can be made superfluous by placing the line
import static java.lang.Math.*;
17.4 Exceptions
Java program faults that leave the program in a state that can be further influ-
enced by additional user-provided routines are termed exceptions, while errors
(e.g. VirtualMachineError) typically cannot be handled by the program. Java
exceptions subclass the Exception type, through additional inheritance levels
of predefined exception types such as ArithmeticException, SecurityExcep-
tion, NoSuchMethodException, IOException, and NoSuchFieldException.
Any such types can be further subclassed by writing e.g.
public class myException extends ArithmeticException { ...
where the try block can contain any portion of a program including various
function calls. If the exception is thrown, the catch and finally blocks are executed
and the program continues with the first statement following the finally block. If
continue or break clauses are encountered in control logic constructs, the finally
174 Java classes and objects
clauses are implicitly activated; thus if the following lines are encountered within
a function
import java.io.*;
try { switch ( k ){
case 0: throw new IOException( );
case 1: throw new Exception( );
case 2: break; }
System.out.println( ''End'' );
}
catch ( IOException e ) { System.out.println( ''IO'' ); }
finally { System.out.println( ''F'' ); }
then for k = 0 the code handles the exception so that output is IO and F, for
k = 1, F is printed and the exception is passed to the caller through a throws
Exception clause in the enclosing function signature, while for k = 2 only F is
printed.
which is printed with System.out.println( SB1 );. The buffer contents are
changed through functions such as insert( int index, String String2 ), which
inserts String2 into the original string at the position of index. A string can
further be parsed, i.e. divided into a set of tokens (words), by first placing it into
a StringTokenizer class
where the optional argument Delimiters contains the characters that indicate
the start or end of each token. Default delimiters include the whitespace char-
acters space, tab, newline and carriage return. Subsequently, each token in the
string is obtained with ST1.nextToken( ). Strings are converted into the various
numerical types by e.g. Double.parseDouble( S ), where S is a string (a Num-
berFormatException is thrown if the string cannot be interpreted as a numeric
value).
Finally, all classes possess toString( ) functions. However, the default
toString( ) function, if not overridden by a class method, prints out the class
name followed by a (generally unique) hexadecimal code (hash code) related
to the memory address of the object. To print out meaningful information
about an object, each user-defined class should contain a public function
String toString( ) that instead prints a formatted description of all internal
class variables, calling in this process the toString( ) function of any internal
class member variables of non-primitive, object type. The toString( ) function
is called automatically if the object appears as an argument of, for example,
System.out.println( ).
Arrays. An array comprises an index-accessible sequence of variables of a similar
type that in Java can be both declared and initialized through the allocation of
storage space by either of the two equivalent statements
int a[ ] = { 0, 1, 2, 3 };
176 Java classes and objects
To alter the contents of the array after it has been defined, however, requires
iteration, as in
If arrays are equated, both of them refer to the same memory. Changing an
array element through one array name then changes the corresponding element
referred to by the second array name,
import java.util.Vector;
Vector <Double> Vector1 = new Vector<Double>;
Vector1.addElement ( new Double( 3.5 ) );
System.out.println( Vector1.elementAt( 0 ) ); // Output: 3.5
System.out.println( Vector1.size( ) ); // Output: 1
To enter primitive types from the terminal (or a String or any InputStream) we
use the syntax
which sends the string “Hello World” to the terminal together with a carriage
return. Replacing println with print eliminates the carriage return. Integer num-
bers are formatted by
after which
yields the output 100,000. Calling the methods of NF1 yields alternative output
formats. Floating-point number formatting is specified by
java.text.DecimalFormat DF1 =
new java.text.DecimalFormat( ''00.####E0'' );
double d = -123.45;
System.out.println( DF1.format( d ) ); // Output: -12.345E1
which prints three digits after the decimal point, followed by an E and the
mantissa. If a 0 appears in the argument of DecimalFormat, a zero is inserted if
a number is absent in the indicated position, while a # sign instead places a blank
character is this position.
178 Java classes and objects
MyProgram accepts input from MyInputFile in the same manner as from the
keyboard and sends output to MyOutputFile.
Chapter 18
Advanced Java features
class C {
public void print( ) {
System.io.println ( ''C'' );
}
}
class D extends C {
public void print( ) {
System.io.println ( ''D'' );
}
}
179
180 Advanced Java features
Accordingly, containers that accept this type act as templates for storing any
variety of objects, but they must subsequently be downcasted from Object to
their actual type through an explicit cast in order to access their specialized
class properties.
abstract class C {
abstract void print( );
}
class D extends C {
void print( ) {
System.out.println ( ''D'' );
}
}
A subclass that does not supply all these definitions must also be marked abstract.
An abstract object cannot be instantiated since all its methods are not fully
defined. However, dynamic method dispatch allows a C reference variable to be
assigned an instance of D at runtime. For example, a call to the print( ) function
through this reference then calls the subclass method. The abstract function
declaration thus serves as a template, that is, specifies a generic form, for the
structure of the derived classes.
18.3 Interfaces
Interfaces provide an alternative to abstract classes. An interface specifies meth-
ods that must be implemented in all classes (often functionally unrelated) that
are derived from (conform to) the interface. An interface and its implement-
ing classes generally require default or public access. Initialized variables in an
interface are implicitly public, static and final since they cannot be changed
by these classes, while all interface methods are implicitly public and must be
declared public in implementing classes, for example,
interface PrintInterface {
void print( );
double pi = 3.14;
}
18.3 Interfaces 181
Since interfaces are distinct from classes, two classes that are unrelated in
their class hierarchy can implement the same interface. This feature can, for
example, be employed to import long lists of constants into any implementing
class. All member functions that conform to an interface in a class, e.g. the
print( ) function above, must possess the same signature as the function in the
interface. Interfaces can extend other interfaces, as in
interface IArgument {
double fun( double aX );
}
class Square implements IArgument {
public double fun( double aX ) { return aX * aX; }
}
class Cube implements IArgument {
public double fun( double aX ) { return aX * aX * aX; }
}
import java.awt.event.*;
public class MyGraph extends Applet implements MouseListener {
MyGraph( ) { addMouseListener( this ); }
public void mouseClicked( MouseEvent event ) {
MyPoint = event.getPoint( );
repaint( ); }
public void mouseEntered( MouseEvent event ) { } ...
Following this method, the bodies, which may be empty, for all of the functions
in the MouseListener interface must be provided to specify the action, if any,
that occurs when a mouse is clicked, enters or leaves an active window and so
on.
As an alternative, one can employ an anonymous inner adapter class that
implicitly provides empty bodies for the functions in the interface so that only
the functions that have non-empty bodies have to be provided:
The words implements MouseListener are then omitted from the signature of
main( ). As an example of the first procedure, the following graphics program
generates a canvas of size 600 by 800 pixels within a window frame labeled
“Drawing”. Text is placed on the canvas and the program then draws a cyan oval
and a line from a point 100 pixels down and to the right of the upper left-hand
18.4 Java event handling 183
corner of the window to the point at which the mouse is clicked. The program
terminates when the mouse leaves the canvas area:
import java.awt.*;
import java.awt.event.*;
public class GraphExample extends Canvas implements MouseListener{
public int iX = 0;
public int iY = 0;
private static Frame window = new Frame( ''Drawing'' );
public static void main ( String[ ] args ) {
GraphExample g = new GraphExample( );
g.setSize( 600, 800 );
window.add( g );
window.pack( );
window.setVisible( true );
}
GraphExample( ) { addMouseListener( this ); }
public void paint( Graphics g ) {
g.drawString( ''A Graph'', 400, 400 );
g.setColor( Color.cyan );
g.fillOval( 300, 300, 80, 275 );
g.drawLine( 100, 100, iX, iY );
}
public void mouseClicked( MouseEvent event ) {
iX = event.getX( );
iY = event.getY( );
repaint( );
}
public void mouseExited( MouseEvent event ) {
System.exit( 0 );
}
public void mouseEntered( MouseEvent event ) { }
public void mouseReleased( MouseEvent event ) { }
public void mousePressed( MouseEvent event ) { }
}
The above code can be turned into an applet that instead applies the Mouse-
Adapter interface with the code below. This program also contains a push button
that generates an audible sound when pressed, through the code written into the
Beep class below that is registered with an ActionListener:
import java.awt.event.*;
import java.applet.Applet;
import java.awt.*;
class Beep implements ActionListener {
public void actionPerformed ( ActionEvent Event ) {
Component C1 = (Component)Event.getSource( );
C1.getToolkit( ).beep( );
GraphExample2 g = new GraphExample2( );
}
}
184 Advanced Java features
The init( ) and stop( ) methods are called when an applet is created and exited,
respectively.
18.5 Multithreading
Every Java program starts inside the main thread, which can be accessed
through
Thread T1 = Thread.currentThread( );
To synchronize the threads so that each thread waits until the second thread has
completed, they must share access to an object that locks the threads:
class MyStaticObject {
static Object O = new Object( );
}
class Thread1 extends Thread {
public void run( ) {
synchronized ( MyStaticObject.O ) {
for (int loop = 0; loop < 10; loop++) {
System.out.println( loop );
yield( );
}
}
}
}
class Thread2 extends Thread {
public void run( ) {
synchronized ( MyStaticObject.O ) {
for (int loop = 0; loop < 10; loop++) {
System.out.println( loop );
yield( );
}
}
}
}
class TestThread {
public static void main( String args[ ] ) {
new Thread1( ).start( );
new Thread2( ).start( );
}
} // Output: 0 1 2 3 ... 0 1 2 3 ... 10
186 Advanced Java features
18.6 Serialization
An object can be saved and reconstructed through the serializable interface.
Object serialization refers to the persistent (permanent) storage of an object,
normally by writing the object contents to a serialized file. All fields in the
object, including fields inherited from its superclasses, are saved in this manner,
except for those explicitly marked as transient. If a field refers to another object,
such as an object that contains a second object as a class member (a field), the
object that is referenced is also serialized. To serialize an instance of a class
MyClass below, a file output stream is defined to provide a file into which the
object will be written. An ObjectOutputStream is defined and is passed the
file output stream as a parameter. The object is then written by the writeObject
serialization method of ObjectOutputStream:
import java.io.*;
class MyClass implements Serializable {
private double[ ] myArray = { 0, 1, 2 };
public double getArrayElement( int aI )
{ return 2 * myArray[aI]; }
}
import java.io.*;
public class ReadSerializableExample {
public static void main( String args[ ] ) throws IOException,
ClassNotFoundException {
ObjectInputStream OI1 = new ObjectInputStream(
new FileInputStream( ''myfile.ser'' ) );
MyClass MyObject = (MyClass) OI1.readObject( );
OI1.close( );
System.out.println( MyObject.getArrayElement( 0 ) );
} // Output: 2
}
function. However, arrays of generic types cannot be created. A class that stores
and can assign a value to any object as an internal class member is
class MyObject<T> {
T iT;
void setT( T aT ) { iT = aT; }
}
class Generic {
As illustrated above, functions can also be defined with type parameters. If such
a function is called without specifying the type parameter, the type is generally
inferred; that is, calling ClassType( D1 ), where D1 is a Double object above,
prints out the name of the class to which D1 belongs, namely java.lang.Double.
Chapter 19
Introductory numerical analysis
df f (x + x) − f (x) +
= lim ≡ lim Dx (f) (19.1)
dx x→0 x x→0
+
In numerical analysis, Dx ( f ) is termed the discrete forward finite-difference
operator, while x , which remains finite in numerical computations, is labeled
the step size or point spacing. The continuous and discrete operators in Eq. (19.1)
differ by an error term that normally varies as a polynomial function of Dx. If
the smallest power of Dx appearing in this polynomial is N, the error decreases
as (Dx)N when x → 0 and the algorithmic accuracy is said to be O(x)N . To
188
19.1 The derivative operator 189
determine N for the derivative approximation of Eq. (19.1), consider the Taylor-
series expansion of a continuous and infinitely differentiable function f about the
point x :
d f (x) (x)2 d 2 f (x)
f (x + x) = f (x) + x + + ··· (19.2)
dx 2! dx2
Inserting Eq. (19.2) into Eq. (19.1) yields
f (x + x) − f (x) d f (x) x d 2 f (x)
= + + ··· (19.3)
x dx 2 dx
which implies, if d 2 f (x)/d x = 0,
d f (x) +
= Dx ( f ) + O(x) (19.4)
dx
+
Since the forward difference expression Dx ( f ) is an operator acting on the
function f, code that represents this operator should possess a function with a pro-
totype such as double aF( double ); as one argument. Recall from Section 6.16
that a function name is an alias (alternative name) for and thus evaluates to the
compiler-assigned memory location of the first instruction of a binary repre-
sentation of the instructions comprising the function (the function record). This
address can be stored in a pointer to a function of a matching type at compile
time or runtime. Since the pointer evaluates to the same starting memory address
as the function name, it can subsequently be passed to a derivative function as a
parameter:
main ( ) {
double deltaX = 1.e-1;
double xValue = 1.0;
int choice;
double (*myFunction) (double); // Stores memory
// address of function
cout << ''Choose a function 1 - cube, 2 - square '' << endl;
cin >> choice;
switch ( choice ) {
case 1: myFunction = cube; break; // Output: 3.31
case 2: myFunction = linear; break; // Output: 1
default: cout << ''Incorrect Input - program exiting'';
}
cout << derivOperator( myFunction, xValue, deltaX );
}
190 Introductory numerical analysis
E
1/n
xnew = x
(19.5)
ε
main ( ) {
double deltaX = 1.0e-1;
double xValue = 1.0;
float x[10], y[10]; // array definitions
for ( int loop = 0; loop < 10; loop++ ) {
y[loop] = derivOperator( cube, xValue, deltaX );
x[loop] = deltaX;
deltaX /= 2;
}
metafl( "XWIN" ); // write to terminal
disini( ); // start plotting program
19.3 Graphical error analysis 191
Figure 19.1
which yields Figure 19.1, verifying the linear dependence of the error on step
length without a mathematical analysis. Similarly, the result R(x) of an O(x)α
numerical procedure plotted as a function of (x)α yields a straight line for
small Dx whose y-intercept is the corrected value. Alternatively, if the result
Rx→0 can be estimated or extrapolated, the associated error estimate varies as
O(x)α = R(x) − Rx→0 for x → 0. Taking the logarithm of both sides yields
log(x)α = α log(x) = log(R(x) − Rx→0 ) so that α corresponds to the slope
of the logarithm of this estimated error plotted against log(x) (the slope of a
log–log plot).
192 Introductory numerical analysis
File: cube.m
function output = cube( aInput )
output = aInputˆ3;
File: derivOperator.m
function output = derivOperator( aF, aXValue, aDeltaX )
output = ( aF( aXValue + aDeltaX ) - aF( aXValue ) ) / aDeltaX;
File: derivplot.m
deltaX = 1.0e-1;
xValue = 1.0;
for loop = 1 : 10
y( loop ) = derivOperator( @cube, xValue, deltaX );
x( loop ) = deltaX;
deltaX = deltaX / 2;
end
plot( x, y, '-s' )
axis tight;
xlabel( 'Step Length' );
ylabel( 'Error' );
Figure 19.2
and repeating the calculation of Figure 19.1 with Eq. (19.7) yields Figure 19.2.
The dependence of the reduced numerical error on (x)2 is apparent.
19.5 Extrapolation
As an alternative to graphically extrapolating results for different x , if the order
of accuracy of an arbitrary numerical procedure is known, the results of the
method for different step sizes can be combined algebraically. For example, if
the result of a calculation with a step length x with an error term of the form
O(x) = a x + O(x)2 , where a represents the x Taylor-series coefficient in
O(x), is added to the result for the same calculation but with a step length
−x , the combined error is a x + O(x)2 + (−a x + O(−x)2 ) = Õ(x)2 . By
similarly combining results for step lengths of ±x, ±2 x, . . ., the accuracy of
any numerical method can generally be greatly increased if the output varies
sufficiently smoothly as a function of x .
stores these quantities, computes the value of the derivative when its calculate
button is depressed:
class DerivativeCalculator {
public:
void setDx( double aDx ) { iDx = aDx; }
void setX( double aX ) { iX = aX; }
double dx( ) const { return iDx; }
double calculateDerivative( ) const
{ return ( iF( iX + iDx ) - iF( iX ) ) / iDx; }
DerivativeCalculator( double aX, double aDx, double aF( double ) ) :
iX( aX ), iDx( aDx ) {
iF = aF;
}
private:
// Store function address as an internal variable
double (*iF) ( double );
double iDx;
double iX;
};
main ( ) {
double deltaX = 1.e-1;
double xValue = 1.0;
int choice;
double (*myFunction) ( double );
cout << ''Choose a function 1 - cube, 2 - square ''<< endl;
cin >> choice;
switch ( choice ) {
case 1: myFunction = cube; break; // Output: 3.31
case 2: myFunction = linear; break; // Output: 1
default: cout << ''Incorrect Input - program
exiting'';
}
DerivativeCalculator DC1( 1.0, 0.1, myFunction );
cout << DC1.calculateDerivative( );
}
19.7 Integration
The definite integral
x
I L f (x) = f (x ′ )d x ′ (19.9)
L
is again an operator that transforms the integrand function f (x) into a second
function I L f (x). This continuous operator is again implemented numerically by
referring to its underlying definition as a limit of a discrete expression,
n−1
I L f (x) = lim InL f (x) = lim x ak (19.10)
n→∞ n→∞
k=0
in which x = (x − L)/N and the ak are suitably chosen values of f (x) within the
interval [x + k x, x + (k + 1)x]. The summation limits from 0 to n − 1 yield n
intervals of length x . A common, subtle programming error is to set the upper
19.7 Integration 195
limit to n, yielding an O(x) numerical error that is often mistaken for the error
of the numerical method.
In calculus, two possible choices for the ak are commonly cited. The rectan-
gular rule,
ak = f (L + k x) (19.11)
evaluates the function at the left endpoint of each interval, while the midpoint
rule
ak = f (L + (k + 0.5)x) (19.12)
instead employs the interval midpoint. The midpoint rule Octave program
File myMidpoint.m:
function result = myMidpoint( aFunction, leftEndPoint, ...
rightEndPoint, numberOfIntervals )
deltaX = ( rightEndPoint - leftEndPoint ) / numberOfIntervals;
result = deltaX * sum( aFunction( leftEndPoint + 0.5 * deltaX + · · ·
deltaX * ( 0 : numberOfIntervals - 1 ) ) )
While the midpoint and rectangular rules yield identical results in the con-
tinuous limit (Dx → 0), the discrete errors differ. For example, the difference
between the rectangular-rule approximation and the exact result equals
n−1 xk +x
However, if the magnitude of the first derivative of f (x) is bounded on the interval
x ∈ [L , R] by M1 , then over the entire interval
M1 n(x)2 M1 (R − L)x
|E rect | < = (19.15)
2 2
since nDx is the total length of the integration interval. Therefore, unless there
occurs an exceptional cancellation between positive and negative contributions
to the total error, the error varies linearly with x since the error in each discrete
interval is O(x)2 but the errors add over L/x intervals. The overall error,
however, decreases to O(x)2 with the midpoint rule, as can be demonstrated by
extending the above methodology.
196 Introductory numerical analysis
The following Octave program implements Simpson’s rule over an interval from
leftEndPoint to rightEndPoint, noting that the values of f (x1 ), f (x2 ), . . . , f (xn−1 )
are evaluated twice in the above expression, yielding weights of 1, 4, 2, 4, 2, . . . ,
4, 1:
aRightLimit = midpoint;
end
end
result = ( aLeftLimit + aRightLimit ) / 2;
Newton’s method can converge slowly or even diverge unless the derivative is
evaluated analytically. Otherwise, once the numerical estimate is close to the root,
the O(x)2 error in the central difference operator imparts an error of the same
magnitude to the root value. While this problem can be alleviated with a high-
order approximation for the derivative and a small delta, truncation errors then
increase, especially if f (x) varies slowly near the root. Further, if x1 is not suffi-
ciently near the root, the sign of d f /d x can be the opposite of its sign at the root.
Hence, x2 is positioned further from the root than x1 , and successive iterations
generally diverge. An interval [a, b] containing the root can then be selected with
x1 set to a. If x2 falls within the interval, Newton’s method is applied; otherwise,
the bisection method is employed to generate an improved starting value.
19.9 Minimization
While the minimum or maximum values of a function can be obtained from
the roots of its numerical derivative, approximating the derivative operator yields
19.9 Minimization 199
The minimum of the parabola passing through the function values at the three
evaluation points can also be employed to improve significantly the choice of
points in the successive iteration step.
Chapter 20
Linear algebra
20.1 Matrices
A linear transformation of variables, or equivalently a linear system of equations,
can be represented by a matrix. For example, the Lorenz transformation from a
system to a second system traveling at a relative velocity v, namely
ct ′ = γ (ct − βx)
x ′ = γ (x − βct) (20.1)
′
y =y
z′ = z
with β = v/c and γ = 1/ 1 − v 2 /c2 is linear since scaling all the input variables
scales the output by the same factor. The transformation is equivalently expressed
as
⎛ ′⎞ ⎛ ⎞⎛ ⎞
ct γ −βγ 0 0 ct
⎜ x ′ ⎟ ⎜−βγ γ 0 0⎟ ⎜ x ⎟
⎟ ⎜
⎜ ⎟=⎜
⎝ y′ ⎠ ⎝ 0
⎟ (20.2)
0 1 0⎠ ⎝ y ⎠
z′ 0 0 0 1 z
200
20.2 Linear-equation solvers 201
c0 + 2c1 = 1
(20.4)
3c0 + c1 = 2
illustrates the procedure. Multiplying the first equation in this set by 3 and
subtracting it from the second equation yields the tridiagonal system
c0 + 2c1 = 1
(20.5)
−5c1 = −1
In the back-substitution step, the second of these equations is solved for c1 and
the result inserted into the first equation to obtain c0 .
By analogy, subtracting Ai0 /A00 times the first row of Eq. (20.3) from each row
i = 0 generates a new equation system for the cj with elements
(1) Ai0 (1) Ai0
Ai j = Ai j − A 0 j , bi = bi − b0 (20.6)
A00 A00
Here the first element is absent from each row except the first (i = 0) row. This
procedure is then repeated within the submatrix formed by excluding the first
row and column from the matrix system. After N – 1 iterations, the tridiagonal
equation system
the process is repeated until the first equation is reached. This yields
(N −1)
(N −1)
c N −1 = b N −1 A N −1,N −1
(N −2) (N −2) (N −2)
c N −2 = (b N −2 − A N −2,N −1 c N −1 ) A N −2,N −2 (20.8)
(N −3) (N −3) (N −3) (N −3)
c N −3 = (b N −3 − A N −3,N −2 c N −2 − A N −3,N −1 c N −1 ) A N −3,N −3
With the above notation, Gaussian elimination is coded in C++ as (where the
input two- and one- dimensional array parameters aA and aB are overwritten)
#include <stdio.h>
const int n = 2;
// Note: both aA and aB are overwritten
void gauss( double aA[ ][n], double aC[ ], double aB[ ] ) {
// Forward elimination
for ( int i = 0; i < n; i ++ ) {
if ( !aA[i][i] ) exit( 0 );
for ( int j = i + 1; j < n; j++ ) {
double d = aA[j][i] / aA[i][i];
for ( int k = i + 1; k < n; k++ )
aA[j][k] -= d*aA[i][k];
aB[j] -= d*aB[i];
}
}
if ( !aA[n-1][n-1] ) exit( 0 );
// Back substitution
for ( int i = n - 1; i >= 0; i-- ) {
aC[i] = aB[i];
for ( int j = i + 1; j < n; j++ )
aC[i] -= aA[i][j] * aC[j];
aC[i] /= aA[i][i];
}
}
main( ) {
double a[n][n] = { { 1, 2 }, { 3, 8 } };
double b[n] = { 2, 5 };
double c[n];
gauss( a, c, b );
cout << c[0] << '\t' << c[1] << endl;
}
main( ) {
double diagonal[2] = { 1, 8 };
double upperCodiagonal[1] = { 2 };
double lowerCodiagonal[1] = { 3 };
double inputVector[2] = { 2, 5 };
double outputVector[2];
double scratch[2];
tridiagonalSolver( lowerCodiagonal, diagonal, upperCodiagonal,
inputVector, outputVector, scratch, 2 );
cout << outputVector[0] << '\t' << outputVector[1] << endl;
}
Often the check for non-zero components of bsave can be omitted. If Ai j remains
unchanged over multiple realizations, the above codes can be accordingly modi-
fied to improve efficiency.
In Octave, function parameters are passed by value so that additional (scratch)
space for preserving the function arguments is superfluous. A tridiagonal matrix
program for either column and row vectors that does not examine bsave can be
coded as
in which the Yi (x) are given functions, from noisy experimental data by minimiz-
ing the deviation of the model predictions from the data.
Given a set of N measurement points (xi , yi ), if the random measurement error
at each xi is known beforehand to be σi and the actual physical value of the output
20.4 Application: least-squares procedure 205
variable is yexact (xi ) , the probability of observing the measured value yi for errors
distributed according to a Gaussian (normal) probability distribution is given by
which requires
N
M 2
∂χ 2 ∂ 1
= a j Y j (xi ) − yi
∂ak ∂ak σi2
i=1 j=1
N
M M
1 ∂
=2 a j Y j (xi ) − yi a j Y j (xi ) − yi
σi2 ∂ak
i=1 j=1 j=1
=0 (20.13)
or, in terms of the N × M matrix A with Ai j = Y j (xi )/σi and the vector b with
bi = yi /σi ,
Ma ≡ AT A a = AT b (20.15)
If all measurements possess the same error, Eq. (20.15) becomes M̃a = d, with
N
N
M̃ k j = Yk (xi )Y j (xi ) , dk = Yk (xi )yi (20.16)
i=1 i=1
If there exist fewer xi values than parameters in the model function, so that
N < M, the equation system of Eq. (20.15) is underdetermined. The singular
value decomposition (SVD) method is then employed to obtain an approximate
solution.
206 Linear algebra
which yields
N N N
1 xi yi
a1 + a2 − =0
σi2 σi2 σi2
i=1 i=1 i=1
(20.18)
N N N
xi xi2 xi yi
a1 + a2 − =0
σi2 σi2 σi2
i=1 i=1 i=1
and therefore
N N N N N N N N
yi xi2 xi xi yi 1 xi yi xi yi
− −
σi2 σi2 σi2 σi2 σi2 σi2 σi2 σi2
i=1 i=1 i=1 i=1 i=1 i=1 i=1 i=1
a1 = N 2 , a2 = 2
N N N N N
1 xi2 xi 1 xi2 xi
− −
σi2 σi2 σi2 σi2 σi2 σi2
i=1 i=1 i=1 i=1 i=1 i=1
(20.19)
must be solved repeatedly. Here I is the identity matrix and x(i) is the ith estimate
for the corresponding eigenvector, where the initial estimate, x(0) can be chosen
effectively randomly. Expressing x(i) as a linear combinations of the eigenvectors,
Qk , of A,
N
(i)
x(i) = ck
k (20.21)
k=1
20.5 Eigenvalues and iterative solvers 207
Since x(i+1) approaches the eigenvector with eigenvalue nearest λ(i) , an improved
eigenvalue estimate is obtained from (x(i+1) )T Ax(i+1) ≈ λ(i+1) (x(i+1) )T x(i+1) or
(x(i+1) )T x(i)
λ(i+1) = λ(i) + (20.25)
(x(i+1) )T x(i+1)
The amplitude of x(i) varies exponentially with i and must be periodically
renormalized.
Chapter 21
Fourier transforms
208
Fourier transforms 209
as l = 4l3 + 2l2 + l1 and m = 4m 3 + 2m 2 + m 1 then the sum in Eq. (21.3) takes the
form
1 1 1
2π (4m 3 +2m 2 +m 1 )(4l3 +2l2 +l1 )
S(m) = s4l3 +2l2 +l1 e−i 8 (21.5)
l3 =0 l2 =0 l1 =0
The graph appears meaningless since the FFT yields a complex result. On the
other hand,
and
display the cosine and the negative of the sine transforms. Since the signal s(m) is
a sine function, the first of these is zero to within numerical precision, while the
negative sine transform of the signal has a negative peak at the second, m = 1,
point in the computational window, indicating, as expected from Eq. (21.2), that
the lowest non-zero frequency is ω1 = 2π /(N t) in the DFT or, equivalently,
f = ω/(2π ) = 1/2π = 1/(N t). Since the frequency spacing therefore varies
with the duration of the signal’s time record (the computational window width),
to increase the frequency resolution, the window must be broadened, even for
localized signals.
Further, since sin(ωt) = (exp(iωt) − exp(−iωt))/(2i) = −i(exp(iωt) −
exp(−iωt))/2, the negative frequency in the sine function yields a value
of +32i at the last, 64th point in the computational window (the sine transform
evaluated at a negative frequency is the negative of its value for the equivalent
positive frequency since the sine function is odd). Since the first point, m = 1
in Eq. (21.3), of the DFT corresponds to zero frequency, the (N /2 + 1)th point,
corresponds to m = N /2 and therefore ωm = π/t , which is termed the Nyquist
frequency. This constitutes the highest positive frequency (or equivalently the
largest negative frequency, since the DFT is periodic in frequency as well as
210 Fourier transforms
time) in the transform. The (next) largest negative frequency appears at the
subsequent point with m = N /2 + 1, since
2π(N /2+1)l
2π(N /2+1)l −i −2πl 2π(N /2+1−N )l 2π(−(N /2−1))l
e−i N =e N
= e−i N = e−i N (21.7)
The smallest negative frequency, f = −1/(N t), is situated at the last point,
m = N − 1. To recover the original signal, the inverse FFT can be applied:
the FFT exhibits broad extrema, since the periodic extension of the signal con-
tains large discontinuities at the computational window edges. To eliminate this
discontinuity, albeit at the cost of slightly distorting low-frequency spectral com-
ponents, the signal can be multiplied by a window function prior to Fourier
transforming. An example of such a window function is the Hamming window
function,
1 2π m
s̃m = sm 1 − cos , m = 0, 1, ..., N −1 (21.8)
2 N
which is explicitly periodic with period N t. Multiplying the signal by s̃ before
applying the FFT yields a frequency-spectrum distribution that more closely
resembles that of a signal with this periodicity:
signalR = sin( 10.5 * pi / 64 * [0 : 63] ) * hamming( 64 )';
plot( imag ( fft ( signalR ) ) );
212
22.1 Euler’s method 213
d3x
= −kv (22.3)
dt 3
becomes
dx
v=
dt
dv
a= (22.4)
dt
da
= −kv
dt
where the product of the O(Dt) error terms in Eq. (22.5) with Dt reduces the
error order to O(Dt)2 . C++ code that retains the above variable names for k = m
= 1, Dt = 0.06 and initial conditions x(0) = 0, v(0) = 1 is given for the DISLIN
plotting package by
main( ) {
double x[NUMBEROFTIMESTEPS], v[NUMBEROFTIMESTEPS], k=1,
m=1, dt=0.06;
x[0] = 0;
v[0] = 1;// second-order equation → two boundary conditions
for ( int loop = 1; loop < NUMBEROFTIMESTEPS; loop++ ) {
x[loop] = x[loop - 1] + dt * v[loop - 1];
v[loop] = v[loop - 1] - k * dt * x[loop - 1] / m;
}
qplot( x, v, NUMBEROFTIMESTEPS )
}
d 2 r
= −g êz − α v (22.7)
dt 2
214 Differential equations
In Octave, for a ball launched from coordinate origin with x and z components
of velocity given by 10 and 100 m/s
numberOfTimeSteps = 100;
deltaTime = 0.2;
gravitationalConstant = -9.8;
dragConstant = 6.0E-2;
If the Euler method is applied to the equation d x/dt = f (t), each evaluation
of x(t + t) adds an increment given by f (t)t to the previous value of x(t),
corresponding to the rectangular integration rule. Thus, the Euler procedure
effectively generalizes rectangular integration to systems of first-order differen-
tial equations.
Newton’s equations can also be programmed directly in second-derivative
form by replacing the continuous second-derivative operator by its centered
0 2
finite-difference operator (Dt ) approximation:
0 2 1 0
Dt f (t) ≡ Dt − Dt
t+t/2
f (t)
t−t/2
t
1
= ([ f (t + t) − f (t)] − [ f (t) − f (t − t)])
(t)2
1
= ( f (t + t) − 2 f (t) + f (t − t)) (22.8)
(t)2
Inserting this expression directly into Eq. (22.1) yields a solution algorithm for
x(t + t) in terms of the initial conditions x(t) and x(t – t).
Figure 22.1
the phase-space plot, Figure 22.1, of velocity v against position x for 80 time
steps with t = 2p/80.
The monotonic increase in spring energy occurs because, since the particle
initially propagates from x = 0 to x = 1, the position at which the restoring
force is evaluated, x(ti ), is closer to the origin than the average displacement,
≈x(ti + t/2) of the spring during the propagation interval. Similarly, the
value of the velocity variable in the first line of Eq. (22.6) that controls the
magnitude of the change in the displacement over a step is larger than its properly
averaged effective value over the time interval. Accordingly, the restoring force is
weakened while the velocity is overestimated, so the displacement after the first
quarter-cycle exceeds its correct value. In contrast, as the particle returns back
to zero displacement, the negative restoring force is instead overestimated and
the magnitude of the negative velocity term underestimated. Therefore over this
quarter-cycle a stronger restoring force operates over a longer time, amplifying
the magnitude of the negative velocity after a half-period, as again evidenced
in Figure 22.1. By extension, the numerical error supplies a fictitious numerical
driving force in resonance with the motion, which, in the absence of a physical
dissipation mechanism, induces a steady growth of the particle energy.
Consistent with the above analysis, the unphysical energy divergence can be
eliminated by balancing the error in the force in Eq. (22.6) against that of the
velocity term. The resulting Euler–Cromer procedure is obtained by replacing
216 Differential equations
The resulting cancellation in the two error contributions can be shown through
algebraic manipulations to restore energy conservation
For e.g. c2 = c3 = 1/2, c1 = 1 and the right-hand side of Eq. (22.11) is replaced
by the average of the function evaluated at the left- and estimated right-hand
endpoints of the interval.
Carrying this analysis further yields the fourth-order accurate procedure
1
x(t + t) = x(t) + t( f(
x (t), t) + 2 F2 + 2 F3 + F4 ) (22.14)
6
22.3 The Runge-Kutta procedure 217
where
t t
F2 = f x + x (t), t), t +
f (
2 2
t t
F3 = f x + F2 , t +
2 2
F4 = f x + t F3 , t + t
f = @cos;
numberOfPoints = 10000;
lowerLimit = 0;
upperLimit = pi / 2;
% Must be smaller than the minimum value of f in the interval
yLower = -2;
% Must be greater than the maximum value of f in the interval
yUpper = +2;
regionArea = abs( yUpper - yLower ) * ( upperLimit - lowerLimit );
218
23.2 Distribution functions 219
clear all
numberOfSteps = 40;
numberOfRealizations = 50000;
% For 2 steps, the possible outcomes are -2, 0 and 2,
% necessitating the +1 below
histogramR = zeros( 1, numberOfSteps + 1 );
for loop = 1 : numberOfRealizations
% Simulate numberOfSteps random steps
% 0 = step to left, 1 = step to right,
% histogramIndex = number of right steps + 1
histogramIndex = sum( round( rand( 1, numberOfSteps ) ) ) + 1;
histogramR(histogramIndex) = histogramR(histogramIndex) + 1;
end
% Normalize distribution to unit sum
histogramR = histogramR / sum( histogramR );
% xScaleR = number of steps to right - number of steps to left
xScaleR = 2 * ( 0 : numberOfSteps ) - numberOfSteps;
semilogy( xScaleR, histogramR, ''o'', 'markersize', 3 );
For the above one-dimensional random walk, the local steps can be biased so
that the probability of moving to the right is 0.6 while that of moving to the left
is 0.4. (Note that, if the random walker does in reality favor moves to the right,
the resulting distribution would be the unbiased solution to the problem.) The
resulting histogram is then oversampled for rightward displacements. However,
the unbiased distribution can be generated by multiplying this distribution by 0.4
for each rightward step and 0.6 for each leftward step, as in the program below:
clear all
numberOfSteps = 40;
numberOfRealizations = 50000;
% For 2 steps, the possible outcomes are -2, 0 and 2,
% necessitating the +1 below
histogramR = zeros( 1, numberOfSteps + 1 );
for loop = 1 : numberOfRealizations
% Simulate numberOfSteps random steps:
% +1 = right step, 0 = left step
% Events biased so that P(1) = 0.6
histogramIndex = sum( round( rand( 1, numberOfSteps ) ) ) + 1;
histogramR(histogramIndex) = histogramR(histogramIndex) + 1;
end
% Unbias result by multiplying by 0.6 for each left move
% and 0.4 for each right move
for loop = 1 : numberOfSteps;
histogramR(loop) = histogramR(loop) * ...
0.4ˆloop * 0.6ˆ( numberOfSteps - loop );
end
% Normalize distribution to unit sum
histogramR = histogramR / sum( histogramR );
xScaleR = 2 * ( 0 : numberOfSteps ) - numberOfSteps;
semilogy( xScaleR, histogramR, 'x', 'markersize', 3 );
The results for the biased (circles) and unbiased (crosses) procedures are pre-
sented in Figure 23.1.
Figure 23.1
based on Markov chains, for which each new system realization slightly modifies
the previous realization. An appropriate rule governs the acceptance or rejection
of each such transition. If a transition is rejected, the histogram is incremented
according to the previous result.
The first such, Metropolis, technique increases the probability of a global
system variable, E, by a factor eβ E (or, with trivial modifications, e−β E ). The
degree of bias toward large E can therefore be adjusted by varying b. In
the method, a Markov chain is first generated by assigning random values to
the local system variables. One or more of these variables is then perturbed in
such a manner that the global variables are changed by a small amount. The
rule then accepts all transitions that increase the value of E, while permitting
transitions that lower E by an amount E < 0 with a probability eβE . If a tran-
sition is rejected, the previous sample is counted again. Since there always exists
a finite probability for a transition to smaller E, states in the Markov chain can
escape from local maxima, although only after an average number of transi-
tions that increases exponentially with the height of the maximum relative to its
surroundings.
To understand the origin of the transition rule, consider a system with two
equally probable global states with different E. The probability of the Markov
23.4 The Metropolis algorithm 223
chain transitioning from the state with smaller E to the state with E + E is
then unity, while the probability of a transition in the reverse direction is e−βE .
However, on average, the number of transitions in one direction must be equal to
the number of transitions in the other direction, otherwise the number of visits to
the state with more incoming transitions would increase until equality is estab-
lished. Accordingly, the average number of times the upper state is visited will
exceed the corresponding number for the lower state by the factor eβE . Since this
argument extends to any distribution of global states, the Metropolis algorithm
oversamples a state with global variable E with a probability proportional to eβ E .
Therefore the physical probability distribution function (in statistical mechanics
the density-of-states function) is obtained by multiplying the resulting probability
distribution by the likelihood function L(E) = e−β E .
The following Metropolis random-walk program implements successive
realizations in a Markov chain formed from a random walk with numberOfSteps
steps by changing the direction of a single, randomly chosen step. The global
variable E is set to the absolute value of the distance of the final position of
the random walk from the origin. If a transition is rejected, the histogram bin
associated with the previous transition is incremented by unity, which is termed
a self-transition. The final histogram is multiplied by the likelihood function to
generate the unbiased probability distribution:
clear all
numberOfRealizations = 100000;
numberOfSteps = 40;
myBeta = 1.0;
histogramR = zeros( 1, numberOfSteps + 1 );
% Simulate numberOfSteps coin flips (or random steps)
stepSequenceR = round( rand( 1, numberOfSteps ) );
histogramIndex = sum( stepSequenceR ) + 1;
centralPoint = numberOfSteps / 2;
for loop = 1 : numberOfRealizations;
flipPosition = fix( rand * numberOfSteps ) + 1;
stepSequenceR(flipPosition) = 1 - stepSequenceR(flipPosition);
histogramIndexNew = sum( stepSequenceR ) + 1;
if rand < exp( myBeta *( ...
abs( histogramIndexNew - centralPoint ) - ...
abs( histogramIndex - centralPoint ) ) )
histogramIndex = histogramIndexNew;
else
stepSequenceR(flipPosition) = ...
1 - stepSequenceR(flipPosition);
end
histogramR(histogramIndex) = histogramR(histogramIndex) + 1;
end
histogramR = histogramR .* exp( - myBeta * ...
abs( [1 : numberOfSteps + 1] - centralPoint ) );
xScaleR = 2 * ( 0 : numberOfSteps ) - numberOfSteps;
semilogy( xScaleR, histogramR / sum( histogramR ) );
224 Monte Carlo methods
Executing the above program for values of myBeta from e.g. 0.2 to 1 after
issuing the command hold on demonstrates the influence of the biasing function.
For large myBeta, the sample space becomes overbiased toward statistically
unlikely events, undersampling the high-probability region of the probability
distribution function. However, the probability distribution function can be eval-
uated for several values of myBeta and each result employed in its region of
greatest accuracy.
clear all
numberOfRealizations = 100000;
numberOfSteps = 40;
numberOfOuterLoops = 4;
histogramR = ones( 1, numberOfSteps + 1 );
stepSequenceOldR = round ( rand( 1, numberOfSteps ) );
stepSequenceNewR = stepSequenceOldR;
histOld = 1;
histogramIndexOld = sum( stepSequenceOldR ) + 1;
along random outgoing angles with new velocities and energies. Monitoring the
evolution of individual molecules or of molecules in selected regions yields the
statistical medium properties.
As a simple one-dimensional illustration, 20 equal-mass point particles with
random but successively ordered positions are generated within a region between
0 and 500 distance units terminated with reflecting boundaries. The particles are
assigned random velocities between −1.0 and +1.0. The minimum time for a
collision to occur, either of a particle with its neighbor to its immediate left or,
for the first and last particle, with a boundary, is calculated. Every particle is then
evolved over this minimum time. If the minimum is associated with a particle
collision, the velocities of the colliding particles are interchanged; otherwise, the
velocity of the particle at the boundary is reversed. This sequence of steps is
repeated for 200 collisions and the trajectory of the central particle over this time
is graphed:
clear all
numberOfParticles = 20;
numberOfCollisions = 200;
windowSize = 500;
collisionTime = 0;
% Assign random positions to the particles
% and order the positions in an array
Particle.positionR = rand( 1, numberOfParticles ) * windowSize;
% Sort particles from left to rightmost in the array
Particle.positionR( : ) = sort( Particle.positionR( : ) );
% Assign random velocities from -1.0 to +1.0 to each particle
Particle.velocityR = ( rand( 1, numberOfParticles ) - 0.5 ) * 2;
for loop = 1 : numberOfCollisions
minimumTime = 1.e20;
for loopParticle = 2 : numberOfParticles
% Check whether the particle to the left will collide
% with the current particle
if Particle.velocityR(loopParticle - 1) - ...
Particle.velocityR(loopParticle) > 0
% Compute the time until collision
deltaTime = ( Particle.positionR(loopParticle) - ...
Particle.positionR(loopParticle - 1) ) / ...
( Particle.velocityR(loopParticle - 1) - ...
Particle.velocityR(loopParticle) );
% If this is smaller than previously recorded times,
% store the time and the particle number
if deltaTime < minimumTime
minimumTime = deltaTime;
particleIndex = loopParticle;
% Set flag to indicate that this is not a wall collision
isWall = 0;
end;
end
end
% Compute similarly the time for the rightmost
% particle to collide with the right wall
if Particle.velocityR(numberOfParticles) > 0
deltaTime = ( windowSize - Particle.positionR(numberOfParticles) ...
) / Particle.velocityR(numberOfParticles);
23.7 The Ising model 227
where
NE
Z= N El e−β El (23.7)
l=1
is termed the partition function. The specific heat capacity per spin, cV , is then
⎛ NE ⎞
−β El
⎜ E l N El e ⎟
1 ∂ Ē 1 ∂β ∂ Ē β 2 ∂ Ē β2 ∂ ⎜ l=1
⎟
cV = = =− =− ⎜
NE
⎟
Ns ∂ T Ns ∂ T ∂β Ns ∂β Ns ∂β ⎜
⎝
⎟
−β El ⎠
N El e
l=1
NE NE
N 2
E
El2 N El e−β El N El e −β El
− E l N El e −β El
clear all
Ising.numberOfEnergies = 1;
Ising.energy(1) = 0;
Ising.numberOfStates(1) = 0;
numberOfSpins = 8;
numberOfRealizations = 2ˆnumberOfSpins;
for loop = 1 : numberOfRealizations
% Convert each number from 0 to 2ˆN-1 into binary
% form to represent a set of spins
23.7 The Ising model 229
230
24.1 PDEs in scientific applications 231
If ϕ(x, 0) = A sin(π x/L) for 0 < x < L, the negative curvature initially yields a
change in ϕ directed in the –i direction. Since the velocity is proportional to the
field magnitude, evolves with time into a imaginary negative sine function. At
this point in time, its curvature is a positive imaginary quanitity. The wavefunction
thus experiences a negative real evolution with time, propagating along the
negative horizontal direction in the complex plane. By extension, the field rotates
in the complex plane so that both the imaginary part and the real part of the
wavefunction oscillate with a 90◦ relative phase shift between them. Alternatively,
since an eigenmode of the diffusion equation decays as e−αt , for t → it , the field
instead varies as e−iαt .
For hyperbolic equations of the form
∂ 2 S(x, t) ∂ 2 S(x, t)
2
=D (24.9)
∂t ∂x2
the acceleration rather than the velocity of the field is proportional to its curvature.
For initial conditions S(x, 0) = A sin(π x/L) and zero boundary conditions, the
acceleration of each spatial point possesses its maximum negative value at t = 0.
A quarter of an oscillation period later S(x, T /4) = 0 and the field attains zero
acceleration but a maximum negative velocity. The acceleration then reverses
sign and becomes increasingly positive until the field distribution reaches the
maximum negative displacement A sin(π x/L) consistent with conventional wave
motion.
Finally elliptic equations are exemplified by the Poisson equation,
∂ 2 V (x, y, z) ∂ 2 V (x, y, z) ∂ 2 V (x, y, z) 1
+ + = − ρ(x, y, z) (24.10)
∂x2 ∂ y2 ∂z 2 ε0
in which −ρ/ε0 , where ρ represents the charge density, is a source for the curvature
of the electric potential V such that these two quantities are proportional at every
point within the problem boundary. The electric field, given by the negative
gradient of the potential, is visualized in Octave for a point charge at the origin
with that q/(4π ε0 ) = 1 as follows:
clear all
numberOfPoints = 6; % must be even
halfWidth = 5; % halfwidth of grid
xPositionR = linspace( -halfWidth, halfWidth, numberOfPoints );
yPositionR = xPositionR;
field.xValueRC = zeros( numberOfPoints, numberOfPoints );
field.yValueRC = zeros( numberOfPoints, numberOfPoints );
for outerLoop = 1 : numberOfPoints
for innerLoop = 1 : numberOfPoints
radius = sqrt( xPositionR(outerLoop)ˆ2 + ...
yPositionR(innerLoop)ˆ2 );
unitX = xPositionR(outerLoop) / radius;
unitY = yPositionR(innerLoop) / radius;
field.xValueRC(innerLoop, outerLoop) = unitX / radiusˆ2;
24.1 PDEs in scientific applications 233
which approaches zero as (α)4 for α → 0. The function 1/r accordingly pos-
sesses a positive curvature in the radial direction but a negative curvature in both
of the orthogonal directions, such that the sum of these curvatures vanishes to
second order.
In general a partial differential equation of the form
∂ 2ξ ∂ 2ξ ∂ 2ξ ∂ξ ∂ξ
A 2
+B +C 2 + D +E + Fξ + G = 0 (24.13)
∂x ∂x ∂y ∂y ∂x ∂y
is termed elliptic, parabolic or hyperbolic depending on whether B 2 − 4AC is <,
= or > zero, respectively.
To obtain a unique solution to a partial differential equation, initial conditions
must be specified at each point in space, while boundary conditions that quan-
tify the interaction of the extremities of the field with its environment must be
given at each boundary point for all times. Dirichlet boundary conditions specify
234 Parabolic PDE solvers
the field value on the boundaries, while Neumann boundary conditions instead
designate the flux (normal derivative) of the field out of the boundaries. Mixed
boundary conditions impose Dirichlet conditions over part of the boundary and
Neumann conditions over the remaining regions. If the material or the numerical
implementation possesses a ring topology typified by a thin rod bent into a circle,
the field obeys periodic boundary conditions, for example, T (0, t) = T (L , t) and
dT (0, t)/d x = dT (L , t)/d x for temperature. The total number of boundary con-
ditions is identical in all cases, since these must be specified at each boundary
point in any number of dimensions.
D t # $
T (xi , t j+1 ) = T (xi , t j ) + 2
T (xi−1 , t j ) − 2T (xi , t j ) + T (xi+1 , t j ) (24.14)
(x)
The above equation with an initial field T (x, 0) = sin(π x/L) and zero-
temperature boundaries T (x0 , t) = T (x N −1 , t) = 0 can be implemented by advanc-
ing the field only over the points i = 1, 2, . . . N − 2, while maintaining a constant
(here zero) temperature at i = 0, N − 1. Note that the temperature field at the cur-
rent step must be saved when computing the updated field, otherwise T (xi , t j ) is
overwritten before T (xi+1 , t j+1 ) is computed. Failure to save fields in this manner
is a common scientific programming error. The program is
tempLast[loop] = temp[loop];
for ( int loop = 1; loop < NUMBEROFPOINTS - 1; loop++ )
temp[loop] = coefficient * ( tempLast[loop - 1] - 2 *
tempLast[loop] + tempLast[loop + 1] ) + temp[loop];
}
}
The output of the program describes an undistorted but decaying sine function.
in which v̄=v
ω
= v/ω represents a modified velocity with units of distance and ω =
√
k/m is the ratio between the maximum velocity and the maximum displacement,
the wave equation can be recast into the form
∂ χ 0 v ∂ χ
= (24.17)
∂t ξ v 0 ∂x ξ
êx ê y êz
∂ H 1
1
∂ ∂ ∂
1 ∂ E x (z)
= ∇ × E =
= ê y
∂t μ0 μ0
∂ x ∂ y ∂z
μ0 ∂z
E (z) 0 0
x
(24.18)
êx êz
ê y
∂ E 1
1
∂ ∂
1 ∂ H (z)
∂ y
=− ∇×H =−
= êx
∂t ε0 ε0
∂ x ∂z
ε0∂y ∂z
0 0
Hy (z)
√
Recalling that in vacuum the light velocity v = c0 = 1/ ε0 μ0 , the above equations
√
transform into Eq. (24.17) with the substitutions χ = ε0 /μ0 E x and ξ = Hy . The
√
free-space impedance, μ0 /ε0 = 377 in MKS units, equals the ratio between
the magnitudes of the electric and magnetic fields in vacuum.
236 Parabolic PDE solvers
which yields the equation of continuity after expanding ρ on the left-hand side
into a multidimensional Taylor series about (x, y, z, t) and retaining only first-
order quantities,
% &
∂ρ +ρ∇
· v
+ v · ∇ρ V t = 0 (24.25)
∂t
In terms of the convective (hydrodynamic or substantive) derivative D/Dt ,
Dρ · v
= −ρ ∇ (24.26)
Dt
24.3 Hyperbolic equations 237
Hence, for example, if a liquid flows at 2.0 m/s in the z-direction and the fluid
density is described by ρ(z) = ρ0 + cz z + ct t , then in a frame moving with the fluid
the change of the density with respect to time is (∂/∂t + 2 ∂/∂z)(ρ0 + cz z + ct t) =
2cz + ct .
Similarly, the change in the velocity of a cubic fluid volume is determined by
the imbalance of forces acting on the sides of the volume. Since the pressure, P,
on the volume is defined to be positive acting inward, tracking a fluid element in
the same manner as e.g. a thrown ball by moving along its path of evolution with
its velocity v, the velocity change of the element with time in the presence of
external forces such as the gravitational field is given by (noting that the initial
mass ρV of the volume is constant)
v x + vx t, y + v y t, z + vz t, t + t − v(x, y, z, t + t)
t
F
=
ρ V
ρ V aexternal 1 x y
= + x y êz Pz x+ ,y+ ,z
ρ V ρ V 2 2
x y
− Pz x+ ,y+ , z + z + ··· (24.27)
2 2
D v P
∇
= aexternal − (24.28)
Dt ρ
The quantity b = vt/x is often termed the Courant number, since its value is
related to the stability of a numerical procedure. However, the FTCS method is
unstable for any t and x , as is apparent from the manner in which the ini-
tial field χ̃(xi , t0 ) = (0, 1, 0, −1, 0, 1 . . .) evolves over one propagation step. From
Eq. (24.29), χ remains invariant for even-numbered points, whereas for odd-
numbered points χ alternately increases and decreases by b, in both cases con-
tributing a term proportional to b2 to the norm of the field. A general mathematical
treatment, the Lax method, evaluates the amplification of a specific term in the
238 Parabolic PDE solvers
This numerical instability is removed in the Lax method for small b by substi-
tuting the average χ (xi−1 , t j ) + χ (xi+1 , t j ) /2 for χ (xi , t j ) on the right-hand side
of Eq. (24.29):
χ (xi+1 , t j ) + χ (xi−1 , t j ) b
χ (xi , t j+1 ) = + χ (xi+1 , t j ) − χ (xi−1 , t j ) (24.33)
2 2
This introduces a fictitious diffusion that couples into the field at each grid point
for every time step a fraction of the field at the two neighboring grid points. Since
the solutions of the diffusion equation decay with time, as observed above, the
numerical diffusion counteracts the instability of the unmodified procedure. A
Lax analysis yields in place of Eq. (24.32)
|β| = cos2 (κ x) + b2 sin2 (κ x) (24.34)
which is less than unity for b < 1, i.e. t < x/v . When b > 1, so that the field
travels a distance greater than the distance between adjacent grid points in a single
time step, the magnitude of the ∂/∂ x convection term exceeds that of the diffusion
term. Consequently the overall field behavior is dominated by convection, leading
to amplification.
The Lax–Wendroff method introduces a diffusion term that is tailored to the
specific analytic properties of the field. In particular, inserting the convection
equation into the Taylor-series expansion
∂χ (t)2 ∂ 2 χ
χ (x, t + t) = χ (x, t) + t + + ···
∂t 2 ∂t 2
2 2 2
∂χ v (t) ∂ χ
= χ (x, t) − v t + + ··· (24.35)
∂x 2 ∂x2
yields the numerical procedure
b
χ (xi , t j+1 ) = χ (xi , t j ) + χ (xi+1 , t j ) − χ (xi−1 , t j )
2
b2
+ χ (xi+1 , t j ) − 2χ (xi , t j ) + χ (xi−1 , t j ) (24.36)
2
In contrast to the Lax method, the diffusive coupling varies as (t)2 , while the
numerical accuracy is enhanced.
The hopscotch method, known as the Yee method in electric-field simulation,
solves Eq. (24.17) by placing the grid points for the field ξ equidistant from the
24.3 Hyperbolic equations 239
(24.38)
Here fractional indices are replaced by integers. The excitation (the elec-
tric field in electromagnetic contexts), field1, enters the computational window
through the first point of the grid. At the rightmost boundary, field2 (the magnetic
field), is set to zero and therefore reverses direction while the direction of field1,
E , remains fixed. Consequently ( E × B ) reverses sign, changing the direction of
propagation. When this reflected field reaches the left boundary, field1 of the
incoming excitation is negligible. The resulting zero boundary condition inverts
the reflected electric field so that the left- and right-propagating electric fields
cancel out at the boundary. Since the Courant number is unity in the above code,
the field is displaced by one grid-point spacing for each time step.
Stability analyses can be applied to numerical procedures written in matrix
form
χ (t j+1 ) = Dχ(t
j) (24.39)
After many time steps, the term or terms with the largest spectral radius |λk |
dominate and λmaximum = lim j→∞ Dχ (t j )/χ (t j ). This ratio can be evaluated at any
spatial point xi , or can be appropriately averaged over all points. If the largest
eigenvalue is unique, the corresponding normalized eigenvector is given by
lim j→∞ χ (t j )/|χ (t j )|.
l=0 l=0
N
N −1
2πm 2πlm 2πm 2πlm
= ei N Al e−i N = ei N Al e−i N
l=1 l=0
2πm
= ei N [FFT(Al )]m (24.45)
Accordingly, if vmn and rmn are the Fourier components of V and ρ , respectively,
fast Fourier transforming in two dimensions the discrete version of Poisson’s
equation (again for x = y ≡ α )
(n) (n) (n) (n) (n) ρ(x, y)
Vi, j+1 + Vi, j−1 + Vi+1, j + Vi−1, j − 4Vi, j = −(α)2 (24.46)
ε0
yields for periodic or, equivalently, zero boundary conditions
2πm
i −i 2πm i 2πn i 2πn 2 r mn
e N +e N +e N +e N − 4 vmn = −(α) (24.47)
ε0
242 Parabolic PDE solvers
or, equivalently,
(α)2 rmn
vmn = − (24.48)
2 (cos (2π m/N ) + cos (2π n/N ) − 2)
after which V = IFFT(v) again in two dimensions.
must be preserved under the transformation ψ(t + t) = Uϕ(t). That is, noting
that (AB)† = B† A† , this requires
Hence U† U = I, or
U† = U−1 (24.55)
i.e. U is unitary. Since U(t) = I − i tH/, this results in the condition that
i t † i t
U† (t)U(t) = I+ H I− H
2
i t t
= I− H − H† − H† H (24.56)
E = ϕi |H|ϕi = ϕi |H† |ϕi = (H|ϕi )† |ϕi = (E|ϕi )† |ϕi = E ∗ ϕi |ϕi = E ∗ (24.58)
However,
N N
†
−i t H −i t H t −i t E m
e |ϕ e |ϕ = ϕn | cn∗ ei En cm e |ϕm
n=1 m=1
N
t
= cn∗ cm e−i (Em −En ) ϕn | ϕm
! "
n,m=1
δnm
N
= cm∗ cm = 1 (24.60)
m=1
t
Hence, e−i H preserves the norm of a propagating field and is therefore unitary.
Numerical procedures, which are typically derived from approximations to
i t
ϕ(t + t) = e−
(T +V )
ϕ(t) (24.61)
α2 2
eα(A+B) ≈ 1 + α(A + B) + A + AB + B A + B 2 + O(α 3 )
2!
α α 2 A2 α2 B 2 α α 2 A2
≈ 1+ A+ 1 + αB + 1+ A+ + O(α 3 )
2 4 2 2 4
A A
≈ eα 2 eα B eα 2 + O(α 3 ) ≡ 3 (α) + O(α 3 ) (24.62)
it ∂ 2
it 2
e 2m ∂ x 2
ψ = IFT e− 2m km [FT(ψ)]m (24.64)
in which e.g.
∞
x(t) x(t)
t DT m
e = (t DT ) (24.69)
p(t) p(t)
m=0
However,
⎛ ⎞
∂ x ∂ T ( p) ∂ x ∂ T ( p) ⎛ ⎞
% & − ∂ T ( p)
x x ⎜∂x ∂p ∂p ∂x ⎟ ⎝
DT = , T ( p) =⎝ = ∂ p ⎠ (24.70)
p p ∂ p ∂ T ( p) ∂ p ∂ T ( p) ⎠
− 0
∂x ∂p ∂p ∂x
while
⎧⎛ ⎞ ⎫
⎨ ∂ T ( p) ⎬
x
DT2 = ⎝ ∂ p ⎠ , T ( p) = 0 (24.71)
p ⎩ 0 ⎭
246 Parabolic PDE solvers
Hence,
⎛ ⎞
t ∂ T ( p)
t DT x(t) x(t) +
e 2 =⎝ 2 ∂p ⎠ (24.72)
p(t) p(t)
Similarly,
⎛ ⎞
x(t)
x(t)
et DV =⎝ ∂ V (x) ⎠ (24.73)
p(t) p − t
∂x
If T ( p) = p2 /(2m), this corresponds to first updating x(t + t/2) = x(t) +
tv(t)/2 and then applying p(t + t) = p(t) + t F(x(t + t/2)) followed by a
subsequent displacement step. Higher-order operator product expansions eval-
uate the quantities in Eqs. (24.72) and (24.73) at non-standard distances and
times.
with δ = −ik0 n 0 . Following the convention that the time dependence of the electric
field is given by exp(iωt), the forward-propagating field is then identified with
the solution of
∂
−δ 1 + X 0 + Y0 + N E =0 (24.79)
∂z
After the rapidly varying component of the electric field has been removed by
introducing the modified field
Nx Ny
2
2
2 2 mx ny
i z 2πm + 2πn 2πi k +Ll
E(xk , yl , z) = E mn (z) e 4n0 k0 Lx L y e Lx y (24.84)
Nx Ny
m=− 2
+1 n=− +1
2
In the normal representation of the FFT the negative m and n values in the above
expression are expressed as positive quantities m + N , n + N .
The unitarity of the above propagation algorithm insures that radiated power
remains within the computational window. Indeed, since the fast Fourier trans-
form is an expansion in functions that are periodic with respect to the window
length, the propagation method solves the problem for which the electric field and
refractive index are periodically continued outside the window boundary. Thus,
the field exiting the right side of the computational window simultaneously reap-
pears at the left side of the window transported by its periodic extension. This
effect can be suppressed only by introducing non-unitarity in the form of a
complex (absorptive) component refractive index near the window boundaries,
typically varying as
1 π |x − xα |
1 + cos |x − xα | < w
2 w (24.85)
0 elsewhere
for each transverse direction x , where x − xα is zero at the boundary position and
the width of the absorbing region w ≪ L α .
In the following program, a localized, Gaussian electric field distribution is
first propagated 500 mm in vacuum with the split-operator fast Fourier transform
method, then through a thin lens with a focal length of 250 mm and finally again
a further 500 mm through vacuum to the corresponding image point. The thin
lens is, for simplicity, here modeled by multiplication by a single operator that
compensates for the unequal path lengths of rays propagating at different angles
to its symmetry axis. In a more comprehensive analysis, a half propagation step
would be employed at the beginning and end of the calculation as well as just
before and just after the lens. Additionally, the exact spatially varying refractive
index of the lens can be introduced and the field advanced through the lens in
numerous small steps according to Eq. (24.82).
To simplify the lens operator, recall that for a thin lens 1/ p + 1/q = 1/ f , where
p and q correspond to the object and image lengths. For p = q = 2 f , the phase
change of a light beam that travels from the object point to a point in the lens a
24.7 FFT methods in optics 249
distance r from the symmetry axis and back to the image point is
2k0 r 2 + 4 f 2 + φ(r ) ≈ 2k0 2 f + r 2 /(4 f ) + φ(r ) (24.86)
where φ(r ) is the phase change in the lens. For Eq. (24.86) to be independent
of r requires
k0 r 2
φ(r ) = c − (24.87)
2f
Since a constant phase change does not affect optical field propagation, in the
program below the central operator of Eq. (24.82) is replaced by −ik0 r 2 /(2 f ):
vacuumWaveLength = 1.0;
gridLength = 200;
numberOfGridPoints = 1024; % Must be even (preferably 2ˆn)
numberOfSpatialSteps = 20; % Total number of propagation steps
propagationDistance = 1000; % Total propagation distance
referenceIndex = 1.0;
propagationStepLength = propagationDistance / numberOfSpatialSteps;
pointLocationsPlus1R = linspace( -gridLength / 2, ...
gridLength / 2, numberOfGridPoints + 1 );
pointLocationsR = pointLocationsPlus1R( 1 : numberOfGridPoints );
k0 = 2 * pi / vacuumWaveLength; % Vacuum wavevector
% Gaussian initial field
fieldR = exp( -pointLocationsR.ˆ2 / ( 2. * 5.0ˆ2 ) );
% Second derivative operatior in Fourier transform representation
squaredFourierWavevectorComponentsR = ...
-( 2 * pi / gridLength )ˆ2 * [ 0 : numberOfGridPoints / 2, ...
-numberOfGridPoints / 2 + 1 : -1 ].ˆ2;
% Propagation operator for one propagation step
propagationOperatorR = exp( - i * propagationStepLength / ...
( 2 * k0 * referenceIndex ) * ...
squaredFourierWavevectorComponentsR );
% Operator for the phase change in a thin lens
lensOperatorR = exp( i * k0 * pointLocationsR.ˆ2 / 500 );
for loop = 1 : numberOfSpatialSteps
% A single propagation step is employed at the
% beginning instead of a half-step
fieldR = ifft( propagationOperatorR .* fft( fieldR ) );
% The lens operator is applied near the
% middle of the propagation distance
if loop == floor( numberOfSpatialSteps / 2 )
fieldR = lensOperatorR .* fieldR;
end
plot( pointLocationsR, abs( fieldR ) )
drawnow
end
The accuracy with which derivatives are evaluated with fast Fourier transform
methods particularly favors the analysis of non-linear equations typified by the
250 Parabolic PDE solvers
∂E 1 ∂2 E
i + + |E|2 E = 0 (24.88)
∂t 2 ∂x2
2) t
E = ae−i (vx+(v−a 2 ) sech(a(x + vx)) (24.89)
that balance the dispersion (broadening) induced by the coupling of the field
at adjacent spatial points through the second derivative term with the spatial
focusing toward regions of large |E|2 supplied by the non-linear term.
A program that propagates a soliton of unit amplitude is given below. The
soliton exits the right-hand side of the computational window and reenters
the left-hand side as a result of the periodic boundary conditions implicit in the
FFT. This behavior is often exploited to examine multiple soliton collisions.
gridLength = 50;
numberOfGridPoints = 512; % Must be even (preferably 2ˆn)
numberOfTimeSteps = 500;
propagationTime = 8;
propagationStepTime = propagationTime / numberOfTimeSteps;
pointLocationsPlus1R = linspace( -gridLength / 2, ...
gridLength / 2, numberOfGridPoints + 1 );
pointLocationsR = pointLocationsPlus1R( 1 : numberOfGridPoints );
% Soliton initial field
fieldR = 1.0 ./ cosh( pointLocationsR - 8. ) .* ...
exp( -i * 2 * pi * sin( 50 * pi / 180. ) * pointLocationsR );
% Free-space propagation operator for a half time step
propagationOperatorR = exp( -i * propagationStepTime * ...
( 2 * pi / gridLength )ˆ2 / 4 * ...
[ 0 : numberOfGridPoints / 2 , -numberOfGridPoints / 2 ...
+ 1 : -1 ] .ˆ2 );
for loop = 1 : numberOfTimeSteps
fieldR = ifft( propagationOperatorR .* ...
fft( exp( i * propagationStepTime * conj( fieldR ) .* ...
fieldR ) .* ifft( propagationOperatorR .* fft( fieldR ) ) ) );
if rem( loop - 1, 50 ) == 0
plot( pointLocationsR, abs( fieldR ) )
drawnow
end
end
in the propagation operator results from the product of three – signs, one in the
kinetic-energy term T = −(2 /(2m))∂/∂ x 2 , the second from multiplication of both
sides of the Schrödinger equation by –i to remove the +i from the time derivative,
and the third from km2 → −(2π m/L)2 in the discrete Fourier representation.
All Schrödinger-equation propagation methods below require the following
square-well potential function (normally wellWidth and wellDepth would be
passed to the function from the calling program either through a global state-
ment or as function parameters). A more accurate treatment insures that the
neighboring grid points are spaced equidistantly from the edges of the square-
well potential.
file potential.m
function y = potential( x )
wellWidth = 5;
wellDepth = 5;
y = zeros( length( x ), 1 );
for loop = 1 : length( x )
if abs( x(loop) ) < wellWidth;
y(loop) = -wellDepth;
end
end
file schfft.m
hold on;
stepLength = 0.005;
numberOfTimeSteps = 500;
fieldWidth = 2;
computationalWindowWidth = 20;
numberOfPoints = 300; % Must be even (preferably 2ˆn)
gridPointsPlus1R = linspace( -computationalWindowWidth / 2, ...
computationalWindowWidth / 2, numberOfPoints + 1 );
gridPointsC = gridPointsPlus1R( 1 : numberOfPoints ).’;
wavefunctionC = exp( -gridPointsC.ˆ2 ./ ( 2 * fieldWidthˆ2 ) );
% Kinetic-energy part of propagation operator
propagationOperatorC = exp( -i * stepLength / 2 * ...
( 2 * pi / computationalWindowWidth )ˆ2 * ...
( [ 0 : numberOfPoints / 2 , ...
-numberOfPoints / 2 + 1 : -1 ]').ˆ2 );
% Potential-energy part of propagation operator
potentialOperatorC = exp( -i * stepLength * ...
potential( gridPointsC ) );
for loop = 1 : numberOfTimeSteps
% Fourier transform method wavefunction propagation
wavefunctionC = ifft( propagationOperatorC .* ...
fft( potentialOperatorC .* wavefunctionC ) );
if ( rem(loop, 50) == 0 )
plot( gridPointsC, abs( wavefunctionC ), ’r’ );
% Analytic expression for Gaussian wavepacket
% propagation in constant potential
coefficient = ( 1 + ( stepLength * loop )ˆ2 / ...
fieldWidthˆ4 );
plot( gridPointsC, coefficientˆ-0.25 * ...
252 Parabolic PDE solvers
where
i t
1+ H χ = ψ(t) (24.94)
2
24.8 The Crank–Nicholson method 253
Accordingly, each time step requires the solution of a tridiagonal equation system.
In multiple dimensions the commutativity of e.g. ∂ 2 /∂ x 2 and ∂ 2 /∂ y 2 further
enables the alternating directional method (ADI),
i ∂2 2
+ ∂2+ ∂2
2
i ∂ 2 i ∂ 2 i ∂ 2
−i
t T
e =e 2m ∂x2 ∂y ∂z = e 2m ∂ x 2 e 2m ∂ y2 e 2m ∂z2 (24.95)
Each exponential operator can then be separately evaluated with the Crank–
Nicholson procedure.
The following Octave program applies the Crank–Nicholson method to
the one-dimensional time-dependent Schrödinger equation for the propaga-
tion of a Gaussian wavepacket in a square-well potential of depth and width
5.0 a.u. (atomic units, for which = m = e = 1) in the presence of zero bound-
ary conditions. The result is then compared with the analytic expression for
V (x) = 0:
hold on;
clear all;
stepLength = .005;
numberOfTimeSteps = 500;
fieldWidth = 2;
computationalWindowWidth = 20;
numberOfPoints = 300;
dx = computationalWindowWidth / ( numberOfPoints - 1 );
gridPointsC = linspace( -computationalWindowWidth / 2, ...
computationalWindowWidth / 2, numberOfPoints ).';
wavefunctionC = exp( -gridPointsC.ˆ2 ./ ( 2 * fieldWidthˆ2 ) );
- wavefunctionC;
else
wavefunctionC = 2 * ( leftMatrixRCs \ wavefunctionC ) - ...
wavefunctionC;
end
if ( rem(loop,50) == 0 )
plot( gridPointsC, abs( wavefunctionC ), ’g’ );
end
drawnow
end
In the above program, either the built-in Octave sparse-matrix equation solu-
tion routines or the tridiagonal matrix solver below can be employed by changing
enableMyTridiagonal from 1 to 0 (if column vectors are passed to myTridiag-
onal( ) the return value is a column vector, whereas a row vector is returned for
row-vector parameters):
numberOfPoints = 100;
computationalWindowWidth = 20;
deltaX = computationalWindowWidth / ( numberOfPoints - 1);
leftEndPoint = -computationalWindowWidth / 2;
MRC = zeros( numberOfPoints, numberOfPoints );
xR = linspace( leftEndPoint, -leftEndPoint, numberOfPoints );
for ( loop = 1 : numberOfPoints )
MRC(loop, loop) = 1 / deltaXˆ2 + potential( xR(loop) );
if ( loop ~ = numberOfPoints )
MRC(loop, loop + 1) = -1 / ( 2 * deltaXˆ2 );
end;
if ( loop ~ = 1 )
MRC(loop, loop - 1) = -1 / ( 2 * deltaXˆ2 );
end;
end
[ eigenVectorsC, eigenValues ] = eigs( MRC, 1, ’sa’ );
plot( xR, eigenVectorsC * ...
sign( eigenVectorsC(numberOfPoints / 2) ) );
(24.98)
256 Parabolic PDE solvers
For e.g. u i∗ (x R ) = u i∗ (xi ) = 0, the second, surface, term vanishes on the right-hand
side of the above equation. Substituting Eq. (24.96) into Eq. (24.98) yields the
matrix eigenvalue equation
⎛ ⎞ ⎛ ⎞
a1 a1
⎜a ⎟ ⎜a ⎟
S ⎝ ⎠ = E S̃ ⎝ 2 ⎠
2 (24.99)
.. ..
. .
and
xR
S̃i j = u i∗ (x)u j (x)d x (24.101)
xL
where for the first function, u 1 (x), x0 = xL coincides with the left endpoint of the
computation interval and for the last function, u N (x), x N +1 = xR coincides with
the right endpoint of the interval. Since the derivative of each basis function is
±1/(x)2 , adding the contributions, termed element matrices, of each interval
to the structure matrices yields (again noting that u 0 = u N +1 = 0 from the zero
boundary conditions), for four points located at xL + x, xL + 2 x, . . . , xL + 4 x
with xL + 5 x = xR ,
⎛ xL +x ⎞
2
+ V (x)u 1 u 1 d x 0 0 0
5
⎜ 2m x xL
⎟
⎜ ⎟
S= S (i) = ⎜
⎜ 0 0 0 0⎟⎟
i=1 ⎝ 0 0 0 0⎠
0 0 0 0
⎛ xL +2x xL +2x
⎞
2 2
⎜ 2m x + V (x)u 1 u 1 d x −
2m x
+ V (x)u 1 u 2 d x 0 0⎟
⎜ x +x xL +x ⎟
⎜ L xL +2x xL +2x ⎟
⎜ 2 2 ⎟
+ ⎜− + V (x)u 1 u 2 d x + V (x)u 2 u 2 d x 0 0⎟ + · · ·
⎜ 2m x xL +x 2m x xL +x
⎟
⎜ ⎟
⎝ 0 0 0 0⎠
0 0 0 0
(24.103)
24.9 Finite-difference procedures 257
together with
⎛ 2 x x
⎞
0 0
⎜ 3 6 ⎟
⎜ x 2 x x ⎟
⎜ 0 ⎟
⎜ 6 3 6 ⎟
S̃ = ⎜ ⎟ (24.104)
⎜ x 2 x x ⎟
⎜ 0 ⎟
⎜
⎝ 6 3 6 ⎟
⎠
x 2 x
0 0
6 3
After the upper triangular parts of the matrices S and S̃ have been evaluated the
full matrices can be obtained through symmetrization (which doubles the diago-
nal elements). If the potential function remains constant, V0 , over a finite element,
the integrals over the potential function in S equal V0 times the corresponding
elements of S̃ . Approximating the potential function over each triangular element
by its value at the center of the element and employing the generalized Octave
eigenvalue solver for Av = λBv , eigs(A, B), yields, after summing the matrices
in Eq. (24.103),
numberOfPoints = 100;
computationalWindowWidth = 20;
deltaX = computationalWindowWidth / ( numberOfPoints - 1 );
leftEndPoint = -computationalWindowWidth / 2;
SRC = zeros( numberOfPoints, numberOfPoints );
SBarRC = zeros( numberOfPoints, numberOfPoints );
% Coordinates of the center of each finite element
xR = linspace( leftEndPoint, -leftEndPoint, numberOfPoints );
% Boundary points are 0 (at x_L = -leftEndPoint - deltaX)
% and numberOfPoints + 1
for ( loop = 1 : numberOfPoints )
SRC(loop, loop) = 1 / ( 2 * deltaX ) + ...
potential( xR(loop) ) * deltaX / 3;
SBarRC(loop, loop) = deltaX / 3;
if ( loop ~ = numberOfPoints )
SRC(loop, loop + 1) = -1 / ( 2 * deltaX ) + ...
potential( xR(loop) ) * deltaX / 6;
SBarRC(loop, loop + 1) = deltaX / 6;
end
end
% Symmetrization
SRC = SRC + SRC’;
SBarRC = SBarRC + SBarRC’;
% Generalized eigenvalue routine
[ eigenVectorsC, eigenValues ] = eigs( SRC, SBarRC, 1, ’sa’ );
plot( xR, eigenVectorsC * ...
sign( eigenVectorsC(numberOfPoints / 2) ) );
Index
259
260 Index