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).
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.
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.
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:
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 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.
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.
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(); }; 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.
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 the
initialization
information[38].
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. 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 back and forth to the
beginning of the scope to know the type of a
variable.
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_counter for 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 – they do
not[39].
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.
Watch out for local variables that
hide
variables from the enclosing scope. In general, using the same name for a nested
variable and a variable that is global to that scope is confusing and error
prone[40].
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.
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 has
been defined[41].
Although the storage is
allocated at the 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(); }; 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.
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) { 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; } Stash::~Stash() { if(storage != 0) { cout << "freeing storage" << endl; delete []storage; } } ///:~
You can see that the require.h
functions 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.h functions (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++) 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 intStash and stringStash go
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.
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 // 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
struct 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"); 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 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
textlines are popped and deleted, but if they weren’t, you’d
get a require( ) message that would mean there was a memory
leak.
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 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 need to be changed if the array size
changes[42]:
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,
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) }; for(int i = 0; i < sizeof zz / sizeof *zz; i++) zz[i].print(); } ///:~
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 Y defined 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 // 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.
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
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.
Solutions to selected exercises
can be found in the electronic document The Thinking in C++ Annotated
Solution Guide, available for a small fee from
www.BruceEckel.com.
[38]
C99, The updated version of Standard C, allows variables to be defined at any
point in a scope, like C++.
[39]
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.
[40]
The Java language considers this such a bad idea that it flags such code as an
error.
[41]
OK, you probably could by fooling around with pointers, but you’d be very,
very bad.
[42]
In Volume 2 of this book (freely available at www.BruceEckel.com), you’ll
see a more succinct calculation of an array size using
templates.