The goal of this lab is for you to become familiar with programming in C. This will also introduce the use of functions, libraries, and makefiles. This lab will walk you through the development of a C program.
Make a new directory for this lab, move into it, and copy the code for this lab from the class directory.
mkdir lab1 cd lab1 cp -r ~tvandrun/Public/cs245/lab1/* .
There are two directories. One contains code for you to inspect. The other is empty---write your code in that directory.
Recall factor1.c
from class.
Open this file in xemacs (or an editor of your choice) and
review the structure.
Now, open a new file tryingoutc1.c
.
Using factor1
as a model, write a program that
calculates the square root of the numbers from 1 to 50.
An algorithm for calculating square roots is first to make a guess
(any number between 0 and the radicand will do, such as half
the radicand), and then repeatedly improve the guess using the
formula below until the square of the guess is "close enough"
to the radicand---let's say, within .001.
If x is our current guess and c is the
number we want to find the square root of (ie, the radicand),
then the following function finds an improved guess:
Notice that you need to be working with double
s,
not int
s.
You may want to use absolute values to check if the guess
is close enough to the real value.
C's absolute value function is fabs()
;
you need to #include<math.h>
to use it.
Write your program so that the output looks something like this:
sqrt(1.000000) = 1.000305 sqrt(2.000000) = 1.414216 sqrt(3.000000) = 1.732143 sqrt(4.000000) = 2.000000 sqrt(5.000000) = 2.236111 ...
After you have written it, recall that you compile C programs with
gcc tryingoutc1.c
This will produce an executable file called a.out
.
Alternately you can specify the name of an executable file
(eg, tryingoutc1
)
using the -o
flag.
Test your program and fix any problems until it works.
Open the file factor2.c
.
In C, a piece of code can be packaged up and reused
in other contexts by making it a function,
the equivalent to a method in Java
(though C functions are specifically like
static methods, not instance methods, in Java).
In C, user-defined modules like functions must be declared before they are used. However, C also distinguishes between defining a function and declaring its prototype, similar to a signature in Java.
Inspect factor2.c
to see how the algorithm
for finding prime factors of a number is encapsulated
in a function.
In that file, the prototype
of the function appears before the main function and a definition
for the function appears at the end of the file.
Copy the contents of tryingoutc1.c
into a new file, tryingoutc2.c
and then modify that file by moving the algorithm for computing the
square root into a function sqrt()
,
using factor2.c
as a model.
For example, your main function should contain a line something like
printf("sqrt(%f) = %f\n", number, sqrt(number));
Test until the program is running correctly.
Open the files factor3.c
,
factor3lib.h
,
and factor3lib.c
.
In order to make code reusable across programs (not just within
one program), we move code like functions into separate
files called libraries to be compiled separately.
A library actually consists in two files.
The header file contains the function prototypes
and the implementation file contains the definitions.
Inspect the header file factor3lib.h
and the implementation file factor3lib.c
.
One way to think of library files is that they do
not have main functions.
The file factor3.c
is our new version
of the program.
Notice the line #include "factor3lib.h"
.
This instructs the compiler to paste everything from
factor3lib.h
into this file.
In this way the function prototypes are declared in the
file that is using them.
Compilation takes three steps. First, comiple the library's implementation.
gcc -c factor3lib.c
The -c
flag means "compile only"---that is,
generate the machine code for the C code in the file but
do not generate an executable file.
If you leave off the -c
flag, the compiler will
complain that there is no main
function.
Next compile the main program, also with the -c
flag.
gcc -c factor3.c
The reason we do not yet want to make an executable file for this
one is that in order to make a runable program we will need
to paste together the compiled code for this and the library---that
will be the next step.
But first, look at the contents of the directory using ls
.
The .o
files are called object files,
and they contain the compiled (but not executable) version of the code.
(The word object in "object file" is of historical use
and has nothing to do with how the word object is used
in object-oriented programming.)
To link the object code together into an
executable file, we use gcc
in the following way:
gcc factor3lib.o factor3.o -o factor3
This now produces the executable, factor3
.
Rewrite your code from tryingoutc2.c
,
separating the parts into three files,
tryingoutc3.c
, tryingoutc3lib.h
,
and tryingoutc3lib.c
,
in imitation of the way we structured code in factor3.c
and related files.
Compile and test.
The advantage of C's compilation model is that only the code that has changed (and code dependent on code that has changed) needs to be recompiled. For example, if the file with the main function is changed as you develop the project, there is no need to recompile the implementation file of the library. Only the steps of recompiling the main file and linking need to be done. Likewise if only the implementation file of the library changes, then we need only to recompile that file and re-link. However, if the header file changes, then everything needs to be recompiled because both the implementation of the library and the file with the main function depend on the prototypes of the functions.
In a large project where you are dealing with many libraries,
this set up can reduce compilation time.
However, it is very difficult to keep track of dependencies among
the different pieces of code, plus you certainly do not
want to issue compilation comands all the time.
This is something that needs to be automated.
Building a software project from source can be automated using
the make
command.
make
reads information about
dependencies among files from makefiles.
Open the file makefile
.
It contains a sequence of rules, each in the form
target: prerequisites commands
The target is usually the name of a file that needs to be made. The prerequisites are names of files used to determine if the target needs to be made---if any of these files have changed or should be rebuilt since last time the target was made, then this rule needs to be executed to update the target. The commands are the things to be executed to build the target.
If the prerequisites themselves are targets, then make
will test if those targets need to be made first.
Targets also can be phony, meaning they
aren't files that need to be made.
For example, some makefiles contain a phoney target called
clean
that has no prerequisites
and has commands to remove old files.
See what makefiles do first by removing the object and executable files.
rm *.o rm factor3
Now run make
.
make
Now write your own makefile for your tryingoutc3
project.
Test.
Finally, add the following features to your tryingoutc3
Use your makefile to compile each step (and test before you do the next one).
Add the functions to the library and appropriate testing code in the
main function.
gcd()
that computes the
greatest common divisor of two integers.
Use the following facts to form an algorithm for computing the gcd:
%
b.
lcm()
that
computes the least common multiple of two integers.
(Hint: use your gcd function.)
You can test this in the same loops as gcd.
Make a typescript showing all the files you edited and their output.