CSC 161 Schedule Readings Labs & Projects Homework Deadlines Resources

Basic 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 readings.

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.

/* 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.