Skip to content
Physics and Astronomy
Home Our Teaching Resources C programming Memory, input and output
Back to top
On this page
Contents

Memory, input and output

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.

Variables and memory

Warmup: scanf() and the &

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:

  1. Read the number "3" from the keyboard
  2. 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;
}
Step through this code

The user enters the value "3".
j (int)
132
0
 
k (int)
134
0

Questions:

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?

  1. 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?
  2. 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.


The computer stores the values of its variables in its memory

Some Horrible History

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.

  1. Study the above example
  2. To understand how the computer treats variables and stores data.
  3. 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.

  1. View the Humanly and Computer executable versions
  2. 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.
  3. 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".
  4. Click on "Next step" so that the index cards with the data values appear.
  5. 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"?
  6. 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.
  7. 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:

              -------------------------------------------------
Byte number:  | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 |
              -------------------------------------------------
Used for:     | <-------- i --------> | <-------- j --------> |
              -------------------------------------------------

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.

An update to the stepper

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.

  1. Compare index-cards and the memory table
  2. To see how our index-card model leads on to how the computer stores its data.
  3. If you have closed the "total cost" program version 2 then reopen it in a new window.
  4. Start the program so that the index cards appear.
  5. 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".
  6. 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.
  7. Now go step the code several times to familiarise yourself with the new memory model.

&i tells us where the variable i is stored in memory

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.

Mistakes with memory addresses

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:
  1. We may use the wrong value of the address.
  2. 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.

Reading in variables with scanf()

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);
Step through this code


Note the &s - see below.

The above code will allow us to type in two integers and will set the values of i and j to the two values we have just typed.

Don't forget the the &

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.

The format to read a double is "%lg"

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;
}
Step through this code


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 format to read a double is "%lg".

Checking the arguments to scanf()

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;
}
Step through this code

 
  1. Step through the above example.
  2. To see what happens when we get formats or arguments to scanf() wrong and we have not enabled advanced error checking.
  3. Step through this horrifying but fun example.
  4. Take careful note of the values of the input at the bottom.
  5. Notice how the values written to the emory bear no relation to the input.
  6. 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.

Looking ahead

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.

Writing to, and reading from, files

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.

  1. Write to a file
  2. To practice opening and writing to a file.
  3. Create a new on-line program in a new window, with a suitable title and opening comment.
  4. At the start of your program declare a FILE * variable with a suitable name to show it's for output.
  5. 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.
  6. Modify the call to printf() to use fprintf() with your FILE * variable as the first argument.
  7. 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.

  1. Deliberately fail to open a file
  2. To see what happens if we don't check for NULL
  3. Go back to your previous "file opening" program.
  4. Modify your file name by putting a (forward) slash / in it, eg "out/put.txt". This will cause the call to fopen() to fail.
  5. Build & run. What happens?
  6. 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().
  7. Build & run. Check the output is correct.
  8. 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.
  9. Build & run. Check the output is correct.
  10. Finally, fix your file name by removing the /.
  11. 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)

Checking for valid input

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:

  1. The co-ordinates of square might be outside of the range one to three. (An invalid square.)
  2. 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.

Approaches to handling the wrong input

There are two possible ways of handling the situation when a users enters an invalid square into noughts and crosses:

  1. 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.
  2. 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:
    1. It's much easier as it's completely self-contained.
    2. 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:

  1. Check the input for validity.
  2. 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.
  3. Advanced: After a few failed attempts just give up and exit the program. (See later.)

Infinite loops and the break statement

The break statement breaks out of the innermost switch() statement or loop (but not an if()).

Putting it together with an infinite loop

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;
}
Step through this code


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.

This loop forms the basis of our optional better input loop.

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.

Summary

The text of each key point is a link to the place in the web page.

Variables and memory

Mistakes with memory addresses

Reading in variables with scanf()

Checking the arguments to scanf()

Writing to, and reading from, files

Log in
                                                                                                                                                                                                                                                                       

Validate   Link-check © Copyright & disclaimer Privacy & cookies Share
Back to top