|
|||||
10:
Name Control
Creating
names is a fundamental activity
in
programming,
and when a project gets
large, the
number
of names can easily be
overwhelming.
427
C++
allows you a great deal of
control over the creation
and
visibility
of names, where storage for
those names is placed,
and
linkage
for names.
The
static
keyword
was overloaded in C before
people knew what
the
term "overload" meant, and C++ has
added yet another
meaning.
The underlying concept with all uses of
static
seems
to be
"something
that holds its position"
(like static electricity),
whether
that
means a physical location in memory or
visibility within a file.
In
this chapter, you'll learn how
static
controls
storage and
visibility,
and an improved way to control access to
names via
C++'s
namespace
feature.
You'll also find out how to use
functions
that
were written and compiled in C.
Static
elements from C
In
both C and C++ the keyword static
has
two basic meanings,
which
unfortunately often step on
each other's toes:
1.
Allocated
once at a fixed address;
that is, the object is
created
in
a special static
data area rather
than on the stack each time
a
function
is called. This is the
concept of static
storage.
2.
Local
to a particular translation unit (and
local to a class
scope
in C++, as you will see later).
Here, static
controls
the
visibility
of a name,
so that name cannot be seen
outside the
translation
unit or class. This also
describes the concept
of
linkage, which
determines what names the
linker will see.
This
section will look at the
above meanings of static
as
they were
inherited
from C.
static
variables inside functions
When
you create a local variable
inside a function, the
compiler
allocates
storage for that variable
each time the function is
called by
428
Thinking
in C++
moving
the stack pointer down an
appropriate amount. If there
is
an
initializer for the variable,
the initialization is performed
each
time
that sequence point is
passed.
Sometimes,
however, you want to retain a value
between function
calls.
You could accomplish this by
making a global variable,
but
then
that variable would not be under the
sole control of the
function.
C and C++ allow you to create a static
object
inside a
function;
the storage for this object
is not on the stack but instead
in
the
program's static data area.
This object is initialized only
once,
the
first time the function is
called, and then retains its
value
between
function invocations. For
example, the following
function
returns
the next character in the
array each time the
function is
called:
//:
C10:StaticVariablesInfunctions.cpp
#include
"../require.h"
#include
<iostream>
using
namespace std;
char
oneChar(const char* charArray = 0)
{
static
const char* s;
if(charArray)
{
s
= charArray;
return
*s;
}
else
require(s,
"un-initialized s");
if(*s
== '\0')
return
0;
return
*s++;
}
char*
a = "abcdefghijklmnopqrstuvwxyz";
int
main() {
//
oneChar(); // require()
fails
oneChar(a);
// Initializes s to a
char
c;
while((c
= oneChar()) != 0)
cout
<< c << endl;
10:
Name Control
429
}
///:~
The
static
char* sholds
its value between calls of
oneChar(
)
because
its storage is not part of
the stack frame of the
function, but
is
in the static storage area
of the program. When you
call
oneChar(
)with
a char*
argument,
s
is
assigned to that
argument,
and
the first character of the
array is returned. Each
subsequent call
to
oneChar(
)without
an
argument produces the
default value of
zero
for charArray
which
indicates to the function
that you are still
,
extracting
characters from the previously
initialized value of s.
The
function
will continue to produce characters until
it reaches the null
terminator
of the character array, at which
point it stops
incrementing
the pointer so it doesn't overrun
the end of the
array.
But
what happens if you call oneChar(
)with
no arguments and
without
previously initializing the
value of s?
In the definition for s,
you
could have provided an
initializer,
static
char* s = 0;
but
if you do not provide an initializer for a
static variable of a
built-in
type, the compiler
guarantees that variable will
be
initialized
to zero (converted to the
proper type) at program
start-
up.
So in oneChar(
) the
first time the function is
called, s
is
zero.
,
In
this case, the if(!s)
conditional
will catch it.
The
initialization above for s
is
very simple, but initialization
for
static
objects (like all other
objects) can be arbitrary
expressions
involving
constants and previously declared
variables and
functions.
You
should be aware that the
function above is very vulnerable
to
multithreading
problems; whenever you design
functions
containing
static variables you should
keep multithreading
issues
in
mind.
430
Thinking
in C++
static
class objects inside functions
The
rules are the same for
static objects of user-defined
types,
including
the fact that some
initialization is required for the
object.
However,
assignment to zero has
meaning only for built-in
types;
user-defined
types must be initialized with
constructor calls.
Thus,
if
you don't specify constructor arguments
when you define the
static
object, the class must have
a default constructor. For
example,
//:
C10:StaticObjectsInFunctions.cpp
#include
<iostream>
using
namespace std;
class
X {
int
i;
public:
X(int
ii = 0) : i(ii) {} // Default
~X()
{ cout << "X::~X()" << endl;
}
};
void
f() {
static
X x1(47);
static
X x2; // Default constructor
required
}
int
main() {
f();
}
///:~
The
static objects of type X
inside
f(
) can
be initialized either with
the
constructor argument list or with
the default constructor.
This
construction
occurs the first time
control passes through the
definition,
and only the first
time.
Static
object destructors
Destructors
for static objects (that is,
all objects with static
storage,
not
just local static objects as
in the example above) are
called when
main(
) exits
or when the Standard C library
function exit(
) is
explicitly
called. In most implementations,
main(
) just
calls exit(
)
when
it terminates. This means
that it can be dangerous to
call
exit(
) inside
a destructor because you can
end up with infinite
10:
Name Control
431
recursion.
Static object destructors
are not
called if
you exit the
program
using the Standard C library
function abort(
)
.
You
can specify actions to take
place when leaving main(
) (or
calling
exit(
))
by using the Standard C
library function atexit(
) In
.
this
case, the functions registered by
atexit(
)may
be called before
the
destructors for any objects constructed
before leaving main(
)
(or
calling exit(
)).
Like
ordinary destruction, destruction of
static objects occurs in
the
reverse
order of initialization. However, only
objects that have
been
constructed
are destroyed. Fortunately,
the C++ development
tools
keep
track of initialization order and
the objects that have
been
constructed.
Global objects are always
constructed before main(
) is
entered
and destroyed as main(
) exits,
but if a function containing
a
local static object is never
called, the constructor for
that object is
never
executed, so the destructor is
also not executed. For
example,
//:
C10:StaticDestructors.cpp
//
Static object
destructors
#include
<fstream>
using
namespace std;
ofstream
out("statdest.out"); // Trace
file
class
Obj {
char
c; // Identifier
public:
Obj(char
cc) : c(cc) {
out
<< "Obj::Obj() for " << c <<
endl;
}
~Obj()
{
out
<< "Obj::~Obj() for " << c <<
endl;
}
};
Obj
a('a'); // Global (static
storage)
//
Constructor & destructor always
called
void
f() {
static
Obj b('b');
}
432
Thinking
in C++
void
g() {
static
Obj c('c');
}
int
main() {
out
<< "inside main()" <<
endl;
f();
// Calls static constructor
for b
//
g() not called
out
<< "leaving main()" <<
endl;
}
///:~
In
Obj,
the char
c acts
as an identifier so the constructor
and
destructor
can print out information about
the object they're
working
on. The Obj
a is
a global object, so the
constructor is
always
called for it before main(
) is
entered, but the
constructors
for
the static
Obj binside
f(
) and
the static
Obj cinside
g(
) are
called
only if those functions are
called.
To
demonstrate which constructors and
destructors are called,
only
f(
) is
called. The output of the
program is
Obj::Obj()
for a
inside
main()
Obj::Obj()
for b
leaving
main()
Obj::~Obj()
for b
Obj::~Obj()
for a
The
constructor for a
is
called before main(
) is
entered, and the
constructor
for b
is
called only because f(
) is
called. When main(
)
exits,
the destructors for the
objects that have been
constructed are
called
in reverse order of their
construction. This means
that if g(
)
is
called,
the order in which the
destructors for b
and
c
are
called
depends
on whether f(
) or
g(
) is
called first.
Notice
that the trace file
ofstreamobject
out
is
also a static object
since
it is defined outside of all functions,
it lives in the
static
storage
area. It is important that
its definition (as opposed
to an
extern
declaration)
appear at the beginning of
the file, before
there
10:
Name Control
433
is
any possible use of out.
Otherwise, you'll be using an
object
before
it is properly initialized.
In
C++, the constructor for a
global static object is
called before
main(
) is
entered, so you now have a simple and
portable way to
execute
code before entering
main(
) and
to execute code with
the
destructor
after exiting main(
).
In C, this was always a
trial that
required
you to root around in the
compiler vendor's
assembly-
language
startup code.
Controlling
linkage
Ordinarily,
any name at file
scope (that
is, not nested inside a
class
or
function) is visible throughout all
translation units in a
program.
This
is often called external
linkage because at
link time the name is
visible
to the linker everywhere,
external to that translation
unit.
Global
variables and ordinary functions
have external
linkage.
There
are times when you'd like to limit
the visibility of a
name.
You
might like to have a variable at
file scope so all the
functions in
that
file can use it, but you
don't want functions outside that
file to
see
or access that variable, or to
inadvertently cause name
clashes
with
identifiers outside the
file.
An
object or function name at
file scope that is
explicitly declared
static
is
local to its translation unit (in
the terms of this book,
the
cpp
file
where the declaration
occurs). That name has
internal
linkage. This
means that you can use
the same name in
other
translation
units without a name
clash.
One
advantage to internal linkage is
that the name can be
placed in
a
header file without worrying that
there will be a clash at link
time.
Names that are commonly
placed in header files, such
as
const
definitions
and inline
functions,
default to internal
linkage.
(However,
const
defaults
to internal linkage only in C++; in C
it
defaults
to external linkage.) Note that
linkage refers only to
434
Thinking
in C++
elements
that have addresses at link/load
time; thus, class
declarations
and local variables have no
linkage.
Confusion
Here's
an example of how the two meanings of
static
can
cross over
each
other. All global objects
implicitly have static
storage class, so
if
you say (at file
scope),
int
a = 0;
then
storage for a
will
be in the program's static
data area, and the
initialization
for a
will
occur once, before main(
) is
entered. In
addition,
the visibility of a
is
global across all translation
units. In
terms
of visibility, the opposite of
static
(visible
only in this
translation
unit) is extern,
which explicitly states that
the visibility
of
the name is across all
translation units. So the
definition above is
equivalent
to saying
extern
int a = 0;
But
if you say instead,
static
int a = 0;
all
you've done is change the
visibility, so a
has
internal linkage.
The
storage class is unchanged
the object resides in the
static data
area
whether the visibility is
static
or
extern.
Once
you get into local variables,
static
stops
altering the
visibility
and
instead alters the storage
class.
If
you declare what appears to be a local
variable as extern,
it
means
that the storage exists
elsewhere (so the variable
is actually
global
to the function). For
example:
//:
C10:LocalExtern.cpp
//{L}
LocalExtern2
#include
<iostream>
int
main() {
10:
Name Control
435
extern
int i;
std::cout
<< i;
}
///:~
//:
C10:LocalExtern2.cpp {O}
int
i = 5;
///:~
With
function names (for
non-member functions), static
and
extern
can
only alter visibility, so if you
say
extern
void f();
it's
the same as the unadorned
declaration
void
f();
and
if you say,
static
void f();
it
means f(
) is
visible only within this translation unit
this is
sometimes
called file
static.
Other
storage class specifiers
You
will see static
and
extern
used
commonly. There are two
other
storage
class specifiers that occur
less often. The auto
specifier
is
almost
never used because it tells
the compiler that this is a
local
variable.
auto
is
short for "automatic" and it refers to
the way the
compiler
automatically allocates storage for
the variable. The
compiler
can always determine this
fact from the context in
which
the
variable is defined, so auto
is
redundant.
A
registervariable
is a local (auto)
variable, along with a hint to
the
compiler
that this particular
variable will be heavily used so
the
compiler
ought to keep it in a register if it can.
Thus, it is an
optimization
aid. Various compilers
respond differently to
this
hint;
they have the option to
ignore it. If you take the
address of the
variable,
the registerspecifier
will almost certainly be ignored.
You
436
Thinking
in C++
should
avoid using registerbecause
the compiler can usually do
a
better
job of optimization than you.
Namespaces
Although
names can be nested inside
classes, the names of
global
functions,
global variables, and classes
are still in a single
global
name
space. The static
keyword
gives you some control over
this
by
allowing you to give variables and
functions internal
linkage
(that
is, to make them file
static). But in a large project,
lack of
control
over the global name
space can cause problems. To
solve
these
problems for classes, vendors
often create long
complicated
names
that are unlikely to clash, but then
you're stuck typing
those
names.
(A typedef
is
often used to simplify
this.) It's not an elegant,
language-supported
solution.
You
can subdivide the global
name space into more
manageable
pieces
using the namespace
feature of
C++. The namespace
keyword,
similar to class,
struct,
enum,
and union,
puts the names
of
its members in a distinct
space. While the other
keywords have
additional
purposes, the creation of a new
name space is the
only
purpose
for namespace
.
Creating
a namespace
The
creation of a namespace is notably
similar to the creation of
a
class:
//:
C10:MyLib.cpp
namespace
MyLib {
//
Declarations
}
int
main() {} ///:~
This
produces a new namespace containing
the enclosed
declarations.
There are significant
differences from class,
struct,
union
and
enum,
however:
10:
Name Control
437
·
A
namespace definition can
appear only at global scope,
or
nested
within another namespace.
·
No
terminating semicolon is necessary
after the closing
brace
of
a namespace definition.
·
A
namespace definition can be
"continued" over multiple
header
files using a syntax that,
for a class, would appear to
be
a redefinition:
//:
C10:Header1.h
#ifndef
HEADER1_H
#define
HEADER1_H
namespace
MyLib {
extern
int x;
void
f();
//
...
}
#endif
// HEADER1_H ///:~
//:
C10:Header2.h
#ifndef
HEADER2_H
#define
HEADER2_H
#include
"Header1.h"
//
Add more names to
MyLib
namespace
MyLib { // NOT a
redefinition!
extern
int y;
void
g();
//
...
}
#endif
// HEADER2_H ///:~
//:
C10:Continuation.cpp
#include
"Header2.h"
int
main() {} ///:~
·
A
namespace name can be aliased
to another
name, so you
don't
have to type an unwieldy name created by
a library
vendor:
//:
C10:BobsSuperDuperLibrary.cpp
namespace
BobsSuperDuperLibrary {
class
Widget { /* ... */ };
438
Thinking
in C++
class
Poppit { /* ... */ };
//
...
}
//
Too much to type! I'll
alias it:
namespace
Bob = BobsSuperDuperLibrary;
int
main() {} ///:~
·
You
cannot create an instance of a
namespace as you can
with
a class.
Unnamed
namespaces
Each
translation unit contains an unnamed
namespace that you
can
add
to by saying "namespace
without
an identifier:
"
//:
C10:UnnamedNamespaces.cpp
namespace
{
class
Arm { /* ... */ };
class
Leg { /* ... */ };
class
Head { /* ... */ };
class
Robot {
Arm
arm[4];
Leg
leg[16];
Head
head[3];
//
...
}
xanthan;
int
i, j, k;
}
int
main() {} ///:~
The
names in this space are
automatically available in
that
translation
unit without qualification. It is guaranteed
that an
unnamed
space is unique for each
translation unit. If you put local
names
in an unnamed namespace, you don't need
to give them
internal
linkage by making them static.
C++
deprecates the use of file
statics in favor of the
unnamed
namespace.
Friends
You
can inject
a
friend
declaration
into a namespace by declaring it
within
an enclosed class:
10:
Name Control
439
//:
C10:FriendInjection.cpp
namespace
Me {
class
Us {
//...
friend
void you();
};
}
int
main() {} ///:~
Now
the function you(
) is
a member of the namespace
Me.
If
you introduce a friend within a class in
the global namespace,
the
friend
is injected globally.
Using
a namespace
You
can refer to a name within a
namespace in three ways:
by
specifying
the name using the
scope resolution operator, with
a
using
directive
to introduce all names in the
namespace, or with a
using
declaration
to introduce names one at a
time.
Scope
resolution
Any
name in a namespace can be
explicitly specified using
the
scope
resolution operator in the
same way that you can refer
to the
names
within a class:
//:
C10:ScopeResolution.cpp
namespace
X {
class
Y {
static
int i;
public:
void
f();
};
class
Z;
void
func();
}
int
X::Y::i = 9;
class
X::Z {
int
u, v, w;
public:
Z(int
i);
int
g();
440
Thinking
in C++
};
X::Z::Z(int
i) { u = v = w = i; }
int
X::Z::g() { return u = v = w = 0;
}
void
X::func() {
X::Z
a(1);
a.g();
}
int
main(){} ///:~
Notice
that the definition
X::Y::i
could
just as easily be referring to
a
data
member of a class Y
nested
in a class X
instead
of a namespace
X.
So
far, namespaces look very much
like classes.
The
using directive
Because
it can rapidly get tedious
to type the full qualification for
an
identifier in a namespace, the
using
keyword
allows you to
import
an entire namespace at once. When
used in conjunction
with
the namespacekeyword
this is called a using
directive.
The
using
directive
makes names appear as if they
belong to the nearest
enclosing
namespace scope, so you can
conveniently use the
unqualified
names. Consider a simple
namespace:
//:
C10:NamespaceInt.h
#ifndef
NAMESPACEINT_H
#define
NAMESPACEINT_H
namespace
Int {
enum
sign { positive, negative
};
class
Integer {
int
i;
sign
s;
public:
Integer(int
ii = 0)
:
i(ii),
s(i
>= 0 ? positive : negative)
{}
sign
getSign() const { return s;
}
void
setSign(sign sgn) { s = sgn;
}
//
...
};
10:
Name Control
441
}
#endif
// NAMESPACEINT_H ///:~
One
use of the using
directive
is to bring all of the names in
Int
into
another
namespace, leaving those
names nested within
the
namespace:
//:
C10:NamespaceMath.h
#ifndef
NAMESPACEMATH_H
#define
NAMESPACEMATH_H
#include
"NamespaceInt.h"
namespace
Math {
using
namespace Int;
Integer
a, b;
Integer
divide(Integer, Integer);
//
...
}
#endif
// NAMESPACEMATH_H ///:~
You
can also declare all of the
names in Int
inside
a function, but
leave
those names nested within
the function:
//:
C10:Arithmetic.cpp
#include
"NamespaceInt.h"
void
arithmetic() {
using
namespace Int;
Integer
x;
x.setSign(positive);
}
int
main(){} ///:~
Without
the using
directive,
all the names in the
namespace would
need
to be fully qualified.
One
aspect of the using
directive
may seem slightly
counterintuitive
at first. The visibility of
the names introduced
with
a
using
directive
is the scope in which the
directive is made. But
you
can override the names from
the using
directive
as if they've
been
declared globally to that
scope!
//:
C10:NamespaceOverriding1.cpp
#include
"NamespaceMath.h"
442
Thinking
in C++
int
main() {
using
namespace Math;
Integer
a; // Hides Math::a;
a.setSign(negative);
//
Now scope resolution is
necessary
//
to select Math::a :
Math::a.setSign(positive);
}
///:~
Suppose
you have a second namespace
that contains some of
the
names
in namespace
Math
:
//:
C10:NamespaceOverriding2.h
#ifndef
NAMESPACEOVERRIDING2_H
#define
NAMESPACEOVERRIDING2_H
#include
"NamespaceInt.h"
namespace
Calculation {
using
namespace Int;
Integer
divide(Integer, Integer);
//
...
}
#endif
// NAMESPACEOVERRIDING2_H ///:~
Since
this namespace is also
introduced with a using
directive,
you
have
the possibility of a collision. However,
the ambiguity appears
at
the point of use
of the
name, not at the using
directive:
//:
C10:OverridingAmbiguity.cpp
#include
"NamespaceMath.h"
#include
"NamespaceOverriding2.h"
void
s() {
using
namespace Math;
using
namespace Calculation;
//
Everything's ok until:
//!
divide(1, 2); //
Ambiguity
}
int
main() {} ///:~
Thus,
it's possible to write using
directives
to introduce a number
of
namespaces with conflicting names without
ever producing an
ambiguity.
10:
Name Control
443
The
using declaration
You
can inject names one at a
time into the current scope
with a
using
declaration. Unlike
the using
directive,
which treats names as
if
they were declared globally to
the scope, a using
declaration
is a
declaration
within the current scope.
This means it can
override
names
from a using
directive:
//:
C10:UsingDeclaration.h
#ifndef
USINGDECLARATION_H
#define
USINGDECLARATION_H
namespace
U {
inline
void f() {}
inline
void g() {}
}
namespace
V {
inline
void f() {}
inline
void g() {}
}
#endif
// USINGDECLARATION_H ///:~
//:
C10:UsingDeclaration1.cpp
#include
"UsingDeclaration.h"
void
h() {
using
namespace U; // Using
directive
using
V::f; // Using
declaration
f();
// Calls V::f();
U::f();
// Must fully qualify to
call
}
int
main() {} ///:~
The
using
declaration
just gives the fully
specified name of the
identifier,
but no type information. This means
that if the
namespace
contains a set of overloaded
functions with the
same
name,
the using
declaration
declares all the functions in
the
overloaded
set.
You
can put a using
declaration
anywhere a normal declaration
can
occur.
A using
declaration
works like a normal declaration in
all
ways
but one: because you don't give an
argument list, it's
possible
for
a using
declaration
to cause the overload of a
function with the
same
argument types (which isn't
allowed with normal
444
Thinking
in C++
overloading).
This ambiguity, however,
doesn't show up until the
point
of use, rather than the
point of declaration.
A
using
declaration
can also appear within a
namespace, and it has
the
same effect as anywhere else
that name is declared within
the
space:
//:
C10:UsingDeclaration2.cpp
#include
"UsingDeclaration.h"
namespace
Q {
using
U::f;
using
V::g;
//
...
}
void
m() {
using
namespace Q;
f();
// Calls U::f();
g();
// Calls V::g();
}
int
main() {} ///:~
A
using
declaration
is an alias, and it allows you to declare
the
same
function in separate namespaces. If you
end up re-declaring
the
same function by importing
different namespaces, it's OK
there
won't be any ambiguities or
duplications.
The
use of namespaces
Some
of the rules above may seem
a bit daunting at first,
especially
if
you get the impression that
you'll be using them all the time.
In
general,
however, you can get away with very
simple usage of
namespaces
as long as you understand how they work. The
key
thing
to remember is that when you introduce a
global using
directive
(via a "using
namespace outside of
any scope) you have
"
thrown
open the namespace for that
file. This is usually fine for
an
implementation
file (a "cpp"
file) because the using
directive
is
only
in effect until the end of
the compilation of that
file. That is, it
doesn't
affect any other files, so you
can adjust the control of
the
namespaces
one implementation file at a
time. For example, if
you
discover
a name clash because of too
many using
directives
in a
10:
Name Control
445
particular
implementation file, it is a simple
matter to change that
file
so that it uses explicit
qualifications or using
declarations
to
eliminate
the clash, without modifying
other implementation
files.
Header
files are a different issue.
You virtually never want to
introduce
a global using
directive
into a header file, because
that
would
mean that any other file
that included your header
would
also
have the namespace thrown
open (and header files
can include
other
header files).
So,
in header files you should
either use explicit
qualification or
scoped
using
directives
and using
declarations.
This is the practice
that
you will find in this book, and by following it you
will not
"pollute"
the global namespace and throw
yourself back into
the
pre-namespace
world of C++.
Static
members in C++
There
are times when you need a
single storage space to be
used by
all
objects of a class. In C, you would use a
global variable, but
this
is
not very safe. Global data
can be modified by anyone, and
its
name
can clash with other
identical names in a large
project. It
would
be ideal if the data could
be stored as if it were global, but
be
hidden
inside a class, and clearly
associated with that
class.
This
is accomplished with static
data
members inside a class.
There
is
a single piece of storage for a
static
data
member, regardless of
how
many objects of that class you
create. All objects share
the
same
static
storage
space for that data member,
so it is a way for
them
to "communicate" with each other. But
the static
data
belongs
to
the class; its name is
scoped inside the class and
it can be public,
private,
or protected
.
Defining
storage for static data members
Because
static
data
has a single piece of
storage regardless of how
many
objects are created, that
storage must be defined in a
single
446
Thinking
in C++
place.
The compiler will not allocate
storage for you. The linker
will
report
an error if a static
data
member is declared but not
defined.
The
definition must occur outside
the class (no inlining is
allowed),
and
only one definition is allowed.
Thus, it is common to put it in
the
implementation file for the
class. The syntax sometimes
gives
people
trouble, but it is actually quite
logical. For example, if
you
create
a static data member inside
a class like this:
class
A {
static
int i;
public:
//...
};
Then
you must define storage for that
static data member in
the
definition
file like this:
int
A::i = 1;
If
you were to define an ordinary
global variable, you would
say
int
i = 1;
but
here, the scope resolution
operator and the class name
are used
to
specify A::i.
Some
people have trouble with the
idea that A::i
is
private,
and yet
here's
something that seems to be
manipulating it right out in the
open.
Doesn't this break the
protection mechanism? It's a
completely
safe practice for two reasons.
First, the only place
this
initialization
is legal is in the definition.
Indeed, if the static
data
were
an object with a constructor, you would
call the constructor
instead
of using the =
operator.
Second, once the definition
has
been
made, the end-user cannot
make a second definition
the
linker
will report an error. And the
class creator is forced to
create
the
definition or the code won't link during
testing. This ensures
that
the definition happens only
once and that it's in the
hands of
the
class creator.
10:
Name Control
447
The
entire initialization expression for a
static member is in
the
scope
of the class. For
example,
//:
C10:Statinit.cpp
//
Scope of static
initializer
#include
<iostream>
using
namespace std;
int
x = 100;
class
WithStatic {
static
int x;
static
int y;
public:
void
print() const {
cout
<< "WithStatic::x = " << x <<
endl;
cout
<< "WithStatic::y = " << y <<
endl;
}
};
int
WithStatic::x = 1;
int
WithStatic::y = x + 1;
//
WithStatic::x NOT ::x
int
main() {
WithStatic
ws;
ws.print();
}
///:~
Here,
the qualification WithStatic::extends
the scope of WithStatic
to
the entire
definition.
static
array initialization
Chapter
8 introduced the static
constvariable
that allows you to
define
a constant value inside a
class body. It's also
possible to
create
arrays of static
objects,
both const
and
non-const.
The syntax
is
reasonably consistent:
//:
C10:StaticArray.cpp
//
Initializing static arrays in
classes
class
Values {
//
static consts are
initialized in-place:
static
const int scSize =
100;
448
Thinking
in C++
static
const long scLong =
100;
//
Automatic counting works
with static arrays.
//
Arrays, Non-integral and
non-const statics
//
must be initialized
externally:
static
const int scInts[];
static
const long scLongs[];
static
const float
scTable[];
static
const char
scLetters[];
static
int size;
static
const float scFloat;
static
float table[];
static
char letters[];
};
int
Values::size = 100;
const
float Values::scFloat =
1.1;
const
int Values::scInts[] = {
99,
47, 33, 11, 7
};
const
long Values::scLongs[] = {
99,
47, 33, 11, 7
};
const
float Values::scTable[] = {
1.1,
2.2, 3.3, 4.4
};
const
char Values::scLetters[] = {
'a',
'b', 'c', 'd',
'e',
'f',
'g', 'h', 'i',
'j'
};
float
Values::table[4] = {
1.1,
2.2, 3.3, 4.4
};
char
Values::letters[10] = {
'a',
'b', 'c', 'd',
'e',
'f',
'g', 'h', 'i',
'j'
};
int
main() { Values v; }
///:~
10:
Name Control
449
With
static
const of
integral types you can
provide the
definitions
s
inside
the class, but for everything
else (including arrays of
integral
types,
even if they are static
const you must
provide a single
)
external
definition for the member.
These definitions have
internal
linkage,
so they can be placed in header
files. The syntax for
initializing
static arrays is the same as
for any aggregate, including
automatic
counting.
You
can also create static
constobjects of
class types and arrays
of
such
objects. However, you cannot initialize
them using the
"inline
syntax"
allowed for static
consts of
integral built-in
types:
//:
C10:StaticObjectArrays.cpp
//
Static arrays of class
objects
class
X {
int
i;
public:
X(int
ii) : i(ii) {}
};
class
Stat {
//
This doesn't work,
although
//
you might want it
to:
//!
static const X
x(100);
//
Both const and non-const
static class
//
objects must be initialized
externally:
static
X x2;
static
X xTable2[];
static
const X x3;
static
const X xTable3[];
};
X
Stat::x2(100);
X
Stat::xTable2[] = {
X(1),
X(2), X(3), X(4)
};
const
X Stat::x3(100);
const
X Stat::xTable3[] = {
X(1),
X(2), X(3), X(4)
450
Thinking
in C++
};
int
main() { Stat v; }
///:~
The
initialization of both const
and
non-const
static arrays of
class
objects
must be performed the same way, following
the typical
static
definition
syntax.
Nested
and local classes
You
can easily put static data
members in classes that are
nested
inside
other classes. The
definition of such members is an
intuitive
and
obvious extension you simply use
another level of
scope
resolution.
However, you cannot have static
data
members inside
local
classes (a local class is a class
defined inside a function).
Thus,
//:
C10:Local.cpp
//
Static members & local
classes
#include
<iostream>
using
namespace std;
//
Nested class CAN have
static data members:
class
Outer {
class
Inner {
static
int i; // OK
};
};
int
Outer::Inner::i = 47;
//
Local class cannot have
static data members:
void
f() {
class
Local {
public:
//!
static int i; //
Error
//
(How would you define
i?)
}
x;
}
int
main() { Outer x; f(); }
///:~
10:
Name Control
451
You
can see the immediate
problem with a static
member
in a local
class:
How do you describe the data
member at file scope in
order
to
define it? In practice,
local classes are used very
rarely.
static
member functions
You
can also create static
member
functions that, like
static
data
members,
work for the class as a whole rather than
for a particular
object
of a class. Instead of making a
global function that lives
in
and
"pollutes" the global or
local namespace, you bring
the
function
inside the class. When you
create a static
member
function,
you are expressing an association with a
particular class.
You
can call a static
member
function in the ordinary way, with
the
dot
or the arrow, in association with an
object. However, it's
more
typical
to call a static
member
function by itself, without any
specific
object, using the
scope-resolution operator, like
this:
//:
C10:SimpleStaticMemberFunction.cpp
class
X {
public:
static
void f(){};
};
int
main() {
X::f();
}
///:~
When
you see static member
functions in a class, remember
that the
designer
intended that function to be
conceptually associated with
the
class as a whole.
A
static
member
function cannot access
ordinary data
members,
only
static
data
members. It can call only
other static
member
functions.
Normally, the address of the
current object (this)
is
quietly
passed in when any member function is
called, but a static
member
has no this,
which is the reason it cannot
access ordinary
members.
Thus, you get the tiny
increase in speed afforded by
a
global
function because a static
member
function doesn't have
the
452
Thinking
in C++
extra
overhead of passing this.
At the same time you get
the
benefits
of having the function
inside the class.
For
data members, static
indicates
that only one piece of
storage for
member
data exists for all objects of a
class. This parallels the
use of
static
to
define objects inside
a function
to mean that only one
copy
of
a local variable is used for all
calls of that
function.
Here's
an example showing static
data
members and static
member
functions
used together:
//:
C10:StaticMemberFunctions.cpp
class
X {
int
i;
static
int j;
public:
X(int
ii = 0) : i(ii) {
//
Non-static member function
can access
//
static member function or
data:
j
= i;
}
int
val() const { return i;
}
static
int incr() {
//!
i++; // Error: static member
function
//
cannot access non-static
member data
return
++j;
}
static
int f() {
//!
val(); // Error: static
member function
//
cannot access non-static
member function
return
incr(); // OK -- calls
static
}
};
int
X::j = 0;
int
main() {
X
x;
X*
xp = &x;
x.f();
xp->f();
X::f();
// Only works with static
members
}
///:~
10:
Name Control
453
Because
they have no this
pointer,
static
member
functions can
neither
access non-static
data
members nor call non-static
member
functions.
Notice
in main(
) that
a static
member
can be selected using
the
usual
dot or arrow syntax, associating that
function with an object,
but
also with no object (because a
static
member
is associated with
a
class, not a particular object),
using the class name and
scope
resolution
operator.
Here's
an interesting feature: Because of
the way initialization
happens
for static
member
objects, you can put a static
data
member
of the same class inside
that
class. Here's an example
that
allows
only a single object of type Egg
to
exist by making the
constructor
private. You can access that
object, but you can't
create
any
new Egg
objects:
//:
C10:Singleton.cpp
//
Static member of same type,
ensures that
//
only one object of this
type exists.
//
Also referred to as the
"singleton" pattern.
#include
<iostream>
using
namespace std;
class
Egg {
static
Egg e;
int
i;
Egg(int
ii) : i(ii) {}
Egg(const
Egg&); // Prevent
copy-construction
public:
static
Egg* instance() { return &e;
}
int
val() const { return i;
}
};
Egg
Egg::e(47);
int
main() {
//!
Egg x(1); // Error --
can't create an Egg
//
You can access the
single instance:
cout
<< Egg::instance()->val() <<
endl;
}
///:~
454
Thinking
in C++
The
initialization for E
happens
after the class declaration
is
complete,
so the compiler has all the
information it needs to
allocate
storage and make the
constructor call.
To
completely prevent the
creation of any other objects,
something
else
has been added: a second
private constructor called
the copy-
constructor. At this
point in the book, you
cannot know why this is
necessary
since the copy constructor
will not be introduced until
the
next chapter. However, as a sneak
preview, if you were to
remove
the copy-constructor defined in
the example above,
you'd
be
able to create an Egg
object
like this:
Egg
e = *Egg::instance();
Egg
e2(*Egg::instance());
Both
of these use the
copy-constructor, so to seal off that
possibility
the
copy-constructor is declared as private
(no definition is
necessary
because it never gets
called). A large portion of
the next
chapter
is a discussion of the copy-constructor
so it should become
clear
to you then.
Static
initialization dependency
Within
a specific translation unit, the
order of initialization of
static
objects
is guaranteed to be the order in which
the object
definitions
appear
in that translation unit. The
order of destruction is
guaranteed
to be the reverse of the
order of initialization.
However,
there is no guarantee concerning
the order of
initialization
of static objects across
translation
units, and the
language
provides no way to specify this
order. This can
cause
significant
problems. As an example of an instant
disaster (which
will
halt primitive operating systems and kill
the process on
sophisticated
ones), if one file
contains
//
First file
#include
<fstream>
10:
Name Control
455
std::ofstream
out("out.txt");
and
another file uses the
out
object
in one of its
initializers
//
Second file
#include
<fstream>
extern
std::ofstream out;
class
Oof {
public:
Oof()
{ std::out << "ouch"; }
}
oof;
the
program may work, and it may not. If the
programming
environment
builds the program so that
the first file is
initialized
before
the second file, then there
will be no problem. However, if
the
second file is initialized
before the first, the
constructor for Oof
relies
upon the existence of out,
which hasn't been constructed
yet
and
this causes chaos.
This
problem only occurs with static
object initializers that
depend
on
each other. The statics in a
translation unit are initialized
before
the
first invocation of a function in
that unit but it could be
after
main(
).
You can't be sure about the
order of initialization of
static
objects
if they're in different
files.
at
the global scope:
extern
int y;
int
x = y + 1;
and
in a second file you have at
the global scope:
extern
int x;
int
y = x + 1;
1Bjarne Stroustrup and
Margaret Ellis, The
Annotated C++ Reference
Manual,
Addison-
Wesley,
1990, pp. 20-21.
456
Thinking
in C++
For
all static objects, the
linking-loading mechanism guarantees
a
static
initialization to zero before
the dynamic
initialization
specified
by the programmer takes
place. In the previous
example,
zeroing
of the storage occupied by
the fstream
outobject
has no
special
meaning, so it is truly undefined until
the constructor is
called.
However, with built-in types,
initialization to zero does
have
meaning,
and if the files are
initialized in the order they
are shown
above,
y
begins
as statically initialized to zero, so
x
becomes
one,
and
y
is
dynamically initialized to two. However, if
the files are
initialized
in the opposite order,
x
is
statically initialized to zero,
y
is
dynamically initialized to one, and
x
then
becomes two.
Programmers
must be aware of this because they
can create a
program
with static initialization dependencies
and get it working
on
one platform, but move it to another
compiling environment
where
it suddenly, mysteriously, doesn't
work.
What
to do
There
are three approaches to
dealing with this
problem:
1.
Don't
do it. Avoiding static initialization
dependencies is the
best
solution.
2.
If
you must do it, put the critical
static object definitions in
a
single
file, so you can portably
control their initialization
by
putting
them in the correct
order.
3.
If
you're convinced it's unavoidable to
scatter static
objects
across
translation units as in the
case of a library,
where
you
can't control the programmer
who uses it there are
two
programmatic
techniques to solve the
problem.
Technique
one
This
technique was pioneered by
Jerry Schwarz while creating
the
iostream
library (because the
definitions for cin,
cout,
and cerr
are
static
and
live in a separate file). It's
actually inferior to the
second
technique
but it's been around a long
time and so you may come
10:
Name Control
457
across
code that uses it; thus
it's important that you
understand
how
it works.
This
technique requires an additional
class in your library
header
file.
This class is responsible for
the dynamic initialization of
your
library's
static objects. Here is a
simple example:
//:
C10:Initializer.h
//
Static initialization
technique
#ifndef
INITIALIZER_H
#define
INITIALIZER_H
#include
<iostream>
extern
int x; // Declarations, not
definitions
extern
int y;
class
Initializer {
static
int initCount;
public:
Initializer()
{
std::cout
<< "Initializer()" <<
std::endl;
//
Initialize first time
only
if(initCount++
== 0) {
std::cout
<< "performing
initialization"
<<
std::endl;
x
= 100;
y
= 200;
}
}
~Initializer()
{
std::cout
<< "~Initializer()" <<
std::endl;
//
Clean up last time
only
if(--initCount
== 0) {
std::cout
<< "performing cleanup"
<<
std::endl;
//
Any necessary cleanup
here
}
}
};
//
The following creates one
object in each
//
file where Initializer.h is
included, but that
//
object is only visible
within that file:
static
Initializer init;
458
Thinking
in C++
#endif
// INITIALIZER_H ///:~
The
declarations for x
and
y
announce
only that these objects
exist,
but
they don't allocate storage for the
objects. However, the
definition
for the Initializer
init
allocates
storage for that object
in
every
file where the header is
included. But because the
name is
static
(controlling
visibility this time, not
the way storage is
allocated;
storage is at file scope by
default), it is visible only
within
that
translation unit, so the linker will not
complain about multiple
definition
errors.
Here
is the file containing the
definitions for x,
y,
and initCount
:
//:
C10:InitializerDefs.cpp {O}
//
Definitions for
Initializer.h
#include
"Initializer.h"
//
Static initialization will
force
//
all these values to
zero:
int
x;
int
y;
int
Initializer::initCount;
///:~
(Of
course, a file static
instance of init
is
also placed in this
file
when
the header is included.)
Suppose that two other files
are
created
by the library user:
//:
C10:Initializer.cpp {O}
//
Static initialization
#include
"Initializer.h"
///:~
and
//:
C10:Initializer2.cpp
//{L}
InitializerDefs Initializer
//
Static initialization
#include
"Initializer.h"
using
namespace std;
int
main() {
cout
<< "inside main()" <<
endl;
10:
Name Control
459
cout
<< "leaving main()" <<
endl;
}
///:~
Now
it doesn't matter which translation unit
is initialized first.
The
first
time a translation unit containing
Initializer.his
initialized,
initCountwill
be zero so the initialization will be
performed. (This
depends
heavily on the fact that
the static storage area is
set to zero
before
any dynamic initialization takes
place.) For all the rest of
the
translation
units, initCountwill
be nonzero and the
initialization
will
be skipped. Cleanup happens in
the reverse order,
and
~Initializer(
)ensures
that it will happen only
once.
This
example used built-in types
as the global static
objects. The
technique
also works with classes, but those
objects must then be
dynamically
initialized by the Initializerclass.
One way to do this
is
to create the classes without
constructors and destructors, but
instead
with initialization and cleanup member
functions using
different
names. A more common
approach, however, is to
have
pointers
to objects and to create them using
new
inside
Initializer(
.)
Technique
two
Long
after technique one was in
use, someone (I don't know who)
came
up with the technique explained in
this section, which is
much
simpler and cleaner than technique
one. The fact that it
took
so
long to discover is a tribute to the
complexity of C++.
This
technique relies on the fact
that static objects inside
functions
are
initialized the first time
(only) that the function is
called. Keep
in
mind that the problem we're
really trying to solve here is
not
when
the
static objects are
initialized (that can be
controlled
separately)
but rather making sure that
the initialization happens
in
the
proper order.
This
technique is very neat and clever.
For any initialization
dependency,
you place a static object
inside a function that
returns
a
reference to that object.
This way, the only way you can
access the
460
Thinking
in C++
static
object is by calling the
function, and if that object
needs to
access
other static objects on which it is
dependent it must call their
functions.
And the first time a
function is called, it forces
the
initialization
to take place. The order of
static initialization is
guaranteed
to be correct because of the
design of the code,
not
because
of an arbitrary order established by
the linker.
To
set up an example, here are
two classes that depend on
each
other.
The first one contains a
bool
that
is initialized only by the
constructor,
so you can tell if the
constructor has been called
for a
static
instance of the class (the
static storage area is
initialized to
zero
at program startup, which produces a
false
value
for the bool
if
the constructor has not been
called):
//:
C10:Dependency1.h
#ifndef
DEPENDENCY1_H
#define
DEPENDENCY1_H
#include
<iostream>
class
Dependency1 {
bool
init;
public:
Dependency1()
: init(true) {
std::cout
<< "Dependency1
construction"
<<
std::endl;
}
void
print() const {
std::cout
<< "Dependency1 init: "
<<
init << std::endl;
}
};
#endif
// DEPENDENCY1_H ///:~
The
constructor also announces when it is
being called, and you
can
print(
)the
state of the object to find out if it
has been
initialized.
The
second class is initialized from an
object of the first class,
which
is
what will cause the
dependency:
//:
C10:Dependency2.h
10:
Name Control
461
#ifndef
DEPENDENCY2_H
#define
DEPENDENCY2_H
#include
"Dependency1.h"
class
Dependency2 {
Dependency1
d1;
public:
Dependency2(const
Dependency1& dep1):
d1(dep1){
std::cout
<< "Dependency2 construction
";
print();
}
void
print() const { d1.print();
}
};
#endif
// DEPENDENCY2_H ///:~
The
constructor announces itself and
prints the state of the
d1
object
so you can see if it has
been initialized by the time
the
constructor
is called.
To
demonstrate what can go wrong, the
following file first puts
the
static
object definitions in the wrong
order, as they would occur if
the
linker happened to initialize
the Dependency2object
before the
Dependency1object.
Then the order is reversed to show how
it
works
correctly if the order
happens to be "right." Lastly,
technique
two
is demonstrated.
To
provide more readable output,
the function separator(
)is
created.
The trick is that you can't
call a function globally
unless
that
function is being used to
perform the initialization of
a
variable,
so separator(
)returns
a dummy value that is used
to
initialize
a couple of global
variables.
//:
C10:Technique2.cpp
#include
"Dependency2.h"
using
namespace std;
//
Returns a value so it can be
called as
//
a global initializer:
int
separator() {
cout
<< "---------------------" <<
endl;
return
1;
462
Thinking
in C++
}
//
Simulate the dependency
problem:
extern
Dependency1 dep1;
Dependency2
dep2(dep1);
Dependency1
dep1;
int
x1 = separator();
//
But if it happens in this
order it works OK:
Dependency1
dep1b;
Dependency2
dep2b(dep1b);
int
x2 = separator();
//
Wrapping static objects in
functions succeeds
Dependency1&
d1() {
static
Dependency1 dep1;
return
dep1;
}
Dependency2&
d2() {
static
Dependency2 dep2(d1());
return
dep2;
}
int
main() {
Dependency2&
dep2 = d2();
}
///:~
The
functions d1(
) and
d2(
) wrap
static instances of Dependency1
and
Dependency2objects.
Now, the only way you can get to
the
static
objects is by calling the
functions and that forces
static
initialization
on the first function call.
This means that
initialization
is
guaranteed to be correct, which you'll
see when you run the
program
and look at the output.
Here's
how you would actually organize the
code to use the
technique.
Ordinarily, the static
objects would be defined in
separate
files (because you're forced
to for some reason;
remember
that
defining the static objects
in separate files is what causes
the
problem),
so instead you define the
wrapping functions in
separate
files.
But they'll need to be declared in header
files:
10:
Name Control
463
//:
C10:Dependency1StatFun.h
#ifndef
DEPENDENCY1STATFUN_H
#define
DEPENDENCY1STATFUN_H
#include
"Dependency1.h"
extern
Dependency1& d1();
#endif
// DEPENDENCY1STATFUN_H ///:~
Actually,
the "extern" is redundant for
the function
declaration.
Here's
the second header
file:
//:
C10:Dependency2StatFun.h
#ifndef
DEPENDENCY2STATFUN_H
#define
DEPENDENCY2STATFUN_H
#include
"Dependency2.h"
extern
Dependency2& d2();
#endif
// DEPENDENCY2STATFUN_H ///:~
Now,
in the implementation files
where you would previously
have
placed the static object
definitions, you instead place
the
wrapping
function definitions:
//:
C10:Dependency1StatFun.cpp {O}
#include
"Dependency1StatFun.h"
Dependency1&
d1() {
static
Dependency1 dep1;
return
dep1;
}
///:~
Presumably,
other code might also be
placed in these files.
Here's
the
other file:
//:
C10:Dependency2StatFun.cpp {O}
#include
"Dependency1StatFun.h"
#include
"Dependency2StatFun.h"
Dependency2&
d2() {
static
Dependency2 dep2(d1());
return
dep2;
}
///:~
So
now there are two files that
could be linked in any order and
if
they
contained ordinary static
objects could produce any
order of
initialization.
But since they contain the
wrapping functions,
there's
no
threat of incorrect
initialization:
464
Thinking
in C++
//:
C10:Technique2b.cpp
//{L}
Dependency1StatFun
Dependency2StatFun
#include
"Dependency2StatFun.h"
int
main() { d2(); }
///:~
When
you run this program you'll see
that the initialization of
the
Dependency1static
object always happens before
the initialization
of
the Dependency2static
object. You can also see
that this is a
much
simpler approach than technique
one.
You
might be tempted to write d1(
) and
d2(
) as
inline functions
inside
their respective header
files, but this is something you
must
definitely
not do. An inline function
can be duplicated in every
file
in
which it appears and this
duplication includes
the
static object
definition.
Because inline functions
automatically default to
internal
linkage, this would result in
having multiple static
objects
across
the various translation
units, which would certainly
cause
problems.
So you must ensure that there is only
one definition of
each
wrapping function, and this
means not making the
wrapping
functions
inline.
Alternate
linkage specifications
What
happens if you're writing a program in
C++ and you want to
use
a C library? If you make the C
function declaration,
float
f(int a, char b);
the
C++ compiler will decorate this
name to something
like
_f_int_charto
support function overloading
(and type-safe
linkage).
However, the C compiler that
compiled your C library
has
most
definitely not
decorated
the name, so its internal
name will be
_f.
Thus, the linker will not be
able to resolve your C++ calls to
f(
).
The
escape mechanism provided in C++ is
the alternate
linkage
specification, which
was produced in the language
by overloading
the
extern
keyword.
The extern
is
followed by a string
that
10:
Name Control
465
specifies
the linkage you want for the
declaration, followed by
the
declaration:
extern
"C" float f(int a, char
b);
This
tells the compiler to give C
linkage to f(
) so
that the compiler
doesn't
decorate the name. The only
two types of linkage
specifications
supported by the standard
are "C"
and
"C++,"
but
compiler
vendors have the option of
supporting other languages
in
the
same way.
If
you have a group of declarations with
alternate linkage, put them
inside
braces, like this:
extern
"C" {
float
f(int a, char b);
double
d(int a, char b);
}
Or,
for a header file,
extern
"C" {
#include
"Myheader.h"
}
Most
C++ compiler vendors handle
the alternate linkage
specifications
inside their header files
that work with both C and
C++,
so you don't have to worry about
it.
Summary
The
static
keyword
can be confusing because in
some situations it
controls
the location of storage, and in
others it controls
visibility
and
linkage of a name.
With
the introduction of C++ namespaces, you
have an improved
and
more flexible alternative to
control the proliferation of
names
in
large projects.
466
Thinking
in C++
The
use of static
inside
classes is one more way to control
names in
a
program. The names do not
clash with global names, and
the
visibility
and access is kept within the
program, giving you
greater
control
in the maintenance of your
code.
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 .
1.
Create
a function with a static variable
that is a pointer
(with
a default argument of zero). When
the caller
provides
a value for this argument it is
used to point at
the
beginning of an array of int.
If you call the
function
with
a zero argument (using the
default argument),
the
function
returns the next value in
the array, until it sees
a
"-1"
value in the array (to
act as an end-of-array
indicator).
Exercise this function in
main(
).
2.
Create
a function that returns the
next value in a
Fibonacci
sequence every time you call
it. Add an
argument
that is a bool
with
a default value of false
such
that
when you give the argument with
true
it
"resets" the
function
to the beginning of the
Fibonacci sequence.
Exercise
this function in main(
).
3.
Create
a class that holds an array
of ints.
Set the size of
the
array using static
const int
inside
the class. Add a
const
int variable,
and initialize it in the
constructor
initializer
list; make the constructor
inline.
Add a static
int
member
variable and initialize it to a specific
value.
Add
a static
member
function that prints the
static
data
member.
Add an inline
member
function called print(
)
to
print out all the values in the
array and to call the
static
member
function. Exercise this
class in main(
).
4.
Create
a class called Monitor
that
keeps track of the
number
of times that its incident(
)member
function has
been
called. Add a print(
)member
function that
displays
10:
Name Control
467
the
number of incidents. Now create a global
function
(not
a member function) containing a
static
Monitor
object.
Each time you call the
function it should
call
incident(
) then
print(
)to
display the incident
count.
,
Exercise
the function in main(
).
5.
Modify
the Monitor
class
from Exercise 4 so that you
can
decrement(
)the
incident count. Make a class
Monitor2
that
takes as a constructor argument a
pointer to a
Monitor1
and
which stores that pointer and
calls
,
incident(
)and
print(
) In
the destructor for Monitor2
.
,
call
decrement(
)and
print(
) Now
make a static
object
.
of
Monitor2inside
a function. Inside main(
),
experiment
with
calling the function and not
calling the function
to
see
what happens with the destructor of
Monitor2
.
6.
Make
a global object of Monitor2and
see what happens.
7.
Create
a class with a destructor that
prints a message and
then
calls exit(
).
Create a global object of
this class and
see
what happens.
8.
In
StaticDestructors.cppexperiment
with the order of
,
constructor
and destructor calls by calling
f(
) and
g(
)
inside
main(
) in
different orders. Does your
compiler get
it
right?
9.
In
StaticDestructors.cpptest
the default error
handling
,
of
your implementation by turning the
original definition
of
out
into
an extern
declaration
and putting the actual
definition
after the definition of
a
(whose
Obj
constructor
sends
information to out).
Make sure there's nothing
else
important
running on your machine when you run
the
program
or that your machine will handle
faults
robustly.
10.
Prove
that file static variables
in header files don't
clash
with
each other when included in
more than one cpp
file.
11.
Create
a simple class containing an
int,
a constructor that
initializes
the int
from
its argument, a member
function
to
set the int
from
its argument, and a print(
)function
that
prints the int.
Put your class in a header file,
and
468
Thinking
in C++
include
the header file in two
cpp
files.
In one cpp
file
make
an instance of your class, and in the
other declare
that
identifier extern
and
test it inside main(
).
Remember,
you'll have to link the two object
files or else
the
linker won't find the
object.
12.
Make
the instance of the object
in Exercise 11 static
and
verify
that it cannot be found by the
linker because of
this.
13.
Declare
a function in a header file.
Define the function
in
one
cpp
file
and call it inside main(
) in
a second cpp
file.
Compile
and verify that it works. Now change
the
function
definition so that it is static
and
verify that the
linker
cannot find it.
14.
Modify
Volatile.cppfrom
Chapter 8 to make
comm::isr(
)something
that could actually work as
an
interrupt
service routine. Hint: an interrupt
service
routine
doesn't take any
arguments.
15.
Write
and compile a simple program
that uses the auto
and
registerkeywords.
16.
Create
a header file containing a
namespace
Inside
the
.
namespacecreate
several function declarations.
Now
create
a second header file that
includes the first one
and
continues
the namespace
adding
several more function
,
declarations.
Now create a cpp
file
that includes the
second
header file. Alias your
namespace to another
(shorter)
name. Inside a function
definition, call one
of
your
functions using scope
resolution. Inside a
separate
function
definition, write a using
directive
to introduce
your
namespace into that function
scope, and show that
you
don't need scope resolution to
call the functions
from
your
namespace.
17.
Create
a header file with an unnamed
namespace.
Include
the header in two separate
cpp
files
and show
that
an unnamed space is unique for
each translation
unit.
10:
Name Control
469
18.
Using
the header file from
Exercise 17, show that
the
names
in an unnamed namespace are
automatically
available
in a translation unit without
qualification.
19.
Modify
FriendInjection.cpp
add
a definition for the
to
friend
function and to call the
function inside main(
).
20.
In
Arithmetic.cpp
demonstrate
that the using
directive
,
does
not extend outside the
function in which the
directive
was made.
21.
Repair
the problem in OverridingAmbiguity.cppfirst
,
with
scope resolution, then instead with a
using
declaration
that forces the compiler to
choose one of the
identical
function names.
22.
In
two header files, create two
namespaces, each
containing
a class (with all inline definitions)
with a
name
identical to that in the
other namespace. Create
a
cpp
file
that includes both header
files. Create a
function,
and
inside the function use
the using
directive
to
introduce
both namespaces. Try creating an
object of the
class
and see what happens. Make
the using
directives
global
(outside of the function) to
see if it makes any
difference.
Repair the problem using
scope resolution,
and
create objects of both
classes.
23.
Repair
the problem in Exercise 22 with a
using
declaration
that forces the compiler to
choose one of the
identical
class names.
24.
Extract
the namespace declarations
in
BobsSuperDuperLibrary.cpp
and
UnnamedNamespaces.cpp
and
put them in separate
header
files, giving the unnamed
namespace a name in
the
process. In a third header file
create a new namespace
that
combines the elements of the
other two namespaces
with
using
declarations.
In main(
),
introduce your new
namespace
with a using
directive
and access all the
elements
of your namespace.
25.
Create
a header file that includes
<string>and
<iostream>but
does not use any using
directives
or
470
Thinking
in C++
using
declarations.
Add "include guards" as you've
seen
in
the header files in this
book. Create a class with
all
inline
functions that contains a
string
member,
with a
constructor
that initializes that
string
from
its argument
and
a print(
)function
that displays the string.
Create a
cpp
file
and exercise your class in main(
).
26.
Create
a class containing a static
double and
long.
Write
a
static
member
function that prints out the
values.
27.
Create
a class containing an int,
a constructor that
initializes
the int
from
its argument, and a print(
)
function
to display the int.
Now create a second
class
that
contains a static
object
of the first one. Add a
static
member
function that calls the
static
object's
print(
)
function.
Exercise your class in main(
).
28.
Create
a class containing both a
const
and
a non-const
static
array
of int.
Write static
methods
to print out the
arrays.
Exercise your class in main(
).
29.
Create
a class containing a string,
with a constructor that
initializes
the string
from
its argument, and a print(
)
function
to display the string.
Create another class
that
contains
both const
and
non-const
static arrays of
objects
of
the first class, and
static
methods
to print out these
arrays.
Exercise this second class
in main(
).
30.
Create
a struct
that
contains an int
and
a default
constructor
that initializes the
int
to
zero. Make this
struct
local
to a function. Inside that
function, create an
array
of objects of your struct
and
demonstrate that each
int
in
the array has automatically
been initialized to
zero.
31.
Create
a class that represents a
printer connection, and
that
only allows you to have one
printer.
32.
In
a header file, create a
class Mirror
that
contains two
data
members: a pointer to a Mirror
object
and a bool.
Give
it two constructors: the default
constructor
initializes
the bool
to
true
and
the Mirror
pointer
to zero.
The
second constructor takes as an
argument a pointer to
a
Mirror
object,
which it assigns to the object's
internal
10:
Name Control
471
pointer;
it sets the bool
to
false.
Add a member function
test(
):
if the object's pointer is
nonzero, it returns
the
value
of test(
) called
through the pointer. If the
pointer is
zero,
it returns the bool.
Now create five cpp
files,
each
of
which includes the Mirror
header.
The first cpp
file
defines
a global Mirror
object
using the default
constructor.
The second file declares
the object in the
first
file
as extern,
and defines a global Mirror
object
using
the
second constructor, with a pointer to
the first object.
Keep
doing this until you reach
the last file, which
will
also
contain a global object
definition. In that file,
main(
)
should
call the test(
) function
and report the result. If
the
result
is true,
find out how to change the linking
order
for
your linker and change it until the
result is false.
33.
Repair
the problem in Exercise 32
using technique one
shown
in this book.
34.
Repair
the problem in Exercise 32
using technique two
shown
in this book.
35.
Without
including a header file,
declare the function
puts(
) from
the Standard C Library. Call
this function
from
main(
).
472
Thinking
in C++
Table of Contents:
|
|||||