C++ Language Tutorial For Beginner - Learn C++ in 7 Days
C++ Language Tutorial For Beginner - Learn C++ in 7 Days
Essentials
Sharam Hekmat
Contents
Contents v Preface x
1. Preliminaries 1
A Simple C++ Program 2
Compiling a Simple C++ Program 3
How C++ Compilation Works 4
Variables 5
Simple Input/Output 7
Comments 9
Memory 10
Integer Numbers 11
Real Numbers 12
Characters 13
Strings 14
Names 15
Exercises 16
2. Expressions 17
Arithmetic Operators 18
Relational Operators 19
Logical Operators 20
Bitwise Operators 21
Increment/Decrement Operators 22
Assignment Operator 23
Conditional Operator 24
Comma Operator 25
The sizeof Operator 26
Operator Precedence 27
Simple Type Conversion 28
Exercises 29
www.pragsoft.com Contents v
3. Statements 30
Simple and Compound Statements 31
The if Statement 32
The switch Statement 34
The while Statement 36
The do Statement 37
The for Statement 38
The continue Statement 40
The break Statement 41
The goto Statement 42
The return Statement 43
Exercises 44
4. Functions 45
A Simple Function 46
Parameters and Arguments 48
Global and Local Scope 49
Scope Operator 50
Auto Variables 51
Register Variables 52
Static Variables and Functions 53
Extern Variables and Functions 54
Symbolic Constants 55
Enumerations 56
Runtime Stack 57
Inline Functions 58
Recursion 59
Default Arguments 60
Variable Number of Arguments 61
Command Line Arguments 63
Exercises 64
6. Classes 82
A Simple Class 83
Inline Member Functions 85
Example: A Set Class 86
Constructors 90
Destructors 92
Friends 93
Default Arguments 95
Implicit Member Argument 96
Scope Operator 97
Member Initialization List 98
Constant Members 99
Static Members 101
Member Pointers 102
References Members 104
Class Object Members 105
Object Arrays 106
Class Scope 108
Structures and Unions 110
Bit Fields 112
Exercises 113
7. Overloading 115
Function Overloading 116
Operator Overloading 117
Example: Set Operators 119
Type Conversion 121
Example: Binary Number Class 124
Overloading << for Output 127
Overloading >> for Input 128
Overloading [] 129
Overloading () 131
Memberwise Initialization 133
Memberwise Assignment 135
Overloading new and delete 136
Overloading ->, *, and & 138
Overloading ++ and -142
Exercises 143
9. Templates 170
Function Template Definition 171
Function Template Instantiation 172
Example: Binary Search 174
Class Template Definition 176
Class Template Instantiation 177
Nontype Parameters 178
Class Template Specialization 179
Class Template Members 180
Class Template Friends 181
Example: Doubly-linked Lists 182
Derived Class Templates 186
Exercises 187
Preface
Since its introduction less than a decade ago, C++ has experienced growing
Since its introduction less than a decade ago, C++ has experienced growing
acceptance as a practical object-oriented programming language suitable for
teaching, research, and commercial software development. The language has
also rapidly evolved during this period and acquired a number of new features
(e.g., templates and exception handling) which have added to its richness.
In designing this book, I have strived to achieve three goals. First, to produce a
concise introductory text, free from unnecessary verbosity, so that beginners can
develop a good understanding of the language in a short period of time. Second,
I have tried to combine a tutorial style (based on explanation of concepts
through examples) with a reference style (based on a flat structure). As a result,
each chapter consists of a list of relatively short sections (mostly one or two
pages), with no further subdivision. This, I hope, further simplifies the reader’s
task. Finally, I have consciously avoided trying to present an absolutely
complete description of C++. While no important topic has been omitted,
descriptions of some of the minor idiosyncrasies have been avoided for the sake
of clarity and to avoid overwhelming beginners with too much information.
Experience suggests that any small knowledge gaps left as a result, will be easily
filled over time through selfdiscovery.
Intended Audience
For the convenience of readers, the sample programs presented in this book
(including the solutions to the exercises) and provided in electronic form.
1. Preliminaries
This chapter introduces the basic elements of a C++ program. We will use
simple examples to show the structure of C++ programs and the way they are
compiled. Elementary concepts such as constants, variables, and their storage in
memory will also be discussed.
Programming
A digital computer is a useful tool for solving a great variety of problems. A
solution to a problem is called an algorithm; it describes the sequence of steps
to be performed for the problem to be solved. A simple example of a problem
and an algorithm for it would be:
Problem : Sort a list of names in ascending lexicographic order. Algorithm: Call the given list list1; create
an empty list, list2, to hold the sorted list. Repeatedly find the ‘smallest’ name in list1, remove it from list1,
and make it the next entry of list2, until list1 is empty.
A machine language is far too cryptic to be suitable for the direct use of
programmers. A further abstraction of this language is the assembly language
which provides mnemonic names for the instructions and a more intelligible
notation for the data. An assembly language program is translated to machine
language by a translator called an assembler.
Even assembly languages are difficult to work with. High-level languages such
as C++ provide a much more convenient notation for implementing algorithms.
They liberate programmers from having to think in very low-level terms, and
help them to focus on the algorithm instead. A program written in a high-level
language is translated to assembly language by a translator called a compiler.
The assembly code produced by the compiler is then assembled to produce an
executable program.
Listing 1.1 shows our first C++ program, which when run, simply outputs the
message Hello World.
Listing 1.1
1 #include <iostream.h>
2 This line defines a function called main. A function may have zero or more
parameters ; these always appear after the function name, between a pair of
brackets. The word void appearing between the brackets indicates that main has
no parameters. A function may also have a return type ; this always appears
before the function name. The return type for main is int (i.e., an integer
number). All C++ programs must have exactly one main function. Program
execution always begins from main.
Dialog 1.1 shows how the program in Listing 1.1 is compiled and run in a
typical UNIX environment. User input appears in bold and system response in
plain. The UNIX command line prompt appears as a dollar symbol ($).
Dialog 1.1
1 $ CC hello.cc
2 $ a.out
3 Hello World
4 $
Annotation 1 The command for invoking the AT&T C++ translator in a UNIX
environment is CC. The argument to this command (hello.cc) is the name of the
file which contains the program. As a convention, the file name should end in .c,
.C, or .cc. (This ending may be different in other systems.)
Dialog 1.2
1 $ CC hello.cc -o hello
2 $ hello
3 Hello World
4 $
Although the actual command may be different depending on the make of the
compiler, a similar compilation procedure is used under MS-DOS.
Windowsbased C++ compilers offer a user-friendly environment where
compilation is as simple as choosing a menu command. The naming convention
under MS-DOS and Windows is that C++ source file names should end in .cpp. ¨
• First, the C++ preprocessor goes over the program text and carries out the
instructions specified by the preprocessor directives (e.g., #include). The result is
a modified program text which no longer contains any directives. (Chapter 12
describes the preprocessor in detail.)
• Then, the C++ compiler translates the program code. The compiler may be a
true C++ compiler which generates native (assembly or machine) code, or just a
translator which translates the code into C. In the latter case, the resulting C code
is then passed through a C compiler to produce native object code. In either case,
the outcome may be incomplete due to the program referring to library routines
which are not defined as a part of the program. For example, Listing 1.1 refers to
the << operator which is actually defined in a separate IO library.
• Finally, the linker completes the object code by linking it with the object code
of any library modules that the program may have referred to. The final result is
an executable file.
Figure 1.1 illustrates the above steps for both a C++ translator and a C++ native
compiler. In practice all these steps are usually invoked by a single command
(e.g., CC) and the user will not even see the intermediate files generated.
Variables
A variable is a symbolic name for a memory location in which data can be stored
and subsequently recalled. Variables are used for holding data values so that
they can be utilized in various computations in a program. All variables have
two important attributes:
• A type which is established when the variable is defined (e.g., integer, real,
character). Once defined, the type of a C++ variable cannot be changed.
• A value which can be changed by assigning a new value to the variable. The
kind of values a variable can assume depends on its type. For example, an
integer variable can only take integer values (e.g., 2, 100, -12).
2
3
4
5
int main (void) { int workDays;
float workHours, payRate, weeklyPay;
6
6
7
8
9
10
11
12
13
} workDays = 5;
workHours = 7.5;
payRate = 38.55;
weeklyPay = workDays workHours payRate; cout << "Weekly Pay = ";
cout << weeklyPay;
cout << '\n';
Annotation
#include <iostream.h>
4 This line defines an int (integer) variable called workDays, which will
represent the number of working days in a week. As a general rule, a variable is
defined by specifying its type first, followed by the variable name, followed by a
semicolon.
5 This line defines three float (real) variables which, respectively, represent the
work hours per day, the hourly pay rate, and the weekly pay. As illustrated by
this line, multiple variables of the same type can be defined at once by
separating them with commas.
9 This line calculates the weekly pay as the product of workDays, workHours,
and payRate (* is the multiplication operator). The resulting value is stored in
weeklyPay.
10-12 These lines output three items in sequence: the string "Weekly Pay = ", the
value of the variable weeklyPay, and a newline character.
When run, the program will produce the following output:
Weekly Pay = 1445.625
Listing 1.3
1 #include <iostream.h>
8 cout << "Weekly Pay = "; 9 cout << weeklyPay; 10 cout << '\n';
11 }
Simple Input/Output
The most common way in which a program communicates with the outside
world is through simple, character-oriented Input/Output (IO) operations. C++
provides two useful operators for this purpose: >> for input and << for output.
We have already seen examples of output using <<. Listing 1.4 also illustrates
the use of >> for input.
Listing 1.4
1 #include <iostream.h>
7 cout << "What is the hourly pay rate? "; 8 cin >> payRate;
Annotation 7 This line outputs the prompt What is the hourly pay rate? to seek
user input.
8 This line reads the input value typed by the user and copies it to payRate. The
input operator >> takes an input stream as its left operand (cin is the standard
C++ input stream which corresponds to data entered via the keyboard) and a
variable (to which the input data is copied) as its right operand.
Both << and >> return their left operand as their result, enabling multiple input
or multiple output operations to be combined into one statement. This is
illustrated by Listing 1.5 which now allows the input of both the daily work
hours and the hourly pay rate.
Listing 1.5
1 #include <iostream.h>
6 cout << "What are the work hours and the hourly pay rate? "; 7 cin >>
workHours >> payRate;
8 weeklyPay = workDays workHours payRate; 9 cout << "Weekly Pay = " <<
weeklyPay << '\n'; 10 }
Annotation 7 This line reads two input values typed by the user and copies them
to workHours and payRate, respectively. The two values should be separated by
white space (i.e., one or more space or tab characters). This statement is
equivalent to:
9 This line is the result of combining lines 10-12 from Listing 1.4. It outputs
"Weekly Pay = ", followed by the value of weeklyPay, followed by a newline
character. This statement is equivalent to:
Because the result of << is its left operand, (cout << "Weekly Pay = ") evaluates
to cout which is then used as the left operand of the next << operator, etc.
Comments
• Anything after // (until the end of the line on which it appears) is considered a
comment.
• Anything enclosed by the pair /* and */ is considered a comment. Listing 1.6
illustrates the use of both forms.
Listing 1.6
1 #include <iostream.h>
• A comment should be easier to read and understand than the code which it tries
to explain. A confusing or unnecessarily-complex comment is worse than no
comment at all.
• Use of descriptive names for variables and other entities in a program, and
proper indentation of the code can reduce the need for using comments. The best
guideline for how to use comments is to simply apply common sense. ¨
Memory
A computer provides a Random Access Memory (RAM) for storing executable
program code as well as the data the program manipulates. This memory can be
thought of as a contiguous sequence of bits, each of which is capable of storing a
binary digit (0 or 1). Typically, the memory is also divided into groups of 8
consecutive bits (called bytes). The bytes are sequentially addressed. Therefore
each byte can be uniquely identified by its address (see Figure 1.2).
causes the compiler to allocate a few bytes to represent salary. The exact number
of bytes allocated and the method used for the binary representation of the
integer depends on the specific C++ implementation, but let us say two bytes
encoded as a 2’s complement integer. The compiler uses the address of the first
byte at which salary is allocated to refer to it. The above assignment causes the
value 65000 to be stored as a 2’s complement integer in the two bytes allocated
(see Figure 1.3).
Integer Numbers
An integer variable may be defined to be of type short, int, or long. The only
difference is that an int uses more or at least the same number of bytes as a short,
and a long uses more or at least the same number of bytes as an int. For example,
on the author’s PC, a short uses 2 bytes, an int also 2 bytes, and a long 4 bytes.
short age = 20;
int salary = 65000; long price = 4500000;
A literal integer (e.g., 1984) is always assumed to be of type int, unless it has an
L or l suffix, in which case it is treated as a long. Also, a literal integer can be
specified to be unsigned using the suffix U or u. For example:
92 // decimal
0134 // equivalent octal
0x5C // equivalent hexadecimal
Octal numbers use the base 8, and can therefore only use the digits 0-7.
Hexadecimal numbers use the base 16, and therefore use the letter A-F (or a-f) to
represent, respectively, 10-15. Octal and hexadecimal numbers are calculated as
follows:
0134 = 1 × 82 + 3 × 81 + 4 × 80 = 64 + 24 + 4 = 92
0x5C = 5 × 161 + 12 × 160 = 80 + 12 = 92 ¨
Real Numbers
A real variable may be defined to be of type float or double. The latter uses
more bytes and therefore offers a greater range and accuracy for representing
real numbers. For example, on the author’s PC, a float uses 4 and a double uses
8 bytes.
A literal real (e.g., 0.06) is always assumed to be of type double, unless it has
an F or f suffix, in which case it is treated as a float, or an L or l suffix, in which
case it is treated as a long double. The latter uses more bytes than a double for
better accuracy (e.g., 10 bytes on the author’s PC). For example:
In addition to the decimal notation used so far, literal reals may also be
expressed in scientific notation. For example, 0.002164 may be written in the
scientific notation as:
2.164E-3 or 2.164e-3
The letter E (or e) stands for exponent. The scientific notation is interpreted as
follows:
2.164E-3 = 2.164 × 10-3
¨
Characters
char ch = 'A';
Single and double quotes and the backslash character can also use the escape
notation:
Literal characters may also be specified using their numeric code value. The
general escape sequence \ooo (i.e., a backslash followed by up to three octal
digits) is used for this purpose. For example (assuming ASCII):
Strings
A long string may extend beyond a single line, in which case each of the
preceding lines should be terminated by a backslash. For example:
"Example to show \
the use of backslash for \ writing a long string"
The backslash in this context means that the rest of the string is continued on the
next line. The above string is equivalent to the single line string:
"Example to show the use of backslash for writing a long string"
The shortest possible string is the null string ("") which simply consists of the
null character. ¨
Names
Programming languages use names to refer to the various entities that make up a
program. We have already seen examples of an important category of such
names (i.e., variable names). Other categories include: function names, type
names, and macro names, which will be described later in this book.
names, and macro names, which will be described later in this book.
C++ imposes the following rules for creating valid names (also called identifiers
). A name should consist of one or more characters, each of which may be a
letter (i.e., 'A'-'Z' and 'a'-'z'), a digit (i.e., '0'-'9'), or an underscore character ('_'),
except that the first character may not be a digit. Upper and lower case letters are
distinct. For example:
Certain words are reserved by C++ for specific purposes and may not be used as
identifiers. These are called reserved words or keywords and are summarized
in Table 1.1:
¨
Exercises
int n = -100;
unsigned int i = -100;
signed int = 2.9;
long m = 2, p = 4;
int 2k;
double x = 2 * m;
float y = y * 2;
unsigned double z = 0.0; double d = 0.67F;
float f = 0.52L;
signed char = -1786;
char c = '$' + 2;
sign char h = '\111';
char *name = "Peter Pan"; unsigned char *num = "276811";
identifier
seven_11
unique
gross-income
gross$income
2by2
default
average_weight_of_a_large_pizza variable
object.oriented
2. Expressions
This chapter introduces the built-in C++ operators for composing expressions.
An expression is any computation which yields a value.
When discussing expressions, we often use the term evaluation. For example,
we say that an expression evaluates to a certain value. Usually the final value is
the only reason for evaluating the expression. However, in some cases, the
expression may also produce sideeffects. These are permanent changes in the
program state. In this sense, C++ expressions are different from mathematical
expressions.
Arithmetic Operators
C++ provides five basic arithmetic operators. These are summarized in Table
2.2.
Except for remainder ( %) all other arithmetic operators can accept a mix of
integer and real operands. Generally, if both operands are integers then the result
will be an integer. However, if one or both of the operands are reals then the
result will be a real (or double to be exact).
When both operands of the division operator ( /) are integers then the division is
performed as an integer division and not the normal division we are used to.
Integer division always results in an integer outcome (i.e., the result is always
rounded down). For example:
The remainder operator ( %) expects integers for both of its operands. It returns
the remainder of integer-dividing the operands. For example 13%3 is calculated
by integer dividing 13 by 3 to give an outcome of 4 and a remainder of 1; the
result is therefore 1.
unsigned char k = 10 * 92; // overflow: 920 > 255 It is illegal to divide a number
by zero. This results in a runtime division-byzero failure which typically causes
the program to terminate. ¨
Relational Operators
C++ provides six relational operators for comparing numeric quantities. These
are summarized in Table 2.3. Relational operators evaluate to 1 (representing the
true outcome) or 0 (representing the false outcome).
The relational operators should not be used for comparing strings, because this
will result in the string addresses being compared, not the string contents. For
example, the expression
C++ provides library functions (e.g., strcmp) for the lexicographic comparison
of string. These will be described later in the book.
¨
Logical Operators
C++ provides three logical operators for combining logical expression. These are
summarized in Table 2.4. Like the relational operators, logical operators
evaluate to 1 or 0.
Logical negation is a unary operator, which negates the logical value of its single
operand. If its operand is nonzero it produce 0, and if it is 0 it produces 1.
Logical and produces 0 if one or both of its operands evaluate to 0. Otherwise, it
produces 1. Logical or produces 0 if both of its operands evaluate to 0.
Otherwise, it produces 1.
Note that here we talk of zero and nonzero operands (not zero and 1). In general,
any nonzero value can be used to represent the logical true, whereas only zero
represents the logical false. The following are, therefore, all valid logical
expressions:
!20 // gives 0
10 && 5 // gives 1
10 || 5.5 // gives 1
10 && 0 // gives 0
C++ does not have a built-in boolean type. It is customary to use the type int for
this purpose instead. For example:
int sorted = 0; // false int balanced = 1; // true ¨
Bitwise Operators
C++ provides six bitwise operators for manipulating the individual bits in an
integer quantity. These are summarized in Table 2.5.
Bitwise operators expect their operands to be integer quantities and treat them as
bit sequences. Bitwise negation is a unary operator which reverses the bits in its
operands. Bitwise and compares the corresponding bits of its operands and
produces a 1 when both bits are 1, and 0 otherwise. Bitwise or compares the
corresponding bits of its operands and produces a 0 when both bits are 0, and 1
otherwise. Bitwise exclusive or compares the corresponding bits of its operands
and produces a 0 when both bits are 1 or both bits are 0, and 1 otherwise.
Bitwise left shift operator and bitwise right shift operator both take a bit
sequence as their left operand and a positive integer quantity n as their right
operand. The former produces a bit sequence equal to the left operand but which
has been shifted n bit positions to the left. The latter produces a bit sequence
equal to the left operand but which has been shifted n bit positions to the right.
Vacated bits at either end are set to 0.
Table 2.6 illustrates bit sequences for the sample operands and results in Table
2.5. To avoid worrying about the sign bit (which is machine dependent), it is
2.5. To avoid worrying about the sign bit (which is machine dependent), it is
common to declare a bit sequence as an unsigned quantity:
x & y 001 0 0 0 0 0 0 0 1
x | y 037 0 0 0 1 1 1 1 1
x ^ y 036 0 0 0 1 1 1 1 0
x << 2 044 0 0 1 0 0 1 0 0
x >> 2 002 0 0 0 0 0 0 1 0
¨
Increment/Decrement Operators
The auto increment (++) and auto decrement (-) operators provide a convenient
way of, respectively, adding and subtracting 1 from a numeric variable. These
are summarized in Table 2.7. The examples assume the following variable
definition:
int k = 5;
Both operators can be used in prefix and postfix form. The difference is
significant. When used in prefix form, the operator is first applied and the
outcome is then used in the expression. When used in the postfix form, the
expression is evaluated first and then the operator applied.
Assignment Operator
The assignment operator is used for storing a value at some memory location
(typically denoted by a variable). Its left operand should be an lvalue, and its
right operand may be an arbitrary expression. The latter is evaluated and the
outcome is stored in the location denoted by the lvalue.
An lvalue (standing for left value ) is anything that denotes a memory location
in which a value may be stored. The only kind of lvalue we have seen so far in
this book is a variable. Other kinds of lvalues (based on pointers and references)
will be described later in this book.
= n = 25
+= n += 25 n = n + 25
-= n -= 25 n = n - 25 = n = 25 n = n * 25 = n = 25 n = n / 25 %= n %= 25 n = n
% 25 &= n &= 0xF2F2 n = n & 0xF2F2 |= n |= 0xF2F2 n = n | 0xF2F2 ^= n ^=
0xF2F2 n = n ^ 0xF2F2
int m, n, p;
m = n = p = 100; // means: n = (m = (p = 100)); m = (n = p = 100) + 2; // means:
m = (n = (p = 100)) + 2;
The conditional operator takes three operands. It has the general form:
operand1 ? operand2 : operand3
int m = 1, n = 2;
int min = (m < n ? m : n); // min receives 1
Note that of the second and the third operands of the conditional operator only
one is evaluated. This may be significant when one or both contain sideeffects
(i.e., their evaluation causes a change to the value of a variable). For example, in
int m = 1, n = 2, p =3;
int min = (m < n ? (m < p ? m : p)
: (n < p ? n : p));
¨
Comma Operator
Multiple expressions can be combined into one expression using the comma
operator. The comma operator takes two operands. It first evaluates the left
operand and then the right operand, and returns the value of the latter as the final
outcome. For example:
int m, n, min;
int mCount = 0, nCount = 0;
//...
min = (m < n ? mCount++, m : nCount++, n);
Here when m is less than n, mCount++ is evaluated and the value of m is stored
in min. Otherwise, nCount++ is evaluated and the value of n is stored in min. ¨
C++ provides a useful operator, sizeof, for calculating the size of any data item
or type. It takes a single operand which may be a type name (e.g., int) or an
expression (e.g., 100) and returns the size of the specified entity in bytes. The
outcome is totally machine-dependent. Listing 2.7 illustrates the use of sizeof on
the built-in types we have encountered so far.
Listing 2.7
1 #include <iostream.h>
11 cout << "1.55 size = " << sizeof(1.55) << " bytes\n";
12 cout << "1.55L size = " << sizeof(1.55L) << " bytes\n";
13 cout << "HELLO size = " << sizeof("HELLO") << " bytes\n";
14 }
When run, the program will produce the following output (on the author’s PC):
char size = 1 bytes char* size = 2 bytes short size = 2 bytes int size = 2 bytes
long size = 4 bytes float size = 4 bytes double size = 8 bytes 1.55 size = 8 bytes
1.55L size = 10 bytes HELLO size = 6 bytes
¨
Operator Precedence
Lowest ,
Binary
Order
Both
Left to Right
Right to Left
Left to Right Left to Right Left to Right Left to Right Left to Right Left to Right Left to Right Left to Right
Left to Right Left to Right Left to Right Left to Right
For example, in a == b + c * d
c * d is evaluated first because * has a higher precedence than + and ==. The
result is then added to b because + has a higher precedence than ==, and then ==
is evaluated. Precedence rules can be overridden using brackets. For example,
rewriting the above expression as
(int) 3.14 // converts 3.14 to an int to give 3 (long) 3.14 // converts 3.14 to a long
to give 3L (double) 2 // converts 2 to a double to give 2.0 (char) 122 // converts
122 to a char whose code is 122 (unsigned short) 3.14 // gives 3 as an unsigned
short
As shown by these examples, the built-in type identifiers can be used as type
operators . Type operators are unary (i.e., take one operand) and appear inside
brackets to the left of their operand. This is called explicit type conversion.
When the type name is just one word, an alternate notation may be used in which
the brackets appear around the operand:
The above rules represent some simple but common cases for type conversion.
The above rules represent some simple but common cases for type conversion.
More complex cases will be examined later in the book after we have discussed
other data types and classes.
Exercises
2.6 Add extra brackets to the following expressions to explicitly show the order
in which the operators are evaluated:
2.7 What will be the value of each of the following variables after its
initialization:
2.8 Write a program which inputs a positive integer n and outputs 2 raised to the
power of n.
2.9 Write a program which inputs three numbers and outputs the message Sorted
if the numbers are in ascending order, and outputs Not sorted otherwise. ¨
3. Statements
This chapter introduces the various forms of C++ statements for composing
programs. Statements represent the lowest-level building blocks of a program.
Roughly speaking, each statement represents a computational step which has a
certain sideeffect. (A sideeffect can be thought of as a change in the program
state, such as the value of a variable changing because of an assignment.)
Statements are useful because of the sideeffects they cause, the combination of
which enables the program to serve a specific purpose (e.g., sort a list of names).
A running program spends all of its time executing statements. The order in
which statements are executed is called flow control (or control flow). This term
reflect the fact that the currently executing statement has the control of the CPU,
which when completed will be handed over (flow) to another statement. Flow
control in a program is typically sequential, from one statement to the next, but
may be diverted to other paths by branch statements. Flow control is an
important consideration because it determines what is executed during a run and
what is not, therefore affecting the overall outcome of the program.
Compound statements are useful in two ways: (i) they allow us to put multiple
statements in places where otherwise only single statements are allowed, and (ii)
they allow us to introduce a new scope in the program. A scope is a part of the
program text within which a variable remains defined. For example, the scope of
min, i, and j in the above example is from where they are defined till the closing
brace of the compound statement. Outside the compound statement, these
variables are not defined.
The if Statement
if (expression) statement;
First expression is evaluated. If the outcome is nonzero then statement is
executed. Otherwise, nothing happens.
For example, when dividing two values, we may want to check that the
denominator is nonzero:
if (count != 0)
average = sum / count;
To make multiple statements dependent on the same condition, we can use a
compound statement:
if (balance > 0) {
interest = balance * creditRate;
balance += interest;
if ( expression) statement1;
else
statement2;
if (balance > 0) {
interest = balance * creditRate; balance += interest;
} else {
interest = balance * debitRate; balance += interest;
Given the similarity between the two alternative parts, the whole statement can
be simplified to:
if (balance > 0)
interest = balance * creditRate;
else
interest = balance * debitRate;
balance += interest;
Or simplified even further using a conditional expression: interest = balance *
(balance > 0 ? creditRate : debitRate); balance += interest;
Or just: balance += balance * (balance > 0 ? creditRate : debitRate); If
statements may be nested by having an if statement appear inside another if
statement. For example:
if (callHour > 6) {
if (callDuration <= 5)
charge = callDuration * tarrif1;
else
charge = 5 tarrif1 + (callDuration - 5) tarrif2; } else
charge = flatFee;
A frequently-used form of nested if statements involves the else part consisting
of another if-else statement. For example:
First expression (called the switch tag) is evaluated, and the outcome is
compared to each of the numeric constants (called case labels), in the order they
appear, until a match is found. The statements following the matching case are
then executed. Note the plural: each case may be followed by zero or more
statements (not just one statement). Execution continues until either a break
statement is encountered or all intervening statements until the end of the switch
statement are executed. The final default case is optional and is exercised if none
of the earlier cases provide a match.
For example, suppose we have parsed a binary arithmetic operation into its three
components and stored these in variables operator, operand1, and operand2. The
following switch statement performs the operation and stored the result in result.
switch (operator) {
case '+': result = operand1 + operand2;
break;
switch (operator) {
case '+': result = operand1 + operand2;
case '+': result = operand1 + operand2;
break;
Because case 'x' has no break statement (in fact no statement at all!), when this
case is satisfied, execution proceeds to the statements of the next case and the
multiplication is performed.
It should be obvious that any switch statement can also be written as multiple if-
else statements. The above statement, for example, may be written as:
if (operator == '+')
result = operand1 + operand2;
else if (operator == '-')
result = operand1 - operand2;
else if (operator == 'x' || operator == '*')
result = operand1 * operand2;
else if (operator == '/')
result = operand1 / operand2;
else
cout << "unknown operator: " << ch << '\n';
For example, suppose we wish to calculate the sum of all numbers from 1 to
some integer denoted by n. This can be expressed as:
i = 1;
sum = 0;
while (i <= n)
sum += i++;
For n set to 5, Table 3.10 provides a trace of the loop by listing the values of the
variables involved and the loop condition.
Table 3.10 While loop trace.
Iteration i n i <= n sum += i++
First 1 5 1 1
Second 2 5 1 3
Third 3 5 1 6
Fourth 4 5 1 10 Fifth 5 5 1 15 Sixth 6 5 0
It is not unusual for a while loop to have an empty body (i.e., a null statement).
The following loop, for example, sets n to its greatest odd factor.
while (n % 2 == 0 && n /= 2)
;
Here the loop condition provides all the necessary computation, so there is no
real need for a body. The loop condition not only tests that n is even, it also
divides n by two and ensures that the loop will terminate should n be zero.
¨
The do Statement
The do statement (also called do loop) is similar to the while statement, except
that its body is executed first and then the loop condition is examined. The
general form of the do statement is:
do statement;
while (expression);
The do loop is less frequently used than the while loop. It is useful for situations
where we need the loop body to be executed at least once, regardless of the loop
condition. For example, suppose we wish to repeatedly read a value and print its
square, and stop when the value is zero. This can be expressed as the following
loop:
do {
cin >> n;
cout << n * n << '\n';
} while (n != 0);
Unlike the while loop, the do loop is never used in situations where it would
have a null body. Although a do loop with a null body would be equivalent to a
similar while loop, the latter is always preferred for its superior readability.
The for statement (also called for loop) is similar to the while statement, but has
two additional components: an expression which is evaluated only once before
everything else, and an expression which is evaluated once at the end of each
iteration. The general form of the for statement is:
expression1;
while (expression2) { statement;
expression3;
}
The most common use of for loops is for situations where a variable is
incremented or decremented with every iteration of the loop. The following for
loop, for example, calculates the sum of all integers from 1 to n.
sum = 0;
for (i = 1; i <= n; ++i)
sum += i;
This is preferred to the while-loop version we saw earlier. In this example, i is
usually called the loop variable.
C++ allows the first expression in a for loop to be a variable definition. In the
above loop, for example, i can be defined inside the loop itself:
for (int i = 1; i <= n; ++i) sum += i;
Contrary to what may appear, the scope for i is not the body of the loop, but the
loop itself. Scope-wise, the above is equivalent to:
int i;
for (i = 1; i <= n; ++i) sum += i;
Any of the three expressions in a for loop may be empty. For example, removing
the first and the third expression gives us something identical to a while loop:
(1,1)
(1,2)
(1,3)
(2,1)
(2,2)
(2,3)
(3,1)
(3,2)
(3,3)
The continue statement terminates the current iteration of a loop and instead
jumps to the next iteration. It applies to the loop immediately enclosing the
continue statement. It is an error to use the continue statement outside a loop.
In while and do loops, the next iteration commences from the loop condition. In
a for loop, the next iteration commences from the loop’s third expression. For
example, a loop which repeatedly reads in a number, processes it but ignores
negative numbers, and terminates when the number is zero, may be expressed as:
do {
cin >> num;
if (num < 0) continue; // process num here...
A variant of this loop which reads in a number exactly n times (rather than until
the number is zero) may be expressed as:
When the continue statement appears inside nested loops, it applies to the loop
immediately enclosing it, and not to the outer loops. For example, in the
following set of nested loops, the continue applies to the for loop, and not the
while loop:
while (more) {
for (i = 0; i < n; ++i) {
cin >> num;
if (num < 0) continue; // causes a jump to: ++i // process num here...
}
//etc...
}
A break statement may appear inside a loop (while, do, or for) or a switch
statement. It causes a jump out of these constructs, and hence terminates them.
Like the continue statement, a break statement only applies to the loop or switch
immediately enclosing it. It is an error to use the break statement outside a loop
or a switch.
For example, suppose we wish to read in a user password, but would like to
allow the user a limited number of attempts:
for (i = 0; i < attempts; ++i) {
cout << "Please enter your password: ";
cin >> password;
if (Verify(password))
break;
cout << "Incorrect!\n";
}
// check password for correctness // drop out of the loop
Here we have assumed that there is a function called Verify which checks a
password and returns true if it is correct, and false otherwise.
Rewriting the loop without a break statement is always possible by using an
additional logical variable (verified) and adding it to the loop condition:
verified = 0;
for (i = 0; i < attempts && !verified; ++i) {
cout << "Please enter your password: ";
cin >> password;
verified = Verify(password));
if (!verified)
cout << "Incorrect!\n";
}
The goto statement provides the lowest-level of jumping. It has the general form:
goto label;
where label is an identifier which marks the jump destination of goto. The label
should be followed by a colon and appear before a statement within the same
function as the goto statement itself.
For example, the role of the break statement in the for loop in the previous
section can be emulated by a goto:
goto out;
cout << "Incorrect!\n";
}
out:
//etc...
// check password for correctness // drop out of the loop
Because goto provides a free and unstructured form of jumping (unlike break
and continue), it can be easily misused. Most programmers these days avoid
using it altogether in favor of clear programming. Nevertheless, goto does have
some legitimate (though rare) uses. Because of the potential complexity of such
cases, furnishing of examples is postponed to the later parts of the book.
The return statement enables a function to return a value to its caller. It has the
general form:
return expression;
where expression denotes the value returned by the function. The type of this
value should match the return type of the function. For a function whose return
type is void, expression should be empty:
return;
The only function we have discussed so far is main, whose return type is always
int. The return value of main is what the program returns to the operating system
when it completes its execution. Under UNIX, for example, it its conventional to
return 0 from main when the program executes without errors. Otherwise, a
nonzero error code is returned. For example:
When a function has a non-void return value (as in the above example), failing
to return a value will result in a compiler warning. The actual return value will
be undefined in this case (i.e., it will be whatever value which happens to be in
its corresponding memory location at the time).
Exercises
3.10 Write a program which inputs a person’s height (in centimeters) and weight
(in kilograms) and outputs one of the messages: underweight, normal, or
overweight, using the criteria:
Underweight: Normal:
Overweight: weight < height/2.5
height/2.5 <= weight <= height/2.3 height/2.3 < weight
3.11 Assuming that n is 20, what will the following code fragment output when
executed?
if (n >= 0)
if (n < 10)
cout << "n is small\n";
else
cout << "n is negative\n"; 3.12 Write a program which inputs a date in the
format dd/mm/yy and outputs it in the format month dd, year. For example,
25/12/61 becomes:
December 25, 1961
3.13 Write a program which inputs an integer value, checks that it is positive,
and outputs its factorial, using the formulas:
factorial(0) = 1
factorial(n) = n × factorial(n-1)
3.14 Write a program which inputs an octal number and outputs its decimal
equivalent. The following example illustrates the expected behavior of the
program: Input an octal number: 214 Octal(214) = Decimal(532)
3.15 Write a program which produces a simple multiplication table of the
following format for integers in the range 1 to 9:
1 x 1 = 1
1 x 2 = 2
...
9 x 9 = 81 ¨
4. Functions
This chapter describes userdefined functions as one of the main building blocks
of C++ programs. The other main building block — userdefined classes — will
be discussed in Chapter 6.
A Simple Function
Listing 4.8
1
2
3 int Power (int base, unsigned int exponent) { int result = 1;
4
5
6
7
Annotation
2 3
} for (int i = 0; i < exponent; ++i) result *= base;
return result;
This line defines the function interface. It starts with the return type of the
function (int in this case). The function name appears next followed by its
parameter list. Power has two parameters (base and exponent) which are of types
int andunsigned int, respectively Note that the syntax for parameters is similar to
the syntax for defining variables: type identifier followed by the parameter
name. However, it is not possible to follow a type identifier with multiple
comma-separated parameters:
Listing 4.9 illustrates how this function is called. The effect of this call is that
first the argument values 2 and 8 are, respectively, assigned to the parameters
base and exponent, and then the function body is evaluated.
Listing 4.9
1
2
3
4
4
5
Listing 4.8 shows the definition of a simple function which raises an integer to
the power of another, positive integer.
1
#include <iostream.h>
main (void)
{
cout << "2 ^ 8 = " << Power(2,8) << '\n';
}
When run, this program will produce the following output: 2 ^ 8 = 256
3 main (void)
4 {
5 cout << "2 ^ 8 = " << Power(2,8) << '\n';
6 }
#include <iostream.h>
Foo(x);
cout << "x = " << x << '\n'; return 0;
num = 0;
x = 10;
A reference parameter, on the other hand, receives the argument passed to it
and works on it directly. Any changes made by the function to a reference
parameter is in effect directly applied to the argument. Reference parameters will
be further discussed in Chapter 5.
Within the context of function calls, the two styles of passing arguments are,
respectively, called pass-by-value and pass-by-reference. It is perfectly valid
for a function to use pass-by-value for some of its parameters and pass-by-
reference for others. The former is used much more often in practice.
Everything defined at the program scope level (i.e., outside functions and
classes) is said to have a global scope . Thus the sample functions we have seen
so far all have a global scope. Variables may also be defined at the global scope:
int year = 1994; int Max (int, int); int main (void)
{
//... }
// global variable // global function // global function
int xyz;
void Foo (int xyz) {
if (xyz > 0) { double xyz; //...
}
}
// xyz is global
// xyz is local to the body of Foo
// xyz is local to this block
Generally, the lifetime of a variable is limited to its scope. So, for example,
global variables last for the duration of program execution, while local variables
are created when their scope is entered and destroyed when their scope is exited.
The memory space for global variables is reserved prior to program execution
commencing, whereas the memory space for local variables is allocated on the
fly during program execution.
Scope Operator
Because a local scope overrides the global scope, having a local variable with
the same name as a global variable makes the latter inaccessible to the local
scope. For example, in
int error;
void Error (int error)
{
//...
}
the global error is inaccessible inside Error, because it is overridden by the local
error parameter.
This problem is overcome using the unary scope operator :: which takes a global
entity as argument:
int error;
Auto Variables
This is rarely used because all local variables are by default automatic. ¨
Register Variables
The storage class specifier register may be used to indicate to the compiler that
the variable should be stored in a register if possible. For example: for (register
int i = 0; i < n; ++i)
sum += i;
Here, each time round the loop, i is used three times: once when it is compared
to n, once when it is added to sum, and once when it is incremented. Therefore it
makes sense to keep i in a register for the duration of the loop.
Note that register is only a hint to the compiler, and in some cases the compiler
may choose not to use a register when it is asked to do so. One reason for this is
that any machine has a limited number of registers and it may be the case that
they are all in use.
Even when the programmer does not use register declarations, many optimizing
compilers try to make an intelligent guess and use registers where they are likely
to improve the performance of the program.
Use of register declarations can be left as an after thought; they can always be
added later by reviewing the code and inserting it in appropriate places. ¨
//... }
//...
The same argument may be applied to the global variables in this file that are for
the private use of the functions in the file. For example, a global variable which
records the length of the shortest route so far is best defined as static:
A local variable in a function may also be defined as static. The variable will
remain only accessible within its local scope; however, its lifetime will no longer
be confined to this scope, but will instead be global. In other words, a static local
variable is a global variable which is only accessible within its local scope.
Static local variables are useful when we want the value of a local variable to
persist across the calls to the function in which it appears. For example, consider
an Error function which keeps a count of the errors and aborts the program when
the count exceeds a preset limit:
Because a global variable may be defined in one file and referred to in other
files, some means of telling the compiler that the variable is defined elsewhere
may be needed. Otherwise, the compiler may object to the variable as undefined.
This is facilitated by an extern declaration. For example, the declaration
informs the compiler that size is actually defined somewhere (may be later in
this file or in another file). This is called a variable declaration (not definition)
because it does not lead to any storage being allocated for size.
extern int size = 10; // no longer a declaration! If there is another definition for
size elsewhere in the program, it will eventually clash with this one.
Function prototypes may also be declared as extern, but this has no effect when a
prototype appears at the global scope. It is more useful for declaring function
prototypes inside a function. For example:
Symbolic Constants
With pointers, two aspects need to be considered: the pointer itself, and the
object pointed to, either of which or both can be constant:
// illegal! // ok
// illegal! // ok
// illegal! // illegal!
{
return "5.2.1";
}
The usual place for constant definition is within header files so that they can be
shared by source files.
¨
Enumerations
introduces four enumerators which have integral values starting from 0 (i.e.,
north is 0, south is 1, etc.) Unlike symbolic constants, however, which are
readonly variables, enumerators have no allocated memory.
An enumeration can also be named, where the name becomes a userdefined type.
This is useful for defining variables which can only be assigned a limited set of
values. For example, in
switch (d) {
case north: //...
case south: //...
case east: //...
case west: //...
}
}
Runtime Stack
Like many other modern programming languages, C++ function call execution is
based on a runtime stack. When a function is called, memory space is allocated
on this stack for the function parameters, return value, and local variables, as
well as a local stack area for expression evaluation. The allocated space is called
a stack frame. When a function returns, the allocated stack frame is released so
that it can be reused.
For example, consider a situation where main calls a function called Solve which
in turn calls another function called Normalize:
int Normalize (void)
{
//...
}
Figure 4.5 illustrates the stack frame when Normalize is being executed.
Figure 4.5 Function call stack frames. main Solve Normalize
Inline Functions
{
return n > 0 ? n : -n;
}
The disadvantage of the function version, however, is that its frequent use can
lead to a considerable performance penalty due to the overheads associated with
calling a function. For example, if Abs is used within a loop which is iterated
thousands of times, then it will have an impact on performance. The overhead
can be avoided by defining Absas an inline function:
{
return n > 0 ? n : -n;
}
The effect of this is that when Abs is called, the compiler, instead of generating
code to call Abs, expands and substitutes the body of Abs in place of the call.
While essentially the same computation is performed, no function call is
involved and hence no stack frame is allocated.
Because calls to an inline function are expanded, no trace of the function itself
will be left in the compiled code. Therefore, if a function is defined inline in one
file, it may not be available to other files. Consequently, inline functions are
file, it may not be available to other files. Consequently, inline functions are
commonly placed in header files so that they can be shared.
Like the register keyword, inline is a hint which the compiler is not obliged to
observe. Generally, the use of inline should be restricted to simple, frequently
used functions. A function which contains anything more than a couple of
statements is unlikely to be a good candidate. Use of inline for excessively long
and complex functions is almost certainly ignored by the compiler. ¨
Recursion
• Factorial of 0 is 1.
• Factorial of a positive number n is n times the factorial of n-1.
The second line clearly indicates that factorial is defined in terms of itself and
hence can be expressed as a recursive function:
int Factorial (unsigned int n)
{
return n == 0 ? 1 : n * Factorial(n-1);
}
For n set to 3, Table 4.11 provides a trace of the calls to Factorial. The stack
frames for these calls appear sequentially on the runtime stack, one after the
other.
Second 2 0 2 * Factorial(1) 2
Third 1 0 1 * Factorial(0) 1
Fourth 0 1 1
A recursive function must have at least one termination condition which can be
satisfied. Otherwise, the function will call itself indefinitely until the runtime
stack overflows. The Factorial function, for example, has the termination
condition n == 0 which, when satisfied, causes the recursive calls to fold back.
(Note that for a negative n this condition will never be satisfied and Factorial
will fail).
For factorial, for example, a very large argument will lead to as many stack
frames. An iterative version is therefore preferred in this case:
Default Arguments
Default arguments are suitable for situations where certain (or all) function
parameters frequently take the same values. In Error, for example, severity 0
errors are more common than others and therefore a good candidate for default
argument. A less appropriate use of default arguments would be:
Menu can access its arguments using a set of macro definitions in the header file
stdarg.h, as illustrated by Listing 4.11. The relevant macros are highlighted in
bold.
Listing 4.11
1 #include <iostream.h>
2 #include <stdarg.h>
9 do {
10 cout << ++count << ". " << option << '\n'; 11 } while ((option = va_arg(args,
char*)) != 0);
12 Finally, va_end is called to restore the runtime stack (which may have been
modified by the earlier calls).
The sample call
int n = Menu(
"Open file",
"Close file",
"Revert to saved file", "Delete file",
"Quit application", 0);
As an example, consider a program named sum which prints out the sum of a set
of numbers provided to it as command line arguments. Dialog 4.3 illustrates how
two numbers are passed as arguments to sum ($ is the UNIX prompt).
Dialog 4.3
1 $ sum 10.4 12.5
2 22.9
3 $
Command line arguments are made available to a C++ program via the main
function. There are two ways in which main can be defined:
int main (void);
int main (int argc, const char* argv[]);
The latter is used when the program is intended to accept command line
arguments. The first parameter, argc, denotes the number of arguments passed to
the program (including the name of the program itself). The second parameter,
argv, is an array of the string constants which represent the arguments. For
example, given the command line in Dialog 4.3, we have:
argc is 3
argv[0] is "sum"
argv[1] is "10.4"
argv[2] is "12.5"
Listing 4.12 illustrates a simple implementation for sum. Strings are converted to
real numbers using atof, which is defined in stdlib.h.
Listing 4.12
1 #include <iostream.h>
2 #include <stdlib.h>
Exercises
x = 10;
y = 20;
Swap(x, y);
4.19 Write a function which outputs all the prime numbers between 2 and a
given positive integer n:
void Primes (unsigned int n);
A number is prime if it is only divisible by itself and 1.
4.20 Define an enumeration called Month for the months of the year and use it to
define a function which takes a month as argument and returns it as a constant
string.
4.21 Define an inline function called IsAlpha which returns nonzero when its
argument is a letter, and zero otherwise.
4.22 Define a recursive version of the Power function described in this chapter.
4.23 Write a function which returns the sum of a list of real values double Sum
(int n, double val ...); where n denotes the number of values in the list. ¨
An array consists of a set of objects (called its elements), all of which are of the
same type and are arranged contiguously in memory. In general, only the array
itself has a symbolic name, not its elements. Each element is identified by an
index which denotes the position of the element in the array. The number of
elements in an array is called its dimension. The dimension of an array is fixed
and predetermined; it cannot be changed during program execution.
Arrays are suitable for representing composite data which consist of many
similar, individual items. Examples include: a list of names, a table of world
cities and their current temperatures, or the monthly transactions for a bank
account.
Pointers are useful for creating dynamic objects during program execution.
Unlike normal (global and local) objects which are allocated storage on the
runtime stack, a dynamic object is allocated memory from a different storage
area called the heap. Dynamic objects do not obey the normal scope rules. Their
scope is explicitly controlled by the programmer.
Arrays
An array variable is defined by specifying its dimension and the type of its
elements. For example, an array representing 10 height measurements (each
being an integer quantity) may be defined as:
int heights[10];
The individual elements of the array are accessed by indexing the array. The first
array element always has the index 0. Therefore, heights[0] and heights[9]
denote, respectively, the first and last element of heights. Each of heights
elements can be treated as an integer variable. So, for example, to set the third
element to 177, we may write:
heights[2] = 177;
Processing of an array usually involves a loop which goes through the array
element by element. Listing 5.13 illustrates this using a function which takes an
array of integers and returns the average of its elements.
array of integers and returns the average of its elements.
Listing 5.13
1 const int size = 3;
Like other variables, an array may have an initializer. Braces are used to specify
a list of comma-separated initial values for array elements. For example, int
nums[3] = {5, 10, 15};
initializes the three elements of nums to 5, 10, and 15, respectively. When the
number of values in the initializer is less than the number of elements, the
remaining elements are initialized to zero:
Another situation in which the dimension can be omitted is for an array function
parameter. For example, the Average function above can be improved by
rewriting it so that the dimension of nums is not fixed to a constant, but specified
by an additional parameter. Listing 5.14 illustrates this.
Listing 5.14
1 double Average (int nums[], int size)
2 {
3 double average = 0;
Multidimensional Arrays
An array may have more than one dimension (i.e., two, three, or higher). The
organization of the array in memory is still the same (a contiguous sequence of
elements), but the programmer’s perceived organization of the elements is
different. For example, suppose we wish to represent the average seasonal
temperature for three major Australian capital cities (see Table 5.12).
int seasonTemp[3][4] = {
{26, 34, 22, 17},
{24, 32, 19, 13},
{28, 38, 25, 20}
};
int seasonTemp[3][4] = {
26, 34, 22, 17, 24, 32, 19, 13, 28, 38, 25, 20
};
int seasonTemp[3][4] = {{26}, {24}, {28}}; We can also omit the first
dimension (but not subsequent dimensions) and let it be derived from the
initializer:
int seasonTemp[][4] = { {26, 34, 22, 17}, {24, 32, 19, 13}, {28, 38, 25, 20}
};
Listing 5.15
1 const int rows = 3;
2 const int columns = 4;
3 int seasonTemp[rows][columns] = {
4 {26, 34, 22, 17},
4 {26, 34, 22, 17},
5 {24, 32, 19, 13},
6 {28, 38, 25, 20}
7 };
Pointers
The symbol & is the address operator; it takes a variable as argument and
returns the memory address of that variable. The effect of the above assignment
is that the address of num is assigned to ptr1. Therefore, we say that ptr1 points
to num. Figure 5.7 illustrates this diagrammatically.
In general, the type of a pointer must match the type of the data it is set to point
to. A pointer of type void*, however, will match any type. This is useful for
defining pointers which may point to data of different types, or whose type is
originally unknown.
A pointer may be cast (type converted) to another type. For example, ptr2 =
(char*) ptr1;
converts ptr1 to char pointer before assigning it to ptr2.
Regardless of its type, a pointer may be assigned the value 0 (called the null
pointer). The null pointer is used for initializing pointers, and for marking the
end of pointer-based data structures (e.g., linked lists).
Dynamic Memory
In addition to the program stack (which is used for storing global variables and
stack frames for function calls), another memory area, called the heap, is
provided. The heap is used for dynamically allocating memory blocks during
program execution. As a result, it is also called dynamic memory. Similarly, the
program stack is also called static memory.
Two operators are used for allocating and deallocating memory blocks on the
heap. The new operator takes a type as argument and allocated a memory block
for an object of that type. It returns a pointer to the allocated block. For example,
when Foo returns, the local variable str is destroyed, but the memory block
pointed to by str is not. The latter remains allocated until explicitly released by
the programmer.
Dynamic objects are useful for creating data which last beyond the function call
which creates them. Listing 5.16 illustrates this using a function which takes a
string parameter and returns a copy of the string.
Listing 5.16
1 #include <string.h>
5 strcpy(copy, str);
6 return copy;
7 }
Annotation 1 This is the standard string header file which declares a variety of
functions for manipulating strings.
4 The strlenfunction (declared in string.h) counts the characters in its string
argument up to (but excluding) the final null character. Because the null
character is not included in the count, we add 1 to the total and allocate an array
of characters of that size.
5 The strcpy function (declared in string.h) copies its second argument to its
first, character by character, including the final null character.
Because of the limited memory resources, there is always the possibility that
dynamic memory may be exhausted during program execution, especially when
many large blocks are allocated and none released. Should new be unable to
allocate a block of the requested size, it will return 0 instead. It is the
responsibility of the programmer to deal with such possibilities. The exception
handling mechanism of C++ (explained in Chapter 10) provides a practical
method of dealing with such problems.
Pointer Arithmetic
In C++ one can add an integer quantity to or subtract an integer quantity from a
pointer. This is frequently used by programmers and is called pointer arithmetic.
Pointer arithmetic is not the same as integer arithmetic, because the outcome
depends on the size of the object pointed to. For example, suppose that an int is
represented by 4 bytes. Now, given
str++ advances str by one char (i.e., one byte) so that it points to the second
character of "HELLO", whereas ptr++ advances ptr by one int (i.e., four bytes)
so that it points to the second element of nums. Figure 5.8 illustrates this
diagrammatically.
Listing 5.17
1 void CopyString (char *dest, char *src)
2 {
3 while (*dest++ = *src++)
4 ;
5 }
Annotation 3 The condition of this loop assigns the contents of src to the
contents of dest and then increments both pointers. This condition becomes 0
when the final null character of src is copied to dest.
In turns out that an array variable (such as nums) is itself the address of the first
element of the array it represents. Hence the elements of nums can also be
referred to using pointer arithmetic on nums, that is, nums[i] is equivalent to *
(nums + i). The difference between nums and ptr is that nums is a constant, so it
cannot be made to point to anything else, whereas ptr is a variable and can be
made to point to any other integer.
Listing 5.18 shows how the HighestTemp function (shown earlier in Listing
5.15) can be improved using pointer arithmetic.
Listing 5.18
1
2
3 int HighestTemp (const int *temp, const int rows, const int columns) { int
highest = 0;
}
4
5
6
7
8
9
for (register i = 0; i < rows; ++i)
for (register j = 0; j < columns; ++j) if (*(temp + i columns + j) > highest)
highest = (temp + i * columns + j); return highest;
Annotation 1
6 Instead of passing an array to the function, we pass an int pointer and two
additional parameters which specify the dimensions of the array. In this way, the
function is not restricted to a specific array size.
Listing 5.19
1
2
3
int HighestTemp (const int *temp, const int rows, const int columns) { int
highest = 0;
}
4
5
6
7
8
for (register i = 0; i < rows * columns; ++i) if (*(temp + i) > highest)
highest = *(temp + i);
return highest;
¨
Function Pointers
defines a function pointer named Compare which can hold the address of any
function that takes two constant character pointers as arguments and returns an
integer. The string comparison library function strcmp, for example, is such.
Therefore:
Compare = &strcmp; // Compare points to strcmp function The & operator is not
necessary and can be omitted:
Compare = strcmp; // Compare points to strcmp function Alternatively, the
pointer can be defined and initialized at once:
int (*Compare)(const char*, const char*) = strcmp;
When a function address is assigned to a function pointer, the two types must
match. The above definition is valid because strcmp has a matching function
prototype:
2 Compare is the function pointer to be used for comparing item against the
array elements.
7 Each time round this loop, the search span is reduced by half. This is repeated
until the two ends of the search span (denoted by bot and top) collide, or until a
match is found.
11 If item is less than the middle item, then the search is restricted to the lower
half of the array.
14 If item is greater than the middle item, then the search is restricted to the
upper half of the array.
16 Returns -1 to indicate that there was no matching item.
The following example shows how BinSearch may be called with strcmp passed
as the comparison function:
char *cities[] = {"Boston", "London", "Sydney", "Tokyo"}; cout <<
BinSearch("Sydney", cities, 4, strcmp) << '\n'; This will output 2 as expected. ¨
References
defines num2 as a reference to num1. After this definition num1 and num2 both
refer to the same object, as if they were the same variable. It should be
emphasized that a reference does not create a copy of an object, but merely a
symbolic alias for it. Hence, after
num1 = 0.16;
both num1 and num2 will denote the value 0.16.
A reference must always be initialized when it is defined: it should be an alias
for something. It would be illegal to define a reference and initialize it later.
double &num3; // illegal: reference without an initializer num3 = num1;
You can also initialize a reference to a constant. In this case a copy of the
constant is made (after any necessary type conversion) and the reference is set to
refer to the copy.
int &x = 1;
++x;
int y = x + 1;
The 1 in the first and the 1 in the third line are likely to be the same object (most
compilers do constant optimization and allocate both 1’s in the same memory
location). So although we expect y to be 3, it could turn out to be 4. However, by
forcing x to be a copy of 1, the compiler guarantees that the object denoted by x
will be different from both 1’s.
Listing 5.21
18 }
Annotation
1 Although Swap1 swaps x and y, this has no effect on the arguments passed to
the function, because Swap1 receives a copy of the arguments. What happens to
the copy does not affect the original.
7 Swap2overcomes the problem of Swap1 by using pointer parameters instead.
By dereferencing the pointers, Swap2 gets to the original values and swaps them.
10, 20
20, 10
10, 20 ¨
Typedefs
Typedef is a syntactic facility for introducing symbolic names for data types.
Just as a reference defines an alias for an object, a typedef defines an alias for a
type. Its main use is to simplify otherwise complicated type declarations as an
aid to improved readability. Here are a few examples:
The effect of these definitions is that String becomes an alias for char*, Name
becomes an alias for an array of 12 chars, and uint becomes an alias for unsigned
int. Therefore:
String str; // is the same as: char *str; Name name; // is the same as: char
name[12]; uint n; // is the same as: unsigned int n;
The complicated declaration of Compare in Listing 5.20 is a good candidate for
typedef:
typedef int (*Compare)(const char*, const char*);
int BinSearch (char item, char table[], int n, Compare comp) {
//...
if ((cmp = comp(item, table[mid])) == 0) return mid;
//...
}
The typedef introduces Compare as a new type name for any function with the
given prototype. This makes BinSearch’s signature arguably simpler. ¨
Exercises
5.24 Define two functions which, respectively, input values for the elements of
an array of reals and output the array elements:
void ReadArray (double nums[], const int size);
void WriteArray (double nums[], const int size);
5.25 Define a function which reverses the order of the elements of an array of
reals: void Reverse (double nums[], const int size);
5.26 The following table specifies the major contents of four brands of breakfast
cereals. Define a two-dimensional array to capture this data:
Fiber Sugar Fat Salt Top Flake 12g 25g 16g 0.4g Cornabix 22g 4g 8g 0.3g Oatabix 28g 5g 9g 0.5g
Ultrabran 32g 7g 2g 0.2g
Write another function which sorts the list using bubble sort: void BubbleSort
(char *names[], const int size);
Bubble sort involves repeated scans of the list, where during each scan adjacent
items are compared and swapped if out of order. A scan which involves no
swapping indicates that the list is sorted.
5.29 Rewrite BubbleSort (from 5.27) so that it uses a function pointer for
comparison of names.
5.30 Rewrite the following using typedefs:
6. Classes
This chapter introduces the class construct of C++ for defining new data types. A
data type consists of two things:
• A concrete representation of the objects of the type.
• A set of operations for manipulating the objects.
Added to these is the restriction that, other than the designated operations, no
other operation should be able to manipulate the objects. For this reason, we
often say that the operations characterize the type, that is, they decide what can
and what cannot happen to the objects. For the same reason, proper data types as
such are often called abstract data types – abstract because the internal
representation of the objects is hidden from operations that do not belong to the
type.
A class definition consists of two parts: header and body. The class header
specifies the class name and its base classes. (The latter relates to derived
classes and is discussed in Chapter 8.) The class body defines the class
members . Two types of members are supported:
• Data members have the syntax of variable definitions and specify the
representation of class objects.
• Member functions have the syntax of function prototypes and specify the
class operations, also called the class interface.
Class members fall under one of three different access permission categories:
• Public members are accessible by all class users.
• Private members are only accessible by the class members.
• Protected members are only accessible by the class members and the members
of a derived class.
The data type defined by a class is used in exactly the same way as a built-in
type.
A Simple Class
Listing 6.22 shows the definition of a simple class for representing points in two
dimensions.
Listing 6.22
1 class Point {
2 int xVal, yVal;
3 public:
4 void SetPt (int, int);
5 void OffsetPt (int, int);
6 };
Annotation 1 This line contains the class header and names the class as Point. A
class definition always begins with the keyword class, followed by the class
name. An open brace marks the beginning of the class body.
2 This line defines two data members, xVal and yVal, both of type int. The
default access permission for a class member is private. Both xVal and yVal are
therefore private.
3 This keyword specifies that from this point onward the class members are
public.
4-5 These two are public member functions. Both have two integer parameters
and a void return type.
6 This brace marks the end of the class body.
The order in which the data and member functions of a class are presented is
largely irrelevant. The above class, for example, may be equivalently written as:
largely irrelevant. The above class, for example, may be equivalently written as:
class Point {
public:
void SetPt (int, int);
void OffsetPt (int, int); private:
int xVal, yVal;
};
The actual definition of the member functions is usually not part of the class and
appears separately. Listing 6.23 shows the separate definition of SetPt and
OffsetPt.
Listing 6.23
1 void Point::SetPt (int x, int y)
2 {
3 xVal = x;
4 yVal = y;
5 }
3-4 Note how SetPt (being a member of Point) is free to refer to xVal and yVal.
Nonmember functions do not have this privilege.
Once a class is defined in this way, its name denotes a new data type, allowing
us to define variables of that type. For example:
Point pt;
pt.SetPt(10,20); pt.OffsetPt(2,2); // pt is an object of class Point // pt is set to
(10,20)
// pt becomes (12,22)
Member functions are called using the dot notation: pt.SetPt(10,20) calls SetPt
for the object pt, that is, pt is an implicit argument to SetPt.
By making xVal and yVal private members of the class, we have ensured that a
user of the class cannot manipulate them directly:
At this stage, we should clearly distinguish between object and class. A class
denotes a type, of which there is only one. An object is an element of a particular
type (class), of which there may be many. For example,
defines three objects ( pt1, pt2, and pt3) all of the same class (Point).
Furthermore, operations of a class are applied to objects of that class, but never
the class itself. A class is therefore a concept that has no concrete existence other
than that reflected by its objects. ¨
class Point {
int xVal, yVal;
public:
void SetPt (int x,int y) { xVal = x; yVal = y; } void OffsetPt (int x,int y) { xVal
+= x; yVal += y; }
};
Note that because the function body is included, no semicolon is needed after the
prototype. Furthermore, all function parameters must be named. ¨
Listing 6.24
1 #include <iostream.h>
2 const maxCard = 100; 3 enum Bool {false, true};
4 class Set {
5 public:
6 void EmptySet
7 Bool Member
8 void AddElem
9 void RmvElem 10 void Copy 11 Bool Equal 12 void Intersect 13 void Union
14 void Print 15 private:
16 int elems[maxCard]; // set elements 17 int card; // set cardinality 18 };
(void) { card = 0; } (const int);
(const int);
(const int);
(Set&);
(Set&);
(Set&, Set&);
(Set&, Set&);
(void);
6 EmptySet clears the contents of the set by setting its cardinality to zero.
7 Member checks if a given number is an element of the set.
8 AddElem adds a new element to the set. If the element is already in the set
then nothing happens. Otherwise, it is inserted. Should this result in an overflow
then the element is not inserted.
9 RmvElem removes an existing element from the set, provided that element is
already in the set.
10 Copy copies one set to another. The parameter of this function is a reference
to the destination set.
11 Equal checks if two sets are equal. Two sets are equal if they contain exactly
the same elements (the order of which is immaterial).
12 Intersect compares two sets to produce a third set (denoted by its last
parameter) whose elements are in both sets. For example, the intersection of
{2,5,3} and {7,5,2} is {2,5}.
13 Union compares two sets to produce a third set (denoted by its last parameter)
whose elements are in either or both sets. For example, the union of {2,5,3} and
{7,5,2} is {2,5,3,7}.
14 Print prints a set using the conventional mathematical notation. For example,
a set containing the numbers 5, 2, and 10 is printed as {5,2,10}. 16 The elements
of the set are represented by the elems array.
17 The cardinality of the set is denoted by card. Only the first card entries in
elems are considered to be valid elements.
The following main function creates three Set objects and exercises some of its
member functions.
int main (void)
{
Set s1, s2, s3;
cout << "s1 = "; s1.Print(); cout << "s2 = "; s2.Print();
s2.RmvElem(50); cout << "s2 - {50} = "; s2.Print(); if (s1.Member(20)) cout <<
"20 is in s1\n";
s1.Intersect(s2,s3); s1.Union(s2,s3); cout << "s1 intsec s2 = "; s3.Print(); cout <<
"s1 union s2 = "; s3.Print();
s1 = {10,20,30,40}
s2 = {30,50,10,60}
s2 - {50} = {30,10,60}
20 is in s1
s1 intsec s2 = {10,30}
s1 union s2 = {30,10,60,20,40} s1 /= s2
Constructors
It is possible to define and at the same time initialize objects of a class. This is
supported by special member functions called constructors. A constructor always
has the same name as the class itself. It never has an explicit return type. For
example,
class Point {
int xVal, yVal;
public:
Point (int x,int y) {xVal = x; yVal = y;} // constructor void OffsetPt (int,int);
};
is an alternative definition of the Point class, where SetPt has been replaced by a
constructor, which in turn is defined to be inline.
Now we can define objects of type Point and initialize them at once. This is in
fact compulsory for classes that contain constructors that require arguments:
Point pt1 = Point(10,20);
Point pt2; // illegal!
The former can also be specified in an abbreviated form.
Point pt1(10,20);
A class may have more than one constructor. To avoid ambiguity, however, each
of these must have a unique signature. For example,
class Point {
int xVal, yVal;
public:
Point (int x, int y) Point (float, float); Point (void)
void OffsetPt (int, int);
};
{ xVal = x; yVal = y; }
// polar coordinates { xVal = yVal = 0; } // origin
offers three different constructors. An object of type Point can be defined using
any of these:
class Set {
public:
Set (void) { card = 0; }
//...
};
This has the distinct advantage that the programmer need no longer remember to
call EmptySet. The constructor ensures that every set is initially empty.
The Set class can be further improved by giving the user control over the
maximum size of a set. To do this, we define elems as an integer pointer rather
than an integer array. The constructor can then be given an argument which
specifies the desired size. This means that maxCard will no longer be the same
for all Set objects and therfore needs to become a data member itself:
//... private:
int
int
int
};
*elems; maxCard; card;
// set elements
// maximum cardinality // set cardinality
The constructor simply allocates a dynamic array of the desired size and
initializes maxCard and card accordingly:
Destructors are generally useful for classes which have pointer data members
which point to memory blocks allocated by the class itself. In such cases it is
important to release member-allocated memory before the object is destroyed. A
destructor can do just that.
For example, our revised version of Set uses a dynamically-allocated array for
the elems member. This memory should be released by a destructor:
// set elements
// maximum cardinality
// set cardinality
Now consider what happens when a Set is defined and used in a function:
When Foo is called, the constructor for s is invoked, allocating storage for
s.elems and initializing its data members. Next the rest of the body of Foo is
executed. Finally, before Foo returns, the destructor for s is invoked, deleting the
storage occupied by s.elems. Hence, as far as storage allocation is concerned, s
behaves just like an automatic variable of a built-in type, which is created when
its scope is entered and destroyed when its scope is left.
Friends
Suppose that we have defined two variants of the Set class, one for sets of
integers and one for sets of reals:
class IntSet {
public:
//...
private:
int elems[maxCard];
int card;
};
class RealSet {
public:
//...
private:
float elems[maxCard];
int card;
};
};
Although this works, the overhead of calling AddElem for every member of the
set may be unacceptable. The implementation can be improved if we could gain
access to the private members of both IntSet and RealSet. This can be arranged
by declaring SetToRealas a friend of RealSet.
class RealSet {
//...
friend void IntSet::SetToReal (RealSet&);
};
void IntSet::SetToReal (RealSet &set) {
set.card = card;
for (register i = 0; i < card; ++i) set.elems[i] = (float) elems[i]; }
class A;
class B {
//...
friend class A; // abbreviated form
};
class IntSet {
//...
friend void SetToReal (IntSet&, RealSet&);
};
class RealSet {
//...
friend void SetToReal (IntSet&, RealSet&);
};
Although a friend declaration appears inside a class, that does not make the
function a member of that class. In general, the position of a friend declaration in
a class is irrelevant: whether it appears in the private, protected, or the public
section, it has the same meaning.
Default Arguments
For example, a constructor for the Point class may use default arguments to
provide more variations of the way a Point object may be defined:
class Point {
int xVal, yVal;
public:
Point (int x = 0, int y = 0);
//...
};
class Point {
int xVal, yVal;
public:
Point (int x = 0, int y = 0);
Point (float x = 0, float y = 0); // polar coordinates //...
};
Point pt(10,20);
pt.OffsetPt(2,2);
Scope Operator
In some situations, using the scope operator is essential. For example, the case
where the name of a class member is hidden by a local variable (e.g., member
function parameter) can be overcome using the scope operator:
class Point {
public:
Point (int x, int y) { Point::x = x; Point::y = y; }
//...
private:
int x, y;
}
Here x and y in the constructor (inner scope) hide x and y in the class (outer
scope). The latter are referred to explicitly as Point::x and Point::y. ¨
There are two ways of initializing the data members of a class. The first
approach involves initializing the data members using assignments in the body
of a constructor. For example:
class Image {
public:
};
};
Image::Image (const int w, const int h) : width(w), height(h) {
//... }
A member initialization list may be used for initializing any data member of a
class. It is always placed between the constructor header and body. A colon is
used to separate it from the header. It should consist of a comma-separated list of
data members whose initial value appears within a pair of brackets.
Constant Members
};
However, data member constants cannot be initialized using the same syntax as
for other constants:
class Image {
const int width = 256; const int height = 168; //...
};
// illegal initializer! // illegal initializer!
};
Image::Image (const int w, const int h) : width(w), height(h) {
//... }
//... private:
const
int
int
};
maxCard;
elems[maxCard]; // illegal! card;
the array elems will be rejected by the compiler for not having a constant
dimension. The reason for this being that maxCard is not bound to a value
during compilation, but when the program is run and the constructor is invoked.
Member functions may also be defined as constant. This is used to specify which
member functions of a class may be invoked for a constant object. For example,
Set
Bool Member void AddElem //...
};
(void) { card = 0; } (const int) const;
(const int);
const Set s;
s.AddElem(10); // illegal: AddElem not a const member s.Member(10); // ok
¨
Static Members
A data member of a class can be defined to be static. This ensures that there will
be exactly one copy of the member, shared by all objects of the class. For
example, consider a Window class which represents windows on a bitmap
display:
class Window {
static Window *first; // linked-list of all windows Window *next; // pointer to
next window //...
};
Here, no matter how many objects of type Window are defined, there will be
only one instance of first. Like other static variables, a static data member is by
default initialized to 0. It can be initialized to an arbitrary value in the same
scope where the member function definitions appear:
The alternative is to make such variables global, but this is exactly what static
members are intended to avoid; by including the variable in a class, we can
ensure that it will be inaccessible to anything outside the class.
For example, the Window class might use a call-back function for repainting
exposed areas of the window:
class Window {
//...
static void PaintProc (Event *event); // call-back
};
Because static members are shared and do not rely on the this pointer, they are
best referred to using the class::member syntax. For example, first and PaintProc
would be referred to as Window::first and Window::PaintProc. Public static
members can be referred to using this syntax by nonmember functions (e.g.,
global functions).
Member Pointers
Recall how a function pointer was used in Chapter 5 to pass the address of a
comparison function to a search function. It is possible to obtain and manipulate
the address of a member function of a class in a similar fashion. As before, the
idea is to make a function more flexible by making it independent of another
function.
defines a member function pointer type called Compare for a class called Table.
This type will match the address of any member function of Table which takes
two constant character pointers and returns an int. Compare may be used for
passing a pointer to a Searchmember of Table:
class Table {
public:
Table (const int slots);
int Search (char *item, Compare comp);
int CaseSesitiveComp (const char*, const char*); int NormalizedComp (const
char*, const char*); private:
int slots;
char **entries;
};
Note that comp can only be invoked via a Table object (the this pointer is used in
this case). None of the following attempts, though seemingly reasonable, will
work:
The above class member pointer syntax applies to all members except for static.
Static members are essentially global entities whose scope has been limited to a
class. Pointers to static members use the conventional syntax of global entities.
In general, the same protection rules apply as before: to take the address of a
class member (data or function) one should have access to it. For example, a
function which does not have access to the private members of a class cannot
take the address of any of those members.
References Members
class Image {
int width;
int height; int &widthRef; //...
};
class Image {
int width;
int height;
int &widthRef = width; // illegal!
//...
};
};
Image::Image (const int w, const int h) : widthRef(width) {
//... }
class Rectangle {
public:
Rectangle (int left, int top, int right, int bottom);
//...
private:
Point topLeft;
Point botRight;
};
The constructor for Rectangle should also initialize the two object members of
the class. Assuming that Point has a constructor, this is done by including
topLeft and botRight in the member initialization list of the constructor for
Rectangle:
Rectangle::Rectangle (int left, int top, int right, int bottom) : topLeft(left,top),
botRight(right,bottom)
{
}
If the constructor for Point takes no parameters, or if it has default arguments for
all of its parameters, then the above member initialization list may be omitted.
Of course, the constructor is still implicitly called.
The order of initialization is always as follows. First, the constructor for topLeft
is invoked, followed by the constructor for botRight, and finally the constructor
for Rectangle itself. Object destruction always follows the opposite direction.
First the destructor for Rectangle (if any) is invoked, followed by the destructor
for botRight, and finally for topLeft. The reason that topLeft is initialized before
botRight is not that it appears first in the member initialization list, but because it
appears before botRight in the class itself. Therefore, defining the constructor as
follows would not change the initialization (or destruction) order:
Object Arrays
An array of a userdefined type is defined and used much in the same way as an
array of a built-in type. For example, a pentagon can be defined as an array of 5
points:
Point pentagon[5];
This definition assumes that Point has an ‘argument-less’ constructor (i.e., one
which can be invoked without arguments). The constructor is applied to each
element of the array.
The array can also be initialized using a normal array initializer. Each entry in
the initialization list would invoke the constructor with the desired arguments.
When the initializer has less entries than the array dimension, the remaining
elements are initialized by the argument-less constructor. For example,
Point pentagon[5] = {
Point(10,20), Point(10,30), Point(20,30), Point(30,20)
};
initializes the first four elements of pentagon to explicit points, and the last
element is initialized to (0,0).
When the constructor can be invoked with a single argument, it is sufficient to
just specify the argument. For example,
Unless the [] is included, delete will have no way of knowing that pentagon
denotes an array of points and not just a single point. The destructor (if any) is
applied to the elements of the array in reverse order before the array is deleted.
Omitting the [] will cause the destructor to be applied to just the first element of
the array:
Since the objects of a dynamic array cannot be explicitly initialized at the time of
creation, the class must have an argument-less constructor to handle the implicit
initialization. When this implicit initialization is insufficient, the programmer
can explicitly reinitialize any of the elements later:
pentagon[0].Point(10, 20);
pentagon[1].Point(10, 30);
//...
class Polygon {
public:
//...
private:
Point *vertices; // the vertices
int nVertices; // the number of vertices };
¨
Class Scope
A class introduces a class scope much in the same way a function (or block)
introduces a local scope. All the class members belong to the class scope and
thus hide entities with identical names in the enclosing scope. For example, in
class Process {
int fork (void);
//...
};
the member function fork hides the global system function fork. The former can
refer to the latter using the unary scope operator:
• At the global scope. This leads to a global class, whereby it can be referred to
by all other scopes. The great majority of C++ classes (including all the
examples presented so far in this chapter) are defined at the global scope.
• At the class scope of another class. This leads to a nested class, where a class
is contained by another class.
• At the local scope of a block or function. This leads to a local class, where the
class is completely contained by a block or function.
A nested class is useful when a class is used only by one other class. For
example,
class Rectangle { // a nested class
public:
Rectangle (int, int, int, int);
//..
//..
private:
class Point {
public:
Point (int, int);
private:
int x, y;
};
Point topLeft, botRight;
};
A nested class may still be accessed outside its enclosing class by fully
qualifying the class name. The following, for example, would be valid at any
scope (assuming that Pointis made public within Rectangle):
Rectangle::Point pt(1,1); A local class is useful when a class is used by only one
function — be it a global function or a member function — or even just one
block. For example,
A local class must be completely defined inside the scope in which it appears.
All of its functions members, therefore, need to be defined inline inside the class.
This implies that a local scope is not suitable for defining anything but very
simple classes.
struct Point {
};
is equivalent to:
};
The struct construct originated in C, where it could only contain data members.
It has been retained mainly for backward compatibility reasons. In C, a structure
can have an initializer with a syntax similar to that of an array. C++ allows such
initializers for structures and classes all of whose data members are public:
class Employee {
public:
char *name;
int age;
double salary;
};
Employee emp = {"Jack", 24, 38952.25};
The initializer consists of values which are assigned to the data members of the
structure (or class) in the order they appear. This style of initialization is largely
superseded by constructors. Furthermore, it cannot be used with a class that has
a constructor.
A union is a class all of whose data members are mapped to the same address
within its object (rather than sequentially as is the case in a class). The size of an
object of a union is, therefore, the size of its largest data member.
The main use of unions is for situations where an object may assume values of
different types, but only one at a time. For example, consider an interpreter for a
simple programming language, called P, which supports a number of data types
such as: integers, reals, strings, and lists. A value in this language may be
defined to be of the type:
union Value {
long integer; double real; char *string; Pair list; //...
};
where Pair is itself a userdefined type for creating lists:
class Pair {
Value *head; Value *tail; //...
};
class Object {
private:
enum ObjType {intObj, realObj, strObj, listObj};
ObjType type; // object type
Value val; // object value
//...
};
where type provides a way of recording what type of value the object currently
has. For example, when type is set to strObj, val.string is used for referring to its
value.
Because of the unique way in which its data members are mapped to memory, a
union may not have a static data member or a data member which requires a
constructor.
Like a structure, all of the members of a union are by default public. The
keywords private, public, and protected may be used inside a struct or a union in
exactly the same way they are used inside a class for defining private, public,
and protected members.
Bit Fields
class Packet {
Bit type : 2; Bit acknowledge : 1; Bit channel : 4; Bit sequenceNo : 4; Bit
moreData : 1; //...
};
};
// 2 bits wide // 1 bit wide // 4 bits wide // 4 bite wide // 1 bit wide
A bit field is referred to in exactly the same way as any other data member.
Because a bit field does not necessarily start on a byte boundary, it is illegal to
take its address. For the same reason, a bit field cannot be defined as static.
Use of enumerations can make working with bit fields easier. For example,
given the enumerations
enum PacketType {dataPack, controlPack, supervisoryPack}; enum Bool {false,
true};
we can write:
Packet p;
p.type = controlPack;
p.acknowledge = true; ¨
Exercises
6.31 Explain why the Set parameters of the Set member functions are declared as
references.
6.33 Define a class named Menu which uses a linked-list of strings to represent a
menu of options. Use a nested class, Option, to represent the set elements.
Define a constructor, a destructor, and the following member functions for
Menu:
6.35 Define a class named Sequence for storing sorted strings. Define a
constructor, a destructor, and the following member functions for Sequence:
• Find which searches the sequence for a given string and returns true if it finds
it, and false otherwise.
• Print which prints the sequence strings.
6.36 Define class named BinTree for storing sorted strings as a binary tree.
Define the same set of member functions as for Sequence from the previous
exercise.
6.37 Define a member function for BinTree which converts a sequence to a
binary tree, as a friend of Sequence. Use this function to define a constructor for
BinTree which takes a sequence as argument.
6.38 Add an integer ID data member to the Menu class (Exercise 6.33) so that all
menu objects are sequentially numbered, starting from 0. Define an inline
member function which returns the ID. How will you keep track of the last
allocated ID?
6.39 Modify the Menu class so that an option can itself be a menu, thereby
allowing nested menus.
¨
7. Overloading
This chapter discusses the overloading of functions and operators in C++. The
term overloading means ‘providing multiple definitions of’. Overloading of
functions involves defining distinct functions which share the same name, each
of which has a unique signature. Function overloading is appropriate for:
Operators are similar to functions in that they take operands (arguments) and
return a value. Most of the built-in C++ operators are already overloaded. For
example, the + operator can be used to add two integers, two reals, or two
addresses. Therefore, it has multiple definitions. The built-in definitions of the
operators are restricted to built-in types. Additional definitions can be provided
by the programmer, so that they can also operate on userdefined types. Each
additional definition is implemented by a function.
Unlike functions and operators, classes cannot be overloaded; each class must
have a unique name. However, as we will see in Chapter 8, classes can be altered
and extended through a facility called inheritance. Also functions and classes
can be written as templates, so that they become independent of the data types
they employ. We will discuss templates in Chapter 9.
Function Overloading
Consider a function, GetTime, which returns in its parameter(s) the current time
of the day, and suppose that we require two variants of this function: one which
returns the time as seconds from midnight, and one which returns the time as
hours, minutes, and seconds. Given that these two functions serve the same
purpose, there is no reason for them to have different names.
C++ allows functions to be overloaded, that is, the same function to have more
than one definition:
long GetTime (void); // seconds from midnight void GetTime (int &hours, int
&minutes, int &seconds);
When GetTime is called, the compiler compares the number and type of
arguments in the call against the definitions of GetTime and chooses the one that
matches the call. For example:
int h, m, s;
long t = GetTime(); // matches GetTime(void)
GetTime(h, m, s); // matches GetTime(int&, int&, int&);
class Time {
//...
long GetTime (void); // seconds from midnight void GetTime (int &hours, int
&minutes, int &seconds);
};
Function overloading is useful for obtaining flavors that are not possible using
default arguments alone. Overloaded functions may also have default arguments:
void Error (int errCode, char *errMsg = "");
void Error (char *errMsg);
¨
Operator Overloading
C++ allows the programmer to define additional meanings for its predefined
operators by overloading them. For example, we can overload the + and
operators for adding and subtracting Point objects:
class Point {
public:
Point (int x, int y) {Point::x = x; Point::y = y;}
Point operator + (Point &p) {return Point(x + p.x,y + p.y);}
Point operator - (Point &p) {return Point(x - p.x,y - p.y);} private:
int x, y;
};
After this definition, + and - can be used for adding and subtracting points, much
in the same way as they are used for adding and subtracting numbers:
Point p1(10,20), p2(10,20); Point p3 = p1 + p2;
Point p4 = p1 - p2;
class Point {
public:
Point (int x, int y) {Point::x = x; Point::y = y;}
friend Point operator + (Point &p, Point &q)
{return Point(p.x + q.x,p.y + q.y);}
friend Point operator - (Point &p, Point &q)
{return Point(p.x - q.x,p.y - q.y);} private:
int x, y;
};
Binary: + - * % & | ^ << >> = += -= = %= &= |= ^= << >> = = == != < > <= >=
&& || [] () ,
The Set class was introduced in Chapter 6. Most of the Set member functions are
better defined as overloaded operators. Listing 7.25 illustrates.
Listing 7.25
1 #include <iostream.h>
2 const maxCard = 100; 3 enum Bool {false, true};
4 class Set {
5 public:
6 Set (void) { card = 0; }
7 friend Bool operator & (const int, Set&); // membership
8 friend Bool operator == (Set&, Set&); // equality
9 friend Bool operator != (Set&, Set&); // inequality 10 friend Set operator *
(Set&, Set&); // intersection 11 friend Set operator + (Set&, Set&); // union 12
//...
13 void AddElem (const int elem);
14 void Copy (Set &set);
15 void Print (void);
16 private:
17 int elems[maxCard]; // set elements 18 int card; // set cardinality 19 };
Here, we have decided to define the operator functions as global friends. They
could have just as easily been defined as member functions. The implementation
of these functions is as follow.
{
return !(set1 == set2);
}
// use overloaded &
Set res;
for (register i = 0; i < set1.card; ++i)
if (set1.elems[i] & set2) // use overloaded & res.elems[res.card++] =
set1.elems[i];
return res;
}
Set operator + (Set &set1, Set &set2) {
Set res;
set1.Copy(res);
for (register i = 0; i < set2.card; ++i) res.AddElem(set2.elems[i]);
return res;
}
The syntax for using these operators is arguably neater than those of the
functions they replace, as illustrated by the following main function: int main
(void) {
Set s1, s2, s3;
s1.AddElem(10); s1.AddElem(20); s1.AddElem(30); s1.AddElem(40);
s2.AddElem(30); s2.AddElem(50); s2.AddElem(10); s2.AddElem(60);
cout << "s1 = "; s1.Print(); cout << "s2 = "; s2.Print();
if (20 & s1) cout << "20 is in s1\n";
cout << "s1 intsec s2 = "; (s1 * s2).Print(); cout << "s1 union s2 = "; (s1 +
s2).Print();
if (s1 != s2) cout << "s1 /= s2\n"; return 0;
}
When run, the program will produce the following output:
s1 = {10,20,30,40}
s2 = {30,50,10,60}
20 is in s1
s1 intsec s2 = {10,30}
s1 union s2 = {10,20,30,40,50,60} s1 /= s2
Type Conversion
The normal built-in type conversion rules of the language also apply to
overloaded functions and operators. For example, in
if ('a' & set)
//...
the first operand of & (i.e., 'a') is implicitly converted from char to int, because
overloaded & expects its first operand to be of type int.
class Point {
//...
friend Point operator + (Point, Point);
friend Point operator + (int, Point);
friend Point operator + (Point, int);
};
A better approach is to use a constructor to convert the object to the same type as
the class itself so that one overloaded operator can handle the job. In this case,
we need a constructor which takes an int, specifying both coordinates of a point:
class Point {
//...
Point (int x) { Point::x = Point::y = x; } friend Point operator + (Point, Point);
};
For constructors of one argument, one need not explicitly call the constructor:
Point p = 10; // equivalent to: Point p(10);
Here, 5 is first converted to a temporary Point object and then added to p. The
temporary object is then destroyed. The overall effect is an implicit type
conversion from int to Point. The final value of q is therefore (15,25).
What if we want to do the opposite conversion, from the class type to another
type? In this case, constructors cannot be used because they always return an
object of the class to which they belong. Instead, one can define a member
function which explicitly converts the object to the desired type.
For example, given a Rectangle class, we can define a type conversion function
which converts a rectangle to a point, by overloading the type operator Pointin
Rectangle:
class Rectangle {
public:
Rectangle (int left, int top, int right, int bottom);
Rectangle (Point &p, Point &q);
//...
operator Point () {return botRight - topLeft;}
private:
private:
Point topLeft; Point botRight;
};
Point p(5,5);
Rectangle r(10,10,20,30);
r + p;
class X {
//...
X (Y&); // convert Y to X operator Y (); // convert X to Y
};
To illustrate possible ambiguities that can occur, suppose that we also define a
type conversion constructor for Rectangle (which takes a Point argument) as
well as overloading the + and - operators:
class Rectangle {
public:
Rectangle (int left, int top, int right, int bottom);
Rectangle (Point &p, Point &q);
Rectangle (Point &p);
private:
Point topLeft;
Point botRight;
};
Now, in
Point p(5,5);
Rectangle r(10,10,20,30); r + p;
r + p
can be interpreted in two ways. Either as r + Rectangle(p) // yields a Rectangle
or as: Point(r) + p // yields a Point Unless the programmer resolves the
ambiguity by explicit type conversion, this will be rejected by the compiler.
¨
Listing 7.26 defines a class for representing 16-bit binary numbers as sequences
of 0 and 1 characters.
Listing 7.26
1 #include <iostream.h>
2 #include <string.h>
4 class Binary {
4 class Binary {
5 public:
6 Binary (const char*);
7 Binary (unsigned int);
8 friend Binary operator + (const Binary, const Binary);
9 operator int (); 10 void Print (void); 11 private:
12 char bits[binSize]; 13 }; // type conversion
// binary quantity
Annotation 6 This constructor produces a binary number from its bit pattern. 7
This constructor converts a positive integer to its equivalent binary
representation.
8 The + operator is overloaded for adding two binary numbers. Addition is done
bit by bit. For simplicity, no attempt is made to detect overflows. 9 This type
conversion operator is used to convert a Binary object to an int object.
while (iSrc >= 0 && iDest >= 0) // copy bits bits[iDest--] = (num[iSrc--] == '0' ?
'0' : '1');
while (iDest >= 0) // pad left with zeros bits[iDest--] = '0';
}
{
for (register i = binSize - 1; i >= 0; --i) { bits[i] = (num % 2 == 0 ? '0' : '1');
num >>= 1;
}
}
}
return res;
}
Binary::operator int () {
unsigned value = 0;
The following main function creates two objects of type Binary and tests the +
operator.
main () {
Binary n1 = "01011";
Binary n2 = "11010";
n1.Print();
n2.Print();
(n1 + n2).Print();
cout << n1 + Binary(5) << '\n'; // add and then convert to
int
cout << n1 - 5 << '\n'; // convert n2 to int and then
subtract
}
The last two lines of main behave completely differently. The first of these
converts 5 to Binary, does the addition, and then converts the Binary result to
int, before sending it to cout. This is equivalent to:
In either case, the userdefined type conversion operator is applied implicitly. The
output produced by the program is evidence that the conversions are performed
correctly:
0000000000001011
0000000000011010
0000000000100101
16
6
¨
The simple and uniform treatment of output for built-in types is easily extended
to userdefined types by further overloading the << operator. For any given
userdefined type T, we can define an operator<< function which outputs objects
of type T:
The first parameter must be a reference to ostream so that multiple uses of <<
can be concatenated. The second parameter need not be a reference, but this is
more efficient for large objects.
For example, instead of the Binary class’s Print member function, we can
overload the << operator for the class. Because the first operand of << must be
an ostream object, it cannot be overloaded as a member function. It should
therefore be defined as a global function:
class Binary {
//...
friend ostream& operator << (ostream&, Binary&);
};
};
Given this definition, << can be used for the output of binary numbers in a
manner identical to its use for the built-in types. For example,
Binary n1 = "01011", n2 = "11010";
cout << n1 << " + " << n1 << " = " << n1 + n2 << '\n'; will produce the
following output:
0000000000001011 + 0000000000011010 = 0000000000100101
In addition to its simplicity and elegance, this style of output eliminates the
burden of remembering the name of the output function for each userdefined
type. Without the use of overloaded <<, the last example would have to be
written as (assuming that \nhas been removed from Print):
The first parameter must be a reference to istream so that multiple uses of >> can
be concatenated. The second parameter must be a reference, since it will be
modified by the function.
Continuing with the Binary class example, we overload the >> operator for the
input of bit streams. Again, because the first operand of >> must be an istream
object, it cannot be overloaded as a member function:
class Binary {
//...
friend istream& operator >> (istream&, Binary&);
};
Given this definition, >> can be used for the input of binary numbers in a
manner identical to its use for the built-in types. For example,
Binary n; cin >> n; will read a binary number from the keyboard into to n. ¨
Overloading []
Listing 7.27
1 #include <iostream.h>
2 #include <string.h>
3 class AssocVec {
4 public:
5 AssocVec (const int dim);
6 ~AssocVec (void);
7 int& operator [] (const char *idx);
8 private:
9 struct VecElem {
10 char *index;
11 int value;
12 } *elems; // vector elements 13 int dim; // vector dimension 14 int used; //
elements used so far 15 };
7 The overloaded [] operator is used for accessing vector elements. The function
which overloads [] must have exactly one parameter. Given a string index, it
searches the vector for a match. If a matching index is found then a reference to
its associated value is returned. Otherwise, a new element is created and a
reference to this value is returned.
AssocVec::~AssocVec (void) {
for (register i = 0; i < used; ++i) delete elems[i].index;
delete [] elems;
}
int& AssocVec::operator [] (const char *idx)
{
for (register i = 0; i < used; ++i) // search existing
elements
if (strcmp(idx,elems[i].index) == 0)
return elems[i].value;
}
}
static int dummy = 0;
return dummy;
Using AssocVec we can now create associative vectors that behave very much
like normal vectors:
AssocVec count(5);
count["apple"] = 5;
count["orange"] = 10;
count["fruit"] = count["apple"] + count["orange"];
Overloading ()
Listing 7.28 defines a matrix class. A matrix is a table of values (very similar to
a two-dimensional array) whose size is denoted by the number of rows and
columns in the table. An example of a simple 2 x 3 matrix would be:
M =
10 20 30 21 52 19
Listing 7.28
1 #include <iostream.h>
1 #include <iostream.h>
2 class Matrix {
3 public:
4 Matrix
5 ~Matrix
6 double& operator () (const short row, const short col); (const short rows, const
short cols); (void) {delete elems;}
7 friend ostream&
8 friend Matrix
9 friend Matrix 10 friend Matrix operator << (ostream&, Matrix&); operator +
(Matrix&, Matrix&); operator - (Matrix&, Matrix&); operator * (Matrix&,
Matrix&);
11 private:
12 const short rows; // matrix rows
13 const short cols; // matrix columns
14 double *elems; // matrix elements
15 };
6 The overloaded () operator is used for accessing matrix elements. The function
which overloads () may have zero or more parameters. It returns a reference to
the specified element’s value.
{
elems = new double[rows * cols];
}
Matrix m(2,3);
m(1,1) = 10; m(1,2) = 20; m(1,3) = 30; m(2,1) = 15; m(2,2) = 25; m(2,3) = 35;
cout << m << '\n';
10 20 30
15 25 35
¨
Memberwise Initialization
{
elems = m.elems;
}
MemberwiseDynamicCopy of m
rowsBlock cols
elems
MemberwiseInvalidCopy of m
rowsBlock cols
elems
// memberwise copy m to n
// memberwise copy n and return copy
class Matrix {
Matrix (const Matrix&);
//...
};
Memberwise Assignment
Objects of the same class are assigned to one another by an internal overloading
of the = operator which is automatically generated by the compiler. For example,
to handle the assignment in
{
if (rows == m.rows && cols == m.cols) { // must match int n = rows * cols;
for (register i = 0; i < n; ++i) // copy elements
elems[i] = m.elems[i];
}
return *this;
In general, for any given class X, the = operator is overloaded by the following
member of X:
X& X::operator = (X&)
Operator = can only be overloaded as a member, and not globally. ¨
Objects of different classes usually have different sizes and frequency of usage.
As a result, they impose different memory requirements. Small objects, in
particular, are not efficiently handled by the default versions of new and delete.
Every block allocated by new carries some overhead used for housekeeping
purposes. For large objects this is not significant, but for small objects the
overhead may be even bigger than the block itself. In addition, having too many
small blocks can severely slow down subsequent allocation and deallocation.
The performance of a program that dynamically creates many small objects can
be significantly improved by using a simpler memory management strategy for
those objects.
The dynamic storage management operators new and delete can be overloaded
for a class, in which case they override the global definition of these operators
when used for objects of that class.
As an example, suppose we wish to overload new and delete for the Point class,
so that Point objects are allocated from an array:
#include <stddef.h> #include <iostream.h>
const int maxPoints = 512;
class Point {
public:
//...
void* operator new
void operator delete private:
int xVal, yVal;
};
(size_t bytes);
(void *ptr, size_t bytes);
The type name size_t is defined in stddef.h. New should always return a void*.
The parameter of new denotes the size of the block to be allocated (in bytes).
The corresponding argument is always automatically passed by the compiler.
The first parameter of delete denotes the block to be deleted. The second
parameter is optional and denotes the size of the allocated block. The
corresponding arguments are automatically passed by the compiler.
Since blocks, freeList and used are static they do not affect the size of a Point
object (it is still two integers). These are initialized as follows:
New takes the next available block from blocks and returns its address. Delete
frees a block by inserting it in front of the linked-list denoted by freeList. When
used reaches maxPoints, new removes and returns the first block in the linked-
list, but fails (returns 0) when the linked-list is empty:
Point::operator new and Point::operator delete are invoked only for Point
objects. Calling new with any other type as argument will invoke the global
definition of new, even if the call occurs inside a member function of Point. For
example:
Even when new and delete are overloaded for a class, global new and delete are
used when creating and destroying object arrays:
For classes that do not overload ->, this operator is always binary: the left
operand is a pointer to a class object and the right operand is a class member
name. When the left operand of -> is an object or reference of type X (but not
pointer), X is expected to have overloaded -> as unary. In this case, -> is first
applied to the left operand to produce a result p. If p is a pointer to a class Ythen
p is used as the left operand of binary -> and the right operand is expected to be
a member of Y. Otherwise, p is used as the left operand of unary -> and the
whole procedure is repeated for class Y. Consider the following classes that
overload ->:
class A {
//...
B& operator -> (void);
};
class B {
//...
Point* operator -> (void);
};
The effect of applying -> to an object of type A
A obj;
int i = obj->xVal;
Unary operators * and & can also be overloaded so that the semantic
correspondence between ->, *, and & is preserved.
As an example, consider a library system which represents a book record as a
raw string of the following format:
"%Aauthor\0%Ttitle\0%Ppublisher\0%Ccity\0%Vvolume\0%Yyear\0\n"
Each field starts with a field specifier (e.g., %A specifies an author) and ends
with a null character (i.e., \0). The fields can appear in any order. Also, some
fields may be missing from a record, in which case a default value must be used.
For efficiency reasons we may want to keep the data in this format but use the
following structure whenever we need to access the fields of a record:
struct Book {
char raw; // raw format (kept for reference) char author;
char *title;
char *publisher;
char *city;
short vol;
short year;
};
The default field values are denoted by a global Book variable:
Book defBook = {
"raw", "Author?", "Title?", "Publisher?", "City?", 0, 0
};
We now define a class for representing raw records, and overload the unary
pointer operators to map a raw record to a Book structure whenever necessary.
#include <iostream.h>
#include <iostream.h>
#include <stdlib.h> // needed for atoi() below
int const cacheSize = 10;
class RawBook { public:
private:
Book* RawToBook (void);
char *data;
static Book *cache; // cache memory
static short curr; // current record in cache static short used; // number of used
cache records
};
The private member function RawToBook searches the cache for a RawBook
and returns a pointer to its corresponding Book structure. If the book is not in the
cache, RawToBook loads the book at the current position in the cache:
for (;;) {
while (*str++ != '%') ;
switch (*str++) { // skip to next specifier
// get a field case 'A': bk->author = str; break; case 'T': bk->title = str; break; case
'P': bk->publisher = str; break; case 'C': bk->city = str; break; case 'V': bk->vol =
atoi(str); break; case 'Y': bk->year = atoi(str); break;
}
while (*str++ != '\0')
;
if (*str == '\n') break; // end of record }
return bk;
}
// skip till end of field
The overloaded operators ->, *, and & are easily defined in terms of
RawToBook:
The identical definitions for -> and & should not be surprising since -> is unary
in this context and semantically equivalent to &.
The following test case demonstrates that the operators behave as expected. It
sets up two book records and prints each using different operators.
main ()
{
It will produce the following output: A. Peters, Blue Earth, Phedra, Sydney, 0,
1981 F. Jackson, Pregnancy, Miles, City?, 0, 1987 ¨
Overloading ++ and -
The auto increment and auto decrement operators can be overloaded in both
prefix and postfix form. To distinguish between the two, the postfix version is
specified to take an extra integer argument. For example, the prefix and postfix
versions of ++ may be overloaded for the Binary class as follows:
class Binary {
//...
friend Binary operator ++ (Binary&); // prefix friend Binary operator ++
(Binary&, int); // postfix
};
Although we have chosen to define these as global friend functions, they can
also be defined as member functions. Both are easily defined in terms of the +
operator defined earlier:
{
return n = n + Binary(1);
}
Note that we have simply ignored the extra parameter of the postfix version.
When this operator is used, the compiler automatically supplies a default
argument for it. The following code fragment exercises both versions of the
argument for it. The following code fragment exercises both versions of the
operator:
Binary n1 = "01011";
Binary n2 = "11010";
cout << ++n1 << '\n';
cout << n2++ << '\n';
cout << n2 << '\n';
0000000000001100
0000000000011010
0000000000011011
The prefix and postfix versions of - may be overloaded in exactly the same way.
¨
Exercises
7.40 Write overloaded versions of a Max function which compares two integers,
two reals, or two strings, and returns the ‘larger’ one.
7.41 Overload the following two operators for the Set class:
• Operator - which gives the difference of two sets (e.g. s - t gives a set
consisting of those elements of s which are not in t).
• Operator <= which checks if a set is contained by another (e.g., s<= t is true if
all the elements of s are also in t).
7.42 Overload the following two operators for the Binary class:
• Operator - which gives the difference of two binary values. For simplicity,
assume that the first operand is always greater than the second operand.
• Operator [] which indexes a bit by its position and returns its value as a 0 or 1
integer.
7.43 Sparse matrices are used in a number of numerical methods (e.g., finite
element analysis). A sparse matrix is one which has the great majority of its
elements set to zero. In practice, sparse matrices of sizes up to 500 × 500 are not
uncommon. On a machine which uses a 64-bit representation for reals, storing
such a matrix as an array would require 2 megabytes of storage. A more
economic representation would record only nonzero elements together with their
positions in the matrix. Define a SparseMatrix class which uses a linked-list to
record only nonzero elements, and overload the +, -, and * operators for it. Also
define an appropriate memberwise initialization constructor and memberwise
assignment operator for the class.
7.44 Complete the implementation of the following String class. Note that two
versions of the constructor and = are required, one for initializing/assigning to a
String using a char*, and one for memberwise initialization/assignment.
Operator [] should index a string character using its position. Operator + should
concatenate two strings.
private:
char *chars; // string characters short len; // length of string
};
7.45 A bit vector is a vector with binary elements, that is, each element is either
a 0 or a 1. Small bit vectors are conveniently represented by unsigned integers.
For example, an unsigned char can represent a bit vector of 8 elements. Larger
bit vectors can be defined as arrays of such smaller bit vectors. Complete the
implementation of the Bitvec class, as defined below. It should allow bit vectors
of any size to be created and manipulated using the associated operators.
BitVec operator ~ BitVec operator & BitVec operator | BitVec operator ^ BitVec
operator << BitVec operator >> Bool operator == Bool operator != (void);
(const BitVec&); (const BitVec&); (const BitVec&); (const short n); (const short
n); (const BitVec&);
private:
uchar vec; // vector of 8bytes bits short bytes; // bytes in the vector
};
¨
8. Derived Classes
In practice, most classes are not entirely unique, but rather variations of existing
ones. Consider, for example, a class named RecFile which represents a file of
records, and another class named SortedRecFile which represents a sorted file of
records. These two classes would have much in common. For example, they
would have similar member functions such as Insert, Delete, and Find, as well as
similar data members. In fact, SortedRecFile would be a specialized version of
RecFile with the added property that its records are organized in sorted order.
Most of the member functions in both classes would therefore be identical, while
a few which depend on the fact that file is sorted would be different. For
example, Find would be different in SortedRecFile because it can take advantage
of the fact that the file is sorted to perform a binary search instead of the linear
search performed by the Findmember of RecFile.
Given the shared properties of these two classes, it would be tedious to have to
define them independently. Clearly this would lead to considerable duplication
of code. The code would not only take longer to write it would also be harder to
maintain: a change to any of the shared properties would have to be consistently
applied to both classes.
An illustrative Class
3 class Contact {
4 public:
5 Contact
6
7 ~Contact
8 const char* Name
9 const char* Address 10 const char* Tel 11 friend ostream& operator <<
(ostream&, Contact&); (const char *name,
const char *address, const char *tel); (void);
(void) const (void) const (void) const {return name;} {return address;} {return
tel;}
12 private:
13 char *name; // contact name
14 char *address; // contact address
15 char *tel; // contact telephone number
16 };
17 //------------------------------------------------------------------
18 class ContactDir {
19 public:
20 ContactDir (const int maxSize);
21 ~ContactDir(void);
22 void Insert (const Contact&);
23 void Delete (const char *name);
24 Contact* Find (const char *name);
25 friend ostream& operator <<(ostream&, ContactDir&);
26 private:
27 int Lookup (const char *name);
18 ContactDir allows us to insert into, delete from, and search a list of personal
contacts.
22 Insert inserts a new contact into the directory. This will overwrite an existing
contact (if any) with identical name.
23 Delete deletes a contact (if any) whose name matches a given name. 24 Find
returns a pointer to a contact (if any) whose name matches a given name.
27 Lookup returns the slot index of a contact whose name matches a given
name. If none exists then Lookup returns the index of the slot where such an
entry should be inserted. Lookup is defined as private because it is an auxiliary
function used only by Insert, Delete, and Find.
Contact::~Contact (void) {
delete name; delete address; delete tel;
}
ContactDir::~ContactDir (void) {
for (register i = 0; i < dirSize; ++i) delete contacts[i];
delete [] contacts;
}
void ContactDir::Insert (const Contact& c) {
++dirSize;
}
contacts[idx] = new Contact(c.Name(), c.Address(), c.Tel());
}
}
The following main function exercises the ContactDir class by creating a small
directory and calling the member functions:
};
We would like to define a class called SmartDir which behaves the same as
ContactDir, but also keeps track of the most recently looked-up entry. SmartDir
is best defined as a derivation of ContactDir, as illustrated by Listing 8.30.
Listing 8.30
1 class SmartDir : public ContactDir {
2 public:
3 SmartDir(const int max) : ContactDir(max) {recent = 0;}
4 Contact* Recent (void);
5 Contact* Find (const char *name);
6 private:
7 char *recent; // the most recently looked-up name
8 };
Annotation 1 A derived class header includes the base classes from which it is
derived. A colon separates the two. Here, ContactDir is specified to be the base
class from which SmartDir is derived. The keyword public before ContactDir
specifies that ContactDir is used as a public base class.
3 SmartDir has its own constructor which in turn invokes the base class
constructor in its member initialization list.
4 Recent returns a pointer to the last looked-up contact (or 0 if there is none).
5 Find is redefined so that it can record the last looked-up entry.
7 This recent pointer is set to point to the name of the last looked-up entry. The
member functions are defined as follows:
{
return recent == 0 ? 0 : ContactDir::Find(recent);
}
Because ContactDir is a public base class of SmartDir, all the public members of
ContactDir become public members of SmartDir. This means that we can invoke
a member function such as Insert on a SmartDir object and this will simply be a
call to ContactDir::Insert. Similarly, all the private members of
ContactDirbecome private members of SmartDir.
SmartDir redefines the Find member function. This should not be confused with
overloading. There are two distinct definitions of this function: ContactDir::Find
and SmartDir::Find (both of which have the same signature, though they can
have different signatures if desired). Invoking Find on a SmartDir object causes
the latter to be invoked. As illustrated by the definition of Findin SmartDir, the
former can still be invoked using its full name.
The following code fragment illustrates that SmartDir behaves the same as
ContactDir, but also keeps track of the most recently looked-up entry:
SmartDir dir(10);
dir.Insert(Contact("Mary", "11 South Rd", "282 1324"));
dir.Insert(Contact("Peter", "9 Port Rd", "678 9862")); dir.Insert(Contact("Jane",
"321 Yara Ln", "982 6252")); dir.Insert(Contact("Fred", "2 High St", "458
2324")); dir.Find("Jane");
dir.Find("Peter");
cout << "Recent: " << *dir.Recent() << '\n';
This will produce the following output: Recent: (Peter , 9 Port Rd , 678 9862)
Figure 8.12 Base and derived class objects. ContactDir object contacts
dirSize
maxSize
SmartDir object
contacts
dirSize
maxSize
recent¨
A class hierarchy is usually illustrated using a simple graph notation. Figure 8.13
illustrates the UML notation that we will be using in this book. Each class is
represented by a box which is labeled with the class name. Inheritance between
two classes is illustrated by a directed line drawn from the derived class to the
base class. A line with a diamond shape at one end depicts composition (i.e., a
class object is composed of one or more objects of another class). The number of
objects contained by another object is depicted by a label (e.g., n).
Figure 8.13 is interpreted as follows. Contact, ContactDir, and SmartDir are all
classes. A ContactDir is composed of zero or more Contact objects. SmartDiris
derived from ContactDir.
A derived class may have constructors and a destructor. Since a derived class
may provide data members on top of those of its base class, the role of the
constructor and destructor is to, respectively, initialize and destroy these
additional members.
When an object of a derived class is created, the base class constructor is applied
to it first, followed by the derived class constructor. When the object is
destroyed, the destructor of the derived class is applied first, followed by the
base class destructor. In other words, constructors are applied in order of
derivation and destructors are applied in the reverse order. For example, consider
a class C derived from B which is in turn derived from A. Figure 8.14 illustrates
how an object c of type C is created and destroyed.
class A { /* ... */ }
class B : public A { /* ... */ }
class C : public B { /* ... */ }
Figure 8.14 Derived class object construction and destruction order. c being
constructed c being destroyed
A::A A::~A
B::B B::~B
C::C ......... C::~C
In general, all that a derived class constructor requires is an object from the base
class. In some situations, this may not even require referring to the base class
constructor:
Although the private members of a base class are inherited by a derived class,
they are not accessible to it. For example, SmartDir inherits all the private (and
public) members of ContactDir, but is not allowed to directly refer to the private
members of ContactDir. The idea is that private members should be completely
hidden so that they cannot be tampered with by the class clients.
This restriction may prove too prohibitive for classes from which other classes
are likely to be derived. Denying the derived class access to the base class
private members may convolute its implementation or even make it impractical
to define.
The restriction can be relaxed by defining the base class private members as
protected instead. As far as the clients of a class are concerned, a protected
member is the same as a private member: it cannot be accessed by the class
clients. However, a protected base class member can be accessed by any class
derived from it.
class ContactDir {
//...
protected:
int Lookup (const char *name);
Contact **contacts; // list of contacts
int dirSize; // current directory size int maxSize; // max directory size
};
As a result, Lookup and the data members of ContactDir are now accessible to
SmartDir.
The access keywords private, public, and protected can occur as many times as
desired in a class definition. Each access keyword specifies the access
characteristics of the members following it until the next access keyword:
class Foo {
public:
// public members...
private:
// private members...
protected:
// protected members...
public:
// more public members...
protected:
// more protected members...
}; ¨
class A {
private: int x; public: int y; protected: int z;
};
class B : A {};
class C : private A {}; class D : public A {}; class E : protected A {}; void Fx
(void); void Fy (void); void Fz (void);
• All the members of a private base class become private members of the derived
class. So x, Fx, y, Fy, z, and Fzall become private members of B and C.
The members of a public base class keep their access characteristics in the•
•
derived class. So, x and Fxbecomes private members of D, y and Fy become
public members of D, and z and Fzbecome protected members of D.
The private members of a protected base class become private members of the
derived class. Whereas, the public and protected members of a protected base
class become protected members of the derived class. So, x and Fx become
private members of E, and y, Fy, z, and Fz become protected members of E.
It is also possible to individually exempt a base class member from the access
changes specified by a derived class, so that it retains its original access
characteristics. To do this, the exempted member is fully named in the derived
class under its original access characteristic. For example:
class C : private A {
//...
public: A::Fy; // makes Fy a public member of C
protected: A::z; // makes z a protected member of C
}; ¨
Virtual Functions
This can be achieved through the dynamic binding of Lookup: the decision as
to which version of Lookup to call is made at runtime depending on the type of
the object.
In C++, dynamic binding is supported by virtual member functions. A member
function is declared as virtual by inserting the keyword virtual before its
prototype in the base class. Any member function, including constructors and
destructors, can be declared as virtual. Lookup should be declared as virtual in
ContactDir:
class ContactDir {
//...
protected:
virtual int Lookup (const char *name); //...
};
return mid;
else if (cmp < 0)
pos = top = mid - 1; else
pos = bot = mid + 1; }
return pos < 0 ? 0 : pos; }
// return item index
SortedDir dir(10);
dir.Insert(Contact("Mary", "11 South Rd", "282 1324"));
dir.Insert(Contact("Peter", "9 Port Rd", "678 9862")); dir.Insert(Contact("Jane",
"321 Yara Ln", "982 6252")); dir.Insert(Contact("Jack", "42 Wayne St", "663
2989")); dir.Insert(Contact("Fred", "2 High St", "458 2324")); cout << dir;
Multiple Inheritance
class OptionList {
public:
OptionList (int n);
~OptionList (void);
//...
};
class Window {
public:
Window (Rect &bounds);
~Window (void);
//...
};
A menu is a list of options displayed within its own window. It therefore makes
sense to define Menuby deriving it from OptionList and Window:
Under multiple inheritance, a derived class inherits all of the members of its base
classes. As before, each of the base classes may be private, public, or protected.
The same base member access principles apply. Figure 8.15 illustrates the class
hierarchy for Menu.
The order in which the base class constructors are invoked is the same as the
order in which they are specified in the derived class header (not the order in
which they appear in the derived class constructor’s member initialization list).
For Menu, for example, the constructor for OptionList is invoked before the
constructor for Window, even if we change their order in the constructor:
Figure 8.16 Base and derived class objects. OptionList object Window object Menu object
OptionList data members Window
data members OptionList data members
Window
data members
Menu data members
In general, a derived class may have any number of base classes, all of which
must be distinct:
Ambiguity
Multiple inheritance further complicates the rules for referring to the members of
a class. For example, suppose that both OptionList and Window have a member
function called Highlight for highlighting a specific part of either object type:
class OptionList {
public:
//...
void Highlight (int part);
};
class Window {
public:
public:
//...
void Highlight (int part);
};
The derived class Menu will inherit both these functions. As a result, the call
m.Highlight(0);
(where m is a Menu object) is ambiguous and will not compile, because it is not
clear whether it refers to OptionList::Highlight or Window::Highlight. The
ambiguity is resolved by making the call explicit:
m.Window::Highlight(0);
Alternatively, we can define a Highlight member for Menu which in turn calls
the Highlight members of the base classes:
Type Conversion
For any derived class there is an implicit type conversion from the derived class
to any of its public base classes. This can be used for converting a derived class
object to a base class object, be it a proper object, a reference, or a pointer:
Such conversions are safe because the derived class object always contains all of
its base class objects. The first assignment, for example, causes the Window
component of menu to be assigned to win.
A base class object cannot be assigned to a derived class object unless there is a
type conversion constructor in the derived class defined for this purpose. For
example, given
the following would be valid and would use the constructor to convert win to a
Menu object before assigning:
menu = win; // invokes Menu::Menu(Window&) ¨
Consider the problem of recording the average time required for a message to be
transmitted from one machine to another in a long-haul network. This can be
represented as a table, as illustrated by Table 8.14.
Table 8.14
Listing 8.32
1
2
3
4
5
6
7
8
9
10
11
12
13
14
double& Table1::operator () (const char src, const char dest) {
return this->Matrix::operator()(
this->AssocVec::operator[](src), this->AssocVec::operator[](dest) );
}
Table tab(3);
tab("Sydney","Perth") = 12.45;
cout << "Sydney -> Perth = " << tab("Sydney","Perth") << '\n';
which produces the following output: Sydney -> Perth = 12.45 Another way of
defining this class is to derive it from Matrix and include an AssocVec object as
a data member (see Listing 8.33).
Listing 8.33 Message transmission time (in seconds).
Sydney Melbourne Perth Sydney 0.00 3.55 12.45
Melbourne 2.34 0.00 10.31
Perth 15.36 9.32 0.00
The row and column indices for this table are strings rather than integers, so the
Matrix class (Chapter 7) will not be adequate for representing the table. We need
a way of mapping strings to indices. This is already supported by the AssocVec
class (Chapter 7). As shown in Listing 8.32, Table1 can be defined as a derived
class of Matrix and AssocVec.
The inevitable question is: which one is a better solution, Table1 or Table2? The
answer lies in the relationship of table to matrix and associative vector:
• A table is a form of matrix.
• A table is not an associative vector, but rather uses an associative vector to
manage the association of its row and column labels with positional indexes.
It is worth considering which of the two versions of table better lends itself to
generalization. One obvious generalization is to remove the restriction that the
table should be square, and to allow the rows and columns to have different
labels. To do this, we need to provide two sets of indexes: one for rows and one
for columns. Hence we need two associative vectors. It is arguably easier to
expand Table2 to do this rather than modify Table1 (see Listing 8.34).
Figure 8.17 shows the class hierarchies for the three variations of table.
Figure 8.17 Variations of table. Matrix AssocVec Matrix Matrix
Table1Table2 1 AssocVecTable3 2 AssocVec
Listing 8.34
8 private:
9 AssocVec rowIdx; // row index 10 AssocVec colIdx; // column index 11 };
For a derived class which also has class object data members, the order of object
construction is as follows. First the base class constructors are invoked in the
order in which they appear in the derived class header. Then the class object data
members are initialized by their constructors being invoked in the same order in
which they are declared in the class. Finally, the derived class constructor is
invoked. As before, the derived class object is destroyed in the reverse order of
construction.
Recall the Menu class and suppose that its two base classes are also multiply
derived:
class OptionList : public Widget, List { /*...*/ }; class Window : public Widget,
Port { /*...*/ }; class Menu : public OptionList, public Window { /*...*/ };
Since Widget is a base class for both OptionList and Window, each menu object
will have two widget objects (see Figure 8.19a). This is not desirable (because a
menu is considered a single widget) and may lead to ambiguity. For example,
when applying a widget member function to a menu object, it is not clear as to
which of the two widget objects it should be applied. The problem is overcome
by making Widget a virtual base class of OptionList and Window. A base class
is made virtual by placing the keyword virtual before its name in the derived
class header:
class OptionList : virtual public Widget, List { /*...*/ }; class Window : virtual
public Widget, Port { /*...*/ }; This ensures that a Menu object will contain
exactly one Widget object. In other words, OptionList and Window will share
the same Widget object.
An object of a class which is derived from a virtual base class does not directly
contain the latter’s object, but rather a pointer to it (see Figure 8.19b and 8.19c).
This enables multiple occurrences of a virtual class in a hierarchy to be collapsed
into one (see Figure 8.19d).
A virtual base class object is initialized, not necessarily by its immediate derived
class, but by the derived class farthest down the class hierarchy. This rule
ensures that the virtual base class object is initialized only once. For example, in
a menu object, the widget object is initialized by the Menu constructor (which
overrides the invocation of the Widgetconstructor by OptionList or Window):
{ //... }
Overloaded Operators
Except for the assignment operator, a derived class inherits all the overloaded
operators of its base classes. An operator overloaded by the derived class itself
hides the overloading of the same operator by the base classes (in exactly the
same way member functions of a derived class hide member functions of base
classes).
//...
private:
int depth;
};
Exercises
8.46 Consider a Year class which divides the days in a year into work days and
off days. Because each day has a binary value, Yearis easily derived from
BitVec:
enum Month {
Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec };
class Year : public BitVec { public:
Days are sequentially numbered from the beginning of the year, starting at 1 for
January 1st. Complete the Year class by implementing its member functions.
M k n+1 = ∑M k i × X i
i=1
Given that there may be at most n elements in a set (n being a small number) the
set can be efficiently represented as a bit vector of n elements. Derive a class
named EnumSet from BitVec to facilitate this. EnumSet should overload the
following operators:
• Operators >> and << for, respectively, adding an element to and removing an
element from a set.
8.49 An abstract class is a class which is never used directly but provides a
skeleton for other classes to be derived from it. Typically, all the member
functions of an abstract are virtual and have dummy implementations. The
following is a simple example of an abstract class:
9. Templates
This chapter describes the template facility of C++ for defining functions and
classes. Templates facilitate the generic definition of functions and classes so
that they are not tied to specific implementation types. They are invaluable in
that they dispense with the burden of redefining a function or class so that it will
work with yet another data type.
Templates provide direct support for writing reusable code. This in turn makes
them an ideal tool for defining generic libraries.
We will present a few simple examples to illustrate how templates are defined,
instantiated, and specialized. We will describe the use of nontype parameters in
class templates, and discuss the role of class members, friends, and derivations
in the context of class templates.
Listing 9.35
1 template <class T>
2 T Max (T val1, T val2)
3 {
4 return val1 > val2 ? val1 : val2;
5 }
In the first call to Max, both arguments are integers, hence T is bound to int. In
the second call, both arguments are reals, hence T is bound to double. In the final
call, both arguments are characters, hence T is bound to char. A total of three
functions are therefore generated by the compiler to handle these cases:
Max(10, 12.6);
unsigned nValues = 4;
double values[] = {10.3, 19.5, 20.6, 3.5};
Max(values, 4); // ok
Max(values, nValues); // illegal! nValues does not match int
Listing 9.36
Both definitions of Max assume that the > operator is defined for the type
substituted in an instantiation. When this is not the case, the compiler flags it as
an error:
For some other types, the operator may be defined but not produce the desired
effect. For example, using Max to compare two strings will result in their pointer
values being compared, not their character sequences:
#include <string.h>
char* Max (char str1, char str2) // specialization of Max {
Given this specialization, the above call now matches this function and will not
result in an instance of the function template to be instantiated for char*. ¨
Example: Binary Search
Listing 9.37
1 template <class Type>
2 int BinSearch (Type &item, Type *table, int n)
3 {
4 int bot = 0;
5 int top = n - 1;
6 int mid, cmp;
Now let us instantiate BinSearch for a userdefined type such as RawBook (see
Chapter 7). First, we need to ensure that the comparison operators are defined
for our userdefined type:
class RawBook {
public:
//...
int operator < (RawBook &b)
int operator > (RawBook &b)
int operator == (RawBook &b) private:
int Compare (RawBook&);
//...
};
All are defined in terms of the private member function Compare which
compares two books by giving priority to their titles, then authors, and finally
publishers. The code fragment
RawBook books[] = {
RawBook("%APeters\0%TBlue
Earth\0%PPhedra\0%CSydney\0%Y1981\0\n"),
RawBook("%TPregnancy\0%AJackson\0%Y1987\0%PMiles\0\n"),
RawBook("%TZoro\0%ASmiths\0%Y1988\0%PMiles\0\n")
};
cout << BinSearch(RawBook("%TPregnancy\0%AJackson\0%PMiles\0\n"),
books, 3) << '\n';
produces the output 1
The definition of a class template is very similar to a normal class, except that
the specified type parameters can be referred to within the definition. The
definition of Stack is shown in Listing 9.38.
Listing 9.38
1 template <class Type>
2 class Stack {
3 public:
4 Stack (int max) : stack(new Type[max]),
5 top(-1), maxSize(max) {}
6 ~Stack (void) {delete [] stack;}
7 void Push (Type &val);
8 void Pop (void) {if (top >= 0) --top;}
9 Type& Top (void) {return stack[top];}
10 friend ostream& operator << (ostream&, Stack&);
11 private:
12 Type *stack; 13 int top; 14 const int maxSize; 15 };
// stack array
// index of top stack entry // max size of stack
The member functions of Stack are defined inline except for Push. The <<
operator is also overloaded to display the stack contents for testing purposes.
These two are defined as follows:
for (register i = 0; i <= s.top; ++i) os << s.stack[i] << " ";
return os;
}
Except for within the class definition itself, a reference to a class template must
include its template parameter list. This is why the definition of Push and << use
the name Stack<Type>instead of Stack. ¨
s1.Push(10);
s1.Push(20);
s1.Push(30);
cout << s1 << '\n';
class Sample {
Stack<int> intStack; // ok
Stack<Type> typeStack; // illegal! Type is undefined //...
};
The combination of a class template and arguments for all of its type parameters
(e.g., Stack<int>) represents a valid type specifier. It may appear wherever a
C++ type may appear.
If a class template is used as a part of the definition of another class template (or
function template), then the former’s type parameters can be bound to the latter’s
template parameters. For example:
Nontype Parameters
Unlike a function template, not all parameters of a class template are required to
represents types. Value parameters (of defined types) may also be used. Listing
9.39 shows a variation of the Stack class, where the maximum size of the stack
is denoted by a template parameter (rather than a data member).
Listing 9.39
1 template <class Type, int maxSize>
2 class Stack {
3 public:
4 Stack (void) :
5 ~Stack (void) stack(new Type[maxSize]), top(-1) {} {delete [] stack;}
6 void Push (Type &val);
7 void Pop (void) {if (top >= 0) --top;}
8 Type &Top (void) {return stack[top];}
9 private:
10 Type *stack; // stack array
11 int top; // index of top stack entry 12 };
Both parameters are now required for referring to Stack outside the class. For
example, Push is now defined as follows:
Unfortunately, the operator << cannot be defined as before, since value template
parameters are not allowed for nonmember functions:
template <class Type, int maxSize> // illegal! ostream &operator << (ostream&,
Stack<Type, maxSize>&);
{
{
if (top+1 < maxSize) {
stack[++top] = new char[strlen(val) + 1]; strcpy(stack[top], val);
}
}
specializes the Push member for the char* type. To free the allocated storage,
Pop needs to be specialized as well:
void Stack<char*>::Pop (void) {
if (top >= 0)
delete stack[top--]; }
It is also possible to specialize a class template as a whole, in which case all the
class members must be specialized as a part of the process:
Stack<Str>::Stack
~Stack (void) void Push (Str val); void Pop (void); Str Top (void) friend
ostream& operator << (ostream&, Stack<Str>&); (int max) : stack(new
Str[max]), top(-1), maxSize(max) {}
{delete [] stack;}
{return stack[top];}
private:
Str *stack; int top; const int maxSize;
};
// stack array
// index of top stack entry // max size of stack
A class template may have constant, reference, and static members just like an
ordinary class. The use of constant and reference members is exactly as before.
Static data members are shared by the objects of an instantiation. There will
Static data members are shared by the objects of an instantiation. There will
therefore be an instance of each static data member per instantiation of the class
template.
As an example, consider adding a static data member to the Stack class to enable
Top to return a value when the stack is empty:
//...
int Stack<int>::dummy = 0;
¨
We wish to define a class named Sample and declare Foo and Stack as its
friends. The following makes a specific instance of Foo and Stack friends of all
instances of Sample:
Alternatively, we can make each instance of Foo and Stack a friend of its
corresponding instance of Sample:
This means that, for example, Foo<int> and Stack<int> are friends of
Sample<int>, but not Sample<double>.
The extreme case of making all instances of Foo and Stack friends of all
instances of Sample is expressed as:
The choice as to which form of friendship to use depends on the intentions of the
programmer.
¨
Example: Doubly-linked Lists
Because a container class can conceivably contain objects of any type, it is best
defined as a class template. Listing 9.40 show the definition of doubly-linked
lists as two class templates.
Listing 9.40
1 #include <iostream.h>
2 enum Bool {false, true};
3 template <class Type> class List; // forward declaration
29-30 First and last, respectively, point to the first and last element in the list.
Note that these two are declared of type ListElem<Type>* and not ListElem*,
because the declaration is outside the ListElem class.
Insert, Remove, and Element are all defined as virtual to allow a class derived
from List to override them.
All of the member functions of ListElem are defined inline. The definition of
List member functions is as follows:
if (last == 0)
last = handy;
first = handy;
}
//-----------------------------------------------------------------
-
template <class Type>
void List<Type>::Remove (const Type &val)
{
ListElem<Type> *handy;
handy->next->prev = handy->prev;
else
last = handy->prev;
if (handy->prev != 0)
handy->prev->next = handy->next;
else
first = handy->next;
delete handy;
}
}
}
//-----------------------------------------------------------------
-
-
template <class Type>
Bool List<Type>::Member (const Type &val)
{
ListElem<Type> *handy;
The << is overloaded for both classes. The overloading of << for ListElem does
not require it to be declared a friend of the class because it is defined in terms of
public members only:
os << elem.Value();
return os;
}
//-----------------------------------------------------------------
-
template <class Type>
ostream& operator << (ostream &os, List<Type> &list)
{
ListElem<Type> *handy = list.first;
Here is a simple test of the class which creates the list shown in Figure 9.20:
int main (void) {
List<int> list;
list.Insert(30);
list.Insert(20);
list.Insert(10);
list.Insert(10);
cout << "list = " << list << '\n';
if (list.Member(20)) cout << "20 is in list\n"; cout << "Removed 20\n";
list.Remove(20);
cout << "list = " << list << '\n';
return 0;
A class template (or its instantiation) can serve as the base of a derived class:
template <class Type>
class SmartList : public List<Type>; // template base
class Primes : protected List<int>; // instantiated base A template base class,
such as List, should always be accompanied with its parameter list (or arguments
if instantiated). The following is therefore invalid: template <class Type>
class SmartList : public List; // illegal! <Type> missing It would be equally
incorrect to attempt to derive a nontemplate class from a (non-instantiated)
template class:
class SmartList : public List<Type>; // illegal! template expected It is, however,
perfectly acceptable for a normal class to serve as the base of a derived template
class:
class X;
template <class Type> class Y : X; // ok
Listing 9.41
1 template <class Type>
2 class Set : public List<Type> {
3 public:
4 virtual void Insert (const Type &val)
5 {if (!Member(val)) List<Type>::Insert(val);}
6 };
Exercises
9.50 Define a Swap function template for swapping two objects of the same
type.
9.51 Rewrite the BubbleSort function (Exercise 5.4) as a function template.
Provide a specialization of the function for strings.
9.52 Rewrite the BinaryTree class (Exercise 6.6) as a class template. Provide a
specialization of the class for strings.
9.53 Rewrite the Database, BTree, and BStar classes (Exercise 8.4) as class
templates.
¨
Exception handling consists of three things: (i) the detecting of a runtime error,
(ii) raising an exception in response to the error, and (ii) taking corrective action.
The latter is called recovery. Some exceptions can be fully recovered from so
that execution can proceed unaffected. For example, an invalid argument value
passed to a function may be handled by substituting a reasonable default value
for it. Other exceptions can only be partially handled. For example, exhaustion
of the heap memory can be handled by abandoning the current operation and
returning to a state where other operations (such as saving the currently open
files to avoid losing their contents) can be attempted.
C++ provides a language facility for the uniform handling of exceptions. Under
this scheme, a section of code whose execution may lead to runtime errors is
labeled as a try block. Any fragment of code activated during the execution of a
try block can raise an exception using a throw clause. All exceptions are typed
(i.e., each exception is denoted by an object of a specific type). A try block is
followed by one or more catch clauses. Each catch clause is responsible for the
handling of exceptions of a particular type.
When an exception is raised, its type is compared against the catch clauses
following it. If a matching clause is found then its handler is executed.
Otherwise, the exception is propagated up, to an immediately enclosing try block
(if any). The process is repeated until either the exception is handled by a
matching catch clause or it is handled by a default handler.
Flow Control
Figure 10.21 illustrates the flow of control during exception handling. It shows a
function e with a try block from which it calls f; f calls another function g from
its own try block, which in turn calls h. Each of the try blocks is followed by a
list of catch clauses. Function h throws an exception of type B. The enclosing try
block's catch clauses are examined (i.e., A and E); neither matches B. The
exception is therefore propagated to the catch clauses of the enclosing try block
(i.e., C and D), which do not match B either. Propagating the exception further
up, the catch clauses following the try block in e (i.e., A, B, and C) are examined
next, resulting in a match.
At this point flow of control is transferred from where the exception was raised
in h to the catch clause in e. The intervening stack frames for h, g, and f are
unwound: all automatic objects created by these functions are properly destroyed
by implicit calls to their destructors.
catch clauses A
B
C
C
try block
g(...);
catch clauses C
D
function g
try block function h h(...);
Two points are worth noting. First, once an exception is raised and handled by a
matching catch clause, the flow of control is not returned to where the exception
was raised. The best that the program can do is to re-attempt the code that
resulted in the exception (e.g., call f again in the above example). Second, the
only role of a catch clause in life is to handle exceptions. If no exception is
raised during the execution of a try block, then the catch clauses following it are
simply ignored.
For example, recall the Stack class template discussed in Chapter 9 (see Listing
10.42).
Listing 10.42
1 template <class Type>
2 class Stack {
3 public:
4 Stack (int max);
5 ~Stack (void) {delete [] stack;}
6 void Push (Type &val);
7 void Pop (void);
8 Type& Top (void);
9 friend ostream& operator << (ostream&, Stack<Type>); 10 private:
11 Type *stack;
12 int top;
13 const int maxSize;
14 };
There are a number of potential runtime errors which may affect the member
functions of Stack:
• The constructor parameter max may be given a nonsensical value. Also, the
constructor’s attempt at dynamically allocating storage for stack may fail due to
heap exhaustion. We raise exceptions BadSize and HeapFail in response to
these:
if (max <= 0)
throw BadSize();
if ((stack = new Type[max]) == 0) throw HeapFail();
top = -1;
}
if (top < 0)
throw Empty();
return stack[top]; }
Suppose that we have defined a class named Error for exception handling
purposes. The above exceptions are easily defined as derivations of Error:
class Error { /* ... */ }; class BadSize : public Error {}; class HeapFail : public
Error {}; class Overflow : public Error {}; class Underflow : public Error {};
class Empty : public Error {};
try {
statements
}
A try block is followed by catch clauses for the exceptions which may be raised
during the execution of the block. The role of the catch clauses is to handle the
respective exceptions. A catch clause (also called a handler) has the general
form
where type is the type of the object raised by the matching exception, par is
optional and is an identifier bound to the object raised by the exception, and
statements represents zero or more semicolon-terminated statements.
try {
Stack<int> s(3);
s.Push(10); //...
s.Pop();
//...
}
catch (Underflow) catch (Overflow) catch (HeapFail) catch (BadSize) catch
(Empty)
{cout << "Stack underflow\n";}
{cout << "Stack overflow\n";} {cout << "Heap exhausted\n";} {cout << "Bad
stack size\n";} {cout << "Empty stack\n";}
For simplicity, the catch clauses here do nothing more than outputting a relevant
message.
When an exception is raised by the code within the try block, the catch clauses
are examined in the order they appear. The first matching catch clause is selected
and its statements are executed. The remaining catch clauses are ignored.
• Both are pointers and one can be converted to another by implicit type
conversion rules.
Because of the way the catch clauses are evaluated, their order of appearance is
significant. Care should be taken to place the types which are likely to mask
other types last. For example, the clause type void* will match any pointer and
should therefore appear after other pointer type clauses:
try {
//...
}
catch (char*) {/*...*/}
catch (Point*) {/*...*/}
catch (void*) {/*...*/}
will match any exception type and if used, like a default case in a switch
statement, should always appear last.
The statements in a catch clause can also throw exceptions. The case where the
matched exception is to be propagated up can be signified by an empty throw:
catch (char*) {
//...
throw; // propagate up the exception
An exception which is not matched by any catch clause after a try block, is
propagated up to an enclosing try block. This process is continued until either
the exception is matched or no more enclosing try block remains. The latter
causes the predefined function terminate to be called, which simply terminates
the program. This function has the following type:
For example,
void Encrypt (File &in, File &out, char *key)
throw (InvalidKey, BadFile, const char*);
In absence of a throw list, the only way to find the exceptions that a function
may throw is to study its code (including other functions that it calls). It is
generally expected to at least define throw lists for frequently-used functions.
Should a function throw an exception which is not specified in its throw list, the
predefined function unexpected is called. The default behavior of unexpected is
to terminate the program. This can be overridden by calling set_unexpected
(which has the same signature as set_terminate) and passing the replacing
function as its argument:
TermFun set_unexpected(TermFun);
As before, set_unexpected returns the previous setting.
¨
Exercises
10.54 Consider the following function which is used for receiving a packet in a
network system:
void ReceivePacket (Packet pack, Connection c)
{
switch (pack->Type()) {
case controlPack: //...
Define suitable exceptions for the above and modify ReceivePacket so that it
throws an appropriate exception when any of the above cases is not satisfied.
Also define a throw list for the function.
10.55 Define appropriate exceptions for the Matrix class (see Chapter 7) and
modify its functions so that they throw exceptions when errors occur, including
the following:
• When the sizes of the operands of + and - are not identical.
• When the number of the columns of the first operand of * does not match the
number of rows of its second operand.
• When the row or column specified for () is outside its range.
• When heap storage is exhausted.
¨
Figure 11.22 relates these header files to a class hierarchy for a UNIX-based
implementation of the iostream class hierarchy. The highest-level classes appear
implementation of the iostream class hierarchy. The highest-level classes appear
unshaded. A user of the iostream library typically works with these classes only.
Table 11.16 summarizes the role of these high-level classes. The library also
provides four predefined stream objects for the common use of programs. These
are summarized by Table 11.17.
Output
ostream
ofstream ostrstream
Connected to standard input (e.g., the keyboard) Connected to standard output (e.g., the monitor) Connected
to standard error (e.g., the monitor) Connected to standard error (e.g., the monitor)
A stream may be used for input, output, or both. The act of reading data from an
input stream is called extraction. It is performed using the >> operator (called
the extraction operator) or an iostream member function. Similarly, the act of
writing data to an output stream is called insertion, and is performed using the
<< operator (called the insertion operator) or an iostream member function. We
therefore speak of ‘extracting data from an input stream’ and ‘inserting data into
an output stream’.
The iostream library is based on a two layer model. The upper layer deals with
formatted IO of typed objects (built-in or userdefined). The lower layer deals
with unformatted IO of streams of characters, and is defined in terms of
streambuf objects (see Figure 11.23). All stream classes contain a pointer to a
streambuf object or one derived from it.
The streambuf layer provides buffering capability and hides the details of
physical IO device handling. Under normal circumstances, the user need not
worry about or directly work with streambuf objects. These are indirectly
employed by streams. However, a basic understanding of how a streambuf
employed by streams. However, a basic understanding of how a streambuf
operates makes it easier to understand some of the operations of streams.
• A put pointer points to the position of the next character to be deposited into
the sequence as a result of an insertion.
• A get pointer points to the position of the next character to be fetched from the
sequence as a result of an extraction.
For example, ostream only has a put pointer, istream only has a get pointer, and
iostream has both pointers.
Figure 11.24 Streambuf put and get pointers. get pointer
d a t a p r e s e n t ...sequence
put pointer
When a stream is created, a streambuf is associated with it. Therefore, the stream
classes provide constructors which take a streambuf* argument. All stream
classes overload the insertion and extraction operators for use with a streambuf*
operand. The insertion or extraction of a streambuf causes the entire stream
represented by it to be copied. ¨
Ostream provides formatted output capability. Use of the insertion operator <<
for stream output was introduced in Chapter 1, and employed throughout this
book. The overloading of the insertion operator for userdefined types was
discussed in Chapter 7. This section looks at the ostream member functions.
os.put('a');
inserts 'a' into os.
Similarly, write inserts a string of characters into an output stream. For example,
os.write(str, 10);
os.seekp(10, ios::cur);
The second argument may be one of:
• ios::beg for positions relative to the beginning of the stream,
Table 11.18 summarizes the ostream member functions. All output functions
with an ostream& return type, return the stream for which they are invoked.
Multiple calls to such functions can be concatenated (i.e., combined into one
statement). For example,
os.put('a'); os.put('b');
Table 11.18 Member functions of ostream.
ostream (streambuf*);
The constructor associates a streambuf (or its derivation) with the class to provide an output stream.
ostream& put (char);
Inserts a character into the stream.
ostream& write (const signed char*, int n);
ostream& write (const unsigned char*, int n);
Inserts n signed or unsigned characters into the stream. ostream& flush ();
Flushes the stream.
long tellp ();
Returns the current stream put pointer position.
ostream& seekp (long, seek_dir = ios::beg);
Moves the put pointer to a character position in the stream relative to
the beginning, the current, or the end position:
enum seek_dir {beg, cur, end};
Istream provides formatted input capability. Use of the extraction operator >>
for stream input was introduced in Chapter 1. The overloading of the extraction
operator for userdefined types was discussed in Chapter 7. This section looks at
the istream member functions.
extracts and returns the character denoted by the get pointer of is, and advances
the get pointer. A variation of get, called peek, does the same but does not
advance the get pointer. In other words, it allows you to examine the next input
character without extracting it. The effect of a call to get can be canceled by
calling putback which deposits the extracted character back into the stream:
is.putback(ch);
The return type of get and peekis an int (not char). This is because the end-offile
character (EOF) is usually given the value -1.
The behavior of get is different from the extraction operator in that the former
does not skip blanks. For example, an input line consisting of
x y
(i.e., 'x', space, 'y', newline) would be extracted by four calls to get. the same line
would be extracted by two applications of >>.
Other variations of get are also provided. See Table 11.19 for a summary.
The read member function extracts a string of characters from an input stream.
For example,
char buf[64];
is.read(buf, 64);
extracts up to 64 characters from is and deposits them into buf. Of course, if
EOF is encountered in the process, less characters will be extracted. The actual
number of characters extracted is obtained by calling gcount.
is similar to the above call to read but stops the extraction if a tab character is
encountered. The delimiter, although extracted if encountered within the
specified number of characters, is not deposited into buf.
Input characters can be skipped by calling ignore. For example,
is.seekg(-10, ios::cur);
As with seekp, the second argument may be one of ios::beg, ios::cur, or ios::end.
Table 11.19 summarizes the istream member functions. All input functions with
an istream& return type, return the stream for which they are invoked. Multiple
calls to such functions can therefore be concatenated. For example,
is.get(ch1); is.get(ch2); The iostream class is derived from the istream and
ostream classes and inherits their public members as its own public members:
An iostream object is used for both insertion and extraction; it can invoke any of
the functions listed in Tables 11.18 and 11.19.
Table 11.19 Member functions of istream.
istream (streambuf*)
The constructor associates a streambuf (or its derivation) with the class to provide an input stream.
The first version extracts the next character (including EOF). The second and third versions are similar
but instead deposit the character into their parameter. The last version extracts and deposit characters into
the given streambuf until the delimiter denoted by its last parameter is encountered.
Returns the next input character without extracting it. istream& putback (char);
Pushes an extracted character back into the stream. istream& read (signed char*, int n);
istream& read (unsigned char*, int n);
Extracts up to n characters into the given array, but stops if EOF is
encountered.
istream& getline (signed char*, int n, char = '\n'); istream& getline (unsigned
char*, int n, char = '\n');
Extracts at most n-1 characters, or until the delimiter denoted by the
last parameter or EOFis encountered, and deposit them into the given
array, which is always null-terminated. The delimiter, if encountered
and extracted, is not deposited into the array.
int gcount ();
Returns the number of characters last extracted as a result of calling
read or getline.
istream& ignore (int n = 1, int = EOF);
Skips up to ncharacters, but extracts and stops if the delimiter denoted
by the last parameter is encountered.
long tellg ();
Returns the current stream get pointer position.
istream& seekg (long, seek_dir = ios::cur);
Moves the get pointer to a character position in the stream relative to
the beginning, the current, or the end position:
enum seek_dir {beg, cur, end};
The definition of ios contains a number of public enumerations whose values are
summarized by Table 11.20. The io_state values are used for the state data
member which is a bit vector of IO error flags. The formatting flags are used for
the x_flags data member (a bit vector). The open_mode values are bit flags for
specifying the opening mode of a stream. The seek_dir values specify the seek
direction for seekp and seekg.
enum io_state :
ios::goodbit ios::eofbit ios::badbit ios::failbit ios::hardfail
Anonymous enum:
ios::left
ios::right
ios::internal ios::dec
ios::oct
ios::hex
ios::showbase ios::showpoint ios::uppercase ios::showpos ios::fixed
ios::scientifi
c
ios::skipws ios::unitbuf
enum open_mode :
ios::in
ios::out
ios::app
ios::ate
ios::trunc
ios::noreplace ios::nocreate ios::binary
enum seek_dir :
ios::beg
ios::cur
ios::end
IO operations may result in IO errors, which can be checked for using a number
of ios member functions. For example, good returns nonzero if no error has
occurred:
if (s.good())
// all is ok...
where s is an iostream. Similarly, bad returns nonzero if an invalid IO operation
has been attempted:
if (s.bad())
// invalid IO operation...
and fail returns true if the last attempted IO operation has failed (or if bad() is
true):
if (s.fail())
// last IO operation failed... A shorthand for this is provided, based on the
overloading of the ! operator: if (!s) // same as: if (s.fail()) // ...
The opposite shorthand is provided through the overloading of the void* so that
it returns zero when fail returns nonzero. This makes it possible to check for
errors in the following fashion:
The entire error bit vector can be obtained by calling rdstate, and cleared by
calling clear. Userdefined IO operations can report errors by calling setstate. For
example,
s.setstate(ios::eofbit | ios::badbit);
sets the eofbit and badbit flags.
Ios also provides various formatting member functions. For example, precision
can be used to change the precision for displaying floating point numbers:
cout.precision(4);
cout << 233.123456789 << '\n';
This will produce the output:
233.1235
The width member function is used to specify the minimum width of the next
output object. For example,
cout.width(5);
cout << 10 << '\n';
An object requiring more than the specified width will not be restricted to it.
Also, the specified width applies only to the next object to be output. By default,
spaces are used to pad the object up to the specified minimum size. The padding
character can be changed using fill. For example,
cout.width(5);
cout.fill('*');
cout << 10 << '\n';
will produce: ***10 The formatting flags listed in Table 11.20 can be
manipulated using the setf member function. For example,
cout.setf(ios::scientific); cout << 3.14 << '\n';
Another version of setf takes a second argument which specifies formatting flags
which need to be reset beforehand. The second argument is typically one of:
Formatting flags can be reset by calling unsetf, and set as a whole or examined
by calling flags. For example, to disable the skipping of leading blanks for an
input stream such as cin, we can write:
cin.unsetf(ios::skipws);
Table 11.21 summarizes the member functions of ios.
Table 11.21 Member functions of ios.
ios (streambuf*);
The constructor associates a streambuf (or its derivation) with the class.
void init (streambuf*);
Associates the specified streambuf with the stream. streambuf* rdbuf (void);
Returns a pointer to the stream’s associated streambuf. int good (void);
Examines ios::state and returns zero if bits have been set as a
result of an error.
int bad (void);
Examines the ios::badbit and ios::hardfail bits in
ios::state and returns nonzero if an IO error has occurred. int fail (void);
Examines the ios::failbit, ios::badbit, and
ios::hardfail bits in ios::state and returns nonzero if an
operation has failed.
int eof (void);
Examines the ios::eofbit in ios::state and returns nonzero if
the end-of-file has been reached.
void clear (int = 0);
Sets the ios::state value to the value specified by the parameter. void setstate (int);
Sets the ios::state bits specified by the parameter. int rdstate (void);
Returns ios::state.
int precision (void);
int precision (int);
The first version returns the current floating-point precision. The second
version sets the floating-point precision and returns the previous
floating-point precision.
int width (void);
int width (int);
The first version returns the current field width. The second version sets
the field width and returns the previous setting.
char fill (void);
char fill (char);
The first version returns the current fill character. The second version
changes the fill character and returns the previous fill character.
Stream Manipulators
Description
Inserts a newline character and flushes the stream. Inserts a null-terminating character.
Flushes the output stream.
Sets the conversion base to decimal.
Sets the conversion base to hexadecimal. Sets the conversion base to octal.
Extracts blanks (white space) characters. Sets the conversion base to one of 8, 10, or 16. Clears the status
flags denoted by the argument. Sets the status flags denoted by the argument. Sets the padding character to
the argument. Sets the floating-point precision to the argument. Sets the field width to the argument.
A program which performs IO with respect to an external file should include the
header file fstream.h. Because the classes defined in this file are derived from
iostream classes, fstream.h also includes iostream.h.
A file can be opened for output by creating an ofstream object and specifying the
file name and mode as arguments to the constructor. For example, ofstream
log("log.dat", ios::out);
opens a file named log.dat for output (see Table 11.20 for a list of the open mode
values) and connects it to the ofstream log. It is also possible to create an
ofstream object first and then connect the file later by calling open:
ofstream log;
log.open("log.dat", ios::out);
Because ofstream is derived from ostream, all the public member functions of
the latter can also be invoked for ofstream objects. First, however, we should
check that the file is opened as expected:
if (!log)
cerr << "can't open 'log.dat'\n";
else {
char *str = "A piece of text";
log.write(str, strlen(str));
log << endl;
}
The external file connected to an ostream can be closed and disconnected by
calling close:
log.close();
A file can be opened for input by creating an ifstream object. For example,
ifstream inf("names.dat", ios::in);
opens the file names.dat for input and connects it to the ifstream inf. Because
ifstream is derived from istream, all the public member functions of the latter
can also be invoked for ifstream objects.
The fstream class is derived from iostream and can be used for opening a file for
input as well as output. For example:
fstream iof;
iof.open("names.dat", ios::out); // output iof << "Adam\n";
iof.close();
char name[64];
iof.open("names.dat", ios::in); // input iof >> name;
iof.close();
Table 11.23 summarizes the member functions of ofstream, istream, and fstream
(in addition to those inherited from their base classes).
ifstream (void);
ifstream (int fd);
ifstream (int fd, char* buf, int size);
ifstream (const char*, int=ios::in, int=filebuf::openprot);
Similar to ofstream constructors.
fstream (void);
fstream (int fd);
fstream (int fd, char* buf, int size);
fstream (const char*, int, int=filebuf::openprot);
Similar to ofstream constructors.
void open (const char*, int, int = filebuf::openprot);
Opens a file for an ofstream, ifstream, or fstream.
void close (void);
Closes the associated filebuf and file.
void attach(int);
Connects to an open file descriptor.
void setbuf(char*, int);
Assigns a userspecified buffer to the filebuf.
filebuf* rdbuf (void);
Returns the associated filebuf.
The static version ( ssta) is more appropriate for situations where the user is
certain of an upper bound on the stream buffer size. In the dynamic version, the
object is responsible for resizing the buffer as needed.
After all the insertions into an ostrstream have been completed, the user can
obtain a pointer to the stream buffer by calling str:
char *buf = odyn.str();
This freezes odyn (disabling all future insertions). If str is not called before odyn
goes out of scope, the class destructor will destroy the buffer. However, when str
is called, this responsibility rests with the user. Therefore, the user should make
sure that when buf is no longer needed it is deleted:
delete buf;
An istrstream object is used for input. Its definition requires a character array to
be provided as a source of input:
char data[128];
//...
istrstream istr(data, 128);
Alternatively, the user may choose not to specify the size of the character array:
istrstream istr(data);
The advantage of the former is that extraction operations will not attempt to go
beyond the end of data array.
Table 11.24 summarizes the member functions of ostrstream, istrstream, and
strstream (in addition to those inherited from their base classes).
Suppose we are using a language compiler which generates error message of the
form:
Error 21, invalid expression
where 21 is the number of the line in the program file where the error has
occurred. We would like to write a tool which takes the output of the compiler
and uses it to annotate the lines in the program file which are reported to contain
errors, so that, for example, instead of the above we would have something like:
0021 x = x * y +;
Error: invalid expression
8-9 InProg and inData are, respectively, connected to istreams prog and data.
12 Lineis defined to be an istrstreamwhich extracts from dLine.
21 Each time round this loop, a line of text is extracted from data into dLine, and
then processed.
22-26 We are only interested in lines which start with the word Error. When a
match is found, we reset the get pointer of data back to the beginning of the
stream, ignore characters up to the space character before the line number,
extract the line number into lineNo, and then ignore the remaining characters up
to the comma following the line number (i.e., where the actual error message
starts).
27-29 This loop skips prog lines until the line denoted by the error message is
reached.
30-33 These insertions display the prog line containing the error and its
annotation. Note that as a result of the re-arrangements, the line number is
effectively removed from the error message and displayed next to the program
line.
Listing 11.43
1 #include <fstream.h>
2 #include <strstream.h>
3 #include <iomanip.h>
4 #include <string.h>
17 if (!prog || !data) {
18 cerr << "Can't open input files\n";
19 return -1;
20 }
{
return Annotate("prog.dat", "data.dat");
}
data.dat :
Error 1, Unknown directive: defone Note 3, Return type of main assumed int
Error 5, unknown type: integer
Error 7, ) expected
Exercises
11.56 Use the istream member functions to define an overloaded version of the
>> operator for the Set class (see Chapter 7) so that it can input sets expressed in
the conventional mathematical notation (e.g., {2, 5, 1}).
11.57 Write a program which copies its standard input, line by line, to its
standard output.
11.58 Write a program which copies a userspecified file to another userspecified
file. Your program should be able to copy text as well as binary files.
11.59 Write a program which reads a C++ source file and checks that all
instances of brackets are balanced, that is, each ‘(’ has a matching ‘)’, and
similarly for [] and {}, except for when they appear inside comments or strings.
A line which contains an unbalanced bracket should be reported by a message
such as the following sent to standard output:
Figure 12.25 illustrates the effect of the preprocessor on a simple file. It shows
the preprocessor performing the following:
• Removing program comments by substituting a single white space for each
comment.
• Performing the file inclusion (#include) and conditional compilation (#ifdef,
etc.) commands as it encounters them.
Preprocessor Directives
The # symbol should be the first non-blank character on the line (i.e., only
spaces and tabs may appear before it). Blank symbols may also appear between
the # and directive. The following are therefore all valid and have exactly the
same effect:
A directive usually occupies a single line. A line whose last non-blank character
is \, is assumed to continue on the line following it, thus making it possible to
define multiple line directives. For example, the following multiple line and
single line directives have exactly the same effect:
#define CheckError \
if (error) \
exit(1)
#define CheckError if (error) exit(1)
A directive line may also contain comment; these are simply ignored by the
preprocessor. A # appearing on a line on its own is simply ignored.
Table 12.25 summarizes the preprocessor directives, which are explained in
detail in subsequent sections. Most directives are followed by one or more
tokens. A token is anything other than a blank.
Macro Definition
Macros are defined using the #define directive, which takes two forms: plain and
parameterized. A plain macro has the general form:
#define identifier tokens
Plain macros are used for defining symbolic constants. For example:
Use of macros for defining symbolic constants has its origins in C, which had no
language facility for defining constants. In C++, macros are less often used for
this purpose, because consts can be used instead, with the added benefit of
proper type checking.
n = Max (n - 2, k +6);
is macro-expanded to:
n = (n - 2) > (k + 6) ? (n - 2) : (k + 6);
Note that the ( in a macro call may be separated from the macro identifier by
blanks.
Overlooking the fundamental difference between macros and functions can lead
to subtle programming errors. Because macros work at a textual level, the
semantics of macro expansion is not necessarily equivalent to function call. For
example, the macro call
Max(++i, j)
is expanded to
((++i) > (j) ? (++i) : (j))
which means that i may end up being incremented twice. Where as a function
version of Max would ensure that i is only incremented once.
Two facilities of C++ make the use of parameterized macros less attractive than
in C. First, C++ inline functions provide the same level of code efficiency as
macros, without the semantics pitfalls of the latter. Second, C++ templates
provide the same kind of flexibility as macros for defining generic functions and
classes, with the added benefit of proper syntax analysis and type checking.
#undef size
#define size 128
#undef Max
CheckPtr(tree->left);
is expanded as:
if ((tree->left) == 0) cout << "tree->left" << " is zero!\n"; Note that defining the
macro as
#define CheckPtr(ptr) \
if ((ptr) == 0) cout << "ptr is zero!\n"
would not produce the desired effect, because macro substitution is not
performed inside strings.
The concatenation operator (##) is binary and is used for concatenating two
tokens. For example, given the definition
This operator is rarely used for ordinary programs. It is very useful for writing
translators and code generators, as it makes it easy to build an identifier out of
fragments.
¨
File Inclusion
A file can be textually included in another file using the #include directive. For
example, placing
#include "constants.h"
#include "../file.h"
#include "usrlocal/file.h" #include "..\file.h"
#include "\usr\local\file.h" // include from parent dir (UNIX) // full path (UNIX)
// include from parent dir (DOS) // full path (DOS)
When including system header files for standard libraries, the file name should
be enclosed in <> instead of double-quotes. For example:
#include <iostream.h>
When the preprocessor encounters this, it looks for the file in one or more
prespecified locations on the system (e.g., the directory usrinclude/cpp on a
UNIX system). On most systems the exact locations to be searched can be
specified by the user, either as an argument to the compilation command or as a
system environment variable.
File inclusions can be nested. For example, if a file f includes another file g
which in turn includes another file h, then effectively f also includes h.
Although the preprocessor does not care about the ending of an included file
(i.e., whether it is .h or .cpp or .cc, etc.), it is customary to only include header
files in other files.
Multiple inclusion of files may or may not lead to compilation problems. For
example, if a header file contains only macros and declarations then the compiler
will not object to their reappearance. But if it contains a variable definition, for
example, the compiler will flag it as an error. The next section describes a way
of avoiding multiple inclusions of the same file.
¨
Conditional Compilation
#endif
If identifier is not a #defined symbol then code is included in the compilation process. Otherwise, it is
excluded.
If expression evaluates to nonzero then code is included in the compilation process. Otherwise, it is
excluded.
If identifier is a #defined symbol then code1 is included in the compilation process and code2 is excluded.
Otherwise, code2 is included and code1 is excluded. Similarly, #elsecan be used with #ifndefand #if.
If expression1 evaluates to nonzero then only code1 is included in the compilation process. Otherwise, if
expression2 evaluates to nonzero then only code2 is included. Otherwise, code3 is included.
As before, the #else part is optional. Also, any number of #elif directives may appear after a #if
directive.
DisplayBetaDialog();
#else
CheckRegistration();
#endif
One of the common uses of #if is for temporarily omitting code. This is often
done during testing and debugging when the programmer is experimenting with
suspected areas of code. Although code may also be omitted by commenting its
out (i.e., placing /* and */ around it), this approach does not work if the code
already contains /*...*/ style comments, because such comments cannot be
nested.
#if 0
...code to be omitted
#endif
#ifndef fileh_
#define fileh_
contents of file.h goes here
#endif
When the preprocessor reads the first inclusion of file.h, the symbol fileh_ is
undefined, hence the contents is included, causing the symbol to be defined.
Subsequent inclusions have no effect because the #ifndef directive causes the
contents to be excluded.
Other Directives
makes the compiler believe that the current line number is 20 and the current file
name is file.h. The change remains effective until another #line directive is
encountered. The directive is useful for translators which generate C++ code. It
allows the line numbers and file name to be made consistent with the original
input file, instead of any intermediate C++ file.
The #error directive is used for reporting errors by the preprocessor. It has the
general form
#error error
where error may be any sequence of tokens. When the preprocessor encounters
this, it outputs error and causes compilation to be aborted. It should therefore be
only used for reporting errors which make further compilation pointless or
impossible. For example:
#ifndef UNIX
#error This software requires the UNIX OS.
#endif
// align name and val starting addresses to multiples of 8 bytes: #pragma align 8
(name, val)
char name[9];
double val;
Predefined Identifiers
The predefined identifiers can be used in programs just like program constants.
For example,
#define Assert(p) \
if (!(p)) cout << __FILE__ << ": assertion on line " \ << __LINE__ << "
failed.\n" defines an assert macro for testing program invariants. Assuming that
the sample call
Assert(ptr != 0); appear in file prog.cpp on line 50, when the stated condition
fails, the following message is displayed:
prog.cpp: assertion on line 50 failed.
¨
Exercises
12.63 Write a macro named When which returns the current date and time as a
string (e.g., "25 Dec 1995, 12:30:55"). Similarly, write a macro named Where
which returns the current location in a file as a string (e.g., "file.h: line 25").
¨
Solutions to Exercises
1.1
#include <iostream.h>
cout << fahrenheit << " degrees Fahrenheit = " << celsius << " degrees
Celsius\n";
return 0; }
// valid
// invalid: no variable name // valid
// invalid: 'sign' not recognized // valid
// valid
1.3 identifier
seven_11
unique
gross-income
gross$income
2by2
default
average_weight_of_a_large_pizza variable
object.oriented
// valid
// valid
// valid
// invalid: - not allowed in id // invalid: $ not allowed in id
// valid
// invalid: . not allowed in id
2.1
// test if n is even: n%2 == 0
2.2 (((n <= (p + q)) && (n >= (p - q))) || (n == 0)) (((++n) * (q--)) / ((++p) - q))
(n | ((p & q) ^ (p << (2 + q))))
((p < q) ? ((n < p) ? ((q * n) - 2) : ((q / n) + 1)) : (q - n))
2.4
#include <iostream.h>
int main (void) {
long n;
2.5
#include <iostream.h>
int main (void)
{
double n1, n2, n3; cout << "Input three numbers: ";
cin >> n1 >> n2 >> n3;
cout << (n1 <= n2 && n2 <= n3 ? "Sorted" : "Not sorted") << '\n'; return 0;
3.1
#include <iostream.h>
int main (void) {
double height, weight;
cout << "Person's height (in centimeters): "; cin >> height;
cout << "Person's weight (in kilograms: "; cin >> weight;
if (n >= 0)
if (n < 10)
cout << "n is small\n";
else
cout << "n is negative\n"; is therefore misleading, because it is understood by the
compiler as: if (n >= 0)
if (n < 10)
cout << "n is small\n"; else
cout << "n is negative\n"; The problem is fixed by placing the second if within a
compound statement: if (n >= 0) {
if (n < 10)
cout << "n is small\n"; } else
cout << "n is negative\n"; 3.3
#include <iostream.h>
cout << "Input a date as dd/mm/yy: "; cin >> day >> ch >> month >> ch >>
year;
switch (month) {
case 1: cout << "January"; break; case 2: cout << "February"; break; case 3: cout
<< "March"; break; case 4: cout << "April"; break; case 5: cout << "May";
break; case 6: cout << "June"; break; case 7: cout << "July"; break; case 8: cout
<< "August"; break; case 9: cout << "September"; break; case 10: cout <<
"October"; break; case 11: cout << "November"; break; case 12: cout <<
"December"; break;
}
cout << ' ' << day << ", " << 1900 + year << '\n'; return 0;
3.4
#include <iostream.h>
cout << "Factorial of " << n << " = " << factorial << '\n'; }
return 0;
3.5
#include <iostream.h>
}
// process each digit // right-most digit
cout << "Octal(" << octal << ") = Decimal(" << decimal << ")\n"; return 0;
}
{
return 5 * (fahren - 32) / 9;
}
4.1b
#include <iostream.h>
char* CheckWeight (double height, double weight) {
if (weight < height/2.5) return "Underweight";
cout << "Person's height (in centimeters): "; cin >> height;
cout << "Person's weight (in kilograms: "; cin >> weight;
cout << CheckWeight(height, weight) << '\n';
return 0; }
4.2 The value of x and y will be unchanged because Swap uses value parameters.
Consequently, it swaps a copy of xand yand not the originals.
4.3 The program will output:
Parameter Local
Global
Parameter
4.4
enum Bool {false, true};
void Primes (unsigned int n) {
Bool isPrime;
if (num%i == 0) {
isPrime = false;
break;
}
if (isPrime)
cout << num << '\n';
} }
{
switch (month) { case Jan: case Feb: case Mar: case Apr: case May: case Jun:
case Jul: case Aug: case Sep: case Oct: case Nov: case Dec: default:
}
return "January"; return "february"; return "March";
return "April";
return "May";
return "June";
return "July";
return "July";
return "August"; return "September"; return "October"; return "November";
return "December"; return "";
4.6
inline int IsAlpha (char ch)
{
return ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z';
}
4.7
int Power (int base, unsigned int exponent)
{
return (exponent <= 0)
? 1
: base * Power(base, exponent - 1);
}
va_end(args); // clean up args return sum;
5.1
void ReadArray (double nums[], const int size)
{ for (register i = 0; i < size; ++i) { cout << "nums[" << i << "] = "; cin >>
nums[i];
nums[i];
}
}
void WriteArray (double nums[], const int size) {
for (register i = 0; i < size; ++i) cout << nums[i] << '\n'; }
5.2
void Reverse (double nums[], const int size) {
double temp;
}
}
5.3 double contents[][4] = { { 12, 25, 16, 0.4 }, { 22, 4, 8, 0.3 }, { 28, 5, 9, 0.5 },
{ 32, 7, 2, 0.2 }
};
void WriteContents (const double *contents, const int rows, const int cols)
{
for (register i = 0; i < rows; ++i) {
for (register j = 0; j < cols; ++j)
cout << (contents + i rows + j) << ' '; cout << '\n';
}
}
5.4
enum Bool {false, true};
void ReadNames (char *names[], const int size) {
char name[128];
for (register i = 0; i < size; ++i) { cout << "names[" << i << "] = "; cin >> name;
names[i] = new char[strlen(name) + 1]; strcpy(names[i], name);
}
}
void WriteNames (char *names[], const int size) {
void WriteNames (char *names[], const int size) {
for (register i = 0; i < size; ++i) cout << names[i] << '\n'; }
do {
swapped = false;
for (register i = 0; i < size - 1; ++i) {
}
}
} while (swapped);
}
*res-- = '\0';
while (*str)
res-- = str++; return result;
}
5.6
typedef int (*Compare)(const char*, const char*);
do {
swapped = false;
for (register i = 0; i < size - 1; ++i) {
5.7
typedef void (*SwapFun)(double, double);
SwapFun Swap;
typedef char *Table[];
Table table;
typedef char *&Name;
Name name;
typedef unsigned long *Values[10][20];
Values values;
6.1 Declaring Set parameters as references avoids their being copied in a call.
Call-byreference is generally more efficient than call-by-value when the objects
involved are larger than the built-in type objects.
6.2
class Complex { public:
private:
double real; // real part
double imag; // imaginary part
};
Complex Complex::Add (Complex &c)
{
return Complex(real + c.real, imag + c.imag);
}
{
return Complex(real - c.real, imag - c.imag);
}
6.3
#include <iostream.h> #include <string.h>
const int end = -1; // denotes the end of the list
private:
Menu::~Menu (void) {
Menu::Option handy, next;
}
}
void Menu::Insert (const char *str, const int pos) {
// set prev to point to before the insertion position: for (handy = first; handy != 0
&& idx++ != pos; handy = handy>Next())
prev = handy;
}
}
// set prev to point to before the deletion position: for (handy = first;
handy != 0 && handy->Next() != 0 && idx++ != pos;
handy = handy->Next())
prev = handy;
if (handy != 0) {
if (prev == 0)
first = handy->Next(); else
// it's the first entry
// it's not the first prev->Next() = handy->Next(); delete handy;
}
}
do {
n = 0;
for (handy = first; handy != 0; handy = handy->Next())
cout << ++n << ". " << handy->Name() << '\n'; cout << "Option? ";
cin >> choice;
6.4
#include <iostream.h>
const int maxCard = 10; enum Bool {false, true};
private:
class Element { public:
int
Element*& private:
int
int
Element };
Element (const int val) {value = val; next = 0;} Value (void) Next (void) {return
value;} {return next;}
}
}
{
if (!Member(elem)) {
Set::Element *option = new Set::Element(elem); option->Next() = first; //
prepend first = option;
}
}
// set prev to point to before the deletion position: for (handy = first;
// set prev to point to before the deletion position: for (handy = first;
handy != 0 && handy->Next() != 0 && handy->Value() != elem;
handy = handy->Next())
prev = handy;
if (handy != 0) {
if (prev == 0)
first = handy->Next(); else
// it's the first entry
// it's not the first prev->Next() = handy->Next(); delete handy;
}
}
void Set::Copy (Set &set) {
Set::Element *handy;
for (handy = first; handy != 0; handy = handy->Next()) set.AddElem(handy-
>Value());
}
Bool Set::Equal (Set &set) { Set::Element *handy;
if (Card() != set.Card())
return false;
for (handy = first; handy != 0; handy = handy->Next()) if (!set.Member(handy-
>Value()))
return false;
return true;
}
6.5
#include <iostream.h> #include <string.h>
enum Bool {false, true}; typedef char *String;
class BinNode; class BinTree;
protected:
char **entries; const int slots; int used; // sorted array of string entries // number
of sequence slots // number of slots used so far
};
void
Sequence::Delete (const char *str) {
entries[j] = entries[j+1];
--used;
break;
}
}
}
Bool
Sequence::Find (const char *str) {
return false;
}
void
Sequence::Print (void) {
6.6
#include <iostream.h> #include <string.h>
enum Bool {false,true};
class BinNode { public:
char*&
BinNode*& BinNode*& BinNode ~BinNode Value
Left
Left
Right
(const char*);
(void) {delete value;} (void) {return value;} (void) {return left;} (void) {return
right;}
private:
char *value; // node value
BinNode left; // pointer to left child BinNode right; // pointer to right child
};
{root->FreeSubtree(root);}
{root->DeleteNode(str, root);} {return root->FindNode(str, root) != 0;}
void Print (void) {root->PrintNode(root); cout << '\n';} protected:
void
BinNode::FreeSubtree (BinNode *node) {
if (node != 0) {
FreeSubtree(node->left); FreeSubtree(node->right); delete node;
}
}
void
BinNode::InsertNode (BinNode node, BinNode &subtree) {
if (subtree == 0)
subtree = node;
else if (strcmp(node->value, subtree->value) <= 0) InsertNode(node, subtree-
>left);
else
InsertNode(node, subtree->right);
}
void
BinNode::DeleteNode (const char str, BinNode &subtree) {
int cmp;
if (subtree == 0)
return;
if ((cmp = strcmp(str, subtree->value)) < 0)
DeleteNode(str, subtree->left);
else if (cmp > 0)
DeleteNode(str, subtree->right);
else {
BinNode* handy = subtree;
if (subtree->left == 0) // no left subtree subtree = subtree->right;
else if (subtree->right == 0) // no right subtree subtree = subtree->left;
else { // left and right subtree subtree = subtree->right;
// insert left subtree into right subtree: InsertNode(subtree->left, subtree->right);
}
delete handy;
}
}
const BinNode*
BinNode::FindNode (const char str, const BinNode subtree) {
int cmp;
return (subtree == 0)
? 0
: ((cmp = strcmp(str, subtree->value)) < 0
void
BinNode::PrintNode (const BinNode *node) {
if (node != 0) {
PrintNode(node->left); cout << node->value << ' '; PrintNode(node->right);
}
}
BinTree::BinTree (Sequence &seq)
{
root = MakeTree(seq, 0, seq.Size() - 1);
}
void
BinTree::Insert (const char *str) {
};
BinTree::BinTree (Sequence &seq)
BinNode*
BinTree::MakeTree (Sequence &seq, int low, int high) {
6.8 A static data member is used to keep track of the last allocated ID (see lastId
below).
class Menu {
public:
//...
int ID (void) {return id;} private:
//...
int id; // menu ID
static int lastId; // last allocated ID };
int Menu::lastId = 0;
6.9
#include <iostream.h> #include <string.h>
const int end = -1; class Option;
// denotes the end of the list
private:
class Option { public:
private:
char *name;
const Menu *submenu;
Option *next;
// option name // submenu
// next option
};
Menu::Option::~Option (void) {
delete name; delete submenu;
}
{
if (submenu == 0)
return 0;
else
return submenu->Choose(); }
int Menu::lastId = 0;
Menu::~Menu (void) {
Menu::Option handy, next;
}
}
void Menu::Insert (const char str, const Menu submenu, const int pos) {
Menu::Option *option = new Option(str, submenu); Menu::Option handy, prev =
0;
int idx = 0;
// set prev to point to before the insertion position: for (handy = first; handy != 0
&& idx++ != pos; handy = handy>Next())
prev = handy;
}
}
// set prev to point to before the deletion position: for (handy = first;
handy != 0 && handy->Next() != 0 && idx++ != pos;
handy = handy->Next())
prev = handy;
if (handy != 0) {
if (prev == 0)
first = handy->Next(); else
// it's the first entry
// it's not the first prev->Next() = handy->Next(); delete handy;
}
}
do {
n = Print();
cout << "Option? ";
cin >> choice;
>Next())
++n;
// choose the option:
n = handy->Choose();
n = handy->Choose();
7.1
#include <string.h>
const int Max (const int x, const int y)
{
return x >= y ? x : y;
}
{
return x >= y ? x : y;
}
{
return strcmp(x,y) >= 0 ? x : y;
}
};
Set operator - (Set &set1, Set &set2) {
Set res;
for (register i = 0; i < set1.card; ++i) if (!(set1.elems[i] & set2))
res.elems[res.card++] = set1.elems[i]; return res;
}
Bool operator <= (Set &set1, Set &set2) {
if (set1.card > set2.card)
return false;
for (register i = 0; i < set1.card; ++i) if (!(set1.elems[i] & set2)) return false;
return true;
}
}
return res;
}
7.4
#include <iostream.h>
class Matrix { public:
Matrix (const int rows, const int cols); Matrix (const Matrix&);
~Matrix (void);
double& operator () (const int row, const int col); Matrix& operator = (const
Matrix&);
// nonzero element
Element* void
private:
};
CopyList(Element list); DeleteList (Element list);
double& InsertElem (Element *elem, const int row, const int col);
int rows, cols; // matrix dimensions Element *elems; // linked-list of elements };
first = copy;
else
prev->Next() = copy;
prev->Next() = copy;
prev = copy;
}
return first;
}
void Matrix::Element::DeleteList (Element *list) {
Element *next;
}
}
// InsertElem creates a new element and inserts it before // or after the element
denoted by elem.
double& Matrix::InsertElem (Element *elem, const int row, const int col)
{
} else {
// insert after elem:
newElem->Next() = elem->Next();
elem->Next() = newElem;
}
return newElem->Value();
}
Matrix::~Matrix (void)
{ elems->DeleteList(elems);
}
{
if (elems == 0 || row < elems->Row() || row == elems->Row() && col < elems-
>Col()) // create an element and insert in front: return InsertElem(elems, row,
col);
// check if it's the first element in the list: if (row == elems->Row() && col ==
elems->Col()) return elems->Value();
// search the rest of the list:
for (Element *elem = elems; elem->Next() != 0; elem = elem->Next()) if (row
== elem->Next()->Row()) {
// doesn't exist
// doesn't exist // create new element and insert just after elem: return
InsertElem(elem, row, col);
InsertElem(elem, row, col);
}
ostream& operator << (ostream &os, Matrix &m) {
Matrix::Element *elem = m.elems;
for (register row = 1; row <= m.rows; ++row) {
for (register col = 1; col <= m.cols; ++col)
if (elem != 0 && elem->Row() == row && elem->Col() == col)
{
os << elem->Value() << '\t'; elem = elem->Next();
} else
os << 0.0 << '\t';
os << '\n';
}
return os;
}
Matrix operator + (Matrix &p, Matrix &q) {
Matrix m(p.rows, q.cols);
// copy p:
for (Matrix::Element *pe = p.elems; pe != 0; pe = pe->Next()) m(pe->Row(), pe-
>Col()) = pe->Value();
// add q:
for (Matrix::Element *qe = q.elems; qe != 0; qe = qe->Next()) m(qe->Row(), qe-
>Col()) += qe->Value();
return m;
}
Matrix operator - (Matrix &p, Matrix &q) {
Matrix m(p.rows, q.cols);
// copy p:
for (Element *pe = p.elems; pe != 0; pe = pe->Next()) m(pe->Row(), pe->Col())
= pe->Value(); // subtract q:
for (Element *qe = q.elems; qe != 0; qe = qe->Next()) m(qe->Row(), qe->Col())
-= qe->Value(); return m;
}
Matrix operator * (Matrix &p, Matrix &q) {
Matrix m(p.rows, q.cols);
for (Element pe = p.elems; pe != 0; pe = pe->Next()) for (Element qe = q.elems;
qe != 0; qe = qe->Next()) if (pe->Col() == qe->Row())
m(pe->Row(),qe->Col()) += pe->Value() * qe->Value(); return m;
}
7.5
#include <string.h> #include <iostream.h>
protected:
char *chars; // string characters short len; // length of chars
};
String::~String (void)
{
delete chars;
delete chars;
}
}
strcpy(chars, str.chars);
}
return(*this);
}
strcpy(result.chars, str1.chars);
strcpy(result.chars + str1.len, str2.chars); return(result);
7.6
#include <string.h> #include <iostream.h>
#include <string.h> #include <iostream.h>
enum Bool {false, true}; typedef unsigned char uchar;
protected:
uchar vec; // vector of 8bytes bits short bytes; // bytes in the vector
};
// set the bit denoted by idx to 1 inline void BitVec::Set (const short idx) {
// reset the bit denoted by idx to 0 inline void BitVec::Reset (const short idx) {
{
{
return (*this) = (*this) & v;
}
{
return (*this) = (*this) | v;
}
{
return (*this) = (*this) ^ v;
}
{
return (*this) = (*this) << n;
}
{
return (*this) = (*this) >> n;
}
// return the bit denoted by idx
inline int BitVec::operator [] (const short idx)
{
return vec[idx/8] & (1 << idx%8) ? true : false;
}
{
return *this == v ? false : true;
}
// bitwise AND
BitVec BitVec::operator & (const BitVec &v) {
// bitwise OR
BitVec BitVec::operator | (const BitVec &v) {
// bitwise exclusive-OR
BitVec BitVec::operator ^ (const BitVec &v) {
for (i = bytes - 1; i >= zeros; --i) // shift bytes left r.vec[i] = vec[i - zeros];
for (i = zeros; i < r.bytes; ++i) { // shift bits left r.vec[i] = (r.vec[i] << shift) |
prev;
prev = vec[i - zeros] >> (8 - shift); }
return r;
}
uchar prev = 0; for (i = r.bytes - zeros - 1; i >= 0; --i) { // shift bits right r.vec[i] =
(r.vec[i] >> shift) | prev;
prev = vec[i + zeros] << (8 - shift);
}
return r;
}
8.1
#include "bitvec.h"
enum Month {
Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec };
inline Bool LeapYear(const short year) {return year%4 == 0;}
class Year : public BitVec { public:
{
return (*this)[day] == 1 ? true : false;
}
short Year::Day (const short day, const Month month, const short year) {
static short days[12] = {
31, 28, 31, 30, 31, 30, 31, 31, 20, 31, 30, 31 };
days[Feb] = LeapYear(year) ? 29 : 28;
LinEqns (const int n, double *soln); void Generate (const int coef);
void Solve (void);
private:
Matrix solution;
};
}
}
}
for (diag = 1; diag <= Rows(); ++diag) { // diagonal piv = diag; // pivot for (r =
for (diag = 1; diag <= Rows(); ++diag) { // diagonal piv = diag; // pivot for (r =
diag + 1; r <= Rows(); ++r) // upper triangle
// back substitute:
Matrix soln(Rows(), 1);
8.3
#include "bitvec.h"
{
return s | t;
}
{
return s & ~t;
}
{
return s & t;
}
{
{
return t[elem];
}
{
return (t & s) == s;
}
8.4 typedef int Key; typedef double Data; enum Bool { false, true };
#include <iostream.h>
#include "database.h"
const int maxOrder = 256; // max tree order
class BTree : public Database {
public:
class Page;
class Item { // represents each stored item
public:
Item (void) {right = 0;} Item (Key, Data);
Key& KeyOf (void) {return key;} Data& DataOf (void) {return data;} Page*&
Subtree (void) {return right;}
public:
virtual void virtual void virtual Bool friend ostream& operator << (ostream&,
BTree&); BTree ~BTree Insert Delete Search (const int order);
(void) {FreePages(root);} (Key key, Data data);
(Key key);
(Key key, Data &data);
protected:
const int order; // order of tree Page Page *root; // root of the tree
const int order; // order of tree Page Page *root; // root of the tree
*bufP; // buffer page for distribution/merging
virtual void DeleteAux1 (Key key, Page page, Bool &underflow); DeleteAux2
(Page parent,Page *page,
const int idx, Bool &underflow); Underflow (Page page, Page child,
int idx, Bool &underflow);
};
{
return ofItem <= 0 ? left: items[ofItem - 1].Subtree();
}
{
return ofItem < 0 ? left : items[ofItem].Subtree();
return ofItem < 0 ? left : items[ofItem].Subtree();
}
do {
mid = (low + high) / 2;
if (key <= items[mid].KeyOf())
int BTree::Page::CopyItems (Page *dest, const int srcIdx, const int destIdx,
const int count)
{
for (register i = 0; i < count; ++i) // straight copy dest->items[destIdx + i] =
items[srcIdx + i]; return count;
}
}
}
void BTree::Delete (Key key) {
Bool underflow;
if (item == 0)
return false;
data = item->DataOf();
return true;
}
{
if (page != 0) {
FreePages(page->Left(0));
for (register i = 0; i < page->Used(); ++i)
FreePages(page->Right(i));
delete page;
}
}
// recursively search the tree for an item with matching key
if (tree == 0)
return 0;
if (tree->BinarySearch(key, idx))
return &((*tree)[idx]);
return SearchAux(idx < 0 ? tree->Left(0) : tree->Right(idx), key); }
}
}
return 0;
}
// delete an item from a page and deal with underflows
if (page->BinarySearch(key, idx)) {
if ((child = page->Left(idx)) == 0) { // page is a leaf underflow = page-
>DeleteItem(idx);
// handle underflows
void BTree::Underflow (Page page, Page child, int idx, Bool &underflow)
{
Page left = idx < page->Used() - 1 ? child : page->Left(idx); Page right = left
== child ? page->Right(++idx) : child;
// copy contents of left, parent item, and right onto bufP: int size = left-
>CopyItems(bufP, 0, 0, left->Used()); (*bufP)[size] = (*page)[idx];
bufP->Right(size++) = right->Left(0);
size += right->CopyItems(bufP, 0, size, right->Used());
- 1);
right->Left(0) = bufP->Right(half);
(*page)[idx] = (*bufP)[half];
page->Right(idx) = right;
underflow = false;
} else {
// merge, and free the right page:
left->Used() = bufP->CopyItems(left, 0, 0, size); underflow = page-
>DeleteItem(idx);
delete right;
}
}
A B*-tree is a B-tree in which most nodes are at least 2/3 full (instead of 1/2
full). Instead of splitting a node as soon as it becomes full, an attempt is made to
evenly distribute the contents of the node and its neighbor(s) between them. A
node is split only when one or both of its neighbors are full too. A B*-tree
facilitates more economic utilization of the available store, since it ensures that
at least 66% of the storage occupied by the tree is actually used. As a result, the
height of the tree is smaller, which in turn improves the search speed. The search
and delete operations are exactly as in a B-tree; only the insertion operation is
different.
virtual void BStar Insert (const int order) : BTree(order) {} (Key key, Data data);
protected:
virtual Item* virtual Item* InsertAux Overflow (Item item, Page page); (Item
item, Page page,
}
}
// inserts and deals with overflows
if (page->BinarySearch(item->KeyOf(), idx))
return 0; // already in tree if ((child = page->Right(idx)) != 0) {
// child not a leaf:
if ((item = InsertAux(item, child)) != 0)
return Overflow(item, page, child, idx);
// handles underflows
Item* BStar::Overflow (Item item, Page page, Page *child, int idx)
{
Page left = idx < page->Used() - 1 ? child : page->Left(idx); Page right = left
== child ? page->Right(++idx) : child;
// copy left, overflown and parent items, and right into buf: bufP->Used() = left-
>CopyItems(bufP, 0, 0, left->Used()); if (child == left ) {
bufP->InsertItem(*item, bufP->Used());
bufP->InsertItem((*page)[idx], bufP->Used());
bufP->Right(bufP->Used() - 1) = right->Left(0);
bufP->Used() +=
- 1);
right->Left(0) = bufP->Right(half); (*page)[idx] = (*bufP)[half]; page-
>Right(idx) = right;
return 0;
} else {
// split int 3 pages:
Page newP = new Page(2 order); int mid1, mid2;
9.1 template <class Type> void Swap (Type &x, Type &y) {
Type tmp = x; x = y;
y = tmp;
9.2
#include <string.h>
enum Bool {false, true};
Bool swapped;
do {
swapped = false;
swapped = false;
for (register i = 0; i < size - 1; ++i) {
}
} while (swapped); }
// specialization:
void BubbleSort (char **names, const int size) {
Bool swapped;
do {
swapped = false;
for (register i = 0; i < size - 1; ++i) {
}
}
} while (swapped);
}
9.3
#include <string.h> #include <iostream.h>
enum Bool {false,true};
typedef char *Str;
Type&
BinNode*& BinNode*& BinNode ~BinNode Value
Left
Right
(const Type&);
(const Type&);
(void) {}
(void) {return value;} (void) {return left;} (void) {return right;}
private:
Type value; // node value
BinNode left; // pointer to left child BinNode right; // pointer to right child
};
template <class Type> class BinTree {
public:
BinTree (void);
~BinTree(void);
void Insert (const Type &val); void Delete (const Type &val); Bool Find (const
Type &val); void Print (void);
protected:
BinNode<Type> *root; // root node of the tree };
value = val;
left = right = 0; }
// specialization:
if (node != 0) {
FreeSubtree(node->left); FreeSubtree(node->right); delete node;
}
}
if (subtree == 0)
subtree = node;
else if (node->value <= subtree->value) InsertNode(node, subtree->left);
else
InsertNode(node, subtree->right); }
// specialization:
if (subtree == 0)
subtree = node;
else if (strcmp(node->value, subtree->value) <= 0) InsertNode(node, subtree-
>left);
else
InsertNode(node, subtree->right);
}
int cmp;
if (subtree == 0)
return;
if (val < subtree->value)
if (val < subtree->value)
DeleteNode(val, subtree->left);
else if (val > subtree->value)
DeleteNode(val, subtree->right);
else {
BinNode* handy = subtree;
if (subtree->left == 0) // no left subtree subtree = subtree->right;
else if (subtree->right == 0) // no right subtree subtree = subtree->left;
else { // left and right subtree subtree = subtree->right;
// insert left subtree into right subtree: InsertNode(subtree->left, subtree->right);
}
delete handy;
}
}
// specialization:
void BinNode<Str>::DeleteNode (const Str &str, BinNode<Str> *&subtree) {
int cmp;
if (subtree == 0)
return;
if ((cmp = strcmp(str, subtree->value)) < 0) DeleteNode(str, subtree->left);
else if (cmp > 0)
DeleteNode(str, subtree->right);
else {
BinNode<Str>* handy = subtree;
if (subtree->left == 0) // no left subtree subtree = subtree->right;
else if (subtree->right == 0) // no right subtree subtree = subtree->left;
else { // left and right subtree subtree = subtree->right;
// insert left subtree into right subtree: InsertNode(subtree->left, subtree->right);
}
delete handy;
}
}
if (subtree == 0)
if (subtree == 0)
return 0;
if (val < subtree->value)
return FindNode(val, subtree->left);
if (val > subtree->value)
return FindNode(val, subtree->right);
return subtree;
}
// specialization:
const BinNode<Str>*
BinNode<Str>::FindNode (const Str &str, const BinNode<Str> *subtree) {
int cmp;
return (subtree == 0)
? 0
: ((cmp = strcmp(str, subtree->value)) < 0
if (node != 0) {
PrintNode(node->left); cout << node->value << ' '; PrintNode(node->right); }
root = 0; }
root = 0; }
root->FreeSubtree(root); }
root->DeleteNode(val, root); }
9.4
#include <iostream.h>
enum Bool { false, true };
template <class Key, class Data> class Page; template <class Key, class Data>
class Item { // represents each stored item public:
BTree ~BTree virtual void Insert virtual void Delete virtual Bool Search friend
ostream& operator << (ostream&, BTree&);
protected:
const int order; // order of tree
Page<Key, Data> *root; // root of the tree Page<Key, Data> *bufP; // buffer
page for distribution/merging
page for distribution/merging
virtual void DeleteAux1 (Key key, Page<Key, Data> *page, Bool &underflow);
};
used = 0;
left = 0;
items = new Item<Key, Data>[size];
}
// return the left subtree of an item
// return the left subtree of an item
int low = 0;
int high = used - 1; int mid;
do {
mid = (low + high) / 2;
if (key <= items[mid].KeyOf())
{
for (register i = 0; i < count; ++i) // straight copy dest->items[destIdx + i] =
items[srcIdx + i];
items[srcIdx + i];
return count;
}
// insert an item into a page
for (register i = used; i > atIdx; --i) // shift right items[i] = items[i - 1];
items[atIdx] = item; // insert
return ++used >= size; // overflow? }
for (register i = atIdx; i < used - 1; ++i) // shift left items[i] = items[i + 1];
return --used < size/2; // underflow? }
char margBuf[128];
// build the margin string: for (int i = 0; i <= margin; ++i) margBuf[i] = ' ';
margBuf[i] = '\0';
// print the left-most child:
if (Left(0) != 0)
Left(0)->PrintPage(os, margin + 8);
root = 0;
bufP = new Page<Key, Data>(2 * order + 2); }
bufP = new Page<Key, Data>(2 * order + 2); }
}
}
template <class Key, class Data> void BTree<Key, Data>::Delete (Key key) {
Bool underflow;
if (item == 0)
return false;
data = item->DataOf();
return true;
}
}
if (tree.root != 0)
tree.root->PrintPage(os, 0);
return os;
}
if (page != 0) {
FreePages(page->Left(0));
for (register i = 0; i < page->Used(); ++i)
FreePages(page->Right(i));
delete page;
}
}
// recursively search the tree for an item with matching key Item<Key, Data>*
BTree<Key, Data>::
if (tree == 0)
return 0;
if (tree->BinarySearch(key, idx))
return &((*tree)[idx]);
return SearchAux(idx < 0 ? tree->Left(0) : tree->Right(idx), key); }
if (page->BinarySearch(item->KeyOf(), idx))
return 0; // already in tree
if ((child = page->Right(idx)) != 0)
item = InsertAux(item, child); // child is not a leaf
if (item != 0) { // page is a leaf, or passed up if (page->Used() < 2 * order) { //
insert in the page page->InsertItem(*item, idx + 1);
} else { // page is full, split Page<Key, Data> newP = new Page<Key, Data>(2
order);
bufP->Used() = page->CopyItems(bufP, 0, 0, page->Used()); bufP-
>InsertItem(*item, idx + 1);
int size = bufP->Used(); int half = size/2;
page->Used() = bufP->CopyItems(page, 0, 0, half); newP->Used() = bufP-
>CopyItems(newP, half + 1, 0, size half - 1);
newP->Left(0) = bufP->Right(half);
}
}
return 0;
}
// delete an item from a page and deal with underflows
template <class Key, class Data>
void BTree<Key, Data>::DeleteAux1 (Key key,
if (page->BinarySearch(key, idx)) {
if ((child = page->Left(idx)) == 0) { // page is a leaf underflow = page-
>DeleteItem(idx);
// handle underflows
// copy contents of left, parent item, and right onto bufP: int size = left-
>CopyItems(bufP, 0, 0, left->Used()); (*bufP)[size] = (*page)[idx];
bufP->Right(size++) = right->Left(0);
size += right->CopyItems(bufP, 0, size, right->Used());
- 1);
right->Left(0) = bufP->Right(half);
(*page)[idx] = (*bufP)[half];
page->Right(idx) = right;
underflow = false;
} else {
// merge, and free the right page:
left->Used() = bufP->CopyItems(left, 0, 0, size); underflow = page-
>DeleteItem(idx);
delete right;
}
}
//------------------------------------------------------------
template <class Key, class Data> class BStar : public BTree<Key, Data> {
public:
}
}
// inserts and deals with overflows
template <class Key, class Data>
Item<Key, Data>* BStar<Key, Data>::InsertAux (Item<Key, Data> *item,
if (page->BinarySearch(item->KeyOf(), idx))
return 0; // already in tree
if ((child = page->Right(idx)) != 0) {
// child not a leaf:
if ((item = InsertAux(item, child)) != 0)
return Overflow(item, page, child, idx);
} else if (page->Used() < 2 * order) { // item fits in node page-
>InsertItem(*item, idx + 1);
} else { // node is full int size = page->Used();
bufP->Used() = page->CopyItems(bufP, 0, 0, size); bufP->InsertItem(*item, idx
+ 1);
bufP->CopyItems(page, 0, 0, size);
item = (bufP)[size];
return item;
}
return 0;
}
// handles underflows
template <class Key, class Data>
// copy left, overflown and parent items, and right into buf: bufP->Used() = left-
>CopyItems(bufP, 0, 0, left->Used()); if (child == left ) {
bufP->InsertItem(*item, bufP->Used());
bufP->InsertItem((*page)[idx], bufP->Used());
bufP->Right(bufP->Used() - 1) = right->Left(0);
bufP->Used() +=
- 1);
right->Left(0) = bufP->Right(half); (*page)[idx] = (*bufP)[half]; page-
>Right(idx) = right;
return 0;
} else {
// split int 3 pages:
Page<Key, Data> newP = new Page<Key, Data>(2 order); int mid1, mid2;
10.1
10.1
enum PType {controlPack, dataPack, diagnosePack}; enum Bool {false, true};
class Packet {
public:
//...
PType Type (void) {return dataPack;}
Bool Valid (void) {return true;} };
class Connection {
public:
//...
Bool Active (void) {return true;} };
switch (pack->Type()) {
case controlPack: //... break;
10.2
#include <iostream.h>
class DimsDontMatch {}; class BadDims {}; class BadRow {}; class BadCol
{}; class HeapExhausted {};
const short Rows (void) const short Cols (void) {return rows;} {return cols;}
private:
const short rows; // matrix rows const short cols; // matrix columns double
*elems; // matrix elements
};
Matrix::Matrix (const short r, const short c) : rows(r), cols(c) {
if (rows <= 0 || cols <= 0) throw BadDims();
elems = new double[rows * cols]; if (elems == 0)
throw HeapExhausted();
}
Matrix::Matrix (const Matrix &m) : rows(m.rows), cols(m.cols) {
{
{
if (rows == m.rows && cols == m.cols) { // must match int n = rows * cols;
for (register i = 0; i < n; ++i) // copy elements
elems[i] = m.elems[i];
} else
throw DimsDontMatch();
return *this;
}
}
Matrix operator + (Matrix &p, Matrix &q) {
if (p.rows != q.rows || p.cols != q.cols) throw DimsDontMatch();
Matrix m(p.rows, p.cols);
if (p.rows == q.rows && p.cols == q.cols) for (register r = 1; r <= p.rows; ++r)
for (register c = 1; c <= p.cols; ++c) m(r,c) = p(r,c) + q(r,c); return m; }
Matrix operator - (Matrix &p, Matrix &q) {
if (p.rows != q.rows || p.cols != q.cols) throw DimsDontMatch();
Matrix m(p.rows, p.cols);
if (p.rows == q.rows && p.cols == q.cols) for (register r = 1; r <= p.rows; ++r)
for (register c = 1; c <= p.cols; ++c) m(r,c) = p(r,c) - q(r,c); return m;
}
Matrix operator * (Matrix &p, Matrix &q) {
if (p.cols != q.rows)
throw DimsDontMatch();
Matrix m(p.rows, q.cols);
if (p.cols == q.rows)
for (register r = 1; r <= p.rows; ++r)