In this lesson we add an update to our memory model in preparation for the next
lecture and we deal properly with the "peek-ahead" of
scanf() in an earlier lecture.
The two are quite closely related: as we shall see, in our linited use of
scanf() we have already been using memory addresses without even realising it.
As mentioned in our peek-ahead the scanf() function reads the
values of variables from the keyboard. Its format statement
is similar to printf()'s.
Consider a specific example:
We have two int variables j and k,
both initialised to zero.
We wish to use scanf() to read in an integer
and assign that value to j.
The user types in the value "3".
The scanf() function must:
Read the number "3" from the keyboard
Write the value "3" onto j's index card (so
that when we next go to read the value of j we
will read 3, not zero).
How do we tell scanf() what card to
write to?
Were it not for the peek-ahead in a previous
lesson our first attempt to read j might look this:
printf("Please enter an integer\n");
scanf("%d", j); // ERROR
C always passes values of mathematical
expressions to functions, never variables.
In this flawed example all
we are passing to the scanf() function is the
value of j, the number 0. Thus scanf()
does not know which of the variables j and k
we wish it to change, as the only information it has been
given is "zero" and there are two variables with that value..
(Obviously the scanf() function
cannot see our code to know what we are trying to do as the
function comes with the compiler and was written long before
we wrote our program.)
We can see this in its complete context below, where we have
also shown the index cards used to store the values of j
and k:
Code
Data
#include <stdio.h>
int main() {
int j = 0, k = 0;
printf("j and k are: %d %d\n", j, k);
printf("Please enter an integer\n");
scanf("%d", j); // ERROR
printf("%d %d\n", j, k);
return 0;
}
NB: the answer to the first of these
questions is very simple but incredibly important!
Given that passing the value of j
does not tell scanf() which index card to write the
new value on, what do we tell it?
Looking at the above figure of the code and its data what
number must we pass to scanf() so it knows which
card to write the value "3" to change the value of j?
What do you think is the meaning of &j
in scanf("%d", &j);?
We shall return to your answers to these questions below, after
we have discussed a little more about the computer's memory.
In the very early days of computers, programs were written in
an extremely low-level syntax called machine code.
There were no variable names and programmers had to manually
keep track of what data was stored where. Locations were
identified by numerical address, just like our index cards,
and the programmer would have to think in terms like:
Store the floating-point value 83.2 in
location 12.
Store the floating-point value 4.1 in location 16.
Multiply the floating-point number stored in location 12 by the floating-point number stored in
location 16 and store the result in
location 4.
The notation float@12 is an example of
something called pseudo-code. Pseudo-code is
something that isn't actually valid but is just there to
explain something. We shall use this particular pseudo-code
quite a lot.
If we use the notation "float@12" for
"the floating-point number stored in location 12"
then this might have been coded (with comments) as:
float@12 = 83.2; // mass
float@16 = 4.1; // acceleration
float@4 = float@12 * float@16; // force
Obviously, such programs were rather hard to understand: one
big problem was simply remembering which value was stored at
which location.
The invention of the compiler allowed the
programmer to give programmer-friendly
names to the individual memory locations . The compiler also took care of the job of
deciding which memory location was given which name to avoid
the danger of the programmer accidentally using the same
location twice. This allowed the above instructions to be
rewritten as something like:
Original code
"Compiled" pseudo-code
float mass, acceleration,
force;
mass = 83.2;
acceleration = 4.1;
force = mass * acceleration;
# Store mass at 12, acceleration
# at 16 and force at 4, as floats
float@12 = 83.2;
float@16 = 4.1;
float@4 = float@12 * float@16;
The compiled executable is the same as before but the
original source code was much easier for the programmer to
understand.
The "location number" above is referred to as the variable's
address and is a very important
concept. If we know the address (and type) of a variable we
can access and change its value, bypassing the
programmer-friendly name altogether.
Study the above example
To understand how the computer treats variables and
stores data.
Spend a few minutes studying this simple example
to satisfy yourself
that the "compiled pseudo-code" version does what we expect.
The concept of data's address
becomes extremely important in
the next few lessons.
View the Humanly and Computer executable versions
To cement our understanding of how the computer treats
variables, and to demonstrate how the simple
feature of being able to replace memory locations by
names makes our program much easier to understand.
Open the following link in a new window:
"total cost" program version 2 .
(This is just the very first example program we ever saw
back in the "Introduction to C" but
with one of the advanced options shown by default.)
You will see a new option at the top of the code, currently
set to "Program view".
Click on "Next step" so that the index cards with the
data values appear.
Now toggle between "Programmmer view" and "Combined
view".
How much more difficult is the program to understand
when we switch from "Program view" to "Combined view"?
Run through the code several times in "Combined view".
(You can reset the program by reloading the page.) Notice how
in each case the "combined" variable name tells you which
card the data is written on, as well as whattype it is.
Finally, choose "Computer view" and run through the code
again until you are satisfied that all three views do
exactly the same thing.
How much more difficult is the program to understand
when we switch from "Program view" to "Computer view"?
An update to our memory model
So far we have presented a simple model of a human computer
executing our instructions and writing the variable values
on numbered index cards. The memory model of the actual
computers we use is very similar and almost as simple (some
would say too simple, as we shall see in the next lecture).
Computer programs store
their temporary, "running" data in the computer's memory
(RAM - for Random Access Memory). The computer's
memory is measured in bytes and we can think of it being
arranged in one huge block with its bytes numbered 1, 2,
... 4381712, etc..
Typically it takes four bytes to store an int, so
when the computer encounters a line like:
int i, j;
it reserves two chunks of four bytes each to store the values
of these two variables:
Here we have shown i and j as being
stored next to each other, but the computer is under no
obligation to do so.
The location where the variable is stored in
memory is then the address of the
variable. In the above example the variable i has
the address of 400.
In our "card index" model of our data this value is also
the address of the index card (which is why doubles
always have an address which is a multiple of 8).
There is no variable
with address 401, but that doesn't matter, as the
compiler has set aside bytes 400-403 inclusive to use for
storing the
value of i. From now on whenever the
compiler encounters the name i it will replace it by int@400
and it will replace j by int@404.
Strictly speaking at the compiler assigns the
variable an address to be added to an offset to be
determined when the function starts running, but we shall
ignore this distinction most of the time.
A variable name is just a programmer-friendly
name for the contents of a particular part of the computer's
memory.
As we saw in the previous mini-exercise this has the
side-effect of making our program almost impossible to
understand!
The instructions generated by the
compiler contain only constants and the memory addresses of
variables. To access the values of variables the compiler
generates instructions to read from or write to those
addresses.
So far the C program stepper has just shown us the index-card
model. variables and their values. It can also show us a
visualisation of the computer's memory.
Compare index-cards and the memory table
To see how our index-card model leads on to how the
computer stores its data.
Click on the "Show Advanced options" button at the bottom left
and a new control should appear at the top right, which
will initially say "Show index cards".
Switch between the three options (Index cards, memory table or both)
to see how our "index card" model relates to how the computer stores its data.
Note in particular that the variables have the same address in each case.
Now go step the code several times to familiarise yourself with
the new memory model.
Hopefully your answers to the warmup questions were:
The scanf() function needs to know the card number
(ie the address of the card) not the number written on it.
&x tells us the address of x (ie its card number).
Of course, this carries over directly to our updated model of the computer's memory.
C has an operator for (almost) everything and if we ever want
to know where a particular variable is stored in memory then C
provides the & operator and even
a printf() format %p to go
with it:
printf("i has the value %d and is stored at %p\n", i, &i);
In our example, the above code would print out the value of i
followed by its address (location in memory) which is "400",
although it would almost certainly print it out in
hexadecimal. It's most unlikely you'll ever need to do this.
The & operator can tell
us the address of a variable, i.e.
where it is being stored, which allows us to bypass the
"programmer-friendly" variable name altogether.
The & operator is unusual in that it makes no attempt to evaluate the
object it operates on, it just finds its address. (Later on in the module we shall
encounter a similar operator which tells us the number of bytes needed to store the
value of a variable.)
The variable type determines how many bytes are read and how to
interpret them
When the computer is reading the value of a variable from its memory then
if the variable is a float the computer will have to retrieve
four bytes of memory, if it is a double it will
retrieve eight bytes. Less obviously, it will interpret the 32
bits it retrieves for a four-byte int differently
from those of a four-byte float.
An Intel processor stores the number 7 in
binary as follows:
7 stored as an int: 00000111 00000000
00000000 00000000
7 stored as a float: 00000000 00000000
11100000 01000000
(This is because for floating-point numbers some of the bits are used
for the (binary) exponent.)
Whilst we never need to know those values we can see that they
are different. It turns out that if we write 7 as a float but
mistakenly read it as an int the processor will interpret that
as 1088421888. If we write 7 as an int but read it in as a
float the processor will interpret it as 9.80909e-45.
Thus it's not enough to know the address of something in
memory: in order to be able to use that address we have to
tell the compiler the type of the object that is stored there.
The value of a variable is the binary contents of
the memory stored at the address of that variable interpreted
according to the type of that variable.
For a variable x the expression &x
is said to point to x and the
memory address of an object whose type is known is referred to
as a pointer expression.
It follows that we can make two basic mistakes when using a
memory address: we can use the wrong value or we can use the
correct value but tell the compiler it points the the wrong
type. We will see both of these below when we discuss errors
with scanf().
When using a memory address there are two
possible mistakes:
We may use the wrong value of the address.
We may use the correct address but tell the compiler it
points the the wrong type of object, such as a float
rather than an int.
Apart from the last sub-section when we deal with
reading from and writing to files, this section is just an extended
recap of the section on scanf() in the earlier lecture
together with some entertaining examples of what happens when we go
wrong.
C provides the scanf() function
to read in variables from the keyboard (or whatever the
default input for our program is).
scanf() reads data from the keyboard.
scanf() is a "user-friendly" input
function, it skips over spaces and new lines so if we are
required to input two integers we could type in the two
integers either both on the same line, or one on each line.
Unlike printf() it's not normal to put any text in
the format string, or \n at the end, if we want to read in two
integers the format is just: "%d %d".
To read in more than one value just use the two
individual formats separated by spaces such as "%d %d".
Do not put in commas or \n at the
end.
So to read in two integers i and j from
the keyboard we write:
scanf(), and its siblings are probably
the only time you will ever need to use the "&" operator.
printf("Please enter two integers ");
scanf("%d %d", &i, &j);
We always pass memory addresses
to scanf() so variables which are arguments to scanf()
must have an & in front of
them: scanf("%d", &k);
Companies that deliver washing
machines will take away your old one (for an extra charge)
There's no point in telling the delivery company "I'm not
going to tell you the address to deliver the new one to, just
that the old machine is a broken-down washing machine with the
serial number xyz1234". They need to know the address to deliver it to, not the model
(value) of the existing machine.
Knowing the address and type of something is
much more powerful than just knowing its value as it allows us
not only to know its value but also to change it.
This is one of the rare exceptions to C's helpful rule of
treating all floating-point operations as double. It
does this because it has no choice. Consider the following:
#include <stdio.h>
int main() {
float littlevar; // Four bytes
double bigvar; // Eight bytes
printf("Please enter two numbers:\n");
scanf("%g", &littlevar);
scanf("%lg", &bigvar);
printf("%g %g\n", littlevar, bigvar);
return 0;
}
The first call to scanf() requires us to read in a
number and then write that number into the four
bytes starting at the address (memory location) of littlevar,
the second writes eight bytes to the memory
location of bigvar. Since all scanf() is
told about littlevar and bigvar is their
memory addresses, which are just numbers, we have to tell it
what they are the addresses of.
The scanf() function is unusual in that the number of
arguments depends on the format string. Thus
scanf() is not protected by C's usual prototype argument
checking as its prototype just says its arguments are a
character string possibly followed by other arguments of unknown type. If we
give scanf() the address of
a float when it needs the address of an int, or
get the number of arguments wrong then things will go
very badly wrong indeed (see the example below).
Turning on advanced argument checking
The compiler we use can understand
format strings and check we have provided the correct number and
type of arguments if we turn on the advanced
options: -Wall -Werror . The first of these warns us, and is enabled by default in the on-line compiler. The second
causes the compiler to treat warnings and errors and refuse to
go any further). Thus if you use your own computer
we strongly recommend you
enable advanced warnings.
If we get formats wrong
If we do not turn on the advanced argument checking
we lay ourselves open to all sorts of errors with formats:
If we use %g instead of %lg we will write
only the first four bytes of an eight-byte double.
If we use %lg instead of %g we will write
eight bytes into a four-byte float float and trash whatever we stored next.
If we use %d instead of %g or vice we will write the correct
number of bytes but they will contain the wrong bits.
Using the wrong format for the variable type will always
produce major problems. The following horrific example first confuses %d
and %g, "accidentally" using them for a float and int
respectively. We then read in only four bytes of an eight-byte double.
Finally we do something even worse: we use %lg for
a float thus writing eight bytes into the location of a
four-byte object. This has the effect of trashing the variable
stored after it.
// This has deliberate error in all the formats
int main() {
int k = 1;
float floatvar = 0;
double bigvar = 0;
printf("Please enter some numbers:\n");
scanf("%d", &floatvar);
scanf("%g", &bigvar);
scanf("%g", &k);
k = 10;
scanf("%lg", &floatvar);
return 0;
}
Take careful note of the values of the input at the bottom.
Notice how the values written to the emory bear no relation
to the input.
Notice how reading four bytes into a float trashed whatever was
stored after it.
The compiler can warn about us errors in scanf()
- make sure you enable the checks!
When pointers attack: right value but the wrong type
This is the first in a number of examples in these lectures
where making mistakes in our handling of computers memory using
pointer expressions leads to strange errors and random
behaviour. In this case we have given scanf() the
correct memory addresses but have made a mistake in saying what
type of object it is the address of.
This has been our first use of the fact that knowing the type and address
of an object (a "pointer to" the object) gives us the ability to read and change its value, and
that if we pass its address to a function that also knows its type
then that function can also read and change that value. This extremely
simple and important concept is the basis for all
high-level data features in C, not just this example.
Of course, we don't yet know how to write functions that receive and
use these addresses. In future lectures we will learn not just one but
three ways to do this, two of which are "convenience" methods for
specific, frequently used situations in much the same way as the for()
loop doesn't do anything that the while() loop couldn't do
but just conveniently packages several things together.
For the time being the important thing is to understand the principle that knowing the type and address of an
object gives us the ability to read and change its value so that we
are ready to understand the practice of doing it in the following
lectures and to use this to do things we would otherwise be unable to do.
So far, we have always written to the standard output or stdout for short which is usually the
terminal window. Similarly scanf() reads from standard
input or stdin for short, usually the
keyboard.
Reading from, or writing to, files on the computer is very
similar once we have opened the file we wish to read from or
write to.
We shall show some individual code fragments first followed by
a couple of short complete examples.
Opening a file
The fopen() function opens a file for
reading ("r") or writing ("w"). Note Opening an existing file for writing
over-writes what was already there so there is also an append
option ("a") which is like writing but appends it to
the existing content.
FILE *infile;
infile = fopen("input.txt", "r");
(See below for a discussion about the "FILE *"
variable.)
fopen("myfile.txt", "r") opens
"myfile.txt" for reading. Use "w" to write to a file or "a" to add to the
end of it.
Reading and writing from and to files
Once we have opened a file we can use the fscanf()
and fprintf() functions read from, or write
to, that file . They behave exactly like scanf() and printf()
except for a "file" argument immediately before the format.
fscanf(infile, "%d", &input);
printf("The value is %d\n", input);
fscanf() and fprintf()behave
exactly like scanf() and printf() except for
a "FILE *" argument immediately before the format.
Closing the file
The fclose() function closes a
file when we've finished with it. (All files are automatically
closed for us when the program finishes.)
fclose(infile);
Example
As an example, let us revisit our "Hello, world" program, this
time making it write to a file:
/*
* "Hello world" - to a file!
*/#include <stdio.h>
int main() {
FILE *outfile;
/* Open a file for writing */outfile = fopen("helloworld.txt", "w");
fprintf(outfile, "Hello, world\n");
fclose(outfile);
return 0;
}
The first thing we see is the highlighted FILE *
variable called outfile. The precise meaning of FILE *
varies from system to system and is defined in stdio.h;
but whatever system we are using we can always use FILE *
and it will mean the right thing for that system.
Don't forget the * !
We shall see in a later lecture that this signifies a variable
whose value is the memory address of
something else.
We then open the file using fopen(). The first
argument to fopen() is the name of the file we wish to
open. This is the only time we use the name
of the file, after that we just refer to it using the
variable outfile. The second argument to fopen()
tells the system how we want to use the file; "w"
means to write to the file, over-writing any existing contents
of the file, if any. We could also have used "a" to append our new data after the existing
contents of the file. As mentioned above, "r" is used
for reading.
We then just use fprintf() to write to the file.
This is exactly the same as printf() except for the
additional first argument which is the FILE *
variable, outfile, not the file name.
Finally, we close the file, again using outfile, not
the file name. This step is optional in this case, as the
program is about to exit anyway.
The only time we use the name of
the file is as an argument to fopen(). All other
functions take the FILE * variable as an
argument.
At the start of your program declare a FILE *
variable with a suitable name to show it's for output.
Now call fopen() to open (and hence create) a file
for writing. Make sure the file name has the form "someletters.txt"
such as "hello.txt", "output.txt" etc..
The on-line compiler will show the contents of any files
you create but only if their name ends in ".txt" and some characters
cause problems in file names.
Modify the call to printf() to use
fprintf() with your FILE * variable as
the first argument.
Build & run. Check the output is correct.. The compiler should show you the contents of your file.
What if we can't open the file? - NULL and exit()
Our program has a weakness in that when it tries to open the
file "helloworld.txt" it doesn't check to see if the call to fopen()
failed. This might be because the disk was full, for example, or
because we are trying to create a file inside a directory (or
"folder" in Mac-speak) we don't have permission to write to.
This raises two questions: "How do we know if fopen()
failed?" and "what do we do if it does?".
If fopen() fails it returns the special value NULL, defined in stdio.h. We can
then test for failure using:
if ( outfile == NULL ) ... /* We could not open the file */
return returns from that function, exit()
exits from the whole program.
What we choose to do then this will vary according to our
program but the simplest thing is just to print a warning
message and exit from the program. We can do this from inside
any function, not just main(), by calling the exit() function with an integer argument.
This is defined inside the #include file stdlib.h ; the first time we have used
this file. Just as when we return from main(),
the convention is to use non-zero for failure.
We demonstrate this by making our code into a function:
#include <stdio.h>#include <stdlib.h>/*
* Write "Hello world" - to a file, exiting on failure
*/
void writehello() {
FILE *outfile;
outfile = fopen("helloworld.txt", "w");
if ( outfile == NULL ) {
printf("I cannot open the file\n");
exit(1);
}
fprintf(outfile, "Hello, world\n");
fclose(outfile);
}
Always check to see if fopen() returns NULL
and if so take appropriate action.
Deliberately fail to open a file
To see what happens if we don't check for NULL
Go back to your previous "file opening" program.
Modify your file name by putting a (forward) slash /
in it, eg "out/put.txt". This will cause the call to fopen() to fail.
Build & run. What happens?
Now after the call to
fopen()
use an if() to check that your FILE *
variable does not have the value NULL. If it does
have the value NULL, print a message and call
exit(1). You should #include<stdlib.h> for
the definition of exit().
Build & run. Check the output is correct.
Now modify your code so that instead of calling exit()
you instead put your call to fprintf() inside an else
so that it is only called if the FILE *
variable is not NULL.
This illustrates the fact that sometimes it's OK for a call
to fopen() to fail, or that we may wish the program to continue for
some other reason.
Build & run. Check the output is correct.
Finally, fix your file name by removing the /.
Build & run. Check the output is correct.
Reading from a file with fscanf()
This works in exactly the same way:
/*
* Read an integer from a file
*/#include <stdio.h>#include <stdlib.h>
int main() {
int input;
FILE *infile;
infile = fopen("input.txt", "r");
if ( infile == NULL ) {
printf("I cannot open the file\n");
exit(1);
}
fscanf(infile, "%d", &input);
printf("The value is %d\n", input);
/* Do something interesting */
fclose(infile);
return 0;
}
It's worth noticing here that since we are inside main()
we could have used either exit() or return.
Also notice how we have been slightly paranoid by printing out
the value of input immediately after we have read it.
This is a good idea!
How do we know when we have run out of input?
When reading data from a file, we may not know in advance
exactly how many data values there are in there. Fortunately C
makes it easy for us as scanf() and fscanf()
both return the number of values they have successfully read in
or a special value called EOF if they have reached the
end of the file. This allows us to write simple loops such as:
while ( fscanf(infile, "%d", &i) == 1 ) {
... /* Do something interesting with i */
}
Had we been reading in two data values we would have checked
that fscanf() had returned the value 2:
while ( fscanf(infile, "%d %d", &i, &j) == 2 ) {
... /* Do something even more interesting with i and j */
}
This tends to a be a lot easier that, say, writing the number
of data values at the start of the file. It's much less useful
when reading from the screen as scanf() just assumes
it's waiting for the user to type the numbers in.
scanf() and fscanf() return the
number of values they have successfully read in. This can be
extremely useful as the control of a while() loop.
The three defaults: stdin, stdout and stderr
We have briefly mentioned standard output (stdout) and
standard input (stdin) above. Specifically, C sets up
three default channels for input and output.
stdin is used for input
scanf("%d", &i) is identical to fscanf(stdin,
"%d", &i)
stdout is used for output
printf("Hello, world\n") is identical to fprintf(stdout,
"Hello, world\n")
stderr is used for errors, warnings and diagnostics
Thus it's good practice to use fprintf(stderr, ...) for
error messages, rather than printf(...). For example:
if (infile == NULL ) {
fprintf(stderr, "I cannot open the file\n");
exit(1);
}
is better than using printf("I cannot open the file\n")
.
Normally stdout and stderr both go to the
terminal window but they can be sent to different places.
We won't penalise you for sending all your error messages to stdout
using printf() but you should be aware that it's
better practice to use stderr.
stdin reads from standard
input, stdout writes to standard
output and stderr writes to
standard error. Eg: fscanf(stdin, "%d", &k) is the same as scanf("%d",
&k) fprintf(stdin, "%d\n", k) is the same as printf("%d\n",
k)
Let's remind ourselves of our "input checking loop"
and slightly extend it to check we have read in two numbers
and use stderr.
Reminder: what we are trying to do
Often we need to read in some values that must be in a
particular range, or satisfy some other criteria. If the data is
invalid the worst thing we can do is to just let the program
carry on with the wrong data. (We gave a short example
of this in the lesson on loops, here we build on it a little.)
Always check input is valid and do something
helpful rather than just carry on.
Error checking is tedious but necessary.
The wider the program's user-base the better the
error checking must be.
A program we will only be using ourselves can expect the user
to be reasonably tolerant if it crashes when we enter the wrong
number. A program people pay to use will have to be very
tolerant of its users!
It is better to have a few standard techniques we
use as often as possible than for every case to be treated
differently.
Example: noughts and crosses
Consider the example of a player entering the co-ordinates of
the square they wish to play into a noughts and crosses program.
The person is quite likely to make a mistake but would
like the chance to fix it.
We might start off by writing something like this:
printf("Please enter the x and y coordinates in the range 1-3 ");
scanf("%d %d", &x, &y);
This fine assuming the user hasn't made a mistake. There are
two basic mistakes they may make:
The co-ordinates of square might be outside of the range
one to three. (An invalid square.)
The square may have valid co-ordinates but already have
been played.
It would be very irritating to be playing noughts and crosses
and the program to quit or crash becasue we entered "4" when we
meant to type "3", or if the square had already been played. So
we need the program to handle this reasonably gracefully and to
give the user a chance to correct their error. Notice too that
there are two possible errors and it would be nice to print a
different message for each one.
What if scanf() can't read anything?
A rarer problem is if scanf() can't read any
numbers at all. This can happen when, unknown to use, stdin
is actaully being read from a file if if the user accidentally types
a letter such
"q" instead of a number. We shall see later in the course how to give the
user another chance if they type aletter, but for the time being we shall
just quit if we don't read in the expected number of values.
There are two possible ways of handling the situation when a
users enters an invalid square into noughts and crosses:
We could simply accept the numbers and pass them onto the
rest of the program. The problem with this is that it becomes
very difficult to recover the situation when the program
discovers the input is incorrect.
We could enclose our scanf() inside a loop,
immediately check the input is valid and only exit the loop
when it is. We then end up with a piece of code that reads in
data and guarantees it is valid. This has two advantages:
It's much easier as it's completely self-contained.
Although it's a bit of effort the first time it gives
us a "recipe" that we can use in any situation just by
changing the error test(s) and diagnostic message(s).
This suggests a reasonable course of action when reading data
would be to put our scanf() statement inside a loop
and:
Check the input for validity.
Check for each possible error in turn and if it's wrong
print a helpful error message exlaining what the problem is
and try again.
Advanced: After a few failed
attempts just give up and exit the program. (See later.)
Consider our noughts and crosses program and imagine we have
already written a function squareisplayed() to tell us
if a square has already been played. We can put our scanf()
statement inside a loop as follows:
//
// Demonstrate the break statement as part of a noughts and crosses program
//
#include <stdio.h>#include <stdlib.h>
int squareisplayed(int x, int y); // Written elsewhere
int main() {
int x, y;
printf("Welcome to the noughts and crosses program\n");
while ( 1 == 1 ) {
printf("Please enter the x and y coordinates in the range 1-3 ");
if ( scanf("%d %d", &x, &y) != 2) {
fprintf(stderr, "Could not read x and y coordinates\n");
exit(99);
}
else if ( x < 1 || x > 3 || y < 1 || y > 3 )
printf("\nThe inputs are in the wrong range, please try again.\n\n");
else if ( squareisplayed(x, y) )
printf("\nSorry, the square (%i, %i) has already been played\n", x, y);
else
break;
}
printf("Your move is: (%d, %d)\n", x, y);
return 0;
}
Notice the logic here: first we check that scanf() was even able
to read in two numbers at all. If not we exit the program.
(Again, we shall see
later in the course how to throw away whatever the user typed to give them
another chamce if they accidentally typed a letter instead of a number.)
We then look for each possible error in turn
(invalid square, or square already played). If one error
condition is true the appropriate part of if() is
triggered and a message is printed. Control passes to the end of
the if() and immediately hits the end of the loop
which then repeats.
Only if none of the "wrong data" tests is true does the program
reach "else" branch containing the break and
the loop quits.
If any of the error tests is true the message will get printed,
the rest of the if will be skipped and the program will hit the
end of the loop and go round again. Only if all of the error
tests fail with the break statement be reached and the
look exit.
Variation: an infinite for(;;) loop
Another popular way to write an infinite loop is to use an
infinite for(;;):
for (;;) {
...
}
Note we have still got the two semi-colons inside the for(;;).
This works because in a for() loop if the test is
absent it is assumed to be true. The apparent weakness of this
construction (it looks strange!) is also its strength: it look
so strange it's hard to mistake it for anything else.
The next time you are running one of your
programs that expects you to type in a number try typing a
letter instead! What happens? We shall deal with this in a later
program.
When reading data from the keyboard check it's
valid and if not print an error message and give the user
another chance.