Computer Science 010

Lecture Notes 11

C Preprocessor, File Manipulation in C

C Preprocessor

See last lecture.

File I/O

The I/O that we have done so far only takes input from the keyboard and puts all output on the screen. We have seen that we can redirect input and output using Unix so that we can read from files and write to files. This is not sufficient for real file manipulation, however. First, for many programs, we want the application to decide when to use files, not the user. Second, a program might want to use more than one input file or more than one output file. This cannot be done with Unix redirection. Consider Emacs for example. We might have multiple files open, each in a different buffer. To allow the editing of multiple files within an Emacs session, there must be some way to write code that allows us to open, read, and write multiple files during a running program.

File types and functions

The two primary operations on files are reading from a file, and writing to a file. For each of these operations, we need to tell the operation which file we want manipulate. We could give a filename to each call to read and write a file, but that is awkward and also results in slow execution. Instead, before we read or write a file, we must open the file. The open function call returns a value that is a file pointer. We then pass this file pointer to subsequent read and write function calls in order that they can be implemented more efficiently. Here is the prototype for the open function:

FILE *fopen (const char *filename, const char *mode);

The type of a file pointer is FILE *. The FILE type is defined in stdio.h as are the I/O functions we have been using. When we include stdio.h, we are therefore also getting the definition of FILE. The filename is simply the name of the file we want to open. The filename can include directory names just as we would use them on the Unix command line. The final argument is the mode. This indicates if we intend to read from the file, write to the file, or both.

While mode is declared to be a string, there are actually a very small set of strings that are valid. The most common are:

"r"

Read

"w"

Write

"a"

Append

"r+"

Read and write

write creates the file if it doesn't exist. If it does exist, it overwrites the file. append creates the file if it doesn't exist. If it does exist, it puts the new output at the end of the file.

fopen returns NULL if the file could not be opened. There are many reasons why we might be unable to open a file. Perhaps the file does not exist, at least not with the spelling used in the program. Perhaps the person running the program does not have read/write permission that is compatible with how the fopen call claims it will use the file. Operating systems also typically limit the number of files that may open by one program simultaneously; we may have already reached the limit. These types of errors are very unpredictable. As a result, it is imperative that we check the return value for NULL and do some appropriate error handling if NULL is returned. (More on error checking later.)

The inverse of opening a file is closing it. You should do this when you no longer need to use the file. This will prevent you from accidentally using the file after you should be done with it. It also forces all writes you made to become persistent on disk. (If you don't close the file, it's possible that the changes will only be made in memory and will be lost if the system crashes.) You also need to close and reopen a file if you want to use it in a different mode. The prototype for the close function is:

int fclose (FILE *stream);

fclose returns 0 if it successfully closes the file. It returns EOF if the close operation is not successful. Just as every call to malloc should have a call to free, we also should have a call to fclose for every call to fopen.

To write to a file we use a function very similar to printf called fprintf. The signature of fprintf is the same as printf except that it takes an additional argument that identifies the file to write to:

int fprintf (FILE *stream, const char *format, ...);

The ... in the signature indicates that fprintf takes a varying number of arguments of varying types, just as printf does. fprintf (and printf) return the number of characters output. They return -1 if an error occurs.

There is also an input function that is analogous to scanf called fscanf. It is like scanf but takes an additional argument identifying the file to read:

int fscanf (FILE *stream, const char *format, ...);

fscanf (and scanf) return the number of values assigned to variables. This will be fewer than the number of variables in the argument list if the end of the file is reached or if there is a type mismatch between the data and the format string conversion. For example, if the format string contains "%d", but the next input is not an integer, there is a mismatch. Both scanf and fscanf would return at this point and the return value would reflect how many successful assignments had occurred prior to the mismatch. These both return EOF if no assignments were made and the end-of-file was reached.

We've already seen fgets, but now its signature should make more sense.

     char *fgets(char *str, int size, FILE *stream);

The last parameter is a FILE *, thus we could pass it a value returned by fopen. fgets will read a file line-by-line, guaranteeing not to read more than size characters ata time.

To determine if we have attempted to read beyond the end of a file, we can use feof:

int feof (FILE *stream);

This returns non-zero (true) if we are at the end of the file. Otherwise it returns 0 (false). Note that this only returns true after a failed read. We have to try to read and then test if the read failed. We cannot detect EOF and then decide whether to read or not.

Identifying files

There are several ways that we could tell fopen what file to open. In any event, the type must be a char *, but it might get its value in different ways:

argc and argv

It is simple to get arguments from the command. We must change the signature of the main program. Thus far, we have been using a simple signature:

int main ();

C also recognizes one other signature:

int main (int argc, char **argv);

argc is the number of command line arguments including the program name used to invoke the command. argv is a dynamically-sized array of strings. It has one entry for each word on the command line. argv[0] is the program name. For example, suppose a user executes the following Unix command:

-> spellcheck myfile

argc will be set to 2. argv[0] is "spellcheck". argv[1] is "myfile". You can then take argv[1] and assign it to a more meaningful variable name and use it as a filename to open. (Of course, it does not need to be a filename. The user needs to know what type of arguments your program expects.)

stdin, stdout, stderr

stdio.h also defines three identifiers whose types are FILE *. These are stdin, stdout, and stderr. stdin is the standard input. stdout is the standard output. stderr is the standard error device. stderr is separate from stdout so that if a program reports errors and the user redirects normal output to a file, the error output will still appear on the user's screen. This is generally useful so that the user gets immediate feedback if something goes wrong. To write error messages to the user's screen, you would use:

fprintf (stderr, "My error message\n");

Whether the user redirects standard output or not, this message will appear on the user's screen.

It is also possible to use stdout as an argument to fprintf and stdin as an argument to fscanf. This would normally happen if you wanted to write a program that read from standard input if the user did not give a filename on the command line, such as:

#include <stdio.h>
#include <stdlib.h>
   
int main (int argc, char **argv) {
  FILE *infile;
  int someint;
   
  if (argc == 1) {
    infile = stdin;
  }
  else {
    infile = fopen (argv[1], "r");
    if (infile == NULL) {
      printf ("Could not open %s\n", argv[1]);
      return EXIT_FAILURE;
    }
  }
   
  fscanf (infile, "%d", &someint);
  fclose (infile);
  return EXIT_SUCCESS;
}

Error handling

Predefined functions nearly always return values. Often there will be one or more return values that indicate an error occurred. For example, if malloc is unable to allocate memory because there is not enough free memory left, it will return NULL. When writing C programs, one should always test the return value of these functions to see that no error occurred.

When an error does occur, there could be multiple reasons why the error occurred. In those cases, a predefined function will set the value of a predefined variable called errno. If you detect that an error occurred based on a return value and then want to know what specific error occurred, you need to know the value of errno. To have access to errno, you must include errno.h in your program. If you look at /usr/include/errno.h, you will see that it simply includes /usr/include/sys/errno.h. Looking at this file, you will see a long list of #define preprocessor commands. errno is an integer variable that will take on one of these values. If you want to do something special depending upon which error occurred, you will need to include errno.h and compare errno to the #define identifies to determine what happened. From there, you can execute the appropriate error handling code.

Often you will want to report the error to the user. Use the function perror:

void perror (const char *s);

perror looks at the value of errno and prints out a more useful error message. The string that you pass in is printed at the beginning of the error message. This allows you to customize the message while still making the underlying cause of the error visible to the user. Here's an example:

#include <stdio.h>
#include <stdlib.h>
   
int main (int argc, char **argv) {
  FILE *infile;
  int someint;
   
  if (argc == 1) {
    infile = stdin;
  }
  else {
    infile = fopen (argv[1], "r");
    if (infile == NULL {
      perror (argv[1]);
      return EXIT_FAILURE;
  }
   
  fscanf (infile, "%d", &someint);
  fclose (infile);
  return EXIT_SUCCESS;
}

If we run this program and tell it to use a file that does not exist, the user will get this message:

main trying to open first argument: No such file or directory

Another benefit of using perror is that every error that has the same cause will have a similar error message. This makes it easier for the user or programmer to understand what might have happened. perror is declared in stdio.h. It is not necessary to include errno.h to use perror.

Not all functions that return error codes set errno. To find out if a function does or not, you need to look at the man page for the function. You need to look for two things in particular. First, you need to look for a discussion of the return value of the function. Second, you need to look for a discussion of errors. There is normally a section of the man page called "RETURN VALUES". Read this to see if the function ever returns an error code. If it sets errno, it will usually say this explicitly when discussing the error return value. Then there is generally a separate section called "ERRORS" that lists the #define error values that it might set along with a short description of what that error code means for that function.

For example, here is what the man page for fopen says (partially):

RETURN VALUES
Upon successful completion fopen(), fdopen() and freopen() return a FILE pointer. Otherwise, NULL is returned and the global variable errno is set to indicate the error.
ERRORS
[EINVAL] The mode provided to fopen(), fdopen(), or freopen()
was invalid.
 
The fopen(), fdopen() and freopen() functions may also fail and set errnofor any of the errors specified for the routine malloc(3).
 
The fopen() function may also fail and set errno for any of the errors
specified for the routine open(2).
 
The fdopen() function may also fail and set errno for any of the errors
specified for the routine fcntl(2).
 
The freopen() function may also fail and set errno for any of the errors
specified for the routines open(2), fclose(3) and fflush(3).

EINVAL indicates that the mode string was not one of "r", "w", or "a" (or a very few other possible modes).

For the other error values, we need to look at other man pages. Here are some of the other possible errors, there are actually many of these:

ENOMEM
Ran out of memory trying to open the file.
ENOTDIR
A component of the path prefix is not a directory.
ENAMETOOLONG
A component of a pathname exceeded 255 characters, or an entire path name exceeded 1023 characters.
EACCES
The required permissions (for reading and/or writing) are denied for the given flags.
ENOENT
A component of the path name that must exist does not exist.
EISDIR
The named file is a directory, and the arguments specify it is to be opened for writing.
EROFS
The named file resides on a read-only file system, and the file is to be modified.

You could use this information to do something like the following:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
   
int main (int argc, char **argv) {
  FILE *infile;
  int someint;
  char c;
   
  if (argc == 1) {
    infile = stdin;
  }
  else {
    infile = fopen (argv[1], "r+");
    if (infile == NULL {
      if (errno == ENOENT) {
        printf ("%s does not exist.  Create it? (y/n) ", argv[1]);
        c = getchar();
        if (c == 'y') {
          fclose (fopen (argv[1], "w"));
          infile = fopen (argv[1], "r");
        }
        else {
          printf ("Exiting program.\n");
          return EXIT_FAILURE;
      }
      else {
        perror (argv[1]);
        return EXIT_FAILURE;
      }
    }
  }
   
  fscanf (infile, "%d", &someint);
  fclose (infile);
  return EXIT_SUCCESS;
}

If the file does not exist, this program prompts the user to find out if the user would like to create it. If the user wants to create it, it is created by opening it in write mode (which automatically creates the file if it doesn't exist), closing the file and reopening it in read mode. (This is rather silly since now we have an empty file which won't be very useful for reading, but it shows you how to use the error values.)

Also, note that there should be more error checking in our code. We should be checking the result of our attempt to open the file in write mode. We should check the result of the fclose calls. We should also check the return value of fscanf. In fact, good C programs develop a large percentage of their lines of code to doing error checking and error recovery.

sprintf, sscanf

There is another variation of printf and scanf that is extremely useful. In this variation, the first argument is a string rather than a file pointer. sprintf is like printf except the string that is the result of the formatting is placed into a string variable instead of output. This is useful for building larger strings out of smaller ones and also for converting integers to strings. Be sure to allocate memory for the result before calling sprintf:

char date[100];
int month, day, year;
sprintf (date, "%d/%d/%d", month, day, year);

The above code sets the value of date.

sscanf does the reverse. Given a string as the first argument, it will extract pieces of the string and put the results into variables. The following code sets the value of month, day, and year.

char date[] = "1/22/2002";
int month, day, year;
sscanf (date, "%d/%d/%d", &month, &day, &year);


Return to CS 010 Home Page