Lab 3a: Functions and Libraries


Introduction

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.

Review

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

Planning a Library

To keep our presentation simple, we will construct our library in our folder for this exercise so begin by creating a project named metric in which to store your work. As its name implies, the library we will create today will provide us with a set of functions to convert English-system measurements into their metric-system counterparts.

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

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

Header File Structure

Begin by creating a project named metric, which will create a folder of the same name. Then save copies of metric.h, metric.cpp, metric.doc, and driver.cpp from your browser into your metric folder.

In your Developer's Studio window, use File -> Open to open and 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
   PrototypeList
where 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.

Function Design

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.

Behavior

   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.

Objects

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 That is, a parameter must be declared for each value that a function receives from its caller.

A precise function specification tell us how to build the function's prototype. To illustrate:

  1. The specification tells us that FeetToMeters() must return a real value to its caller, and so its ReturnType should be double.
  2. The specification also tells us that FeetToMeters() must receive one real value from its caller (the number of feet to be converted), so FeetToMeters() should have one double parameter, which we will call feet.
We can thus complete our header file with this prototype of function FeetToMeters():
   double FeetToMeters(double feet);

After you have added this line to metric.h, copy-and-paste it into metric.doc, below its specification. (Navigate in My Computer to metric.doc, double-click on it, move the cursor below the specification of FeetToMeters(), and then paste.)

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

Operations

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
As we shall see, the C++ parameter mechanism permits a function to automatically receive a value from its caller, and the C++ return statement permits a function to return a value to its caller.

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.

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.
Given an algorithm, we are ready to define FeetToMeters() in our implementation file.

Implementation File Structure

As stated previously, the definitions of library functions are stored in the library's implementation file, so 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"

   DefinitionList
Take 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.)

Defining A Function (Coding)

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. then compile it using Build -> Compile metric.cpp (not Build -> Build metric.exe). Since we did not create metric.cpp as a part of this project, Visual C++ will ask if you wish to add it to the project. Since we do, click "yes". If all is well, the function stub in metric.cpp should compile and generate a single error message:
   ... 'FeetToMeters' : function must return a value
If you get other errors besides this one, 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 only error the compiler generated was that our function stub had no return value. How can this be?

The answer is the idea of separate compilation. Translation of a program is actually a two-step process:

  1. compiling the source file creates an object file (with a .obj suffix, stored in the Debug folder), and
  2. linking the object file to any libraries it uses creates a binary executable file.
The Build -> Compile ... menu-option tells Visual C++ to stop after compiling, instead of continuing on and trying to link.

Once we have a stub, we can "fill it in" using our algorithm and stepwise translation.

1. Receive feet from the caller.

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.

2. Halt the program if feet is negative.

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, or cassert.h depending on how ANSI-compliant your compiler is).

3. Compute meters = feet * 0.3048.

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.

4. Return meters.

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 (again, using Build -> Compile...). This time, you should get no compilation errors. When metric.cpp is free of errors, continue on in the exercise.

To see the difference compilation and linking, choose Build -> Build metric.exe. You should get a linking error message like:

   ... unresolved external symbol _main
which essentially means the linker was unable to find a main function. Since testing FeetToMeters() requires that we call it from a program, let's proceed to writing a program to test our function. But keep in mind the library's structure: 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.

Writing a Program to Test the Library

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.

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

Coding the Driver Program

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, plus the using namespace std; directive.

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:

  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.
So if your program compiles correctly but fails to link, you can infer that there is a mismatch between the parameters in your function's prototype and definition.

Multi-File Translation

Since our program involves more than one file (i.e., driver.cpp and metric.cpp), translating it becomes slightly more complicated. Both of these files must be "in" the project in order for Build -> Build metric.exe to work correctly. The simplest way to add driver.cpp to the project is to choose Build -> Compile driver.cpp. A dialog will appear asking if you want to add driver.cpp to the project -- clicking on "yes" solves our problem.

An alternative approach is to choose

   Project -> Add To Project -> Files...
and then select driver.cpp in the resulting dialog box.

Using either of these approaches, add driver.cpp to the project. Then choose Build -> metric.exe. This will compile any uncompiled files in the project, and then link them into a binary executable named metric.exe.

Testing

Once metric.exe is successfully translated, we are ready to test what we have written, to see if we can discover any logic errors. Execute driver, using the following sample data, to test FeetToMeters() correctness:

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!

More About Build

Before we quit, let's learn a bit more about how Build works. Choose Window -> metric.h and make a trivial change (e.g., add a blank line at the end of the file.) Then choose Build -> metric.exe and watch carefully in the Build window as the translation occurs. You should see three separate actions occur

  1. driver.cpp compiled, producing a binary (but not executable) object file, driver.obj;
  2. metric.cpp compiled, producing a binary (but not executable) object file, metric.obj; and
  3. these two object files are linked, producing a binary executable file, metric.exe.
(the .obj files are stored in the Debugfolder, and are a part of the reason it consumes so much space.)

Now, switch windows back to driver.cpp and alter it 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 actions occur again? If not, which step is omitted?

Windows 95 associates a "last modified date" with each file. To avoid recompiling a file needlessly, the Build command uses these dates to determine what files need to be recompiled. That is, the date of metric.exe was more recent than that of metric.cpp. Build 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.obj. Build noted this, concluded that driver.cpp had been modified, and so re-compiled driver.cpp. This in turn made driver.obj newer than metric.exe. Recognizing this, Build re-linked (the new) driver.obj and (the old) metric.obj to form a new metric.exe.

In general, Build will only re-compile the source files that have been modified since the last translation. To see this, choose Build metric.exe one last time (without modifying anything) and note what actions Build takes.

Execute metric.exe and print a hard copy of its output. Then delete (recycle) the Debug folder, save your work on a floppy, and delete your work from the hard drive of your machine.

Phrases you should now understand:

Library, Function, Parameter, Argument, Driver Program.


Submit:

Hard copies of your final versions of driver.cpp, metric.cpp, metric.h, and metric.doc plus a hard copy showing the execution of driver.exe.


Back to This Lab's Home Page

Back to the Prelab Questions

Forward to the Homework Projects


Copyright 1998 by Joel C. Adams. All rights reserved.