As you likely found out the last time you purchased your last textbook, books can be quite expensive. One way that this expense can be reduced is the idea of sharing: If you are unable to afford the full price for a book, and can find another person with the same problem, then you might purchase the book together, and share it. By having one person use the book and then another person re-use the book, the individual cost of using the book is cut in half.
A library is a generalization on this idea of sharing. If a community of people pool their resources, then they can buy and share a centralized collection of books. By cooperating this way, each person has access to a greater set of books than he or she could afford individually.
The ideas of sharing, libraries and re-use are very important in C++, and we have already (unwittingly) used them extensively. For example, in the previous lab exercises, we wrote
#include <iostream>Recall that the effect of this directive is to insert the contents of the file iostream (in which cin, cout, << and >> are declared) into our source program. Recall also that each of the programs we have written has re-used this same set of declarations. The file iostream is thus similar to a library of books, in that it contains a set of declarations that can be shared and re-used by any program that needs them. In fact, iostream is a part of the iostream library -- a software library of I/O related functions.
Software libraries are particularly useful and important, because with carefuly planning, they can reduce the cost of programming. For example, in a previous lab exercise, we used the sqrt() function from the C++ math library, and so avoided the extra "cost" of having to write our own square root function. Libraries thus provide a place where groups of related functions can be stored, so that we (or even other programmers) can access them and thus avoid "re-inventing the wheel."
While C++ provides a number of ready-made libraries for us, C++ also permits programmers to create their own libraries, which are technically called library modules, or just modules. (We will use the terms library and module interchangeably.) Creating a library is the subject of today's exercise.
Let us briefly review what we know about function libraries. We have seen that C++ provides a variety of function libraries, including cmath that provides various mathematical functions, and cctype that provides character-processing functions.
To use a library function, a program must use the #include directive to include the library's header file:
#include <cmath>which inserts declarations of the library's functions into the program.
To call a function, you must give its name and a pair of parentheses containing any arguments the function needs to do its job:
result = pow(x, y);When control reaches such a function call, the arguments x and y are evaluated and passed to the function, which performs its task and (if appropriate) returns a value back to its caller (in this case x^y).
To keep our presentation simple, we will construct our library in our working directory for this exercise (e.g., ~/labs/3a), so begin by creating that directory and then change directory so that it is the working directory.
The library we will create today will provide us with a set of functions to convert English-system measurements into their metric-system counterparts. We will call our library metric.
The first thing that we must decide is what measurement conversions we wish our library to provide. For example, the following are just a few of the useful conversions:
| English Unit | Metric Unit | Conversion Formula |
|---|---|---|
| Inches | Centimeters | 1 inch = 2.54 cm |
| Feet | Centimeters | 1 foot = 30.48 cm |
| Feet | Meters | 1 foot = 0.3048 m |
| Yards | Meters | 1 yard = 0.9144 m |
| Miles | Kilometers | 1 mile = 1.609344 km |
| Ounces | Grams | 1 ounce = 28.349523 g |
| Pounds | Kilograms | 1 pound = 0.453592 kg |
| Tons | Kilograms | 1 ton = 907.18474 kg |
| Pints | Liters | 1 pint = 0.473163 l |
| Quarts | Liters | 1 quart = 0.946326 l |
| Gallons | Liters | 1 gallon = 3.785306 l |
Our exercise today is to write a subprogram (more precisely, a function) to convert feet into meters. By storing this function in library metric, it can be shared by any and all programs that need to convert feet into meters, allowing them to avoid "redefining the wheel."
A library consists three separate files:
The header file is necessary, and its role is to provide an interface to the library, by providing just enough information for a program to use the functions it provides, without specifying their details. The part of the function that is provided in the header file is called its declaration or prototype. We will use the term prototype throughout this manual.
By contrast, the role of the implementation file is to provide complete definitions of the library's functions. An implementation file thus contains a series of function definitions.
Why this separation? The reason has to do with program maintenance. If we are writing a library, then we expect that a number of programs will make use of it. It is often the case that even a well-designed library may need to be updated (i.e., maintained), if a better way is discovered to perform one of its functions. If we have designed our functions carefully, then updating a library function should simply involve altering its definition (in the implementation file), not its prototype (in the header file).
Now suppose that a program could access and make use of the definition details of a library function. If it did, then updating the function will likely change those details, and if the program were written in such a way as to be dependent on those details, then it would have to be updated, too. That is what we want to avoid at all costs -- we should never have to modify a program as a result of updating a library function.
By separating a library into two files, we can place the prototypes in one file and make it visible to a program (via the #include directive), while keeping the definitions "hidden" from the program in the implementation file. This makes it impossible to write a program that is dependent on the definition details of a function.
Begin by making sure that your working directory is labs/3a. Then save copies of Makefile, metric.h, metric.cpp, metric.doc, and driver.cpp to your working directory.
Using your text editor, edit metric.h (the name of the library's header file usually ends in .h) and personalize its opening documentation. A simplified general pattern for a library header file is as follows:
OpeningDocumentation PrototypeListwhere OpeningDocumentation is a comment describing the purpose of the library, who wrote it, when it was last updated, and so on; and PrototypeList is any sequence of valid C++ declarations.
As we have seen, software that is carefully designed is better than software that is poorly designed. The same holds true for functions, so let's spend a few moments using object-centered design to design our function.
Our function should receive from its caller the number of feet to be converted, and should check that this value is positive. It should convert that quantity to meters by multiplying it by 0.3048, and return the resulting value to the caller.Note that where a program typically inputs values from the keyboard, a function typically receives values from whoever called the function. Similarly, where a program typically outputs values to the screen, a function typically returns a value to whoever called the function
Note also that we want to try and anticipate what could go wrong. Since the function will only produce a correct result if the value it receives is not negative, we say that this function has a precondition, a condition that must be true in order for the function to execute correctly.
The objects of a function are similar to those of a program, except that in addition to describing the type and kind of each object, we also want to describe its movement: does it move into the function from outside, does it move from the function out to the outside, or is it purely local to the function?
| Description | Type | Kind | Movement | Name |
|---|---|---|---|---|
| the number of feet | double | varying | in | feet |
| the conversion factor | double | constant | local | 0.3048 |
| the corresponding number of meters | double | varying | out | meters |
This information provides us with what we need to specify the problem our function solves:
Specification:
receive: feet, the (double) number of feet to be converted.
precondition: feet is not negative.
return: meters, the (double) equivalent of feet in meters.
Note that we include the function's precondition as part of its
specification.
This specification in turn provides us with what we need to write a function prototype. The simplified pattern for a function prototype is
ReturnType FunctionName ( ParameterDecList ) ;where
A precise function specification tell us how to build the function's prototype. To illustrate:
double FeetToMeters(double feet);
After you have added this line to metric.h, copy-and-paste it into metric.doc, below its specification.
Since parameters are like variables, their names usually begin with a lowercase letter, with every word after the first capitalized. By contrast, each word in a function's name is capitalized.
As this illustrates, the stages of software design are fluid, not firm. We can build a function's prototype (a part of coding) during our design stage, as soon as we have enough information. (And the specification provides all of the necessary information to write the prototype.)
Continuing with our design, our list of needed operations is quite short:
| Description | Predefined? | Name | Library? |
|---|---|---|---|
| get a value from the caller | yes | parameter mechanism | built-in |
| check that the value is not negative | yes | value >= 0 | built-in |
| halt the program if the value is negative | yes | assert() | cassert |
| multiple two real values | yes | * | built-in |
| return a value to the caller | yes | return | built-in |
To test our precondition and halt the program if it fails, we can use the assert() mechanism and a boolean expression that returns true if and only if feet is not negative.
We can then organize these operations and objects into the following algorithm:
1. Receive feet from the caller. 2. Halt the program if feet is negative. 3. Compute meters = feet * 0.3048. 4. Return meters.Given an algorithm, we are ready to define FeetToMeters() in our implementation file.
As stated previously, the definitions of library functions are stored in the library's implementation file, so using your text editor, open the file metric.cpp. (Implementation file names end in the .cpp suffix.
The general pattern for an implementation file is as follows:
OpeningDocumentation #include "HeaderFileName" DefinitionListTake a moment to personalize the opening documentation, and add a #include directive that inserts the header file of our metric library. (Note that quotes, not <>, surround the file name. This tells the compiler to search the working directory for the file, instead of the special system include directory.)
The general pattern for a function definition is
ReturnType FunctionName ( ParameterDefList )
{
StatementList
}
where
A function stub is a minimal function definition -- like a function prototype, but followed by a pair of empty braces, instead of a semicolon. For example, we might define the following stub for FeetToMeters():
double FeetToMeters (double feet)
{
}
Insert this stub at the end of the implementation file and
then compile it using the command:
Compile command: g++ -c metric.cppIf all is well, then the function stub in metric.cpp should compile without generating any error messages. If you do get errors, find and correct them before continuing.
Take a moment and consider what we just did. We just compiled a file that contains no main program and the compiler didn't generate an error. How can this be?
The answer is the idea of separate compilation. Translation of a program is actually a two-step process:
Now that we have a stub, we can "fill it in" using our algorithm and stepwise translation.
This step is taken care of for us by the C++ function-call mechanism. When the caller evaluates an expression that calls our function, such as:
cout << FeetToMeters(2.5);a copy of the argument 2.5 will automatically be stored in parameter feet in our function. Since we have already declared parameter feet in our function, we need do nothing more to accomplish step 1.
We can accomplish this step using the C++ assert() mechanism:
assert(feet >= 0);
Given a boolean expression, the assert() mechanism
allows execution to proceed normally
if the boolean expression evaluates to true, but halts the program
and displays a diagnostic message if it evaluates to false.
It is common practice to use assert() to test a function's
preconditions.
To use assert(), you must #include the header file cassert (or assert.h, depending on how ANSI-compliant your compiler is).
Using the declaration statements and expressions that we learned about in lab 2, add a declaration statement to FeetToMeters() that performs step 3 of our algorithm. Recompile metric.cpp, and continue when its syntax is correct.
The third operation can be performed using the C++ return statement, whose general pattern is:
return Expression;where Expression is any valid C++ expression.
In the stub of FeetToMeters(), write a return statement that accomplishes the third operation. Then recompile metric.cpp to verify that what you have written is free of syntax errors. When it is, continue on in the exercise.
When metric.cpp is free of syntax errors, we are ready to test FeetToMeters(). To do so requires that we call FeetToMeters() from a program. The header file metric.h provides an interface to the function, since it contains a prototype of FeetToMeters(); and the implementation file metric.cpp provides the details of how FeetToMeters() is defined.
Next, open driver.cpp and personalize its documentation. In this file we will create a program whose sole purpose is to test FeetToMeters(). Such programs are called driver programs, because all they do is "test-drive" the functions in a library.
Most driver programs use the same algorithm:
1. Display a prompt for whatever values the function requires as arguments.
2. Read those values from cin.
3. Display via cout the result of calling the function,
using the input values as arguments (plus a descriptive label).
Since every driver program uses this same basic algorithm,
we can skip the entire Design stage and proceed straight
to Coding.
Personalize the opening documentation in driver.cpp. Then add a C++ program containing the necessary output, declaration, and input statements to perform steps 1 and 2 of our algorithm. Don't forget to add #include directives to insert the header files of both the iostream library and our metric library.
To perform step 3, recall that an argument is a value that is passed to a function when the function is called. For example, if we were to write:
cout << "\nThe number of meters is " << FeetToMeters(inValue) << endl;then the value of inValue would be the argument in this call to FeetToMeters().
There are three important rules to remember when calling a function:
| Feet | Meters (Predicted) |
Meters (Observed) |
|---|---|---|
| 1.0 | 0.3048 | ______________ |
| 3.3 | 1.00584 | ______________ |
Since FeetToMeters() is a linear function, two test values are sufficient to verify its correctness. Be sure to note and correct any discrepancies. When driver executes FeetToMeters() correctly, our task is finished!
Before we quit, let's learn a bit more about how make works. If you were watching closely when driver translated, you saw three separate commands execute:
Using your editor, alter driver.cpp in some trivial way (e.g., add a blank line at its beginning). Then use the compile command to invoke make again. Do all three steps occur again? If not, which step is omitted?
UNIX systems associate a "last modified date" with each file. To avoid recompiling a file needlessly, the make program uses these dates to determine what files need to be recompiled. That is, the date of metric.o was more recent than that of metric.cpp. The make program noted this and concluded that metric.cpp had not been modified, and so did not waste time recompiling it.
By contrast, when we made our trivial modification to driver.cpp and then saved it, the file driver.cpp became newer than the file driver.o. The make program noted this, concluded that driver.o was outdated, and so re-made driver.o. This in turn made driver.o newer than driver. Recognizing this, the make program reperformed the command to make driver, which relinked (the new) driver.o and (the old) metric.o.
In general, make will only re-make a target if any of that target's components are out of date. To see this, use make to translate driver one last time (without modifying anything) and note the output.
On a final note, add the following lines at the end of your Makefile:
clean:
rm -f driver *.o *~ *#
Then save your Makefile, go to an xterm, make sure that labs/3a is its working directory, and use ls to examine the contents of the directory. Then enter:
% make cleanand you should see make "clean" your directory for you! Use ls again and see how much tidier it is! If you then enter
% makeyou will see make rebuild driver for you!
Here is how it works: The added lines specify the name clean as a target to be made, with no components, so "clean" has no dependencies. When you type make clean, the make program searches for the name clean as a target. Finding the lines you just added, it executes the command we specified for making clean, which uses the rm command to remove driver, all object files, and all of the backup files xemacs creates.
If you always store each of your programming projects in a separate directory, and always supply a Makefile to translate the project, then managing your projects becomes trivial -- you simply cd to the directory where the project resides, enter make, and it will translate your project for you! Then when you are done enter make clean and make will clean up everything except your Makefile and your source files, saving disk space.
The make utility is a very powerful feature, and we have barely scratched the surface of what it can do. For more information about using make, a good book is Mastering Make (2cd Ed.), by Tondo, Nathanson and Yount (Prentice Hall, 1994), which contains information on using it on DOS-based, OS/2 and UNIX systems.
Library, Function, Parameter, Argument, Driver Program.
Hard copies of your final versions of driver.cpp, metric.cpp, metric.h, metric.doc and Makefile, plus a script file showing the execution of driver.
Forward to the Homework Projects