Constructors and Destructors in Review

Now that we’ve gone through a few examples of constructors and destructors, you might want to pause and assimilate what has passed. To help you, here is a summary of these methods.

A constructor is a special class member function that’s called whenever an object of that class is created. A class constructor has the same name as its class, but through the miracle of function overloading, you can have more than one constructor with the same name, provided that each has its own signature, or argument list. Also a constructor has no declared type. Usually a constructor is used to initialize members of a class object. Your initialization should match the constructor’s argument list. For example, suppose the Bozo class has the following prototype for a class constructor:

Bozo(const char * fname, const char * lname);   // constructor prototype

In this case, you can use it to initialize new objects as follows:

Bozo bozetta = bozo("Bozetta", "Biggens");   // primary form
Bozo fufu("Fufu", "O'Dweeb");                // short form
Bozo *pc = new Bozo("Popo", "Le Peu");       // dynamic object

If C++11 rules are in effect, you can use list initialization instead:

Bozo bozetta = {"Bozetta", "Biggens"};       // C++11
Bozo fufu{"Fufu", "O'Dweeb"}                 // C++11;
Bozo *pc = new Bozo{"Popo", "Le Peu"};       // C++11

If a constructor has just one argument, that constructor is invoked if you initialize an object to a value that has the same type as the constructor argument. For example, suppose you have this constructor prototype:

Bozo(int age);

Then you can use any of the following forms to initialize an object:

Bozo dribble = bozo(44);   // primary form
Bozo roon(66);             // secondary form
Bozo tubby = 32;           // special form for one-argument constructors

Actually, the third example is a new point, not a review point, but it seemed like a nice time to tell you about it. Chapter 11 mentions a way to turn off this feature because it can lead to unpleasant surprises.


Caution

A constructor that you can use with a single argument allows you to use assignment syntax to initialize an object to a value:

Classname object = value;

This feature can cause problems, but it can be blocked, as described in Chapter 11.


A default constructor has no arguments, and it is used if you create an object without explicitly initializing it. If you fail to provide any constructors, the compiler defines a default constructor for you. Otherwise, you have to supply your own default constructor. It can have no arguments or else it must have default values for all arguments:

Bozo();                                   // default constructor prototype
Bistro(const char * s = "Chez Zero");     // default for Bistro class

The program uses the default constructor for uninitialized objects:

Bozo bubi;             // use default
Bozo *pb = new Bozo;   // use default

Just as a program invokes a constructor when an object is created, it invokes a destructor when an object is destroyed. You can have only one destructor per class. It has no return type (not even void), it has no arguments, and its name is the class name preceded by a tilde. For example, the Bozo class destructor has the following prototype:

~Bozo();    // class destructor

Class destructors that use delete become necessary when class constructors use new.

Knowing Your Objects: The this Pointer

You can do still more with the Stock class. So far each class member function has dealt with but a single object: the object that invokes it. Sometimes, however, a method might need to deal with two objects, and doing so may involve a curious C++ pointer called this. Let’s look at how the need for this can unfold.

Although the Stock class declaration displays data, it’s deficient in analytic power. For example, by looking at the show() output, you can tell which of your holdings has the greatest value, but the program can’t tell because it can’t access total_val directly. The most direct way of letting a program know about stored data is to provide methods to return values. Typically, you use inline code for this, as in the following example:

class Stock
{
private:
    ...
    double total_val;
    ...
public:
    double total() const { return total_val; }
    ...
};

This definition, in effect, makes total_val read-only memory as far as a direct program access is concerned. That is, you can use the total_val() method to obtain the value, but the class doesn’t provide a method for specifically resetting the value of total_val. (Other methods, such as buy(), sell(), and update(), do modify total_val as a by-product of resetting the shares and share_val members.)

By adding this function to the class declaration, you can let a program investigate a series of stocks to find the one with the greatest value. However, you can take a different approach, one that helps you learn about the this pointer. The approach is to define a member function that looks at two Stock objects and returns a reference to the larger of the two. Attempting to implement this approach raises some interesting questions, which we’ll look into now.

First, how do you provide the member function with two objects to compare? Suppose, for example, that you decide to name the method topval(). Then the function call stock1.topval() accesses the data of the stock1 object, whereas the message stock2.topval() accesses the data of the stock2 object. If you want the method to compare two objects, you have to pass the second object as an argument. For efficiency, you can pass the argument by reference. That is, you can have the topval() method use a type const Stock & argument.

Second, how do you communicate the method’s answer back to the calling program? The most direct way is to have the method return a reference to the object that has the larger total value. Thus, the comparison method should have the following prototype:

const Stock & topval(const Stock & s) const;

This function accesses one object implicitly and one object explicitly, and it returns a reference to one of those two objects. The const in parentheses states that the function won’t modify the explicitly accessed object, and the const that follows the parentheses states that the function won’t modify the implicitly accessed object. Because the function returns a reference to one of the two const objects, the return type also has to be a const reference.

Suppose, then, that you want to compare the Stock objects stock1 and stock2 and assign the one with the greater total value to the object top. You can use either of the following statements to do so:

top = stock1.topval(stock2);
top = stock2.topval(stock1);

The first form accesses stock1 implicitly and stock2 explicitly, whereas the second accesses stock1 explicitly and stock2 implicitly (see Figure 10.3). Either way, the method compares the two objects and returns a reference to the one with the higher total value.

Figure 10.3. Accessing two objects by using a member function.

Image

Actually, this notation is a bit confusing. It would be clearer if you could somehow use the relational operator > to compare the two objects. You can do so with operator overloading, which Chapter 11 discusses.

Meanwhile, there’s still the implementation of topval() to attend to. It raises a slight problem. Here’s a partial implementation that highlights the problem:

const Stock & Stock::topval(const Stock & s) const
{
    if (s.total_val > total_val)
        return s;               // argument object
     else
        return ?????;           // invoking object
}

Here s.total_val is the total value for the object passed as an argument, and total_val is the total value for the object to which the message is sent. If s.total_val is greater than total_val, the function returns a reference to s. Otherwise, it returns a reference to the object used to evoke the method. (In OOP talk, that is the object to which the topval message is sent.) Here’s the problem: What do you call that object? If you make the call stock1.topval(stock2), then s is a reference for stock2 (that is, an alias for stock2), but there is no alias for stock1.

The C++ solution to this problem is to use a special pointer called this. The this pointer points to the object used to invoke a member function. (Basically, this is passed as a hidden argument to the method.) Thus, the function call stock1.topval(stock2) sets this to the address of the stock1 object and makes that pointer available to the topval() method. Similarly, the function call stock2.topval(stock1) sets this to the address of the stock2 object. In general, all class methods have a this pointer set to the address of the object that invokes the method. Indeed, total_val in topval() is just shorthand notation for this->total_val. (Recall from Chapter 4, “Compound Types,” that you use the -> operator to access structure members via a pointer. The same is true for class members.) (See Figure 10.4.)

Figure 10.4. this points to the invoking object.

Image


Note

Each member function, including constructors and destructors, has a this pointer. The special property of the this pointer is that it points to the invoking object. If a method needs to refer to the invoking object as a whole, it can use the expression *this. Using the const qualifier after the function argument parentheses qualifies this as being a pointer to const; in that case, you can’t use this to change the object’s value.


What you want to return, however, is not this because this is the address of the object. You want to return the object itself, and that is symbolized by *this. (Recall that applying the dereferencing operator * to a pointer yields the value to which the pointer points.) Now you can complete the method definition by using *this as an alias for the invoking object:

const Stock & Stock::topval(const Stock & s) const
{
    if (s.total_val > total_val)
       return s;           // argument object
    else
       return *this;       // invoking object
}

The fact that the return type is a reference means that the returned object is the invoking object itself rather than a copy passed by the return mechanism. Listing 10.7 shows the new header file.

Listing 10.7. stock20.h


// stock20.h -- augmented version
#ifndef STOCK20_H_
#define STOCK20_H_
#include <string>

class Stock
{
private:
    std::string company;
    int shares;
    double share_val;
    double total_val;
    void set_tot() { total_val = shares * share_val; }
public:
    Stock();        // default constructor
    Stock(const std::string & co, long n = 0, double pr = 0.0);
    ~Stock();       // do-nothing destructor
    void buy(long num, double price);
    void sell(long num, double price);
    void update(double price);
    void show()const;
    const Stock & topval(const Stock & s) const;
};

#endif


Listing 10.8 presents the revised class methods file. It includes the new topval() method. Also now that you’ve seen how the constructors and destructor work, Listing 10.8 replaces them with silent versions.

Listing 10.8. stock20.cpp


// stock20.cpp -- augmented version
#include <iostream>
#include "stock20.h"

// constructors
Stock::Stock()        // default constructor
{
    company = "no name";
    shares = 0;
    share_val = 0.0;
    total_val = 0.0;
}

Stock::Stock(const std::string & co, long n, double pr)
{
    company = co;

    if (n < 0)
    {
        std::cout << "Number of shares can't be negative; "
                   << company << " shares set to 0. ";
        shares = 0;
    }
    else
        shares = n;
    share_val = pr;
    set_tot();
}

// class destructor
Stock::~Stock()        // quiet class destructor
{
}

// other methods
void Stock::buy(long num, double price)
{
     if (num < 0)
    {
        std::cout << "Number of shares purchased can't be negative. "
             << "Transaction is aborted. ";
    }
    else
    {
        shares += num;
        share_val = price;
        set_tot();
    }
}

void Stock::sell(long num, double price)
{
    using std::cout;
    if (num < 0)
    {
        cout << "Number of shares sold can't be negative. "
             << "Transaction is aborted. ";
    }
    else if (num > shares)
    {
        cout << "You can't sell more than you have! "
             << "Transaction is aborted. ";
    }
    else
    {
        shares -= num;
        share_val = price;
        set_tot();
    }
}

void Stock::update(double price)
{
    share_val = price;
    set_tot();
}

void Stock::show() const
{
    using std::cout;
    using std::ios_base;
    // set format to #.###
    ios_base::fmtflags orig =
        cout.setf(ios_base::fixed, ios_base::floatfield);
    std::streamsize prec = cout.precision(3);

    cout << "Company: " << company
        << "  Shares: " << shares << ' ';
    cout << "  Share Price: $" << share_val;
    // set format to #.##
    cout.precision(2);
    cout << "  Total Worth: $" << total_val << ' ';

    // restore original format
    cout.setf(orig, ios_base::floatfield);
    cout.precision(prec);
}

const Stock & Stock::topval(const Stock & s) const
{
    if (s.total_val > total_val)
        return s;
    else
        return *this;
}


Of course, you want to see if the this pointer works, and a natural place to use the new method is in a program with an array of objects, which leads us to the next topic.

An Array of Objects

Often, as with the Stock examples, you want to create several objects of the same class. You can create separate object variables, as the examples have done so far in this chapter, but it might make more sense to create an array of objects. That might sound like a major leap into the unknown, but, in fact, you declare an array of objects the same way you declare an array of any of the standard types:

Stock mystuff[4]; // creates an array of 4 Stock objects

Recall that a program always calls the default class constructor when it creates class objects that aren’t explicitly initialized. This declaration requires either that the class explicitly define no constructors at all, in which case the implicit do-nothing default constructor is used, or, as in this case, that an explicit default constructor be defined. Each element—mystuff[0], mystuff[1], and so on—is a Stock object and thus can be used with the Stock methods:

mystuff[0].update();      // apply update() to 1st element
mystuff[3].show();        // apply show() to 4th element
const Stock * tops = mystuff[2].topval(mystuff[1]);
      // compare 3rd and 2nd elements and set tops
      // to point at the one with a higher total value

You can use a constructor to initialize the array elements. In that case, you have to call the constructor for each individual element:

const int STKS = 4;
Stock stocks[STKS] = {
    Stock("NanoSmart", 12.5, 20),
    Stock("Boffo Objects", 200, 2.0),
    Stock("Monolithic Obelisks", 130, 3.25),
    Stock("Fleep Enterprises", 60, 6.5)
    };

Here the code uses the standard form for initializing an array: a comma-separated list of values enclosed in braces. In this case, a call to the constructor method represents each value. If the class has more than one constructor, you can use different constructors for different elements:

const int STKS = 10;
Stock stocks[STKS] = {
    Stock("NanoSmart", 12.5, 20),
    Stock(),
    Stock("Monolithic Obelisks", 130, 3.25),
};

This initializes stocks[0] and stocks[2] using the Stock(const string & co, long n, double pr) constructor as well as stocks[1] using the Stock() constructor. Because this declaration only partially initializes the array, the remaining seven members are initialized using the default constructor.

Listing 10.9 applies these principles to a short program that initializes four array elements, displays their contents, and tests the elements to find the one with the highest total value. Because topval() examines just two objects at a time, the program uses a for loop to examine the whole array. Also it uses a pointer-to-Stock to keep track of which element has the highest value. This listing uses the Listing 10.7 header file and the Listing 10.8 methods file.

Listing 10.9. usestok2.cpp


// usestok2.cpp -- using the Stock class
// compile with stock20.cpp
#include <iostream>
#include "stock20.h"

const int STKS = 4;
int main()
{
// create an array of initialized objects
    Stock stocks[STKS] = {
        Stock("NanoSmart", 12, 20.0),
        Stock("Boffo Objects", 200, 2.0),
        Stock("Monolithic Obelisks", 130, 3.25),
        Stock("Fleep Enterprises", 60, 6.5)
        };

    std::cout << "Stock holdings: ";
    int st;
    for (st = 0; st < STKS; st++)
        stocks[st].show();
// set pointer to first element
    const Stock * top = &stocks[0];
    for (st = 1; st < STKS; st++)
        top = &top->topval(stocks[st]);
// now top points to the most valuable holding
    std::cout << " Most valuable holding: ";
    top->show();
     return 0;
}


Here is the output from the program in Listing 10.9:

Stock holdings:
Company: NanoSmart  Shares: 12
  Share Price: $20.000  Total Worth: $240.00
Company: Boffo Objects  Shares: 200
  Share Price: $2.000  Total Worth: $400.00
Company: Monolithic Obelisks  Shares: 130
  Share Price: $3.250  Total Worth: $422.50
Company: Fleep Enterprises  Shares: 60
  Share Price: $6.500  Total Worth: $390.00

Most valuable holding:
Company: Monolithic Obelisks  Shares: 130
  Share Price: $3.250  Total Worth: $422.50

One thing to note about Listing 10.9 is that most of the work goes into designing the class. When that’s done, writing the program itself is rather simple.

Incidentally, knowing about the this pointer makes it easier to see how C++ works under the skin. For example, the original Unix implementation used a C++ front-end cfront that converted C++ programs to C programs. To handle method definitions, all it had to do is convert a C++ method definition like

void Stock::show() const
{
    cout << "Company: " << company
         << "  Shares: " << shares << ' '
         << "  Share Price: $" << share_val
         << "  Total Worth: $" << total_val << ' ';
}

to the following C-style definition:

void show(const Stock * this)
{
    cout << "Company: " << this->company
         << "  Shares: " << this->shares << ' '
         << "  Share Price: $" << this->share_val
         << "  Total Worth: $" << this->total_val << ' ';
}

That is, it converted a Stock:: qualifier to a function argument that is a pointer to Stock and then uses the pointer to access class members.

Similarly, the front end converted function calls like

top.show();

to this:

show(&top);

In this fashion, the this pointer is assigned the address of the invoking object. (The actual details might be more involved.)

Class Scope

Chapter 9 discusses global (or file) scope and local (or block) scope. Recall that you can use a variable with global scope anywhere in the file that contains its definition, whereas a variable with local scope is local to the block that contains its definition. Function names, too, can have global scope, but they never have local scope. C++ classes introduce a new kind of scope: class scope.

Class scope applies to names defined in a class, such as the names of class data members and class member functions. Items that have class scope are known within the class but not outside the class. Thus, you can use the same class member names in different classes without conflict. For example, the shares member of the Stock class is distinct from the shares member of a JobRide class. Also class scope means you can’t directly access members of a class from the outside world. This is true even for public function members. That is, to invoke a public member function, you have to use an object:

Stock sleeper("Exclusive Ore", 100, 0.25);  // create object
sleeper.show();      // use object to invoke a member function
show();              // invalid -- can't call method directly

Similarly, you have to use the scope-resolution operator when you define member functions:

void Stock::update(double price)
{
    ...
}

In short, within a class declaration or a member function definition you can use an unadorned member name (the unqualified name), as when sell() calls the set_tot() member function. A constructor name is recognized when it is called because its name is the same as the class name. Otherwise, you must use the direct membership operator (.), the indirect membership operator (->), or the scope-resolution operator (::), depending on the context, when you use a class member name. The following code fragment illustrates how identifiers with class scope can be accessed:

class Ik
{
private:
   int fuss;       // fuss has class scope
public:
   Ik(int f  = 9) {fuss = f; }  // fuss is in scope
   void ViewIk() const;         // ViewIk has class scope
};

void Ik::ViewIk() const    //Ik:: places ViewIk into Ik scope
{
   cout << fuss << endl;   // fuss in scope within class methods
}
...
int main()
{
   Ik * pik = new Ik;
   Ik ee = Ik(8); // constructor in scope because has class name
   ee.ViewIk();   // class object brings ViewIk into scope
   pik->ViewIk(); // pointer-to-Ik brings ViewIk into scope
...

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

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