Library Mechanics
Functions are named "chunks" of code that calculate a value or that carry out an action. I think of them like the "Magic 8-ball"; you ask a question, and get an answer, never knowing how it is accomplished.
In C++, a function associates a computation—specified by a block of code that forms the body of the function—with a particular name. If a function calculates a value, what we call a fruitful function, then it may be used in an expression; if it carries out an action (called a void function in C++ or procedure in other languages), it cannot.
Using functions reduces bugs and make maintenance more effective by allowing you to reuse proven code, instead of duplicating it. Placing related functions into a library, allows you to reuse them in many different contexts.
Organization
Programs using functions can be organized in several different ways. The question is, "Where do definitions and declarations go?"
- You may define your functions before calling them. If you have only one or two functions in a "throw-away" program, this is fine. Because your functions need to appear in a particular order, though, your code is often harder to understand and maintain. In general, you will not to do this.
-
You can prototype (or declare) your functions at the top of your
file (usually under the library
#include statements) and then define
the functions later in the file, usually after the main
function. This makes it easier to read and understand your program, because the
primary logic appears before the function details.
It also makes your code much easier to maintain, because you can then define your functions in any order you like. Do this for small programs and for functions that are unique to a particular program.
- If you have functions which you want to reuse in different programs, you should place those function in a library, (a collection of similar functions) and place the prototype into a header file. That is what you'll generally do in this class for all of your functions from now on.
In the next Lesson, you will learn to use separate compilation for your programs.
Separate Compilation
To define a library in C++, you'll supply four parts:
- the client or test program, which uses the functions in the library.
- the interface, which provides information needed to use the library. The interface consists of declarations or prototypes, and will go into a header file.
- the implementation, which provides the details. The implementation consists of function definitions.
- the makefile which puts the parts together to produce the executable.
To try this out, we'll write a library containing three functions:
-
int firstDigit(int n)
, returning the first digit of its argument -
int lastDigit(int n)
, returning the last digit of its argument -
int numDigits(int n)
returning the number of digits in its argument
Calling firstDigit(1729)
will return 1; calling
lastDigit(1729)
will return 9;
calling numDigits(1729)
returns 4.
The Client or Test Program
Open your CodeSpace IDE, (or, you can do this in Replit if you like), and create a folder. Add a file named client.cpp. Add the usual #include statements and an empty main(). In the main() function you need to:
- Call each of your functions with some known input.
- Compare the value returned (this is called theactual value), with the value thatshould have been returned (this is called the expected value).
- Print a message indicating whether you got it right or wrong.
Here's some code, which calls each function, compares it to the expected value, and then uses the conditional operator to print PASS or FAIL. Note that each expression needs to be enclosed in parentheses:
cout << "Testing digits library" << endl;
cout << ((firstDigit(1729) == 1) ? "PASS" : "FAIL") << endl;
cout << ((lastDigit(1729) == 9) ? "PASS" : "FAIL") << endl;
cout << ((numDigits(1729) == 4) ? "PASS" : "FAIL") << endl;
When you compile with make client, you'll get the error message:
An undeclared error message is a compiler syntax error whic means you are missing a prototype or you are calling a function incorrectly.
Basically, the compiler is saying "I don't know what the word firstDigit means. It's up to us to tell it what that means. Which leads us to the interface file.
The Interface or Header File
A library may contain several definitions: functions, types, and constants. In C++, the interface and implementation are in two files: a header (or interface) file and an implementation file. The interface file usually ends with the extension .h.
Add #include "digits.h" in your client file right after the using namespace std line. Then, create and open digits.h and let's look at header guards.
Preprocessor Header Guards
It is possible for one header file to include another. You must do something to make sure that the compiler doesn’t include the same interface twice. You do that by adding three lines to every header file that are known as the interface boilerplate, or header guards. They look like this:
#ifndef FILE_IDENTIFIER
#define FILE_IDENTIFIER
// Entire contents of the header file
#endif
This pattern appears in every interface. These are instructions to the preprocessor, a program that examines and modifies your code before it is sent to the compiler. The boilerplate consists of the #ifndef and #define at the beginning and the #endif at the end.
- The #ifndef preprocessor directive checks whether the FILE_IDENTIFIER symbol has been defined in the current translation unit. When the preprocessor reads this interface file for the first time, the answer is no.
- The next line defines the symbol, using #define. Thus, if asked later to #include the same interface, the FILE_IDENTIFIER will already be defined, and the preprocessor will skip over the contents of the interface this time around, not including them a second time.
A common convention to create FILE_IDENTIFIER is to simply capitalize the name of the file itself, replacing the dot with an underscore. You may use another convention if you like, but make sure that the name will be unique when you build your project.
Go ahead and add the header guards to digits.h now.
Adding the Prototypes
When the compiler encounters a function call in your program, it needs information in order to generate the correct code; the compiler doesn’t need to know how the function is implemented, but it does need to know:
- what types each of the arguments to the function are (and how many)
- what type of value the function returns
That information is provided by a prototype, or function declaration (as opposed to a function definition).
#ifndef DIGITS_H
#define DIGITS_H
int firstDigit(int);
int lastDigit(int);
int numDigits(int);
#endif
These prototypes associate the names firstDigit, lastDigit, and numDigits each with a function that takes a single int as an argument and which returns an int as its result. These are function declarations. Go ahead and complete the prototypes now.
In a prototype, parameter names are optional. The compiler doesn't care about the names, but they help you remember which parameter matches which argument.
double focalLength(double d, rouble r1, double r2, double n);
Supplying names in a prototype often helps the reader. The parameter names in a prototype are in prototype scope; they have no meaning after the prototype ends, and, specifically, they do not need to match the names used in the definition.
Library Types in Interfaces
If the prototype includes any types from the standard library (such as string or vector), then you must #include the correct header, and fully qualify the name of the type. Here's an example:
// Header file
std::string zipZap(const std::string& str);
// Implementation file
string zipZap(const string& str) { . . . }
Header files should never use identifiers from the standard library without explicitly including the std:: qualifier. In the implementation file, you may use the name as is, because your implementation file will contain a using declaration or directive.
Here are three rules to remember.
- Never add using namespace std to a header file. Header files are #included in other files; doing so changes the environment of that file.
- Always add std:: in front of every library type, such as std::string, but never in front of primitive types like double.
- For all library types, #include the appropriate header file inside of your header file. If you use the std::string type, you must #include <string> Note that when including standard libraries, you enlose them in angle brackets (<>), while your header files use double quotes when included.
Linker Errors
Once you have finished prototyping all three functions, build your project again by by typing make client. When you do, you won't see any compiler error messages; the client program compiles. However, you will see some linker error messages. Your function was declared correctly, but the definition could not be found at linking time.
client.cpp:(.text+0x32): undefined reference to `firstDigit(int)'
If you still see undeclared (instead of undefined), make sure you have added the line #include "digit.h" to the top of the client program.
Two words to note in your compiler's error messages: undefined and undeclared. Recognizing these will help you locate and fix the problem.
- An undeclared error message is a compiler syntax error. It means you are missing a prototype or you are calling a function incorrectly.
- An undefined error message comes from the linker and means that you are missing the definition for a function.
The Implementation File
Place your definitions in an implementation file. The implementation file has the same root or base name as the header file, but a different extension. You'll use .cpp in this class but other conventions include .cxx, .cc, and .C (a capital 'c').
- Create an implementation file using the extension .cpp.
- Add a file comment to the top of the file.
- #include the interface file for the library you are implementing.
- If you use other libraries, such as string, you should include them as well. This example does not.
- Add a using directive if you are using any library types. This library does not.
- Copy the prototypes from each of the functions in the header file into the implementation file. You don't need to bring across the documentation. Make sure, also, that you don't copy the header guards.
- Stub out each of the functions.
This is purely mechanical. You want to practice it until it becomes second nature. You should memorize this part, so you don't have to expend any brain cells to complete it.
Stubbing the Implementation
Always start by writing a "skeleton" or stub for your function. Make sure your code starts out syntactically correct, and then stays that way.
- Copy the prototype or declaration into the implementation file. You don't need to bring the documentation with the prototype, but you may.
- Remove the semicolon at the end of the prototype and add some braces to supply a body for the function.
- Unless your function is a procedure (void function), you must create a return variable to hold the result. Look at the function return type to decide what type to make this variable. Initialize it to the "empty" value.
- Add a return statement at the very end of your function.
Warning. Make sure your stubs always include a return statement of the correct type, or your function may crash at runtime.
Here's a stubbed-out version of one function:
#include "digits.h"
int firstdigit(int n)
{
int result;
return result;
}
Once you've stubbed out the two other functions, you'd expect your program to compile and link, but it does not. You get the same linker errors that appeared earlier.
Why? Since you now have two, separately compiled portions of object code, you have to tell the compiler how to link them together. You do that with a makefile.
The Make File
For the homework your instructor provides a project or make file. For this lesson, though, you'll get to build one on your own. Create a new file named makefile. It has no extension. Then, add the following code:
EXE=digit-tester
OBJS=client.o digits.o
$(EXE): $(OBJS)
$(CXX) $(CXXFLAGS) $(OBJS) -o $(EXE)
run: $(EXE)
./$(EXE)
Here are what these three sections mean:
- Macros: these are like variables that can be expanded later. EXE is the name of the executable, and OBJS contains the names of the object files. Since you have two .cpp files in your project, you'll have two object files.
- Both of the next two sections contain targets, dependencies and actions.
- Line 4 expands the macros EXE and OBJS, producing a line that says: digit-tester : client.o digits.o
- Interpret this line as meaning : "to build digit-tester, make sure that client.o and digits.o are both up to date."
- digit-tester is a target (what we are trying to build), while client.o and digits.o are dependencies
- Line 5 is the action to perform to produce digit-tester. Each action line must start with a tab character, not spaces. Your editor may do this already, but if not, you'll need to configure it. Line 5 expands a few other macros meaning "run the compiler with these object files and produce t his executable".
- Line 7 is called a pseudo target. When you type make run, the action line is executed. If you just type make, only the first target is built.
Once you've saved your make file, type make and the linker errors should disappear. If they don't then go over the previous steps (or reach out on the Discussion Board). Type make run, and the client program should run (even if you have some warnings about unused parameters).
Even though all of your tests fail, that's OK. The purpose of the stub is to get the mechanical details out of the way, so that you can use all of your brainpower to concentrate on solving the problems.