Skip to content
Physics and Astronomy
Home Our Teaching Resources C programming 8.html
Back to top

switch(), external variables and unformatted input

In this lecture we shall introduce the switch() statement for choosing (switching between) one of several choices and variables that are external to any one function.

We will then move on to unformatted input, for reading text containing spaces and any other characters,

The switch() statement

A situation that can occur quite often is the need to choose between one of several possibilities and take the appropriate action for each. This could be done with a large if() statement but it is rather clumsy:

// This works but is a bit clumsy 

  int number;

  // Assign a value to number... 
  if ( number == value1 ) {
    // ... Do something 
  }
  else if ( number == value2 ) {
    // ... Do something else again 
  }
  else if ( number == value3 || number == value4 ) {
  // ... Do something else yet again 
  }
  else {
    // ... Do yet another thing 
  }
 

C provides a special statement for dealing with this situation, the switch() statement, subject to two conditions:

  1. The expression that decides the choice must be an integer.
  2. The options must be constants and must all be different.

The outside of a switch() statement has the same familiar form as an if() or while() statement:

  switch (integer-expression) {
    ...
  }

The { ... } block contains the options to be chosen (switched) between. They have the general form:

  case integer-constant:
    statements;

The program leaps to the case whose value equals that of the control expression, jumping over any intervening code without executing it.

To illustrate, consider a simple menu:

Reminder:C joins several strings separated by spaces, without commas, into one large string. And '\t' is the tab character.

  int option;

  while ( 1 ) {
    printf("\n Menu\n\n"
           "\t 1. Some option\n"
           "\t 2. Another option\n"
           "\t 3. A third option\n\n"
           "\t 0. Quit\n");
    scanf("%d", &option);
    
    switch (option) {
    case 0:
      printf("Goodbye, thanks for using the switch program\n");
      return 0;
      
    case 1:
      printf("You chose option one.\n");
      break;
      
    case 2:
      printf("You chose option two.\n");
      break;
      
    case 3:
      printf("You chose option three.\n");
      break;
      
    default:
      printf("Unknown option: %d\n", option);      
    }
  }
Step through this code

 

To act on the choice entered by the user, we can immediately follow this with a switch() statement as follows:

  

Within the switch() statement, the program jumps to the choice (technically referred to as a label) that matches the value of the expression. If none of them match control jumps to the default label. A few things should be noted:

1. The choices (labels) are followed by colons

Also, it's conventional to "pull back" the labels to align them with the switch() statement itself.

2. The break statement

The break statement breaks out of a switch() statement in exactly the same way as a while(), for() or do...while() loop. The program immediately jumps to the statement following the switch() statement's { ... } block.

This is by far the most common use of the break statement as the program does not break out of the switch() statement when it encounters the next label, it just carries on as if it were not there.

A common use of this is to assign the same action to two or more cases. We shall see some examples of this below.

There is no need for for a break statement after label 0 as the return returns from the whole function so label 1 would never be reached if option had had the value zero.

A common bug is to forget the break statement. If you deliberately omit the break statement I suggest putting in a comment to indicate this, except in the case where one label immediately follows another. An example of this is shown next week.

3. The default label

Rather like the else of an if() statement, the default label applies if none of the other values match. Just like with else it is optional.

The default label can occur anywhere inside the switch(), but for obvious reasons it is usual to put it at the end.

If there is no default label and none of the other labels match, the program jumps over the entire { ... } block.

Example: multiple labels with the same action

Consider a simple function to test if a character is a vowel. This makes use of the fact that in C characters are one-byte integers and so can be used as the labels of a switch().

/*
 * Return one if argument is a vowel, zero if not
 * Demonstrates the switch statement
 */
int isvowel(char c) {
  switch (tolower(c)) {
  case 'a':
  case 'e':
  case 'i':
  case 'o':
  case 'u':
    return 1;

  default:
    return 0;
  }
}
 

In this case either of the letters 'a', 'e', etc. gives rise to the same action. Again there is no need for for a break statement following a return statement.

Static and global variables

So far all of the variables we have used have been of a type called automatic: variables declared inside of functions that automatically come into existence when the function starts and automatically disappear when the function returns.

Two variations exist.

Static variable

Static variables are declared inside functions like any other but have the qualifier static in front of them. Unlike automatic variables they are permanent and if a function is called several times the variable retains its value between calls.

Here is an example of a diagnostic to count how many times a function has been called, for example if we suspect the program has gone into an infinite loop and is repeatedly calling the function:

  void myfun(int somearg) {
    static int called;

    ++called;
    if ( called > 1000 )
      printf("myfun has been called %d times, it has argument %d\n", 
        called, somearg);

    ... Body of the real function here
  }

It's very hard to find a use for a static variable that doesn't involve it being initialised, hence the rule.

Unlike automatic variables static variables are initialised to zero when the program starts, this value of zero can be changed in the obvious way:

    static int called = 100;

This initialisation still occurs just once when the program starts. If a call to the function changes the value of the variable, that value is retained.

Global variables: sharing variables between functions

Relying too heavily on external variables is fraught with peril since it leads to programs whose data connections are not at all obvious - variables can be changed in unexpected and even inadvertent ways and the program is hard to modify.
Kernighan & Ritchie.

GLOBAL variables (to be used only if you really need them)..
Torvalds

Variables defined outside of a function are called external variables (also known as global variables) and can be used by any function in the file, provided only that the function definition follows the variable definition.

We have deliberately not mentioned these before because some new programmers can seize these as an opportunity to avoid passing parameters to functions "let's just make it a global variable!".

Remember: the main point about using functions is to let us think about only a small part of the program at any one time. Global variables destroy this.

Like most bodges, this works fine as long as there is only one of them. But as our code grows and we use it more often, it becomes hopelessly confused.

When to use global variables

External, or global, variables should be used when both of the conditions are satisfied:

  1. They are a truly "global" property of the program that will be used by many of its functions.
  2. There can logically only one of that property. This is quite different from "I'm sure there will only be one of this!"

Example

As an example of when to use global variables, consider a simple "notebook" program with the job of storing some short, one-line notes.

A single note, or string of characters, would be stored as a one-dimensional character array. Therefore an array of several such notes will be stored as a two-dimensional character array. (A two-dimensional character array can be used an array of one-dimensional character arrays.)

We might therefore propose a global two-dimensional character array to store the notebook entries as follows:

#define MAXLEN 128
#define MAXENTRIES 256
char entries[MAXENTRIES][MAXLEN];

void myfun1() {
  /* myfun1 can now use the array "entries" */
}

void myfun2() {
  /* so can myfun2 */
}

Initialisation

External variables are initialised in exactly the same way as static internal variables. In the above case the entire array will be initialised to zeros which, it turns out, is quite useful.

Unformatted input for characters and strings

Quick discussion:

In this section we shall be dealing with situations where it is fairly easy to describe what we want to happen but actually making it happen involves some irritating and potentially confusing details.

Turn to your neighbour and ask:

  • In situations such as these, what should be our instinctive reaction?.

Recap: formatted input

The scanf() family of functions we have used so far are intelligent functions for situations where we know what to expect (for example, an integer) and want interpret the input accordingly.

They are extremely convenient for reading in numbers as they skip over white space, including new lines. The user can leave any number of spaces between inputs, or even just put one per line. This is referred to as formatted input as the function has to know the format of the data it is expecting (integer, floating-point number, text string without spaces, etc.)

After it has read the expected characters, the system leaves itself positioned at the next character after the last one it has used, which is nearly always a space or a new-line, '\n'.

An example

Consider a file that starts:

1 2 
8.7
32.8
Mary had a little lamb
...

As far as our program is concerned, it is as if it were a giant character string starting:

Notice how I had accidentally typed a space at the end of the first line. This is quite common.
"1 2 \n8.7\n32.8\nMary had a little lamb\n..."

If the code now executes the statement:

fscanf(infile, "%d %d %lf", &j, &k, &x);

The value 1, 2 and 8.7 get read into j, k and x respectively with fscanf() conveniently skipping over unwanted spaces and new-line characters. Having read in everything up to and including "8.7" the imaginary character string now contains:

You will notice the remaining "string" starts with a newline character, '\n' . This will be very important later on.
"\n32.8\nMary had a little lamb\n..."

As long as we continue to use fscanf() to read in integer and numbers everything will be fine as all the unwanted spaces just get skipped over.

Reading in text without interpreting it

Sometimes we just want to read in a whole line of text into a character array, referred to as unformatted input. We don't expect it to have any special form such as a number. The most common reason is to be able to input text containing spaces, for example people's names or free-form text for a notebook application.

We shall first look at the mechanics of reading in the text and then deal with the subtleties of combining formatted and unformatted input.

Unformatted input: fgets()

The fgets() function reads a line from a file without interpreting it. That is, fgets() reads the unread input up to and including the next new-line character. The "file" can be the keyboard if stdin (standard input) is used. It has the form:

  fgets(buffer, maxbytes, file);
Here file is a FILE *. It can be obtained obtained in the usual way using fopen() or we could use the predefined value stdin if we want to read from standard input.

buffer is a character array at least maxbytes long. Like snprintf(), fgets() is "well behaved" and always puts a zero, '\0', at the end of the text even if the input line is too long, thus always leaving a valid zero-terminated string.

Notice that for unformatted input the FILE is the last argument, unlike in fscanf() where it is the first.

Thus fgets() reads in at most maxbytes - 1 bytes of actual input, or up to the next new-line character, whichever comes first. It returns NULL if the read failed completely, for example if we have reached the end of the input file.

Removing the new-line character

If space permits, fgets() includes the new-line character, sent when the user presses the "Return" or "Enter" key. This is always the final character of the string. If we don't want this character, we just need to replace it with '\0', thus shortening the string by one.

The following snippet calls fgets() to read a line from standard input and then checks to see if the last character of a string stored inside a character array is '\n'. If so it replaces it with '\0', thus shortening the string by one.

If this were a program we were writing for other people to use we would need to consider what to do if the final character were not a new line as it probably indicates that the line was too long for out buffer.

  if (fgets(line, N, stdin) != NULL) {
    int end = strlen(line) - 1;
    if (line[end] == '\n')
      line[end] = '\0';
  }

With this we can look at a very short program that uses fgets() to read a line of text from the keyboard and print it out again:

Mixing formatted and unformatted input

Whether we have a file of data or are reading from the keyboard, we are always free to mix formatted and unformatted input.

A common situation is to use formatted input to get options from a menu, or to read in data values, and to then need to read in a complete line of text.

The first attempt often looks like this:

// 
// Flawed attempt to read in an integer followed by some text.
//
int main() {
  char line[N];
  int value;

  printf("Please enter the integer value\n");
  scanf("%d", &value);

  printf("Now please type in the text string, spaces are allowed!\n");

  if (fgets(line, N, stdin) != NULL) {
    // Chop off final '\n';
    int end = strlen(line) - 1;
    if (line[end] == '\n')
      line[end] = '\0';
  }

  printf("The value is %d, the text is >%s<\n", value, line);
  return 0;
}
Step through this code

 

The "conversation" goes like this:

Please enter the integer value
12
Now please type in the text string, spaces are allowed!
The value is 12, the text is ><

The user is given no chance to type in a line of text, instead fgets() just seems to read a completely blank line. What's happening?

The answer is that, when we typed in "12" we actually typed three characters, '1', '2' and the carriage return, '\n'. As in the previous example, the system reads in the two characters '1' and '2' that form the integer 12 and leaves itself positioned at the very next character, which is the new line '\n'. Thus the "next line" is completely empty!

We can illustrate this by typing at the keyboard not just "12<return>" (three keystrokes) but "12<space>abc<return>" (seven keystrokes). The final line of output now looks like this:

The value is 12, the text is > abc<

Recap: scanf() is reading the two characters '1' and '2', inspecting the next character, seeing it is white-space, which ends the number and is therefore ignored and left for the next input function to deal with. If that next function is another call to scanf() that's fine as scanf() skips over white-space. But fgets() doesn't so it just sees the new-line character which it treats as being an empty line.

There are various bad solutions at this point (some people's first reaction is just to read in one more character in the hope that nobody will ever type 12<space><return>), but the most common situation is that we require the input line to be non-blank. In this case it's easy to write a loop that carries on reading a line from the file, or keyboard, until it finds a line that contains a non-space character.

If we are reading from stdin it looks like this:

/*
 * Read in a line of text, looping until it has  some non-spaces.
 * We trim the final '\n' the end of the line.
 * Return 1 for success, zero for failure.
 */
int readoneline(char line[], int maxbytes) {
  while ( 1 == 1 ) {
    int i;

    if ( fgets(line, maxbytes, stdin) == NULL )
      return 0;  /* Out of data */

    /* We don't the new-line character so chop it */
    i = strlen(line) - 1;
    if ( line[i] == '\n')
      line[i] = '\0';

    /* Look for a non-blank character. */
    for (i = 0; line[i] != '\0'; ++i)
      if ( isspace(line[i]) == 0)
        return 1;
  }
}

This is quite a useful function and we shall use it next week.

Handling bad input

This is an advanced topic and can be omitted if desired.

So far we have handled checking the values of numbers typed in at the keyboard by enclosing the call to scanf() inside an infinite loop, checking the values typed in and printing an error message if they are incorrect or breaking out of the loop if they are OK.

But you may already have encountered the situation where you have typed a non-numeric character by mistake, say 'q' instead of '1'. The problem is that in this situation scanf() leaves the input at the first character that doesn't match what it expects, the 'q'. The loop, if it's properly written, does not break so scanf() is called again and does exactly the same thing again: it stops at the 'q'. And so on for ever.

This might be thought to be an unhelpful response, but the question arises "what should the system do in this situation?".

Quick discussion

Turn to your neighbour and ask what would be the best thing to do in this situation.

One possible approach

The general idea here is to print a helpful message to the screen and skip the rest of the line. This only makes sense if we are reading from the keyboard there is no point in doing this if we are reading from a file.

Even here there are a few subtleties, for example: what happens if we reach the end of the input? This could be because stdin is coming from a file not the keyboard, or because the terminal window has closed. Or the input may be coming over the network and the connection might break.

With that in mind we can write an error handling function which we shall call skipline():

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

void skipline(void);

int main() {
  int x, y;

  while (1 == 1) {
    printf("Please enter two integers > 0\n");

    if (scanf("%d %d", &x, &y) != 2)
      skipline();
    else if ( x <= 0 || y <= 0 )
      printf("Only integers greater than zero are allowed\n");
    else
      break;

    printf("\n\tPlease try again.\n\n");
  }
    
  printf("Read: %d %d\n", x, y);
  return 0;
}

//
// Read and discard the rest of the line from stdin, printing it so
// the user knows what's going on. 
//
void skipline(void) {
  int i;
  
  printf("\nSkipping unexpected input: ");

  while ((i = getc(stdin)) != '\n') {
    if ( i == EOF ) {
      printf("End of standard input\n");
      exit(1);
    }    
    putc(i, stdout);
  }

  putc('\n', stdout);
}
Step through this code

 

getchar() and putchar()

getchar() reads a single character from standard input (there is a version getc(file) to read from a file).

If you look closely at the code above you will see it returns an int, not a char as we might expect. The reason is the one we mentioned above: we need a way of telling if we have run out of input. All the possible values of a char (including zero) are by definition possible successful values of getchar().

So on failure getchar() returns EOF which is not a possible char.

Example

If we want to read in a char variable, let's call it c, we might write:

char c;
int i;

if ((i = getchar()) == EOF ) {
  fprintf(stderr, "Out of data!\n");
  exit(99);
}
/* Else */
c = i;  /* Success */

Similarly putchar(int value) and putc(int value, FILE *) print a single character to stdout or a file respectively.

With that in mind skipline() should be reasonably clear. It's important to notice that we have put all of the nastiness of checking for the end of file inside of skipline(), we have not left any of it for the calling function to handle.

Dealing with errors is always tedious and should be separated from the main logic of the code as far as possible.

                                                                                                                                                                                                                                                                       

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