File Input and Output

Textbook Reading

Program environments are often transient. Files are a mechanism for more persistent storage of data. The necessary background details on high-level file reading and writing (input and output, or I/O) are given in the following textbook reading.

Binary I/O in C

Writing output

fwrite literally writes bits (the 0s and 1s that represent whatever is pointed to in its first argument) to the file you specify in its last argument. However, to know how many bits to write you have to tell fwrite how big (how many bytes) the data you want to write is and how much data there is. This information corresponds to the second and third arguments respectively.

Suppose you want to write an array of 10 doubles (we'll call it array) to a file stream pointer. The call to fwrite would look something like this:

fwrite (array, sizeof(double), 10, yourStream)

Similarly, if you wanted to write just one Pixel structure you had previously declared as pxl, the call would resemble:

fwrite (&pxl, sizeof(Pixel), 1, yourStream)

Note that because the raw bits (rather than an ASCII representation) are being written, trying to view the resulting file data in the terminal with a program like cat or less will yield gibberish.

Reading input

Not suprisingly, fread functions exactly like fwrite except in reverse. It reads the data in in the same manner—where you specify the data to be read, the sizeof the data, and how many 'packets' of data you want.

Handling Errors

Nearly all of the C library calls we have introduced have the ability to report a status about their execution. The functions might report errors, successes, or something else. The programmer must decide whether to heed this information, and if so, how to handle it.

This type of handling is even more important where files are concerned, because so many other complicating factors enter into the fray, such as permissions or disk and network failure.

Many programming languages provide robust exception handling mechanisms. Because C is a low-level programming language, one should develop the necessary habit of detecting and handling errors. Often this means simply printing a helpful message and exiting the program. To see the plethora of possible failures, type man errno in your Linux terminal.

Good design accommodates failures. When things go wrong, it is important to deliver the unfortunate news to the right audience. When you write library code (code linked to other programs), the correct approach is usually an appropriate return code. After all, your client program likely wants to decide how to deal with your failure, rather than have your library print output to the user that is likely to befuddle them.

When writing programs directly for the end user, the correct approach to an unrecoverable error is usually to print an informative error message to the unbuffered stderr file stream. If the failure is due to a system or library call, the standard library functions perror and strerror can assist in providing an even more informative message.

You are likely used to the terminal utilities providing such helpful messages. You could try the following example yourself.

cat /imaginary/file
cat: cannot access /imaginary/file: No such file or directory
/bin/cat /etc/shadow-
/bin/cat:  /etc/shadow-: Permission denied

How do these programs do that? The following example gives us one possible approach.

fopen-test.c
/* Program to demonstrate simple error reporting */

#include <stdio.h>  /* for fopen, fprintf */
#include <stdlib.h> /* for EXIT_FAILURE, normally used with exit function */
#include <string.h> /* for strerror */
#include <errno.h>  /* for errno variable */

int
main (int argc, char* argv[])
{

  if (argc != 2)                            /* Verify user command-line input */
  {
    fprintf (stderr, "Usage: %s filename\n",argv[0]);
    return EXIT_FAILURE;
  }
  
  FILE* stream = fopen (argv[1],"r");      /* Open the file named by the user */

  if (stream == NULL)                       /* Verify the open was successful */
  {                                         /* Report a failure */
    fprintf (stderr, "%s: Cannot open %s: %s\n",
             argv[0], argv[1], strerror(errno));
    return EXIT_FAILURE;
  }

  /* stream ready for reading with fread, fgets, etc. ... */

  if (fclose (stream))                       /* Close the file stream */
  {
    fprintf (stderr, "%s: Error closing file %s: %s\n",
             argv[0], argv[1], strerror(errno));
    return EXIT_FAILURE;
  }
  
  return EXIT_SUCCESS;
} // main

This approach allows us to give very specifically formatted error messages. For example, we include the name of the executable (just like cat does) and the file we are unable to open (which can help disambiguate errors when multiple files could be the cause of failure).

Sometimes we do not need such extensive, configurable prefaces; the simpler perror would then suffice (and it only requires you to #include <stdio.h>). For example:

perror ("Cannot open file");

The perror function prints your message followed by the separating colon and most recent error.

Note in both cases that the error message is printed to stderr, a different stream designed to receive error messages. We may not want to clutter up normal output (particularly when it is captured with shell-based redirection) and we may want to see errors immediately (standard output may be buffered; standard error is completely unbuffered by default so error messages can appear immediately).

Finally, it is important to reiterate that perror and strerror are only applicable if a system or standard library function call fails. For other functions (i.e., one of your own, the MyroC library), you will likely need to craft your own error message for use with fprintf(stderr,...).

Thus, while we might be able to change the error reporting to use perror within the (stream == NULL) or fclose(stream) tests, we cannot change the fprintf of the initial argc test to use perror because the program usage error is not caused by a failed function call.