Notes on C


Contents:


Compiling C programs

On unix systems, use the command
    gcc -Wall -O -g filename.c
to compile filename.c. If the compile succeeds, a file called a.out will be created - you can run this new program using the command:
    ./a.out
Notes: You can use the -o option (which takes an argument) to specify the name of the executable to produce (instead of the default a.out). For example,
    gcc -Wall -O -g -o hello hello.c
creates an executable called hello.

Where to compile

The preferred machines to compile on are the linux machines in one of the linux labs (aviary, EN-2036; aquarium, EN-2031) or the freebsd machines in the pc lab (arboretum, EN-1049). As there is generally only one person using each of these machines, compiles will be faster.

Once you have your program working, you should compile and test it on a variety of machines (i.e., ones with different kinds of CPUs). This is done because it often turns up bugs that are not apparent (i.e., are not provoked) on the machine you developed the program on. A minimal set of machines would be a PC (intel x86 CPU) running linux (e.g., a machine in the linux lab) or freebsd (e.g., phobos), a sun (sparc CPU) such as maple or mercury, and an alpha (alpha CPU) such as garfield or riemann.

If you are doing your assignments under dos or windows, make sure you compile and test it using gcc on several of the machines in the department before handing it in, as these machines are where the automatic testing will be done.

If you have a PC at home running only dos (or ms-windows), but don't have a compiler, there is a free one called DJGPP that you can download (this is the dos version of gcc which we use on most of our unix systems). You can find details on how to get and configure this package from Chad Follett's DJGPP page.


Running the debugger

On unix systems, use the command
    gdb executable
to start the debugger, where executable is the name of the program you wish to debug (e.g., a.out). It will print a bunch of information and then give you a prompt ("(gdb) "). At this point, you can tell the debugger what to do; some options are:
run
runs the program, starting from the beginning. The program will stop when it exits, gets a fatal error, or hits a break point. You can redirect the input (and output) of the program just as you do in the shell using < inputfile (or >outfile).
bt
gives a stack backtrace (bt stands for BackTrace) - this indicates what functions are currently being called.
l function-name
lists the first few lines of the function function-name; an l with no arguments continues listing where it left off last.
b function-name
sets a break point in function function-name - this means that when the program runs next, it will stop when the specified function is executed.
b line-number
sets a break point at line-number in the current file - this means that when the program runs next, it will stop when the code at the specified line is executed.
c
continues running the program; typically used after the program has hit a break point.
p variable-name
prints the current contents of variable-name. Note that the C-compiler (gcc) will sometimes optimize away variables so you can no longer see them from the debugger. If this happens, you can try compiling without the -O option (but this won't always fix the problem).
q
quits the debugger - if the program is still running, gdb will ask for confirmation.
If you get a core dump, you can use the debugger to figure out where your program is dieing very easily. Consider the program:
    int equal(char *s1, char *s2);

    int
    main()
    {
	equal("hi", "there");
	equal("again", (char *) 0);
	return 0;
    }
    /* Check if two strings are equal */
    int
    equal(char *s1, char *s2)
    {
	while (*s1) {
	    if (*s1 != *s2)
		return 0;
	    s1++;
	    s2++;
	}
	if (*s2)
	    return 0;
	return 1;
    }
Assume this is in a file called t.c.
    $ gcc -Wall -g t.c		# compile program with -g
    $ a.out			# run the program
    Memory fault (core dumped)
    $ gdb a.out core		# start debugger with core file
    GDB 4.16 (alpha-dec-osf1), Copyright 1996 Free Software Foundation, Inc...
    Core was generated by `a.out'.
    Program terminated with signal 11, Segmentation fault.
    Reading symbols from /usr/shlib/libc.so...done.
    #0  0x1200012c0 in equal (s1=0x140000028 "again", s2=0x0) at t.c:15
    15                  if (*s1 != *s2)
    (gdb)	# the above indicates that the program died at line 15,
		# in the if statement note that the function and its
		# arguments are also printed
    (gdb) bt			# use backtrace command
    #0  0x1200012c0 in equal (s1=0x140000028 "again", s2=0x0) at t.c:15
    #1  0x12000124c in main () at t.c:7
    (gdb)	# the above indicates the program is currently in the 
		# function equal (at line 15), and the function was
		# called from main (at line 7)
	
    (gdb) f 0			# goto `frame 0' - the nearest function
    #0  0x1200012c0 in equal (s1=0x140000028 "again", s2=0x0) at t.c:15
    15                  if (*s1 != *s2)
    (gdb)	# the above lists the line that is being executed in frame 0
    (gdb) l			# lists the lines around where
				# the program died
    10          /* Check if two strings are equal */
    11          int
    12          equal(char *s1, char *s2)
    13          {
    14              while (*s1) {
    15                  if (*s1 != *s2)
    16                      return 0;
    17                  s1++;
    18                  s2++;
    19              }
    (gdb) p s1			# print the contents of the variable s1
    $1 = 0x140000028 "again"
    (gdb) p s2			# print the contents of s2
    $2 = 0x0
		# the program dies at line 15 because the contents
		# of s2 (*s2) is being used - s2 is null, so the program
		# dumps core
    (gdb) q			# leave gdb
    $ 

Some important notes about gdb:


How to use the indent program

Proper indentation of programs makes the program much easier to read and understand -- it is visually apparent which statements are a part of which loops, conditions, etc. I stongly suggest that as you are writting the program, you format it so you can see what is going on. There are several aids you can use to help you write well formatted programs:


How to use the make command

The make command is used to speed up and simplify the compiling of multi-file programs (i.e., programs whose source code is contained in several different files). The basic idea is you tell make what files make up your program and how to link them all together, and it takes care of doing the work (running the compiler as needed) - all you have to do is type `make'.

The Makefile

The make command looks in a file called Makefile for information on how to compile a program. This file consists of two basic components: macro assignments and rules that indicate how to build a program. For example, consider the file
    OFILES=main.o module.o

    a.out: $(OFILES)
	    gcc -O -Wall $(OFILES)
the first line is a macro - it sets the `variable' or macro called OFILES to the value `main.o module.o'. Any time the string $(OFILES) appears in the rest of the file, it is replaced by `main.o module.o' (macros are normally used to localize information - only one place needs to be updated instead of two). The last two lines form a rule - it consists of two parts: a dependency line and a command. Together, they tell make that the program called a.out depends on the files main.o and module.o (the files that appear after the colon), and that the a.out file can be created using the command specified gcc command. The make command looks at these lines and knows that in order to create the a.out program, it must first create those two .o files (a.out depends on main.o and module.o). Assume for now that these files exist. Make, when run, will simply execute the command
    gcc -O -g main.o module.o
to create the a.out file. Now, what if the two files a.out depends on don't exist? It turns out that make knows that .o files can be created from .c (or .cc) files - so it looks around for a matching .c files and (assuming it finds them) runs the C compiler to create the needed .o files.

One very important detail: make is picky about how the command in a rule is specified - it must be on the line immediately after the dependency line (the one with the colon in it), and it must start with a tab character (not some number of spaces). Non-command lines must not start with a tab character.

Make actually does a bit more than is described in the previous paragraph. It not only checks for the existence of the needed files, it checks to see if they are up to date. If the matching .c file of a .o file is newer than the .o file, the .o file is out of date (it needs to be recompiled). So, when you run make for the first time, it creates the two .o files from the .c files by running the compiler, then it links the two .o files together, using the provided command, to create the a.out file. If you run make again, it will do nothing as none of the files are out of date (in other words, a.out is up to date). However, if you change one of .c files and then run make again, make will re-compile that .c file to create an up to date .o file, and then it will run the link command again to create an up to date a.out file. All you have to do is type make and it will figure out what needs doing and then do it. This saves alot of typing...

More on Make

Some of the things make does, such as creating a .o file from a .c file, are done `automatically' - without you having to tell it how to run the compiler. Generally, this is good, but it isn't if you want to compile your programs in a particular way - for example, with a particular compiler or with with certain options such as the -Wall or -O options. Make allows you to specify things using two special macros: CC and CFLAGS. The CC macro specifies the C compiler to use to create a .o file from a .c file, while the CFLAGS macro specifies what flags to pass to the compiler. A more correct Makefile is:
    CC=gcc
    CFLAGS=-O -Wall
    OFILES=main.o module.o

    a.out: $(OFILES)
	    $(CC) $(CFLAGS) $(OFILES)
Here we've specified that the gcc compiler is to be used in compiling C programs, and the flags `-O -Wall' are to be passed to the compiler. Note that the file used to link the object files also uses these macros - this is a matter of choice, but it makes sense to use them if they are available (again, a single point of change should one wish to use different options, or a different compiler).

Make can also be used to carry out (somewhat) arbitrary commands. This can be done by specifying several `rules' in a single Makefile. For example, the Makefile

    CC=gcc
    CFLAGS=-O -Wall
    OFILES=main.o module.o

    a.out: $(OFILES)
	    $(CC) $(CFLAGS) $(OFILES)
    clean:
	    rm a.out $(OFILES)
has two `targets' (a target is something that appears on the left hand side of a dependency line). When you run make with no arguments, it attempts to build the first target it finds in the Makefile. You can specify what target you want built by naming it on the command line (e.g., `make clean' says build the `clean' target). Make will look at the Makefile, and see that the file `clean' does not depend on any files (there is nothing after the colon); it will also note that there is currently no file called clean in the current directory and so it will decide to run the command to create the `clean' file - the fact that the given rm command doesn't create the clean file is irrelevant to the make program - it is content to run the command; as long as the command succeeds, it is happy. This brings up the question of what happens if a command fails? Make will generally print an error and exit as soon as a command fails (a command `fails' if it exits with a non-zero value - remember the `return 0;' at the end of the main() in most C programs? This is what it is all about; generally, C compilers will exit with a non-zero value when they run across errors). Back to the clean example - typing `make clean' causes the a.out program and the .o files to be removed. This is very handy when you are finished with a program and want to save disk space. It is also handy when you want to compile your program on a different kind of machine, as one machine generally won't know what to do with the .o files of another machine.

Making C++ programs

Naturally, C++ programs can also be created using the make command. The only difference is the macros that tell make what C++ compiler (vs C compiler) to use and what options to use. The macros for C++ are CXX and CXXFLAGS, so the file
    CXX=g++
    CXXFLAGS=-O -Wall
    OFILES=main.o module.o

    a.out: $(OFILES)
	    $(CXX) $(CXXFLAGS) $(OFILES)
will compile a C++ program consisting of two files, main.cc and module.cc. (Of course, all bets are off if both a main.c and a main.cc exist; make will generally prefer either the .c over the .cc, or visa versa)

Unfortunately, all makes are not created equal - some know how to compile C++ files, others don't (e.g., the make on garfield doesn't know how). Luckly, you can often teach make new tricks. Simply add the lines

.SUFFIXES: .cc
.cc.o:; $(CXX) $(CXXFLAGS) -c $?
to the end of you Makefile, and everything should work out ok (for an explanation of these lines, see the make man page).


Web information on C


C books


This page is adapted from that for CS3710 (Winter 1999) created by Mike Rendell.


Created: January 17, 2000
Last Modified: January 31, 2000