A Review Example and Static Class Members

We haven’t used new and delete for a while, so let’s review them with a short program. While we’re at it, let’s look at a new storage class: the static class member. The vehicle will be a StringBad class, later to be superseded by the slightly more able String class. (You’ve already seen the standard C++ string class, and you’ll learn more about it in Chapter 16, “The string Class and the Standard Template Library.” Meanwhile, the humble StringBad and String classes in this chapter provide some insight into what underlies such a class. A lot of programming techniques go into providing such a friendly interface.)

StringBad and String class objects will hold a pointer to a string and a value representing the string length. We’ll use the StringBad and String classes primarily to give an inside look at how new, delete, and static class members operate. For that reason, the constructors and destructors will display messages when called so that you can follow the action. Also we’ll omit several useful member and friend functions, such as overloaded ++ and >> operators and a conversion function, in order to simplify the class interface. (But rejoice! The review questions for this chapter give you the opportunity to add those useful support functions.) Listing 12.1 shows the class declaration.

Listing 12.1. strngbad.h


// strngbad.h -- flawed string class definition
#include <iostream>
#ifndef STRNGBAD_H_
#define STRNGBAD_H_
class StringBad
{
private:
    char * str;                // pointer to string
    int len;                   // length of string
    static int num_strings;    // number of objects
public:
    StringBad(const char * s); // constructor
    StringBad();               // default constructor
    ~StringBad();              // destructor
// friend function
    friend std::ostream & operator<<(std::ostream & os,
                       const StringBad & st);
};
#endif


Why call the class StringBad? This is to remind you that StringBad is an example under development. It’s the first stage of developing a class by using dynamic memory allocation, and it does the obvious things correctly; for example, it uses new and delete correctly in the constructors and destructor. It doesn’t really do bad things, but the design omits doing some additional good things that are necessary but not at all obvious. Seeing the problems the class has should help you understand and remember the non-obvious changes you will make later, when you convert it to the more functional String class.

You should note two points about this declaration. First, it uses a pointer-to-char instead of a char array to represent a name. This means that the class declaration does not allocate storage space for the string itself. Instead, it uses new in the constructors to allocate space for the string. This arrangement avoids straitjacketing the class declaration with a predefined limit to the string size.

Second, the definition declares the num_strings member as belonging to the static storage class. A static class member has a special property: A program creates only one copy of a static class variable, regardless of the number of objects created. That is, a static member is shared among all objects of that class, much as a phone number might be shared among all members of a family. If, say, you create 10 StringBad objects, there would be 10 str members and 10 len members, but just 1 shared num_strings member (see Figure 12.1). This is convenient for data that should be private to a class but that should have the same value for all class objects. The num_strings member, for example, is intended to keep track of the number of objects created.

Figure 12.1. A static data member.

Image

By the way, Listing 12.1 uses the num_strings member as a convenient means of illustrating static data members and as a device to point out potential programming problems. In general, a string class doesn’t need such a member.

Take a look at the implementation of the class methods in Listing 12.2. Notice how it handles using a pointer and a static member.

Listing 12.2. strngbad.cpp


// strngbad.cpp -- StringBad class methods
#include <cstring>                    // string.h for some
#include "strngbad.h"
using std::cout;

// initializing static class member
int StringBad::num_strings = 0;

// class methods

// construct StringBad from C string
StringBad::StringBad(const char * s)
{
    len = std::strlen(s);             // set size
    str = new char[len + 1];          // allot storage
    std::strcpy(str, s);              // initialize pointer
    num_strings++;                    // set object count
    cout << num_strings << ": "" << str
         << "" object created ";    // For Your Information
}

StringBad::StringBad()                // default constructor
{
    len = 4;
    str = new char[4];
    std::strcpy(str, "C++");          // default string
    num_strings++;
    cout << num_strings << ": "" << str
         << "" default object created ";  // FYI
}

StringBad::~StringBad()               // necessary destructor
{
    cout << """ << str << "" object deleted, ";    // FYI
    --num_strings;                    // required
    cout << num_strings << " left "; // FYI
    delete [] str;                    // required
}

std::ostream & operator<<(std::ostream & os, const StringBad & st)
{
    os << st.str;
    return os;
}


First, notice the following statement from Listing 12.2:

int StringBad::num_strings = 0;

This statement initializes the static num_strings member to 0. Note that you cannot initialize a static member variable inside the class declaration. That’s because the declaration is a description of how memory is to be allocated but it doesn’t allocate memory. You allocate and initialize memory by creating an object using that format. In the case of a static class member, you initialize the static member independently, with a separate statement outside the class declaration. That’s because the static class member is stored separately rather than as part of an object. Note that the initialization statement gives the type and uses the scope operator, but it doesn’t use the static keyword.

This initialization goes in the methods file, not in the class declaration file. That’s because the class declaration is in a header file, and a program may include a header file in several other files. That would result in multiple copies of the initialization statement, which is an error.

The exception to the noninitialization of a static data member inside the class declaration (see Chapter 10, “Objects and Classes”) is if the static data member is a const of integral or enumeration type.


Note

A static data member is declared in the class declaration and is initialized in the file containing the class methods. The scope operator is used in the initialization to indicate to which class the static member belongs. However, if the static member is a const integral type or an enumeration type, it can be initialized in the class declaration itself.


Next, notice that each constructor contains the expression num_strings++. This ensures that each time a program creates a new object, the shared variable num_strings increases by one, keeping track of the total number of String objects. Also the destructor contains the expression --num_strings. Thus, the String class also keeps track of deleted objects, keeping the value of the num_strings member current.

Now look at the first constructor in Listing 12.2, which initializes a String object with a regular C string:

StringBad::StringBad(const char * s)
{
    len = std::strlen(s);          // set size
    str = new char[len + 1];       // allot storage
    std::strcpy(str, s);           // initialize pointer
    num_strings++;                 // set object count
    cout << num_strings << ": "" << str
         << "" object created "; // For Your Information
}

Recall that the class str member is just a pointer, so the constructor has to provide the memory for holding a string. You can pass a string pointer to the constructor when you initialize an object:

String boston("Boston");

The constructor must then allocate enough memory to hold the string, and then it must copy the string to that location. Let’s go through the process step-by-step.

First, the function initializes the len member, using the strlen() function to compute the length of the string. Next, it uses new to allocate sufficient space to hold the string, and then it assigns the address of the new memory to the str member. (Recall that strlen() returns the length of a string, not counting the terminating null character, so the constructor adds one to len to allow space for the string, including the null character.)

Next, the constructor uses strcpy() to copy the passed string into the new memory. Then it updates the object count. Finally, to help you monitor what’s going on, the constructor displays the current number of objects and the string stored in the object. This feature will come in handy later when we deliberately lead the Stringbad class into trouble.

To understand this approach, you should realize that the string is not stored in the object. The string is stored separately, in heap memory, and the object merely stores information that tells where to find the string.

Note that you do not use this:

str = s;  // not the way to go

This merely stores the address without making a copy of the string.

The default constructor behaves similarly, except that it provides a default string of "C++".

The destructor contains the example’s most important addition to the handling of classes:

StringBad::~StringBad()               // necessary destructor
{
    cout << """ << str << "" object deleted, ";    // FYI
    --num_strings;                    // required
    cout << num_strings << " left "; // FYI
    delete [] str;                    // required
}

The destructor begins by announcing when the destructor gets called. This part is informative but not essential. However, the delete statement is vital. Recall that the str member points to memory allocated with new. When a StringBad object expires, the str pointer expires. But the memory str pointed to remains allocated unless you use delete to free it. Deleting an object frees the memory occupied by the object itself, but it does not automatically free memory pointed to by pointers that were object members. For that, you must use the destructor. By placing the delete statement in the destructor, you ensure that the memory that a constructor allocates with new is freed when the object expires.


Warning

Whenever you use new in a constructor to allocate memory, you should use delete in the corresponding destructor to free that memory. If you use new [] (with brackets), then you should use delete [] (with brackets).


Listing 12.3, which is taken from a program under development at The Daily Vegetable, illustrates when and how the StringBad constructors and destructors work. The program places the object declarations within an inner block because the destructor is called when execution exits the block in which an object is defined. Without the inner block, the destructors would be called after execution exits main(), which would prevent you in some environments from seeing the destructor messages before the execution window closes. Be sure to compile Listing 12.2 along with Listing 12.3.

Listing 12.3. vegnews.cpp


// vegnews.cpp -- using new and delete with classes
// compile with strngbad.cpp
#include <iostream>
using std::cout;
#include "strngbad.h"

void callme1(StringBad &);  // pass by reference
void callme2(StringBad);    // pass by value

int main()
{
    using std::endl;
    {
        cout << "Starting an inner block. ";
        StringBad headline1("Celery Stalks at Midnight");
        StringBad headline2("Lettuce Prey");
        StringBad sports("Spinach Leaves Bowl for Dollars");
        cout << "headline1: " << headline1 << endl;
        cout << "headline2: " << headline2 << endl;
        cout << "sports: " << sports << endl;
        callme1(headline1);
        cout << "headline1: " << headline1 << endl;
        callme2(headline2);
        cout << "headline2: " << headline2 << endl;
        cout << "Initialize one object to another: ";
        StringBad sailor = sports;
        cout << "sailor: " << sailor << endl;
        cout << "Assign one object to another: ";
        StringBad knot;
        knot = headline1;
        cout << "knot: " << knot << endl;
        cout << "Exiting the block. ";
    }
    cout << "End of main() ";

    return 0;
}

void callme1(StringBad & rsb)
{
    cout << "String passed by reference: ";
    cout << "    "" << rsb << "" ";
}

void callme2(StringBad sb)
{
    cout << "String passed by value: ";
    cout << "    "" << sb << "" ";
}



Note

This first draft of a design for StringBad has some deliberate flaws that make the exact output undefined. Some compilers I used, for example, produced versions that aborted before completing. However, although the output details may differ, the basic problems and solutions (soon to be revealed!) are the same.


Here is the output produced after compiling the program in Listing 12.3 with the Borland C++ 5.5 command-line compiler:

Starting an inner block.
1: "Celery Stalks at Midnight" object created
2: "Lettuce Prey" object created
3: "Spinach Leaves Bowl for Dollars" object created
headline1: Celery Stalks at Midnight
headline2: Lettuce Prey
sports: Spinach Leaves Bowl for Dollars
String passed by reference:
    "Celery Stalks at Midnight"
headline1: Celery Stalks at Midnight
String passed by value:
    "Lettuce Prey"
"Lettuce Prey" object deleted, 2 left
headline2: Dû°
Initialize one object to another:
sailor: Spinach Leaves Bowl for Dollars
Assign one object to another:
3: "C++" default object created
knot: Celery Stalks at Midnight
Exiting the block.
"Celery Stalks at Midnight" object deleted, 2 left
"Spinach Leaves Bowl for Dollars" object deleted, 1 left
"Spinach Leaves Bowl for Doll8" object deleted, 0 left
"@g" object deleted, -1 left
"-|" object deleted, -2 left
End of main()

The various nonstandard characters that appear in the output will vary from system to system; they are one of the signs that StringBad deserves to be called bad. Another sign is the negative object count. Newer compiler/operating system combinations typically abort the program just before displaying the line about having -1 objects left, and some of them report a General Protection Fault (GPF). A GPF indicates that a program tried to access a memory location forbidden to it; this is another bad sign.

Program Notes

The program in Listing 12.3 starts out fine, but it staggers to a strange and ultimately disastrous conclusion. Let’s begin by looking at the good parts. The constructor announces that it has created three StringBad objects, it numbers them, and the program uses the overloaded << operator to list them:

1: "Celery Stalks at Midnight" object created
2: "Lettuce Prey" object created
3: "Spinach Leaves Bowl for Dollars" object created
headline1: Celery Stalks at Midnight
headline2: Lettuce Prey
sports: Spinach Leaves Bowl for Dollars

Then the program passes headline1 to the callme1() function and redisplays headline1 after the call. Here’s the code:

callme1(headline1);
cout << "headline1: " << headline1 << endl;

And here’s the result:

String passed by reference:
    "Celery Stalks at Midnight"
headline1: Celery Stalks at Midnight

This section of code seems to work fine, too.

But then the program executes the following code:

callme2(headline2);
cout << "headline2: " << headline2 << endl;

Here, callme2() passes headline2 by value instead of by reference, and the result indicates a serious problem:

String passed by value:
    "Lettuce Prey"
"Lettuce Prey" object deleted, 2 left
headline2: Dû°

First, passing headline2 as a function argument somehow causes the destructor to be called. Second, although passing by value is supposed to protect the original argument from change, the function messes up the original string beyond recognition, and some nonstandard characters get displayed. (The exact text displayed will depend on what happens to sitting in memory.)

Even worse, look at the end of the output when the destructor gets called automatically for each of the objects created earlier:

Exiting the block.
"Celery Stalks at Midnight" object deleted, 2 left
"Spinach Leaves Bowl for Dollars" object deleted, 1 left
"Spinach Leaves Bowl for Doll8" object deleted, 0 left
"@g" object deleted, -1 left
"-|" object deleted, -2 left
End of main()

Because automatic storage objects are deleted in an order opposite to that in which they are created, the first three objects deleted are knots, sailor, and sport. The knots and sailor deletions look okay, but for sport, Dollars has become Doll8. The only thing the program does with sport is use it to initialize sailor, but that act appears to have altered sport. And the last two objects deleted, headline2 and headline1, are unrecognizable. Something messes up these strings before they are deleted. Also the counting is bizarre. How can there be -2 objects left?

Actually, the peculiar counting is a clue. Every object is constructed once and destroyed once, so the number of constructor calls should equal the number of destructor calls. Because the object count (num_strings) is decremented two times more than it is incremented, a constructor that doesn’t increment num_strings must be creating two objects. The class definition declares and defines two constructors (both of which increment num_strings), but it turns out that the program uses three constructors! For example, consider this line:

StringBad sailor = sports;

What constructor is used here? Not the default constructor, and not the constructor with a const char * parameter. Remember, initialization using this form is another syntax for the following:

StringBad sailor = StringBad(sports); //constructor using sports

Because sports is type StringBad, a matching constructor could have this prototype:

StringBad(const StringBad &);

And it turns out that the compiler automatically generates this constructor (called a copy constructor because it makes a copy of an object) if you initialize one object to another. The automatic version would not know about updating the num_strings static variable, so it would mess up the counting scheme. Indeed, all the problems exhibited by this example stem from member functions that the compiler generates automatically, so let’s look at that topic now.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.140.204