C++ For Java Programmers PDF
C++ For Java Programmers PDF
Contents
Preface
xv
Chapter 0 Introduction
0.1 A History Lesson
0.2 High Level Differences
0.2.1 Compiled vs. Interpreted Code
0.2.2 Security and Robustness
0.2.3 Multithreading
0.2.4 API Differences
0.3 Ten Reasons To Use C++
0.3.1 C++ Is Still Widely Used
0.3.2 Templates
0.3.3 Operator Overloading
0.3.4 Standard Template Library
0.3.5 Automatic Reclamation of Resources
0.3.6 Conditional Compilation
0.3.7 Distinctions Between Accessor and Mutator
0.3.8 Multiple Implementation Inheritance
0.3.9 Space Efficiency
0.3.10 Private Inheritance
0.4 Key Points
0.5 Exercises
1
1
4
4
4
5
5
6
6
6
6
6
6
8
8
8
8
8
8
9
vi
Contents
11
11
11
12
13
13
13
13
14
14
14
15
15
15
16
16
16
16
17
17
18
19
21
21
21
21
22
22
24
24
25
26
27
28
29
30
Contents
vii
30
31
31
33
34
35
35
37
37
38
43
43
43
45
45
46
48
48
49
49
49
50
50
50
50
51
53
54
55
56
58
61
64
64
64
64
viii
Contents
91
91
93
94
97
97
103
103
105
105
106
107
Contents
ix
110
110
111
113
113
115
116
116
117
118
119
121
122
125
126
128
130
130
130
131
131
132
132
133
133
133
133
134
Chapter 7 Templates
7.1 Review of Generic Programming in Java
7.2 Function Templates
7.3 Class Templates
7.4 Separate Compilation
7.4.1 Everything in the Header
7.4.2 Explicit Instantiation
7.4.3 The export Directive
137
137
138
141
144
145
146
147
Contents
147
147
147
148
148
150
150
152
152
153
154
155
155
158
158
159
160
160
160
160
160
161
161
162
162
162
162
164
164
165
166
167
167
169
171
174
Contents
9.5
9.6
9.7
9.8
9.9
9.10
9.11
xi
Files
Random Access
String Streams
endl
Serialization
Key Points
Exercises
175
176
177
179
180
180
181
183
183
183
184
186
187
188
188
191
191
191
192
193
193
194
194
195
195
196
197
197
198
200
200
200
202
202
203
203
204
xii
Contents
10.13 Exercises
205
207
207
208
210
215
219
219
220
222
222
223
223
225
227
227
229
229
230
231
232
233
235
235
237
242
243
243
243
244
245
246
246
249
249
Contents
xiii
252
253
254
255
256
259
260
260
260
262
265
268
268
270
270
272
273
Bibliography
275
Index
277
xiv
Contents
Preface
Organization
The book begins with a brief overview of C++ in Chapter 0. In Chapter 1, we describe some of
the basic expressions and statements in C++, which mostly mirrors simple Java syntax. Functions, arrays, strings, and parameter passing are discussed in Chapter 2. We use the modern
alternative of introducing and using the standard vector and string classes in the C++ library,
xv
xvi
Preface
Acknowledgements
For this text, I would like to thank my editor Alan Apt and his assistants Toni Holm,
Patrick Linder, and Jake Warde.
<<Add acks for copy-edits, cover, production, marketing>>
I also thank the following reviewers, who provided valuable comments, many of which
have been incorporated into the text:
XXXX, University of YYY
XXXX, University of YYY
XXXX, University of YYY
XXXX, University of YYY
Some of the material in this text (especially Chapters 1, 2, 3, 11, and 12) is adapted from
my textbook Efficient C Programming: A Practical Approach (Prentice-Hall, 1995).
My World Wide Web page http://www.cs.fiu.edu/~weiss will contain updated
source code, an errata list, and a link for receiving bug reports.
M.A.W
Miami, Florida
June, 2003
H A P T E R
Introduction
Chapter 0 Introduction
Gigabyte of main memory, and 120 Gigabytes of hard drive space. By the time you read this, the
computer just described may well be a relic.
On the other hand, a PDP-11/45, which this author actually used as a college undergraduate, sold for well over $10,000 (in 1970 dollars). Our model had 128 Kilobytes of main memory,
and executed only several thousand instructions per second. The PDP-11/45 was a 16 bit
machine, so not surprisingly, the int type was 16 bits. But other PDP models, had 12, or 18, or
even 36 bits. The C compiler on the PDP-11/45 would typically take about 30 seconds to compile a trivial one hundred line program. But since it used 32K of memory to do so, compilations
were queued, like printer jobs, to avoid compiling two programs at once and crashing the system! Our PDP-11/45 supported over 20 simultaneous users, all connected via old-style dumb
terminals. Most of the terminals operated at 110 baud, though there were a few fast ones that
went at 300 baud. At 110 baud, approximately 10 characters per second are transmitted. So
screen editors were not widely used; instead editing was done a line at a time, using the Unix
editor ed (which still exists!). C thus provided rather terse syntax to minimize typing, and more
importantly displaying.
In this environment, it is not surprising that the number one goal of the compiler was to
compile correct programs as fast as possible into code that was as fast as possible. And since the
main users were typically experts, dealing with incorrect programs was left to the programmer,
rather than the compiler (or runtime system). No checks were performed to ensure that a variable was assigned a value prior to use of its value. After all, who wanted to wait any longer for
the program to compile? Arrays were implemented in the same manner as in an assembly language, with no bounds checking to consume precious CPU cycles. A host of programming practices that involved the use of pointers to achieve performance benefits emerged. There was even
a reserved word, register, that was used to suggest to the compiler that a particular local
variable should be stored in a machine register, rather than on the runtime stack, to make the
program run faster, since the compiler would not try to do any flow analysis on its own.
Soon, Unix became popular in the academic community, and with it, the C language grew.
At one point, C was known as the great portable language, suitable for systems use on many
machines. Unix itself was ported to a host of platforms. Eventually a host of software vendors
started producing C compilers, adding their own extra features, but in so doing, made the language less portable, in part because the language specification was vague in places, allow competing interpretations. Also, a host of clever but nonetheless unacceptable and unportable
programming tricks had emerged; it became clear that these tricks should be disallowed. Since
compiler technology had become better and computers had become faster, it also because feasible to add helpful features to the language, in the hopes of enhancing portability, and reducing
the chances of undetected errors. Eventually, in 1989, this resulted in ANSI C, which is now the
gold standard of C that most programmer adhere to. In 1999, C99 was adopted, adding some
new features, but since C99 is not yet widely implemented in compilers, few programmers find
a need to use those features.
A History Lesson
Chapter 0 Introduction
ming errors avoided, system security against hacking is enhanced, since hacking generally
works by having the system do something it is not designed to do.
The Java designers have several advantages. First, in designing the language, they can
expect the compiler to work hard. We no longer use PDP-11s, and compiler research has
advanced greatly to the point that modern optimizing compilers can do a terrific job of creating
code, without requiring the programmer to resort to various tricks common in the good old days.
Second, Java designers specified most of the language (classes, inheritance, exceptions) at once,
adding only a second minor revision (inner classes), with most changes after Java 1.1 dealing
with the library. In doing so, they were able to have language features that dont clash.
Although Java is the new kid on the block (unless C# becomes the next new kid), and has
lots of nice features, it is certainly not true that Java is better than C++, Nor would we say that
C++ is better than Java. Instead, a modern programmer should be able to use both languages, as
each language has applications that can make it the logical choice.
Java consists of both a compiler and a Virtual Machine. The compiler generates Java bytecode,
and the bytecode is then interpreted at runtime. As a result, not only is Java code portable, the
compiled bytecode is portable and in theory can be run on any platform.
On the other hand, a C++ compiler generates native code. The C++ language specification
provides no guidance on the specifics of what the native code looks like, so not only is the result
of the compilation not transferable to a different type of computer, parts of a single program that
are compiled on the same computer by different compilers are almost always not compatible
with each other.
Originally, the difference between compiled code and interpreted code meant that C++
was as much as 50 times faster than Java. Recent improvements in the Java compiler, and more
importantly, the Virtual Machine have dramatically closed the gap. In some cases, Java code
may actually be faster than C++, but generally speaking, one would expect that since C++ does
less runtime checks than Java, equally skilled compiler writers should be able to generate somewhat faster C++ code than Java code, assuming similar coding styles.
0.2.2
We have already seen that Java is very concerned with not allowing unsafe code to compile, and
with throwing exceptions at the first sign of trouble. C++ is unfortunately, somewhat lax in this
regard. C++ suffers several problems that can never occur in pure Java code. Four that stand out
are the following:
First, it is possible in C++ to have a pointer or reference to an object that has been returned
back to the memory heap, which is a sure disaster. This is because standard C++ does not do garbage collection; instead the programmer must manage memory themselves, and programmers
are surprisingly bad at doing so. However, some C++ systems include garbage collection and
add runtime checks to avoid using stale pointers. These systems are quite close to the Java standard of avoiding memory problems.
Second, Standard C++ does not check array indexes, and a common hacker attack is to
find an input routine that reads a string, but doesnt check that there is enough space for a very
very long string. By judiciously passing a huge string, the hacker can overflow the buffer, writing replacement values onto variables that are stored in memory adjacent to the buffer. Although
this could never happen in Java, since the C++ specification does not disallow bounds checks,
there is no reason that a safe C++ system couldnt check array bounds. It would simply be
slower (but safer) than a competitor, and so it is not widely done by default.
Third, old C++ typecasts allow type confusion, in which a type is cast to an unrelated type.
This can never happen in Java, but is allowed in C++.
Fourth, in Java, all variables have a definite assigned value prior to use of their value. This
is because variables that are not local to a method by default are initialized to zero for primitives
and null for references. For local variables, an entire chapter of the Java Language Specification is devoted to definite assignment, whereby the compiler is required to perform a flow analysis and produce an error message if a local variable cannot be proven (under a long set of rules)
to have been definitely assigned to through all flows of the method. In C++, this behavior is not
required. Rather, a program that uses an uninitialized variable is said to be incorrect, but the
compiler is not required to take any particular action. Most C++ compilers will print warning
messages if uninitialized variables are detected. But the program will still compile. A similar
story occurs in the case of having a flow that fails to return a value in a non-void function.
0.2.3
Multithreading
C++ does not support multithreading as part of the language. Instead, one must use a set of
library routines that are native to the particular platform. Although Java supports multithreading,
the Java memory model and threading specification has recently been discovered to be inadequate and is undergoing revision.
0.2.4
API Differences
The Java API is huge. The core library includes, among other things, the Swing package
javax.swing for designing (mostly) portable GUIs, a networking package in java.net,
database connectivity in java.sql, built-in compression, serialization, and other I/O in
java.util.zip and java.io, reflection in java.lang.reflect, and even support for
remote methods, servlets, and XML.
Chapter 0 Introduction
Standard C++ has a very small API, containing little more than some I/O support, a complex number package, and a Collections API, known as the Standard Template Library (STL).
Of course, compiler vendors augment Standard C++ with huge libraries, but each vendor has
different versions rather than implementing a single standard.
Perhaps the most important reason to learn C++ is that it is still very-widely used, and since it is
a more complex and difficult language than C++, it is likely that there will always be a strong
market for knowledgeable C++ programmers.
0.3.2
Templates
Perhaps the most requested missing feature in Java is the equivalent of the C++ template. In
C++, templates allow the writing of generic, type-independent code, such as sorting algorithms
and generic data structures that work for any type. In Java, this is done by using inheritance, but
the downside is that many errors are not detected at compile time, but instead linger until run
time. Further, using templates in C++ seems to lead to faster code than the inheritance-based
alternative in Java. Generics are under consideration for Java 1.5. Template are discussed in
Chapter 7.
0.3.3
Operator Overloading
Java does not allow the user to defined operators for class types. C++ does, and this is known as
operator overloading. We discuss operator overloading in Chapter 5.
0.3.4
C++ provides a large library, known as the Standard Template Library (STL) for data structures
and generic algorithms. The STL has several advantages compared to the Java Collections API,
and we discuss these differences in Chapter 10.
0.3.5
Although Java provides garbage collection, which is a fantastic feature, it is hard to control the
management of other resources. For instance, in Java, the programmer must remember to close
files and database connections when they are no longer in use, dispose of graphics contexts, and
so on. Although many (non-memory) resources are released by object finalization, relying on
object finalization in Java is a poor idea, since objects need not be reclaimed if the garbage collector deems that memory is not low.
Figure 0-1
Feature
Java
C++
Compiler output
native code
Portability
Hi
Moderate
Garbage collection
Yes
No
Yes
No
Multithreading
Yes
Platform specific
API
Huge
Small
Security checks
Yes
No
Compiler checks
Numerous
Some
Operator overloading
No
Yes
Templates
No
Yes
Multiple Inheritance
Interface only
Yes
Yes
No
Data Structures
Collections API
STL
Exception Handling
Yes
Conditional Compilation
No
Yes
Global Functions
No
Yes
Class grouping
Packages
Namespaces
Pointer variables
No
Yes
Class documentation
Javadoc
Not in standard
Reflection
Yes
No
Space efficient
Not really
Yes
Chapter 0 Introduction
In C++, each class can provide a special method known as the destructor, which will automatically be invoked when an object is no longer active. Careful C++ programmers need not
remember to release non-memory resources, and memory resource can often be released by layering the memory allocations inside of classes. Many Java programmers who have prior C++
experience lament the lack of destructors. We describe destructors in Section 4.6.
0.3.6
Conditional Compilation
In C++, it is possible to write code that is compiled only if certain conditions are met at compiletime. This is achieved by use of the preprocessor and is a useful feature during debugging. Java
has only a limited way of doing this. The preprocessor is described in Section 12.1.
0.3.7
In Java, when a reference variable is marked as final, it simply means that the reference variable
cannot change; there is no easy way to signal that the state of the object being referenced cannot
be changed (i.e. that the object is immutable). C++ provides syntax to distinguish between methods that are accessors and mutators, and marking an object as const (the equivalent of final)
would allow only the accessors of the object to be invoked. Many Java programmers who have
prior C++ experience lament the difficulty of enforcing immutability. We describe accessors and
mutators in Section 4.2.
0.3.8
Java does not allow multiple implementation inheritance because it is notoriously difficult to use
generally. However, there are always some cases when it is very useful, and C++ does allow
multiple implementation inheritance. We discuss this in Section 6.8.
0.3.9
Space Efficiency
Java programs are notoriously space inefficient. For instance, a String of length 16 uses
roughly 76 bytes under Suns Java 1.3 compiler. C++ programs are often more space efficient
that Java programs.
0.3.10 Private Inheritance
Java supports only public inheritance, via the extends clause. C++ supports private inheritance, which is occasionally useful for changing visible interfaces, and implementing adapter
patterns. Private inheritance is discussed in Section 6.10.2.
Exercises
The most important consideration in the design of C++ is to make correct programs run as
fast as possible.
Compile-time checks in C++ are not as rigid as in Java, but now many compilers perform
some of the same checks as a Java compiler, yield warning messages.
Run time checks in C++ are not as rigid in Java. In C++, bad array indexes, bad type casts,
and bad pointers do not automatically cause a runtime error. Some compilers will do more
than the minimum, and issue runtime errors for you, but this cannot be relied on.
Compiled units are not compatible across different types of machines, nor are the compatible on the same machine, when generated by different compilers.
Although the STL in C++ is excellent, the remainder of the Standard C++ library pales in
comparison to Java. But many non-standard additions are available.
0.5 Exercises
1. Compare and contrast the basic design goals of C++ and Java.
2. What are the different versions of C++?
3. What are the basic differences between compiled and interpreted languages?
4. What is a buffer overflow problem?
5. List features of Java that are not part of Standard C++ and result in C++ being a less safe
language.
6. Describe some features of C++ that make it more attractive than Java.
10
Chapter 0 Introduction
H A P T E R
main Function
Like Java, control starts at main. Unlike Java, main is not part of any class; instead, it is a nonclass method. In C++ methods are called member functions, and we will adopt the convention
that a function that is not declared as part of a class is simply a function (with the adjective member conveniently omitted). C++ also allows global variables.
11
12
1
2
3
4
5
6
7
8
#include <iostream>
using namespace std;
int main( )
{
cout << "Hello world" << endl;
return 0;
}
Figure 1-1
1
2
3
4
5
6
7
Figure 1-2
main must always be declared in global scope, it must have return type int, which by
convention will be 0 unless a non-zero error code is to be transmitted back to the invoking process, in a manner that is similar to calling System.exit (we will always return 0). Although
some programmers prefer to use a void return type, the language specification is clear that the
return type of main should be int.
main can take additional parameters for command-line arguments (we will discuss this in
Section 11.5). Because main is in global scope, there can be only one version of main. This
contrasts with Java, which allows one main per class.
1.1.2
The Preprocessor
Line 1 is an include directive that reads the declarations for the standard I/O routines. The
include directive has the effect of having the compiler logically insert the source code taken
from another file. Specifically, the file name that resides in between < and > refers to a system
file. Alternatively, a pair of double quotes can be used to specify a user-defined (non-system)
file. Thus in our example, the entire contents of the file iostream, which resides in a systemdependent location, is substituted for line 1. In Section 2.1.7 we discuss how the include directive is typically used to enable faster compilations.
In C++, lines that begin with the # are preprocessor directives. We will see an important
use of preprocessor directives in Section 4.12.
Primitive Types
1.1.3
13
The declaration at line 2 is a using directive, and is the moral equivalent of an import directive in
Java. Whereas classes in package java.lang in Java are automatically known without requiring the full class name that includes their package, in C++, classes in the equivalent namespace,
std, require a full class name unless the using directive is supplied. Thus this using directive is
the C++ equivalent of
import java.lang.*;
We discuss the using directive and the C++ equivalent of packages in Section 4.15.
1.1.4
Output
Ignoring the return statement, the simple program in Figure 1-1 consists of a single statement.
This statement, shown at line 6 is the output mechanism in C++. Here a constant string is placed
on the standard output stream cout. Simple terminal input is similar to output, with cin
replacing cout, and >> replacing <<. As an example:
int x;
cout << "Enter a value of x: ";
cin >> x;
reads the next series of characters (skipping whitespace), interpreting them as an integer. Of
course, this example fails to discuss handling input errors, which is crucial in any serious application. Input and output is discussed in more detail in Chapter 9.
Integer Types
The basic integer type in C++, like Java, is int. However, whereas Java specifies the precise
range of int, C++ makes no such guarantees, and an int can be 16, 32, or 64 bits, depending
on the platform. Even for 32 bit ints, the precise range can vary from machine to machine.
The int type can be augmented with the reserved words short or long. Alternatively
short and long can be used by themselves without int. Once again, the language specification makes few guarantees about how large short and long are, except to state that an int is
never shorter than a short, and never longer than a long. Historically, a short has been 16
bits, a long has been 32 bits, and an int has been whatever fits best on the machine (currently
32 bits). A long constant includes an L at the end, as in 1000L (the L is never needed if the constant is larger than what could be stored in an int).
Two modifiers to the int type are signed or unsigned. These modifiers can be
applied to any of the integer types (or char). An unsigned int will never be negative, and
in effect instructs the compiler to interpret what would normally be the sign bit as an extra bit.
14
Thus, whereas a short might store values in the range -32768 to 32767, an unsigned
short could store values in the range 0 to 65535. This use of unsigned to double the set of
range of positive integers carries the significant danger that mixing unsigned and signed values
can produce surprising results, so it is best to use unsigned types judiciously. An unsigned constant includes a U at the end, as in 1000UL. By default, the integer types are signed.
C++ does not define an equivalent to the byte data type. Historically, C++ programmers
have used signed char, which in C++ is simply eight bits, for this purpose.
1.2.2
Floating point types in C++ are represented with float and double. Like Java, double provides more significant digits than a float and its use is preferred to minimize roundoff errors.
Java specifies constants for not a number and both positive and negative infinity. Though the
C++ standard does not require it, some C++ implementations will be able to store and print such
values when they occur.
1.2.3
Character Type
The char type, as in Java, is used to store characters. However, unlike Java, where a character
is a 16-bit Unicode, the C++ standard does not specify a size, and C++ compilers generally use
only 8 bits, and store ASCII characters. Additionally, whether a char is signed or unsigned by
default is unspecified.
As character constant is represented in a pair of single quotes, as is done in Java. Additionally, there is a rich set of escape sequences that can be used to express unprintable characters,
quotes, backslashes, and so forth. The Java escape sequences are part of C++, as well some addition sequences including \a for the bell, \? for the question mark, and \v for the vertical tab.
The escape sequence that allows up to three octal characters is also part of C++. The escape
sequence that uses four hexadecimal characters is not part of Standard C++.
The standard header file cctype contains routines such isupper, islower, and
isalpha for determining if a character has a certain property. Also present are toupper and
tolower that return upper and lower-case versions of a character, or the character itself, if it is
not a lower case letter. In effect these routines are versions of the static methods in
java.lang.Character.
Recent C++ compilers have adopted the Java style of supporting Unicode characters with
the addition of the wchar_t type and the wide-character literal. Wide character literals begin
with an L prior to the opening quote, as in Lx or L"hello". As one may expect by now, the
specific behavior of the wchar_t type is implementation dependent.
1.2.4
Boolean Type
Like Java, C++ has a Boolean type. In C++, this type is bool, and has values true or false.
However, unlike Java, this Boolean type was not originally part of C++, which can result in
erroneous code that is legal. We discuss this in Section 1.3.2.
Syntactic Differences
15
Operators in C++ are for the most part identical to their Java counterparts, though in many cases,
dubious Java code has well-defined semantics, whereas the same code in C++ can be implementation dependent. Classic examples of this include the overuse of the ++ operator, as in
int x = 5;
x = x++;
In Java this code will set x to be 5 (since the ++ is executed prior to the assignment of the original value of x). In C++, the result of this code is implementation dependent. In both languages
the code is poor style. More generally, Java specifies that arguments to methods and operands of
an operator (except for assignment operators) are evaluated in left-to-right order, whereas C++
makes no such guarantees. Thus, if function readInt is intended to read one integer from standard input, the result of readInt()-readInt() when 4 and 3 are placed on standard input
is guaranteed to be 1 in Java, but can be 1 or -1 in C++.
In Java, integer division rounds down, so 8/-5 is always -1. In C++, the result is implementation dependent and can be -1 or -2, depending on whether the division truncates or rounds
to the nearest integer.
C++ adds the comma operator, but the only good use is the use allowed in Java in the
expressions that are part of a for loop. The comma operator takes two expressions, evaluates the
first, then the second, and the result of the comma operator is the second expression. This is
more trouble than its worth, as illustrated by the fact that mistyping 3.14 as 3,14 does not
generate a compiler error (it simply evaluates to 14). Similarly, the call pow(3,4) returns 81
(three to the fourth power), whereas pow((3,4)) does not compile because (3,4) evaluates
to 4, and then pow is short a parameter. Yuk.
Java has the >>> shift operator, which does bit shifting (to the right), filling in high bits
with 0s. The >> shift operator in Java fills in high bits with whatever the high bit of the left operand was originally. In C++, only the >> shift operator is available, and its semantics depend on
whether the left operand is signed or unsigned. If it is unsigned, high bits are filled with 0; if it is
signed, high bits are filled in with the high bit of the left operand.
1.3.2
Conditionals
The if statement in C++ is identical to Java, except that in C++ the condition of the if statement
can be either an int or a bool. This stems from the fact that historically, early implementations of C++ did not have a bool type, but instead interpreted 0 as false, and anything non-zero
as true. The unfortunate consequence of this decision is that code such as
if( x = 0 )
16
which uses = instead of the intended == is not flagged as a compiler error, but instead sets x to
zero and evaluates the condition to false. This is possibly the most common trivial C++ programming error.
Occasionally, especially when reading old C++ code, shorthands that make use of the fact
that nonzero evaluates to true are placed in the conditional expression. Thus it is not uncommon
to see tests that should be
if( i != 0 )
rewritten as
if( i )
Newly written code should avoid these types of shortcuts since they tend to make the code less
readable, and no more efficient.
1.3.3
Loops
Like Java, C++ has the for loop, the while loop, and the do loop, along with the break and continue statements. However in C++, the use of the labelled break and labelled continue to affect
flow of an outer loop is not allowed.
C++ allows the goto, but its use is strongly discouraged.
1.3.4
Definite Assignment
An entire section of the Java Language Specification deals with errors that a Java compiler is
required to detect; such errors cause the compilation to fail. For instance, a Java compiler must
apply a conservative flow analysis is to ensure that every local variable has been definitely
assigned a value, regardless of the flow of control. Although occasionally this forces the programmer to rewrite valid code, much more often, it finds programming errors at compilation
time. A Java compiler is required to verify that all flows of control return a value in a non-void
method. C++ compilers are not required to perform such analyses, though many do if requested
to with suitable compilation options. It wont take long until you will write a C++ program that
compiles and uses an uninitialized variable, causing you runtime grief; so pay attention to warning messages and turn on the compilers ability to generate extra warnings.
In Java, the typecast operator has two basic uses: downcasting a reference variable to a subclass
reference type, and interpreting a primitive type as a different type (for instance, casting an int
to a byte). C++ has several different type casts. In this section we discuss the typecasts that are
used for primitive types.
Additional Syntax
17
First, let us mention that C++ is more liberal than Java in accepting code without a cast. In
Java,
double x = 6.0;
int y;
y = x;
does not compile, but the code does compile (possibly with a warning about losing precision) in
C++. In some cases, typecasting is essential in order to produce a correct answer, as in
double quotient;
int x = 6;
int y = 10;
quotient = x / y;
In this example, in both Java and C++, quotient evaluates to 0.0 rather than 0.6, because
the operands are both of type int, and so division is performed with truncation to an int.
There are several syntactical ways to handle the typecast. The Java style, which also works in
C++,
quotient = (double) x / (double) y;
in which we cast both operands is easy to read. Since only one operand needs to be a double,
quotient = (double) x / y;
also works. However, this code is hard to read because one must be aware that the precedence of
the typecast operator is higher than the precedence of division, which is why the typecast applies
to x, and not x/y. An alternative in C++ is preferred:
quotient = double( x ) / y;
Here, the parentheses surround the expression to be casted, rather than the new type. (Note this
does not work for complex types, such as unsigned int).
Although this second style is preferable, it is hard to find the typecasts that are being used
in a program, since the syntax does not stand out. A third form, a late addition to C++, is to use
the static cast:
quotient = static_cast<double>( x ) / y;
Labels
C++ allows labels prior to virtually any statement, for use with the goto statement. Since gotos
typically are symptomatic of poorly designed code, one would rarely expect to see labels in C++
code.
1.4.3
typedef Statement
The typedef statement is provided by C++ to allow the programmer to assign meaningful names
18
to existing types. As an example, suppose that we need to declare objects of type 32-bit
unsigned integer. On some machines this might be unsigned long, while on other, perhaps an
unsigned int is the most appropriate type. On the first machine, we would use
typedef unsigned long uint32;
Now, in the rest of the code, either machine can declare variables of type uint32:
uint32 x, y, z;
meaning that the non-portable aspect of the code is confined to one line.
Exercises
19
The typedef statement allows the programmer to assign meaningful names to existing
types.
1.6 Exercises
1. Compile, link, and run the program in Figure 1-1.
2. Write a program to print out the values of the largest and smallest int on your system by
printing the appropriate values in the standard header file <climits>.
3. What is the result of
if( x = y )
cout << x << endl;
else
cout << y << endl;
20
H A P T E R
2.1 Functions
In Section 1.1.1 we mentioned that C++ allows methods that are not members of a class. Typically we refer to these as functions. This section discusses functions; member functions are discussed in Chapter 4.
2.1.1
Function definition
Functions, not being part of a class, simply consist of a return type, function name, parameter
list, and body. This complete set, in which the function body is included, is often called a function definition. A function can be invoked from any other function simply by providing an
appropriate set of parameters. Figure 2-1 illustrates a function definition of max2 that looks
similar to Java code, except that both a visibility modifier and the static reserved word are
omitted. (We use max2 to avoid an accidental match with a Standard Library routine named
max).
2.1.2
Function Invocation
A function is invoked by simply providing arguments of type that are compatible with the formal parameters. In C++, there is more latitude granted when the arguments are not of the exact
type required. For instance, a long can be an acceptable argument (possibly with a warning)
even if the formal parameter is an int. When parameters are passed as in Figure 2-1, call-by-
21
22
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
using namespace std;
int max2( int a, int b )
{
return a > b ? a : b;
}
int main( )
{
int x = 37;
int y = 52;
cout << "Max is " << max2( x, y ) << endl;
return 0;
}
Figure 2-1
value is used. Thus the formal parameters become copies of the actual arguments, and as in Java,
the values of the actual arguments cannot be changed as a result of the function invocation. C++
also provides additional ways of passing parameters, including call-by-reference, which does
allow changes to the values of the actual arguments. This is discussed in Section 2.3.
2.1.3
Function Overloading
C++ allows function overloading along the same lines as Java. Several functions can have the
same name as long as they have different signatures. As with Java, the signature of a function
includes the function name and the number and types of the parameters. (It also includes the
parameter passing mechanism, which we discuss in Section 2.3). Like Java, the signature does
not include the return type. C++ has a complicated set of rules that are used to resolve an overloaded call in the case where there are several candidates.
2.1.4
Function Declarations
When a function is invoked, the C++ compiler will check actual parameters against the signatures of all the functions that it has seen thus far in order to resolve overloading. However,
because historically a C++ compiler processes the source code from top to bottom, if a function
definition has not already been seen, a compiler would not include its signature in the list of candidates. As a specific example, the code in Figure 2-2, in which the max2 method is defined
after the invocation is attempted does not compile.
To resolve this, the programmer would have to ensure that every function is defined prior
to its invocation. This is tedious at best, and possibly impossible in the case of mutual recursion
(in which two functions call each other). Thus C++ introduced the notion of the function prototype.
Functions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
23
#include <iostream>
using namespace std;
int main( )
{
int x = 37;
int y = 52;
cout << "Max is " << max2( x, y ) << endl;
return 0;
}
int max2( int a, int b )
{
return a > b ? a : b;
}
Figure 2-2
The syntax of the function prototype is for all intents and purposes identical to the listing
of a method in a Java interface. For instance, the prototype for max2 is:
int max2( int a, int b );
The prototype does not include the body, but instead terminates the declaration with a
semicolon. The prototype, which is also known as a function declaration, allows the max2 function, which is presumed to be defined elsewhere, to be a candidate when max2 is invoked. Typ1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;
int max2( int a, int b );
int main( )
{
int x = 37;
int y = 52;
cout << "Max is " << max2( x, y ) << endl;
return 0;
}
int max2( int a, int b )
{
return a > b ? a : b;
}
Figure 2-3
24
ical strategy would thus involve listing all the prototypes prior to the first function definition,
thus assuring that all functions can call all other functions. Figure 2-3 illustrates the use of the
function prototype.
2.1.5
Default Parameters
C++ allows the user to specify default values for formal parameters. Typically the default values
are included in the function declaration. As an example, the following declaration for
printInt specifies that by default integers should be printed in decimal:
void printInt( int n, int base = 10 );
// Outputs 50
// Outputs 62 (50 in octal)
If a default value is specified for a formal parameter, all subsequent formal parameters
must have default values too. In the example above, for instance, we could not specify
void printInt( int base = 10, int n );
Consequently, parameters that might be omitted are arranged so that the most likely to assume
default values will go last.
A default value cannot be specified in both the function declaration and function definition. A default value can be specified in the function definition instead of the declaration, but
this is considered bad practice because it requires that the function definition be placed prior to
the function invocation, in order for the signature that does not include parameters that have
defaults to be considered as a candidate during the overload resolution.
Overuse of default values can lead to ambiguities. For instance,
void printInt( int n, int base = 10 );
void printInt( int n );
Inline Functions
In some situations, the overhead of making a function call can be significant. For instance, the
max2 routine is trivial, and so one might be tempted to simply replace the function invocation in
main with the code that max2 is logically performing:
cout << "Max is " << ( x > y ? x : y ) << endl;
Of course, this would be sacrificing good programming practice for speed. To avoid this
one can use the inline directive.
The inline directive suggests to the compiler that it should generate code that avoids the
overhead of a function call but is nonetheless semantically equivalent. The inline directive can
appear in either a function declaration or definition; however, the function definition must be
available at the point of invocation. Its use is illustrated in Figure 2-4.
Functions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
25
#include <iostream>
using namespace std;
inline int max2( int a, int b )
{
return a > b ? a : b;
}
int main( )
{
int x = 37;
int y = 52;
cout << "Max is " << max2( x, y ) << endl;
return 0;
}
Figure 2-4
Modern compilers have very sophisticated techniques that are used decide if honoring the
directive produces better code. The compiler may well refuse to perform the optimization for
functions that are too long. Additionally, the directive is likely to be ignored for recursive functions.
2.1.7
Separate Compilation
Often we would like to split a program up into several source files. In such a case, we expect to
be able to invoke a function that is defined in one file from a point that is not in the same file.
This is acceptable as long as a prototype is visible.
The typical scenario to allow this is that instead of having each file list its function declarations at the top, each file creates a corresponding .h file with the function declarations. Then,
any file that needs these declarations can provide an appropriate include directive.
In our example this gives three files all shown in Figure 2-5. The file max2.h simply lists
the function declarations for all functions defined in max2.cpp. The main program is defined
is a separate file and provides the include directive. Recall that this directive replaces line 5 with
the contents of the file max2.h. Finally, max2.cpp provides an include directive also. This
include directive is not needed, but would be typical in the case in which max2.cpp had several functions that were calling each other. Section 4.12 discusses one other issue that is common with this technique.
Compiling this program depends on the platform. Most IDEs perform two steps: compilation and linking. The compilation stage verifies the syntax and generates object code for each of
the files. The linking stage is used to resolve the function invocations with the actual definitions
(sometimes this occurs at runtime).
26
1
2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1
2
3
4
5
6
7
#include "max2.h"
int main( )
{
int x = 37;
int y = 52;
cout << "Max is " << max2( x, y ) << endl;
return 0;
}
Figure 2-5
In a typical IDE, the .cpp files would be made part of the project; the .h files would not.
Since the contents of the .h files are logically copied into the .cpp files by the include directives, the .h files should not define any functions or global variables, since this would result in
an error message concerning multiple definitions during the linking stage. One exception to this
rule is that inline functions should be placed in the .h files; the compiler will make arrangements to avoid the multiple definition error message, even if the inline directive is not honored.
If a function is declared, and it is invoked, but there is no definition available anywhere,
then during the linking stage an error will occur, stating that the function is undefined. Often this
means either that your function declaration is not the same as the function definition or that your
project does not include the file that contains the missing function definition. Most compilers
will tell you not only the name, but the signature of the function that is either undefined or multiply defined.
27
comparable as the difference between Javas built-in array type and ArrayList library type.
Actually, this is probably an understatement, because using built-in arrays in C++ is much more
difficult than using the Java counterpart, whereas vector and ArrayList are for all intents
and purposes identical. The reason is that using C++ built-in arrays may require you to independently maintain the array size and also provide additional code to reclaim memory. vector has
neither of these problems. The built-in C++ array type is discussed in Chapter 11. If you would
like to keep your blood pressure low, its a good idea to stick with the vector type as much as
possible.
Similarly, strings in C++ come in two basic flavors. The primitive string is simply a builtin array of characters, and are exasperating and dangerous to use. See Chapter 11. The library
type, string, is a full-fledged string class and is easy to use.
2.2.1
To use the standard vector, your program must include the standard header file vector, as in
#include <vector>
A using directive may be needed if one has not already been provided.
As we will discuss in more detail in Chapter 3, C++ deals with objects differently than
Java. Specifically, in Java, objects are accessed by reference variables, and are treated, by
design, differently from primitive types. In C++, objects are meant to look just like primitive
types. Thus, they can be declared as local variables on the runtime stack, rather than allocated
from the memory heap by calling new.
A vector is created by giving it a name, telling the compiler what type the elements are,
and optionally providing a size (that defaults to zero). The vector maintains its current size
and current capacity in the same manner as an ArrayList. If additions to the vector cause
its size to exceed capacity, than internally the capacity is expanded. Thus in the following:
vector<int> arr1( 3 );
vector<int> arr2;
arr1 is a vector that stores ints; the valid indices are currently 0, 1, 2, and an attempt to use
any other index is incorrect. arr2 is a vector that also stores ints. There are no valid indices. A vector is indexed by using [].
The size of a vector can always be obtained by invoking member function size. It can
be resized by invoking member function resize.
The add method in ArrayList, which adds to the end of the collection of objects is
renamed as push_back in the vector. Thus, the result of arr1.push_back(2) is that
arr1.size() now returns 4, and arr1[3] is now 2.
Figure 2-6 shows a vector that is used to store the first 100 perfect squares. In this code,
replacing line 10 with squares[i]=i*i does not work; the array index would be out-ofbounds. Unfortunately, unlike Java, this does not cause an exception; rather the program runs
and some part of memory gets unintentionally overwritten.
Java programmers might be tempted to create a vector of size 0 by using code such as
28
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <vector>
using namespace std;
int main( )
{
vector<int> squares;
for( int i = 0; i < 100; i++ )
squares.push_back( i * i );
for( int j = 0; j < squares.size( ); j++ )
cout << j << " squared is " << squares[ j ] << endl;
return 0;
}
Figure 2-6
vector<int> arr3( );
thinking that the default parameter would use size 0. Unfortunately this is wrong! The declaration above does not create a vector; instead, it states that arr3 is a function that takes no
parameters and returns a vector<int>. Ouch! The result would almost certainly be a bizarre
string of unintelligible error messages when arr3 was used later.
2.2.2
To use the string library type, you must have the include directive (and possibly the usual
using namespace std directive if it is not already present):
#include <string>
Because objects in Java are meant to look like primitive types, strings in C++ are also
easier to use than in Java. Most importantly, the normal equality and relational operators ==, !=,
<, <=, >, >= all work for the C++ string type. No more bugs because you forgot to use
.equals. When = is applied to a string, a copy is made; changes to the original do not
affect the copy. The length of a string can always be obtained by calling the length member function.
Java strings are immutable: once created a Java strings state cannot be changed. In contrast, the C++ string is mutable. This has two consequences. First, using the array indexing
operator [], not only can individual characters be accessed, they easily can be changed (however, as in vector, no bounds checking is performed). Second, the += operator is efficient; no
more StringBuffers! Figure 2-7 illustrates the use of string concatenation;
makeLongString takes quadratic time in Java, but is linear in C++.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
29
#include <iostream>
#include <string>
using namespace std;
// return a string that contains n As
// In Java this code takes forever
string makeLongString( int n )
{
string result = "";
for( int i = 0; i < n; i++ )
result += "A";
return result;
}
int main( )
{
string manyAs = makeLongString( 250000 );
cout << "Short string is " << makeLongString( 20 ) << endl;
cout << "Length is " << manyAs.length( ) << endl;
return 0;
}
Figure 2-7
Two member functions deserve special attention. First, the member function to get substrings is substr. However, unlike Java, the parameters in C++ represent the starting position
and length of the substring, rather than the starting position and first non-included position. Thus
in
string s = "hello";
string sub = s.substr( 2, 2 ); // gives "ll"
Arrays of Objects
In Java, there is no such thing as an array of objects. Instead, what you have is an array of references; when the array is created, by default all the references are null. In C++, when one creates an array of objects, either using a primitive array or a vector, we really get an array of
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;
// Incorrect implementation of swap2
void swap2( int val1, int val2 )
{
int tmp = val1;
val1 = val2;
val2 = tmp;
}
int main( )
{
int x = 37;
int y = 52;
swap2( x, y );
cout << x << " " << y << endl;
return 0;
}
Figure 2-8
objects. Specifically, all the items in the array are objects that have been created by calling an
appropriate zero-parameter constructor. In C++, this does not work well when the objects are
different types (for instance they are related via inheritance), but if they are the same type, e.g.
string, it works fine and no special syntax is required.
2.2.4
Occasionally, we revert to primitive arrays when we have a global constants. The reason is the
convenient array initialization, that is similar to Java and illustrated as:
const int DAYS_INT_MONTH[ ] = { 31, 28, 31, 30, 31, 30,
31, 31, 30, 31, 30, 31 };
Unlike Java, the [] follows the identifier. However, like Java, this creates an array of 12
items. The number of items can be determined by
const int NUM_MONTHS = sizeof( DAYS_IN_MONTH ) /
sizeof( DAYS_IN_MONTH[0] );
2.2.5
Multidimensional Arrays
The primitive implementation of multidimensional arrays is truly exasperating to use, and is discussed briefly in Section 11.6. There is no library type available, but it is fairly trivial to write
one ourselves, and we do so in Section 7.3.
Parameter Passing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
31
#include <iostream>
using namespace std;
// Correct implementation of swap2
void swap2( int & a, int & b )
{
int tmp = a;
a = b;
b = tmp;
}
int main( )
{
int x = 37;
int y = 52;
swap2( x, y );
cout << x << " " << y << endl;
return 0;
}
Figure 2-9
Call by Reference
Figure 2-8 illustrates a function, swap2, that attempts to swap the values of its parameters. But
if main is run, the values of x and y are not swapped, because of call-by-value. The code swaps
val1 and val2, but since these are simply copies of x and y, it is not possible to write a swap
routine that changes x and y when x and y are passed using call-by-value.
C++ allows call-by-reference. To pass a parameter using call-by-reference, we simply
place an & prior to each of the parameters that are to be passed using this mechanism. Thus,
some parameters can be passed using call-by-value, and others using call-by-reference. The
modified version of swap2, which is now correct, is shown in Figure 2-9. Observe that main is
unchanged no special syntax is used to invoke the method.
32
Because formal arguments that are passed using call-by-reference are modifiable in the
invoked function, it is illegal to pass a constant using call by reference. Thus the code
swap2(x,3) would not compile.
Actual arguments must be type-compatible with the formal arguments, without the use of
a typecast. This is required because a typecast generates a temporary variable, and the temporary
variable would become the actual argument, and then changes to the formal parameter in the
invoked function would change the temporary variable (instead of the original), leading to hardto-find bugs.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <vector>
using namespace std;
// Broken binarySearch because of call-by-value
int binarySearch( vector<int> arr, int x )
{
int low = 0, high = arr.size( ) - 1;
while( low <= high )
{
int mid = ( low + high ) / 2;
if( arr[ mid ] == x )
return mid;
else if( x < arr[ mid ] )
high = mid - 1;
else
low = mid + 1;
}
return -1; // not found
}
int main( )
{
vector<int> v;
for( int i = 0; i < 30000; i++ )
v.push_back( i * i );
for( int j = 100000; j < 105000; j++ )
if( binarySearch( v, j ) >= 0 )
cout << j << " is a perfect square" << endl;
return 0;
}
Figure 2-10
Parameter Passing
33
Finally, we mention that the parameter passing mechanism is part of the signature of the
method and must be included in function declarations.
2.3.2
Figure 2-10 illustrates a second problem of call-by-value. Here we have a binary search algorithm that returns the index of an item in a sorted vector, and a main that repeatedly calls
binary search. In theory, binary search is an efficient algorithm, requiring only a logarithmic
number of steps, so on a modern machine, 5000 binary searches in a 30,000 item array should
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <vector>
using namespace std;
// Fixed binarySearch uses call-by-constant reference
int binarySearch( const vector<int> & arr, int x )
{
int low = 0, high = arr.size( ) - 1;
while( low <= high )
{
int mid = ( low + high ) / 2;
if( arr[ mid ] == x )
return mid;
else if( x < arr[ mid ] )
high = mid - 1;
else
low = mid + 1;
}
return -1; // not found
}
int main( )
{
vector<int> v;
for( int i = 0; i < 30000; i++ )
v.push_back( i * i );
for( int j = 100000; j < 105000; j++ )
if( binarySearch( v, j ) >= 0 )
cout << j << " is a perfect square" << endl;
return 0;
}
Figure 2-11
34
execute in less than a millisecond. However, the code takes noticeable longer, using up seconds
of CPU time. Here the problem is not the binary search, but the parameter passing: because we
are using call-by-value, each call to binarySearch makes a complete copy of vector v.
Needless to say, 5,000 copies of a 30,000 element vector doesnt come cheap. This
problem never occurs in Java because all objects (non-primitive entities) are accessed using Java
reference variables, and so objects are always shared, and never copied by using =.
But in C++, a variable of type vector<int> stores the entire state of the object, and =
copies an entire object to another object. Similarly call-by-value copies the entire state of the
actual argument to the formal parameter.
Certainly one way to avoid this problem would be to use call-by-reference. Then the formal parameter is just another name for the actual argument; no copy is made. Although this
would significantly increase the speed of the program, and solve the problem, there are two serious drawbacks. First, using call-by-reference changes the semantics of the function in that the
caller no longer knows that the actual argument will be unchanged after the call. Second, as we
mentioned in Section 2.3.1, constants or actual arguments requiring typecasts would no longer
be acceptable parameters.
The solution, shown in Figure 2-11 is to augment the call-by-reference with the reserved
word const, signifying that the parameters are to be passed by reference, but that the function
promises not to make any changes to the formal parameter. Thus the actual argument is protected from being changed. If the function implementation attempts to make a change to a
const formal parameter, the compiler will complain and the function will not compile (it is
possible to cast away the const-ness and subvert this rule, but thats life with C++). We denote
this parameter passing mechanism as call-by-constant reference (even though the more verbose
call-by-reference to a constant is more accurate)
When a parameter is passed using call-by-constant reference, it is acceptable to supply a
constant or an actual argument that requires a typecast. In effect, as far as the caller is concerned,
call-by-constant reference has the same semantics as call-by-value, except that copying is
avoided.
2.3.3
In C++ choosing a parameter-passing mechanism is an easily overlooked chore of the programmer that can affect correctness and efficiency. The rules are actually relatively simple:
Call by reference is required for any object that may be altered by the function.
Call by value is appropriate for small objects that should not be altered by the function.
This generally includes primitive types and also function objects (Section 7.6.3).
Call by constant reference is appropriate for large objects that should not be altered by the
function. This generally includes library containers such as vector, general class types,
and even string.
Key Points
35
2.5 Exercises
1. Grab the most recent tax table. Write a function that takes an adjusted gross income and
filing status (married, single, etc.) and returns the amount of tax owed. Write a test program to verify that your function behaves correctly.
2. Write a function to compute X N for nonegative integers N. Assume that X 0 = 1.
3. What is the difference between a function declaration and a function definition?
4. Describe default parameters in C++.
5. What is an inline directive in C++?
6. Describe how C++ supports separate compilation.
7. Write a function that accepts a vector of strings and returns a vector containing the strings
in the vector parameter that have the longest length (in other words, if there are ten strings
that are tied for being the longest length, the return value is a vector of size ten containing
those strings). Then write a test program that reads an arbitrary number of strings, invokes
the function, and outputs the strings returned by the vector.
8. What are the basic differences between the C++ string and the Java String classes?
9. What are the different parameters passing mechainsms in C++, and when are they used?
36
H A P T E R
37
38
the runtime stack, and the local primitive variable is automatically invalidated when the block
that it was created in ends.
The C++ memory model is significantly more complicated. As we have already seen,
local variables, including objects in C++ (such as vectors and strings) can be created without calling new. In such as case, these local objects are allocated on the runtime stack and have
the same semantics as primitive types. Two important consequences of this are as follows:
First, when = is applied to objects, the state of one object is copied. This contrasts to Java,
where objects are accessed indirectly, by reference variables, and an = copies the value of the
reference variable, rather than the state of the object.
Second, when the block in which a local object is created ends, the object will automatically be reclaimed by the system.
Thus in the following code:
void silly( )
{
vector<int> arr1;
vector<int> arr2;
...
// other code not shown
arr2 = arr1;
...
// other code not shown
}
arr1 and arr2 are separate vector<int> objects. The statement arr2=arr1 copies the
entire contents of vector<int> arr1 into vector<int> arr2. When silly returns,
arr1 and arr2 will be destroyed, and their memory reclaimed, as part of the function return
sequence.
This example illustrates a signficant difference between Java and C++. In Java, objects are
shared by several reference variables and are rarely copied. C++ encourages, by default, the
copying of objects. However, copying objects can take time, and often we need to avoid this
expense. For instance, we have already seen in Section 2.3 additional syntax to allow copies to
be avoided.
In a more general setting inherited from C, C++ allows the programmer to obtain a variable that stores the memory address where the object is being kept. Such a variable is called a
pointer variable in C++. Pointer variables in C++ have many of the semantics as reference variables in Java, with extra flexibility (that implies extra dangers). C++ also has another type of
variable called the reference variable, which despite its name is not similar to the reference variable in Java. In the remainder of this chapter, we will discuss both types of variables, and several
tricky C++ issues that are associated with their use.
3.2 Pointers
A pointer variable in C++ is a variable that stores the memory address of any other entity. The
entity can be a primitive type or a class type; however, using pointer variables for primitive
types in good C++ code is rarely needed. Since it does simplify our examples, we will make use
Pointers
39
The value represented by ptr is an address. As with integer objects, this declaration does not
initialize ptr to any particular value, so using ptr before assigning to it invariably produces
bad results (such as a program crash). Suppose we also have the following declarations:
int x = 5;
int y = 7;
We can make ptr point at x by assigning to ptr the memory location where x is stored. Thus
ptr = &x;
// LEGAL
sets ptr to point at x. Figure 3-1 illustrates this in two ways. On the left a memory model shows
where each object is stored. The figure on the right uses an arrow to indicate pointing.
(&x) 1000
x=5
(&y) 1004
y=7
(&ptr) 1200
1000
ptr
Figure 3-1
x = 10
(&y) 1004
y=7
ptr
Figure 3-2
10
Pointer illustration
(&x) 1000
(&ptr) 1200
Result of *ptr=10
40
The data being pointed at is obtained by the unary dereferencing operator *. In Figure 3-1
*ptr will evaluate to 5, which is the value of the pointed-at variable x. It is illegal to dereference something that is not a pointer. The * operator is the inverse of & (for example, *&x=5 is
the same as x=5 as long as &x is legal). Dereferencing works not only for reading values from
an object but also for writing new values to the object. Thus, if we say
*ptr = 10;
// LEGAL
we have changed the value of x to 10. Figure 3-2 shows the changes that result. This shows the
problem with pointers: Unrestricted alterations are possible, and a runaway pointer can overwrite all sorts of variables unintentionally.
We could also have initialized ptr at declaration time by having it point to x:
int x = 5;
int y = 7;
int *ptr = &x;
// LEGAL
The declaration says that x is an int initialized to 5, y is an int initialized to 7, and ptr is a
pointer to an int and is initialized to point at x. Let us look at what could have gone wrong.
The following declaration sequence is incorrect:
int *ptr = &x;
int x = 5;
int y = 7;
Here we are using x before it has been declared, so the compiler will complain. Here is another
common error:
int x = 5;
int y = 7;
int *ptr = x;
In this case we are trying to have ptr point at x, but we have forgotten that a pointer holds an
address. Thus we need an address on the right side of the assignment. The compiler will complain that we have forgotten the &, but its error message may initially appear cryptic.
(&x) 1000
x=5
(&y) 1004
y=7
ptr
(&ptr) 1200
Figure 3-3
ptr = ?
Uninitialized pointer
Pointers
41
Continuing with this example, suppose that we have the correct declaration but with ptr
uninitialized:
int x = 5;
int y = 7;
int *ptr;
What is the value of ptr? As Figure 3-3 shows, the value is undefined because it was never initialized. Thus the value of *ptr is also undefined. However, using *ptr when ptr is undefined is worse because ptr could hold an address that makes absolutely no sense at all, thus
causing a program crash if it is dereferenced. Even worse, ptr could be pointing at an address
that is accessible, in which case the program will not immediately crash but will be erroneous. If
*ptr is the target of an assignment, then we would be accidentally changing some other data,
which could result in a crash at a later point. This is a tough error to detect because the cause and
symptom may be widely separated in time.
We have already seen the correct syntax for the assignment:
ptr = &x;
// LEGAL
rightly generates a compiler error. There are two ways to make the compiler shut up. One is to
take the address on the right side, as in the correct syntax. The other method is erroneous:
*ptr = x;
// Semantically incorrect
The compiler is quiet because the statement says that the int to which ptr is pointing should
get the value of x. For instance, if ptr is &y, then y is assigned the value of x. This assignment
is perfectly legal, but it does not make ptr point at x. Moreover, if ptr is uninitialized, dereferencing it is likely to cause a run-time error, as discussed above. This error is obvious from Figure 3-3. The moral is to always draw a picture at the first sign of pointer trouble.
Using *ptr=x instead of ptr=&x is a common error for two reasons. First, since it
makes the compiler quiet, programmers feel comfortable about using the incorrect semantics.
Second, it looks somewhat like the syntax used for initialization at declaration time. The difference is that the * at declaration time is not a dereferencing * but rather just an indication that the
object is a pointer type.
Some final words before we get to some substantive uses: First, sometimes we want to
state explicitly that a pointer is pointing nowhere, as opposed to an undefined location. The
NULL pointer points at a memory location that is guaranteed to be incapable of holding anything. Consequently, a NULL pointer cannot be dereferenced. The symbolic constant NULL was
prevalent in C, but is being phased out in favor of an explicit 0. But many still feel that NULL is
more readable, and so we use NULL, assuming that the following declaration exists:
const int NULL = 0;
Pointers are best initialized to the NULL pointer because in many cases they have no
default initial values (these rules apply to other predefined types as well).
42
Second, a dereferenced pointer behaves just like the object that it is pointing at. Thus, after
the following three statements, the value stored in x is 15:
x = 5;
ptr = &x;
*ptr += 10;
However, we must be cognizant of precedence rules because (as we discuss in Section 11.3) it is
possible to perform arithmetic not only on the dereferenced values but also on the (un-dereferenced) pointers themselves.1 As an example, the following two statements are very different:
*ptr += 1;
*ptr++;
In the first statement the += operator is applied to *ptr, but in the second statement the ++
operator is applied to ptr. The result of applying the ++ operator to ptr is that ptr will be
changed to point at a memory location one memory unit larger than it used to. (We discuss why
this might be useful in Section 11.3.)
Third, if ptr1 and ptr2 are pointers to the same type, then
ptr1 = ptr2;
assigns the dereferenced ptr1 the value of the dereferenced ptr2. Figure 3-4 shows that these
statements are quite different. Moreover, when the wrong form is used mistakenly, the consequences might not be obvious immediately. In the previous examples, after the assignment,
*ptr1 and *ptr2 are both 7. Similarly, the expression
ptr1 == ptr2
is true if the two pointers are pointing at the same memory location, while
*ptr1 == *ptr2
is true if the values stored at the two indicated addresses are equal. It is a common mistake to use
the wrong form.
The requirement that ptr1 and ptr2 point to the same type is a consequence of the fact
that C++ is strongly typed: We cannot mix different types of pointers without an explicit type
conversion, unless the user has provided an implicit type conversion.
If several pointers are declared in one statement, the * must precede each variable:
int *ptr1, *ptr2;
int *ptr1, ptr2;
Finally, when pointers are declared, the white space that surrounds the * is unimportant to
the compiler. Pick a style that you like.
1.
This is an unfortunate consequence of C++s very liberal rules that allow arithmetic on pointers, making use of
the fact that pointers are internally stored as integers. We discuss the reasoning for this in Section 11.3.
43
5
ptr1
5
ptr1
7
ptr2
ptr1
7
ptr2
(a)
Figure 3-4
7
ptr2
(b)
y
(c)
(a) Initial state; (b) ptr1=ptr2 starting from initial state; (c) *ptr1=*ptr2
starting from initial state
Like Java, objects can be created from the memory heap by calling new. The result of new is a
pointer to the newly created object, allocated from the memory heap, rather than the runtime
stack. new behaves the same in C++ and Java, and parameters can be provided to control initialization of the newly created object.
Figure 3-5 illustrates the issues involved in memory heap allocation, which is often called
dynamic memory allocation in C++. It must be emphasized that this example is a poor use of
dynamic memory and an automatic string should be used instead. We do it only to illustrate
dynamic memory allocation in a simple context.
In Figure 3-5, line 9 creates a new string object dynamically. Note that strPtr is a
pointer to a string, so the string itself is accessed by *strPtr, as shown on lines 10 to
14. The parentheses are needed at line 11 because of precedence rules.
3.3.2
In Java, when an object is no longer referenced, it is subject to automatic garbage collection. The
programmer does not have to worry about it. C++ does not have garbage collection. When an
object that is allocated by new is no longer referenced, the delete operator must be applied to
the object (through a pointer). Otherwise, the memory that it consumes is lost (until the program
terminates). This is known as a memory leak. Memory leaks are, unfortunately, common occur-
44
rences in many C++ programs. The delete operator is illustrated at line 16.
An example of a memory leak is shown in Figure 3-6, in which we return at line 9 without
calling delete. Fortunately, many sources of memory leaks can be automatically removed
with care. One important rule is to not use new when a stack-allocated variable can be used
instead. A stack-allocated variable is automatically cleaned up (hence it is also known as an
automatic variable in C++). Thus in this code, it would make sense to allocate the vector on the
runtime stack (and avoid the pointer) instead of using new to create it on the heap.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <string>
using namespace std;
int main( )
{
string *strPtr;
strPtr = new string( "hello" );
cout << "The string is: " << *strPtr << endl;
cout << "Its length is: " << (*strPtr).length( ) << endl;
*strPtr += " world";
cout << "Now the string is " << *strPtr << endl;
delete strPtr;
return 0;
}
Figure 3-5
1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <vector>
using namespace std;
void leak( int i )
{
vector<int> *ptrToVector = new vector<int>( i );
if( i % 2 == 1 )
return;
// some other code not shown ...
delete ptrToVector;
}
Figure 3-6
3.3.3
45
Stale pointers
One reason that programmers can get in trouble when using pointers is that it is possible, and
generally expected, that one object may have several pointers pointing at it. Consider the following code:
string *s = new string( "hello" ); // s points at new string
string *t = s;
// t points there, too
delete t;
// The object is gone
Nobody would deliberately write these three lines of code next to each other; instead,
assume that they are scattered in a complex function. Prior to the call to delete, we have one
dynamically allocated object that has two pointers pointing to it.
After the call to delete, the values of s and t (that is, where they are pointing) are
unchanged. However, as illustrated in Figure 3-7, they are now stale. A stale pointer is a pointer
whose value no longer refers to a valid object. Dereferencing s and t can lead to unpredictable
results. What makes things especially difficult is that although it is obvious that t is stale, the
fact that s is stale is much less obvious, if, as assumed, these statements are scattered in a complex function. Furthermore, it is possible that in some situations, the memory that was occupied
by the object is unchanged until a later call to new claims the memory, which can give the illusion that there is no problem.
3.3.4
Double-delete
Since s is stale, the object that it points to is no longer valid. Trouble in the form of a runtime
error is likely to result.
Thus, we see the perils of dynamic memory allocation. We must be certain to never call
delete more than once on an object, and then only after we no longer need it. But if we dont
call delete at all, we get a memory leak. And if we have a pointer variable and intend to call
delete, we must be certain that the object being pointed at was created by a call to new. When
we have functions calling functions calling other functions, it is hard to keep track of everything.
s
t
Figure 3-7
"hello"
Stale pointers: because of the call to delete t, pointers s and t are now
pointing at an object that no longer exists; a call to delete s would now be
an illegal double-deletion
46
3.3.5
If we allocate a large object as local variable and then return it, then we incur the overhead of
large copy. For this reason, it is not uncommon to see functions that return pointers instead of
objects themselves. Returning pointers is also common in the implementation of linked data
structures (in this case, we are returning pointers to nodes). However, when the return type of a
function is a pointer, one must be extremely careful to avoid stale pointers, memory leaks, and
invalid deletes.
Pointers can go stale even if no dynamic allocation is performed. Consider the code in Figure 3-8. For no good reason (except to illustrate the error), function dup returns a pointer to a
string. If dup calls new to create a string, then the caller will be responsible for calling
delete. Rather than burdening the caller, the programmer has mistakenly decided to have dup
use a stack-allocated string, and return its address. The program compiles but may or may not
work. It has an error. The problem is that the value that dup returns is a pointer. But the pointer
is pointing at ret, which no longer exists, because it is an stack-allocated variable and dup has
returned. One possible scenario, is that the first print at line 10 works, but in invoking the print
routine, the runtime stack overwrites the part of the runtime stack where the local variable ret
was being stored. If so, the second print at line 11 fails to produce the same answer, which can
be quite shocking. Another possibility is that the code works fine, and you have a latent bug.
When returning pointers, make sure that you have something to point to, and that the something
exists after the return is complete.
Suppose that it is important for dup to return a pointer to a string. How can we fix the
stale pointer problem? The easiest way would appear to be to create the new string at line 3 on
the memory heap, as shown in Figure 3-9. However, by doing so, we now force the caller to
clean up memory by calling delete when the caller is done using the return value. If the caller
doesnt do so, a memory leak has been created. In this situation, one would expect to see comments accompanying the dup method instructing callers of their obligations.
1
2
3
4
5
6
7
8
9
10
11
12
13
Figure 3-8
A stale pointer: the pointee, ret, does not exist after dup returns
1
2
3
4
5
Figure 3-9
1
2
3
4
5
6
47
Safer code, but the caller must call delete or there may be a memory leak
Figure 3-10
Using static local variable frees caller from having to call delete
A third option that is sometimes used frees the caller from reclaiming memory. Here, we
return a pointer to a variable that is not allocated from the heap, yet is not allocated on the runtime stack. Two such entities qualify: a global variable, or a static local variable. A static local
variable is essentially the same as a global variable, except that it is only visible inside the function in which it was declared. The static local variable is created once (the first time the function
is invoked), and the variable retains its values between calls to the same function. Thus like a
static class variable in Java, a static local variable could be used to keep track of the number of
times a function has been invoked. Figure 3-10 illustrates this version of dup.
When a function returns a pointer to a static local variable, the caller no longer has to manage memory. However, now the caller must use the return value and, in particular, the object
being pointed at by the return value, prior to making another call to the function. Otherwise, in
the following code fragment,
string *s1 = dup( "hello" );
string *s2 = dup( "world" );
cout << *s1 << " " << *s2 << endl;
the string worldworld is printed twice, because both s1 and s2 are pointing at the same
static object (ret), which is storing the result of the last call to dup. Thus once again, it is
incumbent on the programmer to document that the return value is a pointer to a static variable,
and that the return value must be quickly used.
You should never use delete on an object that was not created by new; if you do, runtime havoc is likely to result. For instance, an attempt to call delete on s1 or s2 in the last
example may lead to a disaster on some C++ implementations. This shows the largest problem
with pointers in C++: when you receive a pointer variable, if the implementation of the function
that is sending you the pointer is hidden (which we would typically expect), unless there are
comments, you have no way of knowing if you are responsible for calling delete, or if you
48
should not call delete, and making the wrong decision results in either a memory leak or perhaps an invalid delete. Furthermore, if you have an array of pointers, and the objects being
pointed at were allocated from the memory heap, then you may need to call delete on these
objects when you are done with the array. If any object is being pointed at twice, the programmer must write extra code to avoid double-deletions.
Reference variables must be initialized when they are declared and cannot be changed to
reference another variable. This is because an attempted reassignment via
count = someOtherObject;
49
Using C++ reference variables instead of pointer variables translates into a notational convenience, especially because it allows parameters to be passed by reference without the excess
baggage of the & operator on the actual arguments and the * operator that tends to clutter up C
programs.
By the way, pointers can be passed by reference. This is used to allow a function to change
where a pointer, passed as a parameter, is pointing at. A pointer that is passed using call-byvalue cannot be changed to point to a new location (because the formal parameter stores only a
copy of the where value).
Because a reference variable must be initialized at the moment it is declared, it is illegal to
have an array of reference variables.
Finally, we mention that a function can return by reference (or constant reference) in order
to possibly avoid the overhead of a copy. In such a case, the expression in the return statement
must be an object whose lifetime extends past the end of the function. In other words, an object
allocated on the runtime stack should not be returned by reference. We will discuss returning by
reference in Chapter 4. Returning by reference (or constant reference) can sometimes make the
program more efficient by avoiding the overhead of a copy. But doing so is fairly tricky, and
modern optimizing C++ compilers have ways of avoid the overhead of a copy anyway. As a
result, except for a few standard places where the return by reference idiom is used, we do not
recommend it.
As we have mentioned, libraries that are written to be compatible with C will use pointer variables to achieve either call-by-reference or call-by-constant reference. The idiom is discussed in
Section 12.3. In newly written C++ code, you should not need to use pointers for this purpose.
3.6.2
Primitive arrays and primitive strings in C++ are implemented using pointer variables. This
implementation is a remnant of C, and has been made obsolete by the vector and string
library types.
For strings, the primitive variable type that you will often see is char * (or const
char *, for strings that are not to be changed). Here the pointer variable points at the first character in the string, and the characters are all stored in an array, with a special character \0 signifying the end of the string (this character would not be included in determining the string
length). If you need a primitive string (e.g. as a filename to perform I/O), use the string class
50
to construct the string, and then invoke the c_str member function from the string class
to obtain the primitive string.
Other than this, and command line-arguments (Chapter 12) you should not need to use
pointer variables for arrays and strings, but if you must interact with legacy code, see
Chapter 11.
3.6.3
If presented with an array of objects that need to be rearranged often, you may find it better to
follow the Java style and store an array of pointers to objects, rather than the array of objects.
You may be able to avoid using new, depending on the particular situation by allocating the
objects in a permanent array, and then using the addresses of those objects. If you cannot guarantee the lifetime of your objects, then you will need allocate them from the memory heap using
new.
3.6.4
Classic linked data structures, such as linked lists and binary search trees use pointer variables.
Since these data structures are typically implemented as a class, with proper design we can
lessen the chances of memory problems with reasonable care. We will discuss implementation
of linked data structures later in the text in the context of classes and class templates.
3.6.5
Inheritance
Any serious use of inheritance will require pointer variables and heap memory allocation. The
code will look much like Java code, except that the programmer will have the significant burden
of reclaiming inactive objects. This issue is unavoidable and is a major reason why many people
claim Java is an easier-to-use language for object-oriented programming than C++. We will discuss this in more detail in Chapter 6.
Exercises
51
After calling delete on an object, all pointers to that object become stale and should not
be used.
Never attempt to delete an object twice.
A static local variable is created once per program, but the variable is only visible from
inside the function in which it is declared. Each invocation of the function reuses the same
variable, and its value is retained between function invocations.
The -> operator is used to access members of a pointed at class type.
A reference variable in C++ is a pointer constant that is always dereferenced implicitly.
One can view it is a synonym for another name of an object. Reference variables must be
initialized when they are declared and cannot be changed to reference other variables.
Arrays of reference variables are illegal.
Using pointer variables to simulate call-by-reference, or to implement arrays or strings
should be avoided if possible.
Pointer variables will be used to avoid large data moves, implementing linked data structures, and especially in inheritance.
3.8 Exercises
1. How is the C++ memory model different from the Java memory model?
2. What is a pointer variable?
3. What is a stale pointer?
4. What is a memory leak?
5. Which objects have their memory automatically reclaimed?
6. What does the delete operator do?
7. What happens if delete is invoked twice on the same object?
8. What happens if delete is invoked on an object that is not heap-allocated?
9. If a function returns a pointer to an object, why cant the object be a stack-based local variable?
10. If a function returns a pointer to static data, what must the user be sure to do?
11. If a function returns a pointer to heap-allocated data, what must the user be sure to do?
12. What does the -> operator do?
13. What is a reference variable in C++?
14. Why must C++ reference variables be initialized when declared?
15. Consider
int a, b;
int *ptr;
int *ptrptr;
ptr = &a;
ptrptr = &ptr;
52
a. Is this legal?
b. What are the values of *ptr and **ptrptr?
c. Is ptrptr=ptr legal?
16. Is *&x always equal to x? If not, give an example.
17. Is &*x always equal to x? If not, give an example.
18. For the declaration
int a = 5;
int *ptr = &a;
H A P T E R
Object-Based
Programming: Classes
Figure 4-1
53
54
class IntCell
{
public:
IntCell( int initialValue = 0 )
{ storedValue = initialValue; }
int getValue( )
{ return storedValue; }
void setValue( int val )
{ storedValue = val; }
private:
int storedValue;
};
Figure 4-2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Initial version of C++ class that stores an int value (needs more work)
int main( )
{
IntCell m1;
IntCell m2 = 37;
IntCell m3( 55 );
cout << m1.getValue( ) << " " << m2.getValue( )
<< " " << m3.getValue( ) << endl;
m1 = m2;
m2.setValue( 40 );
cout << m1.getValue( ) << " " << m2.getValue( ) << endl;
return 0
}
Figure 4-3
1
2
3
4
5
6
7
8
55
Figure 4-4
Routine to return true if a vector of IntCells contains at least one zero. Not
compatible with original version of IntCell
Two other difference are immediate. First, instead of supplying a visibility modifier for
each member, we simply provide visibility modifiers for sections of the class. Thus in Figure 42, the constructors and methods are public, while the data is private. There is no package visible
specifier in C++. In a C++ class, visibility is private until a public modifier is seen.
Second, in Java, constructors can invoke each other using a call to this. In C++, this is
not allowed. Instead, there are two typical alternatives. The first alternative, shown in Figure 42, is to use default parameters in the constructor. Thus an IntCell can be constructed with
either an int or no parameters. The default parameter of 0 signals that if no parameter is provided, the parameter defaults to 0. Another alternative that works if the constructors are too different to be expressed with default parameters is to declare a private member function that can
be invoked by all the constructors. (Although the initialization routine can be a public member
function, like Java, it is best to avoid doing so, because of considerations that come into play
with inheritance). Default parameters must be compile-time constants.
Figure 4-3 illustrates the use of the IntCell class. The class declaration of IntCell
must be placed prior to using the IntCell type name. Typically, one would place it in a .h
file, and the main program would use an include directive. Observe that in main we create
IntCell objects on the runtime stack, using both no parameters and one parameter constructors. Also observe that if the constructor accepts parameters, they can be placed in parentheses
(if there are two or more parameters, they must be placed in parentheses). Line 9 shows that
objects can be copied. As a result of this statement, the contents of the IntCell object m2 are
copied into IntCell object m1. This would be the equivalent of cloning in Java. Thus the second output statement prints 37 and then 40.
56
to one of the array elements, and thus the array. Though the compiler could look at the implementation of getValue and see that no changes are made to the IntCell, typically this
implementation might not be available in C++ form (in the most general case, it may be invoking other member functions that are already compiled into a library). Thus we need some syntax
to tell the compiler that getValue is not going to change the state of the IntCell.
A member function that looks at an object but promises not to changed the state of the
object is known as an accessor. A member function that might change the state of the object is
known as a mutator. In the IntCell class, getValue is logically an accessor, and
setValue is logically a mutator.
In C++, a member function is assumed to be a mutator unless it is explicitly marked as an
accessor. To mark a member function as an accessor, we place a const at the end of its signature as shown:
int getValue( ) const
{ return storedValue; }
1
2
3
4
5
6
7
8
9
57
int main( )
{
IntCell m;
m = 3;
cout << m.getValue( ) << endl;
return 0;
}
Figure 4-5
Example of implicit type conversion, which may or may not be desirable depending on context.
However this code compiles. The reason for this is that C++s type compatibility rules are
somewhat lenient in places. The thinking of the compiler is that since m is an IntCell, it
should (by default) expect to see an IntCell on the right-hand side of the assignment operator.
What it sees is not an IntCell. However, the compiler is willing to do a type conversion. So
the question it must answer is whether it can fabricate a temporary variable of type IntCell in
place of the 3. That requires constructing the temporary, and since there is an IntCell constructor that takes an int, the compiler will use that constructor to fabricate the temporary and
then copy that value into m. Whether this is a good idea or not depends on the application. For
instance, in a class RationalNumber, if there is a constructor that takes a single int, then a
statement such as r=0, where r is of type RationalNumber would be convenient, and would
in fact compile.
On the other hand, since the vector type has a one parameter constructor, this would
open the door for allowing:
vector<double> arr(20);
...
arr = 5;
which would create a temporary vector of size 5, copy its contents into arr, and then free up the
temporary, instead of doing the sensible thing and reporting an error.
Faced with this dilemma, C++ adopts the following rule: A one-parameter constructor
automatically implies the ability to perform a type conversion. Further, by default the compiler
will apply the type conversion if needed, even if not requested, to satisfy assignments and
parameter matches (but not call-by-reference, which requires exact matches). This is known as
an implicit type conversion. The reason for this is that C++ is trying to treat objects in the same
fashion as it treats primitives. To disallow implicit type conversions, the programmer can mark
the constructor as explicit.
The explicit directive is meaningless for constructors that do not take one parameter. If a
constructor is marked as explicit, then it will not be considered in object creations in which
the initialization uses =; instead you must explicitly place the one parameter in parentheses.
58
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class IntCell
{
public:
explicit IntCell( int initialValue = 0 )
{ storedValue = initialValue; }
int getValue( ) const
{ return storedValue; }
void setValue( int val )
{ storedValue = val; }
private:
int storedValue;
};
Figure 4-6
Second version of C++ class that stores an int value (still needs more work)
What this means is that if IntCell is marked explicit, then in Figure 4-3, the declaration
of m1 and m3 are acceptable, but the declaration of m2 is in error.
As a general rule, one parameter constructors should be marked explicit unless it
makes sense to allow implicit type conversions. Thus in the vector type, the single parameter
constructor has been marked explicit. In the string type, in which a const char *
(primitive string constant) can be passed as a parameter, the constructor is not marked as
explicit to facilitate the mixing of const char * and string library types.
The revised version of IntCell, with the changes made to signify that getValue is an
accessor and the constructor is marked explicit is shown in Figure 4-6.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
59
Figure 4-7
When a class includes data members that are not primitive entities, a whole new set of
considerations are introduced. For example, the code in Figure 4-7 shows a Java class with three
name
"Jane"
birthDate
Jan 1, 1984
gpa
Figure 4-8
3.9
Memory layout in Java, and also in C++ when data members include pointers
60
name
"Jane"
birthDate
Jan 1, 1984
gpa
3.9
Figure 4-9
Memory layout in C++, when objects are declared as non-pointer data members
private data members, the syntactically similar C++ class (which has different semantics), and a
second C++ class whose semantics more closely mirror the Java class.
First, Figure 4-8 shows the layout in the Virtual Machine for Java objects of type
Student. Since name and birthDate are objects, they are accessed by reference variables.
The intent of the picture is that the data fields that are stored as part of a Student instance are
simply pointer variables.
Figure 4-9 shows the layout in C++ for the first declaration of type Student. The declaration is cosmetically identical to the Java declaration, but as is evident from Figure 4-9, complete instances of the data members are stored as part of the Student entity. We can expect that
a copy of Student objects copies all the data members, and this looks good in C++, except of
course, that the copy can be expensive if the data members are large. Recall also that by default
parameter passing and returning makes copies, which is why it can be important to pass objects
using call-by-constant reference instead of call-by-value.
Figure 4-9 shows one new issue that C++ must deal with. In Java, in the constructor, each
of the reference data members is initialized to null, and then assigned to reference an object of
the appropriate type. In C++, the first step of initializing members to null does not work. The
alternative, initializing each member with a default constructor might work, but there are limitations. For instance, there might not be a zero-parameter constructor for type Date. Thus, we
need some syntax to specify how each of the data members is initialized. We discuss this in
Section 4.5.
Although the first piece of C++ in Figure 4-7 looks similar to Java code, we have seen that
it is quite different semantically. Further, often it is the case that the data members are pointer
variables; as we mentioned in Section 3.6, this may be needed in linked data structures and programs that involve inheritance. The last piece of code in Figure 4-7 shows the data members
declared as pointer variables. With this declaration, the memory layout mirrors the Java layout.
However, this layout creates numerous subtleties.
First, a copy is no longer a real copy; when data members are copied, the result is that the
birthDate and name are shared amongst two instances of Student. This is known as a
Initializer Lists
61
shallow copy. Typically with C++, we expect a deep copy. Certainly the visible semantics of
C++ should not depend on the implementation details, and the default visible semantics that we
would expect to see should be a deep copy. Thus when data members are pointer variables, if we
expect deep copy semantics, we must redefine the assignment operation to ensure correct behavior. We discuss this in Section 4.6.
A related problem concerns how objects of type Student are returned. By default, a
copy is made, but the copy creates a new temporary object. This is not an assignment operator,
but instead a different kind of constructor, known as the copy constructor. We discuss this in
Section 4.6. The default can be time-consuming, so in some cases it is worth trying to avoid it by
returning using constant reference. We discuss this in Section 4.10.
Another related problem is that in the first implementation, when objects of type
Student are created and destroyed, all of its constituent components, which are part of
Student are automatically destroyed too. In the second implementation this is no longer true
by default, because name and birthDate are simply pointer variables. The objects they are
pointing at are not reclaimed unless delete is applied to them; and as these are private variables it is difficult for the user to do so. Instead we need to provide a routine, called the destructor, that ensures that private heap objects allocated by the Student class are reclaimed when
the Student is itself reclaimed. We discuss this in Section 4.6.
Another complication deals with the fact that in Java, the language consists of both the
Java Language Specification and the Java Virtual Machine Specification. Specifically, a Java
compiler can look at Java bytecode to decide the valid methods for a class. The C++ setup places
the class declaration in a .h file, whose contents are logically copied into all source files that
provide an appropriate include directive. This can make compilation slow. In C++, we can specify the declaration of the class, which lists its member functions and memory layout, and provide an implementation separately, thus reducing the size of the .h file. We discuss this in
Sections 4.11 and 4.12.
Syntactically, the initializer list is part of the constructor and appears prior to the opening
brace that signifies the body of the constructor. Although the behavior appears the same, the
revised constructor shown above is different from the original.
In the new form, in which an initializer list is used, the storedValue data member is
immediately initialized to using initialValue when the memory for it is set aside in the
62
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Figure 4-10
newly constructed IntCell object. Because the body of the constructor is empty, no further
operations are performed.
The original form behaves slightly differently. When the memory for storedValue is
set aside for the newly constructed IntCell object, storedValue is initialized by using the
default initialization for int. Then in the body of the constructor, initialValue is copied
into storedValue using the copy assignment operator (for int).
The difference between these two is minor when data members are primitives. However,
when data members are not primitives, failing to use initializer lists can cause inferior performance, and may even cause code to not compile.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Figure 4-11
Initializer Lists
63
Consider, for instance, two alternatives for a Student class constructor. The first version
is shown in Figure 4-10 without an initializer list, while an improved version is shown in
Figure 4-11 with an initializer list.
Consider the initialization of the birthDate data member. In this instance, using an initializer list allows us to initialize the Date data member directly. Without an initializer list, one
must first create a default Date. Since a default Date must represent a valid Date, this Date
may well be initialized to the current Date (today); it certainly will not be some random memory values. Then in the body of the constructor, the intended Date must be copied into to the
Date data member, overwriting the initialized state. Obviously this means that we have wasted
CPU cycles in initializing the Date to todays date. Depending on how complex the Date class
itself is, and how often Student objects are constructed, this could be nontrivial, as it could
involve creation of strings to store months, and so on.
Because initialization of each class member should usually be done with its own constructor, when possible you should use explicit initializer lists. Note however, that this form is
intended for relatively simple cases only. If the initialization is not simple (e.g. if error checks
are needed, or the initialization of one data member depends on another), perhaps the body of
the constructor should be used for more complex logic.
It is important to note that the order of evaluation of the initializer list is given by the order
in which class members are listed. This is one reason why it is bad style to have the initialization
of a data member depend on another data member. If your code depends on the order of initialization, its probably dubious code, and should be avoided; if this is impossible, at least comment the fact that there is an order dependency, so a future programmer does not change the
order of the data members.
An initializer list is required in four common situations.
1. If any data member does not have a zero-parameter constructor, the data member must be
initialized in the initializer list.
2. If a superclass does not have a constructor, the subclass must use an initializer list to initialize the inherited component. (Chapter 6). One can view this as being the same as the
first situation.
3. Constant data members must be initialized in the initializer list. A constant data member
can never be altered after the data member is constructed. This means you could not apply
the copy assignment operator in the body of the class constructor to set the value of the
constant data member. An example of a constant data member could be the identification
number in a Student class. Each Student has his or own unique identification number, but presumably the identification number never changes. This is similar to final data
members in Java, except that in Java, it is the reference variable that is final, not the
object. In C++ it is the data member.
4. A data member that is a reference variable (for instance an ostream &) must be initialized in the constructor.
64
Destructor
The destructor is called whenever an object goes out of scope or is subjected to a delete. Typically, the only responsibility of the destructor is to free up any resources that were allocated
during the use of the object. This includes calling delete for any corresponding news, closing
any files that were opened, and so on. The default applies the destructor on each data member.
4.6.2
Copy Constructor
There is a special constructor that is required to construct a new object, initialized to a copy of
the same type of object. This is the copy constructor. For any object, such as an IntCell
object, a copy constructor is called in the following instances:
a declaration with initialization, such as
IntCell copy = original;
IntCell copy( original );
but not
copy = original; // Assignment operator, discussed later
an object passed using call-by-value (instead of by & or const &), which, as mentioned
in Section 2.3, should usually not be done anyway for large objects.
an object returned by value (instead of by & or const &)
The first case is the simplest to understand because the constructed objects were explicitly
requested. The second and third cases construct temporary objects that are never seen by the
user. In both cases we are constructing new objects as copies of existing objects, so certainly the
copy constructor is applicable.
By default the copy constructor is implemented by applying copy constructors to each
data member in turn. For data members that are primitive types (for instance, int, double, or
pointers), simple assignment is done. This would be the case for the storedValue data member in our IntCell class. For data members that are themselves class objects, the copy constructor for each data members class is applied to that data member.
4.6.3
operator=
The copy assignment operator, operator=, is called when = is applied to two already-constructed objects. lhs=rhs is intended to copy the state of rhs into lhs. By default
operator= is implemented by applying operator= to each data member in turn.
4.6.4
65
If we examine the IntCell class, we see that the defaults are perfectly acceptable, and so we
do not have to do anything. This is often the case. If a class consists of data members that are
exclusively primitive types and objects for which the defaults make sense, the class defaults will
usually make sense. Thus a class whose data members are int, double, vector<int>,
string, and even vector<string> can accept the defaults.
The main problem occurs in a class that contains a data member that is a pointer. Suppose
the class contains a single data member that is a pointer. This pointer points at a heap-allocated
object. The default destructor for pointers does nothing (recall that we must delete ourselves),
so there may be a memory leak if the default is used. Furthermore, the copy constructor and
operator= both copy not the objects being pointed at, but simply the value of the pointer.
Thus we will simply have two class instances that contain pointers that point to the same object.
As discussed in Section 4.4, this is a so-called shallow copy. Typically, we would expect a deep
copy, in which a clone of the entire object is made. Thus, when a class contains pointers as data
members, and deep semantics are important, we typically must implement the destructor,
operator=, and copy constructor (in other words, the Big-Three) ourselves.
For IntCell, the signatures of these operations are:
~IntCell( );
// destructor
IntCell( const IntCell & rhs );
// copy constructor
const IntCell & operator=( const IntCell & rhs );
Although the defaults for IntCell are acceptable, we can write the implementations
anyway, as shown in Figure 4-12. For the destructor, after the body is executed, the destructors
are called for the data members. So the default is an empty body. For the copy constructor, the
default is an initializer list of copy constructors, followed by execution of the body.
operator= is the most interesting. Line 18 is an alias test, to make sure we are not copying to ourselves. Assuming we are not, we apply operator= to each data member (at line 19).
We then return a reference to the current object, at line 20, so assignments can be chained, as in
a=b=c. (The return is actually a constant reference so that the nonsensical (a=b)=c is disallowed by the compiler; however, it seems that returning a reference instead of a constant reference is more common, and is the default).
Let us look at the uses of the keyword this in more detail. In C++, the pointer this is
defined to point at the current object in exactly the same way as this references the current
object in Java. Consequently, *this is the current object, and returning *this achieves the
desired result. As in Java, under no circumstances will the compiler knowingly allow you to
modify this. As we see, the return at line 20 uses *this. The other use of this is at line 18.
The expression a=a is logically a non-operation (no-op). In some cases, although not
here, failing to treat this as a special case can result in the destruction of a. As an example, consider a program that copies one file to another. A normal algorithm begins by truncating the target file to zero length. If no check is performed to make sure the source and target file are indeed
66
different, then the source file will be truncated, hardly a desirable feature. When performing
copies, the first thing we should do is check for this special case, known as aliasing.
In the routines that we write, if the defaults make sense, we will always accept them. However, if the defaults do not make sense, we will need to implement the destructor, and
operator=, and the copy constructor. When the default does not work, the copy constructor
can generally be implemented by mimicking normal construction and then calling operator=.
Another often-used option is to give a reasonable working implementation of the copy constructor, but then place it in the private section, to disallow call-by-value.
4.6.5
The most common situation in which the defaults do not work occurs when a data member is a
pointer type, and the pointee is heap-allocated by some object member function (such as the
constructor). As an example, suppose we implement the IntCell by dynamically allocating an
int, as shown in Figure 4-13.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class IntCell
{
public:
~IntCell( )
{
// Does nothing since IntCell contains only an int data
// member. If IntCell contained any class objects their
// destructors would be called.
}
IntCell( const IntCell & rhs )
: storedValue( rhs.storedValue )
{
}
IntCell & IntCell::operator=( const IntCell & rhs )
{
if( this != &rhs )
// Standard alias test
storedValue = rhs.storedValue;
return *this;
}
...
private:
int storedValue;
};
Figure 4-12
1
2
3
4
5
6
7
8
9
10
11
12
13
class IntCell
{
public:
explicit IntCell( int initialValue = 0 )
{ storedValue = new int( initialValue ); }
int getValue( ) const
{ return *storedValue; }
void setValue( int val )
{ *storedValue = val; }
private:
int *storedValue;
};
Figure 4-13
1
2
3
4
5
6
7
8
9
10
11
12
13
67
int f( )
{
IntCell a( 2 );
IntCell b = a;
IntCell c;
c = b;
a.setValue( 4 );
cout << a.getValue( ) << endl << b.getValue( ) << endl
<< c.getValue( ) << endl;
return 0;
}
Figure 4-14
There are now numerous problems that are exposed in Figure 4-14. First, the output is
three 4s, even though logically only a should be 4. The problem is that the default copy constructor and operator= copy the pointer storedValue. Thus a.storedValue,
b.storedValue, and c.storedValue all point at the same int value. These copies are
a.storedValue
b.storedValue
c.storedValue
Figure 4-15
After line 5 in Figure 4-14, default copy constructor generates shallow copies
68
shallow: the pointers, rather than the pointees are copied. A second less obvious problem is a
memory leak. The int initially allocated by as constructor remains allocated and needs to be
reclaimed. The int allocated by cs constructor is no longer referenced by any pointer variable.
It also needs to be reclaimed, but we no longer have a pointer to it. These problems are illustrated in Figures 4-15 and 4-16.
a.storedValue
b.storedValue
c.storedValue
Figure 4-16
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
After line 7 in Figure 4-14, default operator= generates shallow copy, and
leaks memory
class IntCell
{
public:
explicit IntCell( int initialValue = 0 )
{ storedValue = new int( initialValue ); }
IntCell( const IntCell & rhs )
{ storedValue = new int( *rhs.storedValue ); }
~IntCell( )
{ delete storedValue; }
IntCell & operator=( const IntCell & rhs )
{
if( this != &rhs )
*storedValue = *rhs.storedValue;
return *this;
}
int getValue( ) const
{ return *storedValue; }
void setValue( int val )
{ *storedValue = val; }
private:
int *storedValue;
};
Figure 4-17
69
To fix these problems, we implement the Big-Three. The result is shown in Figure 4-17.
Generally speaking, if a destructor is necessary to reclaim memory, then the defaults for copy
assignment and copy construction are not acceptable.
4.6.6
Linked data structures, such as linked lists and binary search trees provide classic examples in
which the Big-Three need to be written. Although the Standard Library provides implementations of stacks, queues, lists, sets, and maps, you may on occasion find that you need to implement your own. For instance, the linked list (and thus queue) implementations in the Standard
Library use doubly-linked lists, and a singly-linked list can be implemented faster and with less
space. In this section we illustrate a singly-linked list implementation of a queue, with minor
syntactical improvements in sections that follow. Our interest in this implementation is concerned mostly with memory management and the Big-Three.
Recall that a queue supports insertion at one end (the back), and deletion at the other end
(the front). These operations are enqueue and dequeue, respectively. As Figure 4-18 shows,
a singly-linked list in which we store a pointer to both the front and the back of the list can be
used to represent a queue. In an empty queue, the data member front is NULL. In this case, the
linked list contains list nodes that each store the data and a pointer to the next node in the list and
this is implemented in a ListNode class shown in Figure 4-19. The class uses public data
because the data members need to be accessed from the queue class. There are several alternate
solutions, including the use of nested classes, that we will discuss in Sections 4.7 4.9.
Figure 4-20 shows that to enqueue a new integer x, we create a new ListNode and
attach it after the last node in the list, in the process updating back. Figure 4-21 shows that to
dequeue the front item, we simply advance front, after saving the data in the front node so it
can be returned. In Java, when we advance front, the node that was formerly referenced by
front becomes unreferenced and eligible for garbage collection. In C++, we must clean up the
memory ourselves.
front
Figure 4-18
back
70
1
2
3
4
5
6
7
8
9
class ListNode
{
public:
int
element;
ListNode *next;
ListNode( int theElement, ListNode * n = NULL )
: element( theElement ), next( n ) { }
};
Figure 4-19
ListNode class
back
Before
...
back
After
Figure 4-20
...
71
front
Before
...
...
front
After
Figure 4-21
In fact, we can now see that several other routines require that we clean up memory: two
obvious candidates are makeEmpty and the destructor, both of which must clean up all the
nodes in the list. Furthermore, if we write a destructor, we must write operator=, and if we
copy a large queue into a small queue, it is evident that nodes will have to be reclaimed.
72
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class UnderflowException { };
class IntQueue
{
private:
ListNode *front;
ListNode *back;
public:
IntQueue( ) : front( NULL ), back( NULL )
{ }
IntQueue( const IntQueue & rhs )
: front( NULL ), back( NULL )
{ *this = rhs; }
~IntQueue( )
{ makeEmpty( ); }
const IntQueue & operator= ( const IntQueue & rhs )
{
if( this != &rhs )
{
makeEmpty( );
ListNode *rptr = rhs.front;
for( ; rptr != NULL; rptr = rptr->next )
enqueue( rptr->element );
}
return *this;
}
void makeEmpty( )
{
while( !isEmpty( ) )
dequeue( );
}
Figure 4-22
The implementation of the queue is provided in the IntQueue class that begins in
Figure 4-22. UnderflowException is simply a class that is used if getFront or
dequeue is attempted on an empty queue. We will discuss exceptions in more detail in
Chapter 8, but for now we remark that UnderflowException will behave exactly like a
Java runtime exception that is uncaught. We show the two data members at the top of the
IntQueue class declaration instead of the bottom simply so we can see them in this discussion.
Normally, the private section is placed at the end of a C++ class declaration.
73
The zero-parameter constructor, shown at lines 10 and 11 does no more than initialize
front and back to NULL; this is NOT done by default. The copy constructor at lines 13 to 15
first makes the queue empty, and then copies rhs into it using operator=. Since
operator= might try to reclaim nodes in this queue prior to the copy, it is important that this
queue be placed into a respectable state prior to invoking the copy assignment operator. The
destructor is shown at lines 17 and 18. Instead of reclaiming the memory itself, it delegates the
dirty work to makeEmpty, which presumably cleans up the memory. The copy assignment
operator operator=, is shown at lines 20 to 30. After the alias test, it empties out this queue,
and then steps through rhs, enqueueing each item it sees. The alias test is crucial here, since
otherwise, a self-assignment makes the queue empty!
makeEmpty is shown at lines 32 to 36. Recall that the destructor was relying on
makeEmpty to reclaim all the list nodes. makeEmpty is implemented simply by calling
dequeue until the queue is empty, thus delegating the dirty work of memory management yet
again.
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
Figure 4-23
74
isEmpty, getFront, and enqueue are all shown in Figure 4-23, and are relatively
straightforward, (in some cases trivial), as they are only a few lines of code each and do not
involve reclaiming memory. The reclaiming of memory however, can only be deferred for so
long, and finally in dequeue, shown in lines 55 to 62 we have to bite the bullet. As the code
shows, a call at line 57 to getFront gives us the frontItem that can be returned at line 61
(or throws an UnderflowException that is unhandled). Prior to advancing front at line 59,
we save a pointer to it (line 58) so we can reclaim the node (line 60) by invoking the delete
operator. Thus we have managed to funnel all memory management to three lines of code,
which is generally your best strategy, since memory management is so bug-prone.
4.6.7
Default Constructor
If no constructors (not including the special copy constructor) are provided for a class, then a
default constructor is automatically generated. The default takes no parameters. Each data member that is a class type is initialized by its zero-parameter constructor. However the primitive
members of objects allocated on the runtime stack or from the memory heap are not guaranteed
to be initialized.
4.6.8
Disabling Copying
Both operator= and the copy constructor can be disabled (independently) by placing their
declarations in the private section of the class. It is still worthwhile to provide an implementation of these methods. Similarly, as in Java, constructors can be private; as in Java, this is common in the case of factory classes (such as InetAddress) that provide static methods to
create object instances. C++ supports the same idiom. Destructors should generally not be private.
If the class contains data members that do not have the ability to copy themselves, then the
default operator= will not work.
4.7 Friends
Typically we would like to make data members (and occasionally member functions) private.
However as we saw in Section 4.6.6, it often is inconvenient to do so, because there may be one
other class (or possibly a few select classes) that needs access to implementation details. But
making the members public grants access to everyone.
Java has an intermediate visibility modifier, package visibility, in which specific members
are marked as being accessible to other classes. However, while this allows access only to
classes that happen to be in the same package (typically not a severe limitation), the access is
granted to all such classes.
In C++, there is no such visibility modifier. Instead, the class can grant waivers to others
of the normal privacy restrictions. Such a waiver would apply to the access of all of the class
private members. This waiver can granted only by the class that is willing to allow access to its
Friends
1
2
3
4
5
6
7
8
9
10
11
75
class ListNode
{
private:
int
element;
ListNode *next;
ListNode( int theElement, ListNode * n = NULL )
: element( theElement ), next( n ) { }
friend class IntQueue;
};
Figure 4-24
private members. The recipient of the waiver can be either an entire class, or a specific function.
There is no limit to the number of waivers that a class can grant.
A waiver is known as a friend declaration.
4.7.1
Classes as friends
Figure 4-24 offers an alternative to the ListNode class that was previously declared in
Figure 4-19. Notice that here the class contains private members only, and thus with no friend
declarations, is unusable (even instances cannot be created). The friend declaration at line 10
allows any of the IntQueue member functions to access the private members.
4.7.2
Methods as friends
A class can also grant access to a specific function. For instance, though it makes little sense,
friend int main( );
grants access to IntQueues enqueue member function that takes an int. The friend declaration is very specific:
friend int IntQueue::getFront( );
does not grant access to the getFront member function implemented in Figure 4-23, because
the getFront method is an accessor, and the declaration that was provided signals that the
access was granted to a mutator. The friend declaration
friend int IntQueue::getFront( ) const;
76
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class IntQueue
{
public:
...
private:
class ListNode
{
public:
int
element;
ListNode *next;
ListNode( int theElement, ListNode * n = NULL )
: element( theElement ), next( n ) { }
};
ListNode *front;
ListNode *back;
};
Figure 4-25
ing benefits that the private section offers. An alternative approach to our original problem of
granting IntCell access to ListNode internals is to use nested classes, and this is discussed
in the next section.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
77
Figure 4-26
Two versions to find the maximum string (alphabetically); only the first version
is correct
C++ allows local classes in which a class is declared inside a function. However, their utility is dubious because unlike Java, automatic local variables in the enclosing function cannot be
accessed (static local variables can be accessed, but this hardly seems like sufficient justification
to introduce the added complexity of using a local class).
C++ does not allow anonymous classes.
78
to avoid the copy of the return value. But this is tricky, and there are two important issues.
First, as Figure 4-26 shows, if you are returning a constant reference, then the expression
that is being returned must have lifetime that extends past the end of the function. Thus
findMaxWrong is incorrect because it returns a reference to maxValue, and maxValue
does not exist once this function returns. The first implementation is correct, since it is guaranteed that the array item exists when the function returns. Notice that a return expression such as
return arr[ maxIndex ] + "";
does not work. The result of the string concatenation is an unnamed temporary string whose
destructor will be called as soon as the function terminates.
Ensuring that the return expression has long lifetime is only half of the task. Consider the
following three calls to findMax.
string s1 = findMax( arr );
const string & s2 = findMax( arr );
string & s3 = findMax( arr );
// copies
// no copy
// illegal
The first call is legal, but defeats the entire purpose of returning by constant reference.
Specifically, the object s1 is a string, it is being created, its initial value is another string,
so this statement causes execution of a string copy constructor. The similar code
string s1;
s1 = findMax( arr );
is no better; it creates a default string, and then the second line causes execution of the
string copy assignment operator.
The second call, in which we initialize s2 is the correct way to avoid the copy. The declaration says that s2 is a reference variable. Thus, it is not a new string. The initialization
states that s2 references the same string object as the return value of findMax. Since
findMax returns by (constant) reference, its return value references the maximum string that is
actually contained in arr. Thus s2 references the maximum string that is actually contained
in arr.
If findMax returned by value, then this would appear to be dubious code, because s2
would be referencing an unnamed temporary, whose destructor could be called as soon as the
statement terminated. Using s2 at the next line could result in an attempt to access an already
destructed string. However, the language specification has specifically contemplated this scenario and it is guaranteed that the temporary variable will not be destroyed while the reference
variable is active. Even so, in this situation, the return by value causes a copy to create the temporary variable. Thus, to avoid the copy, we must both:
return by (const, if appropriate) reference
have the caller use a (const, if appropriate) reference variable to access the return value
The third call should not compile, because it attempts to throw away the const-ness of the
reference variable returned by findMax. Specifically, findMax was returning a reference that
1
2
3
4
5
6
7
8
9
10
11
79
class IntCell
{
public:
explicit IntCell( int initialValue = 0 );
int getValue( ) const;
void setValue( int val );
private:
int storedValue;
};
Figure 4-27
Third version of C++ class that stores an int value, now with class declaration
separate from implementation (still needs more work)
could now be used to modify the referenced object, and if this declaration were to be allowed by
the compiler, then s3 could be used to modify the referenced object, in violation of the expectation of findMax.
However, the const-ness can be cast away using the special const_cast:
string & s3 = const_cast<string &> ( findMax( arr ) );
Now an attempt to change the object that was being referenced compiles (though if the
object was actually stored in read-only-memory, the code may fail at runtime). Needless to say,
using a const_cast to circumvent const-ness is almost always inadvisable and is best
avoided.
80
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "IntCell.h"
IntCell::IntCell( int initialValue )
: storedValue( initialValue )
{
}
int IntCell::getValue( ) const
{
return storedValue;
}
void IntCell::setValue( int val )
{
storedValue = val;
}
Figure 4-28
Third version of C++ class that stores an int value; class implementation (final version)
mented, is the class implementation. The class declaration is also known as the class specification, and is sometimes known as the class interface.
Figure 4-27 shows the class declaration for IntCell, which we last saw in Figure 4-6.
The class implementation is shown in Figure 4-28. Observe that we use the :: scoping operator
to signify that we are implementing member functions, rather than plain (non-member) functions. The main program is shown in Figure 4-29.
The same syntax is used whether these member functions are public or private. The signature of the member functions in the implementation file must exactly match the signature in the
class specification, including parameter passing mechanism, return mechanism, and all uses of
const. The names of the formal parameters do not have to match, and in fact, may be omitted
in the class declaration.
It is illegal to provide an implementation of a member function that was not listed in the
class declaration. If a member function is listed in the class declaration, but is not implemented,
the program will still compile. It will link and run if the missing member function is never actually invoked. This allows the programmer to implement and debug the class in stages.
The compilation mechanism is the same as described in Section 2.1.7. The .cpp files
(main.cpp and IntCell.cpp) would be compiled as part of the project, and the .h files
would automatically be handled by the include directives.
If the implementation of a member function in IntCell.cpp changes, then only
IntCell.cpp needs to be recompiled. If the class declaration in IntCell.h changes, then
all .cpp files that have referenced the class declaration must be recompiled.
We do remark that implementing a member function inside the class declaration does have
the advantage that an aggressive compiler can perform inline optimization. Thus, often trivial
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
81
#include "IntCell.h"
#include <iostream>
using namespace std;
int main( )
{
IntCell m1;
IntCell m2 = 37;
IntCell m3( 55 );
cout << m1.getValue( ) << " " << m2.getValue( )
<< " " << m3.getValue( ) << endl;
m1 = m2;
m2.setValue( 40 );
cout << m1.getValue( ) << " " << m2.getValue( ) << endl;
return 0;
}
Figure 4-29
Example showing the use of IntCell when class is separated (no changes)
one-liners that are not likely to undergo changes in future versions are implemented in the class
declaration. Most notably, this often includes constructors and destructors.
Whereas function declarations can be repeated, and the declarations subsequent to the first
are simply ignored, a class declaration cannot be repeated within a single .cpp file. Thus this
revision does not compile.
Of course, nobody would deliberately write this kind of code. Unfortunately, in a complicated project, we often require that some of the .h files have include directives that reference
other .h files. In that case, it is possible that the effect of a series of include directives is to
include the same .h file more than once, and it may be impossible to avoid this scenario.
Figure 4-30 shows the C++ idiom that is used to avoid this problem. This idiom is placed
inside the header file, and the idea is that the compiler should be directed to read the .h file only
once per compilation of a .cpp file. At line 2, prior to reading the class declaration for
IntCell, the preprocessor defines the symbol INTCELL_H. Presumably, this is a unique symbol based on the name of the header file for which no other attempts have ever been made to
define. Thus, at line 1, if the INTCELL_H symbol is NOT defined, it is safe to read the class
82
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifndef INTCELL_H
#define INTCELL_H
class IntCell
{
public:
explicit IntCell( int initialValue = 0 );
int getValue( ) const;
void setValue( int val );
private:
int storedValue;
};
#endif
Figure 4-30
Final version of C++ class that stores an int value, with class declaration separate from implementation, includes ifndef/endif idiom
declaration. So #ifndef is a preprocessor directive and stands for not defined. The #endif
at line 15 closes the body of the #ifndef. As a matter of safe programming, this idiom, which
we refer to as the ifndef/endif idiom, should be used in all header files. Even if there is no class
declaration in the header file, using this idiom allows the compiler to avoid some parsing effort
if the file appears a second time in a chain of include directives.
Static Members
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
83
class Ticket
{
public:
Ticket( ) : id( ++ticketCount )
{ }
int getID( ) const
{ return id; }
static int getTicketCount( )
{ return ticketCount; }
private:
int id;
static int ticketCount;
Ticket( const Ticket & rhs )
{
id = ++ticketCount;
}
const Ticket & operator= ( const Ticket & rhs )
{
if( this != &rhs )
id = ++ticketCount;
return *this;
}
};
int Ticket::ticketCount = 0;
Figure 4-31
In this scenario, the ticket id is a data member that is part of each unique ticket instance,
but the ticket count is shared data, and is thus static. ticketCount is declared at line 15 as
being a static variable. Unfortunately, this does not cause the creation of the shared
ticketCount object; this must be provided separately as shown on line 30. Note that this definition cannot be placed in the header file, because if the header file is included in separate
.cpp files, ticketCount will be multiply defined. (The ifndef/endif idiom only avoids multiple reading of the file for each .cpp file, not for separate .cpp files). ticketCount is initialized when it is defined.
The constructor, shown at lines 4 and 5 increments ticketCount and uses it to initialize id. getTicketCount is a static method, since it could theoretically return 0 if invoked
prior to the first creation of a ticket. Also, observe the idiom of disabling the copy constructor
and copy assignment operator at lines 17 to 27 by placing them in the private section. Allowing
84
1
2
3
4
5
6
7
8
9
10
int main( )
{
cout << Ticket::getTicketCount( ) << " tickets" << endl;
Ticket t1, t2, t3;
cout << Ticket::getTicketCount( ) << " tickets" << endl;
cout << "t2 is " << t2.getID( ) << endl;
return 0;
}
Figure 4-32
a real copy would create duplicate tickets with identical ids, but using the implementation that
is provided is not really a copy, so in this case it seems reasonable to disable both forms of copying.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class MathUtils
{
...
private:
static vector<int> primes;
static bool forceStaticInit;
static bool staticInit( int n )
{
// Use Sieve of Erastothenis to eliminate non-primes
vector<bool> nums( n + 1, true );
for( int i = 2; i * i <= n; i++ )
for( int j = i * 2; j <= n; j += i )
nums[ j ] = false;
for( int k = 2; k <= n; k++ )
if( nums[ k ] )
primes.push_back( k );
return true;
}
};
// In implementation file
vector<int> MathUtils::primes;
bool
MathUtils::forceStaticInit = staticInit( 1000 );
Figure 4-33
Simulating the static initilizer in C++: initialize array with prime numbers less
than or equal to 1000
Anonymous Objects
1
2
3
4
5
6
7
8
9
85
class Utilities
{
public:
static const int BITS_PER_BYTES = 8;
...
};
// In Utilities.cpp
const int Utilities::BITS_PER_BYTES;
Figure 4-34
Disabling the copy constructor means, among other things, that a Ticket object may not
be passed using call-by-value, nor returned using return-by-value.
To invoke the getTicketCount member function, we once again use the :: scoping
operator, as shown on both lines 3 and 6 in Figure 4-32.
If a static data member involves a complex initialization, Java would process the initialization in a static initializer block. C++ does not have a static initializer block, but the effect can be
achieved as follows: Define a private static member function staticInit, and use a call to
this static member function to initialize the static data member. If the static data member does
not have appropriate copy semantics that would easily allow this, fabricate a second private
static data member of type bool, and have it invoke staticInit. Inside staticInit, you
can then explicitly initialize all the static data members of the class. An example of this strategy
is shown in Figure 4-33.
Because a static member function does not affect any particular instance of a class, static
member functions are never marked as accessors. Normally, static member functions would be
implemented in the .cpp file along with the non-static member functions.
If a static data member is a constant integer type (int, short, long, char, etc.) its initialization can be performed in the class declaration. However, it must still be defined in the
.cpp file, and the definition must not have any initialization. This hardly seems worth the
effort, except that initializing this way makes the static data member a constant integral expression, and qualifies it to be used as a case in a switch statement, and a few other places where
constant integral expressions are specifically required. Figure 4-34 illustrates the syntax.
86
Suppose we want to add defaults. The default string is "", the default double is 0.0,
but we need a default Date. Presumably there is an appropriate constructor, so assuming the
existence of a zero-parameter constructor, Date() represents a default date. Thus the constructor is:
Student( const string & n = "", const Date & b = Date( ),
double g = 0.0 )
: name( n ), birthDate( b ), gpa( g )
{ }
4.15 Namespaces
The C++ equivalent of packages is the namespace. To declare a namespace, we simply write
namespace namespaceName
{
...
}
where namespaceName is the name of the namespace. Inside the braces can be functions,
objects, and class declarations. A class ClassName declared in namespace namespaceName
is formally known as namespaceName::ClassName.
As with Java packages, entities inside of a namespace can be accessed without specifying
the namespace. Like packages, namespaces are open-ended, so one can have several separate
namespace declarations.
As in Java, it can be inconvenient to write the complete class name, that includes the
namespace name. A using directive is the equivalent of an import directive. The first form,
using namespaceName::ClassName;
is the equivalent of the Java wild-card import directive (that ends .*), and allows all entities in
the namespace namespaceName to be known by their shorthands. (Older compilers handle
this wild-card form better than the more specific using directive above, so C++ code tends to use
the wild-card using directive).
In Java, classes can be declared as public or package visible. No such syntax exists in
C++; all (top-level) classes in the namespace are visible outside of the namespace.
Namespaces can be nested, with the :: operator used to access the nested namespace.
Namespace names are normal C++ identifiers and do not include the dot (.) that is typical
in Java namespaces.
Classes, functions, and objects that are declared outside of any namespace are considered
to be in the global namespace. These can always be accessed with a leading ::, as in
::IntCell. This may be necessary from code that is inside another namespace that also contains a class called IntCell.
87
Classes, functions, and objects can be declared in an anonymous namespace. Such entities
are not visible outside of the compilation unit (i.e. .cpp file) in which they are declared. This
allows us to declare classes without fear of conflict.
Here, both class A and class B contain a data member that is a pointer to an object of the
other class type. The declaration of class A will generate an error, because the compiler does not
know that B is a class type. Clearly switching the order of declaration for classes A and B wont
work. The solution is an incomplete class declaration that serves solely to allow the compiler to
know the existence of the class type. In our example:
class B;
// incomplete class declaration
class A
{
...
B *data;
};
class B
{
...
A *data;
};
Note that if a class makes a more active use of another class, an incomplete class declaration may not be sufficient. For instance, if class A contained a data member of type B, rather than
simply a pointer, the compiler would still complain. In this case, the complete class declaration
of B (the implementation is not needed, but the memory layout is) would have to precede A.
Class B could not then have a data member of type A (since this would imply A and B are infinitely large). Class B could have a data member that is a pointer to A.
88
Exercises
89
A copy constructor is always called whenever a brand new object is created that is initialized to be a copy of an existing object. This includes formal parameters that are passed
using call-by-value.
A copy assignment (operator=) is always called whenever an already existing objects
state is changed to be a copy of another already existing object.
When an object goes out of scope, its destructor is invoked. A destructor is needed for any
class that allocates resources (heap memory, file handles, etc.).
If a destructor is written, then typically a copy constructor and copy assignment operator
also need to be written to provide deep copy semantics.
C++ requires that the class designer differentiate accessor member functions (const
member functions) from mutator member functions (non-const member functions). The
const-ness of a member function is part of its signature.
A one-parameter constructor implies the existence of a type-cast operator. The type-cast
operator may be used by the system implicitly, unless the constructor is declared
explicit.
Initializer lists are used by the constructor to initialize data members by invoking their
constructors. A constant data member, or data member without a zero-parameter constructor must be initialized in the initializer list.
A friend of a class is able to access private members of the class. Typically a class grants
friendship to an entire other class, but it can also grant friendship on a function-by-function basis.
C++ supports nested classes but not inner classes or anonymous classes. Although local
classes are allowed, because automatic variables in the function are not accessible, they
have less utility than their Java counterpart.
C++ allows static members. Although there is no static initializer, one can relatively easily
write a function that simulates the behavior of the static initializer. Static data members
must be declared in the class declaration and defined in the implementation file.
Member functions can return parameters by constant reference to avoid copying but it is
fairly tricky to do so.
C++ namespaces are the rough equivalent of packages. The using directive is the equivalent of the import directive.
Incomplete class declarations are used to inform the compiler of the existence of a class
and is used if two or more classes refer to each other circularly.
4.19 Exercises
1. How are public and private class members listed in C++?
2. What is the difference between the class interface and implementation in C++?
3. In Java, it is legal to declare a method with the same name as the class; the result is not a
constructor. Is this also legal C++?
90
H A P T E R
Operator Overloading
91
92
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person
{
public:
Person( int s, const string & n = "" )
: ssn( s ), name( n )
{ }
const string & getName( ) const
{ return name; }
int getSsn( ) const
{ return ssn; }
void print( ostream & out = cout ) const
{ out << "[ " << ssn << ", " << name << " ]"; }
bool equals( const Person & rhs ) const
{ return ssn == rhs.ssn; }
private:
const int ssn;
string name;
};
Figure 5-1
Person objects p1 and p2, the result of p1.equals(p2) is the same in this method as it
would be in Java.
However, primitive types are compared with ==. It would be nice if we could write
p1==p2. This is what operator overloading allows us to do. The syntax in this case is easy: we
replace the member function name equals with the name operator==, and voila, it works.
There is no reason to remove equals, we could in fact keep both names, and this is shown in
Figure 5-2.
It is clear from this syntax, that if lhs and rhs are of type Person, that lhs==rhs is
simply a shorthand for lhs.operator==(rhs) that the compiler winds up calling behind
the scenes. And in fact, this is true. Both lhs==rhs and lhs.operator==(rhs) will compile, though obviously using the latter form defeats the whole purpose of defining
operator== in the first place.
In the declaration for both equals and operator==, observe that rhs, which is the
second operand, is passed using constant reference, to signify that it is for input only. The member functions are declared as accessors, signifying that lhs, which is the first parameter, is to
not be altered.
Not only can we overload operator==, we can overload all six of the relational and
equality operators. Note that we have already overloaded operator=, the copy assignment
Overloading I/O
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
93
class Person
{
public:
Person( int s, const string & n = "" )
: ssn( s ), name( n )
{ }
const string & getName( ) const
{ return name; }
int getSsn( ) const
{ return ssn; }
void print( ostream & out = cout ) const
{ out << "[ " << ssn << ", " << name << " ]"; }
bool equals( const Person & rhs ) const
{ return ssn == rhs.ssn; }
bool operator==( const Person & rhs ) const
{ return equals( rhs ); }
private:
const int ssn;
string name;
};
Figure 5-2
operator. And if it made sense, we could overload operators such as +, +=, <<, >>, etc. Of most
interest is operator<<, which we discuss in the next section.
94
1
2
3
4
5
Figure 5-3
implementation of print at line 15. And since we have operator overloading, shouldnt we be
able to overload operator<<?
The answer to the question is yes, however if we attempt to place operator<< in the
Person class, we see a problem. If the operator signature takes one parameter, as in
operator<< ( ostream & out ) const
Needless to say, changing the ostream class is not going to be a viable solution. Instead,
C++ provides an alternative: we can define operator<< as a non-class function. In that case,
a respectable implementation is shown in Figure 5-3.
Here we see that operator<< takes two parameters: first, the ostream, passed by reference, as expected, and second, a Person, passed by constant reference, since it is not to be
changed and can be a large object that is expensive to copy. Since calls to << are typically
chained, we want operator<< to return a reference to out, so that
cout << p1 << " " << p2;
evaluates to
( cout << p1 ) << " " << p2;
and then the left-hand operand of the second << is still an ostream.
The implementation of operator<< simply invokes the print member function of
Person p. (Note that if this member function was not declared as an accessor in Figure 5-2,
then this invocation would not compile.) We then return the ostream.
This code is boilerplate: any class can provide an implementation of a print member
function and an overloaded operator<< that invokes it. If all classes do so, then we can
always print objects using operator<<. This convention in effect replaces Javas convention
of every class providing a toString method.
95
words, in lhs==rhs, when operator== is a member function *this is lhs. So if the first
operand cannot be of the class type in which the overloaded operator would be a member, as in
the case of operator<< where the first operand is ostream, then we must use a non-member function.
Overloading an operator as a non-member function has a significant disadvantage: since it
is not a member of the class, the implementation cannot access any private data, unless it is
made a friend of the class. Thus, the following code does not compile unless Person has
declared that operator<< is a friend:
ostream & operator<< ( ostream & out, const Person & p )
{
out << "[ " << p.ssn << ", " << p.name << " ]";
return out;
}
On the other hand, the implementation of member function operator== can access private data, since it is a member function of class Person:
bool operator==( const Person & rhs ) const
{ return ssn == rhs.ssn; }
In reality, the fact that non-member functions cannot access private data is rarely a significant liability because the class designer often can implement non-member functions by invoking
public member functions of the class. Furthermore, the member function implementation of
operator== has its own subtle problem. Recall that in Java, we expect equals to be symmetric: a.equals(b) and b.equals(a) should give the same result if both are not null.
The implementation of operator== given above fails this requirement. Specifically,
because the Person constructor is not declared explicit, and because a one-parameter constructor is available (a default parameter can be used for the name), the following code compiles, and returns true:
Person p1( 123, "Joe" );
cout << ( p1 == 123 ) << endl;
In trying to invoke p1==Person, the compiler finds an inexact match, but can use the
one-parameter constructor to generate a temporary Person object from the int 123. However,
cout << ( 123 == p1 ) << endl;
does not compile: when using operator overloading, if the implementation is a class member
function, the first parameter MUST be exact. Implicit type conversion are not allowed.
If instead of implementing operator== as a member function, it was implemented as a
non-member function:
bool operator== ( const Person & lhs, const Person & rhs )
{
return lhs.equals( rhs );
}
both calls to == above would compile and yield true. In the non-member definition, we now
96
have two parameters. Both parameters are input parameters only, and being potentially large are
thus passed using call-by-constant reference. Since the function is not a member function, it cannot be an accessor function. Observe that the fact that this member function cannot access private data does not limit us.
It is illegal to provide both versions of operator==, since invoking p1==p2 would be
ambiguous. In our particular situation, the most sensible approach would be to make the constructor explicit, in which case we would not need to worry about this.
However, there are many cases where implicit type conversions make sense. For instance,
if we have a BigInteger class, it would be nice to be able to write the following code:
int log2( const BigInteger & x )
{
int result = 0;
for( BigInteger b = x; b != 1; b /= 2 )
result++;
}
Certainly, we would expect that using 1!=b in the for loop would also work. (Well presume that ints are 32 bits, and that a BigInteger has less than 2,000,000,000 ( 2 31 ) bits.
Given that this would require 250 Megabytes to represent, thats probably not too unreasonable
a limit). Looking at BigInteger, to support b!=1 and 1!=b we have several choices.
Choice #1 is to provide a single non-member function:
bool operator!= ( const BigInteger & lhs, const BigInteger & rhs )
both b!=1 and 1!=b will generate a temporary BigInteger and in either case, the original
BigInteger and the temporary BigInteger will be compared.
An alternative is to provide a member function, and a non-member function. While they
cannot clash, we can still overload, yielding both:
bool operator!= ( int lhs, const BigInteger & rhs )
bool BigInteger::operator!= ( const BigInteger & rhs ) const
Of course, the second function would also be declared in the BigInteger class declaration.
With this approach, two BigIntegers are compared with operator!=; b!=1 generates a temporary and then the member version of operator!= is used, and finally, 1!=b is an
exact match for the non-member function. Although we can always provide the following
implementation for the non-member function:
bool operator!= ( int lhs, const BigInteger & rhs )
{
return rhs != lhs;
}
we might be able to do better with sufficient effort, based on the fact that lhs must be small. If
this is not the case, we are probably better of with the first choice above. If it is the case, then it
stands to reason that it is worth defining three overloaded functions:
bool operator!= ( int lhs, const BigInteger & rhs )
bool BigInteger::operator!= ( const BigInteger & rhs ) const
97
since any slick algorithm for the non-member version, could also be used to implement the
member function that takes int rhs as a parameter.
Which of these design decisions is best depends on the particular application. The
string class, for instance, provides a host of overloaded operators that handle primitive
strings, characters, and other string objects, rather than relying on the temporaries that are
created in an implicit conversion. The cost of the temporaries is not only the cost to construct
them, but also the cost to invoke their destructors, and in the case of string (and
BigInteger) this is certain to involve calls to new and delete.
98
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include "Rational.h"
#include <iostream>
using namespace std;
// Rational number test program
int main( )
{
Rational x;
Rational sum = 0;
int n = 0;
cout << "Type as many rational numbers as you want" << endl;
for( sum = 0, n = 0; cin >> x; sum += x, n++ )
cout << "Read " << x << endl;
cout << "Read " << n << " rationals" << endl;
cout << "Average is " << ( sum / n ) << endl;
return 0;
}
Figure 5-4
BigDecimal, ComplexNumber, or Rational. The code in Figure 5-4 illustrates a program that uses a rational number class, Rational. To run the program, simply type in rational
numbers, separated by whitespace. Terminate the input with either the end-of-file marker (control-Z on Windows, control-D on Unix), or simply type something that is not a Rational.
The program simply computes the average of its input numbers. If the input numbers are
typed as
3 8/6 -2/5
the program replies with 59/45. If no numbers are typed (prior to the end-of-file marker), the
program replies with indeterminate. If we examine the program, we see that the Rational
type behaves exactly as any other primitive numeric type.
Figure 5-5 shows the first half of the Rational class declaration, which contains all of
the public members. Figure 5-6 shows the second half and also includes operators that are nonmembers. The contents of these figures reside entirely in Rational.h.
Many of the operations are similar (e.g < and >), so only a representative set of operators
are implemented in the code that follows. The implementation of the class follows in a host of
separate code fragments that are all placed in Rational.cpp.
A rational number consists of a numerator and a denominator. The constructors allow,
among other things, a single int as the initializer, and this constructor is not marked
explicit, so that we can take advantage of mixing Rational and int types.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
99
#ifndef _RATIONAL_H
#define _RATIONAL_H
#include <iostream>
using namespace std;
class Rational
{
public:
// Constructors
Rational( int numerator = 0 )
: numer( numerator ), denom( 1 ) { }
Rational( int numerator, int denominator )
: numer( numerator ), denom( denominator )
{ fixSigns( ); reduce( ); }
Figure 5-5
// Assignment Ops
const Rational & operator+=(
const Rational & operator-=(
const Rational & operator/=(
const Rational & operator*=(
const
const
const
const
Rational
Rational
Rational
Rational
// Unary Operators
const Rational & operator++( );
Rational operator++( int );
const Rational & operator--( );
Rational operator--( int );
const Rational & operator+( ) const;
Rational operator-( ) const;
bool operator!( ) const;
//
//
//
//
&
&
&
&
rhs
rhs
rhs
rhs
);
);
);
);
Prefix
Postfix
Prefix
Postfix
We maintain the invariant that the denominator is never negative and that the rational
number is expressed in lowest form. Thus the Rational 9/-6 would be represented with
100
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
private:
// A rational number is represented by a numerator and
// denominator in reduced form
int numer;
// The numerator
int denom;
// The denominator
void fixSigns( );
void reduce( );
};
// Math Binary Ops
Rational operator+( const
Rational operator-( const
Rational operator/( const
Rational operator*( const
//
bool
bool
bool
bool
bool
bool
Rational
Rational
Rational
Rational
&
&
&
&
&
&
&
&
&
&
lhs,
lhs,
lhs,
lhs,
lhs,
lhs,
lhs,
lhs,
lhs,
lhs,
const
const
const
const
const
const
const
const
const
const
Rational
Rational
Rational
Rational
Rational
Rational
Rational
Rational
Rational
Rational
&
&
&
&
&
&
&
&
&
&
rhs
rhs
rhs
rhs
rhs
rhs
rhs
rhs
rhs
rhs
);
);
);
);
);
);
);
);
);
);
// I/O
ostream & operator<< ( ostream & out, const Rational & value );
istream & operator>> ( istream & in, Rational & value );
#endif
Figure 5-6
Rational declaration (private section and non-members)
numerator -3 and denominator 3. We allow the denominator to be zero, thus representing infinity, negative infinity, or (if the numerator is also zero, indeterminate). These invariants are maintained internally by applying fixSigns and reduce as appropriate. Thus, not only is the data
representation private, but so are fixSigns and reduce, as are shown in Figure 5-7.
reduce uses a greatest-common-divisor algorithm, which illustrates the most interesting facet
of Figure 5-7. Although we could make this routine a private static member function, we have
chosen to simply make it a non-member function, but to avoid conflicting with others with the
same name, we limit its scope to the .cpp file by placing it in the anonymous namespace
(Section 4.15). Also, since the behavior of the % operator is undefined if the operands are negative, we insure that they are not by having gcd switch the sign of a negative numerator prior to
calling gcdRec.
Our class defines several named member functions that are all trivial. toDouble and
toInt are used to create a double or int from the Rational. An implicit type conversion
is not supported. We could have tried to allow it by using operator overloading to implement
type conversion operators:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
101
0 && numer != 0 )
numer, denom );
d;
d;
Figure 5-7
Private member routines and local gcd to keep Rationals in normalized form
102
1
2
3
4
5
6
7
8
9
10
11
12
13
1
2
3
4
5
6
7
8
9
10
11
Figure 5-8
1
2
3
4
5
6
7
8
103
}
Figure 5-9
5.5.1
Assignment operator
Assignment Operators
The various assignment operators use concepts that are identical to the copy assignment operator
(operator=), discussed in Section 4.6. operator+= is representative and is shown in
Figure 5-9. Although it is not an issue here, it is always important to consider the possible
effects of aliasing prior to making changes to data members.
As an example, the implementation of operator/= shown below fails to work correctly
when it is invoked using r/r.
const Rational & Rational::operator/=( const Rational & rhs )
{
numer *= rhs.denom;
denom *= rhs.numer;
fixSigns( );
reduce( );
return *this;
}
To fix the problem, either a special aliasing test can be used, or temporaries can be used.
Both ideas are implemented in Figure 5-8.
It is important to note that the existence of operator+= does not imply anything about
operator+.
5.5.2
Arithmetic Operators
Arithmetic operators such as addition and subtraction can be implemented as either member
functions or outside of the class. Recall that as member functions they have access to private
implementation details. However, when implemented as a non-member function, the first oper1
2
3
4
5
6
Rational operator+( const Rational & lhs, const Rational & rhs )
{
Rational answer( lhs );
// Initialize answer with lhs
answer += rhs;
// Add the second operand
return answer;
// Return answer by copy
}
Figure 5-10
Addition operator
104
1
2
3
4
bool operator<=( const Rational & lhs, const Rational & rhs )
{
return !(lhs - rhs).isPositive( );
}
Figure 5-11
Relational operator
and does not have to be an exact match if an implicit type conversion is available. Thus we
choose to use a nonmember function for maximum flexibility.
The existence of operator+ does not imply anything about operator+=. However, it
is customary to ensure that these two operations have consistent semantics. The implementation
in Figure 5-10 shows the typical idiom that works whether or not we implement operator+ as
a member function, does not require access to any private details, and guarantees that
operator+ is implemented consistently with respect to operator+=. Because neither operand is mutable, there are no aliasing issues. Also, observe that the return type is by value,
because we are returning an object (the sum) that did not exist prior to the call and which is created as a stack-allocated local variable.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Figure 5-12
Output operator
5.5.3
105
As was the case for the binary arithmetic operators, we choose to implement the relational and
equality operators as nonmembers. Importantly, the existence of == does not imply anything
about !=, and similarly each of the relational operators must be implemented. We implement the
most complicated of the group in Figure 5-11.
5.5.4
Figure 5-12 shows the output routines using the idiom we discussed in Section 5.2. For input,
we use istream instead of ostream, >> instead of <<, and the second parameter is passed
by reference, since the point of the operator is to change the state of the Rational parameter.
The implementation is shown in Figure 5-13. Since I/O is not discussed until Chapter 9,
we will simply remark that the basic algorithm attempts to read an integer. Then it tries to read a
/. If it reads a character and it is a /, then it reads the denominator, otherwise it puts the character back on the input stream, and assumes the denominator is 1. A anonymous temporary
Rational object is created and then assigned to value at line 17, and the stream is returned at
line 18.
Observe that this implementation accesses no private information from the Rational
class. The creation of the temporary Rational at line 17 could have been avoided if this function was made a friend of the Rational class, or if a setValue member function would
have been provided. In that case, values numerator and denominator could have been set once
num and den were known.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if( !in.eof( ) )
// if read has hit EOF, eof is true
if( ch == '/' )
in >> den;
else
in.putback( ch );
value = Rational( num, den );
return in;
}
Figure 5-13
Input operator
106
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Prefix form
// Postfix form
Figure 5-14
Unary operators
5.5.5
Unary Operators
We continue with the ++ and -- operators. We will examine the auto-increment operator. As in
Java, there are two flavors: prefix (before the operand) and postfix (after the operand). Both add
1 to an object, but the result of the expression (which is meaningful if used in a larger expression) is the new value in the prefix form and the original value in the postfix form. They are
completely different in semantics and precedence. Consequently, we need to write separate routines for each form. Since they have the same name, they must have different signatures to be
distinguished. This is done in C++ by specifying an empty parameter list for the prefix form and
a single (anonymous and unused) int parameter for the postfix form. ++x calls the zeroparameter operator++; x++ calls the one-parameter operator++. This int parameter is
never used; it is present only to give a different signature.
The prefix and postfix forms shown in Figure 5-14 add 1 by increasing numer by the
value of denom. In the prefix form we can then return *this by constant reference, as done for
the assignment operators. The postfix form requires that we return the initial value of *this,
107
and thus we use a temporary. Because of the temporary, we have to return by value instead of
reference. Even if the copy constructor for the return is optimized away, the use of the temporary
suggests that, in many cases, the prefix form will be faster than the postfix form.
The three remaining unary operators have straightforward implementations, as shown in
Figure 5-14. operator! returns true if the object is zero; this is done by applying ! to the
numerator. Unary operator+ evaluates to the current object; a constant reference return can
be used here. operator- returns the negative of the current object by creating a new object
whose numerator is the negative of the current object. The return must be by copy because the
new object is a local variable. However, there is a trap lurking in operator-. If the word
Rational is omitted on line 26, then the comma operator evaluates (-numer,denom) as
denom, and then an implicit conversion gives the Rational denom/1, which is returned.
Have we had enough of the comma operator yet?
mat.length is 3 (because there are three rows), and each row is itself a one-dimensional
array. Thus, mat[0] is of type double[], and mat[0].length is 2. Similarly
mat[1].length is 3, and mat[2].length is 2.
As shown in Figure 5-15, our class will store the two-dimensional array using a vector
of vectors (at line 33). Note that in the declaration of array, white space must separate the
two > characters; otherwise the compiler will interpret the >> token as shift operator. In other
words, we must write
vector<vector< double> > array;
and not
vector<vector<double>> array;
// oops!
The constructor first constructs array as having rows entries each of type
vector<double>. Since each entry of array is constructed with the zero-parameter constructor, it follows that each entry of array is a vector<double> object of size 0. Thus we
have rows zero-length vectors of double. The body of the constructor is then entered and
each row is resized to have cols columns. Thus the constructor terminates with what appears to
be a two-dimensional array. (Note that the doubles themselves are not guaranteed any initialization)
108
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#ifndef _MATRIX_OF_DOUBLE_H
#define _MATRIX_OF_DOUBLE_H
#include <vector>
using namespace std;
class MatrixOfDouble
{
public:
MatrixOfDouble( int rows, int cols ) : array( rows )
{ setNumCols( cols ); }
int
{
int
{
numrows( ) const
return array.size( ); }
numcols( ) const
return numrows( ) > 0 ? array[ 0 ].size( ) : 0; }
Figure 5-15
Because vectors know how to clean up their own memory, we do not need to worry
about a destructor, copy constructor, or copy assignment operator.
The numrows and numcols accessors are easily implemented as shown. As we will see,
it is possible for the user to make the two dimensional array non-rectangular, in which case, the
result from numcols is meaningless.
109
We also provide member functions to change the number of rows and columns. Note that
when the number of rows is changed by resizing the array, any additional rows have length 0
(i.e. no columns).
As our Java example illustrated, the key operation is [], which in C++ can be overloaded
as operator[]. The result of mat[r] is the invocation of mat.operator[](r), and
returns a vector corresponding to row r of matrix mat. Thus we have a skeleton:
vector<double> operator [] ( int row )
{ return array[ row ]; }
The main question is whether this is an accessor or a mutator, and what the return mechanism should be. If we consider the following routine:
void copy( MatrixOfDouble & to, const MatrixOfDouble & from )
{
for( int r = 0; r < to.numrows( ); r++ )
to[ r ] = from[ r ];
}
in which we copy each row of from into the corresponding row of to, we see contradictions.
If operator[] returns a vector<double> by value, then to[r] cannot appear on
the left-hand side of the assignment. The only way to affect a change of the elements stored in
to is if operator[] returns a reference to a vector. Unfortunately, doing so would allow
from[ r ] = to[ r ];
// matrix [] mutator
// vector [] mutator
110
Here row0 is a reference to (i.e. another name for) mat.array[0] that is stored internally in mat, representing row 0. item00 is a reference to mat.array[0][1], stored internally. So changing item01 to 3.14 changes the entry in mat.array[0][1].
Notice that although we are invoking operator[] twice, we are invoking two different
versions of operator[]. Notice that both versions are mutators, but do not make any changes
on their own to the objects they are acting upon. But by returning references to the object, they
open the door for later changes.
which is not intuitive and can be hard to read. Pretty soon operators are used everywhere and
nobody can understand the code. Operator overloading should never be used in place of named
members when the operators do not provide intuitive semantics.
Exercises
111
operator+= and operator+ must be overloaded separately, and consistently. Typically operator+ simply invokes operator+=.
When operator++ is overloaded, typically both a prefix and a postfix version are provided.
When operator[] is overloaded, typically both an accessor and a mutator version are
provided.
5.9 Exercises
1. Which operators cannot be overloaded?
2. Under what circumstances can the precedence, associativity, or arity of an operator be
changed?
3. Explain the difference between overloading operator= as a member function versus a
non-member function.
4. When must an operator be overloaded as a non-member function?
5. Under what circumstances is it a dangerous idea to overload the type conversion operator?
6. Explain how this is used in the implementation of assignment operators.
7. How are operator+= and operator+ implemented to ensure compatible semantics?
8. Implement a Complex class that supports complex numbers. Provide as many operations
as you can.
9. Implement a BigInteger class that supports arbitrary precision integers, providing as
many operations as you can with reasonable efficiency. Provide two implementations: one
maintains the digits in an array, and a second maintains the digits in a linked list.
10. Implement a Polynomial class that supports single-variable polynomials. Support
operator+, operator-, and operator*, as well as the corresponding assignment
operators, operator==, operator!=, as well as an overloaded input and output operator. Add a public method, eval, that takes evaluates the polynomial at a particular point.
You should use a good separation of interface and implementation. The polynomial is
implemented as a sorted vector of pairs, in which each pair represents a coefficient and
an exponent. For instance, the polynomial x^2+4 is represented by a vector of size 2,
with index zero containing (4,0) and index one containing (1,2). If this polynomial is represented by p, then p.eval(2) returns 8. You may assume that all coefficients are of
type double and exponents are non-negative integers. A polynomial can be constructed
by passing a string. For simplicity, you may assume that there are no spaces in the
string. This makes it easy to implement the required operator<<. An example of a
call to the constructor is
Polynomial p( "x^2+8x+15" );
11. Implement a Map class that stores keys and values that are both strings. Like the Java
Map, your map will support isEmpty, size, clear, get, put, and remove. Additionally, support operator[] which takes a key as a parameter, and returns a reference
112
to the corresponding value in the map. If the key is not present, operator[] will insert
it with a default value, and return a reference to the newly inserted value. This implies that
operator[] is a mutator. Add an accessor version with similar semantics, but have it
throw an exception if the key is not found. Provide two separate implementations: one
maintains the items in an array, and a second maintains the items in a linked list. In both
cases, each key and value is stored together in a Pair, which is a nested class that you
should define.
H A P T E R
Object-Oriented
Programming: Inheritance
IKE Java, C++ supports inheritance. Most of the features associated with inheritance that are found in Java have equivalent implementations in C++.
In some cases, these features are not the default behavior, and thus the C++ programmer must be
more careful than a Java programmer. In other cases, the behavior of similar constructs is
slightly different. And additionally, C++ supports some techniques, such as multiple inheritance,
that Java does not allow.
In this chapter, we describe the basics of Java inheritance, see some of the extra code that
the C++ programmer must write to avoid subtle errors, discuss multiple inheritance in C++, and
examine the differences between similar C++ and Java constructs.
The reserved word public is required; otherwise we get private inheritance, which does
not model an IS-A relationship (see Section 6.10.2) and is typically not what we want.
113
114
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Person
{
public:
Person( int s, const string & n = "" )
: ssn( s ), name( n )
{ }
const string & getName( ) const
{ return name; }
int getSsn( ) const
{ return ssn; }
void print( ostream & out = cout ) const
{ out << ssn << ", " << name; }
private:
int ssn;
string name;
};
ostream & operator<< ( ostream & out, const Person & p )
{
p.print( out );
return out;
}
Figure 6-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Figure 6-2
Dynamic Dispatch
115
Clearly p and s are the same object. So if we invoke the print member function on
either p or s, we expect the same answer; certainly this basic polymorphic behavior is what happens in Java. However, in our code, s.print invokes Student::print on our student,
while p.print invokes Person::print on our student.
The problem is that it in the call to p.print, the compile-time type of p is Person,
while the run-time type (the type of the object p actually references when the program is run) is
Student. In C++, by default, the decision on which member function to invoke is made on the
basis of the compile-time type of p. This is known as static dispatch. (For instance methods)
Java always uses the runtime type, which is known as dynamic dispatch.
116
Dynamic dispatch is almost always the preferred course of action. However, dynamic dispatch incurs some run-time overhead because it requires that the program maintain extra information and that the compiler generate code to determine which member function to invoke. This
overhead was once thought to be significant, and would be incurred even if no inheritance was
actually used, and so C++ did not make it the default. This is unfortunate, because we now know
that the overhead of dynamic dispatch is relatively minor.
In order to achieve dynamic dispatch, the C++ programmer must mark the base class
method with the reserved word virtual. A virtual function uses dynamic dispatch. A non-virtual function uses static dispatch. Thus if we rewrite the print member function in class
Person as:
virtual void print( ostream & out = cout ) const
{ out << ssn << ", " << name; }
the code at the start of this section behaves as expected. This also is the minor change that fixes
operator<<.
As a general rule, if a method is overridden in a derived class, it should be declared virtual
in the base class to ensure that the correct method is always selected. In fact, a stronger statement applies: if a method might reasonably be expected to be overridden in a derived class, it
should be declared virtual in the base class. Once a method is marked virtual, it is virtual from
that point down in the inheritance hierarchy.
Only if a member function is intended to be invariant in an inheritance hierarchy (or if an
entire class is not intended to be extended) does it make sense to not mark it as virtual. Thus
getName and getSsn are not marked as virtual. In Java, these would be marked final to
signify that it is illegal to attempt to override. C++ does not have final methods or final classes,
so the lack of virtual in a method declaration is a signal to the reader that the method is
intended to be final, and those semantics are guaranteed if invoked through a base class reference (or pointer).
When the class declaration and implementation are separate, the virtual declaration must
be in the member function declaration and should not be in the separate member function definition.
Defaults
First, we need to decide what the defaults are. For all three, the inherited component is considered to be a data member. Thus, by default:
the copy constructor is implemented by invoking a copy constructor on the base class(es),
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
117
class Student
{
...
Student( const Student & rhs )
: Person( rhs ), gpa( rhs.gpa )
{ }
~Student( )
{
// automatically chains up
}
const Student & operator= ( const Student & rhs )
{
if( this != & rhs )
{
Person::operator= ( rhs );
gpa = rhs.gpa;
}
return *this;
}
...
};
Figure 6-3
followed by invoking copy constructors on each of the newly added data members. If any
of the required copy constructors cannot be called (because they are private), then an
attempt to invoke a default copy assignment operator will generate a compiler error.
the copy assignment operator is implemented by invoking a copy assignment operator on
the base class(es), followed by invoking copy assignment operators on each of the newly
added data members. If any of the required copy assignment operators cannot be called
(because they are private, or the data member is constant or otherwise not assignable),
then an attempt to invoke a default copy assignment operator will generate a compiler
error.
the destructor is implemented by invoking destructors on each of the newly added data
members, followed by invoking the destructor on the base class(es).
6.3.2
If any default is unacceptable, then it can be implemented explicitly. Generally, if one of the
defaults is unacceptable, all three are. The typical scenario is that the base class operations are
fine, because if the base class does not support copying, it generally makes little sense to allow
the subclasses to do so. In this scenario, the newly added data members allocate memory from
the heap without cleaning it up automatically,
118
Figure 6-3 shows an explicit implementation of the defaults for the Student class. To
implement the copy constructor, we need to make sure to include a call to the base class copy
constructor in the initializer list. To implement the destructor, we simply list the additional
actions that must be taken in addition to the default. To implement the copy assignment operator,
we need to make sure that we chain up to the base class.
In short, to implement copy semantics, we must chain up to the base class, while in the
destructor the chaining up is automatic.
6.3.3
Virtual Destructor
A much trickier aspect of the Big-Three concerns polymorphism. Consider the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Figure 6-4
119
Here, s1 points at a Student object, and certainly the second assignment is legal, since
Student IS-A Person. So when delete is invoked, whose destructor is used?
Since the destructor is a member function, the answer depends on whether or not the
destructor is declared virtual. In our original code in Figure 6-1, it is not, so we invoke the
Person destructor instead of the Student destructor. This means that any memory in the
Student data fields that was allocated from the memory heap either directly (via calling new),
or indirectly (e.g. if there is a string, or vector as a data member), is never reclaimed, and
we have a subtle memory leak.
Thus we see an important rule: In a base class, a destructor should always be declared virtual to ensure polymorphic destruction. As with all virtual methods, this costs some space and
time, so classes that are intended to be final can avoid declaring their destructors virtual. The
reason it is easy to do this incorrectly is that the base class destructor should be virtual even if
defaults are used in the entire hierarchy. Otherwise, as we just mentioned, memory that was allocated indirectly, and which otherwise would have been released will leak.
Figure 6-4 shows the Person class with correct virtual declarations. The Student class
in Figure 6-1 needs no changes from the original.
In C++, a class is abstract if it has at least one abstract method, and like Java, abstract
classes cannot be instantiated. A C++ class with no abstract methods is not abstract. This rule is
different than Java: abstract classes in Java do not have to have an abstract method (e.g.
java.awt.event.WindowAdapter). C++ escapes from this rule by allowing a virtual
method that has an implementation to be declared abstract. The sole purpose of doing so seems
to be to allow the class to be abstract.
A common place where this trick is used is the destructor, because an abstract class must
be extended, and thus must have a virtual destructor. The destructor must have an implementation (since the derived classes chain up to it), but if we mark it as abstract, then the class is now
automatically abstract, even if no other abstract methods are present.
Abstract methods in C++ are often denoted as pure virtual methods.
120
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Shape
{
public:
Shape( const string & s ) : shapeType( s ) { }
virtual ~Shape( ) { }
const string & getType( ) const
{ return shapeType; }
virtual double getArea( ) const = 0;
virtual void print( ostream & out ) const
{ out << getType( ) << " of area " << getArea( ); }
private:
string shapeType;
};
ostream & operator<< ( ostream & out, const Shape & s )
{
s.print( out );
return out;
}
class Circle : public Shape
{
public:
Circle( double r ) : Shape( "Circle" ), radius( r )
{ }
double getArea( ) const
{ return 3.14 * radius * radius; }
private:
double radius;
};
class Square : public Shape
{
// similar implementation as Circle not shown
};
Figure 6-5
Figure 6-5 shows an abstract Shape class and a Circle class that implements all the
abstract methods. A Square class would be similar and is not shown (we use all three classes
later). A Shape stores its type information as private data, so this data must be initialized in the
constructor (since the subclasses do not have access to private data). Thus like Java, abstract
Slicing
121
classes do provide constructors that are used by the subclasses. The destructor at line 5 is
marked virtual (we do not make it abstract, since we have other abstract methods).
getType, shown at lines 7 and 8 is not marked virtual, to signify that it is intended as a
final method. Although subclasses can provide different implementations, if getType is
invoked through a Shape reference or pointer, we will always invoke the Shapes getType
method, which is similar to the behavior we would get in Java.
Abstract method getArea is declared at line 10, and is overridden in the Circle class.
The print method is virtual, signalling that we expect that the default implementation we have
provided may need to be overridden. The fact that Circle and Square do not override
print in our implementation does not justify removing the virtual declaration. The declaration
is there to express that the print method is not invariant and may need to be overridden.
In the Circle class, we see that the Circle constructor invokes the Shape constructor
at line 27 to initialize its inherited components.
6.5 Slicing
In our discussion of virtual member functions, we have talked about accessing derived classes
by pointers and reference variables. So why not by direct base class objects? The reason is relatively simple: it doesnt work!
Consider the following example:
Student s( 123456789, "Jane", 4.0 );
Person p( 987654321", "Bob" );
p = s;
p.print( );
Clearly the first two lines create two objects. The first object is of type Student, and the
second is of type Person, as shown in Figure 6-6. And equally, clearly, the third statement copies s into the already existing object p. As Figure 6-6 shows, p only has room for the name and
"Bob"
"Jane"
987654321
123456789
4.0
Figure 6-6
Memory layout of base class and derived class object; shaded portions are inherited data and might not be visible
122
"Jane"
"Jane"
123456789
123456789
4.0
Figure 6-7
ssn data members, and so those are the only members that can be copied. As shown in
Figure 6-7, ss gpa cannot be copied into p because the p has no room for it.
As this example shows, if a derived class object is copied into a base class object, only the
base class portion is actually copied. The derived class object has been sliced; and this effect is
known in C++ as slicing.
Slicing occurs in all forms of object copying. Thus, if s is a Student
Person p = s;
does not work, because s is passed using call-by-value, and only a slice of s is copied into p.
Inside operator<<, the print method is thus invoked on a Person object. As a result, callby-value should never be used in conjunction with inheritance: call-by-value and inheritance
dont mix.
123
In Java, if we want to store a collection of shapes, we can simply throw them in an array of
Shape, and then safely invoke the print and area methods, with automatic dynamic dispatch. But what we actually have is not an array of Shape objects, but rather, an array of reference variables that all reference objects that are (subtypes of) Shapes.
In C++, we cannot store an array of Shape, because of slicing. As soon as we would try
to place a derived class object into the array, it would be sliced. All calls would be on Shape
objects. Of course Shape is abstract, so that is another problem, but the abstractness of Shape
is not really the critical issue. It is the slicing problem.
We can use an array of pointers to Shapes, and then the code looks exactly like Java. In
fact if:
1. we declare every method as virtual
2. only allocate objects using new
3. access all objects by pointers
4. use collections of pointers
we can for the most part do everything that we do in Java, with one important exception: Java
does garbage collection, while in C++ we must eventually call delete. And thats the hard
part.
Using inheritance in C++ implies that we must make significant use of pointers, and allocate objects on the memory heap. Which means we are stuck with the thorny issue of cleaning
up memory, which is quite a nuisance in C++ and notoriously error-prone.
As an example of how we use pointers with inheritance to achieve polymorphic behavior,
the code in Figure 6-8 shows how we store Circles and Squares (and in general any kind of
Shape) in a single collection.
The main routine, shown at lines 36 to 40 simply invokes testShapes mostly so we
can make clear that any allocated memory in testShapes must be cleaned up. In
testShapes, line 25 is the C++ equivalent of creating an empty ArrayList, and then lines
27 to 29 is the equivalent of calling add. We do not have any slicing problems since the
vector is storing pointers, so we are never copying Shape objects themselves. We can then
pass the vector to methods such as printArray and totalArea, which will scan through
the vector, and invoke the appropriate print and area methods on the Shapes being
pointed at.
In testShapes, arr is a local variable allocated on the runtime stack. This means that
when testShapes returns, arrs destructor will be called. This will free the vector, but
not the Shapes that were being pointed at. Since testShapes created objects from the memory heap, it must reclaim them, and this is the job of cleanup.
cleanup simply steps through the array invoking delete. Observe that this would not
work if the Shape class did not correctly declare a virtual destructor. printShapes and
totalArea are similar, and simply illustrate different syntax. Since operator<< accepts
any Shape, and *a[i] is an object that is a (subtype of) Shape we can pass it to
124
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Figure 6-8
operator<< at line 4. The getArea method can be invoked using the normal -> operator,
as shown at line 12. The code here is not much different than the equivalent Java code, except
that we must write cleanup.
Type Conversions
125
then dynamic_cast returns NULL if the runtime type of p is not compatible with s.
dynamic_cast can also be used to cast references:
Student jane( "123456789", "Jane", 4.0 );
Person & pref = jane;
Student & sref = dynamic_cast<Student &>( pref );
If the cast fails because the object is of the wrong type, or if the cast is not applied to a
pointer or a reference, then a bad_cast exception is thrown.
The dynamic_cast only works with types that are polymorphic, meaning that the class
type must have at least one virtual method.
Casting in C++ is much less common than in Java, because most uses of Java casting
involves casting down from Object in generic collections (e.g. the java.util classes). As
we will see in Chapter 7, C++ uses a different mechanism, known as templates, to support
generic algorithms, removing almost all uses of casting.
C++ does not provide an instanceof operator. With pointer variables and the dynamic
cast, the instanceof operator can be simulating by invoking a dynamic cast and verifying
that the return value is not NULL. However, using instanceof in Java is relatively rare in
well-designed object-oriented programs, and the idiom in C++ is even more rare.
126
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Printable
{
public:
virtual ~Printable( ) { }
virtual void print( ostream & out = cout ) const = 0;
};
class Serializable
{
public:
virtual ~Serializable( ) { }
virtual void readMe( istream & in = cin ) = 0;
virtual void writeMe( ostream & out = cout ) const = 0;
};
Figure 6-9
Figure 6-10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
127
class Person
{
public:
Person( const string & n, const string & t )
: name( n ), ptype( t ) { }
virtual ~Person( )
{ }
private:
string name;
string ptype;
};
class Student : public Person
{
public:
Student( const string & n )
: Person( "Student", n ) { ... }
int getHours( ) const;
// number of credit hours taken
private:
int hours;
};
class Employee : public Person
{
public:
Employee( const string & n )
: Person( "Employee", n ) { ... }
int getHours( ) const;
// number of vacation hours left
private:
int hours;
};
class StudentEmployee : public Student, public Employee
{
public:
// Constructor sets type to StudentEmployee???
// ??? getHours ???
private:
// ??? name ???
// ??? hours ???
};
Figure 6-11
For instance, consider the scenario in Figure 6-11. The Person class has subclasses
128
Student and Employee, and then StudentEmployee extends both Student and
Employee, thus having the functionality of both.
But now consider the problems that have to be resolved in StudentEmployee.
Since getHours is defined in both classes, which is inherited? Since there is an ambiguity, either both are inherited, but then invoker must make plain which of the methods is to be
used, or both are overridden in the StudentEmployee class. The first possibility leads to
ugly code; in the second, StudentEmployee will not be computing the same information as
both Student and Employee, violating at least one IS-A relationship. Most likely, we would
want to just change the method name to avoid this conflict.
More tricky is the memory layout. Since Student and Employee both define an
hours data member, it seems that StudentEmployee needs two copies of hours. This is
certainly true, as seen if we change the name of one of the getHours, but leave the data member in tact. A basic tenet of inheritance is that the subclasses inherit the superclass data.
But now what about name? Both Student and Employee have a name data member,
so according to our logic, there should be two copies. But thats no good, since Student and
Employee inherited name from Person. By default however, we get two copies. To get only
one copy, we must use virtual inheritance. When Person is extended, the subclasses use
virtual to signal that any inherited data from Person is stored in a different manner. When
multiple such subclasses are themselves extended, the compiler will be able to distinguish
between data members like hours that were created in the subclasses and data members like
name that are really all part of a single ancestor class.
If the ancestor class supplies some of the data, it makes sense that its constructor should be
invocable. Thus with virtual inheritance, not only can a superclass constructor be invoked, but
also the constructor of an ancestor class that is virtually extended can be invoked in the initializer list. All other initializations of the ancestors data by other initializers in the initializer list
are ignored.
The result of all these changes, with an illustration of the syntax is shown in Figure 6-12.
Observe the deficiency of this approach: although the conflict is at StudentEmployee, it is
the Student and Employee classes that are responsible for using virtual inheritance. Thus if
the base classes have not already declared their use of virtual inheritance, it is difficult to inherit
from both of them without having to disturb existing code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Person
{
public:
Person( const string & t, const string & n )
: ptype( t ), name( n ) { }
virtual ~Person( )
{ }
private:
string ptype;
string name;
};
class Student : virtual public Person
{
public:
Student( const string & n, int h )
: Person( "Student", n ), hours( h ) { ... }
int getCreditHours( ) const;
// credit hours taken
private:
int hours;
};
class Employee : virtual public Person
{
public:
Employee( const string & n, int h )
: Person( "Employee", n ), hours( h ) { ... }
int getVacationHours( ) const;
// vacation hours left
private:
int hours;
};
class StudentEmployee : public Student, public Employee
{
public:
StudentEmployee( const string & n, int ch, int vh )
: Person( "StudentEmployee", n ),
Student( "ignored", ch ), Employee( "ignored", vh )
{ }
private:
// one name, two hours
};
Figure 6-12
129
130
1
2
3
4
5
6
7
8
9
10
11
12
Figure 6-13
Friendship in C++ is not inherited. If a class or method is a friend of class Base, and class
Derived extends Base, then the class or method can only access the portions of Derived
that were inherited from Base. It cannot access the private details of Derived.
Technical Differences
131
ble with IntCell and would still have getValue and setValue. With private inheritance,
getValue and setValue are only visible inside of NewCell, though they can be used in
the implementation of get and put, but not outside of the NewCell class.
6.10.3 Reducing Visibility When Overriding
C++ allows you to reduce the visibility of a method. If a method is public in a base class and private in a derived class, then it is visible if invoked through a base class reference and invisible
otherwise. This is not a pretty picture because it violates the contract of an IS-A relationship.
Specifically, what would happen if a public virtual function in a base class is overridden by a
private function, and then the function is invoked through a base class pointer that is actually
pointing at a derived class object? In C++ the method is invoked using dynamic dispatch and so
the private method is used. This seems like bad semantics.
6.10.4 New Methods Hide Originals in Base Classes
The overloading algorithms in C++ and Java are different. Consider the code in Figure 6-14. In
the code, we have a base class and a derived class. The base class declares member function
foo with no parameters, and the derived class declares member function foo with an int
parameter.
In Java, only the zero-parameter foo is available when foo is invoked by a Base reference. The same is true in C++. However, in Java, both versions of foo are available when foo
is invoked by a derived class reference. However, in C+ this is not true. Instead, when a method
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base
{
public:
virtual void foo( );
};
class Derived : public Base
{
public:
void foo( int x );
};
void test( Base & arg1, Derived & arg2 )
{
arg1.foo( );
// OK
arg1.foo( 4 );
// Illegal, as expected
arg2.foo( 4 );
// Legal, as expected
arg2.foo( );
// Illegal; not like Java
}
Figure 6-14
Derived class methods hide base class methods with same name
132
is declared in a derived class, it hides all methods of the same name in the base class. Thus foo
with no parameters is no longer accessible through a derived class reference, even though it
would be accessible through a base class reference.
There are two ways around this problem. The first is to override all of the hidden methods
and redefine them in the derived class with an implementation that chains to the base class. Thus
in class Derived, we add
void foo( ) { Base::foo( ); }
The other alternative is to provide a using declaration in the derived class. In class
Derived, we add
using Base::bar;
One reason why this rule is important is that an accessor hides a mutator. Most likely this
was unintentional, but many compilers will warn you, and what they are saying, in effect, is that
you made a slight change to the signature when you overrode the original member function. Pay
attention to those warnings.
6.10.5 Overriding With Different Return Type
C++ allows the return type of an overriding method to be slightly changed. Specifically, if the
original return type is a pointer (or reference) to a class type B, the new return type may be a
pointer (or reference) to class D, provided D is a publicly derived class of B. This condition simply states that you can change the return type to be a subclass of the original return type, as long
as an IS-A relationship holds. This is useful for overriding methods such as
virtual const Base & Base::operator++( );
with
virtual const Derived & Derived::operator++( );
// Legal Java
This type compatibility doesnt make much sense, when one sees that
arr[ 0 ] = new Basketball( );
// Compiles in Java
must be allowed to compile, since at compile time arr[0] is of type Object and
BasketBall is an Object. Of course at runtime this throws an exception, but the whole
point of the typing system is to try to avoid runtime problems.
In C++, the equivalent scenario would be an array of pointers, and C++ does not allow an
array of subclass pointers to be type-compatible with an array of superclass pointers.
Key Points
133
6.10.7 Reflection
C++ has little support for reflection. About all you can do is invoke the typeid method by
passing an expression. It returns an object of type type_info representing the runtime type of
the expression. Standard C++ guarantees that this object contains a public data member called
name, and that you can compare type_info objects with == and !=. Thus,
Person *p = new Student( 123456789, "Jane", 4.0 );
Student *s = p;
cout << typeid( *s ).name << endl;
// prints name
cout << ( typeid( *s ) == typeid( Person ) ) << endl; // false (0)
134
C++ does not have interfaces, but the effect can be achieved with an abstract base class
that contains only abstract methods and a virtual destructor.
C++ allows multiple inheritance. If two implementations conflict, both of those implementations should have been declared using virtual inheritance to avoid replication of data
members.
Protected members are visible in derived class implementations, regardless of how they
are accessed.
Friendship is not inherited.
There is no root class that is equivalent to Object. Instead templates are used to implement generic algorithms.
Although it is bad style, C++ allows the reduction of visibility of a method in a derived
class.
Methods declared in the derived class hide the base class methods with the same name.
The return type of a derived class method can be changed from the base class method if it
is replaced with a subclass of the original return type.
An array of a derived type is not type compatible with an array of the base type.
C++ does not support reflection.
6.12 Exercises
1. What is the C++ equivalent of the extends clause?
2. What does virtual do?
3. What is slicing?
4. Why is it bad to declare a base class parameter using call-by-value?
5. How is the superclass constructor invoked in C++?
6. What are the defaults for the Big-Three in a derived class?
7. Why is the default base class destructor unacceptable?
8. How are abstract classes declared in C++?
9. What is a pure virtual function?
10. Why is inheritance harder to do in C++ than in Java?
11. How are heterogeneous collections stored in C++?
12. What are the semantics of dynamic_cast?
13. How is the Java interface programmed in C++?
14. What are the rules for protected and friends with respect to inheritance?
15. What is virtual inheritance?
16. What is private inheritance?
17. What does it mean for a derived class method to hide a base class method?
18. Add a Rectangle class to the hierarchy in Figure 6-5 and modify Figure 6-8 appropriately to include objects of type Rectangle.
Exercises
135
19. Define an abstract base class called Employee that contains a name (string), a social
security number (string), and the respective accessor functions and contains an abstract
method that gets and sets a salary. It also contains a method called print whose task is to
output the name and social security number. You should not use protected members.
Include a two-parameter constructor, using initializer lists, and give all parameters
defaults. Carefully decide which members should be virtual. Next, derive a class called
Hourly that adds a new data member to store an hourly wage (double). Its print
method must print the name, social security number, and salary (with the phrase
"per hour"). It will certainly want to call the base class print. Provide an accessor
and mutator for the salary, and make sure that its constructor initializes a salary. Next,
derive another class called Salaried that adds a new data member to store a yearly salary (double). Its print method must print the name, social security number, and salary
(with phrase "annual"). Provide an accessor and mutator for the salary, and make sure
that its constructor initializes a salary. Exercise the classes by declaring objects of type
Salaried and Hourly via constructors and calling their print methods. Provide a
single operator<< that prints an Employee (by calling print). This method will
automatically work for anything in the Employee hierarchy. In order to hold all employees, create a class called Roster that is able to hold a variable number of Employee *
objects. Roster should have a vector of Employee *. Provide the capability to add
an employee, and print the entire roster of employees. To add an employee, Roster::add is passed a pointer to an Employee object and calls vector::push_back. Don't worry about error checks. To summarize, Roster has public
methods named add and print. Write a short test program, in which you create a Roster object, call new for both kinds of Employee, sending the result to Roster::add,
and output the Roster via a call to print.
20. Define a hierarchy that includes Person, Student, Athlete, StudentAthlete,
FootballPlayer, BasketballPlayer, StudentFootballPlayer,
StudentBasketballPlayer, StudentFootballAndBasketballPlayer.
Give a Person a name, a Student a GPA, an Athlete a uniform number, a
FootballPlayer a boolean representing true for offense, and false for defense, a
BasketballPlayer a scoring average (as a double). Define appropriate data representations, constructors, destructors, and methods, and make use of virtual inheritance.
Observe that StudentFootballAndBasketballPlayer will have two uniform
numbers. Write a test program that obtains both numbers.
136
H A P T E R
Templates
137
138
Chapter 7 Templates
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Figure 7-1
#include "IntCell.h"
#include <string>
using namespace std;
int main( )
{
vector<int>
vector<double>
vector<string>
vector<IntCell>
vector<int>
v1(
v2(
v3(
v4(
v5(
37
40
80
75
75
);
);
);
);
);
<<
<<
<<
<<
<<
findMax(
findMax(
findMax(
findMax(
findMax(
v1
v2
v3
v4
v5
)
)
)
)
)
<<
<<
<<
<<
<<
endl;
endl;
endl;
endl;
endl;
return 0;
}
Figure 7-2
//
//
//
//
//
Function Templates
139
cates that Comparable is the template argument: it can be replaced by any type to generate a
function (both class and typename can be used interchangeably here). For instance, if a call
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
Figure 7-3
140
Chapter 7 Templates
to findMax is made with a vector<string> as parameter, then a function will be generated by replacing Comparable with string. A general rule of thumb is that a function template is not a function; rather, it is simply a function wannabee. When it is expanded, the result is
a function. Figures 7-2 and 7-3 illustrate that function templates are expanded automatically as
needed. Specifically, Figure 7-2 shows the code that the programmer would write, and then
Figure 7-3 shows the code that the compiler generates internally to expand the function template
into real functions. An expansion for each new type generates additional code; this is one example of code bloat, when it occurs in large projects. However, once the template is expanded for a
particular type, it is not expanded a second time for the same type. Thus internally, in Figure 7-3,
only one function is shown for the expansion with int.
Note also, that in Figure 7-2, the call at line 18 will result in a compile-time error. This is
because when Comparable is replaced by IntCell, in Figure 7-3 line 39 becomes illegal:
there is no operator< defined for IntCell. Since this line (line 39 in Figure 7-3) is internally generated, the compiler will most likely flag the calling code (line 18 in Figure 7-1) and
the corresponding line in the template declaration (line 10 in Figure 7-2) in the error message
that it outputs. To help the users of a template avoid these kinds of errors, it is customary to
include, prior to any template, comments that explain what assumptions are made about the template argument(s). This includes assumptions about what kinds of constructors are required. In
findMax, a zero-parameter constructor and copy constructor are both required by vector,
but it is reasonable to assume that the user has satisfied the requirements of the parameters, and
thus we have not included those requirements in the comments.
When line 18 in Figure 7-1 is commented out, the result is that there are three separate
overloaded versions of findMax that have been expanded from the template.
Because template arguments can assume any class type, when deciding on parameter passing and return passing conventions, it should be assumed that template arguments are not primitive types. That is why we have returned by constant reference.
Error messages relating to function templates are generally of two types. Type independent errors, such as missing semicolons, are detected when the template is compiled. Type sensitive errors, such as undefined operators and methods are detected when a particular expansion
causes a problem.
Because a function template is not a function (it is just a wannabee), function templates
can be placed in header files without causing multiple definitions. In fact, this is probably the
simplest way of compiling function templates.
Not surprisingly, there are many arcane rules that deal with function templates. Most of
the problems occur when the template cannot provide an exact match for the parameters, but can
come close. There must be ways to either declare or resolve and ambiguity. For instance, consider the code in Figure 7-4, which illustrates a max2 function template, and also a specific
max2 function that is not a template. Four calls to max2 are made in main. The first two are
expanded with int and double as Comparable, respectively. The third call, at line 25, uses
the non-template function that is defined at lines 12 to 15. Thus if there is an expandable tem-
Class Templates
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
141
#include "IntCell.h"
#include <string>
using namespace std;
template <typename Comparable>
const Comparable & max2( const Comparable & lhs,
const Comparable & rhs )
{
return lhs > rhs ? lhs : rhs;
}
const string & max2( const string & lhs, const string & rhs )
{
return lhs > rhs ? lhs : rhs;
}
int main( )
{
string s = "hello";
int
a = 37;
double b = 3.14;
cout
cout
cout
cout
<<
<<
<<
<<
max2(
max2(
max2(
max2(
a,
b,
s,
a,
a
b
s
b
)
)
)
)
<<
<<
<<
<<
endl;
endl;
endl;
endl;
//
//
//
//
return 0;
}
Figure 7-4
plate and a non-template, and both are equivalent matches, then the non-template wins. The
fourth call, at line 26, is ambiguous because whether we expand with either an int or double,
in both cases, one parameter is an exact match, and one is an approximate match.
142
Chapter 7 Templates
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ifndef _OBJECTCELL_H
#define _OBJECTCELL_H
template <typename Object>
class ObjectCell
{
public:
explicit ObjectCell( const Object & initValue = Object( ) )
: storedValue( initValue )
{ }
const Object & getValue( ) const
{ return storedValue; }
void setValue( const Object & val )
{ storedValue = val; }
private:
Object storedValue;
};
#endif
Figure 7-5
Figure 7-6 shows how ObjectCell can be used to store objects of several types. Notice
that ObjectCell is not a class; it is only a class template. Like function templates, class templates are wannabee classes. In Figure 7-6, the actual classes are ObjectCell<int> and
ObjectCell<double>.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "ObjectCell.h"
#include <iostream>
using namespace std;
int main( )
{
ObjectCell<int>
m1;
ObjectCell<double> m2( 3.14 );
m1.setValue( 37 );
m2.setValue( m2.getValue( ) * 2 );
cout << m1.getValue( ) << endl;
cout << m2.getValue( ) << endl;
return 0;
}
Figure 7-6
Class Templates
143
It should be clear that the vector class is in reality a class template. It turns out that the
string class is an instantiated class template (the class template is basic_string). A third
class template is complex, which is most often instantiated as complex<double> and is
found in the standard header file complex. And finally, ostream and istream are actually
instantiations of class template basic_ostream and basic_istream.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#ifndef _MATRIX_H
#define _MATRIX_H
#include <vector>
using namespace std;
template <typename Object>
class Matrix
{
public:
Matrix( int rows, int cols ) : array( rows )
{ setNumCols( cols ); }
int
{
int
{
numrows( ) const
return array.size( ); }
numcols( ) const
return numrows( ) > 0 ? array[ 0 ].size( ) : 0; }
Figure 7-7
144
Chapter 7 Templates
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include "Matrix.h"
using namespace std;
int main( )
{
Matrix<int> m( 2, 2 );
m[ 0 ][ 0 ] = 1; m[ 0 ][ 1 ] = 2;
m[ 1 ][ 0 ] = 3; m[ 1 ][ 1 ] = 4;
cout << "m has " << m.numrows( ) << " rows and "
<< m.numcols( ) << " cols." << endl;
cout << m[ 0 ][ 0 ] << " " << m[ 0 ][ 1 ] << endl <<
m[ 1 ][ 0 ] << " " << m[ 1 ][ 1 ] << endl;
return 0;
}
Figure 7-8
Many of the classes that we have seen earlier are excellent candidates to be class templates. Two obvious examples are the matrix class from Section 5.6 and the queue class in
Section 4.6.6. Figure 7-7 shows a Matrix class template, that is virtually identical to the
MatrixOfDouble class seen earlier in Figure 5-15, but works for any type, Object, that has
the requisites of a vector. The Matrix class template is easily used, as shown in Figure 7-8.
Separate Compilation
145
quite cumbersome. For instance, to define operator= in the specification requires no extra
baggage. In the implementation, we would have the brutal:
template <typename Object>
const ObjectCell<Object> &
ObjectCell<Object>::operator= ( const ObjectCell<Object> & rhs )
{
if( this != &rhs )
storedValue = rhs.storedValue;
return *this;
}
Even with this, the issue now becomes how to organize the class template declaration and
the member function template definitions. The main problem is that the implementations in
Figure 7-10 are not actually functions; they are still wannabees. They are not even expanded
when the ObjectCell template is instantiated. Each member function template is expanded
only when it is invoked.
7.4.1
The first option is to put both the declaration and implementation in the header file. This would
not work for classes, since we could get multiply defined functions if several different source
files had include directives that processed this header, but since everything here is a wannabee,
there is no problem.
With this strategy, it might be a little easier for reading purposes to simply have the header
file issue an include directive (prior to the #endif) to automatically read in the implementation
file. With this strategy, the .cpp files that store the templates are not compiled directly.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifndef _OBJECTCELL_H
#define _OBJECTCELL_H
template <typename Object>
class ObjectCell
{
public:
explicit ObjectCell( const Object & initValue = Object( ) );
const Object & getValue( ) const;
void setValue( const Object & val );
private:
Object storedValue;
};
#endif
Figure 7-9
146
Chapter 7 Templates
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "ObjectCell.h"
template <typename Object>
ObjectCell<Object>::ObjectCell( const Object & initValue )
: storedValue( initValue )
{
}
template <typename Object>
const Object & ObjectCell<Object>::getValue( ) const
{
return storedValue;
}
template <typename Object>
void ObjectCell<Object>::setValue( const Object & val )
{
storedValue = val;
}
Figure 7-10
7.4.2
Explicit Instantiation
On some compilers we can achieve many of the benefits of separate compilation if we use
explicit instantiation. In this scenario, we set up the .h and .cpp files as would normally be
done for classes. Thus both Figure 7-9 and Figure 7-10 would be exactly as currently shown.
The header file would NOT have an include directive reading the implementation. The main
routine would only have an include directive for the header file. So Figure 7-6 would be
unchanged also. If we compile both .cpp files, we find that the instantiated member functions
are not found. We fix the problem by creating a separate file containing explicit instantiations of
the ObjectCell for all the types we use. An example of these explicit instantiations is shown
in Figure 7-11. This file is compiled as part of the project. We have had success using this technique with several older compilers. The downside is that all of the template expansions have to
be listed by the programmer, and sometimes if the class template uses other class templates,
those have to be listed too. The advantage is that if the implementation of the member functions
in ObjectCell changes, only ObjectCellExpand.cpp needs to be recompiled.
1
2
3
4
#include "ObjectCell.cpp"
template class ObjectCell<int>;
template class ObjectCell<double>;
Figure 7-11
Specialized Templates
7.4.3
147
Newer compilers support the export directive, which is a recent language addition. If we issue an
export directive prior to the class template declaration, as shown in Figure 7-12, then the extra
file that we added in Section 7.4.2 becomes unnecessary, and the program links as we would
have expected. Dont expect to see this correctly implemented on all compilers. The export
directive also works for function templates.
The template parameter list can have more than one parameter. For instance, a map, in which we
store keys and values can be declared as
template <typename KeyType, typename ValueType>
class Map
{
...
};
An instantiation of the Map to store names and birthdates, in which a name is a string and a
birthdate is of type Date would be
Map<string,Date> birthdays;
In fact, map (the class template name is all lower case) is part of the Standard Library and mimics the TreeMap found in java.util.
7.5.2
148
Chapter 7 Templates
The advantage of using a nontype parameter is that size can be used in places that
require constant expressions (such as primitive arrays), so a data member such as
Object buf[ size ];
would be legal in this class template, but would not be legal if size was simply a parameter to
a Buffer constructor, in which only Object was in the template parameter list. The disadvantage, of course, is the potential for code bloat.
Additionally, note that buf1 and buf2 above have different types. This may be an
advantage or a disadvantage.
7.5.3
Template parameter lists can have default values, for both type and nontype parameters. For
instance,
template <typename Object=char, int size=4096>
class Buffer
{
...
};
buf1;
buf2;
// Buffer<int,4096>
// Buffer<char,4096>
Default template parameters are relatively new and not implemented on many compilers.
7.5.4
Member Templates
Member templates are member functions that are themselves templates; this is basically a template inside of a template. As an example of its use, consider the code in Figure 7-13. Even
though a float can be used in place of a double, and an int can be used in place of a long,
if we have
Pair<int,float>
p1( 3, 4.0 );
Pair<long,double> p2 = p1;
the code does not compile because p1 and p2 are not type compatible, and so the default copy
constructor is not applicable. Member templates allow us to solve this problem by providing a
constructor that accepts any Pair, instead of the specific Pair.
Specialized Templates
1
2
3
4
5
6
7
8
9
10
11
12
13
149
Figure 7-13
This strategy is illustrated in Figure 7-14. Lines 9 to 12 contain the member template.
Observe that the constructor for Pair accepts a Pair with arbitrary types and as long as the
types are compatible the constructor template will expand. If the types are not compatible, we
get a compile-time error due to line 11, which is perfect.
Member templates are also a relatively new addition to C++, so many compilers do not
support them. The use shown here in extending type compatibility among different template
instantiations is probably its most common.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Figure 7-14
150
Chapter 7 Templates
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Rectangle
{
public:
explicit Rectangle( int len = 0, int wid = 0 )
: length( len ), width( wid ) { }
int getLength( ) const
{ return length; }
int getWidth( ) const
{ return width; }
void print( ostream & out = cout ) const
{ out << "Rectangle " << getLength( ) << " by "
<< getWidth( ); }
private:
int length;
int width;
};
ostream & operator<< ( ostream & out, const Rectangle & rhs )
{
rhs.print( out );
return out;
}
Figure 7-15
In Java, we typically solve this problem by passing a second parameter to findMax, which is a
function object. The function object is an instance of a class that implements some interface. In
this case, the interface is Comparator (in java.util), which specifies a method that can
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
151
Figure 7-16
compare any two objects. Since we do not have a root class of Object in C++, we can simulate
the Java idea by declaring that Comparator is a class template, and have the findMax template accept a Comparator<Object> as a second parameter. But a better plan is to make the
Comparator a template parameter, so that a Comparator that compares base class objects
can be used on a vector of derived class objects. This resulting Comparator class template
and findMax function template is shown in Figure 7-16. Since we expect the Comparator to
be a base class, it probably should have a virtual destructor and be passed using call-by-constant
reference, instead of call-by-value. But as we will see, this turns out not to be necessary.
1
2
3
4
5
6
7
8
9
10
11
12
int main( )
{
vector<Rectangle> a;
a.push_back( Rectangle( 1, 10 ) );
a.push_back( Rectangle( 10, 1 ) );
a.push_back( Rectangle( 5, 5 ) );
cout << findMax( a, LessThanByLength( ) ) << endl;
return 0;
}
Figure 7-17
152
Chapter 7 Templates
1
2
3
4
5
6
7
Figure 7-18
1
2
3
4
5
6
7
class LessThanByLength
{
public:
bool isLessThan( const Rectangle & lhs,
const Rectangle & rhs ) const
{ return lhs.getLength( ) < rhs.getLength( ); }
};
Figure 7-19
Now main can invoke findMax if it passes a vector of Rectangles and an appropriate function object. main is shown in Figure 7-17. The comparator is an anonymous instance
of class LessThanByLength, which implements the Comparator interface and is shown
in Figure 7-18. Specifically, it extends an instantiated Comparator template by implementing
isLessThan.
Except for the template baggage that replaces the use of Object as a superclass, this
implementation is exactly equivalent to the Java idiom for function objects.
7.6.2
Avoiding Inheritance
If we examine the code carefully, we see that the Comparator class template is not used at all!
Thus we can simplify Figure 7-18, and remove the Comparator class template completely.
The revised version is shown in Figure 7-19. Otherwise findMax and main are unchanged.
7.6.3
We can use operator overloading to make the implementation of the function object look slicker.
To do this, we replace isLessThan with operator(), as shown in Figure 7-20.
Key Points
1
2
3
4
5
6
7
153
class LessThanByLength
{
public:
bool operator( ) ( const Rectangle & lhs,
const Rectangle & rhs ) const
{ return lhs.getLength( ) < rhs.getLength( ); }
};
Figure 7-20
At this point it makes sense to change the name of the parameter to findMax from cmp
to isLessThan. Thus we get the code in Figure 7-21.
To summarize, the function object is declared in Figure 7-20 and provides an implementation of the function call operator. The routine that uses the function object is a template, and type
of the function object is a template parameter. Syntactically, when the function object is used, it
looks like a normal global function call. The template routine can itself be invoked as in Java by
passing an instance of the function object. The compiler will do all the type resolution.
Function objects are used extensively in the STL, which is the C++ equivalent of the Collections API. The STL is discussed in Chapter 10.
Figure 7-21
154
Chapter 7 Templates
7.8 Exercises
1. When using templates, what kinds of errors are detected at compile time that are not
detected until runtime in Java?
2. When are errors in a template definition detected by the compiler?
3. What is code bloat?
4. What strategies are used for separate compilation of templates?
5. What is a member template?
6. How are templates used for function objects?
7. Implement a function template that takes a single vector as a parameter, and sorts the vector (using any simple sorting algorithm).
8. Implement a function template that takes a single vector as a parameter and a function
object that represents the comparator as a second parameter and sorts the vector using the
comparator as the basis for ordering (using any simple sorting algorithm).
9. Reimplement Exercise 4.19 using templates to generalize the types of the objects in the
stack.
10. Reimplement Exercise 4.20 using templates to generalize the types of the objects in the
set.
11. Reimplement Exercise 5.11 using templates to generalize the types of the keys and values
in the map.
H A P T E R
155
156
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Account
{
public:
Account( int b = 0 )
: balance( b ) { }
int getBalance( ) const
{ return balance; }
void deposit( int d )
{ balance += d; }
private:
int balance;
};
int main( )
{
Account *acc1 = new Account( );
Account *acc2;
acc1->deposit( 50 );
cout << acc1->getBalance( ) << endl;
cout << acc2->getBalance( ) << endl;
return 0;
}
Figure 8-1
sense can be seen in Figure 8-3: Here we allow the changing of a constant object. Though C++
puts in a good faith effort to disallow it, if we use the const_cast, we can change an object
that we promised not to change.
But certainly the most evil of C++ issues is shown in Figure 8-4. Here we have an obvious
array index that is out-of-bounds. Almost any language would detect this at runtime. But not
C++. Instead, it uses the four bytes that follow arr[9] as its guess for arr[10]. If arr[10]
were on the left-hand side of the assignment operator, we could even change its value. Almost
certainly this would be some other variable in the program, leading to hard-to-find bugs.
In C, on which primitive C++ arrays are based, the lack of bounds checking was justified
on the grounds that the implementation simply stored a pointer variable. And being a language
of the 1970s, that was used instead of assembly language to implement operating systems, certainly speed was an important criteria.
But the vector class was added in the mid 1990s, and not including bounds checking as
an automatic part of the indexing operator seems inexcusable. Yet it accurately reflects the C++
philosophy of not forcing the user to pay at runtime for anything more than bare necessities, and
certainly not paying for a feature that was free in C.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
157
class Barbell
{
public:
Barbell( double b ) : weight( b ) { }
double getWeight( ) const
{ return weight; }
private:
double weight;
};
int main( )
{
Barbell *bb = new Barbell( 15.6 );
cout << bb->getWeight( ) << endl;
Account *acc = (Account *) bb;
cout << acc->getBalance( ) << endl;
acc->deposit( 40 );
cout << bb->getWeight( ) << endl;
return 0;
}
Figure 8-2
C++ code that might generate a warning but compiles, allowing type confusion
In C++ error handling in general seems to follow this trend. The compiler and runtime
systems are less likely to signal errors than in Java. And even when such errors are signalled, in
many cases the programmer can avoid acknowledging that the error has occurred, and can thus
continue executing code while the program is no longer in a good state.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main( )
{
const string h = "hello";
// two failed attempts to change h
h[ 0 ] = 'j';
// does not compile, thankfully
string & href = h; // does not compile, thankfully
// third time is a charm
string & ref = const_cast<string &> ( h );
ref[ 0 ] = 'j';
cout << h << endl;
// prints jello
return 0;
}
Figure 8-3
158
1
2
3
4
5
6
7
8
9
10
int main( )
{
int i;
vector<int> arr( 10, 37 );
Figure 8-4
This contrasts with Java, where if the Virtual Machine signals an exception, the Virtual
Machine will shut down (the erroneous thread) if the exception is not handled, and where the
programmer is explicitly required to write some code that acknowledges that a checked exception might occur (even if that code simply declares a throws list that allows the exception to
propagate).
If the programmer has detected an unrecoverable error, there are plausible solutions. The best is
to throw an exception and not handle it (or possibly handle the exception in main, and let main
return with a nonzero return value). In a pre-exception world, there are two common ways to
abnormally terminate a program from inside any function.
The functions abort and exit, both declared in the standard header file cstdlib,
cause the program to terminate. exit can provide a parameter that will be used in place of a
return value for main. The signatures of abort and exit are:
void abort( );
void exit( int status );
abort causes the program to terminate abruptly. No destructors are invoked in the process.
exit causes the destructors of static objects to be called, but not any local objects that are in
pending function calls. Do not call exit inside the destructor of a static object. (why?)
Because few destructors are invoked, both abort and exit are inferior to throwing an
uncaught exception. Further, exit should never be used outside of main to handle a normal
termination, since a normal termination should guarantee that all appropriate destructors are
invoked.
As part of the termination process, exit invokes functions that are registered with
atexit in reverse order of their registrations (the functions are also invoked if termination is
1
2
3
4
5
6
7
8
9
10
159
void printExit( )
{
cout << "Invoking the printExit method..." << endl;
}
int main( )
{
atexit( printExit );
foo( );
}
Figure 8-5
normal). Such functions accept no parameters and return void, and the number of registered
functions is implementation dependent. The signature for atexit is:
int atexit( void f( void ) );
with atexit returning 0 if the limit of registered functions has already been reached. Figure 85 illustrates the use of atexit. If foo returns normally, without calling abort (or throwing an
unhandled exception), printExit will be invoked as the program terminates. printExit will
also be invoked if foo calls exit.
8.2.2
Assertions
The assert preprocessor macro (see Section 12.1 for a discussion of preprocessor macros) is
made available by including the standard header file cassert.
If NDEBUG is defined, calls to assert are ignored. Otherwise, if the parameter to
assert is zero, an error message is printed and the program is aborted, by calling abort.
Recall that no destructors are invoked, so this is rather drastic. The error message includes the
actual expression as well as the source code file name and line number.
For instance, in Figure 8-6 if a call to foo is made with a NULL pointer, an error message
similar to
1
2
3
4
5
6
7
8
9
10
Figure 8-6
160
will appear (on the standard error), and the program will terminate abnormally. If NDEBUG is
defined prior to line 8 via
#define NDEBUG
Error states
Many older C++ libraries, including the I/O library use the notion of an error state. If an error
occurs, either it is returned in an error code, a global error code is set, or the object records that it
is in an error state (in an internal data member). In all cases, the error state can be tested, and
possibly cleared if the user invokes appropriate functions. We will discuss how this works with
I/O in Chapter 9. In general, this solution is inferior to exceptions.
Basic syntax
The basic C++ syntax is similar to Java, in that a member function can throw an exception, and
a caller can either ignore the exception and let it propagate to its caller, or attempt to handle the
exception with a try/catch block.
8.3.2
No finally clause
C++ does not have a finally clause. However, it is guaranteed that if a function returns either
normally with no active exception, or abnormally with an active exception, all its destructors
will be invoked. Thus local objects that allocated heap memory indirectly will free the heap
memory back to the system automatically, assuming their destructors are correctly written. This
leaves only heap memory that was directly allocated and is accessed by local pointer variables.
In such a case the auto_ptr, which we discuss in Section 8.4 can be used. The basic idea of
the auto_ptr is that it wraps a pointer variable and has a destructor that calls delete, so if
we create an auto_ptr, the heap memory can be freed by its destructor.
8.3.3
In C++, we can specify a throw list, cosmetically like Java. Notice that the reserved word is
throw, rather than throws, to avoid an additional reserved word in the language. We say that
this is cosmetic because unlike Java, the throw list in C++ is not examined at compile time. So if
the method throws an exception that is not in the throw list, the program will still compile. How-
Exception Handling
161
ever, at run time, if an exception is thrown that was not in the throw list, then a call to
std::unexpected will occur, which normally calls abort. It is possible to change the
behavior of std::unexpected by invoking set_unexpected.
The throw list syntax in C++ is slightly different from Java:
void foo( ) throw( UnderflowException, OverflowException );
An empty (as opposed to missing) throw list signals that no exceptions are expected to be
thrown. If an expected exception is thrown but not caught, the function std:terminate will
be invoked. This function is also called if the exception handling mechanism determines that the
runtime stack has been corrupted, or if a destructor that is executed as part of the handling of an
exception itself throws an exception (to avoid infinite recursion). Calling set_terminate
allows the programmer to supply a function that contains different behavior for the
terminate function.
8.3.4
A missing (as opposed to empty) throw list signals that any exception might be thrown. Thus,
nothing is unexpected. Missing throw lists are part of C++ in order to preserve backward compatibility when exceptions were added. Of course, this is why compile-time checking of throw
lists is not feasible. Most C++ code omits the throw list.
8.3.5
In Java, only objects that are subclasses of Throwable (or Throwable itself) can be thrown
as exceptions. In C++, any object can be thrown as an exception, but clearly, it is better to create
exception
runtime_error
logic_error
bad_alloc
range_error
out_of_range
length_error
bad_cast
overflow_error
underflow_error
invalid_argument domain_error
bad_typeid
bad_exception
ios_base::failure
Figure 8-7
Standard exceptons
162
a class type that can store information about why the exception was thrown.
In Java, Throwable objects contain a stack trace as part of their data. Since there is no
root exception class in C++, you cannot count on much information from the exception object
besides its type.
8.3.6
Although there is no root exception class, C++ attempts to provide an inheritance hierarchy for
its exceptions. These exceptions are shown in Figure 8-7. In Java, most of these exceptions
would be considered runtime exceptions and would not be caught by the programmer. The only
method in class exception, besides the Big-Three and a zero-parameter constructor is the
what method, which returns a primitive string giving you some information. exception and
its subclasses promise not to throw any exception (specifically bad_alloc) when their methods are invoked.
8.3.7
Rethrowing Exceptions
8.3.8
Because there is no root hierarchy, special syntax is needed to catch all exceptions. If ... is
used in a catch handler, it matches any exception. However, the exception object will not be
available (since there is no way to provide its type). As in Java, if there are multiple catch
clauses, the first match wins. Thus ... has to be the last catch block in a series.
8.3.9
Exception Handling
163
hierarchies and the methods are marked virtual, dynamic dispatch will occur when the
exceptions methods are invoked, as long as the exception is not passed by value.
The alert reader, may be wondering why the reference to the exception, e, at line 26 is not
stale. After all, in both cases, a local temporary was storing the exception object, and the function in which that temporary was created has terminated. Clearly if this was true, exceptions
would have to be passed using call-by-value, which would be impossible with inheritance (since
call-by-value and inheritance do not mix). Thus as a special case, the exception handling mechanism guarantees that the exception object will not have its destructor called until it is no longer
an active exception. This means the object is valid until it is caught, and the catch block that it is
caught in terminates, unless the catch block rethrows the exception, in which case it retains its
validity as if the catch block was never executed.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Figure 8-8
164
Even if we have done all the correct things, such as declaring a virtual destructor for
Person, this code could leak memory if foo abruptly terminates with an unhandled exception.
Because C++ does not have a finally block, it is difficult to guarantee that the delete statement is executed. However, it is guaranteed that all local variables will have their destructors
called.
Thus, C++ provides a wrapper class template, the auto_ptr, declared in the standard
header file memory. The idea of auto_ptr is that it wraps a pointer variable, provides sufficient operator overloading so that the wrapped variable can be accessed without any additional
baggage, and since its instances are objects, we can expect a destructor to be invoked. The
destructor can call delete on the pointer variable that it is wrapping. The auto_ptr also
provides a get routine, in case access to the wrapped pointer is needed and relying on an
implicit type conversion is not appropriate. In a simple example;
void foo( )
{
auto_ptr<Person> p( new Student( 123456789, "Jane", 4.0 ) );
p->print( );
...
}
Now, even if print, or some other routine throws an exception that is not handled, the
Students destructor is invoked. The call to print works because auto_ptr overloads
operator->. Also overloaded are operator*, the type conversion operator (a cast to
Person *, in our example), copying, and copy construction.
If an auto_ptr is constructed to point at an object that was not heap-allocated, disaster
can ensue, so dont do that. If two auto_ptrs are constructed to point at the same heap-allocated object, disaster can ensue, because of double-deletion. So dont do that either.
Key Points
165
166
The standard library defines a small hierarchy of what would be Java errors and runtime
exceptions that can be extended by the programmer. This hierarchy is rooted at class
exception.
Inside a catch block, we can rethrow an exception simply by issuing throw. The exception is not required in the throw statement.
To catch all exceptions use ... .
Many of the Java rules relating to exceptions are in effect implemented in C++.
Exceptions should always be caught using call-by-reference or call-by-constant reference.
Catching by reference allows the changing of the state of the exception object prior to
rethrowing it.
The exception object is always valid up to the end of the catch clause that last handles the
exception.
Templates and exceptions dont mix. Avoid using throw lists in function templates.
auto_ptr is a class template that wraps a pointer variable. The pointer variable should
be viewing a heap-allocated object. When the auto_ptr is destroyed, its destructor will
delete the object it is wrapping if the auto_ptr still enjoys ownership. Ownership is
transferred if the auto_ptr is copied into another auto_ptr.
Avoid using auto_ptr objects in container classes such as vector.
8.6 Exercises
1. Describe the semantics of abort and exit.
2. What does assert do?
3. List some standard C++ exceptions.
4. What does a missing throw list mean?
5. What objects can be thrown as exceptions?
6. How is an exception rethrown?
7. How can you catch all exceptions?
8. Why are exceptions never caught using call-by-value?
9. What is an auto_ptr?
10. How long is the exception object valid?
11. Why is it dangerous to include a throw specifier in a method template?
12. What happens if an exception that is not listed in the throw list is thrown?
13. Implement the auto_ptr class template. For an implementation that allows compatability between derived and base class auto_ptrs, you should use member templates.
H A P T E R
167
168
ios_base
ios
istream
ifstream
ostream
istringstream
iostream
fstream
Figure 9-1
ostringstream
ofstream
stringstream
Subclasses of ios_base are all templates that are instantiated with the type of characters
that are used in the implementation. Standard instantiations include one for char, and another
for the wide character type wchar_t.
The basic_ios class encapsulates some information that is common to both input and
output. The instantiation basic_ios<char> is defined with a typedef as ios; and is similar
instantiation basic_ios<wchar_t> is defined with a typedef as wios. In other words, we
have:
template <class chartype>
class basic_ios : public ios_base { ... }
typedef basic_ios<char>
ios;
typedef basic_ios<wchar_t> wios;
Note the use of virtual inheritance since basic_iostream uses multiple inheritance. In
the remainder of this chapter, we will ignore the fact that these are class templates, and instead
refer directly to the instantiations with char.
Error States
169
Eight streams are predefined: cin, cout, cerr, and clog which are in order: standard
input, output, error, and an error stream that is automatically flushed. Also predefined are the
wide character counterparts wcin, wcout, wcerr, and wclog.
does not work, because the end of file stream state is not set until an I/O operation actually fails.
Thus the loop above goes around one time too many, printing the last successfully read value
twice (since x wont change on the unsuccessful read). This is an extremely common error that
could never happen in Java, because an exception would cause the flow to leave prior to the
cout. But in C++, if you do not test the stream state, flow continues as if nothing happened.
If the stream is in an error state, then the expression cin, when used as a condition of an if
or loop, returns false. Thus, commonly seen code is:
while( cin >> x )
cout << "Read " << x << endl;
170
which correctly outputs all successfully read data. However, if the read fails (for instance x is an
int, and there is a non-integer character on the input), this loop terminates at that point, rather
than attempting error recovery.
For more robust code, one can use a test that attempts recovery. An example of this strategy is shown in Figure 9-2. Here we have a routine that reads items, separated by white space, of
arbitrary type (as long as the type has correctly overloaded operator<<).
This code illustrates three important points. First, fail will be true if the I/O operation
has failed for some reason, but that the stream is still in good shape, and so the error is correctable. If, for instance, we are trying to read an integer, fail could indicate that non-integer data,
such as the word "Joe" was found on the input stream. Thus, second, we can solve the problem
by skipping over the next token. However, third, whenever the stream is in an error state, all I/O
on the stream will continue to fail. So before we attempt to read the string that we will discard,
we must first clear the error state by invoking clear. (That read cannot normally fail, since
there must be data, and the stream is in good shape otherwise, but extra critical code would
check an error state after the read also).
A technical point about this example that is not related to I/O, but instead deals with templates. The code can be invoked as:
vector<int> arr;
readData( cin, arr );
If the call readData returned the vector instead of using it as a parameter, then the instantiation of the function template must be explicit, as in
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Figure 9-2
Output
171
Figure 9-3
Method
eof
bad
fail
good
since a function templates return type is not considered in determining a template expansion,
and thus the compiler would have no way to deduce what Object should be.
The method good returns true if the stream is not in an error state. The method bad is
like fail, except it is more severe: the stream has been corrupted for some reason, and so it is
not worth attempting recovery. Figure 9-3 summarizes the methods that can test the state of a
stream.
9.3 Output
As we have already seen, the vast majority of output statements simply overload operator<<.
In addition to operator<<, single characters can be output by invoking the put member
function. The most interesting part of output in C++ is probably the technique that is used to
finely tune how the output is formatted, especially since we might not be happy with the
defaults.
For instance, if we run the code in Figure 9-4, the output that is produced is:
Pat 40000.1
Sandy 125443
Here we see two deficiencies. First, by default doubles are only output to six significant
digits. Second, integer and string types print only the minimum number of characters needed,
making it hard to align output. We would prefer output such as:
Pat
Sandy
40000.11
125443.10
in which we force two decimal places, and require that both the string and double be padded with at least a few spaces. But strings should be placed on the left, followed with padding, while double should be placed on the right, preceded with padding.
The number of significant digits and digits after the decimal point, as well as how much
and where padding of output is done is part of the state of a stream. Specifically, it is part of the
format state. Thus we can invoke methods on the stream to examine and possibly change the format state, on a stream-by-stream basis.
The easiest way to do this is to use manipulators. For instance, some of the manipulators,
of concern to us, with examples of their use on a specific output stream cout, could include
172
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Person
{
public:
Person( const string & n = "", double s = 0.0 )
: name( n ), salary( s ) { }
void print( ostream & out = cout ) const
{ out << name << " " << salary; }
private:
string name;
double salary;
};
ostream & operator<< ( ostream & out, const Person & p )
{
p.print( out );
return out;
}
int main( )
{
vector<Person> arr;
arr.push_back( Person( "Pat", 40000.11 ) );
arr.push_back( Person( "Sandy", 125443.10 ) );
for( int i = 0; i < arr.size( ); i++ )
cout << arr[ i ] << endl;
return 0;
}
Figure 9-4
cout
cout
cout
cout
cout
cout
1
2
3
4
5
6
<< setw( 15 );
// set next field width to 15
<< left;
// field on left, padding on right
<< right;
// field on right, padding on left
<< setprecision( 2 ); // two decimal places
<< scientific:
// output double w/ scientific notation
<< fixed;
// output double no scientific notation
void print( ostream & out = cout ) const
{
out << left << setw( 15 ) << name << " "
<< right << fixed
<< setprecision( 2 ) << setw( 12 ) << salary;
}
Figure 9-5
Output
173
Most of these change the format state for all subsequent operations (until overridden by a contradictory manipulator), except for setw which only applies to the next field. The manipulators
that accept a parameter are available by including the standard header file iomanip. Figure 9-5
shows how we can use these manipulators to generate aligned, nicely-formatted output.
There are a host of manipulators that are available.
Manipulator boolalpha and noboolalpha are used to control whether bools are
printed as false and true or 0 and 1. The latter is the default (for backward compatibility).
Thus
cout << boolalpha << true << " " << noboolalpha << true << endl;
prints
true 1
oct, dec, and hex are manipulators used to control how numbers are output. Alternatively, setbase(b) can be used. The base by default is not printed, but this can be changed by
manipulator showbase; the default is noshowbase. Thus,
cout << 37 << " " << oct << 37 << " "
<< hex << 37 << " " << setbase( 10 ) << 37 << endl;
prints
37 45 25 37
If we have
cout << showbase;
cout << 37 << " " << oct << 37 << " "
<< hex << 37 << " " << setbase( 10 ) << 37 << endl;
uppercase and nouppercase controls whether the x in 0x and e in scientific notation are printed in lower case or upper case. left and right control the positioning of the data
relative to the padding. internal puts fill characters between the sign and the value.
setprecision(n) sets the floating point precision. setw(w) sets the width of the next output only to w. fixed and scientific control whether scientific notation is output.
setfill(ch) sets the fill white space to ch. For instance, setfill(*) can be
used to fill with *, as is commonly done on cashiers checks to prevent fraud. The following
code
cout << setprecision( 2 ) << setfill( * ) << fixed << right;
cout << setw( 8 ) << 12.49 << endl;
cout << setw( 8 ) << 3.1 << endl;
outputs
***12.49
****3.10
174
9.4 Input
We have already seen that input streams make use of overloaded sets of operator<< to do
significant work and in Section 9.2 we saw how the error state of a stream can be accessed and
cleared. In this section we discuss some additional input operations.
Often we want to perform character at a time input. Although operator<< is overloaded to accept a character, using it can be tedious because operator<< skips whitespace by
default. Although the manipulator skipws can change this setting for future reads, and ws can
change the state for the next read, at best this is tedious, and at worse it potentially is time-consuming, if we repeatedly have to set the format state and then reset because the character-at-atime input is interspersed with other input.
For this reason, istreams provide a get method. There are several versions of get, but
the easiest to use is the one with signature
istream & get( char & ch );
Thus, in
char ch;
if( cin.get( ch ) )
cout << "Read " << ch << endl;
else
cout << "Read error" << endl;
a single character is read, and if the read fails the stream is put in a bad state. The unget
method is used to undo a get. The peek method is used to examine the next character in the
input stream without digesting it. The declarations of these methods are:
istream & unget( );
int peek( );
Files
1
2
3
4
5
6
7
8
9
10
175
istream & getline( istream & in, string & str, char delim )
{
char ch;
str = "";
// empty string, will build one char at-a-time
while( in.get( ch ) && ch != delim )
str += ch;
return in;
}
Figure 9-6
The return type of peek is an int, in much the same way as the read method in
java.io.Reader returns a char in an int variable. If peek sees the end of input, it
returns EOF. Unlike Java, if the return value of peek is assigned to a variable of type char
prior to checking if it is EOF, the compiler will not complain, and the code is likely in error, particularly for unformatted data streams.
The function getline, which is not a member of istream, can be used to read a line of
input. The signature is
istream & getline( istream & in, string & str, char delim = '\n' );
getline reads characters from an input stream and forms a string str. Reading stops when
either delim is encountered or the end-of-file is reached. The delim character is not included
in the string but is removed from the input stream. Some older compilers have broken implementations of getline. The implementation in Figure 9-6 shows a plausible implementation
of getline (and not being a template, one that should get preferential consideration by the
compiler), and serves to illustrate the use of get.
Finally, member function ignore is used to skip characters. ignores signature is:
istream & ignore( int n = 1, int delim = EOF );
ignore reads and discards n characters from the istream, or all characters up to and including the delim, or until the end-of-file is encountered.
9.5 Files
Files are modelled by either an ifstream for input, or an ofstream for output (again there
are corresponding class template expansions for wide-character implementations). An
fstream can be used for both input and output, but we do not recommend it. To perform file I/
O, the standard header fstream should be included.
File streams can be constructed with a primitive string (either a string constant, null terminated array of character, or a result of c_str on a string object), and optionally a mode that
describes how the file is to be used. Some examples include:
ifstream file1( "data.txt" );
176
In these examples, first we see that an ifstream can be constructed with a file name. The second example constructs an ofstream with a primitive string (an array of character), and the
default output mode of truncation. Note that although cin>>name2 compiles, using it is very
dangerous, since it can lead to a buffer overflow. Invoking an overloaded version of get that
works with character arrays is a much safer solution. Option number 3 uses a string class
object, and we see that we can invoke c_str to obtain the primitive string that the ofstream
constructor requires. Also, we see an explicit use of a mode, in which we bitwise-or out and
trunc. This is the default. Finally we see a constructor that opens a file for appending. (Older
versions of C++ use ios instead of ios_base). Alternatively, we can simply declare the
stream object and use member function open later. The state of the stream should be tested after
it is opened, as in
if( file4 )
// ok
else
// not ok
When a stream goes out of scope, it is guaranteed that its destructor is called, thereby closing the stream automatically. The user can invoke close if it is desired to close the stream
sooner, perhaps to reopen a different file with the same stream object.
String Streams
177
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Figure 9-7
178
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Figure 9-8
a file that had only one int on each of the first two lines would be processed without error. If
we wanted to insist that every line had two and only two integers, we would need to do more
work.
In Java, we would read one line at a time into a String. Once we had the string, we
could parse it with a StringTokenizer, using code such as Figure 9-8.
This is exactly the behavior that can be implemented with an istringstream, which
like its companion ostringstream, is available by including the standard header sstream.
An istringstream is constructed by passing a string as a parameter. At that point, all of
the basic istream operators, including operator>> and testing of error states are available.
Note that the error states apply to the istringstream, and not the fstream.
Figure 9-9 shows the C++ implementation of twoInts, which is only cosmetically different from the Java code. Observe, first, that fin is a reference to an istream instead of an
ifstream. As with Java, it is always best to use the most generic type. Once we have the
istringstream at line 6, we can do two input operations, and then test the error state, skipping a line. Since there is no equivalent to countTokens to check if there are exactly two
179
endl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Figure 9-9
tokens, we attempt to read a string, which should fail. If it succeeds, we print an error message and go on to the next line. Note carefully that in this code, each iteration of the loop creates
a new istringstream object (on the runtime stack), destroying the original.
Declaring istringstream inside the while loop has the advantage that since each iteration creates a fresh istringstream, we do not have to clear the error state. The obvious disadvantage is the repeated calls to constructors and destructors. An alternative is to use the str
method of istringstream to change str. Then we can put the istringstream object
outside the loop, uninitialized, and set its string each time around the loop. However, now we
must clear the error state. Figure 9-10 shows this approach.
For ostringstream, writes can be directed to a string instead of standard output, files,
or other places. To extract the string from the ostringstream, invoke the str method. A
classic example is the conversion of any (printable) type to a string as shown in Figure 9-11.
9.8 endl
Because endl writes a newline and flushes the stream, using endl can be time-consuming
180
1
2
3
4
5
6
7
8
9
10
11
12
Figure 9-10
when there is significant disk-bound (or network bound I/O). In such a case, writing the "\n"
character directly can be more efficient.
One one of our machines, we copied from one file to another a line at a time, and measured the time spent writing. In this program which is almost exclusively I/O, we observed that
using endl is four times slower for writing, The files were approximately 2,000,000 lines, with
78,000,000 characters. When each line was written using endl, the time spent writing was
approximately 40 seconds. Ending the line with "\n" reduced the time to 10 seconds. Using the
character \n instead of a string "\n" did not affect the running time.
Needless to say, this consideration is important only if a significant portion of the running
time is spend performing I/O.
9.9 Serialization
Serialization is not part of standard C++. Each implementation might provide some customized
support for serialization, but certainly objects written by the implementation could only be read
in the same implementation.
Figure 9-11
Generic toString
Exercises
181
9.11 Exercises
1. Describe the I/O hierarchy in C++.
2. What are the standard predefined I/O streams?
3. How are errors handled in the C++ I/O library?
4. What is wrong with the while(!fin.eof()) idiom?
5. What happens if a file is not closed by the user?
6. List some of the I/O manipulators.
7. What are istringstream and ostringstream?
8. Using Figure 9-11, implement a function template
string operator+( const string & lhs, const Object & rhs )
182
11. Write a program that processes include directives. Since an included file may itself contain
include directives, the basic algorithm is recursive.
12. Write a method that takes the name of a file as a parameter, and reverses the contents of
the file.
H A P T E R
1 0
183
184
ordered. Some collections allow duplicates; others do not. All containers support the following
operations:
int size( ) const
void clear( )
bool empty( ) const
size returns the number of elements in the container; empty returns true if the container contains no elements and false otherwise.
Unlike Java, there is no universal add method; different containers use different names.
Some of the container class templates are vector, deque, list, set, multiset, map,
multimap, and priority_queue.
vector is the equivalent of an ArrayList. The add operation for vector is named
push_back. vector supports operator[]. list is the equivalent of LinkedList. Its
add operation is also named push_back, but list does not support operator[]. However, list does support push_front. deque is an array-based data structure that supports
efficient indexing with operator[], and both push_front and push_back, all in constant time per operation.
set is the equivalent of TreeSet. The add operator for set is insert. multiset
allows duplicates, whereas set does not. map is the equivalent of TreeMap. The add operation is insert, but one must pass the key and value in a single pair object. However, map also
provides an overloaded operator[] that makes the map look just like an array. A
multimap allows duplicate keys.
The STL also contains a priority_queue class. Its operation is known as push.
Comparing these collections with Java, we see that STL supports sets and maps that contain
duplicates, as well as the priority queue, but does not support searching with hash tables.
10.1.2 Iterators
In Java, each container defines an internal iterator type, but exports it through the Iterator
interface type. In C++, each container defines several iterator types, and these specific iterator
types are used by the programmer instead of an abstract type.
For instance, if we have a vector<int>, the basic iterator type is
vector<int>::iterator. Another iterator type, vector<int>::const_iterator,
does not allow changes to the container on which the iterator is operating. This implies that the
basic iterator can be used to change the container.
All iterators are guaranteed to have at least the following set of operations:
++itr and itr++ advance the iterator itr to the next location. Both the prefix and
postfix forms are available. This does not cause any change to container. Some iterators support
--itr and itr--. Those iterators are called bidirectional iterators. Some iterators support
both itr+=k and itr+k. Those iterators are called random-access iterators. itr+=k
advances the iterator k positions. itr+k returns a new iterator that is k positions ahead of itr.
1
2
3
4
5
6
7
8
185
Figure 10-1
*itr returns a reference to the container object that itr is currently representing. The
reference that is returned is modifiable for basic iterators, but is not modifiable (i.e. a constant
reference) for const_iterators.
itr1==itr2 returns true if iterators itr1 and itr2 refer to the same position in the
same container and false otherwise.
itr1!=itr2 returns true if iterators itr1 and itr2 refer to different positions or different containers and false otherwise.
In order to use an iterator, we must obtain one from a container. The way this is done in
C++ is that the container has two methods, begin and end that return iterators. Each collection
defines four methods:
iterator begin( );
const_iterator begin( ) const;
iterator end( );
const_iterator end( ) const;
begin returns an iterator that is positioned at the first item in the container. end returns
an iterator that is position at the endmarker, which represents a position one past the last element
in the container. For instance, on an empty container, begin and end return the same position.
begin and end both make use of the fact that identical-looking methods can be overloaded if one is an accessor and one is a mutator. So if begin is invoked on a constant container, we will get a const_iterator, which wont support any changes to the container. If
begin is invoked on a mutable container, we will get an iterator, which can be used to
change the container.
Typically we initialize a local iterator to be a copy of the begin iterator, and have it step
through the container, stopping as soon as it hits the endmarker. As an example, Figure 10-1
shows a print function that prints the elements of any container, provided that the elements in
the container have provided an operator<<. If the container is a set, its elements are output
in sorted order. Figure 10-2 illustrates four different containers that invoke the print function,
along with the expected output (in comments). Observe that both set and multiset output in
sorted order, with multiset allowing the second insertion of foo.
186
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <vector>
#include <list>
#include <set>
#include <string>
using namespace std;
int main( )
{
vector<int> vec;
vec.push_back( 3 ); vec.push_back( 4 );
list<double> lst;
lst.push_back( 3.14 ); lst.push_front( 6.28 );
set<string> s;
s.insert( "foo" ); s.insert( "bar" ); s.insert( "foo" );
multiset<string> ms;
ms.insert( "foo" ); ms.insert( "bar" ); ms.insert( "foo" );
print(
print(
print(
print(
vec );
lst );
s );
ms );
//
//
//
//
3 4
6.28 3.14
bar foo
bar foo foo
return 0;
}
Figure 10-2
10.1.3 Pairs
If we try to print a map, the program will not compile immediately because the elements of a
map are pairs of keys and values. If operator<< is overloaded for pair, then we can in fact
use the print routine. Figure 10-3 illustrates the general strategy.
As expected, pair is a class template, and stores two data members first and
second, which can be directly accessed, without invoking methods. So we can easily overload
operator<< to output a pair, assuming its components first and second have done so
too.
In Figure 10-4 we can create a map that stores the name of a city and the zip code, both as
strings. (The zip code cannot be an int, since many zip codes begin with 0). Lines 10 and 11
show that a map stores pair objects, and the pair objects can be added by calling insert. Line
12 shows the much more natural equivalent that makes use of operator overloading. We describe
maps in more detail in Section 10.7.
1
2
3
4
5
6
7
8
9
187
#include <iostream>
#include <map>
using namespace std;
template <typename Type1, typename Type2>
ostream & operator<<( ostream & out, const pair<Type1,Type2> & p )
{
return out << "[" << p.first << "," << p.second << "]";
}
Figure 10-3
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main( )
{
map<string,string> zip;
zip.insert( pair<string,string>( "Miami", "33199" ) );
zip.insert( pair<string,string>( "Princeton", "08544" ) );
zip[ "Boston" ] = "02134";
// Prints: [Boston,02134] [Miami,33199] [Princeton,08544]
print( zip );
return 0;
}
Figure 10-4
188
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Figure 10-5
Heterogeneous containers
constructs a new vector clone with the same elements as any container original.
189
costly than expected. Thus lists in C++ have the additional C++ advantage of generally
requiring less data movement compared to arrays when objects are large and a reasonable estimate of the vector capacity is not available at the start.
The basic operations that are supported by both containers are:
void push_back( const Object & x );
Object & back( );
void pop_back( );
Object & front( );
iterator insert( iterator pos, const Object & x );
iterator erase( iterator pos );
iterator erase( iterator start, iterator end );
push_back adds x to the end of the container. back returns the object at the end of the
container; an accessor is also defined that returns a constant reference. pop_back removes the
object at the end of the container. front returns the object at the front of the container.
insert adds x into the container, prior to the position given by the iterator. This is a constant
time operation for list, but not for vector or deque. insert returns an iterator representing the position of the inserted item.
Adding x to the front of c could be implemented as
c.insert( c.begin( ), x );
In the second call, observe that the return value from c.end() is an unnamed temporary whose
position represents the endmarker. Thus the -- operator changes the state of the unnamed temporary to view the last item in the container. After the erase method is called, the unnamed
temporarys destructor is invoked.
Two-parameter erase removes all items beginning at position start, up to but not
including end. The idea of a range being half-open-ended is similar to substring operations in
java.util.String. It means that an entire container can be erased by the call:
c.erase( c.begin( ), c.end( ) );
190
This code makes use of the fact that corresponding to the endmarker is the beginmarker,
which is a valid position, but whose contents should not be accessed. One can see that traversing
in reverse order is messy. We will revisit this in Section 10.4.
For deque and list, two additional operations are available with expected semantics:
void push_front( const Object & x );
void pop_front( );
The list also provides a slice operation that allows the transfer of a sublist to somewhere else.
For vector and deque, additional operations include
Object & operator[] ( int idx );
Object & at( int idx );
int capacity( ) const;
void reserve( int newCapacity );
// vector only
operator[] and at come in both accessor and mutator versions and support array
indexing. at does bounds checking. capacity returns the internal capacity, and reserve
can be used to set the new capacity. reserve is only available for vector. If a good estimate
is available, it can be used to avoid array expansion.
In addition to the usual set of constructors, a vector can be constructed with either an
initial size, or an initial size and an initial value for all the elements. For instance,
vector<int> v1( 20 );
vector<int> v2( 30, 37 );
// 20 ints
// 30 ints, all with value 37
191
192
1
2
3
4
5
6
7
8
9
10
11
12
Figure 10-6
resenting the last position (not the endmarker), and the beginmarker (not the first position). The
reverse iterator is reverse_iterator or const_reverse_iterator, as appropriate,
and for a reverse iterator, ++ moves toward the front while -- moves toward the rear, opposite
to normal iterator semantics.
10.4.2 Stream Iterators
A stream iterator allows us to repeatedly read items or write items. Outputting items is discussed in Section 10.10 in conjunction with the copy algorithm. Use of the input stream iterator
is best done with an example.
Suppose we want to read words from a file "dict.txt" into a set. Assume we are not
concerned with errors. Then we already know we can use the following code:
ifstream fin( "dict.txt" );
string x;
set<string> s;
while( fin >> x )
1
2
3
4
5
6
7
8
9
10
s.insert( x );
// Print the contents of Container c in reverse
template <typename Container>
void printReverse( const Container & c, ostream & out = cout )
{
typename Container::const_reverse_iterator itr;
for( itr = c.rbegin( ); itr != c.rend( ); ++itr )
out << *itr << " ";
out << endl;
}
Figure 10-7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
193
#include <queue>
#include <iostream>
#include <list>
using namespace std;
int main( )
{
queue<int,list<int> > q;
q.push( 37 ); q.push( 111 );
for( ; !q.empty( ); q.pop( ) )
cout << q.front( ) << endl;
return 0;
}
Figure 10-8
Recall that the containers have constructors that take two iterators. The stream iterator
allows us to rewrite this code, passing to it a pair of istream_iterator<string> objects.
The first object represents the start of the file, the second represents the end-of-file marker. The
code becomes:
ifstream fin( "dict.txt" )
set<string> s( istream_iterator<string>( fin ),
istream_iterator<string>( ) );
10.6 Sets
The set class template in C++ behaves in the same manner as Java. A set does not allow
duplicates, and by default, iteration of a set views items in the default order. However, sets
can use a function object to override the default ordering.
194
insert adds x to the set. Since duplicates are not allowed, if x is not present, the
returned pair will contain the iterator representing the already contained x, and false. Otherwise, it will contain the iterator representing the newly inserted x, and true. The two-parameter insert allows specification of a hint, representing the position where x should go. If the
hint is accurate, the insertion is fast. If not, the insertion still has performance that is comparable
to the one-parameter insert. For instance, the following code might be faster using the twoparameter insert than the one-parameter insert:
set<int> s;
for( int i = 0; i < 1000000; i++ )
s.insert( s.end( ), i );
find returns an iterator representing the position of x in the set. If x is not found, the
endmarker is returned. The various erase routines behave in the same manner as for the
sequence containers in Section 10.2, except that erase that takes Object x returns the number of items removed. In a set this is 0 or 1, but could be larger in a multiset.
1
2
3
4
5
6
7
8
Figure 10-9
Sets
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
195
class PtrToPersonLess
{
public:
bool operator() ( const Person *lhs, const Person *rhs ) const
{ return lhs->getSsn( ) < rhs->getSsn( ); }
};
int main( )
{
set<Person *, PtrToPersonLess> s;
s.insert( new Person( 987654321, "Bob" ) );
s.insert( new Student( 123456789, "Jane", 4.0 ) );
print( s );
return 0;
}
lower_bound returns an iterator to the first element in the set with a key that is greater
than or equal to x. upper_bound returns an iterator to the first element in the set with a key
that is greater than x. equal_range returns a pair of iterators representing lower_bound
and upper_bound. These routines are typically most useful in multisets.
10.6.3 multisets
A multiset is like a set except that duplicates are allowed. The return type of insert is
modified to indicate that the insert always succeeds. As a result, we no longer need a pair,
but can simply return an iterator representing the newly inserted x.
iterator insert( const Object & x );
iterator insert( iterator hint, const Object & x );
For the multiset, the erase member function that takes an Object x removes all
occurrences of x. To simply remove one occurrence, use the erase member function that takes
an iterator. To find all occurrences of x, we cannot simply call find; that returns an iterator referencing one occurrence (if there is one), but which specific occurrence is returned is not guaranteed. Instead, the range returned by lower_bound and upper_bound (with
upper_bound not included) contains all of occurrences of x; typically this is obtained by a
call to equal_range.
10.6.4 Using Function Objects to Change Default Ordering
Just as a Java TreeSet can be specified to use either the default ordering or an ordering given
by a comparator, a C++ set uses either a default ordering (operator<) or one provided by a
196
map<string,double> salaries;
1
2
3
4
5
6
7
8
9
10
11
12
function object. Recall from Section 7.6.3 that the function object idiom in C++ is implemented
by providing a class that contains an overloaded operator(), and then instantiating a template with the class name as a template parameter. Figure 10-10 illustrates the idiom, in which
the code seen earlier in Figure 10-5 is adapted to use a set instead of a vector.
10.7 Maps
As we have already seen, a map behaves like a set instantiated with a pair representing a key
and value, with a comparison function that refers only to the key. Thus it supports all of the set
operations, including insert, but as we saw in Figure 10-4, we must insert a properly instantiated pair. The find operation for maps requires only a key, but the iterator that it returns references a pair. Similarly, erase requires only a key, and otherwise behaves like the sets
erase.
Most importantly, the map overloads the array indexing operator[]:
ValueType & operator[] ( const KeyType & key )
The semantics of operator[] are as follows. If the key is present in the map, a reference to the value is returned. If the key is not present in the map, it is inserted with a default
value into the map, and then a reference to the inserted default value is returned. The default
value is obtained by applying a zero-parameter constructor, or is zero for the primitive types.
These semantics do not allow an accessor version of operator[], and so operator[] cannot be used on a map that is constant. For instance, if a map is passed by constant reference,
inside the routine, operator[] is unusable. This could be a case where casting away constness is useful.
The code snippet in Figure 10-11 illustrates two techniques to access items in a map. First
observe that at line 3, the left-hand-side invokes operator[], thus inserting "Pat" and a
double of value 0 into the map, returning a reference to that double. Then the assignment
changes that double, inside the map, to 75000. Line 4 outputs 75000. Unfortunately, line 5
inserts "Jan" and a salary of 0.0 into the map, and then prints it. This may or may not be the
STL Example
197
proper thing to, depending on the application. If it is important to distinguish between items that
are in the map and not in the map, or if it is important to not insert into the map (because it is
immutable), then an alternate approach shown at lines 7 to 12 can be used. There we see a call to
find. If the key is not found, the iterator is the endmarker, and can be tested. If the key is
found, we can access the second item in pair referenced by the iterator, which is the value for
the key. We could make a change to itr->second if instead of a const_iterator, itr is
an iterator.
10.7.1 Multimaps
A multimap is a map in which duplicate keys are allowed. In Java, the effect of a multimap is
achieved by using a map whose values are Lists.
Otherwise, multimaps behave like maps but do not support operator[].
will read from file data.txt, and send the list of words to standard output. For simplicity, we
assume that a word is any sequence of non-white space characters, and thus can easily be
extracted by operator>>.
The idea is to use a map, in which the keys are the words and the line numbers are stored
in a vector<int> or list<int>. Well use vector<int>, but typedef
vector<int> as LList, so we can change it later without much effort. When we see an
existing word, we add its line number to the list that is stored in the map. If the word is new, we
add its line number to the empty list that is created when the word is inserted into the map. After
all the words have been read, we can iterate through the map, and output the words and their line
numbers, and since the keys are sorted, the output will be sorted by words. The line numbers
themselves are output with an iterator.
Complete code is shown in Figure 10-12. The previously mentioned typedef is shown at
line 10. We discuss printConcordance first. The loop from lines 25 to 32 populates the
map. We read one line at a time and then create an istreamstring object at line 27. The
loop at lines 30 and 31 steps though each word on the line and adds it to the map. The tricky
code is clearly line 31. If the word is already seen, then operator[] returns a reference to the
list that is in the map, and thus push_back simply adds a new line number to the end of the
list. If the word is new, operator[] adds it into the map, with an empty list, and returns a reference to the empty list that is now in the map. push_back makes this a one-element list.
We can then step through the map with the standard loop at lines 34 to 36. operator<<
for the pair that is in the map is overloaded at lines 11 to 17.
198
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>
#include <fstream>
#include <sstream>
#include <map>
#include <string>
#include <vector>
#include <iomanip>
using namespace std;
typedef vector<int> LList;
ostream & operator<<( ostream & out,
const pair<string,LList> & rhs )
{
out << left << setw( 20 ) << rhs.first;
print( rhs.second, out );
// Figure 10-1
return out;
}
void printConcordance( istream & in, ostream & out )
{
string
oneLine;
map<string,LList> wordMap;
// Read the words; add them to wordMap
for( int lineNum = 1; getline( in, oneLine ); lineNum++ )
{
istringstream st( oneLine );
string word;
while( st >> word )
wordMap[ word ].push_back( lineNum );
}
map<string,LList>::iterator itr;
for( itr = wordMap.begin( ); itr != wordMap.end( ); ++itr )
out << *itr << endl;
}
First, we print the word which is the first data member, and second we output the list of
line numbers, which is the second data member.
Priority Queue
199
push adds x to the priority queue, top returns the largest element in the priority queue,
and pop removes the largest element from the priority queue. Duplicates are allowed; if there
are several largest elements, only one of them is removed.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <vector>
#include <queue>
#include <functional>
#include <string>
#include <iostream>
using namespace std;
// Empty the priority queue and print its contents.
template <typename PQueue>
void dump( const string & msg, PQueue & pq )
{
if( pq.empty( ) )
cout << msg << " is empty" << endl;
else
{
cout << msg << ": " << pq.top( );
pq.pop( );
while( !pq.empty( ) )
{
cout << " " << pq.top( );
pq.pop( );
}
cout << endl;
}
}
int main( )
{
priority_queue<int>
maxpq;
priority_queue<int,vector<int>,greater<int> > minpq;
minpq.push( 8 ); minpq.push( 5 ); minpq.push( 5 );
maxpq.push( 8 ); maxpq.push( 5 ); maxpq.push( 5 );
dump( "minpq", minpq );
dump( "maxpq", maxpq );
return 0;
}
// minpq: 5 5 8
// maxpq: 8 5 5
200
Sometimes priority queues are set up to remove and access the smallest item instead of the
largest item. In such a case, the priority queue can be instantiated with an appropriate greater
function object to override the default ordering.
The priority queue template is instantiated with an item type, the container type (as in
stack and queue), and the comparator, with defaults allowed for the last two parameters. In
Figure 10-13, line 29 shows the default instantiation of priority_queue, that allows access
to the largest items, while line 30 shows an instantiation that allows access to the smallest item.
The iterators must support random access. The sort algorithm does not guarantee that equal
items retain their original order. For that, we can use stable_sort instead of sort.
As an example, in
sort( v.begin( ), v.end( ) );
sort( v.begin( ), v.end( ), greater<int>( ) );
sort( v.begin( ), v.begin( ) + ( v.end( ) - v.begin( ) ) / 2 );
the first call sorts the entire container, v, in non-decreasing order. The second call sorts the entire
container in non-increasing order. The third call sorts the first half of the container in nondecreasing order. Note that (v.begin()+v.end())/2 is not allowed; instead we can compute a separation distance, halve it, and add it to the begin iterator.
The sorting algorithm is generally quicksort, which yields an O( N log N ) algorithm on
average. However, O( N log N ) worst-case performance is not guaranteed. In addition to sorting, there are also algorithms for selection, shuffling, partitioning, partial sorting, reversing,
rotating, and merging,
10.10.2 Searching
Several generic searching algorithms are available for containers. The two most basic are:
Iterator find( Iterator begin, Iterator end, const Object & x );
Iterator find_if( Iterator begin, Iterator end, Predicate pred );
Generic Algorithms
201
find returns an iterator representing the first occurrence of x in the range specified by begin
and end, or end if x is not found. find_if returns an iterator representing the first occurrence of an object for which the function object pred would return true, or end if no match is
found.
For instance, suppose we want to find the first occurrence of a string of length exactly 9 in
a vector<string>. First, we define a function object that expresses this condition:
class StringLengthComp
{
public:
bool operator() ( const string & s ) const
{ return s.length( ) == 9; }
};
202
find_first_of takes four iterators representing two sequences (the first sequence and
the second sequence). It returns an iterator representing the first occurrence of any of the elements in the second sequence. For instance,
vector<int> wins; //
wins.push_back( 37337
wins.push_back( 46521
wins.push_back( 53810
vector<int> myNumbers;
...
// populate myNumbers
vector<int>::const_iterator itr;
itr = find_first_of( myNumbers.begin( ), myNumbers.end( ),
wins.begin( ), wins.end( ) );
searches the vector myNumbers for any of the numbers in wins and returns an iterator representing the first occurrence of such a number in myNumbers.
A fifth parameter, a predicate, can be used to decide if an item in the second sequence is a
match for an item in the first sequence. Of course, the default is equal_to.
10.10.3 Unary Binder Adapters
The find_if algorithm requires that we pass a function object that returns a Boolean. This is
called a predicate. As we know, the library defines several templates that are predicates: less,
equal_to, etc. However these predicates are not directly suitable for find_if, because their
operator() requires two parameters. find_if requires unary predicates, which are predicates that take only one parameter.
A unary binder adapter is function that takes a binary predicate, such as less (appropriately instantiated) and a value, and constructs a unary predicate in which the value is used as one
of the two parameters to the binary predicate. This is called binding a parameter. We can bind
the first parameter with bind1st or we can bind the second parameter with bind2nd.
For instance, the result of bind2nd(less<int>(),0) is a new unary predicate. If
this new function object is f1, then any call f1(x) is interpreted as less<int>()(x,0).
Similarly, if f2 represents the function object given by bind1st(0,less<int>()), then
any call f2(x) is interpreted as less<int>()(0,x).
If we want to find the first negative number in a vector, v, of ints, we can use:
itr = find_if( v.begin( ), v.end( ), bind2nd( less<int>( ), 0 ) );
10.10.4 Copying
There are several algorithms that deal with copying. These include copy, copy_backwards,
remove, remove_copy, remove_if, remove_copy_if, replace, replace_copy,
replace_if, replace_copy_if, unique, unique_copy. Method names that have
copy and non-copy versions differ in whether they change the original, or leave the original
unchanged and produce a new sequence. A typical routine is copy:
BitSets
203
The copy algorithm copies the range specified by begin and end to the target. The target
must be modifiable and be large enough to store the result. For instance, the code
vector<int> source( 10, 37 );
vector<int> target( 10 );
ostream_iterator<int> out( cout, "\n" );
copy( source.begin( ), source.end( ), target.begin( ) );
copy( source.begin( ), source.end( ), out );
copies the contents of source to target, changing all those values to 37, and then copies the
contents to the standard output stream, printing newlines after each item.
On the other hand, if we had
vector<int> smallArray;
// size is 0
copy( source.begin( ), source.end( ), smallArray.begin( ) );
the call to copy generates difficulties at runtime because the target array is not large enough.
10.10.5 Inserter Adapters
The last example in Section 10.10.4 illustrates a problem with copying. Often, we want the target of a copy (for instance replace_copy_if) to be an initially empty container. Rather than
providing another set of functions, C++ provides inserter adapters. The idea of an inserter
adapter is that we create an iterator, whose operator= is altered.
The most common adapter is the back_inserter. The back_inserter is constructed with a sequence container, and generates an iterator whose assignment operator is
replaced by a push_back operation. As a result, the effect of
copy( source.begin( ), source.end( ), back_inserter( target ) );
10.11BitSets
Like Java, C++ has bitwise operators that can manipulate a set of bits stored in a primitive type
and a bitset class template.
The class template, which is declared in the standard header bitset, is instantiated with
the number of bits to be stored (this must be a compile-time constant), and indexing starts at 0.
Bits can be accessed with test, or alternatively, with the array indexing operator[]. set
and unset can be used to turn on or off a particular bit; with no parameters these methods
204
affect all bits. Alternatively, operator[] can be used on the left-hand side of an assignment.
Also overloaded are the standard bitwise operators, that allow bitwise operations on bitset
types. The bitsets involved in those operations must have identical sizes.
The bitset has a look and feel that is similar to both vector and map; but, it can be
expected to be more efficient that vector, set, or map. However, a set or map could be
space-efficient for cases where there are many bits, but only a few are ever set to be on.
10.12Key Points
Standard STL containers include the sequence containers: vector, list, and deque,
and also set, multiset, map, multimap, and priority_queue. multisets
allow duplicates and multimaps allow duplicate keys.
maps store keys and values. operator[] returns the value associated with a key, and if
the key is not present, it is inserted with a default value, that is then returned.
Containers can be accessed by iterators, which are more powerful than their Java counterparts.
Significant compile-time type checking is performed by the STL.
Little runtime error checking is performed by the STL.
There are several general types of iterators: forward iterators, bidirectional iterators, random access iterators, and stream iterators are the most common. Additionally, there are
const_iterators, reverse_iterators, and const_reverse_iterators.
Iterators use operator overloading extensively. The common operators are ++, *, =, ==,
and !=. Bidirectional iterators allow --. Random access iterators allow - and +, = and
+=.
Stream iterators allow repeated iteration over an input or output stream.
Each container has a begin and end member function that yields iterators that represent
the beginning of the container and the endmarker of the container. There are both accessor
and mutator versions of begin and end.
pair is a class template that stores the first item and second item as public data. The
pair is used in the map, which is a set of pairs, and also in the return type of some set
member functions.
If a standard container is storing a heterogeneous collection, it should store pointers to the
objects.
Six function objects are defined as class templates in the standard header functional.
These are less, greater, equal_to, not_equal_to, great_equal, and
less_equal.
The unary adapters allow the conversion of the standard binary predicates to unary predicates, by supplying one of the parameters to the binary predicate.
The inserter adapters allow copying into empty containers by converting the assignment
operator of an iterator into an insertion operation on the container.
Exercises
205
The standard library includes over 60 function templates for sorting, searching, copying,
and many other generic algorithms.
C++ has a bitset class template. To use it, the number of bits must be known at compile
time. If this is not possible, alternatives such as vector<bool>, set<int> (containing only the true bits), or map<int,bool> can be used, but might not be as fast.
10.13Exercises
1. How does the STL differ from the Java Collections API?
2. Describe the functionality of iterators in C++.
3. What are the different types of iterators?
4. What does end return?
5. What kind of error checks are performed by STL routines?
6. What is a const_iterator and how is it used?
7. What is a reverse iterator and how is it used?
8. What is a stream iterator and how is it used?
9. What is the difference between a set and a multiset?
10. Why is there no operator[] accessor for map?
11. What is a unary binder adapter?
12. What is an inserter adapter?
13. Describe the general categories of STL algorithms and give an example or an algorithm in
each category.
14. In Exercise 6.19, make two modifications. First, in the Employee class, add
operator< that orders employees by name. Then change the implementation of Roster
to use a multiset of Employee * (ordered by name). Part of the multiset template instantiation includes an appropriate function object. The multiset print routine
should output employees in sorted order (by name).
15. Implement a spelling checker. Prompt the user for the name of a file that stores a dictionary of words. Then prompt for the name of a file that you want to spell-check. Any word
that is not in the dictionary is considered to be misspelled. Output, in sorted order, each
misspelled word and the line number(s) on which it occurs. If a word is misspelled more
than once, it is listed once, but with several line numbers. Of course you should verify that
files open correctly. Use the following rule to determine what a word is: The input is considered to be a sequence of tokens separated by whitespace. Any token that ends with a
single period, question mark, comma, semicolon, or colon should have the punctuation
removed. After doing this, any token that contains letters only is considered a word. Convert this word to lower case.
16. Implement the sort template that takes a pair of iterators and a comparator, using any
simple sorting algorithm. Then implement the sort template that takes a pair of iterators.
In order to do this, and reuse the three-parameter sort, you will need to define a phantom
206
four-parameter sort template that takes an object of the type to be sorted as the fourth
parameter. The two-parameter sort will invoke the four-parameter sort, which in turn
will invoke a three-parameter sort, with less<Object>() as the third parameter.
H A P T E R
1 1
has the compiler allocate space to store three integers, namely a[0], a[1], and a[2]. No
index range checking is performed in C++, so an access out of the array index bounds is not
caught by the compiler or runtime system; instead, undefined and occasionally mysterious
behavior occurs. Furthermore, if the array is passed as an actual argument to a function, the
function has no idea how large the array is unless an additional parameter is passed. Finally,
arrays cannot be copied by the = operator. In this section we will stick with the core language
207
208
features of arrays and pointers and discuss why these restrictions come into play.
11.1.1 The C++ Implementation: An Array Name Is a Pointer
When a new array is allocated, the compiler multiplies the size in bytes of the type in the declaration by the array size (the integer constant between the []) to decide how much memory it
needs to set aside. This is essentially the only use for the size component. In fact, after the array
is allocated, with minor exceptions, the size is irrelevant because the name of the array represents a pointer to the beginning of allocated memory for that array. This is illustrated in Figure
11-1.
Suppose we have the declarations
int a[ 3 ];
int i;
The compiler allocates memory as follows: First, three integers are set aside for the array object.
These are referenced by a[0], a[1], and a[2]. The objects in the array are guaranteed to be
stored in one contiguous block of memory. Thus if a[0] is stored at memory location 1000 and
integers require four bytes, it is guaranteed that a[1] is located at memory location 1004 and
a[2] at memory location 1008. (Java does not make this guarantee, in order to allow the garbage collector the option of storing parts of the array separately if needed to minimize memory
fragmentation). Finally, the compiler allocates storage for the object i. One possibility is shown
in Figure 11-1, where i is allocated the next available memory slot.
For any i, we can deduce that a[i] would be stored at memory location 1000 + 4i. The
value stored in a is exactly equal to &a[0]; this equivalence is always guaranteed and tells us
that a is actually a pointer. Note also that &a is not the same as &a[0]. Now we can see that to
access the item a[i], the compiler needs only to fetch the value of a and add to it 4i.
&a[0] (1000)
a[0]
&a[1] (1004)
a[1]
&a[2] (1008)
a[2]
&i
(1012)
i
...
local constant
Figure 11-1
a=1000
Memory model for arrays (assumes 4 byte int); declaration is int a[3]; int i;
Primitive Arrays
209
Now that we have seen how arrays are manipulated in C++, we can see why some of the
limitations discussed earlier occur, and we can also see how arrays are passed as function parameters. First we have the problem of checking that the index is in range. Performing the bounds
check would require that we store the array size in an additional parameter. Certainly this is feasible, but it does incur both time and space overhead. In a common application of arrays (short
strings), the overhead could be significant. As we have mentioned in Section 2.2.1 and illustrated in Section 8.1, the lack of range checking can cause serious problems such as off-by-one
errors in array indexing that can lead to bugs that are very difficult to spot. (If index range
checking is crucial, use the vectors at member function).
The second limitation of the basic array (is solved by vector) is array copying. Suppose
that a and b are arrays of the same type. In many languages, if the arrays are also the same size,
the statement a=b would perform an element-by-element copy of the array b into the array a. In
C++ this statement is illegal because a and b represent constant pointers to the start of their
respective arrays, specifically &a[0] and &b[0]. Then a=b is an attempt to change where a
points, rather than copying the contents of array b into array a. What makes the statement illegal, rather than legal but wrong, is that a cannot be reassigned to point somewhere else because
it is essentially a constant object. The only way to copy two arrays is to do it element by element; there is no shorthand. A similar argument shows that the expression a==b does not evaluate to true if and only if each element of a matches the corresponding element of b. Instead,
this expression is legal. It evaluates to true if and only if a and b represent the same memory
location (that is, they refer to the same array).
Finally, an array can be used as a parameter to a function, and the rules follow logically
from our understanding that an array name is little more than a pointer. Suppose we have a function functionCall that accepts one array of int as its parameter. The caller/callee views are
functionCall( actualArray );
// Function Call
functionCall( int formalArray[ ] ) // Function Declaration
Note that in the function declaration, the brackets serve only as a type declaration, in the same
way that int does. Note that the [] must follow the formal parameter, unlike Java where it can
either follow or precede the formal parameter. In the function call only the name of the array is
passed; there are no brackets. In accordance with the call-by-value conventions of C++, the
value of actualArray is copied into formalArray. Because actualArray represents
the memory location where the entire array actualArray is stored, formalArray[i]
accesses actualArray[i]. This means that the variables represented by the indexed array
are modifiable. Thus an array, when considered as an aggregate, is passed by reference. Furthermore, any size component in the formalArray declaration is ignored, and the size of the
actual array is unknown. If the size is needed, it must be passed as an additional parameter.
Note that passing the aggregate by reference means that functionCall can change elements in the array. We can use the const directive to attempt to disallow this (but this technique is not foolproof because of the ability to cast away const-ness):
functionCall( const int formalArray[ ] );
210
like an array, except that no memory is allocated by the compiler for the array. We know that the
new operator allows us to obtain memory from the memory heap as the program runs. An alternative form is the array new operator, new[], which causes the creation of an array of objects
from the memory heap, invoking a zero-parameter constructor for each array item (unfortunately, for ints, this does not always guarantee any initial value). Thus, the expression
new int [ SIZE ]
allocates the array. The expression evaluates to the address where the start of that memory
resides. It may be assigned only to an int * object, as in
int *a2 = new int [ SIZE ];
As a result, a2 is virtually indistinguishable from a1. The new operator is type-safe, meaning
that
int *a2 = new char[ SIZE ];
there is no difference with respect to what can apepar on the left-hand side. A more important
difference is that SIZE does not have to be a compile-time constant when we use new.
But the most important difference is that the memory for a1 is taken from a different
source than a2, which is allocated from the memory heap and eventually must be released to
avoid a memory leak. The source of the memory, however, is otherwise transparent to the user.
Thus, when a1 is a local variable and the function in which it is declared returns (that is,
when a1 exits scope), the memory associated with the array is reclaimed automatically by the
system. a1 exits scope when the block in which it is declared is exited. For example, in Figure
Primitive Arrays
211
a1
a2
Figure 11-3
11-2, a1 is a local variable in a function f. When f returns, the entire contents of the a1 object,
including the memory associated with the array, is freed. In contrast, when a2 exits scope only
the memory associated with the pointer is freed; the memory allocated by new is now unreferenced, and we have a memory leak. The memory is claimed as used, but unreferenced, and will
not be used to satisfy future new requests, and if the array contains class type objects, those
objects destructors will not have been invoked. The situation is shown graphically in Figure 113.
To recycle the memory, we must use the delete[] operator. The syntax is
delete [ ] a2;
The [] is absolutely necessary here to ensure that all of the objects in the allocated array
have their destructors called prior to reclaiming of the memory for array a2. Without the [] it is
possible that only a2[0]s destructor is called, and the remaing items in the array do not have
their destructors called, nor will a2s memory be reclaimed, which is hardly what we intend.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void f( int i )
{
int a1[ 10 ];
int *a2 = new int [ 10 ];
...
g( a1 );
g( a2 );
//
//
//
//
Figure 11-2
212
With new and delete we have to manage the memory ourselves rather than allow the compiler to do it for us. Why then, would we be interested in this? The answer is that by managing
memory ourselves, we can build expanding arrays. Suppose, for example, that in Figure 11-2 we
decide, after the declarations but before the calls to g at lines 7 and 8, that we really wanted 12
ints instead of 10. In the case of a1 we are stuck, and the call at line 7 cannot work. However,
with a2 we have an alternative, as illustrated by the following maneuver:
int *original = a2;
// 1. Save pointer to the original
a2 = new int [ 12 ];
// 2. Have a2 point at more memory
for( int i = 0; i < 10; i++ ) // 3. Copy the old data over
a2[ i ] = original[ i ];
delete [ ] original;
// 4. Recycle the original array
Figure 11-4 shows the changes that result. A moments thought will convince you that this is an
expensive operation, because we copy all of the elements from original to a1. If, for
instance, this array expansion is in response to reading input, it would be inefficient to re-expand
(a)
(b)
a2
a2
Original
(c)
a2
Original
(d)
a2
Original
Figure 11-4
Array expansion: (a) starting point: a2 points at 10 integers; (b) after step 1:
original points at the 10 integers; (c) after steps 2 and 3: a2 points at 12 integers, the first 10 of which are copied from original; (d) after step 4: the 10 integers are freed
Primitive Arrays
213
every time we read a few elements. Thus when array expansion is implemented, we always
make it some multiplicative constant times as large. For instance, we might expand to make it
twice as large. In this way, when we expand the array from N items to 2N items, the cost of the N
copies can be amortized over the next N items that can be inserted into the array without an
expansion.
214
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
#include <cstdlib>
using namespace std;
// Read an unlimited number of ints with no attempts at error
// recovery; return a pointer to the data, and set ItemsRead
int * getInts( int & itemsRead )
{
int arraySize = 0;
int inputVal;
int *array = NULL;
// Initialize to NULL pointer
itemsRead = 0;
cout << "Enter any number of integers: ";
while( cin >> inputVal )
{
if( itemsRead == arraySize )
{
// Array doubling code
int *original = array;
array = new int[ arraySize * 2 + 1 ];
for( int i = 0; i < arraySize; i++ )
array[ i ] = original[ i ];
delete [ ] original; // Safe if Original is NULL
arraySize = arraySize * 2 + 1;
}
array[ itemsRead++ ] = inputVal;
}
return array;
}
int main( )
{
int *array;
int numItems;
array = getInts( numItems );
for( int i = 0; i < numItems; i++ )
cout << array[ i ] << endl;
return 0;
}
Figure 11-5
To make things more concrete, Figure 11-5 shows a program that reads an unlimited number of integers from the standard input and stores the result in a dynamically expanding array.
The function declaration for getInts tells us that it returns the address where the array will
Primitive Strings
215
reside, and it sets a reference parameter itemsRead to indicate how many items were actually
read.
At the start of getInts, itemsRead is set to 0, as is the initial arraySize. We
repeatedly read new items at line 15. If the array is full, as indicated by a successful test at line
17, then the array is expanded. Lines 19 to 23 perform the array doubling. At line 19 we save a
pointer to the currently allocated block of memory. We have to remember that the first time
through the loop, the pointer will be NULL. At line 20 we allocate a new block of memory,
roughly twice the size of the old. We add one so that the initial doubling converts a zero-sized
array to an array of size one. At line 24 we set the new array size. At line 26, the actual input
item is assigned to the array, and the number of items read is incremented. When the input fails
(for whatever reason), we merely return the pointer to the dynamically allocated memory. Note
carefully that
We do not delete[] the array.
The memory returned is somewhat larger than is actually needed. This can be easily fixed.
The main routine calls getInts, assigning the return value to a pointer.
As we can see, this is lots of work. Thats why modern C++ programmers use vector.
216
// Wrong!
// Wrong!
This follows directly from the facts that str1 and str2 are arrays and array assignment and
comparison are not supported directly by the language. Almost all of the support, in fact, is provided by the C++ library, which specifies routines that work for null-terminated strings. The
prototypes for these routines are given in the standard header cstring. Some routines of interest are shown in Figure 11-6.
strlen(str) gives the length of the string represented by str (not including the null
terminator); the length of "Nina" is four. In this and all routines, if a NULL pointer is passed,
you can expect a program crash. Notice that this is different from passing a pointer to a memory
cell that contains the '\0' character, which represents the empty string of length 0.
strcpy(lhs,rhs) performs the assignment of strings; characters in the array given by rhs
are copied into the array given by lhs until the null terminator is copied. If the string represented by lhs is not large enough to store the copy, then somebody elses memory gets overwritten.
strcpy( lhs, rhs )
The return type char * allows strcpy calls to be chained in the same way as assignments: strcpy(a,strcpy(b,c)) is much like a=b=c. strcat(lhs,rhs) appends a
copy of the string represented by rhs to the end of lhs. As with strcpy, it is the programmers responsibility to assure that lhs is pointing at sufficient memory to store the result.
strcmp compares two strings returning a negative number, zero, or a positive number, depending on whether the first string is lexicographically less than, equal to, or greater than the second.
1
2
3
4
size_t
char *
char *
int
Figure 11-6
Primitive Strings
217
C++, as described so far, provides library routines for strings but no language support. In
fact, the only language support is provided by a string constant. A string constant provides a
shorthand mechanism for specifying a sequence of characters. It automatically includes the null
terminator as an invisible last character. Any character (specified with an escape sequence if
necessary) may appear in the string constant. Thus "Nina" represents a five-character array.
Additionally, a string constant can be used as an initializer for a character array. Thus:
char name1[
] = "Nina"; // name1 is an array of five char
char name2[ 9 ] = "Nina"; // name2 is an array of nine char
char name3[ 4 ] = "Nina"; // name3 is an array of four char
In the first case the size of the array allocated for name1 is determined implicitly, while in the
second case we have over-allocated (which is necessary if we intend later to copy a longer string
into name2). The third case is wrong because we have not allocated enough memory for the
null terminator. Initialization by a string constant is a special exemption; we cannot say
char name4[ 8 ] = name1;
// ILLEGAL!
A string constant can be used in any place that both a string and a constant string can. For
instance, it may be used as the second parameter to strcpy but not as the first parameter. This
is because the declaration for strcpy does not disallow the possibility that the first parameter
might be altered (indeed, we know that it will). Because a string constant can be stored in readonly memory, allowing it to be used as a target of strcpy could result in a hardware error. Note
carefully that we can always send a nonconstant string to a parameter that expects a constant
string. Thus we have
strcpy( name2, "Mark" );
strcpy( "Mark", name2 );
strcpy( name2, name1 );
// LEGAL
// ILLEGAL!
// LEGAL
The declarations for the string routines indicate that the parameters are pointers. This follows from the fact that the name of an array is a pointer. The second parameter to strcpy is a
constant string, meaning that any string can be passed and it is guaranteed to be unchanged. The
first parameter is a non-contant string, and might be changed. Consequently, a constant string
cannot be passed; this includes string constants.
Beginners tend to take the equivalence of arrays and pointers one step too far. Recall that
the fundamental difference between an array and a pointer is that an array definition allocates
enough memory to store the array, while a pointer points to memory that is allocated elsewhere.
Because strings are arrays of characters, this distinction applies to strings. A common error is
declaring a pointer when an array is needed. As examples, consider the following declarations:
char name[ ] = "Nina";
char *name1 = "Nina";
char *name2;
The first declaration allocates five bytes for name, initializing it to a copy of the string constant
"Nina" (including the null terminator). The second declaration states merely that name1
points at the zeroth character of the string constant "Nina". In fact, the declaration is wrong
218
because we are mixing pointer types: the right side is a const char *, while the left side is
merely a char *. Some compilers will complain. The reason for this is that a subsequent
name1[ 3 ] = 'e';
is an attempt to alter the string constant. Since a string constant is supposed to be constant, this
action should not be allowed. The easiest way for the compiler to do this is to follow the convention that if a is a constant array, then a[i] is a constant also and cannot be assigned to. If the
statement
char *name1
= "Nina";
were allowed, this would be hard to enforce. By enforcing const-ness at each assignment, the
problem becomes manageable. It is legal to use
const char *name1 = "Nina";
but that is hardly the same as declaring an array to store a copy of the actual string; furthermore,
name1[3]='e' is easily seen by the compiler to be illegal in this case. A common example
where this would be used is
const char *message = "Welcome to FIU!";
Another common consequence of declaring a pointer instead of an array object is the following statement (in which we assume that name2 is declared as above):
strcpy( name2, name );
Here the programmer expects to copy name into name2 but is fooled because the declaration
for strcpy indicates that two pointers are to be passed. The call fails because name2 is just a
pointer rather than a pointer to sufficient memory to hold a copy of name. If name2 is a NULL
pointer, points at a string constant stored in read-only memory, or points at an illegal random
location, strcpy is certain to attempt to dereference it, generating an error. If name2 points at
a modifiable array (for instance, name2=name is executed), there is no problem.
All these considerations tell us that using the C++ string is a better option that primitive strings in most cases to safely hide all new uses of primitive strings inside of a string.
A string can be constructed from a const char *, and a const char * can be
extracted from a string via the member function c_str. So when interacting with older code
that expects char * and produces char *, one strategy is to create a string as soon as possible, do the string manuipulations safely with the string class, extract a const char * by
using c_str and pass that to the older code. The return value from the older code can be immediately converted into a string. For instance, suppose there is a routine
const char *getenv( const char *prop );
that expects a primitive string and returns a primitive string. We can pass it a string prop,
and assign the result to a string val, as
string val = getenv( prop.c_str( ) );
making use of the automatic implicit conversion from const char * to string.
Pointer Hopping
219
ptr
a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9]
Figure 11-7
220
// Adds 5 to *x
// True if *x is 0
// Divide *x by *y
Notice carefully that because of precedence rules, *x++ is interpreted as *(x++), not
(*x)++. The precedence of the array indexing operator tells us that if x is a pointer, all of the
following operators are applied to the indexed value of x:
5 + x[ 0 ]
0 == x[ 0 ]
++x[ 0 ]
x++[ 0 ]
x == &x[ 0 ]
//
//
//
//
//
In the last example we reiterated that x always stores the memory location of x[0]. The precedence rules are convenient here because we do not need to parenthesize, as in &(x[0]).
11.3.2 What Pointer Arithmetic Means
Suppose that x and y are pointer variables. Now that we have decided on precedence rules, we
need to know what the interpretation is for arithmetic performed on pointers. For instance, what
does it mean to multiply x by 2? The answer in most cases is that arithmetic on pointers would
be totally meaningless and is therefore illegal. Most other languages allow only comparison,
assignment, and dereferencing of pointers. C++ is somewhat more lenient.
Looking at the various operators, we see that none of the multiplicative operators makes
sense. Therefore, a pointer may not be involved in a multiplication. Note carefully that the dereferenced value can, of course, be multiplied, and that what we are restricting is computations
involving addresses.
Equality and logical operators all make sense for pointers, so they are allowed and have
obvious meanings. Two pointers are equal if they both point to NULL or they both point to the
same address. Assignment by = is allowed, as we have seen, but *=, /=, and %= are disallowed.
Therefore, the questionable operators are the additive operators (including +=, -=, ++, --) and
the relational operators (<, <=, >=, >). To make sense, all these operators need to be viewed in
the context of an array.
Figure 11-7 shows an array a, a pointer ptr, and the assignment ptr=a. The figure reinforces the idea that the value stored in a is just the memory location where the zeroth element of
the array is stored and that elements of an array are guaranteed to be stored in consecutive and
increasing memory locations. If the array a is an array of characters, a[1] is stored in memory
location a+1 because characters use one byte. Thus the expression ++ptr would increase ptr
by one, which would equal the memory location of a[1].
We see from this example that adding an integer to a pointer variable can make sense in an
array of characters. If a was an array of four-byte integers, adding 1 to ptr would make only
partial sense under our current interpretation. This is because ptr would not really be pointing
Pointer Hopping
1
2
3
4
5
6
7
8
9
10
11
12
13
14
221
Figure 11-8
Array initialization coded two ways: first by using indexing, second by using
pointer hopping
at an integer but somewhere in the middle and would be misaligned, generally leading to a hardware fault. Since that interpretation would give erroneous results, C++ uses the following interpretation: ++ptr adds the size of the pointed at object to the address stored in ptr.
This interpretation carries over to other pointer operations. The expression x=&a[3]
makes x point at a[3]. Parentheses are not needed, as mentioned earlier. The expression
y=x+4 makes y point at a[7]. We could thus use a pointer to traverse an array instead of using
the usual index iteration method. We will discuss this in Sections 11.3.3 and 11.3.4.
Although it makes sense to add or subtract an integer type from a pointer type, it does not
make sense to add two pointers. It does, however, make sense to subtract two pointers: y-x
evaluates to 4 in the example above (since subtraction is the inverse of addition). Thus pointers
can be subtracted but not added.
Given two pointers x and y, x<y is true if the object x is pointed at is at a lower address
than the object y is pointing at. Assuming that neither is pointing at NULL, this expression is
almost always meaningless unless both are pointing at elements in the same array. In that case
x<y is true if x is pointing at a lower-indexed element than y because, as we have seen, the elements of an array are guaranteed to be stored in increasing and contiguous parts of memory. This
is the only legitimate use of the relational operator on pointers, and all other uses should be
avoided. To summarize, we have the following pointer operations:
Pointers may be assigned, compared for equality (and inequality), and dereferenced in
C++, as well as almost all other languages. The operators are =, ==, !=, and *.
We can apply the prefix or postfix increment operators to a pointer, can add an integer, and
can subtract either an integer or pointer. The operators are ++, --, +, -, +=, and -=.
222
We can apply relational operators to pointers, but the result makes sense only if the pointers point to parts of the same array, or one pointer points to NULL. The operators are <,
<=, >, and >=.
We can test against NULL by applying the ! operator (because the NULL pointer is 0).
We can subscript and delete pointers via [], delete, and delete[].
We can apply trivial operators, such as & and sizeof, to find out information about the
pointer (not the object it is pointing at).
We can apply some other operators, such as ->.
11.3.3 A Pointer-Hopping Example
Figure 11-8 illustrates how pointers can be used to traverse arrays. We have written two versions
of initialize.
The first version, initialize1, uses the normal indexing mechanism to step through
the array and is straightforward. The pointer-hopping version is initialize2. At line 11, we
declare a pointer endMarker that is initialized to point one past the array portion we are trying
to initialize; in STL jargon, it is the endmarker. A second pointer p, repeatedly hops through the
array initializing each entry until it hits the endmarker.
A crucial observation, with respect to the discussion of the STL in Chapter 10, is that in
initialize2, p is used to iterate over the collection of integers, and this is the basis for the
selection of the particular operators such as * and ++ in the STL iterators. In particular, as we
discuss in Section 11.4, this makes the generic algorithms in the standard library usable by primitive arrays.
11.3.4 Is Pointer Hopping Worthwhile?
Why might a pointer implementation be faster than an array implementation? Let us consider a
string of length 3. In the array implementation we access the array via s[0], s[1], s[2], and
s[3]. s[i] is accessed by adding one to the previous value i-1 and then adding s and i to
get the required memory location. In our pointer implementation, s[i] is accessed by adding
one to sp, and we never keep a counter i. Thus we save an addition for each character, paying
only an extra two subtractions during the return statement.
The next question is whether or not the trickier code is worth the time savings. The answer
is that in most programs a few subroutines dominate the total running time. Historically, the use
of trickier code for speed has been justified only in those routines that actually account for a significant portion of the programs running time, or in routines used in enough different programs
to make the optimization worthwhile. Thus in the old days C programs that used pointer hopping
judiciously had a large speed advantage over programs written in other high-level languages.
However, good modern compilers can, in many cases, perform this optimization. Thus the use of
pointers to traverse arrays will help some compilers, will be neutral for others, or may even generate slower code than the typical index addressing mechanism.
223
The moral of the story is that, in many cases, it is best to leave minute coding details to the
compiler and concentrate on the larger algorithmic issues and on writing the clearest code possible. Many systems have a profiler tool that will allow you to decide where a program is spending most of its running time. This will tell you where to apply algorithmic improvements, so it is
important to learn how to use the optimizer and profiler on your system.
sorts the first four (not all five) elements of arr. This is because arr represents an iterator for
the beginning of array arr, while arr+4 is an iterator representing index 4 (and since the second parameter in all STL range operations an endmarker of the range), signalling the first item
not to be included in the sort. Indeed, the iterator that is part of vector is often defined as simply a pointer variable, which helps explain why vector iterators do not have any bounds
checking built in to them.
Functions such as copy work, but since primitive arrays do not have push_back operations, primitive arrays that are the target of a copy need to already have enough space to store the
result.
As an example, the echo command, shown in Figure 11-9, prints out its command-line
arguments. Alternatively, we sometimes see
int main( int argc, char **argv );
making use of the fact that the array variable is simply a pointer. However,
224
1
2
3
4
5
6
7
8
9
10
#include <iostream>
using namespace std;
int main( int argc, char *argv[ ], char *envp[ ] )
{
for( int i = 0; envp[ i ] != NULL; i++ )
cout << envp[ i ] << endl;
return 0;
}
#include <iostream>
using namespace std;
int main( int argc, char *argv[ ] )
{
cout << "Invoking command: " << argv[ 0 ] << endl;
for( int i = 1; i < argc; i++ )
cout << argv[ i ] << " ";
cout << endl;
return 0;
}
Figure 11-9
225
m[0][0]
m[0][1]
m[0][2]
m[1][0]
m[1][1]
m[1][2]
m[0]
m[1]
m[2]
m[3]
m[4]
m[5]
If only one environment variable needs to be examined, that standard library provides
getenv (standard header cstdlib needs to be included), with signature previously discussed
in Section 11.2:
const char *getenv( const char *prop );
Figure 11-10 shows how we list all the environment variables, assuming that the third
parameter to main is being supported.
defines the two-dimensional array m, with the first index ranging from 0 to 1 and the second
index ranging from 0 to 2 (for a total of six objects). The compiler sets aside six memory locations for these objects.
An alternate form allows initialization, in a manner similar to Java:
int m[ ][ ] = { { 1, 2, 3 }, { 4, 5, 6 } };
226
So does
int sum2( int m[ ][ 3 ], int rows )
{
int totalSum = 0;
for( int r = 0; r < rows; r++ )
for( int c = 0; c < 3; c++ )
totalSum += m[ r ][ c ];
return totalSum;
}
Clearly, the compiler needs to know how many columns are in two-dimension array m in order
to do the double indexing, and this information is not available (the simple presence of a parameter named cols is hardly sufficient). If we want our routine to work for any two-dimensional
array, with arbitrary numbers of rows and columns, we would have to write this nonsense:
int sum4( int m[ ], int rows, int cols )
{
int totalSum = 0;
for( int r = 0; r < rows; r++ )
for( int c = 0; c < cols; c++ )
totalSum += m[ r * cols + c ];
return totalSum;
}
and then pass &m[0][0] as a parameter, but once we do so, whats the point? We might as well
declare a one-dimensional array from the start.
Although these problems can be avoided by creating a primitive array of primitive arrays,
the memory management issues are even more challenging than for a single primitive array. And
like one-dimensional arrays, memory management is required if we do not know the dimensions
at compile time. Thus it is highly recommended that a matrix class be used.
Key Points
227
11.8 Exercises
1. How are primitive arrays implemented in C++?
2. How are primtive strings implemented in C++?
3. How is a primitive array created from the memory heap?
4. How is a primitive array returned to the memory heap?
5. What is the difference between delete[] and delete?
6. What is the difference between the declarations int a[3] and int *a?
7. What happens when copying two primitive strings with = is attempted?
8. What is the result of using < on two primitive strings?
9. What is pointer hopping and why is it no longer as useful as it was in the past?
228
H A P T E R
1 2
C-Style C++
229
230
defined is part of the preprocessor also, and returns true if the symbol has been the target of a
#define. A symbol can also be undefined by using #undef.
12.1.1 Simple Textual Substitution
The most interesting (and dangerous if abused) preprocessor directive is #define. The directive
#define IDENTIFIER ReplacementText
causes the substitution of all occurrences of the token IDENTIFIER with the sequence of
tokens given in ReplacementText. This form is commonly used to give symbolic meaning
to constants. Typical usage is
#define MAX 50
all subsequent occurrences of MAX when seen as a separate token (characters inside a string constant do not count) are replaced by 50. Thus
cout << "MAX is" << MAX << endl;
becomes
cout << "MAX is" << 50 << endl;
ReplacementText can be essentially anything, so theoretically, one could see uses that
include variations of the following:
#define FOREVER for( ; ; )
#define extends public :
On the other hand, simple errors in the #define yield compilation errors, but the compiler will flag the error at the point of the expansion, often providing little hint that the problem
was at the #define statement. For instance, these three #define statements
#define MAX 50;
#define MAX = 50
#define MAX 50
// the max
if used in
if( i == MAX)
all of which have syntax errors. Its hard to get in serious trouble here, since the code doesnt
compile. However, the following directive illustrates more dangerous problems:
Preprocessor Macros
231
which is a problem because the precedence is wrong. This means that when using preprocessor
definitions, it is important to parenthesize liberally, as in
#define RANGE_SIZE (MAX-MIN)
The use of #define to provide replacement text (rather than simply the fact that a symbol is defined for the ifndef/endif trick) is obsolete in C++, since symbolic constants can be
declared with const, and textual substitution, such as replacing :public with extends is
probably bad style. It was needed in C because old C did not have a const, and array sizes
needed to be constants.
12.1.2 Parameterized Macros
Parameterized macros were used in C to avoid the overhead of function calls. For instance,
rather than write a function to compute the absolute value, we define a macro:
#define absoluteValue(x)
Of course we parenthesize liberally. There are various syntax rules. One rule is that the parenthesis that follows the macro name cannot be separated with whitespace, since otherwise it looks
like a simple textual replacement.
Note here that the parameters are typeless, which is nice in a pre-template world. Now, the
statement
y = absoluteValue(a-3);
is expanded to
y = ( (a-3) >= 0 ? (a-3) : -(a-3) );
what happens is that the macro is expanded, with a-3 replacing all occurrences of x. The macro
can have several parameters. Also note the use of the ?: operator, which seems to have been
invented just for this, since substitution with a full if statement would not generate legal code.
Although the macro looks good here, it should not be necessary in the modern C++ world,
because optimizing compilers are very good at doing this exact kind of inline optimization. They
are so good at it, that whereas C++ has an inline directive, Java does not even bother, deciding
that the compiler (and possibly the runtime system in the case of Java) doesnt need any hints.
Other uses of the macros such as writing a typeless routine or simulating call by reference (by
putting the changes to the parameters in the substitution text) are better handled by templates or
call-by-reference.
Not only are there better C++ alternatives to the macro, in general macros are not semantically equivalent to a function call, as is demonstrated by
232
1
2
3
4
5
6
7
8
9
10
11
#define printDebug( expr ) cout << __FILE__ << " [" <<
\
__LINE__ << "] (" << #expr << "): " << ( expr ) << endl;
int main( )
{
int x = 5, y = 7;
printDebug( x + y );
return 0;
}
Figure 12-1
Macro for debugging prints with file and line number included
z = absoluteValue( ++a );
which is expanded to
z = ( (++a) >= 0 ? (++a) : -(++a) );
1
2
3
4
5
6
7
8
9
10
11
12
233
Figure 12-2
Heres a poor example: we have a primitive array of char and we want to set all the
entries to zero. We are unhappy with the compilers performance (very unusual) so we decide to
take matters into our own hands. The idea is to treat the array of char as an array of int, and
set the ints to zero. This reduces the number of assignments by a factor of 4, since typically an
int is four chars. The resulting code is shown in Figure 12-2. This code is nonsense because
it is unlikely to be useful in practice.
Figure 12-3
234
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct tm
{
int
tm_sec;
int
tm_min;
int
tm_hour;
int
tm_mday;
int
tm_mon;
int
tm_year;
int
tm_wday;
int
tm_yday;
int
tm_isdst;
};
/*
/*
/*
/*
/*
/*
/*
/*
/*
*/
*/
*/
*/
*/
*/
*/
*/
*/
Figure 12-4
C Library Routines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
235
// Find Friday the 13th birthdays for person born Oct 13, 1937
#include <ctime>
#include <iostream>
using namespace std;
int main( )
{
const int FRIDAY = 6 - 1;
tm theTime = { 0 };
theTime.tm_mon = 10 - 1;
theTime.tm_mday = 13;
// Sunday is 0, etc...
// Set all fields to 0
// January is 0, etc...
// 13th day of the month
Figure 12-5
236
The first parameter is the control string and is output, except that the additional parameters are substituted as appropriate into the control string in places marked by a % conversion
sequence. For instance %d is used to print an integer in decimal, %o prints an integer in octal, %s
prints a (primitive) string, %f prints a float or double, and %% prints a %.
The return value of printf is the number of characters actually written, or -1 if there is
an error. But hardly anybody ever bothers to check the return code. As an example,
int x = 5;
double y = 3.14;
printf( "x is %d, y is %f\n", x, y );
After the % and before the character that specifies the type, a host of options control all of the
same things that can be controlled by the ostream manipulators. For instance in Figure 9-5 we
had:
out << left << setw( 15 ) << name << " "
<< right << fixed
<< setprecision( 2 ) << setw( 12 ) << salary;
The most important thing to know about printf is that it is not type-safe. If the additional parameters do not match the conversion specifiers in the control string, you get gibberish.
Because C is not object-oriented, only the primitive types have conversion specifiers;
even in C++, you cannot define new specifiers for user-defined class types, making printf
vastily inferior to the C++ iostream library.
The C function that reads formatted input is scanf. The basic form is
int scanf( const char *control, void *obj1, void *obj2, ... );
The first parameter is the control string, as before, except that doubles should use %lf
instead of %f. Also, field width specifiers become maximums, instead of minimums (this is useful for strings, because you want to make sure you dont read more characters than you have
room for). The return value is the number of conversion specifiers that are actually matched, so
if this is less than the number of conversion specifiers, something has gone wrong. As an example,
int x;
double y;
char name[ 100 ];
scanf( "%d %lf %99s", &x, &y, name );
reads an int, double, and primitive string, putting them in the objects specified, and hopefully returns 3. Note that name is already an address, so we dont need the &.
C Library Routines
237
Like printf, scanf is not typesafe, and errors are likely to result in an abnormal program termination, because pointer variables are involved. The most common mistake with
scanf is to forget to pass an address, as in
scanf( "%d", x );
This attempts to put an integer in the memory location given by the integer stored in x. It is
unlikely that this is a valid location; it certainly is not xs location.
scanf is a dangerous routine that has little use in a C++ program. If possible you should
replace existing calls to print and scanf with the iostream equivalents. You should avoid
having a program use both libraries, since both libraries buffer, and I/O can be interwoven unexpectedly.
Use of printf and scanf, and all file routines requires the standard header cstdio
(or stdio.h).
12.4.2 File I/O
In C, file I/O is supported with the FILE* type and a set of functions. FILE is a typedef for a
struct, and because in C structs have historically been passed and returned by pointers, all
the I/O routines work with FILE* as the basic stream parameter. The FILE* type is used for
both reading and writing. Three standard streams, stdin, stdout, and stderr are already
predefined as standard input, standard output, and standard error.
In order to use a file, we must obtain a stream for it by using fopen. When we are done,
we close the file with fclose. The parameters to fopen are a primitive string representing the
filename, and a primitive string representing the mode. Typical modes are "r" for reading, "w"
for writing, and "a" for appending. Also "rb", "wb", and "ab" are used for binary files, on
systems that distinguish between text and binary files. fopen returns a FILE*, or NULL if
there is an error. Thus the declarations for fopen and fclose are:
FILE *fopen( const char *fileName, const char *mode );
int fclose( FILE *stream );
The calls getc and putc provide single-character input and output for files. Their declarations are:
int getc( FILE *stream );
int putc( int ch, FILE *stream );
getc returns EOF if the end-of-file is seen. The return type of getc is an int, for the
same reason that method java.io.Reader.read returns an int representing the char
that has been read: a char is not sufficient to store all possible characters and the EOF symbol.
Unfortunately, in C and C++, if the return value of getc is directly assigned to a char, the
code will probably not work on files with full character sets (e.g binary files). putc returns EOF
if the write fails, otherwise it returns ch. Attempts to write on a stream open for reading, or viceversa, yields a bad return value, instead of the compile-time error that you get in C++.
Like printf and scanf, fprintf and fscanf can be used for formatted input and
output. The first parameter is a FILE*, and the remaining parameters are as described earlier.
238
Figure 12-6 shows a routine that copies from one file to another. We write it in C-style,
using primitive strings. Line 4 declares charCounted, and line 5 declares ch which will represent a character read by fgetc. Note that ch must have type int. If it is char, and this program is applied to large binary files, the copy will most likely be short. At line 6, observe that
both sfp and dfp must BOTH be declared as pointer variables. The first * applies only to sfp.
Line 8 is a simple alias test. The error messages at line 10 illustrate the use of the stderr
stream. Lines 13 and 18 open the files as binary files. If the second open fails, we must remem1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Figure 12-6
Copy files using getc and putc; return number of characters copied
C Library Routines
239
ber to close the first file. The copying of files is performed at lines 25 to 32, and then the files are
closed at lines 34 and 35. Failing to close the output file could result in some buffered data not
being written out, especially if the program terminates abruptly. fflush can be used to force
buffered data to be written out, prior to closing the file.
Two similar looking functions are fgetc and fputc. Because getc and putc are preprocessor macros, they can be dangerous to use if the arguments involve side effects. In this
extremely rare case, that is almost certainly poor programming practice, fgetc and fputc,
which are guaranteed to be functions and not macros, can be safely used.
Another routine that is provided is ungetc, which allows the putting back of a character
onto the input stream:
int ungetc( int ch, FILE *stream );
To read and write lines at a time, we can use fgets and fputs:
char *fgets( char *str, int howMany, FILE *stream );
int fputs( const char *str, FILE *stream );
fputs outputs a string to an output stream. It does not supply a newline character unless
one is already present. fgets reads characters from an input stream until one of three events
occurs:
1. EOF is encountered.
2. A newline is encountered.
3. howMany-1 characters are seen, before event 1 or 2 occurs.
After the characters are read, a null terminator is appended. A newline is stored only if it
was encountered. str is returned on success; if no characters were read because of an EOF or
any other error, a NULL pointer is returned.
As an example, suppose we want to read a large file one line at a time, using normal C++:
void processFile( string fileName )
{
ifstream fin( fileName.c_str( ) );
string oneLine;
while( getline( fin, oneLine ) )
...
}
On one of our systems, processing a file of about 78 million bytes containing about 2 million lines takes about 7.5 seconds simply to do the I/O. The slowness seems to have to do with
the fact that this form of getline deals with strings. However, here is an method in
istream called getline, that works somewhat like fgets, except that it does not retain the
newline character.
240
1
2
3
4
5
6
7
8
9
10
11
12
Figure 12-7
Figure 12-7 shows routine called getlineFast that uses this getline routine, presuming that no line is longer than MAX_LINE_LEN. At line 7, the call to gcount returns the
number of characters read by getline. Presumably, if this count is 0, getlineFast should
return false; otherwise we copy the primitive string into oneLine. If we use getlineFast
instead of getline, 7.5 seconds becomes 2.8 seconds! However, getlineFast only works
if lines have less than MAX_LINE_LEN characters. If not, very long lines will be split into sepa1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Figure 12-8
C Library Routines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
241
Figure 12-9
rate lines silently. Although there are some solutions available in the istream class, all of
them will take longer than 2.8 seconds. It turns out that this is a great place to use fgets.
We can write a getline that takes a FILE*, and then have the user create a FILE*
instead of an istream. With reasonable care, we can localize these changes, so that the rest of
the program doesnt have to change. Best of all, our routine will handle arbitrarily long lines and
be faster than getlineFast, taking about 2.4 seconds. The additional getline is shown in
Figure 12-8.
We begin by clearing out oneLine at line 6. Note that assignment of "" to oneLine
with = is significantly slower than calling erase, and since this is done on every line (2000000
times), this inefficiency is significant enough to be worth using erase. In the main loop, the
idea is to keep calling fgets, concatenating the result to oneLine (also line 6) until we read
characters that contain a terminating newline character (that test is performed at line 13). Those
characters are also appended (at line 20 outside of the main loop), after the newline character is
stripped out at line 15.
Other routines that are available include feof, the random access routines fseek, and
ftell that mimic the routines in istream, discussed in Section 9.6. (Actually the istream
routines mimic these.) For fseek, the constants that specify beginning, current, and end, are
SEEK_SET SEEK_CUR, and SEEK_END. The code in Figure 12-9 shows the direct correspondence between the FILE* and istream routines, by recoding Figure 9-7 line-for-line. (However, as we mention in Section 12.5, several aspects of this code that are legal in C++, such as
exceptions, are not legal C).
242
Finally, we mention sprintf and sscanf which allow printing and scanning from a
primitive string. These signatures are:
int sprintf( char *buffer, const char *control, val1, val2, ... );
int sscanf( const char *buffer, const char *control,
void *obj1, void *obj2, ... );
sets x2 to 37 and y2 to 37.0, because sscanf does not maintain a notion of previous parsing of
the buffer string.
sprintf is dangerous because it is possible that buffer does not contain enough characters to hold the result. sscanf is dangerous, especially in the case where the target tokens are
strings. The functionality of these functions is provided in ostringstream and
istringstream, so there is little need to use them.
12.4.3 malloc, calloc, realloc, and free
In C, memory is allocated from the memory heap by calling a family of alloc functions,
malloc, calloc, and realloc. The memory is released back to the memory heap by calling
free. The declarations found in the standard header cstdlib and stdlib.h are:
void
void
void
void
The most important things to know about these routines are that they are not typesafe, and
they do not invoke constructors. Instead, they simply return a pointer to raw memory, that can be
cast to the appropriate type. In the case of malloc, the memory is not initialized. In the case of
calloc, memory is initialized to all bits zero, but that might not be suitable for pointer types or
doubles. realloc can be used to get more memory, which is initialized by using the original
memory (and then freeing the original, which needs to have been heap-allocated). free returns
the memory back to the heap, but does not invoke destructors.
It is important not to mix malloc and delete or new and free. Specifically, memory
that was allocated by malloc should be released by free and not by delete. Memory that
C Library Routines
243
was allocated by new should be released by delete, and not by free. Otherwise, havoc
results.
C-library routines that return heap-allocated memory get the memory from an alloc
function. Thus their memory is returned to the heap by calling free. One such example is
strdup.
12.4.4 atoi, atof, strtol, and strtod
atoi and atof are original C library routines that parse a primitive string, returning an int or
a double respectively. These routines handled errors poorly. Their replacements, strtol and
strtod, which yield long and double, respectively, and were added in a later version of C
do a better job. Although, these are not needed in C++ since istringstream can be used to
do the same thing, it is certainly possible that strtol and strtod could be more efficient
than using an istringstream.
12.4.5 system
The system function, in standard header cstdlib or stdlib.h, is used to invoke a command. It takes a primitive string representing the command; this string is passed to the operating
systems command processor and is run. How this is done is highly system dependent, and obviously non-portable, since few commands are available on all systems. For instance,
system( "dir" );
244
1
2
3
4
5
6
7
8
9
the old style, but of course in C++ we would use the STL sort algorithm in the first place.
qsort is not required to be efficient, and although it is typically implemented as quicksort, older versions are known to have deficiencies that can cause quadratic behavior on degenerate inputs (such as arrays with only two unique items, repeatedly occurring randomly).
Furthermore, because qsort is already compiled, it is unreasonable to expect any inline optimizations to be performed. So applying qsort on an array of ints is going to be significantly
slower than using the STL sort algorithm, or even any quicksort that can be found in a standard textbook.
12.4.7 Variable Number of Arguments
Some routines, such as print and scanf, require a variable number of arguments. Allowing a
variable number of arguments is not a good thing for a type-safe language, which is why the feature is not part of Java. However, it has long been part of C and C++.
To declare an unspecified number of arguments, we use ellipses (...). They can be processed by a set of macros in standard header stdarg.h (or cstdarg). The macros are
va_start, va_end, and va_arg.
A function declares at least one fixed parameter, and then uses ellipses to specify an
unknown (possibly zero) number of additional parameters.
Macro va_start is used to initialize an object of type va_list with the last fixed
argument. Then, each call to va_arg returns the next argument. va_arg takes the va_list
object and the type of the parameter that is expected. This type cannot be one that widens when
passed as an argument; use double, int, or unsigned int in place of shorter alternative
such as float, and short. Prior to returning, the function should call va_end.
As an example, the code in Figure 12-11 illustrates a function printStrings that
prints an arbitrary number of primitive strings. At line 5, we see the ellipses used to specify
additional parameters after the first. The va_list object is declared at line 8, and initialized at
line 11. Then we repeatedly loop at lines 12 to 13 reading parameters of type const char *.
There needs to be some way to signal the end. Alternatives include encoding this information in
the first parameter (as is done in printf and scanf), or simply terminating the list with a
NULL pointer. We use the second alternative. Thus we see at line 20, that the last parameter to
C Programming
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
245
#include <iostream>
#include <cstdarg>
using namespace std;
void printStrings( const char *str1, ... )
{
const char *nextStr;
va_list argp;
cout << str1 << endl;
va_start( argp, str1 );
while( ( nextStr = va_arg( argp, const char * ) ) != NULL )
cout << nextStr << endl;
va_end( argp );
}
int main( )
{
printStrings( "This", "is", "a", "test", (const char *) NULL );
return 0;
}
printStrings is NULL (we use a pointer for the technical reason that we want to ensure that
the type is a pointer type. This would be automatic if we had a fixed parameter list, but not with
variable parameter lists). At the end, we call va_end at line 15.
12.5 C Programming
If you program in C instead of C++, you need to be aware that some C++ features are missing in
C. The partial list, based on ANSI C (note that items 11 and 12 will behave the same in recently
adopted C99 as in C++) includes:
1. C has no classes, and does not support even object-based programming.
2. C has no vector or string type. You have to use pointers.
3. C has weaker type checking than C++. The compiler will catch fewer problems.
4. C does not have reference variables or call-by-reference. You will have to pass pointers to
objects to simulate the effect.
5. C does not provide function overloading. Once you use a name, thats it.
6. C does not provide namespaces.
7. C does not provide operator overloading.
8. C does not have templates.
9. C does not have exceptions.
246
12.7 Exercises
1. Describe the basic functionality of the preprocessor.
2. What does reinterpret_cast do?
Exercises
247
which returns true if at least two of x, y, and z are true, and false otherwise.
7. Using one / and one % operator, implement the macro
#define DIVIDE( numerator, denomibator, quotient, remainder )
that generates the clause that directs a for loop running from low to high, inclusive.
9. Write a macro, max4, to find the maximum of four numbers.
10. A safe macro can be implemented by declaring a block and copying the macro parameters
into block variables. Parameters thus become implicitly typed. Write the max4 macro for
ints as a safe macro.
11. Implement the macro
#define main( )
that prints the compilation date and time and then starts execution of the program.
12. Write a function that takes a month, day, and year, and returns the day of the week. Implement your function by using the routines in standard header file <ctime>.
13. Write a program that uses printf to print out the following numbers in decimal, octal
and hexadecimal: 37, 037, 0x37.
14. Reimplement Exercise 9.9 using I/O routines in standard header file <cstdio>.
15. Reimplement Exercise 9.10 using I/O routines in standard header file <cstdio>.
16. Reimplement Exercise 9.11 using I/O routines in standard header file <cstdio>.
17. Reimplement Exercise 9.12 using I/O routines in standard header file <cstdio>.
18. Implement a version of findMax that takes a variable number of int arguments.
248
H A P T E R
1 3
249
250
Although Java used to be painfully slow, a modern Java implementation has performance
that is comparable to C++ for many applications. Using JNI to achieve performance improvements is possible, but is no longer needed as much as it used to be.
Using JNI has significant downsides. First, you lose portability. A native implementation
must be supplied for each platform. Given the large Java library that already makes use of native
methods, if a new native method has identical C++ code that can be used on all platforms, then it
is likely that the code could have been implemented in Java in the first place. Second, you lose
safety. Native methods are not afforded the same protections as Java methods. Once you enter in
C++ code, all bets are off, and any C++ bug, such as indexing an array out-of-bounds, using a
stale pointer, and trashing memory can occur. Third, the implementation of the native method is
contained in a dynamic library (in a Windows environment, a .dll; in a Unix environment a
.so file). Any Java program that uses native methods must load dynamic libraries, and this is an
operation that the Java Security Manager might object to, because of the safety concerns listed
above. Lack of safety means lack of security, so for instance, by default user-defined native
methods generally cannot be invoked inside an applet. Fourth, the code is cumbersome, often
compiles and often fails to run because of silly typing errors such as poor capitalization or missing semicolons inside of string constants.
Once we decide to use JNI, the basic procedure is relatively straightforward.
1. A Java class declares that some methods have non-Java implementations by marking the
methods as native.
2. A C++ function is written that implements the native method, using the JNI protocols.
3. The C++ function is compiled in a dynamic library.
4. The Java Virtual Machine loads the dynamic library, and then calls to the native method
are handled by invoking the implementation in the dynamic library.
The devil, of course, is in the details, which are numerous, since the JNI is expected to work not
only for C++, but also for C and other languages that are not object-oriented. For instance,
1. How are fields and methods of a Java object used, given that C has no classes?
2. How are parameters passed from Java to C/C++?
3. How is a value returned from C/C++?
4. What about function overloading, since C does not allow it?
5. How do we differentiate between static and non-static members?
6. What about strings and arrays?
7. How can the C/C++ code throw an exception, and what happens if it invokes a Java
method that throws an exception?
We will begin our discussion by first implementing a single native method. To avoid all of
the above complications, our method will be static, with no parameters, and no return type, and
simply print a string. Still, this is tricky, since it is the first use of various incantations that will
JNI Basics
251
be part of all JNI implementations. Then we will access fields and invoke methods of an object,
discuss arrays and strings, have the native method return a value and throw an exception, and
then quickly examine some the JNI support for object monitors.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class HelloNative
{
native public static void hello( );
static
{
System.loadLibrary( "HelloNative" );
}
}
class HelloNativeTest
{
public static void main( String[ ] args )
{
HelloNative.hello( );
}
}
Figure 13-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Figure 13-2
252
and the output is shown in Figure 13-2. We can see that the name of the method is
Java_HelloNative_hello, which reflects the (default) package, class, and method name.
1
2
3
4
5
6
7
8
9
#include <iostream>
using namespace std;
#include "HelloNative.h"
JNIEXPORT void JNICALL
Java_HelloNative_hello( JNIEnv *env, jclass cls )
{
cout << "Hello world" << endl;
}
Figure 13-3
Implementation of HelloNative.cpp
253
The C++ declaration lists two parameters. The first, is a pointer to a JNIEnv object, and
will be used extensively to access fields and methods of objects. The second parameter is a
jclass object, representing information about the HelloNative class. Java programmers
who are familiar with the Reflection API will recognize this as being the equivalent of a Class
object. A jclass object allows us to obtain information and use fields and methods for any
Java class. This second parameter is passed for static methods only. For instance methods, the
second parameter is a jobject object, representing the moral equivalent of the this reference. Given a jobject, one can always obtain the jclass object representing the objects
class type, and at that point, the objects fields can be manipulated, and methods can be invoked.
But more on that in Section 13.4.
Since the native method takes no parameters, the C++ declaration lists no additional
parameters after the first two. We can implement the method trivially, as shown in Figure 13-3.1
You should make sure to include the header file generated by javah, and to give names to the
formal parameters. It is standard to name the first parameter env; the second parameter is often
either cls or ths, depending on whether we are implementing a static or instance method. Do
not use class or this, since these are C++ reserved words (avoid them if you are using C,
too, in case you want to painlessly upgrade to C++ later on).
13.2.1 Compiling a Shared Library
All that is left to do is to compile the C++ code into a shared library. How this is done depends
on your environment. Observe first, that the header file has an include directive for jni.h.
Thus is not part of Standard C++, but instead is part of the JDK.
If the JDK is installed in directory JAVA_HOME, then the header file is in subdirectory
named include of JAVA_HOME. jni.h itself includes a second file, and this second file is
found in a subdirectory of include. This is important, because compiler options must be provided to search for header files in these nonstandard places.
Under Windows, the shared library must end in .dll, but the extension is not part of the
string passed to System.loadLibrary. Under Unix, the shared library must end with .so,
begin with lib, but neither the extension nor prefix is part of the string passed to
System.loadLibrary.
Using Visual Studio 6.0 or Visual Studio DotNet, we can compile the shared library into a
DLL either by using a DLL project (with the include search path augmented as specified above,
and an option to specify the target directory for the DLL file), or by using the command-line
tools. Visual Studio DotNet provides a command line window; in Visual Studio 6.0, after opening an MS/DOS window, find and run vcvars32.bat to set up command-line compilation.
Then command-line compilation can be used. The magic incantation for our example (all on one
line, of course), assuming that the JDK is in C:\jdk is
1.
Note that printing to the standard output from a native method is a bad plan, because writing to standard output
in both C++ and Javas System.out.println could yield intermixed output, due to buffering.
254
The first two options suppress complaints about exception handling not being enabled,
and are not needed to compile C++ programs.
In a Unix world, we can use g++. For g++, we first compile the file into a single .o, and
then create the shared library. Thus, we have two commands (of course the first two lines should
be typed as one):
g++ -c -fPIC -I$JAVA_HOME/include
-I$JAVA_HOME/include/linux HelloNative.cpp
g++ -shared -o libHelloNative.so HelloNative.o
Using g++ on Solaris, replace linux with solaris in the first command.
If you are using the standard Sun C++ compiler, one command compiles into the shared
library:
CC -G -I$JAVA_HOME/include -I$JAVA_HOME/include/solaris
HelloNative.cpp -o libHelloNative.so
For other platforms, consult local documentation. Once we have compiled the shared
library, the Java program should run as expected. If an UnsatisfiedLinkedError is still
being reported, verify that the dynamic library is correctly named, as specified above, and that it
is in a viewable directory. On Unix, make sure the dynamic library is located either in the directory that is current when the Virtual Machine is invoked, or in a directory listed in environment
variable LD_LIBRARY_PATH. On Windows, make sure the dynamic library is located either in
the directory that is current when the Virtual Machine is invoked, or in a directory listed in environment variable PATH.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
255
class Date
{
public Date( int m, int d, int y )
{ month = m; day = d; year = y; }
static
{
System.loadLibrary( "Date" );
}
native public void printDate( );
public int
{ return
public int
{ return
public int
{ return
getMonth( )
month; }
getDay( )
day; }
getYear( )
year; }
Figure 13-4
such as jintArray, and jobjectArray. As was the case with strings, routines are provided
to create a jint * from a jintArray object. However, as we discuss in Section 13.5.2, we
cannot create a jobject * from a jobjectArray.
Finally, two objects are used to represent information about a method and a field. These
are jmethodId and jfieldID, which are the moral equivalents of Method and Field in
the Reflection API.
256
1
2
3
4
5
6
7
8
class TestDate
{
public static void main( String[ ] args )
{
Date d = new Date( 8, 23, 2003 );
d.printDate( );
}
}
Figure 13-5
getYear. The third implementation invokes the toString method, and illustrates how a Cstyle string is extracted from a jstring. The test program that we will use is shown in
Figure 13-5, for completeness.
13.4.1 Accessing Fields
Figure 13-6 shows the header file that is obtained as a result of running javah on class Date.
Observe that the second parameter at line 15 is of type jobject, signalling that we are implementing an instance method.
In order to access fields of a jobject we need to follow the following steps:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Figure 13-6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
257
Figure 13-7
The output of javap is shown in Figure 13-7. As we can see, int is represented as I. On
the other hand, String is Ljava/lang/String;, and omitting the L or the ; will give
incomprehensible runtime errors. Thus it is best to use javap.
258
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "Date.h"
#include <iostream>
using namespace std;
JNIEXPORT void JNICALL
Java_Date_printDate( JNIEnv * env, jobject ths )
{
jclass cls = env->GetObjectClass( ths );
jfieldID monthID = env->GetFieldID( cls, "month", "I" );
jfieldID dayID =
env->GetFieldID( cls, "day", "I" );
jfieldID yearID = env->GetFieldID( cls, "year", "I" );
jint m = env->GetIntField( ths, monthID );
jint d = env->GetIntField( ths, dayID );
jint y = env->GetIntField( ths, yearID );
cout << m << "/" << d << "/" << y << endl;
}
Figure 13-8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
259
#include "Date.h"
#include <iostream>
using namespace std;
JNIEXPORT void JNICALL
Java_Date_printDate( JNIEnv * env, jobject ths )
{
jclass cls = env->GetObjectClass( ths );
jmethodID getMonthID, getDayID, getYearID;
getMonthID = env->GetMethodID( cls, "getMonth", "()I" );
getDayID
= env->GetMethodID( cls, "getDay", "()I" );
getYearID = env->GetMethodID( cls, "getYear", "()I" );
jint m = env->CallIntMethod( ths, getMonthID );
jint d = env->CallIntMethod( ths, getDayID );
jint y = env->CallIntMethod( ths, getYearID );
cout << m << "/" << d << "/" << y << endl;
}
Figure 13-9
where ... represents the parameters to the method. In these calls, XXX represents the (Java)
return type of the method.
260
Figure 13-9 provides our second implementation of printDate and shows how instance
methods are invoked. As before, we get a jclass entity, and then at lines 11 to 13, we obtain
the jmethodIDs for each of the methods that we want to invoke (the jmethodID variable
declarations are all together on line 9 simply to avoid making lines 11 to 13 too long). Once we
have the jmethodIDs, we can use them to invoke the method, as shown at lines 15 to 17. As
with accessing instance fields, the first parameter is the object on which the method is to be
invoked. If this method took additional parameters, they would follow the jmethodIDs in the
parameter list.
Invocation of CallXXXMethod uses dynamic dispatch. An alternative is
XXX CallNonvirtualXXXMethod( jobject ths, jmethodID m, ... );
that does not use dynamic dispatch, but that is hardly ever something you would want to do.
13.4.3 Invoking Constructors
A constructor can be invoked by using environment method NewObject, and passing the
jclass, jmethodID, and parameters. If you already have a jclass entity, you can get a
jclass by invoking FindClass, passing the name. The jmethodID uses "<init>" as
the name of the method. As an example, to create a Date inside of a native method:
jclass dateClass = env->FindClass( "Date" );
jmethodID ctor = env->GetMethodID( dateClass, "<init>", "(III)V" );
jobject d1 = env->NewObject( dateClass, ctor, 8, 23, 2004 );
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
261
#include "Date.h"
#include <iostream>
using namespace std;
JNIEXPORT void JNICALL
Java_Date_printDate( JNIEnv * env, jobject ths )
{
jclass cls = env->GetObjectClass( ths );
jmethodID toStringID = env->GetMethodID( cls, "toString",
"()Ljava/lang/String;" );
jstring str = (jstring) env->CallObjectMethod( ths,
toStringID );
const char *c_ret = env->GetStringUTFChars( str, NULL );
cout << "(calling toString) " << c_ret << endl;
env->ReleaseStringUTFChars( str, c_ret );
}
Figure 13-10 which is our third implementation of printDate, illustrates how we can
access a String. Here printDate invokes the toString method, extracts the primitive
string from the jstring that is returned by toString, and then prints it. Lines 10 and 11
gets the jmethodID for toString, and then line 13 invokes the method (again with the oldstyle cast, to save typing). Simply printing the jstring does not work. If we try it on a Win1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Figure 13-11 Using strings (primitive style) for static native method StringAdd.add
262
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Figure 13-12 Using C++ string library for static native method StringAdd.add
dows platform, we get 008D9E3C; other nonsense is produced on Unix machines. In fact, we
are lucky not to crash the Virtual Machine. Instead, at line 16 we get a null-terminated primitive
string, print it, and then free the C-style string at line 18.
New jstring objects can be created (mostly for the purposes of returning one) by
invoking NewStringUTF. As an example, the code in Figure 13-11 shows a routine that
implements a string concatenation in native code. Once again, this is a silly example, but illustrates the syntax.
As we can see by lines 1 and 2, we are implementing method add in class StringAdd.
This is a static method, since the second parameter is a jclass. The two parameters are both
String, and the return type is String. At lines 4 and 5 we obtain the UTF string, and at line
6, we allocate an array that will store the result of the concatenation. The +1 is needed to provide
space for the null terminator. At lines 8 and 9, we compute the result of concatenation by first
copying a1 to c, and then appending b1 to c. Then we call NewStringUTF at line 10 to form
a jstring that can be returned at line 16. Prior to returning, we must clean up memory. Lines
12 and 13 show the calls to ReleaseStringUTFChars, and line 14 cleans up the call to
new[] with a matching delete[].
A cleaner alternate that avoids the calls to new[] and delete[] by using the C++
string library type is shown in Figure 13-12.
13.5.2 Arrays
The JNI defines eight array types for the primitives. A typical example is jintArray. Additionally, jobjectArray is used to represent an array of Object. The environment function
GetArrayLength can be used get the length of any array object, passed as a parameter. To
access individual items in the array, we have to use one strategy for primitives, and another for
objects.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
263
class NativeSumDemo
{
native public static double sum( double [ ] arr );
static
{
System.loadLibrary( "Sum" );
}
public static void main( String [ ] args )
{
double [ ] arr = { 3.0, 6.5, 7.5, 9.5 };
System.out.println( sum( arr ) );
}
}
To access an array of primitive types, we can obtain a primitive array using the same idioms as was seen for strings. As an example, Figure 13-13 declares a native method, sum, that
returns the sum of the elements in an array of double. It also contains a test program.
When we run javah, we obtain the header file shown in Figure 13-15. There we see that
the double[] parameter in the Java native declarations becomes a jdoubleArray. Implementation of sum is shown in Figure 13-14. The sum is declared as a jdouble (rather than a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "NativeSumDemo.h"
JNIEXPORT jdouble JNICALL Java_NativeSumDemo_sum
( JNIEnv *env, jclass cls, jdoubleArray arr )
{
jdouble sum = 0;
jsize len = env->GetArrayLength( arr );
// Get the elements; don't care to know if copied or not
jdouble *a = env->GetDoubleArrayElements( arr, NULL );
for( jsize i = 0; i < len; i++ )
sum += a[ i ];
// Release elements; no need to flush back
env->ReleaseDoubleArrayElements( arr, a, JNI_ABORT );
return sum;
}
264
double) and initialized at line 6. At line 7, we obtain the length of the array by calling
GetArrayLength.
Line 10 gets a C-style array from the jdoubleArray. As was the case with strings, this
array could be a copy, or it could be the original, depending mostly on the implementation of the
Virtual Machine. With arrays, if double and jdouble have identical representations, and if
the Java array is stored contiguously (which is not guaranteed, since the garbage collector may
elect to move parts of the array to reduce fragmentation), then it is possible that the pointer variable is actually pointing to the memory that stores the original double[] inside the virtual
machine. In such a case, no copy is made. However, once this pointer is handed out, the garbage
collector could not safely move parts of the array without invalidating the pointer. Thus, if no
copy is made, the original array is pinned and cannot relocate until the pointer is released.
Once we have the C-style array, we can compute its sum. Prior to returning, we must
release the array, as shown at line 16. When we were working with strings, releasing the string
returned the memory back to the system. With arrays, there are two independent issues.
First, if the array is a copy, we must copy any changes back to the original; otherwise they
are not reflected. Second, if the array is a copy, we must have memory reclaimed. As a result, the
last parameter to ReleaseXXXArrayElements can be either of 0, JNI_COMMIT, or
JNI_ABORT. If the parameter is 0, we flush the contents back to the original, the reflecting all
changes, and then reclaim the memory if needed. JNI_COMMIT flushes the contents, but does
not reclaim the memory. This can be useful if changes need to be reflected immediately, but
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
265
would not be needed if no copy was made (hence the second parameter to
GetXXXArrayElements could be useful to decide if this call should be made). JNI_ABORT
does not flush the contents, but reclaims the memory if needed. JNI_ABORT would be useful if
no changes were made to the array, because then we would avoid the flushing that would be
done with a parameter of 0. In fact, this is exactly the case we have, so we call
ReleaseXXXArrayElements with JNI_ABORT as the parameter.
Accessing elements in a jobjectArray is more difficult because we cannot obtain the
C-style equivalent. Instead, we must call
GetObjectArrayElement( array, idx );
SetObjectArrayElement( array, idx, val );
through the env pointer. Clearly this makes accessing arrays of objects fairly slow.
New arrays can be created by using
NewObjectArray( jclass cls, int len, jobject default );
NewXXXArray( int len, XXX default );
through the env pointer; the return type is the appropriate jarray type.
266
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class NativeSumDemo
{
native public static double sum( double [] arr )
throws Exception;
static
{
System.loadLibrary( "Sum" );
}
public static void main( String[] args )
{
double [ ] arr1 = { 3.0, 6.5, 7.5, 9.5 };
double [ ] arr2 = { };
try
{
System.out.println( sum( arr1 ) );
System.out.println( sum( arr2 ) );
}
catch( Exception e )
{
System.out.println( "Caught the exception!" );
e.printStackTrace( );
}
}
}
Figure 13-16 Same main that illustrates exception being thrown by native call
If the native code does neither, and instead continues executing native code, even though
an exception is pending, then the behavior when other environment functions are called is undefined and dangerous.
If the native method needs to throw an exception on its own, then it can do so using the
environment functions Throw or ThrowNew. The result of calling either function is that an
exception is now pending; however, as before, the pending exception does not terminate the
native method. Instead, a return statement should immediately follow, and certainly other environment functions should not be called. So if it is important to release array elements, do so
before invoking Throw or ThrowNew.
ThrowNew is much easier to use than Throw because you can simply give the complete
name of the exception class and the parameter that is passed to its constructor. We illustrate how
a native method can throw an exception in Figure 13-16, which provides a main and,
Figure 13-17, which implements the native method itself.
In Figure 13-16, we see that method sum might throw an Exception; the exception is
thrown if the array has length 0. (This is terrible style, and a better exception should be used, in
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
267
#include <iostream>
using namespace std;
#include "NativeSumDemo.h"
JNIEXPORT jdouble JNICALL Java_NativeSumDemo_sum
( JNIEnv *env, jclass cls, jdoubleArray arr)
{
jdouble sum = 0;
jsize len = env->GetArrayLength( arr );
if( len == 0 )
{
env->ThrowNew( env->FindClass( "java/lang/Exception" ),
"Empty array" );
cout << "Throwing an exception, but should exit" << endl;
return 0.0;
}
// Get the elements; don't care to know if copied or not
jdouble *a = env->GetDoubleArrayElements( arr, NULL );
for( jsize i = 0; i < len; i++ )
sum += a[ i ];
// Release elements; no need to flush back
env->ReleaseDoubleArrayElements( arr, a, JNI_ABORT );
return sum;
}
general; however using Exception illustrates a point as we will see when the native method is
implemented). Note also that the library loaded at line 8 is Sum, rather than NativeSumDemo.
When you compile the example, make sure the appropriate link library is created. The rest of the
code is standard fare, and we expect that the second call to sum triggers the catch block.
Figure 13-17 Illustrates the implementation of the native method. First, note that the
throws list declared in Java is not part of the native function name at line 6. The additional code
at lines 12 to 18 shows the test for the case of a zero-length array. When this test succeeds, a new
exception is created, and marked as pending at lines 14 and 15 by the call to ThrowNew. Note
that the name of the exception must reflect the complete class name, including package name.
However, ThrowNew does not cause an immediate return, so the print statement at line 16 will
be executed.
268
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
The return value at line 17 is required to avoid warnings from the C++ compiler, but is
never used by the caller, because the return immediately causes the Virtual Machine to throw a
Java exception.
are rewritten as
(*env)->EnvFunction( env, ... );
which does get ugly after a while, but is a relatively minor difference. Finally, make sure you
place your source code in a .c file, since many C compilers also compile C++ code, and make
their decisions based on the suffix of the source file name.
Figure 13-18 shows a C implementation of the add method in class StringAdd that
was previously written in Figure 13-11. The difference is mostly cosmetic.
JNI References
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
269
array types all represent references to Java objects (whereas jint, jdouble, etc. do not).
When these references are created they are called local references and are valid:
1. only in the thread in which the native method is invoked
2. only for the duration of the native method
This means that you cannot cache local references, nor pass them to other threads. If you
do so, and try to use them later on, you may find that the garbage collector has reclaimed the
objects.
A global reference is a wrapper around a local reference. Unlike local references, global
reference are valid across multiple threads and multiple native calls, until a call to
DeleteGlobalRef. If there is no call to DeleteGlobalRef, the global reference is valid
for the entire duration of the Virtual Machine, which could be a problem for general objects and
arrays, but is probably reasonable behavior for jclass references.
From this discussion, we also know that local references are valid for the duration of the
native method call. However, if the native method makes a time-consuming function call, it
might be worth releasing local references prior to making the function call. Alternatively, if the
native method call creates many local references to large objects, it might be prudent to release
some of the references when they are no longer needed. As an example, if we are iterating
through a jobjectArray, each call to GetObjectArrayElement creates a jobject. It
270
may be worth reclaiming it as we advance to the next array element. This is done with
DeleteLocalRef. Figure 13-19 illustrates this with an example in a native routine that
counts the total string length in an array of strings. We use DeleteLocalRef after accessing
each string, prior to proceeding to the next string. Depending on the underlying implementation
of the JNI, the number of strings, and the size of the string objects, this could help performance,
or simply have no noticeable effect.
is equivalent to
synchronized( obj )
{
/* synchronized block */
}
Dont forget to invoke MonitorExit. Although there are no environment functions to invoke
wait and notifyAll, these can be called by obtaining a jmethodID, and calling the appropriate function using the normal JNI mechanism.
13.10Invocation API
The Invocation API allows the C++ programmer to create a Virtual Machine from inside a C++
program. Once the Virtual Machine is created, the normal mechanism can be used to invoke the
main method of any class.
The code to do so is boilerplate and is shown in Figure 13-20. Clearly it can be generalized to allow any class, and to allow command-line arguments to main. The hard part of this
code is to do the compilation. In short, in addition to providing options to specify the include
directories, you must make sure that library jvm.lib (for Windows) or jvm (for Unix) is used
in the compilation. For Unix this involves using two options: -L to specify the search path for
libraries, and -l to specify the library itself. Using the Visual Studio products, the complete
library name is included with the compilation command, and the PATH environment variable is
Invocation API
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
271
Figure 13-20 Creating a Java VM from C++ main; invokes Hello with no parameters
set to include that hotspot compiler C:\jdk\jre\bin\hotspot. The online code contains
more specific compilation instructions.
272
13.11Key Points
Java native methods are specified with the native reserved word.
After the class is compiled, we can run javah to generate a C/C++ header file.
After the implementation is written in the native language, it must be compiled into a
dynamic library.
The library should be loaded by System.loadLibrary, typically invoked from the
static initializer of the native methods class.
The eight Java primitives have corresponding native types, such as jint.
JNI_TRUE and JNI_FALSE are defined to represent true and false, respectively.
jstring and jobject represent String and Object.
jXXXArray is used to represent the eight primitive Java arrays.
jobjectArray represents an array of Object.
jclass represents a Java class type.
jmethodID represents a method.
jfieldID represents a field.
The Java native method is implemented by a native function whose name incorporates the
package name, class name, and possibly signature of the Java native method.
The first parameter to a native function is the environment pointer.
The second parameter is a jclass, representing the class type for static methods, or a
jobject, representing this, for instance methods.
Additional parameters will be listed in the native function, as declared in the Java native
method.
javap is used to obtain a list of encoded field type signatures and method signatures.
An instance field of an object is accessed by getting a jfieldID from the class type,
field name, and (encoded) field type, and then invoking GetXXXField, with a jobject
and jfieldID. Static fields can be accessed by GetStaticFieldID and
GetStaticXXXField, with a jclass in place of a jobject. Fields can also be
changed with SetXXXField and SetStaticXXXField.
An instance method of an object is invoked using dynamic dispatch by getting a
jmethodID from the class type, method name, and (encoded) method signature, and
then invoking CallXXXMethod, with a jobject, jmethodID, and parameters to the
method. Static methods can be invoked by GetStaticMethodID and
CallStaticXXXMethod, with a jclass in place of a jobject. Methods can also
be invoked without dynamic dispatch by using CallNonvirtualXXXMethod.
A C-style string (const char *) can be extracted from a jstring by invoking the
environment function GetStringUTFChars. ReleaseStringUTFChars should
be called when the C-style string is no longer needed.
A C-style primitive array of primitive types, such as jint * can be extract from a
jarray (such as jintArray) by invoking the environment function
Exercises
273
13.12Exercises
1. Why would it make sense to write a native method?
2. On the Java side, how is a native method declared?
3. On the Java side, how is the library that contains the native method implementation
loaded?
4. For a class that contains native methods, how is the corresponding C++ header file generated?
5. A C++ method is already implemented in a library and we want to be able to invoke it
from Java. What is the typical strategy that is used?
6. What are the first two parameters in the signature of a C++ native method implementation?
7. What is a jclass?
8. What is a jobject?
9. What are the JNI primitive types?
10. Explain how an instance field of a Java object is accessed in C++ code. How are static
fields accessed?
11. Explain how an instance method of a Java object is invoked in native code. How are static
methods invoked?
274
12. How are constructors for new Java objects invoked in C++ code?
13. Why cant a jstring be safely type-cast to a const char *? What is the correct way
to obtain a const char * from a jstring? What memory issues must be dealt with?
14. How is a jstring created from a const char *?
15. Explain how Java arrays are accessed in C++ code.
16. What does it mean for an array to be pinned? How can you tell if an array is pinned?
17. What does the last parameter to ReleaseXXXArrayElements do?
18. How can a C++ native method signal an exception?
19. How can a C++ native method tell if invoking a Java method caused an exception be
raised? Can a C++ native method handle an exception?
20. How does C native code differ from C++ native code?
21. What is a local reference, and how long are local references valid?
22. What is a global reference, and how long are global references valid?
23. How can a C++ native method obtain and release a monitor?
24. What is the invocation API?
25. Can a native method make changes to a final field? Find out by writing a program that
attempts to do so.
26. Can a native method invoke a private method of another class? Find out by writing a program that attempts to do so.
27. Class CIO, defined below is intented to allow the output of a single int, double, or
String, using C-style sprintf formatting. Each of the native methods returns a
String, in which the escape sequence in the control string is replaced by the second
parameter. The native methods are implemented by calling the C library routine sprintf. In
the call to sprintf, provide a large buffer for the first parameter (and hope for the best),
and then pass control and var as parameters to the C library sprintf.
class CIO
{
public native static String sprintf( String control, String var );
public native static String sprintf( String control, int var );
public native static String sprintf( String control, double var );
}
28. Implement the standard matrix multiplication algorithm using a native method, throwing
an exception if the matrices have incompatible sizes. The signature of your method is:
native public static double [][] multiply( double [][] a,
double [][] b );
Bibliography
275
276
Bibliography
Index
277