70 Intermediate C Programming
$ make -f name
In this case, the make program uses name as the input, instead of Makefile. Most people
just use Makefile because it is the default, and everyone understands what the file is for.
The following Makefile includes the dependence of main.o and main.c.
aredis tinct . o: aredist inct . c1
gcc -g - Wall - Wshadow -c aredi stinct .c2
3
main . o: main .c4
gcc -g - Wall - Wshadow -c main . c5
6
# This is a comment7
If a line is blank (e.g., line 3), it is discarded by the make command. In Makefile,
anything after # is treated as a comment and ignored. You can use symbols in Makefile.
Symbols are usually uppercase letters. After creating a symbol, it can be expressed by using
$() to enclose the symbol. The following Makefile replaces gcc -g -Wall -Wshadow using
two symbols GCC and CFLAGS.
GCC = gcc1
CFLAGS = -g - Wall - Wshadow2
3
aredis tinct . o: aredist inct . c4
$( GCC ) $ ( CFLAGS ) -c ared istinct .c # another comment5
6
main . o: main .c7
$( GCC ) $ ( CFLAGS ) -c main . c8
Why are symbols useful? A general principle in software design is to use symbols to
express some common things. If changes are needed later, these modifications can be made
in only one place. For example, the Makefile could be modified to use another compiler,
and only the first line needs to be updated. There is another common reason for updating a
Makefile. When a program has been completed and is ready for customers. In this case, we
want to replace -g with -O. Please notice that the letter is the uppercase O for optimization,
not zero. The former adds debugging information to the program. The latter optimizes the
program and makes it faster. Replacing -g by -O can make a program noticeably faster.
We only need to update the CFLAGS symbol. By using a single symbol, we ensure that the
change is consistent throughout the entire Makefile.
GCC = gcc1
CFLAGS = -O - Wall - Wshadow # replace -g by -O2
3
# This is a comment4
aredis tinct . o: aredist inct . c5
$( GCC ) $ ( CFLAGS ) -c are d istinct .c # another comment6
7
main . o: main .c8
$( GCC ) $ ( CFLAGS ) -c main . c9
The Makefile still needs the command to link the two object files together. This is
placed below the symbols in the Makefile.
GCC = gcc1
CFLAGS = -g - Wall - Wshadow2
Writing and Testing Programs 71
3
prog : aredi stinct .o main .o4
$( GCC ) $ ( CFLAGS ) aredist inct . o main . o -o prog # no -c5
6
aredis tinct . o: aredist inct . c7
$( GCC ) $ ( CFLAGS ) -c are d istinct .c8
9
main . o: main .c10
$( GCC ) $ ( CFLAGS ) -c main . c11
The fourth line says the executable prog depends on both aredistinct.o and main.o.
If either object file is newer than prog, then the executable needs to be rebuilt by linking
the two object files. Line 7 determines whether aredistinct.o needs to be regenerated.
Line 10 determines whether main.o needs to be regenerated.
In a Linux Terminal, type
$ make
The output is
gcc -g -Wall -Wshadow -c aredistinct.c
gcc -g -Wall -Wshadow -c main.c
gcc -g -Wall -Wshadow aredistinct.o main.o -o prog
If you type make in the Terminal again, the output is
make: ‘prog’ is up to date.
If you change main.c (add a comment somewhere) and type make, the output is
gcc -g -Wall -Wshadow -c main.c
gcc -g -Wall -Wshadow aredistinct.o main.o -o prog
As you can see, main.o is regenerated but aredistinct.o is not regenerated.
We have now solved both problems described above in building programs: (1) we have
replaced the long awkward commands with make and (2) make automatically uses depen-
dencies to determine which files need to be recompiled, thus reducing the amount of time
required to build large projects. There are three dependence rules in this Makefile: prog,
aredistinct.o, and main.o. When make is typed, we tell the make program to check the
first rule, i.e., the rule at the top of the Makefile. If we put the prog dependence and the
corresponding action (lines 4 and 5) lower in Makefile, we need to explicitly tell make to
check the prog rule first, as follows:
$ make prog
5.2 Test Using Makefile
A Makefile can be used for many more purposes. One common usage is to test programs.
Before explaining how to test programs, please note the following important rules on testing
programs:
72 Intermediate C Programming
It is possible to test a program and demonstrate that the program is incorrect.
It is almost impossible to test a program and demonstrate that the program is correct.
If a test fails (assuming the test is valid), then we know that the program is wrong.
If a program passes a test, what do we know about the program? Not much.
This may seem puzzling. If a program passes many tests, then the program must be
correct, right? In a way, this is like the theory of “black swans”. If we observe a thousand
white swans, we do not know whether black swans exist or not. Similarly, passing a thousand
tests does not tell us whether a program is correct. In contrast, if we see one black swan,
we know it exists. If a program fails one test, the program has a problem.
The truth is that testing is extremely hard (and important). Passing many tests gives
you some confidence, but no guarantee. A non-trivial program can have many possible test
cases. It is impossible to test so many cases. Even though testing is imperfect, testing is still
useful in developing programs. The following explains how to develop a strategy for testing.
5.2.1 Generating Test Cases
To test a program, we need test cases. To test areDistinct, we need different test cases:
len is zero or not.
arr either contains distinct elements or not.
At least three test cases are needed:
1. an empty file making len zero
2. a file with distinct numbers
3. a file with duplicate numbers
Creating the first test case is easy: Make an empty file. The touch command in Linux
can create an empty file. The second and the third test cases can be created by hand.
Alternatively, test cases can be developed using an on-line random number generator, saving
the results to a file. In this case, how do we know whether the numbers are distinct? The
sort and uniq commands in Linux can be used for this. The first command orders the
numbers and the second command tells whether the sorted numbers are unique or not. If
we add -d after uniq, the command displays which numbers duplicate. We chain the two
commands together using a pipe. The pipe takes the output of the sort command, and
makes it the input to the uniq command. In a Linux Terminal:
$ sort filename | uniq -d
filename is the name of the file that stores the random numbers. If the numbers in this file
are distinct, nothing appears. If some numbers duplicate, then the duplicate numbers are
shown on the screen.
5.2.2 Redirecting Output
Section 1.2 explained how to redirect a program’s output. Instead of printing “The
elements are distinct.” or “The elements are not distinct.” on the computer screen, the
output can be saved to a file. The following command redirects the output to the file whose
name is outputs/output0:
$ ./prog inputs/input0 > outputs/output0
Please check that the directory outputs exists before running this command. If it does not
exist, then use this command to create the directory:
$ mkdir outputs
Writing and Testing Programs 73
5.2.3 Use diff to Compare Output
Next, we use the diff command to compare the expected output with the output of the
program. Section 1.2 mentions this command and now you know how to use it.
$ diff expected/expected0 outputs/output0
If these two files are identical, then nothing appears on the computer screen. This means
that the program generates the correct output for this test case. If these two files are
different, then the difference is shown on the screen. We can add -w after diff to ignore
differences caused only by spaces.
5.2.4 Adding Tests to Makefile
As explained earlier, make can reduce the amount of typing, but it is really much more
than that. With make we can create jobs with dependent jobs, and only the required jobs are
rerun when files are edited. We can use the scheme to add a testing job into our Makefile.
test0 : prog1
./ prog inputs / input0 > outputs / output02
diff e xpected / expected 0 outputs / output03
This is another dependence rule. If this dependence rule is not the first rule in the Makefile,
we need to type:
$ make test0
This dependence follows the same rule mentioned earlier even though test0 is not a file.
Because test0 is not a file, its time can never be later than the time of prog. As a result,
the following two commands (./prog and diff) will always be executed. Before executing
these two commands, make checks the dependence of prog because it is at the right side of
the colon. The make program finds this rule in the Makefile,
prog : aredi s tinct .o main .o1
and make compares the time of these three files. If prog is older, then the executable file
prog will be regenerated. Before regenerating the executable file, make finds another two
rules in the Makefile:
aredis tinct . o : aredi stinct .c1
main . o : main .c2
Each object file will be regenerated if it is older than the corresponding .c file. Because
of these dependences, when you type:
$ make test0
the executable file prog will be regenerated if either .c file has changed since the last time
make was invoked. More rules can be added to the Makefile for running different cases:
test1 : prog1
./ prog inputs / input1 > outputs / output12
diff e xpected / expected1 outputs / output13
4
test2 : prog5
./ prog inputs / input2 > outputs / output26
74 Intermediate C Programming
diff e xpected / expected 2 outputs / output27
8
test3 : prog9
./ prog inputs / input3 > outputs / output310
diff e xpected / expected 3 outputs / output311
12
test4 : prog13
./ prog inputs / input4 > outputs / output414
diff e xpected / expected 4 outputs / output415
To test each case, type
$ make test1
$ make test2
$ make test3
$ make test4
Another rule can be used to test all test cases at once:
testall : test0 test1 test2 test3 test41
Finally, developers usually add a special rule that deletes computer-generated files:
clean :1
/ bin / rm -f *. o prog outputs /*2
When we type
$ make clean
all of the object files (*.o), the executable prog, and the output files outputs/* are deleted.
This is the full Makefile after adding all of these rules:
GCC = gcc1
CFLAGS = -g - Wall - Wshadow2
3
prog : aredi stinct .o main .o4
$( GCC ) $ ( CFLAGS ) aredist inct . o main . o -o prog # no -c5
6
aredis tinct . o: aredist inct . c7
$( GCC ) $ ( CFLAGS ) -c are d istinct .c8
9
main . o: main .c10
$( GCC ) $ ( CFLAGS ) -c main . c11
12
testall : test0 test1 test2 test3 test413
14
test0 : prog15
./ prog inputs / input0 > outputs / output016
diff e xpected / expected 0 outputs / output017
18
test1 : prog19
./ prog inputs / input1 > outputs / output120
diff e xpected / expected 1 outputs / output121
22
test2 : prog23
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset