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.
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
.
this
PointerYou 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.
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.)
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.
// 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.
// 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.
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.
// 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.)
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
...
3.145.9.148