IMPORTANT: if you are finding the course difficult, see me
(Michael
Ashley, Room 129 Old Main Building, phone 9385 5465, e-mail
m.ashley@unsw.edu.au) for
assistance. If there are concepts you are having difficulty with,
please let me know as soon as possible so that I can cover them in
lectures and provide additional notes on the web.

Also, to gain maximum benefit from the course, you should TURN
UP TO LECTURES! It is NOT sufficient to simply download
these notes from the web. The lectures contain additional value. I
also spend considerable time answering questions from the students,
covering exam questions, giving hints on the assignments, and
reviewing the answers to past assignments.

The lectures

Lectures often have an "aside" where I talk for
10-15 minutes on some interesting (to me, hopefully to you as well)
non-examinable topic related to the use of computers in physics. Some
of these asides are mentioned below.

Why learn C?

Even though FORTRAN is still in heavy use by physicists, it is
on its way out.

If you want to get a computer-related job outside the field of
Physics, it is helpful to know C.

Many scientific instruments are programmed in C (e.g., CCD
cameras and special purpose interface cards invariably come with a C
library interfaces).

C is closer to assembly language, so you can have finer control
over what the computer is doing, and thereby make faster programs.

There is a free C compiler available (GNU C, gcc), that is
of very high quality and that has been ported to numerous machines.

UNIX is written in C, so it is easier to interface with
UNIX-like
operating systems (such as GNU/Linux) if you write in C.

Once you have a working knowledge of C, you will find perl easy
to learn, and perl is a very useful language to know, but that is
another story...

Some problems with C

The language was designed with writing operating systems in
mind, and so it was not purpose-built for numerical work.

There is no support for complex numbers (actually, as of the
C-99 standard, there is!).

Handling multi-dimensional arrays, understanding pointers, and
string manipulation are difficult when you first encounter them.

It is easy to write completely opaque code in C.

You have to explicitly declare every variable that you use. This
is actually not a problem at all! It is a feature.

It is possible to bypass many of the protective features of the
language, and get yourself into trouble (alternatively, this can be
seen as an advantage, since the language imposes few limitations on
what you can do).

The ANSI C standard leaves a lot of details undefined. This makes
C non-portable between different operating systems, compilers, and
computer hardware. Java is better in this respect.

It is not easy to write large bug-free programs in C. Java is
often a better choice.

A brief history of C

C evolved from a language called B, written by Ken Thompson at Bell
Labs in 1970. Ken used B to write one of the first implementations of
UNIX. B
in turn was a decendant of the language BCPL (developed at Cambridge
(UK) in 1967), with most of its instructions removed.

In fact, so many instructions were removed in going from BCPL to B,
that Dennis Ritchie of Bell Labs put some back in (in 1972), and called
the language C.

The famous book The C Programming Language was written by
Kernighan and Ritchie in 1978, and has remained the definitive
reference book on C.

The original C was still too limiting, and not standardized, and so
in 1983 an ANSI committee was established to formalise the language
definition.

By 1993 the ANSI standard became well accepted and almost
universally supported by compilers. The GNU C compiler, gcc, has
become, in many ways, the standard implementation of C.

What about C++ and C#?

C++ is a superset of C, incorporating many object-oriented
programming ideas. However, it is not particularly widely used.
Java
is a better choice in most cases.

C# is a Java-like language developed by Microsoft. Enough said.

Interesting books

Numerical Analysis by Burden and Faires.

A New Kind of Science by Wolfram.

Numerical Recipies in C

Other on-line resources

For coverage of typical problems encountered with C, have a look at
the Comp.lang.c
FAQ.

The simplest possible C program

Hello world

Before reading further, have a look at the generic Hello World, program written in
C.

But before we get too excited and start to write really useful
programs, there are a few fundamental concepts that you will need to
understand first.

Computing fundamentals

Bits, bytes, and words

Bit

A bit is the smallest unit of
information. It can be thought of as the answer to a yes/no question. A
bit has the value 1 or 0 (which can be equated with yes/no, true/false).

Byte

A byte consists of eight
bits, ordered from the most significant bit (MSB) on the left, to the least
significant bit (LSB) on the
right. E.g., 011011000 is a byte. A byte can be interpretted as a
base-2
number.

There are 256 (i.e., 2 to the power 8) possible combinations of 8
bits, ranging from
00000000 to 11111111. These 256 combinations could, in principle, be
used to represent any 256 things (e.g., we could define 00000000 to be
"apple" and 00000001 to be 3.14159265359).

In practice there are three
commonly used interpretations of a byte:

Unsigned byte

An unsigned
byte can
represent integers from zero to 255.

Signed byte

Abyte
can also be used to
represent integers from -128 to +127. Such a byte is
called a signed byte, and the
MSB is used to indicate the sign (0 for positive, 1 for negative), -128
is represented as 10000000, -1 is 11111111, 0 is 0, and +127 is
01111111.

Non-examinable: To find the
bit pattern for a negative number, you first write down the
representation of the number without the minus sign, then invert all
the bits, then add one. e.g., -1 is 00000001 inverted (which is
11111110), plus one (which gives 11111111). This representation is
called "2's complement", and is used because (1) it makes binary
arithmetic easier to implement in the computer hardware, and (2) it
avoids the waste of a bit pattern in the case of 1000000, which you
might think was -0.

A byte as an ASCII character

The American
Standard Code for
Information Interchange (ASCII)
associates the various possible bit patterns in a byte with characters
(i..e., things like 'A', 'B', 'C'...'Z', 'a', 'b', 'c', ...'z', '*',
'&', '@', etc). For example, 'A' is stored as 65 (decimal).
The ASCII table allows you to translate a simple text document
into a list of bytes (which you might then store in a file, see below). For a complete list of the ASCII
table, type "man ascii".

Words

In order to represent numbers outside the range of an unsigned or
signed byte, multiple bytes can be grouped together to form words of various lengths. Words
usually contain a power of two bytes, e.g., 2, 4, 8, 16, and so on.
They
may either be signed or unsigned. For example, an unsigned 2-byte word
can represent numbers from 0 to 65536 (2 to the power 16), a signed
2-byte word ranges from -32768 to + 32767. Words are sometimes called
"short words", "words", and "long words", for the 2, 4, and 8-byte
variations, but this usage is not uniform.

32-bit computer, 64-bit computers

Most PCs are 32-bit computers, i.e., they process 32-bits (4 bytes) of
data in one operation. In a few years, 64-bit computer will become very
common.

Binary, octal, decimal, hexadecimal

Integers can be written in various bases. The common bases used with
computers are binary (base-2), octal (base-8), decimal (base-10), and
hexadecimal (base-16). Octal and hexadecimal are often useful when
doing low-level programming, since they encode a binary number in a
smaller amount of typing than using binary notation.

The following table compares the various
numbering systems (note that you can't directly specify a binary
constant in the C programming language); octal constants are
distinguished by a leading zero;
hexadecimal constants by a leading "0x"):

Conversion between binary, octal, and hexadecimal is fairly trivial
using
the above table. For example, to convert the binary number 11100101010
to octal, break the number into groups of three bits starting from the
least significant bit (i.e., 11 100 101 010), then convert each
three-bit group to octal (i.e., 3452) and add a leading 0 to tell C it
is octal (i.e., 03452). To convert the number to hexadecimal, break it
into groups of four bits (i.e., 111 0010 1010), convert each group
(i.e.,
72a) and add a leading "0x" (i.e., 0x72a).

To convert between octal and hexadecimal, it is probably easiest to
go via binary.

To convert from binary to decimal, note that n-th bit from the
least-significant is worth 2 to the power n. So,

Floating-point numbers

Scientific computation often requires the use of floating-point
numbers, e.g., 1.23. There is an IEEE standard that describes how
floating-point numbers can be represented as a group of bytes. The
basic idea is to allocate a certain number of bits to the exponent
(base-2), and the rest to the mantissa (which has been normalised to be
between 0.5 and 1). Both the exponent and mantissa have sign bits.

Floating-point numbers typically come in 4, 8, or 16 byte sizes. In C
these are usually called "float", "double", and "long double"
respectively.

For any given number of bytes allocated to store a floating-point
number, there is clearly a tradeoff between the range of the exponent,
and the
precision of the mantissa: the more bits you use for the exponent, the
fewer are left over for the mantissa. We will see later how this
compromise
has been chosen by the IEEE for the various byte sizes.

Note that there are some redundant bit patterns in the representation
of floating-point numbers: e.g., 0.0 with any exponent is
still zero. One of these bit patterns is used as a flag to indicate
that a calculation produced an illegal result. This bit pattern is
called "NaN" which stands for "not a number". E.g, if you take the
square root of a negative number, you will receive the result NaN. Any
operation on a NaN will continue to produce a NaN (e.g., if you
subtract two NaNs you will still have a NaN), this is useful to
propagate such error conditions in your programs so that you are aware
when they affect the final result of a calculation.

GNU/Linux fundamentals

The operating system

The operating system of a
computer is, somewhat loosely speaking, the program that is always
operating in the background on the computer. It handles the interaction
with the hardware, and running other programs for the user(s) of the
computer. The operating system that we will be using is called GNU/Linux. Other examples of
operating systems are Windows XP, OpenBSD, and FreeBSD.

The shell

The shell is an interactive
program that allows the user to interface with the operating system.
This is the program that provides the command-line prompt (it also
helps you run programs, allows you to use the arrow keys on your
keyboard to edit the command-line, and a bunch of other useful
things). The shell
that we will be using is called bash,
it is the standard shell that is used with GNU/Linux. Other examples of
shells are ksh, tcsh, and zsh.

Files

A file consists of two
components:

File data

The data in a file is simply a list of bytes. The list may be empty
(i.e., the file may have no data). The list of bytes might be used for
a variety of different purposes, for example, it may be a C program, an
e-mail message, a photograph, a song, a movie. In principle there is no
way to distinguish the purpose of a file's data by looking at it. In
practice, you can usually make a pretty good guess. To list the data in
a file, you can, e.g., use "cat myfile".

File metadata

The metadata associated with
a file
consists of miscellaneous additional information that the operating
system keeps with the file. For example: the name of the file, the
number of bytes in the file, the time at which the file was created,
which user owns the file, and the file
permissions (i.e., the list of users who read and/or write the
file, and whether the file is executable
(i.e., is a program that can be run)). The term "metadata" means "data
about data". To list the metadata associated with a file, you can,
e.g., use "stat myfile".

File names and file extensions

When you create a file, you give it a name. The name is a list of
bytes. In principle the name could be bizarre, such as " _1
`", but in practice you should choose simple, lowercase, names without
spaces, e.g., "thesis.txt", "photo.jpg", "assignment0001.c". Note the
convention of suffixing a file name with an extension that gives a
clue as what the file is being used for. So, C programs invariably end
with the ".c" extension.

The GNU/Linux filesystem

Directories (folders)

To assist with organising groups of files, GNU/Linux has the concept of
a directory (or folder). A directory is actually
just a file (this adheres to the UNIX philosophy of making things
simple and logical). A directory contains a list of the file names that
are "in" the directory. Directories can contain directories. This leads
to a hierarchical filesystem,
where, in order to fully specify the location of a file, you have to
specify the
list of directories to follow to reach the file. By convention a slash
character '/' is used to separate the directory names when giving the fully qualified filename. "/"
itself is known as the root directory
- there are no directories above the root directory in the filesystem.
"/thesis.txt" would then refer to file "thesis.txt" in the root
directory. "/home/fred/thesis/chapter4.txt" would be the file
"chapter4.txt" in the directory "thesis", in the directory "fred", in
the directory "home", in the root directory.

The Windows operating system
uses letters such as "C:", "D:", etc to specify different filesystems
(which might be in on different hard disks). GNU/Linux places these
filesystems within the "/" hierarchy, i.e., the floppy disk might be
accessible as "/mnt/floppy", the CD-ROM might be "/mnt/cdrom".

The current working directory

GNU/Linux has the concept of the current
working directory. You can specify a file in the current working
directory by just giving the file name. For convenience, "./" refers to
the current working directory, and "../" refers to the directory above
the current working directory. The fully-qualified filename of a file
in the current working directory can be written as, e.g.,
"./thesis.txt".

Programs

To get a computer to do something for you, you create and run a program. You will be doing this
many times during this course. To create a program, you type in the
program's source code, and
then use, e.g, the C compiler (another
program, which someone else wrote) to turn the source code into a
program. The program itself is just a file. The file's metadata tells
the operating system that the file is executable
(i.e., that it is a program able to be run by the computer). To run the
program, you just type its fully qualified filename.

GNU/Linux commands

GNU/Linux provides you with many hundreds of programs, often called commands,
but they are really exactly like the programs that you can create
yourself (actually, some of the commands are a subset of the bash
shell, but
this is a detail). The commands range from
listing the contents of a directory ("ls"), listing the contents of a
file ("cat"), the
C compiler ("gcc"), to the GNU image manipulation program ("gimp").

To run a program, you simply type its filename. In the case of your own
programs, you should use the fully qualified filename (e.g.,
"./a.out"). In the case of programs provided by GNU/Linux, the
shell
has a list of default directories where it searches for programs, so
you normally only need to type the filename (without any directories).
E.g., the "ls" program to list directories can be run by simply typing
"ls", or "/bin/ls", or, if your current working directory happened to
be "/bin", you could type "./ls".

Input and output

In general, for a program to do useful work, it needs to be provided
with input of some sort, and to produce output.

Input to a program can take many forms: e.g., typing at the keyboard, a
file containing data, movements of a mouse, or something more exotic
such as audio input from a sound card, or video from a camera.

Output from a program can also take many forms, e.g., text appearing on
your computer screen, a graph on your screen, a file of results, or
something more exotic such as audio output, or movement of a robotic
arm.

GNU/Linux provides the following input and output methods for all
programs (I am covering all of the possibilities here, but you are only
expected to really understand the first two at this stage):

Standard input

Standard
input (or STDIN)
is a stream of bytes provided to the program. The program can read from
the stream, and can determine when there is no more data. Standard
input comes from one of three sources depending on how you run your
program. For example, suppose that your program is called "a.out" (the
default name assumed by the C compiler), then

./a.out

standard input comes from the
keyboard

./a.out < file

standard input comes from a file
called "file"

command | ./a.out

standard input comes from the
standard output of "command"

Standard output

Standard
output (or STDOUT)
is a stream of bytes produced by a program. Standard output goes to one
of three locations depending on how you run your program. E.g.,

./a.out

standard output appears on the
computer screen as text

./a.out > file

standard output is sent to the
file called "file"

./a.out | command

standard output is sent to the
standard input of "command"

Standard error

Standard
error (or STDERR)
is a stream of bytes produced by a program. Usually it contains error
messages (e.g., 'divide by zero') that are not normally considered as
part of the program output. Standard error can be sent to one of three
locations. E.g.,

./a.out

standard error appears on the
computer
screen as text, intermingled with the standard output

./a.out 2> file

standard error is sent to the
file called "file"

./a.out 2>&1 | command

standard error (and standard
output) is sent to
the standard input of "command"

Command-line arguments

Anything you type after the program
name, and before any special shell characters such as '>' and '|',
is made available to your program as a command-line argument. These can
be a useful source of additional input to a program. E.g.,

./a.out 23.45 albatross 7

In the above example, "23.45", "albatross", and "7" are three
command-line arguments.We will see later how you can read them from
within a C program.

Exit status

Another form of output that is
available to all programs is an integer exit status.
By convention, this is used to indicate whether the program ran
successfully (in which case the exit status is zero) or if an error
occurred (in which case the exit status is nonzero and its value can be
decoded to determine what the error was). The exit status of a program
is stored in the shell variable called "$?" (don't worry if you don't
understand this at this stage).

Command toolkit

less myfile
- like "cat", but allows you to scroll up and down in the file using
the page-up, and page-down keys. Exit with 'q'.

rm myfile
- remove a file (i.e., delete the file; it is gone for good)

mvmyfile newname -
renames a file

cpmyfile copyOfMyFile
- makes a copy of a file, with a different name

cd mydir
- change directory

mkdir mynewdir - make a new
directory

rmdir mydir - remove the directory
(you have to remove the files in it first)

ls - list the files and
directories in the current working
directory

pwd - print the name of the
current working directory

gcc myprog.c
- compiles your program

./a.out - runs the program
compiled as aboveman mytopic
- displays the manual pages about "mytopic"

man -k mytopic - searches the
manual pages for any information about "mytopic"

gedit myprog.c - starts up a
simple GUI editorlogout - logs out of the
computer

Fundamentals of C

Character constants, escape sequences, and string constants

A character constant in C is a single character (or an escape
sequence such as \n) enclosed in single quotes, e.g., 'A'. It
occupies one byte of storage.

The value of a character constant is the numeric value of the
character in the computer's character set (e.g., 'A' has the value
65). In 99.99% of cases this is the ASCII character set, but this is
not defined by the C standard!

But how do you represent a character such as a single quote itself,
or one that doesn't print (such as the ASCII BEL character)? The
answer is to use an escape sequence.

For reference, here is a program to
print out all the special escape sequences. Have a look at it
now.

In addition, you can specify any 8-bit ASCII character using
either \ooo or \xhh where `ooo' is an octal number (with from 1 to 3
digits), and 'xhh' is a hexadecimal number (with 1 or 2 digits). For
example, \x20 is the ASCII character for SPACE, so ' ' and '\x20' are
identical.

String constants are a sequence of zero or more
characters, enclosed in double quotes. For example, "test", "", "this
is an invalid string" are all valid strings (you can't always believe
what a string tells you!). String constants are stored in the
computer's memory as a sequence of numbers (usually from the ASCII
character set), and are terminated by a null byte (\0). So,
"test" would appear in memory as the numbers 116, 110, 115, 116, 0.

A consequence of the null byte terminator is that a C character
string can never contain a null byte. In practice, this isn't often a
limitation.

We have already used some examples of strings in our programs, e.g,
"hello world\n" was a null-terminated character string that we sent
to the printf function above.

The above program also shows how to add comments to your
C program.

Comments

To make your program more readable, it is good practice to include
comments. These do not influence the operation of the program,
nor
are they printed out when the program runs, they are simply to document
things that you think are important.

Comments are delimited by `/*' and `*/', or from '//' to the end of
the line. Any text between the first `/*' and the next `*/' is ignored
by the compiler, irrespective of the position of line breaks. Note
that this means that comments do not nest. Here are
some examples of the valid use of comments:

/* This is a comment */// and so is this /* here is another one that spans two lines */ i = /* a big number */ 123456;

Here are some problems that can arise when using comments:

i = 123456; /* a comment starts here i = i + 2; this statement is also part of the comment */ /* this is a comment /* and so is this */ but this will generate an error */

The fact that comments don't nest is a real nuisance if you want to
comment-out a whole section of code. In this case, it is simplest to
use pre-processor directives to do the job. For example:

i = 123456;#if 0 // The next two lines are ignored by the compiler. i = i + 2; j = i * i;#endif

Compiling, linking, executing, etc

Source files

C source files contain the C program that you write. The
files invariably end with the suffix ".c". You compile them with the
C compiler to produce...

Object files

Have a ".o" suffix, and are the result of compiling a source
file. They contain the machine language equivalent of the C
program, but they are not ready to execute yet, since they need to be
linked against libraries of other C object files containing
any functions that are used, but not coded for, in the source file
(e.g., the printf function). Linking can be done for you by the C
compiler,
or can be a separate step if you wish. After linking, you have an...

Executable file

This file is ready to run, by typing its name at the command prompt.

Makefiles - the easy way to automate compilation

When developing software, a
great deal of time can be save by using
the UNIX make utility.

The idea behind make
is that you should be able to
compile
a program simply by typing make (followed by ENTER).
For this to work, you have to tell the computer how to build your
program,
by storing instructions in a Makefile.

While this step takes some
effort, you are amply repaid by the time
savings later on. make is particularly useful for large
programs that consist of numerous source files: make only
recompiles the file(s) that need recompiling (it works out which ones
to process by looking at the dates of last modification).

Here is an example of a simple Makefile:

test: test.o; gcc test.o -o test

Notes:

A Makefile consists of a series of lines containing targets,
dependencies, and shell commands for updating the
targets.

In the above example,

test is the target, i.e., the executable program,

test.o is the dependency (i.e., the file that test
depends on), and

gcc test.o -o test is the shell command for making
test
from its dependencies.

Important note: test is a very bad name for a
program since it conflicts with the UNIX shell command of the same
name, hence if you type "test" it will run the UNIX command rather than
your program. To get around this problem, simply use "./test" (if
"test" is in your current directory), or rename the program. It is
always a good idea to use the leading "./" since it avoids a lot of
subtle problems that can be hard to track down.

Makefiles can get much more complicated than this simple
example. Here is the next step in complexity, showing the use of a
macro definition and a program that depends on multiple files.

OBJS = main.o sub1.o sub2.o main: $(OBJS); gcc $(OBJS) -o main

It is well worth learning a
little bit about make since it
can save a lot of time!

How to compile and run a C program

Create a new directory for the program (not essential, but it
helps to be organised).

Use whatever editor you want to generate the program (make sure
the program file has a suffix of '.c').

Construct a Makefile for the program (not essential, but worth
doing).

Type make to compile the program (if you don't have
a Makefile, you will need to type the compiler command yourself).

Run the program by typing its name.

Locate and fix bugs, then go to step 4.

Here is a complete example of the above process for the ``Hello world''
program:

NOTE: the use of "cat > hello.c <<EOF" above
is purely a quick way of entering the source code into the file
hello.c. You would normally do this using an editor
(such as emacs, vi, pico, nano, nedit, xedit, gedit).

EXERCISE: try doing this yourself with some
of the example programs later in these notes.

A program to give information on C data types

Like other programming languages, C has a variety of different data
types. The ANSI standard defines the data types that must be
supported by a compiler, but it doesn't tell you details such as the
range of numbers that each type can represent, or the number of bytes
of storage occupied by each type. These details are implementation
dependent, and defined in the two system include-files "limits.h" and
"float.h". To find out what the limits are, try running the
following program with your favourite C compiler.

Notes:

sizeof looks like a function, but it is actually a
built-in C operator (i.e., just like +,-,*). The compiler
replaces sizeof(data-type-name) (or, in fact, sizeof(variable))
with the number of bytes of storage allocated to the data-type or
variable.

The 'unsigned' data types are useful when you are refering to
things which are naturally positive, such as the number of bytes in a
file. They also give you a factor of two increase in the largest number
that you can represent in a given amount of space.

An "unsigned char" is a single-byte C data type that can
represent
positive integers from 0 to 255. A "signed char" uses the
most-significant bit of the byte as a sign-bit (0 indicates a positive
number, 1 indicates a negative number), leaving the remaining 7 bits
to encode the absolute value of the number; the range of signed chars
is therefore -128 to +127.

Most of the time you don't need to worry about how many bytes are in
each data type, since the limits are usually OK for normal programs.
However, a common problem is that the "int" type can be either 2-bytes
or 4-bytes in length, depending on the compiler.

Here is the output of the preceeding program when run on a
GNU/Linux
PC, using gcc:

Integers are automatically assumed to be of the smallest type
that can represent them (but at least an "int"). For example,
2147483648 was assumed by the compiler to be an "unsigned int" since
this number is too big for an "int". Numbers too big to be an "unsigned
int" are promoted to "long" or "unsigned long" as appropriate.

An integer starting with a zero is assumed to be octal, unless
the character after the zero is an "x" or "X", in which case the number
in hexadecimal.

Unsigned numbers have a suffix of "u" or "U".

Long "int"s or "double"s have a suffix of "l" or "L" (it is
probably better to use "L" so that it isn't mistaken for the number
one).

Floating-point numbers are assumed to be "double"s unless they
have a suffix of "f" or "F" for "float", or "l" or "L" for "long
double".

It pays to be very careful when specifying numbers, to make sure
that you
do it correctly, particularly when dealing with issues of precision.
For example, have a look at this program:

The program gives at output of "-4.4703483581542969e-08", not
zero as you might expect.

The lesson to be learnt here is when writing constants, always
think carefully about what type you want them to be, and use the
suffixes "U", "L", and "F" to be explicit about it. It is not a
good idea
to
rely on the compiler to do what you expect. Don't be surprised if
different
machines/compilers give different answers if you program sloppily.

Conversion between integers and floating point numbers

In C, as in all computer languages, there are rules that the
compiler
uses when a program mixes integers and floating point numbers in the
same expression.

Let's look at what happens if you assign a floating point number to
an integer variable:

This program produces the result "i = 1; j = -1". Note that the
floating
point numbers have been truncated and not rounded.

When converting integers to floating-point, be aware that a "float"
has fewer digits of precision than an "int", even though they both use
4 bytes of storage (on normal PCs). This can result in some strange
behaviour,
e.g.,

Rather than rely on automatic type-conversion, you can be explicit
about it by using a type-cast operator, e.g.,

f = (float)i;

This converts the number or variable or parethesised expression
immediately to its right, to the indicated type. It is a good idea to
use type-casting to ensure that you leave nothing to chance.

Operators

C has a rich set of operators (i.e., things like + - * /),
which allow you to write complicated expressions quite compactly.

Unary operators

Unary operators are operators that only take one argument. The
following
list will be confusing when you first see it, so just keep it mind for
reference later on.

Some of the operators we have already seen (e.g., "sizeof()"),
others are very simple (e.g., +, -), others are
really neat (e.g., ~, !), others are useful for adding/subtracting
1 automatically (e.g., ++i, --i, i++, i--), and the rest involve
pointers and addressing, which will be covered in detail later.

sizeof(i) the number of bytes of storage allocated to i +123 positive 123 -123 negative 123 ~i one's complement (bitwise complement) !i logical negation (i.e., 1 if i is zero, 0 otherwise) *i returns the value stored at the address pointed to by i &i returns the address in memory of i ++i adds one to i, and returns the new value of i --i subtracts one from i, and returns the new value of i i++ adds one to i, and returns the old value of i i-- subtracts one from i, and returns the old value of i i[j] array indexing i (j) calling the function i with argument j i.j returns member j of structure i i->j returns member j of structure pointed to by i

Binary operators

Binary operators work on two operands ("binary" here means 2 operands,
not in the sense of base-2 arithmetic).

Here is a list. All the usual operators that you would expect are
there,
with a whole bunch of interesting new ones.

+ addition - subtraction * multiplication / division % remainder (e.g., 2%3 is 2), also called 'modulo' << left-shift (e.g., i<<j is i shifted to the left by j bits) >> right-shift & bitwise AND | bitwise OR ^ bitwise exclusive-OR (XOR) && logical AND (returns 1 if both operands are non-zero; else 0) || logical OR (returns 1 if either operand is non-zero; else 0) < less than (e.g., i<j returns 1 iff i is less than j) > greater than <= less than or equal >= greater than or equal == equals != does not equal ? conditional operator, explained later...

Note:

Truth and falsity in C is represented by numbers being non-zero
and zero respectively (although logical operators always return 1 or
0).

Don't make the mistake of using "=" when you meant "=="! The
compiler may not give you any warnings of this mistake.

Note the distinction between bitwise operators and logical
operators.

Here is a table showing the result of applying the logical operators
AND, OR, and XOR (exclusive-OR) to all possible combinations of two
single-bit numbers:

Left argument

Right
argument

AND

OR

XOR

0

0

0

0

0

0

1

0

1

1

1

0

0

1

1

1

1

1

1

0

The following table shows the differences between the bitwise
and non-bitwise logical operators when applied to three different
randomly chosen 4-digit binary numbers:

Left argument

Right
argument

&

|

^

&&

||

0011

0101

0001

0111

0110

1

1

1110

1010

1010

1110

0100

1

1

0000

1111

0000

1111

1111

0

1

Assignment operators

Assignment operators are really just binary operators. The simplest
example is "=", which takes the value on the right and places it into
the variable on the left. But C provides you with a host
of other assignment operators which make life easier for the
programmer.

So, for example, "i += j" is equivalent to "i = i + j". The
advantage
of the assignment operators is that they can reduce the amount of
typing
that you have to do, and make the meaning clearer. This is particularly
noticeable when, instead of a simple variable such as "i", you have
something complicated such as "position[wavelength + correction_factor
* 2]";

The thing to the left of the assignment operator has to be
something where a result can be stored, and is known as an "lvalue"
(i.e., something that can be on the left of an "="). Valid
"lvalues" include variables such as "i", and array expressions. It
doesn't make sense, however, to use constants or expressions to the
left of an equals sign, so these are not "lvalues".

The comma operator

C allows you to put multiple expression in the same statement,
separated
by a comma. The expressions are evaluated in left-to-right order.
The value of the overall expression is then equal to that of
the rightmost expression.

The comma operator has the lowest precedence, so it is always
executed
last when evaluating an expression. Note that in the example given
comma is used in two distinct ways inside an argument list for a
function.

Both the above examples are artificial, and not very useful. The
comma operator can be useful when used in "for" and "while"
loops as
we will see later.

Precedence and associativity of operators

The precedence of an operator gives the order in which
operators
are applied in expressions: the highest precedence operator is applied
first, followed by the next highest, and so on.

The associativity of an operator gives the order in which
expressions involving operators of the same precedence are evaluated.

The following table lists all the operators, in order of precedence,
with their associativity:

Operators on the same line have the same precedence, and are
evaluated
in the order given by their associativity.

To specify a different order of evaluation you can use parentheses.
In fact, it is often good practice to use parentheses to guard against
making mistakes in difficult cases, or to make your meaning clear.

Side effects in evaluating expressions

It is possible to write C expressions that give different answers
on different machines, since some aspects of expression-evaluation
are not defined by the ANSI standard. This is deliberate since it
gives the compiler writers the ability to choose different evaluation
orders depending on the underlying machine architecture. You, the
programmer, should avoid writing expressions with side effects.

Here are some examples:

myfunc(j, ++j); /* the arguments may be the same, or differ by one */array[j] = j++; /* is j incremented before being used as an index? */i = f1() + f2(); /* the order of evaluation of the two functions is not defined. If one function affects the results of the other, then side effects will result */

Evaluation of logical AND and OR

A useful aspect of C is that it guarantees the order of evaluation
of
expressions containing the logical AND (&&) and OR (||)
operators:
it is always left-to-right, and stops when the outcome is known.
For example, in the expression "1 || f()", the function "f()" will
not be called since the truth of the expression is known regardless of
the value returned by "f()".

It is worth keeping this in mind. You can often speed up programs
by rearranging logical tests so that the outcome of the test can be
determined as soon as possible.

Another good example is an expression such as "i >= 0 &&
i <n && array[i] == 0". This will guarantee that
the index into "array" is within legal bounds (assuming the array has
"n" elements).

Incidentally, the "bash" shell
uses "&&" and "||" in similar way to C. For example, the shell
command line:

cd /home/fred && ./a.out > output && mv output /home/fred/results/

will first change the current
working directory to "/home/fred". If (and only if) this succeeds, the
program "./a.out" would be run, sending its output to the file
"output". If (and only if) the program was successful (i.e., it had an
exit code of zero), the output file would be moved to the directory
"/home/fred/results/".

What is a C identifier?

An identifier is the name used for a variable, function,
data definition, etc.

Rules for identifiers:

Legal characters are a-z, A-Z, 0-9, and _.

Case is significant.

The first character must be a letter or _.

Identifiers can be of any length (although only the first 31
characters are guaranteed to be significant).

The following are reserved keywords, and may not be used as
identifiers:

Here are some examples of legal identifiers:
i
count
numberOfAardvarks
number_of_aardvarks
MAX_LENGTH

Mathematical functions

Prototypes for the math functions are in the system include-file
"math.h", so you should put the line

#include <math.h>

in any C source file that calls one of them.

Here is a list of the math functions defined by the ANSI standard:

sin(x) sine of xcos(x) cosine of xtan(x) tan of xasin(x) arcsine of x, result between -pi/2 and +pi/2acos(x) arccosine of x, result between 0 and +piatan(x) arctan of x, result between -pi/2 and +pi/2atan2(y,x) arctan of (y/x), result between -pi and +pihsin(x) hyperbolic sine of xhcos(x) hyperbolic cosine of xhtan(x) hyperbolic tan of xexp(x) exponential functionlog(x) natural logarithmlog10(x) logarithm to base 10pow(x,y) x to the power of y (x**y in FORTRAN)sqrt(x) the square root of x (x must not be negative)ceil(x) ceiling; the smallest integer not less than xfloor(x) floor; the largest integer not greater than xfabs(x) absolute value of xldexp(x,n) x times 2**nfrexp(x, int *exp) returns x normalized between 0.5 and 1; the exponent of 2 is in *expmodf(x, double *ip) returns the fractional part of x; the integral part goes to *ipfmod(x,y) returns the floating-point remainder of x/y, with the sign of x

In the above table, "x", and "y" are of type "double", and "n" is an
"int".
All the above functions return "double" results.

C libraries may also include "float" versions of the above. For
example, "fsin(x)" takes a float argument and returns a float result.

How to link with the math library

gcc -o prog prog.c -lm

The "-l" switch stands for "library", which means that the
specified library of pre-compiled C routines is searched in order to
satisfy any external references from your program "prog.c". The
library that is searched in this case is "libm.a" and the path that is
used for the search is the default library search path, which includes
the directory "/usr/lib", where "libm.a" is found.

To use a library in a directory that is not part of the default
library search path, you use the "-L" switch. For example, to search
the library "/usr/users/smith/libnano.a", you would use

gcc -o prog prog.c -L/usr/users/smith -lnano

Note: the order of the switches is important. External references
are only searched for in libraries to the right of the reference.
So, if you have two libraries that call each other, then you need
to do something like the following:

Variables defined within compound statements

You can create variables that are local to a compound statement
by declaring the variables immediately after the leading curly bracket.

Variable storage classes

Variables in C belong to one of two fundamental storage classes:
"static" or "automatic".

A static variable is stored at a fixed memory location in the
computer,
and is created and initialised once when the program is first started.
Such a variable maintains its value between calls to the block (a
function, or compound statement) in which it is defined.

An automatic variable is created, and initialised, each time the
block is entered (if you jump in half-way through a block, the creation
still
works, but the variable is not initialised). The variable is destroyed
when
the block is exited.

Variables can be explicitly declared as "static" or "auto" by using
these
keywords before the data-type definition. If you don't use one of these
keywords, the default is "static" for variables defined outside any
block, and "auto" for those inside a block.

Actually, there is another storage class: "register". This is like
"auto"
except that it asks the compiler to try and store the variable in one
of
the CPU's fast internal registers. In practice, it is usually best not
to use the "register" type since compilers are now so smart that they
can
do a better job of deciding which variables to place in fast storage
than you can.

Const - volatile

Variables can be qualified as "const" to indicate that they are
really constants that can be initialised, but not altered.

Variables can also be termed "volatile" to indicate that their
value may change unexpectedly during the execution of the program
(e.g., they may be hardware registers on a PC, able to be altered by
external events). By using the "volatile" qualifier, you prevent the
compiler from optimising the variable out of loops.

Extern

Variables (and functions) can also be classified as "extern", which
means that they are defined external to the current block (or even to
the current source file). An "extern" variable must be defined once
(and only once) without the "extern" qualifier.

As an example of an "extern" function, all the functions in "libm.a"
(the math library) are external to the source file that calls
them.

An example showing storage class and variable scope

#include <stdio.h>int i; /* i is static, and visible to the entire program */extern j; /* j is static, and visible to the entire program */static int j; /* k is static, and visible to the routines in this source file */

void func(void) { /* i.e., a function that takes no arguments, and doesn't return a value */ int m = 1; /* m is automatic, local to this function, and initialised each time the function is called */ auto int n = 2; /* n is automatic, local to this function, and initialised each time the function is called */ static int p = 3; /* p is static, local to this function, and initialised once when the program is first started up */ extern int q; /* q is static, and defined in some external module */

for (i = 0; i < 10; i++) { int m = 10; /* m is automatic, local to this block, and initialised each time the block is entered */ printf("m = %i\n", m); }}

Initialisation of variables

A variable is initialised by equating it to a constant expression
on the line in which it is defined. For example

int i = 0;

"static" variables are initialised once (to zero if not
explicitly initialised), "automatic" variables are
initialised when the block in which they are defined is entered
(and to an undefined value if not explicitly initialised).

The "constant expression" can contain combinations of any type of
constant,
and any operator (except assignment, incrementing, decrementing,
function call, or the comma operator), including the ability to use
the unary "&" operator to find the address of static variables.

An "automatic" variable can be initialised to any expression,
even one using other variables, since the initialisation is done at
run-time.

It is good style to put all your "extern" variables at the top of
each source file, or even to put them into a header file.

Don't overuse "extern" variables! It is usually better to pass
lots of arguments to functions rather than to rely on hidden variables
being passed (since you end up with clearer code, and reusuable
functions).

Break and continue

Infinite loops

Here are three ways of writing an infinite loop, choose whichever
one
you like:

for (;;;) {statement;}

while (1) {statement;}

do {statement;} while (1);

Infinite loops can be useful. They are normally terminated using a
conditional test with a "break" or "return" statement. For example:

while (1) {statement; if (logical_condition) break;}

goto

C does have a "goto" statement, but you normally don't need it.
Using "goto"
is usually a result of bad programming (although in some circumstances
it
can be a sensible choice).

The switch statement

The switch statement allows you to choose between multiple
paths of execution depending on the value of an expression. It could be
replaced by a series of if statements, but switch
can
make the flow of the program easier to understand.

Formatted output: printf description

The format string given to the "printf" function may contain both
ordinary characters (which are simply printed out) and conversion
characters (beginning with a percent symbol, %, these define how the
value of an internal variable is to be converted into a character
string for output).

Flags: "-" means left-justify (default is right-justify), "+"
means that a sign will always be used, " " prefix a space, "0" pad to
the field width with zeroes, "#" specifies an alternate form (for
details, see the manual!). Flags can be concatenated in any order, or
left off altogether.

Minimum field width: the output field will be at least this wide,
and wider if necessary.

There are differences between the various implementations
of
"printf". The above should be a subset of the available options.
Consult
the manual (e.g., "info gcc") for your particular C compiler to be sure.

"printf" is actually an integer function, which returns the
number of
characters written (or a negative number if an error occurred).

Formatted input: scanf overview

Input in C is similar to output: the same conversion
characters are used. The main difference is that you use a routine
called "scanf", and you must pass the addresses
of the variables you want to change, not their values.

scanf is actually an integer function, which returns the number of
input items assigned (or the integer constant EOF (defined in stdio.h)
if the end-of-file is reached or an error occurred).

The ampersand character "&" is a unary operator that returns
the address of the thing to its right. Recall that a C function can
not alter the value of its arguments (but there is nothing stopping it
altering the value that is pointed to by one of its arguments!).

scanf details

The scanf function is used for reading a line of input from the
standard
input stream (usually the users terminal) and extracting from it one or
more
integers, floating point numbers, character strings, etc.

The first argument to scanf is a format description, which describes
how
the input line should be decoded. For example, if you want to read an
integer,
the format would be "%i" (note that this allows you to enter numbers in
decimal,
hexadecimal or octal (e.g., 123, 0x123, 0123)). If you want to read a
floating
point number, the format would be "%f". If you want to read an integer
followed
by a floating point number, the format woud be "%d %f". If the two
numbers
were separated by a comma on the input line, you would use "%d, %f".

The arguments after the first are pointers to where the data should be
stored.

Examples (where i and j are integers, x and y are floats, and fruit
and colour are char arrays):

Input data

scanf function call to read the data

123

scanf("%i", &i);

123 0x456

scanf("%i %i", &i, &j);

123. 456.

scanf("%i. %i.", &i, &j);

123.1 456.1

scanf("%f %f", &x, &y);

e=2.71 pi=3.14

scanf("e=%f pi=%f", &x, &y);

apples red

scanf("%s %s", fruit, colour);

Guido ate 2 apples

scanf("Guido ate %i %s", &i, fruit);

It is important to check the return value of the scanf function - this
is
the number of items that were successfully extracted from the input. It
may
be less than the number you expected, or even zero. If this happens,
the
input stream pointer is left at the point at which the error occurred,
and
you can try reading again. scanf can also return the special integer
constant
EOF to indicate an end-of-file condition.

Variations on scanf

fscanf is very similar to scanf except it takes a FILE argument ("FILE"
is a structure that is defined in the <stdio.h> include file -
you don't have to worry about what precisely "FILE" is, just follow the
examples) to
specify
which input stream to read from, e.g.,

FILE *fp;float x, y;

fp = fopen("input.dat", "r");fscanf(fp, "%f %f", &x, &y);

"sscanf" takes input from a character array rather than an input
stream,
e.g.,

char input="1.23 4.56";sscanf(input, "%f %f", &x, &y);

Improving the ease of interaction with your programs - GNU readline

While it is possible to use "scanf" for reading input typed by a
person, you can greatly improve the user experience by using the GNU
readline package.

The GNU readline package provides some very convenient features such
as line editing and history. This means, e.g., that you can use the
arrow keys to
move within a line, and between previously entered lines. You can also
use, e.g., CTRL-A to go to the beginning of the line, CTRL-E to go
to the end, and CNTRL-R to search backwards for particular strings. See
"man readline" (on a GNU/Linux computer) for a
complete description.

Assert as a tool for improving program reliability

By now you will be aware that it is very easy to write buggy programs
in
C, and it may not always be obvious at runtime that there is a problem.
Checking
for all possible error conditions in a program can be tedious. A useful
tool
to improve program reliability is the "assert" construct defined in the
system
include-file "assert.h". An example will clarify this:

If the expression inside parentheses after "assert" is true, the
program
continues as normal, if the expression is false (i.e, zero), then the
program
aborts with an error message.

This may sound like a trivial concept, but by using "assert"
extensively,
you will be surprised at how your programming improves.

It is particularly useful to use "assert" to check for conditions that
should
never arise in your program, e.g., suppose you write a function that
should
only ever be called with an argument between -1.0 and 1.0, then, use
"assert"
in the function to check this, e.g.,

double myAsin(double x) { assert(fabs(x) < 1.0); ...}

Also, you can use assert to check for unexpected conditions such as
failure to open a file, or failure to allocate memory, e.g.,

There are several different algorithms for generating a new random
number given an initial random number. One of the simplest (and best)
algorithms for generating random integers is a multiplicative
congruential algorithm: you
simply multiply the current random number generator by a constant, and
then take the modulus. The choice of the constant and the modulus is
critical. A good choice is 16807 and 2147483647 (see Numerical
Recipes in C, Press et al, 2nd Ed, page 278).

But how do you generate the initial random number, the so-called
"seed",
for the algorithm?

One technique is to use the time (in milli or micro-seconds since
the last integral second). This is easy to program, but has a few
disadvantages, e.g, there are only 1000 or 1000000 different seeds, and
these numbers are not necessarily equally likely to occur (e.g., the
operating system might tend to start programs at particular times - see here for more details).

Another technique on GNU/Linux systems is to read from the file
/dev/random. /dev/random contains random numbers generated from
"random" events such as the arrival time of internet packets, the
movement of the mouse, the timing of user keystrokes, etc. If you "od
-h /dev/random" ("od -h" prints out the file as hexadecimal characters)
you will find that it will eventually stop, waiting for
new events to generate randomness - move the mouse and it will
continue. An alternative is /dev/urandom,
which will continuously generate random numbers, using an algorithm if
events are too infrequent.

If you require very high quality random numbers, then you should
read about the subject, perhaps using Numerical Recipies in C
as a starting point.

The C libraries provide the functions "srandom" and "random" to
generate random integers between 0 and RAND_MAX (defined in
"stdlib.h") inclusive. Use the on-line manual pages to learn about
"srandom" and "random" (i.e., type "man srandom" at the
prompt). Here is an example program
showing their usage, and seeding them with the time in
microseconds.

Quasi-random numbers

For some applications, you don't want truly random numbers, but would
prefer numbers that cover an n-dimensional space more unformly. For
example, let's take a more detailed look at Monte-Carlo integration.

When using random numbers, the error involved in Monte-Carlo
integration goes down as 1/sqrt(N), where N is the number of points.
This means, e.g., if you use 10 times as many points, the error in your
estimation of the integral will be reduced by a factor of about
1/sqrt(10), or 0.32. Can we do better than this? It turns out that a
simple uniform grid of points has an error that goes as 1/N, which is
about 3 times better than the random case. However, a uniform grid has
the undesirable property that you have to decide on the grid spacing in
advance, and you can't simply add more points at a later time.

What we need is a quasi-random number generator that chooses numbers
that tend to fill the gaps in the sequence. There are several
possibilities, and we will explore one designed by a Russian
mathematician by the name of Sobel in the week 7 assignment.

Sorting

A frequent requirement is to sort quantities (numbers, character
strings, or more complicated structures) into some order (ascending,
descending, or something more complex). The C library provides the
function "qsort", an implementation of the famous quicksort algorithm.
It is usually best to use "qsort" than write your own sorting algorithm.

Gnuplot

Numerical integration

Here is an example of using
numerical integration to follow the motion of a planet around the
Sun:

Writing fast programs

Reasons for worrying about how fast a program is include:

if it is interactive, and reponsiveness is important,

if it is pushing the boundaries of practicality (e.g., it might
takes weeks to run).

Hints on improving speed:

use a clever algorithm.

identify the time-critical parts of the program, and concentrate
on improving those.

examine memory use as well (e.g., try to reduce the amount of
memory being used, and keep the usage localised as opposed to jumping
around randomly).

iterate most rapidly over the rightmost subscript in arrays,
since this will lead to better localisation of memory use (and hence
greater likelyhood of cache hits).

use optimisation (e.g., -O1, -O2, or -O3; see "man gcc" for
details).

try optimising for small program size (-Os).

in general, try not to be "too clever" - the C compiler can quite
often do a better job if your program is clear.

make the critical parts of the program as small (in terms of
bytes of instructions) as possible - they will then be more likely to
fit in the CPU's "instruction cache", resulting in better performance.

declare often-called functions as "inline", which eliminates the
expense of the function call and copying the arguments to/from the
stack. This technique should be used with caution, since it makes the
program larger, in which case it may no longer fit in the instruction
cache.

avoid printing out lots of unnecessary information. Formatting
and writing text to the screen is quite CPU intensive.

2D cellular
automata - Conway's Game of Life

In 1970, the mathematician John Horton Conway published the
description of a simple 2D cellular automata which he called the Game
of Life. Cells in a 2D grid were either "alive" or "dead", and their
state in the next generation was determined by their current state and
the number of nearest neighbours (from 0 to 8, inclusive). The rules
were chosen to simulate some properties of biological systems (e.g., a
cell would die of "overcrowding" if too many of its neighbours were
alive, and die of "lack of support" if too few of its neighbours were
alive).

If we are interested in following the evolution of the game for
many thousands of generations, it is advantageous to think of ways of
speeding up our simple implementation. The first thing to try is to
turn on full optimisation during the compilation, using the "-O4"
switch. The next step is to make the critical functions "inline" (see
here for the code). We can also think
about the algorithm, and realise that a lot of our calculation of the
number of nearest neighbours was involved in handling the special case
of being on the boundary; we can re-write
our program to separate out the
boundary case, thereby allowing a simplified (faster) function to do
most of the neighbour calculations, at the expense of increased
complexity in handling the evolution from one generation to the
next. Finally, we can try to use the fact that much of the Life
"universe" tends to be sparsely populated, allowing us to produce a
second neighbourhood-calculating function for the special case that the preceeding cell had
no nearest neighbours. These are by no means the only speed-ups
that are possible, but they give you some flavour of what is
possible.

The following table gives the time taken to follow one million
generations using a 850MHz Pentium III computer and the various tricks
from the last paragraph.

So, we have managed almost a factor of ten improvement in speed over
our original attempt.

To show why this might be useful, let's now alter our program to
automatically identify glider patterns (those that can
self-propagate). We will choose a brute-force technique where we
randomly seed the centre of our rectangular "universe" with alive/dead
cells, and then evolve the system until one of the boundary cells is
hit. When this happens we stop the evolution and print a 21x21 grid
around the cell, hopefully capturing a glider in full flight! Here is the code, and you can see that it
is a straightforward variation on the
earlier program. It typically finds a glider within 2 seconds on a
850MHz Pentium III computer.

The on-line manual pages

Use "man -k keyword" to search for information about "keyword". For
example, suppose you want to know something about random number
generators: "man -k random" on a GNU/Linux system will return output
containing lines like these:

The number in parentheses is called the section
number (e.g., section 3 is reserved for C functions, section 4 for
low-level kernel interfaces), and you need to specify this if there
are entries in multiple volumes. So, e.g., to read about the kernel
random number source devices, you would type "man 4 random".

The GNU C debugger "gdb"

When debugging a program, a common technique is to insert "print"
statements to display the values of variables. A much more powerful
technique is to use the GNU C debugger, "gdb". Other C compilers will
have debuggers as well.

Firstly, you must compile your program with the debug switch
(-g). It is also a good idea to disable optimisation with -O0.

gcc -g -O0 main.c

Then run your program using gdb:

gdb a.out

The following are some of the more common commands that are
available:

help - obtain helphelp {arg} - obtain help on topic {arg}list - list 10 lines around current positionlist {line} - list 10 lines around line number {line}list {beg},{end} - list lines from line number {beg} to {end}run arg1 arg2 ... - begin execution of the program, with the argumentsbreak line - suspend execution at the given line numberstep - steps (and executes) one line in your programprint {exp} ... - print the value of the given expressionsprintf "string", exp, ... - print expressions using format string(C)quit - quit gdb<ENTER> - repeats the last command (very useful with "step")

Arrays

In mathematics and physics you often need to group numbers together
into arrays, i.e., 1, 2, or
higher-dimensional structures.

Arrays are declared in C as follows (for example):

int counts[100];float temperature[1024];double mat[2][3];

In this example, "count" is an array that can hold 100 integers,
"temperature" is an array that can hold 1024 floats, and "mat" is a 2x3
array (or matrix) of doubles.

Note carefully that the first element of an array in C is element
number 0 (rather than 1 as in FORTRAN). While this may be confusing to
die-hard FORTRAN programmers, it is really a more natural choice. A
side-effect of this choice is that the last element in an array has an
index that is one less that the declared size of the
array. This is a source of some confusion, and something to watch
out for.

So, "counts[0]" is the first element of the array "counts", defined
above. And "counts[99]" is the last element. There are 100 elements
in total.

Initialising arrays

To initialise an array to have particular numerical values, specify
the initial values in a list within curly brackets. For example:

In this example, "primes" is initialised with the values of the
first
100 primes (trust me!), and the first two elements (temp[0] and
temp[1])
of "temp" are initialised to 5.0F and 2.3F respectively. The remaining
elements of "temp" are set to 0.0F. Note that we use the trailing "F"
on these numbers to indicate that they are floats, not doubles.

The array "trouble" in the above example contains three double
numbers. Note that its length is not explicitly declared, C is smart
enough to calculate the length for you.

Static arrays that are not explicitly initialised are set to
zero. Automatic arrays that are not explicitly initialised, have
undefined values.

Multidimensional arrays

Multidimensional arrays are declared and referenced in C by using
multiple sets of square brackets. For example,

int table[2][3][4];

"table" is a 2x3x4 array, with the rightmost array subscript
changing
most rapidly as you move through memory (the first element is
"table[0][0][0]", the next element is "table[0][0][1]", and the
last element is "table[1][2][3]").

When writing programs that use large arrays (say, more than
a few megabytes), you should be very careful to ensure that array
references in your program are as close as possible to being
consecutive (otherwise
you may get severe swapping problems, i.e., the memory is
swapped to hard disk, resulting in more than a factor of 1000
slow-down).

Initialisation of multidimensional arrays

This is best demonstrated with an example:

int mat[3][4] = { { 0, 1, 2, 23}, { 3, 4, 5, 24}, { 6, 7, 8, 25} }

Operations on arrays

If you wish to, e.g., add two arrays together, you have to do it
element-by-element yourself using a "for" loop. For example:

int a0[10], a1[10], a2[10];

for (int i = 0; i < 10; i++) { a0[i] = a1[i] + a2[i];}

Even something as simple as setting one array to have the same value as
another has to be done with a "for" loop. E.g. :

int a0[10], a1[10];

for (int i = 0; i < 10; i++) { a0[i] = a1[i];}

Character arrays

Arrays can be of any type. In C, you manipulate character strings
as arrays of characters, and operations on the strings (such as
concatenation, searching) are done by calling special libary functions
(e.g., strcat, strcmp).

Note that when calling a string-manipulation function, the end of
the
string is taken as the position of the first NUL character (which has a
numerical value of zero) in the
string.

Character arrays can be initialised in the following ways:

char str[] = {'a', 'b', 'c'};char prompt[] = "please enter a number";

In the example, "str" has length 3 bytes, and "prompt" has
length 22 bytes, which is one more than the number of characters in
"please enter a number", the extra byte is used to store a NUL
character (zero) as an indication of the end of the string. "str" does
not having a trailing NUL.

Pointers

If there is one thing that sets C apart from many other languages,
it is the pervasive use of pointers.

To understand pointers, it helps to have a mental image of the way
in which the memory of a
computer is organised. e.g., imagine that your computer has 128MB of
memory; then you could label each one of these 128 million bytes of
memory with a number: byte 0 would be the first byte in memory, byte 1
the second, and so on up to 128 million. These numbers are referred to
as the addresses of the
memory cells (note: I'm actually simplifying the true situation; in a
modern computer each separate process has it's own virtual
address-space,
and there are several layers of indirection involved in going from the
addresses that C uses, to the actual physical piece of memory on the
computer motherboard). A pointer
is then simply a variable containing a memory address. This is a very
natural concept when you
think in terms of the hardware of the computer, which is a viewpoint
that C encourages.

Suppose that "i" is declared as "int i". Then the address in memory
at which "i" is stored is given by "&i", where "&" is the unary address operator. One says
that "&i" is a pointer to "i". On a 32-bit computer such as a
typical PC, a pointer is 4 bytes in length (therefore allowing up to
4GB of memory to be addressed; this is an uncomfortably small number).

Pointers are declared by specifying the type of thing they point
at. For example,

int *p;

defines "p" as a pointer to an int (so, therefore, "*p" is an int,
hence
the form of the declaration).

Note carefully, then by declaring "p" as in the above example, the
compiler simply allocates space for the pointer (4 bytes for 32-bit
computers, 8 bytes for 64-bit computers), not for the variable
that the pointer points to! This is a very important point, and
is often overlooked. Before using "*p" in an expression, you have to
ensure that "p" is set to point to a valid int. This "int" must have
had space allocated for it, either statically, automatically, or by
dynamically allocating memory at run-time (e.g., using the "malloc"
function, see later).

Here is a rather convoluted example showing some of the uses of
pointers:

By using the GNU debugger, gdb, we can identify the actual memory
locations that the C compiler uses to store the variable in the above
program. It is then possible to draw up a table (see below) showing
the detailed operation of the program. You should follow this
example very carefully so that you understand what is going
on.

The memory addresses are shown in hexadecimal, and only the
rightmost three digits are shown (e.g., 98c) for convenience in
displaying the table. Memory addresses are always given in units of
bytes. The above table shows, for example,
that the variable "m" occupies the memory addresses 0xbffff98c,
0xbffff98d, 0xbffff98e, and 0xbffff98f. You will note that 16 bytes
are reserved for "msg", even though "msg" only needs 6 bytes (5 for
the characters in "hello" and one for the trailing null byte). The C
compiler won't guarantee any particular placement in memory, and may
leave gaps, particularly to align numbers with 4-byte boundaries
(i.e., so that their memory addresses are divisable by 4).

Note the very important point that the name of an array ("msg"
in the above example), if used without an index, is considered
to be a pointer to the first element of the array. In fact,
an array name followed by an index is exactly equivalent to
a pointer followed by an offset. For example,

Note, however, that "cp" is a variable, and can be changed, whereas
"msg" is a constant, and is not an lvalue (i.e., it can not
be used on the left of an equals sign).

Pointer arithmetic

You can add and subtract numbers to/from pointers, but the results
may surprise you. For example, if "p" is a pointer to an "int" (i.e.,
"int *p"), then "p++" actually adds 4 to "p"! The logic behind this is
that adding one to "p" should make it point to the next integer, which
has a memory address that is 4 bytes greater.

Pointers used as arguments to functions

We have already seen that C functions can not alter their
arguments. The simple reason for this is that when you call a function,
the compiler passes copies of the
values of the arguments to the function. For example, if you
have a function as follow:

void myfunc(int n) { n = 6;}

and you call it with

int i = 0;myfunc(i);

Then "myfunc" is passed the number 0. "myfunc" knows nothing whatsoever
about where this number came from. The zero is stored in the local
variable n. "myfunc" changed n to 6, but this doesn't have any effect
on i in the main program.

In order to change the value of a variable in the main program, you
need to give the function a pointer to the variable. In other words,
you tell the function whereabouts in memory the variable is. With this
crucial bit of information, the function can go ahead and alter the
value of the variable. For example,

void myfunc(int *n) { *n = 6;}

and you call it with

int i = 0;myfunc(&i);

Now, the value of i in the main program will be altered by the call to
"myfunc".

Here is an example of a function that swaps the values of
its two arguments, which must be pointers to integers:

void swap_args(int *pi, int *pj) { int temp;

temp = *pi; *pi = *pj; *pj = temp;}

If all the asterisks were left out of this routine, then it would
still compile OK, but its only effect would be to swap local copies
of "pi" and "pj".

Pointers and arrays

Consider an array declared as follows:

int a[10];

Then, as we have seen, a[0] is the first element of the array, and a[9]
is the last. The address of the first element is "&a[0]", and by
convention this can also be written simply as "a".

Structures

Any programming language worth its salt has to allow you to
manipulate more
complex data types than simply ints, floats, and arrays. You need to
have
structures
of some sort. This is best illustrated by example:

To define a structure, use the following syntax:

struct time { int hour; int minute; float second;};

This defines a new data type, called "time", which
contains
3 fields (stored in
consecutive locations in the computer's memory).

Logically, it makes sense to associate the various fields of "time"
together, since they are part of one physical entity: the time.

To declare a variable of
type "time", you could do the following:

struct time t1[10], t2 = {12, 0, 0.0F};

This defines "t1" to be an array of 10 "time" structures. The variable
"t2" is also a "time" structure, and is initialised to 12 hours, 0
minutes, 0.0 seconds.

For an example program using structures, see the solution to Q12 of
the sample mid-session test here.

To refer to an individual element of a structure, you use the
following
syntax:

Alternatively, and somewhat confusingly, if "p", say, is a pointer
to a structure, then you can access the elements of the structure using
notation like "p->hour". For example:

struct time *p; // p is a pointer to a time structurestruct time t = {1,2,3.0}; // t is a time structurep = &t; // p now points to tp->hour = 6; // this statement is exactly equivalent to...t.hour = 6; // ...this one

Structures can contain any type, including arrays and other
structures. A common use is to create a linked list by having a
structure
contain a pointer to a structure of the same type, for example,

where "person.next_person" is actually a pointer to a "person" struct.
Sounds complicated, but it is simple when you get the hang of it.

Structures are the first step towards object-oriented
programming (OOP), where most of the programming effort is
devoted to defining the best representation of the data, and the
operations which can be performed on the data.

In physics, a structure can be used to represent the state of a
physical system. For example, here is a structure that might be used to
represent the state of a particle in three-dimensional space:

String functions

The standard C libraries include a bunch of functions for
manipulating
strings (i.e., arrays of chars).

Note that before using any of these functions, you should include
the
line "#include <string.h>" in your program. This include-file
defines
prototypes for the following functions, as well as defining special
types
such as "size_t", which are operating-system dependent.

int strncmp(s1, s2, n) Like strcmp, except at most `n' characters are compared.

char *strncpy(s1, s2) Like strcpy, except at most `n' characters are copied.

char *strrchr(s, c) Like strchr, except searches from the end of the string.

int strstr(s1, s2) Searches for the string s2 in s1. Returns a pointer if found, otherwise the null-pointer.

size_r strspn(s1, s2) Returns the number of consecutive characters in s1, starting at the beginning of the string, that are contained within s2.

size_r strcspn(s1, s2) Returns the number of consecutive characters in s1, starting at the beginning of the string, that are not contained within s2.

char *strpbrk(s1, s2) Returns a pointer to the first character in s1 that is present in s2 (or NULL if none).

char *strerror(n) Returns a pointer to a string describing the error code `n'.

char *strtok(s1, s2) Searches s1 for tokens delimited by characters from s1. The first time it is called with a non-null s1, it writes a NULL into s1 at the first position of a character from s2, and returns a pointer to the beginning of s1. When strtok is then called with a null s1, it finds the next token in s1, starting at one position beyond the previous null.

File operations

<stdio> defines a number of functions that are used for
accessing files. Before using a file, you have to declare a pointer
to it, so that it can be referred to with the functions. You do this
with, for example,

FILE *in_file;FILE *out_file;

where "FILE" is a type defined in <stdio> (it is usually a
complicated structure of some sort). To open a file, you do, for
example:

Note the use of "r" to indicate read access, and "w" to indicate
write access. The following modes are available:

"r" read"w" write (destroys any existing file with the same name)"rb" read a binary file "wb" write a binary file (overwriting any existing file) "r+" opens an existing file for random read access, and writing to its end"w+" opens a new file for random read access, and writing to its end (destroys any existing file with the same name)

Once the file is opened, you can use the following functions to
read/write it:

int fread(void *ptr, int size, int nelm, FILE *fp) Reads nelm elements of data, each size bytes long, storing them at the location pointed to by ptr. Returns the number of elements read.

int getc(FILE *fp) Returns the next character from `fp', or EOF on error or end-of-file.

int putc(int c, FILE *fp) Write the character c to the file `fp', returning the character written, or EOF on error.

int fscanf(FILE *fp, char *format, ...) Like scanf, except input is taken from the file fp.

int fprintf(FILE *fp, char *format, ...) Like printf, except output is written to the file fp.

char *fgets(char *line, int n, FILE *fp) Gets the next line of input from the file fp, up to `n-1' characters in length. The newline character is included at the end of the string, and a null is appended. Returns `line' if successful, else NULL if end-of-file or other error.

Note that the above "functions" are actually defined as
pre-processor
macros in C. This is quite a common thing to do.

Multiple precision arithmetic

As an example of how to program in C, let's explore the topic
of multiple precision arithmetic. All the hard work will be done by
GNU's Multiple Precision Arithmetic Library (GNU MP), written by
Torbjorn Granlund.

Multiple precision arithmetic allows you to perform calculations
to a greater precision than the host computer will normally allow.
The penalty you pay is that the operations are slower, and that you
have to call subroutines to do all the calculations.

GNU MP can perform operations on integers and rational numbers.
It uses preprocessor macros (defined in gmp.h) to define special data
types for storing these numbers. MP_INT is an integer, and MP_RAT is a
rational number. However, since a multiple precision number may occupy
an arbitrarily large amount of memory, it is not sufficient to allocate
memory for each number at compile time. GNU MP copes with this problem
by dynamically allocating more memory when necessary.

To begin with you need to initialise each variable that you are
going to use. When you are finished using a variable, you should
free the space it uses by calling a special function.

MP_INT x, y; mpz_init(&x); mpz_init(&y);

/* operations on x and y */

mpz_clear(&x); mpz_clear(&y);

Let's now try a full program. For example, calculating the square
root of two to about 200 decimal places:

Command-line arguments, and returning a status to the shell

Up until now, our "main" function has always been declared as
"int main(void)". We have seen that the integer return value is
available to the UNIX "shell" in the "$?" environment variable, and
this can be used to alter the execution of scripts. It turns out that
"main" can actually take arguments, and these are traditionally given
as "int argc, char *argv[]", where argc is one plus the number of
command-line
arguments provided to the program, and argv is an array of character
strings containing the arguments. argv[0] is usually the name of the
program itself. An example will hopefully clarify this:

Allocating memory at run-time - malloc

The situation often arises that you don't know the size of a data
structure in your program at compile-time. For example, suppose you
are writing a program to read an 2D image from a file into the
computer's memory. If the size of the image can vary at run-time, it
would be nice not to have to pre-define the size of the memory array
in your program. C allows you to do this through the concept of
allocating and deallocating memory.

Allocating memory means
requesting the computer to give you access to a contiguous block of
memory. E.g., suppose you need a megabyte; you ask the computer "can I
use a megabyte of memory, and if so, can you please tell me where the
memory is?". You ask the question by calling the function "malloc",
which takes a single argument: the number of bytes of memory that you
want. malloc will attempt to allocate the memory, and if it is
successful it will return a pointer to the first byte of the memory
block (if malloc
was unsuccessful, it returns NULL; this is a
rare
enough occurence for reasonable memory requests that you should handle
it with "assert").

Deallocating memory means
telling the computer "I have now finished using this block of memory
that you previously allocated for me with malloc, so you can now have
the memory back again for some other purpose". You deallocate memory by
calling the "free" function.

Some important points to remember when using malloc:

malloc returns a pointer to void, i.e., "(void *)", which just
means that malloc itself knows nothing about what the block of memory
is to be used for, which is fair enough since you didn't tell it.
However, you do have to let the C compiler know, and you do this by
type-casting malloc's result (see the example below).

You should always call "free" to deallocate any memory that you
have finished with. Note carefully that the argument to "free" must be
a pointer obtained from malloc, and that you can only call free once
for a given block of memory.

After freeing memory, it is good practice to set any pointers
that referred to it to NULL, this will prevent you from inadvertantly
using deallocated memory.

The memory that malloc allocates can have random data in it. If
you need it to be zeroed, either do this yourself with a "for" loop of
use the "calloc" function (see "man calloc").

Here is an example to get you started:

#include <stdio.h>#include <stdlib.h>#include <assert.h>

/* Generates a two dimensional matrix, and fills it randomly with zeroes and ones. */

// And remove all information about where the memory used to be. // This is overkill in this example, since the program immediately // terminates, but it is good practice, in order to avoid // accidentally using free'ed memory.