Lab 4: Functions and Libraries


Introduction

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.

Review

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 doubles), 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 yth 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";

Planning a Library

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.

Library Structure

A library consists three separate files:

The documentation file is not strictly necessary, any more than are comments within a program; however it is good style to somehow provide such a file, and store within it all of the information needed to use the library. Our custom is to place function specifications in this file.

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.

Files

Directory: lab4

Create the specified directory, and copy the files above into the new directory. Only 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.

Header File Structure

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.

Function Design

Nothing beats a good design. So we'll use OCD to design our functions.

Behavior

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.

Objects

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

ReturnType FunctionName (ParameterList) ;
where

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:

Now we can assemble these components into our prototype:
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.

Operations

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
We've already partially seen the parameter mechanism that allows us to get the 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.

Algorithm

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.
Our behavior description and the operations lead directly to this algorithm.

Question #4.7: Write an algorithm for the meters-to-feet function.

Implementation File Structure

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.

Defining A Function

The general pattern for a function definition is

ReturnType FunctionName (ParameterList) { StatementList }
where The first bullet point here is very important. Once you've written your prototype, you can copy this line over to the implementation file for the beginning of the function definition. Just be sure to remove the semicolon at the end. A prototype requires that punctuation, but it will cause an error in a function definition.

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.

1. Receive 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.

2. Halt the program if 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().

3. Compute 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.

4. Return meters.

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.

Writing a Program to Test the Library

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.

Designing The Driver Program

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. Call the function that you're testing using the input values as arguments.
  4. Display via cout the result of calling the function plus a descriptive label.
Since every driver program uses this same basic algorithm, we're going to jump to the coding stage of our development.

Coding the Driver Program

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.

Calling a Function

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 yth 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:

  1. The number of arguments in a function call must equal the number of parameters in the function's prototype, or a compilation error will occur.
  2. The number of arguments in a function call must equal the number of parameters in the function's definition, or a linking error will occur.
  3. The types of the first argument and first parameter, the second argument and second parameter, the third argument and third parameter, etc. must be the same, or a compilation error will occur.
If you get any complaints about your function, make sure you check the function call, the function prototype, and the function definition.

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:

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.

Testing

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
Since 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.

Maintenance

Better Error Messages

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():

  1. If feet is negative, then...
    1. Display a helpful error message.
    2. Exit the program.

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.

Repeated Tests

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 (Type loopVar = firstValue; loopVar <= lastValue; loopVar++)
{
  Statements
}
There's a fair amount of information that we need to provide for a counting for loop:

This sounds like it could work for our driver. Here's a new algorithm:

  1. Repeat three times:
    1. Display a prompt for whatever values the function requires as arguments.
    2. Read those values from cin.
    3. Call the functions that you're testing using the input values as arguments.
    4. Display via 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.

Submit

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.

Terminology

argument, argument, counting 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
Lab Home Page | Prelab Questions | Homework Projects
© 2003 by Prentice Hall. All rights reserved.
Report all errors to Jeremy D. Frens.