This is the thirteenth article in the Making Sense of C series.
In this article, we're going to discuss how to open and close files in C
.
We'll also go into a few topics we haven't mentioned yet, including the const
keyword.
This article is going to go through two of the standard library functions that
we'll use in our first program, fopen
and fclose
, which will move a file
from our hard drive to our program.
fopen
First, we're going to need some consistent way to interact with a file, so we need some consistent object to pass to all the file manipulation functions in the future. We also need to specify how we want to interact with a file. For example, we could have permission to read from a file but not modify it, such as a file with system information. Lastly, we're going to need to know the name of the file before we can open it.
FILE
TypeSince we need some consistent way to interact with a file, we need to store data
about the file, such as where we are in the file, whether we're reading from or
writing to the file, etc.
We could store this data in separate variables, but doing so would be an
absolute nightmare.
Even Dennis Ritchie et al couldn't figure out how to write the Unix OS (the
original C
project) without some way to group everything together.
So, they came up with the idea of a struct
, which represents some data grouped
together.
We're going to discuss struct
s in more depth later, but for now, just know
that a FILE
is a struct
with a bunch of data relevant to file operations.
fopen
's SyntaxRemember that we need to create a FILE
object from the name of the file (which
will be an array of characters or a char *
) and how we want to interact with
it (which we could represent in multiple ways, so let's leave it unknown for
now), which means the declaration of fopen
will look something like:
FILE fopen(char * filename, ??? how_to_interact);
where the ???
means we haven't figured out the type yet.
As we said before, we can read from files and we can write to files. We can also add stuff to the end of files, like if we're recording a log of everything that's happened on a server or something. Lastly, we might be in situations where we want to be able to read and write to a file, such as when we're using save files in a video game (saving is writing or appending to a file and loading is reading from a file) or if we're updating settings in an application.
In short, Ritchie et al decided that how_to_interact
should be easy for
programmers to remember, so he decided to make the input short strings.
Operation | Input String |
---|---|
Read from a file. | "r" |
Write to a file. | "w" |
Append to a file. | "a" |
Read from and write to a file. | "r+" or "w+" |
Read from and append to a file. | "a+" |
Read, write, and append are just the first letters of each word, and if you want
to read and write, add a +
at the end.
You can only open a file that already exists with "r+"
, but if the file does
not exist, then opening a file with "w+"
will create a new file with the name
you provide.
In general, "r"
and "r+"
can only work on existing files while "w"
,
"w+"
, "a"
, and "a+"
will create new files if you give it the name of a
file that doesn't exist.
"w"
and "w+"
will overwrite files if they already exist, but "a"
and
"a+"
will not.
Lastly, "a"
and "a+"
will always append to a file and they cannot change
anything that was in the file before it was opened.
fopen
's SyntaxKnowing that, we should have something like
FILE fopen(char * filename, char * mode);
right?
We actually have a few problems with the declaration of fopen
as is.
We'll see the first problem when we try to call the function.
It was back in the article on memory
addresses, but do you remember how I said that you could only initialize an
array once and that just creating a char *
will only allocate enough memory to
store a memory address?
Until this point, I just told you that was a fact of life and didn't offer any
explanation and now is a good time to explain it.
When you write the following code
char string[] = "Hello, World!";
the compiler converts that into machine instructions that say
{ 'H', 'e', 'l', 'l', 'o', ',', ' ', 'W', 'o', 'r', 'l',
'd', '!', '\0' }
into those fourteen bytes of memory.
That sounds simple enough, but where exactly are the bytes that make up the
string?
How does the computer know which bytes to copy into string
?
First, whenever someone runs your program, it is moved into RAM and so it has its own set of memory addresses for things like instructions (what your code becomes) and data (stuff in your program such as strings). Whenever your compiler sees a string literal (i.e., almost anything in double quotes), it will copy it directly into the executable, which you can see if you open it in a text editor like Sublime Text, VS Code, Notepad, etc.
char string[] = "Hello, World!"; // "Hello, World!" is a string literal.
Whenever you use the string literal, it will hand the memory address of the first byte in the string to whatever you're using it for just as it would for a string. Furthermore, when you use a string literal multiple times, it will store the string once and give you the same memory address.
char string1[] = "Hello, World!"; // Copy the fourteen bytes starting at // memory address 1234 (which contain // "Hello, World!") into the fourteen bytes // reserved for string1 char string2[] = "Hello, World!"; // Copy the fourteen bytes starting at // memory address 1234 (which contain // "Hello, World!") into the fourteen bytes // reserved for string2
If we try to store the memory address in a char *
, we'll get a compiler error
in our code because we would be modifying the executable file itself if
the compiler let us.
A program being able to modify itself is how Skynet or other AIs are supposed to
take over the world, but your computer is more likely to produce wrong outputs
or delete some data in the worst case.
To see how we could modify an executable, let's come up with a simple program
and convert it into assembly (a more natural language version of assembly, to be
precise) like a compiler.
Assume that the bytes "Hello, World!" is stored in memory addresses 1234
to
1247
within the program itself.
char * string1 = "Hello, World!"; // Copy the memory address 1234 into string1 string1[0] = 'h'; // Set the value at memory address 1234 to // 'h'.
Memory address 1234
is actually within the program itself, meaning that the
next time you run the program, string1
will be initialized with "hello,
World!"
instead of "Hello, World!"
.
Since your compiler doesn't want you to write such a program, it will throw a
compilation error and not make an executable.
If we could guarantee the compiler that we would not modify the string literal,
then we should be fine to store the memory address.
const
KeywordThe const
keyword guarantees the compiler that we aren't going to modify
whatever we apply it to.
If the compiler sees us modifying it, then it will throw an error to make sure
that everything runs properly.
const int a = 7; a = 4; // ERROR int b = a + 5; // No error because we aren't modifying a
So for string literals, we need to use a const char *
instead.
const char * string = "Hello, World!"; // No error string[0] = 'h'; // ERROR
You should read const char * string;
as "string
stores a memory address
to a bunch of char
s that we cannot change through the variable string
.
We can also do
char string[] = "Hello, World!"; // string has copied "Hello, World!", so it // can do whatever it wants to the copy // without messing up the program. const char * str = string; // str is a pointer to characters that it // can't modify. string[0] = 'h'; // string can modify it since it's just a // char * str[0] = 'H'; // ERROR: str is a const char *, which means // it can't modify the data at the memory // address it stores.
Declaring a variable const
just means that you can't use it to modify the
memory it holds, you can use other variables to modify the memory so long as you
create it yourself.
Lastly, we can store any type into a const
version of the type, but we can't
store pointers to const
types in pointers to regular types.
char string[] = "Hello, World!"; // Valid const char * str_literal; // Valid char * str = string; // Valid str_literal = str; // ERROR
fopen
Well, since we don't plan on modifying either of the arguments, we can declare
them const
and be done with it.
FILE fopen(const char * filename, const char * mode);
The syntax for fopen
is almost right as we have it, but we have a slightly
more efficient way of using it.
Instead of returning a FILE
object, we're going to return a pointer to a
FILE
object, like so
FILE * fopen(const char * filename, const char * mode);
If we do this, we can then modify the underlying FILE
object with functions
instead of having to take the address of the FILE
and pass it into the
function or modify the FILE
object in our code.
Furthermore, it allows us to return NULL
to indicate a problem in opening the
file.
fclose
Since we're opening a file, we need to close it when we're done, so we'll have a
function called fclose
.
fclose
only needs to know which file to close, so it has the syntax
int fclose(FILE * file_object);
where the return value is 0
if the file closed properly or the constant EOF
if the file was not closed properly.
fopen
and fclose
in Our ProgramNow that we can open and close files in our program, let's add it into our code.
We just want to read from a file, so we're going to use "r"
as our mode.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | #include <stdio.h> int check_if_strings_differ(char * str1, char * str2); int main(int argc, char ** argv) { char * program_name = argv[0]; if (argc < 3) { // TODO: Print Usage Message return -1; } char * filename = argv[1]; char * word = argv[2]; FILE * reader = fopen(filename, "r"); // TODO: Count number of occurrences in a file fclose(reader) return 0; } int check_if_strings_differ(const char * str1, const char * str2) { int i = 0; while (str1[i] && str2[i] && (str1[i] == str2[i])) { i += 1; } return str1[i] != str2[i]; } |
I also modified our check_if_strings_differ
function to take in const char
*
s instead of char *
s because we will not modify the strings and it will
allow us to call it with const char *
s and char *
s.
Between the first two highlighted lines, we now can use the variable reader
to
read from the file.
In this article, we learned about
FILE
type, which will allow us to interact with files,
fopen
, which will allow us to create a file
object from a filename and a
mode,
"Hello, World!"
),
const
, which tells the compiler we will not modify something and allows us to
use certain things like string literals,
fclose
, which will allow us to clean up a file
object.
In the next article, Compilers and IDEs for
C, we're going to go through the basics of setting up a compiler and and IDE
for C
so that we can actually compile programs.