We’ve always had the capability to use the C standard’s input/output functions, even from our first program:

#include <stdio.h>

Up to now we’ve used printf and scanf for output/input, respectively. But with file IO we open the door to way more interesting possibilities.

File IO in C is done using streams. A stream is just a general name for something that can provide input, or receive output.

Input Streams:

  • keyboard
  • controller
  • files
  • mouse
  • network socket

Output Streams

  • screen
  • controller (think rumble or PS controller light)
  • speakers/audio devices
  • files

We most often use files for our input/output streams, like text files, JSON files, or any other type of file you can think of! This usually entails:

Opening the file. Reading from/writing to the file. Closing the file.

Files in C

The stdio.h header includes a struct called a FILE, and we most often use FILE pointers when we want to do file operations in C. Below is a simple FileIO example in C:

1. FILE *fp; //We've created a File pointer, used for opening/closing/reading/writing to a file
 
2. fp = fopen("myfile.txt", "w");    //Open the file for write (could be any file name)
 
3. fputs("Line number 1\n", fp); //Write text to file      
3. fputs("Line number 2\n", fp); //Write more text to file 
3. fputs("Line number 3\n", fp); //Write even more text    
 
4. fclose(fp);                   //Close the file          
 

Let’s walk through the steps above, in order.

  1. We create a FILE pointer, a structure used by C’s standard input/output library:
FILE *fp = NULL; 
//We've created a File pointer, used for opening/closing/reading/writing to a file

We can set it equal to NULL if we like. Nothing crazy going on yet.

  1. We call a function, fopen, to open our file:
fp = fopen("myfile.txt", "w");    //Open the file for write (could be any file name)

We’re specifying the name of the file to open, and for what purpose we’re opening the file. In the example above the file we’re attempting to open is called “myfile.txt”. The second argument is another string with specific characters in it. These characters denote what we’d like to do with this file once it is open. Specifically, we’re passing in a string with the ‘w’ character in it to signify that we’d like to open this file for writing. Not only that, but we’re CREATING this file on disk, in the same location as the executable we’re running, with the intent to fill it with information/data.

For more information on what the different fopen options are, we can look here.

We set our FILE pointer, which we’ve called fp, equal to the return from fopen. Once the file has been opened, it is now the “handle” to our file in our running program. We generally don’t do much more with our FILE pointer at this stage, other than passing it to functions that act on it.

  1. We write some strings into the file, as text:
fputs("Line number 1\n", fp); //Write text to file      
fputs("Line number 2\n", fp); //Write more text to file 
fputs("Line number 3\n", fp); //Write even more text

We’re specifying which FILE pointer to use, and what strings to write into it. Question: how does fputs know how much information to write, if we’re not passing the length of our string to fputs?

  1. We close the file.
fclose(fp); //Close the file  

Absolute and Relative Paths

There’s one thing we forgot to do in Step 2 though… we need to check that we succeeded in opening/creating the file!

fp = fopen("myfile.txt", "w");    //Open the file for write
if(fp == NULL)
{
	//either return or do something else here
	printf("Couldn't open file for writing!");
}

fopen will return NULL if it fails to open the file, meaning it is missing or corrupted. Especially for new programmers, you WILL encounter errors opening files. This is a CERTAINTY. Most often this will happen because your program won’t be able to find the file that it is trying to open. Wait… what do we mean by “find”?

Consider that you could have a file on your desktop called Songs.txt. You might also have a folder on your desktop called MySongs, and a different file called Songs.txt in that folder. Whenever your program tries to open Songs.txt, which of the two files will it open? If you just call fopen("Songs.txt", "r") to open and read the file… then you likely won’t open either file! fopen will return an error!

Whenever you try to open a file in your program, you need to supply not only the name of the file you’re trying to open, but the full path to that file! You can do so using either a relative path or a absolute path. An absolute path starts with the drive you’re looking in, and the full path on that drive to the file you’d like to open. Most often using absolute paths is NOT what you want to do, but a couple of examples would look like this:

//Full path to the first file we mentioned:
"C:\Users\ethan\Desktop\Songs.txt"
 
//Full path to the second file we mentioned:
"C:\Users\ethan\Desktop\MySongs\Songs.txt"

Consider that my desktop looks different from your desktop… which means if you wrote a program with an absolute path hardcoded into it, it would almost certainly fail if ported to a different computer.

On the other hand, a relative path is… well relative! But relative to what? It turns out that your program keeps track of a “current directory” whenever it is running, and that directory happens to be initialized to the directory that your program starts in! This means if you package the files needed to work with your program in such a way that they’re always located in the same place relative to the executable, then your program will work on ANY machine.

Say that our program executable is in a folder called MyProgram on the desktop. To open the two files we discussed previously, we would use relative paths that look like this:

//Relative path to the first file we mentioned:
"..\Songs.txt"
 
//Relative path to the second file we mentioned:
"..\MySongs\Songs.txt"

The .. portion of the relative path is saying “Go up one directory from our current directory”. So in the first example what we’re doing is looking up one directory (so we’re getting out of the MyProgram folder back to the Desktop) and then looking for Songs.txt on the Desktop. In the second example, we’re doing the same, except we’re looking in the MySongs directory on the desktop.

For reference, if you ever need to go up multiple directories, you can use .. however many times you need! For example, the following would go up three directories: ..\..\..\

You WILL make the mistake of looking in the wrong place for your files; make sure you wrap your fopen code with error handling, and maybe use breakpoints while you’re at it! It will make your life SO much easier!

Error Handling

Not being able to find the file isn’t the only kind of error you may encounter when working with files. Can you spot the error in the code below?

FILE *fp = fopen("myfile", "w");
if(fp == NULL)                  
{
	printf("Failed to open the file: myfile.txt\n");
}
else
{
	fputs("Writing info to my file, Duba, Duba\n\
	Writing info to  my file, all the livelong day!", fp);
}
 
//close the file
fclose(fp);

Answer: we are trying to fclose the file even if the fopen call failed.

Should be common sense, but don’t do any of the following:

  • Close a file that hasn’t been opened
  • Close a file multiple times
  • Write to a file that has been closed

There are certain scenarios (especially with the low complexity of programs in our class) where you may not see any negative repercussions, for doing the “wrong thing”. For example, you could end your program without closing an opened file, and the operating system will clean up after you. But in more advanced scenarios you could run into trouble, so just make it a habit to clean up after yourself!

//Open file for write
FILE *fp = fopen("myfile", "w");
 
if(fp == NULL)                  
{
	printf("Failed to open the file: myfile.txt\n");
}
else
{
	//Write some stuff to the file
 
	//OH CRAP, FORGOT TO CLOSE THE FILE!
}

Also, make sure you’re correctly checking for the end of the file!

There are lots of different FileIO functions included in the standard, so we should take a look at some of them!

Standard Streams

You may have seen the terms stdin, stdout, and stderr somewhere. There are three standard streams that are provided to us as part of the C Standard. They’re called stdin, stdout, and stderr (Standard In, Standard Out, and Standard Error). These three streams are already ready to use when our program starts; we don’t need to declare or initialize them or open a file related to them, or anything like that.

By default, stdin represents keyboard input, and stdout/stderr represent the console window.

Whenever we use the standard input/output functions like printf and scanf, these functions are using stdin and stdout. Funny enough, we can actually do the following:

fputs("Writing some text to... the console?", stdout); //Write text to file  

And this will just print our text to the console, just like printf! You wouldn’t really need/want to do this necessarily, but the point is that stdin/stdout/sterr are streams, and so are files.

There are ways to redirect the standard streams when using a command line (like powershell or the command prompt) but we don’t have time now to jump into that. You’ll definitely see it more in CS170 though!

Text vs Binary Files

You must be careful when reading/writing files to know whether or not you’re working with a binary file, or a text file. The contents of a text file are stored as… well, text! This could be a .txt file or a .json file, any kind of file whose contents are meant to be represented as text. For example, say I’ve got a text file representing a list of all 151 original Pokemon, called MonsterList.txt:

Bulbasaur
Ivysaur
Venusaur
Charmander
Charmeleon
Charizard
Squirtle
Wartortle
Blastoise
Caterpie
...

This is what that file would look like opened as text. We want to read/write to this file as text, since this is what we’d want humans to see when they open the file to inspect its contents.

However, say you had an array of integers representing the price of 50 million different homes across the US. That data might look something like:

400843
785976
600850
1237800
70000
11487400
...

There’s no earthly reason to keep this data in a text format, because no human being is going to look at those 50 million rows of data by cracking open the text file. Additionally, think of how much extra space is wasted here! Each number above represents 6 bytes of character storage (sometimes 7 or 8!) when in reality we only need 4 bytes if each price is an integer. And of course that doesn’t take into account that we’d need to convert these strings into integers when loading this data into a program, and barf them out as strings when writing to the output file!

So instead of reading/writing from the file as if it is text, we’d read/write our file as if it is binary data:

FILE *fp = fopen("myfile", "r+b");
 
int homePrices[50000000];
fread(homePrices, sizeof(int), 50000000, fp);

This way, whenever we start up our program, we can read the integers directly from the file straight into an integer array! No need to convert strings to numbers or any such nonsense. We could even do this with structs!

Atoi

In a similar vein, it can be annoying to read in a text file and encounter a number… as text. What to do with it? How can a string that represents a number, say something like "5000", be converted into an integer?

A function called atoi, from the header <stdlib.h>, can help us out here:

 
//the integer myNum should now have the value 5000
int myNum = atoi("5000");
 

There may be cases where you need to convert an integer in string/text form into a number… and now you can!

fprintf

So once a file is open and ready for reading/writing, how do we actually interact with it? The first function we can use is called fprintf, and it works more or less just like printf! We supply a string to fprintf, and we also supply variables on the end if we’ve used any format specifiers in our string. There is ONE difference though…

FILE* myFile;
//pretend that we fopened the file here...
int myInt = 87;
 
fprintf(myFile, "Write out the value of my int: %d", );

In addition to our formatted string and variables, we also supply a FILE pointer. Instead of writing to the console (which is actually just stdout) we will see that our file as been altered by fprintf!

fscanf

As you can guess, fscanf works similarly to scanf, with the difference that we get to specify the stream we’re reading from (instead of defaulting to stdin):

FILE* myFile;
//pretend that we fopened the file here...
 
int myInt = 0;
fscanf(myFile, "%d", &myInt);

One thing that is interesting about fscanf is that if we call it multiple times, we will continue to scan new things in from our file. The implication here is that our program is somehow keeping track of our current place in our open file, and shifting that location every time we read from the file! Most of the file reading/writing operations will do so, which makes sense; you wouldn’t want to write two lines of text into a file (using fprintf twice), and have the second line overwrite the first!

fputs and fgets

fputs, known as “File put string”, allows us to write strings into files, like so:

FILE* myFile;
//pretend that we fopened the file here...
 
//fputs(const char* s, FILE* stream);
fputs("Oh look, a string!", myFile);

fgets, known as “File get string”, allows us to get string input from a file. In the example below, we’re reading a string from the file into a character array. The function will either read until it hits a newline character or until it reads the number of characters specified in the function call, whichever comes first (so below it would be either 128 characters, OR it would be all the characters up to and including the first encountered newline character).

FILE* myFile;
//pretend that we fopened the file here...
 
char myString[128];
 
//fgets(char* s, int n, FILE* stream);
fgets(myString, 128, myFile);

fputs and fgets similar to fprintf/fscanf… but not formatted!

fgets fread fwrite sprintf