More about the StockItem class Interface

Now let's continue with our analysis of the class interface for the StockItem class (Figure 6.3 on page 312). Before we can do anything with a StockItem object, we have to enter the inventory data for that object. This means that we need another constructor that actually sets the values into a StockItem. We also need some way to display the data for a StockItem on the screen, which means writing a Display function.

The next line of that figure is the declaration of the “normal” constructor that creates an object with actual data:

StockItem(std::string Name, short InStock, short Price, std::string Distributor, std:
:string UPC);

We can tell that this function is a constructor because its name, StockItem, is the same as the name of the class. One interesting thing about this constructor is that its name is the same as the previous constructor: namely, StockItem. This is a very handy facility, called function overloading, which isn't limited to constructors. The combination of the function name and argument types is called the signature of a function; two functions that have the same name but differ in the type of at least one argument are distinct functions.[14] In the case of the default constructor, there are no arguments, so that constructor is used where no initial data are specified for the object. The statement StockItem soup; in the sample program (Figure 6.1) fits that description, so the default constructor is used. However, in the next statement of the sample program, we have the expression:

[14] Note that the names of the arguments are not part of the signature; in fact, you don't have to specify them in the function declaration. However, you should use meaningful argument names in the function declaration so the user of the function has some idea what the arguments to the function might represent. After all, the declaration StockItem(string, short, short, string, string); doesn't provide much information on what its arguments actually mean.

StockItem("Chunky Chicken", 32, 129, "Bob's Distribution", "123456789");

This is clearly a call to a constructor, because the name of the function is the name of a class, StockItem. Therefore, the compiler looks for a constructor that can handle the set of arguments in this call and finds the following declaration:

StockItem(std::string Name, short InStock, short Price, std::string Distributor, std:
:string UPC);

The first, fourth, and fifth arguments to the constructor are strings, while the second and third are shorts. Since these types all match those specified in the expression in the sample program, the compiler can translate that expression into a call to this constructor.

Figure 6.7 shows the code for that constructor.

Figure 6.7. Another constructor for the StockItem class (from codeitem1.cpp)
StockItem::StockItem(string Name, short InStock,
short Price, string Distributor, string UPC)
: m_Name(Name), m_InStock(InStock), m_Price(Price),
  m_Distributor(Distributor), m_UPC(UPC)
{
}

As you can see, nothing about this constructor is terribly complex; it merely uses the member initialization list to set the member variables of the object being constructed to the values of their corresponding arguments.

Referring to Standard Library Identifiers in Header Files

Before we get to the implementation of this function, there's one thing in this header file that we haven't seen before in real code, although I've mentioned it in passing: the use of the standard library namespace specifier std as it is used in the declaration:

StockItem(std::string Name, short InStock, short Price, std::string Distributor, std:
:string UPC);

What does std::string mean? It notifies the compiler that the name string refers to a name from the standard library. Until this point, whenever we needed to refer to identifiers from the standard library, we have either given the compiler blanket permission to import all the names from the standard library into the current namespace that we are using, or to import specific names from the standard library into the current namespace. To give the compiler permission to import all the names in the standard library we use the statement using namespace std; and an example of blanket namespace permission for a specific identifier is using std::cout;. These two ways of telling the compiler what we mean seem to cover all of the possibilities and are much more convenient than having to write std:: many times. So why do we need to use this seemingly redundant method of specifying the namespace of something from the standard library?

Because it's a very bad idea to have any blanket namespace permissions in a header file. That can change the behavior of an implementation file without the knowledge of the person who wrote that implementation file. Let's say, for example, that someone has made up their own string class and therefore doesn't want to tell the compiler to import the identifier string from the standard library. If we wrote the line using std::string; in a header file that he included in his implementation file, the compiler would import the identifier string from the standard library, which would cause confusion between his string and the standard library version. So the only sensible thing to do is to avoid using statements in header files.

But sometimes, we do need to refer to standard library names in our header files. In that event, we just attach std:: to the beginning of the names of standard library identifiers and then the compiler knows exactly what we mean.

The Need for More Than One Constructor

Now that that's cleared up, you may be wondering why we need more than one constructor for the StockItem class. Susan had that same question, and I had some answers for her.

But why do we need more than one constructor? Susan had that same question, and I had some answers for her.

Susan: How many constructors do you need to say the same thing?

Steve: They don't say exactly the same thing. It's true that every constructor in the StockItem class makes a StockItem; however, each argument list varies. The default constructor makes an empty StockItem and therefore doesn't need any arguments, whereas the constructor StockItem::StockItem(string Name, short InStock, short Price, string Distributor, string UPC) makes a StockItem with the values specified by the Name, InStock, Price, Distributor, and UPC arguments in the constructor call.

Susan: Are you saying that in defining a class you can have two functions that have the same name, but they are different in only their arguments and that makes them unique?

Steve: Exactly. This is the language feature called function overloading.

Susan: So StockItem soup; is the default constructor in case you need something that can create uninitialized objects?

Steve: Not quite; the default constructor for the StockItem class is StockItem::StockItem(), which doesn't need any arguments, because it constructs an empty StockItem. The line StockItem soup; causes the default constructor to be called to create an empty StockItem named soup.

Susan: And the line StockItem("Chunky Chicken",32,129,"Bob's Distribution","123456789"); is a constructor that finally gets around to telling us what we are trying to accomplish here?

Steve: Again, not quite. That line causes a StockItem with the specified contents to be created, by calling the constructor StockItem::StockItem (string Name, short InStock, short Price, string Distributor, string UPC);.

Susan: So are you saying that for every new StockItem you have to have a new constructor for it?

Steve: No, there's one constructor for each way that we can construct a StockItem. One for situations where we don't have any initial data (the default constructor), one for those where we're copying one StockItem to another (the compiler-generated copy constructor), and one for situations where we are supplying the data for a StockItem. There could be other ones too, but those are all we have right now.

Once that expression has been translated, the compiler has to figure out how to assign the result of the expression to the StockItem object called soup, as requested in the whole statement:

soup = StockItem("Chunky Chicken", 32, 129, "Bob's Distribution", "123456789");

Since the compiler has generated its own version of the assignment operator = for the StockItem class, it can translate that part of the statement as well. The result of the entire statement is that the StockItem object named soup has the value produced by the constructor.

Finally, we have the line soup.Display(); which asks soup to display itself on the screen. Figure 6.8 shows the code for that function.

Figure 6.8. Display member function for the StockItem class (from codeitem1.cpp)
void StockItem::Display()
{
  cout << "Name: ";
  cout << m_Name << endl;
  cout << "Number in stock: ";
  cout << m_InStock << endl;
  cout << "Price: ";
  cout << m_Price << endl;
  cout << "Distributor: ";
  cout << m_Distributor << endl;
  cout << "UPC: ";
  cout << m_UPC << endl;
}

This is also not very complicated; it just uses << to copy each of the parts of the StockItem object to cout, along with some identifying information that makes it easier to figure out what the values represent.

Susan wanted to know how we could use << without defining a special version for this class.

Susan: Hey, how come you don't have to define << as a class operator? Does the compiler just use the native <<? And that works OK?

Steve: We're using << only for types that the standard library already knows about, which includes all of the native types as well as its string class. If we wanted to apply << to a StockItem itself, we'd have to write our own version; you'll see how we do that when we go into our implementation of string in Chapter 7.

Susan: Then please explain to me why << is being used in Figure 6.8, which is for the StockItem class.

Steve: It's being used for strings and shorts, not objects of the StockItem class. The fact that the strings and shorts are inside the StockItem class is irrelevant in this context; they're still strings and shorts, and therefore can be displayed by the << operators that handle strings and shorts.

Susan: So the stuff you get out of the standard library is only for the use of class types? Not native?

Steve: No. The iostream part of the standard library is designed to be able to handle both native types and class types; however, the latter use requires the class writer to do some extra work, as we'll see when we add these functions to our version of the string class in Chapter 8.

Susan: So that means that the library is set up to understand things we make up for our class types?

Steve: Sort of. As long as we follow the library's rules when creating our own versions of << and >>, we'll be able to use those operators to read and write our data as though the ability to handle those types was built into the library.

That should clear up most of the potential problems with the meaning of this Display function. However, it does contain one construct that we haven't seen before: void. This is the return type of the Display function, as might be apparent from its position immediately before the class name StockItem. But what sort of return value is a void? In this context, it means simply that this function doesn't supply a return value at all.

You won't be surprised to learn that Susan had a few questions about this idea of functions that return no value.

Susan: How can a function not return a value? Then what is the point? Then would it "have no function"?

Steve: Now you're telling programming jokes? Seriously, though, the point of calling a function that returns no value is that it causes something to happen. The Display function is one example; it causes the value of a StockItem object to be displayed on the screen. Another example is a "storage function"; calling such a function can cause it to modify the value of some piece of data it is maintaining so when you call the corresponding "retrieval function", you'll get back the value the "storage function" put away. Such lasting effects of a function call (other than returning a value) are called side effects.

Susan: But even a side effect is a change, so then it does do something after all, right?

Steve: Sure, it does something; every function should do something, or you wouldn't write (or call) it. However, some functions don't return any value to the calling program, in which case we specify their return type as void.

That takes care of the public part of the class definition. Now what about the private part? As I mentioned before in the discussion of how a class is defined, the access specifier private means that only member functions of the class can access the items after that specifier. It's almost always a good idea to mark all the member variables in a class as private, for two reasons.

  1. If we know that only member functions of a class can change the values of member data, then we know where to look if the values of the data are incorrect. This can be extremely useful when debugging a program.

  2. Marking member variables as private simplifies the task of changing or deleting those member variables should that become necessary. If the member variables are public, then we have no idea what functions are relying on their values. That means that changing or deleting these member variables can cause havoc anywhere in the system. Allowing access only by member functions means that we can make changes freely as long as all of the member functions are kept up to date.

Both of these advantages of keeping member variables private can be summed up in the term encapsulation, which means "hiding the details inside the class implementation rather than exposing them in the interface". This is one of the primary organizing principles that characterizes object-oriented programming.

Now that we've covered all of the member functions and variables of the StockItem class, Figure 6.9 shows the interface for the StockItem class again. As noted previously, the test program for this class, itemtst1.cpp, is shown in Figure 6.1.

Figure 6.9. The initial interface of the StockItem class (codeitem1.h)
class StockItem
{
public:
  StockItem();

  StockItem(std::string Name, short InStock, short Price,
  std::string Distributor, std::string UPC);

  void Display();

private:
  short m_InStock;
  short m_Price;
  std::string m_Name;
  std::string m_Distributor;
  std::string m_UPC;
};

There's only one more point about the member variables in the StockItem class that needs clarification; surely the price of an object in the store should be in dollars and cents, and yet we have only a short to represent it. As you know by now, a short can hold only a whole number, from –32768 to 32767. What's going on here?

Only that I've decided to store the price in cents rather than dollars and cents. That is, when someone types in a price I'll assume that it's in cents, so "246" would mean 246 cents or $2.46. This would of course not be acceptable in a real program, but for now it's not a problem.

This “trick” allows prices up to $327.67 (as well as negative numbers for things like coupons), which should be acceptable for our hypothetical grocery store. In Chapter 9, I'll give you some tips on using a different kind of numeric variable that can hold a greater variety of values. For now, though, let's stick with the short.

Figure 6.10 shows the implementation for the StockItem class.

Figure 6.10. The initial implementation of the StockItem class (codeitem1.cpp)
#include <iostream>
#include <string>
#include "item1.h"
using namespace std;

StockItem::StockItem()
: m_Name(), m_InStock(0), m_Price(0), m_Distributor(), m_UPC()
{
}

StockItem::StockItem(string Name, short InStock,
short Price, string Distributor, string UPC)
: m_Name(Name), m_InStock(InStock), m_Price(Price),
  m_Distributor(Distributor), m_UPC(UPC)
{
}

void StockItem::Display()
{
  cout << "Name: ";
  cout << m_Name << endl;
  cout << "Number in stock: ";
  cout << m_InStock << endl;
  cout << "Price: ";
  cout << m_Price << endl;
  cout << "Distributor: ";
  cout << m_Distributor << endl;
  cout << "UPC: ";
  cout << m_UPC << endl;
}

Susan had a few questions about the way that we specified the header files in this program:

Susan: How come the #includeitem1.h" are in "" instead of <>? I thought all header files are in <>?

Steve: Putting an include file name in "" tells the compiler to start looking for the file in the current directory and then search the "standard places". Using the <> tells the compiler to skip the current directory and start with the "standard places"; that is, the places where the standard library header files are stored. What the "standard places" are depends on the compiler.

Susan: I know I have not been the most focused test reader in the last week, but did I miss something here? Did you explain way back in an earlier chapter and I just forgot? I want to make sure that you have explained this in the book, and even if you had earlier it would not be a bad idea to remind the reader about this.

Steve: Yes, as a matter of fact I did mention it briefly, in a footnote on page 160, but I'm not surprised that you don't recall reading it. Actually, you've been quite focused, although not in quite the same area.

Assuming that you've installed the software from the CD in the back of this book, you can try out this program. First, you have to compile it by following the compilation instructions on the CD. Then type itemtst1 to run the program. You'll see that it indeed prints out the information in the StockItem object. You can also run it under the debugger by following the usual instructions for that method.

That's good as far as it goes, but how do we use this class to keep track of all of the items in the store? Surely we aren't going to have a separately named StockItem variable for each one.

Handling a Number of StockItems with a Vec

This is another application for our old friend the Vec; specifically, we need a Vec of StockItems to hold the data for all the StockItems in the store. In a real application we would need to be able to vary the number of elements in the Vec, unlike our previous use of Vecs. After all, the number of items in a store can vary from time to time.

As it happens, the size of a Vec can be changed after it is created. However, in our example program we'll ignore this complication and just use a Vec that can hold 100 StockItems.[15] Even with this limitation, we will have to keep track of the number of items that are in use, so that we can store each new StockItem in its own Vec element and keep track of how many items we may have to search through to find a particular StockItem. Finally, we need something to read the data for each StockItem from the inventory file where it's stored when we're not running the program.

[15] We'll see how to change the size of a Vec in Chapter 11.

Susan had some questions about these details of the test program:

Susan: In the paragraph when you are talking about the number of items, I am a little confused. That is, do you mean the number of different products that the store carries or the quantity of an individual item available in the store at any given time?

Steve: The number of different products, which is the same as the number of StockItems. Remember, each StockItem represents any number of objects of that exact description.

Susan: So what you're referring to is basically all the inventory in the store at any given period of time?

Steve: Exactly.

Susan: What do you mean by "need something to read the data" and "where it's stored when we're not running the program"? I don't know what you are talking about, I don't know where that place would be.

Steve: Well, where are the data when we're not running the program? The disk. Therefore, we have to be able to read the information for the StockItems from the disk when we start the program.

Susan: Okay, that makes sense now.

Steve: I'm glad to hear it.

Figure 6.11 is a little program that shows the code necessary to read the data for the StockItem Vec into memory when the program starts up.

Figure 6.11. Reading and displaying a Vec of StockItems (codeitemtst2.cpp)
#include <iostream>
#include <fstream>
#include <string>
#include "Vec.h"
#include "item2.h"
using namespace std;

int main()
{
   ifstream ShopInfo("shop2.in");
   Vec<StockItem> AllItems(100);
   short i;
   short InventoryCount;

   for (i = 0; i < 100; i ++)
      {
      AllItems[i].Read(ShopInfo);
      if (ShopInfo.fail() != 0)
          break;
      }

   InventoryCount = i;

   for (i = 0; i < InventoryCount; i ++)
      {
      AllItems[i].Display();
      }

   return 0;
}

This program has a number of new features that need examination. First, we've had to add the "file stream" header file <fstream> to the list of include files, so that we will be able to read data in from a file. The way we do this is to create an ifstream object that is "attached" to a file when the object is constructed. In this case, the line ifstream ShopInfo("shop2.in"); creates an ifstream object called ShopInfo, and connects it to the file named shop2.in.

The first line in the upper loop is AllItems[i].Read(ShopInfo);, which calls the function Read for the ith StockItem in the AllItems Vec, passing the ShopInfo ifstream object to a new StockItem member function called Read, which uses ShopInfo to get data from the file and store it into its StockItem (i.e., the ith element in the Vec).

This whole process was anything but obvious to Susan.

Susan: How does just adding the header file fstream enable you to read data in from a file?

Steve: The file fstream contains the declaration of the ifstream class.

Susan: Where did the ifstream class come from?

Steve: It's part of the standard library.

Susan: Where did it come from and how did it get there? Who defined it and when was it written? Your only reference to this is just "The way we do this is to create an ifstream object that is `attached' to a file when the object is constructed". If this is just something that you wrote to aid this program that we don't have to worry about at this time, then please mention this.

Steve: I didn't write it, but we don't have to worry about it very much. I'll explain it to the minimum extent necessary.

After that bit of comic relief, let's get back to reality. Figure 6.12 is the implementation of the new Read function.

Figure 6.12. The Read function for the StockItem class (from codeitem2.cpp)
void StockItem::Read(istream& is)
{
 getline(is,m_Name);
 is >> m_InStock;
 is >> m_Price;
 is.ignore();
 getline(is,m_Distributor);
 getline(is,m_UPC);
}

The task this function performs is fairly simple, but there are some complexities in its implementation due to peculiarities of the standard library. Let's start with the first line of the body of the function:

getline(s,m_Name);

What does this do? It gets a line of data from the input stream, whose name is s, and puts it into our m_Name variable. Why can't we use the >> operator for this purpose, as we indeed do for the next two member variables that we are reading, m_Instock and m_Price?

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

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