As you likely found out the last time you purchased your last textbook, books can be quite expensive. One way you could save yourself some money would be to share the book with someone else. In fact, the more people you can share your book with, the more you can save.
A library (a.k.a. a library module or simply a module) is a generalization on this idea of sharing. When a community of people pool their resources, then they can buy and share a centralized collection of books. By cooperating this way, everyone has access to a greater set of books than they could afford individually.
In the digital world, sharing libraries is even easier (much to the consternation of the mass media companies). While a physical book can be actively used by only one person at a time, a digital file can be shared by many people all at the same time. More particular to our situation, we can write code once and share it among many other programmers.
In fact, we've already been sharing code. More accurately, we've be
taking advantage of the code others have written. For
example, in all of the programs we've seen so far, we've used the
iostream
library. This library defined cout
and
cin
as well as the input and output operators. There's
actually a lot in that library, and we certainly don't want to write
all of that code for every program we write. By reusing the library,
all C++ programmers saves themselves enormous amounts of time.
As we'll see in this lab, there's nothing magical about a library, You'll see all the basic tools for creating your own library. The main difference between the libraries you write and the standard libraries of C++ is size, but that's mostly because these standard libraries have had so many people and so much time spent on them.
We'll write our own library in this lab.
Let us briefly review what we know about function libraries. We have
seen that C++ provides a variety of function libraries, including
iostream
that provides input and ouput, 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>This includes basic definitions of the library into your program.
To call a function,
you must specify its name and a list of arguments in parentheses. For example, the cmath
library contains a function named pow
which computes
the exponential power of a number. If we have variables x
and y
(which should be double
s), we can call the
function like so:
pow(x, y)
Question #4.1: In the function call above, what is the name of the function being called? What are the arguments in the function call?
The code associated with the function is executed, and the result is
returned, x
to the y
th power. Since this is an
expression, you will undoubtedly put it into a context:
double result = pow(x, y);or
cout << "The answer is " << pow(x,y) << ".\n";
The library we create 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. Here are just some of the conversion we could do:
English Unit | Metric Unit | Conversion Equation |
---|---|---|
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 is to write two functions to convert feet into meters and meters into feet. By
storing these function in our library metric
, it can be
shared by any and all programs that need to convert between feet and
meters, allowing all of us to avoid "redefining the wheel."
You may notice that you've already been provided with the feet-to-meters conversion. We'll walk through this example, and you'll be in charge of writing the meters-to-feet conversion.
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 in the library, without specifying their details. In contrast, the role of the implementation file is to provide complete definitions of the library's functions. You'll notice some redundancy in these two files, but this is unavoidable.
Helpful hint: Remember to check both your header file and implementation file when something goes wrong with something in your library.
Why all of these files? The reason has to do with program maintenance. If we are writing a library, then we expect that many programs will use of it. It is often the case that even a well-designed library may need to be updated if a better way is discovered to perform one of its functions or even if a function needs to be fixed. If we have designed our functions carefully, then updating a library function should simply involve altering its definition which is in the implementation file. We may be able to leave the header file alone which would save many people from having to recompile their programs.
Directory: lab4
metric.h
, metric.cpp
, and
metric.doc
implement a metric conversion library.driver.cpp
implements a driver for the metric
conversion library.Makefile
is a makefile.gcc
users need a makefile; all others
should create a project and add all of the .cpp
files to
it.
Add your name, date, and purpose to the opening documentation of the code and documentation files; if you're modifying and adding to the code written by someone else, add your data as part of the file's modification history.
Edit metric.h
, the header file of our library. For
convenience, we base the name of this file after our library. Common
convention tacks a .h
to the end of the filename to designate
this as the header file.
Personalize the opening comment to the modification section of the library.
A simplified general pattern for a library header file is as follows:
OpeningDocumentation
PrototypeList
The opening documentation is a comment describing the purpose of the library, who wrote it, and when it was last updated. The prototype list is a sequence of prototypes; we have to learn about designing functions to learn about prototypes.
Nothing beats a good design. So we'll use OCD to design our functions.
First, we describe the behavior of our feet-to-meters 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 then 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.
This distinction is very important. Overwhelmingly, most
functions do not use cin
or cout
. We won't
need them in our functions.
We also want to try and anticipate what could go wrong. The function will only produce a correct result if the value it receives is not negative; this is a precondition since it's a condition that must be true before the function can be executed.
Now let's consider our other function, converting meters to feet. This function won't be much different than the feet-to-meters function. So, the basic structure for the behavior should be the same. The values we receive and return will be different, and the computation is different (division instead of multiplication), but still pretty much the same.
Question #4.2: Write the behavior paragraph for the meters-to-feet function.
We list the objects of a function in the same way that we do for a program, except we also want to describe their movement. For each object, 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 |
The movement column of this chart summarizes what data is coming in and going out very nicely. This allows us to write our specification which also includes our precondition:
Specification:receive:feet
, the number of feet to be converted.
precondition:feet
is not negative.
return:meters
, the equivalent of feet in meters.
Keep in mind that none of this information here is magical. The behavior paragraph is used to create the object chart. The nouns in the behavior are our objects, and the behavior explains the type, kind, and movement of the objects. The behavior even suggests the names of our objects.
Once we have the object chart, we can come up with the specification, looking for the objects moving in and out of the function. The behavior paragraph establishes our preconditions.
Question #4.3: Using your behavior description, come up with an object chart for the meters-to-feet function.
Question #4.4: Using your behavior description and object chart, come up with a specification for the meters-to-feet function.
The specification of a function provides us all we need to write a prototype. A prototype is the way we write the specification for our library. We'll need a prototype for every function we write.
The simplified pattern for a function prototype is
whereReturnType
FunctionName
(ParameterList
) ;
ReturnType
is the type of value the function
returns. It's any data type like int
or double
.FunctionName
is the name of the function. You get
to choose this.ParameterList
is a list of declarations of
parameters.Each value that our function receives must be declared as a parameter. A parameter is really a variable just like the ones we've already declared and used. The differences are subtle: a parameter is declared as part of the function (not inside it) and each parameter represents a value that the function receives.
With our specification and some imagination, we can come up with the components of our prototype:
double
.feetToMeters()
.double
that we'll call feet
.double feetToMeters(double feet);
You will see this prototype in both the header file (metric.h
) and the documentation file (metric.doc
). C++
requires the prototype in the header file so that our program (and
others) can see it. We document it in the documentation file so that
others know how to use our function; that's why you'll see the
specification just above it.
Question #4.5: Write a prototype for the meters-to-feet function.
Add your prototype to both the header file and the documentation file. Also, add a comment containing the specification for the prototype in the documentation file.
Since parameters are like variables, their names begin with a lowercase letter, with every word after the first capitalized. The same convention is used for naming function. These are common conventions, although not universal; they are the convention we'll use.
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.
Continuing with our design, our list of needed operations is quite short:
Description | Predefined? | Name | Library |
---|---|---|---|
get a value from the caller | yes | feet | built-in |
check that the value is not negative | yes | >= 0 | built-in |
halt the program if the value is negative | yes | assert() | cassert |
multiply two real values | yes | * | built-in |
return a value to the caller | yes | return | built-in |
feet
value. We'll see more when we write our
function. We'll also see the return mechanism in action. The key
now is that all of these operations are built into C++, and we'll
definitely make use of them.
The relational operators and the assert()
function will all
help us in handling our preconditions.
Question #4.6: Write down the operation chart for the meters-to-feet function.
We can then organize these operations and objects into the following algorithm:
feet
from the caller.feet
is negative.meters
= feet
* 0.3048.meters
.Question #4.7: Write an algorithm for the meters-to-feet function.
We store declarations in the header file. That's all a function prototype is: a declaration. It declares the function's name, it's parameters, and return type. But we haven't written any code for the function to execute!
That's what the implementation file is for. In an implementation
file, we put the definitions of our library functions. This will include another
declaration of the function plus the code for the function.
Our implementation file are often named the same as our header file,
excepting ending in .cpp
.
The general pattern for an implementation file is as follows:
OpeningDocumentation
Includes
DefinitionList
Take a moment to personalize the opening documentation in metric.cpp
if you haven't already.
While it's not needed for every library, it's a good habit to include
our library's header file in the implementation file. However, we
have to use a variation of the #include
directive. We've
normally used this form:
#include <iosteam>for accessing system libraries. A personal library that we have in our working directory uses different punctuation:
#include "metric.h"
Go ahead and add this line to the implementation file.
The general pattern for a function definition is
whereReturnType
FunctionName
(ParameterList
) {StatementList
}
ReturnType
, FunctionName
, and ParameterList
are exactly the same as in a function prototype.StatementList
is a sequence of valid C++ statements.A function stub is a
minimal function definition. For example, this is the function stub
for feetToMeters()
:
double feetToMeters (double feet) { }Why bother with a function stub? We can compile the program with a function stub. It gives us a great place to stop to check our program to make sure that at least the compiler likes what we've done.
Of course, we eventually fill the stub with code so that the function
does something. That's already been done for feetToMeters()
. Nothing's been done yet for the meters-to-feet
function.
Start by writing a function stub for the meters-to-feet function, and
add it to the implementation file. Compile the program. You should
be able to compile the program without errors. You may get a warning
that your function does not return anything, but we'll let that slide.
Also, you only have to compile the metric.cpp
file itself;
you do not have to link it with the driver yet. You can, but
you may get errors that you should ignore.
Now we can actually write code for our function using our algorithm.
feet
from the caller.This step is taken care of for us by the C++ function-call mechanism. We've already done it! The mere act of declaring a parameter tells C++ that we want to receive this value. That's the whole purpose for parameters.
It's good to put this step in your algorithm to remind you where the data comes from, but once you have the parameters declared, the function has already received the values from whatever called it.
feet
is negative.This is our precondition. We decided that we'd do this with the
assert()
statement. That's exactly what's been done in
feetToMeters()
.
meters
= feet
* 0.3048.This step has been implemented in one declaration. Remember that we
have to declare all of our variables. The feet
variable
has already been declared as a parameter---that counts! However,
this is the first time we've used meters
, so we declare it
and initialize it with our computation.
The third operation can be performed using the C++ return statement. Its general pattern is:
return Expression
;
where Expression
is any valid C++ expression. The result
of the expression is sent back to whatever code called this function.
Now it's your turn. Use your algorithm for the meters-to-feet
function plus the feetToMeters()
example to implement your
algorithm in the stub that you wrote earlier. Compile the code as
you go along (probably after you write each line of code); be sure it
compiles without errors or warnings when you finish.
Next, open driver.cpp
and personalize its documentation.
There's a program in this file to test the functions we've written. 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:
cin
.cout
the result of calling the function plus a
descriptive label.You've been given the code for the driver for this lab; well, most of the code. You'll have to write the drivers for future labs, so make sure you read over this code carefully and that you understand what it does.
Step 1 of the algorithm is merely an output statement. You've written a few of these already.
Step 2 is a matter of reading in data. This take a little more thought: you need to know what objects you need to read in and their data types. Declare variables for these objects, and then read in the data.
Step 3 will usually consist of a variable declaration with an initialization that calls the function we're testing.
Step 4 is just an output statement that displays a label describing the result and displays the result itself (otherwise, what's the point!).
Now, before we can use the library, we must include the header file
in the driver file. Notice the saving we're already getting. We
write the header file once, but we're using it twice.
And this is just in one program! Be sure to use the #include "..."
form to include a header file from your
personal library.
Let's look more closely at Step 3 of our driver algorithm.
At the beginning of this lab, we quickly looked at the pow()
function. If we had two variables x
and y
, we can
call the function like so: pow(x,y)
. Put in a context, we
can then use the value that this returns.
But we got the pow()
function from a system library. What do
we do with a function that we write ourselves? Exactly the same
thing.
Consider feetToMeters()
. It receives a number of feet. If
we want to convert 1.0 foot to meters, we could write this:
feetToMeters(1.0)(For now, don't worry about the context, what we do with the result). If we want to convert 2.0 feet to meters, we could write this:
feetToMeters(2.0)If we want to convert
x
raised to the y
th power feet
to meters, we could write:
feetToMeters(pow(x,y))In the case of our driver, though, the driver reads in a value, places it in
feet
, and we want to convert that value:
feetToMeters(feet)The driver uses this as the expression to initialize the
meters
variable.
Our approach to the function has changed. When we declared the
function, we declared a parameter---a variable---to receive the
number of feet. But now that we're calling the function, we can pass
in a literal, a variable, or any double
expression. A value
that we pass into a function is known as an argument.
Helpful hint: A parameter is always a variable, it is part of a function definition, and we get to choose the name. An argument can be any expression, and it is part of a function call.
There are three important rules to remember when calling a function:
Compile and link all of your code, and test out your program.
As it stands, though, the program only tests feetToMeters()
.
It should also test metersToFeet()
. There are several ways to
do this:
feetToMeters()
to test the
meters-to-feet function. This leads to a lot of extra code, but
not nearly as much as the first option.Let's try the third option here. So instead of reading in a number
of feet, let's read in a generic number. Let's call it measurement
. We can then use this one value as an argument to both functions. Its meaning in these functions will be different, a
number of feet in the one and a number of meters in the other, but
that's fine. Here's what the code would look like to test feetToMeters()
:
// Step 2. double measurement; cin >> measurement; // Step 3. double meters = feetToMeters (measurement); // Step 4. cout << measurement << " feet == " << meters << " meters\n";
Now all you have to do is use this code in the driver, and then duplicate Steps 3 and 4 for the meters-to-feet function.
Once the program is successfully translated, we are ready to test
what we have written, to see if we can discover any logic errors.
Execute the program, and use the following sample data to test feetToMeters()
correctness:
Feet | Meters |
---|---|
1.0 | 0.3048 |
3.3 | 1.00584 |
-5.9 | ERROR |
feetToMeters()
is a linear function, two test values
are sufficient to verify its correctness. Be sure to note and
fix any discrepancies.
The third test value tests our precondition. This is also a good idea to make sure that the function fails when it's supposed to.
Question #4.8: What error message does the program give when you enter the invalid value?
Since you've been given feetToMeters()
, it should run
correctly. Your meters-to-feet function may not. The sample data
above can also be used to test your meters-to-feet function.
Question #4.9: What sample data are you going to use and expect for your meters-to-feet function.
Debug your program until you get the correct results from both functions.
After you release your library, you might get some complaints about
the error message the functions give when they fail. The error
message is far too cryptic. An assert()
statement is not
always the best way to react to problems; alternatively, we can code
our own precondition handling.
Let's consider feetToMeters()
. We came up with its
precondition almost from the beginning, and we have an expression for
it: feet >= 0
. We chose to use the assert()
statement to test the precondition; this is not the only choice we
have.
Think about what we want to happen: if the precondition is false
(i.e., feet
is negative), then we want to display a
helpful error message and stop the program.
This new behavior, introduces one new object: cerr
. Similar
to cout
, cerr
is of type ostream
, and will
display its output to the screen. However, cout
should be
used for normal output with the user while cerr
should be use for error messages. You use cerr
just the same way you use cout
.
We have four operations here:
Description | Predefined? | Name | Library |
---|---|---|---|
feet is negative | yes | < | built-in |
if...then... | yes | if statement | built-in |
display error message | yes | << | iostream |
stop the program | yes | exit() | cstdlib |
The exit()
function, when executed, will stop the program
immediately. The function takes one argument, an int
. The
actual value of the argument is really up to you; convention is that
any non-zero value means the program stopped because of a problem.
(Yes, a zero value does mean that the program stopped normally.) You
could be very sophisticated with your exit codes, but we'll keep it
simple. So, use 1
or -1
.
Using all of this information, we can replace Step 2 of the algorithm
for feetToMeters()
:
feet
is negative, then...
What about that "if" operation? This is a language feature in C++
that we haven't seen yet, but it is built in to C++. The form for a
simple if
statement looks like this:
if (BooleanExpression
) {Statements
}
So, in the code, we can replace the call to assert()
with this code:
if (feet < 0) { cerr << "The number of feet is negative in feetToMeters()." << endl; exit (-1); }That's a much more informative message; the people who use the library will be much happier.
Make this change to feetToMeters()
. Compile and test your
program, especially the failure test.
Once you have this working for feetToMeters()
, make similar
changes to your meters-to-feet function. The new behavior, objects,
operations, and algorithm should parallel the new design for FeetToMeteres()
as should the new code.
You might find it a bit annoying to have to run the driver three
times for the three test values we have above. It might be easier if
the program ran through three tests. When we want something done
more than once, we turn to loops. And
when we do any counting, we turn to a counting for
loop.
The general form of a counting for
loop looks like this:
for (There's a fair amount of information that we need to provide for a countingType
loopVar
=firstValue
;loopVar
<=lastValue
;loopVar
++) {Statements
}
for
loop:
Type
will nearly always be int
; it's the data
type of loopVar
.loopVar
is the counting variable. It is initialized to
firstValue
, is incremented each time through the loop
(with loopVar
++
), and stops when loopVar
is greater than lastValue
.Statements
are C++ statements which will be executed
once for each value of loopVar
, firstValue
through lastvalue
.This sounds like it could work for our driver. Here's a new algorithm:
cin
.cout
the result of calling the
function plus a descriptive label.The "repeat" action can be implemented with a loop:
for (int i = 1; i <= 3; i++) { ... }The question is, what goes in the body of the function, replacing the
...
? According to the new algorithm, it's the same code we
had before!
Add this counting for
loop to your program, making sure the
old steps of the algorithm are all put between the curly braces of
the loop. Also make sure the counting for
loop is inside the
curly braces of main()
.
Now that our code is getting a little more complex, we should address the issue of indentation:
Helpful hint: You will find it much easier to debug your program if you indent your code properly.
You may also find yourself losing a lot of points on your assignments if you don't indent properly.
Convention suggests that every time you have an opening curly brace, you should indent a little to the right (two to four spaces). We've already done this with our functions; you should also do this with your loops and selection statements.
Compile and execute your program. Make sure it works correctly.
Finally, here's something to think about: how could you get this loop to execute five times? Fifty times? Even better: how could you let the user choose how many times to execute the loop? You have the tools to do this, but we've already done quite a bit in this lab.
Turn in a copy of all of your code, the answers to the question, and a sample run of your program proving that the functions work properly.
for
loo, declaration, definition, documentation file, driver
program, function, function call, function stub, header file, if
statment, implementation file, interface, library, library module, loop, module, parameter, precondition, prototype, return statement