Review

The most important concept in this chapter is the idea of creating user-defined data types. In C++, this is done by defining a class for each such data type. Each class has both a class interface, which describes the behavior that the class displays to the "outside world" (i.e., other, unrelated functions), and a class implementation, which tells the compiler how to perform the behaviors promised in the interface definition. A variable of a class type is called an object. With proper attention to the interface and the implementation of a class, it is possible to make objects behave just like native variables; that is, they can be initialized, assigned, compared, passed as function arguments, and returned as function return values.

Both the interface and the implementation of a class are described in terms of the functions and variables of which the class is composed. These are called member functions and member variables, because they belong to the class rather than being "free-floating" or localized to one function like the global functions and local variables we encountered earlier.

Of course, one obvious question is why we need to make up our own variable types. What's wrong with char, short, and the rest of the native types built into C++? The answer is that it's easier to write an inventory control program, for example, if we have data types representing items in the stock of a store, rather than having to express everything in terms of the native types. An analogy is the universal preference of professionals to use technical jargon rather than "plain English". Jargon conveys more information, more precisely, in less time.

Creating our own types of variables allows us to use objects rather than functions as the fundamental building blocks of our programs, which is the basis of the "object-oriented programming" paradigm.

Then we examined how creating classes differs from using classes, which we have been doing throughout the book. A fairly good analogy is that creating your own classes is to using classes as writing a program is to using a program.

Next, we went through the steps needed to actually create a new class; our example is the StockItem class, which is designed to simulate tracking of inventory for a small grocery store. These steps include writing the interface definition, writing the implementation, writing the program that uses the class, compiling the implementation, compiling the program that uses the class, and linking the object files resulting from these compilation steps together with any needed libraries to produce the final executable program.

Then we moved from the general to the specific, analyzing the particular data and functions that the StockItem class needed to perform its duties in an application program. The member variables needed for each StockItem object included the name, count, distributor, price, and UPC. Of course, merely having these member variables doesn't make a StockItem object very useful if it can't do anything with them. This led us to the topic of what member functions might be needed for such a class.

Rather than proceed immediately with the specialized member functions that pertain only to StockItem, however, we started by discussing the member functions that nearly every class needs to make its objects act like native variables. A class that has (at least) the capabilities of a native type is called a concrete data type. Such a class requires the following member functions:

  1. A default constructor, which makes it possible to create an object of that class without supplying any initial data,

  2. A copy constructor, which makes it possible to create a new object of this type with the same contents as an existing object of the same type,

  3. An assignment operator, which copies the contents of one object of this type to another object of the same type,

  4. A destructor, which performs whatever cleanup is needed when an object of this type "dies".

Since these member functions are so important to the proper functioning of a class, the compiler will create a version of each of them for us if we don't write them ourselves. In the case of StockItem, these compiler-generated member functions are perfectly acceptable, with the exception of the default constructor. The compiler-generated default constructor doesn't initialize a new StockItem object to a valid state, so we had to write that constructor ourselves to be sure of what a newly created StockItem contains. Next, we looked at the first version of a class interface specification for StockItem (Figure 6.3), which tells the user (and the compiler) exactly what functions objects of this class can perform. Some items of note in this construct are these:

  1. The access specifiers public and private, which control access to the implementation of a class by functions not in the class (nonmember functions). Member variables and functions in the public section are available for use by nonmember functions, whereas member variables and functions in the private section are accessible only by member functions.

  2. The declarations of the constructor functions, which construct a new object of the class. The first noteworthy point about constructors is that they have the same name as the class, which is how the compiler identifies them as constructors. The second point of note is that there can be more than one constructor for a given class; all constructors have the same name and are distinguished by their argument lists. This facility, called function overloading, is applicable to C++ functions in general, not just constructors. That is, you can have any number of functions with the same name as long as they have different argument lists; the difference in argument lists is enough to make the compiler treat them as different functions. In this case, we have written two constructors: the default constructor, which is used to create a StockItem when we don't specify an initial value, and a constructor that has arguments to specify values for all of the member variables.[22]

    [22] The compiler has also supplied a copy constructor for us, so that we can use StockItem objects as function arguments and return values. In this case, the compiler-generated copy constructor does exactly what we want, so we don't have to write our own. As we'll see in the rest of the book, these compiler-generated functions don't always behave properly, especially with more complicated classes than the StockItem class, where the compiler can't figure out how to copy or assign objects correctly without more help from us.

  3. The declaration of a "normal" member function (that is, not a constructor or other predefined function) named Display, which as its name indicates, is used to display a StockItem on the screen.

  4. The declarations of the member variables of StockItem, which are used to keep track of the information for a given object of the StockItem class.

Once we'd defined the class interface, we started on the class implementation by writing the default constructor for the StockItem class: StockItem::StockItem(). The reason for the doubled name is that when we write the implementation of a member function, we have to specify what class that member function belongs to. In this example, the first StockItem is the name of the class, whereas the second StockItem is the name of the function, which, as always with constructors, has the same name as the class. By contrast, we didn't have to specify the class name when declaring member functions in the interface definition, because all functions defined there are automatically member functions of that class. During this discussion, we saw that the preferred way to set the values of member variables is by using a member initialization list.

The next topic we visited was the scope of member variables, which is class scope. Each object of a given class has one set of member variables, which live as long as the object does. These member variables can be accessed from any member function as though they were global variables.

Then we examined how the default constructor was actually used in the example program, discovering that the line StockItem soup; was enough to cause it to be called. This is appropriate because one of the design goals of C++ was to allow a class object to be as easy to use as a native variable. Since a native variable can be created simply by specifying its type and name, the same should be true of a class object.

This led to a discussion of the fact that the person who writes a class isn't always the person who uses it. One reason for this is that the skills required to write a program using a class are not necessarily the same as those required to create the class in the first place.

Next, we covered the other constructor we wrote for the StockItem class. This one has arguments specifying the values for all of the member variables that make up the data part of the class.

Then we got to the final function of the first version of the StockItem class, the Display function, which as its name indicates is used to display the contents of a StockItem on the screen. This function uses the pre-existing ability of << to display shorts and strings, including those that hold the contents of a StockItem. The return type of this function is a type we hadn't seen before, void, which simply means that there is no return value from this function. We don't need a return value from the Display function because we call it solely for its side effect: displaying the value of its StockItem on the screen.

Next, we took up the private part of the StockItem class definition, which contains the member variables. We covered two reasons why it is a good idea to keep the member variables private: first, it makes debugging easier, because only the member functions can modify the member variables; second, we can change the names or types of our member variables or delete them from the class definition much more easily if we don't have to worry about what other functions might be relying on them. While we were on the subject of the member variables of StockItem, I also explained how we could use a short to store a price; by expressing the price in cents, rather than dollars and cents, any price up to $327.67 could be stored in such a variable.

As we continued with the analysis of how the StockItem objects would be used, we discovered that our example program actually needed a Vec of such objects, one for each different item in the stock. We also needed some way to read the information for these StockItem objects from a disk file, so we wouldn't have to type it in every time we started the program up. So the next program we examined provided this function via a C++ library class we hadn't seen before: ifstream (for input from a file). We also added a new function called Read to use ifstream to read information for a StockItem from the file containing that information.

While looking at the implementation of the new Read member function we ran into the idea of a reference argument, which is an argument that is another name for the caller's variable, rather than a copy of that variable (a value argument). This makes it possible to change the caller's variable by changing the value of the argument in the function. In most cases, we don't want to be able to change the caller's variable, but it is essential when reading from a stream, because otherwise we'd get the same data every time we read something from the stream. Therefore, we have to use a reference argument in this case, so that the stream's internal state will be updated correctly when we retrieve data from it.

Then we got to the question of how we could tell when there were no data left in the input file. The answer was to call the ifstream member function fail, which returns zero if some data remain in the stream and nonzero if we have tried to read past the end of the file. We used a nonzero return value from fail to trigger a break statement, which terminates whatever loop contains the break. In this case, the loop was the one that read data from the input file, so the loop would stop whenever we got to the end of the input file or when we had read 100 records, whichever came first.

This led to a detailed investigation of whether the number of records read was always calculated correctly. The problem under discussion was the potential for a fencepost error, also known as an off by one error. After careful consideration, I concluded that the code as written was correct.

Having cleared up that question, we proceeded to some other scenarios that might occur in the grocery store for which this program theoretically was being written. All of the scenarios we looked at had a common requirement: to be able to look up a StockItem, given some information about it. We first tried to handle this requirement by reading the UPC directly from each StockItem object in the Vec. When we found the correct StockItem, we would display and update the inventory for that StockItem. However, this didn't compile, because we were trying to access private member variables of a StockItem object from a nonmember function, which is illegal. While we could have changed those variables from private to public, that would directly contradict the reason that we made them private in the first place; that is, to prevent external functions from interfering in the inner workings of our StockItem objects. Therefore, we solved the problem by adding some new member functions (CheckUPC and DeductSaleFromInventory) to check the UPC of a StockItem and manipulate the inventory information for each StockItem, respectively. At the same time, we examined a new data type, bool, which is limited to the two values true and false; it is handy for keeping track of information such as whether we have found the StockItem object we are looking for and communicating such information back to a calling function.

While I was making these changes, I noticed that the original version of the test program wasn't very helpful to its user; it didn't tell the user whether the UPC was found, the name of the item, or how much inventory was available for sale. So I added some more member functions (GetInventory and GetName) to allow this more "user-friendly" information to be displayed.

Then we progressed to the second of the grocery store scenarios, in which the task was to find the price of an item, given its UPC. This turned out to be very similar to the previous problem of finding an item to update its inventory. Therefore, it was a pretty obvious step to try to make a function out of the "find an item by UPC" operation, rather than writing the code for the search again. Since we're doing "object-oriented" programming, such a function should probably be a member function. The question was "of which class?" It couldn't be a member function of StockItem, because the whole idea of this function was to locate a StockItem. A member function of StockItem needs a StockItem object to work on, but we didn't have the StockItem object yet.

The solution was to make another class, called Inventory, which had member functions to read the inventory information from the disk file (LoadInventory) and search it for a particular StockItem (FindItem). Most of this class was pretty simple, but we did run into an interesting design question: What should the FindItem function return if the UPC didn't match anything in the inventory? After some consideration, I decided to use a null object of the class StockItem; that is, one that exists solely to serve as a placeholder representing a non-existent object. This solution required adding an IsNull member function to the StockItem class, so that the user of FindItem could determine whether the returned object was "real" or just an indication of a UPC that wasn't found in the inventory.

Then we updated the test program to use this new means of locating a StockItem. Since the new version of the test program could perform either of two functions (price check or sale), we also added some output and input statements to ask the user what he wanted to do. To make this process more flexible, we allowed the user to type in either an upper or lower case letter to select which function to perform. This brought up the use of the "logical OR" operator || to allow the controlled block of an if statement to be executed if either (or both) of two expressions is true. We also saw how to combine an else with a following if statement, when we wanted to select among more than two alternatives.

We needed two more functions to make this new version of the application program work correctly: one to update the item in the inventory (Inventory::UpdateItem(StockItem Item)) and one to get the UPC of a StockItem (StockItem::GetUPC()). The reason that we had to add these new functions to the interfaces of Inventory and StockItem, respectively, is that we were no longer operating on the "real" StockItem, as we had been when we accessed the inventory Vec directly in the previous version of the application program. Instead, we were getting a copy of the StockItem from the Inventory object and changing that copy; thus, to have the final result put back into the Inventory object, we had to add the UpdateItem member function of Inventory that overwrote the original StockItem with our changed version. The GetUPC function's role in all this was to allow the UpdateItem function to look up the correct StockItem to be replaced without the main program having to pass the UPC in explicitly; instead, the GetUPC function allowed the UpdateItem function to retrieve the correct UPC from the updated object provided by the main program.

This brought us to the final scenario, which required us to look up the inventory for an item, given its UPC. As it happened, we had already solved that problem by the simple expedient of displaying the name and inventory of the StockItem as soon as it was located.

Finally, I mentioned a few other factors, such as alternative means of looking up an item without knowing its UPC, that would be important in writing a real application program and noted that we couldn't go into them here due to space limitations, but would deal with similar issues later in the book.

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

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