Inapoi
Inainte
Cuprins
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
// 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");
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.
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 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();
} ///:~
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 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.
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
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
www.BruceEckel.com.
- 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.
- Add a
destructor to Exercise 1 that prints out a message to tell you that it’s
been called.
- 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.
- Demonstrate
that destructors are still called even when goto is used to jump out of a
loop.
- 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.
- Modify the
Handle.h, Handle.cpp, and UseHandle.cpp files at the end of
Chapter 5 to use constructors and
destructors.
- 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.
- 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.
- 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.
- 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.
[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.
 |
|