Lesson #24

File I/O

Overview

In this lesson I will cover the following topics

  1. Opening and closing files explicitly.

  2. Using the file handling versions of the standard I/O functions—especially fgets.

  3. Various common open modes and the difference between text mode and binary mode.

Body

Opening, using, and closing files

So far our programs have only used the standard input and standard output files. This is fine for interacting with a live user. With I/O redirection it is also possible for such programs to read a single input file and write a single output file. However, using just the standard, pre-opened files is limiting. A program can't both interact with a user and manipulate a file because once I/O has been redirected away from the terminal the user is taken out of the picture. Also many programs need to manipulate multiple files at once. I/O redirection can't (easily) be used to do this.

The C standard library comes with a complete set of file handling functions that allow you to manipulate files. Since these functions are standard they will work the same way on any system that supports C. This is a nice feature since the way files are stored and accessed by the system itself tends to vary quite a bit from one system to another.

Before you can use a file, you must "open" it. When you are finished using a file you should "close" it. The steps of opening, using, and closing occur in many areas of programming. If you are not familiar with this approach, you should pay close attention. You will see it not only when you program with files, but also with many other types of resources.

All of C's standard file handling functions are declared in <stdio.h> Thus you will need to include that header in any source file that needs to use them. Then, before you can do anything with a file, you need to declare a variable of type FILE *. Such a variable is called a "pointer to FILE". The name "FILE" is defined in <stdio.h> in some suitable way. You don't need (or want) to know what it really is.

Next, to open the file you must use the fopen function. Here is how that looks.

#include <stdio.h>

int main(void)
{
  FILE *input;

  input = fopen("afile.txt", "r");

  ...

Note the following things:

  1. I have to create a variable of type pointer to FILE, not just FILE. Students often leave off the asterick in that declaration.

  2. The fopen function takes two parameters. The first is the name of the file. The format of the name depends on the operating system. Any name that makes sense to the operating system will work. Note that if you are opening a file under Windows and if the name includes backslashes, you will need to escape the backslashes just as you would in any string. For example:

    input = fopen("c:\\Program Files\\MyApplication\\config.txt", "r");
    
  3. The second parameter of fopen is the open "mode". It needs to be a string—not just a character. (Use double quotes, not single quotes). In my example above my mode string just happens to be a string of one character. The "r" means I wish to open the file for reading (input). I could use a "w" if I wanted to open the file for writing (output).

  4. The fopen function returns the NULL pointer if it fails to open the file for some reason. The most common reason for fopen to fail when you open a file for reading is that the file does not exist. There are, however, other reasons for failure so don't assume that is what went wrong for certain.

It is very bad to attempt to use a file that didn't open correctly. Although it won't cause any file corruption, but it will cause your program to dump core. Because of this, you should always check the result of your fopen calls. More typically my example would be written like this

#include <stdio.h>

int main(void)
{
  FILE *input;

  if ((input = fopen("afile.txt", "r")) == NULL) {
    printf("Error: Can't open afile.txt for reading!\n");
  }
  else {
    // I opened the file fine. Do something with it.
  }
  return 0;
}

It is important that you don't forget to close your files after you are finished using them. However, you shouldn't attempt to close a file that didn't open correctly (again... core dump). So before even writing the code that works with the file, I suggest that you add a call to fclose at the appropriate spot. My example becomes

#include <stdio.h>

int main(void)
{
  FILE *input;

  if ((input = fopen("afile.txt", "r")) == NULL) {
    printf("Error: Can't open afile.txt for reading!\n");
  }
  else {
    // I opened the file fine. Do something with it.

    fclose(input);
  }
  return 0;
}

Actually ANSI standard C guarentees that all files opened with fopen will be automatically closed when main returns and the program ends. As a result if it is your intention to hold a file open during the entire time your program executes, you don't really need to worry about closing it. However, I strongly suggest that you do explicitly close all your files anyway. Code has a tendency to get copied from one program to another. While closing files might be optional in your first program it might not be optional in your next one. There are times when you really must remember to close your files. For example, a program that runs for a long time (like a server) and manipulates many files during its lifetime, must be sure to close them in a timely manner or the system will run out of resources. If you get in the habit of always closing your files you will have fewer problems when you try to write such a program.

Finally there are many other resources that follow the open/use/close approach. If you get used to worrying about closing files you will be better prepared to manage these other resources when you want to use them. For example, programs that work with the network have to first open a network connection, use the connection, and then close the connection. If a program fails to close the network connections that it opens, it can cause odd problems. The ANSI C standard does nothing to insure that such a resource is properly cleaned up.

So how do I use files?

Once you have opened a file successful you can then read material from the file if it's an input file or write material to the file if it's an output file. It is easy to do this.

The standard C library contains functions for doing I/O at three different levels. You can do raw character-at-a-time I/O, you can do I/O of entire lines of text, and you can do "formatted" I/O to read or write the values of variables. When you are using the standard input and standard output files these functions are as follows

Output Input
Character putchar getchar
Line puts gets
Formatted printf scanf

You have already looked at these functions. Here are a few points to remember about them.

  1. The puts and gets functions are designed to work together. The gets function does not put the terminating '\n' character into the array it has been given. The puts function adds a '\n' character on the output.

  2. The gets function just takes a single pointer to character parameter. It has no way of knowing how much space is in the array it has been given. For this reason gets should not be used in a serious program.

There are six other functions that are very similar to the ones above that you can use with file I/O. All of these functions need a FILE * parameter to specify which file you are trying to use.

Output Input
Character putc getc
Line fputs fgets
Formatted fprintf fscanf

Here are more details on these functions.

int putc(int ch, FILE *stream);
  /* This function writes the character ch to the output file stream. It
     returns ch or EOF if it encounters an error. */

int getc(FILE *stream);
  /* This function reads a character from the input file stream. It
     returns what it reads or EOF if it encounters the end of the file
     (or an error). */

int fputs(char *line, FILE *stream);
  /* This function writes the characters in the array pointed at by line
     to the output file stream. It does NOT add a '\n' character auto-
     mattically. It returns EOF if it encounters an error. */

char *fgets(char *buffer, int size, FILE *stream);
  /* This function reads characters from the input file stream. It will
     place these characters into the array pointed at by buffer until
     one of three things happen. 1) The function encounters (and stores)
     a '\n' character. 2) The function stores size-1 characters. 3) The
     function encounters the end of the input file or an error
     condition. If the function stores anything in the array it adds a
     null terminator to the array and then returns buffer. Otherwise it
     returns the NULL pointer. */

int fprintf(FILE *stream, char *Format, ...);
  /* This function works just like printf except that it sends its
     output to the output file specified by stream. */

int fscanf(FILE *stream, char *Format, ...);
  /* This function works just like scanf except that it reads its input
     from the input file specified by stream. */

Function fgets certainly sounds complicated. However, this is because it is actually doing more than gets does. In addition to reading from an explicitly opened file, fgets also insures that the destination buffer does not overflow. You must provide fgets with a total size for that buffer. It will store at most size-1 characters into it, leaving one byte for the null character (which it also adds). Notice also that fgets does not just drop the '\n' character the way gets does. This goes along with how fputs works. The fputs function does not add a '\n' automatically. Thus lines read with fgets can be written with fputs directly.

Keep in mind that fgets normally reads just one line of input. Like gets, it will stop reading when it encounters a '\n.' The issue with the size parameter is just so that you can insure fgets will not read too much. Because fgets has this safety feature, it should be preferred over gets.

What happens if you give fgets a very long line to read? It will read the first size-1 characters of the line and then return. The next time it is called, it will pick up where it left off, reading another size-1 characters of the very long line. Finally when it encounters a '\n' it will just return that last portion of the line. The next call after that will cause fgets to attempt to read the next line.

If fgets encounters the end of the file in the middle of a line (that is, if the last line of the file does not have a '\n' at the end), it will return normally at that point. It will only return NULL if it encounters the end of the file without reading any other characters.

All in all, fgets is a very robust function. You should get in the habit of using it. Let me add it to my example:

#include <stdio.h>

int main(void)
{
  FILE *input;
  char  line_buffer[256];

  if ((input = fopen("afile.txt", "r")) == NULL) {
    printf("Error: Can't open afile.txt for reading!\n");
  }
  else {

    // Read the input file a line at a time and print it onto stdout.
    while (fgets(line_buffer, 256, input) != NULL) {
      printf("%s", line_buffer);
    }

    fclose(input);
  }
  return 0;
}

Here I set aside an array of 256 characters to serve as the line buffer. This allows 254 characters for "normal" text on each line. Why 254? The fgets function will need one character for the null character and one to hold the '\n' character. By setting aside an array of 256 characters, I'm saying that I don't expect there to be any lines longer than 254 characters in the file. However, if there is, fgets will just read the line in multiple pieces. As long as my program knows how to deal with that it will still work fine. It also works fine if the last line is without a '\n' character. In that case fgets will still return the last line fine and won't return NULL until it has tried to read a "naked" end of file.

Notice how when I used printf to output the lines I did not include a '\n' in the format string. That's because there should still be '\n' characters in the line buffer. The fgets function does not remove them. If fgets reads a hugely long line in multiple pieces, printf will just print out the pieces as desired.

What about filter programs?

We've written several programs that act as filters. Those programs have processed their standard input and written to their standard ouput. We have used I/O redirection to make it possible for such a program to manipulate files. If you want to do the file handling explicitly in your program you can do so. Here is an example that shows how to open both an input file and an output file.

#include <stdio.h>

int main(void)
{
  FILE *input;
  FILE *output;
  int   ch;

  // Try to open the input file. If it fails, print a message.
  if ((input = fopen("afile.txt", "r")) == NULL) {
    printf("Can't open afile.txt for reading!\n");
  }

  // Now try to open the output file. If it fails, close the input.
  else if ((output = fopen("bfile.txt", "w")) == NULL) {
    printf("Can't open bfile.txt for writing!\n");
    fclose(input);
  }

  // If the files opened okay, loop over the input one character at a time.
  else {
    while ((ch = getc(input)) != EOF) {

      // Process ch and output it.
      putc(ch, output);
    }

    // Close the files.
    fclose(input);
    fclose(output);
  }

  return 0;
}

This program is basically an if...else if... chain. I first try to open the two files. If that works, the final else clause executes where I use the same loop we've talked about before. Notice how I close the files when I am done processing them. Notice also how I close the input file if the output file fails to open. It is easy to forget that. This program shows character-at-a-time I/O. If your needs require that you read the input a line at a time, you can just replace the while loop with one that uses fgets.

More about open modes

So far you've seen me using the "r" and the "w" modes. Are there others? Here is a list of modes that you might want to use.

"r" : This opens a file for reading in text mode. The file must exist.
"rb" : This opens a file for reading in binary mode. The file must exist.
"w" : This opens a file for writing in text mode. If the file exists, its old contents are lost. If the file does not exist, it is created.
"wb" : This opens a file for writing in binary mode. If the file exists, its old contents are lost. If the file does not exist, it is created.
"a" : This opens a file for appending in text mode. If the file exists, all output is added on to the end of the file. If the file does not exist, it is created and this mode is just like "w".
"ab" : This opens a file for appending in binary mode. If the file exists, all output is added on to the end of the file. If the file does not exist, it is created and this mode is just like "wb".

All of these modes are for "sequential" access of the file. Such access involves reading or writing the file only from the beginning to the end. There are other modes (and functions) that allow you to jump around in a file and access its contents in any order you please. We will not discuss those techniques in this course.

What is the difference between "text mode" and "binary mode?" Actually under Unix there is no difference at all. The Unix operating system regards all files exactly the same way: as collections of raw bytes. Text files are not special in the eyes of the system. Unix is somewhat unusual in this regard.

Other operating systems sometimes store text files in special ways. Consequently you need to specify when you open a file how you intend to handle that file so the operating system can do whatever is necessary to make that work. Under Unix "r" and "rb" are identical. However, you should always specify the appropriate mode so that your program is easier to get working in another environment.

Under Windows the biggest issue is CR/LF translations. Text files in Windows have two characters to mark the ends of lines: a carriage return and a line feed. Since C programs expect there to be a single "newline" character at the end of each line somebody has to translate CR/LF pairs into '\n' as the (text) file is being read and '\n' characters to CR/LF pairs as the (text) file is being written. This is handled by the C library, but you must help by opening the file in the proper mode.

Summary

  1. Before you can use a file, you must open it with fopen. When you are doing using a file, you should close it with fclose. The fopen function returns a pointer to FILE which you then use in all the subsequent file handling functions. You do not need the name of the file after you've opened it. The fopen function returns NULL if the file can't be opened.

  2. The file I/O functions come in three levels: character at a time, line at a time, and formatted. You should use the level that best suits your application. The fgets function, which reads a line at a time from a file, is much more robust than the gets function that reads a line from the standard input. The fgets function will not overflow the buffer it has been given.

  3. You can open a file for reading, writing, or appending. When you open a file for reading, it must exist. When you open a file for writing it is erased if it exists or created if it does not exist. When you open a file for appending the new output is added to the end of the file.

    Some systems distinguish between binary files and text files. The main issue is that under some systems (such as Windows), there are '\n' to CR/LF translations that must be done for text files. Unix does not need these translations so under Unix there is no issue of text vs binary files.

© Copyright 2003 by Peter C. Chapin.
Last Revised: July 4, 2003