Am avut 371678 vizite de la lansarea siteului.




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.

  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. 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.



[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.

Home   |   Web Faq   |   Radio Online   |   About   |   Products   |   Webmaster Login

The quality software developer.™
© 2003-2004 ruben|labs corp. All Rights Reserved.
Timp de generare a paginii: 17583 secunde
Versiune site: 1.8 SP3 (build 2305-rtm.88542-10.2004)