ZeePedia

Initialization & Cleanup:Guaranteed cleanup with the destructor, Aggregate initialization

<< Hiding the Implementation:C++ access control, Handle classes
Function Overloading & Default Arguments:Overloading example, Default arguments >>
img
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
img
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++
img
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
img
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++
img
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
img
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++
img
};
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
img
.
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
the initialization information1.
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++
img
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
img
.
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 ­
they do not2.
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++
img
..
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 prone3.
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
has been defined4. 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();
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
img
};
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++
img
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
img
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++
img
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
img
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++
img
// 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
img
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++
img
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
img
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++
img
.
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 changes5:
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
img
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++
img
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
img
// 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++
img
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
img
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++