Lab 1: Trying out C

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.

1. Set up

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.

2. Writing a program.

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 doubles, not ints. 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:

sqroot(1.000000) = 1.000305
sqroot(2.000000) = 1.414216
sqroot(3.000000) = 1.732143
sqroot(4.000000) = 2.000000
sqroot(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.

3. Writing functions.

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 sqroot(), using factor2.c as a model. (Don't call your function sqrt() because the math library already has a function with that name.) For example, your main function should contain a line something like

       printf("sqroot(%f) = %f\n", number, sqroot(number));

Test until the program is running correctly.

4. Writing libraries.

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

5. Writing makefiles

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.

6. Developing a project

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.

7. To turn in

Make a typescript showing all the files you edited and their output.


Thomas VanDrunen
Last modified: Mon Aug 27 12:22:36 CDT 2012