A Little C Primer/C Console IO

This chapter covers console (keyboard/display) and file I/O. You've already seen one console-I/O function, "printf()", and there are several others. C has two separate approaches toward file I/O, one based on library functions that is similar to console I/O, and a second that uses "system calls". These topics are discussed in detail below.

Console I/O in general means communications with the computer's keyboard and display. However, in most modern operating systems the keyboard and display are simply the default input and output devices, and user can easily redirect input from, say, a file or other program and redirect output to, say, a serial I/O port:

   type infile > myprog > com

The program itself, "myprog", doesn't know the difference. The program uses console I/O to simply read its "standard input (stdin)"—which might be the keyboard, a file dump, or the output of some other program—and print to its "standard output (stdout)"—which might be the display or printer or another program or a file. The program itself neither knows nor cares.

Console I/O requires the declaration:

   #include <stdio.h>

Useful functions include:

   printf()      Print a formatted string to stdout.
   scanf()       Read formatted data from stdin.
   putchar()     Print a single character to stdout.
   getchar()     Read a single character from stdin.
   puts()        Print a string to stdout.
   gets()        Read a line from stdin.

Windows-based compilers also have an alternative library of console I/O functions. These functions require the declaration:

   #include <conio.h>

The three most useful Windows console I/O functions are:

   getch()    Get a character from the keyboard (no need to press Enter).
   getche()   Get a character from the keyboard and echo it.
   kbhit()    Check to see if a key has been pressed. 

The "printf()" function, as explained previously, prints a string that may include formatted data:

   printf( "This is a test!\n" );

—which can include the contents of variables:

   printf( "Value1:  %d   Value2:  %f\n", intval, floatval );

The available format codes are:

   %d    decimal integer
   %ld   long decimal integer
   %c    character
   %s    string
   %e    floating-point number in exponential notation
   %f    floating-point number in decimal notation
   %g    use %e and %f, whichever is shorter
   %u    unsigned decimal integer
   %o    unsigned octal integer
   %x    unsigned hex integer

Using the wrong format code for a particular data type can lead to bizarre output. Further control can be obtained with modifier codes; for example, a numeric prefix can be included to specify the minimum field width:

   %10d

This specifies a minimum field width of ten characters. If the field width is too small, a wider field will be used. Adding a minus sign:

   %-10d

—causes the text to be left-justified.

A numeric precision can also be specified:

   %6.3f

This specifies three digits of precision in a field six characters wide.

A string precision can be specified as well, to indicate the maximum number of characters to be printed. For example:

   /* prtint.c */

   #include <stdio.h>

   int main(void)
   {
     printf( "<%d>\n", 336 );
     printf( "<%2d>\n", 336 );
     printf( "<%10d>\n", 336 );
     printf( "<%-10d>\n", 336 );
     return 0;
   }

This prints:


   <336>
   <336>
   <       336>
   <336       >

Similarly:

   /* prtfloat.c */

   #include <stdio.h>

   int main(void)
   {
     printf( "<%f>\n", 1234.56 );
     printf( "<%e>\n", 1234.56 );
     printf( "<%4.2f>\n", 1234.56 );
     printf( "<%3.1f>\n", 1234.56 );
     printf( "<%10.3f>\n", 1234.56 );
     printf( "<%10.3e>\n", 1234.56 );
     return 0;
   }

—prints:

   <1234.560000>
   <1.234560e+03>
   <1234.56>
   <1234.6>
   <  1234.560>
   < 1.234e+03>

And finally:

   /* prtstr.c */

   #include <stdio.h>

   int main(void)
   {
     printf( "<%2s>\n", "Barney must die!" );
     printf( "<%22s>\n", "Barney must die!" );
     printf( "<%22.5s>\n", "Barney must die!" );
     printf( "<%-22.5s>\n", "Barney must die!" );
     return 0;
   }

—prints:

   <Barney must die!>
   <      Barney must die!>
   <                 Barne>
   <Barne                 >

Just for convenience, the table of special characters listed in chapter 2 is repeated here. These characters can be embedded in "printf" strings:

   '\a'     alarm (beep) character
   '\p'     backspace
   '\b'     backspace
   '\f'     formfeed
   '\n'     newline
   '\r'     carriage return
   '\t'     horizontal tab
   '\v'     vertical tab
   '\\'     backslash
   '\?'     question mark
   '\''      single quote
   '\"'     double quote
   '%%'     percentage
   '\0NN'   character code in octal
   '\xNN'   character code in hex
   '\0'     null character

The "scanf()" function reads formatted data using a syntax similar to that of "printf", except that it requires pointers as parameters, since it has to return values. For example:


   /* cscanf.c */

   #include <stdio.h>

   int main(void)
   {
     int val;
     char name[256];
   
     printf( "Enter your age and name.\n" );
     scanf( "%d %s", &val, name ); 
     printf( "Your name is: %s -- and your age is: %d\n", name, val );
     return 0;
   }

There is no "&" in front of "name" since the name of a string is already a pointer. Input fields are separated by whitespace (space, tab, or newline), though a count, for example "%10d", can be included to define a specific field width. Formatting codes are the same as for "printf()", except:

  • There is no "%g" format code.
  • The "%f" and "%e" format codes work the same.
  • There is a "%h" format code for reading short integers.

If characters are included in the format code, "scanf()" will read in the characters and discard them. For example, if the example above were modified as follows:

   scanf( "%d,%s", &val, name );

—then "scanf()" will assume that the two input values are comma-separated and swallow the comma when it is encountered.

If a format code is preceded with an asterisk, the data will be read and discarded. For example, if the example were changed to:

   scanf( "%d%*c%s", &val, name );

—then if the two fields were separated by a ":", that character would be read in and discarded.

The "scanf()" function will return the value EOF (an "int"), defined in "stdio.h", when its input is terminated.

The "putchar()" and "getchar()" functions handle single character I/O. For example, the following program accepts characters from standard input one at a time:

   /* inout.c */

   #include <stdio.h>

   int main (void)
   {
     unsigned int ch; 
   
     while ((ch = getchar()) != EOF)
     {
       putchar( ch ); 
     }
     return 0;
   }

The "getchar" function returns an "int" and also terminates with an EOF. Notice the neat way C allows a program to get a value and then test it in the same expression, a particularly useful feature for handling loops.

One word of warning on single-character I/O: if a program is reading characters from the keyboard, most operating systems won't send the characters to the program until the user presses the "Enter" key, meaning it's not possible to perform single-character keyboard I/O this way.

The little program above is the essential core of a character-mode text "filter", a program that can perform some transformation between standard input and standard output. Such a filter can be used as an element to construct more sophisticated applications:

   type file.txt > filter1 | filter2 > outfile.txt

The following filter capitalizes the first character in each word in the input. The program operates as a "state machine", using a variable that can be set to different values, or "states", to control its operating mode. It has two states: SEEK, in which it is looking for the first character, and REPLACE, in which it is looking for the end of a word.

In SEEK state, it scans through whitespace (space, tab, or newline), echoing characters. If it finds a printing character, it converts it to uppercase and goes to REPLACE state. In REPLACE state, it converts characters to lowercase until it hits whitespace, and then goes back to SEEK state.

The program uses the "tolower()" and "toupper()" functions to make case conversions. These two functions will be discussed in the next chapter.

   /* caps.c */

   #include <stdio.h>
   #include <ctype.h>

   #define SEEK 0
   #define REPLACE 1

   int main(void)
   {
     int ch, state = SEEK;
     while(( ch = getchar() ) != EOF )
     {
       switch( state )
       {
       case REPLACE:
         switch( ch )
         {
         case ' ':
         case '\t':
         case '\n':   state = SEEK;
                      break;
         default:     ch = tolower( ch );
                      break;
         }
         break;
       case SEEK:
         switch( ch )
         {
         case ' ':
         case '\t':
         case '\n':   break;
         default:     ch = toupper( ch );
                      state = REPLACE;
                      break;
         }
       }
       putchar( ch );
     }
     return 0;
   }

The "puts()" function is like a simplified version of "printf()" without format codes. It prints a string that is automatically terminated with a newline:

   puts( "Hello world!" );

The "fgets()" function is particularly useful: it reads a line of text terminated by a newline. It is much less finicky about its inputs than "scanf()":

   /* cgets.c */

   #include <stdio.h>
   #include <string.h>

   #include <stdlib.h>

   int main(void)
   {
     char word[256], 
          *guess = "blue\n";
     int i, n = 0;

     puts( "Guess a color (use lower case please):" );
     while( fgets(word, 256, stdin) != NULL )
     {
       if( strcmp( word, guess ) == 0 )
       {
          puts( "You win!" );
          break;
       }
       else
       {
          puts( "No, try again." );
       }
     }
     return 0;
   }

This program includes the "strcmp" function, which performs string comparisons and returns 0 on a match. This function is described in more detail in the next chapter.

These functions can be used to implement filters that operate on lines of text, instead of characters. A core program for such filters follows:

   /* lfilter.c */

   #include <stdio.h>

   int main (void)
   {
     char b[256];
     while (( fgets(b, 256, stdin) ) != NULL )
     {
       puts( b ); 
     }
     return 0;
   }

The "fgets()" function returns NULL, defined in "stdio.h", on input termination or error.

The Windows-based console-I/O functions "getch()" and "getche()" operate much as "getchar()" does, except that "getche()" echoes the character automatically.

The "kbhit()" function is very different in that it only indicates if a key has been pressed or not. It returns a nonzero value if a key has been pressed, and zero if it hasn't. This allows a program to poll the keyboard for input, instead of hanging on keyboard input and waiting for something to happen. As mentioned, these functions require the "conio.h" header file, not the "stdio.h" header file.