|
|||||
8:
Constants
The
concept of constant
(expressed by
the const
keyword)
was created to allow the
programmer to
draw
a line between what changes
and what doesn't.
This
provides safety and control
in a C++
programming
project.
353
Since
its origin, const
has
taken on a number of different
purposes.
In
the meantime it trickled
back into the C language
where its
meaning
was changed. All this can
seem a bit confusing at
first, and
in
this chapter you'll learn when, why, and
how to use the const
keyword.
At the end there's a
discussion of volatile
which
is a near
,
cousin
to const
(because
they both concern change) and
has
identical
syntax.
The
first motivation for const
seems
to have been to eliminate
the
use
of preprocessor #defines
for value substitution. It has
since
been
put to use for pointers, function
arguments, return types,
class
objects
and member functions. All of these
have slightly
different
but
conceptually compatible meanings and will
be looked at in
separate
sections in this
chapter.
Value
substitution
When
programming in C, the preprocessor is
liberally used to
create
macros and to substitute values.
Because the
preprocessor
simply
does text replacement and
has no concept nor facility
for
type
checking, preprocessor value
substitution introduces
subtle
problems
that can be avoided in C++ by
using const
values.
The
typical use of the
preprocessor to substitute values for
names
in
C looks like this:
#define
BUFSIZE 100
BUFSIZE
is
a name that only exists during
preprocessing, therefore
it
doesn't occupy storage and
can be placed in a header
file to
provide
a single value for all translation
units that use it. It's
very
important
for code maintenance to use
value substitution instead
of
so-called
"magic numbers." If you use
magic numbers in your
code,
not only does the reader
have no idea where the
numbers
come
from or what they represent, but if you decide to
change a
value,
you must perform hand editing, and you
have no trail to
354
Thinking
in C++
follow
to ensure you don't miss one of your
values (or
accidentally
change
one you shouldn't).
Most
of the time, BUFSIZE
will
behave like an ordinary
variable,
but
not all the time. In addition,
there's no type information.
This
can
hide bugs that are very
difficult to find. C++ uses
const
to
eliminate
these problems by bringing
value substitution into
the
domain
of the compiler. Now you can
say
const
int bufsize = 100;
You
can use bufsize
anyplace
where the compiler must know
the
value
at compile time. The
compiler can use bufsize
to
perform
constant
folding, which
means the compiler will
reduce a
complicated
constant expression to a simple
one by performing the
necessary
calculations at compile time.
This is especially
important
in
array definitions:
char
buf[bufsize];
You
can use const
for
all the built-in types
(char,
int,
float,
and
double)
and their variants (as well as
class objects, as you'll
see
later
in this chapter). Because of
subtle bugs that the
preprocessor
might
introduce, you should always
use const
instead
of #define
value
substitution.
const
in header files
To
use const
instead
of #define,
you must be able to place const
definitions
inside header files as you
can with #define.
This way,
you
can place the definition for
a const
in
a single place and
distribute
it to translation units by including
the header file. A
const
in
C++ defaults to internal
linkage; that
is, it is visible only
within
the file where it is defined
and cannot be seen at link time
by
other
translation units. You must always
assign a value to a const
when
you define it, except
when you
make an explicit declaration
using
extern:
8:
Constants
355
extern
const int bufsize;
Normally,
the C++ compiler avoids
creating storage for a const,
but
instead
holds the definition in its
symbol table. When you
use
extern
with
const,
however, you force storage to be
allocated (this
is
also true for certain other
cases, such as taking the
address of a
const).
Storage must be allocated because
extern
says
"use external
linkage,"
which means that several
translation units must be able
to
refer
to the item, which requires it to
have storage.
In
the ordinary case, when extern
is
not part of the definition,
no
storage
is allocated. When the const
is
used, it is simply folded in at
compile
time.
The
goal of never allocating
storage for a const
also
fails with
complicated
structures. Whenever the
compiler must allocate
storage,
constant folding is prevented
(since there's no way for
the
compiler
to know for sure what the value of
that storage is if it
could
know that, it wouldn't need to allocate
the storage).
Because
the compiler cannot always
avoid allocating storage for
a
const,
const
definitions
must
default to
internal linkage, that
is,
linkage
only within
that
particular translation unit.
Otherwise,
linker
errors would occur with complicated
consts
because they
cause
storage to be allocated in multiple
cpp
files.
The linker would
then
see the same definition in
multiple object files, and
complain.
Because
a const
defaults
to internal linkage, the
linker doesn't try to
link
those definitions across
translation units, and there
are no
collisions.
With built-in types, which are
used in the majority
of
cases
involving constant expressions, the
compiler can always
perform
constant folding.
Safety
consts
The
use of const
is
not limited to replacing #defines
in constant
expressions.
If you initialize a variable with a value
that is
produced
at runtime and you know it will not change for
the
356
Thinking
in C++
lifetime
of that variable, it is good
programming practice to make
it
a
const
so
the compiler will give you an
error message if you
accidentally
try to change it. Here's an
example:
//:
C08:Safecons.cpp
//
Using const for
safety
#include
<iostream>
using
namespace std;
const
int i = 100; // Typical
constant
const
int j = i + 10; // Value
from const expr
long
address = (long)&j; // Forces
storage
char
buf[j + 10]; // Still a
const expression
int
main() {
cout
<< "type a character &
CR:";
const
char c = cin.get(); // Can't
change
const
char c2 = c + 'a';
cout
<< c2;
//
...
}
///:~
You
can see that i
is
a compile-time const,
but j
is
calculated from i.
However,
because i
is
a const,
the calculated value for
j
still
comes
from
a constant expression and is itself a
compile-time constant.
The
very next line requires the
address of j
and
therefore forces the
compiler
to allocate storage for j.
Yet this doesn't prevent the
use of
j
in
the determination of the
size of buf
because
the compiler
knows
j
is
const
and
that the value is valid even
if storage was
allocated
to hold that value at some
point in the program.
In
main(
),
you see a different kind of const
in
the identifier c
because
the value cannot be known at
compile time. This
means
storage
is required, and the compiler
doesn't attempt to
keep
anything
in its symbol table (the
same behavior as in C).
The
initialization
must still happen at the
point of definition, and
once
the
initialization occurs, the
value cannot be changed. You
can see
that
c2
is
calculated from c
and
also that scoping works for
consts
as
it does for any other type yet
another improvement over
the
use
of #define.
8:
Constants
357
As
a matter of practice, if you think a
value shouldn't change,
you
should
make it a const.
This not only provides insurance
against
inadvertent
changes, it also allows the
compiler to generate
more
efficient
code by eliminating storage and memory
reads.
Aggregates
It's
possible to use const
for
aggregates, but you're virtually
assured
that the compiler will not be
sophisticated enough to
keep
an
aggregate in its symbol
table, so storage will be allocated.
In
these
situations, const
means
"a piece of storage that
cannot be
changed."
However, the value cannot be
used at compile time
because
the compiler is not required to know
the contents of the
storage
at compile time. In the following
code, you can see
the
statements
that are illegal:
//:
C08:Constag.cpp
//
Constants and
aggregates
const
int i[] = { 1, 2, 3, 4 };
//!
float f[i[3]]; //
Illegal
struct
S { int i, j; };
const
S s[] = { { 1, 2 }, { 3, 4 } };
//!
double d[s[1].j]; //
Illegal
int
main() {} ///:~
In
an array definition, the
compiler must be able to generate
code
that
moves the stack pointer to
accommodate the array. In
both of
the
illegal definitions above,
the compiler complains
because it
cannot
find a constant expression in the
array definition.
Differences
with C
Constants
were introduced in early
versions of C++ while the
Standard
C specification was still
being finished. Although the
C
committee
then decided to include const
in
C, somehow it came to
mean
for them "an ordinary variable that
cannot be changed." In C,
a
const
always
occupies storage and its
name is global. The C
compiler
cannot treat a const
as
a compile-time constant. In C, if
you
say
358
Thinking
in C++
const
int bufsize = 100;
char
buf[bufsize];
you
will get an error, even though it
seems like a rational thing
to
do.
Because bufsize
occupies
storage somewhere, the C
compiler
cannot
know the value at compile
time. You can optionally
say
const
int bufsize;
in
C, but not in C++, and the C compiler
accepts it as a declaration
indicating
there is storage allocated
elsewhere. Because C
defaults
to
external linkage for consts,
this makes sense. C++
defaults to
internal
linkage for consts
so if you want to accomplish the
same
thing
in C++, you must explicitly change
the linkage to
external
using
extern:
extern
const int bufsize; //
Declaration only
This
line also works in C.
In
C++, a const
doesn't
necessarily create storage. In C a
const
always
creates storage. Whether or not
storage is reserved for a
const
in
C++ depends on how it is used. In
general, if a const
is
used
simply to replace a name with a value
(just as you would use
a
#define),
then storage doesn't have to be
created for the const.
If
no
storage is created (this
depends on the complexity of
the data
type
and the sophistication of the
compiler), the values may
be
folded
into the code for greater
efficiency after type checking,
not
before,
as with #define.
If, however, you take an address of a
const
(even
unknowingly, by passing it to a function
that takes a
reference
argument) or you define it as extern,
then storage is
created
for the const.
In
C++, a const
that
is outside all functions has
file scope (i.e., it
is
invisible
outside the file). That is,
it defaults to internal
linkage.
This
is very different from all other
identifiers in C++ (and from
const
in
C!) that default to external
linkage. Thus, if you declare
a
const
of
the same name in two
different files and you don't take
the
8:
Constants
359
address
or define that name as
extern,
the ideal C++
compiler
won't
allocate storage for the
const,
but simply fold it into the code.
Because
const
has
implied file scope, you can
put it in C++ header
files
with no conflicts at link time.
Since
a const
in
C++ defaults to internal linkage, you
can't just
define
a const
in
one file and reference it as an
extern
in
another
file.
To give a const
external
linkage so it can be referenced
from
another
file, you must explicitly define it as
extern,
like this:
extern
const int x = 1;
Notice
that by giving it an initializer and
saying it is extern,
you
force
storage to be created for the
const
(although
the compiler still
has
the option of doing constant
folding here). The
initialization
establishes
this as a definition, not a declaration.
The declaration:
extern
const int x;
in
C++ means that the
definition exists elsewhere
(again, this is not
necessarily
true in C). You can now see why C++
requires a const
definition
to have an initializer: the
initializer distinguishes a
declaration
from a definition (in C it's always a
definition, so no
initializer
is necessary). With an extern
const declaration,
the
compiler
cannot do constant folding
because it doesn't know
the
value.
The
C approach to const
is
not very useful, and if you want to use
a
named
value inside a constant
expression (one that must
be
evaluated
at compile time), C almost
forces
you to use
#define
in
the
preprocessor.
Pointers
Pointers
can be made const.
The compiler will still
endeavor to
prevent
storage allocation and do constant
folding when dealing
with
const
pointers,
but these features seem less
useful in this case.
360
Thinking
in C++
More
importantly, the compiler will
tell you if you attempt to
change
a const
pointer,
which adds a great deal of
safety.
When
using const
with
pointers, you have two options:
const
can
be
applied to what the pointer is
pointing to, or the
const
can
be
applied
to the address stored in the
pointer itself. The syntax
for
these
is a little confusing at first but
becomes comfortable with
practice.
Pointer
to const
The
trick with a pointer definition, as with
any complicated
definition,
is to read it starting at the
identifier and work your way
out.
The const
specifier
binds to the thing it is "closest
to." So if you
want
to prevent any changes to the
element you are pointing
to,
you
write a definition like
this:
const
int* u;
Starting
from the identifier, we read
"u
is
a pointer, which points to
a
const
int." Here,
no initialization is required because
you're
saying
that u
can
point to anything (that is,
it is not const),
but the
thing
it points to cannot be
changed.
Here's
the mildly confusing part. You might
think that to make
the
pointer
itself unchangeable, that
is, to prevent any change to
the
address
contained inside u,
you would simply move the const
to
the
other side of the int
like
this:
int
const* v;
It's
not all that crazy to think that
this should read "v is
a const
pointer
to an int."
However, the way it actually
reads is
"v
is
an
ordinary
pointer to an int
that
happens to be const."
That is, the
const
has
bound itself to the int
again,
and the effect is the same
as
the
previous definition. The
fact that these two
definitions are the
same
is the confusing point; to
prevent this confusion on
the part of
your
reader, you should probably
stick to the first
form.
8:
Constants
361
const
pointer
To
make the pointer itself a
const,
you must place the const
specifier
to the right of the *,
like this:
int
d = 1;
int*
const w = &d;
Now
it reads:
"w
is
a pointer, which is const,
that points to an int."
Because
the pointer itself is now
the const,
the compiler requires
that
it be given an initial value
that will be unchanged for the
life of
that
pointer. It's OK, however, to change what
that value points to
by
saying
*w
= 2;
You
can also make a const
pointer
to a const
object
using either of
two
legal forms:
int
d = 1;
const
int* const x = &d; //
(1)
int
const* const x2 = &d; //
(2)
Now
neither the pointer nor the
object can be
changed.
Some
people argue that the
second form is more consistent
because
the
const
is
always placed to the right of what it
modifies. You'll
have
to decide which is clearer for your
particular coding
style.
Here
are the above lines in a
compileable file:
//:
C08:ConstPointers.cpp
const
int* u;
int
const* v;
int
d = 1;
int*
const w = &d;
const
int* const x = &d; //
(1)
int
const* const x2 = &d; //
(2)
int
main() {} ///:~
362
Thinking
in C++
Formatting
This
book makes a point of only putting
one pointer definition on
a
line,
and initializing each pointer at
the point of
definition
whenever
possible. Because of this,
the formatting style
of
"attaching"
the `*'
to the data type is
possible:
int*
u = &i;
as
if int*
were
a discrete type unto itself. This
makes the code
easier
to
understand, but unfortunately that's not
actually the way
things
work.
The `*'
in fact binds to the
identifier, not the type. It
can be
placed
anywhere between the type
name and the identifier. So
you
could
do this:
int
*u = &i, v = 0;
which
creates an int*
u,
as before, and a non-pointer int
v.
Because
readers
often find this confusing, it is
best to follow the form shown
in
this book.
Assignment
and type checking
C++
is very particular about type checking,
and this extends to
pointer
assignments. You can assign
the address of a non-const
object
to a const
pointer
because you're simply promising not
to
change
something that is OK to change. However,
you can't assign
the
address of a const
object
to a non-const
pointer
because then
you're
saying you might change the
object via the pointer.
Of
course,
you can always use a cast to
force such an assignment,
but
this
is bad programming practice
because you are then breaking
the
constness
of the object, along with any
safety promised by
the
const.
For example:
//:
C08:PointerAssignment.cpp
int
d = 1;
const
int e = 2;
int*
u = &d; // OK -- d not const
//!
int* v = &e; // Illegal -- e
const
int*
w = (int*)&e; // Legal but bad
practice
8:
Constants
363
int
main() {} ///:~
Although
C++ helps prevent errors it
does not protect you from
yourself
if you want to break the safety
mechanisms.
Character
array literals
The
place where strict constness
is not enforced is with character
array
literals. You can say
char*
cp = "howdy";
and
the compiler will accept it without
complaint. This is
technically
an error because a character
array literal ("howdy"
in
this
case) is created by the compiler as a
constant character
array,
and
the result of the quoted
character array is its
starting address in
memory.
Modifying any of the characters in the
array is a runtime
error,
although not all compilers enforce
this correctly.
So
character array literals are
actually constant character
arrays. Of
course,
the compiler lets you get
away with treating them as non-
const
because
there's so much existing C code
that relies on this.
However,
if you try to change the values in a
character array
literal,
the
behavior is undefined, although it will
probably work on many
machines.
If
you want to be able to modify the string,
put it in an array:
char
cp[] = "howdy";
Since
compilers often don't enforce
the difference you won't be
reminded
to use this latter form and so
the point becomes
rather
subtle.
Function
arguments
&
return values
The
use of const
to
specify function arguments and return
values is
another
place where the concept of
constants can be confusing.
If
364
Thinking
in C++
you
are passing objects by
value,
specifying const
has
no meaning to
the
client (it means that
the passed argument cannot
be modified
inside
the function). If you are
returning an object of a
user-defined
type
by value as a const,
it means the returned value
cannot be
modified.
If you are passing and returning
addresses,
const
is
a
promise
that the destination of the
address will not be changed.
Passing
by const value
You
can specify that function
arguments are const
when
passing
them
by value, such as
void
f1(const int i) {
i++;
// Illegal -- compile-time
error
}
but
what does this mean? You're
making a promise that
the
original
value of the variable will not be
changed by the
function
f1(
).
However, because the argument is
passed by value, you
immediately
make a copy of the original
variable, so the promise
to
the
client is implicitly
kept.
Inside
the function, the const
takes
on meaning: the
argument
cannot
be changed. So it's really a
tool for the creator of
the
function,
and not the caller.
To
avoid confusion to the
caller, you can make the
argument a
const
inside
the
function, rather than in the
argument list. You
could
do this with a pointer, but a nicer
syntax is achieved with
the
reference, a
subject that will be fully developed in
Chapter 11.
Briefly,
a reference is like a constant
pointer that is
automatically
dereferenced,
so it has the effect of
being an alias to an object.
To
create
a reference, you use the
&
in
the definition. So the
non-
confusing
function definition looks
like this:
void
f2(int ic) {
const
int& i = ic;
i++;
// Illegal -- compile-time
error
}
8:
Constants
365
Again,
you'll get an error message, but
this time the constness
of
the
local object is not part of
the function signature; it only
has
meaning
to the implementation of the
function and therefore
it's
hidden
from the client.
Returning
by const value
A
similar truth holds for the return
value. If you say that
a
function's
return value is const:
const
int g();
you
are promising that the
original variable (inside
the function
frame)
will not be modified. And again, because
you're returning it
by
value, it's copied so the
original value could never
be modified
via
the return value.
At
first, this can make
the specification of const
seem
meaningless.
You
can see the apparent
lack of effect of returning
consts
by value
in
this example:
//:
C08:Constval.cpp
//
Returning consts by
value
//
has no meaning for built-in
types
int
f3() { return 1; }
const
int f4() { return 1;
}
int
main() {
const
int j = f3(); // Works
fine
int
k = f4(); // But this works
fine too!
}
///:~
For
built-in types, it doesn't
matter whether you return by value
as
a
const,
so you should avoid confusing
the client programmer
and
leave
off the const
when
returning a built-in type by
value.
Returning
by value as a const
becomes
important when you're
dealing
with user-defined types. If a function
returns a class
object
by
value as a const,
the return value of that
function cannot be an
366
Thinking
in C++
lvalue
(that is, it cannot be
assigned to or otherwise modified).
For
example:
//:
C08:ConstReturnValues.cpp
//
Constant return by
value
//
Result cannot be used as an
lvalue
class
X {
int
i;
public:
X(int
ii = 0);
void
modify();
};
X::X(int
ii) { i = ii; }
void
X::modify() { i++; }
X
f5() {
return
X();
}
const
X f6() {
return
X();
}
void
f7(X& x) { // Pass by non-const
reference
x.modify();
}
int
main() {
f5()
= X(1); // OK -- non-const return
value
f5().modify();
// OK
//
Causes compile-time
errors:
//!
f7(f5());
//!
f6() = X(1);
//!
f6().modify();
//!
f7(f6());
}
///:~
f5(
) returns
a non-const
X object,
while f6(
) returns
a const
X
object.
Only the non-const
return
value can be used as an
lvalue.
8:
Constants
367
Thus,
it's important to use
const
when
returning an object by
value
if
you want to prevent its use as an
lvalue.
The
reason const
has
no meaning when you're returning a
built-in
type
by value is that the
compiler already prevents it from
being an
lvalue
(because it's always a
value, and not a variable). Only
when
you're
returning objects of user-defined
types by value does
it
become
an issue.
The
function f7(
) takes
its argument as a non-const
reference
(an
additional
way of handling addresses in C++ and the
subject of
Chapter
11). This is effectively the
same as taking a non-const
pointer;
it's just that the
syntax is different. The
reason this won't
compile
in C++ is because of the creation of a
temporary.
Temporaries
Sometimes,
during the evaluation of an expression,
the compiler
must
create temporary
objects. These
are objects like any other:
they
require
storage and they must be constructed and
destroyed. The
difference
is that you never see them
the compiler is
responsible
for
deciding that they're needed
and the details of their
existence.
But
there is one thing about
temporaries: they're
automatically
const.
Because you usually won't be able to get
your hands on a
temporary
object, telling it to do something
that will change that
temporary
is almost certainly a mistake
because you won't be able
to
use that information. By
making all temporaries
automatically
const,
the compiler informs you when you
make that mistake.
In
the above example, f5( )
returns
a non-const
X object.
But in the
expression:
f7(f5());
the
compiler must manufacture a temporary
object to hold the
return
value of f5(
) so
it can be passed to f7(
).
This would be fine if
f7(
) took
its argument by value; then
the temporary would be
copied
into f7(
) and
it wouldn't matter what happened to
the
368
Thinking
in C++
temporary
X.
However, f7(
) takes
its argument by
reference, which
means
in this example takes the
address of the temporary
X.
Since
f7(
) doesn't
take its argument by
const
reference,
it has permission
to
modify the temporary object. But
the compiler knows that
the
temporary
will vanish as soon as the
expression evaluation is
complete,
and thus any modifications you make to
the temporary X
will
be lost. By making all temporary
objects automatically const,
this
situation causes a compile-time
error so you don't get
caught
by
what would be a very difficult bug to
find.
However,
notice the expressions that
are legal:
f5()
= X(1);
f5().modify();
Although
these pass muster for the
compiler, they are
actually
problematic.
f5(
) returns
an X
object,
and for the compiler to
satisfy
the
above expressions it must create a
temporary to hold that
return
value. So in both expressions
the temporary object is
being
modified,
and as soon as the expression is
over the temporary is
cleaned
up. As a result, the modifications
are lost so this code
is
probably
a bug but the compiler doesn't
tell you anything about
it.
Expressions like these are
simple enough for you to detect
the
problem,
but when things get more
complex it's possible for a
bug
to
slip through these
cracks.
The
way the constness
of class objects is preserved is shown
later in
the
chapter.
Passing
and returning addresses
If
you pass or return an address (either a
pointer or a reference),
it's
possible
for the client programmer to
take it and modify the
original
value. If you make the
pointer or reference a const,
you
prevent
this from happening, which may save you
some grief. In
fact,
whenever you're passing an
address into a function, you
should
make it a const
if
at all possible. If you don't,
you're
8:
Constants
369
excluding
the possibility of using
that function with anything
that
is
a const.
The
choice of whether to return a pointer or
reference to a const
depends
on what you want to allow your client programmer to
do
with
it. Here's an example that
demonstrates the use of
const
pointers
as function arguments and return
values:
//:
C08:ConstPointer.cpp
//
Constant pointer
arg/return
void
t(int*) {}
void
u(const int* cip) {
//!
*cip = 2; // Illegal -- modifies
value
int
i = *cip; // OK -- copies
value
//!
int* ip2 = cip; //
Illegal: non-const
}
const
char* v() {
//
Returns address of static
character array:
return
"result of function
v()";
}
const
int* const w() {
static
int i;
return
&i;
}
int
main() {
int
x = 0;
int*
ip = &x;
const
int* cip = &x;
t(ip);
// OK
//!
t(cip); // Not OK
u(ip);
// OK
u(cip);
// Also OK
//!
char* cp = v(); // Not
OK
const
char* ccp = v(); //
OK
//!
int* ip2 = w(); // Not
OK
const
int* const ccip = w(); //
OK
const
int* cip2 = w(); //
OK
//!
*w() = 1; // Not OK
370
Thinking
in C++
}
///:~
The
function t(
) takes
an ordinary non-const
pointer
as an
argument,
and u(
) takes
a const
pointer.
Inside u(
) you
can see
that
attempting to modify the destination of
the const
pointer
is
illegal,
but you can of course copy
the information out into a non-
const
variable.
The compiler also prevents
you from creating a non-
const
pointer
using the address stored
inside a const
pointer.
The
functions v(
) and
w(
) test
return value semantics. v(
) returns
a
const
char* that is
created from a character array
literal. This
statement
actually produces the
address of the character
array
literal,
after the compiler creates
it and stores it in the static
storage
area.
As mentioned earlier, this
character array is technically
a
constant,
which is properly expressed by the return
value of v(
).
The
return value of w(
) requires
that both the pointer and
what it
points
to must be const.
As with v(
),
the value returned by
w(
) is
valid
after the function returns
only because it is static.
You never
want
to return pointers to local stack
variables because they will be
invalid
after the function returns
and the stack is cleaned
up.
(Another
common pointer you might return is the
address of
storage
allocated on the heap, which is
still valid after the
function
returns.)
In
main(
),
the functions are tested
with various arguments. You
can
see that t(
) will
accept a non-const
pointer
argument, but if you
try
to pass it a pointer to a const,
there's no promise that
t(
) will
leave
the pointer's destination
alone, so the compiler gives
you an
error
message. u(
) takes
a const
pointer,
so it will accept both
types
of
arguments. Thus, a function
that takes a const
pointer
is more
general
than one that does
not.
As
expected, the return value of
v(
) can
be assigned only to a
pointer
to a const.
You would also expect that
the compiler refuses
to
assign the return value of
w(
) to
a non-const
pointer,
and
accepts
a const
int* constbut it might be a
bit surprising to see
that
,
8:
Constants
371
it
also accepts a const
int* which is not an
exact match to the
return
,
type.
Once again, because the
value (which is the address
contained
in
the pointer) is being
copied, the promise that
the original
variable
is untouched is automatically kept.
Thus, the second const
in
const
int* constis only
meaningful when you try to use it as
an
lvalue,
in which case the compiler
prevents you.
Standard
argument passing
In
C it's very common to pass by
value, and when you want to pass
of
these approaches is preferred in
C++. Instead, your first
choice
when
passing an argument is to pass by
reference, and by const
reference
at that. To the client
programmer, the syntax is
identical
to
that of passing by value, so
there's no confusion about
pointers
they
don't even have to think about
pointers. For the creator of
the
function,
passing an address is virtually always
more efficient than
passing
an entire class object, and if you
pass by const
reference
it
means
your function will not change the
destination of that
address,
so the effect from the
client programmer's point of view
is
exactly
the same as pass-by-value (only
more efficient).
Because
of the syntax of references
(it looks like pass-by-value
to
the
caller) it's possible to
pass a temporary object to a
function that
takes
a const
reference,
whereas you can never pass a
temporary
object
to a function that takes a
pointer with a pointer, the
address
must
be explicitly taken. So passing by
reference produces a new
situation
that never occurs in C: a
temporary, which is always
const,
can have its address
passed to
a function. This is why, to
allow
temporaries to be passed to functions by
reference, the
argument
must be a const
reference.
The following example
demonstrates
this:
1
Some folks go as far as
saying that everything
in C is
pass by value, since when
you
pass
a pointer a copy is made (so
you're passing the pointer
by value). However
precise
this might be, I think it
actually confuses the
issue.
372
Thinking
in C++
//:
C08:ConstTemporary.cpp
//
Temporaries are const
class
X {};
X
f() { return X(); } //
Return by value
void
g1(X&) {} // Pass by non-const
reference
void
g2(const X&) {} // Pass by const
reference
int
main() {
//
Error: const temporary
created by f():
//!
g1(f());
//
OK: g2 takes a const
reference:
g2(f());
}
///:~
f(
) returns
an object of class
X by value. That
means when you
immediately
take the return value of
f(
) and
pass it to another
function
as in the calls to g1(
) and
g2(
),
a temporary is created and
that
temporary is const.
Thus, the call in g1( )
is
an error because
g1(
) doesn't
take a const
reference,
but the call to g2(
) is
OK.
Classes
This
section shows the ways you
can use const
with
classes. You
may
want to create a local const
in
a class to use inside
constant
expressions
that will be evaluated at compile
time. However, the
meaning
of const
is
different inside classes, so you
must
understand
the options in order to
create const
data
members of a
class.
You
can also make an entire
object const
(and
as you've just seen,
the
compiler always makes
temporary objects const).
But
preserving
the constness
of an object is more complex.
The
compiler
can ensure the constness
of a built-in type but it cannot
monitor
the intricacies of a class. To
guarantee the constness
of a
class
object, the const
member
function is introduced: only a const
member
function may be called for a const
object.
8:
Constants
373
const
in classes
One
of the places you'd like to
use a const
for
constant expressions
is
inside classes. The typical
example is when you're creating
an
array
inside a class and you want to use a
const
instead
of a
#define
to
establish the array size and
to use in calculations
involving
the array. The array
size is something you'd like to
keep
hidden
inside the class, so if you
used a name like size,
for
example,
you could use that name in
another class without a
clash.
The
preprocessor treats all #defines
as global from the point
they
are
defined, so this will not achieve
the desired effect.
You
might assume that the
logical choice is to place a
const
inside
the
class. This doesn't produce
the desired result. Inside a
class,
const
partially
reverts to its meaning in C. It
allocates storage
within
each object and represents a
value that is initialized
once
and
then cannot change. The use
of const
inside
a class means
"This
is constant for the lifetime of
the object." However,
each
different
object may contain a different
value for that
constant.
Thus,
when you create an ordinary
(non-static)
const
inside
a class,
you
cannot give it an initial
value. This initialization must
occur in
the
constructor, of course, but in a special
place in the
constructor.
Because
a const
must
be initialized at the point it is
created, inside
the
main body of the constructor the
const
must
already
be
initialized.
Otherwise you're left with
the choice of waiting
until
some
point later in the
constructor body, which means
the const
would
be un-initialized for a while. Also,
there would be nothing to
keep
you from changing the value of
the const
at
various places in
the
constructor body.
The
constructor initializer list
The
special initialization point is
called the constructor
initializer list,
and
it was originally developed for
use in inheritance (covered
in
Chapter
14). The constructor
initializer list which, as the
name
implies,
occurs only in the definition of
the constructor is a list
of
"constructor
calls" that occur after
the function argument list
and a
374
Thinking
in C++
colon,
but before the opening brace
of the constructor body.
This is
to
remind you that the
initialization in the list
occurs before any of
the
main constructor code is executed.
This is the place to put
all
const
initializations.
The proper form for const
inside
a class is
shown
here:
//:
C08:ConstInitialization.cpp
//
Initializing const in
classes
#include
<iostream>
using
namespace std;
class
Fred {
const
int size;
public:
Fred(int
sz);
void
print();
};
Fred::Fred(int
sz) : size(sz) {}
void
Fred::print() { cout << size
<< endl; }
int
main() {
Fred
a(1), b(2), c(3);
a.print(),
b.print(), c.print();
}
///:~
The
form of the constructor initializer
list shown above is
confusing
at
first because you're not
used to seeing a built-in type
treated as if
it
has a constructor.
"Constructors"
for built-in types
As
the language developed and
more effort was put into
making
user-defined
types look like built-in
types, it became apparent
that
there
were times when it was helpful to
make built-in types
look
like
user-defined types. In the
constructor initializer list, you
can
treat
a built-in type as if it has a
constructor, like
this:
//:
C08:BuiltInTypeConstructors.cpp
#include
<iostream>
using
namespace std;
8:
Constants
375
class
B {
int
i;
public:
B(int
ii);
void
print();
};
B::B(int
ii) : i(ii) {}
void
B::print() { cout << i <<
endl; }
int
main() {
B
a(1), b(2);
float
pi(3.14159);
a.print();
b.print();
cout
<< pi << endl;
}
///:~
This
is especially critical when initializing
const
data
members
because
they must be initialized before the
function body is
entered.
It
made sense to extend this
"constructor" for built-in types
(which
simply
means assignment) to the
general case, which is why the
float
pi(3.14159)
definition
works in the above
code.
It's
often useful to encapsulate a
built-in type inside a class
to
guarantee
initialization with the constructor.
For example, here's
an
Integer
class:
//:
C08:EncapsulatingTypes.cpp
#include
<iostream>
using
namespace std;
class
Integer {
int
i;
public:
Integer(int
ii = 0);
void
print();
};
Integer::Integer(int
ii) : i(ii) {}
void
Integer::print() { cout << i << ' ';
}
376
Thinking
in C++
int
main() {
Integer
i[100];
for(int
j = 0; j < 100; j++)
i[j].print();
}
///:~
The
array of Integers
in main(
) are
all automatically initialized to
zero.
This initialization isn't
necessarily more costly than a
for
loop
or
memset(
) Many
compilers easily optimize
this to a very fast
.
process.
Compile-time
constants in classes
The
above use of const
is
interesting and probably useful in
cases,
but
it does not solve the
original problem which is: "how do
you
make
a compile-time constant inside a
class?" The answer
requires
the
use of an additional keyword which will not be
fully
introduced
until Chapter 10: static.
The static
keyword,
in this
situation,
means "there's only one
instance, regardless of how
many
objects of the class are
created," which is precisely what
we
need
here: a member of a class which is
constant, and which cannot
change
from one object of the class
to another. Thus, a static
const
of
a built-in type can be treated as a
compile-time constant.
There
is one feature of static
constwhen used
inside classes which
is
a bit unusual: you must provide
the initializer at the point
of
definition
of the static
const This is
something that only
occurs
.
with
the static
const as much as you might
like to use it in
other
;
situations
it won't work because all other data
members must be
initialized
in the constructor or in other
member functions.
Here's
an example that shows the
creation and use of a static
const
//:
C08:StringStack.cpp
//
Using static const to create
a
2
At the time of this
writing, not all compilers
supported this
feature.
8:
Constants
377
//
compile-time constant inside a
class
#include
<string>
#include
<iostream>
using
namespace std;
class
StringStack {
static
const int size =
100;
const
string* stack[size];
int
index;
public:
StringStack();
void
push(const string*
s);
const
string* pop();
};
StringStack::StringStack()
: index(0) {
memset(stack,
0, size * sizeof(string*));
}
void
StringStack::push(const string* s)
{
if(index
< size)
stack[index++]
= s;
}
const
string* StringStack::pop() {
if(index
> 0) {
const
string* rv = stack[--index];
stack[index]
= 0;
return
rv;
}
return
0;
}
string
iceCream[] = {
"pralines
& cream",
"fudge
ripple",
"jamocha
almond fudge",
"wild
mountain blackberry",
"raspberry
sorbet",
"lemon
swirl",
"rocky
road",
"deep
chocolate fudge"
};
const
int iCsz =
378
Thinking
in C++
sizeof
iceCream / sizeof
*iceCream;
int
main() {
StringStack
ss;
for(int
i = 0; i < iCsz; i++)
ss.push(&iceCream[i]);
const
string* cp;
while((cp
= ss.pop()) != 0)
cout
<< *cp << endl;
}
///:~
Since
size
is
used to determine the size
of the array stack,
it is
indeed
a compile-time constant, but one
that is hidden inside
the
class.
Notice
that push(
) takes
a const
string* as an
argument, pop(
)
returns
a const
string*, and
StringStackholds
const
string* If
this
.
were
not true, you couldn't use a
StringStackto
hold the pointers
in
iceCream
However,
it also prevents you from doing
anything
.
that
will change the objects
contained by StringStack
Of
course,
.
not
all containers are designed with
this restriction.
The
"enum hack" in old code
In
older versions of C++,
static
const was not
supported inside
classes.
This meant that const
was
useless for constant
expressions
inside
classes. However, people still
wanted to do this so a
typical
solution
(usually referred to as the "enum
hack") was to use an
untagged
enum
with
no instances. An enumeration must have
all
its
values established at compile
time, it's local to the
class, and its
values
are available for constant
expressions. Thus, you will
commonly
see:
//:
C08:EnumHack.cpp
#include
<iostream>
using
namespace std;
class
Bunch {
enum
{ size = 1000 };
int
i[size];
};
8:
Constants
379
int
main() {
cout
<< "sizeof(Bunch) = " <<
sizeof(Bunch)
<<
", sizeof(i[1000]) = "
<<
sizeof(int[1000]) << endl;
}
///:~
The
use of enum
here
is guaranteed to occupy no storage in
the
object,
and the enumerators are all
evaluated at compile time.
You
can
also explicitly establish
the values of the
enumerators:
enum
{ one = 1, two = 2, three
};
With
integral enum
types,
the compiler will continue
counting
from
the last value, so the
enumerator three
will
get the value 3.
In
the StringStack.cppexample
above, the line:
static
const int size =
100;
would
be instead:
enum
{ size = 100 };
Although
you'll often see the
enum
technique
in legacy code, the
static
constfeature
was added to the language to
solve just this
problem.
However, there is no overwhelming reason
that you must
choose
static
constover
the enum
hack,
and in this book the
enum
hack
is used because it is supported by
more compilers at the
time
this
book was written.
const
objects & member functions
Class
member functions can be made
const.
What does this mean?
To
understand, you must first grasp
the concept of const
objects.
A
const
object
is defined the same for a
user-defined type as a built-
in
type. For example:
const
int i = 1;
const
blob b(2);
380
Thinking
in C++
Here,
b
is
a const
object
of type blob.
Its constructor is called with
an
argument of two. For the
compiler to enforce constness,
it must
ensure
that no data members of the
object are changed during
the
object's
lifetime. It can easily
ensure that no public data
is modified,
but
how is it to know which member functions will
change the data
and
which ones are "safe" for a
const
object?
If
you declare a member function
const,
you tell the compiler
the
function
can be called for a const
object.
A member function that
is
not
specifically declared const
is
treated as one that will
modify
data
members in an object, and the
compiler will not allow you to
call
it for a const
object.
It
doesn't stop there, however.
Just claiming
a member
function is
const
doesn't
guarantee it will act that way, so
the compiler forces
you
to reiterate the const
specification
when defining the
function.
(The
const
becomes
part of the function
signature, so both
the
compiler
and linker check for constness.)
Then it enforces constness
during
the function definition by
issuing an error message if
you
try
to change any members of the
object or
call a
non-const
member
function.
Thus, any member function you
declare const
is
guaranteed
to behave that way in the
definition.
To
understand the syntax for
declaring const
member
functions,
first
notice that preceding the
function declaration with const
means
the return value is const,
so that doesn't produce the
desired
results.
Instead, you must place the
const
specifier
after
the
argument
list. For example,
//:
C08:ConstMember.cpp
class
X {
int
i;
public:
X(int
ii);
int
f() const;
};
X::X(int
ii) : i(ii) {}
int
X::f() const { return i;
}
8:
Constants
381
int
main() {
X
x1(10);
const
X x2(20);
x1.f();
x2.f();
}
///:~
Note
that the const
keyword
must be repeated in the definition
or
the
compiler sees it as a different
function. Since f(
) is
a const
member
function, if it attempts to change
i
in
any way or
to
call
another
member function that is not
const,
the compiler flags it
as
an
error.
You
can see that a const
member
function is safe to call with
both
const
and
non-const
objects.
Thus, you could think of it as the
most
general
form of a member function (and
because of this, it is
unfortunate
that member functions do not
automatically default to
const).
Any function that doesn't modify
member data should be
declared
as const,
so it can be used with const
objects.
Here's
an example that contrasts a
const
and
non-const
member
function:
//:
C08:Quoter.cpp
//
Random quote
selection
#include
<iostream>
#include
<cstdlib> // Random number
generator
#include
<ctime> // To seed random
generator
using
namespace std;
class
Quoter {
int
lastquote;
public:
Quoter();
int
lastQuote() const;
const
char* quote();
};
Quoter::Quoter(){
lastquote
= -1;
srand(time(0));
// Seed random number
generator
382
Thinking
in C++
}
int
Quoter::lastQuote() const {
return
lastquote;
}
const
char* Quoter::quote() {
static
const char* quotes[] =
{
"Are
we having fun yet?",
"Doctors
always know best",
"Is
it ... Atomic?",
"Fear
is obscene",
"There
is no scientific evidence "
"to
support the idea "
"that
life is serious",
"Things
that make us happy, make us
wise",
};
const
int qsize = sizeof
quotes/sizeof *quotes;
int
qnum = rand() %
qsize;
while(lastquote
>= 0 && qnum ==
lastquote)
qnum
= rand() % qsize;
return
quotes[lastquote = qnum];
}
int
main() {
Quoter
q;
const
Quoter cq;
cq.lastQuote();
// OK
//!
cq.quote(); // Not OK;
non const function
for(int
i = 0; i < 20; i++)
cout
<< q.quote() << endl;
}
///:~
Neither
constructors nor destructors can be
const
member
functions
because they virtually always perform
some modification
on
the object during initialization and
cleanup. The quote(
)
member
function also cannot be
const
because
it modifies the data
member
lastquote(see
the return
statement).
However,
lastQuote(
)makes
no modifications, and so it can be
const
and
can
be
safely called for the
const
object
cq.
8:
Constants
383
mutable:
bitwise vs. logical const
What
if you want to create a const
member
function, but you'd still
like
to change some of the data
in the object? This is
sometimes
referred
to as the difference between
bitwise
const
and
logical const
(also
sometimes called memberwise
const).
Bitwise
const
means
that
every
bit in the object is
permanent, so a bit image of
the object will
never
change. Logical const
means
that, although the entire
object
is
conceptually constant, there may be
changes on a member-by-
member
basis. However, if the compiler is told
that an object is
const,
it will jealously guard that
object to ensure bitwise
constness.
To
effect logical constness,
there are two ways to change a
data
member
from within a const
member
function.
The
first approach is the
historical one and is called
casting
away
constness. It is
performed in a rather odd fashion. You
take this
(the
keyword
that produces the address of
the current object) and cast
it
to
a pointer to an object of the
current type. It would seem
that this
is
already
such a
pointer. However, inside a const
member
function
it's
actually a const
pointer,
so by casting it to an ordinary
pointer,
you
remove the constness
for that operation. Here's an
example:
//:
C08:Castaway.cpp
//
"Casting away"
constness
class
Y {
int
i;
public:
Y();
void
f() const;
};
Y::Y()
{ i = 0; }
void
Y::f() const {
//!
i++; // Error -- const
member function
((Y*)this)->i++;
// OK: cast away
const-ness
//
Better: use C++ explicit
cast syntax:
(const_cast<Y*>(this))->i++;
}
384
Thinking
in C++
int
main() {
const
Y yy;
yy.f();
// Actually changes
it!
}
///:~
This
approach works and you'll see it used in
legacy code, but it is
not
the preferred technique. The
problem is that this lack
of
constness
is hidden away in a member function
definition, and you
have
no clue from the class
interface that the data of
the object is
actually
being modified unless you
have access to the source
code
(and
you must suspect that constness
is being cast away, and
look
for
the cast). To put everything out in
the open, you should use
the
mutable
keyword
in the class declaration to
specify that a
particular
data member may be changed
inside a const
object:
//:
C08:Mutable.cpp
//
The "mutable" keyword
class
Z {
int
i;
mutable
int j;
public:
Z();
void
f() const;
};
Z::Z()
: i(0), j(0) {}
void
Z::f() const {
//!
i++; // Error -- const
member function
j++;
// OK: mutable
}
int
main() {
const
Z zz;
zz.f();
// Actually changes
it!
}
///:~
This
way, the user of the class
can see from the declaration
which
members
are likely to be modified in a
const
member
function.
8:
Constants
385
ROMability
If
an object is defined as const,
it is a candidate to be placed in
read-
only
memory (ROM), which is often an important
consideration in
embedded
systems programming. Simply
making an object const,
however,
is not enough the requirements for
ROMability are
much
stricter. Of course, the
object must be bitwise-const,
rather
than
logical-const.
This is easy to see if
logical constness
is
implemented
only through the mutable
keyword,
but probably not
detectable
by the compiler if constness
is cast away inside a const
member
function. In addition,
1.
The
class
or
struct
must
have no user-defined constructors
or
destructor.
2.
There
can be no base classes (covered in
Chapter 14) or
member
objects with user-defined constructors
or
destructors.
The
effect of a write operation on any part
of a const
object
of a
ROMable
type is undefined. Although a suitably
formed object
may
be placed in ROM, no objects are
ever required
to be
placed in
ROM.
volatile
The
syntax of volatileis
identical to that for const,
but volatile
means
"This data may change outside
the knowledge of the
compiler."
Somehow, the environment is
changing the data
(possibly
through multitasking, multithreading or
interrupts), and
volatiletells
the compiler not to make any
assumptions about
that
data,
especially during optimization.
If
the compiler says, "I read
this data into a register
earlier, and I
haven't
touched that register," normally it
wouldn't need to read
the
data again. But if the data
is volatile
the
compiler cannot make
,
such
an assumption because the
data may have been changed
by
another
process, and it must reread that
data rather than
386
Thinking
in C++
optimizing
the code to remove what would normally be
a
redundant
read.
You
create volatileobjects
using the same syntax
that you use to
create
const
objects.
You can also create
const
volatileobjects,
which
can't be changed by the
client programmer but
instead
change
through some outside agency.
Here is an example
that
might
represent a class associated with
some piece of
communication
hardware:
//:
C08:Volatile.cpp
//
The volatile keyword
class
Comm {
const
volatile unsigned char
byte;
volatile
unsigned char flag;
enum
{ bufsize = 100 };
unsigned
char buf[bufsize];
int
index;
public:
Comm();
void
isr() volatile;
char
read(int index)
const;
};
Comm::Comm()
: index(0), byte(0), flag(0)
{}
//
Only a demo; won't actually
work
//
as an interrupt service
routine:
void
Comm::isr() volatile {
flag
= 0;
buf[index++]
= byte;
//
Wrap to beginning of
buffer:
if(index
>= bufsize) index = 0;
}
char
Comm::read(int index) const
{
if(index
< 0 || index >= bufsize)
return
0;
return
buf[index];
}
int
main() {
8:
Constants
387
volatile
Comm Port;
Port.isr();
// OK
//!
Port.read(0); // Error, read()
not volatile
}
///:~
As
with const,
you can use volatilefor
data members, member
functions,
and objects themselves. You can only
call volatile
member
functions for volatileobjects.
The
reason that isr(
) can't
actually be used as an interrupt
service
routine
is that in a member function,
the address of the
current
object
(this)
must be secretly passed, and an ISR
generally wants no
arguments
at all. To solve this
problem, you can make
isr(
) a
static
member
function, a subject covered in
Chapter 10.
The
syntax of volatileis
identical to const,
so discussions of the two
are
often treated together. The
two are referred to in combination
as
the
c-v
qualifier.
Summary
The
const
keyword
gives you the ability to
define objects,
function
arguments,
return values and member functions as
constants, and
to
eliminate the preprocessor for
value substitution without
losing
any
preprocessor benefits. All this
provides a significant
additional
form
of type checking and safety in your
programming. The use
of
so-called
const
correctness (the
use of const
anywhere
you possibly
can)
can be a lifesaver for
projects.
Although
you can ignore const
and
continue to use old C
coding
practices,
it's there to help you.
Chapters 11 and on begin
using
references
heavily, and there you'll see
even more about how
critical
it is to use const
with
function arguments.
Exercises
Solutions
to selected exercises can be found in
the electronic document
The
Thinking in C++
Annotated
Solution
Guide,
available for a small fee
from .
388
Thinking
in C++
1.
Create
three const
int values,
then add them together to
produce
a value that determines the
size of an array in an
array
definition. Try to compile the
same code in C and
see
what happens (you can generally
force your C++
compiler
to run as a C compiler by using a
command-line
flag).
2.
Prove
to yourself that the C and C++
compilers really do
treat
constants differently. Create a
global const
and
use
it
in a global constant expression; then
compile it under
both
C and C++.
3.
Create
example const
definitions
for all the built-in
types
and
their variants. Use these in
expressions with other
consts
to make new const
definitions.
Make sure they
compile
successfully.
4.
Create
a const
definition
in a header file, include
that
header
file in two .cpp
files,
then compile those files
and
link
them. You should not get any
errors. Now try the
same
experiment with C.
5.
Create
a const
whose
value is determined at runtime by
reading
the time when the program
starts (you'll have to
use
the <ctime>
standard
header). Later in the
program,
try
to read a second value of
the time into your const
and
see
what happens.
6.
Create
a const
array
of char,
then try to change one of
the
chars.
7.
Create
an extern
constdeclaration
in one file, and put a
main(
) in
that file that prints
the value of the extern
const.
Provide an extern
constdefinition
in a second file,
then
compile and link the two files
together.
8.
Write
two pointers to const
long using
both forms of the
declaration.
Point one of them to an array of
long.
Demonstrate
that you can increment or
decrement the
pointer,
but you can't change what it points
to.
9.
Write
a const
pointer
to a double,
and point it at an array
of
double.
Show that you can change
what the pointer
8:
Constants
389
points
to, but you can't increment or
decrement the
pointer.
10.
Write
a const
pointer
to a const
object.
Show that you can
only
read the value that
the pointer points to, but
you
can't
change the pointer or what it
points to.
11.
Remove
the comment on the
error-generating line of
code
in PointerAssignment.cpp
see
the error that your
to
compiler
generates.
12.
Create
a character array literal with a
pointer that points
to
the beginning of the array.
Now use the pointer
to
modify
elements in the array. Does
your compiler report
this
as an error? Should it? If it
doesn't, why do you think
that
is?
13.
Create
a function that takes an
argument by value as a
const;
then try to change that argument in
the function
body.
14.
Create
a function that takes a
float
by
value. Inside the
function,
bind a const
float&to the
argument, and only
use
the reference from then on to ensure
that the
argument
is not changed.
15.
Modify
ConstReturnValues.cpp
removing
comments on
the
error-causing lines one at a
time, to see what
error
messages
your compiler generates.
16.
Modify
ConstPointer.cpp
removing
comments on the
error-causing
lines one at a time, to see
what error
messages
your compiler generates.
17.
Make
a new version of ConstPointer.cpp
called
ConstReference.cpp
which
demonstrates references
instead
of pointers (you may need to look forward
to
Chapter
11).
18.
Modify
ConstTemporary.cpp
removing
the comment on
the
error-causing line to see what
error messages your
compiler
generates.
19.
Create
a class containing both a
const
and
a non-const
float.
Initialize these using the
constructor initializer
list.
390
Thinking
in C++
20.
Create
a class called MyStringwhich
contains a string
and
has a constructor that
initializes the string,
and a
print(
)function.
Modify StringStack.cppso
that the
container
holds MyStringobjects,
and main(
) so
it prints
them.
21.
Create
a class containing a const
member
that you
initialize
in the constructor initializer
list and an
untagged
enumeration that you use to
determine an
array
size.
22.
In
ConstMember.cpp
remove
the const
specifier
on the
,
member
function definition, but leave it on
the
declaration,
to see what kind of compiler error
message
you
get.
23.
Create
a class with both const
and
non-const
member
functions.
Create const
and
non-const
objects
of this
class,
and try calling the different
types of member
functions
for the different types of
objects.
24.
Create
a class with both const
and
non-const
member
functions.
Try to call a non-const
member
function from
a
const
member
function to see what kind of
compiler
error
message you get.
25.
In
Mutable.cpp
remove
the comment on the
error-
,
causing
line to see what sort of
error message your
compiler
produces.
26.
Modify
Quoter.cppby
making quote(
)a
const
member
function
and lastquotemutable.
27.
Create
a class with a volatiledata
member. Create both
volatileand
non-volatilemember
functions that modify
the
volatiledata
member, and see what the
compiler
says.
Create both volatileand
non-volatileobjects
of
your
class and try calling both
the volatileand
non-
volatilemember
functions to see what is successful
and
what
kind of error messages the
compiler produces.
28.
Create
a class called bird
that
can fly(
) and
a class rock
that
can't. Create a rock
object,
take its address, and
8:
Constants
391
assign
that to a void*.
Now take the void*,
assign it to a
bird*
(you'll
have to use a cast), and
call fly(
) through
that
pointer. Is it clear why C's
permission to openly
assign
via a void*
(without
a cast) is a "hole" in the
language,
which couldn't be propagated into
C++?
392
Thinking
in C++
Table of Contents:
|
|||||