The problem with variables is that they only exist in memory. As soon as your program stops, all your variables vanish, and there's no way to recover their values.
Luckily, C lets us read from and write to files. And thank goodness, because without that, our programs would be seriously limited. Files are saved to your computer's hard drive, which means they stick around — even after the program or the computer itself is turned off ;)
To work with files, we'll be using everything we've learned so far: pointers, structures, pointers to structures, strings, and so on. So make sure all of that is clear in your mind before going further. If not, no worries — just go back and review the earlier chapters. There's no rush. Learning C isn't a race. Those who take their time will be the ones who really get it.
Opening and Closing a File
To read and write files, we'll be using functions from the stdio library — the same one we've already been using.
Yep, that's the one with printf and scanf that we've been calling over and over! But there's more to it: this library also includes file-related functions.
All the libraries we've used so far (stdlib.h, stdio.h, math.h, string.h...) are part of what we call the standard libraries. These come bundled with your IDE and work across all operating systems. So feel free to use them whether you're on Windows, Linux, Mac, or something else.
Standard libraries are few and only allow for basic operations — everything we've done up to now, basically. For more advanced stuff, like opening windows, you'll need to install additional libraries. We'll get into that soon ;)
For now, make sure your .c file includes at least these two libraries at the top:
#include <stdlib.h> #include <stdio.h>
These two are so essential that I recommend including them in every program you write, no matter what it does.
Great! With the right libraries in place, we're ready to dive in ^^
Whenever you want to open a file — whether it's for reading or writing — follow this basic sequence:
- Call the fopen function, which gives you a pointer to the file.
- Check if the file opened successfully by testing the pointer. If it's NULL, the file couldn't be opened, and you should stop there (maybe print an error message).
- If the file opened (i.e., the pointer isn't NULL), you can then read from or write to it using other functions we'll explore soon.
- When you're done with the file, remember to close it using fclose.
Let's start by learning how to use fopen and fclose. Once you've got those down, we'll look at how to actually read and write data.
fopen: Opening a File
Back in the chapter on strings, we treated function prototypes like instruction manuals. That's what real programmers do: read the prototype, and figure out what the function does (though yeah, sometimes even we need a little extra help).
So here's the prototype for fopen:
FILE* fopen(const char* fileName, const char* mode);
This function takes two arguments:
- The name of the file to open.
- The mode in which to open it — whether you want to read, write, or both.
But hey, by now you know what that means ^^ It's a pointer to a FILE structure, which is defined in stdio.h. You could look it up to see what it's made of, but honestly — it's not worth it ;)
You might wonder: why is FILE written in all caps? Aren't uppercase names usually reserved for constants and defines?
Well, that's just a naming rule I like to follow (and lots of other programmers do too). It's not mandatory. Apparently, the folks who wrote stdio weren't following the same rules ^^
Don't let that throw you off. In fact, you'll see that future libraries we study will stick to the style I've been teaching: capitalize just the first letter of a structure's name.
Back to fopen. It returns a FILE*. It's super important to store this pointer because you'll need it to read or write to the file.
So let's declare a FILE pointer at the beginning of our function (say, inside main):
int main(int argc, char *argv[]) { FILE* file = NULL;return 0; }
The pointer is initialized to NULL from the start. Remember, always initialize your pointers to NULL if you don't yet have a value to assign. If you don't, your program might crash later on ;)
Now let's call fopen and store the returned value in our pointer file. But first, we need to understand how the second parameter works — the one that sets the file mode. You need to provide a short code to specify whether the file should be opened for reading, writing, or both.
Here are the main modes:
- "r": Read only. You can read from the file, but not write to it. The file must already exist.
- "w": Write only. You can write to the file, but not read from it. If the file doesn't exist, it will be created.
- "a": Append mode. You can write to the file, starting at the end. New text gets added after the current content. If the file doesn't exist, it will be created.
- "r+": Read and write. You can both read and write. The file must already exist.
- "w+": Read and write, but wipes the file clean first. It deletes all existing content, then lets you read and write. If the file doesn't exist, it will be created.
- "a+": Read and append. You can read and write, but only at the end of the file. If the file doesn't exist, it will be created.
And that's not even the full list! There's a second version of each mode where you add a "b" — like "rb", "wb", "ab", etc. — to open the file in binary mode. That's a bit more advanced, so we won't cover it here. Basically, text mode stores characters, while binary mode stores raw bytes — great for handling things like Word documents.
The logic is almost identical to what we'll see here anyway.
For now, those six modes are more than enough ^^
Personally, I use "r
" (read), "w
" (write), and "r+
" (read/write) the most. "w+
" is a bit dangerous because it erases the file's contents right away with no warning. Only use it if you want to wipe the file clean.
Append mode ("a") can be useful if you want to keep adding to the file without overwriting anything.
If you just want to read a file, it's best to go with "r". Technically, "r+" would work too, but "r" prevents you from making changes by mistake — it's safer.
Here's how to open siteraw.txt
in read/write mode:
int main(int argc, char *argv[]) { FILE* file = NULL;file = fopen("siteraw.txt", "r+");
return 0; }
Now file is a pointer to the file siteraw.txt
.
siteraw.txt
be?It should be in the same folder as your executable (.exe). For this chapter, create a file called siteraw.txt and place it in the same folder as your compiled program.
.txt
?Nope. You can choose any extension you like. You could even create your own, like .level, to store game levels, for example ;)
.exe
?Not at all. It can be in a subfolder:
file = fopen("folder/siteraw.txt", "r+");
Here, siteraw.txt is inside a folder named folder. This is called a relative path, and it's super handy — it works no matter where your program is installed.
You can also use an absolute path to open a file anywhere on your drive:
file = fopen("C:\\Program Files\\Siteraw\\readme.txt", "r+");
This opens readme.txt located in C:\Program Files\Siteraw
.
Note the double backslashes (\\). If you used a single one, your computer would think you're trying to insert a special character like \n or \t. To use an actual backslash in a string, you need to double it up. That way, the compiler knows you meant the \ character itself.
The downside of absolute paths? They only work on a specific OS. They're not portable. If you were on Linux, you'd write the path like this:
file = fopen("/home/siteraw/folder/readme.txt", "r+");
So I recommend using relative paths instead. Only use absolute paths if your program is designed for a specific system and needs to access a specific file somewhere on the disk.
Checking if the File Opened
The file pointer should now point to a FILE structure, which fopen() loaded into memory for you.
From here, there are two possibilities:
- The file opened successfully, and you can move forward (read/write).
- The file failed to open — maybe it didn't exist, or another program is using it. In that case, stop right away.
Right after opening the file, always check if the operation succeeded. It's easy: if the pointer is NULL, the file couldn't be opened. If it's not NULL, you're good to go.
Here's the basic pattern to follow:
int main(int argc, char *argv[]) { FILE* file = NULL;file = fopen("siteraw.txt", "r+");
if (file != NULL) { // You can read and write to the file } else { // Optional: display an error message printf("Could not open the file siteraw.txt"); }
return 0; }
Always do this check. If you skip it and the file doesn't exist, your program might crash later.
fclose: Closing the File
If the file opened properly, you can now read or write to it (we'll get to that next). Once you're done, make sure to close the file. The fclose function frees the memory — it removes the file from RAM.
Here's its prototype:
int fclose(FILE* filePointer);
It takes one argument: your file pointer. It returns an int:
- 0: if the file was closed successfully.
- EOF: if something went wrong. EOF is a special constant defined in stdio.h — used to indicate an error or end-of-file. In this case, it means there was an error.
In general, closing a file works just fine, so I don't usually check the return value. But feel free to do so if you want.
To close the file, just write:
fclose(file);
That's it :)
Final Pattern for Opening/Closing a File
Here's the full structure we'll use from now on:
int main(int argc, char *argv[]) { FILE* file = NULL;file = fopen("siteraw.txt", "r+");
if (file != NULL) { // Read and write operations go here
// ...
fclose(file); // Close the file once you're done }
return 0; }
I didn't include the else block for the error message here, but feel free to add one if you'd like.
Always remember to close your files once you're done with them. That way, you free up memory. If you forget, your program might end up using way more memory than it needs. In small examples like this, it's not a big deal — but in a larger program? Yikes :p
Forgetting to free memory happens. And it'll probably happen to you at some point. That's when you'll run into memory leaks. Your program starts consuming more and more memory for no apparent reason. Often, the issue is something simple — like a forgotten fclose. Sometimes the fix for a mysterious bug is almost laughably simple (well, sometimes :D)
Methods for Reading and Writing
Now that we've written the code to open and close a file, all that's left is to add the part where we actually read from and write to it. ^^
We'll start by looking at how to write to a file (it's a little simpler), and then we'll move on to reading from a file.
Writing to a File
There are several functions available for writing to a file. It'll be up to you to choose the one that best fits your needs. Here are the three we'll be covering:
- fputc: writes a single character to the file (only ONE character at a time).
- fputs: writes an entire string to the file.
- fprintf: writes a formatted string to the file, and works pretty much like printf.
fputc
This function writes one character at a time to the file. Its prototype looks like this:
int fputc(int character, FILE* filePointer);
It takes two parameters:
- The character you want to write (of type int, which, as I mentioned earlier, is roughly equivalent to a char — just with a bigger range of possible values). So yes, you can simply write 'A' directly.
- The file pointer where you want to write. In our case, we've called it file. The cool thing about passing the file pointer every time is that you can open multiple files at once and read/write from all of them. You're not stuck with just one file open at a time!
The function returns an int — this is an error code. If something goes wrong, it returns EOF; otherwise, it returns some other value. Since the file usually opens successfully, I don't always bother checking whether each fputc worked, but you absolutely can if you want to.
Here's some sample code that writes the letter 'A' to siteraw.txt. If the file exists, it gets overwritten; if it doesn't, it gets created. Everything's in there: open, check, write, and close.
int main(int argc, char *argv[]) { FILE* file = NULL;file = fopen("siteraw.txt", "w");
if (file != NULL) { fputc('A', file); // Writing the character A fclose(file); }
return 0; }
Now open your siteraw.txt
file. What do you see? It's magic (well, okay — not really :p ), but the file now contains the letter 'A'!
fputs
This function is very similar to fputc, except it writes a whole string, which is usually much more convenient than doing it one character at a time. ^^ That said, fputc is still handy when you do need to go character-by-character — which happens pretty often :)
Here's the function prototype:
int fputs(const char* string, FILE* filePointer);
The two parameters are pretty straightforward:
- string: the string you want to write. Notice the type is const char*. Adding const to the prototype tells you that the function won't touch your string — it'll treat it as read-only. Which makes sense: fputs is supposed to read your string and write it, not mess with it. It's both informative and a safety feature to prevent accidental changes.
- filePointer: just like in fputc, this is your FILE* pointing to the opened file.
The function returns EOF if there was an error; otherwise, it worked. Again, I usually don't check the return value, but feel free to do so.
Let's try writing a string to a file:
int main(int argc, char *argv[]) { FILE* file = NULL;file = fopen("siteraw.txt", "w");
if (file != NULL) { fputs("Hey there Noobs!\nHow's it going?", file); fclose(file); }
return 0; }
fprintf
Here's another flavor of the printf function. This one lets you write formatted text directly into a file. It works just like printf, except you provide a file pointer as the first argument.
This code asks the user for their age and writes it into a file:
int main(int argc, char *argv[]) { FILE* file = NULL; int age = 0;file = fopen("siteraw.txt", "w");
if (file != NULL) { // Ask the user for their age printf("How old are you? "); scanf("%d", &age);
// Write it to the file fprintf(file, "The person using this program is %d years old", age); fclose(file); }
return 0; }
And just like that, you can reuse everything you already know about printf to write to a file. Pretty cool, right? :)
That's actually why I tend to use fprintf most often — it's easy and super handy.
Reading from a File
We can use nearly the same functions as for writing — only their names change slightly:
- fgetc: reads a single character
- fgets: reads a string
- fscanf: reads a formatted string
I'm going to speed things up a little here, but if you understood what we covered above, you should be good to go ;)
fgetc
First up, here's the prototype:
int fgetc(FILE* filePointer);
This function returns an int: the character that was read. If it fails to read a character, it returns EOF.
Well, as you read through a file, there's this "cursor" that moves forward. It's a virtual cursor — you won't see it onscreen ;) But you can think of it like that blinking bar you see when typing in Notepad. It shows where you are in the file.
We'll see later how to check the cursor's position or move it (say, back to the beginning or to a specific character like the 10th one).
Each time you call fgetc, the cursor moves forward one character. Call it again, and it reads the next character. You can totally use a loop to read a file one character at a time this way :)
Let's write some code that reads all characters from a file one by one and prints them out to the screen. The loop stops when fgetc returns EOF (which means End Of File).
int main(int argc, char *argv[]) { FILE* file = NULL; int currentChar = 0;file = fopen("siteraw.txt", "r");
if (file != NULL) { currentChar = fgetc(file); // Initialize currentChar
// Read characters one by one while (currentChar != EOF) // Keep going until fgetc returns EOF { printf("%c", currentChar); // Print the current character currentChar = fgetc(file); // Read the next character }
fclose(file); }
return 0; }
The console will display the entire content of the file. For example:
fgets
This function reads an entire string from a file. That saves you from reading every character individually. It reads up to one line at most (it stops at the first \n it encounters). If you want to read multiple lines, you'll need a loop.
Here's the prototype for fgets:
char* fgets(char* string, int maxCharsToRead, FILE* filePointer);
It includes one especially useful parameter: the number of characters to read. That tells fgets to stop reading if the line is longer than X characters.
Why is this helpful? Because it prevents memory overflows! If the line is too long to fit into your string, reading more than the allocated space would probably crash your program.
Reading a Line with fgets
Let's start by reading just one line using fgets (we'll see how to read the whole file next).
You create a string that's large enough to store the line (hopefully ^^). This is where using a #define to set the array size really shines:
#define MAX_SIZE 1000 // Array size of 1000int main(int argc, char *argv[]) { FILE* file = NULL; char string[MAX_SIZE] = ""; // Empty string with size MAX_SIZE
file = fopen("siteraw.txt", "r");
if (file != NULL) { fgets(string, MAX_SIZE, file); // Read up to MAX_SIZE characters into "string" printf("%s", string); // Print the string
fclose(file); }
return 0; }
You'll get the same output as before — the file content is printed to the console:
The difference is: no loop this time. The entire line gets printed all at once.
Now you've probably realized how useful #define is for setting array sizes. MAX_SIZE is used in two places:
- Once to define the array size.
- And again in fgets to limit how many characters to read.
The advantage? If your string isn't big enough, you just change the #define value and recompile :) No need to hunt through your code to find all the places using that size.
Reading the Whole File with fgets
As mentioned, fgets reads one line at a time — up to a max length you set.
Simple: with a loop :)
fgets returns NULL if it fails to read the line. So the loop should stop when that happens.
Just loop while fgets doesn't return NULL!
#define MAX_SIZE 1000int main(int argc, char *argv[]) { FILE* file = NULL; char string[MAX_SIZE] = "";
file = fopen("siteraw.txt", "r");
if (file != NULL) { while (fgets(string, MAX_SIZE, file) != NULL) // Keep reading while there's no error { printf("%s", string); // Print the line just read }
fclose(file); }
return 0; }
This code reads and prints the entire file line by line.
The key line here is the while (actually, it's the only new part since last time :D):
while (fgets(string, MAX_SIZE, file) != NULL)
It does two things: reads a line from the file and checks whether it got NULL back. So you can think of it as: "Keep reading lines until you hit the end of the file."
fscanf
Same concept as scanf, except this time we're reading from a file that was written in a specific format.
Let's say your file contains 3 numbers separated by spaces — like high scores in a game: 10 20 30
You want to read each number into an int variable. fscanf makes this a breeze:
int main(int argc, char *argv[]) { FILE* file = NULL; int scores[3] = {0}; // Array for the top 3 scoresfile = fopen("siteraw.txt", "r");
if (file != NULL) { fscanf(file, "%d %d %d", &scores[0], &scores[1], &scores[2]); printf("Top scores are: %d, %d and %d", scores[0], scores[1], scores[2]);
fclose(file); }
return 0; }
Result:
As you can see, fscanf expected 3 numbers separated by spaces ("%d %d %d
"). It stored them in our 3-slot array.
Then we printed them out.
Up until now, you've only seen scanf with a single "%d
". But now you've seen that you can use multiple ones and mix them as needed. If your file follows a specific format, this method is fast and efficient for retrieving data :)
Moving Around in a File
Earlier, I mentioned a sort of virtual "cursor." Now it's time to dig into that in a bit more detail.
Every time you open a file, there's an internal cursor that tracks your current position in the file. You can picture it like the blinking cursor in a text editor (like Notepad). It shows where you are in the file — so that's where reading or writing will happen.
In short: this cursor system lets you read from or write to a specific position in the file.
There are three key functions you need to know:
- ftell: tells you your current position in the file
- fseek: moves the cursor to a specific position
- rewind: moves the cursor back to the start (same as using fseek to jump to the beginning)
ftell: your position in the file
This one's super straightforward. It returns the current position of the cursor as a long:
long ftell(FILE* filePointer);
The number it returns tells you where the cursor currently is in the file.
fseek: move the cursor
Here's the function prototype:
int fseek(FILE* filePointer, long offset, int origin);
The fseek function lets you move the cursor by a certain number of bytes (offset), starting from a reference point (origin).
The offset can be positive (move forward), zero, or negative (move backward). As for the origin, it can be one of the three constants (usually defined macros) listed below:
SEEK_SET
: start of the fileSEEK_CUR
: current position of the cursorSEEK_END
: end of the file
Here are a few examples to show how offset and origin work together:
- Move the cursor 2 characters after the start:
fseek(file, 2, SEEK_SET);
- Move the cursor 4 characters before the current position:
fseek(file, -4, SEEK_CUR);
(Note the negative offset since we're going backward.) - Move the cursor to the end of the file:
fseek(file, 0, SEEK_END);
If you write to a file after moving the cursor to the end, your data will be added to the file (it extends the file). But if you move to the beginning and write something, you'll overwrite what was already there. There's no built-in way to insert text into the middle of a file (unless you code it yourself: first read and store the data that comes after, then overwrite it all afterward!).
Well, that's up to you. 😉 If you wrote the file yourself, you know how it's structured. So you'll know where to look for your data. (For example: best scores start at position 0, player names start at position 50, and so on...)
We'll do a hands-on project later that'll help you get the hang of this (if it's not already clicking by now). Just remember: you define the structure of your file. It's up to you to say, "I'll put the top score on the first line, the second-best on the second line," and so on.
One last note: fseek can act weird when used on files opened in text mode. It's mainly intended for use with binary files. When reading or writing a text file, it's more common to process it character by character. About the only safe thing to do with fseek in text mode is jump to the beginning or the end.
In short: fseek is awesome — but best used with binary files. It's not really how we navigate through text files.
rewind: jump back to the beginning
This one's like hitting the rewind button on an old VCR — it's literally the same name!
The prototype is as simple as it gets:
void rewind(FILE* filePointer);
And using it is just as easy. Honestly, I won't even bother with an example this time (my fingers are starting to cramp up writing this chapter 😆).
Renaming and Deleting a File
Let's wrap this chapter up on a chill note by looking at two super simple functions:
- rename: renames a file
- remove: deletes a file
The cool thing about these two is they don't require a file pointer. All you have to do is pass the name of the file you want to rename or delete. In other words, it doesn't get much simpler than this :D
rename: renaming a file
This'll be a quick one.
int rename(const char* oldName, const char* newName);
The function returns 0 if the rename was successful; otherwise... well, anything but 0.
Do you really need an example? Oh fine, here you go:
int main(int argc, char *argv[]) { rename("siteraw.txt", "siteraw_renamed.txt");return 0; }
Wow, that was toouugh :-° And just like that, my file is renamed.
remove: deleting a file
This one deletes a file — no questions asked:
int remove(const char* fileToDelete);
Be extremely careful with this one! It deletes the file without asking for confirmation. It's not moved to the trash or anything — it's gone from the hard drive. No way to recover it.
This is perfect timing actually — I don't need siteraw.txt
anymore, so let's get rid of it ;)
int main(int argc, char *argv[]) { remove("siteraw.txt");return 0; }
Surprisingly enough, this chapter didn't teach you anything new about the C language itself.
Pointers, arrays, structs — that's the kind of stuff that makes up the C language.
Here, we've just been working with functions from stdio, the standard input/output library. So what we've really done is study a library (not even all of it, but hey, we've covered a big chunk! 😉).
I'm not saying we've seen everything C has to offer but... we're getting there ^^ Honestly, the basics of C don't take that long to learn. The hardest part is wrapping your head around pointers, and understanding the difference between values and addresses. But once you've got that down, you're theoretically ready to build anything. What really matters next is learning to use libraries — like we just did here: using the functions they provide.
Without libraries, a program can't do much. Even printf wouldn't be available without stdio ;)
Enjoyed this C / C++ course?
If you liked this lesson, you can find the book "Learn C Programing for Beginners" from the same authors, available on SiteRaw, in bookstores and in online libraries in either digital or paperback format. You will find a complete C / C++ workshop with many exclusive bonus chapters.