|
|||||
6:
Initialization
&
Cleanup
Chapter
4 made a significant improvement in
library
use
by taking all the scattered
components of a typical
C
library and encapsulating
them into a structure
(an
abstract
data type, called a class from now
on).
301
This
not only provides a single unified
point of entry into a library
component,
but it also hides the names
of the functions within
the
class
name. In Chapter 5, access
control (implementation
hiding)
was
introduced. This gives the
class designer a way to
establish
clear
boundaries for determining what the
client programmer is
allowed
to manipulate and what is off limits. It
means the internal
mechanisms
of a data type's operation
are under the control
and
discretion
of the class designer, and
it's clear to client
programmers
what
members they can and should pay
attention to.
Together,
encapsulation and access control
make a significant
step
in
improving the ease of library
use. The concept of "new
data
type"
they provide is better in some ways than
the existing built-in
data
types from C. The C++ compiler
can now provide type-
checking
guarantees for that data type and thus
ensure a level of
safety
when that data type is being
used.
When
it comes to safety, however,
there's a lot more the
compiler
can
do for us than C provides. In this and future
chapters, you'll
see
additional features that
have been engineered into C++
that
make
the bugs in your program
almost leap out and grab
you,
sometimes
before you even compile the
program, but usually in the
form
of compiler warnings and errors.
For this reason, you
will
soon
get used to the
unlikely-sounding scenario that a
C++
program
that compiles often runs right the
first time.
Two
of these safety issues are
initialization and cleanup. A
large
segment
of C bugs occur when the
programmer forgets to
initialize
or
clean up a variable. This is
especially true with C libraries,
when
client
programmers don't know how to initialize a
struct,
or even
that
they must. (Libraries often do not
include an initialization
function,
so the client programmer is
forced to initialize the
struct
by
hand.) Cleanup is a special
problem because C programmers
are
comfortable
with forgetting about variables
once they are
finished,
so
any cleaning up that may be necessary for
a library's struct
is
often
missed.
302
Thinking
in C++
In
C++, the concept of
initialization and cleanup is essential
for
easy
library use and to eliminate
the many subtle bugs that
occur
when
the client programmer
forgets to perform these
activities.
This
chapter examines the
features in C++ that help
guarantee
proper
initialization and cleanup.
Guaranteed
initialization with the
constructor
Both
the Stash
and
Stack
classes
defined previously have
a
function
called initialize(
,
which hints by its name that
it should
)
be
called before using the
object in any other way.
Unfortunately,
this
means the client programmer
must ensure proper
initialization.
Client
programmers are prone to
miss details like
initialization in
their
headlong rush to make your amazing
library solve their
problem.
In C++, initialization is too
important to leave to the
client
programmer.
The class designer can
guarantee initialization of
every
object by providing a special
function called the constructor. If
a
class has a constructor, the
compiler automatically calls
that
constructor
at the point an object is
created, before
client
programmers
can get their hands on
the object. The constructor
call
isn't
even an option for the
client programmer; it is performed
by
the
compiler at the point the
object is defined.
The
next challenge is what to name
this function. There are
two
issues.
The first is that any name
you use is something that
can
potentially
clash with a name you might like to
use as a member in
the
class. The second is that
because the compiler is
responsible for
calling
the constructor, it must always know
which function to call.
The
solution Stroustrup chose
seems the easiest and most
logical:
the
name of the constructor is
the same as the name of
the class. It
makes
sense that such a function
will be called automatically on
initialization.
Here's
a simple class with a
constructor:
6:
Initialization & Cleanup
303
class
X {
int
i;
public:
X();
// Constructor
};
Now,
when an object is defined,
void
f() {
X
a;
//
...
}
the
same thing happens as if a
were
an int:
storage is allocated for
the
object. But when the program
reaches the sequence
point (point
of
execution) where a
is
defined, the constructor is
called
automatically.
That is, the compiler
quietly inserts the call to
X::X(
)
for
the object a
at
the point of definition.
Like any member
function,
the
first (secret) argument to
the constructor is the
this
pointer
the
address
of the object for which it is being
called. In the case of
the
constructor,
however, this
is
pointing to an un-initialized block
of
memory,
and it's the job of the
constructor to initialize this
memory
properly.
Like
any function, the constructor
can have arguments to allow
you
to
specify how an object is created,
give it initialization values,
and
so
on. Constructor arguments
provide you with a way to
guarantee
that
all parts of your object are
initialized to appropriate values.
For
example,
if a class Tree
has
a constructor that takes a
single integer
argument
denoting the height of the
tree, then you must create a
tree
object like this:
Tree
t(12);
//
12-foot tree
If
Tree(int)is
your only constructor, the compiler won't
let you
create
an object any other way. (We'll
look at multiple constructors
and
different ways to call constructors in
the next chapter.)
That's
really all there is to a constructor;
it's a specially
named
function
that is called automatically by
the compiler for
every
304
Thinking
in C++
object
at the point of that
object's creation. Despite
it's simplicity, it
is
exceptionally valuable because it
eliminates a large class
of
problems
and makes the code easier to
write and read. In the
preceding
code fragment, for example, you don't
see an explicit
function
call to some initialize(
)function
that is conceptually
separate
from definition. In C++, definition and
initialization are
unified
concepts you can't have
one without the
other.
Both
the constructor and destructor
are very unusual types of
functions:
they have no return value. This is
distinctly different
from
a void
return
value, in which the function
returns nothing but
you
still have the option to
make it something else.
Constructors
and
destructors return nothing and you don't
have an option. The
acts
of bringing an object into and out of the
program are special,
like
birth and death, and the compiler
always makes the
function
calls
itself, to make sure they
happen. If there were a return
value,
and
if you could select your own, the
compiler would somehow
have
to know what to do with the return value, or
the client
programmer
would have to explicitly call
constructors and
destructors,
which would eliminate their
safety.
Guaranteed
cleanup with the
destructor
As
a C programmer, you often think about
the importance of
initialization,
but it's rarer to think about
cleanup. After all, what
do
you need to do to clean up an int?
Just forget about it.
However,
with
libraries, just "letting go" of an
object once you're done with
it
is
not so safe. What if it modifies some
piece of hardware, or
puts
something
on the screen, or allocates
storage on the heap? If
you
just
forget about it, your object
never achieves closure upon
its exit
from
this world. In C++, cleanup is as
important as initialization
and
is therefore guaranteed with the
destructor.
6:
Initialization & Cleanup
305
The
syntax for the destructor is
similar to that for the
constructor:
the
class name is used for the
name of the function. However,
the
destructor
is distinguished from the constructor by
a leading tilde
(~).
In addition, the destructor
never has any arguments
because
destruction
never needs any options.
Here's the declaration for
a
destructor:
class
Y {
public:
~Y();
};
The
destructor is called automatically by
the compiler when the
object
goes out of scope. You can
see where the constructor
gets
called
by the point of definition of
the object, but the only
evidence
for
a destructor call is the
closing brace of the scope
that surrounds
the
object. Yet the destructor is
still called, even when you
use goto
to
jump out of a scope. (goto
still
exists in C++ for backward
compatibility
with C and for the times when it comes in
handy.)
You
should note that a nonlocal
goto,
implemented by the Standard
C
library functions setjmp(
)and
longjmp(
) doesn't
cause
,
destructors
to be called. (This is the
specification, even if your
compiler
doesn't implement it that way.
Relying on a feature
that
isn't
in the specification means your
code is nonportable.)
Here's
an example demonstrating the
features of constructors and
destructors
you've seen so far:
//:
C06:Constructor1.cpp
//
Constructors & destructors
#include
<iostream>
using
namespace std;
class
Tree {
int
height;
public:
Tree(int
initialHeight);
//
Constructor
~Tree();
// Destructor
void
grow(int years);
void
printsize();
306
Thinking
in C++
};
Tree::Tree(int
initialHeight) {
height
= initialHeight;
}
Tree::~Tree()
{
cout
<< "inside Tree destructor"
<< endl;
printsize();
}
void
Tree::grow(int years) {
height
+= years;
}
void
Tree::printsize() {
cout
<< "Tree height is " <<
height << endl;
}
int
main() {
cout
<< "before opening brace" <<
endl;
{
Tree
t(12);
cout
<< "after Tree creation" <<
endl;
t.printsize();
t.grow(4);
cout
<< "before closing brace" <<
endl;
}
cout
<< "after closing brace" <<
endl;
}
///:~
Here's
the output of the above
program:
before
opening brace
after
Tree creation
Tree
height is 12
before
closing brace
inside
Tree destructor
Tree
height is 16
after
closing brace
You
can see that the
destructor is automatically called at
the closing
brace
of the scope that encloses
it.
6:
Initialization & Cleanup
307
Elimination
of the definition block
In
C, you must always define all the
variables at the beginning of
a
block,
after the opening brace.
This is not an uncommon
requirement
in programming languages, and the
reason given has
often
been that it's "good
programming style." On this
point, I have
my
suspicions. It has always
seemed inconvenient to me, as
a
programmer,
to pop back to the beginning of a
block every time I
need
a new variable. I also find code
more readable when
the
variable
definition is close to its
point of use.
Perhaps
these arguments are
stylistic. In C++, however,
there's a
significant
problem in being forced to
define all objects at
the
beginning
of a scope. If a constructor exists, it
must be called when
the
object is created. However, if the
constructor takes one or
more
initialization
arguments, how do you know you will have
that
initialization
information at the beginning of a
scope? In the general
programming
situation, you won't. Because C has no
concept of
private,
this separation of definition and
initialization is no
problem.
However, C++ guarantees that when an
object is created,
it
is simultaneously initialized. This
ensures that you will have
no
uninitialized
objects running around in your system. C
doesn't
care;
in fact, C encourages
this
practice by requiring you to
define
variables
at the beginning of a block
before you necessarily
have
In
general, C++ will not allow you to create an
object before you
have
the initialization information for
the constructor. Because
of
this,
the language wouldn't be feasible if you
had to define
variables
at the beginning of a scope. In
fact, the style of
the
language
seems to encourage the
definition of an object as close
to
its
point of use as possible. In
C++, any rule that applies to
an
"object"
automatically refers to an object of a
built-in type as well.
1
C99, The updated
version of Standard C, allows
variables to be defined at any
point
in
a scope, like C++.
308
Thinking
in C++
This
means that any class object
or variable of a built-in type
can
also
be defined at any point in a scope. It
also means that you
can
wait
until you have the information for a
variable before
defining
it,
so you can always define and
initialize at the same
time:
//:
C06:DefineInitialize.cpp
//
Defining variables
anywhere
#include
"../require.h"
#include
<iostream>
#include
<string>
using
namespace std;
class
G {
int
i;
public:
G(int
ii);
};
G::G(int
ii) { i = ii; }
int
main() {
cout
<< "initialization value? ";
int
retval = 0;
cin
>> retval;
require(retval
!= 0);
int
y = retval + 3;
G
g(y);
}
///:~
You
can see that some
code is executed, then retval
is
defined,
initialized,
and used to capture user input, and then
y
and
g
are
defined.
C, on the other hand, does
not allow a variable to be
defined
anywhere except at the
beginning of the
scope.
In
general, you should define
variables as close to their
point of use
as
possible, and always initialize them when
they are defined.
(This
is
a stylistic suggestion for built-in
types, where initialization
is
optional.)
This is a safety issue. By
reducing the duration of
the
variable's
availability within the scope, you
are reducing the
chance
it
will be misused in some other
part of the scope. In
addition,
readability
is improved because the
reader doesn't have to
jump
6:
Initialization & Cleanup
309
back
and forth to the beginning of the
scope to know the type of a
variable.
for
loops
In
C++, you will often see a
for
loop
counter defined right
inside
the
for
expression:
for(int
j =
0;
j < 100; j++) {
cout
<<
"j
= " << j << endl;
}
for(int
i =
0;
i < 100; i++)
cout
<<
"i
= " << i << endl;
The
statements above are
important special cases, which
cause
confusion
to new C++ programmers.
The
variables i
and
j
are
defined directly inside the
for
expression
(which
you cannot do in C). They are then
available for use in
the
for
loop.
It's a very convenient syntax because
the context removes
all
question about the purpose
of i
and
j,
so you don't need to use
such
ungainly names as i_loop_counterfor
clarity.
However,
some confusion may result if you
expect the lifetimes
of
the
variables i
and
j
to
extend beyond the scope of
the for loop
Chapter
3 points out that while
and
switch
statements
also allow
the
definition of objects in their
control expressions, although
this
usage
seems far less important
than with the for
loop.
2
An earlier iteration of
the C++ draft standard
said the variable lifetime
extended to
the
end of the scope that
enclosed the for
loop.
Some compilers still implement
that,
but
it is not correct so your
code will only be portable
if you limit the scope to
the for
loop.
310
Thinking
in C++
Watch
out for local variables that
hide variables from the
enclosing
scope.
In general, using the same
name for a nested variable and
a
I
find small scopes an indicator of
good design. If you have
several
pages
for a single function, perhaps you're
trying to do too much
with
that function. More granular
functions are not only
more
useful,
but it's also easier to find
bugs.
Storage
allocation
A
variable can now be defined at any
point in a scope, so it might
seem
that the storage for a
variable may not be defined until
its
point
of definition. It's actually more
likely that the compiler
will
follow
the practice in C of allocating all
the storage for a scope
at
the
opening brace of that scope.
It doesn't matter because, as
a
programmer,
you can't access the storage
(a.k.a. the object) until
it
beginning
of the block, the
constructor call doesn't
happen until the
sequence
point where the object is
defined because the
identifier
isn't
available until then. The
compiler even checks to make
sure
that
you don't put the object definition
(and thus the
constructor
call)
where the sequence point
only conditionally passes through
it,
such
as in a switch
statement
or somewhere a goto
can
jump past
it.
Uncommenting the statements in
the following code will
generate
a warning or an error:
//:
C06:Nojump.cpp
//
Can't jump past
constructors
class
X {
public:
X();
3
The Java language
considers this such a bad
idea that it flags such
code as an error.
4
OK, you probably
could by fooling around with
pointers, but you'd be very,
very
bad.
6:
Initialization & Cleanup
311
};
X::X()
{}
void
f(int i) {
if(i
< 10) {
//!
goto jump1; // Error: goto
bypasses init
}
X
x1; // Constructor called
here
jump1:
switch(i)
{
case
1 :
X
x2; // Constructor called
here
break;
//!
case 2 : // Error: case
bypasses init
X
x3; // Constructor called
here
break;
}
}
int
main() {
f(9);
f(11);
}///:~
In
the code above, both
the goto
and
the switch
can
potentially
jump
past the sequence point
where a constructor is called.
That
object
will then be in scope even if the
constructor hasn't
been
called,
so the compiler gives an
error message. This once
again
guarantees
that an object cannot be
created unless it is
also
initialized.
All
the storage allocation
discussed here happens, of
course, on the
stack.
The storage is allocated by
the compiler by moving the
stack
pointer
"down" (a relative term, which may
indicate an increase or
decrease
of the actual stack pointer
value, depending on your
machine).
Objects can also be
allocated on the heap using
new,
which
is something we'll explore further in
Chapter 13.
312
Thinking
in C++
Stash
with constructors and
destructors
The
examples from previous chapters
have obvious functions
that
map
to constructors and destructors:
initialize(
)and
cleanup(
)
.
Here's
the Stash
header
using constructors and
destructors:
//:
C06:Stash2.h
//
With constructors &
destructors
#ifndef
STASH2_H
#define
STASH2_H
class
Stash {
int
size;
//
Size of each space
int
quantity; // Number of storage
spaces
int
next;
//
Next empty space
//
Dynamically allocated array of
bytes:
unsigned
char* storage;
void
inflate(int increase);
public:
Stash(int
size);
~Stash();
int
add(void* element);
void*
fetch(int index);
int
count();
};
#endif
// STASH2_H ///:~
The
only member function definitions
that are changed
are
initialize(
)and
cleanup(
) which
have been replaced with
a
,
constructor
and destructor:
//:
C06:Stash2.cpp {O}
//
Constructors & destructors
#include
"Stash2.h"
#include
"../require.h"
#include
<iostream>
#include
<cassert>
using
namespace std;
const
int increment = 100;
Stash::Stash(int
sz) {
6:
Initialization & Cleanup
313
size
= sz;
quantity
= 0;
storage
= 0;
next
= 0;
}
int
Stash::add(void* element) {
if(next
>= quantity) // Enough space
left?
inflate(increment);
//
Copy element into
storage,
//
starting at next empty
space:
int
startBytes = next *
size;
unsigned
char* e = (unsigned
char*)element;
for(int
i = 0; i < size; i++)
storage[startBytes
+ i] = e[i];
next++;
return(next
- 1); // Index number
}
void*
Stash::fetch(int index) {
require(0
<= index, "Stash::fetch
(-)index");
if(index
>= next)
return
0; // To indicate the
end
//
Produce pointer to desired
element:
return
&(storage[index * size]);
}
int
Stash::count() {
return
next; // Number of elements in
CStash
}
void
Stash::inflate(int increase) {
require(increase
> 0,
"Stash::inflate
zero or negative
increase");
int
newQuantity = quantity +
increase;
int
newBytes = newQuantity *
size;
int
oldBytes = quantity *
size;
unsigned
char* b = new unsigned
char[newBytes];
for(int
i = 0; i < oldBytes; i++)
b[i]
= storage[i]; // Copy old to
new
delete
[](storage); // Old
storage
storage
= b; // Point to new
memory
quantity
= newQuantity;
}
314
Thinking
in C++
Stash::~Stash()
{
if(storage
!= 0) {
cout
<< "freeing storage" <<
endl;
delete
[]storage;
}
}
///:~
You
can see that the
require.hfunctions
are being used to watch
for
programmer
errors, instead of assert(
) The
output of a failed
.
assert(
)is
not as useful as that of the
require.hfunctions
(which
will
be shown later in the
book).
Because
inflate(
)is
private, the only way a require(
)could
fail is if
one
of the other member
functions accidentally passed an
incorrect
value
to inflate(
) If
you are certain this can't
happen, you could
.
consider
removing the require(
) but
you might keep in mind that
,
until
the class is stable, there's
always the possibility that
new code
might
be added to the class that
could cause errors. The
cost of the
require(
)is
low (and could be automatically
removed using the
preprocessor)
and the value of code
robustness is high.
Notice
in the following test program how
the definitions for Stash
objects
appear right before they are
needed, and how the
initialization
appears as part of the
definition, in the
constructor
argument
list:
//:
C06:Stash2Test.cpp
//{L}
Stash2
//
Constructors & destructors
#include
"Stash2.h"
#include
"../require.h"
#include
<fstream>
#include
<iostream>
#include
<string>
using
namespace std;
int
main() {
Stash
intStash(sizeof(int));
for(int
i = 0; i < 100; i++)
intStash.add(&i);
for(int
j = 0; j < intStash.count();
j++)
6:
Initialization & Cleanup
315
cout
<< "intStash.fetch(" << j << ") =
"
<<
*(int*)intStash.fetch(j)
<<
endl;
const
int bufsize = 80;
Stash
stringStash(sizeof(char) *
bufsize);
ifstream
in("Stash2Test.cpp");
assure(in,
" Stash2Test.cpp");
string
line;
while(getline(in,
line))
stringStash.add((char*)line.c_str());
int
k = 0;
char*
cp;
while((cp
= (char*)stringStash.fetch(k++))!=0)
cout
<< "stringStash.fetch(" << k << ") =
"
<<
cp << endl;
}
///:~
Also
notice how the cleanup(
)calls
have been eliminated, but
the
destructors
are still automatically
called when intStashand
stringStashgo
out of scope.
One
thing to be aware of in the Stash
examples:
I'm being very
careful
to use only built-in types;
that is, those without
destructors.
If
you were to try to copy class
objects into the Stash,
you'd run
into
all kinds of problems and it wouldn't work
right. The Standard
C++
Library can actually make
correct copies of objects into
its
containers,
but this is a rather messy and
complicated process. In
the
following Stack
example,
you'll see that pointers are
used to
sidestep
this issue, and in a later
chapter the Stash
will
be
converted
so that it uses
pointers.
Stack
with constructors &
destructors
Reimplementing
the linked list (inside
Stack)
with constructors and
destructors
shows how neatly constructors and
destructors work
with
new
and
delete.
Here's the modified header
file:
//:
C06:Stack3.h
316
Thinking
in C++
//
With constructors/destructors
#ifndef
STACK3_H
#define
STACK3_H
class
Stack {
struct
Link {
void*
data;
Link*
next;
Link(void*
dat, Link* nxt);
~Link();
}*
head;
public:
Stack();
~Stack();
void
push(void* dat);
void*
peek();
void*
pop();
};
#endif
// STACK3_H ///:~
Not
only does Stack
have
a constructor and destructor, but so
does
the
nested class Link:
//:
C06:Stack3.cpp {O}
//
Constructors/destructors
#include
"Stack3.h"
#include
"../require.h"
using
namespace std;
Stack::Link::Link(void*
dat, Link* nxt) {
data
= dat;
next
= nxt;
}
Stack::Link::~Link()
{ }
Stack::Stack()
{ head = 0; }
void
Stack::push(void* dat) {
head
= new Link(dat,head);
}
void*
Stack::peek() {
require(head
!= 0, "Stack empty");
6:
Initialization & Cleanup
317
return
head->data;
}
void*
Stack::pop() {
if(head
== 0) return 0;
void*
result = head->data;
Link*
oldHead = head;
head
= head->next;
delete
oldHead;
return
result;
}
Stack::~Stack()
{
require(head
== 0, "Stack not
empty");
}
///:~
The
Link::Link(
)constructor
simply initializes the data
and
next
pointers,
so in Stack::push(
)the
line
head
= new Link(dat,head);
not
only allocates a new link (using dynamic
object creation with
the
keyword new,
introduced in Chapter 4), but it
also neatly
initializes
the pointers for that
link.
You
may wonder why the destructor for
Link
doesn't
do anything
in particular, why doesn't it delete
the
data
pointer?
There are
two
problems. In Chapter 4, where
the Stack
was
introduced, it
was
pointed out that you cannot properly
delete
a
void
pointer
if it
points
to an object (an assertion
that will be proven in Chapter
13).
But
in addition, if the Link
destructor
deleted the data
pointer,
pop(
) would
end up returning a pointer to a
deleted object, which
would
definitely be a bug. This is
sometimes referred to as the
issue
of
ownership: the
Link
and
thus the Stack
only
holds the pointers,
but
is not responsible for cleaning them up.
This means that you
must
be very careful that you know who is
responsible.
For
example,
if you don't pop(
) and
delete
all
the pointers on the
Stack,
they won't get cleaned up automatically
by the Stack's
destructor.
This can be a sticky issue
and leads to memory leaks, so
knowing
who is responsible for cleaning up an
object can make
the
318
Thinking
in C++
difference
between a successful program and a buggy
one that's
why
Stack::~Stack(
)
prints
an error message if the
Stack
object
isn't
empty upon destruction.
Because
the allocation and cleanup of
the Link
objects
are hidden
within
Stack
it's part of the underlying
implementation you
don't
see it happening in the test
program, although you are
responsible
for deleting the pointers
that come back from
pop(
):
//:
C06:Stack3Test.cpp
//{L}
Stack3
//{T}
Stack3Test.cpp
//
Constructors/destructors
#include
"Stack3.h"
#include
"../require.h"
#include
<fstream>
#include
<iostream>
#include
<string>
using
namespace std;
int
main(int argc, char* argv[])
{
requireArgs(argc,
1); // File name is
argument
ifstream
in(argv[1]);
assure(in,
argv[1]);
Stack
textlines;
string
line;
//
Read file and store
lines in the stack:
while(getline(in,
line))
textlines.push(new
string(line));
//
Pop the lines from
the stack and print
them:
string*
s;
while((s
= (string*)textlines.pop()) != 0) {
cout
<< *s << endl;
delete
s;
}
}
///:~
In
this case, all the lines in
textlinesare
popped and deleted, but if
they
weren't, you'd get a require(
)message
that would mean there
was
a memory leak.
6:
Initialization & Cleanup
319
Aggregate
initialization
An
aggregate
is just
what it sounds like: a bunch of things
clumped
together.
This definition includes
aggregates of mixed types,
like
structs
and classes.
An array is an aggregate of a single
type.
Initializing
aggregates can be error-prone and
tedious. C++
aggregate
initialization makes it
much safer. When you create an
object
that's an aggregate, all you must do is
make an assignment,
and
the initialization will be taken
care of by the compiler.
This
assignment
comes in several flavors,
depending on the type
of
aggregate
you're dealing with, but in all cases
the elements in the
assignment
must be surrounded by curly braces. For
an array of
built-in
types this is quite
simple:
int
a[5] = { 1, 2, 3, 4, 5 };
If
you try to give more initializers than
there are array elements,
the
compiler
gives an error message. But what
happens if you give
fewer
initializers?
For example:
int
b[6] = {0};
Here,
the compiler will use the
first initializer for the
first array
element,
and then use zero for all the
elements without initializers.
Notice
this initialization behavior
doesn't occur if you define
an
array
without a list of initializers. So the
expression above is a
succinct
way to initialize an array to zero,
without using a for
loop,
and
without any possibility of an off-by-one
error (Depending on
the
compiler, it may also be more
efficient than the for
loop.)
A
second shorthand for arrays is
automatic
counting, in which
you
let
the compiler determine the
size of the array based on
the
number
of initializers:
int
c[] = { 1, 2, 3, 4 };
Now
if you decide to add another
element to the array, you
simply
add
another initializer. If you can
set your code up so it needs to
be
320
Thinking
in C++
changed
in only one spot, you reduce
the chance of errors
during
modification.
But how do you determine the size of
the array? The
expression
sizeof
c / sizeof *c
(size
of the entire array divided
by
the
size of the first element)
does the trick in a way that
doesn't
for(int
i = 0; i < sizeof c / sizeof *c;
i++)
c[i]++;
Because
structures are also
aggregates, they can be initialized in
a
similar
fashion. Because a C-style
struct
has
all of its members
public,
they can be assigned
directly:
struct
X {
int
i;
float
f;
char
c;
};
X
x1 = { 1, 2.2, 'c' };
If
you have an array of such
objects, you can initialize them
by
using
a nested set of curly braces for
each object:
X
x2[3] = { {1, 1.1, 'a'},
{2, 2.2, 'b'} };
Here,
the third object is initialized to
zero.
If
any of the data members are
private
(which
is typically the case
for
a well-designed class in C++), or
even if everything's public
but
there's
a constructor, things are
different. In the examples
above,
the
initializers are assigned
directly to the elements of
the
aggregate,
but constructors are a way of forcing
initialization to
occur
through a formal interface. Here,
the constructors must be
called
to perform the initialization. So if you
have a struct
that
looks
like this,
5
In Volume 2 of this book
(freely available at ),
you'll see a
more
succinct calculation of an array
size using templates.
6:
Initialization & Cleanup
321
struct
Y {
float
f;
int
i;
Y(int
a);
};
You
must indicate constructor calls.
The best approach is
the
explicit
one as follows:
Y
y1[] = { Y(1), Y(2), Y(3)
};
You
get three objects and three
constructor calls. Any time
you
have
a constructor, whether it's a
struct
with
all members public
or
a
class
with
private
data
members, all the initialization must
go
through
the constructor, even if
you're using
aggregate
initialization.
Here's
a second example showing
multiple constructor
arguments:
//:
C06:Multiarg.cpp
//
Multiple constructor
arguments
//
with aggregate
initialization
#include
<iostream>
using
namespace std;
class
Z {
int
i, j;
public:
Z(int
ii, int jj);
void
print();
};
Z::Z(int
ii, int jj) {
i
= ii;
j
= jj;
}
void
Z::print() {
cout
<< "i = " << i << ", j = " << j <<
endl;
}
int
main() {
Z
zz[] = { Z(1,2), Z(3,4),
Z(5,6), Z(7,8) };
322
Thinking
in C++
for(int
i = 0; i < sizeof zz / sizeof *zz;
i++)
zz[i].print();
}
///:~
Notice
that it looks like an
explicit constructor is called for
each
object
in the array.
Default
constructors
A
default
constructor is one
that can be called with no
arguments. A
default
constructor is used to create a
"vanilla object," but it's
also
important
when the compiler is told to create an
object but isn't
given
any details. For example, if you
take the struct
Ydefined
previously
and use it in a definition like
this,
Y
y2[2] = { Y(1) };
the
compiler will complain that it
cannot find a default
constructor.
The
second object in the array
wants to be created with no
arguments,
and that's where the
compiler looks for a
default
constructor.
In fact, if you simply define an array of
Y
objects,
Y
y3[7];
the
compiler will complain because it must
have a default
constructor
to initialize every object in
the array.
The
same problem occurs if you
create an individual object
like
this:
Y
y4;
Remember,
if you have a constructor, the
compiler ensures that
construction
always
happens,
regardless of the situation.
The
default constructor is so important
that if
(and only
if) there are
no
constructors for a structure (struct
or
class),
the compiler will
automatically
create one for you. So this
works:
//:
C06:AutoDefaultConstructor.cpp
6:
Initialization & Cleanup
323
//
Automatically-generated default
constructor
class
V {
int
i; // private
};
// No constructor
int
main() {
V
v, v2[10];
}
///:~
If
any constructors are defined,
however, and there's no
default
constructor,
the instances of V
above
will generate compile-time
errors.
You
might think that the compiler-synthesized
constructor should
do
some intelligent initialization,
like setting all the memory for
the
object
to zero. But it doesn't that would
add extra overhead
but
be
out of the programmer's control. If you
want the memory to be
initialized
to zero, you must do it yourself by writing
the default
constructor
explicitly.
Although
the compiler will create a
default constructor for you,
the
behavior
of the compiler-synthesized constructor
is rarely what
you
want. You should treat this
feature as a safety net, but
use it
sparingly.
In general, you should define your
constructors
explicitly
and not allow the compiler to do it for
you.
Summary
The
seemingly elaborate mechanisms
provided by C++ should
give
you
a strong hint about the
critical importance placed
on
initialization
and cleanup in the language. As
Stroustrup was
designing
C++, one of the first
observations he made
about
productivity
in C was that a significant
portion of programming
problems
are caused by improper
initialization of variables.
These
kinds
of bugs are hard to find, and
similar issues apply to
improper
cleanup.
Because constructors and destructors
allow you to
guarantee
proper
initialization and cleanup (the
compiler will not
324
Thinking
in C++
allow
an object to be created and destroyed
without the proper
constructor
and destructor calls), you get
complete control and
safety.
Aggregate
initialization is included in a similar
vein it prevents
you
from making typical initialization
mistakes with aggregates of
built-in
types and makes your code
more succinct.
Safety
during coding is a big issue in
C++. Initialization and
cleanup
are an important part of
this, but you'll also see
other
safety
issues as the book
progresses.
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.
Write
a simple class called
Simple
with
a constructor that
prints
something to tell you that
it's been called. In
main(
) make
an object of your class.
2.
Add
a destructor to Exercise 1 that
prints out a message
to
tell you that it's been
called.
3.
Modify
Exercise 2 so that the class
contains an int
member.
Modify the constructor so that it
takes an int
argument
that it stores in the class
member. Both the
constructor
and destructor should print out the
int
value
as
part of their message, so you
can see the objects
as
they
are created and
destroyed.
4.
Demonstrate
that destructors are still
called even when
goto
is
used to jump out of a loop.
5.
Write
two for
loops
that print out values from zero to
10.
In
the first, define the
loop counter before the
for
loop,
and
in the second, define the
loop counter in the
control
expression
of the for
loop.
For the second part of
this
exercise,
modify the identifier in the
second for
loop
so
that
it as the same name as the
loop counter for the
first
and
see what your compiler
does.
6:
Initialization & Cleanup
325
6.
Modify
the Handle.h
Handle.cpp and
UseHandle.cpp
,
,
files
at the end of Chapter 5 to
use constructors and
destructors.
7.
Use
aggregate initialization to create an
array of double
in
which you specify the size of
the array but do not
provide
enough elements. Print out
this array using
sizeof
to
determine the size of the
array. Now create an
array
of double
using
aggregate initialization and
automatic
counting. Print out the
array.
8.
Use
aggregate initialization to create an
array of string
objects.
Create a Stack
to
hold these strings
and step
through
your array, pushing each
string
on
your Stack.
Finally,
pop
the
strings
off your Stack
and
print each
one.
9.
Demonstrate
automatic counting and
aggregate
initialization
with an array of objects of the
class you
created
in Exercise 3. Add a member function to
that
class
that prints a message.
Calculate the size of the
array
and
move through it, calling your new member
function.
10.
Create
a class without any constructors, and show
that
you
can create objects with the
default constructor. Now
create
a nondefault constructor (one with an
argument)
for
the class, and try compiling
again. Explain what
happened.
326
Thinking
in C++
Table of Contents:
|
|||||